Public
Edited
Feb 19
1 fork
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
function colorSpace3D(config, ...layers) {
const { element, illo, addPoint } = colorSpace3DImpl(config, ...layers);

const result = htl.html`<div><div style="float: right;">drag to rotate</div> ${element}</div>`;
result.addPoint = addPoint;
result.illo = illo;
return result;
}
Insert cell
function colorSpace3DImpl(config, ...legacyLayers) {
if (typeof config === "function") {
config = {
mapper: config
};
}

let {
isAnimated = false,
dragRotateGroup = "default",
mapper,
width = 500,
height = 500,
background = "transparent",
layers,
tag = "canvas",
rotation = {
x: (-Zdog.TAU * xRotation) / 16,
y: (Zdog.TAU * yRotation) / 16,
z: 0
}
} = config;

const element =
tag === "canvas"
? htl.html`<canvas width=${width} height=${height}>`
: tag === "svg"
? htl.html`<svg width=${width} height=${height}>`
: tag + "?";
element.style.background = background;
let isDragging = false;

const group = getDragRotateGroup(dragRotateGroup);

if (config.lookAtPlane) {
rotation = lookAtPlane(config.lookAtPlane.map(mapper));
}

const illo = new Zdog.Illustration({
element: element.tagName === "CANVAS" ? p3CanvasProxy(element) : element,
dragRotate: true,
rotate: rotation,
onDragStart() {
isDragging = true;
},
onDragMove() {
syncDragRotateGroup(illo, group);
dragRotateGroupsLastRotation.set(dragRotateGroup, illo.dragRotate.rotate);
},
onDragEnd() {
isDragging = false;
}
});

if (config.lookAtPlane) {
syncDragRotateGroup(illo, group);
dragRotateGroupsLastRotation.set(dragRotateGroup, rotation);
}

syncIlloFromDragRotateGroup(illo, dragRotateGroup);

group.add(illo);

const addPointImpl = (color, stroke) => {
const { x, y, z } = mapper(color);
return new Zdog.Shape({
addTo: illo,
stroke: stroke,
translate: { x, y, z },
color: toColorString(color)
});
};

for (const layer of layers) {
for (const color of layer.colors) {
addPointImpl(color, layer.stroke);
}
}

illo.updateRenderGraph();

function animate() {
if (isDragging || isAnimated) {
illo.updateRenderGraph();
}
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

const addPoint = (point, stroke) => {
const result = addPointImpl(point, stroke);
illo.updateRenderGraph();
return {
update(newPoint) {
const { x, y, z } = mapper(newPoint);
result.translate.x = x;
result.translate.y = y;
result.translate.z = z;
result.color = toColorString(newPoint);
illo.updateRenderGraph();
}
};
};

return { element, illo, addPoint };
}
Insert cell
p3CanvasProxy = (element) =>
new Proxy(element, {
get(target, key) {
if (key === "getContext") {
return () => target.getContext("2d", { colorSpace: "display-p3" });
}
const original = target[key];
if (typeof original === "function") {
return original.bind(target);
}
return original;
},
set(target, key, value) {
target[key] = value;
return true;
}
})
Insert cell
Insert cell
toColorString = (color) =>
culori.formatCss(
culori.displayable(color) ? culori.rgb(color) : culori.p3(color)
)
Insert cell
Insert cell
dragRotateGroups = new Map()
Insert cell
dragRotateGroupsLastRotation = new Map()
Insert cell
function getDragRotateGroup(dragRotateGroup) {
let group;
if (dragRotateGroups.has(dragRotateGroup)) {
group = dragRotateGroups.get(dragRotateGroup);
} else {
group = new Set([]);
dragRotateGroups.set(dragRotateGroup, group);
}
return group;
}
Insert cell
function syncDragRotateGroup(illo, group) {
for (const illo2 of group) {
if (illo === illo2) {
continue;
}
illo2.dragRotate.rotate.x = illo.dragRotate.rotate.x;
illo2.dragRotate.rotate.y = illo.dragRotate.rotate.y;
illo2.dragRotate.rotate.z = illo.dragRotate.rotate.z;
illo2.updateRenderGraph();
}
}
Insert cell
function syncIlloFromDragRotateGroup(illo, dragRotateGroup) {
if (dragRotateGroupsLastRotation.has(dragRotateGroup)) {
const { x, y, z } = dragRotateGroupsLastRotation.get(dragRotateGroup);
illo.rotate.x = x;
illo.rotate.y = y;
illo.rotate.z = z;
illo.updateRenderGraph();
}
}
Insert cell
Insert cell
Insert cell
Insert cell
getNormalXYZ = (points) => {
const cX = d3.mean(points, ({ x }) => x);
const cY = d3.mean(points, ({ y }) => y);
const cZ = d3.mean(points, ({ z }) => z);
const relativePoints = points.map(({ x, y, z }) => [x - cX, y - cY, z - cZ]);
return svd(relativePoints).V.map((a) => a[2]);
}
Insert cell
lookAtPlane = (points) => {
if (!(points?.length > 2)) {
return new THREE.Euler();
}

const [x, y, z] = getNormalXYZ(points);

return new THREE.Euler().setFromRotationMatrix(
new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(-y, x, 0),
Math.acos(z)
)
);
}
Insert cell
Insert cell
Insert cell
colorToRotatedSRgbPoint = (color) => {
const { r, g, b } = culori.rgb(color);
return new Vector3(r, g, b)
.addScalar(-0.5)
.multiplyScalar(-275)
.applyAxisAngle(axisX, -Math.PI / 4)
.applyAxisAngle(axisZ, Math.atan(1 / Math.sqrt(2)))
.applyAxisAngle(axisY, -Math.PI / 2);
}
Insert cell
colorSpace3D({
mapper: colorToRotatedSRgbPoint,
layers: [{ stroke: 20, colors: exampleSRgbGrid }]
})
Insert cell
Insert cell
colorToLabPoint = (color) => {
const { l, a, b } = culori.lab(color);
return new Vector3(-b, 2.0612 * (50 - l), -a).multiplyScalar(1.75);
}
Insert cell
colorSpace3D({
mapper: colorToLabPoint,
layers: [{ stroke: 5, colors: exampleSRgbGrid }]
})
Insert cell
Insert cell
colorToHsvPoint = (cone) => (color) => {
let { h, s, v } = culori.hsv(color);
if (isNaN(h)) h = 0;

const { r, g, b } = culori.rgb(color);

const sCone = Math.max(r, g, b) - Math.min(r, g, b);
const hsv = {
h,
s: cone ? sCone : s,
v
};

hsv.h += 135;

const rad = (Math.PI * hsv.h) / 180;
return new Vector3(hsv.s * 150, 150 - 300 * hsv.v, 0).applyAxisAngle(
axisY,
rad - Math.PI / 4
);
}
Insert cell
colorToHsvConePoint = colorToHsvPoint(true)
Insert cell
colorSpace3D({
mapper: colorToHsvConePoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleHsvGrid }]
})
Insert cell
colorToHsvCylinderPoint = colorToHsvPoint(false)
Insert cell
colorSpace3D({
mapper: colorToHsvCylinderPoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleHsvGrid }]
})
Insert cell
Insert cell
colorToHslPoint = (biCone) => (color) => {
let { h, s, l } = culori.hsl(color);
if (isNaN(s)) s = 0;
if (isNaN(h)) h = 0;

h += 135;

const rad = (Math.PI * h) / 180;
const coneCoeff = biCone ? 2 * (0.5 - Math.abs(l - 0.5)) : 1;
return new Vector3(s * 150 * coneCoeff, 150 - 300 * l, 0).applyAxisAngle(
axisY,
rad - Math.PI / 4
);
}
Insert cell
colorToHslBiConePoint = colorToHslPoint(true)
Insert cell
colorSpace3D({
mapper: colorToHslBiConePoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleHslGrid }]
})
Insert cell
colorToHslCylinderPoint = colorToHslPoint(false)
Insert cell
colorSpace3D({
mapper: colorToHslCylinderPoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleHslGrid }]
})
Insert cell
Insert cell
Insert cell
colorToRotatedP3Point = (color) => {
const { r, g, b } = culori.p3(color);
return new Vector3(r, g, b)
.addScalar(-0.5)
.multiplyScalar(-275)
.applyAxisAngle(axisX, -Math.PI / 4)
.applyAxisAngle(axisZ, Math.atan(1 / Math.sqrt(2)))
.applyAxisAngle(axisY, -Math.PI / 2);
}
Insert cell
colorSpace3D({
mapper: colorToRotatedP3Point,
layers: [{ stroke: 5, colors: exampleP3Grid }]
})
Insert cell
colorToRotatedHdrP3Point = (color) => {
const { r, g, b } = culori.p3(color);
return new Vector3(r, g, b)
.divideScalar(1.7)
.addScalar(-0.5)
.multiplyScalar(-275)
.applyAxisAngle(axisX, -Math.PI / 4)
.applyAxisAngle(axisZ, Math.atan(1 / Math.sqrt(2)))
.applyAxisAngle(axisY, -Math.PI / 2);
}
Insert cell
colorSpace3D({
mapper: colorToRotatedHdrP3Point,
layers: [{ stroke: 5, colors: exampleP3Grid }]
})
Insert cell
Insert cell
colorToItpPoint = (color) => {
const { i, t, p } = culori.itp(color);
return new Vector3(-t, i - 0.29034444052080544, p).multiplyScalar(-540);
}
Insert cell
colorSpace3D({
mapper: colorToItpPoint,
layers: [{ stroke: 5, colors: exampleP3Grid }]
})
Insert cell
colorToCompressedItpPoint = (color) => {
const itp = culori.itp(color);
const { i, t, p } = itp;
return new Vector3(-t / 2, i - 0.29034444052080544, p)
.multiplyScalar(-540)
.multiplyScalar(1.25);
}
Insert cell
colorSpace3D({
mapper: colorToCompressedItpPoint,
layers: [{ stroke: 5, colors: exampleP3Grid }]
})
Insert cell
Insert cell
colorToOkLabPoint = (color) => {
const { l, a, b } = culori.oklab(color);
return new Vector3(
250 * -b,
100 * 2.0612 * (0.5 - l),
250 * -a
).multiplyScalar(2);
}
Insert cell
colorSpace3D({
mapper: colorToOkLabPoint,
layers: [{ stroke: 5, colors: exampleP3Grid }]
})
Insert cell
Insert cell
colorToOkHsvPoint = (cone) => (color) => {
let { h, s, v } = culori.okhsv(color);
if (isNaN(h)) h = 0;

const { r, g, b } = culori.p3(color);

const sCone = Math.max(r, g, b) - Math.min(r, g, b);
const hsv = {
h,
s: cone ? sCone : s,
v
};

hsv.h += 135;

const rad = (Math.PI * hsv.h) / 180;
return new Vector3(hsv.s * 150, 150 - 300 * hsv.v, 0).applyAxisAngle(
axisY,
rad - Math.PI / 4
);
}
Insert cell
colorToOkHsvConePoint = colorToOkHsvPoint(true)
Insert cell
colorSpace3D({
mapper: colorToOkHsvConePoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleOkHsvGrid }]
})
Insert cell
colorToOkHsvCylinderPoint = colorToOkHsvPoint(false)
Insert cell
colorSpace3D({
mapper: colorToOkHsvCylinderPoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleOkHsvGrid }]
})
Insert cell
Insert cell
colorToOkHslPoint = (biCone) => (color) => {
let { h, s, l } = culori.okhsl(color);
if (isNaN(s)) s = 1.5;
if (isNaN(h)) h = 1.5;

h += 135;

const rad = (Math.PI * h) / 180;

const coneCoeff = biCone ? 2 * (0.5 - Math.abs(l - 0.5)) : 1;

return new Vector3(s * 150 * coneCoeff, 150 - 300 * l, 0).applyAxisAngle(
axisY,
rad - Math.PI / 4
);
}
Insert cell
colorToOkHslBiConePoint = colorToOkHslPoint(true)
Insert cell
colorSpace3D({
mapper: colorToOkHslBiConePoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleOkHslGrid }]
})
Insert cell
colorToOkHslCylinderPoint = colorToOkHslPoint(false)
Insert cell
colorSpace3D({
mapper: colorToOkHslCylinderPoint,
background: "#ddd",
layers: [{ stroke: 5, colors: exampleOkHslGrid }]
})
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
import { cg } from "@devgru/cg-latest"
Insert cell
Zdog = require("zdog@1/dist/zdog.dist.min.js")
Insert cell
culori = import("culori@4")
Insert cell
THREE = import("three@0")
Insert cell
numeric = import("https://cdn.skypack.dev/numeric@1?min")
Insert cell
svd = numeric.default.svd
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