338 lines
9.4 KiB
JavaScript
338 lines
9.4 KiB
JavaScript
// Min distance under which 2 colours are considered similar
|
|
let distanceThreshold = 10;
|
|
|
|
// Threshold used to consider a colour "dark"
|
|
let darkColoursThreshold = 50;
|
|
// Threshold used to tell if 2 dark colours are similar
|
|
let darkColoursSimilarityThreshold = 40;
|
|
|
|
// Threshold used to consider a colour "light"
|
|
let lightColoursThreshold = 190;
|
|
// Threshold used to tell if 2 light colours are similar
|
|
let lightColoursSimilarityThreshold = 30;
|
|
|
|
let referenceWhite = {
|
|
x: 95.05,
|
|
y: 100,
|
|
z: 108.89999999999999
|
|
};
|
|
const example = {
|
|
"red": [
|
|
"#bf6f4a",
|
|
"#e07438",
|
|
"#c64524",
|
|
"#ff5000"
|
|
],
|
|
"green": [
|
|
"#99e65f",
|
|
"#5ac54f",
|
|
"#33984b"
|
|
],
|
|
"blue": [
|
|
"#0069aa",
|
|
"#0098dc",
|
|
"#00cdf9"
|
|
],
|
|
"cyan": [
|
|
"#0069aa",
|
|
"#0098dc",
|
|
"#00cdf9",
|
|
"#0cf1ff"
|
|
],
|
|
"yellow": [
|
|
"#ffa214",
|
|
"#ffc825",
|
|
"#ffeb57"
|
|
],
|
|
"magenta": [
|
|
"#db3ffd"
|
|
],
|
|
"light": [
|
|
"#ffffff",
|
|
"#f9e6cf",
|
|
"#fdd2ed"
|
|
],
|
|
"dark": [
|
|
"#131313",
|
|
"#1b1b1b",
|
|
"#272727",
|
|
"#3d3d3d",
|
|
"#5d5d5d"
|
|
],
|
|
"brown": [
|
|
"#e69c69",
|
|
"#f6ca9f",
|
|
"#f9e6cf",
|
|
"#edab50",
|
|
"#e07438",
|
|
"#ed7614",
|
|
"#ffa214",
|
|
"#ffc825",
|
|
"#ffeb57"
|
|
],
|
|
"neon": [
|
|
"#ff0040",
|
|
"#ff5000",
|
|
"#ed7614",
|
|
"#ffa214",
|
|
"#ffc825",
|
|
"#0098dc",
|
|
"#00cdf9",
|
|
"#0cf1ff",
|
|
"#7a09fa",
|
|
"#3003d9"
|
|
]
|
|
};
|
|
const COLOR_META = {
|
|
red: { color: "#ff0000", flux:{ h:25, v:40, s:40} },
|
|
green: { color: "#00ff00", flux:{ h:35} },
|
|
blue: { color: "#0077dd", flux:{ h:25, v:30, s:30} },
|
|
cyan: { color: "#00ffff", flux:{ h:25, v:40, s:40} },
|
|
yellow: { color: "#ffff00", flux:{ h:25, v:40, s:40} },
|
|
magenta: { color: "#ff00ff", flux:{ h:15, v:40, s:40} },
|
|
light: { color: "#ffffff", flux:{ v:10, s:30} },
|
|
dark: { color: "#000000", flux:{ v:30, v:40, s:20} },
|
|
brown: { color: "#ffaa00", flux:{ h:20} },
|
|
neon: { color: "#00ffff", flux:{ s:20, v:20} },
|
|
};
|
|
Object.keys(COLOR_META).forEach(metaName=>{
|
|
COLOR_META[metaName].colorMeta = colorMeta(COLOR_META[metaName].color);
|
|
});
|
|
function paletteMeta(colorArr) {
|
|
const colorMetaArr = colorArr.map(colorMeta);
|
|
//////console.log('colorMetaArr === ',colorMetaArr);
|
|
|
|
const ret = {};
|
|
Object.keys(COLOR_META).forEach(metaName=>{
|
|
const {color,colorMeta,flux} = COLOR_META[metaName];
|
|
|
|
const fluxKeys = Object.keys(flux);
|
|
|
|
ret[metaName] = colorArr.filter((c,i)=>{
|
|
const colorMeta2 = colorMetaArr[i];
|
|
return fluxKeys.filter(k=>{
|
|
return (colorMeta[k] + flux[k]) > colorMeta2[k]
|
|
&&
|
|
(colorMeta[k] - flux[k]) < colorMeta2[k]
|
|
;
|
|
}).length === fluxKeys.length;
|
|
});
|
|
|
|
});
|
|
//////console.log(JSON.stringify(ret,null,4));
|
|
return ret;
|
|
}
|
|
function colorMeta(colorStr) {
|
|
const rgb = colorToRGB(colorStr);
|
|
const hsv = rgb2hsv(rgb.r, rgb.g, rgb.b);
|
|
const lab = rgb2lab(rgb.r, rgb.g, rgb.b);
|
|
const cie = {c:lab.l,i:lab.a,e:lab.b};
|
|
return {
|
|
...rgb,
|
|
...hsv,
|
|
...cie
|
|
};
|
|
}
|
|
function rgb2hex(r, g, b) {
|
|
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
}
|
|
function rgb2hsv(r, g, b) {
|
|
let rabs, gabs, babs, rr, gg, bb, h, s, v, diff, diffc, percentRoundFn;
|
|
rabs = r / 255;
|
|
gabs = g / 255;
|
|
babs = b / 255;
|
|
v = Math.max(rabs, gabs, babs),
|
|
diff = v - Math.min(rabs, gabs, babs);
|
|
diffc = c => (v - c) / 6 / diff + 1 / 2;
|
|
percentRoundFn = num => Math.round(num * 100) / 100;
|
|
if (diff == 0) {
|
|
h = s = 0;
|
|
} else {
|
|
s = diff / v;
|
|
rr = diffc(rabs);
|
|
gg = diffc(gabs);
|
|
bb = diffc(babs);
|
|
|
|
if (rabs === v) {
|
|
h = bb - gg;
|
|
} else if (gabs === v) {
|
|
h = (1 / 3) + rr - bb;
|
|
} else if (babs === v) {
|
|
h = (2 / 3) + gg - rr;
|
|
}
|
|
if (h < 0) {
|
|
h += 1;
|
|
}else if (h > 1) {
|
|
h -= 1;
|
|
}
|
|
}
|
|
return {
|
|
h: Math.round(h * 360),
|
|
s: percentRoundFn(s * 100),
|
|
v: percentRoundFn(v * 100)
|
|
};
|
|
}
|
|
|
|
function similarColors(rgb1, rgb2) {
|
|
let ret = differenceCiede2000(rgb1, rgb2)
|
|
//////console.log(ret);
|
|
return (ret < distanceThreshold && lightColoursCheck(rgb1, rgb2)) || darkColoursCheck(rgb1, rgb2);
|
|
}
|
|
function lightColoursCheck(rgb1, rgb2) {
|
|
let rDelta = Math.abs(rgb1.r - rgb2.r);
|
|
let gDelta = Math.abs(rgb1.g - rgb2.g);
|
|
let bDelta = Math.abs(rgb1.b - rgb2.b);
|
|
|
|
// Checking only if the colours are dark enough
|
|
if (rgb1.r > lightColoursThreshold && rgb1.g > lightColoursThreshold && rgb1.b > lightColoursThreshold &&
|
|
rgb2.r > lightColoursThreshold && rgb2.g > lightColoursThreshold && rgb2.b > lightColoursThreshold) {
|
|
return rDelta < lightColoursSimilarityThreshold && gDelta < lightColoursSimilarityThreshold &&
|
|
bDelta < lightColoursSimilarityThreshold;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
function darkColoursCheck(rgb1, rgb2) {
|
|
let rDelta = Math.abs(rgb1.r - rgb2.r);
|
|
let gDelta = Math.abs(rgb1.g - rgb2.g);
|
|
let bDelta = Math.abs(rgb1.b - rgb2.b);
|
|
|
|
// Checking only if the colours are dark enough
|
|
if (rgb1.r < darkColoursThreshold && rgb1.g < darkColoursThreshold && rgb1.b < darkColoursThreshold &&
|
|
rgb2.r < darkColoursThreshold && rgb2.g < darkColoursThreshold && rgb2.b < darkColoursThreshold) {
|
|
return rDelta < darkColoursSimilarityThreshold && gDelta < darkColoursSimilarityThreshold &&
|
|
bDelta < darkColoursSimilarityThreshold;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
// Distance based on CIEDE2000 (https://en.wikipedia.org/wiki/Color_difference#CIEDE2000)
|
|
function differenceCiede2000(rgb1, rgb2) {
|
|
var kL = 1,
|
|
kC = 1,
|
|
kH = 0.9;
|
|
var LabStd = rgb2lab(rgb1);
|
|
var LabSmp = rgb2lab(rgb2);
|
|
|
|
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.20 * 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)
|
|
);
|
|
}
|
|
function rgb2lab(r, g, b) {
|
|
// Convert to XYZ first via matrix transformation
|
|
let x = 0.412453 * r + 0.357580 * g + 0.180423 * b;
|
|
let y = 0.212671 * r + 0.715160 * g + 0.072169 * b;
|
|
let z = 0.019334 * r + 0.119193 * g + 0.950227 * b;
|
|
|
|
let xFunc = CIELABconvF(x / referenceWhite.x);
|
|
let yFunc = CIELABconvF(y / referenceWhite.y);
|
|
let zFunc = CIELABconvF(z / referenceWhite.z);
|
|
|
|
let myL = 116 * yFunc - 16;
|
|
let myA = 500 * (xFunc - yFunc);
|
|
let myB = 200 * (yFunc - zFunc);
|
|
|
|
return {
|
|
l: myL,
|
|
a: myA,
|
|
b: myB
|
|
};
|
|
|
|
}
|
|
|
|
function CIELABconvF(value) {
|
|
if (value > Math.pow(6 / 29, 3)) {
|
|
return Math.cbrt(value);
|
|
}
|
|
return 1 / 3 * Math.pow(6 / 29, 2) * value + 4 / 29;
|
|
}
|
|
|
|
function colorToRGB(color) {
|
|
if(window.colorCache && window.colorCache[color]){
|
|
return window.colorCache[color];
|
|
}
|
|
if (!window.cachedCtx) {
|
|
window.cachedCtx = document.createElement("canvas").getContext("2d");
|
|
window.colorCache = {};
|
|
}
|
|
let ctx = window.cachedCtx;
|
|
ctx.fillStyle = color;
|
|
return hexToRgb(ctx.fillStyle);
|
|
|
|
function hexToRgb(hex) {
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
return {
|
|
r: parseInt(result[1], 16),
|
|
g: parseInt(result[2], 16),
|
|
b: parseInt(result[3], 16)
|
|
};
|
|
}
|
|
} |