Published
Edited
Jan 29, 2022
Importers
48 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
//
// An interface for building palettes from just one color
//
function paletteBuilder(options = {}) {
let {
startColor = "#aa3333",
size = 300,
ncolors = 5,
tension = 0.3,
slSpacing = 0.2,
hueSpacing = 20
} = options;

// Controls
let palDisplay = html`<div>`;
let controls = paged(
{
hueSpacing: Inputs.range([1, 120], {
label: "Hue separation",
step: 1,
value: hueSpacing
}),
slSpacing: Inputs.range([0.01, 0.4], {
label: "Sat Lum spacing",
step: 0.01,
value: slSpacing
}),
tension: Inputs.range([0.1, 0.5], {
label: "SL curve tension",
value: tension,
step: 0.05
}),
nColors: Inputs.range([2, 9], {
label: "Num colors",
step: 1,
value: ncolors
})
},
undefined,
palDisplay
);
Object.assign(controls.style, {
display: "inline-block",
verticalAlign: "top",
marginRight: "10px"
});

// The sat lum plane
let { h, s, l } = d3.hsl(startColor);
let sz = size;
let slchart = SLChart(h, [s, l], sz);
Object.assign(slchart.style, {
display: "inline-block",
marginRight: "10px"
});

let npoints = ncolors - 1;
let sls;

// Compute sl points and draw them on the sl chart
const updateSlChart = () => {
let ctx = slchart.getContext("2d");
let sz = slchart.width;
let { slToXy, xyToSl } = colorplane(sz, sz);
let q = Vec(...slToXy(s, l));
const options = {
spacing: controls.value.slSpacing,
tension: controls.value.tension,
sizeX: sz,
sizeY: sz
};
let points = colorPlaneCurve(q, npoints, options);
sls = points.map((p) => xyToSl(p.x, p.y));

// Draw points
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
for (let { x, y } of points) {
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.stroke();
}
};
updateSlChart();

// The hue bar
let hueselector = hueSelector(h, 20, sz);
Object.assign(hueselector.style, {
display: "inline-block",
marginRight: "10px"
});
let hues;

// Compute and draw the interpolated hues;
const updateHue = () => {
let ctx = hueselector.getContext("2d");

let hSpacing = controls.value.hueSpacing; // degrees
hues = d3.range(1, (npoints >> 1) + 1).map((i) => (h + i * hSpacing) % 360);
hues = hues.concat(
d3
.range(1, npoints - (npoints >> 1) + 1)
.map((i) => (h - i * hSpacing + 360) % 360)
);
ctx.lineWidth = 1;
for (let hue of hues) {
let y = (hue / 360) * sz;
ctx.beginPath();
ctx.arc(10, y, 4, 0, Math.PI * 2);
ctx.stroke();
}
};
updateHue();

// Container for sl and hue
let div = html`<div>${slchart}${hueselector}${controls}</div>`;

function setValue() {
div.value = d3
.zip(sls, hues)
.map(([[s, l], h]) => d3.hsl(h, s, l).formatHsl());
div.value.splice(npoints >> 1, 0, d3.hsl(h, s, l).formatHsl());
palDisplay.innerHTML = "";
palDisplay.append(displayPalette(div.value));
div.dispatchEvent(new CustomEvent("input"));
}

function handleSlInput(e) {
[s, l] = slchart.value;
updateHue();
updateSlChart();
setValue();
e.preventDefault();
}

function handleHueInput(e) {
h = hueselector.value;
slchart.setHue(h);
updateHue();
updateSlChart();
setValue();
e.preventDefault();
}

function updateOnControl() {
npoints = controls.value.nColors - 1;
slchart.draw();
hueselector.draw();
updateSlChart();
updateHue();
setValue();
}

controls.addEventListener("input", updateOnControl);

slchart.addEventListener("input", handleSlInput);
hueselector.addEventListener("input", handleHueInput);
setValue();

return div;
}
Insert cell
function paletteAdjuster(palOrInput, options = {}) {
let { size = 300, hInc = 20, sInc = 0.2, lInc = 0.2 } = options;

// Controls
let palDisplay = html`<div>`;
let controls = paged(
{
hInc: Inputs.range([-90, 90], {
label: "Hue increment",
step: 1,
value: hInc
}),
sInc: Inputs.range([-0.5, 0.5], {
label: "Sat increment",
step: 0.01,
value: sInc
}),
vInc: Inputs.range([-0.5, 0.5], {
label: "Lum increment",
step: 0.01,
value: lInc
})
},
undefined,
palDisplay
);
Object.assign(controls.style, {
display: "inline-block",
verticalAlign: "top",
marginRight: "10px"
});

let div = html`<div>${controls}`;
function setValue() {
let pal = palOrInput instanceof Array ? palOrInput : palOrInput.value;
div.value = adjustPalette(
pal,
controls.value.hInc,
controls.value.sInc,
controls.value.vInc
);
palDisplay.innerHTML = "";
palDisplay.append(displayPalette(div.value));
div.dispatchEvent(new CustomEvent("input"));
}

if (!(palOrInput instanceof Array)) {
palOrInput.addEventListener("input", setValue);
invalidation.then(() => palOrInput.removeEventListener("input", setValue));
}
controls.addEventListener("input", setValue);
setValue();

return div;
}
Insert cell
//
// Conversion from HSL to HSV (minus H, which is the same)
// Assumes normalized coordinates
//
function slToSv(s, l) {
let V = l + s * Math.min(l, 1 - l);
let S = V == 0 ? 0 : 2 * (1 - l / V);
return [S, V];
}
Insert cell
//
// Returns the hsv components of a color
//
function hsv(color) {
let { h, s, l } = d3.hsl(color);
let [S, v] = slToSv(s, l);
return { h, s: S, v };
}
Insert cell
//
// Conversion from HSV to HSL (minus H, which is the same)
// Assumes normalized coordinates
//
function svToSl(S, V) {
let l = V * (1 - S / 2);
let s = l == 0 || l == 1 ? 0 : (V - l) / Math.min(l, 1 - l);
return [s, l];
}
Insert cell
//
// Provides functions to map to/from the SL plane to x,y coordinates
//
function colorplane(width, height) {
return {
slToXy: (s, l) => {
let [S, V] = slToSv(s, l);
return [S * width, (1 - V) * height];
},
xyToSl: (x, y) => svToSl(x / width, 1 - y / height)
};
}
Insert cell
// Samples the color plane (where the sat/lum values are defined)
// with a smooth curve passing through the middle point. Returns nPoints points
// in addition to middle
function colorPlaneCurve(middle, nPoints, options = {}) {
let { spacing = 0.1, tension = 0.2, sizeX = 1, sizeY = 1 } = options;
let n1 = nPoints >> 1;
let n2 = nPoints - n1;
let bez1 = new Bezier(
...smoothControlPoints(
[
middle,
Vec(sizeX, sizeY),
Vec(sizeX, sizeY * 2),
Vec(-sizeX, sizeY * 2),
Vec(-sizeX, 0),
Vec(0, 0)
],
tension
).slice(1)
);
let bez2 = new Bezier(
...smoothControlPoints(
[
middle,
Vec(0, 0),
Vec(-sizeX, 0),
Vec(-sizeX, 2 * sizeY),
Vec(sizeX, 2 * sizeY),
Vec(sizeX, sizeY)
],
tension
).slice(1)
);

function arcLenBez(bez, n) {
const m = n * 10;
let baseCurve = new Curve(...d3.range(m + 1).map((t) => bez.point(t / m)));
return baseCurve
.resample(Math.round(1 / spacing) + 1)
.slice(0, n)
.map(({ x, y }) =>
Vec(Math.min(sizeX, Math.max(0, x)), Math.min(sizeY, Math.max(0, y)))
);
}

let result = arcLenBez(bez1, n1 + 1);
return result
.slice(1, n1 + 1)
.reverse()
.concat(arcLenBez(bez2, n2 + 1).slice(1, n2 + 1));
}
Insert cell
//
// A saturation/lightness plane
//
function SLChart(hue, value = [0.8, 0.8], sz = 500, interactive = true) {
let { slToXy, xyToSl } = colorplane(sz, sz);

let ctx = new DOM.context2d(sz, sz, 1);
let canvas = ctx.canvas;
let n = 100;
let cellSz = sz / n;

const draw = () => {
for (let i = 0; i < n; i++) {
let x = (i / n) * sz;
for (let j = 0; j < n; j++) {
let y = (j / n) * sz;
let [s, l] = xyToSl(x, y);
ctx.fillStyle = `hsl(${hue},${s * 100}%,${l * 100}%)`;
ctx.fillRect(x, y, cellSz + 1, cellSz + 1);
}
}
let [s, l] = value;
let [x, y, r] = [...slToXy(s, l), 6];
ctx.strokeStyle = "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.stroke();
};

const setValue = (sl) => {
value = canvas.value = sl;
draw();
canvas.dispatchEvent(new CustomEvent("input"));
};

const setHue = (h) => {
hue = h;
draw();
};

if (interactive) {
let isDown = false;
canvas.onmousedown = (e) => {
isDown = true;
setValue(xyToSl(e.offsetX, e.offsetY));
};
canvas.onmousemove = (e) => {
if (!isDown) return;
setValue(xyToSl(e.offsetX, e.offsetY));
};
canvas.onmouseup = () => {
isDown = false;
};
}
canvas.value = value;
canvas.setValue = setValue;
canvas.setHue = setHue;
canvas.draw = draw;
draw();
return canvas;
}
Insert cell
//
// A hue vertical bar that can be used to select a hue (between 0 and 360)
//
function hueSelector(value = 0, w = 20, sz = 500, interactive = true) {
let ctx = new DOM.context2d(w, sz, 1);
let canvas = ctx.canvas;
let n = 200;
let cellSz = sz / n;

const draw = () => {
for (let i = 0; i < n; i++) {
ctx.fillStyle = `hsl(${(360 * i) / n},100%,50%)`;
ctx.fillRect(0, i * cellSz, w, cellSz + 1);
}

let [x, y, r] = [w / 2, (value / 360) * sz, w / 3];
ctx.strokeStyle = "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.stroke();
};

const setValue = (y) => {
canvas.value = value = Math.max(0, Math.min(360, (y / sz) * 360));
draw();
canvas.dispatchEvent(new CustomEvent("input"));
};

if (interactive) {
let isDown = false;
canvas.onmousedown = (e) => {
isDown = true;
setValue(e.offsetY);
};
canvas.onmousemove = (e) => {
if (!isDown) return;
setValue(e.offsetY);
};
canvas.onmouseup = () => {
isDown = false;
};
}
canvas.value = value;
canvas.setValue = setValue;
canvas.draw = draw;
draw();

return canvas;
}
Insert cell
//
// A very crude palette display
//
function displayPalette(pal, options = {}) {
let {
background = "white",
cellWidth = "30px",
cellHeight = "60px",
padding = 0,
cellPadding = 0
} = options;
let div = html`<div>`;
Object.assign(div.style, { background, padding, display: "inline-block" });
for (let color of pal) {
let cell = html`<div>`;
Object.assign(cell.style, {
display: "inline-block",
padding: cellPadding,
width: cellWidth,
minWidth: cellWidth,
height: cellHeight,
minHeight: cellHeight,
background: color
});
div.append(cell);
}
const button = html`<input type=button value="Copy">`;
Object.assign(button.style, {
display: "inline-block",
minHeight: cellHeight,
padding: "10px",
marginLeft: "10px",
verticalAlign: "top"
});
button.onclick = () =>
navigator.clipboard.writeText(pal.map((col) => d3.color(col).formatHex()));

div.append(button);
return div;
}
Insert cell
//
// Returns an adjusted palette where hue, saturation and value
// are incremented by (small) amounts. Note that hInc is given in degrees
// while sInc and lInc are given in normalized format, i.e., numbers between 0 and 1
//
function adjustPalette(pal, hInc = +10, sInc = 0.1, lInc = 0.1) {
let result = [];
for (let color of pal) {
let { h, s, l } = d3.hsl(color);
h = (h + hInc + 360) % 360;
s = Math.min(1, Math.max(0, s + sInc));
l = Math.min(1, Math.max(0, l + lInc));
result.push(d3.hsl(h, s, l).formatHsl());
}
return result;
}
Insert cell
Insert cell
import { Vec, Bezier, Curve } from "@esperanc/2d-geometry-utils"
Insert cell
import { smoothControlPoints } from "@esperanc/smooth-polygon"
Insert cell
import { combo, paged, tabbed } from "@esperanc/aggregated-inputs"
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