pixel-editor/js/color_utils3.js

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)
};
}
}