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.

///<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 = [];
  }
}