/* AFTERLIGHT · Film 1 · The Stone That Remembered Keyframe module. Multiplane cut paper; one room, one cast, one stone. All drawing is Canvas 2D, deterministic (seeded PRNG), no text in picture. */ (function () { 'use strict'; /* ---------- seeded random ---------- */ function rng(seed) { let a = seed >>> 0; return function () { a = (a + 0x6D2B79F5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /* ---------- geometry ---------- */ function ell(cx, cy, rx, ry, rot, n) { n = n || 30; rot = rot || 0; const p = [], c = Math.cos(rot), s = Math.sin(rot); for (let i = 0; i < n; i++) { const a = i / n * Math.PI * 2, x = Math.cos(a) * rx, y = Math.sin(a) * ry; p.push([cx + x * c - y * s, cy + x * s + y * c]); } return p; } function capsule(x1, y1, x2, y2, r1, r2) { if (r2 == null) r2 = r1; const a = Math.atan2(y2 - y1, x2 - x1), p = []; for (let i = 0; i <= 8; i++) { const t = a + Math.PI / 2 + i / 8 * Math.PI; p.push([x1 + Math.cos(t) * r1, y1 + Math.sin(t) * r1]); } for (let i = 0; i <= 8; i++) { const t = a - Math.PI / 2 + i / 8 * Math.PI; p.push([x2 + Math.cos(t) * r2, y2 + Math.sin(t) * r2]); } return p; } /* torn / scissor-cut edge: subdivide, jitter perpendicular */ function jag(pts, R, amp, step) { step = step || 10; const out = []; for (let i = 0; i < pts.length; i++) { const a = pts[i], b = pts[(i + 1) % pts.length]; const dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy) || 1; const n = Math.max(1, Math.round(L / step)), nx = -dy / L, ny = dx / L; for (let k = 0; k < n; k++) { const t = k / n, j = (R() - 0.5) * amp; out.push([a[0] + dx * t + nx * j, a[1] + dy * t + ny * j]); } } return out; } function path(ctx, pts) { ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]); for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]); ctx.closePath(); } /* one cut-paper piece: cast shadow on the plane below, fill, pale cut edge */ function piece(ctx, pts, fill, R, o) { o = o || {}; const amp = o.amp == null ? 1.6 : o.amp, sh = o.sh == null ? 3 : o.sh; const p = amp > 0 ? jag(pts, R, amp, o.step) : pts; if (sh > 0) { ctx.save(); ctx.translate(sh * 0.8, sh); ctx.fillStyle = o.shc || 'rgba(28,14,6,0.32)'; path(ctx, p); ctx.fill(); ctx.restore(); } if (o.grad) { const g = ctx.createLinearGradient(o.grad[0], o.grad[1], o.grad[2], o.grad[3]); g.addColorStop(0, fill); g.addColorStop(1, o.grad[4]); ctx.fillStyle = g; } else ctx.fillStyle = fill; path(ctx, p); ctx.fill(); if (o.edge !== false) { ctx.strokeStyle = o.edgec || 'rgba(255,238,214,0.22)'; ctx.lineWidth = o.ew || 0.8; ctx.stroke(); } return p; } function line(ctx, pts, col, wdt) { ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]); for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]); ctx.strokeStyle = col; ctx.lineWidth = wdt; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.stroke(); } /* ---------- palette ---------- */ const C = { wall: '#b48a58', wallDark: '#8c6a43', floor: '#6a4529', plank: '#5a3a22', wood: '#3d2617', woodL: '#5a3a24', sill: '#a8957a', stone: '#7f7a70', stoneD: '#5f5a52', stoneL: '#a39d92', eSkin: '#c4977a', eSkinD: '#9c7058', eShawl: '#6f4f41', eShawlD: '#563a30', eDress: '#3f3436', eHair: '#cfc6bb', cSkin: '#dba882', cSkinD: '#b5815f', cSmock: '#5f7184', cSmockD: '#465566', cHair: '#4a2f1e', cSkirt: '#7b5d3b', cloth: '#d6cab0', clothD: '#b3a68a', chair: '#2e1d12', mirror: '#c9d2d4', mirrorRim: '#6b5232' }; /* ---------- the set (world space 1280 x 720) ---------- window: x 140..420, y 110..420 ; sill y 420..446 ; stone on sill at (330,410) elder seated at x ~900, hands in lap at (900,478) sun beam = window swept by (380,260) */ const WIN = { x0: 140, x1: 420, y0: 110, y1: 420 }; const SUN = [380, 260]; const STONE = [330, 408]; const HANDS = [900, 478]; const PATCH = [968, 486]; function inBeam(x, y) { const tx0 = (x - WIN.x1) / SUN[0], tx1 = (x - WIN.x0) / SUN[0]; const ty0 = (y - WIN.y1) / SUN[1], ty1 = (y - WIN.y0) / SUN[1]; const lo = Math.max(0, tx0, ty0), hi = Math.min(1, tx1, ty1); return lo <= hi; } function setCam(ctx, w, h, cam, depth) { const s = w / 1280, z = cam.z * s; const px = (cam.cx - 640) * (depth - 1) * 0.35; ctx.setTransform(z, 0, 0, z, w / 2 - (cam.cx + px) * z, h / 2 - cam.cy * z); } /* back plane: wall, floor, window, sill, stone */ function backPlane(ctx, st, R) { // wall piece(ctx, [[-400, -200], [1700, -200], [1700, 540], [-400, 540]], C.wall, R, { amp: 0, sh: 0, edge: false, grad: [0, 0, 0, 540, C.wallDark] }); // wainscot strip piece(ctx, [[-400, 470], [1700, 470], [1700, 545], [-400, 545]], '#7d5a37', R, { amp: 1.2, sh: 4 }); // floor piece(ctx, [[-400, 535], [1700, 535], [1700, 1100], [-400, 1100]], C.floor, R, { amp: 1, sh: 0, grad: [0, 535, 0, 800, '#4a2f1b'] }); ctx.save(); for (let i = 0; i < 9; i++) { const y = 560 + i * i * 4.2 + i * 8; line(ctx, [[-400, y], [1700, y + (R() - 0.5) * 3]], 'rgba(40,22,10,0.35)', 1.4); } ctx.restore(); // window recess piece(ctx, [[WIN.x0 - 26, WIN.y0 - 26], [WIN.x1 + 26, WIN.y0 - 26], [WIN.x1 + 26, WIN.y1 + 4], [WIN.x0 - 26, WIN.y1 + 4]], '#6e4f31', R, { amp: 1.4, sh: 5 }); // sky through the glass (photographed plate in production; painted stand-in here) const g = ctx.createLinearGradient(0, WIN.y0, 0, WIN.y1); if (st.sun > 0.5) { g.addColorStop(0, '#d9c8a4'); g.addColorStop(0.55, '#f2d8a2'); g.addColorStop(0.8, '#f6c98a'); g.addColorStop(1, '#c99a68'); } else { g.addColorStop(0, '#2c3550'); g.addColorStop(0.6, '#4a5470'); g.addColorStop(0.88, '#8a6d6a'); g.addColorStop(1, '#5e4a50'); } ctx.fillStyle = g; ctx.fillRect(WIN.x0, WIN.y0, WIN.x1 - WIN.x0, WIN.y1 - WIN.y0); // distant roofline cut-out across the bottom of the panes const roof = [[WIN.x0, WIN.y1], [WIN.x0, 352], [190, 352], [190, 330], [232, 312], [274, 330], [274, 356], [330, 356], [330, 366], [372, 366], [372, 344], [WIN.x1, 344], [WIN.x1, WIN.y1]]; piece(ctx, roof, st.sun > 0.5 ? '#6d5a4c' : '#232838', R, { amp: 0.8, sh: 0, edge: false }); if (st.sun > 0.5) { // low disc of sun, half behind roof, pale (not a glow) ctx.save(); ctx.beginPath(); ctx.rect(WIN.x0, WIN.y0, WIN.x1 - WIN.x0, 350 - WIN.y0); ctx.clip(); ctx.fillStyle = '#fff1d0'; ctx.beginPath(); ctx.arc(232, 336, 22, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } // frame + muntins const fw = C.wood; piece(ctx, [[WIN.x0 - 8, WIN.y0 - 8], [WIN.x1 + 8, WIN.y0 - 8], [WIN.x1 + 8, WIN.y0 + 4], [WIN.x0 - 8, WIN.y0 + 4]], fw, R, { amp: 0.8 }); piece(ctx, [[WIN.x0 - 8, WIN.y0 - 8], [WIN.x0 + 4, WIN.y0 - 8], [WIN.x0 + 4, WIN.y1], [WIN.x0 - 8, WIN.y1]], fw, R, { amp: 0.8 }); piece(ctx, [[WIN.x1 - 4, WIN.y0 - 8], [WIN.x1 + 8, WIN.y0 - 8], [WIN.x1 + 8, WIN.y1], [WIN.x1 - 4, WIN.y1]], fw, R, { amp: 0.8 }); piece(ctx, [[279, WIN.y0], [285, WIN.y0], [285, WIN.y1], [279, WIN.y1]], fw, R, { amp: 0.6, sh: 2 }); for (const y of [210, 315]) piece(ctx, [[WIN.x0, y], [WIN.x1, y], [WIN.x1, y + 6], [WIN.x0, y + 6]], fw, R, { amp: 0.6, sh: 2 }); // curtain, left piece(ctx, [[60, 70], [150, 70], [140, 200], [132, 330], [150, 480], [70, 486], [84, 300], [74, 180]], '#7a5d4a', R, { amp: 2.2, sh: 6, grad: [60, 0, 150, 0, '#634b3c'] }); for (let i = 0; i < 4; i++) line(ctx, [[86 + i * 15, 80], [92 + i * 14, 300], [86 + i * 16, 480]], 'rgba(40,24,14,0.35)', 2); // sill piece(ctx, [[WIN.x0 - 34, WIN.y1], [WIN.x1 + 34, WIN.y1], [WIN.x1 + 44, WIN.y1 + 16], [WIN.x0 - 44, WIN.y1 + 16]], C.sill, R, { amp: 1, sh: 3 }); piece(ctx, [[WIN.x0 - 44, WIN.y1 + 16], [WIN.x1 + 44, WIN.y1 + 16], [WIN.x1 + 44, WIN.y1 + 28], [WIN.x0 - 44, WIN.y1 + 28]], '#7f6c55', R, { amp: 1, sh: 6 }); // a cup on the sill for scale piece(ctx, [[196, 386], [222, 386], [219, 420], [199, 420]], '#d8cfbd', R, { amp: 0.7, sh: 3 }); if (st.stoneOnSill) stone(ctx, STONE[0], STONE[1], 1, st, R); } function stone(ctx, x, y, k, st, R) { const p = ell(x, y, 34 * k, 16 * k, -0.06, 34); piece(ctx, p, C.stone, R, { amp: 1.4 * k, sh: 4 * k, grad: [x, y - 16 * k, x, y + 16 * k, C.stoneD] }); // flatter top face, lit only as grey light on grey stone piece(ctx, ell(x - 4 * k, y - 6 * k, 24 * k, 7 * k, -0.06, 24), st.sun > 0.5 ? '#b2ab9f' : '#8a857c', R, { amp: 0.8 * k, sh: 0, edge: false }); const r = rng(77); ctx.fillStyle = 'rgba(40,36,30,0.45)'; for (let i = 0; i < 26; i++) { const a = r() * 6.28, d = Math.sqrt(r()); ctx.fillRect(x + Math.cos(a) * 30 * k * d, y + Math.sin(a) * 13 * k * d, 1.4 * k, 1.4 * k); } ctx.fillStyle = 'rgba(230,225,214,0.35)'; for (let i = 0; i < 14; i++) { const a = r() * 6.28, d = Math.sqrt(r()); ctx.fillRect(x + Math.cos(a) * 28 * k * d, y + Math.sin(a) * 12 * k * d - 2 * k, 1.1 * k, 1.1 * k); } } /* sunbeam: pale translucent plane + floor patch + dust */ function beam(ctx, st, R) { if (st.sun < 0.5) return; ctx.save(); ctx.globalCompositeOperation = 'screen'; const q = [[WIN.x1, WIN.y0], [WIN.x1 + SUN[0], WIN.y0 + SUN[1]], [WIN.x1 + SUN[0], WIN.y1 + SUN[1]], [WIN.x0 + SUN[0], WIN.y1 + SUN[1]], [WIN.x0, WIN.y1], [WIN.x0, WIN.y0]]; const g = ctx.createLinearGradient(WIN.x0, WIN.y0, WIN.x1 + SUN[0], WIN.y1 + SUN[1]); g.addColorStop(0, 'rgba(255,226,170,0.20)'); g.addColorStop(1, 'rgba(255,214,150,0.05)'); ctx.fillStyle = g; path(ctx, q); ctx.fill(); // patch on the floor (window shape, muntin shadows) const fy = 610, sk = 0.5; const pt = (u, v) => [WIN.x0 + SUN[0] - 40 + u * 300 + v * 120, fy - 36 + v * 100 * sk]; ctx.fillStyle = 'rgba(255,220,160,0.26)'; for (const [u0, u1] of [[0, 0.48], [0.52, 1]]) for (const [v0, v1] of [[0, 0.31], [0.35, 0.65], [0.69, 1]]) { path(ctx, [pt(u0, v0), pt(u1, v0), pt(u1, v1), pt(u0, v1)]); ctx.fill(); } // light on the sill and stone ctx.fillStyle = 'rgba(255,230,180,0.22)'; path(ctx, [[WIN.x0 - 30, WIN.y1 + 1], [WIN.x1 + 30, WIN.y1 + 1], [WIN.x1 + 40, WIN.y1 + 15], [WIN.x0 - 40, WIN.y1 + 15]]); ctx.fill(); // dust motes const r = rng(311); for (let i = 0; i < 520; i++) { const x = WIN.x0 + r() * (WIN.x1 - WIN.x0 + SUN[0]), y = WIN.y0 + r() * (WIN.y1 - WIN.y0 + SUN[1]); if (!inBeam(x, y)) continue; const a = 0.25 + r() * 0.5, s = 0.6 + r() * 1.6; ctx.fillStyle = 'rgba(255,240,210,' + a.toFixed(2) + ')'; ctx.beginPath(); ctx.arc(x, y, s, 0, 6.283); ctx.fill(); } ctx.restore(); } /* ---------- hands ---------- local frame: origin at wrist, +y toward fingertips, unit ~ world px */ function hand(ctx, x, y, ang, k, mode, skin, skinD, side, R, sh) { ctx.save(); ctx.translate(x, y); ctx.rotate(ang); ctx.scale(k * side, k); const o = { amp: 0.7, sh: sh == null ? 2 : sh, step: 5, edgec: 'rgba(255,236,214,0.3)', ew: 0.5 }; const fx = [-7.5, -2.5, 2.5, 7.5], fl = [17, 21, 20, 15]; if (mode === 'cup') { piece(ctx, ell(0, 14, 12, 13, 0, 26), skin, R, o); piece(ctx, ell(0, 15, 8, 8, 0, 20), skinD, R, { amp: 0.4, sh: 0, edge: false }); for (let i = 0; i < 4; i++) piece(ctx, capsule(fx[i], 24, fx[i] * 1.05, 24 + fl[i] * 0.55, 3.3, 3.0), skin, R, o); for (let i = 0; i < 4; i++) piece(ctx, ell(fx[i] * 1.05, 24 + fl[i] * 0.55, 2.6, 1.6, 0, 10), skinD, R, { amp: 0, sh: 0, edge: false }); piece(ctx, capsule(11, 6, 15, 22, 3.8, 3.2), skin, R, o); } else if (mode === 'fist') { // cold: fingers folded in, knuckles up piece(ctx, ell(0, 14, 12, 12, 0, 24), skin, R, o); for (let i = 0; i < 4; i++) piece(ctx, ell(fx[i] * 1.02, 26, 3.6, 3.2, 0, 14), skin, R, o); line(ctx, [[-11, 23], [11, 23]], skinD, 0.9); piece(ctx, capsule(10, 10, -1, 21, 3.6, 3.2), skin, R, o); for (let i = 0; i < 4; i++) line(ctx, [[fx[i] - 1.2, 25], [fx[i] + 1.2, 25]], 'rgba(120,70,50,0.6)', 0.7); } else if (mode === 'flat') { // palm down, fingers out piece(ctx, ell(0, 13, 11, 13, 0, 26), skin, R, o); for (let i = 0; i < 4; i++) piece(ctx, capsule(fx[i], 22, fx[i] * 1.25, 22 + fl[i], 3.3, 2.8), skin, R, o); piece(ctx, capsule(10, 8, 18, 22, 3.8, 3.1), skin, R, o); for (let i = 0; i < 4; i++) line(ctx, [[fx[i] * 1.12 - 1.4, 22 + fl[i] * 0.55], [fx[i] * 1.12 + 1.4, 22 + fl[i] * 0.55]], 'rgba(120,70,50,0.45)', 0.6); } else if (mode === 'lift') { // withdrawn: wrist bent back, fingertips raised piece(ctx, ell(0, 12, 11, 12, 0, 26), skin, R, o); for (let i = 0; i < 4; i++) piece(ctx, capsule(fx[i], 20, fx[i] * 1.35 + 2, 20 + fl[i] * 0.8, 3.2, 2.7), skin, R, o); piece(ctx, capsule(10, 6, 19, 17, 3.6, 3.0), skin, R, o); } else if (mode === 'wrap') { // holding: fingers curve over an object piece(ctx, ell(0, 12, 12, 12, 0, 24), skin, R, o); for (let i = 0; i < 4; i++) { const p0 = [fx[i], 20], p1 = [fx[i] * 0.9 + 3, 32], p2 = [fx[i] * 0.7 + 8, 40]; piece(ctx, capsule(p0[0], p0[1], p1[0], p1[1], 3.4, 3.2), skin, R, o); piece(ctx, capsule(p1[0], p1[1], p2[0], p2[1], 3.2, 2.8), skin, R, o); line(ctx, [[p1[0] - 2.4, p1[1] - 0.6], [p1[0] + 2.4, p1[1] + 0.6]], 'rgba(120,70,50,0.55)', 0.6); } piece(ctx, capsule(11, 6, 6, 24, 3.8, 3.2), skin, R, o); } ctx.restore(); } /* ---------- the elder (seated, right of room) ---------- */ function chair(ctx, R) { piece(ctx, [[808, 170], [992, 170], [1004, 560], [796, 560]], C.chair, R, { amp: 1.6, sh: 8 }); piece(ctx, [[818, 186], [982, 186], [990, 540], [810, 540]], '#3c2818', R, { amp: 1.2, sh: 0 }); for (const x of [802, 990]) piece(ctx, [[x, 540], [x + 12, 540], [x + 14, 700], [x + 2, 700]], C.chair, R, { amp: 1, sh: 5 }); } function elder(ctx, st, R) { const cold = st.elder === 'cold'; const lift = cold ? -6 : 0; // shoulders drawn up when cold // skirt / lap piece(ctx, [[812, 470], [990, 470], [1040, 560], [1046, 700], [800, 700], [790, 560]], C.eDress, R, { amp: 2, sh: 6, grad: [0, 470, 0, 700, '#2c2426'] }); line(ctx, [[860, 560], [850, 700]], 'rgba(20,14,14,0.5)', 2); line(ctx, [[950, 560], [962, 700]], 'rgba(20,14,14,0.5)', 2); // torso piece(ctx, [[846, 300 + lift], [954, 300 + lift], [978, 480], [822, 480]], C.eDress, R, { amp: 1.5, sh: 4 }); // shawl const shawl = cold ? [[826, 286 + lift], [974, 286 + lift], [1000, 360], [980, 450], [900, 470], [820, 450], [800, 360]] : [[830, 292], [970, 292], [1008, 380], [990, 446], [930, 430], [900, 452], [870, 430], [810, 446], [792, 380]]; piece(ctx, shawl, C.eShawl, R, { amp: 2.2, sh: 6, grad: [0, 290, 0, 470, C.eShawlD] }); for (let i = 0; i < 9; i++) { const x = 812 + i * 22; line(ctx, [[x, 446 - Math.abs(i - 4) * 4], [x + (R() - 0.5) * 6, 462 - Math.abs(i - 4) * 4]], C.eShawlD, 1.6); } // neck + head (profile turned toward the window) piece(ctx, [[886, 262 + lift], [914, 262 + lift], [916, 300 + lift], [884, 300 + lift]], C.eSkinD, R, { amp: 0.6, sh: 2 }); const hy = 232 + lift * 1.4, hx = 896; const head = [[hx + 26, hy - 30], [hx + 30, hy], [hx + 24, hy + 28], [hx + 4, hy + 38], [hx - 12, hy + 34], [hx - 18, hy + 22], [hx - 16, hy + 14], [hx - 24, hy + 6], [hx - 18, hy + 1], [hx - 20, hy - 10], [hx - 12, hy - 30], [hx + 6, hy - 38]]; piece(ctx, head, C.eSkin, R, { amp: 0.8, sh: 3, grad: [hx - 24, 0, hx + 30, 0, C.eSkinD] }); piece(ctx, [[hx - 10, hy - 34], [hx + 10, hy - 42], [hx + 30, hy - 30], [hx + 34, hy + 4], [hx + 22, hy + 14], [hx + 14, hy - 8], [hx - 6, hy - 22]], C.eHair, R, { amp: 1, sh: 2 }); piece(ctx, ell(hx + 34, hy - 14, 11, 10, 0, 18), C.eHair, R, { amp: 0.8, sh: 2 }); // bun line(ctx, [[hx - 14, hy - 4], [hx - 8, hy - 5]], 'rgba(70,40,30,0.7)', 1.2); // closed eye line(ctx, [[hx - 12, hy + 24], [hx - 4, hy + 25]], 'rgba(110,60,45,0.6)', 1); // arms into lap const H = HANDS; if (st.elder === 'cup' || st.elder === 'hold' || st.elder === 'open') { piece(ctx, capsule(842, 340, H[0] - 30, H[1] - 14, 15, 10), C.eShawl, R, { amp: 1.4, sh: 4 }); piece(ctx, capsule(958, 340, H[0] + 30, H[1] - 14, 15, 10), C.eShawl, R, { amp: 1.4, sh: 4 }); } if (st.elder === 'cup') { hand(ctx, H[0] - 22, H[1] - 14, -0.25, 1.15, 'cup', C.eSkin, C.eSkinD, 1, R); hand(ctx, H[0] + 22, H[1] - 14, 0.25, 1.15, 'cup', C.eSkin, C.eSkinD, -1, R); } else if (cold) { piece(ctx, capsule(846, 350, 892, 420, 15, 11), C.eShawl, R, { amp: 1.4, sh: 4 }); piece(ctx, capsule(954, 350, 906, 418, 15, 11), C.eShawl, R, { amp: 1.4, sh: 4 }); hand(ctx, 890, 412, -0.9, 1.5, 'fist', C.eSkin, C.eSkinD, 1, R); hand(ctx, 912, 408, 0.75, 1.5, 'fist', C.eSkin, C.eSkinD, -1, R); } } /* ---------- the child ---------- */ function child(ctx, st, R) { if (st.child === 'window' || st.child === 'mirror') { const x = 560, fy = 640; // standing, three-quarter back, facing the window piece(ctx, [[x - 30, fy - 140], [x + 34, fy - 140], [x + 44, fy - 40], [x - 40, fy - 40]], C.cSkirt, R, { amp: 1.4, sh: 6 }); for (const dx of [-14, 12]) piece(ctx, capsule(x + dx, fy - 44, x + dx - 2, fy - 4, 8, 7), '#3b2a22', R, { amp: 0.8, sh: 4 }); piece(ctx, [[x - 30, fy - 238], [x + 30, fy - 238], [x + 36, fy - 136], [x - 34, fy - 136]], C.cSmock, R, { amp: 1.4, sh: 5, grad: [x - 34, 0, x + 36, 0, C.cSmockD] }); piece(ctx, [[x - 8, fy - 252], [x + 8, fy - 252], [x + 8, fy - 236], [x - 8, fy - 236]], C.cSkinD, R, { amp: 0.4, sh: 1 }); const hx = x - 2, hy = fy - 278; piece(ctx, ell(hx, hy, 24, 27, 0, 28), C.cSkin, R, { amp: 0.8, sh: 3 }); piece(ctx, [[hx - 24, hy - 4], [hx - 20, hy - 24], [hx, hy - 32], [hx + 22, hy - 24], [hx + 28, hy], [hx + 26, hy + 26], [hx + 6, hy + 20], [hx - 6, hy + 4]], C.cHair, R, { amp: 1, sh: 2 }); if (st.child === 'window') { piece(ctx, capsule(x - 26, fy - 226, x - 34, fy - 160, 9, 8), C.cSmock, R, { amp: 1, sh: 3 }); hand(ctx, x - 34, fy - 162, 0.1, 0.8, 'flat', C.cSkin, C.cSkinD, 1, R); piece(ctx, capsule(x + 26, fy - 226, x + 32, fy - 160, 9, 8), C.cSmock, R, { amp: 1, sh: 3 }); hand(ctx, x + 32, fy - 162, -0.1, 0.8, 'flat', C.cSkin, C.cSkinD, -1, R); } else { // left arm raised toward the window, mirror in hand; right arm steadies const m = [486, 318]; piece(ctx, capsule(x - 24, fy - 228, m[0] + 20, m[1] + 22, 9, 7), C.cSmock, R, { amp: 1, sh: 3 }); hand(ctx, m[0] + 22, m[1] + 22, 2.55, 0.8, 'wrap', C.cSkin, C.cSkinD, 1, R); // mirror: handle + round glass, tilted toward elder piece(ctx, capsule(m[0] + 10, m[1] + 10, m[0] + 24, m[1] + 30, 3, 3), C.mirrorRim, R, { amp: 0.4, sh: 2 }); piece(ctx, ell(m[0], m[1], 15, 19, -0.5, 26), C.mirrorRim, R, { amp: 0.5, sh: 3 }); piece(ctx, ell(m[0], m[1], 12, 16, -0.5, 24), '#f4ead2', R, { amp: 0.3, sh: 0, edge: false }); piece(ctx, capsule(x + 24, fy - 228, x + 8, fy - 176, 9, 8), C.cSmock, R, { amp: 1, sh: 3 }); hand(ctx, x + 6, fy - 176, 0.5, 0.8, 'fist', C.cSkin, C.cSkinD, -1, R); } } } /* reflected spot: a thin shaft in the dust and a bright patch beside the hands */ function reflection(ctx) { ctx.save(); ctx.globalCompositeOperation = 'screen'; const m = [486, 318]; const g = ctx.createLinearGradient(m[0], m[1], PATCH[0], PATCH[1]); g.addColorStop(0, 'rgba(255,236,196,0.22)'); g.addColorStop(1, 'rgba(255,236,196,0.06)'); ctx.fillStyle = g; path(ctx, [[m[0] - 3, m[1] - 10], [m[0] + 3, m[1] + 10], [PATCH[0] - 10, PATCH[1] + 12], [PATCH[0] + 10, PATCH[1] - 12]]); ctx.fill(); ctx.restore(); ctx.save(); ctx.globalCompositeOperation = 'screen'; const rg = ctx.createRadialGradient(PATCH[0], PATCH[1], 2, PATCH[0], PATCH[1], 26); rg.addColorStop(0, 'rgba(255,246,222,0.95)'); rg.addColorStop(0.6, 'rgba(255,232,186,0.7)'); rg.addColorStop(1, 'rgba(255,232,186,0)'); ctx.fillStyle = rg; path(ctx, ell(PATCH[0], PATCH[1], 26, 14, 0.15, 30)); ctx.fill(); ctx.restore(); } /* dusk: cool multiply over the whole frame, keeps paper but drains warmth */ function dusk(ctx, w, h) { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalCompositeOperation = 'multiply'; ctx.fillStyle = '#8692ab'; ctx.fillRect(0, 0, w, h); ctx.restore(); } function finish(ctx, w, h, seed, warm) { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); // paper grain const r = rng(seed), n = Math.round(w * h / 300); for (let i = 0; i < n; i++) { const v = r(); ctx.fillStyle = v < 0.5 ? 'rgba(255,245,225,0.05)' : 'rgba(20,10,4,0.06)'; ctx.fillRect(r() * w, r() * h, 1 + r() * 2.2, 1 + r() * 1.2); } // vignette const g = ctx.createRadialGradient(w / 2, h / 2, h * 0.3, w / 2, h / 2, w * 0.72); g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, warm ? 'rgba(30,14,4,0.55)' : 'rgba(8,10,20,0.6)'); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); ctx.restore(); } function room(ctx, w, h, st, cam, seed) { ctx.save(); ctx.fillStyle = '#1b120b'; ctx.fillRect(0, 0, w, h); const R = rng(seed); setCam(ctx, w, h, cam, 0.9); backPlane(ctx, st, rng(11)); setCam(ctx, w, h, cam, 1.0); chair(ctx, rng(21)); elder(ctx, st, rng(31)); setCam(ctx, w, h, cam, 1.0); beam(ctx, st, R); setCam(ctx, w, h, cam, 1.08); child(ctx, st, rng(41)); if (st.patch) { setCam(ctx, w, h, cam, 1.0); reflection(ctx); } ctx.restore(); if (st.sun < 0.5) dusk(ctx, w, h); finish(ctx, w, h, seed + 5, st.sun > 0.5); } /* ---------- close-up planes (same set, own detail) ---------- */ /* KF3: hands at sunset, cold. The patch is gone; the empty place beside the hands stays in frame. */ function drawColdHands(ctx, w, h) { ctx.save(); ctx.fillStyle = '#20150d'; ctx.fillRect(0, 0, w, h); const cam = { cx: 900, cy: 430, z: 2.6 }; setCam(ctx, w, h, cam, 0.9); backPlane(ctx, { sun: 0, stoneOnSill: true }, rng(11)); setCam(ctx, w, h, cam, 1.0); chair(ctx, rng(21)); elder(ctx, { elder: 'cold' }, rng(31)); // the shawl edge drawn tight under the chin by one fist: extra fold const R = rng(91); piece(ctx, [[870, 300], [930, 300], [948, 344], [900, 360], [856, 340]], C.eShawlD, R, { amp: 1.4, sh: 3 }); // knee where the patch lay: plain dark cloth, nothing on it line(ctx, [[940, 500], [1000, 492]], 'rgba(10,8,10,0.4)', 2); ctx.restore(); dusk(ctx, w, h); finish(ctx, w, h, 303, false); } /* KF4: the sill stone, the child's second touch */ function drawTouch(ctx, w, h) { ctx.save(); ctx.fillStyle = '#20150d'; ctx.fillRect(0, 0, w, h); const cam = { cx: 345, cy: 380, z: 3.4 }; setCam(ctx, w, h, cam, 0.9); backPlane(ctx, { sun: 0, stoneOnSill: true }, rng(11)); const R = rng(404); // child leaning in from the right, profile facing left, eyes on the stone const hx = 428, hy = 336; piece(ctx, [[hx + 6, hy + 18], [hx + 70, hy + 30], [hx + 90, hy + 140], [hx + 10, hy + 140]], C.cSmock, R, { amp: 1, sh: 4, grad: [hx, 0, hx + 90, 0, C.cSmockD] }); const face = [[hx + 14, hy - 26], [hx + 30, hy - 24], [hx + 36, hy], [hx + 30, hy + 22], [hx + 12, hy + 28], [hx - 2, hy + 22], [hx - 6, hy + 12], [hx - 12, hy + 6], [hx - 7, hy + 2], [hx - 9, hy - 8], [hx - 2, hy - 20]]; piece(ctx, face, C.cSkin, R, { amp: 0.5, sh: 2, step: 4, grad: [hx - 12, 0, hx + 36, 0, C.cSkinD] }); piece(ctx, [[hx - 4, hy - 22], [hx + 14, hy - 32], [hx + 36, hy - 24], [hx + 46, hy + 4], [hx + 42, hy + 30], [hx + 28, hy + 26], [hx + 24, hy - 2], [hx + 8, hy - 14]], C.cHair, R, { amp: 0.8, sh: 2, step: 4 }); line(ctx, [[hx - 4, hy - 2], [hx + 1, hy + 1]], 'rgba(60,30,20,0.85)', 0.9); // eye, looking down line(ctx, [[hx - 6, hy - 7], [hx + 2, hy - 8]], 'rgba(60,30,20,0.6)', 0.6); // brow, lifted // arm reaching down-left, palm settling flat on the stone (the second touch) piece(ctx, capsule(hx + 30, hy + 44, 362, 398, 7.5, 6), C.cSmock, R, { amp: 0.8, sh: 3, step: 5 }); hand(ctx, 360, 398, 1.62, 0.95, 'flat', C.cSkin, C.cSkinD, -1, R, 1.2); // the other hand, withdrawn after the first touch, held near the chest, fingers curled piece(ctx, capsule(hx + 56, hy + 56, 452, 384, 7, 6), C.cSmockD, R, { amp: 0.8, sh: 3, step: 5 }); hand(ctx, 450, 382, 3.4, 0.8, 'lift', C.cSkin, C.cSkinD, 1, R, 1.2); ctx.restore(); dusk(ctx, w, h); finish(ctx, w, h, 404, false); } /* KF5: the stone wrapped in cloth, given; elder's hands close around it */ function drawGiven(ctx, w, h) { ctx.save(); ctx.fillStyle = '#20150d'; ctx.fillRect(0, 0, w, h); const cam = { cx: 900, cy: 480, z: 3.6 }; setCam(ctx, w, h, cam, 0.9); backPlane(ctx, { sun: 0 }, rng(11)); setCam(ctx, w, h, cam, 1.0); chair(ctx, rng(21)); const R = rng(505); // lap piece(ctx, [[780, 440], [1020, 440], [1060, 600], [760, 600]], C.eDress, R, { amp: 1.2, sh: 4, grad: [0, 440, 0, 600, '#2c2426'] }); for (let i = 0; i < 7; i++) line(ctx, [[820 + i * 26, 452], [818 + i * 27, 600]], 'rgba(18,12,12,0.35)', 1.2); // elder's sleeves descend into frame from above piece(ctx, capsule(800, 380, 846, 476, 17, 13), C.eShawl, R, { amp: 1.2, sh: 4, step: 6 }); piece(ctx, capsule(1000, 380, 954, 476, 17, 13), C.eShawl, R, { amp: 1.2, sh: 4, step: 6 }); // child's sleeve from lower-left, child's hands under the bundle, sliding away piece(ctx, capsule(760, 580, 850, 532, 12, 9), C.cSmock, R, { amp: 1, sh: 4, step: 6 }); hand(ctx, 850, 532, -1.25, 0.95, 'lift', C.cSkin, C.cSkinD, 1, R, 2); // the bundle: linen around the stone const bx = 900, by = 486; piece(ctx, ell(bx, by, 50, 26, -0.05, 34), C.cloth, R, { amp: 1.2, sh: 5, step: 5, grad: [bx, by - 20, bx, by + 20, C.clothD] }); piece(ctx, [[bx - 30, by - 14], [bx - 50, by - 40], [bx - 20, by - 22]], C.cloth, R, { amp: 0.8, sh: 3, step: 4 }); // tied corner piece(ctx, [[bx + 26, by + 10], [bx + 52, by + 22], [bx + 38, by + 2]], C.clothD, R, { amp: 0.8, sh: 3, step: 4 }); for (let i = 0; i < 5; i++) line(ctx, [[bx - 26 + i * 12, by - 14], [bx - 22 + i * 12, by + 14]], 'rgba(120,104,80,0.5)', 0.8); // a sliver of grey stone showing at a fold piece(ctx, ell(bx + 4, by - 18, 16, 5, 0, 16), C.stone, R, { amp: 0.4, sh: 1, edge: false }); // elder's hands closed around it: fingers relaxed and long, no fists hand(ctx, 846, 478, -1.45, 1.05, 'wrap', C.eSkin, C.eSkinD, -1, R, 2.5); hand(ctx, 954, 478, 1.45, 1.05, 'wrap', C.eSkin, C.eSkinD, 1, R, 2.5); // age marks on the backs of the hands ctx.fillStyle = 'rgba(120,76,52,0.45)'; for (const p of [[842, 474], [850, 482], [958, 474], [950, 482], [846, 488]]) { ctx.beginPath(); ctx.arc(p[0], p[1], 0.9, 0, 6.283); ctx.fill(); } ctx.restore(); dusk(ctx, w, h); finish(ctx, w, h, 505, false); } /* ---------- references (verified by opening each page) ---------- */ const REF = { trombe: { kind: 'research paper', title: 'Building a Better Trombe Wall (NREL Buildings Research fact sheet, DOE/GO-10098-660)', creator: 'National Renewable Energy Laboratory', date: '1998-12', url: 'https://www.nrel.gov/docs/legosti/fy98/22834.pdf', rights: 'public domain', use: 'citation: masonry absorbs sun, stores heat and releases it for hours after sunset', verified: true }, doe: { kind: 'source text', title: 'Consumer Guide to Passive Solar Home Design (DOE/EE-2467)', creator: 'U.S. Department of Energy, Energy Saver', date: '2021', url: 'https://www.energy.gov/sites/default/files/2021-08/ES-Passive%20Solar%20design_081321.pdf', rights: 'public domain', use: 'citation: thermal mass defined as materials that retain or store the heat produced by sunlight', verified: true }, sunbeams: { kind: 'still image', title: 'Dust Motes Dancing in the Sunbeams (Støvkornenes dans i solstrålerne), oil on canvas', creator: 'Vilhelm Hammershøi', date: '1900', url: 'https://en.wikipedia.org/wiki/Dust_Motes_Dancing_in_Sunbeams', rights: 'public domain', use: 'lighting reference: slanted beam, visible dust, unlit wall around it (Ordrupgaard collection)', verified: true }, strandgade: { kind: 'still image', title: 'Stue i Strandgade med solskin på gulvet (Interior in Strandgade, Sunlight on the Floor), KMS3696', creator: 'Vilhelm Hammershøi', date: '1901', url: 'https://open.smk.dk/artwork/image/KMS3696', rights: 'public domain', use: 'lighting reference: window-shaped patch on the floor with muntin shadows (SMK open data marks it public domain)', verified: true }, sleep: { kind: 'archive footage', title: 'Sleep for Health', creator: 'Encyclopaedia Britannica Films', date: '1950', url: 'https://archive.org/details/Sleepfor1950', rights: 'public domain', use: 'background plate: shot list at 11:24:35 "Sunlight streams through window"; keyed into the window recess and sill', verified: true }, newengland: { kind: 'archive footage', title: 'New England: Yesterday and Today', creator: 'Handy (Jam) Organization', date: 'ca. 1938', url: 'https://archive.org/details/0315_New_England_Yesterday_and_Today_16_00_54_25', rights: 'unknown — not granted', use: 'background plate: "Pan over Plymouth cemetery on Burial Hill at sunset" as the sky seen through the glass; Jethro Coffin House interiors as set reference. Item page states no licence.', verified: true }, mirror: { kind: 'archive footage', title: 'The Face in the Mirror (I Wonder), Part I', creator: 'Handy (Jam) Organization', date: '1940', url: 'https://archive.org/details/Faceinth1940', rights: 'public domain', use: 'rotoscope reference: shot at 20:43:00, a man holds a small mirror in his palm; grip and wrist angle for the child', verified: true }, hope: { kind: 'archive footage', title: 'Hope Is in Your Hand', creator: 'Hardcastle Film Associates (dir. Jack W. Murray)', date: 'ca. 1960', url: 'https://archive.org/details/0247_Hope_Is_in_Your_Hand_18_11_36_00', rights: 'unknown — not granted', use: 'rotoscope reference: 11:33:22 a woman\'s hands hold a deaf boy\'s hands against his cheeks to feel vibration; 11:36:42 an old woman\'s hand sews. Item page states no licence.', verified: true }, si: { kind: 'museum object', title: 'warmer, boot (soapstone), NMAH 318370', creator: 'National Museum of American History, Smithsonian', date: 'undated', url: 'https://americanhistory.si.edu/collections/object/nmah_318370', rights: 'CC0', use: 'texture: grey soapstone surface, speckle and worn edges for the paper stone', verified: true }, maine: { kind: 'museum object', title: 'Soap Stone Foot Warmer, ca. 1900 (Southern Aroostook Agricultural Museum)', creator: 'Maine Memory Network', date: 'ca. 1900', url: 'https://www.mainememory.net/record/17255', rights: 'unknown — not granted', use: 'citation: a stone heated then carried to warm a body (record says it was heated on a stove for a sleigh)', verified: true }, shaker: { kind: 'museum object', title: 'Warmer, Bed (soapstone slab, Canterbury NH), 1960.11707.1', creator: 'Shaker Museum', date: 'undated', url: 'https://www.shakermuseum.us/object?id=23043', rights: 'unknown — not granted', use: 'citation / prop reference: domestic soapstone bed warmer', verified: true } }; window.AFTERLIGHT_FILMS.push({ id: 'F1', n: 1, title: 'The Stone That Remembered', role: 'The introduction', understand: 'Keeping an image is different from retaining warmth; a physical effect can become an act of care.', medium: 'Multiplane cut paper (Norstein-style glass planes) over photographed window plates: matte paper cannot glow, so temperature has to be performed by hands and posture, which is what this film asks for.', archivePlan: 'Each shot is a COMPOSED SCENE · archive fragments re-performed · not a recorded event. The window glass, sunbeam and floor patch are PHOTOGRAPHED plates from Prelinger films (Sleep for Health, New England: Yesterday and Today), keyed into an INVENTED cut-paper room; the elder, child, stone and cloth are INVENTED paper figures matted over those plates, with hand and mirror gestures rotoscoped from Prelinger footage. Captions, if any, sit on paper slips beside the frame (for example: "the stone\'s memory is a metaphor; the warmth is stored heat"), and every layer carries its PHOTOGRAPHED or INVENTED tag in the credits.', basis: [ { title: 'Building a Better Trombe Wall (NREL, 1998)', url: REF.trombe.url, note: 'Supports the treatment\'s physical claim: masonry absorbs sunlight, stores heat and releases it for hours after sunset.', verified: true }, { title: 'Consumer Guide to Passive Solar Home Design (US DOE)', url: REF.doe.url, note: 'Defines thermal mass as materials that retain or store heat produced by sunlight. Does not mention Trombe walls; release-later is stated in the NREL sheet.', verified: true }, { title: 'Soap Stone Foot Warmer, ca. 1900 (Maine Memory Network)', url: REF.maine.url, note: 'Historical practice of carrying a warmed stone to a body. That warmer was heated on a stove, not by the sun; the sun-warmed sill stone is the film\'s fiction built on the same physics.', verified: true }, { title: 'warmer, boot, soapstone (Smithsonian NMAH 318370)', url: REF.si.url, note: 'Object record (material only, no use notes). CC0 per Smithsonian Open Access.', verified: true }, { title: 'Treatment fiction', url: '', note: 'The stone "remembering" is metaphor (as the treatment says). The child, elder, mirror attempt and gift are invented drama, not documented events.', verified: false } ], keyframes: [ { n: 1, t: 0.0, title: 'The cold room', shot: 'WS, locked off at seated eye height; window left, elder right, stone on the sill in the beam', action: 'Near sunset a low beam crosses the room and stops short of the elder, who sits with cupped hands in her lap. The child stands at the window between them; the grey stone lies on the sill in full sun.', mechanism: 'sun → sill stone (absorbs, all afternoon) ; sun → floor patch (falls short of elder) ; cupped hands ⇝ child (she wants warmth)', layers: [ { name: 'window sky plate, low sun', source: 'PHOTOGRAPHED', ref: 1 }, { name: 'sunbeam and dust plate', source: 'PHOTOGRAPHED', ref: 0 }, { name: 'room set: wall, floor, curtain, sill (cut paper)', source: 'INVENTED', ref: 2 }, { name: 'sill stone (cut paper, soapstone texture)', source: 'INVENTED', ref: 3 }, { name: 'elder and child figures (cut paper, matted)', source: 'INVENTED', ref: null } ], prompt: 'Wide shot of a plain wooden room near sunset in layered cut-paper animation, Norstein multiplane depth. Low sun through a small four-pane window throws a slanted beam with floating dust onto the floorboards. A grey stone rests on the windowsill in the beam. An old woman in a brown shawl sits in a high-backed chair in shadow on the right, cupped hands in her lap. A child stands at the window. Warm ochre wall, umber floor, paper grain, no text, no logos', draw: (ctx, w, h) => room(ctx, w, h, { sun: 1, stoneOnSill: true, elder: 'cup', child: 'window' }, { cx: 640, cy: 380, z: 1.0 }, 101), refs: [REF.sleep, REF.newengland, REF.sunbeams, REF.si] }, { n: 2, t: 14.0, title: 'The borrowed patch', shot: 'MWS from behind the child, slightly low; mirror in the beam, elder\'s lap in the right third', action: 'The child raises a hand mirror into the beam and tilts it until a bright patch lands on the elder\'s knee, beside her cupped hands. The elder turns toward it; her hands do not move.', mechanism: 'sun → mirror → patch (reflects, light only while the sun lasts) ; patch ⇝ child (she reads brightness as warmth) ; hands stay cold (patch lands beside them)', layers: [ { name: 'sunbeam, dust and floor patch plate', source: 'PHOTOGRAPHED', ref: 2 }, { name: 'room set and elder (cut paper)', source: 'INVENTED', ref: 1 }, { name: 'child with hand mirror (cut paper, rotoscoped grip)', source: 'INVENTED', ref: 0 }, { name: 'reflected shaft and patch (lighting pass)', source: 'INVENTED', ref: 1 } ], prompt: 'Medium wide shot, cut-paper multiplane interior at sunset. A child in a blue-grey smock seen from behind lifts a small round hand mirror into a slanted sunbeam full of dust. A thin reflected shaft crosses the room and lays a small bright oval on the dark skirt of an old woman seated in a high-backed chair, just beside her cupped hands. Grey stone visible on the windowsill. Ochre, umber, pale gold, matte paper, no text, no logos', draw: (ctx, w, h) => room(ctx, w, h, { sun: 1, stoneOnSill: true, elder: 'cup', child: 'mirror', patch: true }, { cx: 700, cy: 400, z: 1.32 }, 202), refs: [REF.mirror, REF.strandgade, REF.sleep] }, { n: 3, t: 26.0, title: 'The patch goes out', shot: 'MCU on the elder\'s hands and chest, dusk; the knee where the patch lay stays in frame', action: 'The sun drops behind the roofline and the patch vanishes with it. The elder\'s hands close into loose fists and climb to her chest, gripping the shawl closed; her shoulders rise.', mechanism: 'sun ⊣ mirror (no source, no patch) ; patch ⇝ nothing kept (an image, not stored heat) ; cold → hands (fists, shawl pulled up)', layers: [ { name: 'dusk sky plate in background window', source: 'PHOTOGRAPHED', ref: 0 }, { name: 'elder\'s hands (cut paper, rotoscoped from archive hands)', source: 'INVENTED', ref: 1 }, { name: 'shawl, dress, chair (cut paper)', source: 'INVENTED', ref: null }, { name: 'dusk grade', source: 'INVENTED', ref: 2 } ], prompt: 'Medium close-up in cut-paper animation at dusk: an old woman\'s hands, now closed into loose fists, pulled up to her chest gripping a brown wool shawl, shoulders raised against the cold. Her dark skirt below is empty of light. Cool blue-grey dusk wash over warm paper colours, soft paper shadows between layers, fine grain, quiet and still, no text, no logos', draw: drawColdHands, refs: [REF.newengland, REF.hope, REF.sunbeams] }, { n: 4, t: 38.0, title: 'Second touch', shot: 'CU on the sill from the child\'s side, slightly high; stone centre, child\'s face in profile upper right', action: 'The child touches the sill stone, snatches the hand back, then lays the palm on it again and leaves it there. Her other hand hovers at her chest; her brows lift.', mechanism: 'stone → hand (releases stored heat after sunset) ; first touch ⇝ surprise (withdraw) ; second touch ⇝ recognition ; no glow: temperature reads only from the hand', layers: [ { name: 'dusk window and sill plate', source: 'PHOTOGRAPHED', ref: 3 }, { name: 'stone (cut paper, soapstone texture)', source: 'INVENTED', ref: 0 }, { name: 'child\'s hands and face (cut paper, rotoscoped touch)', source: 'INVENTED', ref: 1 }, { name: 'paper slip caption: stored heat, not memory', source: 'INVENTED', ref: 2 } ], prompt: 'Close-up in layered cut-paper animation at dusk: a child leans over a windowsill, face in profile looking down, one palm resting flat on a plain grey speckled stone, the other hand drawn back near the chest with fingers curled. The stone is matte grey, not glowing. Blue dusk through the window behind, wood sill, soft paper shadows, attentive stillness, no text, no logos', draw: drawTouch, refs: [REF.si, REF.hope, REF.trombe, REF.sleep] }, { n: 5, t: 52.0, title: 'The hands close', shot: 'ECU on the elder\'s lap, top-down three-quarter; ends on the hands', action: 'The child sets the cloth-wrapped stone in the elder\'s cupped hands and slides her own hands away. The elder\'s fingers uncurl and fold around the bundle; her grip loosens instead of tightening.', mechanism: 'stone → elder\'s hands (heat through linen) ; child ⇝ elder (a physical effect becomes an act of care) ; open fingers, relaxed wrists ⇝ viewer (warmth, without glow)', layers: [ { name: 'elder\'s hands (cut paper, rotoscoped from archive hands)', source: 'INVENTED', ref: 0 }, { name: 'linen bundle with stone (cut paper)', source: 'INVENTED', ref: 2 }, { name: 'child\'s withdrawing hand (cut paper)', source: 'INVENTED', ref: 0 }, { name: 'lap and chair (cut paper)', source: 'INVENTED', ref: null } ], prompt: 'Extreme close-up in cut-paper animation: an old woman\'s spotted hands with long relaxed fingers close around a small bundle of undyed linen tied at one corner, a sliver of grey stone at the fold, resting on a dark skirt. A child\'s small hand slides away at lower left. Cool dusk light, no glow on the stone, gentle paper shadows, tender and still, no text, no logos', draw: drawGiven, refs: [REF.hope, REF.maine, REF.shaker, REF.trombe] } ] }); })();