dsh-drawai 0.3.3 → 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 +5283 -223
  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,901 @@
1
+ <!doctype html>
2
+ <html><head><meta charset="utf-8">
3
+ <link rel="stylesheet" href="/assets/fonts/fonts.css">
4
+ <style>
5
+ @font-face{font-family:'Outfit Morph';src:url('/demos/08-morph/assets/fonts/Outfit-SemiBold.ttf') format('truetype');font-weight:1 1000;font-display:block}
6
+ html,body{margin:0;background:#000;overflow:hidden}
7
+ canvas{display:block}
8
+ </style></head>
9
+ <body>
10
+ <canvas id="c" width="1920" height="1080"></canvas>
11
+ <svg id="svg" width="0" height="0" style="position:absolute;left:-10px;top:-10px"></svg>
12
+ <script>
13
+ (async function () {
14
+ const W = 1920, H = 1080, FPS = 30, FR = 1 / FPS;
15
+ window.DEMO = { width: W, height: H, fps: FPS, duration: 10, motionBlur: { samples: 4, shutter: 0.5 } };
16
+ const cues = await (await fetch('/demos/08-morph/cues.json')).json();
17
+ const GL = await (await fetch('/demos/08-morph/glyphs.json')).json();
18
+ // reel v5 (only with ?v5=1): the end wordmark becomes the style name 形变动画, window.linkAt(t), ?nolink=dot,title
19
+ const QS = new URLSearchParams(location.search), V5 = QS.get('v5') === '1';
20
+ const NOLINK = new Set(V5 ? (QS.get('nolink') || '').split(',').map(s => s.trim()).filter(Boolean) : []);
21
+ const HIDE_DOT = NOLINK.has('dot'), HIDE_TITLE = NOLINK.has('title');
22
+ // v5.1: the end card sits on the film's cream paper, the title is ink and built from the film's own parts (buildGV51)
23
+ const cv = document.getElementById('c'); let ctx = cv.getContext('2d');
24
+ const off = document.createElement('canvas'); off.width = 1920; off.height = 1080; const octx = off.getContext('2d');
25
+ const C = { ink: '#16151B', cream: '#F4EEE3', mari: '#FFB627', verm: '#FF4F2B', ultra: '#2438E8' };
26
+
27
+ // ---------------------------------------------------------------- math / easing / colour
28
+ const clamp = (x, a = 0, b = 1) => x < a ? a : x > b ? b : x, lerp = (a, b, t) => a + (b - a) * t;
29
+ const smooth = (a, b, x) => { const t = clamp((x - a) / (b - a)); return t * t * (3 - 2 * t); };
30
+ const deg = Math.PI / 180;
31
+ const qT = x => Math.round(x * FPS) / FPS, SH = 0.5 / FPS; // frame-centre time (identical for every harness mb sample) + the 0.5 shutter (s)
32
+ const EXI = x => x <= 0 ? 0 : x >= 1 ? 1 : (Math.pow(2, 10 * x) - 1) / 1023; // expo-in
33
+ function bez(x1, y1, x2, y2) {
34
+ const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx, cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
35
+ const sx = t => ((ax * t + bx) * t + cx) * t, sy = t => ((ay * t + by) * t + cy) * t;
36
+ return x => { if (x <= 0) return 0; if (x >= 1) return 1; let lo = 0, hi = 1, t = x;
37
+ for (let i = 0; i < 40; i++) { const v = sx(t); if (Math.abs(v - x) < 1e-7) break; if (v < x) lo = t; else hi = t; t = (lo + hi) / 2; }
38
+ return sy(t); };
39
+ }
40
+ const EM = bez(0.7, 0, 0.3, 1); // the morph curve from the brief
41
+ const EO = bez(0.16, 1, 0.3, 1); // expo-out (field blooms, arrivals)
42
+ const EIO = bez(0.45, 0, 0.55, 1);
43
+ const EB = bez(0.42, 0, 0.12, 1); // field bloom
44
+ const hexRgb = h => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16) / 255);
45
+ const toLin = c => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
46
+ const toSrgb = c => c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
47
+ function oklab(h) { const [r, g, b] = hexRgb(h).map(toLin);
48
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b), m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b), s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
49
+ return [0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s, 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s]; }
50
+ function fromOklab([L, a, b]) {
51
+ const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3), m = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3), s = Math.pow(L - 0.0894841775 * a - 1.2914855480 * b, 3);
52
+ const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, bb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
53
+ return '#' + [r, g, bb].map(c => Math.round(clamp(toSrgb(clamp(c))) * 255).toString(16).padStart(2, '0')).join(''); }
54
+ const mixC = (h1, h2, t) => t <= 0 ? h1 : t >= 1 ? h2 : fromOklab(oklab(h1).map((v, i) => lerp(v, oklab(h2)[i], t)));
55
+ const tone = (h, dL) => { const A = oklab(h); A[0] += dL; return fromOklab(A); };
56
+
57
+ // ---------------------------------------------------------------- geometry
58
+ const NB = 240, NH = 72, NM = 24, NS = 60;
59
+ const svg = document.getElementById('svg');
60
+ function sampleD(d, n, closed = true) {
61
+ const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p.setAttribute('d', d); svg.appendChild(p);
62
+ const L = p.getTotalLength(), o = [], den = closed ? n : n - 1;
63
+ for (let i = 0; i < n; i++) { const q = p.getPointAtLength(L * i / den); o.push([q.x, q.y]); }
64
+ svg.removeChild(p); return o;
65
+ }
66
+ const area = P => { let a = 0; for (let i = 0; i < P.length; i++) { const p = P[i], q = P[(i + 1) % P.length]; a += p[0] * q[1] - q[0] * p[1]; } return a / 2; };
67
+ const orient = P => area(P) < 0 ? P.slice().reverse() : P;
68
+ const centroid = P => { let x = 0, y = 0; for (const p of P) { x += p[0]; y += p[1]; } return [x / P.length, y / P.length]; };
69
+ function resamplePoly(P, n) {
70
+ const L = [0]; for (let i = 1; i <= P.length; i++) { const a = P[i - 1], b = P[i % P.length]; L.push(L[i - 1] + Math.hypot(b[0] - a[0], b[1] - a[1])); }
71
+ const tot = L[P.length], o = []; let j = 0;
72
+ for (let i = 0; i < n; i++) { const s = tot * i / n; while (L[j + 1] < s) j++; const a = P[j], b = P[(j + 1) % P.length], u = (s - L[j]) / ((L[j + 1] - L[j]) || 1); o.push([lerp(a[0], b[0], u), lerp(a[1], b[1], u)]); }
73
+ return o;
74
+ }
75
+ const circ = (cx, cy, r, n) => Array.from({ length: n }, (_, i) => { const a = 2 * Math.PI * i / n; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; });
76
+ function bestShift(A, B) { const n = A.length; let best = Infinity, bs = 0;
77
+ for (let s = 0; s < n; s++) { let e = 0; for (let i = 0; i < n; i += 2) { const b = B[(i + s) % n], dx = A[i][0] - b[0], dy = A[i][1] - b[1]; e += dx * dx + dy * dy; if (e >= best) break; } if (e < best) { best = e; bs = s; } }
78
+ return bs; }
79
+ const shiftP = (B, s) => B.map((_, i) => B[(i + s) % B.length]);
80
+ const alignTo = (A, B) => shiftP(B, bestShift(A, B));
81
+ const lerpP = (A, B, t) => A.map((a, i) => [lerp(a[0], B[i][0], t), lerp(a[1], B[i][1], t)]);
82
+ const rep = (p, n) => Array.from({ length: n }, () => [p[0], p[1]]);
83
+ const S = d => orient(sampleD(d, NB));
84
+ function rpoly(pts, r) { const n = pts.length; let d = '';
85
+ for (let i = 0; i < n; i++) { const p = pts[i], a = pts[(i - 1 + n) % n], b = pts[(i + 1) % n], rr = Array.isArray(r) ? r[i] : r;
86
+ const da = Math.hypot(a[0] - p[0], a[1] - p[1]), db = Math.hypot(b[0] - p[0], b[1] - p[1]), k1 = Math.min(rr, da / 2) / da, k2 = Math.min(rr, db / 2) / db;
87
+ d += (i ? 'L' : 'M') + (p[0] + (a[0] - p[0]) * k1) + ',' + (p[1] + (a[1] - p[1]) * k1) + 'Q' + p[0] + ',' + p[1] + ' ' + (p[0] + (b[0] - p[0]) * k2) + ',' + (p[1] + (b[1] - p[1]) * k2); }
88
+ return d + 'Z'; }
89
+ const strokeD = (d, w) => ({ pts: sampleD(d, NM, false), w });
90
+ const ray = (a, r1, r2, w, cx = 0, cy = 0) => { const c = Math.cos(a * deg), s = Math.sin(a * deg); return strokeD(`M${cx + r1 * c},${cy + r1 * s}L${cx + r2 * c},${cy + r2 * s}`, w); };
91
+ const dotS = p => ({ pts: rep(p, NM), w: 0 });
92
+ const hole = (pts, on = true) => ({ pts, on });
93
+ const noHole = p => hole(rep(p, NH), false);
94
+ const noShade = p => ({ pts: rep(p, NS), on: false });
95
+ function trim(pts, f) { // keep the first fraction f of an open polyline
96
+ return pts.map((_, i) => { const u = i / (NM - 1) * f * (NM - 1), k = Math.min(NM - 2, Math.floor(u)), r = u - k; return [lerp(pts[k][0], pts[k + 1][0], r), lerp(pts[k][1], pts[k + 1][1], r)]; }); }
97
+
98
+ // ---------------------------------------------------------------- icons (local px, origin = visual centre)
99
+ const I = {};
100
+ { // CUP — one-radius mug (14 px corners) + ring handle; its hole is THE circle (r 22 px = pin eye = full stop)
101
+ const HR = 22 / 0.95, HC = [138, 30], HO = HR + 30, yj = Math.sqrt(HO * HO - (HC[0] - 105) ** 2), q = 14 / 0.95;
102
+ const body = S(`M ${-130 + q},-70 L ${105 - q},-70 Q 105,-70 105,${-70 + q} L 105,${HC[1] - yj} A ${HO},${HO} 0 1 1 105,${HC[1] + yj} L 105,${135 - q} Q 105,135 ${105 - q},135 L ${-130 + q},135 Q -130,135 -130,${135 - q} L -130,${-70 + q} Q -130,-70 ${-130 + q},-70 Z`);
103
+ const st = Array.from({ length: 8 }, () => dotS([-12, 30]));
104
+ st[7] = strokeD('M -75,-98 C -97,-128 -53,-150 -75,-182', 19); st[7].steam = 0;
105
+ st[0] = strokeD('M -12,-98 C -34,-133 10,-160 -12,-202', 19); st[0].steam = 1;
106
+ st[1] = strokeD('M 51,-98 C 29,-128 73,-150 51,-182', 19); st[1].steam = 2;
107
+ st[4] = strokeD('M -178,170 L 150,170', 19); st[4].saucer = true;
108
+ I.cup = { body, holes: [hole(circ(HC[0], HC[1], HR, NH))], strokes: st, shade: { pts: orient(sampleD(rpoly([[105, -70], [105, 135], [-8, 135]], [3, 3, 3]), NS)), on: true },
109
+ shadeC: mixC(C.ink, C.cream, 0.1), fill: C.ink, pos: [942, 548], scale: 0.95, piv: [0, 170] };
110
+ }
111
+ { // SUN
112
+ const st = Array.from({ length: 8 }, (_, k) => ray(-90 + 45 * k, 165, 214, 18));
113
+ I.sun = { body: circ(0, 0, 125, NB), holes: [noHole([0, 0])], strokes: st, shade: noShade([0, 0]), fill: C.verm, pos: [960, 468], scale: 1, piv: [0, 0] };
114
+ }
115
+ { // SUNSET — sun centre on the horizon (local y 40); 5 upper rays; 3 lower rays became reflection waves
116
+ const st = [];
117
+ const up = { 6: -168, 7: -129, 0: -90, 1: -51, 2: -12 };
118
+ for (const k in up) st[k] = ray(up[k], 192, 234, 18, 0, 40);
119
+ st[3] = strokeD('M -150,100 L 150,100', 18); st[4] = strokeD('M -100,140 L 100,140', 18); st[5] = strokeD('M -52,180 L 52,180', 18);
120
+ I.sunset = { body: circ(0, 40, 150, NB), holes: [noHole([0, 40])], strokes: st, shade: noShade([0, 40]), fill: C.mari, pos: [960, 520], scale: 1, piv: [0, 40] };
121
+ }
122
+ { // GULL — two crescent wings whose upper edges are arcs of the SUN's own circle (r 125); flapping
123
+ const arcP = (a, b, R, n) => { const mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2, dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy), h = Math.sqrt(Math.max(0, R * R - L * L / 4));
124
+ let nx = -dy / L, ny = dx / L; if (ny < 0) { nx = -nx; ny = -ny; }
125
+ const c = [mx + nx * h, my + ny * h], a0 = Math.atan2(a[1] - c[1], a[0] - c[0]); let da = Math.atan2(b[1] - c[1], b[0] - c[0]) - a0;
126
+ while (da > Math.PI) da -= 2 * Math.PI; while (da < -Math.PI) da += 2 * Math.PI;
127
+ return Array.from({ length: n }, (_, i) => [c[0] + R * Math.cos(a0 + da * i / (n - 1)), c[1] + R * Math.sin(a0 + da * i / (n - 1))]); };
128
+ const gp = ty => { const TL = [-205, ty], TR = [205, ty];
129
+ return [...arcP(TL, [-13, 12], 125, 48), [0, 18], ...arcP([13, 12], TR, 125, 48), ...arcP(TR, [21, 40], 250, 48).slice(1),
130
+ ...Array.from({ length: 13 }, (_, i) => [21 * Math.cos(Math.PI * (i + 1) / 14), 40 + 21 * Math.sin(Math.PI * (i + 1) / 14)]), ...arcP([-21, 40], TL, 250, 48).slice(0, -1)]; };
131
+ const up = orient(resamplePoly(gp(-62), NB));
132
+ const dn = alignTo(up, orient(resamplePoly(gp(40), NB)));
133
+ I.gull = { body: up, down: dn, holes: [noHole([0, 20])], strokes: Array.from({ length: 8 }, () => dotS([0, 22])), shade: noShade([0, 20]), fill: C.cream, pos: [960, 424], scale: 1.02, piv: [0, 0] };
134
+ }
135
+ { // PAPER PLANE — nose up-right (heading -35deg), keel facet
136
+ const N = [205, -125], L = [-205, 5], F = [-40, 45], K = [-5, 160], Q = [F[0] + 0.32 * (N[0] - F[0]), F[1] + 0.32 * (N[1] - F[1])];
137
+ const body = S(rpoly([N, L, F, K], [19, 19, 19, 19]));
138
+ const shade = orient(sampleD(rpoly([F, K, Q], [8, 19, 8]), NS));
139
+ I.plane = { body, holes: [noHole([0, 0])], strokes: Array.from({ length: 8 }, () => dotS([0, 10])), shade: { pts: shade, on: true },
140
+ fill: C.cream, shadeC: mixC(C.cream, C.ultra, 0.30), pos: [690, 642], scale: 0.74, rot0: 35 * deg, piv: [0, 0] };
141
+ }
142
+ { // PIN — circle + rotated square (the heart's own construction), eye = THE circle (r 22 px), fold facet = the plane's facet
143
+ const R = 140, cy = -3, v = cy + R * Math.SQRT2, t = R / Math.SQRT2, q = 14 / 0.85 / Math.SQRT2;
144
+ const body = S(`M ${-q},${v - q} L ${-t},${cy + t} A ${R},${R} 0 1 1 ${t},${cy + t} L ${q},${v - q} Q 0,${v} ${-q},${v - q} Z`);
145
+ const shade = orient(sampleD(rpoly([[0, v + 4], [t + 2, cy + t], [0, cy]], [2, 2, 2]), NS));
146
+ I.pin = { body, holes: [hole(circ(0, cy, 22 / 0.85, NH))], strokes: Array.from({ length: 8 }, () => dotS([0, cy])), shade: { pts: shade, on: true }, shadeC: '#D93A1C',
147
+ fill: C.verm, pos: [960, 480], scale: 0.85, piv: [0, 190] };
148
+ }
149
+ { // HEART — two circles on the upper sides of a rotated square (primitive logic), 14 px tip
150
+ const a = 234, h = a / Math.SQRT2, rc = a / 2, yc = 6, q = 14 / 0.86 / Math.SQRT2;
151
+ const body = S(`M ${-q},${yc + h - q} L ${-h},${yc} A ${rc},${rc} 0 0 1 0,${yc - h} A ${rc},${rc} 0 0 1 ${h},${yc} L ${q},${yc + h - q} Q 0,${yc + h} ${-q},${yc + h - q} Z`);
152
+ I.heart = { body, holes: [noHole([0, 0])], strokes: Array.from({ length: 8 }, () => dotS([0, 0])), shade: { pts: orient(sampleD(rpoly([[0, yc + h + 4], [h + 2, yc], [0, yc]], [2, 2, 2]), NS)), on: true },
153
+ shadeC: mixC(C.cream, C.verm, 0.15), fill: C.cream, pos: [960, 522], scale: 0.86, piv: [0, 0] };
154
+ }
155
+
156
+ // ---------------------------------------------------------------- wordmark (real Outfit SemiBold outlines)
157
+ const WM = (() => {
158
+ const FS = 250, s = FS / GL.upm, track = -5, yB = 566, word = 'morphe';
159
+ const advs = [...word].map(ch => GL.glyphs[ch].adv * s);
160
+ const dotR = 22, dotGap = 9;
161
+ const total = advs.reduce((a, b) => a + b, 0) + track * (word.length - 1) + dotGap + 2 * dotR;
162
+ let x = 960 - total / 2; const letters = [];
163
+ [...word].forEach((ch, k) => {
164
+ const g = GL.glyphs[ch]; const cs = g.contours.map(c => c.map(([gx, gy]) => [x + gx * s, yB - gy * s]));
165
+ let oi = 0; g.areas.forEach((a, i) => { if (Math.abs(a) > Math.abs(g.areas[oi])) oi = i; });
166
+ const outer = orient(resamplePoly(cs[oi], k === 0 ? NB : 200));
167
+ const holes = cs.filter((_, i) => i !== oi).map(c => orient(resamplePoly(c, NH)));
168
+ const cen = centroid(outer), ar = Math.abs(area(outer));
169
+ letters.push({ ch, outer, holes, cen, r: Math.sqrt(ar / Math.PI) * 0.82, right: x + advs[k] });
170
+ x += advs[k] + track;
171
+ });
172
+ const e = letters[letters.length - 1];
173
+ const dot = { cen: [e.right + dotGap + dotR - track, yB - dotR], r: dotR };
174
+ return { letters, dot, yB, FS };
175
+ })();
176
+ { const m = WM.letters[0];
177
+ I.m = { body: m.outer.map(p => [p[0] - m.cen[0], p[1] - m.cen[1]]), holes: [noHole([0, 0])], strokes: Array.from({ length: 8 }, () => dotS([0, 0])), shade: noShade([0, 0]),
178
+ fill: C.cream, pos: m.cen, scale: 1, piv: [0, WM.yB - m.cen[1]] }; }
179
+
180
+ // ---------------------------------------------------------------- v5 title 形变动画 (real ResourceHanRounded Heavy outlines = morph targets)
181
+ // heart -> circle (8.30, at the title centre) -> the circle divides into four cells + the full stop -> each cell blooms into its character
182
+ let TV = null;
183
+ // v5.1 glyphs: 形变动画 constructed from the film's vocabulary: round-cap bars of ONE width (the icons' strokes), arcs for the
184
+ // falling strokes, true discs for the dots (the full stop's size), 画's 田 as one disc quartered (the four cells). Design px in a
185
+ // 240 box, y down; returned in the old font-JSON shape (upm 240, y up) so the bloom build below is unchanged.
186
+ function buildGV51() {
187
+ const w = 32, hw = w / 2, BASE = 218;
188
+ const L2 = (x0, y0, x1, y1) => [[x0, y0], [x1, y1]];
189
+ const arcC = (cx, cy, R, a0, a1, n = 40) => Array.from({ length: n }, (_, i) => { const a = (a0 + (a1 - a0) * i / (n - 1)) * deg; return [cx + R * Math.cos(a), cy + R * Math.sin(a)]; });
190
+ const arc3 = (p0, p1, sg, n = 40) => { // arc p0 -> p1 with sagitta sg (+ = bulges to the right of the travel direction on screen)
191
+ const mx = (p0[0] + p1[0]) / 2, my = (p0[1] + p1[1]) / 2, dx = p1[0] - p0[0], dy = p1[1] - p0[1], c = Math.hypot(dx, dy), nx = -dy / c, ny = dx / c;
192
+ return Array.from({ length: n }, (_, i) => { const u = i / (n - 1), b = 4 * sg * u * (1 - u); // parabola ~ arc for small sagitta
193
+ return [p0[0] + dx * u + nx * b, p0[1] + dy * u + ny * b]; }); };
194
+ const stroke = (...segs) => { // centreline (tangent-continuous segments) -> closed outline with round caps
195
+ const P = []; for (const sg of segs) for (const p of sg) { const q = P[P.length - 1]; if (!q || Math.hypot(p[0] - q[0], p[1] - q[1]) > 1e-6) P.push(p); }
196
+ const n = P.length, Lf = [], Rt = [], tan = i => { const a = P[Math.max(0, i - 1)], b = P[Math.min(n - 1, i + 1)], d = Math.hypot(b[0] - a[0], b[1] - a[1]); return [(b[0] - a[0]) / d, (b[1] - a[1]) / d]; };
197
+ for (let i = 0; i < n; i++) { const [tx, ty] = tan(i); Lf.push([P[i][0] - ty * hw, P[i][1] + tx * hw]); Rt.push([P[i][0] + ty * hw, P[i][1] - tx * hw]); }
198
+ const cap = (c, t, a0, a1) => { const th = Math.atan2(t[1], t[0]), o = []; for (let j = 1; j < 16; j++) { const a = th + (a0 + (a1 - a0) * j / 16) * deg; o.push([c[0] + hw * Math.cos(a), c[1] + hw * Math.sin(a)]); } return o; };
199
+ return [...Lf, ...cap(P[n - 1], tan(n - 1), 90, -90), ...Rt.reverse(), ...cap(P[0], tan(0), -90, -270)]; };
200
+ const bar = (...a) => stroke(L2(...a));
201
+ const disc = (cx, cy, r) => circ(cx, cy, r, 96);
202
+ const quarters = (cx, cy, R, g) => [[1, 1], [-1, 1], [-1, -1], [1, -1]].map(([sx, sy]) => { // a disc cut into four by a cross of width g
203
+ const h = g / 2, e = Math.sqrt(R * R - h * h), aY = Math.atan2(sy * h, sx * e), aX = Math.atan2(sy * e, sx * h); let da = aX - aY;
204
+ while (da > Math.PI) da -= 2 * Math.PI; while (da < -Math.PI) da += 2 * Math.PI;
205
+ return [[cx + sx * h, cy + sy * h], ...Array.from({ length: 33 }, (_, i) => [cx + R * Math.cos(aY + da * i / 32), cy + R * Math.sin(aY + da * i / 32)])]; });
206
+ const G = {
207
+ '形': [bar(26, 40, 124, 40), bar(16, 118, 128, 118), stroke(L2(58, 40, 58, 146), arcC(58 - 84, 146, 84, 0, 60)), bar(110, 40, 110, 222),
208
+ stroke(arc3([212, 26], [160, 72], -6)), stroke(arc3([218, 94], [158, 146], -6)), stroke(arc3([224, 160], [150, 220], -7))],
209
+ '变': [disc(120, 18, 19), bar(22, 58, 218, 58), bar(98, 58, 98, 102), bar(142, 58, 142, 102), disc(50, 104, 17), disc(190, 104, 17),
210
+ bar(58, 150, 178, 150), stroke(arc3([178, 150], [38, 224], -14)), stroke(arc3([86, 172], [210, 224], 10))],
211
+ '动': [bar(26, 46, 92, 46), bar(16, 104, 100, 104), bar(58, 104, 26, 208), bar(26, 208, 92, 194), disc(88, 150, 18),
212
+ bar(136, 78, 220, 78), stroke(L2(220, 78, 220, 190), arcC(190, 190, 30, 0, 90)), stroke(L2(176, 16, 176, 120), arcC(176 - 110, 120, 110, 0, 50))],
213
+ '画': [bar(20, 16, 220, 16), bar(22, 100, 22, 222), bar(22, 222, 218, 222), bar(218, 100, 218, 222), ...quarters(120, 124, 60, 11)],
214
+ };
215
+ const glyphs = {};
216
+ for (const ch in G) { const comps = G[ch].map(pts => ({ outer: pts.map(([x, y]) => [x, BASE - y]), holes: [] }));
217
+ const xs = comps.flatMap(c => c.outer.map(p => p[0])), ys = comps.flatMap(c => c.outer.map(p => p[1]));
218
+ glyphs[ch] = { adv: 240, bbox: [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)], comps }; }
219
+ return { upm: 240, glyphs };
220
+ }
221
+ if (V5) {
222
+ const GV = buildGV51();
223
+ const F = 240, s = F / GV.upm, track = 8, dotR = 22, gap = 14, chars = [...'形变动画'], CY = 487, XS = -7; // XS, CY: keep the full stop where v5.0 had it
224
+ const top = Math.max(...chars.map(c => GV.glyphs[c].bbox[3])), bot = Math.min(...chars.map(c => GV.glyphs[c].bbox[1]));
225
+ const g0 = GV.glyphs[chars[0]], g3 = GV.glyphs[chars[3]], adv = F + track;
226
+ const inkW = 3 * adv + (g3.bbox[2] - g0.bbox[0]) * s, blockW = inkW + gap + 2 * dotR;
227
+ const X0 = 960 + XS - blockW / 2 - g0.bbox[0] * s, yB = CY + (top + bot) / 2 * s;
228
+ const perim = P => { let L = 0; for (let i = 0; i < P.length; i++) L += Math.hypot(P[(i + 1) % P.length][0] - P[i][0], P[(i + 1) % P.length][1] - P[i][1]); return L; };
229
+ const rc = 96; // radius of each character's cell (the bridge circle)
230
+ const items = chars.map((ch, k) => {
231
+ const g = GV.glyphs[ch], xo = X0 + k * adv, tr = c => c.map(([gx, gy]) => [xo + gx * s, yB - gy * s]);
232
+ const cen = [xo + (g.bbox[0] + g.bbox[2]) / 2 * s, yB - (g.bbox[1] + g.bbox[3]) / 2 * s];
233
+ const comps = g.comps.map((c, j) => {
234
+ const raw = tr(c.outer), n = Math.max(160, Math.round(perim(raw) / 2.4)), outer = orient(resamplePoly(raw, n));
235
+ const holes = c.holes.map(h => { const hr = tr(h); return { raw: hr, pts: orient(resamplePoly(hr, Math.max(48, Math.round(perim(hr) / 2.4)))) }; });
236
+ const circle = alignTo(outer, circ(0, 0, rc, n)); // unit cell (relative to the cell centre), first vertex aligned to the outline
237
+ return { raw, outer, holes, circle, j };
238
+ }).sort((a, b) => Math.abs(area(b.outer)) - Math.abs(area(a.outer))); // biggest part first: small strokes pinch off last
239
+ return { ch, k, cen, comps };
240
+ });
241
+ const inkX0 = X0 + g0.bbox[0] * s, inkX1 = X0 + 3 * adv + g3.bbox[2] * s;
242
+ TV = { F, s, yB, items, rc, t0: 8.30, split: 0.12, m0: 8.38, st: 0.025, cst: 0.006, md: 0.20, box: [inkX0, yB - top * s, inkX1, yB - bot * s],
243
+ stop: [inkX1 + gap + dotR, yB - g3.bbox[1] * s - dotR], stopR: dotR };
244
+ TV.done = TV.m0 + 3 * TV.st + (Math.max(...items.map(it => it.comps.length)) - 1) * TV.cst + TV.md; // every stroke formed
245
+ WM.dot.cen = TV.stop; WM.dot.r = dotR; // the end-card machinery (chapter dots, lub-dub rings) now targets the title's full stop
246
+ TV.CY = CY;
247
+ }
248
+
249
+ // ---------------------------------------------------------------- icon "life" (pure in t)
250
+ function state(name, t) {
251
+ const ic = I[name];
252
+ if (name === 'cup') {
253
+ const strokes = ic.strokes.map((s, k) => {
254
+ if (s.steam !== undefined) { const j = s.steam, ph = j * 1.9, ta = 0.40 + [0.02, 0, 0.04][j], tb = ta + 0.2, base = s.pts[0];
255
+ if (t < tb) { if (t < ta) return { pts: rep(base, NM), w: 0 }; // crown droplet flung from the contact point, lands on the rim
256
+ const e = EIO((t - ta) / (tb - ta)), S0 = [[-150, 118], [128, 110], [160, 124]][j], Cc = [[-205, -330], [70, -420], [175, -300]][j];
257
+ const hd = lerp(0.05, 1, e); return { pts: Array.from({ length: NM }, (_, i) => qbez(S0, Cc, base, Math.max(0, hd - 0.05 * (1 - i / (NM - 1))))), w: lerp(26, 17, e) }; }
258
+ const f = smooth(tb, tb + 0.42, t);
259
+ const pts = trim(s.pts, Math.max(0.001, f)).map((p, i) => { const u = i / (NM - 1) * f; return [p[0] + 10 * u * Math.sin(u * 5 - t * 16 + ph), p[1] - 7 * u * Math.sin(t * 6.8 + ph)]; });
260
+ return { pts, w: lerp(17, s.w, clamp(f * 3)) }; }
261
+ if (s.saucer) { const f = EO(clamp((t - 0.37) / 0.3)); return { pts: s.pts.map(p => [lerp(-12, p[0], f), p[1]]), w: s.w * clamp(f * 3) }; }
262
+ return s; });
263
+ const hf = smooth(0.5, 0.66, t); const hc = [138, 30];
264
+ return { ...ic, strokes, holes: [hole(lerpP(rep(hc, NH), ic.holes[0].pts, hf))] };
265
+ }
266
+ if (name === 'sun') { const nb = clamp((t - 1.98) / 0.4, 0, 2.2), a = 12 * (Math.floor(nb) + EO(clamp((nb % 1) / 0.45))) * deg, c = Math.cos(a), s = Math.sin(a), push = 1 + 0.075 * Math.sin(Math.PI * clamp((t - 1.97) / 0.34));
267
+ return { ...ic, strokes: ic.strokes.map(st => ({ w: st.w, pts: st.pts.map(p => [(p[0] * c - p[1] * s) * push, (p[0] * s + p[1] * c) * push]) })) }; }
268
+ if (name === 'sunset') return { ...ic, strokes: ic.strokes.map((st, k) => k >= 3 && k <= 5 ? { w: st.w, pts: st.pts.map(p => [p[0] * (1 + 0.05 * Math.sin(t * 6.3 + k * 2.1)) + (k % 2 ? 9 : -9) * Math.cos(Math.PI * (t - 3.2) / 0.4), p[1]]) } : st) };
269
+ if (name === 'gull') { const f = 0.5 - 0.5 * Math.cos(2 * Math.PI * (t - 4.2) / 0.2); return { ...ic, body: lerpP(ic.body, ic.down, f) }; }
270
+ return ic;
271
+ }
272
+
273
+ // ---------------------------------------------------------------- morph table
274
+ const MP = {
275
+ 'cup>sun': { via: [0, 18, 116], rot: 24, ctrl: [70, -10], sq: 0.13 },
276
+ 'sun>sunset': { rot: 0, ctrl: [0, 0], sq: 0.05 },
277
+ 'sunset>gull': { via: [0, 30, 118], rot: -18, ctrl: [-60, 10], sq: 0.12 },
278
+ 'gull>plane': { via: [0, 10, 98], rot: -26, ctrl: [110, -70], sq: 0.14 },
279
+ 'plane>pin': { via: [0, 5, 100] },
280
+ 'pin>heart': { via: [0, -12, 120], rot: 18, ctrl: [0, -190], sq: 0.13, sw: [0.49, 0.51] },
281
+ 'heart>m': { via: [0, -6, 112], rot: -20, ctrl: [40, -70], sq: 0.12 },
282
+ };
283
+ for (const m of cues.morphs) {
284
+ const key = m.a + '>' + m.b, P = MP[key]; if (!P) continue; m.key = key; m.P = P;
285
+ const A = I[m.a].body, B = I[m.b].body;
286
+ if (P.via) { const c = circ(P.via[0], P.via[1], P.via[2], NB); P.Ca = alignTo(A, c); P.Cb = alignTo(B, c); }
287
+ else P.sAB = bestShift(A, B);
288
+ }
289
+ const drop = cues.morphs[0];
290
+ drop.Ca = alignTo(I.cup.body, circ(-12, 30, 105, NB));
291
+ if (V5) { // v5: the heart travels to the title centre while it becomes the circle (u 0.5 = 8.30), with almost no stretch left at the circle
292
+ const A = I.heart.pos, tgt = [960, TV.CY - 6], mp = MP['heart>m'];
293
+ mp.ctrl = [0, -60]; mp.sq = 0.035; mp.rot = -14;
294
+ I.m.fill = C.ink; mp.sw = [0.04, 0.20]; // v5.1 figure-ground swap: the cream heart turns ink (8.02-8.12) while the cream paper spills out from under it
295
+ I.m.pos = [2 * tgt[0] - A[0] - mp.ctrl[0], 2 * tgt[1] - A[1] - mp.ctrl[1]];
296
+ }
297
+
298
+ // ---------------------------------------------------------------- flight path
299
+ const LP = cues.loop;
300
+ const FP = (() => { const { cx, cy, r, ground: g } = LP, x0 = I.plane.pos[0];
301
+ const d = `M ${x0},${g} L ${cx},${g} A ${r},${r} 0 0 0 ${cx + r},${cy} A ${r},${r} 0 0 0 ${cx},${cy - r} A ${r},${r} 0 0 0 ${cx - r},${cy} L ${I.pin.pos[0]},${I.pin.pos[1]}`;
302
+ const n = 1200, pts = sampleD(d, n, false); const len = (cx - x0) + 1.5 * Math.PI * r + (I.pin.pos[1] - cy);
303
+ return { pts, n, len, uTop: ((cx - x0) + Math.PI * r) / len }; })();
304
+ function pathAt(u) { const x = clamp(u) * (FP.n - 1), k = Math.min(FP.n - 2, Math.floor(x)), r = x - k, a = FP.pts[k], b = FP.pts[k + 1];
305
+ const k0 = Math.max(0, k - 2), k1 = Math.min(FP.n - 1, k + 3); const ang = Math.atan2(FP.pts[k1][1] - FP.pts[k0][1], FP.pts[k1][0] - FP.pts[k0][0]);
306
+ return { p: [lerp(a[0], b[0], r), lerp(a[1], b[1], r)], ang }; }
307
+ const FL = cues.flight;
308
+ const ECL = bez(0.32, 0, 0.1, 1); // launch: quick acceleration, long deceleration up to the stall at the top
309
+ function flightU(t) { if (t <= FL.launch) return 0; const u0 = FP.uTop - 6 / FP.len;
310
+ if (t < FL.apex + 0.03) return u0 * ECL((t - FL.launch) / (FL.apex + 0.03 - FL.launch));
311
+ if (t < FL.dive) return u0 + (8 / FP.len) * smooth(FL.apex + 0.03, FL.dive + 0.05, t); // the hang: upside down at the top
312
+ const ud = u0 + (8 / FP.len) * smooth(FL.apex + 0.03, FL.dive + 0.05, FL.dive);
313
+ return Math.min(1, ud + (1 - ud) * Math.pow(clamp((t - FL.dive) / (FL.t1 - FL.dive)), 1.65)); }
314
+
315
+ // ---------------------------------------------------------------- impacts / squash
316
+ const IMP = [ { t: 0.4, A: 0.26, who: 'cup' }, { t: 2.0, A: 0.07, who: 'sun', uni: true }, { t: 3.2, A: 0.07, who: 'sunset' },
317
+ { t: 6.4, A: 0.24, who: 'pin' }, { t: 7.6, A: 0.14, who: 'heart' }, { t: 8.6, A: 0.1, who: 'm' } ];
318
+ const ANT = { cup: 0.17, sunset: 0.1, gull: 0.12, heart: 0.12 };
319
+ function impact(t, who) { let q = 0, uni = 0;
320
+ for (const im of IMP) { if (im.who !== who) continue; const τ = t - (im.t - 2 * FR); if (τ < 0 || τ > 1.2) continue;
321
+ const v = im.A * Math.sin(2 * Math.PI * τ / 0.28) * Math.exp(-τ * 7.5); if (im.uni) uni += v; else q += v; }
322
+ for (const m of cues.morphs) { const an = ANT[m.a]; if (!an || m.a !== who) continue; const τ = t - (m.t0 - 0.3);
323
+ if (τ > 0 && τ < 0.3) q += an * EIO(clamp(τ / 0.2)); else if (τ >= 0.3 && τ < 0.42) q += an * (1 - EO((τ - 0.3) / 0.12)) - 0.35 * an * Math.sin(Math.PI * (τ - 0.3) / 0.12); }
324
+ if (who === 'pin') { const τ = t - 6.86; if (τ > 0 && τ < 0.2) q += 0.12 * Math.sin(Math.PI * τ / 0.2); } // anticipation before the hop
325
+ if (who === 'heart') { for (const [tb, a] of [[cues.heartbeat[0], 0.09], [cues.heartbeat[1], 0.06]]) { const τ = t - tb + FR; if (τ > 0 && τ < 0.16) uni += a * Math.sin(Math.PI * τ / 0.16); } }
326
+ return { q, uni }; }
327
+
328
+ // transform: local -> world. impact squash about pivot (local axes), scale*rot, stretch along phi about origin, translate
329
+ function makeM(pos, rot, sc, phi, k, q, piv) {
330
+ const c = Math.cos(rot), s = Math.sin(rot);
331
+ const R = [c * sc, -s * sc, s * sc, c * sc];
332
+ const D = [1 + q, 0, 0, 1 - q];
333
+ const cp = Math.cos(phi), sp = Math.sin(phi), ik = 1 / k;
334
+ const Q = [cp * cp * k + sp * sp * ik, cp * sp * (k - ik), cp * sp * (k - ik), sp * sp * k + cp * cp * ik];
335
+ const mul = (a, b) => [a[0] * b[0] + a[1] * b[2], a[0] * b[1] + a[1] * b[3], a[2] * b[0] + a[3] * b[2], a[2] * b[1] + a[3] * b[3]];
336
+ const QR = mul(Q, R), A = mul(QR, D);
337
+ const dp = [piv[0] - (D[0] * piv[0]), piv[1] - (D[3] * piv[1])];
338
+ return { A, b: [pos[0] + QR[0] * dp[0] + QR[1] * dp[1], pos[1] + QR[2] * dp[0] + QR[3] * dp[1]] };
339
+ }
340
+ const qbez = (a, c, b, t) => [lerp(lerp(a[0], c[0], t), lerp(c[0], b[0], t), t), lerp(lerp(a[1], c[1], t), lerp(c[1], b[1], t), t)];
341
+ function heroClip(t) { if (t <= 2.3 || t >= 4.45) return null; return seaSurface(t) + 300 * EIO(clamp((t - 3.84) / 0.13)); } // the sun breaks the surface 2 f before the circle
342
+ function seaSurface(t) { if (t < cues.wipes.sea) return H + 10; return lerp(H + 10, 560, EO(clamp((t - cues.wipes.sea) / 0.5))); }
343
+
344
+ function blendParts(A, B, u, P) { // u = linear time fraction of the morph
345
+ let body, holes, strokes, shade;
346
+ const via = P && P.Ca, gA = EM(clamp(u / 0.5)), gB = EM(clamp((u - 0.5) / 0.5)), g = EM(u);
347
+ if (via) body = u < 0.5 ? lerpP(A.body, P.Ca, gA) : lerpP(P.Cb, B.body, gB);
348
+ else body = lerpP(A.body, shiftP(B.body, P ? P.sAB : 0), g);
349
+ const hA = A.holes[0], hB = B.holes[0];
350
+ if (hA.on && hB.on && !via) holes = [hole(lerpP(hA.pts, hB.pts, g))];
351
+ else holes = [u < 0.5 ? hole(lerpP(hA.pts, rep(centroid(hA.pts), NH), clamp(gA / 0.6))) : hole(lerpP(rep(centroid(hB.pts), NH), hB.pts, clamp((gB - 0.4) / 0.6)))];
352
+ strokes = A.strokes.map((sa, k) => { const sb = B.strokes[k], o = [7, 0, 1, 2, 6, 3, 5, 4].indexOf(k) * 0.03, gk = EIO(clamp((u - 0.06 - o) / 0.7));
353
+ const dies = sb.w === 0 && sa.w > 0, wk = dies ? 1 - smooth(0.2, 0.48, u) : 1; // strokes with no B slot shrink + fade in place before the circle
354
+ let pts = dies ? (() => { const md = sa.pts[NM >> 1], f = 1 - 0.7 * smooth(0.05, 0.48, u); return sa.pts.map(p => [lerp(md[0], p[0], f), lerp(md[1], p[1], f)]); })() : lerpP(sa.pts, sb.pts, gk);
355
+ if (via && P.via && !dies) { const s = Math.sin(Math.PI * clamp(u)) * (dies ? 1 - smooth(0.4, 0.55, u) : 1), cx = P.via[0], cy = P.via[1], Rm = (P.via[2] + lerp(sa.w, sb.w, gk) / 2 + 18) * Math.min(1, 1.6 * s);
356
+ const mid = pts[NM >> 1], sh = 1 - 0.55 * s; pts = pts.map(p => { let q = [lerp(mid[0], p[0], sh), lerp(mid[1], p[1], sh)]; const dx = q[0] - cx, dy = q[1] - cy, d = Math.hypot(dx, dy) || 1;
357
+ return d < Rm ? [cx + dx / d * Rm, cy + dy / d * Rm] : q; }); }
358
+ return { pts, w: lerp(sa.w, sb.w, gk) * wk }; });
359
+ const sA = A.shade, sB = B.shade;
360
+ shade = u < 0.5 ? { pts: lerpP(sA.pts, rep(centroid(sA.pts), NS), clamp(gA / 0.5)), on: sA.on } : { pts: lerpP(rep(centroid(sB.pts), NS), sB.pts, clamp((gB - 0.5) / 0.5)), on: sB.on };
361
+ return { body, holes, strokes, shade };
362
+ }
363
+
364
+ function heroAt(t) {
365
+ const L = cues.land;
366
+ // ---- intro: falling drop -> mug
367
+ if (t < drop.t1) {
368
+ const ic = I.cup, st = state('cup', t), tl = L.cup;
369
+ const body = t < 0.36 ? drop.Ca : lerpP(drop.Ca, st.body, EM((t - 0.36) / 0.26));
370
+ const tt = Math.min(t, tl), y = ic.pos[1] - 700 * tl - 1600 * tl * tl + 700 * tt + 1600 * tt * tt; // gravity, lands at tl
371
+ const k = 1 + 0.35 * (1 - smooth(tl - 2 * FR, tl + FR, t));
372
+ const im = impact(t, 'cup');
373
+ return { ...st, body, fill: C.ink, strokeC: C.ink, shadeC: ic.shadeC, shade: { pts: lerpP(rep(centroid(st.shade.pts), NS), st.shade.pts, EM(clamp((t - 0.36) / 0.26))), on: t > 0.4 }, M: makeM([ic.pos[0], y], 0, ic.scale, Math.PI / 2, k, im.q, [0, 135]) };
374
+ }
375
+ // ---- flight 5.6 -> 6.4 (plane, then plane -> capsule -> pin in the dive)
376
+ if (t >= FL.t0 && t < FL.t1) {
377
+ const u = flightU(t), pa = pathAt(u), u2 = flightU(t + 0.004), pb = pathAt(u2);
378
+ const speed = Math.hypot(pb.p[0] - pa.p[0], pb.p[1] - pa.p[1]) / 0.004;
379
+ const m = cues.morphs.find(x => x.key === 'plane>pin'); const mu = clamp((t - m.t0) / (m.t1 - m.t0)), g = EM(mu);
380
+ const A = state('plane', t), B = state('pin', t);
381
+ const parts = t < m.t0 ? A : blendParts(A, B, mu, m.P);
382
+ const rot = mu < 0.5 ? pa.ang + 35 * deg : pa.ang - 90 * deg;
383
+ // anticipation: pull back + squash along the heading (6 f), hold 3 f, release on the beat
384
+ const ant = t < FL.launch ? EIO(clamp((t - FL.t0 - 0.02) / 0.2)) : 1 - EO(clamp((t - FL.launch) / 0.1));
385
+ const hang = Math.sin(Math.PI * clamp((t - FL.apex + 0.01) / 0.15)); // the stall at the top: squash
386
+ const k = 1 - 0.16 * ant + 0.13 * clamp(speed / 2600) - 0.1 * hang + 0.08 * Math.sin(Math.PI * g);
387
+ const pos = [pa.p[0] - 34 * ant * (t < FL.launch ? 1 : 0.6), pa.p[1]];
388
+ const im = impact(t, 'pin');
389
+ return { ...parts, fill: C.cream, sweep: { to: C.verm, p: clamp(clamp((qT(t) - m.t0) / (m.t1 - m.t0)) / 0.45), dir: pa.ang }, strokeC: C.cream, shadeC: mu < 0.5 ? I.plane.shadeC : I.pin.shadeC,
390
+ M: makeM(pos, rot, lerp(I.plane.scale, I.pin.scale, g), pa.ang, k, mu > 0.5 ? im.q : 0, [0, 190]) };
391
+ }
392
+ // ---- morphs
393
+ const m = cues.morphs.find(x => x.P && x.key !== 'plane>pin' && t >= x.t0 && t < x.t1);
394
+ if (m) {
395
+ const u = clamp((t - m.t0) / (m.t1 - m.t0)), g = EM(u), A = state(m.a, t), B = state(m.b, t), IA = I[m.a], IB = I[m.b], P = m.P;
396
+ const parts = blendParts(A, B, u, P);
397
+ const ctrl = [(IA.pos[0] + IB.pos[0]) / 2 + P.ctrl[0], (IA.pos[1] + IB.pos[1]) / 2 + P.ctrl[1]];
398
+ const pos = qbez(IA.pos, ctrl, IB.pos, g), p2 = qbez(IA.pos, ctrl, IB.pos, Math.min(1, g + 0.02)), p1 = qbez(IA.pos, ctrl, IB.pos, Math.max(0, g - 0.02));
399
+ const phi = Math.hypot(p2[0] - p1[0], p2[1] - p1[1]) > 0.5 ? Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) : Math.PI / 2;
400
+ const k = 1 + P.sq * Math.sin(Math.PI * g);
401
+ const imB = impact(t, m.b), imA = impact(t, m.a), fc = smooth(0.4, 0.6, u);
402
+ const im = u > 0.5 ? imB : imA, piv = u > 0.5 ? IB.piv : IA.piv;
403
+ const swp = IA.fill !== IB.fill ? clamp((clamp((qT(t) - m.t0) / (m.t1 - m.t0)) - (P.sw ? P.sw[0] : 0.08)) / (P.sw ? P.sw[1] - P.sw[0] : 0.37)) : 0;
404
+ if (V5 && m.key === 'heart>m' && swp > 0) parts.shade = { ...parts.shade, on: false }; // v5.1: the facet fades to cream, then goes with the ink wipe
405
+ return { ...parts, fill: IA.fill, sweep: IA.fill !== IB.fill ? { to: IB.fill, p: swp, dir: phi } : null, strokeC: mixC(IA.fill, IB.fill, smooth(0.47, 0.53, u)),
406
+ shadeC: V5 && m.key === 'heart>m' ? mixC(IA.shadeC, IA.fill, smooth(0, 0.04, u)) : u < 0.5 ? (IA.shadeC || IB.shadeC) : (IB.shadeC || IA.shadeC),
407
+ clipY: heroClip(t),
408
+ M: makeM(pos, lerp(IA.rot0 || 0, IB.rot0 || 0, g) + P.rot * deg * Math.sin(Math.PI * g), lerp(IA.scale, IB.scale, g) * (1 + im.uni), phi, k, im.q, piv) };
409
+ }
410
+ // ---- holds
411
+ const order = ['cup', 'sun', 'sunset', 'gull', 'plane', 'pin', 'heart', 'm'];
412
+ let cur = 'cup'; for (const n of order) if (t >= L[n]) cur = n;
413
+ const ic = I[cur], st = state(cur, t), im = impact(t, cur);
414
+ let pos = ic.pos;
415
+ if (cur === 'gull') pos = [pos[0], pos[1] - 10 * Math.sin(2 * Math.PI * (t - 4.2) / 0.4)];
416
+ let rot = 0;
417
+ if (cur === 'pin') { const τ = t - L.pin; rot = 6 * deg * Math.sin(2 * Math.PI * τ / 0.42) * Math.exp(-τ * 3.2) * smooth(0, 0.06, τ); // the stake wobbles about its tip
418
+ const pv = ic.piv, c = Math.cos(rot), s = Math.sin(rot); pos = [pos[0] + ic.scale * (pv[0] - (c * pv[0] - s * pv[1])), pos[1] + ic.scale * (pv[1] - (s * pv[0] + c * pv[1]))]; }
419
+ // holds breathe with the music: a small kick-like swell on every quarter note (arp accents), none on the logo
420
+ const bq = ((t % 0.4) + 0.4) % 0.4, pulse = t < 8.0 ? 0.02 * Math.exp(-bq * 11) * (1 - Math.exp(-bq * 60)) * 1.25 : 0;
421
+ return { ...st, fill: ic.fill, strokeC: ic.fill, shadeC: ic.shadeC, clipY: heroClip(t),
422
+ M: makeM(pos, rot, ic.scale * (1 + im.uni + pulse), Math.PI / 2, 1, im.q, ic.piv) };
423
+ }
424
+
425
+ // ---------------------------------------------------------------- fields (colour-block wipes) + per-field decor
426
+ const WP = cues.wipes;
427
+ const _org = {}; const org = k => _org[k] || (_org[k] = heroAt(WP[k]).M.b.slice());
428
+ const FEATHER = 0.4 * 0.5 / FPS; // analytic motion blur for moving field edges: radial speed x shutter (x0.7, the harness samples add the rest)
429
+ const farR = o => Math.max(...[[-160, -160], [W + 160, -160], [-160, H + 160], [W + 160, H + 160]].map(c => Math.hypot(c[0] - o[0], c[1] - o[1])));
430
+ function circleReg(t, t0, t1, o, r0 = 40, ease = EB) { if (t < t0) return null; if (t >= t1) return 'full';
431
+ const rmax = farR(o), rr = x => lerp(r0, rmax, ease(clamp((x - t0) / (t1 - t0))));
432
+ return { circle: [o[0], o[1], rr(t)], fw: Math.min(60, Math.abs(rr(t + 0.004) - rr(t - 0.004)) / 0.008 * FEATHER) }; }
433
+ function shockReg(t) { const t0 = cues.shock, T = 0.55, tc = 0.045; if (t < t0) return null; if (t >= t0 + T) return 'full'; // 14 px contact ring 1 f early, ~52 % radius ON the hit
434
+ const o = [960, LP.ground], rmax = farR(o), rr = x => { const τ = clamp(x - t0, 0, T); return lerp(14, rmax, (1 - Math.exp(-τ / tc)) / (1 - Math.exp(-T / tc))); };
435
+ return { circle: [o[0], o[1], rr(t)], fw: Math.min(60, Math.abs(rr(t + 0.003) - rr(Math.max(t0, t - 0.003))) / (t + 0.003 - Math.max(t0, t - 0.003)) * FEATHER) }; }
436
+ function seaTop(t) { if (t < WP.flood) return seaSurface(t); return lerp(560, -80, EIO(clamp((t - WP.flood) / 0.42))); }
437
+ const fields = [
438
+ { c: C.cream, ink: C.ink, reg: () => 'full' },
439
+ { c: C.mari, ink: C.ink, reg: t => circleReg(t, WP.sun, WP.sun + 0.6, org('sun'), 50) },
440
+ { c: C.verm, ink: C.cream, reg: t => circleReg(t, WP.sky, WP.sky + 0.6, org('sky'), 110) },
441
+ { c: C.ultra, ink: C.cream, reg: t => { const y = seaTop(t); if (y >= H + 60) return null; if (y <= -60) return 'full';
442
+ return { rect: [-400, y, W + 800, H - y + 400], fw: Math.min(60, Math.abs(seaTop(t + 0.004) - seaTop(t - 0.004)) / 0.008 * FEATHER) }; } },
443
+ { c: C.cream, ink: C.ink, reg: shockReg },
444
+ { c: C.verm, ink: C.cream, reg: t => circleReg(t, WP.heart, WP.heart + 0.55, org('heart'), 98) },
445
+ V5 ? { c: C.cream, ink: C.ink, reg: t => circleReg(t, WP.ink, WP.ink + 0.28, org('ink'), 60) } // v5.1: the end is on the cream paper (full frame by 8.30)
446
+ : { c: C.ink, ink: C.cream, reg: t => circleReg(t, WP.ink, WP.ink + 0.6, org('ink'), 60) },
447
+ ];
448
+ const WORDS = ['早安', '日出', '日落', '海风', '远行', '抵达', '心动'];
449
+ const EN = ['GOOD MORNING', 'SUNRISE', 'SUNSET', 'SEA BREEZE', 'TAKE OFF', 'ARRIVAL', 'LOVE'];
450
+ function drawUI(ink, t) {
451
+ const a = 1 - smooth(8.0, 8.3, t); if (a <= 0) return;
452
+ ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.fillStyle = ink; ctx.strokeStyle = ink; ctx.textBaseline = 'alphabetic';
453
+ const tc = cues.captions; let i = 0; for (let k = 0; k < tc.length; k++) if (t >= tc[k]) i = k;
454
+ const p = i === 0 ? clamp((t - 0.05) / 0.36) : clamp((t - tc[i]) / 0.36), e = EM(p);
455
+ const x = 96, y = 1002;
456
+ ctx.save(); ctx.beginPath(); ctx.rect(x - 6, y - 44, 720, 58); ctx.clip();
457
+ const draw = (k, dy, al) => { if (al <= 0.001) return; ctx.globalAlpha = a * al;
458
+ ctx.font = '500 19px "IBM Plex Mono"'; ctx.letterSpacing = '1px'; ctx.fillText(String(k + 1).padStart(2, '0'), x, y + dy - 3);
459
+ ctx.globalAlpha = a * al * 0.45; ctx.fillText('/07', x + 26, y + dy - 3); ctx.globalAlpha = a * al;
460
+ ctx.font = '500 34px "HarmonyOS Sans SC"'; ctx.letterSpacing = '4px'; ctx.fillText(WORDS[k], x + 104, y + dy);
461
+ const w = ctx.measureText(WORDS[k]).width; ctx.font = '500 14px "IBM Plex Mono"'; ctx.letterSpacing = '2.5px'; ctx.globalAlpha = a * al * 0.6; ctx.fillText(EN[k], x + 104 + w + 16, y + dy - 3); };
462
+ if (i === 0) draw(0, 46 * (1 - e), e); else if (p < 1) { draw(i - 1, -46 * e, 1 - e); draw(i, 46 * (1 - e), e); } else draw(i, 0, 1);
463
+ ctx.restore();
464
+ // progress: 7 dots, the active one stretches into a capsule
465
+ if (t < 8.0) { const act = (i === 0 ? 0 : i - 1 + e); ctx.globalAlpha = a; ctx.lineCap = 'round';
466
+ let xr = W - 96; const yd = y - 11;
467
+ for (let k = 6; k >= 0; k--) { const wgt = Math.max(0, 1 - Math.abs(k - act)), len = 22 * EM(wgt), r = 4.5;
468
+ ctx.globalAlpha = a * (0.3 + 0.7 * wgt); ctx.beginPath(); ctx.arc(xr - len, yd, r, Math.PI / 2, 3 * Math.PI / 2); ctx.arc(xr, yd, r, -Math.PI / 2, Math.PI / 2); ctx.closePath(); ctx.fill(); xr -= len + 20; } }
469
+ ctx.restore();
470
+ }
471
+ function drawDecor(ink, t) {
472
+ // dashed route of the plane
473
+ const tqc = qT(t), T1c = 6.4 + 4 * FR, band = tqc > 6.4 + 2 * FR - 1e-4 && tqc - SH / 2 < T1c - 1e-6; // collapse frames 194-196
474
+ if (t >= FL.launch && (t < 6.40 + 4 * FR || band)) { // at contact the loop collapses (expo-in, 4 f) into the pin's ground ring = the 2nd ripple
475
+ const ue = Math.min(flightU(Math.min(t, 6.4)) - 125 / FP.len, 1), us = 0, ce = clamp((t - 6.4) / (4 * FR)) ** 3;
476
+ if (band && ue > us + 0.002) { // s3: the collapse is too fast for sample accumulation (comb of loop copies) -> evaluated at the
477
+ // frame centre and drawn as one analytic smear frame: a trailing smear over the swept region + a crisp leading edge
478
+ const ta = Math.max(6.4, tqc - SH / 2), tb = Math.min(T1c, tqc + SH / 2), wgt = (tb - ta) / SH;
479
+ const CE = x => clamp((x - 6.4) / (4 * FR)) ** 3, c0 = CE(ta), c1 = CE(tb);
480
+ const P = (v, c) => { const p = pathAt(lerp(us, ue, v)).p, a = Math.PI - 2 * Math.PI * v; return [lerp(p[0], 960 + 24 * Math.cos(a), c), lerp(p[1], 642 + 24 * 0.24 * Math.sin(a), c)]; };
481
+ const n = 400, A = [], B = []; let sw = 0, sx = 0;
482
+ for (let i = 0; i <= n; i++) { A.push(P(i / n, c0)); B.push(P(i / n, c1)); const d = Math.hypot(B[i][0] - A[i][0], B[i][1] - A[i][1]); sw += d / (n + 1); sx = Math.max(sx, d); }
483
+ const lw = lerp(3, 5, c1), N = Math.min(96, Math.ceil(sx / (0.5 * lw))), Ak = Math.min(0.3, 0.3 * (sw / N) / lw);
484
+ ctx.save();
485
+ if (t > 6.1 && !HIDE_DOT) { const h = heroAt(t), Am = h.M.A, bm = h.M.b, T = p => [Am[0] * p[0] + Am[1] * p[1] + bm[0], Am[2] * p[0] + Am[3] * p[1] + bm[1]];
486
+ const hp = h.holes[0].pts.map(T), cc = centroid(hp); let R = 0; for (const q of hp) R = Math.max(R, Math.hypot(q[0] - cc[0], q[1] - cc[1]));
487
+ if (R > 2) { ctx.beginPath(); ctx.rect(-400, -400, W + 800, H + 800); ctx.arc(cc[0], cc[1], R + 8, 0, 2 * Math.PI, true); ctx.clip(); } }
488
+ // trailing smear: N analytic sub-samples of the curve across the shutter, density ramping up towards the leading edge
489
+ ctx.strokeStyle = ink; ctx.lineWidth = lw; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.setLineDash([]);
490
+ for (let k = 1; k < N; k++) { const f = k / N; ctx.globalAlpha = wgt * Ak * f * f; ctx.beginPath();
491
+ for (let i = 0; i <= n; i += 2) { const q = [lerp(A[i][0], B[i][0], f), lerp(A[i][1], B[i][1], f)]; i ? ctx.lineTo(q[0], q[1]) : ctx.moveTo(q[0], q[1]); }
492
+ ctx.stroke(); }
493
+ ctx.strokeStyle = ink; ctx.lineWidth = lw; ctx.lineCap = 'butt'; ctx.globalAlpha = wgt * 0.92; ctx.setLineDash(c1 > 0.5 ? [] : [18, 13]);
494
+ ctx.beginPath(); B.forEach((p, i) => i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])); ctx.stroke(); ctx.restore();
495
+ } else if (ue > us + 0.002) { ctx.save();
496
+ if (t > 6.1 && !HIDE_DOT) { // keep the route out of the pin's eye: clip away the hero hole (+ margin)
497
+ const h = heroAt(t), A = h.M.A, b = h.M.b, T = p => [A[0] * p[0] + A[1] * p[1] + b[0], A[2] * p[0] + A[3] * p[1] + b[1]];
498
+ const hp = h.holes[0].pts.map(T), cc = centroid(hp); let R = 0; for (const q of hp) R = Math.max(R, Math.hypot(q[0] - cc[0], q[1] - cc[1]));
499
+ if (R > 2) { ctx.beginPath(); ctx.rect(-400, -400, W + 800, H + 800); ctx.arc(cc[0], cc[1], R + 8, 0, 2 * Math.PI, true); ctx.clip(); } }
500
+ const tq = qT(t), whip = tq > 6.16 && tq < 6.30; // in the whip a dashed line accumulates into a barcode: continuous + 50 %
501
+ ctx.strokeStyle = ink; ctx.globalAlpha = whip ? 0.5 : 0.92; ctx.lineWidth = lerp(3, 5, ce); ctx.lineCap = 'butt'; ctx.setLineDash(whip || ce > 0.5 ? [] : [18, 13]);
502
+ ctx.beginPath(); const n = 400; for (let i = 0; i <= n; i++) { const v = i / n; let p = pathAt(lerp(us, ue, v)).p;
503
+ if (ce > 0) { const a = Math.PI - 2 * Math.PI * v; p = [lerp(p[0], 960 + 24 * Math.cos(a), ce), lerp(p[1], 642 + 24 * 0.24 * Math.sin(a), ce)]; }
504
+ i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]); } ctx.stroke(); ctx.restore(); }
505
+ }
506
+ // impact rings + contact shadow under the pin
507
+ if (t >= 6.38 && t < 7.4) { ctx.save(); ctx.strokeStyle = ink;
508
+ for (let j = 0; j < 3; j++) { const τ = t - [6.4, 6.4 + 4 * FR, 6.64][j]; if (τ < 0 || τ > 0.85) continue; const e = EO(τ / 0.85);
509
+ ctx.globalAlpha = 0.5 * (1 - τ / 0.85); ctx.lineWidth = 4.5 * (1 - e) + 1; ctx.beginPath(); ctx.ellipse(960, 642, 24 + 360 * e, (24 + 360 * e) * 0.24, 0, 0, 2 * Math.PI); ctx.stroke(); }
510
+ ctx.restore(); }
511
+ if (t >= 6.36 && t < 7.2 && !HIDE_DOT) { const hop = t > 7.0 ? clamp((t - 7.0) / 0.12) : 0; ctx.save(); ctx.fillStyle = ink; ctx.globalAlpha = 0.14 * smooth(6.36, 6.44, t) * (1 - hop);
512
+ ctx.beginPath(); ctx.ellipse(960, 644, 62 * (1 - 0.4 * hop), 11, 0, 0, 2 * Math.PI); ctx.fill(); ctx.restore(); }
513
+ drawUI(ink, t);
514
+ }
515
+ function drawFields(t) {
516
+ let start = 0; for (let i = fields.length - 1; i >= 0; i--) if (fields[i].reg(t) === 'full') { start = i; break; }
517
+ for (let i = start; i < fields.length; i++) { const r = fields[i].reg(t); if (!r) continue; ctx.save();
518
+ if (r !== 'full') { ctx.beginPath(); if (r.circle) ctx.arc(r.circle[0], r.circle[1], r.circle[2], 0, 2 * Math.PI); else ctx.rect(...r.rect); ctx.clip(); }
519
+ if (r !== 'full' && r.fw > 1.5) { ctx.restore(); drawSoftField(fields[i], r, t); continue; }
520
+ ctx.fillStyle = fields[i].c; ctx.fillRect(-400, -400, W + 800, H + 800); drawDecor(fields[i].ink, t); ctx.restore(); }
521
+ }
522
+ function drawSoftField(f, r, t) { // field + its decor drawn offscreen, masked by a speed-wide feathered edge
523
+ const main = ctx; ctx = octx; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; ctx.clearRect(0, 0, W, H);
524
+ applyCam(ctx, CAM); ctx.fillStyle = f.c; ctx.fillRect(-400, -400, W + 800, H + 800); drawDecor(f.ink, t);
525
+ applyCam(ctx, CAM); ctx.globalCompositeOperation = 'destination-in'; let g;
526
+ if (r.circle) { const [x, y, rr] = r.circle; g = ctx.createRadialGradient(x, y, Math.max(0, rr - r.fw / 2), x, y, rr + r.fw / 2); g.addColorStop(0, '#000'); g.addColorStop(1, 'rgba(0,0,0,0)'); }
527
+ else { const y = r.rect[1]; g = ctx.createLinearGradient(0, y - r.fw / 2, 0, y + r.fw / 2); g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, '#000'); }
528
+ ctx.fillStyle = g; ctx.fillRect(-400, -400, W + 800, H + 800); ctx.globalCompositeOperation = 'source-over';
529
+ ctx = main; ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.drawImage(off, 0, 0); ctx.restore(); }
530
+
531
+ // ---------------------------------------------------------------- camera: world drift + hero push to the loop apex + whip + impact shake
532
+ let CAM = null;
533
+ function camAt(t) {
534
+ const dA = 1 - smooth(8.3, 9.0, t), rot = dA * 0.5 * deg * Math.sin(2 * Math.PI * t / 6.4 + 0.6);
535
+ const zx = clamp((t - 8.5) / 1.9); let z = 1.018 + dA * 0.012 * Math.sin(2 * Math.PI * t / 3.2 + 2.2) + 0.036 * (1 - (1 - zx) * (1 - zx));
536
+ const push = EIO(clamp((t - 5.78) / 0.28)), rel = EIO(clamp((t - 6.16) / 0.14));
537
+ const hz = (0.1 * push + 0.012 * smooth(6.04, 6.16, t)) * (1 - rel);
538
+ const apex = [LP.cx, LP.cy - LP.r], tip = [960, LP.ground], A = [lerp(apex[0], tip[0], rel), lerp(apex[1], tip[1], rel)];
539
+ let dx = 0, dy = 110 * push * (1 - rel);
540
+ for (const [ti, amp] of [[6.4, 13], [0.4, 5], [7.6, 5]]) { const τ = t - ti; if (τ > 0 && τ < 0.5) dy += amp * Math.sin(2 * Math.PI * τ / 0.11) * Math.exp(-τ * 14); }
541
+ const Z = z * (1 + hz), c = Math.cos(rot), s = Math.sin(rot), vx = -hz * A[0] - W / 2, vy = -hz * A[1] - H / 2;
542
+ return { a: Z * c, b: Z * s, c: -Z * s, d: Z * c, e: z * (c * vx - s * vy) + W / 2 + dx, f: z * (s * vx + c * vy) + H / 2 + dy };
543
+ }
544
+ const applyCam = (c, k) => c.setTransform(k.a, k.b, k.c, k.d, k.e, k.f);
545
+
546
+ // ---------------------------------------------------------------- drawing the hero
547
+ function polyPath(path, P) { path.moveTo(P[0][0], P[0][1]); for (let i = 1; i < P.length; i++) path.lineTo(P[i][0], P[i][1]); path.closePath(); }
548
+ function fillShape(parts, M, fill, shadeC, clipY, sweep) {
549
+ const { A, b } = M, T = p => [A[0] * p[0] + A[1] * p[1] + b[0], A[2] * p[0] + A[3] * p[1] + b[1]];
550
+ const body = parts.body.map(T); let y0 = 1e9, y1 = -1e9; for (const p of body) { if (p[1] < y0) y0 = p[1]; if (p[1] > y1) y1 = p[1]; }
551
+ const path = new Path2D(); polyPath(path, body); for (const h of parts.holes) polyPath(path, h.pts.map(T));
552
+ ctx.save(); if (clipY != null) { ctx.beginPath(); ctx.rect(-400, -400, W + 800, clipY + 400); ctx.clip(); }
553
+ const gr = ctx.createLinearGradient(0, y0, 0, y1); const gk = gradK; gr.addColorStop(0, tone(fill, 0.028 * gk)); gr.addColorStop(1, tone(fill, -0.022 * gk));
554
+ ctx.fillStyle = gr; ctx.fill(path, 'evenodd');
555
+ if (sweep && sweep.p > 0) { const c = b; let R = 0; for (const q of body) R = Math.max(R, Math.hypot(q[0] - c[0], q[1] - c[1]));
556
+ ctx.save(); ctx.clip(path, 'evenodd'); const g2 = ctx.createLinearGradient(0, y0, 0, y1); g2.addColorStop(0, tone(sweep.to, 0.028 * gradK)); g2.addColorStop(1, tone(sweep.to, -0.022 * gradK));
557
+ ctx.fillStyle = g2; ctx.beginPath();
558
+ if (sweep.dir == null) ctx.arc(c[0], c[1], R * 1.03 * EIO(sweep.p), 0, 2 * Math.PI);
559
+ else { const nx = Math.cos(sweep.dir), ny = Math.sin(sweep.dir); let p0 = 1e9, p1 = -1e9; for (const q of body) { const d = q[0] * nx + q[1] * ny; if (d < p0) p0 = d; if (d > p1) p1 = d; }
560
+ const e = p1 - (p1 - p0 + 4) * EIO(sweep.p), ox = nx * e, oy = ny * e, tx = -ny * 5000, ty = nx * 5000;
561
+ ctx.moveTo(ox - tx, oy - ty); ctx.lineTo(ox + tx, oy + ty); ctx.lineTo(ox + tx + nx * 5000, oy + ty + ny * 5000); ctx.lineTo(ox - tx + nx * 5000, oy - ty + ny * 5000); ctx.closePath(); }
562
+ ctx.fill(); ctx.restore(); }
563
+ if (parts.shade && parts.shade.on) { const sp = new Path2D(); polyPath(sp, parts.shade.pts.map(T)); ctx.save(); ctx.clip(path, 'evenodd'); ctx.fillStyle = shadeC; ctx.fill(sp); ctx.restore(); }
564
+ ctx.restore();
565
+ return T;
566
+ }
567
+ let gradK = 1;
568
+ function drawHero(t) {
569
+ if (V5 && t >= TV.t0) { gradK = 0; drawTitleV5(t); return null; }
570
+ if (t >= MB.t0 && t < MB.t1) { gradK = 0; drawMBuild(t); return null; }
571
+ gradK = 1 - smooth(8.1, 8.5, t); const h = heroAt(t);
572
+ const T = HIDE_DOT ? (p => [h.M.A[0] * p[0] + h.M.A[1] * p[1] + h.M.b[0], h.M.A[2] * p[0] + h.M.A[3] * p[1] + h.M.b[1]]) // v5 nolink=dot: the hero body is not drawn (its strokes stay)
573
+ : fillShape(h, h.M, h.fill, h.shadeC, h.clipY, h.sweep);
574
+ const sc = Math.sqrt(Math.abs(h.M.A[0] * h.M.A[3] - h.M.A[1] * h.M.A[2]));
575
+ ctx.save(); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = h.strokeC;
576
+ for (const s of h.strokes) { const w = s.w * sc; if (w < 0.6) continue; const Q = s.pts.map(T); let len = 0; for (let i = 1; i < Q.length; i++) len += Math.hypot(Q[i][0] - Q[i - 1][0], Q[i][1] - Q[i - 1][1]);
577
+ const al = smooth(0.7, 1.0, len / w); if (al <= 0.01) continue; // a slot shorter than its width is only a round-cap speck
578
+ ctx.globalAlpha = al; ctx.lineWidth = w; ctx.beginPath(); Q.forEach((q, i) => i ? ctx.lineTo(q[0], q[1]) : ctx.moveTo(q[0], q[1])); ctx.stroke(); }
579
+ ctx.restore();
580
+ return h;
581
+ }
582
+
583
+ // ---------------------------------------------------------------- letters built from the film's two primitives (ring + capsule)
584
+ // geometry measured from the Outfit outlines at this layout (screen px); the true outline takes over in the last 2 f
585
+ const LG = {
586
+ o: { c: [783.8, 505.5], Ro: 64, Ri: 31.5 },
587
+ r: { c: [926, 500], Ro: 57, Ri: 27, arc: 'r', stems: [[864, 897, 441, 568, 500]] },
588
+ p: { c: [1029.3, 505.6], Ro: 63.5, Ri: 31, stems: [[968, 1000, 441, 618.5, 505]] },
589
+ h: { c: [1167, 499], Ro: 57, Ri: 24, arc: 'arch', stems: [[1110, 1143, 383, 568, 499], [1191, 1224, 499, 568, 499]] },
590
+ e: { c: [1298.9, 505.5], Ro: 62, Ri: 33, arc: 'e', bar: [1262, 1366, 488, 522] },
591
+ };
592
+ function ringPath(p, c, Ro, Ri, a0, a1) { // annular sector a0 -> a1 (clockwise on screen); full ring when it spans 2π
593
+ if (a1 - a0 >= 2 * Math.PI - 1e-4) { p.moveTo(c[0] + Ro, c[1]); p.arc(c[0], c[1], Ro, 0, 2 * Math.PI); p.closePath();
594
+ if (Ri > 0.3) { p.moveTo(c[0] + Ri, c[1]); p.arc(c[0], c[1], Ri, 2 * Math.PI, 0, true); p.closePath(); } return p; }
595
+ p.moveTo(c[0] + Ro * Math.cos(a0), c[1] + Ro * Math.sin(a0)); p.arc(c[0], c[1], Ro, a0, a1);
596
+ p.lineTo(c[0] + Ri * Math.cos(a1), c[1] + Ri * Math.sin(a1)); p.arc(c[0], c[1], Math.max(0, Ri), a1, a0, true); p.closePath(); return p; }
597
+ function capMask(x0, x1, top, bot) { const m = new Path2D(), rr = (x1 - x0) / 2 + 7; m.roundRect(x0 - 7, top - rr, x1 - x0 + 14, Math.max(0, bot - top) + 2 * rr, rr); return m; }
598
+ const _gp = {};
599
+ function glyphP(l) { if (_gp[l.ch]) return _gp[l.ch]; const p = new Path2D(); polyPath(p, l.outer); for (const h of l.holes) polyPath(p, h); return (_gp[l.ch] = p); }
600
+ function fillChannel(rings, cens, ri, ro, cu) { // open a ring's lower half with a channel as wide as the counter, rising from below: never a slit
601
+ const cl = new Path2D(); cl.rect(-400, -400, W + 800, H + 800); if (cu > 0) for (const c of cens) cl.rect(c[0] - ri, lerp(c[1] + ro + 1, c[1], cu), 2 * ri, H);
602
+ ctx.save(); if (cu > 0) ctx.clip(cl, 'evenodd'); for (const r of rings) ctx.fill(r); ctx.restore();
603
+ }
604
+ function smearCap(c, pA, pB, r, col) { // analytic box-shutter blur of a disc moving pA -> pB while the shutter is open: capsule + linear alpha ramps
605
+ const dx = pB[0] - pA[0], dy = pB[1] - pA[1], L = Math.hypot(dx, dy), mx = (pA[0] + pB[0]) / 2, my = (pA[1] + pB[1]) / 2;
606
+ if (L < 1.5) { c.fillStyle = col; c.beginPath(); c.arc(mx, my, r, 0, 2 * Math.PI); c.fill(); return; }
607
+ const [R0, G0, B0] = hexRgb(col).map(v => Math.round(v * 255)), cs = a => `rgba(${R0},${G0},${B0},${a})`, ux = dx / L, uy = dy / L, T = L + 2 * r, am = Math.min(1, 2 * r / L), o1 = Math.min(2 * r, L) / T, an = Math.atan2(dy, dx);
608
+ const gr = c.createLinearGradient(mx - ux * T / 2, my - uy * T / 2, mx + ux * T / 2, my + uy * T / 2);
609
+ gr.addColorStop(0, cs(0)); gr.addColorStop(o1, cs(am)); gr.addColorStop(1 - o1, cs(am)); gr.addColorStop(1, cs(0));
610
+ c.fillStyle = gr; c.beginPath(); c.arc(pA[0], pA[1], r, an + Math.PI / 2, an + 3 * Math.PI / 2); c.arc(pB[0], pB[1], r, an - Math.PI / 2, an + Math.PI / 2); c.closePath(); c.fill();
611
+ }
612
+ function drawStems(stems, s, gp) { for (const [x0, x1, y0, y1, fr] of stems || []) { ctx.save(); ctx.clip(capMask(x0, x1, lerp(fr, y0, s), lerp(fr, y1, s))); ctx.fill(gp, 'evenodd'); ctx.restore(); } }
613
+ // "m": the hero circle divides like a cell into two rings sharing a wall, their lower halves open into arches, three legs extrude
614
+ const MB = { t0: 8.30, t1: 8.6, arches: [[571.5, 497], [650, 497]], Ro: 55.5, Ri: 22.5,
615
+ legs: [[515.4, 548.5, 441, 568, 497], [594, 627, 497, 568, 497], [673, 705.6, 497, 568, 497]] };
616
+ let MB_R0 = null;
617
+ const worldBody = h => { const A = h.M.A, b = h.M.b; return h.body.map(p => [A[0] * p[0] + A[1] * p[1] + b[0], A[2] * p[0] + A[3] * p[1] + b[1]]); };
618
+ function drawMBuild(t) {
619
+ const hm = heroAt(t), A = hm.M.A, mc = centroid(worldBody(hm));
620
+ if (MB_R0 == null) MB_R0 = Math.sqrt(Math.abs(area(worldBody(heroAt(MB.t0)))) / Math.PI);
621
+ const dt = Math.sqrt(Math.abs(A[0] * A[3] - A[1] * A[2])) || 1, cen = WM.letters[0].cen;
622
+ const sp = EIO(clamp((t - MB.t0) / 0.15)), R = lerp(MB_R0, MB.Ro, sp), ri = MB.Ri * EO(clamp((t - 8.37) / 0.09));
623
+ const cu = EO(clamp((t - 8.44) / 0.12)), s = EO(clamp((t - 8.40) / 0.14)), xg = qT(t) >= 8.566 ? 1 : 0;
624
+ ctx.save(); ctx.translate(mc[0], mc[1]); ctx.transform(A[0] / dt, A[2] / dt, A[1] / dt, A[3] / dt, 0, 0); ctx.translate(-cen[0], -cen[1]);
625
+ ctx.fillStyle = C.cream;
626
+ const gp = glyphP(WM.letters[0]); if (xg) { ctx.fill(gp, 'evenodd'); ctx.restore(); return; }
627
+ const ac = MB.arches.map(a => [lerp(cen[0], a[0], sp), lerp(cen[1], a[1], sp)]);
628
+ fillChannel(ac.map(c => ringPath(new Path2D(), c, R, ri, 0, 2 * Math.PI)), ac, MB.Ri, R, cu);
629
+ drawStems(MB.legs, s, gp);
630
+ ctx.restore();
631
+ }
632
+ // ---------------------------------------------------------------- end card: the seven chapter dots fly into the full stop
633
+ const DOTC = [C.cream, C.mari, C.verm, C.ultra, I.plane.shadeC, C.cream, C.verm].map(c => V5 && c === C.cream ? C.ink : c); // each chapter's field colour (v5.1: cream -> ink on the cream end card)
634
+ const ED = { t0: 8.44, st: 0.04, dur: 0.70 };
635
+ const edArr = k => ED.t0 + ED.st * k + ED.dur; // arrivals 9.14 .. 9.38, in chapter order
636
+ const EDK = [[0, 0], [0.36, 0.55], [0.8, 0.87], [1, 1]], EDE = bez(0.4, 0, 0.5, 1); // fast rewind along the bottom, slower sweep, rise
637
+ const edF = x => { const y = EDE(clamp(x)); for (let i = 1; i < EDK.length; i++) if (y <= EDK[i][0]) return lerp(EDK[i - 1][1], EDK[i][1], (y - EDK[i - 1][0]) / (EDK[i][0] - EDK[i - 1][0])); return 1; };
638
+ const RT0 = [[1824, 991], [1560, 999], [1200, 1003], [860, 996], [620, 966], [548, 896], [616, 846], [790, 846], [960, 858], [1130, 852], [1300, 830], [1416, 772], [1414, 660]];
639
+ if (V5) { RT0[11] = [TV.stop[0] - 15, 776]; RT0[12] = [TV.stop[0] - 3, 672]; } // v5: rise straight up into the title's full stop, clear of the 画
640
+ function edRoute(E) { // ONE route for all 7: back along the bottom (the day rewinds), one left -> right sweep under the tagline, up into the stop
641
+ const P = [...RT0, E], out = [P[0].slice()], len = [0];
642
+ for (let i = 0; i < P.length - 1; i++) { const p0 = P[Math.max(0, i - 1)], p1 = P[i], p2 = P[i + 1], p3 = P[Math.min(P.length - 1, i + 2)];
643
+ for (let j = 1; j <= 24; j++) { const u = j / 24, u2 = u * u, u3 = u2 * u, o = out[out.length - 1];
644
+ const q = [0, 1].map(d => 0.5 * (2 * p1[d] + (p2[d] - p0[d]) * u + (2 * p0[d] - 5 * p1[d] + 4 * p2[d] - p3[d]) * u2 + (3 * p1[d] - p0[d] - 3 * p2[d] + p3[d]) * u3));
645
+ len.push(len[len.length - 1] + Math.hypot(q[0] - o[0], q[1] - o[1])); out.push(q); } }
646
+ const L = len[len.length - 1];
647
+ return { L, at: s => { s = clamp(s, 0, L); let lo = 0, hi = len.length - 1; while (hi - lo > 1) { const md = (lo + hi) >> 1; if (len[md] < s) lo = md; else hi = md; }
648
+ const f = (s - len[lo]) / (len[hi] - len[lo] || 1); return [lerp(out[lo][0], out[hi][0], f), lerp(out[lo][1], out[hi][1], f)]; } };
649
+ }
650
+ function stopScreen() { const p = new DOMMatrix([CAM.a, CAM.b, CAM.c, CAM.d, CAM.e, CAM.f]).transformPoint(new DOMPoint(WM.dot.cen[0], WM.dot.cen[1])); return [p.x, p.y]; }
651
+ function drawEndDots(t) {
652
+ if (t < 8.0) return;
653
+ const tf = qT(t), E = stopScreen(), yd = 1002 - 11, w6 = 1 - smooth(8.3, 8.6, t), ct = smooth(8.4, 8.75, t);
654
+ const ks = Math.sqrt(Math.abs(CAM.a * CAM.d - CAM.b * CAM.c)), route = tf >= ED.t0 ? edRoute(E) : null;
655
+ ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.lineCap = 'round';
656
+ // arrivals: a thin ring in the dot's colour just outside the stop (the stop itself stays #FF4F2B)
657
+ for (let k = 0; k < 7; k++) { const τ = t - edArr(k); if (τ < 0 || τ > 0.14) continue;
658
+ ctx.globalAlpha = 1 - smooth(0.05, 0.14, τ); ctx.strokeStyle = DOTC[k]; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(E[0], E[1], (26 + 8 * EO(τ / 0.12)) * ks, 0, 2 * Math.PI); ctx.stroke(); }
659
+ if (t > 9.0) { const cl = new Path2D(); cl.rect(0, 0, W, H); cl.arc(E[0], E[1], WM.dot.r * ks * dotBeat(t) - 0.5, 0, 2 * Math.PI); ctx.clip(cl, 'evenodd'); } // the stop absorbs them
660
+ let xr = W - 96;
661
+ for (let k = 6; k >= 0; k--) {
662
+ const wgt = k === 6 ? w6 : 0, len = 22 * EM(wgt), r = 4.5, x = xr - len / 2; xr -= len + 20;
663
+ const τf = tf - (ED.t0 + ED.st * k);
664
+ const paint = (al, col) => { if (al <= 0.002) return; ctx.globalAlpha = al; ctx.strokeStyle = col; };
665
+ if (τf < 0) { const al0 = 0.3 + 0.7 * Math.max(wgt, ct);
666
+ const c0 = V5 && hx(fieldAt(t, [x, yd])) === hx(C.cream) ? C.ink : C.cream; // v5.1: ink over the cream paper
667
+ for (const [al, col] of [[al0 * (1 - ct), c0], [al0 * ct, DOTC[k]]]) { if (al <= 0.002) continue; paint(al, col); ctx.lineWidth = 2 * r; ctx.beginPath(); ctx.moveTo(x - len / 2, yd); ctx.lineTo(x + len / 2 + 0.01, yd); ctx.stroke(); }
668
+ continue; }
669
+ if (τf >= ED.dur) continue;
670
+ const s0 = W - 96 - x, sAt = v => s0 + (route.L - s0) * edF(v / ED.dur), rd = lerp(r, 11, smooth(0, 0.3, τf / ED.dur)); // grows to the 22 px family dot
671
+ ctx.globalAlpha = 1; smearCap(ctx, route.at(sAt(Math.max(0, τf - SH / 2))), route.at(sAt(τf + SH / 2)), rd, DOTC[k]);
672
+ }
673
+ ctx.restore();
674
+ }
675
+ const RINGS = [[9.4, C.mari, 260], [9.43, C.verm, 200], [9.53, C.ultra, 240], [9.56, V5 ? C.ink : C.cream, 160]]; // v5.1: ink replaces cream on the cream end card // the last heartbeat has reach
676
+ function drawEndRings(t) { if (t < 9.35) return; const [cx, cy] = WM.dot.cen; ctx.save();
677
+ for (const [t0, col, R] of RINGS) { const τ = t - t0 + FR; if (τ <= 0 || τ > 0.42) continue; const e = EO(clamp(τ / 0.45));
678
+ ctx.globalAlpha = 1 - smooth(0.08, 0.42, τ); ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(cx, cy, 30 + (R - 30) * e, 0, 2 * Math.PI); ctx.stroke(); }
679
+ ctx.restore(); }
680
+ // ---------------------------------------------------------------- wordmark letters bud out of the hero circle
681
+ const LT = cues.letters;
682
+ const EFLY = bez(0.2, 0.6, 0.3, 1);
683
+ const LANE = [352, 352, 346, 340, 296, 290], _lp = {}; // apex lane (centre y) per letter: clears every glyph already set by >= 12 px
684
+ function letterPath(it) { // bud out of the hero, rise to the lane, drop onto the slot from above-right (never across the previous letter)
685
+ if (_lp[it.k]) return _lp[it.k];
686
+ const ts = LT.t0 + LT.step * it.k, src = heroAt(ts).M.b, T = it.dot ? it.cen : LG[it.ch].c, Y = LANE[it.k];
687
+ const P = [[src[0] + 30, src[1] - 24], [src[0] + 70, Y - 70], [T[0] + 120, Y - 50], T];
688
+ return (_lp[it.k] = s => { const a = 1 - s, w = [a * a * a, 3 * a * a * s, 3 * a * s * s, s * s * s]; return [0, 1].map(j => w[0] * P[0][j] + w[1] * P[1][j] + w[2] * P[2][j] + w[3] * P[3][j]); });
689
+ }
690
+ function drawLetters(t) {
691
+ if (V5 || t < LT.t0 - FR) return;
692
+ const tf = qT(t), items = WM.letters.slice(1).map((l, i) => ({ ...l, k: i })); items.push({ dot: true, k: 5, cen: WM.dot.cen, r: WM.dot.r });
693
+ for (const it of items) {
694
+ // flight + stage are evaluated at the frame centre (all mb samples identical -> no onion skin) and smeared analytically at
695
+ // the full speed x shutter; the cuts/extrusions stay continuous in t so the harness accumulation blurs them
696
+ const ts = LT.t0 + LT.step * it.k, τ = t - ts, τf = tf - ts; if (τf < 0) continue;
697
+ const g = it.dot ? null : LG[it.ch], path = letterPath(it), sAt = x => EFLY(clamp(x / LT.fly));
698
+ const pA = path(sAt(Math.max(0, τf - SH / 2))), pB = path(sAt(τf + SH / 2));
699
+ const R = it.dot ? it.r : g.Ro, rr = lerp(R * 0.22, R, EO(clamp(τf / 0.22))), land = impactSimple(τ - 0.34);
700
+ if (it.dot) { ctx.save(); const br = dotBeat(t), col = mixC(C.cream, C.verm, smooth(0.1, 0.35, τf));
701
+ if (τf < LT.fly) smearCap(ctx, pA, pB, rr * br, col);
702
+ else { const [cx, cy] = it.cen; ctx.translate(cx, cy + rr * land * 0.5); ctx.scale(1 + land, 1 - land); ctx.fillStyle = col; ctx.beginPath(); ctx.arc(0, 0, rr * br, 0, 2 * Math.PI); ctx.fill(); }
703
+ ctx.restore(); continue; }
704
+ ctx.save();
705
+ if (land) { ctx.translate(it.cen[0], WM.yB); ctx.scale(1 + land, 1 - land); ctx.translate(-it.cen[0], -WM.yB); }
706
+ const xg = τf >= 0.34 ? 1 : 0, gp = glyphP(it);
707
+ if (τf < 0.14) smearCap(ctx, pA, pB, rr, C.cream);
708
+ else if (xg < 1) { // ring (counter opens) + capsules extruding from it; arcs cut open where the letter has no bowl
709
+ const [cx, cy] = path(sAt(τ)), dx = cx - g.c[0], dy = cy - g.c[1]; ctx.translate(dx, dy); ctx.fillStyle = C.cream;
710
+ const ri = g.Ri * EO(clamp((τ - 0.14) / 0.1)), s = g.arc === 'arch' ? EO(clamp((τ - 0.13) / 0.15)) : EIO(clamp((τ - 0.15) / 0.16));
711
+ let a0 = 0, a1 = 2 * Math.PI;
712
+ if (g.arc === 'r') { a1 = 300 * deg; a0 = lerp(a1 - 2 * Math.PI, Math.PI, EO(clamp((τ - 0.12) / 0.16))); }
713
+ else if (g.arc === 'e') { a1 = 372 * deg; a0 = lerp(12 * deg, 41 * deg, EO(clamp((τ - 0.2) / 0.12))); }
714
+ const ring = ringPath(new Path2D(), g.c, rr, ri, a0, a1);
715
+ if (g.arc === 'arch') fillChannel([ring], [g.c], g.Ri, rr, EO(clamp((τ - 0.17) / 0.12))); else ctx.fill(ring);
716
+ drawStems(g.stems, s, gp);
717
+ if (g.bar) { const [x0, x1, y0, y1] = g.bar, rb = (y1 - y0) / 2, xe = lerp(x0 + 30, x1, s), mk = new Path2D(); mk.roundRect(x0, y0, xe - x0 + rb, y1 - y0, rb); ctx.save(); ctx.clip(mk); ctx.fill(gp, 'evenodd'); ctx.restore(); }
718
+ ctx.translate(-dx, -dy); }
719
+ if (xg > 0) { ctx.globalAlpha = xg; ctx.fillStyle = C.cream; ctx.fill(gp, 'evenodd'); }
720
+ ctx.restore();
721
+ }
722
+ }
723
+ // ---------------------------------------------------------------- v5 title build (pure in t)
724
+ let TV_H = null;
725
+ const TC = C.ink; // v5.1: the title is ink on the cream paper
726
+ function tvHero() { if (TV_H) return TV_H; const wb = worldBody(heroAt(TV.t0)); return (TV_H = { c: centroid(wb), R: Math.sqrt(Math.abs(area(wb)) / Math.PI) }); }
727
+ const tvSplit = x => EIO(clamp((x - TV.t0) / TV.split));
728
+ function tvCell(it, x) { const h = tvHero(), sp = tvSplit(x); return { c: [lerp(h.c[0], it.cen[0], sp), lerp(h.c[1], it.cen[1], sp)], r: lerp(h.R, TV.rc, sp) }; }
729
+ const tvStart = (k, i) => TV.m0 + k * TV.st + i * TV.cst;
730
+ function tvStop(x) { // the full stop squeezes out of the dividing circle (under the cells), lands 8.46, then lives with the end card's beat
731
+ const h = tvHero(), ss = EIO(clamp((x - TV.t0) / 0.16)), E = TV.stop, land = impactSimple(x - (TV.t0 + 0.16));
732
+ const r = lerp(h.R, TV.stopR, EO(clamp((x - TV.t0) / 0.12))) * (x >= TV.t0 + 0.16 ? dotBeat(x) : 1);
733
+ return { c: [lerp(h.c[0], E[0], ss), lerp(h.c[1], E[1], ss) - 36 * Math.sin(Math.PI * ss)], r, land, fly: x < TV.t0 + 0.16,
734
+ col: mixC(TC, C.verm, smooth(TV.t0 + 0.04, TV.t0 + 0.10, x)) };
735
+ }
736
+ function drawTitleV5(t) {
737
+ const tf = qT(t);
738
+ if (!HIDE_DOT) { // the stop is drawn first: while it is still inside the cells it is hidden by them
739
+ const S = tvStop(t);
740
+ if (S.fly) { const a = tvStop(Math.max(TV.t0, tf - SH / 2)), b = tvStop(tf + SH / 2); smearCap(ctx, a.c, b.c, tvStop(tf).r, tvStop(tf).col); }
741
+ else { ctx.save(); ctx.translate(S.c[0], S.c[1] + S.r * S.land * 0.5); ctx.scale(1 + S.land, 1 - S.land); ctx.fillStyle = S.col; ctx.beginPath(); ctx.arc(0, 0, S.r, 0, 2 * Math.PI); ctx.fill(); ctx.restore(); }
742
+ }
743
+ if (HIDE_TITLE) return;
744
+ ctx.save(); ctx.fillStyle = TC;
745
+ for (const it of TV.items) {
746
+ if (t < tvStart(it.k, 0)) { // a cell in flight: frame-centre time + analytic smear (no onion skin under the harness mb)
747
+ const a = tvCell(it, Math.max(TV.t0, tf - SH / 2)), b = tvCell(it, tf + SH / 2); smearCap(ctx, a.c, b.c, tvCell(it, tf).r, TC); continue; }
748
+ const cell = tvCell(it, t), ks = cell.r / TV.rc;
749
+ it.comps.forEach((cp, i) => {
750
+ const τ = t - tvStart(it.k, i), u = clamp(τ / TV.md), g = EM(u), land = 0.6 * impactSimple(τ - TV.md);
751
+ const st = 1 + 0.1 * Math.sin(Math.PI * g), cx = it.cen[0], by = TV.yB + 22; // bloom: a little vertical stretch mid-morph, landing squash on the baseline
752
+ const X = p => [cx + (p[0] - cx) * (1 + land) / st, by + (p[1] - by) * (1 - land) * st - (st - 1) * (by - it.cen[1])];
753
+ const path = new Path2D();
754
+ if (u >= 1) { polyPath(path, cp.raw.map(X)); for (const h of cp.holes) polyPath(path, h.raw.map(X)); } // exact outline once formed
755
+ else {
756
+ polyPath(path, cp.circle.map((q, n) => X(lerp2([cell.c[0] + q[0] * ks, cell.c[1] + q[1] * ks], cp.outer[n], g))));
757
+ for (const h of cp.holes) { const hc = centroid(h.pts), gh = clamp((g - 0.3) / 0.7); polyPath(path, h.pts.map(q => X([lerp(hc[0], q[0], gh), lerp(hc[1], q[1], gh)]))); }
758
+ }
759
+ ctx.fill(path, 'evenodd');
760
+ });
761
+ }
762
+ ctx.restore();
763
+ }
764
+ const lerp2 = (a, b, t) => [lerp(a[0], b[0], t), lerp(a[1], b[1], t)];
765
+ function dotBeat(t) { let b = 1 + 0.02 * Math.sin(2 * Math.PI * (t - 8.8) / 0.8) * smooth(9.0, 9.3, t);
766
+ for (let k = 0; k < 7; k++) { const τ = t - edArr(k); if (τ > 0 && τ < 0.1) b += 0.06 * Math.sin(Math.PI * τ / 0.1); } // each chapter dot lands in the stop
767
+ for (const [tb, a] of [[9.4, 0.2], [9.53, 0.13]]) { const τ = t - tb + FR; if (τ > 0 && τ < 0.16) b += a * Math.sin(Math.PI * τ / 0.16) * (1 - τ / 0.32); }
768
+ return b; }
769
+ function impactSimple(τ) { if (τ < 0 || τ > 1) return 0; return 0.1 * Math.sin(2 * Math.PI * τ / 0.26) * Math.exp(-τ * 8); }
770
+ function drawTagline(t) {
771
+ if (V5) { drawTaglineV5(t); return; }
772
+ const t0 = cues.tagline; if (t < t0) return;
773
+ ctx.save(); ctx.fillStyle = C.cream; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; ctx.letterSpacing = '0px';
774
+ ctx.font = '500 40px "HarmonyOS Sans SC"'; const TR = 11, GAP = 30, yb = 702, wd = ch => ctx.measureText(ch).width; // 0.28 em tracking, 一形 · 万象 as two words
775
+ const xs = [960 - GAP - wd('形') - TR - wd('一'), 960 - GAP - wd('形'), 960 + GAP, 960 + GAP + wd('万') + TR];
776
+ ['一', '形', '万', '象'].forEach((ch, i) => { const e = EO(clamp((t - t0 - i * 0.05) / 0.45)); ctx.globalAlpha = e * 0.94; ctx.fillText(ch, xs[i], yb + 22 * (1 - e)); });
777
+ const e2 = EO(clamp((t - t0 - 0.12) / 0.4)); ctx.globalAlpha = e2 * 0.95; ctx.fillStyle = C.verm; ctx.beginPath();
778
+ ctx.arc(960, yb - 15 + 22 * (1 - e2), 4.8 * e2 * (1 + 1.6 * (dotBeat(t) - 1 - 0.02 * Math.sin(2 * Math.PI * (t - 8.8) / 0.8) * smooth(9.0, 9.3, t))), 0, 2 * Math.PI); ctx.fill();
779
+ const e3 = EO(clamp((t - t0 - 0.25) / 0.5)); ctx.globalAlpha = e3 * 0.65; ctx.fillStyle = C.cream; ctx.textAlign = 'center'; ctx.font = '500 17px "IBM Plex Mono"'; ctx.letterSpacing = '4px';
780
+ ctx.fillText('ONE SHAPE · EVERY STORY', 962, 758 + 14 * (1 - e3));
781
+ ctx.restore();
782
+ }
783
+ function drawTaglineV5(t) { // v5: the small English line under the title, the tagline's vermilion dot between the words
784
+ const t0 = 8.62; if (t < t0 || HIDE_TITLE) return;
785
+ ctx.save(); ctx.textBaseline = 'alphabetic'; ctx.font = '500 30px "IBM Plex Mono"'; ctx.letterSpacing = '9px';
786
+ const yb = 694, e = EO(clamp((t - t0) / 0.45)), e2 = EO(clamp((t - t0 - 0.1) / 0.4)), dy = 18 * (1 - e);
787
+ ctx.globalAlpha = e * 0.8; ctx.fillStyle = C.ink; ctx.textAlign = 'right'; ctx.fillText('SHAPE', 960 - 22 + 9, yb + dy); // +9: the trailing letter-spacing
788
+ ctx.textAlign = 'left'; ctx.fillText('MORPH', 960 + 22, yb + dy);
789
+ ctx.globalAlpha = e2 * 0.95; ctx.fillStyle = C.verm; ctx.beginPath();
790
+ ctx.arc(960, yb - 11 + 18 * (1 - e2), 5.5 * e2 * (1 + 1.6 * (dotBeat(t) - 1 - 0.02 * Math.sin(2 * Math.PI * (t - 8.8) / 0.8) * smooth(9.0, 9.3, t))), 0, 2 * Math.PI); ctx.fill();
791
+ ctx.restore();
792
+ }
793
+
794
+ // ---------------------------------------------------------------- finishing: vignette + animated grain
795
+ const grain = document.createElement('canvas'); grain.width = grain.height = 256;
796
+ { const g = grain.getContext('2d'), im = g.createImageData(256, 256); let s = 1234567;
797
+ const rnd = () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
798
+ for (let i = 0; i < 256 * 256; i++) { const v = 128 + ((rnd() + rnd() + rnd() - 1.5) * 90) | 0; im.data[i * 4] = im.data[i * 4 + 1] = im.data[i * 4 + 2] = v; im.data[i * 4 + 3] = 255; }
799
+ g.putImageData(im, 0, 0); }
800
+ const GT = 512, GN = new Int8Array(GT * GT);
801
+ { let s = 987654321; const rnd = () => { s = (s * 1664525 + 1013904223) >>> 0; return (s + 0.5) / 4294967296; };
802
+ const a = new Float32Array(GT * GT); for (let i = 0; i < a.length; i++) a[i] = Math.sqrt(-2 * Math.log(rnd())) * Math.cos(2 * Math.PI * rnd());
803
+ const b = new Float32Array(GT * GT); let ss = 0; // soften to ~1.5-2 px grain
804
+ for (let y = 0; y < GT; y++) for (let x = 0; x < GT; x++) { let v = 0; for (let j = -1; j <= 1; j++) for (let i = -1; i <= 1; i++) v += a[((y + j) & (GT - 1)) * GT + ((x + i) & (GT - 1))] * (2 - Math.abs(i)) * (2 - Math.abs(j)); b[y * GT + x] = v; ss += v * v; }
805
+ const k = 2.0 / Math.sqrt(ss / b.length); for (let i = 0; i < b.length; i++) GN[i] = Math.max(-6, Math.min(6, Math.round(b[i] * k))); }
806
+ let PAPER = null;
807
+ { const im = new Image(); im.src = '/assets/textures/paper/Paper001_4K_color.jpg'; await im.decode(); const c = document.createElement('canvas'); c.width = W; c.height = H;
808
+ const g = c.getContext('2d'); g.drawImage(im, 0, 0, 4096, 4096, 0, 0, 2304, 2304); const d = g.getImageData(0, 0, W, H).data, L = new Float32Array(W * H); let m = 0;
809
+ for (let i = 0; i < L.length; i++) { L[i] = d[i * 4] * 0.3 + d[i * 4 + 1] * 0.59 + d[i * 4 + 2] * 0.11; m += L[i]; } m /= L.length; let v = 0; for (let i = 0; i < L.length; i++) { L[i] -= m; v += L[i] * L[i]; }
810
+ const k = 0.022 / Math.sqrt(v / L.length); PAPER = new Float32Array(W * H); for (let i = 0; i < L.length; i++) PAPER[i] = Math.max(-0.06, Math.min(0.06, L[i] * k)) / 765; }
811
+ function finish(f) {
812
+ const img = ctx.getImageData(0, 0, W, H), d = img.data, ox = (f * 197) % GT, oy = (f * 331) % GT;
813
+ for (let y = 0; y < H; y++) { const row = ((y + oy) & (GT - 1)) * GT; let i = y * W * 4, j = y * W;
814
+ for (let x = 0; x < W; x++, i += 4, j++) { const r = d[i], g = d[i + 1], b = d[i + 2], pm = 1 + PAPER[j] * (r + g + b), n = GN[row + ((x + ox) & (GT - 1))];
815
+ d[i] = r * pm + n; d[i + 1] = g * pm + n; d[i + 2] = b * pm + n; } }
816
+ ctx.putImageData(img, 0, 0); }
817
+ const vig = ctx.createRadialGradient(W / 2, H / 2, H * 0.35, W / 2, H / 2, H * 1.05); vig.addColorStop(0, 'rgba(0,0,0,0)'); vig.addColorStop(1, 'rgba(0,0,0,0.13)');
818
+
819
+ // ---------------------------------------------------------------- v5 link elements: window.linkAt(t), pure in t, 1920x1080 output px
820
+ const camPt = (K, p) => [K.a * p[0] + K.c * p[1] + K.e, K.b * p[0] + K.d * p[1] + K.f];
821
+ const hx = c => c.toLowerCase();
822
+ function ellipseOf(P) { // area centroid + second moments -> equivalent ellipse; dev = max normalised radial deviation from it
823
+ let A = 0, cx = 0, cy = 0, sxx = 0, syy = 0, sxy = 0;
824
+ for (let i = 0; i < P.length; i++) { const [x0, y0] = P[i], [x1, y1] = P[(i + 1) % P.length], c = x0 * y1 - x1 * y0;
825
+ A += c; cx += (x0 + x1) * c; cy += (y0 + y1) * c; sxx += (x0 * x0 + x0 * x1 + x1 * x1) * c; syy += (y0 * y0 + y0 * y1 + y1 * y1) * c; sxy += (x0 * y1 + 2 * x0 * y0 + 2 * x1 * y1 + x1 * y0) * c; }
826
+ A /= 2; cx /= 6 * A; cy /= 6 * A;
827
+ const vxx = sxx / 12 / A - cx * cx, vyy = syy / 12 / A - cy * cy, vxy = sxy / 24 / A - cx * cy;
828
+ const tr = vxx + vyy, dt = Math.sqrt(Math.max(0, (vxx - vyy) ** 2 / 4 + vxy * vxy)), l1 = tr / 2 + dt, l2 = Math.max(1e-6, tr / 2 - dt);
829
+ const ang = 0.5 * Math.atan2(2 * vxy, vxx - vyy), ra = 2 * Math.sqrt(l1), rb = 2 * Math.sqrt(l2), ca = Math.cos(ang), sa = Math.sin(ang);
830
+ let dev = 0; for (const [x, y] of P) { const dx = x - cx, dy = y - cy; dev = Math.max(dev, Math.abs(Math.hypot((dx * ca + dy * sa) / ra, (-dx * sa + dy * ca) / rb) - 1)); }
831
+ return { x: cx, y: cy, rx: ra, ry: rb, rot: ang, dev, area: Math.abs(A) };
832
+ }
833
+ function clipAbove(P, yc) { const o = []; for (let i = 0; i < P.length; i++) { const a = P[i], b = P[(i + 1) % P.length], ia = a[1] <= yc, ib = b[1] <= yc;
834
+ if (ia) o.push(a); if (ia !== ib) { const f = (yc - a[1]) / (b[1] - a[1]); o.push([lerp(a[0], b[0], f), yc]); } } return o; }
835
+ function fieldAt(t, p) { let c = C.cream; for (const f of fields) { const r = f.reg(t); if (!r) continue;
836
+ if (r === 'full' || (r.circle && Math.hypot(p[0] - r.circle[0], p[1] - r.circle[1]) < r.circle[2]) || (r.rect && p[1] >= r.rect[1])) c = f.c; } return c; }
837
+ const HNAME = { cup: 'mug', sun: 'sun disc', sunset: 'setting sun', gull: 'gull', plane: 'paper plane', pin: 'location pin', heart: 'heart', m: 'm' };
838
+ function heroPhase(t) {
839
+ if (t < drop.t1) return t < 0.36 ? 'ink drop, falling (1.35x motion stretch)' : 'ink drop -> mug';
840
+ if (t >= FL.t0 && t < FL.t1) return t < 6.3 ? 'paper plane (loop flight)' : 'plane -> capsule -> pin';
841
+ const m = cues.morphs.find(x => x.P && x.key !== 'plane>pin' && t >= x.t0 && t < x.t1);
842
+ if (m) return m.P.via ? `${HNAME[m.a]} -> circle -> ${HNAME[m.b]} (u ${((t - m.t0) / (m.t1 - m.t0)).toFixed(2)})` : `${HNAME[m.a]} -> ${HNAME[m.b]} (circle to circle)`;
843
+ let cur = 'cup'; for (const n of ['cup', 'sun', 'sunset', 'gull', 'plane', 'pin', 'heart', 'm']) if (t >= cues.land[n]) cur = n; return HNAME[cur] + ' (hold)';
844
+ }
845
+ function linkAtV5(t) {
846
+ t = clamp(t, 0, 9.999); const K = camAt(t), ks = Math.sqrt(Math.abs(K.a * K.d - K.b * K.c)), crot = Math.atan2(K.b, K.a), out = [];
847
+ const dotEl = (x, y, rx, ry, rot, a, fill, note, extra = {}) => { const g = gradK0(t);
848
+ out.push({ id: 'dot', type: 'circle', x: +x.toFixed(1), y: +y.toFixed(1), r: +Math.sqrt(rx * ry).toFixed(1), a: +a.toFixed(3), look: 'grain-flat', fill: hx(fill), stroke: hx(fill), sw: 0,
849
+ grad: [hx(tone(fill, 0.028 * g)), hx(tone(fill, -0.022 * g))], rx: +rx.toFixed(1), ry: +ry.toFixed(1), rot: +(rot / deg).toFixed(1), note, ...extra }); };
850
+ if (t < TV.t0) { // the hero shape: it IS the dot whenever it is (close to) a circle
851
+ const h = heroAt(t), wb = worldBody(h), sc = wb.map(p => camPt(K, p)), E = ellipseOf(sc);
852
+ let vis = 1; if (h.clipY != null) { const cl = clipAbove(wb, h.clipY); vis = cl.length > 2 ? Math.abs(area(cl)) / Math.abs(area(wb)) : 0; }
853
+ const fill = h.sweep && h.sweep.p >= 0.5 ? h.sweep.to : h.fill, round = 1 - smooth(0.03, 0.10, E.dev), asp = E.rx / E.ry;
854
+ const cw = centroid(wb), bg = fieldAt(t, cw), wiping = h.sweep && h.sweep.p > 0 && h.sweep.p < 1, same = hx(bg) === hx(fill); // same colour as the field = no visible edge
855
+ dotEl(E.x, E.y, E.rx, E.ry, E.rot, round * vis * (same ? 0.2 : 1), fill, 'hero shape: ' + heroPhase(t) + (wiping ? ', colour wipe running' : ''),
856
+ { round: +(1 - E.dev).toFixed(3), clean: E.dev < 0.03 && asp < 1.06 && vis > 0.98 && !wiping && !same, vis: +vis.toFixed(3), bg: hx(bg) });
857
+ } else { // the full stop: squeezes out of the dividing circle (8.30-8.46), then holds to the end
858
+ const S = tvStop(t), cy = S.c[1] + (S.fly ? 0 : S.r * S.land * 0.5), p = camPt(K, [S.c[0], cy]);
859
+ const rx = S.r * ks * (S.fly ? 1 : 1 + S.land), ry = S.r * ks * (S.fly ? 1 : 1 - S.land);
860
+ let vis = 1; if (S.fly && t > TV.t0 + 0.5 / FPS) for (const it of TV.items) { const c = tvCell(it, t), d = Math.hypot(S.c[0] - c.c[0], S.c[1] - c.c[1]); // hidden while inside a cell
861
+ vis = Math.min(vis, clamp((d + S.r - c.r) / (2 * S.r))); }
862
+ dotEl(p[0], p[1], rx, ry, crot, vis, S.col, S.fly ? 'the dividing circle shrinking into the full stop (hidden inside the cells until ~8.40)' : 'full stop of 形变动画 (vermilion)',
863
+ { round: 1, clean: !S.fly && Math.abs(S.land) < 0.01, vis: +vis.toFixed(3), bg: hx(fieldAt(t, S.c)) });
864
+ const b = TV.box, cs = [[b[0], b[1]], [b[2], b[1]], [b[2], b[3]], [b[0], b[3]]].map(q => camPt(K, q)), c = camPt(K, [(b[0] + b[2]) / 2, (b[1] + b[3]) / 2]);
865
+ out.push({ id: 'title', type: 'rect', x: +c[0].toFixed(1), y: +c[1].toFixed(1), w: +((b[2] - b[0]) * ks).toFixed(1), h: +((b[3] - b[1]) * ks).toFixed(1), rot: +(crot / deg).toFixed(2),
866
+ a: +clamp((t - TV.m0) / (TV.done - TV.m0)).toFixed(3), fill: hx(TC), note: '形变动画 title block (ink box; a = build progress, 1 = fully formed)' });
867
+ }
868
+ const y = seaTop(t); // the sunset horizon: vermilion sky / ultramarine sea colour edge (rises 2.6-3.1, floods up out of frame 3.98-4.40)
869
+ if (t >= WP.sea && y > -40 && y < H + 20) { const p0 = camPt(K, [-200, y]), p1 = camPt(K, [W + 200, y]), yAt = x => lerp(p0[1], p1[1], (x - p0[0]) / (p1[0] - p0[0]));
870
+ out.push({ id: 'horizon', type: 'line', x0: 0, y0: +yAt(0).toFixed(1), x1: W, y1: +yAt(W).toFixed(1), w: 2, a: 1, fill: hx(C.ultra), note: 'sea / sky colour edge (sunset horizon)' }); }
871
+ for (const i of [1, 2, 4, 5, 6]) { const r = fields[i].reg(t); if (!r || r === 'full') continue; // an expanding field colour circle (iris)
872
+ const p = camPt(K, r.circle); out.push({ id: 'bloom', type: 'circle', x: +p[0].toFixed(1), y: +p[1].toFixed(1), r: +(r.circle[2] * ks).toFixed(1), a: 1, look: 'flat', fill: hx(fields[i].c), stroke: hx(fields[i].c), sw: 0,
873
+ note: 'colour field blooming out of the hero (iris wipe)' }); }
874
+ return out;
875
+ }
876
+ const gradK0 = t => (V5 && t >= TV.t0) ? 0 : 1 - smooth(8.1, 8.5, t);
877
+ if (V5) window.linkAt = linkAtV5;
878
+
879
+ window.renderAt = async (t) => {
880
+ t = clamp(t, 0, 9.999);
881
+ ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; ctx.letterSpacing = '0px';
882
+ CAM = camAt(t); applyCam(ctx, CAM);
883
+ drawFields(t); applyCam(ctx, CAM);
884
+ drawHero(t);
885
+ drawEndRings(t);
886
+ drawLetters(t);
887
+ drawTagline(t);
888
+ drawEndDots(t);
889
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
890
+ ctx.fillStyle = vig; ctx.fillRect(0, 0, W, H);
891
+ finish(Math.round(t * FPS));
892
+ };
893
+ await document.fonts.load('500 20px "IBM Plex Mono"', '0123/'); await document.fonts.load('500 34px "HarmonyOS Sans SC"', WORDS.join('') + '一形万象');
894
+ await document.fonts.load('600 40px "Outfit Morph"', 'morphe');
895
+ if (V5) await document.fonts.load('500 30px "IBM Plex Mono"', 'SHAPE MORPH');
896
+ await document.fonts.ready;
897
+ await window.renderAt(0);
898
+ window.__ready = true;
899
+ })().catch(e => { console.error('INIT ERROR', e && e.stack || e); });
900
+ </script>
901
+ </body></html>