// AFTERLIGHT film 3 — The Last Beacon // Keyframe module. Plain browser JS, Canvas 2D only, deterministic. (function () { // ---------- shared drawing kit ---------- const W0 = 1280, H0 = 720; function rng(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; } function setup(ctx, w, h) { ctx.save(); ctx.scale(w / W0, h / H0); } // palette: tinted night layers const P = { skyTop: '#121827', skyMid: '#2b3352', dusk: '#b8683f', duskHi: '#e0a060', far: '#323a58', mid: '#1d2238', near: '#0c0e18', black: '#06070c', storm: '#161a28', fire: '#ffb347', fireCore: '#fff1c4', ember: '#c4471c', warm: '#f0b060', rim: '#ff9a3c' }; function sky(ctx, top, mid, low, lowY) { const g = ctx.createLinearGradient(0, 0, 0, lowY); g.addColorStop(0, top); g.addColorStop(0.6, mid); g.addColorStop(1, low); ctx.fillStyle = g; ctx.fillRect(0, 0, W0, H0); } // ridge silhouette: y(x) = base + sum of sines (seeded phases) + optional peaks function ridgeY(x, o) { let y = o.base; for (let i = 0; i < o.f.length; i++) y += Math.sin(x * o.f[i] + o.ph[i]) * o.a[i]; if (o.peaks) for (const p of o.peaks) { const d = (x - p.x) / p.wd; y -= p.ht * Math.exp(-d * d); } return y; } function ridge(ctx, o, color) { ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(0, H0); for (let x = 0; x <= W0; x += 8) ctx.lineTo(x, ridgeY(x, o)); ctx.lineTo(W0, H0); ctx.closePath(); ctx.fill(); } function R(base, seed, amp, peaks) { const r = rng(seed); return { base, f: [0.004 + r() * 0.002, 0.011 + r() * 0.004, 0.031 + r() * 0.01], ph: [r() * 6, r() * 6, r() * 6], a: [amp, amp * 0.4, amp * 0.12], peaks }; } function glow(ctx, x, y, r, col, a) { const g = ctx.createRadialGradient(x, y, 0, x, y, r); g.addColorStop(0, col); g.addColorStop(1, 'rgba(0,0,0,0)'); ctx.globalAlpha = a; ctx.fillStyle = g; ctx.fillRect(x - r, y - r, r * 2, r * 2); ctx.globalAlpha = 1; } // a flame stand: tongues of fire, seeded so each fire has its own shape function flame(ctx, x, y, size, seed, strength) { const r = rng(seed); glow(ctx, x, y - size * 0.6, size * 4, 'rgba(255,150,60,0.9)', 0.45 * strength); const tongues = 7; for (let i = 0; i < tongues; i++) { const dx = (r() - 0.5) * size * 1.1, hgt = size * (1.2 + r() * 1.6) * strength, wd = size * (0.25 + r() * 0.25); ctx.fillStyle = i % 3 === 0 ? P.ember : P.fire; ctx.beginPath(); ctx.moveTo(x + dx - wd, y); ctx.quadraticCurveTo(x + dx - wd * 0.8, y - hgt * 0.6, x + dx + (r() - 0.5) * wd, y - hgt); ctx.quadraticCurveTo(x + dx + wd * 0.8, y - hgt * 0.5, x + dx + wd, y); ctx.closePath(); ctx.fill(); } ctx.fillStyle = P.fireCore; ctx.beginPath(); ctx.ellipse(x, y - size * 0.35 * strength, size * 0.35, size * 0.6 * strength, 0, 0, Math.PI * 2); ctx.fill(); } // beacon crib: stone footing + log-cabin stack of split wood function crib(ctx, x, y, s, col, seed, rimCol) { const r = rng(seed); ctx.fillStyle = col; // stone footing ctx.beginPath(); ctx.moveTo(x - 70 * s, y); ctx.lineTo(x - 62 * s, y - 16 * s); ctx.lineTo(x + 62 * s, y - 16 * s); ctx.lineTo(x + 70 * s, y); ctx.closePath(); ctx.fill(); for (let i = 0; i < 9; i++) { const yy = y - 16 * s - i * 11 * s, inset = i * 3.2 * s, cross = i % 2; if (cross) { // log ends facing camera for (const sx of [-1, 1]) { ctx.beginPath(); ctx.ellipse(x + sx * (46 * s - inset), yy - 5 * s, 7 * s, 5.5 * s, 0, 0, Math.PI * 2); ctx.fill(); } ctx.fillRect(x - 46 * s + inset, yy - 9 * s, 92 * s - inset * 2, 4 * s); } else { ctx.fillRect(x - 58 * s + inset + r() * 3 * s, yy - 10 * s, 116 * s - inset * 2, 9 * s); } } // brush crown for (let i = 0; i < 26; i++) { const a = -Math.PI / 2 + (r() - 0.5) * 1.8, L = (14 + r() * 22) * s, bx = x + (r() - 0.5) * 50 * s, by = y - 112 * s; ctx.strokeStyle = col; ctx.lineWidth = 2 * s; ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(bx + Math.cos(a) * L, by + Math.sin(a) * L); ctx.stroke(); } if (rimCol) { // rim light on the right edge of the stack ctx.strokeStyle = rimCol; ctx.lineWidth = 1.5 * s; ctx.beginPath(); ctx.moveTo(x + 70 * s, y); ctx.lineTo(x + 30 * s, y - 112 * s); ctx.stroke(); } } // human silhouette. feet at (x,y), height hgt. pose controls arms. // pose: {face:1|-1, lean, lArm:[angle,len,elbowBend], rArm:..., hood, cloak, stride} function person(ctx, x, y, hgt, col, pose) { const u = hgt / 100, f = pose.face || 1, lean = (pose.lean || 0) * u; ctx.fillStyle = col; ctx.strokeStyle = col; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; const hipY = y - 48 * u, shY = y - 80 * u, hx = x + lean * 0.4, sx = x + lean; // legs const st = (pose.stride || 0.12) * 40 * u; ctx.lineWidth = 8 * u; ctx.beginPath(); ctx.moveTo(hx - 3 * u, hipY); ctx.lineTo(x - st, y - 2 * u); ctx.stroke(); ctx.beginPath(); ctx.moveTo(hx + 3 * u, hipY); ctx.lineTo(x + st, y - 2 * u); ctx.stroke(); // feet ctx.beginPath(); ctx.ellipse(x - st + f * 3 * u, y - 2 * u, 6 * u, 2.5 * u, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(x + st + f * 3 * u, y - 2 * u, 6 * u, 2.5 * u, 0, 0, Math.PI * 2); ctx.fill(); // torso / cloak ctx.beginPath(); if (pose.cloak) { ctx.moveTo(sx - 11 * u, shY); ctx.lineTo(sx + 11 * u, shY); ctx.quadraticCurveTo(hx + 20 * u, y - 40 * u, hx + 17 * u, y - 20 * u); ctx.lineTo(hx - 17 * u, y - 18 * u); ctx.quadraticCurveTo(hx - 20 * u, y - 40 * u, sx - 11 * u, shY); } else { ctx.moveTo(sx - 11 * u, shY); ctx.lineTo(sx + 11 * u, shY); ctx.lineTo(hx + 9 * u, hipY + 4 * u); ctx.lineTo(hx - 9 * u, hipY + 4 * u); } ctx.closePath(); ctx.fill(); // head + neck const headX = sx + f * 1.5 * u + lean * 0.15, headY = shY - 12 * u; ctx.lineWidth = 6 * u; ctx.beginPath(); ctx.moveTo(sx, shY); ctx.lineTo(headX, headY); ctx.stroke(); ctx.beginPath(); ctx.ellipse(headX, headY - 2 * u, 7.5 * u, 9 * u, 0, 0, Math.PI * 2); ctx.fill(); // nose hint (profile) ctx.beginPath(); ctx.moveTo(headX + f * 6.5 * u, headY - 3 * u); ctx.lineTo(headX + f * 9.5 * u, headY + 1 * u); ctx.lineTo(headX + f * 6 * u, headY + 2 * u); ctx.fill(); if (pose.hood) { ctx.beginPath(); ctx.moveTo(headX - f * 10 * u, headY + 8 * u); ctx.quadraticCurveTo(headX - f * 12 * u, headY - 14 * u, headX + f * 2 * u, headY - 12 * u); ctx.quadraticCurveTo(headX + f * 7 * u, headY - 10 * u, headX + f * 5 * u, headY - 4 * u); ctx.lineTo(headX - f * 4 * u, headY + 9 * u); ctx.fill(); } // arms: shoulder -> elbow -> hand, angles in radians (0 = right, PI/2 = down) function arm(side, spec) { if (!spec) return; const [ang, len, bend] = spec, L = len * u; const ax = sx + side * 9 * u, ay = shY + 3 * u; const ex = ax + Math.cos(ang) * L * 0.5, ey = ay + Math.sin(ang) * L * 0.5; const a2 = ang + (bend || 0); const hx2 = ex + Math.cos(a2) * L * 0.5, hy2 = ey + Math.sin(a2) * L * 0.5; ctx.lineWidth = 6.5 * u; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ex, ey); ctx.lineTo(hx2, hy2); ctx.stroke(); // hand: palm + thumb so it reads as a hand ctx.beginPath(); ctx.ellipse(hx2 + Math.cos(a2) * 3 * u, hy2 + Math.sin(a2) * 3 * u, 4.6 * u, 3.4 * u, a2, 0, Math.PI * 2); ctx.fill(); ctx.lineWidth = 2.2 * u; ctx.beginPath(); ctx.moveTo(hx2, hy2); ctx.lineTo(hx2 + Math.cos(a2 - 0.9) * 6 * u, hy2 + Math.sin(a2 - 0.9) * 6 * u); ctx.stroke(); } arm(-1, pose.lArm); arm(1, pose.rArm); if (pose.staff) { ctx.lineWidth = 3 * u; ctx.beginPath(); ctx.moveTo(pose.staff[0], pose.staff[1]); ctx.lineTo(pose.staff[2], pose.staff[3]); ctx.stroke(); } } function stormFront(ctx, x0, x1, yTop, yBase, seed, col, rainCol, rainN) { const r = rng(seed); ctx.fillStyle = col; ctx.beginPath(); ctx.moveTo(x0, yTop); for (let x = x0; x <= x1; x += 22) ctx.lineTo(x, yBase - r() * 30 - Math.sin(x * 0.01) * 18); ctx.lineTo(x1, yTop); ctx.closePath(); ctx.fill(); // billows for (let i = 0; i < 18; i++) { ctx.beginPath(); ctx.arc(x0 + r() * (x1 - x0), yBase - 20 - r() * 60, 30 + r() * 50, 0, Math.PI * 2); ctx.fill(); } ctx.strokeStyle = rainCol; ctx.lineWidth = 1; for (let i = 0; i < rainN; i++) { const rx = x0 + r() * (x1 - x0), ry = yBase - 10 + r() * 140, L = 40 + r() * 60; ctx.globalAlpha = 0.25 + r() * 0.35; ctx.beginPath(); ctx.moveTo(rx, ry); ctx.lineTo(rx - L * 0.35, ry + L); ctx.stroke(); } ctx.globalAlpha = 1; } function stars(ctx, seed, n, yMax, a) { const r = rng(seed); ctx.fillStyle = '#dfe6ff'; for (let i = 0; i < n; i++) { ctx.globalAlpha = a * (0.3 + r() * 0.7); ctx.fillRect(r() * W0, r() * yMax, 1.4, 1.4); } ctx.globalAlpha = 1; } function house(ctx, x, y, s, col, lit) { // small stone house: walls + pitched roof + window ctx.fillStyle = col; ctx.fillRect(x, y - 30 * s, 46 * s, 30 * s); ctx.beginPath(); ctx.moveTo(x - 5 * s, y - 30 * s); ctx.lineTo(x + 23 * s, y - 50 * s); ctx.lineTo(x + 51 * s, y - 30 * s); ctx.fill(); if (lit) { ctx.fillStyle = P.warm; ctx.fillRect(x + 28 * s, y - 22 * s, 7 * s, 7 * s); glow(ctx, x + 31 * s, y - 18 * s, 18 * s, 'rgba(240,170,90,0.8)', 0.35); } } function grain(ctx, seed, n, a) { const r = rng(seed); for (let i = 0; i < n; i++) { ctx.fillStyle = r() > 0.5 ? '#fff' : '#000'; ctx.globalAlpha = a * r(); ctx.fillRect(r() * W0, r() * H0, 1.5, 1.5); } ctx.globalAlpha = 1; } function vignette(ctx, a) { const g = ctx.createRadialGradient(W0 / 2, H0 / 2, H0 * 0.35, W0 / 2, H0 / 2, H0 * 0.95); g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, 'rgba(0,0,0,' + a + ')'); ctx.fillStyle = g; ctx.fillRect(0, 0, W0, H0); } // shared geography (seeded once; the same valley in every frame) const FAR_PEAK = R(330, 31, 14, [{ x: 250, wd: 90, ht: 70 }]); // the first beacon's summit (upper left) const MID = R(430, 47, 20); // valley floor's far bank const NEAR = R(560, 59, 26, [{ x: 760, wd: 260, ht: 70 }]); // keeper's ridge // ---------- references (all checked) ---------- const REF = { lookout: { kind: 'archive footage', title: 'Fire Lookout', creator: 'Dawson (Larry) Productions; publ. International Film Bureau', date: '1957', url: 'https://archive.org/details/0865_Fire_Lookout_19_14_59_00', rights: 'unknown — not granted', use: 'rotoscope reference for a watcher scanning a horizon from a high post (posture, head turns); Prelinger item, no licence stated on the item page', verified: true }, clouds: { kind: 'archive footage', title: 'Clouds', creator: 'U.S. Department of Agriculture, Educational Film Service (with the Weather Bureau)', date: 'c. 1920s', url: 'https://archive.org/details/0928_Clouds_15_46_11_14', rights: 'unknown — not granted', use: 'candidate sky plate for the approaching weather; item page has no shot description, so storm content must be checked by viewing', verified: true }, thunder: { kind: 'archive footage', title: 'Project Thunderstorm, 05/1947 - 09/1947', creator: 'National Archives and Records Administration (USAF / Navy / Weather Bureau project film)', date: '1947', url: 'https://www.movingimagearchive.com/sources/project-thunderstorm-05-1947-09-1947-e8ef7ef6?clip=c70b7c6e-8937-5869-a047-5ea223325914', rights: 'public domain', use: 'background plate: storm cloud mass matted behind the cut-paper far ridge', verified: true }, bolt: { kind: 'archive footage', title: 'Thunderbolt Hunters', creator: 'Wilding Picture Productions', date: '1941', url: 'https://archive.org/details/0645_Thunderbolt_Hunters_02_50_38_00', rights: 'unknown — not granted', use: 'lighting reference for storm flashes on the ridge line (item description lists storm and lightning footage)', verified: true }, rainier: { kind: 'archive footage', title: 'Above the Clouds in Rainier National Park', creator: 'unknown (Prelinger Archives, uploaded by A/V Geeks)', date: 'c. 1920s', url: 'https://archive.org/details/0975_Above_the_Clouds_in_Rainier_National_Park_01_28_03_02', rights: 'unknown — not granted', use: 'candidate mountain-ridge plate at dusk for the far summit; no shot description on the item page', verified: true }, pioneers: { kind: 'archive footage', title: 'Kentucky Pioneers', creator: 'unknown (Prelinger Archives)', date: '1941', url: 'https://www.movingimagearchive.com/sources/kentucky-pioneers-061890c6?clip=1afe5116-344d-50c4-aae4-249fe8268590', rights: 'public domain', use: 'rotoscope / costume reference: settlers sharing shelter and keeping watch at a frontier fort, cabin timberwork for the shelter door', verified: true }, forestfire: { kind: 'archive footage', title: 'Forest Fire...Naturally!', creator: 'U.S. National Park Service (Sequoia and Kings Canyon NP); dir. George S. Briggs', date: '1976', url: 'https://archive.org/details/0894_Forest_Fire_Naturally_08_20_14_00', rights: 'unknown — not granted', use: 'texture / lighting reference for flame on wood at close range; federal production but no licence stated on the item page', verified: true }, survival: { kind: 'archive footage', title: 'Survival Stresses', creator: 'U.S. Department of the Air Force, Air Photographic and Charting Service', date: '1961', url: 'https://www.movingimagearchive.com/sources/survival-stresses?clip=8101dc12-b8ea-53b9-b87f-c5af16b758c7', rights: 'public domain', use: 'candidate only: cold-weather survival scenes; no flint-and-tinder shot is confirmed on the page — hands must be rotoscoped from a live shoot if none is found', verified: true }, currier: { kind: 'still image', title: 'Signal fires on the Slievenamon Mountains - Ireland, 1848', creator: 'N. Currier (lithographer/publisher)', date: '1848', url: 'https://commons.wikimedia.org/wiki/File:Signal_fires_on_the_Slievenamon_Mountains_-_Ireland,_1848_LCCN2001705841.jpg', rights: 'public domain', use: 'composition reference: figures tending a hilltop fire. Contents and PD-1923 tag confirmed on a Commons mirror (download.osmand.net); the Commons URL itself did not load in the fetch tool', verified: false }, tower: { kind: 'still image', title: 'A beacon tower on the Great Wall of China', creator: 'William Cooper collection (photographer not named)', date: 'est. 1912–1919', url: 'https://www.hpcbristol.sjtu.edu.cn/visual/WC01-173', rights: 'unknown — not granted', use: 'reference for a built, permanent beacon station (mass and profile of the summit cairn); Historical Photographs of China, University of Bristol', verified: true }, aeschylus: { kind: 'source text', title: 'Aeschylus, Agamemnon 281–316 (beacon speech), trans. H. W. Smyth (1926)', creator: 'Aeschylus; Perseus Digital Library', date: '458 BCE / 1926', url: 'https://atlas.perseus.tufts.edu/library/passage/urn:cts:greekLit:tlg0085.tlg005.perseus-eng3:245-317/text', rights: 'public domain', use: 'citation: the counter-model. "Beacon passed beacon on to us by courier-flame" is exactly the travelling-fire image this film refuses', verified: true }, polybius: { kind: 'source text', title: 'Polybius, Histories 10.43–47 (fire-signalling), Loeb trans. W. R. Paton', creator: 'Polybius; LacusCurtius (Bill Thayer)', date: 'c. 150 BCE / 1922–27', url: 'https://penelope.uchicago.edu/Thayer/E/Roman/Texts/Polybius/10*.html', rights: 'public domain', use: 'citation: a simple beacon carries only what was agreed in advance (10.43.6–10) — the keeper must supply the inference', verified: true }, kent: { kind: 'research paper', title: 'The Beacon System in Kent (Archaeologia Cantiana 46, pp. 77–96)', creator: 'H. T. White', date: '1934', url: 'https://heritage.kent.gov.uk/Source/SKE7794', rights: 'unknown — not granted', use: 'citation for the staffed, provisioned English county beacon system; bibliographic record only, article not read', verified: true }, juyan: { kind: 'source text', title: 'Han frontier slips: 17 Juyan slips on beacon-fire regulations (news report)', creator: 'China Daily HK', date: '2026-01-27', url: 'https://www.chinadailyhk.com/article/627825', rights: 'unknown — not granted', use: 'citation: Han 烽燧 stations followed written rules on how many fires to light — each station decides by protocol, not by fire travelling', verified: true }, falsealarm: { kind: 'source text', title: 'Beacons (Romney Marsh history page): 1545 false alarm', creator: 'romneymarshhistory.co.uk', date: 'n.d.', url: 'https://romneymarshhistory.co.uk/beacons', rights: 'unknown — not granted', use: 'loose historical parallel for the village’s distrust: beacons lit on rumour in 1545, militia marched before being stood down. Secondary web page, not independently checked', verified: true } }; const frames = []; // ---------- KF1 · Dusk: the distant flare ---------- frames.push({ n: 1, t: 0.0, title: 'Distant flare', shot: 'Wide, from behind and left of the keeper on the near ridge; cold beacon stack mid-right, far summit upper left', action: 'At dusk the keeper stands beside the cold, stacked beacon. On the far summit a small fire appears; the keeper turns toward it and goes still.', mechanism: 'far fire → keeper’s eye (light arrives) ; flare ⇝ keeper (a warning, if the far keeper is right) ; nothing yet ⊣ the near stack', layers: [ { name: 'dusk sky plate', source: 'PHOTOGRAPHED', ref: 0 }, { name: 'far summit with first beacon (cut paper)', source: 'INVENTED', ref: 2 }, { name: 'keeper silhouette (rotoscoped watcher posture)', source: 'INVENTED', ref: 1 }, { name: 'beacon stack and near ridge (cut paper)', source: 'INVENTED', ref: null } ], prompt: 'Wide dusk landscape of layered cut-paper silhouettes: two mountain ridges with a valley between, the far summit upper left holding a single small orange signal fire. Foreground right, a hooded keeper seen from behind stands beside an unlit log-crib beacon on a stone footing. Sky graded from slate blue to amber at the horizon, low contrast, matte paper texture, tinted night layers in indigo and charcoal, no text, no logos', draw: (ctx, w, h) => { setup(ctx, w, h); sky(ctx, P.skyTop, P.skyMid, P.dusk, 420); glow(ctx, 520, 410, 420, 'rgba(230,150,80,0.6)', 0.5); stars(ctx, 101, 60, 200, 0.6); ridge(ctx, FAR_PEAK, P.far); // first beacon on far summit: a cairn and its own fire ctx.fillStyle = P.far; ctx.fillRect(244, 248, 12, 12); flame(ctx, 250, 250, 5, 7, 1); ridge(ctx, MID, P.mid); // keeper's village down in the valley, unlit for (let i = 0; i < 6; i++) house(ctx, 470 + i * 38 + (i % 2) * 8, 470 + (i % 3) * 6, 0.55, '#151a2d', false); ridge(ctx, NEAR, P.near); const gy = ridgeY(820, NEAR); crib(ctx, 820, gy + 6, 1.15, P.black, 13, null); person(ctx, 1010, ridgeY(1010, NEAR) + 10, 230, P.black, { face: -1, hood: true, cloak: true, lean: -4, lArm: [Math.PI * 0.62, 40, 0.2], rArm: [Math.PI * 0.45, 40, -0.1] }); vignette(ctx, 0.55); grain(ctx, 3, 1500, 0.08); ctx.restore(); }, refs: [REF.rainier, REF.lookout, REF.tower, REF.aeschylus] }); // ---------- KF2 · Hesitation ---------- frames.push({ n: 2, t: 13.0, title: 'Hesitation', shot: 'Medium-wide, eye level on the near ridge; three figures at the stack, storm wall over the far ridge behind them', action: 'Two villagers have come up. One holds the keeper’s forearm: last time the fire was lit for nothing. Over the far ridge the weather is already coming on.', mechanism: 'past false alarm ⊣ the keeper’s hand ; storm → valley (approaching, visible) ; far flare ⇝ villagers (doubted)', layers: [ { name: 'storm front plate', source: 'PHOTOGRAPHED', ref: 0 }, { name: 'lightning flicker on cloud edge', source: 'PHOTOGRAPHED', ref: 1 }, { name: 'far summit and flare (cut paper)', source: 'INVENTED', ref: null }, { name: 'keeper and two villagers (silhouette puppets)', source: 'INVENTED', ref: 2 } ], prompt: 'Medium-wide silhouette scene on a dark mountain ridge at last light: a hooded keeper beside a tall unlit log-crib beacon, an older villager gripping the keeper’s forearm, a second villager pointing back down toward the valley. Behind them a heavy storm wall with slanted rain curtains crosses the far ridge, a tiny orange fire still on a distant summit. Cut-paper layers, indigo and slate palette, no text, no logos', draw: (ctx, w, h) => { setup(ctx, w, h); sky(ctx, '#0f1422', '#252c47', '#7a4a3a', 420); ridge(ctx, FAR_PEAK, P.far); ctx.fillStyle = P.far; ctx.fillRect(244, 248, 12, 12); flame(ctx, 250, 250, 5, 7, 1); stormFront(ctx, 560, W0, 0, 300, 202, P.storm, '#8a93b8', 260); ridge(ctx, MID, P.mid); // nearer ground plane for medium shot const G = R(600, 73, 10); ridge(ctx, G, P.near); const gy = 606; crib(ctx, 640, gy, 1.7, P.black, 13, null); // keeper (center-right), arm reaching toward stack; elder holds that forearm person(ctx, 860, gy + 6, 300, P.black, { face: -1, hood: true, cloak: true, lean: -6, lArm: [Math.PI * 0.9, 44, -0.15], rArm: [Math.PI * 0.5, 40, 0.1] }); person(ctx, 1000, gy + 8, 270, '#090a10', { face: -1, cloak: true, lean: -14, lArm: [Math.PI * 1.02, 48, 0.12], rArm: [Math.PI * 0.55, 40, 0] }); // second villager, turned half away, pointing down to the village person(ctx, 1150, gy + 10, 285, '#0a0b12', { face: 1, lean: 4, lArm: [Math.PI * 0.55, 40, 0.1], rArm: [Math.PI * 0.18, 46, 0.05] }); vignette(ctx, 0.6); grain(ctx, 4, 1500, 0.08); ctx.restore(); }, refs: [REF.thunder, REF.bolt, REF.pioneers, REF.falsealarm, REF.polybius] }); // ---------- KF3 · Own fuel, own flint ---------- frames.push({ n: 3, t: 26.0, title: 'Own flint', shot: 'Close-up, low angle at the base of the stack; the keeper’s two hands with steel and flint over a tinder nest', action: 'The keeper kneels, strikes steel on flint into dry tinder packed under the stack, and blows. The near fire starts here, from this fuel and this spark.', mechanism: 'steel ⊣ flint → spark → tinder (ignites) ; keeper’s decision → own beacon ; far fire ⇝ reason only (no flame crosses)', layers: [ { name: 'flame-on-wood texture', source: 'PHOTOGRAPHED', ref: 0 }, { name: 'hands, steel, flint (rotoscoped from a live shoot)', source: 'INVENTED', ref: 1 }, { name: 'stack timbers and tinder (cut paper)', source: 'INVENTED', ref: null } ], prompt: 'Close-up low angle at the base of a log-crib beacon at night: two weathered hands, one holding a C-shaped fire steel, the other a flint over a nest of dry grass tinder, a few short sparks and a first small flame catching. Warm orange rim light on knuckles against near-black split logs, rain-dark sky above. Cut-paper silhouette layers with photographic flame texture, no text, no logos', draw: (ctx, w, h) => { setup(ctx, w, h); sky(ctx, '#0b0f1b', '#141a2c', '#1d2133', 720); // big log ends of the stack filling the upper frame ctx.fillStyle = '#07080d'; for (let row = 0; row < 4; row++) { const y = 360 - row * 110; if (row % 2 === 0) { ctx.fillRect(80, y - 70, 1120, 64); } else { for (const x of [200, 1080]) { ctx.beginPath(); ctx.ellipse(x, y - 40, 66, 52, 0, 0, Math.PI * 2); ctx.fill(); } ctx.fillRect(140, y - 54, 1000, 22); } } // log end grain rings on the near crossed ends ctx.strokeStyle = '#191b24'; ctx.lineWidth = 2; for (const x of [200, 1080]) for (let r = 12; r < 50; r += 11) { ctx.beginPath(); ctx.ellipse(x, 210, r * 1.25, r, 0, 0, Math.PI * 2); ctx.stroke(); ctx.beginPath(); ctx.ellipse(x, 430, r * 1.25, r, 0, 0, Math.PI * 2); ctx.stroke(); } // ground and tinder ctx.fillStyle = '#05060a'; ctx.fillRect(0, 560, W0, 160); const rr = rng(303); ctx.strokeStyle = '#3a2a18'; ctx.lineWidth = 2; for (let i = 0; i < 90; i++) { const a = rr() * Math.PI, L = 30 + rr() * 50, x = 640 + (rr() - 0.5) * 160, y = 560 + (rr() - 0.5) * 30; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(a) * L, y - Math.sin(a) * L * 0.4); ctx.stroke(); } flame(ctx, 640, 562, 26, 31, 0.9); // sparks, local only, falling from the strike into the tinder ctx.fillStyle = P.fireCore; for (let i = 0; i < 26; i++) { const x = 700 + (rr() - 0.5) * 90, y = 470 + rr() * 80; ctx.globalAlpha = 0.5 + rr() * 0.5; ctx.fillRect(x, y, 2.5, 2.5); } ctx.globalAlpha = 1; // left hand with flint (from left), right hand with C-steel (from right), striking above tinder const skin = '#120d0b', rim = P.rim; function hand(cx, cy, dir, rot, grip) { ctx.save(); ctx.translate(cx, cy); ctx.rotate(rot); ctx.scale(dir, 1); // forearm with sleeve ctx.fillStyle = '#0a0a10'; ctx.beginPath(); ctx.moveTo(-420, -70); ctx.lineTo(-120, -40); ctx.lineTo(-110, 46); ctx.lineTo(-420, 90); ctx.fill(); ctx.fillStyle = skin; ctx.beginPath(); ctx.moveTo(-120, -34); ctx.quadraticCurveTo(-40, -50, 10, -30); ctx.lineTo(18, 26); ctx.quadraticCurveTo(-50, 50, -114, 40); ctx.fill(); // palm/back // four curled fingers for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.ellipse(16, -22 + i * 14, 26, 8, 0.15, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(30, -16 + i * 14, 10, 7, 1.2, 0, Math.PI * 2); ctx.fill(); } // thumb over the object ctx.beginPath(); ctx.moveTo(-50, -38); ctx.quadraticCurveTo(0, -70, 34, -52); ctx.lineTo(38, -42); ctx.quadraticCurveTo(0, -50, -40, -26); ctx.fill(); // knuckle rim light toward the fire (below) ctx.strokeStyle = rim; ctx.lineWidth = 2.5; ctx.globalAlpha = 0.8; ctx.beginPath(); ctx.moveTo(-110, 42); ctx.quadraticCurveTo(-40, 52, 20, 34); ctx.stroke(); for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.arc(38, -16 + i * 14, 7, -0.2, 1.4); ctx.stroke(); } ctx.globalAlpha = 1; if (grip === 'flint') { ctx.fillStyle = '#2b2a2e'; ctx.beginPath(); ctx.moveTo(30, -64); ctx.lineTo(74, -78); ctx.lineTo(88, -46); ctx.lineTo(50, -36); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#7f7a74'; ctx.lineWidth = 1.5; ctx.stroke(); } if (grip === 'steel') { ctx.strokeStyle = '#4a4c55'; ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(40, -60); ctx.lineTo(40, 40); ctx.arc(54, 40, 14, Math.PI, 0, true); ctx.stroke(); ctx.strokeStyle = rim; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(44, -60); ctx.lineTo(44, 40); ctx.stroke(); } ctx.restore(); } hand(560, 440, 1, -0.12, 'flint'); hand(800, 450, -1, 0.18, 'steel'); glow(ctx, 640, 520, 260, 'rgba(255,150,60,0.7)', 0.35); vignette(ctx, 0.7); grain(ctx, 5, 1500, 0.08); ctx.restore(); }, refs: [REF.forestfire, REF.survival, REF.polybius, REF.juyan] }); // ---------- KF4 · Across the valley ---------- frames.push({ n: 4, t: 40.0, title: 'Across the valley', shot: 'Reverse angle from the far side of the valley: OTS of the observer in a house doorway, the keeper’s beacon burning small on the opposite ridge', action: 'In a doorway across the valley a woman sees the new fire on the keeper’s ridge. She turns back into the house, arm raised, calling the household awake.', mechanism: 'keeper’s fire → observer’s eye (light arrives) ; fire ⇝ observer (danger is coming, the keeper believed it) ; observer → household (wakes them)', layers: [ { name: 'night valley and storm plate', source: 'PHOTOGRAPHED', ref: 0 }, { name: 'keeper’s burning beacon, distant (cut paper + flame texture)', source: 'INVENTED', ref: null }, { name: 'observer, doorway and lit interior (silhouette puppet)', source: 'INVENTED', ref: 1 } ], prompt: 'Night reverse angle across a mountain valley: in the foreground a stone house doorway framing a woman in silhouette, half turned back into the warmly lit room, one arm raised as she calls inside. Across the dark valley a single small beacon fire burns on the opposite ridge. A storm wall with rain moves in from the right. Layered cut-paper silhouettes in indigo, charcoal and amber, no text, no logos', draw: (ctx, w, h) => { setup(ctx, w, h); sky(ctx, '#0a0e1a', '#1a2038', '#2a2b40', 460); stars(ctx, 404, 50, 220, 0.5); stormFront(ctx, 820, W0, 0, 260, 404, P.storm, '#7d86aa', 200); // opposite (keeper's) ridge seen from across the valley const OPP = R(390, 88, 16, [{ x: 640, wd: 160, ht: 60 }]); ridge(ctx, OPP, '#343d5e'); const by = ridgeY(640, OPP); ctx.fillStyle = '#343d5e'; ctx.fillRect(632, by - 14, 16, 14); flame(ctx, 640, by - 12, 8, 41, 1); // valley floor ridge(ctx, R(520, 92, 12), '#0f1322'); // house wall at left with doorway ctx.fillStyle = '#040508'; ctx.fillRect(0, 0, 420, H0); ctx.fillStyle = '#5a3a1e'; ctx.fillRect(150, 150, 190, 470); // lit interior through door const ig = ctx.createLinearGradient(150, 0, 340, 0); ig.addColorStop(0, 'rgba(240,170,90,0.9)'); ig.addColorStop(1, 'rgba(120,70,30,0.6)'); ctx.fillStyle = ig; ctx.fillRect(150, 150, 190, 470); // sleeping-place silhouettes inside: a figure rising on one elbow person(ctx, 250, 600, 120, '#2a1608', { face: 1, lean: 30, lArm: [Math.PI * 0.75, 40, -0.3], rArm: [Math.PI * 0.4, 36, 0.4] }); ctx.fillStyle = '#2a1608'; ctx.fillRect(160, 590, 170, 30); // door leaf swung open outward ctx.fillStyle = '#07080c'; ctx.beginPath(); ctx.moveTo(340, 150); ctx.lineTo(440, 120); ctx.lineTo(440, 650); ctx.lineTo(340, 620); ctx.fill(); ctx.fillStyle = '#040508'; ctx.fillRect(130, 130, 230, 20); ctx.fillRect(130, 130, 20, 500); // light spill on the ground outside ctx.fillStyle = 'rgba(220,150,80,0.18)'; ctx.beginPath(); ctx.moveTo(150, 620); ctx.lineTo(340, 620); ctx.lineTo(560, 720); ctx.lineTo(120, 720); ctx.fill(); // the observer in the doorway, body toward the room, head toward the valley, right arm raised person(ctx, 360, 640, 330, '#030306', { face: 1, cloak: true, lean: -10, lArm: [Math.PI * 1.25, 44, -0.6], rArm: [Math.PI * 0.42, 40, 0.1] }); vignette(ctx, 0.55); grain(ctx, 6, 1500, 0.08); ctx.restore(); }, refs: [REF.thunder, REF.pioneers, REF.lookout, REF.currier] }); // ---------- KF5 · The door ---------- frames.push({ n: 5, t: 52.0, title: 'The door', shot: 'Medium, static, eye level on the shelter door in the hillside; valley and ridges behind at left', action: 'Rain has reached the valley. On the far summit the first beacon has burned out; the keeper’s fire is low. The shelter door stands open and the household goes in. Hold on the door.', mechanism: 'first fire ⊣ out (spent fuel) ; keeper’s fire → low ; warning ⇝ household (believed) → shelter (entered) ; what continues is the action, not the flame', layers: [ { name: 'rain and storm plate', source: 'PHOTOGRAPHED', ref: 0 }, { name: 'shelter door timbers (cut paper after archive cabin work)', source: 'INVENTED', ref: 1 }, { name: 'household entering (silhouette puppets)', source: 'INVENTED', ref: 1 }, { name: 'dark first summit, low second fire (cut paper)', source: 'INVENTED', ref: null } ], prompt: 'Rainy night, medium static shot of a timber-framed shelter door set into a grassy hillside, open, warm light inside. An adult carrying a child and an older person with a stick step through while the woman from the house holds the door. At left behind them, the valley: a dark far summit with a thin smoke trace, a small low fire on the nearer ridge. Cut-paper silhouette layers, indigo and amber, slanting rain, no text, no logos', draw: (ctx, w, h) => { setup(ctx, w, h); sky(ctx, '#0a0d17', '#161b2c', '#1e2335', 460); // far summit, beacon dark: cairn and a wisp of smoke, no fire ridge(ctx, FAR_PEAK, '#262c43'); ctx.fillStyle = '#262c43'; ctx.fillRect(244, 248, 12, 12); ctx.strokeStyle = 'rgba(150,160,190,0.35)'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(250, 248); ctx.bezierCurveTo(240, 220, 268, 200, 252, 168); ctx.stroke(); ctx.fillStyle = '#5a2414'; ctx.fillRect(247, 256, 6, 3); // last embers only // keeper's ridge, fire burning low const OPP = R(420, 88, 12, [{ x: 520, wd: 140, ht: 50 }]); ridge(ctx, OPP, '#1a1f32'); flame(ctx, 520, ridgeY(520, OPP) - 8, 5, 41, 0.6); // rain over everything stormFront(ctx, 0, W0, 0, 120, 505, 'rgba(20,24,38,0.85)', '#8b94b6', 420); // hillside bank, right two thirds ctx.fillStyle = '#07090f'; ctx.beginPath(); ctx.moveTo(560, H0); ctx.lineTo(600, 330); ctx.quadraticCurveTo(820, 220, W0, 250); ctx.lineTo(W0, H0); ctx.fill(); // grass tufts on the bank's edge const gr = rng(55); ctx.strokeStyle = '#07090f'; ctx.lineWidth = 2; for (let i = 0; i < 70; i++) { const x = 600 + gr() * 680, y = 330 - (x - 600) * 0.12 + Math.sin(x * 0.01) * 10 - 40 * Math.max(0, (x - 600) / 680) ; ctx.beginPath(); ctx.moveTo(x, y + 10); ctx.lineTo(x + (gr() - 0.5) * 10, y - 8 - gr() * 12); ctx.stroke(); } // door frame and lit opening const dx = 820, dy = 360, dw = 190, dh = 300; const ig = ctx.createLinearGradient(0, dy, 0, dy + dh); ig.addColorStop(0, '#e8a35a'); ig.addColorStop(1, '#8a4f22'); ctx.fillStyle = ig; ctx.fillRect(dx, dy, dw, dh); glow(ctx, dx + dw / 2, dy + dh * 0.6, 340, 'rgba(240,160,80,0.7)', 0.28); ctx.fillStyle = '#030406'; ctx.fillRect(dx - 26, dy - 30, dw + 52, 30); ctx.fillRect(dx - 26, dy - 30, 26, dh + 30); ctx.fillRect(dx + dw, dy - 30, 26, dh + 30); // stone threshold and light on the wet ground ctx.fillStyle = 'rgba(232,163,90,0.22)'; ctx.beginPath(); ctx.moveTo(dx, dy + dh); ctx.lineTo(dx + dw, dy + dh); ctx.lineTo(dx + dw + 120, H0); ctx.lineTo(dx - 160, H0); ctx.fill(); // door leaf swung open toward camera-left ctx.fillStyle = '#05060a'; ctx.beginPath(); ctx.moveTo(dx - 26, dy - 30); ctx.lineTo(dx - 150, dy - 10); ctx.lineTo(dx - 150, dy + dh + 30); ctx.lineTo(dx - 26, dy + dh); ctx.fill(); ctx.strokeStyle = '#141620'; ctx.lineWidth = 3; for (let i = 1; i < 4; i++) { ctx.beginPath(); ctx.moveTo(dx - 26 - i * 31, dy - 30 + i * 5); ctx.lineTo(dx - 26 - i * 31, dy + dh + i * 7); ctx.stroke(); } // people: elder already inside the opening (backlit), adult carrying child stepping through, the observer holding the door person(ctx, 940, dy + dh, 210, '#1a0e06', { face: 1, cloak: true, lean: 8, lArm: [Math.PI * 0.5, 38, 0], rArm: [Math.PI * 0.35, 40, -0.2], staff: [985, dy + 140, 1000, dy + dh] }); person(ctx, 860, dy + dh + 20, 280, '#030305', { face: 1, cloak: true, lean: 10, lArm: [Math.PI * 0.3, 36, -1.5], rArm: [Math.PI * 0.25, 38, -1.4], stride: 0.25 }); // child held on the adult's shoulder/chest ctx.fillStyle = '#030305'; ctx.beginPath(); ctx.ellipse(880, dy + 92, 24, 34, -0.3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(892, dy + 50, 15, 0, Math.PI * 2); ctx.fill(); person(ctx, 610, dy + dh + 34, 300, '#020204', { face: 1, cloak: true, lean: 6, lArm: [Math.PI * 0.55, 40, 0], rArm: [-Math.PI * 0.05, 42, 0.05] }); vignette(ctx, 0.6); grain(ctx, 7, 1600, 0.08); ctx.restore(); }, refs: [REF.clouds, REF.pioneers, REF.thunder, REF.tower] }); window.AFTERLIGHT_FILMS.push({ id: 'F3', n: 3, title: 'The Last Beacon', role: 'Communication and responsibility', understand: 'A message continues because someone interprets and recreates it—not because the original fire travels.', medium: 'Tinted cut-paper silhouette landscape in night layers (two ridges, one valley): silhouette keeps every fire a fixed point on its own hill, so nothing can be read as travelling between them.', archivePlan: 'PHOTOGRAPHED layers are sky, storm and rain plates (Project Thunderstorm 1947, Clouds c.1920s, Thunderbolt Hunters 1941) matted behind INVENTED cut-paper ridges, beacons and silhouette puppets; flame texture comes from Forest Fire...Naturally! (1976) and is re-cut separately for each station, never shared or moved between stations. Bodies are rotoscoped from Fire Lookout (1957) and Kentucky Pioneers (1941); the flint strike must be shot live, since no confirmed archive shot exists. Every frame carries a paper-slip label: "COMPOSED SCENE · archive fragments re-performed · not a recorded event" — the settlement is fictional — and each layer is slipped PHOTOGRAPHED or INVENTED.', basis: [ { title: 'Aeschylus, Agamemnon 281–316, trans. Smyth (Perseus)', url: 'https://atlas.perseus.tufts.edu/library/passage/urn:cts:greekLit:tlg0085.tlg005.perseus-eng3:245-317/text', note: 'Clytemnestra’s chain Ida → Lemnos → Athos → Macistus → Messapion → Cithaeron → Aegiplanctus → Arachnaeus; "courier-flame" is the poetic image the film rejects. The perseus.tufts.edu/hopper page is robots-blocked; the Perseus atlas/Scaife text was checked instead.', verified: true }, { title: 'Polybius, Histories 10.43–47 (LacusCurtius, Paton)', url: 'https://penelope.uchicago.edu/Thayer/E/Roman/Texts/Polybius/10*.html', note: '10.43: simple beacons carry only pre-agreed messages; unforeseen events "defied communication by fire-signal". 10.45–46: the Cleoxenus/Democleitus torch-and-letter-tablet system. Supports the film’s point that each keeper interprets.', verified: true }, { title: 'Spanish Armada (Wikipedia)', url: 'https://en.wikipedia.org/wiki/Spanish_Armada', note: 'One sentence only: news "conveyed to London by a system of beacons" along the south coast. General confirmation of the 1588 chain, no detail.', verified: true }, { title: 'H. T. White, The Beacon System in Kent, Archaeologia Cantiana 46 (1934) 77–96', url: 'https://heritage.kent.gov.uk/Source/SKE7794', note: 'Scholarly source for the English county beacon system; only the bibliographic record was opened, not the article.', verified: true }, { title: 'Romney Marsh history: Beacons', url: 'https://romneymarshhistory.co.uk/beacons', note: 'Secondary web page: paid 24-hour watchers, shire-supplied fuel, a 1545 false alarm on rumour. Useful parallel for distrust; not independently checked.', verified: true }, { title: 'Han frontier slips on beacon-fire rules (China Daily HK, 2026)', url: 'https://www.chinadailyhk.com/article/627825', note: '17 Juyan slips set how many fires to light by size of incursion and how to start them — station-level protocol.', verified: true }, { title: '烽火台 (Chinese Wikipedia)', url: 'https://zh.wikipedia.org/wiki/%E7%83%BD%E7%81%AB%E5%8F%B0', note: 'Smoke by day (brushwood and dung), flame by night; Wujing Zongyao rule of stations c. 30 li apart, each in sight of the next. King You story is legend.', verified: true }, { title: 'A beacon tower on the Great Wall of China (Historical Photographs of China, Bristol)', url: 'https://www.hpcbristol.sjtu.edu.cn/visual/WC01-173', note: 'Photo, est. 1912–19, William Cooper collection; rights terms on a separate page, treated as not granted.', verified: true }, { title: 'N. Currier, Signal fires on the Slievenamon Mountains - Ireland, 1848 (LoC 2001705841, Wikimedia Commons)', url: 'https://commons.wikimedia.org/wiki/File:Signal_fires_on_the_Slievenamon_Mountains_-_Ireland,_1848_LCCN2001705841.jpg', note: 'PD hand-coloured lithograph of men at a hilltop signal fire. Verified on a Commons mirror only; Commons and loc.gov did not load.', verified: false } ], keyframes: frames }); })();