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

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