1// The pencil drawings' shared kit, for the scripts that draw in SVG (the dissertation's opening, detour map, stories, 2// vignettes and afterword, the spike and risk pages, the solver's and the mesh's deep dives): elements, a seeded random 3// stream, easing, and pencil lines that bow the same way for the same seed on every visit. Loaded before the page's 4// own script, which takes what it needs from window.Sketch. 5window.Sketch = (() => { 6 "use strict"; 7 const NS = "http://www.w3.org/2000/svg"; 8 const el = (tag, attrs, parent) => { const n = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs || {})) n.setAttribute(k, v); if (parent) parent.appendChild(n); return n; }; 9 // a small seeded generator (mulberry32), so a drawing is the same on every visit; and a standard normal from it 10 function mulberry32(a) { return function () { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } 11 function gauss(r) { let u = 0; while (u === 0) u = r(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * r()); } 12 const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x)); 13 const ease = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2); 14 const seg = (t, a, b) => ease(clamp((t - a) / (b - a))); // 0 before a, 1 after b 15 const fade = (n, t) => { n.style.opacity = clamp(t); }; 16 // A one-shot motion: once the step's progress passes `at` it plays over `dur` seconds whatever the scroll speed, and 17 // runs back if the reader scrolls above `at` again. With reduced motion it jumps to where it is headed. step(t, dt) 18 // returns 0..1, not eased. 19 function oneShot(at, dur, reduced) { 20 let v = 0, to = 0; 21 return { 22 step(t, dt) { to = t >= at ? 1 : 0; v = reduced ? to : v + clamp(to - v, -dt / dur, dt / dur); return v; }, 23 get moving() { return v !== to; }, 24 get playing() { return to === 1 && v < 1; }, // on its way forward: worth finishing before the drawing is put away 25 jump(t) { to = v = t >= at ? 1 : 0; }, // reached from below: already where the progress puts it 26 }; 27 } 28 // a pencil line through points: each segment bowed by up to wob either way, deterministically, so it reads as drawn 29 function pencil(pts, seed, wob) { 30 const r = mulberry32(seed); 31 let d = `M${pts[0][0].toFixed(1)},${pts[0][1].toFixed(1)}`; 32 for (let i = 1; i < pts.length; ++i) { const [x0, y0] = pts[i - 1], [x1, y1] = pts[i]; d += ` Q${((x0 + x1) / 2 + (r() - 0.5) * wob * 2).toFixed(1)},${((y0 + y1) / 2 + (r() - 0.5) * wob * 2).toFixed(1)} ${x1.toFixed(1)},${y1.toFixed(1)}`; } 33 return d; 34 } 35 // a path drawn on by the progress: set(t) uncovers it from 0 to 1 36 function stroke(g, d, cls, width = 1.6) { 37 const a = el("path", { d, class: "pencil " + (cls || ""), "stroke-width": width }, g); 38 const len = a.getTotalLength() || 1; 39 a.style.strokeDasharray = `${len} ${len}`; a.style.strokeDashoffset = len; 40 return { a, set(t) { a.style.strokeDashoffset = len * (1 - clamp(t)); } }; 41 } 42 // the same line drawn twice, the second faint and a hair off (dx, dy), as a pencil goes over its own stroke 43 function stroke2(g, d, cls, w, base, dx, dy) { 44 const a = el("path", { d, class: base + " " + (cls || ""), "stroke-width": w }, g); 45 const b = el("path", { d, class: base + " soft " + (cls || ""), "stroke-width": w * 0.6, transform: `translate(${dx},${dy})` }, g); 46 const len = a.getTotalLength(); 47 for (const p of [a, b]) { p.style.strokeDasharray = `${len} ${len}`; p.style.strokeDashoffset = len; } 48 return { a, b, len, set(t) { const o = len * (1 - clamp(t)); a.style.strokeDashoffset = o; b.style.strokeDashoffset = o; } }; 49 } 50 const line = (g, x1, y1, x2, y2, cls, w = 1) => el("line", { x1, y1, x2, y2, class: "pencil " + (cls || ""), "stroke-width": w }, g); 51 const poly = (pts) => "M" + pts.map((p) => p[0].toFixed(1) + "," + p[1].toFixed(1)).join(" L"); 52 const text = (g, x, y, s, cls, anchor = "middle") => { const t = el("text", { x, y, class: cls || "", "text-anchor": anchor }, g); t.textContent = s; return t; }; 53 // The reading position in a scroll-told story: the step whose middle is nearest the reading line (the page's 54 // window.readLine() where it sets one, else 55% down the window), and its progress, from 0 as its top reaches the 55 // line to 1 once 70% of it has passed, so its drawing builds while the paragraph is read. null without steps. 56 function reading(steps) { 57 const vh = window.innerHeight, line = window.readLine ? window.readLine() : vh * 0.55; 58 let best = null, bestD = Infinity; 59 for (const s of steps) { const r = s.getBoundingClientRect(), d = Math.abs(r.top + r.height / 2 - line); if (d < bestD) { bestD = d; best = s; } } 60 if (!best) return null; 61 const r = best.getBoundingClientRect(); 62 return { step: best, prog: clamp((line - r.top) / (r.height * 0.7)) }; 63 } 64 // a progress line's width: how far down the page the reader is 65 const progressBar = (bar) => { if (bar) { const h = document.documentElement.scrollHeight - window.innerHeight; bar.style.width = (h > 0 ? (100 * window.scrollY) / h : 0) + "%"; } }; 66 // the drawing follows the reading progress no faster than pace a second: a flick of the wheel is caught up over a 67 // moment, not skipped 68 const follow = (shown, prog, dt, pace) => (Math.abs(prog - shown) <= pace * dt ? prog : shown + Math.sign(prog - shown) * pace * dt); 69 // On a phone the drawings' labels are set larger (the page's stylesheet). A label that would then run past the 70 // drawing's edge, 600 units wide, is shrunk back until it fits, measured from where it is anchored. 71 function fitLabels(svg) { 72 const phone = window.matchMedia("(max-width: 820px)"); 73 let fitQueued = false;
74 function fit() { 75 fitQueued = false; 76 const mode = phone.matches ? "phone" : "wide"; 77 for (const t of svg.querySelectorAll("text")) { 78 const key = mode + t.textContent; 79 if (t.fitKey === key) continue; // measured already, for this text at this width 80 t.fitKey = key; t.style.fontSize = ""; 81 if (mode !== "phone" || !t.textContent) continue; 82 const b = t.getBBox(), x = +t.getAttribute("x") || 0, a = t.getAttribute("text-anchor") || "start"; 83 const room = a === "middle" ? 2 * Math.min(x, 600 - x) : a === "end" ? x : 600 - x; 84 if (b.width > room && room > 0) t.style.fontSize = (parseFloat(getComputedStyle(t).fontSize) * room / b.width).toFixed(2) + "px"; 85 } 86 } 87 const queueFit = () => { if (!fitQueued) { fitQueued = true; requestAnimationFrame(fit); } }; 88 new MutationObserver(queueFit).observe(svg, { childList: true, subtree: true, characterData: true }); 89 phone.addEventListener("change", queueFit); 90 } 91 return { NS, el, mulberry32, gauss, clamp, ease, seg, fade, oneShot, pencil, stroke, stroke2, line, poly, text, reading, progressBar, follow, fitLabels }; 92})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.