Source code of plot #034 back to plot
Download full working sketch as 034.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, shuffle} from "./utils/random.js"
import {lnPtDist} 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 faceH = h;
let faceW = w * 0.7;
let sideW = faceW * 0.2;
let nDiags = 7;
let insetLen = 20;
let longitudinalHatchGap = 7;
let higherExtraWidth = 15;
// Pre-calculated from parameters
let faceLeft = (w - (faceW+sideW)) / 2;
let faceTop = (h - faceH) / 2;
let faceRight = faceLeft + faceW;
// Toggle: draw squiggly lines (slow, so we're turning it off while tuning the geomtry)
let squiggly = true;
let cmVisible = false;
let rf = 5; // Occlusion canvas is this many times larger than our area
let cm; // Canvas masker
let noise;
let simplex;
let seed = Math.round(Math.random() * 65535);
// seed = 16819;
let noiseSeed = seed;
setSketch(function () {
setRandomGenerator(mulberry32(seed));
simplex = new SimplexNoise(noiseSeed);
info("Seed: " + seed);
init(w, h, pw, ph);
if (!cmVisible) cm = new CanvasMasker(w, h, rf);
else cm = new CanvasMasker(w, h, rf, document.querySelector("#drawing"), "canvas-calc");
noise = genBlueNoise(32, 32);
noise.forEach((val, ix) => noise[ix] = Math.pow((val - noise.length / 2) / noise.length, 0.5) * 3);
draw();
});
let Faces = {
Front: "Front",
Side: "Side",
Top: "Top",
Bottom: "Bottom",
};
function draw() {
paper.project.currentStyle.strokeColor = "black";
paper.project.currentStyle.strokeWidth = 2;
// Generate diagonals, shuffle them
let diags = genDiags(nDiags);
shuffle(diags);
// Center vertically, and extend z-higher ones horizontally
let minY = h, maxY = 0;
diags.forEach(diag => {
diag.pts.forEach(pt => {
minY = Math.min(minY, pt.y);
maxY = Math.max(maxY, pt.y);
});
});
let yOfs = h / 2 - (minY + maxY) / 2;
for (let i = 0; i < diags.length; ++i)
diags[i].updateGeo(diags.length / 2 - i, yOfs);
// Draw and fill... all in the smart order for correct occlusion
for (let i = 0; i < diags.length; ++i) drawFrontAndSide(diags, i);
for (let i = 0; i < diags.length; ++i) calcTopBottomSegs(diags, i);
let tbFillables = [];
for (let i = 0; i < diags.length; ++i)
tbFillables.push(...drawTopsBottoms(diags, i));
fillTopsBottoms(diags, tbFillables);
}
function calcTopBottomSegs(diags, ix) {
let diag = diags[ix];
let vec = new Point(sideW, 0);
cm.clear();
blockFaces(diags, ix, Faces.Front);
blockFaces(diags, -1, Faces.Side);
if (diag.sinkingTop) {
let visibleLines = cm.getMaskedLine(...diag.sinkingTop);
for (const vl of visibleLines) {
if (vl[1].subtract(vl[0]).length < 5) continue;
diag.sinkingTopSegs.push(vl);
}
}
if (diag.risingBottom) {
let visibleLines = cm.getMaskedLine(...diag.risingBottom);
for (const vl of visibleLines) {
if (vl[1].subtract(vl[0]).length < 5) continue;
diag.risingBottomSegs.push(vl);
}
}
}
function drawTopsBottoms(diags, ix) {
let fillables = [];
let diag = diags[ix];
let vec = new Point(sideW, 0);
cm.clear();
blockFaces(diags, ix, Faces.Front);
blockFaces(diags, -1, Faces.Side);
blockFaces(diags, -1, Faces.Bottom);
blockFaces(diags, ix, Faces.Top);
for (const seg of diag.sinkingTopSegs) {
let pts = [
seg[0],
seg[0].add(vec),
seg[1].add(vec),
seg[1],
];
for (let i = 0; i < pts.length; ++i) {
let pt1 = pts[i];
let pt2 = pts[(i+1)%pts.length];
let vlines = cm.getMaskedLine(pt1, pt2);
for (const vl of vlines) {
if (squiggly) drawOutlineSegment(...vl, false);
else project.activeLayer.addChild(Path.Line(...vl));
}
}
fillables.push({ix, pts, top: true});
}
cm.clear();
blockFaces(diags, ix, Faces.Front);
blockFaces(diags, -1, Faces.Side);
blockFaces(diags, -1, Faces.Top);
blockFaces(diags, ix, Faces.Bottom);
for (const seg of diag.risingBottomSegs) {
let pts = [
seg[0],
seg[0].add(vec),
seg[1].add(vec),
seg[1],
];
for (let i = 0; i < pts.length; ++i) {
let pt1 = pts[i];
let pt2 = pts[(i+1)%pts.length];
let vlines = cm.getMaskedLine(pt1, pt2);
for (const vl of vlines) {
if (squiggly) drawOutlineSegment(...vl, false);
else project.activeLayer.addChild(Path.Line(...vl));
}
}
fillables.push({ix, pts, top: false});
}
return fillables;
}
function fillTopsBottoms(diags, fillables) {
cm.clear();
blockFaces(diags, -1, Faces.Front);
blockFaces(diags, -1, Faces.Side);
fillables.sort((a, b) => b.pts[0].x - a.pts[0].x);
for (const f of fillables) {
cm.includePoly(f.pts);
if (f.top) fillTop(f.pts);
else fillBottom(f.pts, diags[f.ix].getBottomGap());
cm.blockPoly(f.pts);
}
}
function drawFrontAndSide(diags, ix) {
let diag = diags[ix];
let vec = new Point(sideW, 0);
// Lower indexes: face and side block me
cm.clear();
for (let i = 0; i < ix; ++i) {
let pts = diags[i].pts;
cm.blockPoly(pts);
cm.blockPoly([pts[0], pts[0].add(vec), pts[1].add(vec), pts[1]]);
}
// Outline of front
for (let pass = 0; pass < 2; ++pass) {
for (let i = 0; i < diag.pts.length; ++i) {
let pt1 = diag.pts[i];
let pt2 = diag.pts[diag.ixmod(i + 1)];
let visibleLines = cm.getMaskedLine(pt1, pt2);
for (const vl of visibleLines) {
if (squiggly) drawOutlineSegment(...vl, (pass % 2) == 0);
else project.activeLayer.addChild(Path.Line(...vl));
}
}
}
// Outline of side
let vlines = [];
vlines.push(...cm.getMaskedLine(diag.pts[0], diag.pts[0].add(vec)));
vlines.push(...cm.getMaskedLine(diag.pts[0].add(vec), diag.pts[1].add(vec)));
vlines.push(...cm.getMaskedLine(diag.pts[1].add(vec), diag.pts[1]));
for (let pass = 0; pass < 2; ++pass) {
for (const vl of vlines) {
if (squiggly) drawOutlineSegment(...vl, (pass % 2) == 0);
else project.activeLayer.addChild(Path.Line(...vl));
}
}
// Fill side
fillSide(diag);
// Fill front
cm.includePoly(diag.pts);
fillVertNoise(diag.pts);
}
function drawOutlineSegment(pt1, pt2, reverse) {
let squiggleLen = 10;
let bgain = 0.7;
let sgain = 0.8;
if (squiggly) drawSquigglyLine([pt1, pt2], reverse, squiggleLen, noise, bgain, sgain, 0, null);
else {
let ln = Path.Line(pt1, pt2);
project.activeLayer.addChild(ln);
}
}
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 blockFaces(diags, excludeIx, face) {
let vec = new Point(sideW, 0);
for (let ix = 0; ix < diags.length; ++ix) {
if (ix == excludeIx) continue;
let d = diags[ix];
if (face == Faces.Front) cm.blockPoly(d.pts);
else if (face == Faces.Side) {
cm.blockPoly([d.pts[0], d.pts[0].add(vec), d.pts[1].add(vec), d.pts[1]]);
}
else if (face == Faces.Top) {
for (const seg of d.sinkingTopSegs) {
cm.blockPoly([seg[0], seg[0].add(vec), seg[1].add(vec), seg[1]]);
}
}
else if (face == Faces.Bottom) {
for (const seg of d.risingBottomSegs) {
cm.blockPoly([seg[0], seg[0].add(vec), seg[1].add(vec), seg[1]]);
}
}
}
}
function genDiags(nDiags) {
let res = [];
let leftYs = [], rightYs = [];
let gap = faceH / (nDiags + 1);
for (let i = 0; i < nDiags; ++i) {
let ly = faceTop + gap * (i + 0.5);
ly += rand_range(-1, 1) * gap * 0.3;
leftYs.push(ly);
let ry = faceTop + gap / 2 + gap * (i + 0.5);
ry += rand_range(-1, 1) * gap * 0.3;
rightYs.push(ry);
}
let l2r;
while (!l2r) l2r = getL2R(nDiags);
for (let i = 0; i < l2r.length; ++i) {
let lh, rh;
while (true) {
lh = rand_range(gap * 0.2, gap * 1.5);
rh = rand_range(gap * 0.2, gap * 1);
let ratio = lh / rh;
if (ratio < 1) ratio = 1 / ratio;
if (ratio > 1.5) break;
}
res.push(new Diag([
new Point(faceRight, rightYs[l2r[i]] - rh / 2),
new Point(faceRight, rightYs[l2r[i]] + rh / 2),
new Point(faceLeft, leftYs[i] + lh / 2),
new Point(faceLeft, leftYs[i] - lh / 2),
]));
}
return res;
}
function getL2R(nDiags) {
let rightTaken = new Set();
let l2r = [];
for (let i = 0; i < nDiags; ++i) l2r.push(-1);
let ix = 0;
let iters = 0, maxIters = nDiags * 5;
while (rightTaken.size < nDiags && iters < maxIters) {
matchFromLeft(ix);
++ix;
}
if (iters != maxIters) return l2r;
return null;
function matchFromLeft(ix) {
let opts = [];
if (ix - 3 >= 0) opts.push(ix - 3);
if (ix - 2 >= 0) opts.push(ix - 2);
if (ix - 1 >= 0) opts.push(ix - 1);
if (ix + 3 < nDiags) opts.push(ix + 3);
if (ix + 2 < nDiags) opts.push(ix + 2);
if (ix + 1 < nDiags) opts.push(ix + 1);
let to;
while (iters < maxIters) {
to = rand_select(opts);
if (!rightTaken.has(to)) break;
++iters;
}
l2r[ix] = to;
rightTaken.add(to);
}
}
class Diag {
constructor(pts) {
this.pts = pts;
this.path = null;
this.sinkingTop = null;
this.risingBottom = null;
this.sinkingTopSegs = [];
this.risingBottomSegs = [];
this.init(pts);
}
init(pts) {
this.pts = pts;
this.path = new Path({segments: pts, closed: true});
this.sinkingTop = null;
this.risingBottom = null;
for (let i = 0; i < this.pts.length; ++i) {
let pt1 = this.pts[i];
let pt2 = this.pts[this.ixmod(i + 1)];
if (pt2.x > pt1.x && pt2.y > pt1.y) this.sinkingTop = [pt1, pt2];
if (pt2.x < pt1.x && pt2.y > pt1.y) this.risingBottom = [pt1, pt2];
}
this.sinkingTopSegs = [];
this.risingBottomSegs = [];
}
updateGeo(horizExtension, yOfs) {
let pts = [];
this.pts.forEach(pt => pts.push(pt.clone()));
pts[0].x += horizExtension * higherExtraWidth;
pts[1].x += horizExtension * higherExtraWidth;
pts[2].x -= horizExtension * higherExtraWidth;
pts[3].x -= horizExtension * higherExtraWidth;
pts.forEach(pt => pt.y += yOfs);
this.init(pts);
}
ixmod(ix) {
while (ix < 0) ix += this.pts.length;
return ix % this.pts.length;
}
getBottomGap() {
// Zero is horizontal; the steeper, the more negative
let angle = this.pts[0].subtract(this.pts[3]).angle;
angle = -angle;
let gap = 3.5 + angle * 0.2;
return Math.max(3.5, gap);
}
}
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;
}
function fillVertNoise(pts) {
let squiggleLen = 10;
let bgain = 0.6;
let sgain = 4;
let targetDarkness = rand_range(0.5, 2);
let hSpacing = rand_range(6, 12);
let vSpacing = 8;
let stickLen = vSpacing;
let lenVariability = 0.3;
let ditherThreshold = (1 / targetDarkness) * (15-hSpacing) * 0.05;
// The path to be filled
let remaining = new Path({segments: pts, closed: true});
let rx = remaining.bounds.left;
let ry = remaining.bounds.top;
let rw = remaining.bounds.width;
let rh = remaining.bounds.height;
let nsz = 64;
let bn_ints = genBlueNoise(nsz, nsz);
let bn = [];
bn_ints.forEach(val => bn.push(val / (nsz **2)));
let nHoriz = Math.round(rw / hSpacing);
let nVert = Math.round(rh / vSpacing);
let topLeft = new Point(rx, ry);
for (let hix = 0; hix < nHoriz; ++hix) {
let linesForColumn = [];
for (let vix = 0; vix < nVert; ++vix) {
let noiseX = hix % nsz;
let noiseY = vix % nsz;
let noiseVal = bn[noiseY * 64 + noiseX];
if (noiseVal < ditherThreshold) continue;
let cx = hSpacing * (hix + 0.5);
let cy = vSpacing * (vix + 0.5);
cy += rand_range(-vSpacing / 2, vSpacing / 2) * lenVariability;
let pt = new Point(cx, cy).add(topLeft);
let len = stickLen + rand_range(-stickLen, stickLen) * lenVariability;
let vec = new Point(0, len / 2);
linesForColumn.push([pt.subtract(vec), pt.add(vec)]);
}
drawSimplifiedLinesForColumns(linesForColumn, (hix%2)==0);
}
function drawSimplifiedLinesForColumns(lines, reverse) {
if (lines.length == 0) return;
let sofs = lines[0].x;
let minGap = 3;
// Join overlapping ones
let sarr = [lines[0]];
for (let i = 1; i < lines.length; ++i) {
let lastLn = sarr[sarr.length - 1];
let ln = lines[i];
// Starts after previous line
if (ln[0].y > lastLn[1].y + minGap) {
sarr.push(ln);
continue;
}
// Overlaps with previous line, and ends after
if (ln[1].y > lastLn[1].y) lastLn[1].y = ln[1].y;
}
// Draw
for (const ln of sarr) {
let visibleLines = cm.getMaskedLine(ln[0], ln[1], true);
for (const vl of visibleLines) {
if (squiggly) drawSquigglyLine(vl, reverse, squiggleLen, noise, bgain, sgain, sofs, null);
else project.activeLayer.addChild(Path.Line(...vl));
}
}
}
}
function fillSide(diag) {
let innerHatchGap = 9;
let squiggleLen = 10;
let bgain = 0.5;
let sgain = 3;
// Hatch around the side only, in case we're tall enough to have a whole inside
// Otherwise, hatch entire right face
let innerRect = [];
if (diag.pts[1].y - diag.pts[0].y > 3 * insetLen) {
cm.includeRect(diag.pts[0].x, diag.pts[0].y, sideW, insetLen);
cm.includeRect(diag.pts[1].x, diag.pts[1].y - insetLen, sideW, insetLen);
cm.includeRect(diag.pts[0].x, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);
cm.includeRect(diag.pts[0].x + sideW - insetLen, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);
innerRect.push(new Point(diag.pts[0].x + insetLen, diag.pts[0].y + insetLen));
innerRect.push(new Point(diag.pts[0].x + sideW - insetLen, diag.pts[0].y + insetLen));
innerRect.push(new Point(diag.pts[1].x + sideW - insetLen, diag.pts[1].y - insetLen));
innerRect.push(new Point(diag.pts[1].x + insetLen, diag.pts[1].y - insetLen));
}
else cm.includeRect(diag.pts[0].x, diag.pts[0].y, sideW, diag.pts[1].y - diag.pts[0].y);
let gap = 5;
let hatchRect = Path.Rectangle(diag.pts[0], new Size(sideW, diag.pts[1].y - diag.pts[0].y));
let nHatches = Math.round(sideW / gap);
let lines = genHatchLines(hatchRect, 0, sideW / nHatches);
for (const [pt1, pt2] of lines) {
let mlns = cm.getMaskedLine(pt1, pt2, true);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
const reverse = i % 2 == 0;
let sofs = pta.x * 0.5;
if (squiggly) drawSquigglyLine([pta, ptb], reverse, squiggleLen, noise, bgain, sgain, sofs, null);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
}
// We're not a tube: done here
if (innerRect.length == 0) return;
// Draw inner outline
for (let pass = 0; pass < 2; ++pass) {
for (let i = 0; i < innerRect.length; ++i) {
let pt1 = innerRect[i];
let pt2 = innerRect[(i + 1) % innerRect.length];
let mlns = cm.getMaskedLine(pt1, pt2);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
if (squiggly) drawOutlineSegment(pta, ptb, (pass % 2) == 0);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
}
}
// Block boundary that we already hatched.
cm.blockRect(diag.pts[0].x, diag.pts[0].y, sideW, insetLen);
cm.blockRect(diag.pts[1].x, diag.pts[1].y - insetLen, sideW, insetLen);
cm.blockRect(diag.pts[0].x, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);
cm.blockRect(diag.pts[0].x + sideW - insetLen, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);
// Explicitly include inner rectangle.
cm.includeRect(diag.pts[0].x + insetLen, diag.pts[0].y + insetLen, sideW - 2 * insetLen, diag.pts[1].y - diag.pts[0].y - 2 * insetLen);
let innerW = innerRect[1].x - innerRect[0].x;
// We've got a rising bottom? Paint the "roof" inside the tube.
let risingTop = [diag.pts[3], diag.pts[0]];
if (risingTop[1].y > risingTop[0].y - 10) risingTop = null; // -10: Let's not draw roof for near-horizontal
if (risingTop) {
let vec = risingTop[0].subtract(risingTop[1]);
vec.length *= innerW / -vec.x;
let blockPts = [innerRect[1], innerRect[1].add(vec), innerRect[0]];
// Corner line from top right inner corner
let ln = [innerRect[1], innerRect[1].add(vec)];
let mlns = cm.getMaskedLine(ln[0], ln[1], true);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
if (squiggly) drawOutlineSegment(pta, ptb, false);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
let gap = diag.getBottomGap();
vec.length *= gap / vec.y;
for (let i = 0; i < (innerRect[2].y - innerRect[1].y) / gap; ++i) {
let pt1 = innerRect[1].add(vec.multiply(i));
let pt2A = pt1.add(new Point(-sideW, 0));
let mlns = cm.getMaskedLine(pt1, pt2A, true);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
if (squiggly) drawOutlineSegment(pta, ptb, false);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
}
cm.blockPoly(blockPts);
}
// We've got a sinking bottom? Paint the "floor" inside the tube.
let sinkingBottom = [diag.pts[1], diag.pts[2]];
if (sinkingBottom[1].y > sinkingBottom[0].y - 10) sinkingBottom = null; // -10: Let's not draw roof for near-horizontal
if (sinkingBottom) {
let vec = sinkingBottom[1].subtract(sinkingBottom[0]);
vec.length *= innerW / -vec.x;
let blockPts = [innerRect[2], innerRect[3], innerRect[2].add(vec)];
// Corner line from bottom right inner corner
let ln = [innerRect[2], innerRect[2].add(vec)];
let mlns = cm.getMaskedLine(ln[0], ln[1], true);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
if (squiggly) drawOutlineSegment(pta, ptb, false);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
let breadth = lnPtDist(ln[0], ln[1], innerRect[3]);
let nLines = Math.round(breadth / longitudinalHatchGap);
for (let i = 0; i < nLines; ++i) {
let pt1 = innerRect[2].subtract(new Point(innerW * i / nLines, 0));
let pt2A = pt1.add(vec);
let mlns = cm.getMaskedLine(pt1, pt2A, true);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
if (squiggly) drawOutlineSegment(pta, ptb, false);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
}
cm.blockPoly(blockPts);
}
// Fill inside rect (whatever is not blocked) with vertical lines
for (let x = innerRect[1].x - innerHatchGap; x > innerRect[0].x; x -= innerHatchGap) {
let pt1 = new Point(x, innerRect[0].y);
let pt2 = new Point(x, innerRect[3].y);
let mlns = cm.getMaskedLine(pt1, pt2, true);
for (let i = 0; i < mlns.length; ++i) {
const [pta, ptb] = mlns[i];
if (squiggly) drawOutlineSegment(pta, ptb, false);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
}
}
function fillTop(pts) {
// Pts are CW from top left
// To calculate breadth, we must extend sinking top into quasi infinity
// Otherwise distance from line segment is larger than distance from line, and we end up with too small a gap
let topLine = pts[3].subtract(pts[0]);
let breadth = lnPtDist(pts[0].add(topLine.multiply(500)), pts[0].subtract(topLine.multiply(500)), pts[1]);
let nLines = Math.ceil(breadth / longitudinalHatchGap);
let vec = new Point(sideW, 0);
for (let i = 1; i < nLines; ++i) {
let pt1 = pts[0].add(vec.multiply(i/nLines));
let pt2 = pts[3].add(vec.multiply(i/nLines));
let mlns = cm.getMaskedLine(pt1, pt2, true);
for (const [pta, ptb] of mlns) {
if (squiggly) drawOutlineSegment(pta, ptb, false);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
}
}
function fillBottom(pts, gap) {
// Pts are CW from top left
let vside = pts[0].subtract(pts[3]);
let nLines = Math.round((pts[2].y - pts[1].y) / gap);
let vec = new Point(sideW, 0);
for (let i = 1; i < nLines; ++i) {
let pt1 = pts[3].add(vside.multiply(i/nLines));
let pt2 = pt1.add(vec);
let mlns = cm.getMaskedLine(pt1, pt2, true);
for (const [pta, ptb] of mlns) {
if (squiggly) drawOutlineSegment(pta, ptb, false);
else project.activeLayer.addChild(Path.Line(pta, ptb));
}
}
}