Published
Edited
Sep 29, 2022
10 stars
Also listed in…
Selected works
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
// draw bg
ctx.fillStyle = palette.bg;
ctx.fillRect(0, 0, width, height);

drawPts(ctx, pixelData, { fill: palette.fg });
}
Insert cell
pixelData = {
randomize;
let data = shapes;
data = debug ? applyDoF(data, { e: 0.1, m: 0.1 }) : applyDoF(data);
return data;
}
Insert cell
function drawPts(ctx, data, opts = {}) {
const { fill, r } = Object.assign(
{
fill: "#fff",
r: 0.125
},
opts
);
ctx.beginPath();
ctx.globalCompositeOperation = "screen";
ctx.fillStyle = fill;
data.forEach((pt) => {
const [x0, y0] = pt.position;

const x = x0 + width / 2;
const y = y0 + height / 2;
ctx.moveTo(x, y);
const R = random.range(r * 0.25, r * 1.5);
ctx.arc(x, y, R, 0, 2 * Math.PI);
});
ctx.fill();
ctx.globalCompositeOperation = "source-over";
}
Insert cell
function applyDoF(pts, opts = {}) {
const { m, e, pixelsPerPoint, frustum } = Object.assign(
{
m: 0.9, // 0.99
e: 0.6, // 0.99
pixelsPerPoint: 100,
frustum: 2000
},
opts
);

const c = camera.slice();
const f = dst(focusPoint, c);

return pts
.map((v) => {
const d = dst(v, c);
const r = m * Math.pow(Math.abs(f - d), e);

return d3.range(pixelsPerPoint).map(() => {
const w = GLVec3.add([], v, rndSphere(r));
const vec3Projected = projectPointOnPlane(w, projectionPlane);
const distToProjectPlane = dst(w, vec3Projected);

const [x, y, z] = vec3Projected;

if (x < -width / 2 || x > width / 2) return false;
if (y < -height / 2 || y > height / 2) return false;
if (distToProjectPlane > frustum) return false;
if (projectionPlane[2] < z) return false;

return {
position: [x, y]
};
});
})
.flat()
.filter(Boolean);
}
Insert cell
function applyRotations(points, opts = {}) {
const { rotateX, rotateY } = Object.assign(
{
rotateX: 1 / 18,
rotateY: 1 / 12
},
opts
);
let pts = points.slice();
pts = pts.map((pt) => GLVec3.rotateX([], pt, [0, 0, 0], -rotateX));
pts = pts.map((pt) => GLVec3.rotateY([], pt, [0, 0, 0], -rotateY));
return pts;
}
Insert cell
shapes = {
randomize;

let shapes = packSphere({
maxCount: 150,
minRadius: 0.1,
maxRadius: 0.75
})
.map((s) => {
const typeOfShape = random.weightedSet(typeOfShapes);
const { position, radius } = s;
if (typeOfShape === "cube") return cube(position, radius);
if (typeOfShape === "square") return square(position, radius);
if (typeOfShape === "line") return line(position, radius);
if (typeOfShape === "triangle") return triangle(position, radius);
return false;
})
.filter(Boolean)
.flat();

return shapes;
}
Insert cell
function line(pos, radius) {
const position = GLVec3.scale([], pos, scaling);
const r = radius * scaling;

const theta = random.range(Math.PI);
const phi = random.range(2 * Math.PI);

const dx = r * Math.sin(theta) * Math.cos(phi);
const dy = r * Math.sin(theta) * Math.sin(phi);
const dz = r * Math.cos(theta);

const p1 = [position[0] + dx, position[1] + dy, position[2] + dz];
const p2 = [position[0] - dx, position[1] - dy, position[2] - dz];

return interpolateBetweenPoints(p1, p2);
}
Insert cell
function triangle(pos, radius) {
const h = Math.sqrt(3) / 2;
const dy = Math.sqrt(3) / 6;
const unitTriangle = [
[-0.5, 0, 0],
[0.5, 0, 0],
[0, h, 0]
].map((p) => GLVec3.add([], p, [0, -dy, 0]));

const position = GLVec3.scale([], pos, scaling);
const r = radius * scaling;
const a = (r * 2) / Math.sqrt(3);

const theta = random.range(Math.PI);
const phi = random.range(2 * Math.PI);

const shiftedTriangle = unitTriangle
.map((p) => GLVec3.scale([], p, a / 2))
.map((p) => GLVec3.rotateX([], p, [0, 0, 0], theta))
.map((p) => GLVec3.rotateZ([], p, [0, 0, 0], phi))
.map((p) => GLVec3.add([], p, position));

const [p1, p2, p3] = shiftedTriangle;

return [
interpolateBetweenPoints(p1, p2),
interpolateBetweenPoints(p2, p3),
interpolateBetweenPoints(p3, p1)
].flat();
}
Insert cell
function square(pos, radius) {
const unitSquare = [
[1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[-1, 1, 0]
];
const position = GLVec3.scale([], pos, scaling);
const r = radius * scaling;
const a = (r * 2) / Math.sqrt(3);

const theta = random.range(Math.PI);
const phi = random.range(2 * Math.PI);

const shiftedSquare = unitSquare
.map((p) => GLVec3.scale([], p, a / 2))
.map((p) => GLVec3.rotateX([], p, [0, 0, 0], theta))
.map((p) => GLVec3.rotateZ([], p, [0, 0, 0], phi))
.map((p) => GLVec3.add([], p, position));

const [p1, p2, p3, p4] = shiftedSquare;

return [
interpolateBetweenPoints(p1, p2),
interpolateBetweenPoints(p2, p3),
interpolateBetweenPoints(p3, p4),
interpolateBetweenPoints(p4, p1)
].flat();
}
Insert cell
function cube(pos, radius) {
const unitCube = [
[0.5, 0.5, 0.5],
[0.5, -0.5, 0.5],
[-0.5, -0.5, 0.5],
[-0.5, 0.5, 0.5],
[0.5, 0.5, -0.5],
[0.5, -0.5, -0.5],
[-0.5, -0.5, -0.5],
[-0.5, 0.5, -0.5]
];
const position = GLVec3.scale([], pos, scaling);
const r = radius * scaling;
const a = (r * 2) / Math.sqrt(3);

const theta = random.range(Math.PI);
const phi = random.range(2 * Math.PI);

const shiftedSquare = unitCube
.map((p) => GLVec3.scale([], p, a / 2))
.map((p) => GLVec3.rotateX([], p, [0, 0, 0], theta))
.map((p) => GLVec3.rotateZ([], p, [0, 0, 0], phi))
.map((p) => GLVec3.add([], p, position));

const [p1, p2, p3, p4, p5, p6, p7, p8] = shiftedSquare;

return [
interpolateBetweenPoints(p1, p2),
interpolateBetweenPoints(p2, p3),
interpolateBetweenPoints(p3, p4),
interpolateBetweenPoints(p4, p1),

interpolateBetweenPoints(p5, p6),
interpolateBetweenPoints(p6, p7),
interpolateBetweenPoints(p7, p8),
interpolateBetweenPoints(p8, p5),

interpolateBetweenPoints(p1, p5),
interpolateBetweenPoints(p2, p6),
interpolateBetweenPoints(p3, p7),
interpolateBetweenPoints(p4, p8)
].flat();
}
Insert cell
typeOfShapes = [
{ value: "cube", weight: 1 },
{ value: "line", weight: 1 },
{ value: "square", weight: 19 },
{ value: "triangle", weight: 19 }
]
Insert cell
focusPoint = [0, 0, 0]
Insert cell
camera = [0, 0, Math.max(width, height) * 2]
Insert cell
Insert cell
scaling = Math.max(width, height) / 2
Insert cell
palette = {
if (colorScheme === "bw") {
return {
bg: `hsl(0,0%,5%)`,
fg: opacify(`hsl(0,0%,95%)`, 0.85)
};
}

return colors;
}
Insert cell
colors = {
randomize;
const bg = getRandomColor(0.4);
let fg = getRandomContrastColor(bg);
fg = opacify(fg, 0.8);

return { bg, fg };
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function rnd() {
return random.value();
}
Insert cell
lerp = math.lerp
Insert cell
dst = distance
Insert cell
function rndSphere(r) {
return random.insideSphere(r);
}
Insert cell
function degToRad(deg) {
return (Math.PI / 180) * deg;
}
Insert cell
function interpolateBetweenPoints(p1, p2) {
const d = distance(p1, p2);

return math
.linspace(d * 2, true)
.map((u) => [
math.lerp(p1[0], p2[0], u),
math.lerp(p1[1], p2[1], u),
math.lerp(p1[2], p2[2], u)
]);
}
Insert cell
function opacify(color, alpha = 1.0) {
const obj = culori.rgb(color);

obj.alpha = alpha;

return culori.formatRgb(obj);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { getRandomColor, getRandomContrastColor } from "@saneef/random-colors"
Insert cell
import { footer } from "@saneef/notebooks-footer"
Insert cell
Insert cell
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