Source code of plot #056 back to plot
Download full working sketch as 056.tar.gz.
Unzip, then start a local web server and load the page in a browser.
Unless otherwise noted, code published here is © Gábor L Ugray, shared under the Creative Commons
BY-NC-SA license (Attribution, Non-Commercial, Share-Alike). Files in lib/thirdparty
, and additional
libraries in the downloadable archive, are shared under their respective open-source licenses, attributed
to their authors.
///<reference path="../pub/lib/paper.d.ts" />
import {info, init, loadLib, setSketch, spin} from "./utils/boilerplate.js";
import {mulberry32, setRandomGenerator, rand} from "./utils/random.js"
import {kdTree} from "./utils/kdTree.js";
const pw = 2100; // Paper width
const ph = 1480; // Paper height
const w = 1480; // Drawing width
const h = 1050; // Drawing height
const segLen = 2;
let seed = Math.round(Math.random() * 65535);
setSketch(async function () {
setRandomGenerator(mulberry32(seed));
info("Seed: " + seed);
init(w, h, pw, ph);
await spin(50);
const startTime = performance.now();
await draw();
const elapsed = performance.now() - startTime;
console.log(`Drawn in ${elapsed} msec`);
});
async function draw() {
paper.project.currentStyle.strokeWidth = 2;
paper.project.currentStyle.strokeColor = "black";
const kdt = new kdTree([], (a, b) => Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2), ["x", "y"]);
// How many curves; how much of canvas width/height they take up
const nCurves = 35;
const curveW = 0.95;
const curveH = 0.95;
const interruptProb = 0.5;
const thick3Prob = 0.5;
const parallelGap = 2;
const parallelRamp = 20;
const minLineDist = 5;
const isectShorten = 6;
const minPathLengthPts = 28;
// One curve
const drawCurve = (reverse) => {
// How many harmonics; they respective amplitudes
const prms = [];
const nHarm = Math.floor(7 * rand()) + 1;
while (prms.length < nHarm) {
prms.push({
h: (prms.length + 2) * 0.5,
a: (rand() - 0.5) ** 3,
});
}
// Generate one waveform's points
const curvePts = makeCurvePath(prms);
if (reverse) curvePts.reverse();
// With some probability, keep only sections of curve
const onOffPaths = [];
if (rand() < interruptProb) onOffPaths.push(...onoff(curvePts));
else onOffPaths.push(curvePts);
// Translate to canvas coordinates
// With some probability, add parallel curves for thickness
const canvasPaths = [];
for (const pts of onOffPaths) {
let canvasPts = pts.map(p => new Point(w * (0.5 + p.x * curveW * 0.5), h * (0.5 + p.y * curveH * 0.5)));
canvasPts = sparser(canvasPts, segLen);
const rnd = rand();
if (rnd < thick3Prob) canvasPaths.push(...tripleCurve(canvasPts, parallelGap, parallelRamp));
else canvasPaths.push(canvasPts);
}
// Draw with masking (line hiding)
const drawnPaths = [];
for (const pts of canvasPaths) {
const visiblePaths = getVisiblePaths(pts, kdt, minLineDist);
for (const pathPts of visiblePaths) {
let shortenedPts = shorten(pathPts, isectShorten);
if (shortenedPts.length < minPathLengthPts) continue;
const path = new paper.Path(shortenedPts);
paper.project.activeLayer.addChild(path);
drawnPaths.push(shortenedPts);
}
}
for (const pts of drawnPaths) {
for (const pt of pts) kdt.insert(pt);
}
}
// All the curves
for (let i = 0; i < nCurves; ++i) {
drawCurve((i % 2) == 0);
if ((i%2) == 0) await spin();
}
}
function makeCurvePath(prms) {
const eps = 1e-6;
const nSegs = 1e4;
let pts = [];
let max = Number.MIN_VALUE;
for (let i = 0; i <= nSegs; ++i) {
const x = 2 * i / nSegs - 1;
let y = 0;
for (const prm of prms) {
let phase = 0;
if (Math.abs((prm.h%1) - 0.5) < eps)
phase = Math.PI * 0.5;
y += prm.a * Math.sin(prm.h * x * Math.PI + phase);
}
pts.push(new Point(x, y));
if (Math.abs(y) > max) max = Math.abs(y);
}
for (const pt of pts) {
pt.y /= max;
pt.y *= Math.exp(-4*(pt.x)**2);
}
return pts;
}
function onoff(pts) {
const lenFact = 0.2;
const gapFact = 0.2;
let startIx = Math.max(0, (rand() - 0.5) * 2 * gapFact);
startIx = Math.round(startIx * pts.length);
const res = [];
while (startIx < pts.length) {
const count = Math.round((rand() + 0.5) * lenFact * pts.length);
const endIx = Math.min(pts.length, startIx + count);
res.push(pts.slice(startIx, endIx));
startIx = endIx + Math.round((rand() + 0.5) * gapFact * pts.length);
}
return res;
}
function tripleCurve(pts, gap, ramp) {
let ofsStart1 = Number.MAX_VALUE, ofsStart2 = Number.MAX_VALUE;
for (let i = 1, travel = 0; i < pts.length && travel <= 2 * ramp; ++i) {
travel += pts[i].subtract(pts[i-1]).length;
if (ofsStart1 == Number.MAX_VALUE && travel >= ramp) ofsStart1 = i;
if (ofsStart2 == Number.MAX_VALUE && travel >= 2 * ramp) ofsStart2 = i;
}
let ofsEnd1 = Number.MIN_VALUE, ofsEnd2 = Number.MIN_VALUE;
for (let i = pts.length - 2, travel = 0; i >= 0 && travel <= 2 * ramp; --i) {
travel += pts[i].subtract(pts[i+1]).length;
if (ofsEnd1 == Number.MIN_VALUE && travel >= ramp) ofsEnd1 = i;
if (ofsEnd2 == Number.MIN_VALUE && travel >= 2 * ramp) ofsEnd2 = i;
}
const pts1 = [], pts2 = [], pts3 = [];
const addPoint = (ix, orto) => {
if (ix <= ofsEnd2) pts1.push(pts[ix].add(orto));
if (ix >= ofsStart1 && ix <= ofsEnd1) pts2.push(pts[ix]);
if (ix >= ofsStart2) pts3.push(pts[ix].subtract(orto));
};
for (let i = 1; i < pts.length; ++i) {
const orto = pts[i].subtract(pts[i-1]).rotate(90);
orto.length = gap;
if (i == 1) addPoint(0, orto);
addPoint(i, orto);
}
const res = [];
if (pts1.length > 0) res.push(pts1);
if (pts2.length > 0) res.push(pts2);
if (pts2.length > 0) res.push(pts3);
return res;
}
function sparser(pts, minLen) {
const res = [pts[0]];
let travel = 0;
for (let i = 1; i < pts.length; ++i) {
travel += pts[i].subtract(pts[i-1]).length;
if (travel >= minLen || i == pts.length - 1) {
res.push(pts[i]);
travel = 0;
}
}
return res;
}
function shorten(pts, byLen) {
if (byLen == 0) return pts;
let firstIx = -1, lastIx = -1;
for (let travel = 0, i = 1; travel < byLen && i < pts.length; ++i) {
travel += pts[i].subtract(pts[i-1]).length;
if (travel >= byLen) firstIx = i;
}
for (let travel = 0, i = pts.length - 2; travel < byLen && i >= 0; --i) {
travel += pts[i].subtract(pts[i+1]).length;
if (travel >= byLen) lastIx = i;
}
if (firstIx == -1 || lastIx == -1 || firstIx >= lastIx)
return [];
return pts.slice(firstIx, lastIx + 1);
}
function getVisiblePaths(pts, kdt, dist) {
// Result: array of array of points.
const polys = [];
let currPts = [];
for (const pt of pts) {
const neighbors = kdt.nearest(pt, 1, dist);
const visible = neighbors.length == 0;
if (!visible) addCurrentPoints();
else currPts.push(pt);
}
addCurrentPoints();
return polys;
function addCurrentPoints() {
if (currPts.length == 0) return;
if (currPts.length >= 2) {
polys.push(currPts);
}
currPts = [];
}
}