Source code of plot #031 back to plot

Download full working sketch as 031.tar.gz.
Unzip, then start a local web server and load the page in a browser.

///<reference path="../pub/lib/paper.d.ts" />
import {init, info, loadLib, setSketch, dbgRedraw} from "./utils/boilerplate.js";
import {getMaskedLine, getMaskedPoly} from "./utils/geo.js"
import {rand, setRandomGenerator, mulberry32, rand_range, shuffle} from "./utils/random.js"

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

const nSticks = 30;
const nFrames = 5;
const stickWidth = [15, 30];
const sunLineGap = 7;
const layerColors = ["black", "orange"]

let seed = Math.round(Math.random() * 65535);
//seed = 14309; // Uncomment with fixed seed to reproduce a concrete output

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

function draw() {
  paper.project.addLayer(new paper.Layer({ name: "1-color" }));
  addAlignmentMarks([0, 1]);

  paper.project.layers[0].activate();
  paper.project.currentStyle.strokeColor = layerColors[0];
  paper.project.currentStyle.strokeWidth = 2;

  // Generate polylines that fill a circle
  const circleLinePts = genSunPaths(sunLineGap);

  // Generate random sticks
  let sticks = [];
  for (let i = 0; i < nSticks; ++i)
    sticks.push(genStick());

  // Frames are sticks!
  const frameWidth = (w - (nFrames + 1) * margin) / nFrames;
  const frameSticks = [];
  for (let i = 0; i < nFrames; ++i) {
    const frameStick = new Stick(
      new Point((i + 1) * margin + i * frameWidth + frameWidth / 2, h / 2),
      90, frameWidth);
    frameStick.isFrame = true;
    frameStick.outline = new Path({
      segments: frameStick.getOutlinePts(),
      closed: true,
    });
    sticks.push(frameStick);
    frameSticks.push(frameStick);
    paper.project.activeLayer.addChild(frameStick.outline.clone());
  }

  let sunBlockers = []
  for (let i = 0; i < frameSticks.length; ++i) {
    sticks = shuffleSticks(sticks);
    drawSticksInFrame(sticks, frameSticks[i].outline);
    sunBlockers = [...sunBlockers, ...getSunBlockers(sticks, frameSticks[i])]
  }

  paper.project.layers[1].activate();
  paper.project.currentStyle.strokeColor = layerColors[1];
  let frameUnion = frameSticks[0].outline;
  for (let i = 1; i < frameSticks.length; ++i)
    frameUnion = frameUnion.unite(frameSticks[i].outline);
  drawSun(circleLinePts, sunBlockers, frameUnion);
}

function drawSun(polyLines, blockers, frame) {
  for (const pl of polyLines) {
    const visiblePaths = getMaskedPoly(pl, blockers, [frame])
    for (const pathPts of visiblePaths) {
      const path = new paper.Path(pathPts);
      paper.project.activeLayer.addChild(path);
    }
  }
}

function getSunBlockers(sticks, frameStick) {
  const blockers = [];
  // Find index of frame stick
  let ix;
  for (let i = 0; i < sticks.length; ++i) {
    if (sticks[i] == frameStick) {
      ix = i;
      break;
    }
  }
  // Everything above is a blocker
  for (let i = ix + 1; i < sticks.length; ++i) {
    if (sticks[i].isFrame) continue;
    blockers.push(sticks[i].outline.intersect(frameStick.outline));
  }
  return blockers;
}

function drawSticksInFrame(sticks, frame) {

  for (let i = 0; i < sticks.length; ++i) {

    // The stick I'm drawing
    const stick = sticks[i];

    // We draw frames separately - they are *also* the frames
    if (stick.isFrame) continue;

    // My blockers: everyone above me
    const blockers = [];
    for (let j = i + 1; j < sticks.length; ++j) {
      blockers.push(sticks[j].outline);
    }

    const outlinePts = stick.getOutlinePts();
    const maskedLinesA = getMaskedLine(outlinePts[0], outlinePts[1], blockers, [frame]);
    drawLines(maskedLinesA);
    const maskedLinesB = getMaskedLine(outlinePts[2], outlinePts[3], blockers, [frame]);
    drawLines(maskedLinesB);
  }
}

function genSunPaths(gap) {

  const nPeriods = 3;
  const step = 2;
  const ptArrs = [];
  const r = h * 0.3 + rand() * h * 0.2;
  const r2 = r * r;
  const ampl = r / nPeriods / 3;
  for (let ly = -r - ampl; ly <= r + ampl; ly += gap) {
      let ptArr = [];
      for (let x = -r; x <= r; x += step) {
        const t = x / r * nPeriods * Math.PI;
        const y = ly - ampl * Math.sin(t);
        if (x * x + y * y <= r2) {
          ptArr.push(new Point(x, y));
          continue;
        }
        if (ptArr.length > 1) ptArrs.push(ptArr);
        ptArr = [];
      }
      if (ptArr.length > 1) ptArrs.push(ptArr);
  }
  const cx = margin + w * 0.15 + rand() * h * 0.7;
  const cy = margin + h * 0.15 + rand() * h * 0.7;
  const center = new Point(cx, cy);
  const rot = rand_range(-30, 30);
  for (const ptArr of ptArrs) {
    for (let i = 0; i < ptArr.length; ++i) {
      ptArr[i] = ptArr[i].rotate(rot).add(center);
    }
  }
  return ptArrs;
}

// Shuffle sticks for different blocking order;
//   re-insert frame sticks in middle region
// Framesticks too high or too low create unpleasant extremes
//   too empty or too crowded in a given frame
function shuffleSticks(sticks) {
  const frameSticks = sticks.filter(s => s.isFrame);
  sticks = sticks.filter(s => !s.isFrame);
  shuffle(sticks);
  for (const ws of frameSticks) {
    const ix = Math.round(rand_range(sticks.length * 0.3, sticks.length * 0.9));
    sticks = [...sticks.slice(0, ix), ws, ...sticks.slice(ix)];
  }
  return sticks;
}

// Generate a random stick
function genStick() {
  let center = new Point(
    rand_range(2 * margin, w - 2 * margin),
    rand_range(2 * margin, h - 2 * margin));
  let angle = rand_range(0, 360);
  let breadth = rand_range(stickWidth[0], stickWidth[1]);
  let stick = new Stick(center, angle, breadth);
  stick.outline = new Path({
    segments: stick.getOutlinePts(),
    closed: true,
  });
  return stick;
}

// One stick
class Stick {
  constructor(center, angle, breadth) {
    this.center = center;
    this.angle = angle;
    this.breadth = breadth;
    this.isFrame = false;
  }

  getOutlinePts() {
    if (!this.isFrame) {
      let dir = new Point(1, 0).rotate(this.angle);
      let orto = dir.rotate(90);
      let halfBreadth = this.breadth / 2;
      let len = Math.sqrt(w * w + h * h);
      return [
        this.center.add(orto.multiply(halfBreadth)).subtract(dir.multiply(len)),
        this.center.add(orto.multiply(halfBreadth)).add(dir.multiply(len)),
        this.center.subtract(orto.multiply(halfBreadth)).add(dir.multiply(len)),
        this.center.subtract(orto.multiply(halfBreadth)).subtract(dir.multiply(len)),
      ];
    }
    else {
      return [
        new Point(this.center.x + this.breadth / 2, margin),
        new Point(this.center.x + this.breadth / 2, h - margin),
        new Point(this.center.x - this.breadth / 2, h - margin),
        new Point(this.center.x - this.breadth / 2, margin),
      ];
    }
  }
}

function drawLines(lines) {
  for (const vl of lines) {
    const ln = Path.Line(vl[0], vl[1]);
    paper.project.activeLayer.addChild(ln);
  }
}

function addAlignmentMarks(layers) {
  for (const lix of layers) {
    paper.project.layers[lix].activate();
    paper.project.currentStyle.strokeColor = layerColors[lix];
    drawMark(w - margin / 2, h - margin / 2);
    drawMark(w - margin / 2, h - margin);
    drawMark(w - margin / 2, h - margin * 1.5);
  }
  function drawMark(x, y) {
    let ln = Path.Line(x - margin / 4, y, x + margin / 4, y);
    paper.project.activeLayer.addChild(ln);
    ln = Path.Line(x, y - margin / 4, x, y + margin / 4);
    paper.project.activeLayer.addChild(ln);
  }
}