Public
Edited
Aug 1, 2023
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewer.animate()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
transitionColorSpaceCoord
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
cvd = Generators.input(viewof sampleCvd)
Insert cell
// smelly but effective
transitionFilter(
transitionGenerator(viewof sampleCvd.children[1], {
states: ["none", "protan", "deuteran", "tritan"]
}),
viewof transCvd
)
Insert cell
// used to hold filtered value
viewof transCvd = Inputs.input()
Insert cell
transMatCvd = transitionInterpolate(transCvd, (state) =>
matrixCvd(state, cvd.severity)
)
Insert cell
matCvd = matrixCvd(cvd.condition, cvd.severity)
Insert cell
Insert cell
fnCvd = (x) => math.multiply(transMatCvd, x)
Insert cell
Insert cell
fnSRgbCvd = _.flow([
colorUtils.linearRgbFromSRgb,
fnCvd,
colorUtils.linearRgbToSRgb
])
Insert cell
Insert cell
sampleSRgb = colorUtils.hexToSRgb(sampleHex)
Insert cell
Insert cell
sampleSRgbCvd = fnSRgbCvd(sampleSRgb)
Insert cell
Insert cell
sampleHexCvd = colorUtils.hexFromSRgb(sampleSRgbCvd)
Insert cell
transitionFilter(
transitionGenerator(viewof sampleColorSpace, {
states: Array.from(colorUtils.mapColorSpaces().values())
}),
viewof transitionColorSpace
)
Insert cell
// used to hold filtered value
viewof transitionColorSpace = Inputs.input()
Insert cell
Insert cell
callbackColorSpace = function (state) {
return _.flow([colorSpaces[state].fromSRgb, colorSpaces[state].to100])(
sampleSRgbCvd
);
}
Insert cell
Insert cell
transitionColorSpaceCoord = transitionInterpolate(
transitionColorSpace,
callbackColorSpace
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class ColorViewer {
constructor({
colors = cvConfig.colors(),
gamut = cvConfig.gamut(),
surfaces = [],
inputs = cvConfig.inputs(),
colorspace = "munsell",
height = 600,
camera = cvConfig.camera()
} = {}) {
this.inputs = inputs;

this.colors = colors.srgb.map((x) => ({
linearRGB: colorUtils.linearRgbFromSRgb(x),
mesh: threeColor(colors.size)
}));

this.scene = new THREE.Scene();

this.colors.forEach((x) => this.scene.add(x.mesh));

// `width` is available from Observable
const aspect = width / height;
this.camera = new THREE.PerspectiveCamera(
camera.fov,
aspect,
camera.near,
camera.far
);
this.camera.position.set(...camera.position);
this.camera.up = new THREE.Vector3(1, 0, 0);

this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(devicePixelRatio);

this.controls = new THREE.OrbitControls(
this.camera,
this.renderer.domElement
);

this.controls.target = new THREE.Vector3(50, 0, 0);

this.controls.addEventListener("change", () =>
this.renderer.render(this.scene, this.camera)
);

this.view = this.renderer.domElement;

this.animate = function* () {
// initialize

// `children[2]`: ColorSpace input
const transGenColorSpace = transitionGenerator(this.inputs.children[2], {
states: Array.from(colorUtils.mapColorSpaces().values())
});

// `children[3]`: CVD input
const transGenCvd = transitionGenerator(
this.inputs.children[3].children[1],
{
states: ["none", "protan", "deuteran", "tritan"]
}
);

let transitionColorSpaceOld = {};
let transitionMatCvdOld = [];

while (true) {
// color space
let transitionColorSpace = transGenColorSpace.next().value;

let changedColorSpace = !_.isEqual(
transitionColorSpaceOld,
transitionColorSpace
);
transitionColorSpaceOld = _.clone(transitionColorSpace);

// background color
// let background = fnBackgroundToSRgb(this.inputs.value.background);
let background = transitionInterpolate(
transitionColorSpace,
(state) => {
return _.flow([
(x) => [x, 0, 0],
colorSpaces[state].from100,
colorSpaces[state].toSRgb,
colorUtils.clampRgbGamut
])(this.inputs.value.background);
}
);
this.scene.background = new THREE.Color(...background);

// scale of color-spheres
let scale = transitionInterpolate(transitionColorSpace, (state) => {
// about 1 for Munsell, OkLab; about 2 for CIELuv
return (
(colorSpaces[state].scale.chroma /
colorSpaces[state].scale.luminance) *
2.5 +
0.75
);
});

// color-vision deficiency
let transitionCvd = transGenCvd.next().value;

let transitionMatCvd = transitionInterpolate(transitionCvd, (state) =>
matrixCvd(state, this.inputs.value.cvd.severity)
);

let changedCvd = !_.isEqual(transitionMatCvdOld, transitionMatCvd);
transitionMatCvdOld = _.cloneDeep(transitionMatCvd);

// Number[3] -> Number[3]
let linearRgbtoCvdSRgbClamp = _.flow([
(x) => math.multiply(transitionMatCvd, x),
colorUtils.linearRgbToSRgb,
colorUtils.clampRgbGamut
]);

if (changedCvd || changedColorSpace) {
this.colors.forEach((x) => {
// calculate new sRGB values, coordinates
const sRgbCvd = linearRgbtoCvdSRgbClamp(x.linearRGB);
const coordsCvd = transitionInterpolate(
transitionColorSpace,
(state) => {
return _.flow([
colorSpaces[state].fromSRgb,
colorSpaces[state].to100
])(sRgbCvd);
}
);

// implement
x.mesh.position.set(...coordsCvd);
x.mesh.material.color.setRGB(...sRgbCvd);
x.mesh.material.needsUpdate = true;

// size
x.mesh.scale.setScalar(scale);
});
}

this.controls.autoRotate = true;
this.controls.autoRotateSpeed = this.inputs.value.rotationSpeed;
this.controls.update();
this.renderer.render(this.scene, this.camera);

yield changedCvd || changedColorSpace;
}
};

invalidation.then(() => (this.controls.dispose(), this.renderer.dispose()));
}
}
Insert cell
cvConfig = ({
inputs: function ({
background = 50,
rotationSpeed = 0,
colorSpace = "munsell",
cvd = { condition: "deuteran", severity: 0 }
} = {}) {
return Inputs.form({
background: Inputs.range([0, 100], {
label: "Background luminance",
step: 1,
value: background
}),
rotationSpeed: Inputs.range([-5, 5], {
label: "Rotation speed",
step: 0.5,
value: rotationSpeed
}),
colorSpace: Inputs.radio(colorUtils.mapColorSpaces(), {
label: "Color space",
value: "munsell"
}),
cvd: inputCvd(cvd)
});
},
colors: function ({
srgb = [
[0, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0],
[1, 0, 1],
[1, 1, 0],
[1, 1, 1]
],
size = 1,
alpha = 1
} = {}) {
return { srgb, size, alpha };
},
gamut: function ({ n = 16 } = {}) {
return { n };
},
camera: function ({
fov = 45,
near = 1,
far = 5000,
position = [150, 50, 50]
} = {}) {
return { fov, near, far, position };
}
})
Insert cell
// see https://observablehq.com/@observablehq/views
Generators.input(viewer.inputs)
Insert cell
threeColor = function (size = 2) {
const geometry = new THREE.SphereGeometry(size, 16, 8);
const material = new THREE.MeshBasicMaterial();
return new THREE.Mesh(geometry, material);
}
Insert cell
Insert cell
threeVersion = "0.153.0"
Insert cell
import { THREE } with { threeVersion } from "@severo/three"
Insert cell
math = import("https://cdn.skypack.dev/mathjs@11.8.2?")
Insert cell
import { changeTable } from "@ijlyttle/change-log"
Insert cell
import { inputCvd, matrixCvd } from "@ijlyttle/cvd-widget"
Insert cell
import { colorSpaces, colorUtils } from "@ijlyttle/color-utilities"
Insert cell
import {
transitionGenerator,
transitionFilter,
transitionInterpolate,
Easing
} from "@ijlyttle/transition-helper"
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