Public
Edited
Oct 14, 2023
2 forks
29 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const { width = 800, height = 600 } = {};
const mainPolygon = subdividePolygon(
[
[0, 0],
[width, 0],
[width, height],
[0, height]
],
5
);

const polygonPerimeter = (polygon) => {
let q = polygon[polygon.length - 1];
let sum = 0;
for (let p of polygon) {
sum += vec2.dist(q, p);
q = p;
}
return sum;
};

const curvePartition = noiseCurvePartition(simplexParams, { width, height });
const rand = makeRandFunction(randomSeed);
const {
maxRecursionLevel,
minArea,
maxPerimeterAreaRatio,
framesPerLevel,
colorVariation,
avgLuminosity,
lineWidth,
lineColor
} = partitionParam;

const recursivePartition = (
polygon,
baseColor,
level,
sideways,
result = []
) => {
const box = boundingBox(polygon);
const boxSides = [box.max[0] - box.min[0], box.max[1] - box.min[1]];
const smallestSide = Math.min(...boxSides);
const boxArea = boxSides[0] * boxSides[1];
const polyArea = Math.abs(polygonArea(polygon));
const polyPerimeter = polygonPerimeter(polygon);
if (
--level < 0 ||
smallestSide < 10 ||
polyArea < minArea ||
polyPerimeter / Math.sqrt(polyArea) > maxPerimeterAreaRatio
) {
result.push([polygon, baseColor]);
return result;
}
let polyA, polyB;
try {
[polyA, polyB] = curvePartition(polygon, sideways);
} catch (error) {
console.log({ error, polygon });
result.push([polygon, baseColor]);
return result;
}
recursivePartition(
polyA,
baseColor, //colorVary(baseColor, rand(-0.2, 0.2), rand(-0.05, 0.05), 0),
level,
!sideways,
result
);
recursivePartition(
polyB,
colorVaryLuv(
baseColor,
rand(-colorVariation, colorVariation),
rand(-colorVariation, colorVariation),
rand(-colorVariation * 0.2, colorVariation * 0.2)
),
level,
!sideways,
result
);
return result;
};
const ctx = DOM.context2d(width, height);
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth;
const dpr = window.devicePixelRatio;
const baseColor = `hsl(${rand(0, 360)},${rand(50, 60)}%,${
avgLuminosity * 100
}%)`;
ctx.fillStyle = baseColor;
ctx.fillRect(0, 0, width, height);
let imgData = ctx.getImageData(0, 0, width * dpr, height * dpr);
for (let level = 1; level <= maxRecursionLevel; level++) {
let pairs = recursivePartition(mainPolygon, baseColor, level);
for (let i = 0; i < framesPerLevel; i++) {
ctx.putImageData(imgData, 0, 0);
let opacity = i / (framesPerLevel - 1);
ctx.globalAlpha = opacity;
for (let [poly, color] of pairs) {
ctx.fillStyle = color;
ctx.beginPath();
for (let p of poly) ctx.lineTo(...p);
ctx.closePath();
ctx.fill();
if (lineWidth > 0) ctx.stroke();
// let q = findPointInPolygon(poly);
// ctx.fillStyle = "black";
// ctx.beginPath();
// ctx.arc(...q, 2, 0, Math.PI * 2);
// ctx.fill();
}
yield ctx.canvas;
}
imgData = ctx.getImageData(0, 0, width * dpr, height * dpr);
}
}
Insert cell
{
const {
width = 800,
height = 600,
sep = 100,
curveStep = 4,
curveLen = 500,
} = {}; // options
const rand = makeRandFunction(randomSeed);
const noise = createSimplexNoise(
Object.assign({ random: rand }, simplexParams)
);
const direction = (x, y) => {
const ang = noise(x, y) * Math.PI;
return [Math.cos(ang) * curveStep, Math.sin(ang) * curveStep];
};
const curveAt = (x, y) => {
const curve = [[x, y]];
let [px, py] = [x, y];
for (let i = 0; i < curveLen; i++) {
const [dx, dy] = direction(px, py);
[px, py] = [px + dx, py + dy];
curve.push([px, py]);
}
curve.reverse();
[px, py] = [x, y];
for (let i = 0; i < curveLen; i++) {
const [dx, dy] = direction(px, py);
[px, py] = [px - dx, py - dy];
curve.push([px, py]);
}
return curve;
};
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext("2d");
const pds = new Poisson(
{
shape: [width, height],
minDistance: sep * 0.8,
maxDistance: sep / 0.8,
tries: 15
},
rand
);
for (let [x, y] of pds.fill()) {
const curve = curveAt(x, y);
ctx.beginPath();
for (let p of curve) ctx.lineTo(...p);
ctx.stroke();
}
return canvas;
}
Insert cell
import {
OnlineBT,
Ball
} from "@esperanc/omohundro-balltree-construction-algorithms"
Insert cell
import { vec2 } from "@esperanc/vec2-utils"
Insert cell
import { pointInPoly, polygonArea } from "@esperanc/funcoes-2d-uteis"
Insert cell
import { createSimplexNoise } from "@esperanc/simplex-noise-exerciser"
Insert cell
Poisson = require("https://bundle.run/poisson-disk-sampling@2.2.2")
Insert cell
function makeRandFunction(randomSeed) {
const baseRandom = d3.randomLcg(1 / (1 + randomSeed));
return (a, b) =>
b === undefined
? a === undefined
? baseRandom()
: baseRandom() * a
: baseRandom() * (b - a) + a;
}
Insert cell
//
// From https://github.com/sanori/queue-js
//
class Queue {
constructor() {
this._stack1 = [];
this._stack2 = [];
}
push(item) {
this._stack1.push(item);
}
shift() {
if (this._stack2.length === 0) {
const tmp = this._stack2;
this._stack2 = this._stack1.reverse();
this._stack1 = tmp;
}
return this._stack2.pop();
}
get length() {
return this._stack1.length + this._stack2.length;
}
}
Insert cell
function boundingBox(pts) {
let min = [Infinity, Infinity];
let max = [-Infinity, -Infinity];
for (let [x, y] of pts) {
min[0] = Math.min(min[0], x);
min[1] = Math.min(min[1], y);
max[0] = Math.max(max[0], x);
max[1] = Math.max(max[1], y);
}
return { min, max };
}
Insert cell
function extremePoints(v, pts) {
let minDot = Infinity,
min = null;
let maxDot = -Infinity,
max = null;
for (let p of pts) {
let dot = vec2.dot(p, v);
if (dot < minDot) [minDot, min] = [dot, p];
if (dot > maxDot) [maxDot, max] = [dot, p];
}
return { min, max };
}
Insert cell
function subdividePolygon(pts, maxEdgeSize) {
let result = [];
const subdivideSegment = (a, b) => {
if (vec2.dist(a, b) > maxEdgeSize) {
const p = vec2.lerp([], a, b, 0.5);
subdivideSegment(a, p);
result.push(p);
subdivideSegment(p, b);
}
};
let q = pts[pts.length - 1];
for (let p of pts) {
subdivideSegment(q, p);
result.push(p);
q = p;
}
return result;
}
Insert cell
function borderDistance(p, pts) {
let a = pts[pts.length - 1];
let d = Infinity;
for (let b of pts) {
d = Math.min(d, vec2.distSegment(p, a, b));
a = b;
}
return d;
}
Insert cell
function findPointInPolygon(polygon) {
// First heuristics: test middle point between two points on the polygon border
const n = polygon.length;
function findPoint(a, b, tries = 10) {
const p = vec2.lerp([], a, b, 0.5);
if (pointInPoly(p, polygon)) return p;
if (--tries <= 0) return false;
return findPoint(a, p, tries) || findPoint(p, b, tries);
}
let p = null,
d = -Infinity;
for (let v of [
[0, 1],
[1, 0],
[1, 1],
[1, -1]
]) {
let { min, max } = extremePoints(v, polygon);
let middle = vec2.lerp([], min, max, 0.5);
for (let [a, b] of [
[min, middle],
[middle, max],
[min, max]
]) {
let q = findPoint(a, b);
if (q) {
let dist = borderDistance(q, polygon);
if (dist > d) [p, d] = [q, dist];
}
}
}
for (let [i, j] of [
[0, ~~(n * 0.5)],
[~~(n * 0.25), ~~(n * 0.75)]
]) {
let [a, b] = [polygon[i], polygon[j]];
let q = findPoint(a, b);
if (q) {
let dist = borderDistance(q, polygon);
if (dist > d) [p, d] = [q, dist];
}
}
return p;
}
Insert cell
function curveGenFunctions(params = simplexParams, options = {}) {
const {
width = 800,
height = 600,
sep = 100,
curveStep = 5,
curveLen = 500,
seed = randomSeed,
} = options;
const rand = makeRandFunction(seed);
const noise = createSimplexNoise(
Object.assign({ random: rand }, params)
);
const direction = (x, y) => {
const ang = noise(x, y) * Math.PI;
return [Math.cos(ang) * curveStep, Math.sin(ang) * curveStep];
};
const sampleForwards = (x, y) => {
const [dx, dy] = direction(x, y);
return [x + dx, y + dy];
};
const sampleBackwards = (x, y) => {
const [dx, dy] = direction(x, y);
return [x - dx, y - dy];
};
const sampleLeft = (x, y) => {
const [dx, dy] = direction(x, y);
return [x + dy, y - dx];
};
const sampleRight = (x, y) => {
const [dx, dy] = direction(x, y);
return [x - dy, y + dx];
};
return { sampleForwards, sampleBackwards, sampleLeft, sampleRight };
}
Insert cell
function colorVary(color, hvar, svar, lvar) {
color = d3.hsl(color);
let { h, s, l } = color;
h = (h + hvar * 360) % 360;
s = s + svar;
l = l + lvar;
return color.copy({ h, s, l }).formatRgb();
}
Insert cell
d3hsluv = (await require("d3-hsluv")).hsluv
Insert cell
function colorVaryLuv(color, lvar, uvar, vvar) {
color = d3hsluv(color);
let { l, u, v } = color;
l = (l + lvar * 360) % 360;
u = Math.max(0, Math.min(100, u + uvar * 100));
v = Math.max(0, Math.min(100, v + vvar * 100));
return color.copy({ l, u, v }).formatRgb();
}
Insert cell
function noiseCurvePartition(params = simplexParams, options = {}) {
const { width = 800, height = 600 } = options;
const { sampleForwards, sampleBackwards, sampleLeft, sampleRight } =
curveGenFunctions(params, {
width,
height
});
const segmentBall = (a, b, i = -1) => {
const r = vec2.dist(a, b) / 2;
const c = vec2.lerp([], a, b, 0.5);
const ball = new Ball(...c, r);
ball.segment = { a, b, i };
return ball;
};
const intersectsOther = (a, b, bt) => {
const ball = segmentBall(a, b);
for (let other of bt.within(ball, ball.r)) {
if (
vec2.segmentsIntersect(
ball.segment.a,
ball.segment.b,
other.entry.segment.a,
other.entry.segment.b
)
)
return other.entry.segment;
}
return false;
};

return (polygon, sideways = false) => {
const bt = new OnlineBT();
let a = polygon[polygon.length - 1];
for (let i = 0; i < polygon.length; i++) {
const b = polygon[i];
bt.insert(segmentBall(a, b, i));
a = b;
}
const p = findPointInPolygon(polygon);
if (!p) throw "can't find a point inside the polygon";
const pts = [p];
a = p;
let count = 1000;
let exitSegmentA;
const [followA, followB] = sideways
? [sampleBackwards, sampleForwards]
: [sampleLeft, sampleRight];
do {
let b = followA(...a);
let seg = intersectsOther(a, b, bt);
if (seg) {
exitSegmentA = seg.i;
let q = vec2.lineIntersection([], a, b, seg.a, seg.b);
pts.push(q);
break;
}
pts.push(b);
a = b;
} while (count--);
console.assert(count > 0);
a = p;
count = 1000;
pts.reverse();
let exitSegmentB;
do {
let b = followB(...a);
let seg = intersectsOther(a, b, bt);
if (seg) {
exitSegmentB = seg.i;
let q = vec2.lineIntersection([], a, b, seg.a, seg.b);
pts.push(q);
break;
}
pts.push(b);
a = b;
} while (count--);
console.assert(count > 0);
if (exitSegmentA == exitSegmentB) {
console.log({ exitSegmentA, first: pts[0], last: pts[pts.length - 1] });
}
const polygonA = [...pts, polygon[exitSegmentB]];
for (
let i = (exitSegmentB + 1) % polygon.length;
i != exitSegmentA;
i = (i + 1) % polygon.length
)
polygonA.push(polygon[i]);
const polygonB = [...pts.reverse()];
for (let i = exitSegmentA; i != exitSegmentB; i = (i + 1) % polygon.length)
polygonB.push(polygon[i]);
return [polygonA, polygonB];
};
}
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