1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | const chromajs = require('chroma-js');
|
14 | const hsluv = require('hsluv');
|
15 | const ciebase = require('ciebase');
|
16 | const ciecam02 = require('ciecam02');
|
17 |
|
18 | const cam = ciecam02.cam({
|
19 | whitePoint: ciebase.illuminant.D65,
|
20 | adaptingLuminance: 40,
|
21 | backgroundLuminance: 20,
|
22 | surroundType: 'average',
|
23 | discounting: false,
|
24 | }, ciecam02.cfs('JCh'));
|
25 |
|
26 | const xyz = ciebase.xyz(ciebase.workspace.sRGB, ciebase.illuminant.D65);
|
27 | const jch2rgb = (jch) => xyz.toRgb(cam.toXyz({ J: jch[0], C: jch[1], h: jch[2] }));
|
28 | const rgb2jch = (rgb) => {
|
29 | const jch = cam.fromXyz(xyz.fromRgb(rgb));
|
30 | return [jch.J, jch.C, jch.h];
|
31 | };
|
32 | const [jch2jab, jab2jch] = (() => {
|
33 | const coefs = { k_l: 1, c1: 0.007, c2: 0.0228 };
|
34 | const π = Math.PI;
|
35 | const CIECAM02_la = (64 / π) / 5;
|
36 | const CIECAM02_k = 1 / ((5 * CIECAM02_la) + 1);
|
37 | const CIECAM02_fl = (0.2 * (CIECAM02_k ** 4) * (5 * CIECAM02_la)) + 0.1 * ((1 - (CIECAM02_k ** 4)) ** 2) * ((5 * CIECAM02_la) ** (1 / 3));
|
38 | return [(jch) => {
|
39 | const [J, C, h] = jch;
|
40 | const M = C * (CIECAM02_fl ** 0.25);
|
41 | let j = ((1 + 100 * coefs.c1) * J) / (1 + coefs.c1 * J);
|
42 | j /= coefs.k_l;
|
43 | const MPrime = (1 / coefs.c2) * Math.log(1.0 + coefs.c2 * M);
|
44 | const a = MPrime * Math.cos(h * (π / 180));
|
45 | const b = MPrime * Math.sin(h * (π / 180));
|
46 | return [j, a, b];
|
47 | }, (jab) => {
|
48 | const [j, a, b] = jab;
|
49 | const newMPrime = Math.sqrt(a * a + b * b);
|
50 | const newM = (Math.exp(newMPrime * coefs.c2) - 1) / coefs.c2;
|
51 | const h = ((180 / π) * Math.atan2(b, a) + 360) % 360;
|
52 | const C = newM / (CIECAM02_fl ** 0.25);
|
53 | const J = j / (1 + coefs.c1 * (100 - j));
|
54 | return [J, C, h];
|
55 | }];
|
56 | })();
|
57 |
|
58 | const jab2rgb = (jab) => jch2rgb(jab2jch(jab));
|
59 | const rgb2jab = (rgb) => jch2jab(rgb2jch(rgb));
|
60 |
|
61 | const con = console;
|
62 |
|
63 |
|
64 |
|
65 | con.color = (color, text = '') => {
|
66 | const col = chromajs(color);
|
67 | const l = col.luminance();
|
68 | con.log(`%c${color} ${text}`, `background-color: ${color};padding: 5px; border-radius: 5px; color: ${l > .5 ? '#000' : '#fff'}`);
|
69 | };
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | con.ramp = (scale, length = 1) => {
|
75 | con.log('%c ', `font-size: 1px;line-height: 16px;background: ${chromajs.getCSSGradient(scale, length)};padding: 0 0 0 200px; border-radius: 2px;`);
|
76 | };
|
77 |
|
78 | const online = (x1, y1, x2, y2, x3, y3, ε = .1) => {
|
79 | if (x1 === x2 || y1 === y2) {
|
80 | return true;
|
81 | }
|
82 | const m = (y2 - y1) / (x2 - x1);
|
83 | const x4 = (y3 + x3 / m - y1 + m * x1) / (m + 1 / m);
|
84 | const y4 = y3 + x3 / m - x4 / m;
|
85 | return (x3 - x4) ** 2 + (y3 - y4) ** 2 < ε ** 2;
|
86 | };
|
87 |
|
88 | const div = (ƒ, dot1, dot2, ε) => {
|
89 | const x3 = (dot1[0] + dot2[0]) / 2;
|
90 | const y3 = ƒ(x3);
|
91 | if (online(...dot1, ...dot2, x3, y3, ε)) {
|
92 | return null;
|
93 | }
|
94 | return [x3, y3];
|
95 | };
|
96 |
|
97 | const split = (ƒ, from, to, ε = .1) => {
|
98 | const step = (to - from) / 10;
|
99 | const points = [];
|
100 | for (let i = from; i < to; i += step) {
|
101 | points.push([i, ƒ(i)]);
|
102 | }
|
103 | points.push([to, ƒ(to)]);
|
104 | for (let i = 0; i < points.length - 1; i++) {
|
105 | const dot = div(ƒ, points[i], points[i + 1], ε);
|
106 | if (dot) {
|
107 | points.splice(i + 1, 0, dot);
|
108 | i--;
|
109 | }
|
110 | }
|
111 | for (let i = 0; i < points.length - 2; i++) {
|
112 | if (online(...points[i], ...points[i + 2], ...points[i + 1], ε)) {
|
113 | points.splice(i + 1, 1);
|
114 | i--;
|
115 | }
|
116 | }
|
117 | return points;
|
118 | };
|
119 |
|
120 | const round = (x, r = 4) => Math.round(x * 10 ** r) / 10 ** r;
|
121 |
|
122 | const getCSSGradient = (scale, length = 1, deg = 90, ε = .005) => {
|
123 | const ptsr = split((x) => scale(x).gl()[0], 0, length, ε);
|
124 | const ptsg = split((x) => scale(x).gl()[1], 0, length, ε);
|
125 | const ptsb = split((x) => scale(x).gl()[2], 0, length, ε);
|
126 | const points = Array.from(
|
127 | new Set(
|
128 | [
|
129 | ...ptsr.map((a) => round(a[0])),
|
130 | ...ptsg.map((a) => round(a[0])),
|
131 | ...ptsb.map((a) => round(a[0])),
|
132 | ].sort((a, b) => a - b),
|
133 | ),
|
134 | );
|
135 | return `linear-gradient(${deg}deg, ${points.map((x) => `${scale(x).hex()} ${round(x * 100)}%`).join()});`;
|
136 | };
|
137 |
|
138 | exports.extendChroma = (chroma) => {
|
139 |
|
140 | chroma.Color.prototype.jch = function () {
|
141 | return rgb2jch(this._rgb.slice(0, 3).map((c) => c / 255));
|
142 | };
|
143 |
|
144 | chroma.jch = (...args) => new chroma.Color(...jch2rgb(args).map((c) => Math.floor(c * 255)), 'rgb');
|
145 |
|
146 |
|
147 | chroma.Color.prototype.jab = function () {
|
148 | return rgb2jab(this._rgb.slice(0, 3).map((c) => c / 255));
|
149 | };
|
150 |
|
151 | chroma.jab = (...args) => new chroma.Color(...jab2rgb(args).map((c) => Math.floor(c * 255)), 'rgb');
|
152 |
|
153 |
|
154 | chroma.Color.prototype.hsluv = function () {
|
155 | return hsluv.rgbToHsluv(this._rgb.slice(0, 3).map((c) => c / 255));
|
156 | };
|
157 |
|
158 | chroma.hsluv = (...args) => new chroma.Color(...hsluv.hsluvToRgb(args).map((c) => Math.floor(c * 255)), 'rgb');
|
159 |
|
160 | const oldInterpol = chroma.interpolate;
|
161 | const RGB2 = {
|
162 | jch: rgb2jch,
|
163 | jab: rgb2jab,
|
164 | hsluv: hsluv.rgbToHsluv,
|
165 | };
|
166 | const lerpH = (a, b, t) => {
|
167 | const m = 360;
|
168 | const d = Math.abs(a - b);
|
169 | if (d > m / 2) {
|
170 | if (a > b) {
|
171 | b += m;
|
172 | } else {
|
173 | a += m;
|
174 | }
|
175 | }
|
176 | return ((1 - t) * a + t * b) % m;
|
177 | };
|
178 |
|
179 | chroma.interpolate = (col1, col2, f = 0.5, mode = 'lrgb') => {
|
180 | if (RGB2[mode]) {
|
181 | if (typeof col1 !== 'object') {
|
182 | col1 = new chroma.Color(col1);
|
183 | }
|
184 | if (typeof col2 !== 'object') {
|
185 | col2 = new chroma.Color(col2);
|
186 | }
|
187 | const xyz1 = RGB2[mode](col1.gl());
|
188 | const xyz2 = RGB2[mode](col2.gl());
|
189 | const grey1 = Number.isNaN(col1.hsl()[0]);
|
190 | const grey2 = Number.isNaN(col2.hsl()[0]);
|
191 | let X;
|
192 | let Y;
|
193 | let Z;
|
194 | switch (mode) {
|
195 | case 'hsluv':
|
196 | if (xyz1[1] < 1e-10) {
|
197 | xyz1[0] = xyz2[0];
|
198 | }
|
199 | if (xyz1[1] === 0) {
|
200 | xyz1[1] = xyz2[1];
|
201 | }
|
202 | if (xyz2[1] < 1e-10) {
|
203 | xyz2[0] = xyz1[0];
|
204 | }
|
205 | if (xyz2[1] === 0) {
|
206 | xyz2[1] = xyz1[1];
|
207 | }
|
208 | X = lerpH(xyz1[0], xyz2[0], f);
|
209 | Y = xyz1[1] + (xyz2[1] - xyz1[1]) * f;
|
210 | Z = xyz1[2] + (xyz2[2] - xyz1[2]) * f;
|
211 | break;
|
212 | case 'jch':
|
213 | if (grey1) {
|
214 | xyz1[2] = xyz2[2];
|
215 | }
|
216 | if (grey2) {
|
217 | xyz2[2] = xyz1[2];
|
218 | }
|
219 | X = xyz1[0] + (xyz2[0] - xyz1[0]) * f;
|
220 | Y = xyz1[1] + (xyz2[1] - xyz1[1]) * f;
|
221 | Z = lerpH(xyz1[2], xyz2[2], f);
|
222 | break;
|
223 | default:
|
224 | X = xyz1[0] + (xyz2[0] - xyz1[0]) * f;
|
225 | Y = xyz1[1] + (xyz2[1] - xyz1[1]) * f;
|
226 | Z = xyz1[2] + (xyz2[2] - xyz1[2]) * f;
|
227 | }
|
228 | return chroma[mode](X, Y, Z).alpha(col1.alpha() + f * (col2.alpha() - col1.alpha()));
|
229 | }
|
230 | return oldInterpol(col1, col2, f, mode);
|
231 | };
|
232 |
|
233 | chroma.getCSSGradient = getCSSGradient;
|
234 | };
|