UNPKG

18.6 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.createScale = createScale;
7exports.luminance = luminance;
8exports.contrast = contrast;
9exports.binarySearch = binarySearch;
10exports.generateBaseScale = generateBaseScale;
11exports.generateContrastColors = generateContrastColors;
12exports.minPositive = minPositive;
13exports.ratioName = ratioName;
14exports.generateAdaptiveTheme = generateAdaptiveTheme;
15
16var _d = _interopRequireDefault(require("d3"));
17
18var d3cam02 = _interopRequireWildcard(require("d3-cam02"));
19
20var _d3Hsluv = _interopRequireDefault(require("d3-hsluv"));
21
22var d3hsv = _interopRequireWildcard(require("d3-hsv"));
23
24var _curve = require("./curve.js");
25
26function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
27
28function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
29
30function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
31
32/*
33Copyright 2019 Adobe. All rights reserved.
34This file is licensed to you under the Apache License, Version 2.0 (the "License");
35you may not use this file except in compliance with the License. You may obtain a copy
36of the License at http://www.apache.org/licenses/LICENSE-2.0
37
38Unless required by applicable law or agreed to in writing, software distributed under
39the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
40OF ANY KIND, either express or implied. See the License for the specific language
41governing permissions and limitations under the License.
42*/
43// Work around node and babel's difference of opinion on the read-onlyness of default
44function assign(dest, ...src) {
45 for (let obj of src) {
46 for (let prop in obj) {
47 if (prop !== 'default') {
48 dest[prop] = obj[prop];
49 }
50 }
51 }
52}
53
54let interpolateJch = (start, end) => {
55 // constant, linear, and colorInterpolate are taken from d3-interpolate
56 // the colorInterpolate function is `nogamma` in the d3-interpolate's color.js
57 const constant = x => () => x;
58
59 const linear = (a, d) => t => a + t * d;
60
61 const colorInterpolate = (a, b) => {
62 const d = b - a;
63 return d ? linear(a, d) : constant(isNaN(a) ? b : a);
64 };
65
66 start = _d.default.jch(start);
67 end = _d.default.jch(end);
68 const zero = Math.abs(start.h - end.h);
69 const plus = Math.abs(start.h - (end.h + 360));
70 const minus = Math.abs(start.h - (end.h - 360));
71
72 if (plus < zero && plus < minus) {
73 end.h += 360;
74 }
75
76 if (minus < zero && minus < plus) {
77 end.h -= 360;
78 }
79
80 const startc = _d.default.hcl(start + '').c;
81
82 const endc = _d.default.hcl(end + '').c;
83
84 if (!startc) {
85 start.h = end.h;
86 }
87
88 if (!endc) {
89 end.h = start.h;
90 }
91
92 const J = colorInterpolate(start.J, end.J),
93 C = colorInterpolate(start.C, end.C),
94 h = colorInterpolate(start.h, end.h),
95 opacity = colorInterpolate(start.opacity, end.opacity);
96 return t => {
97 start.J = J(t);
98 start.C = C(t);
99 start.h = h(t);
100 start.opacity = opacity(t);
101 return start + '';
102 };
103};
104
105function smoothScale(ColorsArray, domains, space) {
106 const points = space.channels.map(() => []);
107 ColorsArray.forEach((color, i) => points.forEach((point, j) => point.push(domains[i], color[space.channels[j]])));
108
109 if (space.name == "hcl") {
110 const point = points[1];
111
112 for (let i = 1; i < point.length; i += 2) {
113 if (isNaN(point[i])) {
114 point[i] = 0;
115 }
116 }
117 }
118
119 points.forEach(point => {
120 const nans = []; // leading NaNs
121
122 for (let i = 1; i < point.length; i += 2) {
123 if (isNaN(point[i])) {
124 nans.push(i);
125 } else {
126 nans.forEach(j => point[j] = point[i]);
127 nans.length = 0;
128 break;
129 }
130 } // all are grey case
131
132
133 if (nans.length) {
134 // hue is not important except for JCh
135 const safeJChHue = _d.default.jch("#ccc").h;
136
137 nans.forEach(j => point[j] = safeJChHue);
138 }
139
140 nans.length = 0; // trailing NaNs
141
142 for (let i = point.length - 1; i > 0; i -= 2) {
143 if (isNaN(point[i])) {
144 nans.push(i);
145 } else {
146 nans.forEach(j => point[j] = point[i]);
147 break;
148 }
149 } // other NaNs
150
151
152 for (let i = 1; i < point.length; i += 2) {
153 if (isNaN(point[i])) {
154 point.splice(i - 1, 2);
155 i -= 2;
156 }
157 } // force hue to go on the shortest route
158
159
160 if (space.name in {
161 hcl: 1,
162 hsl: 1,
163 hsluv: 1,
164 hsv: 1,
165 jch: 1
166 }) {
167 let prev = point[1];
168 let addon = 0;
169
170 for (let i = 3; i < point.length; i += 2) {
171 const p = point[i] + addon;
172 const zero = Math.abs(prev - p);
173 const plus = Math.abs(prev - (p + 360));
174 const minus = Math.abs(prev - (p - 360));
175
176 if (plus < zero && plus < minus) {
177 addon += 360;
178 }
179
180 if (minus < zero && minus < plus) {
181 addon -= 360;
182 }
183
184 point[i] += addon;
185 prev = point[i];
186 }
187 }
188 });
189 const prep = points.map(point => (0, _curve.catmullRom2bezier)(point).map(curve => (0, _curve.prepareCurve)(...curve)));
190 return d => {
191 const ch = prep.map(p => {
192 for (let i = 0; i < p.length; i++) {
193 const res = p[i](d);
194
195 if (res != null) {
196 return res;
197 }
198 }
199 });
200
201 if (space.name == 'jch' && ch[1] < 0) {
202 ch[1] = 0;
203 }
204
205 return _d.default[space.name](...ch) + "";
206 };
207} // assign(d3, d3hsluv, d3hsv, d3cam02);
208
209
210const colorSpaces = {
211 CAM02: {
212 name: 'jab',
213 channels: ['J', 'a', 'b'],
214 interpolator: _d.default.interpolateJab
215 },
216 CAM02p: {
217 name: 'jch',
218 channels: ['J', 'C', 'h'],
219 interpolator: interpolateJch
220 },
221 LCH: {
222 name: 'hcl',
223 channels: ['h', 'c', 'l'],
224 interpolator: _d.default.interpolateHcl,
225 white: _d.default.hcl(NaN, 0, 100),
226 black: _d.default.hcl(NaN, 0, 0)
227 },
228 LAB: {
229 name: 'lab',
230 channels: ['l', 'a', 'b'],
231 interpolator: _d.default.interpolateLab
232 },
233 HSL: {
234 name: 'hsl',
235 channels: ['h', 's', 'l'],
236 interpolator: _d.default.interpolateHsl
237 },
238 HSLuv: {
239 name: 'hsluv',
240 channels: ['l', 'u', 'v'],
241 interpolator: _d.default.interpolateHsluv,
242 white: _d3Hsluv.default.hsluv(NaN, NaN, 100),
243 black: _d3Hsluv.default.hsluv(NaN, NaN, 0)
244 },
245 RGB: {
246 name: 'rgb',
247 channels: ['r', 'g', 'b'],
248 interpolator: _d.default.interpolateRgb
249 },
250 HSV: {
251 name: 'hsv',
252 channels: ['h', 's', 'v'],
253 interpolator: _d.default.interpolateHsv
254 }
255};
256
257function cArray(c) {
258 const color = _d3Hsluv.default.hsluv(c);
259
260 const L = color.l;
261 const U = color.u;
262 const V = color.v;
263 return [L, U, V];
264}
265
266function removeDuplicates(originalArray, prop) {
267 var newArray = [];
268 var lookupObject = {};
269
270 for (var i in originalArray) {
271 lookupObject[originalArray[i][prop]] = originalArray[i];
272 }
273
274 for (i in lookupObject) {
275 newArray.push(lookupObject[i]);
276 }
277
278 return newArray;
279}
280
281function createScale({
282 swatches,
283 colorKeys,
284 colorspace = 'LAB',
285 shift = 1,
286 fullScale = true,
287 smooth = false
288} = {}) {
289 const space = colorSpaces[colorspace];
290
291 if (!space) {
292 throw new Error(`Colorspace “${colorspace}” not supported`);
293 }
294
295 let domains = colorKeys.map(key => swatches - swatches * (_d3Hsluv.default.hsluv(key).v / 100)).sort((a, b) => a - b).concat(swatches);
296 domains.unshift(0); // Test logarithmic domain (for non-contrast-based scales)
297
298 let sqrtDomains = _d.default.scalePow().exponent(shift).domain([1, swatches]).range([1, swatches]);
299
300 sqrtDomains = domains.map(d => {
301 if (sqrtDomains(d) < 0) {
302 return 0;
303 }
304
305 return sqrtDomains(d);
306 }); // Transform square root in order to smooth gradient
307
308 domains = sqrtDomains;
309 let sortedColor = colorKeys // Convert to HSLuv and keep track of original indices
310 .map((c, i) => {
311 return {
312 colorKeys: cArray(c),
313 index: i
314 };
315 }) // Sort by lightness
316 .sort((c1, c2) => c2.colorKeys[2] - c1.colorKeys[2]) // Retrieve original RGB color
317 .map(data => colorKeys[data.index]);
318 let inverseSortedColor = colorKeys // Convert to HSLuv and keep track of original indices
319 .map((c, i) => {
320 return {
321 colorKeys: cArray(c),
322 index: i
323 };
324 }) // Sort by lightness
325 .sort((c1, c2) => c1.colorKeys[2] - c2.colorKeys[2]) // Retrieve original RGB color
326 .map(data => colorKeys[data.index]);
327 let ColorsArray = [];
328 let scale;
329
330 if (fullScale) {
331 ColorsArray = [space.white || '#fff', ...sortedColor, space.black || '#000'];
332 } else {
333 ColorsArray = sortedColor;
334 }
335
336 const stringColors = ColorsArray;
337 ColorsArray = ColorsArray.map(d => _d.default[space.name](d));
338
339 if (space.name == 'hcl') {
340 // special case for HCL if C is NaN we should treat it as 0
341 ColorsArray.forEach(c => c.c = isNaN(c.c) ? 0 : c.c);
342 }
343
344 if (space.name == 'jch') {
345 // JCh has some “random” hue for grey colors.
346 // Replacing it to NaN, so we can apply the same method of dealing with them.
347 for (let i = 0; i < stringColors.length; i++) {
348 const color = _d.default.hcl(stringColors[i]);
349
350 if (!color.c) {
351 ColorsArray[i].h = NaN;
352 }
353 }
354 }
355
356 if (smooth) {
357 scale = smoothScale(ColorsArray, domains, space);
358 } else {
359 scale = _d.default.scaleLinear().range(ColorsArray).domain(domains).interpolate(space.interpolator);
360 }
361
362 let Colors = _d.default.range(swatches).map(d => scale(d));
363
364 let colors = Colors.filter(el => el != null); // Return colors as hex values for interpolators.
365
366 let colorsHex = [];
367
368 for (let i = 0; i < colors.length; i++) {
369 colorsHex.push(_d.default.rgb(colors[i]).formatHex());
370 }
371
372 return {
373 colorKeys: colorKeys,
374 colorspace: colorspace,
375 shift: shift,
376 colors: colors,
377 scale: scale,
378 colorsHex: colorsHex
379 };
380}
381
382function generateBaseScale({
383 colorKeys,
384 colorspace = 'LAB',
385 smooth
386} = {}) {
387 // create massive scale
388 let swatches = 1000;
389 let scale = createScale({
390 swatches: swatches,
391 colorKeys: colorKeys,
392 colorspace: colorspace,
393 shift: 1,
394 smooth: smooth
395 });
396 let newColors = scale.colorsHex;
397 let colorObj = newColors // Convert to HSLuv and keep track of original indices
398 .map((c, i) => {
399 return {
400 value: Math.round(cArray(c)[2]),
401 index: i
402 };
403 });
404 let filteredArr = removeDuplicates(colorObj, "value").map(data => newColors[data.index]);
405 return filteredArr;
406}
407
408function generateContrastColors({
409 colorKeys,
410 base,
411 ratios,
412 colorspace = 'LAB',
413 smooth = false
414} = {}) {
415 if (!base) {
416 throw new Error(`Base is undefined`);
417 }
418
419 if (!colorKeys) {
420 throw new Error(`Color Keys are undefined`);
421 }
422
423 for (let i = 0; i < colorKeys.length; i++) {
424 if (colorKeys[i].length < 6) {
425 throw new Error('Color Key must be greater than 6 and include hash # if hex.');
426 } else if (colorKeys[i].length == 6 && colorKeys[i].charAt(0) != 0) {
427 throw new Error('Color Key missing hash #');
428 }
429 }
430
431 if (!ratios) {
432 throw new Error(`Ratios are undefined`);
433 }
434
435 let swatches = 3000;
436 let scaleData = createScale({
437 swatches: swatches,
438 colorKeys: colorKeys,
439 colorspace: colorspace,
440 shift: 1,
441 smooth: smooth
442 });
443 let baseV = _d3Hsluv.default.hsluv(base).v / 100;
444
445 let Contrasts = _d.default.range(swatches).map(d => {
446 let rgbArray = [_d.default.rgb(scaleData.scale(d)).r, _d.default.rgb(scaleData.scale(d)).g, _d.default.rgb(scaleData.scale(d)).b];
447 let baseRgbArray = [_d.default.rgb(base).r, _d.default.rgb(base).g, _d.default.rgb(base).b];
448 let ca = contrast(rgbArray, baseRgbArray, baseV).toFixed(2);
449 return Number(ca);
450 });
451
452 let contrasts = Contrasts.filter(el => el != null);
453 let newColors = [];
454 ratios = ratios.map(Number); // Return color matching target ratio, or closest number
455
456 for (let i = 0; i < ratios.length; i++) {
457 let r = binarySearch(contrasts, ratios[i], baseV);
458 newColors.push(_d.default.rgb(scaleData.colors[r]).hex());
459 }
460
461 return newColors;
462}
463
464function luminance(r, g, b) {
465 let a = [r, g, b].map(v => {
466 v /= 255;
467 return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
468 });
469 return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
470}
471
472function contrast(color, base, baseV) {
473 let colorLum = luminance(color[0], color[1], color[2]);
474 let baseLum = luminance(base[0], base[1], base[2]);
475 let cr1 = (colorLum + 0.05) / (baseLum + 0.05);
476 let cr2 = (baseLum + 0.05) / (colorLum + 0.05);
477
478 if (baseV < 0.5) {
479 if (cr1 >= 1) {
480 return cr1;
481 } else {
482 return cr2 * -1;
483 } // Return as whole negative number
484
485 } else {
486 if (cr1 < 1) {
487 return cr2;
488 } else {
489 return cr1 * -1;
490 } // Return as whole negative number
491
492 }
493}
494
495function minPositive(r) {
496 if (!r) {
497 throw new Error('Array undefined');
498 }
499
500 if (!Array.isArray(r)) {
501 throw new Error('Passed object is not an array');
502 }
503
504 let arr = [];
505
506 for (let i = 0; i < r.length; i++) {
507 if (r[i] >= 1) {
508 arr.push(r[i]);
509 }
510 }
511
512 return Math.min(...arr);
513}
514
515function ratioName(r) {
516 if (!r) {
517 throw new Error('Ratios undefined');
518 }
519
520 r = r.sort(function (a, b) {
521 return a - b;
522 }); // sort ratio array in case unordered
523
524 let min = minPositive(r);
525 let minIndex = r.indexOf(min);
526 let nArr = []; // names array
527
528 let rNeg = r.slice(0, minIndex);
529 let rPos = r.slice(minIndex, r.length); // Name the negative values
530
531 for (let i = 0; i < rNeg.length; i++) {
532 let d = 1 / (rNeg.length + 1);
533 let m = d * 100;
534 let nVal = m * (i + 1);
535 nArr.push(Number(nVal.toFixed()));
536 } // Name the positive values
537
538
539 for (let i = 0; i < rPos.length; i++) {
540 nArr.push((i + 1) * 100);
541 }
542
543 nArr.sort(function (a, b) {
544 return a - b;
545 }); // just for safe measure
546
547 return nArr;
548}
549
550function generateAdaptiveTheme({
551 colorScales,
552 baseScale,
553 brightness,
554 contrast = 1
555}) {
556 if (!baseScale) {
557 throw new Error('baseScale is undefined');
558 }
559
560 let found = false;
561
562 for (let i = 0; i < colorScales.length; i++) {
563 if (colorScales[i].name !== baseScale) {
564 found = true;
565 }
566 }
567
568 if (found = false) {
569 throw new Error('baseScale must match the name of a colorScales object');
570 }
571
572 if (!colorScales) {
573 throw new Error('colorScales are undefined');
574 }
575
576 if (!Array.isArray(colorScales)) {
577 throw new Error('colorScales must be an array of objects');
578 }
579
580 if (brightness === undefined) {
581 return function (brightness, contrast) {
582 return generateAdaptiveTheme({
583 baseScale: baseScale,
584 colorScales: colorScales,
585 brightness: brightness,
586 contrast: contrast
587 });
588 };
589 } else {
590 // Find color object matching base scale
591 let baseIndex = colorScales.findIndex(x => x.name === baseScale);
592 let baseKeys = colorScales[baseIndex].colorKeys;
593 let baseMode = colorScales[baseIndex].colorspace;
594 let smooth = colorScales[baseIndex].smooth; // define params to pass as bscale
595
596 let bscale = generateBaseScale({
597 colorKeys: baseKeys,
598 colorspace: baseMode,
599 smooth: smooth
600 }); // base parameter to create base scale (0-100)
601
602 let bval = bscale[brightness];
603 let baseObj = {
604 background: bval
605 };
606 let arr = [];
607 arr.push(baseObj);
608
609 for (let i = 0; i < colorScales.length; i++) {
610 if (!colorScales[i].name) {
611 throw new Error('Color missing name');
612 }
613
614 let name = colorScales[i].name;
615 let ratios = colorScales[i].ratios;
616 let smooth = colorScales[i].smooth;
617 let newArr = [];
618 let colorObj = {
619 name: name,
620 values: newArr
621 };
622 ratios = ratios.map(function (d) {
623 let r;
624
625 if (d > 1) {
626 r = (d - 1) * contrast + 1;
627 } else if (d < -1) {
628 r = (d + 1) * contrast - 1;
629 } else {
630 r = 1;
631 }
632
633 return Number(r.toFixed(2));
634 });
635 let outputColors = generateContrastColors({
636 colorKeys: colorScales[i].colorKeys,
637 colorspace: colorScales[i].colorspace,
638 ratios: ratios,
639 base: bval,
640 smooth: smooth
641 });
642
643 for (let i = 0; i < outputColors.length; i++) {
644 let rVal = ratioName(ratios)[i];
645 let n = name.concat(rVal);
646 let obj = {
647 name: n,
648 contrast: ratios[i],
649 value: outputColors[i]
650 };
651 newArr.push(obj);
652 }
653
654 arr.push(colorObj);
655 }
656
657 return arr;
658 }
659} // Binary search to find index of contrast ratio that is input
660// Modified from https://medium.com/hackernoon/programming-with-js-binary-search-aaf86cef9cb3
661
662
663function binarySearch(list, value, baseLum) {
664 // initial values for start, middle and end
665 let start = 0;
666 let stop = list.length - 1;
667 let middle = Math.floor((start + stop) / 2);
668 let minContrast = Math.min(...list);
669 let maxContrast = Math.max(...list); // While the middle is not what we're looking for and the list does not have a single item
670
671 while (list[middle] !== value && start < stop) {
672 // Value greater than since array is ordered descending
673 if (baseLum > 0.5) {
674 // if base is light, ratios ordered ascending
675 if (value < list[middle]) {
676 stop = middle - 1;
677 } else {
678 start = middle + 1;
679 }
680 } else {
681 // order descending
682 if (value > list[middle]) {
683 stop = middle - 1;
684 } else {
685 start = middle + 1;
686 }
687 } // recalculate middle on every iteration
688
689
690 middle = Math.floor((start + stop) / 2);
691 } // If no match, find closest item greater than value
692
693
694 let closest = list.reduce((prev, curr) => curr > value ? curr : prev); // if the current middle item is what we're looking for return it's index, else closest
695
696 return list[middle] == !value ? closest : middle; // how it was originally expressed
697}
\No newline at end of file