Skip to content

Commit

Permalink
fix: handle okhsl/okhsv for lightness and saturation epsilon (ie. ach…
Browse files Browse the repository at this point in the history
…romatic and close to white edge cases)
  • Loading branch information
dmnsgn committed Sep 22, 2024
1 parent 386302a commit 653978f
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 59 deletions.
40 changes: 28 additions & 12 deletions okhsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,22 +190,38 @@ export function toOkhsl([r, g, b, a], out = []) {

const [C0, Cmid, Cmax] = getCs(L, a_, b_);

if (C < Cmid) {
const k0 = 0;
const k1 = 0.8 * C0;
const k2 = 1 - k1 / Cmid;
out[2] = toe(L);

if (out[2] !== 0 && out[2] !== 1 && C !== 0) {
if (C < Cmid) {
const k0 = 0;
const k1 = 0.8 * C0;
const k2 = 1 - k1 / Cmid;

const t = (C - k0) / (k1 + k2 * (C - k0));
out[1] = t * 0.8;
} else {
const k0 = Cmid;
const k1 = (0.2 * Cmid * Cmid * 1.25 * 1.25) / C0;
const k2 = 1 - k1 / (Cmax - Cmid);

const t = (C - k0) / (k1 + k2 * (C - k0));
out[1] = t * 0.8;
const t = (C - k0) / (k1 + k2 * (C - k0));
out[1] = 0.8 + 0.2 * t;
}
} else {
const k0 = Cmid;
const k1 = (0.2 * Cmid * Cmid * 1.25 * 1.25) / C0;
const k2 = 1 - k1 / (Cmax - Cmid);
out[1] = 0;
}

const t = (C - k0) / (k1 + k2 * (C - k0));
out[1] = 0.8 + 0.2 * t;
// Epsilon for lightness should approach close to 32 bit lightness
// Epsilon for saturation just needs to be sufficiently close when denoting achromatic
let εL = 1e-7;
let εS = 1e-4;

const achromatic = Math.abs(out[1]) < εS;
if (achromatic || Math.abs(1 - out[2]) < εL) {
out[0] = 0; // null
if (!achromatic) out[1] = 0;
}

out[2] = toe(L);
return setAlpha(out, a);
}
90 changes: 56 additions & 34 deletions okhsv.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,43 @@ const S0 = 0.5;
* @returns {import("./color.js").color}
*/
export function fromOkhsv(color, h, s, v, α) {
const a_ = Math.cos(TAU * h);
const b_ = Math.sin(TAU * h);
let L = toeInv(v);
let a = 0; // null
let b = 0; // null

const [S, T] = getStMax(a_, b_);
const k = 1 - S0 / S;
// Avoid processing gray or colors with undefined hues
if (L !== 0 && s !== 0) {
const a_ = Math.cos(TAU * h);
const b_ = Math.sin(TAU * h);

const Lv = 1 - (s * S0) / (S0 + T - T * k * s);
const Cv = (s * T * S0) / (S0 + T - T * k * s);
const [S, T] = getStMax(a_, b_);
const k = 1 - S0 / S;

let L = v * Lv;
let C = v * Cv;
const Lv = 1 - (s * S0) / (S0 + T - T * k * s);
const Cv = (s * T * S0) / (S0 + T - T * k * s);

const Lvt = toeInv(Lv);
const Cvt = (Cv * Lvt) / Lv;
L = v * Lv;
let C = v * Cv;

const Lnew = toeInv(L);
C = (C * Lnew) / L;
L = Lnew;
const Lvt = toeInv(Lv);
const Cvt = (Cv * Lvt) / Lv;

oklabToLinearSrgb(TMP, Lvt, a_ * Cvt, b_ * Cvt);
const Lnew = toeInv(L);
C = (C * Lnew) / L;
L = Lnew;

const scaleL = Math.cbrt(1 / Math.max(TMP[0], TMP[1], TMP[2], 0));
oklabToLinearSrgb(TMP, Lvt, a_ * Cvt, b_ * Cvt);

L = L * scaleL;
C = C * scaleL;
const scaleL = Math.cbrt(1 / Math.max(TMP[0], TMP[1], TMP[2], 0));

fromOklab(color, L, C * a_, C * b_);
L = L * scaleL;
C = C * scaleL;

a = C * a_;
b = C * b_;
}

fromOklab(color, L, a, b);

return setAlpha(color, α);
}
Expand All @@ -72,32 +82,44 @@ export function toOkhsv([r, g, b, a], out = []) {
linearSrgbToOklab(TMP, srgbToLinear(r), srgbToLinear(g), srgbToLinear(b));

let C = Math.sqrt(TMP[1] * TMP[1] + TMP[2] * TMP[2]);
const a_ = TMP[1] / C;
const b_ = TMP[2] / C;

let L = TMP[0];
out[0] = 0.5 + (0.5 * Math.atan2(-TMP[2], -TMP[1])) / Math.PI;

const [S, T] = getStMax(a_, b_);
if (L !== 0 && L !== 1 && C !== 0) {
const a_ = TMP[1] / C;
const b_ = TMP[2] / C;
const [S, T] = getStMax(a_, b_);

const t = T / (C + L * T);
const Lv = t * L;
const Cv = t * C;

const Lvt = toeInv(Lv);
const Cvt = (Cv * Lvt) / Lv;

const t = T / (C + L * T);
const Lv = t * L;
const Cv = t * C;
oklabToLinearSrgb(TMP, Lvt, a_ * Cvt, b_ * Cvt);

const Lvt = toeInv(Lv);
const Cvt = (Cv * Lvt) / Lv;
const scaleL = Math.cbrt(1 / Math.max(TMP[0], TMP[1], TMP[2], 0));

oklabToLinearSrgb(TMP, Lvt, a_ * Cvt, b_ * Cvt);
L = L / scaleL;
C = C / scaleL;

const scaleL = Math.cbrt(1 / Math.max(TMP[0], TMP[1], TMP[2], 0));
const toeL = toe(L);
C = (C * toeL) / L;

L = L / scaleL;
C = C / scaleL;
out[1] = ((S0 + T) * Cv) / (T * S0 + T * (1 - S0 / S) * Cv);
out[2] = toeL / Lv;
} else {
out[1] = 0;
out[2] = toe(L);
}

const toeL = toe(L);
C = (C * toeL) / L;
// Epsilon for saturation just needs to be sufficiently close when denoting achromatic
let ε = 1e-4;
if (Math.abs(out[1]) < ε || out[2] === 0) {
out[0] = 0; // null
}

out[1] = ((S0 + T) * Cv) / (T * S0 + T * (1 - S0 / S) * Cv);
out[2] = toeL / Lv;
return setAlpha(out, a);
}
14 changes: 6 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"snowdev": ">=2.2.x"
},
"devDependencies": {
"colorjs.io": "^0.5.2"
"colorjs.io": "github:color-js/color.js"
},
"snowdev": {
"dependencies": []
Expand Down
13 changes: 9 additions & 4 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, it } from "node:test";
import { deepEqual } from "node:assert";
import * as color from "../index.js";
import Color from "colorjs.io";
// import Color from "colorjs.io";

const { default: Color } = await import(
"../node_modules/colorjs.io/src/index.js"
);

// TODO: HSL switch case

Expand Down Expand Up @@ -288,6 +292,7 @@ describe("HEX", () => {
});
});

// TODO: add to css.js
const NORMALIZE_VALUES = {
hue: [360, 100, 100],
hueReversed: [100, 100, 360],
Expand All @@ -310,7 +315,7 @@ const epsilons = {
LCHuv: 0.0088564516,
HSLuv: 0.0088564516,
HPLuv: 0.0088564516,
Okhsv: 10e-6,
Okhsv: 0.003,
};

// References:
Expand All @@ -324,8 +329,6 @@ const getReference = (hex) => {
const color = new Color(hex);

// luv: "luv",
// Okhsv: { id: "okhsv", normalize: "hueOnly" }, // TODO: wait for colorjs release
// Okhsl: { id: "okhsl", normalize: "hueOnly" }, // TODO: wait for colorjs release
const spaceMapping = {
Linear: { id: "srgb-linear" },
P3: { id: "p3" },
Expand All @@ -339,6 +342,8 @@ const getReference = (hex) => {
LCH: { id: "lch", normalize: "lch" },
Oklab: { id: "oklab" }, // [1, [-0.4, 0.4], [-0.4, 0.4]],
Oklch: { id: "oklch", normalize: "hueOnlyReversed" },
Okhsv: { id: "okhsv", normalize: "hueOnly" },
Okhsl: { id: "okhsl", normalize: "hueOnly" },
LCHuv: { id: "lchuv", normalize: "hueReversed" }, // [100, 220, 360]
HSLuv: { id: "hsluv", normalize: "hue" },
HPLuv: { id: "hpluv", normalize: "hue" },
Expand Down

0 comments on commit 653978f

Please sign in to comment.