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.
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 {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);
}
}