diff --git a/DESIGN.md b/DESIGN.md index ffe6813..eae2750 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -43,6 +43,8 @@ src/ vdecay.js neutral-decay "V" daughters shock.js pressure/shock disk model instrument.js chamber optics: structural fan lines, chords, wall arcs + media.js physical-artifact & human layer: grease-pencil marks, film + furniture (sprockets/data box/edge text), réseau grid, splice artifacts.js plate damage: scratches, hairs, dust, rings, fingerprints params.js derive a full tasteful parameter set from a seed (archetypes) bubbles.js polyline → bubbles (shared by all renderers) diff --git a/bubble_chamber.html b/bubble_chamber.html index d5ae089..68cb94c 100644 --- a/bubble_chamber.html +++ b/bubble_chamber.html @@ -193,6 +193,10 @@ function buildPanel() { addSelect(grp, 'palette'); addSelect(grp, 'paperTone'); } + if (g.title === 'Media & Hand') { + addToggle(grp, 'filmEdge'); + addToggle(grp, 'splice'); + } } } @@ -468,6 +472,7 @@ const { spawnVDecay } = __require("src/scene/vdecay.js"); const { generateShock } = __require("src/scene/shock.js"); const { generateArtifacts } = __require("src/scene/artifacts.js"); const { generateInstrument } = __require("src/scene/instrument.js"); +const { generateMedia } = __require("src/scene/media.js"); const { cyrb53 } = __require("src/rng.js"); const LABS = ['BEBC · CERN', 'GARGAMELLE · CERN', '2m HBC · CERN', '82" HBC · SLAC', 'MIRABELLE · IHEP']; @@ -601,7 +606,10 @@ function generateScene(params) { const exposure = `${year}.${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}`; const lab = pick(makeRng(params.seed, 'lab'), LABS); - return { tracks, vertex: fgVertex, shock, artifacts, instrument, hash, plate, exposure, lab }; + const out = { tracks, vertex: fgVertex, shock, artifacts, instrument, hash, plate, exposure, lab }; + // physical-artifact & human layer (grease pencil, film furniture, réseau, splice) + out.media = generateMedia(params, out, makeRng(params.seed, 'media')); + return out; } Object.assign(exports, { generateScene }); @@ -1145,6 +1153,121 @@ function generateInstrument(params, rng) { Object.assign(exports, { generateInstrument }); + }; + __reg["src/scene/media.js"] = function (module, exports, __require) { +/* ============================================================ + media.js — the physical-artifact & human layer. + What a real bubble-chamber frame carried beyond the tracks: + • grease-pencil (chinagraph) hand marks — a scanner ringing a + "good event", an arrow, the event number, an angle/ticks; + • film furniture — sprocket holes, a data box (roll/frame/ + date), edge stock printing, a frame rebate; + • a réseau — the precise grid of small crosses imaged for + distortion correction; + • a tape splice — long rolls were cut and joined, so a strip + is literally taped together, often slightly misregistered. + All seeded. Geometry in the logical [-1,1] square. + ============================================================ */ +const { gauss, pick, chance } = __require("src/rng.js"); + +const TWO_PI = Math.PI * 2; + +function wobblyRing(cx, cy, r, rng, wob = 0.07) { + const n = 52, pts = []; + const start = rng() * TWO_PI; + const turn = TWO_PI * (1.04 + rng() * 0.13); // overshoots — doesn't close cleanly + const dx = gauss(rng) * r * 0.05, dy = gauss(rng) * r * 0.05; + for (let i = 0; i <= n; i++) { + const a = start + turn * (i / n); + const rr = r * (1 + gauss(rng) * wob); + pts.push({ x: cx + Math.cos(a) * rr + dx * (i / n), y: cy + Math.sin(a) * rr + dy * (i / n) }); + } + return pts; +} +function wobblyLine(x1, y1, x2, y2, rng, seg = 9, amp = 0.006) { + const pts = []; + for (let i = 0; i <= seg; i++) { + const t = i / seg; + pts.push({ x: x1 + (x2 - x1) * t + gauss(rng) * amp, y: y1 + (y2 - y1) * t + gauss(rng) * amp }); + } + return pts; +} + +function generateMedia(params, scene, rng) { + const out = { grease: [], reseau: null, film: null, splice: null }; + + /* ---- grease-pencil (chinagraph) hand marks ---- */ + if ((params.annotate ?? 0) > 0) { + const full = params.annotate >= 0.66; // restrained vs studied + const v = scene.vertex || { x: 0, y: 0 }; + // ring a δ-ray curl if there is one, else the vertex region + const deltas = scene.tracks.filter(t => t.kind === 'delta' && t.pts.length > 6); + let cx, cy, cr; + if (deltas.length && chance(rng, 0.75)) { + const d = pick(rng, deltas), p = d.pts[Math.floor(d.pts.length * 0.4)]; + cx = p.x; cy = p.y; cr = 0.05 + rng() * 0.035; + } else { cx = v.x + gauss(rng) * 0.05; cy = v.y + gauss(rng) * 0.05; cr = 0.11 + rng() * 0.05; } + out.grease.push({ kind: 'ring', pts: wobblyRing(cx, cy, cr, rng), width: 2.4 }); + + // arrow from open space toward the ring + const side = cx <= 0 ? 1 : -1; + const ax = cx + side * (0.34 + rng() * 0.12), ay = cy - (0.30 + rng() * 0.12); + const tipx = cx - side * cr * 1.25, tipy = cy - cr * 1.15; + const ang = Math.atan2(tipy - ay, tipx - ax); + out.grease.push({ kind: 'arrow', shaft: wobblyLine(ax, ay, tipx, tipy, rng), tip: { x: tipx, y: tipy, ang }, width: 2.0 }); + + // event number by the ring + out.grease.push({ kind: 'text', x: cx + cr * 1.05, y: cy - cr * 1.05, text: `Nº ${scene.plate}`, size: 0.045, rot: -0.05 + gauss(rng) * 0.03 }); + + if (full) { + // angle arc + reading at the vertex + const a0 = rng() * TWO_PI, a1 = a0 + (0.5 + rng() * 0.6); + out.grease.push({ kind: 'arc', x: v.x, y: v.y, r: 0.12 + rng() * 0.05, a0, a1, width: 1.6 }); + out.grease.push({ kind: 'text', x: v.x + 0.15, y: v.y + 0.03, text: `${Math.round((a1 - a0) * 57)}°`, size: 0.034, rot: 0 }); + // a few scale ticks along a primary track + const prim = scene.tracks.find(t => t.kind === 'primary' && t.pts.length > 30); + if (prim) { + for (let k = 0; k < 4; k++) { + const p = prim.pts[Math.floor(prim.pts.length * (0.3 + k * 0.15))]; + const th = (p.theta ?? 0) + Math.PI / 2, tl = 0.018; + out.grease.push({ kind: 'tick', x1: p.x - Math.cos(th) * tl, y1: p.y - Math.sin(th) * tl, x2: p.x + Math.cos(th) * tl, y2: p.y + Math.sin(th) * tl, width: 1.2 }); + } + } + // a margin scrawl + out.grease.push({ kind: 'text', x: -0.92, y: 0.9, text: pick(rng, ['good event', 'check θ', 'V⁰ ?', 'measure', 're-scan']), size: 0.04, rot: 0.04 }); + } + } + + /* ---- réseau grid (distortion-correction crosses) ---- */ + if ((params.reseau ?? 0) > 0) { + const step = 0.1818, marks = []; + for (let gx = -0.9; gx <= 0.901; gx += step) + for (let gy = -0.9; gy <= 0.901; gy += step) marks.push({ x: gx, y: gy }); + out.reseau = { marks, size: 0.011, opacity: 0.32 * params.reseau }; + } + + /* ---- film furniture ---- */ + if (params.filmEdge) { + const n = 9, sprockets = []; + for (let i = 0; i < n; i++) sprockets.push(-0.9 + 1.8 * (i / (n - 1))); + const roll = (parseInt(scene.hash.slice(0, 4), 16) % 900 + 100); + out.film = { + sprockets, + edgeText: 'KODAK SAFETY FILM', + dataBox: [`ROLL ${roll}`, `FRAME ${scene.plate}`, scene.exposure], + }; + } + + /* ---- tape splice ---- */ + if (params.splice) { + out.splice = { y: -0.05 + gauss(rng) * 0.28, h: 0.05 + rng() * 0.025, tilt: gauss(rng) * 0.02, opacity: 0.16 + rng() * 0.06 }; + } + + return out; +} + +Object.assign(exports, { generateMedia }); + }; __reg["src/render/canvasPhoto.js"] = function (module, exports, __require) { /* ============================================================ @@ -1386,6 +1509,104 @@ function renderCanvasPhoto(ctx, w, h, scene, params, opts = {}) { } ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; + + /* ---------- Pass 10: media & hand (réseau, splice, film furniture, grease) ---------- */ + if (scene.media) drawMedia(ctx, scene.media, w, h, tx, ty, scale, u, Pf, light); +} + +/* ---- media & hand layer ---- */ +function drawMedia(ctx, media, w, h, tx, ty, scale, u, P, light) { + const [fr, fg, fb] = P.ink; + const feat = (a) => `rgba(${fr},${fg},${fb},${a})`; + + // réseau crosses (imaged grid) + if (media.reseau) { + ctx.save(); + ctx.strokeStyle = feat(media.reseau.opacity); + ctx.lineWidth = 1 * u; + const s = media.reseau.size * scale; + for (const m of media.reseau.marks) { + const x = tx(m.x), y = ty(m.y); + ctx.beginPath(); ctx.moveTo(x - s, y); ctx.lineTo(x + s, y); ctx.moveTo(x, y - s); ctx.lineTo(x, y + s); ctx.stroke(); + } + ctx.restore(); + } + + // tape splice + if (media.splice) { + const sp = media.splice, yc = ty(sp.y), hh = sp.h * scale; + ctx.save(); + ctx.translate(w / 2, yc); ctx.rotate(sp.tilt); + ctx.globalCompositeOperation = light ? 'multiply' : 'screen'; + ctx.fillStyle = feat(sp.opacity); + ctx.fillRect(-w, -hh, w * 2, hh * 2); + ctx.globalCompositeOperation = 'source-over'; + ctx.strokeStyle = feat(Math.min(0.6, sp.opacity * 2)); ctx.lineWidth = 1 * u; + ctx.beginPath(); ctx.moveTo(-w, -hh); ctx.lineTo(w, -hh); ctx.moveTo(-w, hh); ctx.lineTo(w, hh); ctx.stroke(); + ctx.restore(); + } + + // film furniture + if (media.film) { + const f = media.film; + ctx.save(); + ctx.strokeStyle = feat(0.5); ctx.lineWidth = 2 * u; + const m = 0.965; + ctx.strokeRect(tx(-m), ty(-m), tx(m) - tx(-m), ty(m) - ty(-m)); + const sw = 0.028 * scale, sh = 0.05 * scale, rr = 4 * u; + for (const y of f.sprockets) { + for (const sx of [tx(-0.985), tx(0.985)]) { + ctx.beginPath(); + if (ctx.roundRect) ctx.roundRect(sx - sw / 2, ty(y) - sh / 2, sw, sh, rr); + else ctx.rect(sx - sw / 2, ty(y) - sh / 2, sw, sh); + ctx.fillStyle = light ? 'rgba(250,248,240,0.7)' : 'rgba(18,16,13,0.85)'; + ctx.fill(); + ctx.strokeStyle = feat(0.4); ctx.lineWidth = 1 * u; ctx.stroke(); + } + } + ctx.fillStyle = feat(0.45); + ctx.font = `${7 * u}px 'JetBrains Mono', monospace`; + ctx.textAlign = 'center'; + ctx.save(); ctx.translate(tx(-0.935), ty(0)); ctx.rotate(-Math.PI / 2); ctx.fillText(f.edgeText, 0, 0); ctx.restore(); + ctx.textAlign = 'left'; + ctx.font = `${9 * u}px 'JetBrains Mono', monospace`; + f.dataBox.forEach((line, i) => ctx.fillText(line, tx(-0.9), ty(0.8) + i * 12 * u)); + ctx.restore(); + } + + // grease pencil (chinagraph) on top + if (media.grease && media.grease.length) { + const c = light ? [156, 30, 30] : [240, 226, 150]; + const stroke = (a) => `rgba(${c[0]},${c[1]},${c[2]},${a})`; + ctx.save(); + ctx.lineCap = 'round'; ctx.lineJoin = 'round'; + ctx.strokeStyle = stroke(0.85); ctx.fillStyle = stroke(0.9); + for (const g of media.grease) { + if (g.kind === 'ring' || g.kind === 'arrow') { + const pts = g.pts || g.shaft; + ctx.lineWidth = g.width * u; + ctx.beginPath(); ctx.moveTo(tx(pts[0].x), ty(pts[0].y)); + for (let i = 1; i < pts.length; i++) ctx.lineTo(tx(pts[i].x), ty(pts[i].y)); + ctx.stroke(); + if (g.kind === 'arrow') { + const { x, y, ang } = g.tip, hl = 0.03 * scale; + ctx.beginPath(); + ctx.moveTo(tx(x), ty(y)); ctx.lineTo(tx(x) - Math.cos(ang - 0.4) * hl, ty(y) - Math.sin(ang - 0.4) * hl); + ctx.moveTo(tx(x), ty(y)); ctx.lineTo(tx(x) - Math.cos(ang + 0.4) * hl, ty(y) - Math.sin(ang + 0.4) * hl); + ctx.stroke(); + } + } else if (g.kind === 'arc') { + ctx.lineWidth = g.width * u; ctx.beginPath(); ctx.arc(tx(g.x), ty(g.y), g.r * scale, g.a0, g.a1); ctx.stroke(); + } else if (g.kind === 'tick') { + ctx.lineWidth = g.width * u; ctx.beginPath(); ctx.moveTo(tx(g.x1), ty(g.y1)); ctx.lineTo(tx(g.x2), ty(g.y2)); ctx.stroke(); + } else if (g.kind === 'text') { + ctx.save(); ctx.translate(tx(g.x), ty(g.y)); ctx.rotate(g.rot || 0); + ctx.font = `${(g.size * scale).toFixed(1)}px 'Bradley Hand','Segoe Script','Comic Sans MS',cursive`; + ctx.fillText(g.text, 0, 0); ctx.restore(); + } + } + ctx.restore(); + } } /* ---- shock disk ---- */ @@ -2330,6 +2551,69 @@ function renderSVG(scene, params, sizePx = 4800) { + ``; } + /* ---------- Media & hand (réseau, splice, film furniture, grease pencil) ---------- */ + let media = ''; + if (scene.media) { + const M = scene.media; + const nx = (x) => (cx + x * scale), ny = (y) => (cy + y * scale); + const esc = (t) => String(t).replace(/[<&]/g, c => (c === '<' ? '<' : '&')); + if (M.reseau) { + let g = ``; + const ss = M.reseau.size * scale; + for (const m of M.reseau.marks) { + const x = nx(m.x), y = ny(m.y); + g += ``; + } + media += g + ``; + } + if (M.splice) { + const yc = ny(M.splice.y), hh = M.splice.h * scale, deg = M.splice.tilt * 57.3; + media += `` + + `` + + `` + + ``; + } + if (M.film) { + const f = M.film, m = 0.965; + media += ``; + const sw = 0.028 * scale, sh = 0.05 * scale, rx = 4 * u, holeFill = inv ? '#faf8f0' : '#12100d'; + let g = ''; + for (const y of f.sprockets) for (const sx of [nx(-0.985), nx(0.985)]) { + g += ``; + } + media += g; + media += `` + + `${esc(f.edgeText)}`; + f.dataBox.forEach((line, i) => { media += `${esc(line)}`; }); + media += ``; + } + if (M.grease && M.grease.length) { + const ch = inv ? '#9c1e1e' : '#f0e296'; + let g = ``; + for (const gm of M.grease) { + if (gm.kind === 'ring' || gm.kind === 'arrow') { + const pts = gm.pts || gm.shaft; + let d = `M ${nx(pts[0].x).toFixed(1)} ${ny(pts[0].y).toFixed(1)}`; + for (let i = 1; i < pts.length; i++) d += ` L ${nx(pts[i].x).toFixed(1)} ${ny(pts[i].y).toFixed(1)}`; + g += ``; + if (gm.kind === 'arrow') { + const { x, y, ang } = gm.tip, hl = 0.03 * scale; + g += ``; + } + } else if (gm.kind === 'arc') { + const x0 = nx(gm.x) + Math.cos(gm.a0) * gm.r * scale, y0 = ny(gm.y) + Math.sin(gm.a0) * gm.r * scale; + const x1 = nx(gm.x) + Math.cos(gm.a1) * gm.r * scale, y1 = ny(gm.y) + Math.sin(gm.a1) * gm.r * scale; + g += ``; + } else if (gm.kind === 'tick') { + g += ``; + } else if (gm.kind === 'text') { + g += `${esc(gm.text)}`; + } + } + media += g + ``; + } + } + /* ---------- Assemble ---------- */ let s = `\n`; s += `\n`; @@ -2343,6 +2627,7 @@ function renderSVG(scene, params, sizePx = 4800) { s += layer('fiducials', 'Fiducials', fids); s += layer('vignette', 'Vignette', vign); s += layer('header', 'Archival header', header); + s += layer('media', 'Media & hand', media); s += `\n`; return s; } @@ -2778,6 +3063,13 @@ const GROUPS = [ { id: 'haloHue', label: 'Halo hue', min: 0, max: 1, step: 0.005, value: 0.55, mode: 'render' }, ], }, + { + title: 'Media & Hand', + controls: [ + { id: 'annotate', label: 'Grease-pencil marks', min: 0, max: 1, step: 0.01, value: 0, mode: 'scene' }, + { id: 'reseau', label: 'Réseau grid', min: 0, max: 1, step: 0.01, value: 0, mode: 'scene' }, + ], + }, ]; const SELECTS = [ @@ -2798,6 +3090,8 @@ const TOGGLES = [ { id: 'showBoundary', label: 'Chamber boundary', value: true, mode: 'render' }, { id: 'showHeader', label: 'Archival header', value: true, mode: 'render' }, { id: 'invert', label: 'Invert · photographic positive', value: true, mode: 'render' }, + { id: 'filmEdge', label: 'Film edge (sprockets, data box)', value: false, mode: 'scene' }, + { id: 'splice', label: 'Tape splice', value: false, mode: 'scene' }, ]; /* Fixed (non-UI) params with sensible defaults. */ @@ -2903,6 +3197,7 @@ function paramsFromSeed(seed) { invert: true, palette: 'mono', saturation: 1.0, // canonical look is B&W; colour is opt-in hueShift: 0, hueCycles: 3, diskSpectrum: 0, halo: 0, haloHue: 0.55, paperTone: 'cream', toneStrength: 1.0, paperBright: 1.0, glow: 0.5, + annotate: 0, reseau: 0, filmEdge: false, splice: false, // media & hand layer (opt-in) }; if (arch === 'dense') { diff --git a/output/iterations-claude-craft/43_magenta-handnote.svg b/output/iterations-claude-craft/43_magenta-handnote.svg new file mode 100644 index 0000000..f8c1d0b --- /dev/null +++ b/output/iterations-claude-craft/43_magenta-handnote.svg @@ -0,0 +1,477 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +KODAK SAFETY FILMROLL 729FRAME 0011976.02.18Nº 001 + + diff --git a/output/iterations-claude-craft/44_magenta-studied.svg b/output/iterations-claude-craft/44_magenta-studied.svg new file mode 100644 index 0000000..45bf687 --- /dev/null +++ b/output/iterations-claude-craft/44_magenta-studied.svg @@ -0,0 +1,477 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +KODAK SAFETY FILMROLL 729FRAME 0011976.02.18Nº 00142°good event + + diff --git a/output/iterations-claude-craft/45_curls.svg b/output/iterations-claude-craft/45_curls.svg new file mode 100644 index 0000000..dc13caf --- /dev/null +++ b/output/iterations-claude-craft/45_curls.svg @@ -0,0 +1,460 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +KODAK SAFETY FILMROLL 729FRAME 0011976.02.18Nº 001 + + diff --git a/output/iterations-claude-craft/46_ember-ends.svg b/output/iterations-claude-craft/46_ember-ends.svg new file mode 100644 index 0000000..57022f4 --- /dev/null +++ b/output/iterations-claude-craft/46_ember-ends.svg @@ -0,0 +1,440 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/output/iterations-claude-craft/47_the-eye.svg b/output/iterations-claude-craft/47_the-eye.svg new file mode 100644 index 0000000..6971ccc --- /dev/null +++ b/output/iterations-claude-craft/47_the-eye.svg @@ -0,0 +1,440 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/output/iterations-claude-craft/48_worn-rim.svg b/output/iterations-claude-craft/48_worn-rim.svg new file mode 100644 index 0000000..e042e7a --- /dev/null +++ b/output/iterations-claude-craft/48_worn-rim.svg @@ -0,0 +1,440 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/output/iterations-claude-craft/49_nucleation.svg b/output/iterations-claude-craft/49_nucleation.svg new file mode 100644 index 0000000..eb3bb1e --- /dev/null +++ b/output/iterations-claude-craft/49_nucleation.svg @@ -0,0 +1,440 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/output/iterations-claude-craft/50_through-fog.svg b/output/iterations-claude-craft/50_through-fog.svg new file mode 100644 index 0000000..6bd7ab7 --- /dev/null +++ b/output/iterations-claude-craft/50_through-fog.svg @@ -0,0 +1,525 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/output/iterations-claude-craft/51_prism.svg b/output/iterations-claude-craft/51_prism.svg new file mode 100644 index 0000000..4ae9dd7 --- /dev/null +++ b/output/iterations-claude-craft/51_prism.svg @@ -0,0 +1,440 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/output/iterations-claude-craft/52_aura.svg b/output/iterations-claude-craft/52_aura.svg new file mode 100644 index 0000000..60b7e97 --- /dev/null +++ b/output/iterations-claude-craft/52_aura.svg @@ -0,0 +1,440 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/output/iterations-claude-craft/53_scanners-mark.svg b/output/iterations-claude-craft/53_scanners-mark.svg new file mode 100644 index 0000000..7d4b3ee --- /dev/null +++ b/output/iterations-claude-craft/53_scanners-mark.svg @@ -0,0 +1,443 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +KODAK SAFETY FILMROLL 729FRAME 0011976.02.18Nº 00142°good event + + diff --git a/output/iterations-claude-craft/54_the-strip.svg b/output/iterations-claude-craft/54_the-strip.svg new file mode 100644 index 0000000..b1a7ab6 --- /dev/null +++ b/output/iterations-claude-craft/54_the-strip.svg @@ -0,0 +1,443 @@ + + +Bubble Chamber · seed=MESON-5113 · hash=1b11f594d3f9d · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +KODAK SAFETY FILMROLL 729FRAME 0011976.02.18Nº 001 + + diff --git a/output/iterations-claude-craft/index.html b/output/iterations-claude-craft/index.html index c0f3e9a..585c7e8 100644 --- a/output/iterations-claude-craft/index.html +++ b/output/iterations-claude-craft/index.html @@ -61,4 +61,22 @@ figcaption{padding:8px 10px;color:#e8e4d8}
On selenium mauve · deep focus
40_magenta-selenium-deep.svg · MESON-5113
Black + faint magenta halo
41_magenta-black-halo.svg · KAON-2026
★ 37's recipe on a dark sepia ground — the contender
42_magenta-darksepia.svg · ISOSPIN-1003
+ +

Round 7 · humanized — the worked-over film frame (40 refined)

+

grease-pencil hand marks · film furniture (sprockets, data box, edge text) · réseau grid · tape splice

+
Restrained — a marked film frame (ring · arrow · Nº · sprockets)
43_magenta-handnote.svg · MESON-5113
+
Studied — full grease + réseau + tape splice
44_magenta-studied.svg · MESON-5113
+
+

Round 8 · theme & variations on 44 — one craft each

+

all MESON-5113 · magenta family on selenium · each piece foregrounds a single element of craft

+
① The curl — δ-ray logarithmic spirals turned up, the nautilus rhyme
45_curls.svg · MESON-5113
+
② The ember — glowing deaths on black, saturated, rising intensity
46_ember-ends.svg · MESON-5113
+
③ The eye — bright striated disk core, the pupil/sun anchor
47_the-eye.svg · MESON-5113
+
④ The worn rim — eroded, stained, mottled disk edge; time made visible
48_worn-rim.svg · MESON-5113
+
⑤ Nucleation — dense bubbles resolving into lines, the discrete made continuous
49_nucleation.svg · MESON-5113
+
⑥ Through fog — depth-of-field, soft deep tracks; the chamber as volume
50_through-fog.svg · MESON-5113
+
⑦ The prism — iridescent oil-on-water disk; the one note of full spectrum
51_prism.svg · MESON-5113
+
⑧ The aura — chromatic halo around the trails; halation, breath
52_aura.svg · MESON-5113
+
⑨ The scanner's mark — full grease-pencil hand: ring, arrow, Nº, angle
53_scanners-mark.svg · MESON-5113
+
⑩ The strip — film as object: sprockets, edge text, réseau, tape splice
54_the-strip.svg · MESON-5113
diff --git a/roadmap.md b/roadmap.md index 6a41d35..b3fe99d 100644 --- a/roadmap.md +++ b/roadmap.md @@ -108,7 +108,12 @@ Take "depth is time/space" all the way. the viewer can move around. The strongest fusion of your layered-print plan and the chamber's actual geometry. -### Phase 4 — The archive & the human hand +### Phase 4 — The archive & the human hand ◐ in progress +**Media & hand layer shipped** (`src/scene/media.js`): grease-pencil (chinagraph) +ring/arrow/event-№/angle/ticks/scrawl; film furniture (sprocket holes, data box, +edge stock text, frame rebate); a réseau grid; and a tape splice — grounded in how +1955–85 bubble-chamber film was shot, spliced and hand-scanned. Toggleable +(`annotate`, `reseau`, `filmEdge`, `splice`), raster + SVG. Next: - **Plate series / typology:** deterministic grids (MUON-001…024), contact-sheet- as-artwork, a fabricated catalogue with consistent archival furniture. - **Annotation fiction:** hand-drawn measurement marks — angle arcs at vertices, diff --git a/src/main.js b/src/main.js index fc6e204..ec4af4e 100644 --- a/src/main.js +++ b/src/main.js @@ -52,6 +52,10 @@ function buildPanel() { addSelect(grp, 'palette'); addSelect(grp, 'paperTone'); } + if (g.title === 'Media & Hand') { + addToggle(grp, 'filmEdge'); + addToggle(grp, 'splice'); + } } } diff --git a/src/render/canvasPhoto.js b/src/render/canvasPhoto.js index 3190fd4..02dad46 100644 --- a/src/render/canvasPhoto.js +++ b/src/render/canvasPhoto.js @@ -237,6 +237,104 @@ export function renderCanvasPhoto(ctx, w, h, scene, params, opts = {}) { } ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; + + /* ---------- Pass 10: media & hand (réseau, splice, film furniture, grease) ---------- */ + if (scene.media) drawMedia(ctx, scene.media, w, h, tx, ty, scale, u, Pf, light); +} + +/* ---- media & hand layer ---- */ +function drawMedia(ctx, media, w, h, tx, ty, scale, u, P, light) { + const [fr, fg, fb] = P.ink; + const feat = (a) => `rgba(${fr},${fg},${fb},${a})`; + + // réseau crosses (imaged grid) + if (media.reseau) { + ctx.save(); + ctx.strokeStyle = feat(media.reseau.opacity); + ctx.lineWidth = 1 * u; + const s = media.reseau.size * scale; + for (const m of media.reseau.marks) { + const x = tx(m.x), y = ty(m.y); + ctx.beginPath(); ctx.moveTo(x - s, y); ctx.lineTo(x + s, y); ctx.moveTo(x, y - s); ctx.lineTo(x, y + s); ctx.stroke(); + } + ctx.restore(); + } + + // tape splice + if (media.splice) { + const sp = media.splice, yc = ty(sp.y), hh = sp.h * scale; + ctx.save(); + ctx.translate(w / 2, yc); ctx.rotate(sp.tilt); + ctx.globalCompositeOperation = light ? 'multiply' : 'screen'; + ctx.fillStyle = feat(sp.opacity); + ctx.fillRect(-w, -hh, w * 2, hh * 2); + ctx.globalCompositeOperation = 'source-over'; + ctx.strokeStyle = feat(Math.min(0.6, sp.opacity * 2)); ctx.lineWidth = 1 * u; + ctx.beginPath(); ctx.moveTo(-w, -hh); ctx.lineTo(w, -hh); ctx.moveTo(-w, hh); ctx.lineTo(w, hh); ctx.stroke(); + ctx.restore(); + } + + // film furniture + if (media.film) { + const f = media.film; + ctx.save(); + ctx.strokeStyle = feat(0.5); ctx.lineWidth = 2 * u; + const m = 0.965; + ctx.strokeRect(tx(-m), ty(-m), tx(m) - tx(-m), ty(m) - ty(-m)); + const sw = 0.028 * scale, sh = 0.05 * scale, rr = 4 * u; + for (const y of f.sprockets) { + for (const sx of [tx(-0.985), tx(0.985)]) { + ctx.beginPath(); + if (ctx.roundRect) ctx.roundRect(sx - sw / 2, ty(y) - sh / 2, sw, sh, rr); + else ctx.rect(sx - sw / 2, ty(y) - sh / 2, sw, sh); + ctx.fillStyle = light ? 'rgba(250,248,240,0.7)' : 'rgba(18,16,13,0.85)'; + ctx.fill(); + ctx.strokeStyle = feat(0.4); ctx.lineWidth = 1 * u; ctx.stroke(); + } + } + ctx.fillStyle = feat(0.45); + ctx.font = `${7 * u}px 'JetBrains Mono', monospace`; + ctx.textAlign = 'center'; + ctx.save(); ctx.translate(tx(-0.935), ty(0)); ctx.rotate(-Math.PI / 2); ctx.fillText(f.edgeText, 0, 0); ctx.restore(); + ctx.textAlign = 'left'; + ctx.font = `${9 * u}px 'JetBrains Mono', monospace`; + f.dataBox.forEach((line, i) => ctx.fillText(line, tx(-0.9), ty(0.8) + i * 12 * u)); + ctx.restore(); + } + + // grease pencil (chinagraph) on top + if (media.grease && media.grease.length) { + const c = light ? [156, 30, 30] : [240, 226, 150]; + const stroke = (a) => `rgba(${c[0]},${c[1]},${c[2]},${a})`; + ctx.save(); + ctx.lineCap = 'round'; ctx.lineJoin = 'round'; + ctx.strokeStyle = stroke(0.85); ctx.fillStyle = stroke(0.9); + for (const g of media.grease) { + if (g.kind === 'ring' || g.kind === 'arrow') { + const pts = g.pts || g.shaft; + ctx.lineWidth = g.width * u; + ctx.beginPath(); ctx.moveTo(tx(pts[0].x), ty(pts[0].y)); + for (let i = 1; i < pts.length; i++) ctx.lineTo(tx(pts[i].x), ty(pts[i].y)); + ctx.stroke(); + if (g.kind === 'arrow') { + const { x, y, ang } = g.tip, hl = 0.03 * scale; + ctx.beginPath(); + ctx.moveTo(tx(x), ty(y)); ctx.lineTo(tx(x) - Math.cos(ang - 0.4) * hl, ty(y) - Math.sin(ang - 0.4) * hl); + ctx.moveTo(tx(x), ty(y)); ctx.lineTo(tx(x) - Math.cos(ang + 0.4) * hl, ty(y) - Math.sin(ang + 0.4) * hl); + ctx.stroke(); + } + } else if (g.kind === 'arc') { + ctx.lineWidth = g.width * u; ctx.beginPath(); ctx.arc(tx(g.x), ty(g.y), g.r * scale, g.a0, g.a1); ctx.stroke(); + } else if (g.kind === 'tick') { + ctx.lineWidth = g.width * u; ctx.beginPath(); ctx.moveTo(tx(g.x1), ty(g.y1)); ctx.lineTo(tx(g.x2), ty(g.y2)); ctx.stroke(); + } else if (g.kind === 'text') { + ctx.save(); ctx.translate(tx(g.x), ty(g.y)); ctx.rotate(g.rot || 0); + ctx.font = `${(g.size * scale).toFixed(1)}px 'Bradley Hand','Segoe Script','Comic Sans MS',cursive`; + ctx.fillText(g.text, 0, 0); ctx.restore(); + } + } + ctx.restore(); + } } /* ---- shock disk ---- */ diff --git a/src/render/svgVector.js b/src/render/svgVector.js index 245086e..692f1f1 100644 --- a/src/render/svgVector.js +++ b/src/render/svgVector.js @@ -220,6 +220,69 @@ export function renderSVG(scene, params, sizePx = 4800) { + ``; } + /* ---------- Media & hand (réseau, splice, film furniture, grease pencil) ---------- */ + let media = ''; + if (scene.media) { + const M = scene.media; + const nx = (x) => (cx + x * scale), ny = (y) => (cy + y * scale); + const esc = (t) => String(t).replace(/[<&]/g, c => (c === '<' ? '<' : '&')); + if (M.reseau) { + let g = ``; + const ss = M.reseau.size * scale; + for (const m of M.reseau.marks) { + const x = nx(m.x), y = ny(m.y); + g += ``; + } + media += g + ``; + } + if (M.splice) { + const yc = ny(M.splice.y), hh = M.splice.h * scale, deg = M.splice.tilt * 57.3; + media += `` + + `` + + `` + + ``; + } + if (M.film) { + const f = M.film, m = 0.965; + media += ``; + const sw = 0.028 * scale, sh = 0.05 * scale, rx = 4 * u, holeFill = inv ? '#faf8f0' : '#12100d'; + let g = ''; + for (const y of f.sprockets) for (const sx of [nx(-0.985), nx(0.985)]) { + g += ``; + } + media += g; + media += `` + + `${esc(f.edgeText)}`; + f.dataBox.forEach((line, i) => { media += `${esc(line)}`; }); + media += ``; + } + if (M.grease && M.grease.length) { + const ch = inv ? '#9c1e1e' : '#f0e296'; + let g = ``; + for (const gm of M.grease) { + if (gm.kind === 'ring' || gm.kind === 'arrow') { + const pts = gm.pts || gm.shaft; + let d = `M ${nx(pts[0].x).toFixed(1)} ${ny(pts[0].y).toFixed(1)}`; + for (let i = 1; i < pts.length; i++) d += ` L ${nx(pts[i].x).toFixed(1)} ${ny(pts[i].y).toFixed(1)}`; + g += ``; + if (gm.kind === 'arrow') { + const { x, y, ang } = gm.tip, hl = 0.03 * scale; + g += ``; + } + } else if (gm.kind === 'arc') { + const x0 = nx(gm.x) + Math.cos(gm.a0) * gm.r * scale, y0 = ny(gm.y) + Math.sin(gm.a0) * gm.r * scale; + const x1 = nx(gm.x) + Math.cos(gm.a1) * gm.r * scale, y1 = ny(gm.y) + Math.sin(gm.a1) * gm.r * scale; + g += ``; + } else if (gm.kind === 'tick') { + g += ``; + } else if (gm.kind === 'text') { + g += `${esc(gm.text)}`; + } + } + media += g + ``; + } + } + /* ---------- Assemble ---------- */ let s = `\n`; s += `\n`; @@ -233,6 +296,7 @@ export function renderSVG(scene, params, sizePx = 4800) { s += layer('fiducials', 'Fiducials', fids); s += layer('vignette', 'Vignette', vign); s += layer('header', 'Archival header', header); + s += layer('media', 'Media & hand', media); s += `\n`; return s; } diff --git a/src/scene/media.js b/src/scene/media.js new file mode 100644 index 0000000..c4b3fd6 --- /dev/null +++ b/src/scene/media.js @@ -0,0 +1,110 @@ +/* ============================================================ + media.js — the physical-artifact & human layer. + What a real bubble-chamber frame carried beyond the tracks: + • grease-pencil (chinagraph) hand marks — a scanner ringing a + "good event", an arrow, the event number, an angle/ticks; + • film furniture — sprocket holes, a data box (roll/frame/ + date), edge stock printing, a frame rebate; + • a réseau — the precise grid of small crosses imaged for + distortion correction; + • a tape splice — long rolls were cut and joined, so a strip + is literally taped together, often slightly misregistered. + All seeded. Geometry in the logical [-1,1] square. + ============================================================ */ +import { gauss, pick, chance } from '../rng.js'; + +const TWO_PI = Math.PI * 2; + +function wobblyRing(cx, cy, r, rng, wob = 0.07) { + const n = 52, pts = []; + const start = rng() * TWO_PI; + const turn = TWO_PI * (1.04 + rng() * 0.13); // overshoots — doesn't close cleanly + const dx = gauss(rng) * r * 0.05, dy = gauss(rng) * r * 0.05; + for (let i = 0; i <= n; i++) { + const a = start + turn * (i / n); + const rr = r * (1 + gauss(rng) * wob); + pts.push({ x: cx + Math.cos(a) * rr + dx * (i / n), y: cy + Math.sin(a) * rr + dy * (i / n) }); + } + return pts; +} +function wobblyLine(x1, y1, x2, y2, rng, seg = 9, amp = 0.006) { + const pts = []; + for (let i = 0; i <= seg; i++) { + const t = i / seg; + pts.push({ x: x1 + (x2 - x1) * t + gauss(rng) * amp, y: y1 + (y2 - y1) * t + gauss(rng) * amp }); + } + return pts; +} + +export function generateMedia(params, scene, rng) { + const out = { grease: [], reseau: null, film: null, splice: null }; + + /* ---- grease-pencil (chinagraph) hand marks ---- */ + if ((params.annotate ?? 0) > 0) { + const full = params.annotate >= 0.66; // restrained vs studied + const v = scene.vertex || { x: 0, y: 0 }; + // ring a δ-ray curl if there is one, else the vertex region + const deltas = scene.tracks.filter(t => t.kind === 'delta' && t.pts.length > 6); + let cx, cy, cr; + if (deltas.length && chance(rng, 0.75)) { + const d = pick(rng, deltas), p = d.pts[Math.floor(d.pts.length * 0.4)]; + cx = p.x; cy = p.y; cr = 0.05 + rng() * 0.035; + } else { cx = v.x + gauss(rng) * 0.05; cy = v.y + gauss(rng) * 0.05; cr = 0.11 + rng() * 0.05; } + out.grease.push({ kind: 'ring', pts: wobblyRing(cx, cy, cr, rng), width: 2.4 }); + + // arrow from open space toward the ring + const side = cx <= 0 ? 1 : -1; + const ax = cx + side * (0.34 + rng() * 0.12), ay = cy - (0.30 + rng() * 0.12); + const tipx = cx - side * cr * 1.25, tipy = cy - cr * 1.15; + const ang = Math.atan2(tipy - ay, tipx - ax); + out.grease.push({ kind: 'arrow', shaft: wobblyLine(ax, ay, tipx, tipy, rng), tip: { x: tipx, y: tipy, ang }, width: 2.0 }); + + // event number by the ring + out.grease.push({ kind: 'text', x: cx + cr * 1.05, y: cy - cr * 1.05, text: `Nº ${scene.plate}`, size: 0.045, rot: -0.05 + gauss(rng) * 0.03 }); + + if (full) { + // angle arc + reading at the vertex + const a0 = rng() * TWO_PI, a1 = a0 + (0.5 + rng() * 0.6); + out.grease.push({ kind: 'arc', x: v.x, y: v.y, r: 0.12 + rng() * 0.05, a0, a1, width: 1.6 }); + out.grease.push({ kind: 'text', x: v.x + 0.15, y: v.y + 0.03, text: `${Math.round((a1 - a0) * 57)}°`, size: 0.034, rot: 0 }); + // a few scale ticks along a primary track + const prim = scene.tracks.find(t => t.kind === 'primary' && t.pts.length > 30); + if (prim) { + for (let k = 0; k < 4; k++) { + const p = prim.pts[Math.floor(prim.pts.length * (0.3 + k * 0.15))]; + const th = (p.theta ?? 0) + Math.PI / 2, tl = 0.018; + out.grease.push({ kind: 'tick', x1: p.x - Math.cos(th) * tl, y1: p.y - Math.sin(th) * tl, x2: p.x + Math.cos(th) * tl, y2: p.y + Math.sin(th) * tl, width: 1.2 }); + } + } + // a margin scrawl + out.grease.push({ kind: 'text', x: -0.92, y: 0.9, text: pick(rng, ['good event', 'check θ', 'V⁰ ?', 'measure', 're-scan']), size: 0.04, rot: 0.04 }); + } + } + + /* ---- réseau grid (distortion-correction crosses) ---- */ + if ((params.reseau ?? 0) > 0) { + const step = 0.1818, marks = []; + for (let gx = -0.9; gx <= 0.901; gx += step) + for (let gy = -0.9; gy <= 0.901; gy += step) marks.push({ x: gx, y: gy }); + out.reseau = { marks, size: 0.011, opacity: 0.32 * params.reseau }; + } + + /* ---- film furniture ---- */ + if (params.filmEdge) { + const n = 9, sprockets = []; + for (let i = 0; i < n; i++) sprockets.push(-0.9 + 1.8 * (i / (n - 1))); + const roll = (parseInt(scene.hash.slice(0, 4), 16) % 900 + 100); + out.film = { + sprockets, + edgeText: 'KODAK SAFETY FILM', + dataBox: [`ROLL ${roll}`, `FRAME ${scene.plate}`, scene.exposure], + }; + } + + /* ---- tape splice ---- */ + if (params.splice) { + out.splice = { y: -0.05 + gauss(rng) * 0.28, h: 0.05 + rng() * 0.025, tilt: gauss(rng) * 0.02, opacity: 0.16 + rng() * 0.06 }; + } + + return out; +} diff --git a/src/scene/params.js b/src/scene/params.js index 01c02be..f702e72 100644 --- a/src/scene/params.js +++ b/src/scene/params.js @@ -58,6 +58,7 @@ export function paramsFromSeed(seed) { invert: true, palette: 'mono', saturation: 1.0, // canonical look is B&W; colour is opt-in hueShift: 0, hueCycles: 3, diskSpectrum: 0, halo: 0, haloHue: 0.55, paperTone: 'cream', toneStrength: 1.0, paperBright: 1.0, glow: 0.5, + annotate: 0, reseau: 0, filmEdge: false, splice: false, // media & hand layer (opt-in) }; if (arch === 'dense') { diff --git a/src/scene/scene.js b/src/scene/scene.js index a53b4ca..a1ea4ca 100644 --- a/src/scene/scene.js +++ b/src/scene/scene.js @@ -11,6 +11,7 @@ import { spawnVDecay } from './vdecay.js'; import { generateShock } from './shock.js'; import { generateArtifacts } from './artifacts.js'; import { generateInstrument } from './instrument.js'; +import { generateMedia } from './media.js'; import { cyrb53 } from '../rng.js'; const LABS = ['BEBC · CERN', 'GARGAMELLE · CERN', '2m HBC · CERN', '82" HBC · SLAC', 'MIRABELLE · IHEP']; @@ -144,5 +145,8 @@ export function generateScene(params) { const exposure = `${year}.${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}`; const lab = pick(makeRng(params.seed, 'lab'), LABS); - return { tracks, vertex: fgVertex, shock, artifacts, instrument, hash, plate, exposure, lab }; + const out = { tracks, vertex: fgVertex, shock, artifacts, instrument, hash, plate, exposure, lab }; + // physical-artifact & human layer (grease pencil, film furniture, réseau, splice) + out.media = generateMedia(params, out, makeRng(params.seed, 'media')); + return out; } diff --git a/src/ui/controls.js b/src/ui/controls.js index cc7798a..caa83a2 100644 --- a/src/ui/controls.js +++ b/src/ui/controls.js @@ -85,6 +85,13 @@ export const GROUPS = [ { id: 'haloHue', label: 'Halo hue', min: 0, max: 1, step: 0.005, value: 0.55, mode: 'render' }, ], }, + { + title: 'Media & Hand', + controls: [ + { id: 'annotate', label: 'Grease-pencil marks', min: 0, max: 1, step: 0.01, value: 0, mode: 'scene' }, + { id: 'reseau', label: 'Réseau grid', min: 0, max: 1, step: 0.01, value: 0, mode: 'scene' }, + ], + }, ]; export const SELECTS = [ @@ -105,6 +112,8 @@ export const TOGGLES = [ { id: 'showBoundary', label: 'Chamber boundary', value: true, mode: 'render' }, { id: 'showHeader', label: 'Archival header', value: true, mode: 'render' }, { id: 'invert', label: 'Invert · photographic positive', value: true, mode: 'render' }, + { id: 'filmEdge', label: 'Film edge (sprockets, data box)', value: false, mode: 'scene' }, + { id: 'splice', label: 'Tape splice', value: false, mode: 'scene' }, ]; /* Fixed (non-UI) params with sensible defaults. */