Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 66 additions & 268 deletions color.js
Original file line number Diff line number Diff line change
@@ -1,289 +1,87 @@
'use strict';

class Color {
constructor(r, g, b) {
this.set(r, g, b);
}

toString() {
return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`;
}

set(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}

hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
const sin = Math.sin(angle);
const cos = Math.cos(angle);

this.multiply([
0.213 + cos * 0.787 - sin * 0.213,
0.715 - cos * 0.715 - sin * 0.715,
0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143,
0.715 + cos * 0.285 + sin * 0.140,
0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787,
0.715 - cos * 0.715 + sin * 0.715,
0.072 + cos * 0.928 + sin * 0.072,
]);
}

grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 + 0.2848 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 + 0.9278 * (1 - value),
]);
}

sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value),
0.769 - 0.769 * (1 - value),
0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value),
0.686 + 0.314 * (1 - value),
0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value),
0.534 - 0.534 * (1 - value),
0.131 + 0.869 * (1 - value),
]);
}

saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value,
0.715 - 0.715 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 + 0.285 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 - 0.715 * value,
0.072 + 0.928 * value,
]);
}

multiply(matrix) {
const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR;
this.g = newG;
this.b = newB;
}

brightness(value = 1) {
this.linear(value);
}
contrast(value = 1) {
this.linear(value, -(0.5 * value) + 0.5);
}

linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}

invert(value = 1) {
this.r = this.clamp((value + this.r / 255 * (1 - 2 * value)) * 255);
this.g = this.clamp((value + this.g / 255 * (1 - 2 * value)) * 255);
this.b = this.clamp((value + this.b / 255 * (1 - 2 * value)) * 255);
}

hsl() {
// Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
const r = this.r / 255;
const g = this.g / 255;
const b = this.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;

if (max === min) {
h = s = 0;
function generateRandomSaturatedColor() {
Copy link

@wolfschaf wolfschaf Dec 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like mentioned, just return hue if we lerp over hue shift

const hue = Math.random(); // Random hue value between 0 and 1
const h = hue * 360; // Convert to degrees
const s = 1; // Full saturation
const v = 1; // Full brightness

const c = v * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - c;

let r, g, b;
if (h < 60) {
[r, g, b] = [c, x, 0];
} else if (h < 120) {
[r, g, b] = [x, c, 0];
} else if (h < 180) {
[r, g, b] = [0, c, x];
} else if (h < 240) {
[r, g, b] = [0, x, c];
} else if (h < 300) {
[r, g, b] = [x, 0, c];
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;

case g:
h = (b - r) / d + 2;
break;

case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
[r, g, b] = [c, 0, x];
}

return {
h: h * 100,
s: s * 100,
l: l * 100,
};
}
r = Math.round((r + m) * 255);
g = Math.round((g + m) * 255);
b = Math.round((b + m) * 255);

clamp(value) {
if (value > 255) {
value = 255;
} else if (value < 0) {
value = 0;
}
return value;
}
return [r, g, b];
}

class Solver {
constructor(target, baseColor) {
this.target = target;
this.targetHSL = target.hsl();
this.reusedColor = new Color(0, 0, 0);
}

solve() {
const result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values),
};
}

solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];

let best = { loss: Infinity };
for (let i = 0; best.loss > 25 && i < 3; i++) {
const initial = [50, 20, 3750, 50, 100, 100];
const result = this.spsa(A, a, c, initial, 1000);
if (result.loss < best.loss) {
best = result;
}
}
return best;
}
function calculateHueShift(r, g, b) {
// Helper function to convert RGB to HSL
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;

solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l;

spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;
l = (max + min) / 2;

let best = null;
let bestLoss = Infinity;
const deltas = new Array(6);
const highArgs = new Array(6);
const lowArgs = new Array(6);
if (max === min) {
h = s = 0; // Achromatic (grey)
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

for (let k = 0; k < iters; k++) {
const ck = c / Math.pow(k + 1, gamma);
for (let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}

const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for (let i = 0; i < 6; i++) {
const g = lossDiff / (2 * ck) * deltas[i];
const ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}
h /= 6;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why divide by 6?
Couldn't we just multiply by 60 and then remove the *360 below?

But I would just restructure this whole thing, since we only need to calc the hue value.

function calculateHueShift(r, g, b) {
    function rgbToHue(r, g, b) {
        r /= 255;
        g /= 255;
        b /= 255;

        const cMax  = Math.max(r,g,b);
        const cMin  = Math.min(r,g,b);
        const delta = cMax - cMin;
    
        if (!delta) return 0;
        
        let hue;
        switch (cMax) {
            case r: hue = (g - b) / delta + (g < b ? 6 : 0); break;
            case g: hue = (b - r) / delta + 2; break;
            case b: hue = (r - g) / delta + 4; break;
        }
        return hue * 60;
    }

    // Convert yellow RGB(255, 255, 0) and target color to hue
    const yellowHue = rgbToHue(255, 255, 0);
    const targetHue = rgbToHue(r, g, b);

    // Calculate hue shift
    let hueShift = targetHue - yellowHue;
    if (hueShift < 0) hueShift += 360;

    return hueShift;
}

}

const loss = this.loss(values);
if (loss < bestLoss) {
best = values.slice(0);
bestLoss = loss;
}
return [h * 360, s, l];
}
return { values: best, loss: bestLoss };

function fix(value, idx) {
let max = 100;
if (idx === 2 /* saturate */) {
max = 7500;
} else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
max = 200;
}

if (idx === 3 /* hue-rotate */) {
if (value > max) {
value %= max;
} else if (value < 0) {
value = max + value % max;
}
} else if (value < 0) {
value = 0;
} else if (value > max) {
value = max;
}
return value;
}
}
// Convert yellow (RGB 255, 255, 0) to HSL
const yellowHsl = rgbToHsl(255, 255, 0);
const yellowHue = yellowHsl[0];

loss(filters) {
// Argument is array of percentages.
const color = this.reusedColor;
color.set(0, 0, 0);
// Convert target color to HSL
const targetHsl = rgbToHsl(r, g, b);
const targetHue = targetHsl[0];

color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);
// Calculate hue shift
let hueShift = targetHue - yellowHue;
if (hueShift < 0) hueShift += 360;

const colorHSL = color.hsl();
return (
Math.abs(color.r - this.target.r) +
Math.abs(color.g - this.target.g) +
Math.abs(color.b - this.target.b) +
Math.abs(colorHSL.h - this.targetHSL.h) +
Math.abs(colorHSL.s - this.targetHSL.s) +
Math.abs(colorHSL.l - this.targetHSL.l)
);
}
return hueShift;
}

css(filters) {
function fmt(idx, multiplier = 1) {
return Math.round(filters[idx] * multiplier);
}
return `filter: sepia(${fmt(1)}%) saturate(${fmt(2) / 10}%) hue-rotate(${fmt(3, 3.6)}deg)`;
}
function hexToRgb(hex){
return [
parseInt(hex.substring(1, 3), 16),
parseInt(hex.substring(3, 5), 16),
parseInt(hex.substring(5, 7), 16),
];
}

function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
]
: null;
}
Loading