refinement
@@ -43,6 +43,8 @@ src/
|
|||||||
vdecay.js neutral-decay "V" daughters
|
vdecay.js neutral-decay "V" daughters
|
||||||
shock.js pressure/shock disk model
|
shock.js pressure/shock disk model
|
||||||
instrument.js chamber optics: structural fan lines, chords, wall arcs
|
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
|
artifacts.js plate damage: scratches, hairs, dust, rings, fingerprints
|
||||||
params.js derive a full tasteful parameter set from a seed (archetypes)
|
params.js derive a full tasteful parameter set from a seed (archetypes)
|
||||||
bubbles.js polyline → bubbles (shared by all renderers)
|
bubbles.js polyline → bubbles (shared by all renderers)
|
||||||
|
|||||||
@@ -193,6 +193,10 @@ function buildPanel() {
|
|||||||
addSelect(grp, 'palette');
|
addSelect(grp, 'palette');
|
||||||
addSelect(grp, 'paperTone');
|
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 { generateShock } = __require("src/scene/shock.js");
|
||||||
const { generateArtifacts } = __require("src/scene/artifacts.js");
|
const { generateArtifacts } = __require("src/scene/artifacts.js");
|
||||||
const { generateInstrument } = __require("src/scene/instrument.js");
|
const { generateInstrument } = __require("src/scene/instrument.js");
|
||||||
|
const { generateMedia } = __require("src/scene/media.js");
|
||||||
const { cyrb53 } = __require("src/rng.js");
|
const { cyrb53 } = __require("src/rng.js");
|
||||||
|
|
||||||
const LABS = ['BEBC · CERN', 'GARGAMELLE · CERN', '2m HBC · CERN', '82" HBC · SLAC', 'MIRABELLE · IHEP'];
|
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 exposure = `${year}.${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}`;
|
||||||
const lab = pick(makeRng(params.seed, 'lab'), LABS);
|
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 });
|
Object.assign(exports, { generateScene });
|
||||||
@@ -1145,6 +1153,121 @@ function generateInstrument(params, rng) {
|
|||||||
|
|
||||||
Object.assign(exports, { generateInstrument });
|
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) {
|
__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.globalCompositeOperation = 'source-over';
|
||||||
ctx.globalAlpha = 1;
|
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 ---- */
|
/* ---- shock disk ---- */
|
||||||
@@ -2330,6 +2551,69 @@ function renderSVG(scene, params, sizePx = 4800) {
|
|||||||
+ `</g>`;
|
+ `</g>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 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 = `<g stroke="${ink}" stroke-opacity="${M.reseau.opacity.toFixed(3)}" stroke-width="${1 * u}">`;
|
||||||
|
const ss = M.reseau.size * scale;
|
||||||
|
for (const m of M.reseau.marks) {
|
||||||
|
const x = nx(m.x), y = ny(m.y);
|
||||||
|
g += `<line x1="${(x - ss).toFixed(1)}" y1="${y.toFixed(1)}" x2="${(x + ss).toFixed(1)}" y2="${y.toFixed(1)}"/><line x1="${x.toFixed(1)}" y1="${(y - ss).toFixed(1)}" x2="${x.toFixed(1)}" y2="${(y + ss).toFixed(1)}"/>`;
|
||||||
|
}
|
||||||
|
media += g + `</g>`;
|
||||||
|
}
|
||||||
|
if (M.splice) {
|
||||||
|
const yc = ny(M.splice.y), hh = M.splice.h * scale, deg = M.splice.tilt * 57.3;
|
||||||
|
media += `<g transform="rotate(${deg.toFixed(2)} ${(w / 2).toFixed(1)} ${yc.toFixed(1)})">`
|
||||||
|
+ `<rect x="0" y="${(yc - hh).toFixed(1)}" width="${w}" height="${(hh * 2).toFixed(1)}" fill="${ink}" fill-opacity="${M.splice.opacity.toFixed(3)}"/>`
|
||||||
|
+ `<line x1="0" y1="${(yc - hh).toFixed(1)}" x2="${w}" y2="${(yc - hh).toFixed(1)}" stroke="${ink}" stroke-opacity="${Math.min(0.6, M.splice.opacity * 2).toFixed(3)}" stroke-width="${u}"/>`
|
||||||
|
+ `<line x1="0" y1="${(yc + hh).toFixed(1)}" x2="${w}" y2="${(yc + hh).toFixed(1)}" stroke="${ink}" stroke-opacity="${Math.min(0.6, M.splice.opacity * 2).toFixed(3)}" stroke-width="${u}"/></g>`;
|
||||||
|
}
|
||||||
|
if (M.film) {
|
||||||
|
const f = M.film, m = 0.965;
|
||||||
|
media += `<rect x="${nx(-m).toFixed(1)}" y="${ny(-m).toFixed(1)}" width="${((nx(m) - nx(-m))).toFixed(1)}" height="${((ny(m) - ny(-m))).toFixed(1)}" fill="none" stroke="${ink}" stroke-opacity="0.5" stroke-width="${2 * u}"/>`;
|
||||||
|
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 += `<rect x="${(sx - sw / 2).toFixed(1)}" y="${(ny(y) - sh / 2).toFixed(1)}" width="${sw.toFixed(1)}" height="${sh.toFixed(1)}" rx="${rx.toFixed(1)}" fill="${holeFill}" fill-opacity="0.7" stroke="${ink}" stroke-opacity="0.4" stroke-width="${u}"/>`;
|
||||||
|
}
|
||||||
|
media += g;
|
||||||
|
media += `<g fill="${ink}" font-family="'JetBrains Mono', monospace">`
|
||||||
|
+ `<text x="${nx(-0.935).toFixed(1)}" y="${ny(0).toFixed(1)}" font-size="${(7 * u).toFixed(0)}" fill-opacity="0.45" text-anchor="middle" transform="rotate(-90 ${nx(-0.935).toFixed(1)} ${ny(0).toFixed(1)})">${esc(f.edgeText)}</text>`;
|
||||||
|
f.dataBox.forEach((line, i) => { media += `<text x="${nx(-0.9).toFixed(1)}" y="${(ny(0.8) + i * 12 * u).toFixed(1)}" font-size="${(9 * u).toFixed(0)}" fill-opacity="0.5">${esc(line)}</text>`; });
|
||||||
|
media += `</g>`;
|
||||||
|
}
|
||||||
|
if (M.grease && M.grease.length) {
|
||||||
|
const ch = inv ? '#9c1e1e' : '#f0e296';
|
||||||
|
let g = `<g stroke="${ch}" fill="${ch}" stroke-linecap="round" stroke-linejoin="round">`;
|
||||||
|
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 += `<path d="${d}" fill="none" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
if (gm.kind === 'arrow') {
|
||||||
|
const { x, y, ang } = gm.tip, hl = 0.03 * scale;
|
||||||
|
g += `<path d="M ${nx(x).toFixed(1)} ${ny(y).toFixed(1)} L ${(nx(x) - Math.cos(ang - 0.4) * hl).toFixed(1)} ${(ny(y) - Math.sin(ang - 0.4) * hl).toFixed(1)} M ${nx(x).toFixed(1)} ${ny(y).toFixed(1)} L ${(nx(x) - Math.cos(ang + 0.4) * hl).toFixed(1)} ${(ny(y) - Math.sin(ang + 0.4) * hl).toFixed(1)}" fill="none" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
}
|
||||||
|
} 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 += `<path d="M ${x0.toFixed(1)} ${y0.toFixed(1)} A ${(gm.r * scale).toFixed(1)} ${(gm.r * scale).toFixed(1)} 0 0 1 ${x1.toFixed(1)} ${y1.toFixed(1)}" fill="none" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
} else if (gm.kind === 'tick') {
|
||||||
|
g += `<line x1="${nx(gm.x1).toFixed(1)}" y1="${ny(gm.y1).toFixed(1)}" x2="${nx(gm.x2).toFixed(1)}" y2="${ny(gm.y2).toFixed(1)}" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
} else if (gm.kind === 'text') {
|
||||||
|
g += `<text x="${nx(gm.x).toFixed(1)}" y="${ny(gm.y).toFixed(1)}" font-size="${(gm.size * scale).toFixed(0)}" font-family="'Bradley Hand','Segoe Script','Comic Sans MS',cursive" fill-opacity="0.9" transform="rotate(${((gm.rot || 0) * 57.3).toFixed(1)} ${nx(gm.x).toFixed(1)} ${ny(gm.y).toFixed(1)})">${esc(gm.text)}</text>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
media += g + `</g>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Assemble ---------- */
|
/* ---------- Assemble ---------- */
|
||||||
let s = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
let s = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
||||||
s += `<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n`;
|
s += `<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n`;
|
||||||
@@ -2343,6 +2627,7 @@ function renderSVG(scene, params, sizePx = 4800) {
|
|||||||
s += layer('fiducials', 'Fiducials', fids);
|
s += layer('fiducials', 'Fiducials', fids);
|
||||||
s += layer('vignette', 'Vignette', vign);
|
s += layer('vignette', 'Vignette', vign);
|
||||||
s += layer('header', 'Archival header', header);
|
s += layer('header', 'Archival header', header);
|
||||||
|
s += layer('media', 'Media & hand', media);
|
||||||
s += `</svg>\n`;
|
s += `</svg>\n`;
|
||||||
return s;
|
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' },
|
{ 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 = [
|
const SELECTS = [
|
||||||
@@ -2798,6 +3090,8 @@ const TOGGLES = [
|
|||||||
{ id: 'showBoundary', label: 'Chamber boundary', value: true, mode: 'render' },
|
{ id: 'showBoundary', label: 'Chamber boundary', value: true, mode: 'render' },
|
||||||
{ id: 'showHeader', label: 'Archival header', value: true, mode: 'render' },
|
{ id: 'showHeader', label: 'Archival header', value: true, mode: 'render' },
|
||||||
{ id: 'invert', label: 'Invert · photographic positive', 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. */
|
/* 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
|
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,
|
hueShift: 0, hueCycles: 3, diskSpectrum: 0, halo: 0, haloHue: 0.55,
|
||||||
paperTone: 'cream', toneStrength: 1.0, paperBright: 1.0, glow: 0.5,
|
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') {
|
if (arch === 'dense') {
|
||||||
|
|||||||
477
output/iterations-claude-craft/43_magenta-handnote.svg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
477
output/iterations-claude-craft/44_magenta-studied.svg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
460
output/iterations-claude-craft/45_curls.svg
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
440
output/iterations-claude-craft/46_ember-ends.svg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
440
output/iterations-claude-craft/47_the-eye.svg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
440
output/iterations-claude-craft/48_worn-rim.svg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
440
output/iterations-claude-craft/49_nucleation.svg
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
525
output/iterations-claude-craft/50_through-fog.svg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
440
output/iterations-claude-craft/51_prism.svg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
440
output/iterations-claude-craft/52_aura.svg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
443
output/iterations-claude-craft/53_scanners-mark.svg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
443
output/iterations-claude-craft/54_the-strip.svg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
@@ -61,4 +61,22 @@ figcaption{padding:8px 10px;color:#e8e4d8}</style></head><body>
|
|||||||
<figure><img src="40_magenta-selenium-deep.svg"><figcaption>On selenium mauve · deep focus<br><span style=color:#555>40_magenta-selenium-deep.svg · MESON-5113</span></figcaption></figure>
|
<figure><img src="40_magenta-selenium-deep.svg"><figcaption>On selenium mauve · deep focus<br><span style=color:#555>40_magenta-selenium-deep.svg · MESON-5113</span></figcaption></figure>
|
||||||
<figure><img src="41_magenta-black-halo.svg"><figcaption>Black + faint magenta halo<br><span style=color:#555>41_magenta-black-halo.svg · KAON-2026</span></figcaption></figure>
|
<figure><img src="41_magenta-black-halo.svg"><figcaption>Black + faint magenta halo<br><span style=color:#555>41_magenta-black-halo.svg · KAON-2026</span></figcaption></figure>
|
||||||
<figure><img src="42_magenta-darksepia.svg"><figcaption>★ 37's recipe on a dark sepia ground — the contender<br><span style=color:#555>42_magenta-darksepia.svg · ISOSPIN-1003</span></figcaption></figure>
|
<figure><img src="42_magenta-darksepia.svg"><figcaption>★ 37's recipe on a dark sepia ground — the contender<br><span style=color:#555>42_magenta-darksepia.svg · ISOSPIN-1003</span></figcaption></figure>
|
||||||
|
</div>
|
||||||
|
<h1 style="margin-top:34px">Round 7 · humanized — the worked-over film frame (40 refined)</h1>
|
||||||
|
<p style="color:#777">grease-pencil hand marks · film furniture (sprockets, data box, edge text) · réseau grid · tape splice</p><div class=grid>
|
||||||
|
<figure><img src="43_magenta-handnote.svg"><figcaption>Restrained — a marked film frame (ring · arrow · Nº · sprockets)<br><span style=color:#555>43_magenta-handnote.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="44_magenta-studied.svg"><figcaption>Studied — full grease + réseau + tape splice<br><span style=color:#555>44_magenta-studied.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
</div>
|
||||||
|
<h1 style="margin-top:34px">Round 8 · theme & variations on 44 — one craft each</h1>
|
||||||
|
<p style="color:#777">all MESON-5113 · magenta family on selenium · each piece foregrounds a single element of craft</p><div class=grid>
|
||||||
|
<figure><img src="45_curls.svg"><figcaption>① The curl — δ-ray logarithmic spirals turned up, the nautilus rhyme<br><span style=color:#555>45_curls.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="46_ember-ends.svg"><figcaption>② The ember — glowing deaths on black, saturated, rising intensity<br><span style=color:#555>46_ember-ends.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="47_the-eye.svg"><figcaption>③ The eye — bright striated disk core, the pupil/sun anchor<br><span style=color:#555>47_the-eye.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="48_worn-rim.svg"><figcaption>④ The worn rim — eroded, stained, mottled disk edge; time made visible<br><span style=color:#555>48_worn-rim.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="49_nucleation.svg"><figcaption>⑤ Nucleation — dense bubbles resolving into lines, the discrete made continuous<br><span style=color:#555>49_nucleation.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="50_through-fog.svg"><figcaption>⑥ Through fog — depth-of-field, soft deep tracks; the chamber as volume<br><span style=color:#555>50_through-fog.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="51_prism.svg"><figcaption>⑦ The prism — iridescent oil-on-water disk; the one note of full spectrum<br><span style=color:#555>51_prism.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="52_aura.svg"><figcaption>⑧ The aura — chromatic halo around the trails; halation, breath<br><span style=color:#555>52_aura.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="53_scanners-mark.svg"><figcaption>⑨ The scanner's mark — full grease-pencil hand: ring, arrow, Nº, angle<br><span style=color:#555>53_scanners-mark.svg · MESON-5113</span></figcaption></figure>
|
||||||
|
<figure><img src="54_the-strip.svg"><figcaption>⑩ The strip — film as object: sprockets, edge text, réseau, tape splice<br><span style=color:#555>54_the-strip.svg · MESON-5113</span></figcaption></figure>
|
||||||
</div></body></html>
|
</div></body></html>
|
||||||
|
|||||||
@@ -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 viewer can move around. The strongest fusion of your layered-print plan and
|
||||||
the chamber's actual geometry.
|
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-
|
- **Plate series / typology:** deterministic grids (MUON-001…024), contact-sheet-
|
||||||
as-artwork, a fabricated catalogue with consistent archival furniture.
|
as-artwork, a fabricated catalogue with consistent archival furniture.
|
||||||
- **Annotation fiction:** hand-drawn measurement marks — angle arcs at vertices,
|
- **Annotation fiction:** hand-drawn measurement marks — angle arcs at vertices,
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ function buildPanel() {
|
|||||||
addSelect(grp, 'palette');
|
addSelect(grp, 'palette');
|
||||||
addSelect(grp, 'paperTone');
|
addSelect(grp, 'paperTone');
|
||||||
}
|
}
|
||||||
|
if (g.title === 'Media & Hand') {
|
||||||
|
addToggle(grp, 'filmEdge');
|
||||||
|
addToggle(grp, 'splice');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,104 @@ export function renderCanvasPhoto(ctx, w, h, scene, params, opts = {}) {
|
|||||||
}
|
}
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
ctx.globalAlpha = 1;
|
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 ---- */
|
/* ---- shock disk ---- */
|
||||||
|
|||||||
@@ -220,6 +220,69 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
|||||||
+ `</g>`;
|
+ `</g>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 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 = `<g stroke="${ink}" stroke-opacity="${M.reseau.opacity.toFixed(3)}" stroke-width="${1 * u}">`;
|
||||||
|
const ss = M.reseau.size * scale;
|
||||||
|
for (const m of M.reseau.marks) {
|
||||||
|
const x = nx(m.x), y = ny(m.y);
|
||||||
|
g += `<line x1="${(x - ss).toFixed(1)}" y1="${y.toFixed(1)}" x2="${(x + ss).toFixed(1)}" y2="${y.toFixed(1)}"/><line x1="${x.toFixed(1)}" y1="${(y - ss).toFixed(1)}" x2="${x.toFixed(1)}" y2="${(y + ss).toFixed(1)}"/>`;
|
||||||
|
}
|
||||||
|
media += g + `</g>`;
|
||||||
|
}
|
||||||
|
if (M.splice) {
|
||||||
|
const yc = ny(M.splice.y), hh = M.splice.h * scale, deg = M.splice.tilt * 57.3;
|
||||||
|
media += `<g transform="rotate(${deg.toFixed(2)} ${(w / 2).toFixed(1)} ${yc.toFixed(1)})">`
|
||||||
|
+ `<rect x="0" y="${(yc - hh).toFixed(1)}" width="${w}" height="${(hh * 2).toFixed(1)}" fill="${ink}" fill-opacity="${M.splice.opacity.toFixed(3)}"/>`
|
||||||
|
+ `<line x1="0" y1="${(yc - hh).toFixed(1)}" x2="${w}" y2="${(yc - hh).toFixed(1)}" stroke="${ink}" stroke-opacity="${Math.min(0.6, M.splice.opacity * 2).toFixed(3)}" stroke-width="${u}"/>`
|
||||||
|
+ `<line x1="0" y1="${(yc + hh).toFixed(1)}" x2="${w}" y2="${(yc + hh).toFixed(1)}" stroke="${ink}" stroke-opacity="${Math.min(0.6, M.splice.opacity * 2).toFixed(3)}" stroke-width="${u}"/></g>`;
|
||||||
|
}
|
||||||
|
if (M.film) {
|
||||||
|
const f = M.film, m = 0.965;
|
||||||
|
media += `<rect x="${nx(-m).toFixed(1)}" y="${ny(-m).toFixed(1)}" width="${((nx(m) - nx(-m))).toFixed(1)}" height="${((ny(m) - ny(-m))).toFixed(1)}" fill="none" stroke="${ink}" stroke-opacity="0.5" stroke-width="${2 * u}"/>`;
|
||||||
|
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 += `<rect x="${(sx - sw / 2).toFixed(1)}" y="${(ny(y) - sh / 2).toFixed(1)}" width="${sw.toFixed(1)}" height="${sh.toFixed(1)}" rx="${rx.toFixed(1)}" fill="${holeFill}" fill-opacity="0.7" stroke="${ink}" stroke-opacity="0.4" stroke-width="${u}"/>`;
|
||||||
|
}
|
||||||
|
media += g;
|
||||||
|
media += `<g fill="${ink}" font-family="'JetBrains Mono', monospace">`
|
||||||
|
+ `<text x="${nx(-0.935).toFixed(1)}" y="${ny(0).toFixed(1)}" font-size="${(7 * u).toFixed(0)}" fill-opacity="0.45" text-anchor="middle" transform="rotate(-90 ${nx(-0.935).toFixed(1)} ${ny(0).toFixed(1)})">${esc(f.edgeText)}</text>`;
|
||||||
|
f.dataBox.forEach((line, i) => { media += `<text x="${nx(-0.9).toFixed(1)}" y="${(ny(0.8) + i * 12 * u).toFixed(1)}" font-size="${(9 * u).toFixed(0)}" fill-opacity="0.5">${esc(line)}</text>`; });
|
||||||
|
media += `</g>`;
|
||||||
|
}
|
||||||
|
if (M.grease && M.grease.length) {
|
||||||
|
const ch = inv ? '#9c1e1e' : '#f0e296';
|
||||||
|
let g = `<g stroke="${ch}" fill="${ch}" stroke-linecap="round" stroke-linejoin="round">`;
|
||||||
|
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 += `<path d="${d}" fill="none" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
if (gm.kind === 'arrow') {
|
||||||
|
const { x, y, ang } = gm.tip, hl = 0.03 * scale;
|
||||||
|
g += `<path d="M ${nx(x).toFixed(1)} ${ny(y).toFixed(1)} L ${(nx(x) - Math.cos(ang - 0.4) * hl).toFixed(1)} ${(ny(y) - Math.sin(ang - 0.4) * hl).toFixed(1)} M ${nx(x).toFixed(1)} ${ny(y).toFixed(1)} L ${(nx(x) - Math.cos(ang + 0.4) * hl).toFixed(1)} ${(ny(y) - Math.sin(ang + 0.4) * hl).toFixed(1)}" fill="none" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
}
|
||||||
|
} 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 += `<path d="M ${x0.toFixed(1)} ${y0.toFixed(1)} A ${(gm.r * scale).toFixed(1)} ${(gm.r * scale).toFixed(1)} 0 0 1 ${x1.toFixed(1)} ${y1.toFixed(1)}" fill="none" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
} else if (gm.kind === 'tick') {
|
||||||
|
g += `<line x1="${nx(gm.x1).toFixed(1)}" y1="${ny(gm.y1).toFixed(1)}" x2="${nx(gm.x2).toFixed(1)}" y2="${ny(gm.y2).toFixed(1)}" stroke-opacity="0.85" stroke-width="${(gm.width * u).toFixed(2)}"/>`;
|
||||||
|
} else if (gm.kind === 'text') {
|
||||||
|
g += `<text x="${nx(gm.x).toFixed(1)}" y="${ny(gm.y).toFixed(1)}" font-size="${(gm.size * scale).toFixed(0)}" font-family="'Bradley Hand','Segoe Script','Comic Sans MS',cursive" fill-opacity="0.9" transform="rotate(${((gm.rot || 0) * 57.3).toFixed(1)} ${nx(gm.x).toFixed(1)} ${ny(gm.y).toFixed(1)})">${esc(gm.text)}</text>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
media += g + `</g>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Assemble ---------- */
|
/* ---------- Assemble ---------- */
|
||||||
let s = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
let s = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
||||||
s += `<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n`;
|
s += `<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n`;
|
||||||
@@ -233,6 +296,7 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
|||||||
s += layer('fiducials', 'Fiducials', fids);
|
s += layer('fiducials', 'Fiducials', fids);
|
||||||
s += layer('vignette', 'Vignette', vign);
|
s += layer('vignette', 'Vignette', vign);
|
||||||
s += layer('header', 'Archival header', header);
|
s += layer('header', 'Archival header', header);
|
||||||
|
s += layer('media', 'Media & hand', media);
|
||||||
s += `</svg>\n`;
|
s += `</svg>\n`;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/scene/media.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ export function paramsFromSeed(seed) {
|
|||||||
invert: true, palette: 'mono', saturation: 1.0, // canonical look is B&W; colour is opt-in
|
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,
|
hueShift: 0, hueCycles: 3, diskSpectrum: 0, halo: 0, haloHue: 0.55,
|
||||||
paperTone: 'cream', toneStrength: 1.0, paperBright: 1.0, glow: 0.5,
|
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') {
|
if (arch === 'dense') {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { spawnVDecay } from './vdecay.js';
|
|||||||
import { generateShock } from './shock.js';
|
import { generateShock } from './shock.js';
|
||||||
import { generateArtifacts } from './artifacts.js';
|
import { generateArtifacts } from './artifacts.js';
|
||||||
import { generateInstrument } from './instrument.js';
|
import { generateInstrument } from './instrument.js';
|
||||||
|
import { generateMedia } from './media.js';
|
||||||
import { cyrb53 } from '../rng.js';
|
import { cyrb53 } from '../rng.js';
|
||||||
|
|
||||||
const LABS = ['BEBC · CERN', 'GARGAMELLE · CERN', '2m HBC · CERN', '82" HBC · SLAC', 'MIRABELLE · IHEP'];
|
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 exposure = `${year}.${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}`;
|
||||||
const lab = pick(makeRng(params.seed, 'lab'), LABS);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/ui/controls.js
vendored
@@ -85,6 +85,13 @@ export const GROUPS = [
|
|||||||
{ id: 'haloHue', label: 'Halo hue', min: 0, max: 1, step: 0.005, value: 0.55, mode: 'render' },
|
{ 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 = [
|
export const SELECTS = [
|
||||||
@@ -105,6 +112,8 @@ export const TOGGLES = [
|
|||||||
{ id: 'showBoundary', label: 'Chamber boundary', value: true, mode: 'render' },
|
{ id: 'showBoundary', label: 'Chamber boundary', value: true, mode: 'render' },
|
||||||
{ id: 'showHeader', label: 'Archival header', value: true, mode: 'render' },
|
{ id: 'showHeader', label: 'Archival header', value: true, mode: 'render' },
|
||||||
{ id: 'invert', label: 'Invert · photographic positive', 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. */
|
/* Fixed (non-UI) params with sensible defaults. */
|
||||||
|
|||||||