Published
Edited
Sep 20, 2021
13 forks
Importers
87 stars
Insert cell
Insert cell
Insert cell
import { Vec, Curve } from "@esperanc/2d-geometry-utils"
Insert cell
cellSize = 20
Insert cell
defaultAngle = Math.PI*0.25
Insert cell
drawArrow = (ctx, x, y, angle, size) => {
let v = Vec(1, 0).rotate(angle);
let p = Vec(x, y);
ctx.moveTo(...p.sub(v.scale(size / 2)).array());
let q = p.add(v.scale(size / 2));
ctx.lineTo(...q.array());
let u = v.scale(size / 4);
ctx.moveTo(...q.add(u.rotate(Math.PI * 0.75)).array());
ctx.lineTo(...q.array());
ctx.lineTo(...q.add(u.rotate(-Math.PI * 0.75)).array());
}
Insert cell
//
// From the gist by shaunlebron at https://gist.github.com/shaunlebron/8832585
//
angleLerp = {
var max = Math.PI * 2;

function shortAngleDist(a0, a1) {
var da = Math.sign(a1 - a0) * (Math.abs(a1 - a0) % max);
return Math.sign(a1 - a0) * ((2 * Math.abs(da)) % max) - da;
// var da = (a1 - a0) % max;
// return ((2 * da) % max) - da;
}

return (a0, a1, t) => {
return a0 + shortAngleDist(a0, a1) * t;
};
}
Insert cell
class GridField {
constructor(width, height, cellSize) {
Object.assign(this, { width, height, cellSize });
this.nx = Math.round(width / cellSize);
this.ny = Math.round(height / cellSize);
this.grid = d3
.range(this.nx)
.map((ix) => d3.range(this.ny).map((iy) => defaultAngle));
}
clone() {
let copy = new GridField(this.width, this.height, this.cellSize);
copy.grid = [...this.grid.map((row) => [...row])];
return copy;
}
getCell(ix, iy) {
ix = Math.min(this.nx - 1, Math.max(0, ix));
iy = Math.min(this.ny - 1, Math.max(0, iy));
return this.grid[ix][iy];
}
setCell(ix, iy, angle) {
if (ix < this.nx && ix >= 0 && iy < this.ny && iy >= 0)
this.grid[ix][iy] = angle;
}
getCellIndex(x, y) {
return [~~(x / this.cellSize), ~~(y / this.cellSize)];
}
getField(x, y) {
let [ix, iy] = this.getCellIndex(x, y);
let alphax = (x % this.cellSize) / this.cellSize;
let alphay = (y % this.cellSize) / this.cellSize;

return angleLerp(
angleLerp(this.getCell(ix, iy), this.getCell(ix + 1, iy), alphax),
angleLerp(this.getCell(ix, iy + 1), this.getCell(ix + 1, iy + 1), alphax),
alphay
);
}
draw(ctx) {
ctx.beginPath();
for (let i = 0; i < this.nx; i++) {
for (let j = 0; j < this.ny; j++) {
drawArrow(
ctx,
(i + 0.5) * this.cellSize,
(j + 0.5) * this.cellSize,
this.getCell(i, j),
cellSize * 0.8
);
}
}
ctx.stroke();
}
}
Insert cell
{
let g = new GridField(600, 600, cellSize);
let ctx = DOM.context2d(600, 600);
g.draw(ctx);
return ctx.canvas;
}
Insert cell
Insert cell
field2 = {
let g = new GridField(600, 600, cellSize);
for (let i of d3.range(g.nx)) {
for (let j of d3.range(g.ny)) {
let angle = (j / g.ny) * Math.PI;
g.setCell(i, j, angle);
}
}
return g;
}
Insert cell
{
let ctx = DOM.context2d(600, 600);
field2.draw(ctx);
return ctx.canvas;
}
Insert cell
Insert cell
function fieldCurve(g, x, y, stepLength, numSteps) {
let p = Vec(x, y);
let q = Vec(x, y);
let n = numSteps >> 1;
let curve = new Curve(p);
while (--n > 0) {
let angle = g.getField(p.x, p.y);
let v = Vec(1, 0).rotate(angle).scale(stepLength);
p = p.add(v);
curve.push(p);
}
curve = curve.reverse();
n = numSteps - (numSteps >> 1);
while (--n > 0) {
let angle = g.getField(q.x, q.y);
let v = Vec(-1, 0).rotate(angle).scale(stepLength);
q = q.add(v);
curve.push(q);
}
return curve;
}
Insert cell
drawCurve = (ctx, curve) => {
ctx.beginPath();
for (let { x, y } of curve) ctx.lineTo(x, y);
ctx.stroke();
}
Insert cell
{
let ctx = DOM.context2d(600, 600);
field2.draw(ctx);
ctx.strokeStyle = "red";
drawCurve(ctx, fieldCurve(field2, 100, 100, 1, 500));
ctx.canvas.onclick = (e) => {
drawCurve(ctx, fieldCurve(field2, e.offsetX, e.offsetY, 1, 500));
};
return ctx.canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
{
await visibility();
let ctx = DOM.context2d(600, 600);
for (let [x, y] of sampleFunction(sampleCount, 600, 600)) {
ctx.beginPath();
ctx.arc(x, y, 2, 0, 2 * Math.PI);
ctx.fill();
}
return ctx.canvas;
}
Insert cell
sampleFunction = sampleFunctionName == "poisson"
? poissonSampling
: sampleFunctionName == "grid"
? gridSampling
: randomSampling
Insert cell
function* randomSampling(n, width, height) {
while (n-- > 0) {
yield [width * Math.random(), height * Math.random()];
}
}
Insert cell
function* gridSampling(n, width, height) {
let d = width / Math.sqrt((n * height) / width);
let [x, y] = [d / 2, d / 2];
while (n-- > 0) {
yield [x, y];
x += d;
if (x >= width) {
x = d / 2;
y += d;
}
}
}
Insert cell
poissonSampling = function (n, width, height) {
let d = width / Math.sqrt((n * height) / width);
let pds = new Poisson({
shape: [width, height],
minDistance: d * 0.8,
maxDistance: d * 1.6,
tries: 15
});
return pds.fill();
}
Insert cell
Poisson = require("https://bundle.run/poisson-disk-sampling@2.2.2")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
await visibility();
let field = makePerlinField(
width,
(width * 9) / 16,
cellSize,
noiseScale,
noiseOctaves,
noiseOffset,
seeds,
angleScale
);
let ctx = DOM.context2d(field.width, field.height);
ctx.strokeStyle = "red";
if (drawLayers.includes("field")) field.draw(ctx);
ctx.strokeStyle = "black";
if (drawLayers.includes("curves")) {
let pos = [...sampleFunction(curveCount, field.width, field.height)];
if (useColors.length) {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, field.width, field.height);
ctx.strokeStyle = "red";
if (drawLayers.includes("field")) field.draw(ctx);
ctx.lineWidth = curveThick;
ctx.globalAlpha = curveAlpha;
for (let [x, y] of pos) {
ctx.strokeStyle = seedColor(x, y);
drawCurve(ctx, fieldCurve(field, x, y, 2, curveLength));
}
} else {
ctx.globalAlpha = curveAlpha;
ctx.strokeStyle = "black";
ctx.lineWidth = curveThick;
for (let [x, y] of pos) {
drawCurve(ctx, fieldCurve(field, x, y, 2, curveLength));
}
}
}
return ctx.canvas;
}
Insert cell
function makePerlinField(
width,
height,
cellSize,
noiseScale = 1,
noiseOctaves = 2,
noiseOffset = 0,
seeds = [0, 0],
angleScale = 4
) {
let g = new GridField(width, height, cellSize);
let [seedX, seedY] = seeds;
let noise = octave(perlin2, noiseOctaves);
for (let i of d3.range(g.nx)) {
for (let j of d3.range(g.ny)) {
let angle =
(noiseOffset +
noise(
(seedX + i / g.nx) * noiseScale,
(seedY + j / g.ny) * noiseScale
)) *
Math.PI *
0.7 *
angleScale;
g.setCell(i, j, angle);
}
}
return g;
}
Insert cell
function drawFieldCurves(ctx, field, curveCount = 1000, curveLength = 100) {
let pos = [...sampleFunction(curveCount, field.width, field.height)];
for (let [x, y] of pos) {
drawCurve(ctx, fieldCurve(field, x, y, 2, curveLength));
}
}
Insert cell
seeds = (reseed, [Math.random(), Math.random()])
Insert cell
import { octave, perlin2 } from "@mbostock/perlin-noise"
Insert cell
Insert cell
import { harmonicColors, paletteDisplay } from "@esperanc/color-harmonies"
Insert cell
import { color as ColorInput } from "@esperanc/color-input"
Insert cell
viewof firstColor = ColorInput({ value: "#aabb00", label: "firstColor" })
Insert cell
viewof bgColor = ColorInput({ value: "#ffffff", label: "background color" })
Insert cell
viewof palette = {
let palette = harmonicColors(firstColor, "rectangle", { separation: 2 });
let display = paletteDisplay(palette);
display.value = palette;
return display;
}
Insert cell
Insert cell
Insert cell
Insert cell
seedSpace = {
seeds;
let height = (width * 9) / 16;
let ctx = DOM.context2d(width, height);
let nc = palette.length;
// Create gradient
let grd = ctx.createLinearGradient(0, 0, width, height);

// let grd = ctx.createRadialGradient(
// width / 2,
// height / 2,
// 0,
// width / 2,
// height / 2,
// height
// );
palette.forEach((color, i) => {
grd.addColorStop(i / (nc - 1), color);
});

// Fill with gradient
ctx.fillStyle = grd;
ctx.fillRect(0, 0, width, height);

// Fill with balls
// ctx.fillStyle = palette[0];
// ctx.fillRect(0, 0, width, height);
for (let pos of [...sampleFunction(nballs, width, height)]) {
ctx.fillStyle = palette[~~(Math.random() * nc)];
let r = minRadius + Math.random() * (maxRadius - minRadius);
ctx.beginPath();
ctx.arc(...pos, r, 0, Math.PI * 2);
ctx.fill();
}
return ctx.canvas;
}
Insert cell
seedColor = {
let ctx = seedSpace.getContext("2d");
return (x, y) => {
let pixel = ctx.getImageData(x, y, 1, 1);
var data = pixel.data;
return `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
};
}
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