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