Public
Edited
Apr 16, 2024
Paused
4 forks
Importers
16 stars
Insert cell
Insert cell
Insert cell
mixbox = require("mixbox@2.0.0/mixbox.js")
Insert cell
spectraljs = require("spectral.js@2")
Insert cell
Insert cell
mixed = {
const rgb1 = "rgb(0, 33, 133)"; // blue
const rgb2 = "rgb(252, 211, 0)"; // yellow
const t = 0.5; // mixing ratio

return mixbox.lerp(rgb1, rgb2, t);
}
Insert cell
Insert cell
rgbMix = {
const rgb1 = "rgb(0, 33, 133)"; // blue
const rgb2 = "rgb(252, 211, 0)"; // yellow
const rgb3 = "rgb(255, 39, 2)"; // red
const z1 = mixbox.rgbToLatent(rgb1);
const z2 = mixbox.rgbToLatent(rgb2);
const z3 = mixbox.rgbToLatent(rgb3);

const zMix = new Array(mixbox.LATENT_SIZE);

for (var i = 0; i < zMix.length; i++) {
// mix:
zMix[i] =
0.3 * z1[i] + // 30% of rgb1
0.6 * z2[i] + // 60% of rgb2
0.1 * z3[i]; // 10% of rgb3
}

return mixbox.latentToRgb(zMix);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
palettePigments = {
const extraPigmentEntries = d3
.range(0, extraPigments)
.map((i) => [`Custom pigment #${i}`, "rgb(0, 0, 0)"]);

return [...Object.entries(DEFAULT_PIGMENTS), ...extraPigmentEntries].filter(
([name, color]) => !removals.includes(name)
);
}
Insert cell
Insert cell
modifedPalletePigments = settle(viewof palette)
Insert cell
pigments = Object.fromEntries(modifedPalletePigments)
Insert cell
mutable removals = []
Insert cell
Insert cell
culori = require("culori@3.3.0/bundled/culori.umd.js")
Insert cell
rgbGamutInOklab = {
const ranges = culori.modeOklab.ranges;
const lRange = d3.range(...ranges.l, 0.02);
const aRange = d3.range(...ranges.a, 0.01);
const bRange = d3.range(...ranges.b, 0.01);

const acc = [];

for (const a of aRange) {
for (const b of bRange) {
for (const l of lRange) {
const oklab = { mode: "oklab", l, a, b };

if (!culori.displayable(oklab)) {
continue;
}

if (Math.random() < 0.9) {
continue;
}

const hex = culori.formatHex(oklab);

acc.push({ hex, ...oklab });
}
}
}
return acc;
}
Insert cell
STEP_SIZE = ({
a: 1 / 150,
b: 1 / 150,
h: 1,
l: 1 / 30
})
Insert cell
rgbMaxLInOklab = {
const ranges = culori.modeOklab.ranges;
const lRange = d3.range(...ranges.l, STEP_SIZE.l);
const aRange = d3.range(...ranges.a, STEP_SIZE.a);
const bRange = d3.range(...ranges.b, STEP_SIZE.b);

const getMaxLightness = (a, b) => {
return _.last(
lRange.filter((l) => {
return culori.displayable({ mode: "oklab", l, a, b });
})
);
};

return aRange
.flatMap((a) =>
bRange.map((b) => {
const l = getMaxLightness(a, b);
const oklab = { mode: "oklab", l, a, b };
const hex = culori.formatHex(oklab);

return { hex, ...oklab };
})
)
.filter((d) => d.l);
}
Insert cell
rgbMaxLInOklabMark = Plot.rect(rgbMaxLInOklab, {
x1: "a",
x2: (d) => d.a + STEP_SIZE.a,
y1: "b",
y2: (d) => d.b + STEP_SIZE.b,
fill: "hex",
fillOpacity: settings.oklabTransparency
})
Insert cell
rgbMaxCInOklch = {
const ranges = culori.modeOklch.ranges;
const lRange = d3.range(...ranges.l, STEP_SIZE.l);
const hRange = d3.range(...ranges.h, STEP_SIZE.h);

return lRange.flatMap((l) =>
hRange.map((h) => {
const c = ranges.c[1];
const oklch = culori.clampChroma({ mode: "oklch", l, c, h }, "oklch");
const hex = culori.formatHex(oklch);
return { hex, ...oklch };
})
);
}
Insert cell
rgbMaxCInOklchMark = Plot.rect(rgbMaxCInOklch, {
x1: (d) => d.h,
x2: (d) => d.h + STEP_SIZE.h,
y1: (d) => d.l,
y2: (d) => d.l + STEP_SIZE.l,
fill: "hex",
fillOpacity: settings.oklabTransparency
})
Insert cell
centerPointMark = Plot.dot([[0, 0]], {
x: () => 0,
y: () => 0,
fill: () => "#000000",
r: 3,
title: () => `#000000
oklch(0, 0, 0°)`
})
Insert cell
Insert cell
Insert cell
Insert cell
mix = (from, to, fromLabel, toLabel) =>
mixingCoefficients.flatMap((t) => {
const [r, g, b] = mixbox.lerp(from, to, t);
const rgb = { mode: "rgb", r: r / 255, g: g / 255, b: b / 255 };

return [
{
algorithm: "Mixbox",
coefficient: t,
hex: culori.formatHex(rgb),
...culori.oklab(rgb),
fromLabel,
toLabel
},
{
algorithm: "Spectral.js",
coefficient: t,
hex: spectraljs.mix(from, to, t),
...culori.oklab(spectraljs.mix(from, to, t)),
fromLabel,
toLabel
}
];
})
Insert cell
mixingPath = mix(
firstPigment,
secondPigment,
pigments[firstMixingPigmentName] === culori.formatHex(firstPigment)
? firstMixingPigmentName
: "Custom pigment",
pigments[secondMixingPigmentName] === culori.formatHex(secondPigment)
? secondMixingPigmentName
: "Custom pigment"
)
Insert cell
explorePaths = {
const length = Object.values(pigments).length;
const acc = [];

for (let j = 0; j < length; ++j) {
acc.push(
...mix(
exploringPigment,
Object.values(pigments)[j],
pigments[exploringPigmentName] === culori.formatHex(exploringPigment)
? exploringPigmentName
: "Custom pigment",
Object.keys(pigments)[j]
)
);
}

return acc;
}
Insert cell
allPaths = {
const length = Object.values(pigments).length;
const acc = [];

for (let i = 0; i < length; ++i) {
for (let j = i; j < length; ++j) {
const mixes = mix(
Object.values(pigments)[i],
Object.values(pigments)[j],
Object.keys(pigments)[i],
Object.keys(pigments)[j]
);

acc.push(...mixes);
}
}

return acc;
}
Insert cell
Insert cell
tooltip = (d) => {
let { l, c, h } = culori.oklch(d.hex);

h = h || 0;

return `Algorithm: ${d.algorithm}
${d.hex}
oklch(${(l * 100).toFixed(0)}, ${c.toFixed(2)}, ${h.toFixed(0)}°)
From: ${d.fromLabel}
To: ${d.toLabel}
Mixing coefficient: ${d.coefficient.toFixed(2)}`;
}
Insert cell
Insert cell
facetDomain = compareSpectralJs ? ["Mixbox", "Spectral.js"] : ["Mixbox"]
Insert cell
renderPlot = ({ points }) =>
Plot.plot({
width: facetDomain.length * 600,
caption: md`${caption}`,
aspectRatio: 1,
fx: {
domain: facetDomain
},
marks: [
rgbMaxLInOklabMark,
// must be rendered after diagram to get tooltip to work
Plot.dot(points, {
x: "a",
y: "b",
sort: "l",
fill: "hex",
fx: "algorithm",
r: settings.pathRadius,
tip: true,
title: tooltip
}),
centerPointMark
]
})
Insert cell
Insert cell
render3dPlot = ({ points }) => {
const data = [
...facetDomain.map((algorithmFacet) => {
// deliberately shadow
const facetPoints = points.filter((d) => d.algorithm === algorithmFacet);

return {
x: facetPoints.map((point) => point.a),
y: facetPoints.map((point) => point.b),
z: facetPoints.map((point) => point.l),
hovertext: facetPoints.map((point) => tooltip(point)),
name: algorithmFacet,
mode: "markers",
type: "scatter3d",
marker: {
size: settings.pathRadius,
color: facetPoints.map((point) => point.hex),
opacity: 1
}
};
}),
{
x: rgbGamutInOklab.map((point) => point.a),
y: rgbGamutInOklab.map((point) => point.b),
z: rgbGamutInOklab.map((point) => point.l),
mode: "markers",
name: "RGB gamut",
type: "scatter3d",
marker: {
size: 5,
color: rgbGamutInOklab.map((point) => point.hex),
opacity: settings.oklabTransparency
}
}
];
const layout = {
margin: {
l: 0,
r: 0,
b: 0,
t: 0
},
height: 600,
width: 800,
scene: {
camera: {
eye: {
x: 0,
y: -0.1,
z: 2
}
},
aspectmode: "manual",
aspectratio: {
x: 1,
y: 1,
z: 1
},
xaxis: {
title: {
text: "a"
}
},
yaxis: {
title: {
text: "b"
}
},
zaxis: {
title: {
text: "L"
}
}
}
};
const div = DOM.element("div");
return Plotly.newPlot(div, data, layout);
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more