Skip to content

Commit

Permalink
Merge pull request #6 from mapbox/STUDIO-2364-d3-color
Browse files Browse the repository at this point in the history
Studio-2364 Update d3-color and remove d3-color-difference dependency
  • Loading branch information
badiuoanaalexandra authored May 28, 2024
2 parents c13c3d5 + 475f96b commit 895c00e
Show file tree
Hide file tree
Showing 8 changed files with 8,449 additions and 12,698 deletions.
9 changes: 9 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

module.exports = {
env: {
test: {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
}
}
};
12 changes: 12 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

const config = {
transform: {
'\\.[jt]s?$': 'babel-jest'
},
testEnvironment: 'node',
transformIgnorePatterns: ['/node_modules/(?!d3-color)'],
clearMocks: true
};

module.exports = config;
20,881 changes: 8,215 additions & 12,666 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mapbox/to-color",
"version": "2.1.0",
"version": "2.1.1",
"description": "Procedurally generate a deterministic, perceptually distributed color palette.",
"main": "dist/to-color.js",
"scripts": {
Expand Down Expand Up @@ -37,8 +37,8 @@
"prettier": "^2.2.1",
"rollup": "^2.38.1",
"rollup-plugin-babel": "^4.4.0",
"serve": "^11.3.2",
"watch": "^1.0.2"
"serve": "^14.2.3",
"watch": "^0.13.0"
},
"babel": {
"presets": [
Expand Down Expand Up @@ -70,6 +70,6 @@
"url": "[email protected]:mapbox/to-color.git"
},
"dependencies": {
"d3-color-difference": "^0.1.3"
"d3-color": "^3.1.0"
}
}
}
105 changes: 105 additions & 0 deletions src/d3-color-difference/differenceCiede2000.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { lab } from 'd3-color';

/*
CIEDE2000 color difference, original Matlab implementation by Gaurav Sharma
Based on "The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations"
by Gaurav Sharma, Wencheng Wu, Edul N. Dalal in Color Research and Application, vol. 30. No. 1, pp. 21-30, February 2005.
http://www2.ece.rochester.edu/~gsharma/ciede2000/
*/

function differenceCiede2000(kL, kC, kH) {
kL = kL !== undefined ? kL : 1;
kC = kC !== undefined ? kC : 1;
kH = kH !== undefined ? kH : 1;

return function (std, smp) {
var LabStd = lab(std);
var LabSmp = lab(smp);

var lStd = LabStd.l;
var aStd = LabStd.a;
var bStd = LabStd.b;
var cStd = Math.sqrt(aStd * aStd + bStd * bStd);

var lSmp = LabSmp.l;
var aSmp = LabSmp.a;
var bSmp = LabSmp.b;
var cSmp = Math.sqrt(aSmp * aSmp + bSmp * bSmp);

var cAvg = (cStd + cSmp) / 2;

var G =
0.5 *
(1 -
Math.sqrt(Math.pow(cAvg, 7) / (Math.pow(cAvg, 7) + Math.pow(25, 7))));

var apStd = aStd * (1 + G);
var apSmp = aSmp * (1 + G);

var cpStd = Math.sqrt(apStd * apStd + bStd * bStd);
var cpSmp = Math.sqrt(apSmp * apSmp + bSmp * bSmp);

var hpStd =
Math.abs(apStd) + Math.abs(bStd) === 0 ? 0 : Math.atan2(bStd, apStd);
hpStd += (hpStd < 0) * 2 * Math.PI;

var hpSmp =
Math.abs(apSmp) + Math.abs(bSmp) === 0 ? 0 : Math.atan2(bSmp, apSmp);
hpSmp += (hpSmp < 0) * 2 * Math.PI;

var dL = lSmp - lStd;
var dC = cpSmp - cpStd;

var dhp = cpStd * cpSmp === 0 ? 0 : hpSmp - hpStd;
dhp -= (dhp > Math.PI) * 2 * Math.PI;
dhp += (dhp < -Math.PI) * 2 * Math.PI;

var dH = 2 * Math.sqrt(cpStd * cpSmp) * Math.sin(dhp / 2);

var Lp = (lStd + lSmp) / 2;
var Cp = (cpStd + cpSmp) / 2;

var hp;
if (cpStd * cpSmp === 0) {
hp = hpStd + hpSmp;
} else {
hp = (hpStd + hpSmp) / 2;
hp -= (Math.abs(hpStd - hpSmp) > Math.PI) * Math.PI;
hp += (hp < 0) * 2 * Math.PI;
}

var Lpm50 = Math.pow(Lp - 50, 2);
var T =
1 -
0.17 * Math.cos(hp - Math.PI / 6) +
0.24 * Math.cos(2 * hp) +
0.32 * Math.cos(3 * hp + Math.PI / 30) -
0.2 * Math.cos(4 * hp - (63 * Math.PI) / 180);

var Sl = 1 + (0.015 * Lpm50) / Math.sqrt(20 + Lpm50);
var Sc = 1 + 0.045 * Cp;
var Sh = 1 + 0.015 * Cp * T;

var deltaTheta =
((30 * Math.PI) / 180) *
Math.exp(-1 * Math.pow(((180 / Math.PI) * hp - 275) / 25, 2));
var Rc =
2 * Math.sqrt(Math.pow(Cp, 7) / (Math.pow(Cp, 7) + Math.pow(25, 7)));

var Rt = -1 * Math.sin(2 * deltaTheta) * Rc;

return Math.sqrt(
Math.pow(dL / (kL * Sl), 2) +
Math.pow(dC / (kC * Sc), 2) +
Math.pow(dH / (kH * Sh), 2) +
(((Rt * dC) / (kC * Sc)) * dH) / (kH * Sh)
);
};
}

var differenceCiede2000Default = differenceCiede2000();

export {
differenceCiede2000Default as default,
differenceCiede2000 as differenceCiede2000Weighted
};
72 changes: 72 additions & 0 deletions src/d3-color-difference/differenceCiede2000.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { differenceCiede2000 } from '.';
import { lab } from 'd3-color';

// Test data from: http://www2.ece.rochester.edu/~gsharma/ciede2000/
let testdata = `50.0000 2.6772 -79.7751 50.0000 0.0000 -82.7485 2.0425
50.0000 3.1571 -77.2803 50.0000 0.0000 -82.7485 2.8615
50.0000 2.8361 -74.0200 50.0000 0.0000 -82.7485 3.4412
50.0000 -1.3802 -84.2814 50.0000 0.0000 -82.7485 1.0000
50.0000 -1.1848 -84.8006 50.0000 0.0000 -82.7485 1.0000
50.0000 -0.9009 -85.5211 50.0000 0.0000 -82.7485 1.0000
50.0000 0.0000 0.0000 50.0000 -1.0000 2.0000 2.3669
50.0000 -1.0000 2.0000 50.0000 0.0000 0.0000 2.3669
50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0009 7.1792
50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0010 7.1792
50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0011 7.2195
50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0012 7.2195
50.0000 -0.0010 2.4900 50.0000 0.0009 -2.4900 4.8045
50.0000 -0.0010 2.4900 50.0000 0.0010 -2.4900 4.8045
50.0000 -0.0010 2.4900 50.0000 0.0011 -2.4900 4.7461
50.0000 2.5000 0.0000 50.0000 0.0000 -2.5000 4.3065
50.0000 2.5000 0.0000 73.0000 25.0000 -18.0000 27.1492
50.0000 2.5000 0.0000 61.0000 -5.0000 29.0000 22.8977
50.0000 2.5000 0.0000 56.0000 -27.0000 -3.0000 31.9030
50.0000 2.5000 0.0000 58.0000 24.0000 15.0000 19.4535
50.0000 2.5000 0.0000 50.0000 3.1736 0.5854 1.0000
50.0000 2.5000 0.0000 50.0000 3.2972 0.0000 1.0000
50.0000 2.5000 0.0000 50.0000 1.8634 0.5757 1.0000
50.0000 2.5000 0.0000 50.0000 3.2592 0.3350 1.0000
60.2574 -34.0099 36.2677 60.4626 -34.1751 39.4387 1.2644
63.0109 -31.0961 -5.8663 62.8187 -29.7946 -4.0864 1.2630
61.2901 3.7196 -5.3901 61.4292 2.2480 -4.9620 1.8731
35.0831 -44.1164 3.7933 35.0232 -40.0716 1.5901 1.8645
22.7233 20.0904 -46.6940 23.0331 14.9730 -42.5619 2.0373
36.4612 47.8580 18.3852 36.2715 50.5065 21.2231 1.4146
90.8027 -2.0831 1.4410 91.1528 -1.6435 0.0447 1.4441
90.9257 -0.5406 -0.9208 88.6381 -0.8985 -0.7239 1.5381
6.7747 -0.2908 -2.4247 5.8714 -0.0985 -2.2286 0.6377
2.0776 0.0795 -1.1350 0.9033 -0.0636 -0.5514 0.9082`
.split('\n')
.map((line) => line.trim().split(/\s+/));

function round(value, precision) {
return Math.round(value * (precision = Math.pow(10, precision))) / precision;
}

describe('differenceCiede2000', () => {
it('Computes correctly the Sharma test data', () => {
for (var i = 0; i < testdata.length; i++) {
let line = testdata[i];

expect(
round(
differenceCiede2000(
lab(line[0], line[1], line[2]),
lab(line[3], line[4], line[5])
),
4
)
).toBe(+line[6]);

expect(
round(
differenceCiede2000(
lab(line[3], line[4], line[5]),
lab(line[0], line[1], line[2])
),
4
)
).toBe(+line[6]);
}
});
});
4 changes: 4 additions & 0 deletions src/d3-color-difference/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
default as differenceCiede2000,
differenceCiede2000Weighted
} from './differenceCiede2000';
54 changes: 27 additions & 27 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { differenceCiede2000 } from 'd3-color-difference';
import { differenceCiede2000 } from './d3-color-difference';

export default class toColor {
HUE_MAX = 360;
Expand All @@ -11,7 +11,7 @@ export default class toColor {
blue: [178, 257],
purple: [257, 282],
pink: [282, 334]
}
};

constructor(seed, options) {
this.options = options || {};
Expand All @@ -38,21 +38,25 @@ export default class toColor {

// Detect color similarity. If values are too close to one another, call
// getColor until enough dissimilarity is achieved.
if (this.known.length &&
this.known.some(v => differenceCiede2000(v, formatted) < ACTUAL_DISTANCE)) {
count++
if (
this.known.length &&
this.known.some(
(v) => differenceCiede2000(v, formatted) < ACTUAL_DISTANCE
)
) {
count++;
return this.getColor(count);
} else {
this.known.push(formatted);
// Apply modifiers after distribution check + regeneration to ensure
// colors with brightness/saturation adjustments remain the same.
return this._colorWithModifiers(h, s, b)
return this._colorWithModifiers(h, s, b);
}
}

_colorWithModifiers = (h, s, b) => {
const clamp = (n, min, max) => n <= min ? min : n >= max ? max : n;
const percentage = (n, per) => ((n / 100) * per) * 100;
const clamp = (n, min, max) => (n <= min ? min : n >= max ? max : n);
const percentage = (n, per) => (n / 100) * per * 100;
const { brightness, saturation } = this.options;

// Modify brightness/saturation if provided
Expand All @@ -68,9 +72,9 @@ export default class toColor {
formatted: this._formatHSL(hsl)
}
};
}
};

_formatHSL = hsl => `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`
_formatHSL = (hsl) => `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;

_pickHue = () => {
let hue = this._pseudoRandom([0, this.HUE_MAX]);
Expand All @@ -85,31 +89,31 @@ export default class toColor {

// Limit the max of some hues if the option is passed.
const { limit } = this.options;

if (limit && limit.length) {
for (let i = 0; i !== limit.length; i++) {
const hueRange = this.hues?.[limit[i]];
if (hueRange && hue > hueRange[0] && hue <= hueRange[1]) {
return this._pickHue();
}
};
}
}

return hue;
}
};

_pickSaturation = (h) => {
const saturationRange = this._getColorInfo(h)[2];
const min = saturationRange[0];
const max = saturationRange[1];
return this._pseudoRandom([min, max]);
}
};

_pickBrightness = (h, s) => {
const min = this._getMinimumBrightness(h, s);
const max = 100;
return this._pseudoRandom([min, max]);
}
};

_getMinimumBrightness = (h, s) => {
const lowerBounds = this._getColorInfo(h)[1];
Expand All @@ -125,7 +129,7 @@ export default class toColor {
}
}
return 0;
}
};

// A linear congruential generator (LCG) algorithm that yields a sequence of
// pseudo-randomized numbers calculated with a discontinuous piecewise linear
Expand All @@ -136,7 +140,7 @@ export default class toColor {
this.seed = (this.seed * 9301 + 49297) % 233280;
const rnd = this.seed / 233280;
return Math.trunc(min + rnd * (max - min));
}
};

_getColorInfo = (hue) => {
// Red is on both ends of the color spectrum. Map them together:
Expand All @@ -145,22 +149,18 @@ export default class toColor {
}

return this._colorDictionary.find((c) => hue >= c[0][0] && hue <= c[0][1]);
}
};

_HSVtoHSL = (h, s, v) => {
const round = (num) => Math.trunc((num + Number.EPSILON) * 100) / 100;
const l = (2 - s / 100) * v / 2;
let saturation = s * v / (l < 50 ? l * 2 : 200 - l * 2);
const l = ((2 - s / 100) * v) / 2;
let saturation = (s * v) / (l < 50 ? l * 2 : 200 - l * 2);

// Handle division-by-zero
if (isNaN(saturation)) saturation = 0;

return [
h,
round(saturation),
round(l)
];
}
return [h, round(saturation), round(l)];
};

_stringToInteger = (string) => {
let total = 0;
Expand All @@ -169,7 +169,7 @@ export default class toColor {
total += string.charCodeAt(i);
}
return total;
}
};

// Color dictionary is a collection of subjective values, each containing:
// - Hue range for a given color
Expand Down

0 comments on commit 895c00e

Please sign in to comment.