Source code of plot #030 back to plot

Download full working sketch as 030.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} from "./utils/boilerplate.js";
import {mulberry32, rand, rand_range, setRandomGenerator} from "./utils/random.js"
import {getMaskedLine, getMaskedPoly, lnPtDist} from "./utils/geo.js";

const w = 1050;
const h = 1480;
const margin = 50;

let seed = Math.round(Math.random() * 65535);

setSketch(function () {
  setRandomGenerator(mulberry32(seed));
  info("Seed: " + seed);
  init(w, h);
  draw();
})

function draw() {
  paper.project.currentStyle.strokeColor = "black";
  paper.project.currentStyle.strokeWidth = 2;

  let dropWidthAvg = rand_range(30, 100);
  let dropCount = 1000 / dropWidthAvg;
  let dropAngle = rand_range(-60, 60);
  let dropPaths = generateDrops(dropCount, dropWidthAvg / 2, dropWidthAvg * 1.5, dropWidthAvg * 3, dropAngle);
  dropPaths.forEach(p => paper.project.activeLayer.addChild(p));

  const nRectsLog = rand_range(2, 6);
  const nRects = Math.round(Math.pow(2, nRectsLog));
  let rects = [new Rect(new Point(margin, margin), w - 2 * margin, h - 2 * margin)];
  while (rects.length < nRects) {
    let newRects = [];
    for (const rect of rects) {
      let parts = rect.split();
      newRects.push(...parts);
    }
    rects = newRects;
  }

  let rectPaths = [];
  for (const rect of rects) {
    rect.inset(8);
    drawMaskedRect(rect, dropPaths);
    rectPaths.push(Path.Rectangle(rect.pos.x, rect.pos.y, rect.w, rect.h));
  }

  let leftHY = rand_range(h / 2, h - margin);
  let rightHY = rand_range(h / 6, 2 * h / 3);
  if (rand() < 0.5) [leftHY, rightHY] = [rightHY, leftHY];
  let bottomPath = new Path({
    segments: [[margin, leftHY], [w - margin, rightHY], [w - margin, h - margin], [margin, h - margin]],
    closed: true,
  });

  const waveLength = rand_range(50, 800);
  const waveAmp = rand_range(waveLength / 10, waveLength / 5);
  const waveGap = rand_range(60 / nRectsLog, 100 / nRectsLog);
  let polys = genWaves(waveAmp, waveLength, waveGap);
  for (const poly of polys) {
    for (const rp of rectPaths) {
      let visiblePaths = getMaskedPoly(poly, [bottomPath, ...dropPaths], [rp]);
      for (const pathPts of visiblePaths) {
        const path = new paper.Path(pathPts);
        paper.project.activeLayer.addChild(path);
      }
    }
  }

  const groundGap = rand_range(10, 35);
  const groundAvgLen = 5 * groundGap;
  const groundAvgSkip = 400 / groundGap;
  let lines = genGround(leftHY, rightHY, groundGap, groundAvgLen * 0.5, groundAvgLen * 1.5, groundAvgSkip * 0.5, groundAvgSkip * 1.5);
  for (const line of lines) {
    for (const rp of rectPaths) {
      const maskedLines = getMaskedLine(line[0], line[1], dropPaths, [rp]);
      for (const vl of maskedLines) {
        const ln = Path.Line(vl[0], vl[1]);
        paper.project.activeLayer.addChild(ln);
      }
    }
  }
}

function genGround(leftHY, rightHY, gap = 15, minLen = 50, maxLen = 200, minSkip = 10, maxSkip = 40) {
  let lines = [];
  let firstLn = [new Point(margin, leftHY), new Point(w - margin, rightHY)];
  lines.push(firstLn);

  let firstLnLen = Path.Line(...firstLn).length;
  let startPt, lnDir, gapDir, corner;
  if (leftHY < rightHY) {
    startPt = new Point(margin, leftHY);
    lnDir = firstLn[1].subtract(firstLn[0]);
    lnDir.length = 1;
    gapDir = lnDir.rotate(90);
    corner = new Point(margin, h - margin);
  }
  else {
    startPt = new Point(w - margin, rightHY);
    lnDir = firstLn[0].subtract(firstLn[1]);
    lnDir.length = 1;
    gapDir = lnDir.rotate(-90);
    corner = new Point(w - margin, h - margin);
  }
  let maxDist = lnPtDist(...firstLn, corner);
  for (let dist = gap; dist < maxDist; dist += gap) {
    for (let pos = 0; pos < firstLnLen * 1.5;) {
      let segLen = rand_range(minLen, maxLen);
      let pt1 = startPt.clone().add(gapDir.multiply(dist));
      pt1 = pt1.add(lnDir.multiply(pos))
      let pt2 = pt1.add(lnDir.multiply(segLen));
      lines.push([pt1, pt2]);
      pos += segLen;
      pos += rand_range(minSkip, maxSkip);
    }
  }

  return lines;
}

function genWaves(amp, waveLen, gap) {
  let polys = [];

  const nPoints = (w - 2 * margin) / 2;
  const periods = (w - 2 * margin) / waveLen;
  for (let y = margin - amp / 2; y < h - margin + amp / 2; y += gap) {
    let pts = [];
    for (let i = 0; i <= nPoints; ++i) {
      let t = i / nPoints;
      let angle = (t - 0.5) * periods * 2 * Math.PI;
      let pt = new Point(margin + (w - 2 * margin) * t, y - amp / 2 * Math.cos(angle));
      pts.push(pt);
    }
    polys.push(pts);
  }
  return polys;
}

/**
 * Draws rectangle, with exclusion masks blocking parts of the outline.
 * @param rect {Rect} The rectangle to draw.
 * @param dropPaths {Array<paper.Path>} Exclusion masks.
 */
function drawMaskedRect(rect, dropPaths) {
  let lines = [];
  lines.push(...getMaskedLine(new Point(rect.pos.x, rect.pos.y), new Point(rect.pos.x + rect.w, rect.pos.y), dropPaths, [], 2));
  lines.push(...getMaskedLine(new Point(rect.pos.x + rect.w, rect.pos.y), new Point(rect.pos.x + rect.w, rect.pos.y + rect.h), dropPaths, [], 2));
  lines.push(...getMaskedLine(new Point(rect.pos.x + rect.w, rect.pos.y + rect.h), new Point(rect.pos.x, rect.pos.y + rect.h), dropPaths, [], 2));
  lines.push(...getMaskedLine(new Point(rect.pos.x, rect.pos.y + rect.h), new Point(rect.pos.x, rect.pos.y), dropPaths, [], 2));
  for (const vl of lines) {
    const ln = Path.Line(vl[0], vl[1]);
    paper.project.activeLayer.addChild(ln);
  }

}

function generateDrops(nDrops, minWidth, maxWidth, minDist, angle) {
  let paths = [];
  let centers = [];
  let frame = Path.Rectangle(margin, margin, w - 2 * margin, h - 2 * margin);

  while (paths.length < nDrops) {
    let rand1 = rand(), rand2 = rand();
    let center = new Point(margin + (w - 2 * margin) * rand1, margin + (h - 2 * margin) * rand2);
    let tooClose = false;
    centers.forEach(c => {
      if (c.getDistance(center) < minDist) tooClose = true;
    });
    if (tooClose) continue;
    let dropWidth = rand_range(minWidth, maxWidth);
    let pts = genDropPoints(center, dropWidth, 2, angle);
    let outsideFrame = false;
    pts.forEach(p => {
      if (!frame.contains(p)) outsideFrame = true;
    });
    if (outsideFrame) continue;
    paths.push(new Path(pts));
    centers.push(center);
  }
  return paths;
}

/**
 * Generates points of a polyline approximating a raindrop.
 * @param center {paper.Point} Center
 * @param width {Number} Raindrop's width
 * @param elongation {Number} Raindrop height is width * elongation.
 * @param angle Rotation (clockwise), in degrees. 0 for vertical.
 * @returns {Array<paper.Point>} Points of the polyline, closed.
 */
function genDropPoints(center, width, elongation, angle) {
  // Piriform of Longchamps
  // https://math.stackexchange.com/a/51556
  const a = .39, b = elongation / 2;
  let pts = [];
  let nPts = Math.round(width * elongation);
  for (let i = 0; i <= nPts; ++i) {
    let t = i / nPts * 2 * Math.PI;
    let pt = new Point(a * (1 - Math.sin(t)) * Math.cos(t), 0 - b * Math.sin(t));
    pt = pt.multiply(width).rotate(angle).add(center);
    pts.push(pt);
  }
  return pts;
}

class Rect {
  constructor(pos, width, height) {
    /** @type {paper.Point} */
    this.pos = pos;
    /** @type {Number} */
    this.w = width;
    /** @type {Number} */
    this.h = height;
  }

  split() {
    const minSide = 100;
    if (this.w < minSide || this.h < minSide) return [this];
    let ratio = 0.5 + (rand() - 0.5) * 0.5;
    // Split into left+right
    if (this.w > this.h) {
      let newW = Math.round(this.w * ratio);
      return [
        new Rect(this.pos, newW, this.h),
        new Rect(new Point(this.pos.x + newW, this.pos.y), this.w - newW, this.h)
      ];
    }
    // Split top-bottom
    else {
      let newH = Math.round((this.h * ratio));
      return [
        new Rect(this.pos, this.w, newH),
        new Rect(new Point(this.pos.x, this.pos.y + newH), this.w, this.h - newH)
      ];
    }
  }

  inset(bleed) {
    let top = this.pos.y;
    let left = this.pos.x;
    let right = this.pos.x + this.w;
    let bottom = this.pos.y + this.h;
    if (top != margin) top += bleed;
    if (bottom != h - margin) bottom -= bleed;
    if (left != margin) left += bleed;
    if (right != w - margin) right -= bleed;
    this.pos = new Point(left, top);
    this.w = right - left;
    this.h = bottom - top;
  }
}