Source code of plot #032 back to plot
Download full working sketch as 032.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, dbgRedraw} from "./utils/boilerplate.js";
import {mulberry32, rand, rand_range, randn_bm, setRandomGenerator} from "./utils/random.js"
import {shuffle} from "./utils/shuffle.js"
import {CanvasMasker} from "./utils/canvas-masker.js";
import {SvgFont} from "./utils/svg-font.js";
const pw = 2100; // Paper width
const ph = 1480; // Paper height
const w = 1480; // Drawing width
const h = 1050; // Drawing height
const margin = 50; // Margin (within drawing)
const rf = 5; // Occlusion canvas is this many times larger than our area
const segLen = 2;
const title = "KEPLER'S JOURNAL";
const year = 1604;
const day = Math.floor(1 + 365 * Math.random());
let seed = hashCode(getRomanStr(year, day));
//seed = 44642;
let font;
setSketch(function () {
setRandomGenerator(mulberry32(seed));
info("Seed: " + seed);
init(w, h, pw, ph);
let reqFont = new XMLHttpRequest();
reqFont.open("GET", "data/fonts_BubblerOne.svg", true);
// reqFont.open("GET", "data/fonts_EMS_EMSReadability.svg", true);
reqFont.send();
reqFont.onload = () => {
font = new SvgFont(reqFont.responseText);
draw();
}
});
const ArcTypes = {
RadialRandom: "RadialRandom",
RadialUniform: "RadialUniform",
RadialFM: "RadialFM",
CrossHatchMoire: "CrossHatchMoire",
ArcSine: "ArcSine",
Cogwheels: "Cogwheels",
};
const ComplementTypes = {
Notches: "Notches",
Circles: "Circles",
Spiral: "Spiral",
};
const ComplementDirection = {
Left: "Left",
Right: "Right",
};
const NotchPosition = {
In: "In",
Mid: "Mid",
Out: "Out",
};
function draw() {
paper.project.currentStyle.strokeColor = "black";
paper.project.currentStyle.strokeWidth = 2;
let center = new Point(w / 2, h / 2);
// Meta parameters for run
const params = {
nArcs: Math.floor(rand_range(4, 8)),
nComplements: Math.floor(2 + 2 * rand()),
radialUniformGapRange: [8, 20],
radialRandomAvgGapRange: [6, 20],
spiralProb: 0.25, // Probability that that the sketch has a spiraling circles complement
notchMaxGap: 10,
fontSize: 38,
}
// The arcs we'll be building.
let arcs = [];
// Dividers on boths sides. These will define from/to angles for arcs.
let rDivs = calcSideDivs(params.nArcs);
let lDivs = calcSideDivs(params.nArcs);
// Get start and end angles from dividers
// Random pair for each band, but pairs cannot be next to each other
let divAngles = [];
for (const yDiv of rDivs) {
let vec = new Point(w - 2.5 * margin - w / 2, h / 2 - yDiv);
divAngles.push(Math.PI * (90 - vec.angle) / 180);
}
for (const yDiv of lDivs) {
let vec = new Point(2.5 * margin - w / 2, h / 2 - yDiv);
divAngles.push(Math.PI * (90 - vec.angle) / 180);
}
// Generate arcs: for now, only the outline geometry, which is determined by the side divs
let arcWidth = (h / 2 - margin) / (params.nArcs + 1);
for (let ix = 0; ix < params.nArcs; ++ix) {
let [startAngle, endAngle] = getAnglePair(divAngles);
let arcLen = endAngle - startAngle;
let radIn = (ix + 1) * arcWidth;
let radOut = (ix + 2) * arcWidth;
let arc = new Arc(center, radIn, radOut, startAngle, startAngle + arcLen);
arcs.push(arc);
}
// Disturb arc widths
for (let ix = 0; ix < arcs.length - 1; ++ix) {
let diff = randn_bm(-arcWidth, arcWidth);
if (arcs.length < 5 && Math.abs(diff) < arcWidth / 3) diff *= 1.5;
// Change this arc's width; distribute difference equally among remaining arcs outwards
let n = arcs.length - 1 - ix;
for (let j = ix; j < arcs.length - 1; ++j) {
let d = arcs.length - 1 - j;
let valHere = diff * d / n;
arcs[j].radOut += valHere;
arcs[j+1].radIn += valHere;
}
}
// Generate outlines
arcs.forEach(arc => arc.genOutline());
// Decide what patterns and complements to use
calcPatternsComplements(arcs, params);
let cm = new CanvasMasker(w, h, rf/*, document.querySelector(".mid"), "canvas-calc"*/);
// If we have a spiral complement, draw it now.
// This will block stuff left and right.
let spiralIx = arcs.findIndex(a => a.complement == ComplementTypes.Spiral);
let spiralQuadrant = -1; // 0: TL, 1: TR, 2: BR, 3: BL
if (spiralIx != -1) {
let arc = arcs[spiralIx];
// let arcAngleLen = arc.endAngle - arc.startAngle;
// let circStartAngle = arc.endAngle - arcAngleLen / 3;
let arcCircles = genCircleSpiral(arc);
for (const ac of arcCircles) {
project.activeLayer.addChild(ac.path);
cm.blockCircle(ac.center, ac.radius);
}
// Which quadrant does spiral end? Might have to move title to upper half.
let lc = arcCircles[arcCircles.length-1].center;
if (lc.x < w / 2) spiralQuadrant = lc.y < h / 2 ? 0 : 3;
else spiralQuadrant = lc.y < h / 2 ? 1 : 2;
}
// Draw arcs: outline, fillers, complements
for (let i = 0; i < arcs.length; ++i) {
let arc = arcs[i];
if (arc.type == ArcTypes.RadialRandom) {
arc.genRadialFillersRandom();
}
else if (arc.type == ArcTypes.RadialUniform) {
arc.genRadialFillers();
}
else if (arc.type == ArcTypes.RadialFM) {
arc.genRadialFillersFM(8, 5, Math.floor(arc.radOut * (arc.endAngle - arc.startAngle) / 200) + 1);
}
else if (arc.type == ArcTypes.CrossHatchMoire) {
arc.genCrossHatchFillers();
}
else if (arc.type == ArcTypes.Cogwheels) {
arc.genCogwheelFillers();
}
else if (arc.type == ArcTypes.ArcSine) {
arc.genArcFillersSine(12, 20, 4);
}
if (arc.complement == ComplementTypes.Circles) {
let arcAngleLen = arc.endAngle - arc.startAngle;
let arcCircles = genCircleArc(arc);
for (let i = 0; i < arcCircles.length; ++i) {
let ac = arcCircles[i];
let eclipsePhase = 0;
if (arc.complementParams.eclipse) {
eclipsePhase = arcCircles.length - i - 1;
if (eclipsePhase > 6) eclipsePhase = 0;
}
let pts = genCirclePts(ac.center, ac.radius, eclipsePhase);
drawMaskedPath(pts, cm);
cm.blockPoly(pts);
//cm.blockCircle(ac.center, ac.radius);
}
}
else if (arc.complement == ComplementTypes.Notches) {
let angleFrom = drawNotchComplement(arc, cm);
// Create an "arc" matching the notched decoration
// The outline path of this is used to block division rays
let notchesArc = new Arc(center, arc.radIn + 2, arc.radOut - 2, angleFrom + 0.01, angleFrom + arc.complementParams.angleLen - 0.01);
notchesArc.genOutline();
cm.blockPoly(notchesArc.outlinePts);
}
if (arc.type != ArcTypes.ArcSine && arc.type != ArcTypes.Cogwheels)
drawMaskedPath(arc.outlinePts, cm);
for (const pts of arc.fillers){
drawMaskedPath(pts, cm);
}
if (arc.type == ArcTypes.Cogwheels) {
// First two polylines are the cogs on the arc itself
// Those are open paths, adding them would block entire arc area
for (let fIx = 2; fIx < arc.fillers.length; ++fIx) {
cm.blockPoly(arc.fillers[fIx]);
}
}
// For blocking, we create a slightly smaller outline
// Otherwise an artifact appears and neighboring outline gets sliced into small chunks
if (arc.type != ArcTypes.Cogwheels) {
let arcBlocker = new Arc(arc.center, arc.radIn + 1, arc.radOut - 1, arc.startAngle + 0.01, arc.endAngle - 0.01);
arcBlocker.genOutline();
cm.blockPoly(arcBlocker.outlinePts);
}
}
let blAngle;
const sideCircleSizes = [15, 30];
for (let i = 0; i < rDivs.length; ++i) {
let yDiv = rDivs[i];
let rSideCircle = rand_range(...sideCircleSizes);
if (i > 0 && (yDiv - rDivs[i-1]) / 2 < rSideCircle) rSideCircle = (yDiv - rDivs[i-1]) / 2;
if (i < rDivs.length - 1 && (rDivs[i+1] - yDiv) / 2 < rSideCircle) rSideCircle = (rDivs[i+1] - yDiv) / 2;
let ln = Path.Line(w - 2.5 * margin, yDiv, w - 1.5 * margin - rSideCircle, yDiv);
project.activeLayer.addChild(ln);
let linePts = [new Point(w / 2, h / 2), new Point(w - 2.5 * margin, yDiv)];
drawMaskedLine(linePts[0], linePts[1], cm);
drawSideCircle(yDiv, rSideCircle, true);
}
for (let i = 0; i < lDivs.length; ++i) {
let yDiv = lDivs[i];
let rSideCircle = rand_range(...sideCircleSizes);
if (i > 0 && (yDiv - lDivs[i-1]) / 2 < rSideCircle) rSideCircle = (yDiv - lDivs[i-1]) / 2;
if (i < lDivs.length - 1 && (lDivs[i+1] - yDiv) / 2 < rSideCircle) rSideCircle = (lDivs[i+1] - yDiv) / 2;
let ln = Path.Line(1.5 * margin + rSideCircle, yDiv, 2.5 * margin, yDiv);
project.activeLayer.addChild(ln);
let linePts = [new Point(w / 2, h / 2), new Point(2.5 * margin, yDiv)];
if (i == rDivs.length - 1) blAngle = (linePts[1].subtract(linePts[0])).angle;
drawMaskedLine(linePts[0], linePts[1], cm);
drawSideCircle(yDiv, rSideCircle, false);
}
drawTitle(params, spiralQuadrant, blAngle, arcs[arcs.length-1].radOut);
}
function drawTitle(params, spiralQuadrant, lAngle, rad) {
// Bottom left or top left
if (spiralQuadrant != 3) {
let [gTitle1, angleLen1] = font.writeOnArc(params.fontSize, rad + params.fontSize + 10, title, true);
gTitle1.rotate(lAngle - 92, new Point(0, 0));
gTitle1.translate(new Point(w / 2, h / 2));
project.activeLayer.addChild(gTitle1);
}
else {
let [gTitle1, angleLen1] = font.writeOnArc(params.fontSize, rad + 10, title, false);
gTitle1.rotate(lAngle + 162, new Point(0, 0));
gTitle1.translate(new Point(w / 2, h / 2));
project.activeLayer.addChild(gTitle1);
}
// Bottom right or top right
if (spiralQuadrant != 2) {
let [gTitle2, angleLen2] = font.writeOnArc(params.fontSize, rad + params.fontSize + 10, getRomanStr(year, day), true);
gTitle2.rotate(92 - lAngle + angleLen2, new Point(0, 0));
gTitle2.translate(new Point(w / 2, h / 2));
project.activeLayer.addChild(gTitle2);
}
else {
let [gTitle2, angleLen2] = font.writeOnArc(params.fontSize, rad + 10, getRomanStr(year, day), false);
gTitle2.rotate(199 - lAngle - angleLen2, new Point(0, 0));
gTitle2.translate(new Point(w / 2, h / 2));
project.activeLayer.addChild(gTitle2);
}
}
function calcSideDivs(nDivs) {
while (true) {
let ys = [];
let usefulH = h - 4 * margin;
for (let i = 0; i < nDivs; ++i) {
let y = 2 * margin + i / (nDivs - 1) * usefulH;
if (i > 0 && i < nDivs - 1) y += rand_range(-usefulH / nDivs, usefulH / nDivs);
ys.push(y);
}
let minDist = Number.MAX_VALUE;
for (let i = 1; i < nDivs; ++i) if (ys[i] - ys[i - 1] < minDist) minDist = ys[i] - ys[i - 1];
if (minDist < usefulH / nDivs / 4) continue;
return ys;
}
}
function calcPatternsComplements(arcs, params) {
// Random-pick type for each arc
// Verify that constraints are met
while (true) {
fillArcTypes();
if (checkArcTypeConstraints()) break;
}
// Fill in type-specific params
for (const arc of arcs) {
if (arc.type == ArcTypes.RadialUniform) {
arc.typeParams.midGap = Math.floor(rand_range(...params.radialUniformGapRange));
}
else if (arc.type == ArcTypes.RadialRandom) {
arc.typeParams.avgMidGap = Math.floor(rand_range(...params.radialRandomAvgGapRange));
}
else if (arc.type == ArcTypes.CrossHatchMoire) {
arc.typeParams.gap = 9;
arc.typeParams.dualAngle = rand_range(3, 8);
arc.typeParams.parallelMoire = rand() < 0.5;
}
}
// Fill complements, enforcing constraints
while (true) {
arcs.forEach(arc => {
arc.complement = null;
arc.complementParams = {};
});
fillComplements();
if (checkComplementConstraints()) break;
}
function checkComplementConstraints() {
if (arcs.length >= 5) return true;
let hasNotches = false;
let hasEclipse = false;
for (const arc of arcs) {
if (arc.complement == ComplementTypes.Notches) hasNotches = true;
if (arc.complement == ComplementTypes.Circles && arc.complementParams.eclipse) hasEclipse = true;
}
return hasNotches || hasEclipse;
}
function fillComplements() {
// Decide if we have a spiral complement
let gotSpiral = rand() < params.spiralProb;
if (gotSpiral) {
while (true) {
// Must start within inner 2/3s of arcs
// Must not be next to cogwheels
let arcIx = rand_range(0, Math.floor(2 * arcs.length / 3));
arcIx = Math.floor(arcIx);
if (arcs[arcIx].type == ArcTypes.Cogwheels) continue;
arcs[arcIx].complement = ComplementTypes.Spiral;
break;
}
}
// Decide where remaining complements will go, and what kinds
let complementCount = gotSpiral ? 1 : 0;
let nTries = 42;
while (complementCount < params.nComplements) {
let type = complementCount % 2 == 0 ? ComplementTypes.Circles : ComplementTypes.Notches;
let arcIx = -1;
let tries = 0;
while (tries < nTries) {
++tries;
arcIx = Math.floor((arcs.length) * rand());
// Already got a complement
if (arcs[arcIx].complement != null) continue;
// We add complements in the outer rings, not two innermost ones
if (arcIx < arcs.length / 3) continue;
// Outermost arc should not get notches
if (type == ComplementTypes.Notches && arcIx == arcs.length - 1) continue;
// Cogwheels should not get a complement
if (arcs[arcIx].type == ArcTypes.Cogwheels) continue;
// We good here
break;
}
if (tries == nTries) break;
arcs[arcIx].complement = type;
++complementCount;
}
// Fill in complement-specific params
let outermostEclipseArc = null;
for (const arc of arcs) {
if (arc.complement == ComplementTypes.Circles) {
let cw = rand() < 0.5;
let radius = rand_range(13, (arc.radOut - arc.radIn) / 3);
if (radius < 13) radius = 13;
let arcAngleLen = arc.endAngle - arc.startAngle;
let fromAngle, toAngle;
if (cw) {
fromAngle = rand_range(arc.startAngle + 0.5 * arcAngleLen, arc.startAngle + 0.8 * arcAngleLen);
toAngle = arc.endAngle + Math.PI * (0.5 + 0.5 * rand());
} else {
fromAngle = rand_range(arc.startAngle + 0.5 * arcAngleLen, arc.startAngle + 0.2 * arcAngleLen);
toAngle = arc.startAngle - Math.PI * (0.5 + 0.5 * rand());
}
let travel = Math.abs(toAngle - fromAngle) * (arc.radIn + arc.radOut) / 2;
let dist = radius * rand_range(4, 6);
let nCircles = Math.round(travel / dist);
arc.complementParams.nCircles = nCircles;
arc.complementParams.radius = radius;
arc.complementParams.fromAngle = fromAngle;
arc.complementParams.toAngle = toAngle;
arc.complementParams.eclipse = nCircles > 7 && radius >= 16;
if (arc.complementParams.eclipse) outermostEclipseArc = arc;
} else if (arc.complement == ComplementTypes.Spiral) {
let cw = rand() < 0.5;
let arcAngleLen = arc.endAngle - arc.startAngle;
let circStartAngle = cw ? arc.endAngle - arcAngleLen / 3 : arc.startAngle + arcAngleLen / 3;
arc.complementParams.rArcStart = (arc.radIn + arc.radOut) * 0.5;
arc.complementParams.fromAngle = circStartAngle;
arc.complementParams.toAngle = cw ? circStartAngle + Math.PI * 0.8 : circStartAngle - Math.PI * 0.8;
arc.complementParams.nCirclesInArcLen = 15;
arc.complementParams.rCircleMid = margin / 4;
} else if (arc.complement == ComplementTypes.Notches) {
let posVal = Math.floor(3 * rand());
if (posVal == 0) arc.complementParams.notchPos = NotchPosition.In;
else if (posVal == 1) arc.complementParams.notchPos = NotchPosition.Mid;
else arc.complementParams.notchPos = NotchPosition.Out;
arc.complementParams.startAngle = arc.endAngle;
let angleLen = 2 * Math.PI - (arc.endAngle - arc.startAngle);
while (angleLen < 0) angleLen += 2 * Math.PI;
while (angleLen > Math.PI * 2) angleLen -= 2 * Math.PI;
// Adjacent to main arc pattern
if (angleLen < Math.PI * 1.1) arc.complementParams.angleLen = angleLen;
// Complements arc pattern symmetrically: same swipe as arc itself
else {
angleLen = arc.endAngle - arc.startAngle;
angleLen = Math.min(angleLen, Math.PI * 0.4);
arc.complementParams.angleLen = angleLen;
}
// Aim for a small notch gap of 0.7
let r = (arc.radIn + arc.radOut) / 2;
let len = angleLen * r;
let notchCount = Math.round(len / params.notchMaxGap);
arc.complementParams.primaryDiv = rand() < 0.5 ? 6 : 8;
let sdVals = [2, 3];
shuffle(sdVals);
arc.complementParams.secondaryDiv = sdVals[0];
let ps = arc.complementParams.primaryDiv * arc.complementParams.secondaryDiv;
while (true) {
if (notchCount / ps == Math.floor(notchCount / ps)) break;
++notchCount;
}
arc.complementParams.tertiaryDiv = notchCount / ps;
}
}
// Only one arc may get an eclipse
for (const arc of arcs) {
if (arc.complementParams && arc.complementParams.eclipse && arc != outermostEclipseArc)
arc.complementParams.eclipse = false;
}
}
function checkArcTypeConstraints() {
const minArcWidthCogwheels = h * 0.06;
let hasCogwheels = false;
let ok = true;
for (let i = 0; i < arcs.length; ++i) {
// Make note of cogwheels and notches
if (arcs[i].type == ArcTypes.Cogwheels) hasCogwheels = true;
// Cogwheel must not be too narrow
if (arcs[i].type == ArcTypes.Cogwheels && arcs[i].radOut - arcs[i].radIn < minArcWidthCogwheels) ok = false;
// Neighboring patterns must be different
if (i > 0 && arcs[i-1].type == arcs[i].type) ok = false;
// Cogwheels and ArcSine are not adjacent
if (arcs[i].type == ArcTypes.ArcSine) {
if (i > 0 && arcs[i-1].type == ArcTypes.Cogwheels) ok = false;
if (i < arcs.length - 1 && arcs[i+1].type == ArcTypes.Cogwheels) ok = false;
}
}
if (arcs.length < 5 && !hasCogwheels) ok = false;
return ok;
}
function fillArcTypes() {
// What arc will have what pattern?
// Scramble in batches, so we reduce unwanted repetitions
let patternBatch = [];
for (const arcType in ArcTypes) patternBatch.push(arcType);
let arcTypes = [];
while (true) {
shuffle(patternBatch);
for (let i = 0; i < patternBatch.length && arcTypes.length < params.nArcs; ++i) {
arcTypes.push(patternBatch[i]);
}
if (arcTypes.length == params.nArcs) break;
}
arcTypes.forEach((t, ix) => arcs[ix].type = t);
}
}
function drawSideCircle(y, r, onRight) {
let arcLen = rand_range(Math.PI * 0.5, Math.PI * 1.8);
let joinAt = rand_range(0.2, 0.8);
let angleStart = -arcLen * joinAt;
if (onRight) angleStart -= Math.PI / 2;
else angleStart += Math.PI / 2;
let nSegs = Math.round(r * arcLen / segLen);
let pts = [];
let center = new Point(onRight ? (w - 1.5 * margin) : (1.5 * margin), y);
for (let i = 0; i <= nSegs; ++i) {
let angle = angleStart + i / nSegs * arcLen;
let pt = new Point(Math.sin(angle), -Math.cos(angle));
pt = pt.multiply(r);
pt = pt.add(center);
pts.push(pt);
}
drawPath(pts, false);
let innerCircle = Path.Circle(center, r - 7);
project.activeLayer.addChild(innerCircle);
}
function genCircleArc(arc) {
let cp = arc.complementParams;
let rArc = (arc.radIn + arc.radOut) / 2;
let res = [];
for (let i = 0; i < cp.nCircles; ++i) {
let angle = cp.fromAngle + (cp.toAngle - cp.fromAngle) * i / cp.nCircles;
let cc = new Point(Math.sin(angle), -Math.cos(angle)).multiply(rArc);
cc.x += w / 2;
cc.y += h / 2;
let circle = Path.Circle(cc, cp.radius);
res.push({
path: circle,
center: cc.clone(),
radius: cp.radius,
});
}
return res;
}
function genCircleSpiral(arc) {
let cp = arc.complementParams;
let res = [];
let angle = cp.fromAngle;
let angleStep = (cp.toAngle - cp.fromAngle) / cp.nCirclesInArcLen;
let rArc = cp.rArcStart;
let rCircle = 0;
let rGrowth = h / 500;
while (true) {
let cc = new Point(Math.sin(angle), -Math.cos(angle)).multiply(rArc);
cc.x += w / 2;
cc.y += h / 2;
if (cc.y - rCircle < margin) break;
if (cc.y + rCircle > h - margin) break;
if (cc.x - rCircle < 3 * margin) break;
if (cc.x + rCircle > w - 3 * margin) break;
if (!pastToAngle(angle)) {
let arcProp = (angle - cp.fromAngle) / (cp.toAngle - cp.fromAngle);
rCircle = 5 + (cp.rCircleMid - 5) * arcProp;
}
let circle = Path.Circle(cc, rCircle);
res.push({
path: circle,
center: cc.clone(),
radius: rCircle,
});
angle += angleStep;
if (pastToAngle(angle)) {
rArc = rArc + rGrowth;
rGrowth *= 1.1;
rCircle = rCircle * 1.2;
}
}
return res;
function pastToAngle(angle) {
if (cp.toAngle > cp.fromAngle) return angle > cp.toAngle;
else return angle < cp.toAngle;
}
}
function drawNotchComplement(arc, masker) {
let center = new Point(w / 2, h / 2);
let nSmallNotches = arc.complementParams.primaryDiv *
arc.complementParams.secondaryDiv *
arc.complementParams.tertiaryDiv;
let breadth = arc.radOut - arc.radIn;
breadth = Math.min(h * 0.07, breadth);
let angleFrom = arc.endAngle;
angleFrom += Math.PI - (arc.endAngle - arc.startAngle + arc.complementParams.angleLen) / 2;
for (let i = 0; i <= nSmallNotches; ++i) {
let length = breadth * 0.4;
if (arc.complementParams.notchPos == NotchPosition.Mid) length = breadth * 0.25;
if (i % (arc.complementParams.primaryDiv) == 0) length = breadth * 0.6;
if (i % (arc.complementParams.primaryDiv * arc.complementParams.secondaryDiv) == 0) length = breadth * 0.9;
let angle = angleFrom + arc.complementParams.angleLen * i / nSmallNotches;
drawNotch(angle, length);
}
return angleFrom;
function drawNotch(angle, length) {
let pt1 = new Point(Math.sin(angle), -Math.cos(angle));
let pt2;
if (arc.complementParams.notchPos == NotchPosition.Out) {
pt1.length = arc.radOut;
pt2 = pt1.clone();
pt2.length -= length;
}
else if (arc.complementParams.notchPos == NotchPosition.In) {
pt1.length = arc.radIn;
pt2 = pt1.clone();
pt2.length += length;
}
else {
pt1.length = (arc.radIn + arc.radOut) / 2;
pt2 = pt1.clone();
pt1.length -= length / 2;
pt2.length += length / 2;
}
pt1 = pt1.add(center);
pt2 = pt2.add(center);
drawMaskedLine(pt1, pt2, masker);
}
}
function getAnglePair(angleArr) {
let ix1, ix2;
while (true) {
ix1 = Math.floor(rand() * angleArr.length / 2);
if (angleArr[ix1] != 0) break;
}
while (true) {
ix2 = angleArr.length / 2 + Math.floor(rand() * angleArr.length / 2);
if (angleArr[ix2] != 0) break;
}
let res = [angleArr[ix1], angleArr[ix2]];
if (rand() < 0.5) res = [res[1], res[0]];
angleArr[ix1] = angleArr[ix2] = 0;
if (res[1] < res[0]) res[1] += 2 * Math.PI;
return res;
}
function angleDiff(angle1, angle2) {
while (angle1 > Math.PI * 2) angle1 -= Math.PI * 2;
while (angle2 > Math.PI * 2) angle2 -= Math.PI * 2;
return Math.abs(angle2 - angle1);
}
class Arc {
constructor(center, radIn, radOut, startAngle, endAngle) {
this.center = center;
this.radIn = radIn;
this.radOut = radOut;
this.startAngle = startAngle; // Zero is top; grows clockwise
this.endAngle = endAngle;
this.type = null;
this.typeParams = {};
this.complement = null;
this.complementParams = {};
this.outlinePts = [];
this.outlinePath = null;
this.fillers = [];
}
genOutline() {
let ptsInner = this.genArcPoints(this.radIn, this.startAngle, this.endAngle);
let ptsOuter = this.genArcPoints(this.radOut, this.endAngle, this.startAngle);
let innerLast = ptsInner[ptsInner.length - 1];
let outerFirst = ptsOuter[0];
let outerLast = ptsOuter[ptsOuter.length - 1];
let innerFirst = ptsInner[0];
let nSideSegs = Math.round((this.radOut - this.radIn) / segLen);
this.outlinePts = [];
this.outlinePts = this.outlinePts.concat(ptsInner);
for (let i = 1; i < nSideSegs; ++i)
this.outlinePts.push(innerLast.add(outerFirst.subtract(innerLast).multiply(i / nSideSegs)));
this.outlinePts = this.outlinePts.concat(ptsOuter);
for (let i = 1; i <= nSideSegs; ++i)
this.outlinePts.push(outerLast.add(innerFirst.subtract(outerLast).multiply(i / nSideSegs)));
this.outlinePath = new Path({segments: this.outlinePts});
}
genCrossHatchFillers() {
this.fillers = [];
let cm = new CanvasMasker(w, h, rf);
cm.includePoly(this.outlinePts);
cm.takeSnapshot();
let midAngle = (this.endAngle + this.startAngle) / 2 * 180 / Math.PI;
if (!this.typeParams.parallelMoire) midAngle += 90;
let angle1 = midAngle - this.typeParams.dualAngle / 2;
let angle2 = midAngle + this.typeParams.dualAngle / 2;
while (angle1 < -90) angle1 += 180;
while (angle1 > 90) angle1 -= 180;
while (angle2 < -90) angle2 += 180;
while (angle2 > 90) angle2 -= 180;
let lines = genHatchLines(this.outlinePath, angle1, this.typeParams.gap);
lines = lines.concat(genHatchLines(this.outlinePath, angle2, this.typeParams.gap));
let maskedLines = [];
for (const [pt1, pt2] of lines) {
let mlns = cm.getMaskedLine(pt1, pt2, true, segLen);
for (const [pta, ptb] of mlns) maskedLines.push([pta, ptb]);
}
this.fillers = maskedLines.map(ln => makeSegs(...ln));
function makeSegs(pt1, pt2) {
let vec = pt2.subtract(pt1);
let nSegs = Math.round(vec.length / segLen);
if (nSegs < 2) return [pt1, pt2];
let pts = [];
for (let i = 0; i <= nSegs; ++i)
pts.push(pt1.add(vec.multiply(i / nSegs)));
return pts;
}
}
genArcFillersSine(gap, waveLenApprox, amplitude) {
this.fillers = [];
let arcAngle = this.endAngle - this.startAngle;
let radMid = (this.radIn + this.radOut) / 2;
let arcMidLen = Math.abs(radMid * arcAngle);
let nPeriods = Math.round(arcMidLen / waveLenApprox);
let radLen = this.radOut - this.radIn;
let nArcs = Math.round((radLen - 2 * amplitude) / gap);
for (let i = 0; i <= nArcs; ++i) {
let pts = [];
let rad = this.radIn + amplitude + (radLen - 2 * amplitude) * i / nArcs;
let arcLen = rad * arcAngle;
let nPoints = Math.round(arcLen / segLen);
for (let j = 0; j <= nPoints; ++j) {
let angle = this.startAngle + j / nPoints * arcAngle;
let pt = new Point(Math.sin(angle), -Math.cos(angle));
pt.length *= rad;
let waveAngle = j / nPoints * 2 * Math.PI * nPeriods;
pt.length += Math.sin(waveAngle) * amplitude;
pt = pt.add(this.center);
pts.push(pt);
}
this.fillers.push(pts);
}
}
genRadialFillersRandom() {
this.fillers = [];
let arcAngle = this.endAngle - this.startAngle;
let radMid = (this.radIn + this.radOut) / 2;
let arcMidLen = Math.abs(radMid * arcAngle);
let nLines = Math.round(arcMidLen / this.typeParams.avgMidGap);
for (let i = 0; i <= nLines; ++i) {
let angle = this.startAngle + rand() * arcAngle;
this.fillers.push(this.genRadialLine(angle));
}
}
genRadialFillersFM(midGap, dev, devPeriods) {
this.fillers = [];
let arcAngle = this.endAngle - this.startAngle;
let radMid = (this.radIn + this.radOut) / 2;
let arcMidLen = Math.abs(radMid * arcAngle);
let nLines = Math.round(arcMidLen / midGap);
let angleStep = arcAngle / nLines;
let cumulativeMod = 0;
for (let i = 0; i <= nLines; ++i) {
let modAngle = 2 * Math.PI * devPeriods * i / nLines - Math.PI * 1.5;
let modVal = Math.sin(modAngle) * angleStep * (dev / midGap);
cumulativeMod += modVal;
let angle = this.startAngle + arcAngle * i / nLines;
angle += cumulativeMod;
if (angle < this.startAngle || angle > this.endAngle) continue;
this.fillers.push(this.genRadialLine(angle));
}
}
genRadialFillers() {
this.fillers = [];
let arcAngle = this.endAngle - this.startAngle;
let radMid = (this.radIn + this.radOut) / 2;
let arcMidLen = Math.abs(radMid * arcAngle);
let nLines = Math.round(arcMidLen / this.typeParams.midGap);
for (let i = 0; i <= nLines; ++i) {
let angle = this.startAngle + i / nLines * arcAngle;
this.fillers.push(this.genRadialLine(angle));
}
}
genRadialLine(angle) {
let nPtsPerLine = Math.round((this.radOut - this.radIn) / segLen);
let pts = [];
let pt1 = new Point(Math.sin(angle), -Math.cos(angle));
let pt2 = pt1.multiply(this.radOut).add(this.center);
pt1 = pt1.multiply(this.radIn).add(this.center);
for (let j = 0; j <= nPtsPerLine; ++j) {
pts.push(pt1.add(pt2.subtract(pt1).multiply(j / nPtsPerLine)));
}
return pts;
}
genCogwheelFillers() {
this.fillers = [];
const rMid = (this.radIn + this.radOut) / 2;
const startAngleDeg = this.startAngle / Math.PI * 180;
const endAngleDeg = this.endAngle / Math.PI * 180;
let wheeler = new Wheeler(this.radIn, this.radOut, 12, startAngleDeg, endAngleDeg);
this.fillers.push(wheeler.innerPts);
this.fillers.push(wheeler.outerPts);
let wheelSizes = [];
let teethOpts = [7, 9, 12, 15, 17, 21, 27];
if (this.radOut - this.radIn > 90) {
teethOpts.shift();
teethOpts.shift();
}
if (this.radOut - this.radIn > 100) teethOpts.shift();
teethOpts.unshift(6);
while (teethOpts.length > 0) {
let teeth = teethOpts.shift();
let dia = wheeler.getWheelDia(teeth);
if (dia > (this.radOut - this.radIn) -10) break;
wheelSizes.push([teeth, dia]);
teeth += 2;
}
let arcAngleRad = 0;
let outer = false;
while (true) {
let lastTeeth = wheelSizes[0][0];
while (true) {
shuffle(wheelSizes);
if (wheelSizes[0][0] != lastTeeth) break;
}
let sz = wheelSizes[0];
let wheelAngle = sz[1] / rMid * Math.PI * 0.45;
if (this.startAngle + arcAngleRad + wheelAngle > this.endAngle) break;
let polys = wheeler.genWheelOnArc(sz[0], (arcAngleRad + wheelAngle / 2) / Math.PI * 180, outer);
for (const pts of polys) {
this.fillers.push(pts);
}
arcAngleRad += wheelAngle;
if (rand() < 0.7) outer = !outer;
}
}
genArcPoints(r, angleFrom, angleTo) {
let arcAngle = angleTo - angleFrom;
let arcLen = Math.abs(r * arcAngle);
let nSegs = Math.round(arcLen / segLen);
let pts = [];
for (let j = 0; j <= nSegs; ++j) {
let angle = angleFrom + arcAngle * j / nSegs;
let pt = new Point(Math.sin(angle), -Math.cos(angle));
pt.length *= r;
pt = pt.add(this.center);
pts.push(pt);
}
return pts;
}
}
function genCirclePts(center, radius, eclipsePhase = 0) {
// No eclipse: just draw a circle
if (eclipsePhase == 0 || eclipsePhase == 4) {
let pts = [];
let cf = 2 * Math.PI * radius;
let nSegs = Math.round(cf / segLen);
let vect = new Point(0, radius);
for (let i = 0; i <= nSegs; ++i) {
let angle = i / nSegs * 360;
let pt = center.add(vect.rotate(angle));
pts.push(pt);
}
return pts;
}
// Where is my occluder?
let eRadius = radius * 1.2;
let eCenter = center.clone();
if (eclipsePhase == 1) eCenter.x += radius * 0.5 + eRadius;
else if (eclipsePhase == 2) eCenter.x += eRadius;
else if (eclipsePhase == 3) eCenter.x += eRadius - radius * 0.5;
else if (eclipsePhase == 5) eCenter.x -= eRadius - radius * 0.5;
else if (eclipsePhase == 6) eCenter.x -= eRadius;
else if (eclipsePhase == 7) eCenter.x -= eRadius + radius * 0.5;
// y-diff of intersection
let cp = Path.Circle(center, radius);
let ecp = Path.Circle(eCenter, eRadius);
// project.activeLayer.addChild(cp);
// project.activeLayer.addChild(ecp);
let isects = cp.getIntersections(ecp);
let isectH = Math.abs(center.y - isects[0].point.y);
let topIsectPt = new Point(isects[0].point.x, center.y - isectH);
// Angle on circle, and on occluder, above horizontal line
let cAngle = -topIsectPt.subtract(center).angle * Math.PI / 180;
let eAngle = eCenter.subtract(topIsectPt).angle * Math.PI / 180;
let pts1, pts2;
if (eCenter.x > center.x)
pts1 = genArcPts(center, radius, cAngle, -cAngle);
else
pts1 = genArcPts(center, radius, -cAngle, cAngle);
if (eCenter.x > center.x)
pts2 = genArcPts(eCenter, eRadius, Math.PI - eAngle, Math.PI + eAngle);
else
pts2 = genArcPts(eCenter, eRadius, Math.PI + eAngle, Math.PI - eAngle);
return pts1.concat(pts2.reverse());
function genArcPts(center, radius, fromAngle, toAngle) {
fromAngle = fromAngle * 180 / Math.PI;
toAngle = toAngle * 180 / Math.PI;
if (toAngle < fromAngle) toAngle += 360;
let pts = [];
let len = radius * (toAngle - fromAngle);
let nSegs = Math.round(len / segLen);
if (nSegs < 2) nSegs = 2;
let vect = new Point(radius, 0);
for (let i = 0; i <= nSegs; ++i) {
let angle = fromAngle + i / nSegs * (toAngle - fromAngle);
let pt = center.add(vect.rotate(angle));
pts.push(pt);
}
return pts;
}
}
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 drawPath(pts, closed) {
let path = new Path({ segments: pts, closed });
paper.project.activeLayer.addChild(path);
}
function drawMaskedPath(pts, cm, mustBeMarkedVisible = false) {
let visiblePaths = cm.getMaskedPoly(pts, mustBeMarkedVisible);
for (const pts of visiblePaths) {
const vp = Path.Line({segments: pts});
paper.project.activeLayer.addChild(vp);
}
}
function drawMaskedLine(pt1, pt2, cm, mustBeMarkedVisible = false) {
const maskedLines = cm.getMaskedLine(pt1, pt2, mustBeMarkedVisible);
for (const vl of maskedLines) {
const ln = Path.Line(vl[0], vl[1]);
paper.project.activeLayer.addChild(ln);
}
}
class Wheeler {
constructor(arcRadInner, arcRadOuter, pitch, startAngle, endAngle) {
this.pitch = pitch;
this.elev = pitch * 0.25;
this.center = new Point(w / 2, h / 2);
this.startAngle = startAngle;
this.endAngle = endAngle;
this.mechRadInner = arcRadInner + this.elev;
this.mechRadOuter = arcRadOuter - this.elev;
this.innerPts = this.genCoggedArcPoints(false);
this.outerPts = this.genCoggedArcPoints(true);
}
genCoggedArcPoints(outer) {
let arcAngle = this.endAngle - this.startAngle;
let rMech = outer ? this.mechRadOuter : this.mechRadInner;
let spoke = new Point(0, -rMech);
let arcLen = Math.abs(rMech * arcAngle / 180 * Math.PI);
let nTeeth = arcLen / this.pitch;
let angleTooth = arcAngle / nTeeth;
let pts = [];
for (let i = 0; i < nTeeth; ++i) {
let angleL = i / nTeeth * arcAngle - angleTooth * 0.25;
angleL += this.startAngle;
let toothPts = this.genToothPoints(spoke, angleL, angleTooth);
pts.push(...toothPts);
}
pts.forEach(pt => {
pt.x += this.center.x;
pt.y += this.center.y;
});
return pts;
}
genToothPoints(spoke, angleL, angleTooth) {
let angleX = angleTooth / 10;
let angleC = angleL + angleTooth * 0.5;
let angleR = angleL + angleTooth;
let pt1 = spoke.rotate(angleL);
let pt2 = spoke.rotate(angleL + angleX);
pt2.length += this.elev;
let pt3 = spoke.rotate(angleC - angleX);
pt3.length += this.elev;
let pt4 = spoke.rotate(angleC + angleX);
pt4.length -= this.elev;
let pt5 = spoke.rotate(angleR - angleX);
pt5.length -= this.elev;
let pt6 = spoke.rotate(angleR);
let pts = [];
pts.push(...this.getLinePts(pt1, pt2));
pts.pop();
pts.push(...this.getLinePts(pt2, pt3));
pts.pop();
pts.push(...this.getLinePts(pt3, pt4));
pts.pop();
pts.push(...this.getLinePts(pt4, pt5));
pts.pop();
pts.push(...this.getLinePts(pt5, pt6));
return pts;
}
getWheelDia(nTeeth) {
let cfMid = this.pitch * nTeeth;
let rMech = cfMid / 2 / Math.PI;
return 2 * (rMech + this.elev);
}
genCogwheel(nTeeth, rotAngle) {
let cfMid = this.pitch * nTeeth;
let rMech = cfMid / 2 / Math.PI;
let pts = [];
let spoke = new Point(0, -rMech);
let angleTooth = 360 / nTeeth;
for (let i = 0; i < nTeeth; ++i) {
let angleL = i / nTeeth * 360 - angleTooth * 0.25 + rotAngle;
let toothPts = this.genToothPoints(spoke, angleL, angleTooth);
pts.push(...toothPts);
}
return [rMech, pts];
}
genWheelOnArc(nTeeth, arcAngle, outer) {
// This is our actual result: multiple polylines
let polys = [];
// The cogwheel, rotated by the right amount so teeth meet
let mechRad = outer ? this.mechRadOuter : this.mechRadInner;
let arcTravel = 2 * mechRad * Math.PI * arcAngle / 360;
let wheelCircf = this.pitch * nTeeth;
let wheelTurns = arcTravel / wheelCircf;
let rot = 360 * wheelTurns;
if (outer) rot = -rot;
let [rad, edgePts] = this.genCogwheel(nTeeth, rot + arcAngle + this.startAngle);
polys.push(edgePts);
// The hub
let hubPts = genCirclePts(new Point(0, 0), 4);
polys.push(hubPts);
let ofs;
if (outer) {
let spoke = new Point(0, -this.mechRadOuter);
ofs = spoke.rotate(this.startAngle + arcAngle);
ofs.length -= rad;
}
else {
let spoke = new Point(0, -this.mechRadInner);
ofs = spoke.rotate(this.startAngle + arcAngle);
ofs.length += rad;
}
ofs = ofs.add(this.center);
// Offset all polylines. We've been working around origin so far.
for (const pts of polys) {
pts.forEach(pt => {
pt.x += ofs.x;
pt.y += ofs.y;
});
}
return polys;
}
getLinePts(pt1, pt2) {
const lineVect = pt2.subtract(pt1);
const lineLength = lineVect.length;
const nSegs = Math.max(2, Math.round(lineLength / segLen));
const segVect = lineVect.divide(nSegs);
const pts = [];
for (let i = 0; i <= nSegs; ++i) {
pts.push(pt1.add(segVect.multiply(i)));
}
return pts;
}
}
function getRomanStr(year, day) {
return romanize(year) + " " + romanize(day);
function romanize(num) {
if (!+num) return undefined;
let digits = String(+num).split('');
let key = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM',
'', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC',
'', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'];
let roman = '', i = 3;
while (i--) roman = (key[+digits.pop() + (i * 10)] || '') + roman;
return Array(+digits.join('') + 1).join('M') + roman;
}
}
function hashCode(str) {
let hash = 0, i, chr;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
hash = hash % 65536;
while (hash < 0) hash += 65536;
return hash;
}