kasaframe 0.0.0-stage → 0.1.3

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 (442) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +148 -0
  3. package/README.md +92 -2
  4. package/app/package.json +12 -0
  5. package/app/server/engine.ts +533 -0
  6. package/app/server/main.ts +209 -0
  7. package/app/web/env.d.ts +2 -0
  8. package/app/web/icons.ts +31 -0
  9. package/app/web/index.html +21 -0
  10. package/app/web/main.ts +1195 -0
  11. package/app/web/style.css +463 -0
  12. package/bin/hooks.mjs +64 -0
  13. package/bin/kf.mjs +32 -0
  14. package/docs/AGENTS.md +286 -0
  15. package/music/README.md +37 -0
  16. package/package.json +44 -4
  17. package/packages/adapters/package.json +36 -0
  18. package/packages/adapters/src/css.ts +11 -0
  19. package/packages/adapters/src/gsap.ts +26 -0
  20. package/packages/adapters/src/index.ts +9 -0
  21. package/packages/adapters/src/lottie.ts +22 -0
  22. package/packages/adapters/src/media.ts +59 -0
  23. package/packages/adapters/src/page.ts +229 -0
  24. package/packages/adapters/src/project.ts +214 -0
  25. package/packages/adapters/src/react.ts +36 -0
  26. package/packages/adapters/src/svelte.ts +22 -0
  27. package/packages/adapters/src/three.ts +18 -0
  28. package/packages/adapters/src/vue.ts +27 -0
  29. package/packages/check/package.json +19 -0
  30. package/packages/check/src/check.ts +280 -0
  31. package/packages/check/src/cli.ts +70 -0
  32. package/packages/check/src/image.ts +146 -0
  33. package/packages/check/src/index.ts +9 -0
  34. package/packages/check/src/ocr.swift +37 -0
  35. package/packages/check/src/probe.ts +328 -0
  36. package/packages/check/src/report.ts +44 -0
  37. package/packages/check/src/rules/assets.ts +45 -0
  38. package/packages/check/src/rules/beats.ts +212 -0
  39. package/packages/check/src/rules/camera.ts +217 -0
  40. package/packages/check/src/rules/captions.ts +49 -0
  41. package/packages/check/src/rules/flash.ts +235 -0
  42. package/packages/check/src/rules/frames.ts +136 -0
  43. package/packages/check/src/rules/layout.ts +281 -0
  44. package/packages/check/src/rules/page.ts +87 -0
  45. package/packages/check/src/rules/runs.ts +20 -0
  46. package/packages/check/src/rules/text.ts +326 -0
  47. package/packages/check/src/scan.ts +240 -0
  48. package/packages/check/src/serve.ts +67 -0
  49. package/packages/check/src/types.ts +80 -0
  50. package/packages/check/src/video.ts +137 -0
  51. package/packages/cli/package.json +30 -0
  52. package/packages/cli/src/blur.ts +80 -0
  53. package/packages/cli/src/browser.ts +203 -0
  54. package/packages/cli/src/command.ts +44 -0
  55. package/packages/cli/src/commands/app.ts +25 -0
  56. package/packages/cli/src/commands/beats.ts +36 -0
  57. package/packages/cli/src/commands/bench.ts +123 -0
  58. package/packages/cli/src/commands/blender.ts +293 -0
  59. package/packages/cli/src/commands/check.ts +25 -0
  60. package/packages/cli/src/commands/direct.ts +210 -0
  61. package/packages/cli/src/commands/doctor.ts +30 -0
  62. package/packages/cli/src/commands/gen.ts +173 -0
  63. package/packages/cli/src/commands/look.ts +46 -0
  64. package/packages/cli/src/commands/lyrics.ts +157 -0
  65. package/packages/cli/src/commands/make.ts +1121 -0
  66. package/packages/cli/src/commands/music.ts +78 -0
  67. package/packages/cli/src/commands/narrate.ts +238 -0
  68. package/packages/cli/src/commands/new.ts +185 -0
  69. package/packages/cli/src/commands/preview.ts +43 -0
  70. package/packages/cli/src/commands/render.ts +250 -0
  71. package/packages/cli/src/commands/series.ts +110 -0
  72. package/packages/cli/src/commands/setup.ts +102 -0
  73. package/packages/cli/src/commands/sheet.ts +114 -0
  74. package/packages/cli/src/commands/skill.ts +43 -0
  75. package/packages/cli/src/commands/still.ts +40 -0
  76. package/packages/cli/src/commands/theme.ts +172 -0
  77. package/packages/cli/src/commands/transitions.ts +40 -0
  78. package/packages/cli/src/deps.ts +122 -0
  79. package/packages/cli/src/ffmpeg.ts +81 -0
  80. package/packages/cli/src/index.ts +8 -0
  81. package/packages/cli/src/kf.ts +64 -0
  82. package/packages/cli/src/lock.ts +72 -0
  83. package/packages/cli/src/mix.ts +131 -0
  84. package/packages/cli/src/out.ts +13 -0
  85. package/packages/cli/src/pool.ts +34 -0
  86. package/packages/cli/src/segments.ts +402 -0
  87. package/packages/cli/src/serve.ts +124 -0
  88. package/packages/clock/package.json +14 -0
  89. package/packages/clock/src/index.ts +25 -0
  90. package/packages/clock/src/install.ts +351 -0
  91. package/packages/clock/src/types.ts +45 -0
  92. package/packages/core/package.json +9 -0
  93. package/packages/core/src/beats.ts +283 -0
  94. package/packages/core/src/ease.ts +87 -0
  95. package/packages/core/src/frame.ts +99 -0
  96. package/packages/core/src/index.ts +27 -0
  97. package/packages/core/src/project.ts +157 -0
  98. package/packages/core/src/rand.ts +56 -0
  99. package/packages/core/src/spring.ts +52 -0
  100. package/packages/core/src/style.ts +100 -0
  101. package/packages/core/src/time.ts +88 -0
  102. package/packages/director/package.json +23 -0
  103. package/packages/director/src/assemble.ts +361 -0
  104. package/packages/director/src/brief.ts +122 -0
  105. package/packages/director/src/catalog.ts +49 -0
  106. package/packages/director/src/content.ts +343 -0
  107. package/packages/director/src/fallback.ts +58 -0
  108. package/packages/director/src/index.ts +13 -0
  109. package/packages/director/src/pattern.ts +90 -0
  110. package/packages/director/src/patterns/fill/cells.ts +115 -0
  111. package/packages/director/src/patterns/formula/stack.ts +93 -0
  112. package/packages/director/src/patterns/util.ts +58 -0
  113. package/packages/director/src/scene.ts +62 -0
  114. package/packages/director/src/select.ts +163 -0
  115. package/packages/director/src/transitions.ts +115 -0
  116. package/packages/gen/PROVIDERS.md +90 -0
  117. package/packages/gen/package.json +9 -0
  118. package/packages/gen/src/ask.ts +132 -0
  119. package/packages/gen/src/audio.ts +91 -0
  120. package/packages/gen/src/blender.ts +54 -0
  121. package/packages/gen/src/capabilities.ts +151 -0
  122. package/packages/gen/src/generate.ts +206 -0
  123. package/packages/gen/src/image/codex.ts +117 -0
  124. package/packages/gen/src/image/og.ts +133 -0
  125. package/packages/gen/src/index.ts +26 -0
  126. package/packages/gen/src/keys.ts +86 -0
  127. package/packages/gen/src/media.ts +150 -0
  128. package/packages/gen/src/music/elevenlabs.ts +41 -0
  129. package/packages/gen/src/music/index.ts +120 -0
  130. package/packages/gen/src/music/library.ts +73 -0
  131. package/packages/gen/src/music/lyria.ts +52 -0
  132. package/packages/gen/src/music/types.ts +28 -0
  133. package/packages/gen/src/plan.ts +106 -0
  134. package/packages/gen/src/registry.ts +54 -0
  135. package/packages/gen/src/seam.ts +103 -0
  136. package/packages/gen/src/tts/dashscope.ts +64 -0
  137. package/packages/gen/src/tts/elevenlabs.ts +84 -0
  138. package/packages/gen/src/tts/gemini.ts +73 -0
  139. package/packages/gen/src/tts/index.ts +241 -0
  140. package/packages/gen/src/tts/local.ts +42 -0
  141. package/packages/gen/src/tts/types.ts +44 -0
  142. package/packages/gen/src/types.ts +90 -0
  143. package/packages/gen/src/video/fal.ts +140 -0
  144. package/packages/gen/src/video/h3.ts +112 -0
  145. package/packages/gen/src/video/http.ts +44 -0
  146. package/packages/gen/src/video/ltx.ts +60 -0
  147. package/packages/gen/src/video/minimax.ts +55 -0
  148. package/packages/gen/src/video/veo.ts +71 -0
  149. package/packages/jizura/package.json +16 -0
  150. package/packages/jizura/src/deny.ts +73 -0
  151. package/packages/jizura/src/index.ts +7 -0
  152. package/packages/jizura/src/install.ts +269 -0
  153. package/packages/jizura/src/layer.ts +325 -0
  154. package/packages/jizura/src/lyrics.ts +81 -0
  155. package/packages/jizura/src/pin.ts +8 -0
  156. package/packages/jizura/src/vendor.ts +140 -0
  157. package/packages/jizura/src/vite.ts +63 -0
  158. package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
  159. package/packages/jizura/vendor/NOTICE +43 -0
  160. package/packages/jizura/vendor/engine.json +47 -0
  161. package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
  162. package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
  163. package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
  164. package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
  165. package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  166. package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
  167. package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
  168. package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
  169. package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
  170. package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
  171. package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
  172. package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
  173. package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
  174. package/packages/jizura/vendor/fonts/fonts.json +52 -0
  175. package/packages/jizura/vendor/jizura_engine.js +35024 -0
  176. package/packages/motion/README.md +153 -0
  177. package/packages/motion/package.json +30 -0
  178. package/packages/motion/src/camera.ts +97 -0
  179. package/packages/motion/src/captions.ts +487 -0
  180. package/packages/motion/src/enter.ts +128 -0
  181. package/packages/motion/src/hangul.ts +129 -0
  182. package/packages/motion/src/impact.ts +100 -0
  183. package/packages/motion/src/index.ts +17 -0
  184. package/packages/motion/src/moves/brushWipe.ts +103 -0
  185. package/packages/motion/src/moves/cloneArray.ts +111 -0
  186. package/packages/motion/src/moves/curtains.ts +81 -0
  187. package/packages/motion/src/moves/dress.ts +191 -0
  188. package/packages/motion/src/moves/echoStack.ts +70 -0
  189. package/packages/motion/src/moves/fit.ts +70 -0
  190. package/packages/motion/src/moves/flood.ts +61 -0
  191. package/packages/motion/src/moves/ghostStack.ts +88 -0
  192. package/packages/motion/src/moves/gridFloor.ts +100 -0
  193. package/packages/motion/src/moves/imageSwarm.ts +113 -0
  194. package/packages/motion/src/moves/index.ts +38 -0
  195. package/packages/motion/src/moves/irisBlades.ts +113 -0
  196. package/packages/motion/src/moves/karaokeLine.ts +108 -0
  197. package/packages/motion/src/moves/ledBoard.ts +94 -0
  198. package/packages/motion/src/moves/liquidTab.ts +72 -0
  199. package/packages/motion/src/moves/markerWrite.ts +63 -0
  200. package/packages/motion/src/moves/menu.ts +138 -0
  201. package/packages/motion/src/moves/morphBox.ts +96 -0
  202. package/packages/motion/src/moves/neonSign.ts +78 -0
  203. package/packages/motion/src/moves/panelPop.ts +78 -0
  204. package/packages/motion/src/moves/paperTear.ts +75 -0
  205. package/packages/motion/src/moves/path.ts +64 -0
  206. package/packages/motion/src/moves/pathText.ts +107 -0
  207. package/packages/motion/src/moves/pointSphere.ts +109 -0
  208. package/packages/motion/src/moves/ransomLetters.ts +73 -0
  209. package/packages/motion/src/moves/shapeMorph.ts +143 -0
  210. package/packages/motion/src/moves/slamWords.ts +96 -0
  211. package/packages/motion/src/moves/sparkTrail.ts +130 -0
  212. package/packages/motion/src/moves/speedLines.ts +91 -0
  213. package/packages/motion/src/moves/splitFlap.ts +149 -0
  214. package/packages/motion/src/moves/stepWords.ts +128 -0
  215. package/packages/motion/src/moves/strikeOut.ts +98 -0
  216. package/packages/motion/src/moves/sunburst.ts +58 -0
  217. package/packages/motion/src/moves/tickerRows.ts +91 -0
  218. package/packages/motion/src/moves/tokenPicker.ts +115 -0
  219. package/packages/motion/src/moves/truchet.ts +85 -0
  220. package/packages/motion/src/moves/wordWall.ts +78 -0
  221. package/packages/motion/src/moves/worldRoll.ts +32 -0
  222. package/packages/motion/src/particles.ts +102 -0
  223. package/packages/motion/src/react/index.tsx +21 -0
  224. package/packages/motion/src/react/parts.tsx +431 -0
  225. package/packages/motion/src/react/time.tsx +97 -0
  226. package/packages/motion/src/text.ts +103 -0
  227. package/packages/motion/src/transition.ts +82 -0
  228. package/packages/motion/src/transition3d/cards.ts +65 -0
  229. package/packages/motion/src/transition3d/chain.ts +84 -0
  230. package/packages/motion/src/transition3d/dive.ts +181 -0
  231. package/packages/motion/src/transition3d/fold.ts +110 -0
  232. package/packages/motion/src/transition3d/gl.ts +219 -0
  233. package/packages/motion/src/transition3d/index.ts +235 -0
  234. package/packages/motion/src/transition3d/live.ts +70 -0
  235. package/packages/motion/src/transition3d/portal.ts +115 -0
  236. package/packages/motion/src/transition3d/react.tsx +29 -0
  237. package/packages/motion/src/transition3d/rig.ts +600 -0
  238. package/packages/motion/src/transition3d/shatter.ts +104 -0
  239. package/packages/motion/src/transition3d/space.ts +173 -0
  240. package/packages/motion/src/transition3d/whip.ts +78 -0
  241. package/packages/motion/src/transition3d/world.ts +737 -0
  242. package/packages/motion/src/transition3d/zoomOut.ts +114 -0
  243. package/packages/motion/src/transitions2d/catalog.ts +127 -0
  244. package/packages/motion/src/transitions2d/css.d.ts +2 -0
  245. package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
  246. package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
  247. package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
  248. package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
  249. package/packages/motion/src/transitions2d/index.ts +2 -0
  250. package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
  251. package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
  252. package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
  253. package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
  254. package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
  255. package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
  256. package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
  257. package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
  258. package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
  259. package/packages/motion/src/transitions2d/react.ts +13 -0
  260. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
  261. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
  262. package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
  263. package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
  264. package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
  265. package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
  266. package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
  267. package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
  268. package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
  269. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
  270. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
  271. package/packages/motion/src/type.ts +131 -0
  272. package/packages/motion/src/ui.ts +172 -0
  273. package/packages/patterns/package.json +15 -0
  274. package/packages/patterns/src/bars/rank.ts +119 -0
  275. package/packages/patterns/src/before-after/shrink.ts +127 -0
  276. package/packages/patterns/src/before-after/strike.ts +125 -0
  277. package/packages/patterns/src/code/terminal.ts +133 -0
  278. package/packages/patterns/src/compare/bars.ts +142 -0
  279. package/packages/patterns/src/compare/duel.ts +103 -0
  280. package/packages/patterns/src/end/lockup.ts +85 -0
  281. package/packages/patterns/src/end/slam.ts +91 -0
  282. package/packages/patterns/src/flow/graph.ts +250 -0
  283. package/packages/patterns/src/hook-number/counter.ts +101 -0
  284. package/packages/patterns/src/hook-number/roll.ts +179 -0
  285. package/packages/patterns/src/hook-number/slam.ts +91 -0
  286. package/packages/patterns/src/index.ts +38 -0
  287. package/packages/patterns/src/kit.ts +449 -0
  288. package/packages/patterns/src/limit/gauge.ts +117 -0
  289. package/packages/patterns/src/list/cards.ts +131 -0
  290. package/packages/patterns/src/list/cycle.ts +22 -0
  291. package/packages/patterns/src/media/panels.ts +100 -0
  292. package/packages/patterns/src/objects.ts +234 -0
  293. package/packages/patterns/src/quote/karaoke.ts +126 -0
  294. package/packages/patterns/src/quote/slam.ts +22 -0
  295. package/packages/patterns/src/screen/focus.ts +85 -0
  296. package/packages/patterns/src/steps/path.ts +137 -0
  297. package/packages/patterns/src/steps/stack.ts +111 -0
  298. package/packages/patterns/src/term-reveal/chips.ts +92 -0
  299. package/packages/patterns/src/term-reveal/expand.ts +127 -0
  300. package/packages/patterns/src/timeline/axis.ts +144 -0
  301. package/packages/patterns/src/title/mask.ts +85 -0
  302. package/packages/patterns/src/title/slam.ts +23 -0
  303. package/packages/patterns/src/toggle-sim/switch.ts +92 -0
  304. package/packages/patterns/src/trend/columns.ts +108 -0
  305. package/packages/patterns/src/trend/draw.ts +140 -0
  306. package/packages/patterns/src/types.ts +67 -0
  307. package/packages/patterns/src/words/moves.ts +42 -0
  308. package/packages/patterns/src/words/slam.ts +4 -0
  309. package/packages/plan/package.json +15 -0
  310. package/packages/plan/src/asks.ts +256 -0
  311. package/packages/plan/src/brief.ts +383 -0
  312. package/packages/plan/src/captions.ts +78 -0
  313. package/packages/plan/src/detect.ts +93 -0
  314. package/packages/plan/src/index.ts +27 -0
  315. package/packages/plan/src/layout.ts +176 -0
  316. package/packages/plan/src/llm.ts +172 -0
  317. package/packages/plan/src/look.ts +222 -0
  318. package/packages/plan/src/plan.ts +179 -0
  319. package/packages/plan/src/request.ts +55 -0
  320. package/packages/plan/src/series.ts +76 -0
  321. package/packages/plan/src/studio.ts +53 -0
  322. package/packages/plan/src/templates.ts +42 -0
  323. package/packages/plan/src/themes.ts +48 -0
  324. package/packages/plan/src/types.ts +157 -0
  325. package/packages/studio/package.json +12 -0
  326. package/packages/studio/src/index.ts +1 -0
  327. package/packages/studio/src/plugin.ts +236 -0
  328. package/packages/studio/src/ui/main.ts +503 -0
  329. package/packages/studio/src/ui/studio.css +165 -0
  330. package/packages/studio/src/ui/timeline.ts +317 -0
  331. package/packages/themes/kit/THEME-MAKE.md +68 -0
  332. package/packages/themes/kit/THEME.md +30 -0
  333. package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
  334. package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
  335. package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
  336. package/packages/themes/kit/effects.ts +5 -0
  337. package/packages/themes/kit/sample/index.html +11 -0
  338. package/packages/themes/kit/sample/main.ts +9 -0
  339. package/packages/themes/kit/sample/scenes/scene.ts +15 -0
  340. package/packages/themes/package.json +19 -0
  341. package/packages/themes/src/factory.ts +296 -0
  342. package/packages/themes/src/fx.ts +14 -0
  343. package/packages/themes/src/index.ts +114 -0
  344. package/packages/themes/src/slotkit.ts +79 -0
  345. package/packages/themes/src/slots.ts +46 -0
  346. package/packages/themes/src/stage3d.ts +197 -0
  347. package/packages/themes/src/types.ts +58 -0
  348. package/presets/kawaii-pastel.json +94 -0
  349. package/presets/neutral-dark.json +82 -0
  350. package/presets/og.json +98 -0
  351. package/series/og-shorts.json +14 -0
  352. package/skills/kasaframe/SKILL.md +33 -0
  353. package/templates/_shared/project/index.html +11 -0
  354. package/templates/_shared/project/lib/react.ts +22 -0
  355. package/templates/_shared/project/lib/stage.ts +292 -0
  356. package/templates/_shared/project/main.ts +9 -0
  357. package/templates/_shared/scenes/default.ts +21 -0
  358. package/templates/_shared/scenes/drop.ts +33 -0
  359. package/templates/_shared/scenes/end.ts +26 -0
  360. package/templates/_shared/scenes/hook.ts +18 -0
  361. package/templates/_shared/scenes/seam.ts +41 -0
  362. package/templates/_shared/scenes/step.ts +49 -0
  363. package/templates/ad/preview.png +0 -0
  364. package/templates/ad/template.json +151 -0
  365. package/templates/demo/preview.png +0 -0
  366. package/templates/demo/template.json +141 -0
  367. package/templates/explainer/preview.png +0 -0
  368. package/templates/explainer/template.json +193 -0
  369. package/templates/launch/preview.png +0 -0
  370. package/templates/launch/template.json +163 -0
  371. package/templates/mv/preview.png +0 -0
  372. package/templates/mv/scenes/default.ts +98 -0
  373. package/templates/mv/template.json +178 -0
  374. package/templates/promo/preview.png +0 -0
  375. package/templates/promo/template.json +185 -0
  376. package/templates/shorts/preview.png +0 -0
  377. package/templates/shorts/scenes/hook.ts +37 -0
  378. package/templates/shorts/template.json +196 -0
  379. package/templates/square/preview.png +0 -0
  380. package/templates/square/template.json +133 -0
  381. package/themes/debug-hud/THEME.md +39 -0
  382. package/themes/debug-hud/effects.ts +389 -0
  383. package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
  384. package/themes/debug-hud/sample/beats.json +1 -0
  385. package/themes/debug-hud/sample/index.html +11 -0
  386. package/themes/debug-hud/sample/kf.json +59 -0
  387. package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
  388. package/themes/debug-hud/sample/lib/og.ts +29 -0
  389. package/themes/debug-hud/sample/main.ts +9 -0
  390. package/themes/debug-hud/sample/scenes/end.ts +62 -0
  391. package/themes/debug-hud/sample/scenes/gather.ts +67 -0
  392. package/themes/debug-hud/sample/scenes/models.ts +70 -0
  393. package/themes/debug-hud/sample/scenes/price.ts +79 -0
  394. package/themes/debug-hud/sample/style.json +91 -0
  395. package/themes/debug-hud/slots.ts +99 -0
  396. package/themes/debug-hud/still.png +0 -0
  397. package/themes/debug-hud/style.json +91 -0
  398. package/themes/debug-hud/theme.json +68 -0
  399. package/themes/og-measure/THEME.md +47 -0
  400. package/themes/og-measure/effects.ts +432 -0
  401. package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
  402. package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
  403. package/themes/og-measure/sample/assets/data/measure.json +353 -0
  404. package/themes/og-measure/sample/beats.json +1 -0
  405. package/themes/og-measure/sample/index.html +11 -0
  406. package/themes/og-measure/sample/kf.json +66 -0
  407. package/themes/og-measure/sample/lib/data.ts +62 -0
  408. package/themes/og-measure/sample/main.ts +9 -0
  409. package/themes/og-measure/sample/measure.ts +78 -0
  410. package/themes/og-measure/sample/scenes/end.ts +47 -0
  411. package/themes/og-measure/sample/scenes/gather.ts +51 -0
  412. package/themes/og-measure/sample/scenes/models.ts +34 -0
  413. package/themes/og-measure/sample/scenes/one.ts +39 -0
  414. package/themes/og-measure/sample/scenes/price.ts +56 -0
  415. package/themes/og-measure/sample/style.json +97 -0
  416. package/themes/og-measure/slots.ts +88 -0
  417. package/themes/og-measure/still.png +0 -0
  418. package/themes/og-measure/style.json +97 -0
  419. package/themes/og-measure/theme.json +79 -0
  420. package/themes/riso-zine/THEME.md +43 -0
  421. package/themes/riso-zine/effects.ts +321 -0
  422. package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
  423. package/themes/riso-zine/sample/beats.json +1 -0
  424. package/themes/riso-zine/sample/index.html +11 -0
  425. package/themes/riso-zine/sample/kf.json +59 -0
  426. package/themes/riso-zine/sample/lib/og.ts +29 -0
  427. package/themes/riso-zine/sample/lib/shot.ts +86 -0
  428. package/themes/riso-zine/sample/lib/zine.ts +42 -0
  429. package/themes/riso-zine/sample/main.ts +9 -0
  430. package/themes/riso-zine/sample/scenes/end.ts +61 -0
  431. package/themes/riso-zine/sample/scenes/gather.ts +62 -0
  432. package/themes/riso-zine/sample/scenes/models.ts +52 -0
  433. package/themes/riso-zine/sample/scenes/price.ts +82 -0
  434. package/themes/riso-zine/sample/style.json +99 -0
  435. package/themes/riso-zine/slots.ts +81 -0
  436. package/themes/riso-zine/still.png +0 -0
  437. package/themes/riso-zine/style.json +99 -0
  438. package/themes/riso-zine/theme.json +75 -0
  439. package/tools/beats/beats.py +372 -0
  440. package/tools/beats/requirements.txt +3 -0
  441. package/tools/lyrics/align.py +164 -0
  442. package/tools/lyrics/requirements.txt +9 -0
@@ -0,0 +1,431 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { combine, toCss, type Style } from "@kasaframe/core";
3
+ import { enter, inOut, type MoveName, type MoveOpts } from "../enter.ts";
4
+ import { typeText, type TypeOpts } from "../type.ts";
5
+ import { counter, letters, sweep, words, type CounterOpts, type SweepOpts } from "../text.ts";
6
+ import { transition, type TransitionName, type TransitionOpts } from "../transition.ts";
7
+ import { beatPulse, dropBurst } from "../impact.ts";
8
+ import { breathe, camera, type BreatheOpts, type CameraOpts } from "../camera.ts";
9
+ import { burst, sparkles, type BurstOpts, type SparkleOpts } from "../particles.ts";
10
+ import { codeReveal, cursor, grow, safeText, terminal, type CursorKey, type TermStep } from "../ui.ts";
11
+ import { useBeats, useCtx, useSceneId, useT } from "./time.tsx";
12
+
13
+ const v = (name: string, fallback: string) => `var(--kf-${name}, ${fallback})`;
14
+ const C = {
15
+ ink: v("ink", "#111"),
16
+ dim: v("dim", "#666"),
17
+ accent: v("accent", "#4A90E2"),
18
+ accentText: v("accent-text", "#2F6FC0"),
19
+ accentSoft: v("accent-soft", "rgba(74,144,226,0.35)"),
20
+ surface: v("surface", "#fff"),
21
+ raised: v("raised", "#f3f5f8"),
22
+ line: v("line", "#dde3ea"),
23
+ bg: v("bg", "#fff"),
24
+ sans: v("font-sans", "sans-serif"),
25
+ mono: v("font-mono", "monospace"),
26
+ display: v("font-display", "sans-serif"),
27
+ };
28
+
29
+ interface Timed {
30
+ /** 장면 안 시각. 안 주면 감싼 KfScene·Sequence 의 시각 */
31
+ t?: number;
32
+ }
33
+
34
+ interface Boxed {
35
+ style?: CSSProperties;
36
+ className?: string;
37
+ children?: ReactNode;
38
+ }
39
+
40
+ function Moved(props: Boxed & { s: Style; as?: "div" | "span" }) {
41
+ const Tag = props.as ?? "div";
42
+ const display = props.as === "span" ? "inline-block" : undefined;
43
+ return (
44
+ <Tag className={props.className} style={{ display, ...props.style, ...toCss(props.s) }}>
45
+ {props.children}
46
+ </Tag>
47
+ );
48
+ }
49
+
50
+ /** 들어온다. out 을 주면 그때 나간다 */
51
+ export function Enter(props: Timed & Boxed & MoveOpts & { move?: MoveName; out?: number; as?: "div" | "span" }) {
52
+ const t = useT(props.t);
53
+ const scene = useSceneId();
54
+ const ctx = useCtx();
55
+ const { move = (ctx?.style.motion.enter as MoveName | undefined) ?? "rise", style, className, children, as, ...o } = props;
56
+ const s = o.out === undefined ? enter(move, t, { scene, ...o }) : inOut(move, t, { scene, ...o });
57
+ return (
58
+ <Moved s={s} style={style} className={className} as={as}>
59
+ {children}
60
+ </Moved>
61
+ );
62
+ }
63
+
64
+ /** 한글 자모 단위 타자 */
65
+ export function Typewriter(props: Timed & TypeOpts & { text: string; caret?: boolean; caretColor?: string; style?: CSSProperties; className?: string }) {
66
+ const t = useT(props.t);
67
+ const st = typeText(safeText(props.text), t, props);
68
+ return (
69
+ <span className={props.className} style={{ whiteSpace: "pre-wrap", ...props.style }}>
70
+ {st.text}
71
+ {props.caret !== false && <Caret on={st.caret} color={props.caretColor} />}
72
+ </span>
73
+ );
74
+ }
75
+
76
+ export function Caret(props: { on: boolean; color?: string; mono?: boolean }) {
77
+ return (
78
+ <span
79
+ aria-hidden
80
+ style={{
81
+ display: "inline-block",
82
+ width: props.mono ? "0.6em" : "0.08em",
83
+ minWidth: 2,
84
+ height: "1.05em",
85
+ marginLeft: "0.06em",
86
+ verticalAlign: "-0.15em",
87
+ background: props.color ?? C.accent,
88
+ opacity: props.on ? 1 : 0,
89
+ }}
90
+ />
91
+ );
92
+ }
93
+
94
+ export function Counter(props: Timed & CounterOpts & { style?: CSSProperties; className?: string }) {
95
+ const t = useT(props.t);
96
+ return (
97
+ <span className={props.className} style={{ fontVariantNumeric: "tabular-nums", ...props.style }}>
98
+ {counter(t, props)}
99
+ </span>
100
+ );
101
+ }
102
+
103
+ /** 낱말이 차례로 들어온다 */
104
+ export function Words(props: Timed & MoveOpts & { text: string; move?: MoveName; each?: number; gap?: string; style?: CSSProperties; className?: string; by?: "word" | "letter" }) {
105
+ const t = useT(props.t);
106
+ const scene = useSceneId();
107
+ const list = (props.by === "letter" ? letters : words)(safeText(props.text), t, { scene, ...props });
108
+ return (
109
+ <span className={props.className} style={{ display: "inline-flex", flexWrap: "wrap", columnGap: props.by === "letter" ? 0 : (props.gap ?? "0.28em"), ...props.style }}>
110
+ {list.map((w) => (
111
+ <span key={w.index} style={{ display: "inline-block", whiteSpace: "pre", ...toCss(w.style) }}>
112
+ {w.text}
113
+ </span>
114
+ ))}
115
+ </span>
116
+ );
117
+ }
118
+
119
+ /** 형광펜 쓸기 — 글 뒤에 강조 띠가 지나간다 */
120
+ export function Highlight(props: Timed & SweepOpts & { color?: string; children?: ReactNode; style?: CSSProperties }) {
121
+ const t = useT(props.t);
122
+ const s = sweep(t, props);
123
+ return (
124
+ <span style={{ position: "relative", display: "inline-block", ...props.style }}>
125
+ <span aria-hidden style={{ position: "absolute", left: "-0.12em", right: "-0.12em", top: "52%", bottom: "4%", background: props.color ?? C.accentSoft, borderRadius: "0.12em", ...toCss(s) }} />
126
+ <span style={{ position: "relative" }}>{props.children}</span>
127
+ </span>
128
+ );
129
+ }
130
+
131
+ /** 두 장면 사이 전환. from·to 를 겹쳐 그리고 전환 밖에서는 한쪽만 */
132
+ export function Transition(props: Timed & Omit<TransitionOpts, "scene"> & { name: TransitionName; from: ReactNode; to: ReactNode; style?: CSSProperties }) {
133
+ const t = useT(props.t);
134
+ const scene = useSceneId();
135
+ const s = transition(props.name, t, { scene, ...props });
136
+ const showOut = t < props.at || s.both;
137
+ const showIn = t >= props.at;
138
+ const layer: CSSProperties = { position: "absolute", inset: 0 };
139
+ return (
140
+ <div style={{ position: "relative", width: "100%", height: "100%", overflow: "hidden", ...props.style }}>
141
+ {showOut && <div style={{ ...layer, ...toCss(s.out) }}>{props.from}</div>}
142
+ {showIn && <div style={{ ...layer, ...toCss(s.in) }}>{props.to}</div>}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ /** 카메라 틀 — 안의 내용을 밀거나 민다 */
148
+ export function Camera(props: Timed & Boxed & Omit<CameraOpts, "scene">) {
149
+ const t = useT(props.t);
150
+ const scene = useSceneId();
151
+ const s = camera(t, { scene, ...props });
152
+ return (
153
+ <Moved s={s} style={{ position: "absolute", inset: 0, transformOrigin: "50% 50%", ...props.style }} className={props.className}>
154
+ {props.children}
155
+ </Moved>
156
+ );
157
+ }
158
+
159
+ /** 숨 쉬는 틀 — 가만히 있는 화면이 멈춤으로 찍히지 않게. 장면 내용 전체를 감싼다 */
160
+ export function Breathe(props: Timed & Boxed & BreatheOpts) {
161
+ const t = useT(props.t);
162
+ const scene = useSceneId();
163
+ const s = breathe(t, { seed: scene, ...props });
164
+ return (
165
+ <Moved s={s} style={{ position: "absolute", inset: 0, transformOrigin: "50% 50%", ...props.style }} className={props.className}>
166
+ {props.children}
167
+ </Moved>
168
+ );
169
+ }
170
+
171
+ /** 박 펄스 — style.json 규칙을 따른다(기본은 드롭 구간만) */
172
+ export function BeatPulse(props: Timed & Boxed & { amount?: number; every?: "beat" | "downbeat" | "bar" | number; source?: string; as?: "div" | "span" }) {
173
+ const t = useT(props.t);
174
+ const beats = useBeats();
175
+ const ctx = useCtx();
176
+ const scene = useSceneId();
177
+ const s = beats ? beatPulse(t, { beats, amount: props.amount, every: props.every, rules: ctx?.style.motion, scene, source: props.source }) : {};
178
+ return (
179
+ <Moved s={s} style={props.style} className={props.className} as={props.as}>
180
+ {props.children}
181
+ </Moved>
182
+ );
183
+ }
184
+
185
+ /**
186
+ * 드롭 터짐 — 섬광·충격파·흔들림. 드롭 번호로만 건다(드롭이 아닌 곳에서 터지지 않게).
187
+ * children 은 흔들리고, 섬광·고리는 위에 덮인다.
188
+ */
189
+ export function DropBurst(props: Timed & Boxed & { drop: number; color?: string; ring?: boolean; px?: number; source?: string }) {
190
+ const t = useT(props.t);
191
+ const beats = useBeats();
192
+ const ctx = useCtx();
193
+ const scene = useSceneId();
194
+ if (!beats) return <>{props.children}</>;
195
+ const b = dropBurst(t, { beats, drop: props.drop, rules: ctx?.style.motion, px: props.px, scene, source: props.source });
196
+ const color = props.color ?? "#fff";
197
+ return (
198
+ <div className={props.className} style={{ position: "absolute", inset: 0, overflow: "hidden", ...props.style }}>
199
+ <Moved s={combine(b.shake, b.punch)} style={{ position: "absolute", inset: 0 }}>
200
+ {props.children}
201
+ </Moved>
202
+ {props.ring !== false && b.ring.opacity > 0.01 && (
203
+ <div
204
+ aria-hidden
205
+ style={{
206
+ position: "absolute",
207
+ left: "50%",
208
+ top: "50%",
209
+ width: "60vmin",
210
+ height: "60vmin",
211
+ marginLeft: "-30vmin",
212
+ marginTop: "-30vmin",
213
+ borderRadius: "50%",
214
+ border: `0.9vmin solid ${C.accent}`,
215
+ ...toCss({ scale: b.ring.scale, opacity: b.ring.opacity }),
216
+ }}
217
+ />
218
+ )}
219
+ {b.flash > 0.01 && <div aria-hidden style={{ position: "absolute", inset: 0, background: color, opacity: b.flash * 0.85 }} />}
220
+ </div>
221
+ );
222
+ }
223
+
224
+ const STAR = "M12 0 L14.2 9.8 L24 12 L14.2 14.2 L12 24 L9.8 14.2 L0 12 L9.8 9.8 Z";
225
+
226
+ /** 반짝이 — 시드 고정 자리에서 깜빡인다 */
227
+ export function Sparkles(props: Timed & Partial<SparkleOpts> & { colors?: string[]; size?: number; style?: CSSProperties }) {
228
+ const t = useT(props.t);
229
+ const ctx = useCtx();
230
+ const w = props.width ?? ctx?.meta.width ?? 1920;
231
+ const h = props.height ?? ctx?.meta.height ?? 1080;
232
+ const list = sparkles(t, { ...props, width: w, height: h });
233
+ const colors = props.colors ?? ["#fff"];
234
+ const size = props.size ?? 28;
235
+ return (
236
+ <svg aria-hidden width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ position: "absolute", inset: 0, pointerEvents: "none", ...props.style }}>
237
+ {list.map((p) =>
238
+ p.opacity < 0.02 ? null : (
239
+ <path
240
+ key={p.i}
241
+ d={STAR}
242
+ fill={colors[Math.floor(p.hue * colors.length) % colors.length]}
243
+ opacity={p.opacity}
244
+ transform={`translate(${p.x.toFixed(1)} ${p.y.toFixed(1)}) rotate(${p.rotate.toFixed(1)}) scale(${((p.scale * size) / 24).toFixed(3)}) translate(-12 -12)`}
245
+ />
246
+ ),
247
+ )}
248
+ </svg>
249
+ );
250
+ }
251
+
252
+ /** 한 점에서 터지는 조각 */
253
+ export function Burst(props: Timed & BurstOpts & { colors?: string[]; size?: number; shape?: "dot" | "star"; style?: CSSProperties }) {
254
+ const t = useT(props.t);
255
+ const ctx = useCtx();
256
+ const w = ctx?.meta.width ?? 1920;
257
+ const h = ctx?.meta.height ?? 1080;
258
+ const list = burst(t, props);
259
+ const colors = props.colors ?? ["#fff"];
260
+ const size = props.size ?? 14;
261
+ return (
262
+ <svg aria-hidden width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ position: "absolute", inset: 0, pointerEvents: "none", ...props.style }}>
263
+ {list.map((p) => {
264
+ const fill = colors[Math.floor(p.hue * colors.length) % colors.length];
265
+ return props.shape === "star" ? (
266
+ <path key={p.i} d={STAR} fill={fill} opacity={p.opacity} transform={`translate(${p.x.toFixed(1)} ${p.y.toFixed(1)}) rotate(${p.rotate.toFixed(1)}) scale(${((p.scale * size) / 24).toFixed(3)}) translate(-12 -12)`} />
267
+ ) : (
268
+ <circle key={p.i} cx={p.x.toFixed(1)} cy={p.y.toFixed(1)} r={((p.scale * size) / 2).toFixed(2)} fill={fill} opacity={p.opacity} />
269
+ );
270
+ })}
271
+ </svg>
272
+ );
273
+ }
274
+
275
+ /** 화살표 커서 + 누른 자리 물결 */
276
+ export function Cursor(props: Timed & { keys: CursorKey[]; size?: number; color?: string }) {
277
+ const t = useT(props.t);
278
+ const c = cursor(t, props.keys);
279
+ const size = props.size ?? 44;
280
+ return (
281
+ <>
282
+ {c.ripple && (
283
+ <div
284
+ aria-hidden
285
+ style={{
286
+ position: "absolute",
287
+ left: c.ripple.x - size,
288
+ top: c.ripple.y - size,
289
+ width: size * 2,
290
+ height: size * 2,
291
+ borderRadius: "50%",
292
+ background: props.color ?? C.accent,
293
+ ...toCss({ scale: c.ripple.scale, opacity: c.ripple.opacity }),
294
+ }}
295
+ />
296
+ )}
297
+ <svg aria-hidden width={size} height={size} viewBox="0 0 24 24" style={{ position: "absolute", left: c.x, top: c.y, transform: `scale(${c.pressed ? 0.86 : 1})`, transformOrigin: "0 0", filter: "drop-shadow(0 2px 3px rgba(0,0,0,0.3))" }}>
298
+ <path d="M2 1 L2 19 L7 14.5 L10.5 22 L13.5 20.6 L10 13.2 L17 13 Z" fill="#fff" stroke="#111" strokeWidth={1.4} strokeLinejoin="round" />
299
+ </svg>
300
+ </>
301
+ );
302
+ }
303
+
304
+ /** 터미널 창 + 입력. 창은 style.json 의 term* 색이 있으면 그걸로 */
305
+ export function Terminal(props: Timed & { steps: TermStep[]; title?: string; at?: number; prompt?: string; maxLines?: number; cps?: number; fontSize?: number; width?: number | string; height?: number | string; style?: CSSProperties }) {
306
+ const t = useT(props.t);
307
+ const st = terminal(t, props.steps, { at: props.at, prompt: props.prompt, maxLines: props.maxLines, cps: props.cps });
308
+ const bg = v("term-surface", "#1A1D23");
309
+ const fg = v("term-text", "#ECEEF3");
310
+ const dim = v("term-dim", "#A0A6B0");
311
+ const acc = v("term-accent", "#5A8CE6");
312
+ const fs = props.fontSize ?? 30;
313
+ return (
314
+ <div
315
+ style={{
316
+ width: props.width ?? 1100,
317
+ height: props.height,
318
+ background: bg,
319
+ color: fg,
320
+ borderRadius: v("radius-md", "18px"),
321
+ boxShadow: v("shadow-soft", "0 8px 24px rgba(0,0,0,0.25)"),
322
+ overflow: "hidden",
323
+ fontFamily: C.mono,
324
+ fontSize: fs,
325
+ lineHeight: 1.45,
326
+ ...props.style,
327
+ }}
328
+ >
329
+ <div style={{ display: "flex", alignItems: "center", gap: fs * 0.3, padding: `${fs * 0.45}px ${fs * 0.6}px`, borderBottom: `1px solid rgba(255,255,255,0.08)` }}>
330
+ {["#FF5F57", "#FEBC2E", "#28C840"].map((c) => (
331
+ <span key={c} style={{ width: fs * 0.42, height: fs * 0.42, borderRadius: "50%", background: c }} />
332
+ ))}
333
+ {props.title && <span style={{ marginLeft: fs * 0.4, color: dim, fontFamily: C.sans, fontSize: fs * 0.62 }}>{props.title}</span>}
334
+ </div>
335
+ <div style={{ padding: `${fs * 0.5}px ${fs * 0.7}px` }}>
336
+ {st.lines.map((l, i) => (
337
+ <div key={i} style={{ whiteSpace: "pre-wrap", color: l.kind === "out" ? dim : fg, minHeight: "1.45em" }}>
338
+ {l.kind === "cmd" && <span style={{ color: acc, marginRight: "0.6em" }}>{l.prompt}</span>}
339
+ {l.text}
340
+ {l.caret && <Caret on={l.text.length > 0 || Math.floor((t + 10) / 0.53) % 2 === 0} color={acc} mono />}
341
+ </div>
342
+ ))}
343
+ </div>
344
+ </div>
345
+ );
346
+ }
347
+
348
+ export function CodeBlock(props: Timed & { code: string; at?: number; mode?: "line" | "type"; lps?: number; fontSize?: number; style?: CSSProperties }) {
349
+ const t = useT(props.t);
350
+ const st = codeReveal(props.code, t, props);
351
+ const fs = props.fontSize ?? 28;
352
+ return (
353
+ <pre style={{ margin: 0, padding: fs * 0.9, background: C.raised, color: C.ink, borderRadius: v("radius-md", "12px"), fontFamily: C.mono, fontSize: fs, lineHeight: 1.5, ...props.style }}>
354
+ {st.lines.map((l, i) => (
355
+ <div key={i} style={{ minHeight: "1.5em", background: i === st.active ? C.accentSoft : "transparent", marginInline: -fs * 0.4, paddingInline: fs * 0.4, borderRadius: 4 }}>
356
+ {l}
357
+ </div>
358
+ ))}
359
+ </pre>
360
+ );
361
+ }
362
+
363
+ /** 막대 차트가 자란다 */
364
+ export function BarChart(props: Timed & { values: number[]; labels?: string[]; max?: number; at?: number; dur?: number; stagger?: number; width?: number; height?: number; color?: string; format?: (v: number) => string; style?: CSSProperties }) {
365
+ const t = useT(props.t);
366
+ const cur = grow(t, props.values, props);
367
+ const max = props.max ?? Math.max(1, ...props.values.filter((x) => Number.isFinite(x)));
368
+ const w = props.width ?? 900;
369
+ const h = props.height ?? 420;
370
+ const gap = 0.28;
371
+ const bw = w / (props.values.length * (1 + gap));
372
+ return (
373
+ <div style={{ position: "relative", width: w, height: h + 60, fontFamily: C.sans, ...props.style }}>
374
+ {cur.map((x, i) => {
375
+ const bh = (Math.max(0, x) / max) * h;
376
+ const left = i * bw * (1 + gap) + (bw * gap) / 2;
377
+ return (
378
+ <div key={i}>
379
+ <div style={{ position: "absolute", left, width: bw, bottom: 60, height: bh, background: props.color ?? C.accent, borderRadius: `${Math.min(12, bw / 4)}px ${Math.min(12, bw / 4)}px 0 0` }} />
380
+ <div style={{ position: "absolute", left, width: bw, bottom: 60 + bh + 8, textAlign: "center", color: C.ink, fontVariantNumeric: "tabular-nums", fontSize: 26, opacity: bh > 1 ? 1 : 0 }}>
381
+ {props.format ? safeText(props.format(x)) : Math.round(x).toLocaleString("en-US")}
382
+ </div>
383
+ {props.labels?.[i] !== undefined && (
384
+ <div style={{ position: "absolute", left: left - 20, width: bw + 40, bottom: 12, textAlign: "center", color: C.dim, fontSize: 24 }}>{safeText(props.labels[i])}</div>
385
+ )}
386
+ </div>
387
+ );
388
+ })}
389
+ </div>
390
+ );
391
+ }
392
+
393
+ /** 기기 틀 — 안에 화면을 넣는다. 크기는 바깥 너비 기준 */
394
+ export function DeviceFrame(props: { kind?: "phone" | "laptop" | "browser"; width?: number; url?: string; children?: ReactNode; style?: CSSProperties }) {
395
+ const kind = props.kind ?? "browser";
396
+ const w = props.width ?? (kind === "phone" ? 420 : 1280);
397
+ if (kind === "phone") {
398
+ const h = w * 2.08;
399
+ return (
400
+ <div style={{ position: "relative", width: w, height: h, borderRadius: w * 0.14, background: "#111", padding: w * 0.035, boxSizing: "border-box", boxShadow: v("shadow-lift", "0 20px 50px rgba(0,0,0,0.35)"), ...props.style }}>
401
+ <div style={{ position: "relative", width: "100%", height: "100%", borderRadius: w * 0.11, overflow: "hidden", background: C.bg }}>
402
+ {props.children}
403
+ <div style={{ position: "absolute", top: w * 0.03, left: "50%", width: w * 0.3, height: w * 0.075, marginLeft: -w * 0.15, borderRadius: w, background: "#111" }} />
404
+ </div>
405
+ </div>
406
+ );
407
+ }
408
+ const bar = Math.max(28, w * 0.034);
409
+ const screen = (
410
+ <div style={{ borderRadius: kind === "laptop" ? w * 0.012 : w * 0.01, overflow: "hidden", background: C.surface, border: `1px solid ${C.line}`, boxShadow: v("shadow-lift", "0 20px 50px rgba(0,0,0,0.25)") }}>
411
+ <div style={{ height: bar, display: "flex", alignItems: "center", gap: bar * 0.25, padding: `0 ${bar * 0.45}px`, background: C.raised, borderBottom: `1px solid ${C.line}` }}>
412
+ {["#FF5F57", "#FEBC2E", "#28C840"].map((c) => (
413
+ <span key={c} style={{ width: bar * 0.36, height: bar * 0.36, borderRadius: "50%", background: c }} />
414
+ ))}
415
+ {props.url !== undefined && (
416
+ <span style={{ marginLeft: bar * 0.6, flex: 1, maxWidth: "60%", height: bar * 0.6, borderRadius: bar, background: C.surface, color: C.dim, fontFamily: C.sans, fontSize: bar * 0.42, display: "flex", alignItems: "center", paddingLeft: bar * 0.4 }}>
417
+ {safeText(props.url)}
418
+ </span>
419
+ )}
420
+ </div>
421
+ <div style={{ position: "relative", width: "100%", aspectRatio: "16 / 10" }}>{props.children}</div>
422
+ </div>
423
+ );
424
+ if (kind === "browser") return <div style={{ width: w, ...props.style }}>{screen}</div>;
425
+ return (
426
+ <div style={{ width: w, ...props.style }}>
427
+ <div style={{ padding: w * 0.018, background: "#1d1f24", borderRadius: `${w * 0.025}px ${w * 0.025}px 0 0` }}>{screen}</div>
428
+ <div style={{ height: w * 0.025, margin: `0 ${-w * 0.06}px`, background: "linear-gradient(#d9dbe0, #a9acb3)", borderRadius: `0 0 ${w * 0.04}px ${w * 0.04}px` }} />
429
+ </div>
430
+ );
431
+ }
@@ -0,0 +1,97 @@
1
+ import { createContext, useContext, useSyncExternalStore, type CSSProperties, type ReactNode } from "react";
2
+ import { frameTime, onFrame, safeTime, type Beats, type SceneCtx } from "@kasaframe/core";
3
+
4
+ interface TimeValue {
5
+ /** 지금 상자 안 시각(초) */
6
+ t: number;
7
+ ctx: SceneCtx | null;
8
+ /** 장면 id — 움직임 이벤트에 붙인다 */
9
+ scene?: string;
10
+ }
11
+
12
+ const TimeContext = createContext<TimeValue | null>(null);
13
+
14
+ /**
15
+ * 장면 맨 위에 둔다. 어댑터가 넘긴 { t, ctx } 를 아래 부품이 useTime()·useCtx() 로 받는다.
16
+ * 감싸지 않으면 부품은 페이지 전체 시각(frameTime)을 쓴다.
17
+ */
18
+ export function KfScene(props: { t: number; ctx: SceneCtx; children?: ReactNode }) {
19
+ return <TimeContext.Provider value={{ t: safeTime(props.t), ctx: props.ctx, scene: props.ctx.id }}>{props.children}</TimeContext.Provider>;
20
+ }
21
+
22
+ const noop = () => () => {};
23
+
24
+ export function useTime(): number {
25
+ const v = useContext(TimeContext);
26
+ const global = useSyncExternalStore(v ? noop : onFrame, frameTime, frameTime);
27
+ return v ? v.t : global;
28
+ }
29
+
30
+ export function useCtx(): SceneCtx | null {
31
+ return useContext(TimeContext)?.ctx ?? null;
32
+ }
33
+
34
+ export function useSceneId(): string | undefined {
35
+ return useContext(TimeContext)?.scene;
36
+ }
37
+
38
+ /** 장면 기준 박자. KfScene 밖이면 null */
39
+ export function useBeats(): Beats | null {
40
+ return useCtx()?.beats ?? null;
41
+ }
42
+
43
+ /** 부품이 t 를 받았으면 그것을, 아니면 감싼 상자의 시각을 */
44
+ export function useT(t?: number): number {
45
+ const ctxT = useTime();
46
+ return t === undefined ? ctxT : safeTime(t);
47
+ }
48
+
49
+ /** from 부터 dur 동안만 그리고, 안에서는 from 을 0 으로 센다 */
50
+ export function Sequence(props: { from: number; dur?: number; children?: ReactNode; keep?: boolean }) {
51
+ const parent = useContext(TimeContext);
52
+ const t = useTime();
53
+ const local = t - props.from;
54
+ const active = local >= 0 && (props.dur === undefined || local < props.dur);
55
+ if (!active && !props.keep) return null;
56
+ return (
57
+ <TimeContext.Provider value={{ t: local, ctx: parent?.ctx ?? null, scene: parent?.scene }}>{props.children}</TimeContext.Provider>
58
+ );
59
+ }
60
+
61
+ /** 자식 SeriesItem 들을 차례로 이어 붙인다 */
62
+ export function Series(props: { children?: ReactNode }) {
63
+ const items: { dur: number; offset: number; node: ReactNode }[] = [];
64
+ let at = 0;
65
+ const kids = Array.isArray(props.children) ? props.children.flat() : [props.children];
66
+ for (const k of kids) {
67
+ if (!k || typeof k !== "object" || !("props" in k)) continue;
68
+ const p = (k as { props: { dur?: number; offset?: number; children?: ReactNode } }).props;
69
+ const dur = p.dur ?? 0;
70
+ const from = at + (p.offset ?? 0);
71
+ items.push({ dur, offset: from, node: p.children });
72
+ at = from + dur;
73
+ }
74
+ return (
75
+ <>
76
+ {items.map((it, i) => (
77
+ <Sequence key={i} from={it.offset} dur={it.dur}>
78
+ {it.node}
79
+ </Sequence>
80
+ ))}
81
+ </>
82
+ );
83
+ }
84
+
85
+ /** Series 의 칸 — 그 자체는 그리지 않고 Series 가 읽는다 */
86
+ export function SeriesItem(_props: { dur: number; offset?: number; children?: ReactNode }): null {
87
+ return null;
88
+ }
89
+
90
+ /** 화면 가득 — 겹쳐 쌓는 층 */
91
+ export function Fill(props: { children?: ReactNode; style?: CSSProperties; className?: string }) {
92
+ return (
93
+ <div className={props.className} style={{ position: "absolute", inset: 0, overflow: "hidden", ...props.style }}>
94
+ {props.children}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,103 @@
1
+ import { progress, toEase, type Ease, type EaseName, type Style, safeTime } from "@kasaframe/core";
2
+ import { enter, stagger, type MoveName, type MoveOpts, type StaggerOpts } from "./enter.ts";
3
+
4
+ export interface CounterOpts {
5
+ from?: number;
6
+ to: number;
7
+ at?: number;
8
+ dur?: number;
9
+ ease?: EaseName | Ease;
10
+ decimals?: number;
11
+ /** 세 자리 쉼표. 기본 켬 */
12
+ group?: boolean;
13
+ prefix?: string;
14
+ suffix?: string;
15
+ /** ko: 1.2만·3.4억 처럼 한국어 큰 단위로 */
16
+ unit?: "none" | "ko";
17
+ }
18
+
19
+ const fmt = (v: number, decimals: number, group: boolean): string => {
20
+ const fixed = Math.abs(v).toFixed(decimals);
21
+ const [int, frac] = fixed.split(".");
22
+ const g = group ? int.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : int;
23
+ return (v < 0 && Number(fixed) !== 0 ? "-" : "") + g + (frac ? `.${frac}` : "");
24
+ };
25
+
26
+ function koUnit(v: number, decimals: number): string {
27
+ const a = Math.abs(v);
28
+ if (a >= 1e8) return `${fmt(v / 1e8, decimals, true)}억`;
29
+ if (a >= 1e4) return `${fmt(v / 1e4, decimals, true)}만`;
30
+ return fmt(v, 0, true);
31
+ }
32
+
33
+ /** 숫자가 굴러 오른다. 늘 글을 돌려주고, 수가 아닌 값이 와도 NaN 을 찍지 않는다 */
34
+ export function counter(t: number, opts: CounterOpts): string {
35
+ t = safeTime(t);
36
+ const to = Number.isFinite(opts.to) ? opts.to : 0;
37
+ const from = opts.from !== undefined && Number.isFinite(opts.from) ? opts.from : 0;
38
+ const p = toEase(opts.ease, toEase("outCubic"))(progress(Number.isNaN(t) ? 0 : t, opts.at ?? 0, opts.dur ?? 1.2));
39
+ const v = from + (to - from) * p;
40
+ const decimals = Math.max(0, Math.min(6, Math.floor(opts.decimals ?? 0)));
41
+ const body = opts.unit === "ko" ? koUnit(v, decimals) : fmt(v, decimals, opts.group ?? true);
42
+ return `${opts.prefix ?? ""}${body}${opts.suffix ?? ""}`;
43
+ }
44
+
45
+ export interface SweepOpts {
46
+ at?: number;
47
+ dur?: number;
48
+ ease?: EaseName | Ease;
49
+ from?: "left" | "right";
50
+ }
51
+
52
+ /** 형광펜 쓸기 — 강조 띠 요소에 Style.clip 으로 준다 */
53
+ export function sweep(t: number, opts: SweepOpts = {}): Style & { p: number } {
54
+ t = safeTime(t);
55
+ const p = toEase(opts.ease, toEase("inOutCubic"))(progress(t, opts.at ?? 0, opts.dur ?? 0.45));
56
+ return opts.from === "right" ? { p, clip: { left: 1 - p } } : { p, clip: { right: 1 - p } };
57
+ }
58
+
59
+ export interface WordState {
60
+ text: string;
61
+ index: number;
62
+ style: Style;
63
+ }
64
+
65
+ /** 낱말 키네틱 — 띄어쓰기로 나눠 차례로 들인다. 빈 낱말은 만들지 않는다 */
66
+ export function words(
67
+ text: string,
68
+ t: number,
69
+ opts: MoveOpts & StaggerOpts & { move?: MoveName } = {},
70
+ ): WordState[] {
71
+ t = safeTime(t);
72
+ const src = typeof text === "string" ? text : "";
73
+ const list = src.split(/\s+/).filter((w) => w.length > 0);
74
+ return list.map((w, index) => ({
75
+ text: w,
76
+ index,
77
+ style: enter(opts.move ?? "rise", t, {
78
+ ...opts,
79
+ at: (opts.at ?? 0) + stagger(index, { ...opts, count: list.length }),
80
+ source: opts.source ?? "words",
81
+ emit: index === 0 && opts.emit !== false,
82
+ }),
83
+ }));
84
+ }
85
+
86
+ /** 글자 단위 — 제목 한 줄을 글자마다 들일 때. 서로게이트 쌍을 쪼개지 않는다 */
87
+ export function letters(text: string, t: number, opts: MoveOpts & StaggerOpts & { move?: MoveName } = {}): WordState[] {
88
+ t = safeTime(t);
89
+ const list = Array.from(typeof text === "string" ? text : "");
90
+ return list.map((ch, index) => ({
91
+ text: ch,
92
+ index,
93
+ style:
94
+ ch.trim() === ""
95
+ ? {}
96
+ : enter(opts.move ?? "rise", t, {
97
+ ...opts,
98
+ at: (opts.at ?? 0) + stagger(index, { ...opts, each: opts.each ?? 0.03, count: list.length }),
99
+ source: opts.source ?? "letters",
100
+ emit: index === 0 && opts.emit !== false,
101
+ }),
102
+ }));
103
+ }