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

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