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