Source code of plot #033 back to plot
Download full working sketch as 033.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, rand_select, randn_bm, setRandomGenerator} from "./utils/random.js"
import {getMaskedLine, getMaskedPoly} from "./utils/geo.js";
import {CanvasMasker} from "./utils/canvas-masker.js";
import {SimplexNoise} from "./utils/simplex-noise.js";
import {genBlueNoise} from "./utils/blue-noise.js";
const pw = 1480; // Paper width
const ph = 2100; // Paper height
const w = 1050; // Drawing width
const h = 1480; // Drawing height
// Sketch parameters
let nSections = 15;
let faceH = h * 0.8;
let faceW = w * 0.6;
let sideW = faceW * 0.2;
let insetLen = 13;
// Pre-calculated from parameters
let faceLeft = (w - (faceW+sideW)) / 2;
let faceTop = (h - faceH) / 2;
let faceRight = faceLeft + faceW;
let faceBottom = faceTop + faceH;
// Toggle: draw squiggly lines (slow, so we're turning it off while tuning the geomtry)
let squiggly = true;
const rf = 5; // Occlusion canvas is this many times larger than our area
let cm; // Canvas masker
let segLen = 2;
let noise;
let simplex;
let seed = Math.round(Math.random() * 65535);
//seed = 7830;
let noiseSeed = seed;
setSketch(function () {
setRandomGenerator(mulberry32(seed));
simplex = new SimplexNoise(noiseSeed);
info("Seed: " + seed);
init(w, h, pw, ph);
cm = new CanvasMasker(w, h, rf/*, document.querySelector(".mid"), "canvas-calc"*/);
draw();
});
let Faces = {
Front: "Front",
Side: "Side",
Top: "Top",
Bottom: "Bottom",
};
function draw() {
paper.project.currentStyle.strokeColor = "black";
paper.project.currentStyle.strokeWidth = 2;
noise = genBlueNoise(32, 32);
noise.forEach((val, ix) => noise[ix] = Math.pow((val - noise.length / 2) / noise.length, 0.5) * 3);
let wholePts = [
new Point(faceLeft + faceW, faceTop + faceH),
new Point(faceLeft + faceW, faceTop),
new Point(faceLeft, faceTop),
new Point(faceLeft, faceTop + faceH),
];
let whole = new Section(wholePts);
let sections = splitSections([whole], nSections);
// Draw outline of the whole
drawOutline(wholePts);
// Draw hatch fill of face (excluding empty inside of sections)
let sectionInners = [];
for (const sect of sections) sectionInners.push(sect.innerPath);
drawShapeHatchFill(whole.path, sectionInners, null, Faces.Front);
// Draw outline of each section, plus their inner depth
for (const sect of sections) {
drawOutline(sect.innerPts);
drawDepth(sect, faceRight, sideW);
}
}
function drawOutlineSegment(pt1, pt2) {
let squiggleLen = 10;
let bgain = 0.5;
let sgain = 3;
if (squiggly) drawSquigglyLine([pt1, pt2], false, squiggleLen, noise, bgain, sgain, 0, null);
else {
let ln = Path.Line(pt1, pt2);
project.activeLayer.addChild(ln);
}
}
function drawOutline(pts) {
for (let i = 0; i < pts.length; ++i) {
let pt1 = pts[i];
let pt2 = pts[(i+1)%pts.length];
drawOutlineSegment(pt1, pt2);
}
}
function drawDepth(sect, faceRight, sideW) {
let hvec = new Point(sideW, 0);
// Right side of whole edifice
if (Math.abs(sect.pts[0].x - faceRight) < 5) {
let rectPts = [
sect.pts[0],
sect.pts[0].add(hvec),
sect.pts[1].add(hvec),
sect.pts[1],
];
let rect = new Path({
segments: rectPts,
closed: true,
});
drawOutline(rectPts);
drawShapeHatchFill(rect, [], null, Faces.Side);
}
// Inside section
let innerLines = [];
// Iterate all sides
for (let i = 0; i < sect.innerPts.length; ++i) {
let pt1 = sect.innerPts[i];
let pt2 = sect.innerPts[sect.ixmod(i+1)];
let pts = [pt1, pt1.add(hvec), pt2.add(hvec), pt2];
let depthPoly = new Path({
segments: pts,
closed: true,
});
innerLines.push([pts[0], pts[1]]);
innerLines.push([pts[1], pts[2]]);
// Vertical line: side face
if (Math.abs(pt1.x - pt2.x) < 2) {
drawShapeHatchFill(depthPoly, [], sect.innerPath, Faces.Side);
}
// Sinking from right to left
else if (pt2.x < pt1.x && pt2.y > pt1.y) {
let angle = pt2.subtract(pt1).angle;
console.log(angle);
drawShapeHatchFill(depthPoly, [], sect.innerPath, Faces.Bottom, (angle - 90) / 90);
}
}
cm.clear();
cm.reqPosCount = 1;
paintPoly(sect.innerPath, false);
cm.takeSnapshot();
for (const ln of innerLines) {
let mlns = cm.getMaskedLine(ln[0], ln[1], true);
for (const ln of mlns)
drawOutlineSegment(...ln);
}
}
function splitSections(sections, nSections) {
if (sections.length == nSections) return sections;
let largest = null;
for (const sect of sections) {
if (largest == null || sect.area > largest.area)
largest = sect;
}
let splitArr = [];
for (const sect of sections) {
if (sect == largest) splitArr.push(...sect.split());
else splitArr.push(sect);
}
return splitSections(splitArr, nSections);
}
class Section {
constructor(pts) {
this.pts = pts;
this.path = new Path({segments: pts, closed: true});
this.area = Math.abs(this.path.area);
this.innerPath = null;
this.innerPts = [];
[this.innerPath, this.innerPts] = inset(this.path, insetLen);
this.smallerInnerPath = inset(this.path, insetLen + 1)[0];
}
split() {
// Start IX of longest and second longest side in points
// Second longest must not be adjacent to longest
let lix = 0, llen = this.pts[1].subtract(this.pts[0]).length;
for (let i = 1; i < this.pts.length; ++i) {
let len = this.pts[this.ixmod(i+1)].subtract(this.pts[i]).length;
if (len > llen) [lix, llen] = [i, len];
}
let six = 0, slen = 0;
for (let i = 0; i < this.pts.length; ++i) {
// Not adjacent
if (i == this.ixmod(lix - 1) || i == lix || i == this.ixmod(lix + 1)) continue;
let len = this.pts[this.ixmod(i + 1)].subtract(this.pts[i]).length;
if (len > slen) [six, slen] = [i, len];
}
return this.splitBy(lix, six);
}
findSplitPoints(ixa, ixb) {
let pta, ptb;
while (true) {
let pa, pb;
pa = rand_range(0.3, 0.7);
if (pa <= 0.5) pa -= 0.1;
else pa += 0.1;
pb = rand_range(0.3, 0.7);
if (pb <= 0.5) pb -= 0.1;
else pb += 0.1;
pta = this.pts[ixa].add(this.pts[this.ixmod(ixa + 1)].subtract(this.pts[ixa]).multiply(pa));
ptb = this.pts[ixb].add(this.pts[this.ixmod(ixb + 1)].subtract(this.pts[ixb]).multiply(pb));
// We don't want splitters that are too close to horizontal
let angle = Math.round(ptb.subtract(pta).angle);
if (angle < -90) angle += 180;
if (angle > 90) angle -=180;
if (Math.abs(angle) < 10) continue;
// Angle between split side and splitter
// Don't want too sharp angles
let angleA = getAngle(this.pts[this.ixmod(ixa + 1)], this.pts[ixa], pta, ptb);
let angleB = getAngle(this.pts[this.ixmod(ixb + 1)], this.pts[ixb], ptb, pta);
if (angleA < 30 || angleB < 30) continue;
break;
}
return [pta, ptb];
function getAngle(a1, a2, b1, b2) {
let angleA = a2.subtract(a1).angle;
let angleB = b2.subtract(b1).angle;
let angle = angleB - angleA;
while (angle < 0) angle += 180;
while (angle > 180) angle -= 180;
if (angle > 90) angle = 180 - angle;
return angle;
}
}
splitBy(ixa, ixb) {
let [pta, ptb] = this.findSplitPoints(ixa, ixb);
let ptsa = [pta];
let i = ixa + 1;
while (true) {
ptsa.push(this.pts[this.ixmod(i)]);
if (this.ixmod(i) == ixb) break;
++i;
}
ptsa.push(ptb);
let ptsb = [ptb];
i = ixb + 1;
while (true) {
ptsb.push(this.pts[this.ixmod(i)]);
if (this.ixmod(i) == ixa) break;
++i;
}
ptsb.push(pta);
let res = [new Section(ptsa), new Section(ptsb)];
res[0].pts = orderSectPts(res[0].pts);
res[1].pts = orderSectPts(res[1].pts);
return res;
}
ixmod(ix) {
while (ix < 0) ix += this.pts.length;
return ix % this.pts.length;
}
}
// Points go counterclockwise, starting from rightmost lower corner
function orderSectPts(pts) {
// Find lowest rightmost point
let startIx = 0, startPt = pts[0];
for (let i = 1; i < pts.length; ++i) {
let pt = pts[i];
if (pt.x > startPt.x || Math.abs(pt.x - startPt.x) < 2 && pt.y > startPt.y)
[startIx, startPt] = [i, pt];
}
let orderedPts = [];
for (let i = startIx; i < startIx + pts.length; ++i)
orderedPts.push(pts[i%pts.length]);
return orderedPts;
}
// Insets the sides of the polygon by the specified value
// If a side is on the face's perimiter, it is inset by twice the length
function inset(poly, len) {
let cutters = [];
for (let i = 0; i < poly.segments.length; ++i) {
let pt1 = poly.segments[i].point;
let pt2 = poly.segments[(i+1)%poly.segments.length].point;
let vec = pt2.subtract(pt1);
let orto = vec.rotate(90);
orto.length = len;
if (isPerimiter(pt1, pt2)) orto.length = len * 2;
cutters.push(new Path({
segments: [
pt1.subtract(vec).subtract(orto),
pt1.subtract(vec).add(orto),
pt2.add(vec).add(orto),
pt2.add(vec).subtract(orto),
],
closed: true
}));
}
let innerPath = poly;
for (const c of cutters) innerPath = innerPath.subtract(c);
let pts = [];
for (const s of innerPath.segments) pts.push(s.point);
pts.reverse();
let innerPts = orderSectPts(pts);
return [innerPath, innerPts];
}
// Checks if a line segment (side of a section) is on the perimiter of the entire face
function isPerimiter(pt1, pt2) {
// Horizontal?
if (Math.abs(pt1.y - pt2.y) < 2) {
// Top or bottom
if (Math.abs(pt1.y - faceTop) < 5) return true;
if (Math.abs(pt1.y - faceBottom) < 5) return true;
}
// Vertical?
if (Math.abs(pt1.x - pt2.x) < 2) {
// Left or right
if (Math.abs(pt1.x - faceLeft) < 5) return true;
if (Math.abs(pt1.x - faceRight) < 5) return true;
}
// Not on perimiter
return false;
}
function drawShapeHatchFill(path, blockers, posMask, face, shade = 1) {
let gap, squiggleLen, bgain, sgain;
let gapper;
if (face == Faces.Side) {
gap = 10;
squiggleLen = 10;
bgain = 0.5;
sgain = 3;
gapper = {
interruptProb: 0.05,
skips: [2, 2, 2, 2, 3, 4],
};
}
else if (face == Faces.Front) {
gap = 6;
squiggleLen = 10;
bgain = 0.5;
sgain = 3;
}
else if (face == Faces.Bottom) {
gap = 10 - 7 * (shade);
squiggleLen = 10;
bgain = 0.5;
sgain = 3;
}
else return;
let bounds = path.bounds;
let hatchRect, nHatches, lines;
if (face == Faces.Bottom) {
hatchRect = Path.Rectangle(bounds.x, bounds.y - 1, bounds.width, bounds.height - 1);
nHatches = Math.round(bounds.height / gap);
lines = genHatchLines(hatchRect, 90, bounds.height / nHatches);
}
else {
hatchRect = Path.Rectangle(bounds.x - 1, bounds.y, bounds.width - 1, bounds.height);
nHatches = Math.round(bounds.width / gap);
lines = genHatchLines(hatchRect, 0, bounds.width / nHatches);
}
cm.clear();
cm.reqPosCount = posMask ? 2 : 1;
paintPoly(path, false);
if (posMask) paintPoly(posMask, false);
for (const b of blockers) paintPoly(b, true);
cm.takeSnapshot();
for (const [pt1, pt2] of lines) {
let mlns = cm.getMaskedLine(pt1, pt2, true, segLen);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
const reverse = i % 2 == 0;
let sofs;
if (face == Faces.Front) sofs = pta.x;
else if (face == Faces.Side) sofs = pta.x * 0.5;
if (squiggly) {
drawSquigglyLine([pta, ptb], reverse, squiggleLen, noise, bgain, sgain, sofs, gapper);
}
else {
let ln = Path.Line(pta, ptb);
project.activeLayer.addChild(ln);
}
}
}
}
function paintPoly(path, block) {
let separatePaths = [];
let pathPts = [];
if (!path.children) separatePaths.push(path);
else {
for (const c of path.children) separatePaths.push(c);
}
for (const sp of separatePaths) {
sp.segments.forEach(seg => pathPts.push(seg.point));
if (block) cm.blockPoly(pathPts);
else cm.includePoly(pathPts);
}
}
function drawSquigglyLine(ln, reverse, squiggleLen, bnoise, bgain, sgain, sofs, gapper) {
let subDiv = Math.round(squiggleLen / 5);
if (subDiv < 1) subDiv = 1;
let vec = ln[1].subtract(ln[0]);
let len = vec.length;
let nsegs = Math.max(2, Math.round(len / squiggleLen));
nsegs *= subDiv;
let orto = vec.rotate(90);
orto.length = 1;
let noiseOfs = Math.floor(rand() * bnoise.length);
let getSimplex = pt => {
let dist = pt.subtract(ln[1]).length;
let test = new Point(sofs, dist);
return simplex.noise2D(test.x / w * 40, test.y / h * 4) * sgain;
}
let getPt = i => ln[0].add(vec.multiply(i/nsegs));
// First, get simplex bias along path. We'll offset to get 0 average.
let sbias = 0;
for (let i = 0; i <= nsegs; ++i) sbias += getSimplex(getPt(i));
sbias /= (nsegs + 1);
let polys = [];
let pts = [];
for (let i = 0; i < nsegs + subDiv; i += subDiv) {
if (i > nsegs) i = nsegs;
if (gapper && pts.length > 1 && rand() < gapper.interruptProb) {
polys.push(pts);
pts = [];
i -= subDiv;
i += rand_select(gapper.skips);
}
let d = bnoise[(i + noiseOfs) % bnoise.length] * bgain;
let pt = getPt(i);
// Add simplex noise for large-ish deviation
let sn = getSimplex(pt);
pt = pt.add(orto.multiply(sn - sbias));
// Add blue noise for squiggles
pt = pt.add(orto.multiply(d));
pts.push(pt);
}
if (pts.length > 1) polys.push(pts);
if (reverse) polys.reverse();
for (const pts of polys) {
if (reverse) pts.reverse();
let path = new Path({segments: pts});
project.activeLayer.addChild(path);
}
}
function genHatchLines(rect, angle, gap) {
let arad = angle / 180 * Math.PI;
if (angle < -90 || angle > 90) throw "angle must be between -90 and 90";
let lines = [];
// Going left from right
if (Math.abs(angle) < 45) {
let v = rect.bounds.height * Math.tan(arad);
let len = Math.sqrt(v ** 2 + rect.bounds.height ** 2);
let adv = gap / Math.cos(arad);
let vec = new Point(0, -len).rotate(angle);
let start = angle > 0 ? rect.bounds.left - v : rect.bounds.left;
let end = angle > 0 ? rect.bounds.right : rect.bounds.right - v;
for (let x = start; x < end; x += adv) {
let pt = new Point(x, rect.bounds.bottom);
lines.push([pt, pt.add(vec)]);
}
return lines;
}
// Going top to bottom
angle = angle - 90;
if (angle < -90) angle += 180;
arad = angle / 180 * Math.PI;
let v = rect.bounds.width * Math.tan(-arad);
let len = Math.sqrt(v ** 2 + rect.bounds.width ** 2);
let adv = gap / Math.cos(arad);
let vec = new Point(len, 0).rotate(angle);
let start = angle > 0 ? rect.bounds.top + v : rect.bounds.top;
let end = angle > 0 ? rect.bounds.bottom : rect.bounds.bottom + v;
for (let y = start; y < end; y += adv) {
let pt = new Point(rect.bounds.left, y);
lines.push([pt, pt.add(vec)]);
if (lines.length > 200) break;
}
return lines;
}