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

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