Public
Edited
Dec 12, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// This gist contains JavaScript functions and tests for:
// - conversion from gamma-corrected (or gamma-compressed) sRGB to linear RGB, to Oklab
// - interpolation through Oklab
// - conversion back to linear RGB, then sRGB
// To use these tests, install nodejs, save this file locally, and run with:
// node OklabExperiments.js
// No other dependencies are required to use this.
// Thanks to some helpful folks in the generative art community for helping me better understand what's happening with this.

// My toddler smacked the keyboard with a piece of cardboard and made me accidentally type:
// zaser~
Insert cell
// Some color / math code tweaked from functions found in this repo: https://github.com/mattdesl/tiny-artblocks
Insert cell
function clamp(value, min, max) {
return Math.max(Math.min(value, max), min);
}
Insert cell
// returns an object literal which is r, g and b integers from an RGB hex string:
function hexToRGB(str) {
var hex = str.replace("#", "");
// NOTE: This can be removed for brevity if you stick with 6-character codes:
// if (hex.length === 3) {hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];}
var num = parseInt(hex, 16);
return {r: num >> 16, g: (num >> 8) & 255, b: num & 255};
};
Insert cell
// converts RGB integer values to hex:
function rgbToHex({r, g, b}) {
// NEXT LINE OPTIONAL, depending on your purposes:
// var r = clamp(r, 0, 255); var g = clamp(g, 0, 255); var b = clamp(b, 0, 255);
return "#" + (b | (g << 8) | (r << 16) | (1 << 24)).toString(16).slice(1);
}

Insert cell
// correlary of first psuedocode block here (f_inv) : https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F ; "applying the inverse of the sRGB nonlinear transform function.." -- keeping the abbreviated syntax of arrow functions and ? : if/then, despite that they confuse and stretch my noob brain:
gammaToLinear = (c) =>
c >= 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92;
Insert cell
// correlary of the first " : "..then switching back" :
linearToGamma = (c) =>
c >= 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c;
Insert cell
// Lab coordinates (parameters):
// L = Lightness (0 (black) to ?? (diffuse white)
// a = green (at negative -- is there a lower bound?) to red (positive)
// b = blue (at negative) to yellow (at positive).
// You can manually construct an object literal to pass to this function this way:
// labVals = {L: 0.75, a: 0.7, b: 0.2};
// sRGBresultObjectLiteral = oklabToSRGB(labVals);
// You can also construct that as just {0.75, 0.7, 0.2}, and still pass it and it will work, apparently
// "..Oklab is represented as an object {L, a, b} where L is between 0 and 1 for normal SRGB colors. a and b have a less clearly defined range, but will normally be between -0.5 and +0.5. Neutral gray colors will have a and b at zero (or very close)." re: https://www.npmjs.com/package/oklab
// numbers updated from C++ example at https://bottosson.github.io/posts/oklab/ as updated 2021-01-25
// helpful references:
// https://observablehq.com/@sebastien/srgb-rgb-gamma
// Other references: https://matt77hias.github.io/blog/2018/07/01/linear-gamma-and-sRGB-color-spaces.html
// Takeaway: before manipulating color for sRGB (gamma-corrected or gamma compressed), convert it to linear RGB or a linear color space. Then do the manipulation, then convert it back to sRGB.
function rgbToOklab({r, g, b}) {
// This is my undersanding: JavaScript canvas and many other virtual and literal devices use gamma-corrected (non-linear lightness) RGB, or sRGB. To convert sRGB values for manipulation in the Oklab color space, you must first convert them to linear RGB. Where Oklab interfaces with RGB it expects and returns linear RGB values. This next step converts (via a function) sRGB to linear RGB for Oklab to use:
r = gammaToLinear(r / 255); g = gammaToLinear(g / 255); b = gammaToLinear(b / 255);
// This is the Oklab math:
var l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
var m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
var s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
// Math.crb (cube root) here is the equivalent of the C++ cbrtf function here: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
l = Math.cbrt(l); m = Math.cbrt(m); s = Math.cbrt(s);
return {
L: l * +0.2104542553 + m * +0.7936177850 + s * -0.0040720468,
a: l * +1.9779984951 + m * -2.4285922050 + s * +0.4505937099,
b: l * +0.0259040371 + m * +0.7827717662 + s * -0.8086757660
}
}
Insert cell
function oklabToSRGB({L, a, b}) {
var l = L + a * +0.3963377774 + b * +0.2158037573;
var m = L + a * -0.1055613458 + b * -0.0638541728;
var s = L + a * -0.0894841775 + b * -1.2914855480;
// The ** operator here cubes; same as l_*l_*l_ in the C++ example:
l = l ** 3; m = m ** 3; s = s ** 3;
var r = l * +4.0767416621 + m * -3.3077115913 + s * +0.2309699292;
var g = l * -1.2684380046 + m * +2.6097574011 + s * -0.3413193965;
var b = l * -0.0041960863 + m * -0.7034186147 + s * +1.7076147010;
// Convert linear RGB values returned from oklab math to sRGB for our use before returning them:
r = 255 * linearToGamma(r); g = 255 * linearToGamma(g); b = 255 * linearToGamma(b);
// OPTION: clamp r g and b values to the range 0-255; but if you use the values immediately to draw, JavaScript clamps them on use:
r = clamp(r, 0, 255); g = clamp(g, 0, 255); b = clamp(b, 0, 255);
// OPTION: round the values. May not be necessary if you use them immediately for rendering in JavaScript, as JavaScript (also) discards decimals on render:
r = Math.round(r); g = Math.round(g); b = Math.round(b);
return {r, g, b};
}
Insert cell
// A DEV TEST that determined min and max range for oklab L, a, and b as found via conversion from all possible RGB256 values (I want those ranges for the curiosity of generating random colors in Oklab space but converting the results to rgb):
// var counter = 0;
// var aMin = 0, aMax = 0, bMin = 0, bMax = 0, lMin = 50; lMax = -50;
// for (var r = 0; r < 256; r++) {
// for (var g = 0; g < 256; g++) {
// for (var b = 0; b < 256; b++) {
// counter += 1;
// var sRGBcolor = {r, g, b};
// var labColor = rgbToOklab(sRGBcolor);
// if (labColor.L < lMin) {lMin = labColor.L;}
// if (labColor.L > lMax) {lMax = labColor.L;}
// if (labColor.a < aMin) {aMin = labColor.a;}
// if (labColor.a > aMax) {aMax = labColor.a;}
// if (labColor.b < bMin) {bMin = labColor.b;}
// if (labColor.b > bMax) {bMax = labColor.b;}
// }
// }
// }
// console.log("counter:", counter);
// console.log("lMin:", lMin, "lMax:", lMax, "aMin:", aMin, "aMax:", aMax, "bMin:", bMin, "bMax:", bMax);
// results without and with gammaToLinear -> linearToGamma and other "OPTIONAL" calculations:
// without extra calculations with extra calculations
// lMin: 0 0
// lMax: 254.99999832226274 0.9999999934735462
// aMin: -118.66066044645729 -0.23388757418790818
// aMax: 118.66066050838631 0.27621639742350523
// bMin: -108.13237868927439 -0.3115281476783751
// bMax: 108.1323881808137 0.19856975465179516

// A DEV TEST that prints so many round trips from sRGB -> Oklab -> sRGB to verify that randomly generated colors remain the same after round trip. They do.
// function getRNDrgbColor() {
// const rndRGB = n => Math.round(Math.random() * 256);
// return {r: rndRGB(), g: rndRGB(), b: rndRGB()}
// }
// for (let i = 0; i < 42; i++) {
// rndRGBvals = getRNDrgbColor();
// console.log(rndRGBvals, "->");
// labVals = rgbToOklab(rndRGBvals); // can pass rndRGBvals bcse it is like {r: val, g: val, b: val}
// console.log(oklabToSRGB(labVals), "<-\n");
// }
Insert cell
// POSSIBILITY FOR GRAYSCALE CALC; you can use the value returned from this with a = 0, b = 0 (for oklab) to convert RGB to gray:
// function linearrgbToOklabLightness({r, g, b}) {
// const l = Math.cbrt(r * +0.4122214708 + g * +0.5363325363 + b * +0.0514459929);
// const m = Math.cbrt(r * +0.2119034982 + g * +0.6806995451 + b * +0.1073969566);
// const s = Math.cbrt(r * +0.0883024619 + g * +0.2817188376 + b * +0.6299787005);
//
// return l * +0.2104542553 + m * +0.7936177850 + s * -0.0040720468;
// };
Insert cell
// returns an array which is a range of values at N intervals over range min-max (interpolations), inclusive of max:
function getLerpRange(min, max, N) {
const dividend = (max - min) / (N - 1);
var arr = [];
let currentVal = min;
for (var i = 0; i < N; i++) {
arr.push(currentVal);
currentVal += dividend;
}
return arr;
}

Insert cell
// returns an array of sRGB hex color codes, from interpolation in numberOfColors steps through Oklab space (and converted back to sRGB), including start and end value:
function getRGBlerpRangeInOklabSpace(startRGBhexColor, endRGBhexColor, numberOfColors) {
var lasInterpolaciones = [];
var startLabVals = rgbToOklab(hexToRGB(startRGBhexColor));
var endLabVals = rgbToOklab(hexToRGB(endRGBhexColor));
var lValsArr = getLerpRange(startLabVals.L, endLabVals.L, numberOfColors);
var aValsArr = getLerpRange(startLabVals.a, endLabVals.a, numberOfColors);
var bValsArr = getLerpRange(startLabVals.b, endLabVals.b, numberOfColors);
for (var i = 0; i < numberOfColors; i++) {
// I could nest function calls here for brevity . . .
var tmpOklabVals = {L: lValsArr[i], a: aValsArr[i], b: bValsArr[i]};
var tmpsRGBvals = oklabToSRGB(tmpOklabVals);
// var hexVals = rgbToHex(tmpsRGBvals);
lasInterpolaciones.push(tmpsRGBvals);
}
return lasInterpolaciones;
}

Insert cell
testInterpolaciones = {
return getRGBlerpRangeInOklabSpace('#0000ff', '#ffff00', 7);
// for (var element of testInterpolaciones) {
// console.log(element);
// };
}
// compare the middle value of that to the blue -> yellow gradient through Oklab space here: https://raphlinus.github.io/color/2021/01/18/oklab-critique.html -- they match. This (and other tests) verifies this is doing things correctly from sRGB -> linear RGB -> Oklab -> transformation -> linear RGB -> sRGB.
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more