dsh-drawai 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (533) hide show
  1. package/LICENSE +4142 -0
  2. package/README.md +43 -10
  3. package/lib/audio-media.js +147 -0
  4. package/lib/audio-service.js +66 -0
  5. package/lib/canvas-format.js +134 -0
  6. package/lib/client.js +5075 -181
  7. package/lib/comfyui-default-workflow.mjs +1825 -0
  8. package/lib/comfyui-workflow.mjs +426 -0
  9. package/lib/document-commands.js +12 -0
  10. package/lib/document-core.js +29 -0
  11. package/lib/drawing-tasks.js +26 -0
  12. package/lib/imagegen.mjs +596 -0
  13. package/lib/index.js +1758 -129
  14. package/lib/media.js +26 -13
  15. package/lib/mgstyle-core.js +75 -0
  16. package/lib/motion/apply.js +180 -0
  17. package/lib/motion/asset-pack-lock.js +10 -0
  18. package/lib/motion/asset-pack.js +335 -0
  19. package/lib/motion/assets.js +290 -0
  20. package/lib/motion/audio.js +103 -0
  21. package/lib/motion/audiolevel.js +63 -0
  22. package/lib/motion/audiomix.js +144 -0
  23. package/lib/motion/beats.js +337 -0
  24. package/lib/motion/builtin-assets.js +62 -0
  25. package/lib/motion/canvas-spec.js +442 -0
  26. package/lib/motion/capture.js +95 -0
  27. package/lib/motion/cards.js +80 -0
  28. package/lib/motion/collect.js +447 -0
  29. package/lib/motion/delivery.js +112 -0
  30. package/lib/motion/edge-speech.py +90 -0
  31. package/lib/motion/entry.jsx +73 -0
  32. package/lib/motion/export.js +200 -0
  33. package/lib/motion/ffmpeg.js +42 -0
  34. package/lib/motion/fonts.js +71 -0
  35. package/lib/motion/jobs.js +113 -0
  36. package/lib/motion/json.js +4 -0
  37. package/lib/motion/media-catalog.js +59 -0
  38. package/lib/motion/mgstyles/BRIEF-TEMPLATE.md +63 -0
  39. package/lib/motion/mgstyles/briefs/01-flat-vector.md +104 -0
  40. package/lib/motion/mgstyles/briefs/02-line-art.md +97 -0
  41. package/lib/motion/mgstyles/briefs/03-isometric.md +108 -0
  42. package/lib/motion/mgstyles/briefs/05-cel-boil.md +145 -0
  43. package/lib/motion/mgstyles/briefs/06-collage.md +149 -0
  44. package/lib/motion/mgstyles/briefs/07-liquid.md +106 -0
  45. package/lib/motion/mgstyles/briefs/08-morph.md +111 -0
  46. package/lib/motion/mgstyles/briefs/09-bauhaus.md +150 -0
  47. package/lib/motion/mgstyles/briefs/10-synthwave.md +151 -0
  48. package/lib/motion/mgstyles/briefs/12-aurora-glass.md +147 -0
  49. package/lib/motion/mgstyles/briefs/18-hanazi.md +140 -0
  50. package/lib/motion/mgstyles/briefs/19-paperclip.md +156 -0
  51. package/lib/motion/mgstyles/briefs/20-pixel.md +118 -0
  52. package/lib/motion/mgstyles/briefs/22-hud.md +118 -0
  53. package/lib/motion/mgstyles/catalog.d/01-flat-vector.json +34 -0
  54. package/lib/motion/mgstyles/catalog.d/02-line-art.json +34 -0
  55. package/lib/motion/mgstyles/catalog.d/03-isometric.json +35 -0
  56. package/lib/motion/mgstyles/catalog.d/05-cel-boil.json +28 -0
  57. package/lib/motion/mgstyles/catalog.d/06-collage.json +28 -0
  58. package/lib/motion/mgstyles/catalog.d/07-liquid.json +28 -0
  59. package/lib/motion/mgstyles/catalog.d/08-morph.json +28 -0
  60. package/lib/motion/mgstyles/catalog.d/09-bauhaus.json +28 -0
  61. package/lib/motion/mgstyles/catalog.d/10-synthwave.json +28 -0
  62. package/lib/motion/mgstyles/catalog.d/12-aurora-glass.json +28 -0
  63. package/lib/motion/mgstyles/catalog.d/18-hanazi.json +28 -0
  64. package/lib/motion/mgstyles/catalog.d/19-paperclip.json +28 -0
  65. package/lib/motion/mgstyles/catalog.d/20-pixel.json +28 -0
  66. package/lib/motion/mgstyles/catalog.d/22-hud.json +28 -0
  67. package/lib/motion/mgstyles/catalog.json +1356 -0
  68. package/lib/motion/mgstyles/upstream/README.md +147 -0
  69. package/lib/motion/mgstyles/upstream/README.zh-CN.md +147 -0
  70. package/lib/motion/mgstyles/upstream/demos/01-flat-vector/cues.json +1 -0
  71. package/lib/motion/mgstyles/upstream/demos/01-flat-vector/index.html +712 -0
  72. package/lib/motion/mgstyles/upstream/demos/02-line-art/cues.json +331 -0
  73. package/lib/motion/mgstyles/upstream/demos/02-line-art/film.js +676 -0
  74. package/lib/motion/mgstyles/upstream/demos/02-line-art/index.html +18 -0
  75. package/lib/motion/mgstyles/upstream/demos/02-line-art/tools/dump_timing.mjs +26 -0
  76. package/lib/motion/mgstyles/upstream/demos/03-isometric/cues.json +109 -0
  77. package/lib/motion/mgstyles/upstream/demos/03-isometric/index.html +25 -0
  78. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/anim.js +82 -0
  79. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/main.js +219 -0
  80. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/night5.js +180 -0
  81. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/render.js +202 -0
  82. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/title.js +167 -0
  83. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/title5.js +294 -0
  84. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/ui.js +141 -0
  85. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/world.js +1399 -0
  86. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/cues.json +157 -0
  87. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/index.html +34 -0
  88. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/js/engine.js +326 -0
  89. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/js/scene.js +2036 -0
  90. package/lib/motion/mgstyles/upstream/demos/06-collage/assets/gen/layout.json +157 -0
  91. package/lib/motion/mgstyles/upstream/demos/06-collage/cues.json +80 -0
  92. package/lib/motion/mgstyles/upstream/demos/06-collage/index.html +889 -0
  93. package/lib/motion/mgstyles/upstream/demos/07-liquid/cues.json +100 -0
  94. package/lib/motion/mgstyles/upstream/demos/07-liquid/index.html +108 -0
  95. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/engine.js +153 -0
  96. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/links.js +64 -0
  97. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/lookdev.js +46 -0
  98. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/scene.js +963 -0
  99. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/shaders.js +647 -0
  100. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/textsdf.js +114 -0
  101. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/type.js +50 -0
  102. package/lib/motion/mgstyles/upstream/demos/08-morph/cues.json +131 -0
  103. package/lib/motion/mgstyles/upstream/demos/08-morph/glyphs.json +1 -0
  104. package/lib/motion/mgstyles/upstream/demos/08-morph/index.html +901 -0
  105. package/lib/motion/mgstyles/upstream/demos/09-bauhaus/cues.json +270 -0
  106. package/lib/motion/mgstyles/upstream/demos/09-bauhaus/film.js +697 -0
  107. package/lib/motion/mgstyles/upstream/demos/09-bauhaus/index.html +12 -0
  108. package/lib/motion/mgstyles/upstream/demos/10-synthwave/cues.json +44 -0
  109. package/lib/motion/mgstyles/upstream/demos/10-synthwave/index.html +1028 -0
  110. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/cues.json +67 -0
  111. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/index.html +16 -0
  112. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/main.js +967 -0
  113. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/shaders.js +468 -0
  114. package/lib/motion/mgstyles/upstream/demos/18-hanazi/cues.json +117 -0
  115. package/lib/motion/mgstyles/upstream/demos/18-hanazi/index.html +541 -0
  116. package/lib/motion/mgstyles/upstream/demos/19-paperclip/assets/countries-110m.json +1 -0
  117. package/lib/motion/mgstyles/upstream/demos/19-paperclip/assets/stickers/meta.json +34 -0
  118. package/lib/motion/mgstyles/upstream/demos/19-paperclip/cues.json +285 -0
  119. package/lib/motion/mgstyles/upstream/demos/19-paperclip/index.html +599 -0
  120. package/lib/motion/mgstyles/upstream/demos/20-pixel/cues.json +42 -0
  121. package/lib/motion/mgstyles/upstream/demos/20-pixel/film.js +914 -0
  122. package/lib/motion/mgstyles/upstream/demos/20-pixel/index.html +18 -0
  123. package/lib/motion/mgstyles/upstream/demos/22-hud/cues.json +108 -0
  124. package/lib/motion/mgstyles/upstream/demos/22-hud/index.html +125 -0
  125. package/lib/motion/mgstyles/upstream/demos/22-hud/js/holo.js +275 -0
  126. package/lib/motion/mgstyles/upstream/demos/22-hud/js/hud.js +855 -0
  127. package/lib/motion/mgstyles/upstream/demos/22-hud/js/post.js +150 -0
  128. package/lib/motion/mgstyles/upstream/demos/22-hud/js/util.js +65 -0
  129. package/lib/motion/mgstyles/upstream/demos/22-hud/js/v5.js +142 -0
  130. package/lib/motion/mgstyles/upstream/harness/render.mjs +319 -0
  131. package/lib/motion/mgstyles/upstream/manifest.json +269 -0
  132. package/lib/motion/mgstyles/upstream/prompts/01-flat-vector.md +98 -0
  133. package/lib/motion/mgstyles/upstream/prompts/02-line-art.md +95 -0
  134. package/lib/motion/mgstyles/upstream/prompts/03-isometric.md +97 -0
  135. package/lib/motion/mgstyles/upstream/prompts/05-cel-boil.md +95 -0
  136. package/lib/motion/mgstyles/upstream/prompts/06-collage.md +95 -0
  137. package/lib/motion/mgstyles/upstream/prompts/07-liquid.md +98 -0
  138. package/lib/motion/mgstyles/upstream/prompts/08-morph.md +95 -0
  139. package/lib/motion/mgstyles/upstream/prompts/09-bauhaus.md +96 -0
  140. package/lib/motion/mgstyles/upstream/prompts/10-synthwave.md +95 -0
  141. package/lib/motion/mgstyles/upstream/prompts/12-aurora-glass.md +96 -0
  142. package/lib/motion/mgstyles/upstream/prompts/18-hanazi.md +95 -0
  143. package/lib/motion/mgstyles/upstream/prompts/19-paperclip.md +96 -0
  144. package/lib/motion/mgstyles/upstream/prompts/20-pixel.md +96 -0
  145. package/lib/motion/mgstyles/upstream/prompts/22-hud.md +95 -0
  146. package/lib/motion/mgstyles/upstream/rubric.md +51 -0
  147. package/lib/motion/mgstyles/upstream/template.md +36 -0
  148. package/lib/motion/pixelqa.js +198 -0
  149. package/lib/motion/preview.js +95 -0
  150. package/lib/motion/project.js +395 -0
  151. package/lib/motion/qa.js +290 -0
  152. package/lib/motion/reference-media.jsx +10 -0
  153. package/lib/motion/render-settings.js +112 -0
  154. package/lib/motion/render.js +159 -0
  155. package/lib/motion/scene-kit.js +183 -0
  156. package/lib/motion/sfx.js +232 -0
  157. package/lib/motion/shotcraft/ClipCard.tsx +204 -0
  158. package/lib/motion/shotcraft/catalog.json +2461 -0
  159. package/lib/motion/shotcraft/upstream/SKILL.md +286 -0
  160. package/lib/motion/shotcraft/upstream/assets/lib/Caption.tsx +54 -0
  161. package/lib/motion/shotcraft/upstream/assets/lib/ClipCard.tsx +200 -0
  162. package/lib/motion/shotcraft/upstream/assets/lib/DigitRoll.tsx +45 -0
  163. package/lib/motion/shotcraft/upstream/assets/lib/FlashCut.tsx +20 -0
  164. package/lib/motion/shotcraft/upstream/assets/lib/FlatPanel.tsx +65 -0
  165. package/lib/motion/shotcraft/upstream/assets/lib/PageCam.tsx +151 -0
  166. package/lib/motion/shotcraft/upstream/assets/lib/VerticalTicker.tsx +129 -0
  167. package/lib/motion/shotcraft/upstream/assets/lib/helpers/camera.tsx +75 -0
  168. package/lib/motion/shotcraft/upstream/assets/lib/helpers/motion.ts +40 -0
  169. package/lib/motion/shotcraft/upstream/assets/lib/helpers/rand.ts +11 -0
  170. package/lib/motion/shotcraft/upstream/assets/lib/helpers/shake.ts +10 -0
  171. package/lib/motion/shotcraft/upstream/assets/scripts/__pycache__/sfx-source.cpython-313.pyc +0 -0
  172. package/lib/motion/shotcraft/upstream/assets/scripts/build-readme-cover.mjs +49 -0
  173. package/lib/motion/shotcraft/upstream/assets/scripts/capture-template.mjs +210 -0
  174. package/lib/motion/shotcraft/upstream/assets/scripts/sfx-source.py +423 -0
  175. package/lib/motion/shotcraft/upstream/assets/scripts/smoke-render-demos.py +170 -0
  176. package/lib/motion/shotcraft/upstream/assets/scripts/test_sfx_source.py +157 -0
  177. package/lib/motion/shotcraft/upstream/demos/README.md +120 -0
  178. package/lib/motion/shotcraft/upstream/demos/_fixtures/Brand.tsx +112 -0
  179. package/lib/motion/shotcraft/upstream/demos/_fixtures/Fixtures.tsx +647 -0
  180. package/lib/motion/shotcraft/upstream/demos/_fixtures/Look.tsx +316 -0
  181. package/lib/motion/shotcraft/upstream/demos/_fixtures/Motion.tsx +82 -0
  182. package/lib/motion/shotcraft/upstream/demos/_fixtures/PageCam2D.tsx +115 -0
  183. package/lib/motion/shotcraft/upstream/demos/_fixtures/Polish.tsx +243 -0
  184. package/lib/motion/shotcraft/upstream/demos/_textures/live-layout.json +389 -0
  185. package/lib/motion/shotcraft/upstream/demos/camera/basic-3d-scene/Basic3DScene.tsx +250 -0
  186. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashImpactReal.tsx +305 -0
  187. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashZoomReal.tsx +246 -0
  188. package/lib/motion/shotcraft/upstream/demos/camera/cursor-flyover/CursorFlyover.tsx +390 -0
  189. package/lib/motion/shotcraft/upstream/demos/camera/depth-layer-moves/MultiplaneReal.tsx +138 -0
  190. package/lib/motion/shotcraft/upstream/demos/camera/graze-face-tour/GrazeFaceTour.tsx +542 -0
  191. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/OverheadTabletopDrop.tsx +315 -0
  192. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/TiltReveal.tsx +299 -0
  193. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/DroneDiveLanding.tsx +173 -0
  194. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/ExplodedView.tsx +348 -0
  195. package/lib/motion/shotcraft/upstream/demos/camera/steep-tilt-glide/SteepTiltGlide.tsx +366 -0
  196. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx +279 -0
  197. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/PullBackIsolation.tsx +184 -0
  198. package/lib/motion/shotcraft/upstream/demos/camera/terminal-3d/Terminal3D.tsx +390 -0
  199. package/lib/motion/shotcraft/upstream/demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx +155 -0
  200. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/AxisRescaleShockV2.tsx +303 -0
  201. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx +290 -0
  202. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx +278 -0
  203. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx +307 -0
  204. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx +215 -0
  205. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/BrakeReticleLock.tsx +299 -0
  206. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx +232 -0
  207. package/lib/motion/shotcraft/upstream/demos/data/timeline-travel/TimelineTravel.tsx +290 -0
  208. package/lib/motion/shotcraft/upstream/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx +273 -0
  209. package/lib/motion/shotcraft/upstream/demos/effects/aurora-bloom-bg-flip/AuroraBloomBgFlip.tsx +167 -0
  210. package/lib/motion/shotcraft/upstream/demos/effects/brand-frame-snap/BrandFrameSnap.tsx +536 -0
  211. package/lib/motion/shotcraft/upstream/demos/effects/fui-hud-moves/ReticleLockOn.tsx +164 -0
  212. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/FlylineArc.tsx +175 -0
  213. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx +197 -0
  214. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx +241 -0
  215. package/lib/motion/shotcraft/upstream/demos/effects/icon-performance-moves/AttentionBounce.tsx +279 -0
  216. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/AnimeImpact.tsx +230 -0
  217. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/HitCounter.tsx +345 -0
  218. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SheenSweepRetry.tsx +275 -0
  219. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SpotlightSweepReveal.tsx +229 -0
  220. package/lib/motion/shotcraft/upstream/demos/effects/radial-ripple-phone-chips/RadialRipplePhoneChips.tsx +312 -0
  221. package/lib/motion/shotcraft/upstream/demos/effects/riso-print-hits/RisoMisregistrationHit.tsx +222 -0
  222. package/lib/motion/shotcraft/upstream/demos/effects/scan-bracket-sweep/ScanBracketSweep.tsx +295 -0
  223. package/lib/motion/shotcraft/upstream/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx +329 -0
  224. package/lib/motion/shotcraft/upstream/demos/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.tsx +315 -0
  225. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ImpactBurstKit.tsx +264 -0
  226. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/KanadaPerspectiveSnap.tsx +260 -0
  227. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ScoreSlam.tsx +263 -0
  228. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/CornerSpotlightReveal.tsx +123 -0
  229. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/GlowWakeSleepPanel.tsx +187 -0
  230. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/SlideSpotlightPan.tsx +175 -0
  231. package/lib/motion/shotcraft/upstream/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx +305 -0
  232. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/DiagramCascadeBuild.tsx +350 -0
  233. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/PanelToCanvasMaterialize.tsx +352 -0
  234. package/lib/motion/shotcraft/upstream/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx +218 -0
  235. package/lib/motion/shotcraft/upstream/demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx +297 -0
  236. package/lib/motion/shotcraft/upstream/demos/interaction/glass-pill-dictation-typing/GlassPillDictationTyping.tsx +157 -0
  237. package/lib/motion/shotcraft/upstream/demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx +218 -0
  238. package/lib/motion/shotcraft/upstream/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx +202 -0
  239. package/lib/motion/shotcraft/upstream/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx +265 -0
  240. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx +457 -0
  241. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx +446 -0
  242. package/lib/motion/shotcraft/upstream/demos/interaction/type-and-filter/TypeAndFilter.tsx +249 -0
  243. package/lib/motion/shotcraft/upstream/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx +278 -0
  244. package/lib/motion/shotcraft/upstream/demos/opening/crane-rise-reveal/CraneRiseReveal.tsx +332 -0
  245. package/lib/motion/shotcraft/upstream/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx +358 -0
  246. package/lib/motion/shotcraft/upstream/demos/opening/fracture/Fracture.tsx +201 -0
  247. package/lib/motion/shotcraft/upstream/demos/opening/icon-field-colorize/IconFieldColorize.tsx +200 -0
  248. package/lib/motion/shotcraft/upstream/demos/opening/letterspace-materialize/LetterspaceMaterialize.tsx +208 -0
  249. package/lib/motion/shotcraft/upstream/demos/opening/magician-card-flourish/MagicianCardFlourish.tsx +341 -0
  250. package/lib/motion/shotcraft/upstream/demos/opening/orbit-ring-title-open/OrbitRingTitleOpen.tsx +389 -0
  251. package/lib/motion/shotcraft/upstream/demos/opening/spotlight-hero-card/SpotlightHeroCard.tsx +293 -0
  252. package/lib/motion/shotcraft/upstream/demos/opening/stroke-segment-build/StrokeSegmentBuild.tsx +183 -0
  253. package/lib/motion/shotcraft/upstream/demos/opening/text-as-mask/TextAsMask.tsx +245 -0
  254. package/lib/motion/shotcraft/upstream/demos/outro/grain-dissolve/GrainDissolve.tsx +222 -0
  255. package/lib/motion/shotcraft/upstream/demos/outro/logo-shrink-wordmark-lockup/LogoShrinkWordmarkLockup.tsx +223 -0
  256. package/lib/motion/shotcraft/upstream/demos/outro/neon-triple-marquee/NeonTripleMarquee.tsx +217 -0
  257. package/lib/motion/shotcraft/upstream/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx +343 -0
  258. package/lib/motion/shotcraft/upstream/demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx +357 -0
  259. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx +187 -0
  260. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx +333 -0
  261. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-cut-moves/PaparazziFlash.tsx +274 -0
  262. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx +270 -0
  263. package/lib/motion/shotcraft/upstream/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx +430 -0
  264. package/lib/motion/shotcraft/upstream/demos/rhythm/quad-split-parallel-scenes/QuadSplitParallelScenes.tsx +252 -0
  265. package/lib/motion/shotcraft/upstream/demos/rhythm/rhythm-interrupt-moves/JumpCutPunchIn.tsx +106 -0
  266. package/lib/motion/shotcraft/upstream/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.tsx +288 -0
  267. package/lib/motion/shotcraft/upstream/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx +362 -0
  268. package/lib/motion/shotcraft/upstream/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx +168 -0
  269. package/lib/motion/shotcraft/upstream/demos/transition/bottom-push-stack-wipe/BottomPushStackWipe.tsx +310 -0
  270. package/lib/motion/shotcraft/upstream/demos/transition/card-flip-reveal/CardFlipReveal.tsx +261 -0
  271. package/lib/motion/shotcraft/upstream/demos/transition/card-flock-tumble/CardFlockTumble.tsx +459 -0
  272. package/lib/motion/shotcraft/upstream/demos/transition/circle-match-iris/CircleMatchIris.tsx +294 -0
  273. package/lib/motion/shotcraft/upstream/demos/transition/gradient-transition/GradientTransition.tsx +161 -0
  274. package/lib/motion/shotcraft/upstream/demos/transition/line-carry-transition/LineCarryTransition.tsx +248 -0
  275. package/lib/motion/shotcraft/upstream/demos/transition/mosaic-reframe/MosaicReframe.tsx +359 -0
  276. package/lib/motion/shotcraft/upstream/demos/transition/page-turn-transitions/BarnDoorSplit.tsx +227 -0
  277. package/lib/motion/shotcraft/upstream/demos/transition/print-texture-transitions/InkBleedReveal.tsx +162 -0
  278. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/PortalWipeV2.tsx +248 -0
  279. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/WhipBrakeReal.tsx +186 -0
  280. package/lib/motion/shotcraft/upstream/demos/transition/tear-streak-transitions/GlitchDisplace.tsx +355 -0
  281. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/InvisibleCut.tsx +272 -0
  282. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/LightLeakBurn.tsx +262 -0
  283. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/VersusSlam.tsx +217 -0
  284. package/lib/motion/shotcraft/upstream/demos/transition/transition-travel/LetterformZoom.tsx +285 -0
  285. package/lib/motion/shotcraft/upstream/demos/transition/white-flash-logo-simplify-cut/WhiteFlashLogoSimplifyCut.tsx +205 -0
  286. package/lib/motion/shotcraft/upstream/demos/transition/wipe-transitions/BlindsSlice.tsx +178 -0
  287. package/lib/motion/shotcraft/upstream/demos/typography/blur-slide/BlurSlide.tsx +147 -0
  288. package/lib/motion/shotcraft/upstream/demos/typography/brace-expand/BraceExpand.tsx +143 -0
  289. package/lib/motion/shotcraft/upstream/demos/typography/brand-ink-open/BrandInkOpen.tsx +134 -0
  290. package/lib/motion/shotcraft/upstream/demos/typography/countdown-arc-scatter/CountdownArcScatter.tsx +217 -0
  291. package/lib/motion/shotcraft/upstream/demos/typography/flying-words/FlyingWords.tsx +198 -0
  292. package/lib/motion/shotcraft/upstream/demos/typography/glitch-cycle/GlitchCycle.tsx +221 -0
  293. package/lib/motion/shotcraft/upstream/demos/typography/gradient-word-sweep/GradientWordSweep.tsx +312 -0
  294. package/lib/motion/shotcraft/upstream/demos/typography/lead-word-zoom-assemble/LeadWordZoomAssemble.tsx +261 -0
  295. package/lib/motion/shotcraft/upstream/demos/typography/marker-underline-title/MarkerUnderlineTitle.tsx +121 -0
  296. package/lib/motion/shotcraft/upstream/demos/typography/outline-word-fill/OutlineWordFill.tsx +153 -0
  297. package/lib/motion/shotcraft/upstream/demos/typography/paper-title-card/PaperTitleCard.tsx +179 -0
  298. package/lib/motion/shotcraft/upstream/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx +140 -0
  299. package/lib/motion/shotcraft/upstream/demos/typography/pill-slot-cycle/PillSlotCycle.tsx +309 -0
  300. package/lib/motion/shotcraft/upstream/demos/typography/text-column-converge/TextColumnConverge.tsx +191 -0
  301. package/lib/motion/shotcraft/upstream/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx +292 -0
  302. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx +135 -0
  303. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/SplitTextStagger.tsx +124 -0
  304. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/TextOnPath.tsx +255 -0
  305. package/lib/motion/shotcraft/upstream/demos/typography/type-entrance-moves/LetterDropPhysics.tsx +173 -0
  306. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/FontWeightPump.tsx +225 -0
  307. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/KaraokeFillSync.tsx +141 -0
  308. package/lib/motion/shotcraft/upstream/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx +137 -0
  309. package/lib/motion/shotcraft/upstream/demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx +147 -0
  310. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-filmstrip/WordRelayFilmstrip.tsx +409 -0
  311. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-geometry/WordRelayGeometry.tsx +280 -0
  312. package/lib/motion/shotcraft/upstream/demos/ui-entrance/avatar-bracket-carousel/AvatarBracketCarousel.tsx +271 -0
  313. package/lib/motion/shotcraft/upstream/demos/ui-entrance/bezier-source-converge-merge/BezierSourceConvergeMerge.tsx +385 -0
  314. package/lib/motion/shotcraft/upstream/demos/ui-entrance/card-stack/CardStack.tsx +312 -0
  315. package/lib/motion/shotcraft/upstream/demos/ui-entrance/carousel-3d/Carousel3D.tsx +303 -0
  316. package/lib/motion/shotcraft/upstream/demos/ui-entrance/clipcard-looping/ClipCardLooping.tsx +77 -0
  317. package/lib/motion/shotcraft/upstream/demos/ui-entrance/cloner-depth-echo/ClonerDepthEcho.tsx +311 -0
  318. package/lib/motion/shotcraft/upstream/demos/ui-entrance/deck-deal-flyin/DeckDealFlyin.tsx +307 -0
  319. package/lib/motion/shotcraft/upstream/demos/ui-entrance/doc-park-left-pill-deal/DocParkLeftPillDeal.tsx +349 -0
  320. package/lib/motion/shotcraft/upstream/demos/ui-entrance/document-typewriter-reveal/DocumentTypewriterReveal.tsx +255 -0
  321. package/lib/motion/shotcraft/upstream/demos/ui-entrance/draw-svg-trace/DrawSvgTrace.tsx +244 -0
  322. package/lib/motion/shotcraft/upstream/demos/ui-entrance/element-body-moves/ContactShadowLift.tsx +217 -0
  323. package/lib/motion/shotcraft/upstream/demos/ui-entrance/integration-hub-map/IntegrationHubMap.tsx +652 -0
  324. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-reveal/ListReveal.tsx +153 -0
  325. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-stack-press/ListStackPress.tsx +238 -0
  326. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-forerun/NeonFrameForerun.tsx +324 -0
  327. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-orbit-drop/NeonFrameForerunOrbit.tsx +354 -0
  328. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx +101 -0
  329. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx +129 -0
  330. package/lib/motion/shotcraft/upstream/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx +297 -0
  331. package/lib/motion/shotcraft/upstream/demos/ui-entrance/radial-wave/RadialWave.tsx +57 -0
  332. package/lib/motion/shotcraft/upstream/demos/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.tsx +155 -0
  333. package/lib/motion/shotcraft/upstream/demos/ui-entrance/row-embed/RowEmbed.tsx +175 -0
  334. package/lib/motion/shotcraft/upstream/demos/ui-entrance/value-stagger-gradient/ValueStaggerGradient.tsx +153 -0
  335. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/BentoLightUp.tsx +146 -0
  336. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/WireframeDrawOn.tsx +226 -0
  337. package/lib/motion/shotcraft/upstream/gallery/api/library.json +1 -0
  338. package/lib/motion/shotcraft/upstream/manifest.json +2715 -0
  339. package/lib/motion/shotcraft/upstream/references/aesthetic-rules.md +193 -0
  340. package/lib/motion/shotcraft/upstream/references/final-review.md +121 -0
  341. package/lib/motion/shotcraft/upstream/references/guided-free-creation.md +199 -0
  342. package/lib/motion/shotcraft/upstream/references/jianying-export.md +183 -0
  343. package/lib/motion/shotcraft/upstream/references/music-beat-sync.md +257 -0
  344. package/lib/motion/shotcraft/upstream/references/pipeline.md +382 -0
  345. package/lib/motion/shotcraft/upstream/references/publish-upload-post.md +111 -0
  346. package/lib/motion/shotcraft/upstream/references/sequences/promo-energy-arc.md +50 -0
  347. package/lib/motion/shotcraft/upstream/references/sfx-sourcing.md +95 -0
  348. package/lib/motion/shotcraft/upstream/references/shots/camera/basic-3d-scene.md +41 -0
  349. package/lib/motion/shotcraft/upstream/references/shots/camera/crash-zoom-punch.md +52 -0
  350. package/lib/motion/shotcraft/upstream/references/shots/camera/cursor-flyover.md +39 -0
  351. package/lib/motion/shotcraft/upstream/references/shots/camera/depth-layer-moves.md +39 -0
  352. package/lib/motion/shotcraft/upstream/references/shots/camera/graze-face-tour.md +57 -0
  353. package/lib/motion/shotcraft/upstream/references/shots/camera/overhead-camera-moves.md +51 -0
  354. package/lib/motion/shotcraft/upstream/references/shots/camera/space-camera-moves.md +46 -0
  355. package/lib/motion/shotcraft/upstream/references/shots/camera/steep-tilt-glide.md +52 -0
  356. package/lib/motion/shotcraft/upstream/references/shots/camera/tension-camera-moves.md +43 -0
  357. package/lib/motion/shotcraft/upstream/references/shots/camera/terminal-3d.md +42 -0
  358. package/lib/motion/shotcraft/upstream/references/shots/data/before-after-slider-scrub.md +48 -0
  359. package/lib/motion/shotcraft/upstream/references/shots/data/chart-live-moves.md +52 -0
  360. package/lib/motion/shotcraft/upstream/references/shots/data/gauge-readout-moves.md +45 -0
  361. package/lib/motion/shotcraft/upstream/references/shots/data/scroll-brake-moves.md +46 -0
  362. package/lib/motion/shotcraft/upstream/references/shots/data/timeline-travel.md +44 -0
  363. package/lib/motion/shotcraft/upstream/references/shots/effects/assemble-then-type-flyin.md +63 -0
  364. package/lib/motion/shotcraft/upstream/references/shots/effects/aurora-bloom-bg-flip.md +54 -0
  365. package/lib/motion/shotcraft/upstream/references/shots/effects/brand-frame-snap.md +58 -0
  366. package/lib/motion/shotcraft/upstream/references/shots/effects/fui-hud-moves.md +35 -0
  367. package/lib/motion/shotcraft/upstream/references/shots/effects/glow-flyline-moves.md +53 -0
  368. package/lib/motion/shotcraft/upstream/references/shots/effects/icon-performance-moves.md +42 -0
  369. package/lib/motion/shotcraft/upstream/references/shots/effects/impact-feedback.md +50 -0
  370. package/lib/motion/shotcraft/upstream/references/shots/effects/light-play-moves.md +46 -0
  371. package/lib/motion/shotcraft/upstream/references/shots/effects/radial-ripple-phone-chips.md +56 -0
  372. package/lib/motion/shotcraft/upstream/references/shots/effects/riso-print-hits.md +49 -0
  373. package/lib/motion/shotcraft/upstream/references/shots/effects/scan-bracket-sweep.md +50 -0
  374. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-annotate-focus.md +53 -0
  375. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-assemble-flyin.md +53 -0
  376. package/lib/motion/shotcraft/upstream/references/shots/effects/slam-entrance-moves.md +55 -0
  377. package/lib/motion/shotcraft/upstream/references/shots/effects/spotlight-sweep-moves.md +52 -0
  378. package/lib/motion/shotcraft/upstream/references/shots/interaction/autolayout-gap-dial.md +60 -0
  379. package/lib/motion/shotcraft/upstream/references/shots/interaction/canvas-materialize-moves.md +57 -0
  380. package/lib/motion/shotcraft/upstream/references/shots/interaction/chip-grid-single-select-blackout.md +51 -0
  381. package/lib/motion/shotcraft/upstream/references/shots/interaction/collab-cursor-moves.md +41 -0
  382. package/lib/motion/shotcraft/upstream/references/shots/interaction/glass-pill-dictation-typing.md +53 -0
  383. package/lib/motion/shotcraft/upstream/references/shots/interaction/hashtag-to-pill-materialize.md +60 -0
  384. package/lib/motion/shotcraft/upstream/references/shots/interaction/picker-carousel-feature-cycle.md +55 -0
  385. package/lib/motion/shotcraft/upstream/references/shots/interaction/segmented-thumb-hero.md +57 -0
  386. package/lib/motion/shotcraft/upstream/references/shots/interaction/theme-switch-moves.md +47 -0
  387. package/lib/motion/shotcraft/upstream/references/shots/interaction/type-and-filter.md +37 -0
  388. package/lib/motion/shotcraft/upstream/references/shots/interaction/voice-waveform-live.md +54 -0
  389. package/lib/motion/shotcraft/upstream/references/shots/opening/brand-ink-open.md +36 -0
  390. package/lib/motion/shotcraft/upstream/references/shots/opening/crane-rise-reveal.md +56 -0
  391. package/lib/motion/shotcraft/upstream/references/shots/opening/dataviz-landscape-open.md +56 -0
  392. package/lib/motion/shotcraft/upstream/references/shots/opening/fracture.md +40 -0
  393. package/lib/motion/shotcraft/upstream/references/shots/opening/icon-field-colorize.md +60 -0
  394. package/lib/motion/shotcraft/upstream/references/shots/opening/letterspace-materialize.md +57 -0
  395. package/lib/motion/shotcraft/upstream/references/shots/opening/magician-card-flourish.md +73 -0
  396. package/lib/motion/shotcraft/upstream/references/shots/opening/orbit-ring-title-open.md +87 -0
  397. package/lib/motion/shotcraft/upstream/references/shots/opening/spotlight-hero-card.md +55 -0
  398. package/lib/motion/shotcraft/upstream/references/shots/opening/stroke-segment-build.md +50 -0
  399. package/lib/motion/shotcraft/upstream/references/shots/opening/text-as-mask.md +59 -0
  400. package/lib/motion/shotcraft/upstream/references/shots/outro/grain-dissolve.md +38 -0
  401. package/lib/motion/shotcraft/upstream/references/shots/outro/logo-shrink-wordmark-lockup.md +38 -0
  402. package/lib/motion/shotcraft/upstream/references/shots/outro/neon-triple-marquee.md +59 -0
  403. package/lib/motion/shotcraft/upstream/references/shots/outro/outro-group-photo-launch.md +39 -0
  404. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-strip-away-outro.md +61 -0
  405. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-to-brand-morph.md +52 -0
  406. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-cut-moves.md +50 -0
  407. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-step-list-theme-cycle.md +66 -0
  408. package/lib/motion/shotcraft/upstream/references/shots/rhythm/panel-grid-moves.md +43 -0
  409. package/lib/motion/shotcraft/upstream/references/shots/rhythm/quad-split-parallel-scenes.md +38 -0
  410. package/lib/motion/shotcraft/upstream/references/shots/rhythm/rhythm-interrupt-moves.md +38 -0
  411. package/lib/motion/shotcraft/upstream/references/shots/rhythm/sakuga-timing-shift.md +56 -0
  412. package/lib/motion/shotcraft/upstream/references/shots/rhythm/speed-ramp-freeze.md +44 -0
  413. package/lib/motion/shotcraft/upstream/references/shots/rhythm/trailer-grammar-moves.md +38 -0
  414. package/lib/motion/shotcraft/upstream/references/shots/transition/bottom-push-stack-wipe.md +63 -0
  415. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flip-reveal.md +57 -0
  416. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flock-tumble.md +63 -0
  417. package/lib/motion/shotcraft/upstream/references/shots/transition/circle-match-iris.md +58 -0
  418. package/lib/motion/shotcraft/upstream/references/shots/transition/gradient-transition.md +45 -0
  419. package/lib/motion/shotcraft/upstream/references/shots/transition/line-carry-transition.md +55 -0
  420. package/lib/motion/shotcraft/upstream/references/shots/transition/mosaic-reframe.md +49 -0
  421. package/lib/motion/shotcraft/upstream/references/shots/transition/page-turn-transitions.md +42 -0
  422. package/lib/motion/shotcraft/upstream/references/shots/transition/print-texture-transitions.md +42 -0
  423. package/lib/motion/shotcraft/upstream/references/shots/transition/shot-transitions.md +45 -0
  424. package/lib/motion/shotcraft/upstream/references/shots/transition/tear-streak-transitions.md +48 -0
  425. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-hidden-cut.md +55 -0
  426. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-travel.md +43 -0
  427. package/lib/motion/shotcraft/upstream/references/shots/transition/white-flash-logo-simplify-cut.md +41 -0
  428. package/lib/motion/shotcraft/upstream/references/shots/transition/wipe-transitions.md +42 -0
  429. package/lib/motion/shotcraft/upstream/references/shots/typography/blur-slide.md +59 -0
  430. package/lib/motion/shotcraft/upstream/references/shots/typography/brace-expand.md +55 -0
  431. package/lib/motion/shotcraft/upstream/references/shots/typography/countdown-arc-scatter.md +65 -0
  432. package/lib/motion/shotcraft/upstream/references/shots/typography/document-typewriter-reveal.md +42 -0
  433. package/lib/motion/shotcraft/upstream/references/shots/typography/flying-words.md +64 -0
  434. package/lib/motion/shotcraft/upstream/references/shots/typography/glitch-cycle.md +64 -0
  435. package/lib/motion/shotcraft/upstream/references/shots/typography/gradient-word-sweep.md +54 -0
  436. package/lib/motion/shotcraft/upstream/references/shots/typography/lead-word-zoom-assemble.md +91 -0
  437. package/lib/motion/shotcraft/upstream/references/shots/typography/marker-underline-title.md +51 -0
  438. package/lib/motion/shotcraft/upstream/references/shots/typography/outline-word-fill.md +66 -0
  439. package/lib/motion/shotcraft/upstream/references/shots/typography/paper-title-card.md +38 -0
  440. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-chip-slot-cycle-handled.md +66 -0
  441. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-slot-cycle.md +58 -0
  442. package/lib/motion/shotcraft/upstream/references/shots/typography/text-column-converge.md +61 -0
  443. package/lib/motion/shotcraft/upstream/references/shots/typography/title-demote-to-label.md +64 -0
  444. package/lib/motion/shotcraft/upstream/references/shots/typography/type-assembly-moves.md +45 -0
  445. package/lib/motion/shotcraft/upstream/references/shots/typography/type-entrance-moves.md +41 -0
  446. package/lib/motion/shotcraft/upstream/references/shots/typography/type-rhythm-sync.md +51 -0
  447. package/lib/motion/shotcraft/upstream/references/shots/typography/typewriter-moves.md +42 -0
  448. package/lib/motion/shotcraft/upstream/references/shots/typography/vertical-word-roll-blur-cycle.md +63 -0
  449. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-filmstrip.md +57 -0
  450. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-geometry.md +75 -0
  451. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/avatar-bracket-carousel.md +59 -0
  452. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/bezier-source-converge-merge.md +67 -0
  453. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/card-stack.md +60 -0
  454. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/carousel-3d.md +66 -0
  455. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/cloner-depth-echo.md +40 -0
  456. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/deck-deal-flyin.md +45 -0
  457. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/doc-park-left-pill-deal.md +62 -0
  458. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/draw-svg-trace.md +55 -0
  459. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/element-body-moves.md +37 -0
  460. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/integration-hub-map.md +55 -0
  461. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-reveal.md +50 -0
  462. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-stack-press.md +43 -0
  463. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-forerun.md +55 -0
  464. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-orbit-drop.md +51 -0
  465. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/page-waterfall-wall.md +51 -0
  466. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/product-card-progressive-assemble.md +56 -0
  467. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/radial-wave.md +49 -0
  468. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/research-card-stack-scroll.md +57 -0
  469. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/row-embed.md +36 -0
  470. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/value-stagger-gradient.md +64 -0
  471. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/wall-reveal-moves.md +47 -0
  472. package/lib/motion/shotcraft/upstream/references/sound-design.md +170 -0
  473. package/lib/motion/shotcraft/upstream/references/workbench.md +157 -0
  474. package/lib/motion/shotcraft/upstream/template/TEMPLATE.md +103 -0
  475. package/lib/motion/shotcraft/upstream/template/THEMES.md +44 -0
  476. package/lib/motion/shotcraft/upstream/template/package.json +20 -0
  477. package/lib/motion/shotcraft/upstream/template/remotion.config.ts +6 -0
  478. package/lib/motion/shotcraft/upstream/template/src/Root.tsx +17 -0
  479. package/lib/motion/shotcraft/upstream/template/src/aifl/Caption.tsx +65 -0
  480. package/lib/motion/shotcraft/upstream/template/src/aifl/DigitRoll.tsx +48 -0
  481. package/lib/motion/shotcraft/upstream/template/src/aifl/FlashCut.tsx +24 -0
  482. package/lib/motion/shotcraft/upstream/template/src/aifl/Main.tsx +192 -0
  483. package/lib/motion/shotcraft/upstream/template/src/aifl/PaperTitleCard.tsx +111 -0
  484. package/lib/motion/shotcraft/upstream/template/src/aifl/live/PageCam.tsx +150 -0
  485. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneDetail.tsx +158 -0
  486. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneFlyIn.tsx +468 -0
  487. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOpen.tsx +552 -0
  488. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOutroLive.tsx +411 -0
  489. package/lib/motion/shotcraft/upstream/template/src/aifl/live/ScenePapers.tsx +200 -0
  490. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneWbr.tsx +324 -0
  491. package/lib/motion/shotcraft/upstream/template/src/aifl/live-layout.json +389 -0
  492. package/lib/motion/shotcraft/upstream/template/src/index.ts +4 -0
  493. package/lib/motion/shotcraft/upstream/template/src/themes/ThemedFilm.tsx +7 -0
  494. package/lib/motion/shotcraft/upstream/template/src/themes/manifest.tsx +68 -0
  495. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.json +1 -0
  496. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.ts +24 -0
  497. package/lib/motion/shotcraft/upstream/template/src/themes/palettes.json +74 -0
  498. package/lib/motion/shotcraft/upstream/template/src/themes/visual-theme.tsx +91 -0
  499. package/lib/motion/shotcraft/upstream/template/src/workbench.ts +213 -0
  500. package/lib/motion/shotcraft/upstream/template/tsconfig.json +15 -0
  501. package/lib/motion/spec.js +113 -0
  502. package/lib/motion/style-engine.js +153 -0
  503. package/lib/motion/style-frames.js +268 -0
  504. package/lib/motion/sync.js +214 -0
  505. package/lib/motion/theme.js +121 -0
  506. package/lib/motion/voice.js +417 -0
  507. package/lib/motion-agent-files.js +82 -0
  508. package/lib/motion-agent-protocol.js +66 -0
  509. package/lib/motion-agent-tools.js +196 -0
  510. package/lib/motion-agent.js +171 -0
  511. package/lib/motion-capture.js +92 -0
  512. package/lib/motion-core.js +238 -0
  513. package/lib/motion-film-review.js +71 -0
  514. package/lib/motion-library.js +210 -0
  515. package/lib/motion-planner.js +254 -0
  516. package/lib/motion-project.js +80 -0
  517. package/lib/motion-render-worker.js +145 -0
  518. package/lib/motion-session.js +32 -0
  519. package/lib/motion-source.js +49 -0
  520. package/lib/motion-tasks.js +452 -0
  521. package/lib/motion-vision.js +43 -0
  522. package/lib/mxfile.js +27 -7
  523. package/lib/routing/node-runtime.js +14 -0
  524. package/lib/routing/router.js +211 -0
  525. package/lib/routing/vendor/README.md +96 -0
  526. package/lib/routing/vendor/libavoid-routing.js +753 -0
  527. package/lib/routing/vendor/libavoid.min.js +59 -0
  528. package/lib/shotcard-core.js +39 -0
  529. package/lib/shotcard-reference.js +56 -0
  530. package/lib/style-kernel.js +37 -3
  531. package/lib/task-patches.js +26 -2
  532. package/lib/video-export.js +17 -2
  533. package/package.json +29 -4
@@ -0,0 +1,889 @@
1
+ <!doctype html>
2
+ <html><head><meta charset="utf-8">
3
+ <link rel="stylesheet" href="/assets/fonts/fonts.css">
4
+ <style>html,body{margin:0;background:#16130F;overflow:hidden}canvas{display:block}</style>
5
+ </head><body><canvas id="c" width="1920" height="1080"></canvas>
6
+ <script type="module">
7
+ // 06-collage — 「脑洞大开」THE MIND IS A COLLAGE. Canvas2D; every frame is a pure function of t.
8
+ // Motion is quantised to 12 fps steps (resting pieces boil at 6 fps) with seeded hand-placement jitter.
9
+ // Round 1: giant-hand hook, 3-pose puppet jaw, lid launch + hinge slap (brass brad, scalp band), POP burst,
10
+ // speech scraps, stepped rostrum camera, per-item spill actions, decluttered hero, page PEEL with kraft verso.
11
+ const W = 1920, H = 1080, D = '/demos/06-collage/';
12
+ window.DEMO = { width: W, height: H, fps: 30, duration: 10, motionBlur: { samples: 1, shutter: 0.5 } };
13
+ // reel v5 (?v5=1): the style name 拼·贴·剪·贴 is the title, window.linkAt(t) reports the link elements, ?nolink=dot hides the dot.
14
+ const QS = new URLSearchParams(location.search), V5 = QS.get('v5') === '1';
15
+ const NOLINK = new Set(V5 ? (QS.get('nolink') || '').split(',').map(s => s.trim()).filter(Boolean) : []);
16
+ const cv = document.getElementById('c'), X = cv.getContext('2d');
17
+ const C = await (await fetch(D + 'cues.json')).json();
18
+ const LAY = await (await fetch(D + 'assets/gen/layout.json')).json();
19
+ const load = src => new Promise((ok, no) => { const i = new Image(); i.onload = () => ok(i); i.onerror = () => no(src); i.src = src; });
20
+ const I = {};
21
+ const GEN = ['p_lid', 'p_jaw', 'p_face', 'space_wedge', 'earth', 'bird_hawk',
22
+ 'rose_purple', 'rose_centi', 'bfly_a', 'bfly_b', 'rocket', 'flame0', 'flame1', 'news_a', 'news_b', 'manicule'];
23
+ await Promise.all([
24
+ ...GEN.map(async n => I[n] = await load(D + 'assets/gen/' + n + '.png')),
25
+ (async () => I.kraft = await load(D + 'assets/gen/page_kraft.jpg'))(),
26
+ (async () => I.red = await load(D + 'assets/gen/page_red.jpg'))(),
27
+ (async () => I.grain = await load('/assets/textures/grain/grain_film35_2048.png'))(),
28
+ (async () => I.dust1 = await load('/assets/textures/dust/dust_01_3840x2160.png'))(),
29
+ (async () => I.dust2 = await load('/assets/textures/dust/dust_03_3840x2160.png'))(),
30
+ (async () => I.tapeW = await load('/assets/textures/washi/washi_solid_14.png'))(),
31
+ (async () => I.tapeK = await load('/assets/textures/washi/washi_solid_12.png'))(),
32
+ ]);
33
+ await Promise.all(Object.values(I).map(i => i.decode()));
34
+ for (const f of ['400 100px "Anton"', '400 100px "Bebas Neue"', '700 100px "Alimama ShuHeiTi"', '900 100px "Noto Serif SC"',
35
+ '700 100px "Noto Sans SC"', '500 30px "IBM Plex Mono"', '600 30px "IBM Plex Mono"'])
36
+ await document.fonts.load(f, '脑洞大开季刊沿虚线剪开随刊附赠剪刀一把咳NOGGINAHEMBANG!0123456789' + (V5 ? '拼贴剪·COLLAGECUT-OUTQUARTERLY—¥' : ''));
37
+ await document.fonts.ready;
38
+
39
+ // ------------------------------------------------------------------ helpers
40
+ const PI = Math.PI, DEG = PI / 180;
41
+ const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
42
+ const lerp = (a, b, u) => a + (b - a) * u;
43
+ const eOut = x => 1 - Math.pow(1 - clamp(x), 3);
44
+ const eIO = x => (x = clamp(x), x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);
45
+ function hash(a, b = 0) { let h = (a * 374761393 + b * 668265263 + 1013904223) | 0; h = Math.imul(h ^ (h >>> 13), 1274126177); h ^= h >>> 16; return (h >>> 0) / 4294967296; }
46
+ const K = t => Math.floor(t * 12 + 1e-4); // 12 fps step index
47
+ const Q = t => K(t) / 12; // quantised time
48
+ const Q6 = t => Math.floor(t * 6 + 1e-4) / 6; // 6 fps (rostrum) time
49
+ const ks = (t, t0) => K(t) - Math.round(t0 * 12); // steps since cue
50
+ function jit(id, t, amp = 2, fps = 12) { // seeded hand-placement jitter (px, px, rad)
51
+ const k = Math.floor(t * fps + 1e-4);
52
+ return [(hash(id, k) * 2 - 1) * amp, (hash(id + 71, k) * 2 - 1) * amp, (hash(id + 913, k) * 2 - 1) * amp * 0.2 * DEG];
53
+ }
54
+ // slap: a piece drops from "camera" onto the page, hitting exactly on t0 (squash on impact, shadow collapses)
55
+ function slap(t, t0, pre = 3) {
56
+ const k = ks(t, t0);
57
+ if (k < -pre) return null;
58
+ if (k < 0) { const u = -k / pre; return { s: 1 + 0.38 * u * u + 0.05 * u, lift: u, k }; }
59
+ if (k === 0) return { s: 0.972, lift: 0, k, sq: 1 };
60
+ if (k === 1) return { s: 1.01, lift: 0.03, k };
61
+ return { s: 1, lift: 0, k };
62
+ }
63
+ function shadow(c, lift = 0, a = 0.36) {
64
+ c.shadowColor = `rgba(34,20,8,${a + 0.08 * Math.min(lift, 1)})`;
65
+ c.shadowBlur = 5 + lift * 34; c.shadowOffsetX = 3 + lift * 24; c.shadowOffsetY = 5 + lift * 34;
66
+ }
67
+ function noShadow(c) { c.shadowColor = 'transparent'; c.shadowBlur = 0; c.shadowOffsetX = 0; c.shadowOffsetY = 0; }
68
+ function piece(c, img, x, y, o = {}) {
69
+ const { rot = 0, s = 1, sx = 1, sy = 1, ax = .5, ay = .5, lift = 0, id = 1, t = 0, amp = 2, fps = 12, alpha = 1, flip = false, sh = true, skx = 0 } = o;
70
+ const [jx, jy, jr] = amp ? jit(id, t, amp, fps) : [0, 0, 0];
71
+ c.save(); c.translate(x + jx, y + jy); c.rotate(rot + jr); if (skx) c.transform(1, 0, skx, 1, 0, 0); c.scale(s * sx * (flip ? -1 : 1), s * sy);
72
+ if (sh) shadow(c, lift); c.globalAlpha = alpha;
73
+ c.drawImage(img, -img.width * ax, -img.height * ay);
74
+ c.restore();
75
+ }
76
+ const boil = (moving) => moving ? { amp: 2, fps: 12 } : { amp: 1.1, fps: 6 };
77
+ const off = (w, h) => { const c = document.createElement('canvas'); c.width = w; c.height = h; return c; };
78
+ function rimmed(img, r, col = '#F4EFE4') { // paper-white scissor rim: dilate the alpha by r px
79
+ const p = Math.ceil(r) + 2, c = off(img.width + 2 * p, img.height + 2 * p), x = c.getContext('2d');
80
+ for (let i = 0; i < 28; i++) { const a = i / 28 * 2 * PI; x.drawImage(img, p + Math.cos(a) * r, p + Math.sin(a) * r); }
81
+ x.globalCompositeOperation = 'source-in'; x.fillStyle = col; x.fillRect(0, 0, c.width, c.height);
82
+ x.globalCompositeOperation = 'source-over'; x.drawImage(img, p, p); c.pad = p; return c;
83
+ }
84
+ I.maniR = rimmed(I.manicule, 3.5);
85
+ const MT = [(LAY.manicule.tip[0] + I.maniR.pad) / I.maniR.width, (LAY.manicule.tip[1] + I.maniR.pad) / I.maniR.height];
86
+ const LIDR = rimmed(I.p_lid, 3.5);
87
+ const PAT = { red: X.createPattern(I.red, 'repeat'), news: X.createPattern(I.news_a, 'repeat'), kraft: X.createPattern(I.kraft, 'repeat') };
88
+ function cutQuad(c, w, h, seed, j = 5) {
89
+ const p = [[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]];
90
+ c.beginPath();
91
+ p.forEach(([x, y], i) => { const dx = (hash(seed, i) * 2 - 1) * j, dy = (hash(seed + 5, i) * 2 - 1) * j; i ? c.lineTo(x + dx, y + dy) : c.moveTo(x + dx, y + dy); });
92
+ c.closePath();
93
+ }
94
+ function tornStrip(c, w, h, seed, rough = 10) {
95
+ c.beginPath(); c.moveTo(-w / 2, -h / 2);
96
+ for (let i = 1; i <= 24; i++) c.lineTo(-w / 2 + w * i / 24, -h / 2 + (hash(seed, i) - .5) * 3);
97
+ for (let i = 1; i <= 10; i++) c.lineTo(w / 2 + (hash(seed + 1, i) - .5) * rough * 2, -h / 2 + h * i / 10);
98
+ for (let i = 1; i <= 24; i++) c.lineTo(w / 2 - w * i / 24, h / 2 + (hash(seed + 2, i) - .5) * 3);
99
+ for (let i = 1; i < 10; i++) c.lineTo(-w / 2 + (hash(seed + 3, i) - .5) * rough * 2, h / 2 - h * i / 10);
100
+ c.closePath();
101
+ }
102
+ // paper fibre / dust flecks puffing out of an impact for 2 steps (k = 0, 1). pts: [x, y, dirx, diry]
103
+ function flecks(c, pts, k, seed, n = 6) {
104
+ if (k < 0 || k > 1) return;
105
+ c.save(); noShadow(c); c.lineCap = 'round';
106
+ for (let i = 0; i < n; i++) {
107
+ const [px, py, dx, dy] = pts[i % pts.length];
108
+ const a = Math.atan2(dy, dx) + (hash(seed, i) - .5) * 1.1, d = (12 + hash(seed + 1, i) * 22) * (k + 1);
109
+ const x = px + Math.cos(a) * d, y = py + Math.sin(a) * d;
110
+ c.globalAlpha = k ? 0.45 : 0.92;
111
+ if (i % 3 === 2) { c.fillStyle = 'rgba(30,20,10,.85)'; c.beginPath(); c.arc(x, y, 2 + hash(seed + 2, i) * 2.5, 0, 2 * PI); c.fill(); }
112
+ else {
113
+ const L = 10 + hash(seed + 3, i) * 14, b = a + (hash(seed + 4, i) - .5) * 2.2;
114
+ c.strokeStyle = '#F3EDDF'; c.lineWidth = 2.6;
115
+ c.beginPath(); c.moveTo(x, y); c.quadraticCurveTo(x + Math.cos(b) * L * .6, y + Math.sin(b) * L * .6, x + Math.cos(a) * L, y + Math.sin(a) * L); c.stroke();
116
+ }
117
+ }
118
+ c.restore();
119
+ }
120
+
121
+ // ------------------------------------------------------------------ portrait rig (local coords = piece px)
122
+ const PL = LAY.portrait, PW = LAY.p_face.w, PH = LAY.p_face.h;
123
+ const OFF = [845 - PL.head_center[0], 554 - PL.head_center[1]]; // local -> main-page screen (at rest)
124
+ const CUT = PL.cut, JAWH = PL.jaw_hinge;
125
+ const cutY = x => { for (let i = 0; i < CUT.length - 1; i++) { const [x0, y0] = CUT[i], [x1, y1] = CUT[i + 1]; if (x <= x1) return lerp(y0, y1, clamp((x - x0) / (x1 - x0))); } return CUT[CUT.length - 1][1]; };
126
+ const WEDGE_BASE = [PL.head_center[0], LAY.space_wedge.anchor[1] + LAY.space_wedge.y0 - OFF[1]];
127
+ const alphaOf = img => { const c = off(PW, PH), x = c.getContext('2d'); x.drawImage(img, 0, 0); return x.getImageData(0, 0, PW, PH).data; };
128
+ const AL = alphaOf(I.p_lid), AF = alphaOf(I.p_face), AJ = alphaOf(I.p_jaw);
129
+ const ext = (A, dy) => { let a = 1e9, b = -1e9; for (let x = 0; x < PW; x++) { const y = Math.round(cutY(x) + dy); if (y >= 0 && y < PH && A[(y * PW + x) * 4 + 3] > 128) { a = Math.min(a, x); b = Math.max(b, x); } } return [a, b]; };
130
+ const LX = ext(AL, -5), FX = ext(AF, 5);
131
+ const HG = [LX[0] + 26, cutY(LX[0] + 26) - 7]; // lid hinge = brass brad at the temple
132
+ // jaw top edge (the scissor cut between moustache and beard), sampled every 4 px
133
+ const JEDGE = []; for (let x = 216; x <= 544; x += 4) { for (let y = 430; y < 700; y++) if (AJ[(y * PW + x) * 4 + 3] > 128) { JEDGE.push([x, y]); break; } }
134
+ const edgeAt = x => { let best = JEDGE[0]; for (const p of JEDGE) if (Math.abs(p[0] - x) < Math.abs(best[0] - x)) best = p; return best[1]; };
135
+ // jaw with a faceted white scissor rim on its upper cut
136
+ const cJaw = off(PW, PH); { const x = cJaw.getContext('2d'); const fac = JEDGE.filter((_, i) => i % 6 === 0).concat([JEDGE[JEDGE.length - 1]]);
137
+ x.lineJoin = 'miter'; x.strokeStyle = '#F4EFE4'; x.lineWidth = 11; x.beginPath(); fac.forEach(([px, py], i) => i ? x.lineTo(px, py - 1) : x.moveTo(px, py - 1)); x.stroke();
138
+ x.drawImage(I.p_jaw, 0, 0); }
139
+ // mouth cavity: jaw silhouette at rest, near-black with a throat glow, a teeth strip under the lip and a 10 px inner shadow
140
+ const cCav = off(PW, PH); { const x = cCav.getContext('2d'); x.drawImage(I.p_jaw, 0, 0); x.globalCompositeOperation = 'source-in'; x.fillStyle = '#0E0B08'; x.fillRect(0, 0, PW, PH);
141
+ x.globalCompositeOperation = 'source-atop';
142
+ const g = x.createRadialGradient(390, 575, 8, 390, 575, 120); g.addColorStop(0, 'rgba(96,26,20,.85)'); g.addColorStop(1, 'rgba(96,26,20,0)'); x.fillStyle = g; x.fillRect(0, 0, PW, PH);
143
+ x.beginPath(); for (let px = 276; px <= 474; px += 6) x.lineTo(px, edgeAt(px) + 2); for (let px = 474; px >= 276; px -= 6) x.lineTo(px, edgeAt(px) + 17); x.closePath(); x.fillStyle = '#E4DAC4'; x.fill();
144
+ x.strokeStyle = 'rgba(14,11,8,.75)'; x.lineWidth = 2.2; for (let px = 290; px < 474; px += 21) { x.beginPath(); x.moveTo(px, edgeAt(px) + 4); x.lineTo(px + 1, edgeAt(px) + 17); x.stroke(); }
145
+ x.shadowColor = 'rgba(0,0,0,.9)'; x.shadowBlur = 10; x.strokeStyle = 'rgba(8,6,4,.9)'; x.lineWidth = 7;
146
+ x.beginPath(); JEDGE.forEach(([px, py], i) => i ? x.lineTo(px, py - 3) : x.moveTo(px, py - 3)); x.stroke(); }
147
+ const cLid = off(PW, PH), cFace = off(PW, PH);
148
+ const xLid = cLid.getContext('2d'), xFace = cFace.getContext('2d');
149
+ const cutPath = (cx, x0, x1, dy0, dy1) => { cx.beginPath(); for (let x = x0; x <= x1; x += 8) cx.lineTo(x, cutY(x) + dy0); for (let x = x1; x >= x0; x -= 8) cx.lineTo(x, cutY(x) + dy1); cx.closePath(); };
150
+ // print dashed coupon line, the snipped gap and (after the POP) the skull-band cut edge onto lid & face
151
+ function bakeHead(stamp, snipX, band) {
152
+ for (const [cx, img, isLid] of [[xLid, I.p_lid, 1], [xFace, I.p_face, 0]]) {
153
+ cx.globalCompositeOperation = 'source-over'; cx.clearRect(0, 0, PW, PH); if (isLid) cx.drawImage(LIDR, -LIDR.pad, -LIDR.pad); else cx.drawImage(img, 0, 0);
154
+ if (!stamp) continue;
155
+ cx.globalCompositeOperation = 'source-atop';
156
+ cx.lineCap = 'butt'; cx.lineJoin = 'round';
157
+ cx.strokeStyle = `rgba(22,19,15,${0.82 * stamp})`; cx.lineWidth = 5; cx.setLineDash([17, 11]);
158
+ cx.beginPath(); CUT.forEach(([x, y], i) => i ? cx.lineTo(x, y) : cx.moveTo(x, y)); cx.stroke();
159
+ cx.setLineDash([]);
160
+ if (snipX > CUT[0][0]) {
161
+ cx.strokeStyle = 'rgba(18,12,8,.95)'; cx.lineWidth = 3.2;
162
+ cx.beginPath(); cx.moveTo(CUT[0][0], CUT[0][1]);
163
+ for (let x = CUT[0][0]; x <= snipX; x += 8) cx.lineTo(x, cutY(x));
164
+ cx.stroke();
165
+ }
166
+ if (band) { // cut edge of the skull: 18 px bone band + printed dashes + white scissor rim
167
+ const [x0, x1] = isLid ? LX : FX, s = isLid ? -1 : 1;
168
+ cutPath(cx, x0 - 20, x1 + 20, 0, 18 * s); cx.fillStyle = '#E3CFAE'; cx.fill();
169
+ cx.strokeStyle = 'rgba(150,118,80,.9)'; cx.lineWidth = 2; cx.beginPath(); for (let x = x0 - 20; x <= x1 + 20; x += 8) cx.lineTo(x, cutY(x) + 18 * s); cx.stroke();
170
+ cx.strokeStyle = 'rgba(22,19,15,.8)'; cx.lineWidth = 3.4; cx.setLineDash([14, 9]);
171
+ cx.beginPath(); for (let x = x0; x <= x1; x += 8) cx.lineTo(x, cutY(x) + 9 * s); cx.stroke(); cx.setLineDash([]);
172
+ cx.globalCompositeOperation = 'source-over';
173
+ cutPath(cx, x0 + 2, x1 - 2, 0, -5 * s); cx.fillStyle = '#F4EFE4'; cx.fill();
174
+ }
175
+ }
176
+ }
177
+ // spilled items: final positions in MAIN-PAGE screen coords, launched from the skull at their cue; each has its own action
178
+ const SPILL = [
179
+ { n: 'rocket', t: C.rocket, F: [1300, 204], r: 0.62, mode: 'launch', id: 31, drift: [26, -20] },
180
+ { n: 'earth', t: C.earth, F: [712, 168], r: -0.22, r0: 1.1, arc: -330, mode: 'lob', id: 32 },
181
+ { n: 'bird_hawk', t: C.birds[0], F: [1560, 196], r: -0.10, r0: 0.35, mode: 'swoop', id: 33 },
182
+ { n: 'rose_centi', t: C.roses, F: [668, 560], r: -0.34, r0: -0.05, mode: 'grow', ay: 0.93, id: 36 },
183
+ { n: 'rose_purple', t: C.roses + 1 / 12, F: [1040, 560], r: 0.42, r0: 0.05, mode: 'grow', ay: 0.95, id: 37 },
184
+ { n: 'bfly_a', t: C.butterflies, F: [1196, 404], r: 0.25, r0: -0.8, arc: -150, mode: 'flutter', id: 38 },
185
+ { n: 'bfly_b', t: C.butterflies + 1 / 12, F: [196, 150], r: -0.3, r0: 0.6, arc: -170, mode: 'flutter', id: 39 },
186
+ ];
187
+ const MOUTH = [PL.head_center[0], PL.cut[3][1] + 10];
188
+ const bez = (a, b, c, u) => (1 - u) * (1 - u) * a + 2 * (1 - u) * u * b + u * u * c;
189
+ function puff(c, x, y, r, seed, tl) { // cut-paper smoke puff
190
+ const [jx, jy] = jit(seed, tl, 1.2, 6);
191
+ c.save(); c.translate(x + jx, y + jy);
192
+ const lobes = [[0, 0, 1], [-.62, .18, .7], [.6, .22, .66], [.05, -.45, .62]];
193
+ c.beginPath(); for (const [ox, oy, rr] of lobes) { c.moveTo(ox * r + rr * r + 6, oy * r); c.arc(ox * r, oy * r, rr * r + 6, 0, 2 * PI); }
194
+ shadow(c, 0.15, 0.3); c.fillStyle = '#FBF7EE'; c.fill(); noShadow(c);
195
+ c.beginPath(); for (const [ox, oy, rr] of lobes) { c.moveTo(ox * r + rr * r, oy * r); c.arc(ox * r, oy * r, rr * r, 0, 2 * PI); }
196
+ c.fillStyle = '#DCD3C1'; c.fill();
197
+ c.restore();
198
+ }
199
+ function drawSpill(c, t, it, tl = t) {
200
+ const k = ks(t, it.t); if (k < 0) return;
201
+ const img = I[it.n], Fl = [it.F[0] - OFF[0], it.F[1] - OFF[1]], M = MOUTH;
202
+ let x, y, s, r, sx = 1, sy = 1, lift = 0.12, skx = 0, moving = false;
203
+ if (it.mode === 'grow') { // roses sprout out of the skull with a 1.08 overshoot
204
+ const g = [0.35, 0.72, 0.96, 1.08, 1.0][Math.min(k, 4)];
205
+ x = Fl[0]; y = Fl[1] + (1 - Math.min(g, 1)) * 250; s = g; r = lerp(it.r0, it.r, Math.min(1, k / 3)); lift = 0.1; moving = k < 4;
206
+ } else if (it.mode === 'launch') { // rocket: 4-step straight launch, flame substitution + paper smoke trail
207
+ const U = [0.08, 0.28, 0.6, 1], fl = 4, u = k < fl ? U[k] : 1;
208
+ x = lerp(M[0], Fl[0], u); y = lerp(M[1], Fl[1], u);
209
+ s = k < fl ? [0.5, 0.7, 0.9, 1][k] : (k === fl ? 1.06 : 1); r = k < fl ? 0.95 : it.r; lift = k < fl ? 0.9 - k * 0.15 : 0.12; moving = k <= fl;
210
+ if (k >= fl) { const a = Q(t) - it.t - fl / 12; x += it.drift[0] * a; y += it.drift[1] * a; }
211
+ const dx = Fl[0] - M[0], dy = Fl[1] - M[1], dl = Math.hypot(dx, dy);
212
+ for (let i = 0; i < 4; i++) { // puff i appears at step i, held 6 steps
213
+ const kk = k - i; if (kk < 0 || kk >= 6) continue;
214
+ const u2 = U[i], px = lerp(M[0], Fl[0], u2) - dx / dl * 150, py = lerp(M[1], Fl[1], u2) - dy / dl * 150;
215
+ puff(c, px, py, (22 + i * 6) * [0.6, 1, 1.08, 1.08, 1.05, 1][kk], 120 + i, tl);
216
+ }
217
+ } else {
218
+ const fl = 6, u = clamp(k / fl), e = eOut(u); moving = k <= fl;
219
+ let cx = (M[0] + Fl[0]) / 2, cy = Math.min(M[1], Fl[1]) + (it.arc || -200);
220
+ if (it.mode === 'swoop') { cx = Fl[0] + 60; cy = M[1] + 170; } // hawk dives low, then climbs
221
+ x = bez(M[0], cx, Fl[0], e); y = bez(M[1], cy, Fl[1], e);
222
+ if (it.mode === 'flutter' && k < fl) { const nx = -(Fl[1] - M[1]), ny = Fl[0] - M[0], nl = Math.hypot(nx, ny); const w = Math.sin(k * 2.3) * 46 * (1 - u); x += nx / nl * w; y += ny / nl * w; }
223
+ s = k < fl ? lerp(0.35, 1, e) : (k === fl ? 1.06 : 1); r = lerp(it.r0, it.r, e); lift = (1 - e) * 0.8 + 0.12;
224
+ if (it.mode === 'lob' && k >= fl) { r = it.r + (Math.floor(tl * 6 + 1e-4) % 2 ? 6 : -6) * DEG; if (k === fl) { sx = 1.04; sy = 0.95; } }
225
+ if (it.mode === 'swoop' && Math.floor(tl * 6 + 1e-4) % 2) { sy = 0.74; skx = -0.12; r += 3 * DEG; } // 2 wing poses @ 6 fps
226
+ if (it.mode === 'flutter') sx = k < fl ? [1, 0.35][K(tl) % 2] : [1, 0.58][Math.floor(tl * 6 + 1e-4) % 2];
227
+ }
228
+ if (it.n === 'rocket') { // flame flicker (replacement frames, 12 fps)
229
+ const f = I[K(tl) % 2 ? 'flame0' : 'flame1'];
230
+ c.save(); c.translate(x, y); c.rotate(r); c.scale(s, s);
231
+ piece(c, f, 0, img.height * 0.46, { ay: 0, id: it.id + 50, t: tl, amp: 1.5, sh: false, sy: k < 4 ? 1.35 : 1 });
232
+ c.restore();
233
+ }
234
+ piece(c, img, x, y, { rot: r, s, sx, sy, skx, ay: it.ay ?? .5, lift, id: it.id, t: tl, ...boil(moving) });
235
+ }
236
+ function brad(c, x, y) { // brass paper fastener (the puppet joint)
237
+ c.save(); shadow(c, 0.05, 0.5);
238
+ const g = c.createRadialGradient(x - 5, y - 6, 1, x, y, 16); g.addColorStop(0, '#FFE9A6'); g.addColorStop(0.35, '#D6A945'); g.addColorStop(1, '#7A5516');
239
+ c.beginPath(); c.arc(x, y, 15, 0, 2 * PI); c.fillStyle = g; c.fill(); noShadow(c);
240
+ c.lineWidth = 1.6; c.strokeStyle = 'rgba(60,38,8,.9)'; c.stroke();
241
+ c.beginPath(); c.arc(x, y, 9, 3.6, 5.2); c.strokeStyle = 'rgba(255,245,210,.8)'; c.lineWidth = 2; c.stroke();
242
+ c.restore();
243
+ }
244
+ // full head: wedge -> lid -> spill -> cavity -> face -> jaw -> brad (lid on top while it flies)
245
+ function drawHead(c, t, S) {
246
+ bakeHead(S.stamp, S.snipX, S.band);
247
+ c.save();
248
+ c.translate(S.x, S.y); c.rotate(S.rot); c.scale(S.s * (S.sqx || 1), S.s * (S.sqy || 1));
249
+ c.translate(-PL.head_center[0], -PL.head_center[1]);
250
+ const lift = S.lift || 0;
251
+ if (S.wedge) {
252
+ const [wx, wy, smear] = S.wedge;
253
+ const dw = (sx, sy, a) => { c.save(); c.globalAlpha = a; c.translate(WEDGE_BASE[0], WEDGE_BASE[1]); c.scale(sx, sy); c.translate(-WEDGE_BASE[0], -WEDGE_BASE[1]);
254
+ if (a === 1) shadow(c, 0.1, 0.3); c.drawImage(I.space_wedge, -OFF[0], LAY.space_wedge.y0 - OFF[1]); c.restore(); noShadow(c); };
255
+ dw(wx, wy, 1);
256
+ if (smear) { dw(wx * 0.97, wy * 0.78, 0.7); dw(wx * 0.93, wy * 0.52, 0.5); } // stop-motion smear (streak copies)
257
+ }
258
+ const L = S.lidT || { a: 0, dx: 0, dy: 0 };
259
+ const drawLid = () => { c.save(); c.translate(HG[0] + (L.dx || 0), HG[1] + (L.dy || 0)); c.rotate(L.a || 0); c.scale(L.sx || 1, L.sy || 1); c.translate(-HG[0], -HG[1]);
260
+ shadow(c, lift + (L.lift || 0)); c.drawImage(cLid, 0, 0); c.restore(); noShadow(c); };
261
+ if (!L.top) drawLid();
262
+ if (S.spill) for (const it of SPILL) if (!S.only || S.only.includes(it.n)) drawSpill(c, S.spillT, it, t);
263
+ noShadow(c);
264
+ c.drawImage(cCav, 0, 0);
265
+ shadow(c, lift); c.drawImage(cFace, 0, 0);
266
+ const j = S.jaw || 0; // 3 replacement poses: 0 closed / .5 half / 1 wide
267
+ c.save(); c.translate(JAWH[0], JAWH[1] + j * 92); c.rotate(j * 9.5 * DEG); c.translate(-JAWH[0], -JAWH[1]);
268
+ shadow(c, lift + j * 0.25); c.drawImage(j > 0 ? cJaw : I.p_jaw, 0, 0); c.restore(); noShadow(c);
269
+ if (L.top) drawLid();
270
+ brad(c, HG[0], HG[1]);
271
+ c.restore(); noShadow(c);
272
+ }
273
+ const toScreen = (lx, ly) => [lx + OFF[0], ly + OFF[1]];
274
+ const lidRest = (lx, ly, a = -95 * DEG) => { const dx = lx - HG[0], dy = ly - HG[1]; return toScreen(HG[0] + dx * Math.cos(a) - dy * Math.sin(a), HG[1] + dx * Math.sin(a) + dy * Math.cos(a)); };
275
+
276
+ // ------------------------------------------------------------------ scissors (two cut-paper blades on a pivot)
277
+ function scissors(c, x, y, rot, open, s = 1) {
278
+ c.save(); c.translate(x, y); c.rotate(rot); c.scale(s, s);
279
+ for (const pass of [0, 1]) for (const sgn of [-1, 1]) {
280
+ c.save(); c.rotate(sgn * open);
281
+ c.beginPath(); c.moveTo(-8, 0); c.lineTo(110, 0); c.lineTo(86, 5 * sgn); c.lineTo(30, 10 * sgn); c.lineTo(-8, 9 * sgn); c.closePath();
282
+ c.lineWidth = pass ? 0 : 11; c.strokeStyle = '#F2ECDF'; c.fillStyle = '#16130F'; c.lineJoin = 'miter';
283
+ if (!pass) { shadow(c, 0.25); c.stroke(); noShadow(c); } else c.fill();
284
+ c.beginPath(); c.ellipse(-50, 18 * -sgn, 22, 15, 0.25 * sgn, 0, 2 * PI);
285
+ c.lineWidth = pass ? 9 : 19; c.strokeStyle = pass ? (sgn > 0 ? '#D2261C' : '#16130F') : '#F2ECDF'; c.stroke();
286
+ c.beginPath(); c.moveTo(-4, 0); c.lineTo(-32, 11 * -sgn); c.lineWidth = pass ? 8 : 18; c.stroke();
287
+ c.restore();
288
+ }
289
+ c.beginPath(); c.arc(0, 0, 4, 0, 2 * PI); c.fillStyle = '#F2ECDF'; c.fill();
290
+ c.restore();
291
+ }
292
+
293
+ // ------------------------------------------------------------------ type pieces
294
+ function chip(c, S, ch, font, size, bg, fg, w, h, seed, rot, x, y, t) {
295
+ const [jx, jy, jr] = jit(seed, t, S.lift > 0 ? 2 : 1.1, S.lift > 0 ? 12 : 6);
296
+ c.save(); c.translate(x + jx, y + jy); c.rotate(rot + jr); c.scale(S.s, S.s);
297
+ cutQuad(c, w + 10, h + 10, seed, 5); c.fillStyle = '#F2ECDF'; shadow(c, S.lift); c.fill(); noShadow(c);
298
+ cutQuad(c, w, h, seed + 9, 4); c.fillStyle = bg; c.fill();
299
+ if (bg === PAT.news) { c.fillStyle = 'rgba(236,229,214,.55)'; c.fill(); }
300
+ c.fillStyle = fg; c.font = `${font.w} ${size}px "${font.f}"`; c.textAlign = 'center'; c.textBaseline = 'middle';
301
+ c.fillText(ch, font.dx || 0, font.dy || 0);
302
+ c.restore();
303
+ }
304
+ function tape(c, img, x, y, w, rot, seed, t) {
305
+ const [jx, jy, jr] = jit(seed, t, 1, 6);
306
+ c.save(); c.translate(x + jx, y + jy); c.rotate(rot + jr); shadow(c, 0, 0.18);
307
+ c.globalAlpha = 0.92; c.drawImage(img, 0, 0, 700, 300, -w / 2, -26, w, 52); c.restore();
308
+ }
309
+ function monoText(c, s, x, y, size = 19, color = 'rgba(22,19,15,.82)', ls = 3, align = 'left') {
310
+ c.save(); c.font = `500 ${size}px "IBM Plex Mono"`; c.letterSpacing = ls + 'px'; c.fillStyle = color; c.textAlign = align; c.textBaseline = 'alphabetic';
311
+ c.fillText(s, x, y); c.restore();
312
+ }
313
+ // ransom-note speech scraps (the portrait "talks", act 1)
314
+ const SPEECH = [
315
+ { t: C.jaw[0], txt: 'AHEM', f: '400 124px "Anton"', ls: 4, bg: 'news', fg: '#16130F', w: 336, h: 156, x: 1236, y: 612, r: -8, seed: 81, dy: 8 },
316
+ { t: C.jaw[1], txt: '咳咳', f: '700 116px "Alimama ShuHeiTi"', ls: 2, bg: 'red', fg: '#F2ECDF', w: 296, h: 160, x: 1500, y: 458, r: 5, seed: 82, dy: 6 },
317
+ { t: C.jaw[2], txt: '!', f: '400 204px "Bebas Neue"', ls: 0, bg: 'ink', fg: '#F2ECDF', w: 118, h: 206, x: 1730, y: 330, r: -3, seed: 83, dy: 12 },
318
+ ];
319
+ function speechScrap(c, sp, i, t) {
320
+ const s = slap(t, sp.t, 3); if (!s) return;
321
+ const kt = ks(t, C.stamp_cut); if (kt >= 3) return; // torn away when the stamp hits
322
+ const O = kt < 0 ? [0, 0, 0, 0] : [[40 + i * 24, -36, 9, 0.7], [300 + i * 70, -80 + i * 50, 27, 1.3], [800, 40 + i * 90, 50, 1.8]][kt];
323
+ const [jx, jy, jr] = jit(sp.seed, t, s.lift || kt >= 0 ? 2 : 1.1, s.lift || kt >= 0 ? 12 : 6);
324
+ const bg = sp.bg === 'news' ? PAT.news : sp.bg === 'red' ? PAT.red : '#16130F';
325
+ c.save(); c.translate(sp.x + 34 * s.lift + O[0] + jx, sp.y - 40 * s.lift + O[1] + jy); c.rotate((sp.r + O[2]) * DEG + jr); c.scale(s.s, s.s);
326
+ const lift = Math.max(s.lift, O[3]);
327
+ // speech tail aimed at the mouth (computed in the scrap's local frame)
328
+ const [mx, my] = toScreen(...PL.mouth), ra = -(sp.r + O[2]) * DEG - jr, vx0 = mx - (sp.x + O[0]), vy0 = my - (sp.y + O[1]);
329
+ const vx = vx0 * Math.cos(ra) - vy0 * Math.sin(ra), vy = vx0 * Math.sin(ra) + vy0 * Math.cos(ra), vl = Math.hypot(vx, vy), ux = vx / vl, uy = vy / vl;
330
+ const tb = Math.min((sp.w / 2 - 16) / Math.abs(ux || 1e-6), (sp.h / 2 - 16) / Math.abs(uy || 1e-6)), bx0 = ux * tb, by0 = uy * tb, TL = sp.w < 150 ? 64 : 84;
331
+ const tail = (g) => { c.beginPath(); c.moveTo(bx0 - uy * (22 - g), by0 + ux * (22 - g)); c.lineTo(bx0 + ux * (TL - g * 0.6), by0 + uy * (TL - g * 0.6)); c.lineTo(bx0 + uy * (22 - g), by0 - ux * (22 - g)); c.closePath(); };
332
+ tail(0); c.lineJoin = 'miter'; c.lineWidth = 12; c.strokeStyle = '#F2ECDF'; shadow(c, lift); c.stroke(); noShadow(c); c.fillStyle = bg; c.fill();
333
+ cutQuad(c, sp.w + 12, sp.h + 12, sp.seed, 5); c.fillStyle = '#F2ECDF'; shadow(c, lift); c.fill(); noShadow(c);
334
+ cutQuad(c, sp.w, sp.h, sp.seed + 9, 4); c.fillStyle = bg; c.fill();
335
+ if (sp.bg === 'news') { c.fillStyle = 'rgba(236,229,214,.6)'; c.fill(); }
336
+ tail(5); c.fillStyle = bg; c.fill(); if (sp.bg === 'news') { c.fillStyle = 'rgba(236,229,214,.6)'; c.fill(); }
337
+ c.fillStyle = sp.fg; c.font = sp.f; c.letterSpacing = sp.ls + 'px'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(sp.txt, 2, sp.dy);
338
+ c.restore();
339
+ }
340
+ // POP burst: newsprint confetti, halftone dots, riso stars and a BANG! ransom word on 12 fps parabolas
341
+ const SCRAPS = Array.from({ length: 13 }, (_, i) => {
342
+ const type = (i === 3 || i === 10) ? 'star' : (i % 3 === 1 ? 'dots' : 'news');
343
+ const a = type === 'bang' ? -32 * DEG : lerp(-168, -12, (i + hash(401, i) * 0.7) / 13.2) * DEG;
344
+ return { type, a, sp: type === 'bang' ? 1780 : 2150 + hash(402, i) * 850, sz: 40 + hash(403, i) * 50, spin: type === 'bang' ? 0.9 : (hash(404, i) - .5) * 16, r0: type === 'bang' ? -0.14 : hash(405, i) * 6,
345
+ seed: 410 + i, delay: type === 'bang' ? 0 : i % 3, ox: (hash(406, i) - .5) * 320 };
346
+ });
347
+ const BANG = [['B', '400 58px "Anton"', 'news', '#16130F'], ['A', '400 62px "Bebas Neue"', 'red', '#F2ECDF'], ['N', '900 52px "Noto Serif SC"', 'white', '#16130F'],
348
+ ['G', '400 58px "Anton"', 'ink', '#F2ECDF'], ['!', '400 60px "Anton"', 'red', '#16130F']];
349
+ function drawScraps(c, t) {
350
+ const kb = ks(t, C.pop); if (kb < 0 || kb > 10) return;
351
+ const [ox, oy] = toScreen(PL.head_center[0], PL.cut[3][1]);
352
+ for (const s of SCRAPS) {
353
+ const k = kb - s.delay; if (k < 0) continue;
354
+ const tau = (k + 1) / 12;
355
+ const x = ox + s.ox * 0.4 + Math.cos(s.a) * s.sp * tau, y = oy + Math.sin(s.a) * s.sp * tau + 0.5 * 1800 * tau * tau;
356
+ if (x < -260 || x > W + 260 || y < -200 || y > H + 200) continue;
357
+ c.save(); c.translate(x, y); c.rotate(s.r0 + s.spin * tau); shadow(c, 0.7, 0.3);
358
+ const z = s.sz;
359
+ if (s.type === 'news') { cutQuad(c, z + 8, z * 0.66 + 8, s.seed, 4); c.fillStyle = '#F2ECDF'; c.fill(); noShadow(c); cutQuad(c, z, z * 0.66, s.seed + 3, 3); c.fillStyle = PAT.news; c.fill(); }
360
+ else if (s.type === 'dots') {
361
+ c.beginPath(); c.arc(0, 0, z / 2 + 4, 0, 2 * PI); c.fillStyle = '#F2ECDF'; c.fill(); noShadow(c);
362
+ c.beginPath(); c.arc(0, 0, z / 2, 0, 2 * PI); c.fillStyle = i2c(s.seed); c.fill(); c.save(); c.clip(); c.fillStyle = '#16130F';
363
+ for (let gx = -z / 2; gx <= z / 2; gx += 7) for (let gy = -z / 2; gy <= z / 2; gy += 7) { const rr = 3.2 * (1 - Math.hypot(gx + z * .3, gy + z * .3) / (z * 1.3)); if (rr > .3) { c.beginPath(); c.arc(gx, gy, rr, 0, 2 * PI); c.fill(); } }
364
+ c.restore();
365
+ } else if (s.type === 'star') {
366
+ c.beginPath(); for (let q = 0; q < 10; q++) { const rr = q % 2 ? z * 0.28 : z * 0.62, aa = q / 10 * 2 * PI - PI / 2; q ? c.lineTo(Math.cos(aa) * rr, Math.sin(aa) * rr) : c.moveTo(Math.cos(aa) * rr, Math.sin(aa) * rr); }
367
+ c.closePath(); c.lineJoin = 'miter'; c.lineWidth = 9; c.strokeStyle = '#F2ECDF'; c.stroke(); noShadow(c); c.fillStyle = PAT.red; c.fill();
368
+ } else { // BANG! ransom word
369
+ noShadow(c); c.scale(1.45, 1.45);
370
+ BANG.forEach(([ch, f, bg, fg], q) => {
371
+ c.save(); c.translate(-120 + q * 60, (q % 2 ? 6 : -6)); c.rotate((q % 2 ? 6 : -5) * DEG);
372
+ cutQuad(c, 60, 74, 470 + q, 3); c.fillStyle = '#F2ECDF'; shadow(c, 0.6); c.fill(); noShadow(c);
373
+ cutQuad(c, 52, 66, 480 + q, 3); c.fillStyle = bg === 'news' ? PAT.news : bg === 'red' ? PAT.red : bg === 'ink' ? '#16130F' : '#F2ECDF'; c.fill();
374
+ c.fillStyle = fg; c.font = f; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(ch, 0, 4); c.restore();
375
+ });
376
+ }
377
+ c.restore(); noShadow(c);
378
+ }
379
+ }
380
+ // BANG! ransom word: its own path over the burst (1000,420) -> (1500,170), 0.8 -> 1.25 -> 1.0, exits up-right by 4.5
381
+ const BANGP = [[1000, 430, 0.8, 0.9], [1262, 292, 1.25, 0.7], [1500, 172, 1.0, 0.1], [1528, 156, 1.0, 0.05], [1716, 40, 1.06, 0.8], [2010, -110, 1.1, 1.2]];
382
+ function drawBang(c, t) {
383
+ const k = ks(t, C.pop); if (k < 0 || k >= BANGP.length) return;
384
+ const [x, y, s, lift] = BANGP[k], [jx, jy, jr] = jit(466, t, 2, 12);
385
+ c.save(); c.translate(x + jx, y + jy); c.rotate(-0.14 + jr + (k >= 4 ? 0.12 * (k - 3) : 0)); c.scale(1.45 * s, 1.45 * s);
386
+ BANG.forEach(([ch, f, bg, fg], q) => {
387
+ c.save(); c.translate(-120 + q * 60, (q % 2 ? 6 : -6)); c.rotate((q % 2 ? 5 : -5) * DEG);
388
+ cutQuad(c, 60, 74, 470 + q, 3); c.fillStyle = '#F2ECDF'; shadow(c, lift); c.fill(); noShadow(c);
389
+ cutQuad(c, 52, 66, 480 + q, 3); c.fillStyle = bg === 'news' ? PAT.news : bg === 'red' ? PAT.red : bg === 'ink' ? '#16130F' : '#F2ECDF'; c.fill();
390
+ c.fillStyle = fg; c.font = f; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(ch, 0, 4); c.restore();
391
+ });
392
+ c.restore(); noShadow(c);
393
+ }
394
+ const i2c = seed => ['#E3CFAE', '#D2261C', '#F2ECDF'][seed % 3];
395
+
396
+ // ------------------------------------------------------------------ stepped rostrum camera
397
+ function cam(t) {
398
+ const q6 = Q6(t), kp = ks(t, C.pop);
399
+ let z = 1, px = 845, py = 560, cx = 0, cy = 0;
400
+ if (q6 >= C.jaw[0] - 1e-4) z = 1 + 0.045 * clamp((q6 - C.jaw[0]) / (C.stamp_cut - C.jaw[0]));
401
+ if (q6 >= C.stamp_cut - 1e-4) z = 1.045 + 0.015 * clamp((q6 - C.stamp_cut) / (C.gap[0] - C.stamp_cut));
402
+ if (kp >= 0) {
403
+ if (kp < 4) { z = [1.11, 1.075, 1.056, 1.05][kp]; cx = [12, -8, 3, 0][kp]; cy = [26, -14, 6, 0][kp]; }
404
+ else z = 1 + 0.05 * (1 - eOut((q6 - C.lid_land) / 1.0));
405
+ }
406
+ if (q6 >= 5.5 - 1e-4) { z = 1 + 0.022 * eIO((q6 - 5.5) / 2.0); px = 900; py = 540; }
407
+ return { z, px, py, cx, cy };
408
+ }
409
+ const applyCam = (c, k) => { c.translate(k.px + k.cx, k.py + k.cy); c.scale(k.z, k.z); c.translate(-k.px, -k.py); };
410
+
411
+ // ------------------------------------------------------------------ MAIN PAGE (kraft) — everything before the tear
412
+ function mainPage(c, t) {
413
+ const kp = ks(t, C.pop);
414
+ c.drawImage(I.kraft, 0, 0);
415
+ const kn0 = ks(t, C.slap_news);
416
+ if (kp < 0 && kn0 >= 2) monoText(c, V5 ? 'COLLAGE QUARTERLY — Nº06 — AUTUMN 2026' : 'NOGGIN QUARTERLY — Nº06 — AUTUMN 2026', 96, 70);
417
+ if (kn0 >= 2) monoText(c, 'PL. VI · CUT & PASTE ON KRAFT, 1860 / 2026', W - 96, H - 52, 17, 'rgba(22,19,15,.72)', 3, 'right');
418
+ const snb = slap(t, C.slap_news + 1 / 12, 2); // left newsprint scrap + kraft tape (held back until the 1.0 slap)
419
+ if (snb) { piece(c, I.news_b, 196 - 50 * snb.lift, 640 + 30 * snb.lift, { rot: -5.5 * DEG, s: snb.s, lift: snb.lift, id: 2, t, ...boil(snb.k < 2) });
420
+ if (snb.k >= 1) tape(c, I.tapeK, 214, 372, 200, -0.14, 3, t); }
421
+ // red constructivist disc behind the head
422
+ const sd = slap(t, C.slap_circle);
423
+ if (sd && !NOLINK.has('dot')) {
424
+ const [jx, jy] = jit(5, t, sd.lift ? 2 : 1, sd.lift ? 12 : 6);
425
+ c.save(); c.translate(845 + jx - 40 * sd.lift, 468 + jy - 20 * sd.lift); c.scale(sd.s, sd.s);
426
+ c.beginPath(); for (let i = 0; i < 44; i++) { const a = i / 44 * 2 * PI, r = 352 + (hash(9, i) - .5) * 5; i ? c.lineTo(Math.cos(a) * r, Math.sin(a) * r) : c.moveTo(r, 0); }
427
+ c.closePath(); shadow(c, sd.lift, 0.3); c.fillStyle = PAT.red; c.fill(); c.restore(); noShadow(c);
428
+ }
429
+ // torn newsprint (top right) + Nº06 chip (bottom right, clear of the hawk)
430
+ const sn = slap(t, C.slap_news);
431
+ if (sn) { piece(c, I.news_a, 1592 + 60 * sn.lift, 222 - 30 * sn.lift, { rot: 3.2 * DEG, s: sn.s, lift: sn.lift, id: 6, t, ...boil(sn.k < 2) }); if (sn.k >= 1) tape(c, I.tapeW, 1452, 60, 170, 0.55, 8, t); }
432
+ const sc = slap(t, C.chip06); // Nº06 chip slaps in at 2.417 as the speech scraps tear away
433
+ if (sc) {
434
+ const [jx, jy, jr] = jit(7, t, 1, 6);
435
+ c.save(); c.translate(1744 + jx + 30 * sc.lift, 906 + jy - 30 * sc.lift); c.rotate(-5 * DEG + jr); c.scale(sc.s, sc.s);
436
+ cutQuad(c, 186, 100, 7, 3); c.fillStyle = '#F2ECDF'; shadow(c, sc.lift); c.fill(); noShadow(c);
437
+ cutQuad(c, 176, 92, 17, 3); c.fillStyle = PAT.red; c.fill();
438
+ c.fillStyle = '#F2ECDF'; c.font = '400 70px "Anton"'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.letterSpacing = '2px';
439
+ c.fillText('Nº06', 2, 4); c.restore();
440
+ }
441
+
442
+ // ---- portrait: carried in by a giant manicule from the top right, slapped at 0.333
443
+ const k0 = ks(t, C.slap_portrait);
444
+ const IN = [[1100, -100, -6, 1.08, 1.6], [560, -50, -3.5, 1.05, 1.2], [200, -18, -1.5, 1.025, 0.8], [40, -4, -0.4, 1.008, 0.4]];
445
+ let S = { x: 845, y: 554, s: 1, rot: 0, lift: 0 };
446
+ if (k0 < 0) { const p = IN[clamp(4 + k0, 0, 3)]; S = { x: 845 + p[0], y: 554 + p[1], rot: p[2] * DEG, s: p[3], lift: p[4] }; }
447
+ else if (k0 === 0) { S.sqx = 1.02; S.sqy = 0.96; S.y += 14; }
448
+ else if (k0 === 1) { S.sqy = 1.01; }
449
+ // jaw: 3 replacement poses per flap (anticipation half, WIDE on the beat, half, closed) + head bob up on each open
450
+ let jaw = 0;
451
+ for (const h of C.jaw) { const k = ks(t, h); if (k >= -1 && k < 3) jaw = Math.max(jaw, [0.5, 1, 0.5, 0][k + 1]); }
452
+ if (jaw === 1) S.y -= 7;
453
+ S.rot += jaw * -1.6 * DEG;
454
+ // stamp + snip
455
+ const kst = ks(t, C.stamp_cut);
456
+ S.stamp = kst < 0 ? 0 : kst === 0 ? 1.15 : 1;
457
+ const snips = C.snips, SX = [CUT[1][0] + 14, 205, 315, 425, 535];
458
+ let snipX = -1e9, scis = null;
459
+ const ks0 = ks(t, snips[0]);
460
+ if (ks0 >= -1) {
461
+ const tq = Q(t); let i = snips.findIndex(v => tq < v - 1e-4); if (i < 0) i = snips.length;
462
+ const kk = ks(t, snips[Math.max(0, i - 1)]);
463
+ if (i === 0) scis = { x: SX[0] - 70, open: 0.42, lift: 0.6 };
464
+ else if (i <= snips.length) {
465
+ const lx = SX[i - 1], nx = i < snips.length ? SX[i] : 700;
466
+ scis = { x: kk === 0 ? lx : lerp(lx, nx, 0.45), open: kk === 0 ? 0.02 : 0.38, lift: 0 };
467
+ snipX = lx;
468
+ if (i === snips.length && kk >= 1) { const f = kk - 1; scis = { x: 620 + f * 150, open: 0.3, lift: 0.3 + f * 0.25, fly: f }; snipX = 640; }
469
+ }
470
+ }
471
+ S.snipX = snipX;
472
+ // lid: rattles, then POP — launches out of frame spinning, falls back and slaps onto its brad at 4.333
473
+ let L = { a: 0, dx: 0, dy: 0 };
474
+ const RAT = { [K(C.rattle[0])]: [-5, 5], [K(C.rattle[0]) + 1]: [-1, 1], [K(C.rattle[1])]: [-8.5, 8], [K(C.rattle[1]) + 1]: [-1.5, 2], [K(C.rattle[2])]: [-13, 12], [K(C.rattle[2]) + 1]: [-3, 3] };
475
+ if (RAT[K(t)]) L = { a: RAT[K(t)][0] * DEG, dx: 0, dy: -RAT[K(t)][1] };
476
+ const LIDF = [[-70, 10, -230, 1.3], [-200, -30, -660, 1.8], [-300, -70, -700, 1.8], [-385, -40, -300, 1.4]];
477
+ if (kp >= 0 && kp < 4) { const f = LIDF[kp]; L = { a: f[0] * DEG, dx: f[1], dy: f[2], lift: f[3], top: 1 }; }
478
+ else if (kp >= 4) { const a = [-95, -89, -97, -95][Math.min(kp - 4, 3)]; L = kp === 4 ? { a: a * DEG, sx: 1.04, sy: 0.93, lift: 0 } : { a: a * DEG, lift: 0.08 }; }
479
+ S.lidT = L; S.band = kp >= 0;
480
+ if (kp >= 0) S.wedge = [[0.94, 1.35, 1], [1.07, 1.1], [0.97, 0.96], [1.01, 1.02], [1, 1]][Math.min(kp, 4)];
481
+ if (kp >= 0 && kp < 3) { S.rot += [2.6, -1.8, 0.8][kp] * DEG; S.y += [16, -8, 3][kp]; }
482
+ S.spill = kp >= 0; S.spillT = t; S.jaw = jaw;
483
+ if (kp >= 0 && kp < 3) S.jaw = [1, 1, 0.5][kp]; // gasp as the lid pops
484
+ const jp = jit(1, t, (k0 < 3 || jaw || kp >= 0 && kp < 5) ? 2 : 1, (k0 < 3 || jaw) ? 12 : 6);
485
+ S.x += jp[0]; S.y += jp[1]; S.rot += jp[2];
486
+ drawHead(c, t, S);
487
+ flecks(c, [[540, 820, -1, .3], [548, 985, -1, .5], [1266, 840, 1, .3], [1258, 990, 1, .5], [700, 300, -.6, -1], [990, 300, .6, -1]], k0, 90, 6);
488
+ if (kp >= 4) { const a = lidRest(LX[1] - 30, cutY(LX[1] - 30) - 40), b = lidRest(PL.head_top[0], PL.head_top[1] + 30), d = lidRest(LX[0] + 120, 150);
489
+ flecks(c, [[a[0], a[1], 0, -1], [b[0], b[1], -1, -.2], [d[0], d[1], -1, .4]], kp - 4, 95, 6); }
490
+ // speech scraps (bottom-right quadrant, act 1)
491
+ SPEECH.forEach((sp, i) => speechScrap(c, sp, i, t));
492
+ // coupon slip with the red rubber stamp (right of the head); blown off by the POP
493
+ if (kst >= 0 && kp < 3) {
494
+ const O = kp < 0 ? [0, 0, 0, 0] : [[90, -110, 14, 0.9], [420, -430, 40, 1.5], [980, -820, 75, 1.8]][kp];
495
+ const sl = slap(t, C.stamp_cut, 1), [jx, jy, jr] = jit(19, t, kp >= 0 ? 2 : 1, kp >= 0 ? 12 : 6);
496
+ c.save(); c.translate(1387 + O[0] + jx, 470 + O[1] + jy); c.rotate((-3.5 + O[2]) * DEG + jr); c.scale(sl.s, sl.s);
497
+ cutQuad(c, 372, 104, 91, 4); c.fillStyle = '#EDE5D3'; shadow(c, Math.max(O[3], sl.lift)); c.fill(); noShadow(c);
498
+ c.globalAlpha = kst === 0 ? 1 : 0.9;
499
+ c.strokeStyle = '#C8261D'; c.lineWidth = 3; c.strokeRect(-165, -38, 330, 70);
500
+ c.fillStyle = '#C8261D'; c.font = '700 30px "Noto Sans SC"'; c.textBaseline = 'middle'; c.textAlign = 'left'; c.fillText('沿虚线剪开', -95, -12);
501
+ c.font = '400 26px "Bebas Neue"'; c.letterSpacing = '5px'; c.fillText('CUT ALONG THE LINE', -95, 18);
502
+ c.globalAlpha = 1; scissors(c, -130, -2, 0, 0.3, 0.3);
503
+ c.restore();
504
+ }
505
+ if (scis) {
506
+ const [sx, sy] = toScreen(scis.x, cutY(scis.x));
507
+ const jj = jit(12, t, 2, 12);
508
+ scissors(c, S.x - 845 + sx + jj[0] + (scis.fly || 0) * 40, S.y - 554 + sy + jj[1] - (scis.fly || 0) * 70 - 4, -0.05 + jj[2] - (scis.fly || 0) * 0.35, scis.open, 1.15 + (scis.lift || 0) * 0.3);
509
+ }
510
+ drawScraps(c, t); drawBang(c, t);
511
+ // manicule #1: a giant ☞ pushes the portrait in (tip on the right shoulder), springs up with an overshoot, exits top right
512
+ if (k0 < 4) { // the artist's giant ☞ (x1.8, white rim) leads: its fingertip drags the portrait in by the brass brad, pins it, springs off left
513
+ let mx, my, ml, ms, mr;
514
+ const dx = HG[0] - PL.head_center[0], dy = HG[1] - PL.head_center[1];
515
+ if (k0 < 0) { mx = S.x + S.s * (dx * Math.cos(S.rot) - dy * Math.sin(S.rot)); my = S.y + S.s * (dx * Math.sin(S.rot) + dy * Math.cos(S.rot)); ml = 0.3 + S.lift * 0.7; ms = 1; mr = S.rot; }
516
+ else { const O = [[0, 4, 0], [-34, -50, -5], [-470, -260, -12], [-1150, -560, -20]][k0]; [mx, my] = toScreen(...HG); mx += O[0]; my += O[1]; mr = O[2] * DEG; ml = [0.05, 0.9, 1.4, 1.8][k0]; ms = [1, 1.03, 1.07, 1.12][k0]; }
517
+ piece(c, I.maniR, mx, my, { ax: MT[0], ay: MT[1], rot: 0.36 + mr, s: 1.8 * ms, sx: k0 === 0 ? 1.03 : 1, sy: k0 === 0 ? 0.97 : 1, lift: ml, id: 13, t });
518
+ }
519
+ // constructivist band + red rule
520
+ const sb = slap(t, C.band, 2);
521
+ if (sb) {
522
+ const [jx, jy, jr] = jit(15, t, sb.k < 2 ? 2 : 1, sb.k < 2 ? 12 : 6);
523
+ const slide = sb.k < 0 ? (-sb.k) * 260 : 0;
524
+ c.save(); c.translate(900 + jx - slide, 842 + jy + slide * 0.18); c.rotate(-10 * DEG + jr); c.scale(sb.s, sb.s);
525
+ tornStrip(c, 2500, 154, 16, 14); shadow(c, sb.lift); c.fillStyle = '#16130F'; c.fill(); noShadow(c);
526
+ c.fillStyle = '#F2ECDF'; c.font = '400 124px "Anton"'; c.letterSpacing = '2.5px'; c.textBaseline = 'middle'; c.textAlign = 'left';
527
+ c.fillText('THE MIND IS A COLLAGE', -700, 6);
528
+ c.restore();
529
+ }
530
+ const sr = slap(t, C.band + 1 / 12, 2);
531
+ if (sr) {
532
+ const [jx, jy, jr] = jit(17, t, 1, 6);
533
+ c.save(); c.translate(760 + jx, 958 + jy); c.rotate(-10 * DEG + jr); c.scale(sr.s, sr.s);
534
+ tornStrip(c, 2300, 30, 18, 10); shadow(c, sr.lift); c.fillStyle = PAT.red; c.fill(); c.restore(); noShadow(c);
535
+ }
536
+ // HERO: 脑 洞 大 开 as ransom-note cut-paper chips (16ths), block scaled 1.12
537
+ const BC = [1601, 552], BS = 1.12, bx = x => BC[0] + (x - 1601.5) * BS, by = y => BC[1] + (y - 571.5) * BS;
538
+ const HT = V5 ? ['拼', '贴', '剪', '贴'] : ['脑', '洞', '大', '开'];
539
+ const CH = [
540
+ [HT[0], { f: 'Alimama ShuHeiTi', w: 700, dy: 7 }, 190, PAT.red, '#16130F', 1484, 468, -6.5, 21],
541
+ [HT[1], { f: 'Noto Serif SC', w: 900, dy: 4 }, 186, PAT.news, '#16130F', 1706, 452, 4.5, 22],
542
+ [HT[2], { f: 'Alimama ShuHeiTi', w: 700, dy: 7 }, 190, '#16130F', '#F2ECDF', 1496, 690, 3, 23],
543
+ [HT[3], { f: 'Noto Serif SC', w: 900, dy: 4 }, 186, '#F2ECDF', '#D2261C', 1720, 676, -4.5, 24],
544
+ ];
545
+ CH.forEach((q, i) => {
546
+ const s = slap(t, C.hero[i], 2); if (!s) return;
547
+ chip(c, s, q[0], q[1], q[2] * BS, q[3], q[4], 212 * BS, 212 * BS, q[8], q[7] * DEG, bx(q[5]) + 30 * s.lift, by(q[6]) - 40 * s.lift, t);
548
+ });
549
+ if (ks(t, C.hero[3]) >= 2) tape(c, I.tapeW, bx(1604), by(344), 200, -0.2, 25, t);
550
+ // the artist's hand returns (6.0): an engraved ☞ slides in from the left edge, points at the brad / open skull, taps twice
551
+ const km = ks(t, C.manicules);
552
+ if (km >= -2 && K(t) < K(C.tear_hand)) {
553
+ const PATH = [[-470, 30], [-150, 10], [-26, 0], [0, 0]];
554
+ const p = PATH[clamp(km + 2, 0, 3)];
555
+ const poke = (km === 2 || km === 6) ? 18 : 0;
556
+ piece(c, I.maniR, 598 + p[0] + poke, 506 + p[1], { ax: MT[0], ay: MT[1], rot: -0.04, s: 0.78, lift: km < 1 ? 0.5 : 0, id: 14, t, ...boil(km < 1) });
557
+ }
558
+ }
559
+
560
+ // ------------------------------------------------------------------ v5 END-CARD TITLE: the style name as the masthead
561
+ // 拼·贴·剪·贴 ransom chips (the hero's four chips, re-pasted in a row) slap on 16ths from 8.167, then a smaller
562
+ // COLLAGE · CUT-OUT torn strip and the mono line. Row ≈ x 97–1085 (≈ 51 % of the width), glyphs ≈ 185 px tall.
563
+ const V5E = { chips: [8.1666667, 8.3333333, 8.5, 8.6666667], strip: 8.8333333, mono: 8.9166667 };
564
+ const ECH = [ // ch, font, size px, bg, fg, w, h, x, y, rot°, seed
565
+ ['拼', { f: 'Alimama ShuHeiTi', w: 700, dy: 9 }, 214, 'kraft', '#16130F', 250, 262, 222, 290, -5, 61],
566
+ ['贴', { f: 'Noto Serif SC', w: 900, dy: 5 }, 208, 'news', '#16130F', 240, 252, 468, 318, 3.5, 62],
567
+ ['剪', { f: 'Alimama ShuHeiTi', w: 700, dy: 9 }, 214, 'ink', '#F2ECDF', 254, 258, 714, 284, -2.5, 63],
568
+ ['贴', { f: 'Noto Serif SC', w: 900, dy: 5 }, 210, 'cream', '#D2261C', 244, 266, 960, 312, 5, 64],
569
+ ];
570
+ const bgOf = b => b === 'kraft' ? PAT.kraft : b === 'news' ? PAT.news : b === 'red' ? PAT.red : b === 'ink' ? '#16130F' : '#F2ECDF';
571
+ function endTitleV5(c, t) {
572
+ ECH.forEach((q, i) => {
573
+ const s = slap(t, V5E.chips[i], 2); if (!s) return;
574
+ chip(c, s, q[0], q[1], q[2], bgOf(q[3]), q[4], q[5], q[6], q[10], q[9] * DEG, q[7] + 30 * s.lift, q[8] - 40 * s.lift, t);
575
+ });
576
+ const sm = slap(t, V5E.strip, 2);
577
+ if (sm) {
578
+ const [jx, jy, jr] = jit(52, t, sm.k < 2 ? 2 : 1, sm.k < 2 ? 12 : 6);
579
+ c.save(); c.translate(478 + jx, 540 + jy); c.rotate(-2 * DEG + jr); c.scale(sm.s, sm.s);
580
+ tornStrip(c, 660, 112, 53, 12); shadow(c, sm.lift); c.fillStyle = '#EFE8DA'; c.fill(); noShadow(c);
581
+ c.fillStyle = '#16130F'; c.font = '400 80px "Anton"'; c.letterSpacing = '5px'; c.textAlign = 'center'; c.textBaseline = 'middle';
582
+ c.fillText('COLLAGE · CUT-OUT', 0, 6); c.restore();
583
+ }
584
+ const s3 = slap(t, V5E.mono, 1);
585
+ if (s3) {
586
+ monoText(c, 'Nº06 · THE MIND IS A COLLAGE · AUTUMN 2026', 156, 684, 22, '#16130F', 3);
587
+ c.save(); c.fillStyle = '#16130F'; c.fillRect(156, 708, 640, 4); c.restore();
588
+ }
589
+ }
590
+
591
+ // ------------------------------------------------------------------ END PAGE (red riso) — revealed by the tear
592
+ function endPage(c, t) {
593
+ c.drawImage(I.red, 0, 0);
594
+ const kj = ks(t, C.end_jaw), kl = ks(t, C.end_lid);
595
+ let jaw = kj >= 0 && kj < 4 ? [1, 1, 0.5, 0][kj] : 0;
596
+ if (kl === 0) jaw = 0.5; // gulp as the lid slams
597
+ const LA = kl < -2 ? [-95, 0] : kl === -2 ? [-101, 0.15] : kl === -1 ? [-46, 0.7] : kl === 0 ? [6, 0] : [0, 0];
598
+ const lidT = { a: LA[0] * DEG, dy: kl < -1 ? -4 : 0, lift: LA[1], sx: kl === 0 ? 1.03 : 1, sy: kl === 0 ? 0.94 : 1, top: kl === -1 ? 1 : 0 };
599
+ const jp = jit(51, t, kl >= 0 && kl < 2 ? 2 : 1.1, kl >= 0 && kl < 2 ? 12 : 6);
600
+ const RX = 1490, RY = 716, RS = 0.60; // deliberate ~120 px bleed off the right edge, clear of the NOGGIN strip
601
+ drawHead(c, t, { x: RX + jp[0], y: RY + jp[1] - (jaw === 1 ? 5 : 0) + (kl === 0 ? 4 : 0), s: RS, rot: 2 * DEG + jp[2] - jaw * 1.5 * DEG, lift: 0, lidT, wedge: [1, 1],
602
+ jaw, stamp: 1, snipX: 640, band: 1, spill: true, spillT: 9.9, only: ['rocket', 'earth', 'rose_purple', 'rose_centi', 'bfly_a'] });
603
+ if (kl >= 0 && kl < 2) { const hx = (x, y) => [RX + (x - PL.head_center[0]) * RS, RY + (y - PL.head_center[1]) * RS];
604
+ flecks(c, [[...hx(LX[0], PL.cut[1][1]), -1, -.3], [...hx(PL.head_top[0], PL.head_top[1] + 10), 0, -1], [...hx(LX[1], PL.cut[4][1]), 1, -.3]], kl, 98, 7); }
605
+ if (V5) endTitleV5(c, t);
606
+ else { // default: NOGGIN strip, 脑洞季刊, mono line
607
+ const sm = slap(t, C.end_masthead, 2);
608
+ if (sm) {
609
+ const [jx, jy, jr] = jit(52, t, sm.k < 2 ? 2 : 1, sm.k < 2 ? 12 : 6);
610
+ c.save(); c.translate(560 + jx, 300 + jy); c.rotate(-3 * DEG + jr); c.scale(sm.s, sm.s);
611
+ tornStrip(c, 900, 250, 53, 16); shadow(c, sm.lift); c.fillStyle = '#EFE8DA'; c.fill(); noShadow(c);
612
+ c.fillStyle = '#16130F'; c.font = '400 238px "Anton"'; c.letterSpacing = '6px'; c.textAlign = 'center'; c.textBaseline = 'middle';
613
+ c.fillText('NOGGIN', 0, 12); c.restore();
614
+ }
615
+ const s2 = slap(t, C.end_masthead + 2 / 12, 2);
616
+ if (s2) {
617
+ const [jx, jy, jr] = jit(54, t, 1, 6);
618
+ c.save(); c.translate(150 + jx, 560 + jy); c.rotate(jr); c.scale(s2.s, s2.s);
619
+ c.fillStyle = '#16130F'; c.font = '900 132px "Noto Serif SC"'; c.textBaseline = 'alphabetic'; c.letterSpacing = '10px';
620
+ c.fillText('脑洞季刊', 0, 0);
621
+ c.restore();
622
+ }
623
+ const s3 = slap(t, C.end_masthead + 4 / 12, 1);
624
+ if (s3) {
625
+ monoText(c, 'Nº06 · THE MIND IS A COLLAGE · AUTUMN 2026', 156, 650, 22, '#16130F', 3);
626
+ c.save(); c.fillStyle = '#16130F'; c.fillRect(156, 676, 640, 4); c.restore();
627
+ }
628
+ }
629
+ // price roundel sticker (lower left) + its little ✂ that snips once at 9.5
630
+ const sb = slap(t, C.end_button, 2);
631
+ if (sb && !NOLINK.has('dot')) {
632
+ const [jx, jy, jr] = jit(55, t, sb.k < 2 ? 2 : 1, sb.k < 2 ? 12 : 6);
633
+ c.save(); c.translate(380 + jx, 870 + jy); c.rotate(-8 * DEG + jr); c.scale(sb.s, sb.s);
634
+ c.beginPath(); for (let i = 0; i < 28; i++) { const a = i / 28 * 2 * PI, r = 118 + (hash(56, i) - .5) * 4; i ? c.lineTo(Math.cos(a) * r, Math.sin(a) * r) : c.moveTo(r, 0); }
635
+ c.closePath(); c.fillStyle = '#EFE8DA'; shadow(c, sb.lift); c.fill(); noShadow(c);
636
+ c.beginPath(); c.arc(0, 0, 106, 0, 2 * PI); c.fillStyle = '#16130F'; c.fill();
637
+ c.fillStyle = '#EFE8DA'; c.textAlign = 'center'; c.textBaseline = 'middle';
638
+ c.font = '700 34px "Noto Sans SC"'; c.letterSpacing = '4px'; c.fillText('随刊附赠', 2, -34);
639
+ c.fillText('剪刀一把', 2, 8);
640
+ c.font = '400 22px "Bebas Neue"'; c.letterSpacing = '3px'; c.fillText('SCISSORS INCLUDED', 0, 48);
641
+ c.restore();
642
+ const kn = ks(t, C.end_snip);
643
+ const open = kn === 0 ? 0.02 : kn === 1 ? 0.4 : 0.28, hop = kn === 0 ? -8 : kn === 1 ? -3 : 0;
644
+ scissors(c, 482, 764 + hop, -0.7 + (kn === 0 ? 0.08 : 0), open, 0.46);
645
+ flecks(c, [[520, 730, 1, -1], [530, 745, 1, 0]], kn, 97, 4);
646
+ }
647
+ monoText(c, V5 ? 'COLLAGE QUARTERLY — ¥19.60' : 'NOGGIN QUARTERLY — ¥19.60', 96, 70, 19, 'rgba(22,19,15,.8)');
648
+ }
649
+
650
+ // ------------------------------------------------------------------ tear transition: rip runs down, then each half PEELS off
651
+ const pageC = off(W, H), pageX = pageC.getContext('2d');
652
+ const TEAR = (() => { const pts = [], n = 90; for (let i = 0; i <= n; i++) { const u = i / n; let x = lerp(1190, 770, u), y = lerp(-60, H + 60, u);
653
+ x += Math.sin(u * 9.1 + 1.3) * 22 + Math.sin(u * 31 + 0.4) * 8 + (hash(77, i) - .5) * 14; pts.push([x, y]); } return pts; })();
654
+ function halfPath(c, side, grow = 0, seed = 0) {
655
+ c.beginPath(); const e = side < 0 ? -600 : W + 600;
656
+ c.moveTo(e, -600);
657
+ TEAR.forEach(([x, y], i) => c.lineTo(x - side * grow * (0.6 + hash(seed, i)), y));
658
+ c.lineTo(e, H + 600); c.closePath();
659
+ }
660
+ const frontC = off(W, H), frontX = frontC.getContext('2d'), flapC = off(W, H), flapX = flapC.getContext('2d');
661
+ const ripX = y => { for (let i = 0; i < TEAR.length - 1; i++) if (TEAR[i + 1][1] >= y) { const [x0, y0] = TEAR[i], [x1, y1] = TEAR[i + 1]; return lerp(x0, x1, (y - y0) / (y1 - y0)); } return TEAR[TEAR.length - 1][0]; };
662
+ // Each half curls over a CR = 100 px cylinder. d = distance past the fold (measured along n, from the fixed part towards the rip).
663
+ // Paper at d maps to screen distance R·sin(d/R) (recto quarter, then verso quarter), and beyond πR lies flat, verso up (πR − d).
664
+ // The fold starts at the top inner corner (step 0 = a small dog-ear triangle) and sweeps out; n is tilted (−10° left, 220° right).
665
+ // Per step: fold normal angle a (the dog-ear starts steep at the corner, then swings towards the rip's lean), paper peeled E, curl radius R.
666
+ const C0 = [ripX(0), 0];
667
+ const PEEL = {
668
+ '-1': { a: [-20, 6, 10, 12], E: [330, 640, 980, 1320], R: [70, 110, 130, 130], yank: [[0, 0], [-10, -22], [-60, -100], [-150, -240]] },
669
+ '1': { a: [220, 198, 185, 180], E: [300, 520, 700, 960], R: [70, 110, 130, 130], yank: [[0, 0], [8, 12], [26, 40], [60, 90]] } };
670
+ const fmap = (d, R) => d <= PI * R ? R * Math.sin(d / R) : PI * R - d;
671
+ function peelGeom(side, j) { const P = PEEL[side], a = P.a[j] * DEG, nx = Math.cos(a), ny = Math.sin(a);
672
+ return { nx, ny, d0: nx * C0[0] + ny * C0[1] - P.E[j], y: P.yank[j], R: P.R[j] }; }
673
+ function peelPoint(side, j, p) { const g = peelGeom(side, j), d = g.nx * p[0] + g.ny * p[1] - g.d0; if (d <= 0) return p.slice();
674
+ const sh = fmap(d, g.R) - d; return [p[0] + g.nx * sh + g.y[0], p[1] + g.ny * sh + g.y[1]]; }
675
+ function band(cx, g, da, db) { // source-space strip d0+da <= n·p <= d0+db
676
+ const L = 6000, ux = -g.ny, uy = g.nx, a = g.d0 + da, b = g.d0 + db;
677
+ cx.beginPath(); cx.moveTo(g.nx * a - ux * L, g.ny * a - uy * L); cx.lineTo(g.nx * a + ux * L, g.ny * a + uy * L);
678
+ cx.lineTo(g.nx * b + ux * L, g.ny * b + uy * L); cx.lineTo(g.nx * b - ux * L, g.ny * b - uy * L); cx.closePath();
679
+ }
680
+ function setMap(cx, g, A, B) { // p -> p + n·(A + (B−1)·d) + yank
681
+ const k = B - 1, o = A - k * g.d0, { nx, ny } = g;
682
+ cx.setTransform(1 + k * nx * nx, k * nx * ny, k * nx * ny, 1 + k * ny * ny, nx * o + g.y[0], ny * o + g.y[1]);
683
+ }
684
+ const shadeV = v => { // verso quarter: v = 1 at the crest (grazing, dark) -> 0 where it rolls flat
685
+ if (v > 0.8) return `rgba(22,10,4,${(0.35 * (v - 0.8) / 0.2).toFixed(3)})`;
686
+ if (v > 0.2) return `rgba(255,248,232,${(0.40 * clamp(1 - Math.abs(v - 0.52) / 0.34)).toFixed(3)})`;
687
+ return `rgba(22,10,4,${(0.14 * (0.2 - v) / 0.2).toFixed(3)})`;
688
+ };
689
+ function curlHalf(c, side, j) {
690
+ if (j >= 4) return; // fully peeled and gone (j = 3 is the roll leaving frame)
691
+ const seed = side > 0 ? 81 : 82;
692
+ const paint = (cx, verso) => {
693
+ halfPath(cx, side, 7, seed); cx.fillStyle = verso ? '#F1EADB' : '#EFE7D6'; cx.fill(); // torn fibre rim
694
+ halfPath(cx, side, 0); cx.clip();
695
+ if (!verso) { cx.drawImage(pageC, 0, 0); return; }
696
+ cx.fillStyle = '#C4A077'; cx.fillRect(-W, -H, 3 * W, 3 * H);
697
+ cx.globalCompositeOperation = 'multiply'; cx.globalAlpha = 0.45; cx.drawImage(I.kraft, 0, 0);
698
+ cx.globalCompositeOperation = 'source-over'; cx.globalAlpha = 0.08; cx.drawImage(pageC, 0, 0); cx.globalAlpha = 1; // mirrored print show-through
699
+ };
700
+ const g = j < 0 ? null : peelGeom(side, j);
701
+ frontX.setTransform(1, 0, 0, 1, 0, 0); frontX.clearRect(0, 0, W, H); frontX.save();
702
+ if (g) { band(frontX, g, -1e4, 0.6); frontX.clip(); }
703
+ paint(frontX, false); frontX.restore();
704
+ c.save(); c.shadowColor = 'rgba(34,20,8,.42)'; c.shadowBlur = 12; c.shadowOffsetX = 4; c.shadowOffsetY = 7; c.drawImage(frontC, 0, 0); c.restore();
705
+ if (!g) return;
706
+ const lift = clamp((j + 1) / 4), CR = g.R;
707
+ // throat: the roll's cast shadow on the revealed page, just beyond the crest
708
+ c.save(); halfPath(c, side, 0); c.clip(); band(c, g, CR - 4, CR + 120); c.clip();
709
+ const tg = c.createLinearGradient(g.nx * (g.d0 + CR), g.ny * (g.d0 + CR), g.nx * (g.d0 + CR + 120), g.ny * (g.d0 + CR + 120));
710
+ tg.addColorStop(0, 'rgba(30,12,4,.45)'); tg.addColorStop(0.3, 'rgba(30,12,4,.22)'); tg.addColorStop(1, 'rgba(30,12,4,0)'); c.fillStyle = tg; c.fillRect(0, 0, W, H); c.restore();
711
+ // the lifted paper: recto quarter (hidden under the roll except at the dog-ear tip), verso quarter (lit crest), flat verso flap
712
+ flapX.setTransform(1, 0, 0, 1, 0, 0); flapX.clearRect(0, 0, W, H);
713
+ const NS = 12, q = PI * CR / 2 / NS;
714
+ for (let pass = 0; pass < 2; pass++) for (let i = 0; i < NS; i++) {
715
+ const da = (pass * NS + i) * q, db = da + q, fa = fmap(da, CR), fb = fmap(db, CR), B = (fb - fa) / q, A = fa - B * da, v = Math.sin((da + db) / 2 / CR);
716
+ flapX.save(); setMap(flapX, g, A, B); band(flapX, g, da - 0.7, db + 0.7); flapX.clip(); paint(flapX, pass === 1);
717
+ flapX.fillStyle = pass ? shadeV(v) : `rgba(20,10,4,${(0.36 * v * v).toFixed(3)})`; flapX.fillRect(-W, -H, 3 * W, 3 * H);
718
+ flapX.restore();
719
+ }
720
+ flapX.save(); setMap(flapX, g, PI * CR, -1); band(flapX, g, PI * CR - 0.7, 1e4); flapX.clip(); paint(flapX, true);
721
+ const e0 = g.d0 + PI * CR, lg = flapX.createLinearGradient(g.nx * e0, g.ny * e0, g.nx * (e0 + 560), g.ny * (e0 + 560));
722
+ lg.addColorStop(0, 'rgba(255,246,226,.12)'); lg.addColorStop(0.28, 'rgba(255,246,226,0)'); lg.addColorStop(1, 'rgba(40,24,10,.34)');
723
+ flapX.fillStyle = lg; flapX.fillRect(-W, -H, 3 * W, 3 * H); flapX.restore();
724
+ flapX.setTransform(1, 0, 0, 1, 0, 0);
725
+ c.save(); c.shadowColor = 'rgba(34,20,8,.45)'; c.shadowBlur = lerp(14, 42, lift); c.shadowOffsetX = lerp(8, 34, lift); c.shadowOffsetY = lerp(12, 58, lift);
726
+ c.drawImage(flapC, 0, 0); c.restore();
727
+ }
728
+ // the artist's hand (the one that pointed at the brad) lifts at 7.417, pinches the top of the page at 7.5 (2 steps, 3% squash),
729
+ // and drags the left half's corner up-left as it curls away
730
+ function tearHand(c, t) {
731
+ const k = K(t) - K(C.tear_hand); if (k < 0 || k > 5) return;
732
+ const G = [ripX(70) - 30, 70];
733
+ let tip, s = 1.3, rot = 0.2, lift = 0, sx = 1, sy = 1;
734
+ if (k === 0) { tip = [lerp(598, G[0] - 90, 0.6), lerp(506, G[1] - 60, 0.6)]; s = 1.12; rot = 0.08; lift = 1.3; }
735
+ else if (k === 1) { tip = G; sx = 1.03; sy = 0.97; lift = 0.02; }
736
+ else if (k === 2) { tip = [G[0] - 8, G[1] - 10]; lift = 0.15; sx = 0.99; sy = 1.02; }
737
+ else { const j = k - 3; tip = peelPoint(-1, j, G); rot = 0.2 - j * 0.12; lift = 0.35 + j * 0.35; s = 1.3 + j * 0.05; }
738
+ piece(c, I.maniR, tip[0], tip[1], { ax: MT[0], ay: MT[1], rot, s, sx, sy, lift, id: 15, t });
739
+ }
740
+
741
+ // ------------------------------------------------------------------ finishing layers
742
+ const dustC = [I.dust1, I.dust2].map((img, n) => { const c = off(W, H), x = c.getContext('2d'); x.filter = 'invert(1)'; x.drawImage(img, 0, 0, W, H);
743
+ if (n === 1) x.clearRect(940, 400, 180, 210); // remove the fibre-loop "ghost ring" from dust_03
744
+ return c; });
745
+ function finish(c, t) {
746
+ const k = K(t);
747
+ c.save(); c.globalCompositeOperation = 'overlay'; c.globalAlpha = 0.26;
748
+ c.drawImage(I.grain, -Math.floor(hash(k, 1) * 120), -Math.floor(hash(k, 2) * 900)); c.restore();
749
+ c.save(); c.globalCompositeOperation = 'multiply'; c.globalAlpha = 0.35; c.drawImage(dustC[Math.floor(k / 2) % 2], (k % 3) * -3, 0, W + 6, H); c.restore();
750
+ const g = c.createRadialGradient(W / 2, H / 2, H * 0.45, W / 2, H / 2, H * 1.05);
751
+ g.addColorStop(0, 'rgba(40,20,5,0)'); g.addColorStop(1, 'rgba(40,20,5,.28)');
752
+ c.save(); c.globalCompositeOperation = 'multiply'; c.fillStyle = g; c.fillRect(0, 0, W, H); c.restore();
753
+ }
754
+
755
+ // ------------------------------------------------------------------ frame
756
+ window.renderAt = async (t) => {
757
+ t = Math.min(Math.max(t, 0), 9.999);
758
+ X.setTransform(1, 0, 0, 1, 0, 0); noShadow(X); X.globalAlpha = 1; X.globalCompositeOperation = 'source-over';
759
+ const kt = ks(t, C.tear[0]); // tear starts at 7.5 (record scratch freezes the page)
760
+ if (kt < 0) {
761
+ X.save(); applyCam(X, cam(t)); mainPage(X, t); X.restore();
762
+ } else {
763
+ const kk = kt - 2; // 0 at 7.667: halves crack apart, then peel
764
+ X.drawImage(I.red, 0, 0);
765
+ X.save();
766
+ if (kk === 0 || kk === 1) X.translate(...[[6, -5], [-3, 3]][kk]); // 2-frame camera shake on the rip
767
+ endPage(X, t);
768
+ pageX.setTransform(1, 0, 0, 1, 0, 0);
769
+ pageX.save(); applyCam(pageX, cam(C.tear[0] - 1e-3)); mainPage(pageX, C.tear[0] - 1e-3); pageX.restore();
770
+ if (kt < 2) { // the rip runs down the page first (2 steps)
771
+ X.drawImage(pageC, 0, 0);
772
+ const n = Math.round(TEAR.length * [0.42, 0.8][kt]);
773
+ X.save(); X.lineJoin = 'round';
774
+ for (const [w, col] of [[11, 'rgba(30,18,8,.35)'], [6, '#EFE7D6']]) {
775
+ X.beginPath(); TEAR.slice(0, n).forEach(([x, y], j) => j ? X.lineTo(x + 2, y) : X.moveTo(x + 2, y));
776
+ X.lineWidth = w; X.strokeStyle = col; X.stroke();
777
+ }
778
+ X.restore();
779
+ } else if (kk < 6) {
780
+ curlHalf(X, -1, kk); curlHalf(X, 1, kk - 1);
781
+ if (kk < 2) flecks(X, TEAR.filter((_, i) => i % 9 === 4).map(([x, y], i) => [x, y, i % 2 ? 1 : -1, (hash(71, i) - .5)]), kk, 700, 10);
782
+ }
783
+ tearHand(X, t);
784
+ X.restore();
785
+ }
786
+ if (kt < 0) tearHand(X, t);
787
+ finish(X, t);
788
+ };
789
+ // ------------------------------------------------------------------ reel v5: link elements (pure functions of t)
790
+ // Everything below mirrors the drawing code above (same slap / jit / cam maths), in 1920×1080 output pixels.
791
+ if (V5) {
792
+ const camPt = (k, x, y) => [(x - k.px) * k.z + k.px + k.cx, (y - k.py) * k.z + k.py + k.cy];
793
+ const headMainS = t => { // = the portrait transform built in mainPage()
794
+ const kp = ks(t, C.pop), k0 = ks(t, C.slap_portrait);
795
+ const IN = [[1100, -100, -6, 1.08], [560, -50, -3.5, 1.05], [200, -18, -1.5, 1.025], [40, -4, -0.4, 1.008]];
796
+ let S = { x: 845, y: 554, s: 1, rot: 0, sqx: 1, sqy: 1 };
797
+ if (k0 < 0) { const p = IN[clamp(4 + k0, 0, 3)]; S = { x: 845 + p[0], y: 554 + p[1], rot: p[2] * DEG, s: p[3], sqx: 1, sqy: 1 }; }
798
+ else if (k0 === 0) { S.sqx = 1.02; S.sqy = 0.96; S.y += 14; }
799
+ else if (k0 === 1) S.sqy = 1.01;
800
+ let jaw = 0; for (const h of C.jaw) { const k = ks(t, h); if (k >= -1 && k < 3) jaw = Math.max(jaw, [0.5, 1, 0.5, 0][k + 1]); }
801
+ if (jaw === 1) S.y -= 7;
802
+ S.rot += jaw * -1.6 * DEG;
803
+ if (kp >= 0 && kp < 3) { S.rot += [2.6, -1.8, 0.8][kp] * DEG; S.y += [16, -8, 3][kp]; }
804
+ const jp = jit(1, t, (k0 < 3 || jaw || kp >= 0 && kp < 5) ? 2 : 1, (k0 < 3 || jaw) ? 12 : 6);
805
+ S.x += jp[0]; S.y += jp[1]; S.rot += jp[2];
806
+ return S;
807
+ };
808
+ const headEndS = t => { // = the reprise-head transform built in endPage()
809
+ const kj = ks(t, C.end_jaw), kl = ks(t, C.end_lid);
810
+ let jaw = kj >= 0 && kj < 4 ? [1, 1, 0.5, 0][kj] : 0; if (kl === 0) jaw = 0.5;
811
+ const jp = jit(51, t, kl >= 0 && kl < 2 ? 2 : 1.1, kl >= 0 && kl < 2 ? 12 : 6);
812
+ return { x: 1490 + jp[0], y: 716 + jp[1] - (jaw === 1 ? 5 : 0) + (kl === 0 ? 4 : 0), s: 0.6, rot: 2 * DEG + jp[2] - jaw * 1.5 * DEG, sqx: 1, sqy: 1 };
813
+ };
814
+ const headPt = (S, lx, ly) => { const ax = (lx - PL.head_center[0]) * S.s * S.sqx, ay = (ly - PL.head_center[1]) * S.s * S.sqy;
815
+ return [S.x + ax * Math.cos(S.rot) - ay * Math.sin(S.rot), S.y + ax * Math.sin(S.rot) + ay * Math.cos(S.rot)]; };
816
+ const earthLocal = (ts, tl) => { // = drawSpill() for the Earth ('lob'): local centre + scale
817
+ const it = SPILL[1], k = ks(ts, it.t); if (k < 0) return null;
818
+ const Fl = [it.F[0] - OFF[0], it.F[1] - OFF[1]], M = MOUTH, fl = 6, e = eOut(clamp(k / fl));
819
+ const cx = (M[0] + Fl[0]) / 2, cy = Math.min(M[1], Fl[1]) + it.arc;
820
+ const s = k < fl ? lerp(0.35, 1, e) : (k === fl ? 1.06 : 1), sx = k === fl ? 1.04 : 1, sy = k === fl ? 0.95 : 1;
821
+ const [jx, jy] = jit(it.id, tl, k <= fl ? 2 : 1.1, k <= fl ? 12 : 6);
822
+ return { x: bez(M[0], cx, Fl[0], e) + jx, y: bez(M[1], cy, Fl[1], e) + jy, s: s * (sx + sy) / 2, k };
823
+ };
824
+ const bbox = (els) => { let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
825
+ for (const [x, y, w, h, r] of els) for (const [u, v] of [[-1, -1], [1, -1], [1, 1], [-1, 1]]) {
826
+ const px = x + (u * w / 2) * Math.cos(r) - (v * h / 2) * Math.sin(r), py = y + (u * w / 2) * Math.sin(r) + (v * h / 2) * Math.cos(r);
827
+ x0 = Math.min(x0, px); y0 = Math.min(y0, py); x1 = Math.max(x1, px); y1 = Math.max(y1, py); }
828
+ return { x: (x0 + x1) / 2, y: (y0 + y1) / 2, w: x1 - x0, h: y1 - y0 }; };
829
+ const R = v => Math.round(v * 10) / 10;
830
+ window.linkAt = (t) => {
831
+ t = Math.min(Math.max(t, 0), 9.999);
832
+ const out = [], kt = ks(t, C.tear[0]);
833
+ // ---- main page (kraft): live until the tear; frozen at 7.5 while the rip runs down; torn apart from 7.667
834
+ if (kt < 2) {
835
+ const tm = kt < 0 ? t : C.tear[0] - 1e-3, k = cam(tm), kp = ks(tm, C.pop);
836
+ const sd = slap(tm, C.slap_circle);
837
+ if (sd) {
838
+ const [jx, jy] = jit(5, tm, sd.lift ? 2 : 1, sd.lift ? 12 : 6), [x, y] = camPt(k, 845 + jx - 40 * sd.lift, 468 + jy - 20 * sd.lift);
839
+ out.push({ id: 'dot', type: 'circle', x: R(x), y: R(y), r: R(352 * sd.s * k.z), a: 1, look: 'paper', fill: '#B42620', stroke: '#B42620', sw: 0,
840
+ vis: kp >= 0 ? 0.11 : sd.k < 0 ? 0.6 : 0.4, z: 'behind the portrait (and after 4.0 behind the space wedge + band)', // vis = measured unoccluded fraction (v5 vs nolink=dot diff)
841
+ note: 'red riso paper disc (constructivist sun) behind the head; cut edge ±2.5 px, soft drop shadow (3,5) blur 5' });
842
+ }
843
+ const S = headMainS(tm), [bx, by] = camPt(k, ...headPt(S, HG[0], HG[1]));
844
+ out.push({ id: 'brad', type: 'circle', x: R(bx), y: R(by), r: R(15 * S.s * k.z), a: 1, look: 'sphere', fill: '#D6A945', stroke: '#3C2608', sw: 1.6,
845
+ grad: ['#FFE9A6', '#7A5516'], note: 'brass paper fastener = the lid hinge (the manicule presses it at 0–0.33 and points at it at 6.0)' });
846
+ const el = earthLocal(tm, tm);
847
+ if (el) { const [ex, ey] = camPt(k, ...headPt(S, el.x, el.y));
848
+ out.push({ id: 'earth', type: 'circle', x: R(ex), y: R(ey), r: R(118 * el.s * S.s * k.z), a: 1, look: 'halftone-photo', fill: '#3F6FA8', stroke: '#F4EFE4', sw: 4,
849
+ note: el.k < 6 ? 'Earth cut-out lobbed out of the open skull' : 'Earth cut-out at rest (halftone photo, white scissor rim, 4 small tabs to r 131)' }); }
850
+ const sb = slap(tm, C.band, 2);
851
+ if (sb) {
852
+ const [jx, jy, jr] = jit(15, tm, sb.k < 2 ? 2 : 1, sb.k < 2 ? 12 : 6), slide = sb.k < 0 ? (-sb.k) * 260 : 0;
853
+ const cx = 900 + jx - slide, cy = 842 + jy + slide * 0.18, a = -10 * DEG + jr, L = 1250 * sb.s;
854
+ let [x0, y0] = camPt(k, cx - Math.cos(a) * L, cy - Math.sin(a) * L), [x1, y1] = camPt(k, cx + Math.cos(a) * L, cy + Math.sin(a) * L);
855
+ const yx = x => y0 + (y1 - y0) * (x - x0) / (x1 - x0);
856
+ if (x0 < 0) { y0 = yx(0); x0 = 0; } if (x1 > W) { y1 = yx(W); x1 = W; }
857
+ out.push({ id: 'band', type: 'line', x0: R(x0), y0: R(y0), x1: R(x1), y1: R(y1), w: R(154 * sb.s * k.z), a: 1, fill: '#16130F',
858
+ note: 'black torn constructivist band "THE MIND IS A COLLAGE" (−10°), a 30 px red rule under it' });
859
+ }
860
+ const BC = [1601, 552], BS = 1.12, bxf = x => BC[0] + (x - 1601.5) * BS, byf = y => BC[1] + (y - 571.5) * BS;
861
+ const HP = [[1484, 468, -6.5, 21], [1706, 452, 4.5, 22], [1496, 690, 3, 23], [1720, 676, -4.5, 24]], ch = [];
862
+ HP.forEach((q, i) => { const s = slap(tm, C.hero[i], 2); if (!s || s.k < 0) return; const [jx, jy, jr] = jit(q[3], tm, 1.1, 6);
863
+ const [x, y] = camPt(k, bxf(q[0]) + 30 * s.lift + jx, byf(q[1]) - 40 * s.lift + jy); ch.push([x, y, (212 * BS + 10) * s.s * k.z, (212 * BS + 10) * s.s * k.z, q[2] * DEG + jr]); });
864
+ if (ch.length) { const b = bbox(ch); out.push({ id: 'title', type: 'rect', x: R(b.x), y: R(b.y), w: R(b.w), h: R(b.h), rot: 0, a: ch.length / 4,
865
+ text: '拼贴剪贴', note: 'hero: 拼·贴·剪·贴 ransom chips, 2×2 block (landed ' + ch.length + '/4)' }); }
866
+ }
867
+ // ---- end card (red riso), revealed by the tear (fully clear from 8.083)
868
+ if (t >= 8.0833) {
869
+ const S = headEndS(t), el = earthLocal(9.9, t), [ex, ey] = headPt(S, el.x, el.y);
870
+ out.push({ id: 'earth', type: 'circle', x: R(ex), y: R(ey), r: R(118 * S.s), a: 1, look: 'halftone-photo', fill: '#3F6FA8', stroke: '#F4EFE4', sw: 2.4,
871
+ note: 'Earth cut-out in the reprise head (end card)' });
872
+ const [bx, by] = headPt(S, HG[0], HG[1]);
873
+ out.push({ id: 'brad', type: 'circle', x: R(bx), y: R(by), r: R(15 * S.s), a: 1, look: 'sphere', fill: '#D6A945', stroke: '#3C2608', sw: 1,
874
+ grad: ['#FFE9A6', '#7A5516'], note: 'brass brad of the reprise head (lid snaps shut on it at 9.5)' });
875
+ const ch = [];
876
+ ECH.forEach((q, i) => { const s = slap(t, V5E.chips[i], 2); if (!s || s.k < 0) return; const [jx, jy, jr] = jit(q[10], t, s.lift > 0 ? 2 : 1.1, s.lift > 0 ? 12 : 6);
877
+ ch.push([q[7] + 30 * s.lift + jx, q[8] - 40 * s.lift + jy, (q[5] + 10) * s.s, (q[6] + 10) * s.s, q[9] * DEG + jr]); });
878
+ if (ch.length) { const b = bbox(ch); out.push({ id: 'title', type: 'rect', x: R(b.x), y: R(b.y), w: R(b.w), h: R(b.h), rot: 0, a: ch.length / 4,
879
+ text: '拼贴剪贴', note: 'end-card masthead: 拼·贴·剪·贴 ransom chips in a row (landed ' + ch.length + '/4)' }); }
880
+ const sb = slap(t, C.end_button, 2);
881
+ if (sb) { const [jx, jy] = jit(55, t, sb.k < 2 ? 2 : 1, sb.k < 2 ? 12 : 6);
882
+ out.unshift({ id: 'dot', type: 'circle', x: R(380 + jx), y: R(870 + jy), r: R(118 * sb.s), a: 1, look: 'sticker', fill: '#16130F', stroke: '#EFE8DA', sw: R(12 * sb.s),
883
+ vis: 1, z: 'top (unoccluded)', note: 'round price sticker 随刊附赠 剪刀一把: cream paper rim + black disc with cream text; its tiny ✂ (top right) snips at 9.583' }); }
884
+ }
885
+ return out;
886
+ };
887
+ }
888
+ window.__ready = true;
889
+ </script></body></html>