videowright 0.1.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 (306) hide show
  1. package/README.md +91 -0
  2. package/dist/cli/argv.d.ts +28 -0
  3. package/dist/cli/argv.d.ts.map +1 -0
  4. package/dist/cli/argv.js +115 -0
  5. package/dist/cli/argv.js.map +1 -0
  6. package/dist/cli/bin.d.ts +7 -0
  7. package/dist/cli/bin.d.ts.map +1 -0
  8. package/dist/cli/bin.js +10 -0
  9. package/dist/cli/bin.js.map +1 -0
  10. package/dist/cli/dev.d.ts +19 -0
  11. package/dist/cli/dev.d.ts.map +1 -0
  12. package/dist/cli/dev.js +104 -0
  13. package/dist/cli/dev.js.map +1 -0
  14. package/dist/cli/discover.d.ts +29 -0
  15. package/dist/cli/discover.d.ts.map +1 -0
  16. package/dist/cli/discover.js +104 -0
  17. package/dist/cli/discover.js.map +1 -0
  18. package/dist/cli/discover_project.d.ts +29 -0
  19. package/dist/cli/discover_project.d.ts.map +1 -0
  20. package/dist/cli/discover_project.js +108 -0
  21. package/dist/cli/discover_project.js.map +1 -0
  22. package/dist/cli/errors.d.ts +10 -0
  23. package/dist/cli/errors.d.ts.map +1 -0
  24. package/dist/cli/errors.js +13 -0
  25. package/dist/cli/errors.js.map +1 -0
  26. package/dist/cli/ffmpeg.d.ts +57 -0
  27. package/dist/cli/ffmpeg.d.ts.map +1 -0
  28. package/dist/cli/ffmpeg.js +122 -0
  29. package/dist/cli/ffmpeg.js.map +1 -0
  30. package/dist/cli/index.d.ts +7 -0
  31. package/dist/cli/index.d.ts.map +1 -0
  32. package/dist/cli/index.js +152 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/cli/playwright_check.d.ts +44 -0
  35. package/dist/cli/playwright_check.d.ts.map +1 -0
  36. package/dist/cli/playwright_check.js +20 -0
  37. package/dist/cli/playwright_check.js.map +1 -0
  38. package/dist/cli/prompt.d.ts +13 -0
  39. package/dist/cli/prompt.d.ts.map +1 -0
  40. package/dist/cli/prompt.js +47 -0
  41. package/dist/cli/prompt.js.map +1 -0
  42. package/dist/cli/render.d.ts +60 -0
  43. package/dist/cli/render.d.ts.map +1 -0
  44. package/dist/cli/render.js +471 -0
  45. package/dist/cli/render.js.map +1 -0
  46. package/dist/cli/script_cmd.d.ts +26 -0
  47. package/dist/cli/script_cmd.d.ts.map +1 -0
  48. package/dist/cli/script_cmd.js +88 -0
  49. package/dist/cli/script_cmd.js.map +1 -0
  50. package/dist/cli/time_shim.d.ts +44 -0
  51. package/dist/cli/time_shim.d.ts.map +1 -0
  52. package/dist/cli/time_shim.js +390 -0
  53. package/dist/cli/time_shim.js.map +1 -0
  54. package/dist/cli/ts_loader.d.ts +28 -0
  55. package/dist/cli/ts_loader.d.ts.map +1 -0
  56. package/dist/cli/ts_loader.js +95 -0
  57. package/dist/cli/ts_loader.js.map +1 -0
  58. package/dist/cli/vite_helpers.d.ts +62 -0
  59. package/dist/cli/vite_helpers.d.ts.map +1 -0
  60. package/dist/cli/vite_helpers.js +273 -0
  61. package/dist/cli/vite_helpers.js.map +1 -0
  62. package/dist/index.d.ts +11 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +14 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/player/hash_router.d.ts +23 -0
  67. package/dist/player/hash_router.d.ts.map +1 -0
  68. package/dist/player/hash_router.js +49 -0
  69. package/dist/player/hash_router.js.map +1 -0
  70. package/dist/player/hud.d.ts +33 -0
  71. package/dist/player/hud.d.ts.map +1 -0
  72. package/dist/player/hud.js +357 -0
  73. package/dist/player/hud.js.map +1 -0
  74. package/dist/player/index.d.ts +123 -0
  75. package/dist/player/index.d.ts.map +1 -0
  76. package/dist/player/index.js +848 -0
  77. package/dist/player/index.js.map +1 -0
  78. package/dist/player/input.d.ts +14 -0
  79. package/dist/player/input.d.ts.map +1 -0
  80. package/dist/player/input.js +90 -0
  81. package/dist/player/input.js.map +1 -0
  82. package/dist/player/slot.d.ts +22 -0
  83. package/dist/player/slot.d.ts.map +1 -0
  84. package/dist/player/slot.js +43 -0
  85. package/dist/player/slot.js.map +1 -0
  86. package/dist/player/transitions/cut.d.ts +7 -0
  87. package/dist/player/transitions/cut.d.ts.map +1 -0
  88. package/dist/player/transitions/cut.js +9 -0
  89. package/dist/player/transitions/cut.js.map +1 -0
  90. package/dist/player/transitions/fade.d.ts +7 -0
  91. package/dist/player/transitions/fade.d.ts.map +1 -0
  92. package/dist/player/transitions/fade.js +18 -0
  93. package/dist/player/transitions/fade.js.map +1 -0
  94. package/dist/player/transitions/index.d.ts +4 -0
  95. package/dist/player/transitions/index.d.ts.map +1 -0
  96. package/dist/player/transitions/index.js +4 -0
  97. package/dist/player/transitions/index.js.map +1 -0
  98. package/dist/player/transitions/slide.d.ts +6 -0
  99. package/dist/player/transitions/slide.d.ts.map +1 -0
  100. package/dist/player/transitions/slide.js +35 -0
  101. package/dist/player/transitions/slide.js.map +1 -0
  102. package/dist/script/index.d.ts +2 -0
  103. package/dist/script/index.d.ts.map +1 -0
  104. package/dist/script/index.js +2 -0
  105. package/dist/script/index.js.map +1 -0
  106. package/dist/script/script.d.ts +10 -0
  107. package/dist/script/script.d.ts.map +1 -0
  108. package/dist/script/script.js +41 -0
  109. package/dist/script/script.js.map +1 -0
  110. package/dist/segment/SegmentRunner.d.ts +52 -0
  111. package/dist/segment/SegmentRunner.d.ts.map +1 -0
  112. package/dist/segment/SegmentRunner.js +187 -0
  113. package/dist/segment/SegmentRunner.js.map +1 -0
  114. package/dist/segment/defineConfig.d.ts +6 -0
  115. package/dist/segment/defineConfig.d.ts.map +1 -0
  116. package/dist/segment/defineConfig.js +7 -0
  117. package/dist/segment/defineConfig.js.map +1 -0
  118. package/dist/segment/defineSegment.d.ts +7 -0
  119. package/dist/segment/defineSegment.d.ts.map +1 -0
  120. package/dist/segment/defineSegment.js +25 -0
  121. package/dist/segment/defineSegment.js.map +1 -0
  122. package/dist/segment/index.d.ts +5 -0
  123. package/dist/segment/index.d.ts.map +1 -0
  124. package/dist/segment/index.js +4 -0
  125. package/dist/segment/index.js.map +1 -0
  126. package/dist/timeline/index.d.ts +73 -0
  127. package/dist/timeline/index.d.ts.map +1 -0
  128. package/dist/timeline/index.js +142 -0
  129. package/dist/timeline/index.js.map +1 -0
  130. package/dist/timeline/loadAudioTrack.d.ts +18 -0
  131. package/dist/timeline/loadAudioTrack.d.ts.map +1 -0
  132. package/dist/timeline/loadAudioTrack.js +44 -0
  133. package/dist/timeline/loadAudioTrack.js.map +1 -0
  134. package/dist/timeline/loadVoiceover.d.ts +18 -0
  135. package/dist/timeline/loadVoiceover.d.ts.map +1 -0
  136. package/dist/timeline/loadVoiceover.js +38 -0
  137. package/dist/timeline/loadVoiceover.js.map +1 -0
  138. package/dist/timeline/resolveTiming.d.ts +28 -0
  139. package/dist/timeline/resolveTiming.d.ts.map +1 -0
  140. package/dist/timeline/resolveTiming.js +63 -0
  141. package/dist/timeline/resolveTiming.js.map +1 -0
  142. package/dist/timeline/validateTiming.d.ts +29 -0
  143. package/dist/timeline/validateTiming.d.ts.map +1 -0
  144. package/dist/timeline/validateTiming.js +62 -0
  145. package/dist/timeline/validateTiming.js.map +1 -0
  146. package/dist/types.d.ts +216 -0
  147. package/dist/types.d.ts.map +1 -0
  148. package/dist/types.js +6 -0
  149. package/dist/types.js.map +1 -0
  150. package/package.json +47 -0
  151. package/skill/SKILL.md +64 -0
  152. package/skill/assets/hello_world/PLAN.md +31 -0
  153. package/skill/assets/hello_world/README.md +27 -0
  154. package/skill/assets/hello_world/audio/audio_plan.md +14 -0
  155. package/skill/assets/hello_world/segments/hello_intro.ts +69 -0
  156. package/skill/assets/hello_world/segments/hello_outro.ts +71 -0
  157. package/skill/assets/hello_world/timeline.ts +15 -0
  158. package/skill/assets/hello_world/voiceover_script/script.md +10 -0
  159. package/skill/assets/install/package.json +10 -0
  160. package/skill/assets/install/tsconfig.json +23 -0
  161. package/skill/assets/styles/editorial-mono/STYLE.md +124 -0
  162. package/skill/assets/styles/editorial-mono/brand.md +85 -0
  163. package/skill/assets/styles/editorial-mono/reference/animations.jsx +752 -0
  164. package/skill/assets/styles/editorial-mono/reference/scenes.html +563 -0
  165. package/skill/assets/styles/editorial-mono/sample/bullet.ts +101 -0
  166. package/skill/assets/styles/editorial-mono/sample/content.ts +104 -0
  167. package/skill/assets/styles/editorial-mono/sample/cta.ts +113 -0
  168. package/skill/assets/styles/editorial-mono/sample/feature.ts +111 -0
  169. package/skill/assets/styles/editorial-mono/sample/grid.ts +97 -0
  170. package/skill/assets/styles/editorial-mono/sample/kinetic.ts +96 -0
  171. package/skill/assets/styles/editorial-mono/sample/section.ts +101 -0
  172. package/skill/assets/styles/editorial-mono/sample/stat.ts +128 -0
  173. package/skill/assets/styles/editorial-mono/sample/title.ts +97 -0
  174. package/skill/assets/styles/editorial-mono/sample/ui-showcase.ts +159 -0
  175. package/skill/assets/styles/editorial-mono/tokens.css +44 -0
  176. package/skill/assets/styles/iso-diagram/STYLE.md +109 -0
  177. package/skill/assets/styles/iso-diagram/brand.md +32 -0
  178. package/skill/assets/styles/iso-diagram/reference/animations.jsx +673 -0
  179. package/skill/assets/styles/iso-diagram/reference/scenes.html +427 -0
  180. package/skill/assets/styles/iso-diagram/sample/bullet.ts +144 -0
  181. package/skill/assets/styles/iso-diagram/sample/content.ts +192 -0
  182. package/skill/assets/styles/iso-diagram/sample/cta.ts +162 -0
  183. package/skill/assets/styles/iso-diagram/sample/feature.ts +205 -0
  184. package/skill/assets/styles/iso-diagram/sample/grid.ts +181 -0
  185. package/skill/assets/styles/iso-diagram/sample/kinetic.ts +102 -0
  186. package/skill/assets/styles/iso-diagram/sample/section.ts +149 -0
  187. package/skill/assets/styles/iso-diagram/sample/stat.ts +164 -0
  188. package/skill/assets/styles/iso-diagram/sample/title.ts +173 -0
  189. package/skill/assets/styles/iso-diagram/sample/ui-showcase.ts +162 -0
  190. package/skill/assets/styles/iso-diagram/tokens.css +40 -0
  191. package/skill/assets/styles/motion-engineering/STYLE.md +106 -0
  192. package/skill/assets/styles/motion-engineering/brand.md +29 -0
  193. package/skill/assets/styles/motion-engineering/reference/animations.jsx +673 -0
  194. package/skill/assets/styles/motion-engineering/reference/scenes.html +513 -0
  195. package/skill/assets/styles/motion-engineering/sample/bullet.ts +176 -0
  196. package/skill/assets/styles/motion-engineering/sample/content.ts +228 -0
  197. package/skill/assets/styles/motion-engineering/sample/cta.ts +209 -0
  198. package/skill/assets/styles/motion-engineering/sample/feature.ts +299 -0
  199. package/skill/assets/styles/motion-engineering/sample/grid.ts +190 -0
  200. package/skill/assets/styles/motion-engineering/sample/kinetic.ts +159 -0
  201. package/skill/assets/styles/motion-engineering/sample/section.ts +196 -0
  202. package/skill/assets/styles/motion-engineering/sample/stat.ts +230 -0
  203. package/skill/assets/styles/motion-engineering/sample/title.ts +219 -0
  204. package/skill/assets/styles/motion-engineering/sample/ui-showcase.ts +267 -0
  205. package/skill/assets/styles/motion-engineering/tokens.css +40 -0
  206. package/skill/assets/styles/neon-terminal/STYLE.md +105 -0
  207. package/skill/assets/styles/neon-terminal/brand.md +27 -0
  208. package/skill/assets/styles/neon-terminal/reference/animations.jsx +673 -0
  209. package/skill/assets/styles/neon-terminal/reference/scenes.html +387 -0
  210. package/skill/assets/styles/neon-terminal/sample/bullet.ts +113 -0
  211. package/skill/assets/styles/neon-terminal/sample/content.ts +117 -0
  212. package/skill/assets/styles/neon-terminal/sample/cta.ts +131 -0
  213. package/skill/assets/styles/neon-terminal/sample/feature.ts +112 -0
  214. package/skill/assets/styles/neon-terminal/sample/grid.ts +128 -0
  215. package/skill/assets/styles/neon-terminal/sample/kinetic.ts +105 -0
  216. package/skill/assets/styles/neon-terminal/sample/section.ts +96 -0
  217. package/skill/assets/styles/neon-terminal/sample/stat.ts +123 -0
  218. package/skill/assets/styles/neon-terminal/sample/title.ts +122 -0
  219. package/skill/assets/styles/neon-terminal/sample/ui-showcase.ts +127 -0
  220. package/skill/assets/styles/neon-terminal/tokens.css +39 -0
  221. package/skill/assets/styles/risograph/STYLE.md +110 -0
  222. package/skill/assets/styles/risograph/brand.md +26 -0
  223. package/skill/assets/styles/risograph/reference/animations.jsx +673 -0
  224. package/skill/assets/styles/risograph/reference/scenes.html +403 -0
  225. package/skill/assets/styles/risograph/sample/bullet.ts +124 -0
  226. package/skill/assets/styles/risograph/sample/content.ts +135 -0
  227. package/skill/assets/styles/risograph/sample/cta.ts +149 -0
  228. package/skill/assets/styles/risograph/sample/feature.ts +152 -0
  229. package/skill/assets/styles/risograph/sample/grid.ts +123 -0
  230. package/skill/assets/styles/risograph/sample/kinetic.ts +125 -0
  231. package/skill/assets/styles/risograph/sample/section.ts +130 -0
  232. package/skill/assets/styles/risograph/sample/stat.ts +145 -0
  233. package/skill/assets/styles/risograph/sample/title.ts +132 -0
  234. package/skill/assets/styles/risograph/sample/ui-showcase.ts +147 -0
  235. package/skill/assets/styles/risograph/tokens.css +39 -0
  236. package/skill/assets/styles/swiss-console/STYLE.md +107 -0
  237. package/skill/assets/styles/swiss-console/brand.md +37 -0
  238. package/skill/assets/styles/swiss-console/reference/animations.jsx +673 -0
  239. package/skill/assets/styles/swiss-console/reference/scenes.html +420 -0
  240. package/skill/assets/styles/swiss-console/sample/bullet.ts +122 -0
  241. package/skill/assets/styles/swiss-console/sample/content.ts +137 -0
  242. package/skill/assets/styles/swiss-console/sample/cta.ts +109 -0
  243. package/skill/assets/styles/swiss-console/sample/feature.ts +163 -0
  244. package/skill/assets/styles/swiss-console/sample/grid.ts +145 -0
  245. package/skill/assets/styles/swiss-console/sample/kinetic.ts +117 -0
  246. package/skill/assets/styles/swiss-console/sample/section.ts +127 -0
  247. package/skill/assets/styles/swiss-console/sample/stat.ts +148 -0
  248. package/skill/assets/styles/swiss-console/sample/title.ts +148 -0
  249. package/skill/assets/styles/swiss-console/sample/ui-showcase.ts +198 -0
  250. package/skill/assets/styles/swiss-console/tokens.css +39 -0
  251. package/skill/install/INSTALL.md +400 -0
  252. package/skill/references/audio/audio_plan.md +199 -0
  253. package/skill/references/audio/build.md +208 -0
  254. package/skill/references/audio/cue_template.md +219 -0
  255. package/skill/references/audio/ffmpeg_cookbook.md +267 -0
  256. package/skill/references/audio/music/music.md +171 -0
  257. package/skill/references/audio/music/providers/elevenlabs.md +170 -0
  258. package/skill/references/audio/music/providers/manual.md +140 -0
  259. package/skill/references/audio/music/providers/openverse.md +265 -0
  260. package/skill/references/audio/sfx/providers/elevenlabs.md +152 -0
  261. package/skill/references/audio/sfx/providers/manual.md +117 -0
  262. package/skill/references/audio/sfx/providers/openverse.md +243 -0
  263. package/skill/references/audio/sfx/sfx.md +149 -0
  264. package/skill/references/audio/styles.md +102 -0
  265. package/skill/references/audio/sync.md +237 -0
  266. package/skill/references/audio/voiceover/animation_sync.md +142 -0
  267. package/skill/references/audio/voiceover/provider_script.md +153 -0
  268. package/skill/references/audio/voiceover/providers/elevenlabs.md +288 -0
  269. package/skill/references/audio/voiceover/providers/manual.md +100 -0
  270. package/skill/references/audio/voiceover/script_writing.md +100 -0
  271. package/skill/references/audio/voiceover/style_intake.md +56 -0
  272. package/skill/references/audio/voiceover/sync_algorithm.md +167 -0
  273. package/skill/references/audio/voiceover.md +296 -0
  274. package/skill/references/audio.md +135 -0
  275. package/skill/references/authoring_segment.md +446 -0
  276. package/skill/references/create_or_edit_video.md +232 -0
  277. package/skill/references/dev_server.md +157 -0
  278. package/skill/references/export.md +145 -0
  279. package/skill/references/new_video.md +117 -0
  280. package/skill/references/project_structure.md +144 -0
  281. package/skill/references/setup.md +109 -0
  282. package/skill/references/setup_new_style.md +158 -0
  283. package/skill/references/styles.md +154 -0
  284. package/skill/references/testing.md +115 -0
  285. package/skill/references/types.md +240 -0
  286. package/src/cli/entry/components/copy_button.ts +42 -0
  287. package/src/cli/entry/components/download_modal.ts +204 -0
  288. package/src/cli/entry/components/empty_state.ts +55 -0
  289. package/src/cli/entry/components/hide_hud_tab.ts +37 -0
  290. package/src/cli/entry/components/icons.ts +31 -0
  291. package/src/cli/entry/components/top_bar.ts +69 -0
  292. package/src/cli/entry/components/video_card.ts +57 -0
  293. package/src/cli/entry/dev_frame.ts +189 -0
  294. package/src/cli/entry/entry_index.ts +16 -0
  295. package/src/cli/entry/entry_video.ts +24 -0
  296. package/src/cli/entry/index.html +12 -0
  297. package/src/cli/entry/parse_slug.ts +14 -0
  298. package/src/cli/entry/render.html +17 -0
  299. package/src/cli/entry/render_entry.ts +121 -0
  300. package/src/cli/entry/styles/base.css +45 -0
  301. package/src/cli/entry/styles/components.css +605 -0
  302. package/src/cli/entry/styles/tokens.css +44 -0
  303. package/src/cli/entry/video.html +22 -0
  304. package/src/cli/entry/views/homepage.ts +66 -0
  305. package/src/cli/entry/views/video_view.ts +286 -0
  306. package/src/cli/entry/virtual.d.ts +8 -0
@@ -0,0 +1,752 @@
1
+ // animations.jsx
2
+ // Reusable animation starter: Stage, Timeline, Sprite, easing helpers.
3
+ // Usage (in an HTML file that loads React + Babel):
4
+ //
5
+ // <Stage width={1280} height={720} duration={10} background="#f6f4ef">
6
+ // <MyScene />
7
+ // </Stage>
8
+ //
9
+ // Inside <Stage>, any child can call useTime() to read the current
10
+ // playhead (seconds). Or wrap content in <Sprite start={1} end={4}>...</Sprite>
11
+ // to only render during that window -- children receive a `localTime` and
12
+ // `progress` via the useSprite() hook.
13
+ //
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ // ── Easing functions (hand-rolled, Popmotion-style) ─────────────────────────
17
+ // All easings take t ∈ [0,1] and return eased t ∈ [0,1] (may overshoot for back/elastic).
18
+ const Easing = {
19
+ linear: (t) => t,
20
+
21
+ // Quad
22
+ easeInQuad: (t) => t * t,
23
+ easeOutQuad: (t) => t * (2 - t),
24
+ easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
25
+
26
+ // Cubic
27
+ easeInCubic: (t) => t * t * t,
28
+ easeOutCubic: (t) => --t * t * t + 1,
29
+ easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
30
+
31
+ // Quart
32
+ easeInQuart: (t) => t * t * t * t,
33
+ easeOutQuart: (t) => 1 - --t * t * t * t,
34
+ easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t),
35
+
36
+ // Expo
37
+ easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))),
38
+ easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
39
+ easeInOutExpo: (t) => {
40
+ if (t === 0) return 0;
41
+ if (t === 1) return 1;
42
+ if (t < 0.5) return 0.5 * Math.pow(2, 20 * t - 10);
43
+ return 1 - 0.5 * Math.pow(2, -20 * t + 10);
44
+ },
45
+
46
+ // Sine
47
+ easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
48
+ easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
49
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
50
+
51
+ // Back (overshoot)
52
+ easeOutBack: (t) => {
53
+ const c1 = 1.70158,
54
+ c3 = c1 + 1;
55
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
56
+ },
57
+ easeInBack: (t) => {
58
+ const c1 = 1.70158,
59
+ c3 = c1 + 1;
60
+ return c3 * t * t * t - c1 * t * t;
61
+ },
62
+ easeInOutBack: (t) => {
63
+ const c1 = 1.70158,
64
+ c2 = c1 * 1.525;
65
+ return t < 0.5
66
+ ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
67
+ : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
68
+ },
69
+
70
+ // Elastic
71
+ easeOutElastic: (t) => {
72
+ const c4 = (2 * Math.PI) / 3;
73
+ if (t === 0) return 0;
74
+ if (t === 1) return 1;
75
+ return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
76
+ },
77
+ };
78
+
79
+ // ── Core interpolation helpers ──────────────────────────────────────────────
80
+
81
+ // Clamp a value to [min, max]
82
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
83
+
84
+ // interpolate([0, 0.5, 1], [0, 100, 50], ease?) -> fn(t)
85
+ // Popmotion-style: linearly maps t across input keyframes to output values,
86
+ // with optional easing per segment (single fn or array of fns).
87
+ function interpolate(input, output, ease = Easing.linear) {
88
+ return (t) => {
89
+ if (t <= input[0]) return output[0];
90
+ if (t >= input[input.length - 1]) return output[output.length - 1];
91
+ for (let i = 0; i < input.length - 1; i++) {
92
+ if (t >= input[i] && t <= input[i + 1]) {
93
+ const span = input[i + 1] - input[i];
94
+ const local = span === 0 ? 0 : (t - input[i]) / span;
95
+ const easeFn = Array.isArray(ease) ? ease[i] || Easing.linear : ease;
96
+ const eased = easeFn(local);
97
+ return output[i] + (output[i + 1] - output[i]) * eased;
98
+ }
99
+ }
100
+ return output[output.length - 1];
101
+ };
102
+ }
103
+
104
+ // animate({from, to, start, end, ease})(t) — simpler single-segment tween.
105
+ // Returns `from` before `start`, `to` after `end`.
106
+ function animate({ from = 0, to = 1, start = 0, end = 1, ease = Easing.easeInOutCubic }) {
107
+ return (t) => {
108
+ if (t <= start) return from;
109
+ if (t >= end) return to;
110
+ const local = (t - start) / (end - start);
111
+ return from + (to - from) * ease(local);
112
+ };
113
+ }
114
+
115
+ // ── Timeline context ────────────────────────────────────────────────────────
116
+
117
+ const TimelineContext = React.createContext({ time: 0, duration: 10, playing: false });
118
+
119
+ const useTime = () => React.useContext(TimelineContext).time;
120
+ const useTimeline = () => React.useContext(TimelineContext);
121
+
122
+ // ── Sprite ──────────────────────────────────────────────────────────────────
123
+ // Renders children only when the playhead is inside [start, end]. Provides
124
+ // a sub-context with `localTime` (seconds since start) and `progress` (0..1).
125
+ //
126
+ // <Sprite start={2} end={5}>
127
+ // {({ localTime, progress }) => <Thing x={progress * 100} />}
128
+ // </Sprite>
129
+ //
130
+ // Or as a plain wrapper — children can call useSprite() themselves.
131
+
132
+ const SpriteContext = React.createContext({ localTime: 0, progress: 0, duration: 0 });
133
+ const useSprite = () => React.useContext(SpriteContext);
134
+
135
+ function Sprite({ start = 0, end = Number.POSITIVE_INFINITY, children, keepMounted = false }) {
136
+ const { time } = useTimeline();
137
+ const visible = time >= start && time <= end;
138
+ if (!visible && !keepMounted) return null;
139
+
140
+ const duration = end - start;
141
+ const localTime = Math.max(0, time - start);
142
+ const progress = duration > 0 && isFinite(duration) ? clamp(localTime / duration, 0, 1) : 0;
143
+
144
+ const value = { localTime, progress, duration, visible };
145
+
146
+ return (
147
+ <SpriteContext.Provider value={value}>
148
+ {typeof children === "function" ? children(value) : children}
149
+ </SpriteContext.Provider>
150
+ );
151
+ }
152
+
153
+ // ── Sample sprite components ────────────────────────────────────────────────
154
+
155
+ // TextSprite: fades/slides text in on entry, holds, then fades out on exit.
156
+ // Props: text, x, y, size, color, font, entryDur, exitDur, align
157
+ function TextSprite({
158
+ text,
159
+ x = 0,
160
+ y = 0,
161
+ size = 48,
162
+ color = "#111",
163
+ font = "Inter, system-ui, sans-serif",
164
+ weight = 600,
165
+ entryDur = 0.45,
166
+ exitDur = 0.35,
167
+ entryEase = Easing.easeOutBack,
168
+ exitEase = Easing.easeInCubic,
169
+ align = "left",
170
+ letterSpacing = "-0.01em",
171
+ }) {
172
+ const { localTime, duration } = useSprite();
173
+ const exitStart = Math.max(0, duration - exitDur);
174
+
175
+ let opacity = 1;
176
+ let ty = 0;
177
+
178
+ if (localTime < entryDur) {
179
+ const t = entryEase(clamp(localTime / entryDur, 0, 1));
180
+ opacity = t;
181
+ ty = (1 - t) * 16;
182
+ } else if (localTime > exitStart) {
183
+ const t = exitEase(clamp((localTime - exitStart) / exitDur, 0, 1));
184
+ opacity = 1 - t;
185
+ ty = -t * 8;
186
+ }
187
+
188
+ const translateX = align === "center" ? "-50%" : align === "right" ? "-100%" : "0";
189
+
190
+ return (
191
+ <div
192
+ style={{
193
+ position: "absolute",
194
+ left: x,
195
+ top: y,
196
+ transform: `translate(${translateX}, ${ty}px)`,
197
+ opacity,
198
+ fontFamily: font,
199
+ fontSize: size,
200
+ fontWeight: weight,
201
+ color,
202
+ letterSpacing,
203
+ whiteSpace: "pre",
204
+ lineHeight: 1.1,
205
+ willChange: "transform, opacity",
206
+ }}
207
+ >
208
+ {text}
209
+ </div>
210
+ );
211
+ }
212
+
213
+ // ImageSprite: scales + fades in; optional Ken Burns drift during hold.
214
+ function ImageSprite({
215
+ src,
216
+ x = 0,
217
+ y = 0,
218
+ width = 400,
219
+ height = 300,
220
+ entryDur = 0.6,
221
+ exitDur = 0.4,
222
+ kenBurns = false,
223
+ kenBurnsScale = 1.08,
224
+ radius = 12,
225
+ fit = "cover",
226
+ placeholder = null, // {label: string} for striped placeholder
227
+ }) {
228
+ const { localTime, duration } = useSprite();
229
+ const exitStart = Math.max(0, duration - exitDur);
230
+
231
+ let opacity = 1;
232
+ let scale = 1;
233
+
234
+ if (localTime < entryDur) {
235
+ const t = Easing.easeOutCubic(clamp(localTime / entryDur, 0, 1));
236
+ opacity = t;
237
+ scale = 0.96 + 0.04 * t;
238
+ } else if (localTime > exitStart) {
239
+ const t = Easing.easeInCubic(clamp((localTime - exitStart) / exitDur, 0, 1));
240
+ opacity = 1 - t;
241
+ scale = (kenBurns ? kenBurnsScale : 1) + 0.02 * t;
242
+ } else if (kenBurns) {
243
+ const holdSpan = exitStart - entryDur;
244
+ const holdT = holdSpan > 0 ? (localTime - entryDur) / holdSpan : 0;
245
+ scale = 1 + (kenBurnsScale - 1) * holdT;
246
+ }
247
+
248
+ const content = placeholder ? (
249
+ <div
250
+ style={{
251
+ width: "100%",
252
+ height: "100%",
253
+ display: "flex",
254
+ alignItems: "center",
255
+ justifyContent: "center",
256
+ background: "repeating-linear-gradient(135deg, #e9e6df 0 10px, #dcd8cf 10px 20px)",
257
+ color: "#6b6458",
258
+ fontFamily: "JetBrains Mono, ui-monospace, monospace",
259
+ fontSize: 13,
260
+ letterSpacing: "0.04em",
261
+ textTransform: "uppercase",
262
+ }}
263
+ >
264
+ {placeholder.label || "image"}
265
+ </div>
266
+ ) : (
267
+ <img
268
+ src={src}
269
+ alt=""
270
+ style={{ width: "100%", height: "100%", objectFit: fit, display: "block" }}
271
+ />
272
+ );
273
+
274
+ return (
275
+ <div
276
+ style={{
277
+ position: "absolute",
278
+ left: x,
279
+ top: y,
280
+ width,
281
+ height,
282
+ opacity,
283
+ transform: `scale(${scale})`,
284
+ transformOrigin: "center",
285
+ borderRadius: radius,
286
+ overflow: "hidden",
287
+ willChange: "transform, opacity",
288
+ }}
289
+ >
290
+ {content}
291
+ </div>
292
+ );
293
+ }
294
+
295
+ // RectSprite: simple rectangle that animates position/size/color via props.
296
+ // Useful demo primitive — takes a `render` fn for per-frame customization.
297
+ function RectSprite({
298
+ x = 0,
299
+ y = 0,
300
+ width = 100,
301
+ height = 100,
302
+ color = "#111",
303
+ radius = 8,
304
+ entryDur = 0.4,
305
+ exitDur = 0.3,
306
+ render, // optional: (ctx) => style overrides
307
+ }) {
308
+ const spriteCtx = useSprite();
309
+ const { localTime, duration } = spriteCtx;
310
+ const exitStart = Math.max(0, duration - exitDur);
311
+
312
+ let opacity = 1;
313
+ let scale = 1;
314
+
315
+ if (localTime < entryDur) {
316
+ const t = Easing.easeOutBack(clamp(localTime / entryDur, 0, 1));
317
+ opacity = clamp(localTime / entryDur, 0, 1);
318
+ scale = 0.4 + 0.6 * t;
319
+ } else if (localTime > exitStart) {
320
+ const t = Easing.easeInQuad(clamp((localTime - exitStart) / exitDur, 0, 1));
321
+ opacity = 1 - t;
322
+ scale = 1 - 0.15 * t;
323
+ }
324
+
325
+ const overrides = render ? render(spriteCtx) : {};
326
+
327
+ return (
328
+ <div
329
+ style={{
330
+ position: "absolute",
331
+ left: x,
332
+ top: y,
333
+ width,
334
+ height,
335
+ background: color,
336
+ borderRadius: radius,
337
+ opacity,
338
+ transform: `scale(${scale})`,
339
+ transformOrigin: "center",
340
+ willChange: "transform, opacity",
341
+ ...overrides,
342
+ }}
343
+ />
344
+ );
345
+ }
346
+
347
+ function Stage({
348
+ width = 1280,
349
+ height = 720,
350
+ duration = 10,
351
+ background = "#f6f4ef",
352
+ fps = 60,
353
+ loop = true,
354
+ autoplay = true,
355
+ persistKey = "animstage",
356
+ children,
357
+ }) {
358
+ const [time, setTime] = React.useState(() => {
359
+ try {
360
+ const v = Number.parseFloat(localStorage.getItem(persistKey + ":t") || "0");
361
+ return isFinite(v) ? clamp(v, 0, duration) : 0;
362
+ } catch {
363
+ return 0;
364
+ }
365
+ });
366
+ const [playing, setPlaying] = React.useState(autoplay);
367
+ const [hoverTime, setHoverTime] = React.useState(null);
368
+ const [scale, setScale] = React.useState(1);
369
+
370
+ const stageRef = React.useRef(null);
371
+ const canvasRef = React.useRef(null);
372
+ const rafRef = React.useRef(null);
373
+ const lastTsRef = React.useRef(null);
374
+
375
+ // Persist playhead
376
+ React.useEffect(() => {
377
+ try {
378
+ localStorage.setItem(persistKey + ":t", String(time));
379
+ } catch {}
380
+ }, [time, persistKey]);
381
+
382
+ // Auto-scale to fit viewport
383
+ React.useEffect(() => {
384
+ if (!stageRef.current) return;
385
+ const el = stageRef.current;
386
+ const measure = () => {
387
+ const barH = 44; // playback bar height
388
+ const s = Math.min(el.clientWidth / width, (el.clientHeight - barH) / height);
389
+ setScale(Math.max(0.05, s));
390
+ };
391
+ measure();
392
+ const ro = new ResizeObserver(measure);
393
+ ro.observe(el);
394
+ window.addEventListener("resize", measure);
395
+ return () => {
396
+ ro.disconnect();
397
+ window.removeEventListener("resize", measure);
398
+ };
399
+ }, [width, height]);
400
+
401
+ // Animation loop
402
+ React.useEffect(() => {
403
+ if (!playing) {
404
+ lastTsRef.current = null;
405
+ return;
406
+ }
407
+ const step = (ts) => {
408
+ if (lastTsRef.current == null) lastTsRef.current = ts;
409
+ const dt = (ts - lastTsRef.current) / 1000;
410
+ lastTsRef.current = ts;
411
+ setTime((t) => {
412
+ let next = t + dt;
413
+ if (next >= duration) {
414
+ if (loop) next = next % duration;
415
+ else {
416
+ next = duration;
417
+ setPlaying(false);
418
+ }
419
+ }
420
+ return next;
421
+ });
422
+ rafRef.current = requestAnimationFrame(step);
423
+ };
424
+ rafRef.current = requestAnimationFrame(step);
425
+ return () => {
426
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
427
+ lastTsRef.current = null;
428
+ };
429
+ }, [playing, duration, loop]);
430
+
431
+ // Keyboard: space = play/pause, ← → = seek
432
+ React.useEffect(() => {
433
+ const onKey = (e) => {
434
+ if (e.target && (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")) return;
435
+ if (e.code === "Space") {
436
+ e.preventDefault();
437
+ setPlaying((p) => !p);
438
+ } else if (e.code === "ArrowLeft") {
439
+ setTime((t) => clamp(t - (e.shiftKey ? 1 : 0.1), 0, duration));
440
+ } else if (e.code === "ArrowRight") {
441
+ setTime((t) => clamp(t + (e.shiftKey ? 1 : 0.1), 0, duration));
442
+ } else if (e.key === "0" || e.code === "Home") {
443
+ setTime(0);
444
+ }
445
+ };
446
+ window.addEventListener("keydown", onKey);
447
+ return () => window.removeEventListener("keydown", onKey);
448
+ }, [duration]);
449
+
450
+ const displayTime = hoverTime != null ? hoverTime : time;
451
+
452
+ const ctxValue = React.useMemo(
453
+ () => ({ time: displayTime, duration, playing, setTime, setPlaying }),
454
+ [displayTime, duration, playing],
455
+ );
456
+
457
+ return (
458
+ <div
459
+ ref={stageRef}
460
+ style={{
461
+ position: "absolute",
462
+ inset: 0,
463
+ display: "flex",
464
+ flexDirection: "column",
465
+ alignItems: "center",
466
+ background: "#0a0a0a",
467
+ fontFamily: "Inter, system-ui, sans-serif",
468
+ }}
469
+ >
470
+ {/* Canvas area — vertically centered in remaining space */}
471
+ <div
472
+ style={{
473
+ flex: 1,
474
+ width: "100%",
475
+ display: "flex",
476
+ alignItems: "center",
477
+ justifyContent: "center",
478
+ overflow: "hidden",
479
+ minHeight: 0,
480
+ }}
481
+ >
482
+ <div
483
+ ref={canvasRef}
484
+ style={{
485
+ width,
486
+ height,
487
+ background,
488
+ position: "relative",
489
+ transform: `scale(${scale})`,
490
+ transformOrigin: "center",
491
+ flexShrink: 0,
492
+ boxShadow: "0 20px 60px rgba(0,0,0,0.4)",
493
+ overflow: "hidden",
494
+ }}
495
+ >
496
+ <TimelineContext.Provider value={ctxValue}>{children}</TimelineContext.Provider>
497
+ </div>
498
+ </div>
499
+
500
+ {/* Playback bar — stacked below canvas, never overlapping */}
501
+ <PlaybackBar
502
+ time={displayTime}
503
+ actualTime={time}
504
+ duration={duration}
505
+ playing={playing}
506
+ onPlayPause={() => setPlaying((p) => !p)}
507
+ onReset={() => {
508
+ setTime(0);
509
+ }}
510
+ onSeek={(t) => setTime(t)}
511
+ onHover={(t) => setHoverTime(t)}
512
+ />
513
+ </div>
514
+ );
515
+ }
516
+
517
+ // ── Playback bar ────────────────────────────────────────────────────────────
518
+ // Play/pause, return-to-begin, scrub track, time display.
519
+ // Uses fixed-width time fields so layout doesn't thrash.
520
+
521
+ function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek, onHover }) {
522
+ const trackRef = React.useRef(null);
523
+ const [dragging, setDragging] = React.useState(false);
524
+
525
+ const timeFromEvent = React.useCallback(
526
+ (e) => {
527
+ const rect = trackRef.current.getBoundingClientRect();
528
+ const x = clamp((e.clientX - rect.left) / rect.width, 0, 1);
529
+ return x * duration;
530
+ },
531
+ [duration],
532
+ );
533
+
534
+ const onTrackMove = (e) => {
535
+ if (!trackRef.current) return;
536
+ const t = timeFromEvent(e);
537
+ if (dragging) {
538
+ onSeek(t);
539
+ } else {
540
+ onHover(t);
541
+ }
542
+ };
543
+
544
+ const onTrackLeave = () => {
545
+ if (!dragging) onHover(null);
546
+ };
547
+
548
+ const onTrackDown = (e) => {
549
+ setDragging(true);
550
+ const t = timeFromEvent(e);
551
+ onSeek(t);
552
+ onHover(null);
553
+ };
554
+
555
+ React.useEffect(() => {
556
+ if (!dragging) return;
557
+ const onUp = () => setDragging(false);
558
+ const onMove = (e) => {
559
+ if (!trackRef.current) return;
560
+ const t = timeFromEvent(e);
561
+ onSeek(t);
562
+ };
563
+ window.addEventListener("mouseup", onUp);
564
+ window.addEventListener("mousemove", onMove);
565
+ return () => {
566
+ window.removeEventListener("mouseup", onUp);
567
+ window.removeEventListener("mousemove", onMove);
568
+ };
569
+ }, [dragging, timeFromEvent, onSeek]);
570
+
571
+ const pct = duration > 0 ? (time / duration) * 100 : 0;
572
+ const fmt = (t) => {
573
+ const total = Math.max(0, t);
574
+ const m = Math.floor(total / 60);
575
+ const s = Math.floor(total % 60);
576
+ const cs = Math.floor((total * 100) % 100);
577
+ return `${String(m).padStart(1, "0")}:${String(s).padStart(2, "0")}.${String(cs).padStart(2, "0")}`;
578
+ };
579
+
580
+ const mono = "JetBrains Mono, ui-monospace, SFMono-Regular, monospace";
581
+
582
+ return (
583
+ <div
584
+ style={{
585
+ display: "flex",
586
+ alignItems: "center",
587
+ gap: 12,
588
+ padding: "8px 16px",
589
+ background: "rgba(20,20,20,0.92)",
590
+ borderTop: "1px solid rgba(255,255,255,0.08)",
591
+ width: "100%",
592
+ maxWidth: 680,
593
+ alignSelf: "center",
594
+
595
+ borderRadius: 8,
596
+ color: "#f6f4ef",
597
+ fontFamily: "Inter, system-ui, sans-serif",
598
+ userSelect: "none",
599
+ flexShrink: 0,
600
+ }}
601
+ >
602
+ <IconButton onClick={onReset} title="Return to start (0)">
603
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
604
+ <path
605
+ d="M3 2v10M12 2L5 7l7 5V2z"
606
+ stroke="currentColor"
607
+ strokeWidth="1.5"
608
+ strokeLinejoin="round"
609
+ strokeLinecap="round"
610
+ />
611
+ </svg>
612
+ </IconButton>
613
+ <IconButton onClick={onPlayPause} title="Play/pause (space)">
614
+ {playing ? (
615
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
616
+ <rect x="3" y="2" width="3" height="10" fill="currentColor" />
617
+ <rect x="8" y="2" width="3" height="10" fill="currentColor" />
618
+ </svg>
619
+ ) : (
620
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
621
+ <path d="M3 2l9 5-9 5V2z" fill="currentColor" />
622
+ </svg>
623
+ )}
624
+ </IconButton>
625
+
626
+ {/* Current time: fixed width so it doesn't thrash */}
627
+ <div
628
+ style={{
629
+ fontFamily: mono,
630
+ fontSize: 12,
631
+ fontVariantNumeric: "tabular-nums",
632
+ width: 64,
633
+ textAlign: "right",
634
+ color: "#f6f4ef",
635
+ }}
636
+ >
637
+ {fmt(time)}
638
+ </div>
639
+
640
+ {/* Scrub track */}
641
+ <div
642
+ ref={trackRef}
643
+ onMouseMove={onTrackMove}
644
+ onMouseLeave={onTrackLeave}
645
+ onMouseDown={onTrackDown}
646
+ style={{
647
+ flex: 1,
648
+ height: 22,
649
+ position: "relative",
650
+ cursor: "pointer",
651
+ display: "flex",
652
+ alignItems: "center",
653
+ }}
654
+ >
655
+ <div
656
+ style={{
657
+ position: "absolute",
658
+ left: 0,
659
+ right: 0,
660
+ height: 4,
661
+ background: "rgba(255,255,255,0.12)",
662
+ borderRadius: 2,
663
+ }}
664
+ />
665
+ <div
666
+ style={{
667
+ position: "absolute",
668
+ left: 0,
669
+ width: `${pct}%`,
670
+ height: 4,
671
+ background: "oklch(72% 0.12 250)",
672
+ borderRadius: 2,
673
+ }}
674
+ />
675
+ <div
676
+ style={{
677
+ position: "absolute",
678
+ left: `${pct}%`,
679
+ top: "50%",
680
+ width: 12,
681
+ height: 12,
682
+ marginLeft: -6,
683
+ marginTop: -6,
684
+ background: "#fff",
685
+ borderRadius: 6,
686
+ boxShadow: "0 2px 4px rgba(0,0,0,0.4)",
687
+ }}
688
+ />
689
+ </div>
690
+
691
+ {/* Duration: fixed width */}
692
+ <div
693
+ style={{
694
+ fontFamily: mono,
695
+ fontSize: 12,
696
+ fontVariantNumeric: "tabular-nums",
697
+ width: 64,
698
+ textAlign: "left",
699
+ color: "rgba(246,244,239,0.55)",
700
+ }}
701
+ >
702
+ {fmt(duration)}
703
+ </div>
704
+ </div>
705
+ );
706
+ }
707
+
708
+ function IconButton({ children, onClick, title }) {
709
+ const [hover, setHover] = React.useState(false);
710
+ return (
711
+ <button
712
+ onClick={onClick}
713
+ title={title}
714
+ onMouseEnter={() => setHover(true)}
715
+ onMouseLeave={() => setHover(false)}
716
+ style={{
717
+ width: 28,
718
+ height: 28,
719
+ display: "flex",
720
+ alignItems: "center",
721
+ justifyContent: "center",
722
+ background: hover ? "rgba(255,255,255,0.12)" : "rgba(255,255,255,0.04)",
723
+ border: "1px solid rgba(255,255,255,0.1)",
724
+ borderRadius: 6,
725
+ color: "#f6f4ef",
726
+ cursor: "pointer",
727
+ padding: 0,
728
+ transition: "background 120ms",
729
+ }}
730
+ >
731
+ {children}
732
+ </button>
733
+ );
734
+ }
735
+
736
+ Object.assign(window, {
737
+ Easing,
738
+ interpolate,
739
+ animate,
740
+ clamp,
741
+ TimelineContext,
742
+ useTime,
743
+ useTimeline,
744
+ Sprite,
745
+ SpriteContext,
746
+ useSprite,
747
+ TextSprite,
748
+ ImageSprite,
749
+ RectSprite,
750
+ Stage,
751
+ PlaybackBar,
752
+ });