{"id":"storm-watch","title":"Storm watch","category":"Video editing","prompt":"Real footage, edited live. Thirty-six frames of a NASA ISS timelapse — Earth's limb, red airglow, lightning storms firing below — play as an image sequence structured as an edit session: a RAW pass where a tracking ring locks onto each lightning cell using centroids measured from the pixels, with live coordinates in a monospace readout; a half-second rewind with REW indicator; then the same footage again through a teal-and-amber blend-mode grade revealed by a sweeping wipe edge. Timecode, frame counter, pass tags, safe-area brackets and a scrubber with pass marks run throughout. End card: video in · tracked · rewound · graded · video out.","designedBy":{"provider":"anthropic","model":"claude-fable-5","inference":{"inputTokens":0,"outputTokens":0,"costUsd":0,"inPerMTokUsd":10,"outPerMTokUsd":50,"interactive":true}},"render":{"jobId":"rnd_1788538452971_1iwezk","credits":8,"priceUsd":0.12,"format":"mp4","renderMs":58502,"frameCount":240,"bytes":5304824,"seed":3647386003,"engineVersion":"v0.5.6","width":1920,"height":1080,"fps":30,"durationSeconds":8},"howToReproduce":{"renderApi":"POST /v1/renders with `renderRequest` below as the body (Authorization: Bearer <your pp_sa_… key>).","editor":"/gallery/storm-watch/editor opens this piece live in the studio.","codeConsole":"/gallery/storm-watch.js is this piece as PinePaper code — paste it into the editor's AI code console."},"renderRequest":{"scene":{"canvas":{"width":960,"height":540},"items":[],"relations":[],"generators":[],"media":[],"skeletons":[],"background":"#05030f","engineOps":[{"tool":"execute_custom_code","args":{"description":"Storm watch: 36-frame NASA footage sequence as an edit session — raw pass with measured flash tracking, fast rewind, blend-mode graded pass, scrubber HUD","code":"var W = 960, H = 540, FPS = 12, N = 36;\n// Measured flash track: brightest-cell centroid per frame (960x540 coords),\n// i = peak luminance, n = refined-cluster size. i>=248 && n>=19 gates the ring.\nvar TRACK = [[545,355,254,67],[669,370,253,80],[673,370,249,10],[483,377,240,4],[764,381,248,5],[762,448,254,43],[838,381,247,24],[869,382,241,51],[637,396,254,93],[614,423,249,6],[928,406,251,19],[499,372,252,57],[692,426,254,109],[591,449,243,14],[606,460,245,17],[558,407,249,25],[871,387,254,64],[286,460,253,36],[681,516,241,19],[607,431,254,61],[636,480,247,20],[318,522,254,111],[653,475,252,33],[655,511,243,17],[691,492,254,99],[414,371,255,46],[447,373,254,21],[446,377,254,51],[460,382,247,16],[222,492,224,12],[225,506,245,15],[560,400,253,59],[495,408,254,66],[480,423,254,94],[513,424,254,39],[535,438,254,41]];\nvar DT = 1 / FPS;\n// Session timeline: raw pass, fast rewind, graded pass, end card.\nvar RAW0 = 0.25, RAW1 = RAW0 + N * DT;      // 0.25 → 3.25\nvar REW0 = RAW1, REW1 = REW0 + 0.5;         // 3.25 → 3.75, 6 reversed samples\nvar GR0 = REW1, GR1 = GR0 + N * DT;         // 3.75 → 6.75\nvar END0 = GR1 + 0.15, DUR = 8;\nvar REWF = [35, 28, 21, 14, 7, 0];\n\n// ── Footage: one raster per frame, opacity-window keyframes ONLY (no\n// The 36 paths are LITERALS, not built from the index at runtime.\n// The Worker resolves scene imagery by scanning the serialized scene\n// for /scene-assets/ strings and shipping those bytes to the container,\n// which has no network of its own. A path assembled as\n//   PREFIX + String(i+1).padStart(2,'0') + '.jpg'   (PREFIX built at runtime)\n// matches nothing that scan can see, so not one frame of footage ever\n// travelled — the video-editing piece played its whole HUD over black.\nvar FRAME_PATHS = ['/scene-assets/storm-f01.jpg', '/scene-assets/storm-f02.jpg', '/scene-assets/storm-f03.jpg', '/scene-assets/storm-f04.jpg', '/scene-assets/storm-f05.jpg', '/scene-assets/storm-f06.jpg', '/scene-assets/storm-f07.jpg', '/scene-assets/storm-f08.jpg', '/scene-assets/storm-f09.jpg', '/scene-assets/storm-f10.jpg', '/scene-assets/storm-f11.jpg', '/scene-assets/storm-f12.jpg', '/scene-assets/storm-f13.jpg', '/scene-assets/storm-f14.jpg', '/scene-assets/storm-f15.jpg', '/scene-assets/storm-f16.jpg', '/scene-assets/storm-f17.jpg', '/scene-assets/storm-f18.jpg', '/scene-assets/storm-f19.jpg', '/scene-assets/storm-f20.jpg', '/scene-assets/storm-f21.jpg', '/scene-assets/storm-f22.jpg', '/scene-assets/storm-f23.jpg', '/scene-assets/storm-f24.jpg', '/scene-assets/storm-f25.jpg', '/scene-assets/storm-f26.jpg', '/scene-assets/storm-f27.jpg', '/scene-assets/storm-f28.jpg', '/scene-assets/storm-f29.jpg', '/scene-assets/storm-f30.jpg', '/scene-assets/storm-f31.jpg', '/scene-assets/storm-f32.jpg', '/scene-assets/storm-f33.jpg', '/scene-assets/storm-f34.jpg', '/scene-assets/storm-f35.jpg', '/scene-assets/storm-f36.jpg'];\n// FOOTAGE: 36 frames as ENGINE image items, one per source frame, each\n// shown only in its own windows — the raw pass, the rewind samples, and\n// the graded pass. They were raw paper.Raster objects with data.keyframes,\n// and in the server renderer that means two fatal things at once: a raw\n// raster never advances a keyframe (so every frame sat at opacity 0 and\n// the video-editing piece played eight seconds of empty HUD over black),\n// and it draws above every app item anyway, which would have buried the\n// timecode, the tracking ring and the scrubber underneath the footage.\n// An image item animates and layers like anything else the app makes.\nfor (var i = 0; i < N; i++) (function (i) {\n  var p = FRAME_PATHS[i];\n  var src = (window.__ppAssets && window.__ppAssets[p]) || p;\n  var kfs = [{ time: 0, properties: { opacity: 0 } }];\n  function win(a, b) {\n    // A hold at zero immediately before the cut. Without it the frame ramps\n    // in from the previous keyframe and all 36 are partly composited at once\n    // — a smear instead of a cut.\n    kfs.push({ time: Math.max(0.001, a - 0.001), properties: { opacity: 0 } });\n    kfs.push({ time: a, properties: { opacity: 1 } });\n    kfs.push({ time: b, properties: { opacity: 1 } });\n    kfs.push({ time: b + 0.001, properties: { opacity: 0 } });\n  }\n  var t0 = RAW0 + i * DT;\n  if (i === 0) { kfs[0].properties.opacity = 1; kfs.push({ time: t0, properties: { opacity: 1 } }); }\n  win(t0, t0 + DT);\n  for (var r = 0; r < REWF.length; r++) if (REWF[r] === i) win(REW0 + r * (0.5 / 6), REW0 + (r + 1) * (0.5 / 6));\n  var g0 = GR0 + i * DT;\n  win(g0, g0 + DT);\n  if (i === N - 1) { kfs.push({ time: GR1, properties: { opacity: 1 } }); kfs.push({ time: DUR, properties: { opacity: 1 } }); }\n  var fid = PinePaper.create('image', { src: src, x: W / 2, y: H / 2, width: W, height: H, opacity: (i === 0) ? 1 : 0 });\n  PinePaper.modifyItem(fid, { animationType: 'keyframe', duration: DUR, keyframes: kfs });\n})(i);\n\n// ── Grade stack (graded pass only): teal multiply + amber screen + edge\n// vignette, blend-mode compositing over the footage, revealed by a wipe.\nvar gradeTeal = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, fillColor: '#0e3a44', opacity: 0 });\ngradeTeal.blendMode = 'multiply';\nvar gradeAmber = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, fillColor: '#ff8a3c', opacity: 0 });\ngradeAmber.blendMode = 'screen';\nvar vig = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, opacity: 0 });\nvig.fillColor = { gradient: { stops: [[new paper.Color(0, 0, 0, 0), 0.55], [new paper.Color(0, 0, 0, 0.8), 1]], radial: true }, origin: [W / 2, H / 2], destination: [W, H / 2] };\nvig.blendMode = 'multiply';\n// The wipe: opacity ramps L→R are not a thing, so the reveal is a hard\n// left-anchored width sweep on a masking duplicate — instead, keyframe the\n// stack's opacity in with the wipe LINE sweeping across as the visual edge.\n[gradeTeal, gradeAmber, vig].forEach(function (r, gi) {\n  var full = [0.55, 0.18, 0.7][gi];\n  PinePaper.modifyItem(r, { animationType: 'keyframe', duration: DUR, keyframes: [\n    { time: 0, properties: { opacity: 0 } }, { time: GR0, properties: { opacity: 0 } },\n    { time: GR0 + 1.0, properties: { opacity: full }, easing: 'linear' },\n    { time: DUR, properties: { opacity: full } }\n  ] });\n});\nvar wipeEdge = PinePaper.create('line', { from: [0, 0], to: [0, H], strokeColor: '#ffd27a', strokeWidth: 3, opacity: 0 });\nPinePaper.modifyItem(wipeEdge, { animationType: 'keyframe', duration: DUR, keyframes: [\n  { time: 0, properties: { opacity: 0, x: 2 } }, { time: GR0, properties: { opacity: 0.9, x: 2 } },\n  { time: GR0 + 1.0, properties: { opacity: 0.9, x: W - 2 }, easing: 'linear' },\n  { time: GR0 + 1.15, properties: { opacity: 0, x: W - 2 } }, { time: DUR, properties: { opacity: 0, x: W - 2 } }\n] });\n\n// ── Tracking overlay (raw pass): ring + crosshair snapping to the measured\n// centroids, readout with live coordinates, gated on flash strength.\nvar ringKfs = [{ time: 0, properties: { opacity: 0, x: TRACK[0][0], y: TRACK[0][1] } }];\nvar roKfs = [{ time: 0, properties: { content: 'ACQ …', opacity: 0 } }];\nfor (var f = 0; f < N; f++) {\n  var t = RAW0 + f * DT, tr = TRACK[f];\n  var hot = tr[2] >= 248 && tr[3] >= 19;\n  ringKfs.push({ time: t, properties: { x: tr[0], y: tr[1], opacity: hot ? 0.95 : 0.25 } });\n  roKfs.push({ time: t, properties: { content: (hot ? 'LOCK ' : 'scan ') + 'x' + tr[0] + ' y' + tr[1] + ' \\u00b7 I' + tr[2], opacity: 1 } });\n}\nringKfs.push({ time: RAW1, properties: { opacity: 0 } }, { time: DUR, properties: { opacity: 0 } });\nroKfs.push({ time: RAW1, properties: { opacity: 0 } }, { time: DUR, properties: { opacity: 0 } });\nvar ring = PinePaper.create('circle', { x: TRACK[0][0], y: TRACK[0][1], radius: 34, strokeColor: '#7CFC9B', strokeWidth: 2.5, fillColor: null, opacity: 0 });\nPinePaper.modifyItem(ring, { animationType: 'keyframe', duration: DUR, keyframes: ringKfs });\nvar readout = PinePaper.create('text', { content: 'ACQ …', x: 132, y: 496, fontSize: 16, fillColor: '#7CFC9B', fontFamily: 'Roboto Mono', opacity: 0 });\nPinePaper.modifyItem(readout, { animationType: 'keyframe', duration: DUR, keyframes: roKfs });\n\n// ── HUD: timecode + frame counter (content keyframes across BOTH passes),\n// pass tags, REW indicator, scrubber with playhead, safe-area brackets.\nfunction tc(t) { var fr = Math.floor(t * 30) % 30, s = Math.floor(t) % 60; return '00:00:' + String(s).padStart(2, '0') + ':' + String(fr).padStart(2, '0'); }\nvar tcKfs = [], fcKfs = [];\nfor (var q = 0; q <= DUR * FPS; q++) {\n  var tq = q / FPS;\n  var fIdx = tq < RAW0 ? 0 : tq < RAW1 ? Math.min(N - 1, Math.floor((tq - RAW0) / DT)) : tq < REW1 ? REWF[Math.min(5, Math.floor((tq - REW0) / (0.5 / 6)))] : tq < GR1 ? Math.min(N - 1, Math.floor((tq - GR0) / DT)) : N - 1;\n  tcKfs.push({ time: tq, properties: { content: tc(tq) } });\n  fcKfs.push({ time: tq, properties: { content: 'F' + String(fIdx + 1).padStart(3, '0') + '/' + String(N).padStart(3, '0') } });\n}\nvar tcT = PinePaper.create('text', { content: '00:00:00:00', x: 96, y: 40, fontSize: 17, fillColor: '#ffffff', fontFamily: 'Roboto Mono' });\nPinePaper.modifyItem(tcT, { animationType: 'keyframe', duration: DUR, keyframes: tcKfs });\nvar fcT = PinePaper.create('text', { content: 'F001/036', x: 868, y: 40, fontSize: 16, fillColor: '#9aa3c0', fontFamily: 'Roboto Mono' });\nPinePaper.modifyItem(fcT, { animationType: 'keyframe', duration: DUR, keyframes: fcKfs });\nfunction tag(txt, col, a, b) {\n  var t = PinePaper.create('text', { content: txt, x: W / 2, y: 40, fontSize: 16, fillColor: col, fontFamily: 'Roboto Mono', opacity: 0 });\n  // HOLD AT ZERO until the instant the window opens. Keyframes interpolate,\n  // so 'opacity 0 at t=0, opacity 1 at t=4' means the tag is half-visible at\n  // t=2 — which is why RAW PASS, REW and GRADED were all legible at once,\n  // stacked on the same baseline, instead of taking turns.\n  PinePaper.modifyItem(t, { animationType: 'keyframe', duration: DUR, keyframes: [\n    { time: 0, properties: { opacity: 0 } },\n    { time: Math.max(0.001, a - 0.05), properties: { opacity: 0 } },\n    { time: a, properties: { opacity: 1 } },\n    { time: b, properties: { opacity: 1 } }, { time: b + 0.05, properties: { opacity: 0 } }, { time: DUR, properties: { opacity: 0 } }\n  ] });\n  return t;\n}\ntag('\\u25cf RAW PASS', '#ff6b6b', 0.02, RAW1);\ntag('\\u25c0\\u25c0 REW', '#ffd27a', REW0, REW1);\ntag('\\u25cf GRADED \\u00b7 teal/amber', '#7dd3fc', GR0, DUR);\n// Scrubber: rail, IN/OUT ticks at pass bounds, playhead tracking session time.\nPinePaper.create('line', { from: [80, 520], to: [880, 520], strokeColor: '#3a4160', strokeWidth: 3 });\n[[RAW0, '#ff6b6b'], [GR0, '#7dd3fc'], [GR1, '#9aa3c0']].forEach(function (m) {\n  PinePaper.create('line', { from: [80 + 800 * (m[0] / DUR), 514], to: [80 + 800 * (m[0] / DUR), 526], strokeColor: m[1], strokeWidth: 2 });\n});\nvar head = PinePaper.create('circle', { x: 80, y: 520, radius: 6, fillColor: '#ffffff' });\nPinePaper.modifyItem(head, { animationType: 'keyframe', duration: DUR, keyframes: [\n  { time: 0, properties: { x: 80 } }, { time: DUR, properties: { x: 880 }, easing: 'linear' }\n] });\n// Safe-area brackets.\n[[36, 24, 1, 1], [924, 24, -1, 1], [36, 516, 1, -1], [924, 516, -1, -1]].forEach(function (c) {\n  PinePaper.create('path', { points: [[c[0] + 18 * c[2], c[1]], [c[0], c[1]], [c[0], c[1] + 18 * c[3]]], strokeColor: '#5a6284', strokeWidth: 2, fillColor: null, closed: false });\n});\n\n// ── End card: what the pipeline just did, and where the footage came from.\nvar plate = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, fillColor: '#05030f', opacity: 0 });\nvar line1 = PinePaper.create('text', { content: 'video in \\u00b7 tracked \\u00b7 rewound \\u00b7 graded \\u00b7 video out', x: W / 2, y: 258, fontSize: 30, fillColor: '#e2e8f0', fontFamily: 'Roboto', opacity: 0 });\nvar line2 = PinePaper.create('text', { content: 'footage: NASA ISS timelapse \\u00b7 public domain \\u00b7 tracking data measured from the pixels', x: W / 2, y: 300, fontSize: 16, fillColor: '#8b93b8', fontFamily: 'Roboto Mono', opacity: 0 });\n[[plate, 0.82], [line1, 1], [line2, 1]].forEach(function (e) {\n  PinePaper.modifyItem(e[0], { animationType: 'keyframe', duration: DUR, keyframes: [\n    { time: 0, properties: { opacity: 0 } }, { time: END0, properties: { opacity: 0 } },\n    { time: END0 + 0.5, properties: { opacity: e[1] }, easing: 'easeOut' }, { time: DUR, properties: { opacity: e[1] } }\n  ] });\n});"}}]},"exportProfile":{"format":"mp4","resolution":"1080p","fps":30,"durationSeconds":6},"source":{"prompt":"Real footage, edited live. Thirty-six frames of a NASA ISS timelapse — Earth's limb, red airglow, lightning storms firing below — play as an image sequence structured as an edit session: a RAW pass where a tracking ring locks onto each lightning cell using centroids measured from the pixels, with live coordinates in a monospace readout; a half-second rewind with REW indicator; then the same footage again through a teal-and-amber blend-mode grade revealed by a sweeping wipe edge. Timecode, frame counter, pass tags, safe-area brackets and a scrubber with pass marks run throughout. End card: video in · tracked · rewound · graded · video out.","provider":"anthropic","model":"claude-fable-5","app":"gallery-reproduction"}},"ops":[{"tool":"pinepaper_set_canvas_size","args":{"width":960,"height":540}},{"tool":"pinepaper_set_background_color","args":{"color":"#05030f"}},{"tool":"pinepaper_execute_custom_code","args":{"description":"Storm watch: 36-frame NASA footage sequence as an edit session — raw pass with measured flash tracking, fast rewind, blend-mode graded pass, scrubber HUD","code":"var W = 960, H = 540, FPS = 12, N = 36;\n// Measured flash track: brightest-cell centroid per frame (960x540 coords),\n// i = peak luminance, n = refined-cluster size. i>=248 && n>=19 gates the ring.\nvar TRACK = [[545,355,254,67],[669,370,253,80],[673,370,249,10],[483,377,240,4],[764,381,248,5],[762,448,254,43],[838,381,247,24],[869,382,241,51],[637,396,254,93],[614,423,249,6],[928,406,251,19],[499,372,252,57],[692,426,254,109],[591,449,243,14],[606,460,245,17],[558,407,249,25],[871,387,254,64],[286,460,253,36],[681,516,241,19],[607,431,254,61],[636,480,247,20],[318,522,254,111],[653,475,252,33],[655,511,243,17],[691,492,254,99],[414,371,255,46],[447,373,254,21],[446,377,254,51],[460,382,247,16],[222,492,224,12],[225,506,245,15],[560,400,253,59],[495,408,254,66],[480,423,254,94],[513,424,254,39],[535,438,254,41]];\nvar DT = 1 / FPS;\n// Session timeline: raw pass, fast rewind, graded pass, end card.\nvar RAW0 = 0.25, RAW1 = RAW0 + N * DT;      // 0.25 → 3.25\nvar REW0 = RAW1, REW1 = REW0 + 0.5;         // 3.25 → 3.75, 6 reversed samples\nvar GR0 = REW1, GR1 = GR0 + N * DT;         // 3.75 → 6.75\nvar END0 = GR1 + 0.15, DUR = 8;\nvar REWF = [35, 28, 21, 14, 7, 0];\n\n// ── Footage: one raster per frame, opacity-window keyframes ONLY (no\n// The 36 paths are LITERALS, not built from the index at runtime.\n// The Worker resolves scene imagery by scanning the serialized scene\n// for /scene-assets/ strings and shipping those bytes to the container,\n// which has no network of its own. A path assembled as\n//   PREFIX + String(i+1).padStart(2,'0') + '.jpg'   (PREFIX built at runtime)\n// matches nothing that scan can see, so not one frame of footage ever\n// travelled — the video-editing piece played its whole HUD over black.\nvar FRAME_PATHS = ['/scene-assets/storm-f01.jpg', '/scene-assets/storm-f02.jpg', '/scene-assets/storm-f03.jpg', '/scene-assets/storm-f04.jpg', '/scene-assets/storm-f05.jpg', '/scene-assets/storm-f06.jpg', '/scene-assets/storm-f07.jpg', '/scene-assets/storm-f08.jpg', '/scene-assets/storm-f09.jpg', '/scene-assets/storm-f10.jpg', '/scene-assets/storm-f11.jpg', '/scene-assets/storm-f12.jpg', '/scene-assets/storm-f13.jpg', '/scene-assets/storm-f14.jpg', '/scene-assets/storm-f15.jpg', '/scene-assets/storm-f16.jpg', '/scene-assets/storm-f17.jpg', '/scene-assets/storm-f18.jpg', '/scene-assets/storm-f19.jpg', '/scene-assets/storm-f20.jpg', '/scene-assets/storm-f21.jpg', '/scene-assets/storm-f22.jpg', '/scene-assets/storm-f23.jpg', '/scene-assets/storm-f24.jpg', '/scene-assets/storm-f25.jpg', '/scene-assets/storm-f26.jpg', '/scene-assets/storm-f27.jpg', '/scene-assets/storm-f28.jpg', '/scene-assets/storm-f29.jpg', '/scene-assets/storm-f30.jpg', '/scene-assets/storm-f31.jpg', '/scene-assets/storm-f32.jpg', '/scene-assets/storm-f33.jpg', '/scene-assets/storm-f34.jpg', '/scene-assets/storm-f35.jpg', '/scene-assets/storm-f36.jpg'];\n// FOOTAGE: 36 frames as ENGINE image items, one per source frame, each\n// shown only in its own windows — the raw pass, the rewind samples, and\n// the graded pass. They were raw paper.Raster objects with data.keyframes,\n// and in the server renderer that means two fatal things at once: a raw\n// raster never advances a keyframe (so every frame sat at opacity 0 and\n// the video-editing piece played eight seconds of empty HUD over black),\n// and it draws above every app item anyway, which would have buried the\n// timecode, the tracking ring and the scrubber underneath the footage.\n// An image item animates and layers like anything else the app makes.\nfor (var i = 0; i < N; i++) (function (i) {\n  var p = FRAME_PATHS[i];\n  var src = (window.__ppAssets && window.__ppAssets[p]) || p;\n  var kfs = [{ time: 0, properties: { opacity: 0 } }];\n  function win(a, b) {\n    // A hold at zero immediately before the cut. Without it the frame ramps\n    // in from the previous keyframe and all 36 are partly composited at once\n    // — a smear instead of a cut.\n    kfs.push({ time: Math.max(0.001, a - 0.001), properties: { opacity: 0 } });\n    kfs.push({ time: a, properties: { opacity: 1 } });\n    kfs.push({ time: b, properties: { opacity: 1 } });\n    kfs.push({ time: b + 0.001, properties: { opacity: 0 } });\n  }\n  var t0 = RAW0 + i * DT;\n  if (i === 0) { kfs[0].properties.opacity = 1; kfs.push({ time: t0, properties: { opacity: 1 } }); }\n  win(t0, t0 + DT);\n  for (var r = 0; r < REWF.length; r++) if (REWF[r] === i) win(REW0 + r * (0.5 / 6), REW0 + (r + 1) * (0.5 / 6));\n  var g0 = GR0 + i * DT;\n  win(g0, g0 + DT);\n  if (i === N - 1) { kfs.push({ time: GR1, properties: { opacity: 1 } }); kfs.push({ time: DUR, properties: { opacity: 1 } }); }\n  var fid = PinePaper.create('image', { src: src, x: W / 2, y: H / 2, width: W, height: H, opacity: (i === 0) ? 1 : 0 });\n  PinePaper.modifyItem(fid, { animationType: 'keyframe', duration: DUR, keyframes: kfs });\n})(i);\n\n// ── Grade stack (graded pass only): teal multiply + amber screen + edge\n// vignette, blend-mode compositing over the footage, revealed by a wipe.\nvar gradeTeal = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, fillColor: '#0e3a44', opacity: 0 });\ngradeTeal.blendMode = 'multiply';\nvar gradeAmber = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, fillColor: '#ff8a3c', opacity: 0 });\ngradeAmber.blendMode = 'screen';\nvar vig = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, opacity: 0 });\nvig.fillColor = { gradient: { stops: [[new paper.Color(0, 0, 0, 0), 0.55], [new paper.Color(0, 0, 0, 0.8), 1]], radial: true }, origin: [W / 2, H / 2], destination: [W, H / 2] };\nvig.blendMode = 'multiply';\n// The wipe: opacity ramps L→R are not a thing, so the reveal is a hard\n// left-anchored width sweep on a masking duplicate — instead, keyframe the\n// stack's opacity in with the wipe LINE sweeping across as the visual edge.\n[gradeTeal, gradeAmber, vig].forEach(function (r, gi) {\n  var full = [0.55, 0.18, 0.7][gi];\n  PinePaper.modifyItem(r, { animationType: 'keyframe', duration: DUR, keyframes: [\n    { time: 0, properties: { opacity: 0 } }, { time: GR0, properties: { opacity: 0 } },\n    { time: GR0 + 1.0, properties: { opacity: full }, easing: 'linear' },\n    { time: DUR, properties: { opacity: full } }\n  ] });\n});\nvar wipeEdge = PinePaper.create('line', { from: [0, 0], to: [0, H], strokeColor: '#ffd27a', strokeWidth: 3, opacity: 0 });\nPinePaper.modifyItem(wipeEdge, { animationType: 'keyframe', duration: DUR, keyframes: [\n  { time: 0, properties: { opacity: 0, x: 2 } }, { time: GR0, properties: { opacity: 0.9, x: 2 } },\n  { time: GR0 + 1.0, properties: { opacity: 0.9, x: W - 2 }, easing: 'linear' },\n  { time: GR0 + 1.15, properties: { opacity: 0, x: W - 2 } }, { time: DUR, properties: { opacity: 0, x: W - 2 } }\n] });\n\n// ── Tracking overlay (raw pass): ring + crosshair snapping to the measured\n// centroids, readout with live coordinates, gated on flash strength.\nvar ringKfs = [{ time: 0, properties: { opacity: 0, x: TRACK[0][0], y: TRACK[0][1] } }];\nvar roKfs = [{ time: 0, properties: { content: 'ACQ …', opacity: 0 } }];\nfor (var f = 0; f < N; f++) {\n  var t = RAW0 + f * DT, tr = TRACK[f];\n  var hot = tr[2] >= 248 && tr[3] >= 19;\n  ringKfs.push({ time: t, properties: { x: tr[0], y: tr[1], opacity: hot ? 0.95 : 0.25 } });\n  roKfs.push({ time: t, properties: { content: (hot ? 'LOCK ' : 'scan ') + 'x' + tr[0] + ' y' + tr[1] + ' \\u00b7 I' + tr[2], opacity: 1 } });\n}\nringKfs.push({ time: RAW1, properties: { opacity: 0 } }, { time: DUR, properties: { opacity: 0 } });\nroKfs.push({ time: RAW1, properties: { opacity: 0 } }, { time: DUR, properties: { opacity: 0 } });\nvar ring = PinePaper.create('circle', { x: TRACK[0][0], y: TRACK[0][1], radius: 34, strokeColor: '#7CFC9B', strokeWidth: 2.5, fillColor: null, opacity: 0 });\nPinePaper.modifyItem(ring, { animationType: 'keyframe', duration: DUR, keyframes: ringKfs });\nvar readout = PinePaper.create('text', { content: 'ACQ …', x: 132, y: 496, fontSize: 16, fillColor: '#7CFC9B', fontFamily: 'Roboto Mono', opacity: 0 });\nPinePaper.modifyItem(readout, { animationType: 'keyframe', duration: DUR, keyframes: roKfs });\n\n// ── HUD: timecode + frame counter (content keyframes across BOTH passes),\n// pass tags, REW indicator, scrubber with playhead, safe-area brackets.\nfunction tc(t) { var fr = Math.floor(t * 30) % 30, s = Math.floor(t) % 60; return '00:00:' + String(s).padStart(2, '0') + ':' + String(fr).padStart(2, '0'); }\nvar tcKfs = [], fcKfs = [];\nfor (var q = 0; q <= DUR * FPS; q++) {\n  var tq = q / FPS;\n  var fIdx = tq < RAW0 ? 0 : tq < RAW1 ? Math.min(N - 1, Math.floor((tq - RAW0) / DT)) : tq < REW1 ? REWF[Math.min(5, Math.floor((tq - REW0) / (0.5 / 6)))] : tq < GR1 ? Math.min(N - 1, Math.floor((tq - GR0) / DT)) : N - 1;\n  tcKfs.push({ time: tq, properties: { content: tc(tq) } });\n  fcKfs.push({ time: tq, properties: { content: 'F' + String(fIdx + 1).padStart(3, '0') + '/' + String(N).padStart(3, '0') } });\n}\nvar tcT = PinePaper.create('text', { content: '00:00:00:00', x: 96, y: 40, fontSize: 17, fillColor: '#ffffff', fontFamily: 'Roboto Mono' });\nPinePaper.modifyItem(tcT, { animationType: 'keyframe', duration: DUR, keyframes: tcKfs });\nvar fcT = PinePaper.create('text', { content: 'F001/036', x: 868, y: 40, fontSize: 16, fillColor: '#9aa3c0', fontFamily: 'Roboto Mono' });\nPinePaper.modifyItem(fcT, { animationType: 'keyframe', duration: DUR, keyframes: fcKfs });\nfunction tag(txt, col, a, b) {\n  var t = PinePaper.create('text', { content: txt, x: W / 2, y: 40, fontSize: 16, fillColor: col, fontFamily: 'Roboto Mono', opacity: 0 });\n  // HOLD AT ZERO until the instant the window opens. Keyframes interpolate,\n  // so 'opacity 0 at t=0, opacity 1 at t=4' means the tag is half-visible at\n  // t=2 — which is why RAW PASS, REW and GRADED were all legible at once,\n  // stacked on the same baseline, instead of taking turns.\n  PinePaper.modifyItem(t, { animationType: 'keyframe', duration: DUR, keyframes: [\n    { time: 0, properties: { opacity: 0 } },\n    { time: Math.max(0.001, a - 0.05), properties: { opacity: 0 } },\n    { time: a, properties: { opacity: 1 } },\n    { time: b, properties: { opacity: 1 } }, { time: b + 0.05, properties: { opacity: 0 } }, { time: DUR, properties: { opacity: 0 } }\n  ] });\n  return t;\n}\ntag('\\u25cf RAW PASS', '#ff6b6b', 0.02, RAW1);\ntag('\\u25c0\\u25c0 REW', '#ffd27a', REW0, REW1);\ntag('\\u25cf GRADED \\u00b7 teal/amber', '#7dd3fc', GR0, DUR);\n// Scrubber: rail, IN/OUT ticks at pass bounds, playhead tracking session time.\nPinePaper.create('line', { from: [80, 520], to: [880, 520], strokeColor: '#3a4160', strokeWidth: 3 });\n[[RAW0, '#ff6b6b'], [GR0, '#7dd3fc'], [GR1, '#9aa3c0']].forEach(function (m) {\n  PinePaper.create('line', { from: [80 + 800 * (m[0] / DUR), 514], to: [80 + 800 * (m[0] / DUR), 526], strokeColor: m[1], strokeWidth: 2 });\n});\nvar head = PinePaper.create('circle', { x: 80, y: 520, radius: 6, fillColor: '#ffffff' });\nPinePaper.modifyItem(head, { animationType: 'keyframe', duration: DUR, keyframes: [\n  { time: 0, properties: { x: 80 } }, { time: DUR, properties: { x: 880 }, easing: 'linear' }\n] });\n// Safe-area brackets.\n[[36, 24, 1, 1], [924, 24, -1, 1], [36, 516, 1, -1], [924, 516, -1, -1]].forEach(function (c) {\n  PinePaper.create('path', { points: [[c[0] + 18 * c[2], c[1]], [c[0], c[1]], [c[0], c[1] + 18 * c[3]]], strokeColor: '#5a6284', strokeWidth: 2, fillColor: null, closed: false });\n});\n\n// ── End card: what the pipeline just did, and where the footage came from.\nvar plate = PinePaper.create('rectangle', { x: W / 2, y: H / 2, width: W, height: H, fillColor: '#05030f', opacity: 0 });\nvar line1 = PinePaper.create('text', { content: 'video in \\u00b7 tracked \\u00b7 rewound \\u00b7 graded \\u00b7 video out', x: W / 2, y: 258, fontSize: 30, fillColor: '#e2e8f0', fontFamily: 'Roboto', opacity: 0 });\nvar line2 = PinePaper.create('text', { content: 'footage: NASA ISS timelapse \\u00b7 public domain \\u00b7 tracking data measured from the pixels', x: W / 2, y: 300, fontSize: 16, fillColor: '#8b93b8', fontFamily: 'Roboto Mono', opacity: 0 });\n[[plate, 0.82], [line1, 1], [line2, 1]].forEach(function (e) {\n  PinePaper.modifyItem(e[0], { animationType: 'keyframe', duration: DUR, keyframes: [\n    { time: 0, properties: { opacity: 0 } }, { time: END0, properties: { opacity: 0 } },\n    { time: END0 + 0.5, properties: { opacity: e[1] }, easing: 'easeOut' }, { time: DUR, properties: { opacity: e[1] } }\n  ] });\n});"}}],"license":"CC0-1.0"}