UNPKG

7.45 kBJavaScriptView Raw
1/*
2Copyright 2019 Adobe. All rights reserved.
3This file is licensed to you under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License. You may obtain a copy
5of the License at http://www.apache.org/licenses/LICENSE-2.0
6
7Unless required by applicable law or agreed to in writing, software distributed under
8the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9OF ANY KIND, either express or implied. See the License for the specific language
10governing permissions and limitations under the License.
11*/
12
13const chromajs = require('chroma-js');
14const hsluv = require('hsluv');
15const ciebase = require('ciebase');
16const ciecam02 = require('ciecam02');
17
18const 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
26const xyz = ciebase.xyz(ciebase.workspace.sRGB, ciebase.illuminant.D65);
27const jch2rgb = (jch) => xyz.toRgb(cam.toXyz({ J: jch[0], C: jch[1], h: jch[2] }));
28const rgb2jch = (rgb) => {
29 const jch = cam.fromXyz(xyz.fromRgb(rgb));
30 return [jch.J, jch.C, jch.h];
31};
32const [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
58const jab2rgb = (jab) => jch2rgb(jab2jch(jab));
59const rgb2jab = (rgb) => jch2jab(rgb2jch(rgb));
60
61const con = console;
62
63// Usage:
64// console.color('rebeccapurple');
65con.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// Usage:
72// console.ramp(chroma.scale(['yellow', 'navy']).mode('hsluv'));
73// console.ramp(scale, 3000); // if you need to specify the length of the scale
74con.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
78const 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
88const 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
97const 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
120const round = (x, r = 4) => Math.round(x * 10 ** r) / 10 ** r;
121
122const 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
138exports.extendChroma = (chroma) => {
139 // JCH
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 // JAB
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 // HSLuv
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) { // black or white
200 xyz1[1] = xyz2[1];
201 }
202 if (xyz2[1] < 1e-10) {
203 xyz2[0] = xyz1[0];
204 }
205 if (xyz2[1] === 0) { // black or white
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};