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,503 @@
1
+ import type { StudioConfig } from "../plugin.ts";
2
+ import { Timeline, type TimelineData } from "./timeline.ts";
3
+
4
+ // SPEC 1번 페이지 계약. 미리보기는 프로젝트와 이것으로만 말한다.
5
+ interface KfMarker {
6
+ t: number;
7
+ label: string;
8
+ kind: "scene" | "beat" | "drop" | "section";
9
+ }
10
+ interface KfPage {
11
+ meta: { fps: number; width: number; height: number; duration: number; music?: string };
12
+ mode: "seek" | "step";
13
+ ready(): Promise<void>;
14
+ seek(t: number): Promise<void>;
15
+ markers?: KfMarker[];
16
+ }
17
+ type KfWindow = Window & { __kf?: Partial<KfPage> };
18
+
19
+ const ICON = {
20
+ first: "M5 5h2.5v14H5zM19 5v14L8.5 12z",
21
+ prev: "M15.5 5v14L5 12zM16.5 5H19v14h-2.5z",
22
+ play: "M7 4.5v15L19.5 12z",
23
+ pause: "M6.5 5h4v14h-4zM13.5 5h4v14h-4z",
24
+ next: "M5 5h2.5v14H5zM8.5 5v14L19 12z",
25
+ last: "M5 5v14l10.5-7zM16.5 5H19v14h-2.5z",
26
+ loop: "M7 7h10v3l4-4-4-4v3H5v6h2zM17 17H7v-3l-4 4 4 4v-3h12v-6h-2z",
27
+ captions: "M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zm0 2v10h16V7zm2 5h5v2H6zm7 0h5v2h-5zm-7-3h12v2H6z",
28
+ };
29
+ const svg = (d: string) => `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${d}"/></svg>`;
30
+
31
+ const app = document.getElementById("app")!;
32
+ app.innerHTML = `
33
+ <header class="top">
34
+ <span class="name"></span>
35
+ <span class="meta"></span>
36
+ <span class="grow"></span>
37
+ <span class="keys">Space 재생 · ←→ 프레임 · Shift 1초 · Alt 박 · ↑↓ 경계 · Shift 끌기 = 붙이기</span>
38
+ <span class="status" data-state="wait">불러오는 중</span>
39
+ </header>
40
+ <main class="stage"><div class="error"></div><div class="frame"><iframe src="/index.html" tabindex="-1" title="project"></iframe></div></main>
41
+ <nav class="bar">
42
+ <button data-act="first" title="처음 (Home)">${svg(ICON.first)}</button>
43
+ <button data-act="prev" title="한 프레임 뒤 (←)">${svg(ICON.prev)}</button>
44
+ <button data-act="play" class="play" title="재생 (Space)">${svg(ICON.play)}</button>
45
+ <button data-act="next" title="한 프레임 앞 (→)">${svg(ICON.next)}</button>
46
+ <button data-act="last" title="끝 (End)">${svg(ICON.last)}</button>
47
+ <button data-act="loop" title="반복 (L)" aria-pressed="false">${svg(ICON.loop)}</button>
48
+ <button data-act="captions" title="화면 자막 (C)" aria-pressed="false" hidden>${svg(ICON.captions)}</button>
49
+ <span class="time">0:00.000</span>
50
+ <span class="frameno"></span>
51
+ <span class="info"></span>
52
+ </nav>
53
+ <section class="timeline"></section>
54
+ `;
55
+ const $ = <T extends Element>(s: string) => app.querySelector(s) as T;
56
+ const iframe = $<HTMLIFrameElement>("iframe");
57
+ const frameBox = $<HTMLDivElement>(".frame");
58
+ const stageBox = $<HTMLElement>(".stage");
59
+ const errorBox = $<HTMLDivElement>(".error");
60
+ errorBox.addEventListener("click", (e) => e.stopPropagation());
61
+ const statusEl = $<HTMLSpanElement>(".status");
62
+ const timeEl = $<HTMLSpanElement>(".time");
63
+ const frameEl = $<HTMLSpanElement>(".frameno");
64
+ const infoEl = $<HTMLSpanElement>(".info");
65
+ const playBtn = $<HTMLButtonElement>('[data-act="play"]');
66
+ const loopBtn = $<HTMLButtonElement>('[data-act="loop"]');
67
+ const capBtn = $<HTMLButtonElement>('[data-act="captions"]');
68
+ // 화면 자막은 프로젝트 페이지 주소의 ?captions=on|off 로 켜고 끈다 — kf.json 은 그대로(kf render --captions 와 같은 길)
69
+ let captions = new URLSearchParams(location.search).get("captions");
70
+ if (captions === "on" || captions === "off") iframe.src = `/index.html?captions=${captions}`;
71
+
72
+ const S = {
73
+ kf: null as KfPage | null,
74
+ meta: { fps: 60, width: 1920, height: 1080, duration: 60 },
75
+ cfg: null as StudioConfig | null,
76
+ data: null as TimelineData | null,
77
+ t: Math.max(0, Number(new URLSearchParams(location.search).get("t")) || 0),
78
+ playing: false,
79
+ resumeAfterScrub: false,
80
+ loop: false,
81
+ wanted: null as number | null,
82
+ busy: false,
83
+ seekMs: 0,
84
+ generation: 0,
85
+ };
86
+
87
+ const audio = new Audio();
88
+ audio.preload = "auto";
89
+ let audioUrl: string | null = null;
90
+ let hasAudio = false;
91
+ let wallStart = 0;
92
+
93
+ const fps = () => S.meta.fps;
94
+ const dur = () => S.meta.duration;
95
+ const toFrame = (t: number) => Math.round(t * fps());
96
+ const frameTime = (t: number) => Math.min(dur(), Math.max(0, toFrame(t) / fps()));
97
+
98
+ function fmt(t: number) {
99
+ const m = Math.floor(t / 60);
100
+ const s = t - m * 60;
101
+ return `${m}:${s.toFixed(3).padStart(6, "0")}`;
102
+ }
103
+
104
+ function setStatus(state: "ok" | "wait" | "err", text: string) {
105
+ statusEl.dataset.state = state;
106
+ statusEl.textContent = text;
107
+ }
108
+
109
+ function showError(msg: string) {
110
+ errorBox.textContent = msg;
111
+ errorBox.classList.add("on");
112
+ setStatus("err", "오류");
113
+ }
114
+
115
+ function clearError() {
116
+ errorBox.textContent = "";
117
+ errorBox.classList.remove("on");
118
+ }
119
+
120
+ // ---- 시계: 곡이 있으면 곡이 기준, 없으면 벽시계 ----
121
+
122
+ function clockNow(): number {
123
+ if (!S.playing) return S.t;
124
+ if (hasAudio) return audio.currentTime - (S.cfg?.from ?? 0);
125
+ return (performance.now() - wallStart) / 1000;
126
+ }
127
+
128
+ function startClock() {
129
+ if (hasAudio) {
130
+ audio.currentTime = (S.cfg?.from ?? 0) + S.t;
131
+ audio.play().catch((e: unknown) => {
132
+ hasAudio = false;
133
+ wallStart = performance.now() - S.t * 1000;
134
+ showError(`곡을 재생하지 못했다 — 소리 없이 돈다 (${String(e)})`);
135
+ });
136
+ } else wallStart = performance.now() - S.t * 1000;
137
+ }
138
+
139
+ function play() {
140
+ if (S.playing) return;
141
+ if (S.t >= dur() - 1 / fps()) S.t = 0;
142
+ S.playing = true;
143
+ startClock();
144
+ playBtn.innerHTML = svg(ICON.pause);
145
+ }
146
+
147
+ function pause() {
148
+ if (!S.playing) return;
149
+ S.t = frameTime(clockNow());
150
+ S.playing = false;
151
+ audio.pause();
152
+ playBtn.innerHTML = svg(ICON.play);
153
+ requestSeek(S.t);
154
+ syncUrl();
155
+ }
156
+
157
+ function setTime(t: number, exact = false) {
158
+ S.t = exact ? Math.min(dur(), Math.max(0, t)) : frameTime(t);
159
+ if (S.playing) startClock();
160
+ requestSeek(S.t);
161
+ syncUrl();
162
+ paint();
163
+ }
164
+
165
+ let urlTimer = 0;
166
+ function syncUrl() {
167
+ clearTimeout(urlTimer);
168
+ urlTimer = window.setTimeout(() => {
169
+ const q = new URLSearchParams(location.search);
170
+ q.set("t", S.t.toFixed(3));
171
+ history.replaceState(null, "", `?${q}`);
172
+ }, 120);
173
+ }
174
+
175
+ // ---- 페이지에 seek: 하나만 돌리고, 밀린 요청은 마지막 것만 ----
176
+
177
+ function requestSeek(t: number) {
178
+ S.wanted = t;
179
+ void pump();
180
+ }
181
+
182
+ async function pump() {
183
+ if (S.busy || S.wanted === null || !S.kf) return;
184
+ const t = S.wanted;
185
+ const gen = S.generation;
186
+ S.wanted = null;
187
+ S.busy = true;
188
+ const t0 = performance.now();
189
+ try {
190
+ await S.kf.seek(t);
191
+ } catch (e) {
192
+ if (gen === S.generation) showError(`seek(${t.toFixed(3)}) 실패: ${e instanceof Error ? e.message : String(e)}`);
193
+ }
194
+ S.busy = false;
195
+ if (gen !== S.generation) return;
196
+ S.seekMs = performance.now() - t0;
197
+ setStatus("ok", `연결됨 · seek ${S.seekMs.toFixed(0)}ms`);
198
+ if (S.wanted !== null) void pump();
199
+ }
200
+
201
+ function tick() {
202
+ if (S.playing) {
203
+ let t = clockNow();
204
+ if (t >= dur()) {
205
+ if (S.loop) {
206
+ S.t = 0;
207
+ startClock();
208
+ t = 0;
209
+ } else {
210
+ S.t = dur();
211
+ pause();
212
+ t = dur();
213
+ }
214
+ }
215
+ S.t = Math.max(0, t);
216
+ // 렌더와 같은 그림을 보이려고 프레임 경계로 내려서 찍는다
217
+ requestSeek(Math.floor(S.t * fps() + 1e-6) / fps());
218
+ }
219
+ paint();
220
+ requestAnimationFrame(tick);
221
+ }
222
+
223
+ // ---- 화면 ----
224
+
225
+ const timeline = new Timeline($<HTMLElement>(".timeline"), {
226
+ seek: (t, snapped) => setTime(t, snapped),
227
+ scrubStart() {
228
+ S.resumeAfterScrub = S.playing;
229
+ pause();
230
+ },
231
+ scrubEnd() {
232
+ if (S.resumeAfterScrub) play();
233
+ },
234
+ });
235
+
236
+ function lastAtOrBefore(ts: number[], t: number): number {
237
+ let i = -1;
238
+ for (let k = 0; k < ts.length && ts[k] <= t + 1e-6; k++) i = k;
239
+ return i;
240
+ }
241
+
242
+ function paint() {
243
+ const t = S.t;
244
+ timeEl.textContent = fmt(t);
245
+ frameEl.textContent = `f ${toFrame(t)} / ${toFrame(dur())}`;
246
+ timeline.setTime(t, S.playing);
247
+ const d = S.data;
248
+ if (!d) return;
249
+ const parts: string[] = [];
250
+ const scene = d.scenes.find((s) => t >= s.from && t < s.end) ?? (t >= dur() ? d.scenes[d.scenes.length - 1] : undefined);
251
+ if (scene) parts.push(`장면 <b>${esc(scene.id)}</b> +${(t - scene.from).toFixed(2)}`);
252
+ const si = lastAtOrBefore(d.sections.map((s) => s.t), t);
253
+ if (si >= 0) parts.push(`구간 <b>${esc(d.sections[si].kind)}</b>`);
254
+ const bi = lastAtOrBefore(d.downbeats, t);
255
+ if (bi >= 0) {
256
+ const inBar = lastAtOrBefore(d.beats, t) - lastAtOrBefore(d.beats, d.downbeats[bi]);
257
+ parts.push(`마디 <b>${bi + 1}.${inBar + 1}</b>`);
258
+ }
259
+ const pi = lastAtOrBefore(d.phrases, t);
260
+ if (pi >= 0) parts.push(`프레이즈 ${pi + 1}`);
261
+ const next = d.drops.find((m) => m.t > t + 1e-6);
262
+ const last = [...d.drops].reverse().find((m) => m.t <= t + 1e-6);
263
+ if (last && t - last.t < 1) parts.push(`<span class="drop">드롭 +${(t - last.t).toFixed(2)}</span>`);
264
+ else if (next) parts.push(`드롭까지 ${(next.t - t).toFixed(2)}`);
265
+ infoEl.innerHTML = parts.join(" · ");
266
+ }
267
+
268
+ function esc(s: string) {
269
+ return s.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
270
+ }
271
+
272
+ function fit() {
273
+ const pad = 20;
274
+ const w = stageBox.clientWidth - pad * 2;
275
+ const h = stageBox.clientHeight - pad * 2;
276
+ const { width, height } = S.meta;
277
+ const k = Math.max(0.02, Math.min(w / width, h / height));
278
+ frameBox.style.width = `${Math.floor(width * k)}px`;
279
+ frameBox.style.height = `${Math.floor(height * k)}px`;
280
+ iframe.style.width = `${width}px`;
281
+ iframe.style.height = `${height}px`;
282
+ iframe.style.transform = `scale(${k})`;
283
+ const metaEl = $<HTMLSpanElement>(".meta");
284
+ const ratio = aspect(width, height);
285
+ metaEl.textContent = `${width}×${height} · ${ratio} · ${S.meta.fps}fps · ${+S.meta.duration.toFixed(3)}초 · ${Math.round(k * 100)}%${S.cfg?.beats?.bpm ? ` · ${S.cfg.beats.bpm} BPM` : ""}${hasAudio ? "" : " · 소리 없음"}`;
286
+ }
287
+
288
+ function aspect(w: number, h: number) {
289
+ const g = (a: number, b: number): number => (b ? g(b, a % b) : a);
290
+ const d = g(w, h);
291
+ return `${w / d}:${h / d}`;
292
+ }
293
+
294
+ new ResizeObserver(fit).observe(stageBox);
295
+
296
+ function buildData(kf: KfPage, cfg: StudioConfig | null): TimelineData {
297
+ const D = kf.meta.duration;
298
+ const markers = (kf.markers ?? []).slice().sort((a, b) => a.t - b.t);
299
+ const of = (kind: KfMarker["kind"]) => markers.filter((m) => m.kind === kind);
300
+ let scenes = of("scene").map((m, i, all) => ({ id: m.label, from: m.t, end: i + 1 < all.length ? all[i + 1].t : D }));
301
+ if (!scenes.length && cfg?.kf?.scenes) scenes = cfg.kf.scenes.map((s) => ({ id: s.id, from: s.from, end: s.from + s.dur }));
302
+ const within = (t: number) => t >= -1e-6 && t <= D + 1e-6;
303
+ const sections = (cfg?.beats?.sections ?? of("section").map((m) => ({ t: m.t, kind: m.label }))).filter((s) => within(s.t));
304
+ return {
305
+ duration: D,
306
+ fps: kf.meta.fps,
307
+ scenes,
308
+ sections,
309
+ beats: of("beat").map((m) => m.t),
310
+ downbeats: (cfg?.beats?.downbeats ?? []).filter(within),
311
+ phrases: (cfg?.beats?.phrases ?? []).filter(within),
312
+ drops: of("drop").map((m) => ({ t: m.t, label: m.label })),
313
+ energy: cfg?.beats?.energy,
314
+ };
315
+ }
316
+
317
+ function setupAudio(cfg: StudioConfig | null, kf: KfPage) {
318
+ // 서버가 kf.json 기준으로 푼 주소가 먼저 — 프로젝트 밖 곡도 /@fs 로 열린다
319
+ let url = cfg?.music ?? null;
320
+ if (!url && kf.meta.music) url = new URL(kf.meta.music, iframe.contentWindow!.location.href).pathname;
321
+ if (url === audioUrl) return;
322
+ audioUrl = url;
323
+ hasAudio = !!url;
324
+ if (url) {
325
+ audio.src = url;
326
+ audio.onerror = () => {
327
+ hasAudio = false;
328
+ showError(`곡을 못 읽었다: ${decodeURI(url!)} — 소리 없이 돈다`);
329
+ fit();
330
+ };
331
+ } else audio.removeAttribute("src");
332
+ if (S.playing) startClock();
333
+ }
334
+
335
+ function syncCaptions(cfg: StudioConfig | null) {
336
+ const c = cfg?.captions;
337
+ capBtn.hidden = !c?.lines;
338
+ if (!c) return;
339
+ const on = (captions ?? (c.mode === "on" || c.mode === "fit" || c.mode === "burned" ? "on" : "off")) === "on";
340
+ capBtn.setAttribute("aria-pressed", String(on));
341
+ capBtn.title = `화면 자막 ${on ? "끄기" : "켜기"} (C) — kf.json 은 ${c.mode}`;
342
+ }
343
+
344
+ // 장면 파일이 깨지면 main.ts 가 못 떠서 __kf 가 영영 안 생긴다 — Vite 가 iframe 에 띄운 오류를 바로 옮겨 온다
345
+ function viteError(win: Window): string | null {
346
+ const el = win.document.querySelector("vite-error-overlay");
347
+ const root = el?.shadowRoot;
348
+ if (!root) return null;
349
+ const text = (sel: string) => root.querySelector(sel)?.textContent?.trim() ?? "";
350
+ return [text(".message"), text(".file")].filter(Boolean).join("\n") || "Vite 오류";
351
+ }
352
+
353
+ function waitFor<T>(get: () => T | null, ms: number): Promise<T | null> {
354
+ const end = performance.now() + ms;
355
+ return new Promise((resolve) => {
356
+ const poll = () => {
357
+ const v = get();
358
+ if (v) return resolve(v);
359
+ if (performance.now() > end) return resolve(null);
360
+ setTimeout(poll, 30);
361
+ };
362
+ poll();
363
+ });
364
+ }
365
+
366
+ // 장면을 저장하면 Vite 가 iframe 만 새로 고친다. 그때마다 다시 붙고 보던 시각으로 돌아간다.
367
+ async function attach() {
368
+ const gen = ++S.generation;
369
+ S.kf = null;
370
+ S.busy = false;
371
+ setStatus("wait", "불러오는 중");
372
+ const win = iframe.contentWindow as KfWindow | null;
373
+ if (!win) return;
374
+ win.addEventListener("pagehide", () => (lastUnload = performance.now()));
375
+ win.addEventListener("error", (e) => showError(`페이지 오류: ${e.message}\n${e.filename}:${e.lineno}`));
376
+ win.addEventListener("unhandledrejection", (e) => showError(`페이지 오류: ${e.reason instanceof Error ? (e.reason.stack ?? e.reason.message) : String(e.reason)}`));
377
+ const found = await waitFor<KfPage | { overlay: string }>(() => {
378
+ if (win.__kf?.seek && win.__kf.meta) return win.__kf as KfPage;
379
+ const msg = viteError(win);
380
+ return msg ? { overlay: msg } : null;
381
+ }, 15000);
382
+ if (gen !== S.generation) return;
383
+ if (found && "overlay" in found) {
384
+ showError(`프로젝트 페이지 오류(저장하면 다시 붙는다)\n${found.overlay}`);
385
+ return;
386
+ }
387
+ const kf = found;
388
+ if (!kf && errorBox.classList.contains("on")) return;
389
+ if (!kf) {
390
+ showError("window.__kf 가 없다 — main.ts 가 mountProject(...) 를 부르는지, 장면 파일에 오류가 없는지 보라(프로젝트 화면에 Vite 오류가 떠 있을 수 있다)");
391
+ return;
392
+ }
393
+ try {
394
+ await kf.ready();
395
+ } catch (e) {
396
+ if (gen === S.generation) showError(`ready() 실패: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`);
397
+ return;
398
+ }
399
+ if (gen !== S.generation) return;
400
+ const cfg = await fetch("/__kf/studio.json", { cache: "no-store" })
401
+ .then((r) => (r.ok ? (r.json() as Promise<StudioConfig>) : null))
402
+ .catch(() => null);
403
+ if (gen !== S.generation) return;
404
+ S.kf = kf;
405
+ S.cfg = cfg;
406
+ S.meta = { fps: kf.meta.fps, width: kf.meta.width, height: kf.meta.height, duration: kf.meta.duration };
407
+ S.t = Math.min(S.t, dur());
408
+ S.data = buildData(kf, cfg);
409
+ document.title = `${cfg?.name ?? "kf"} · kf preview`;
410
+ $<HTMLSpanElement>(".name").textContent = cfg?.name ?? "";
411
+ setupAudio(cfg, kf);
412
+ syncCaptions(cfg);
413
+ fit();
414
+ timeline.setData(S.data);
415
+ clearError();
416
+ setStatus("ok", "연결됨");
417
+ requestSeek(S.playing ? clockNow() : S.t);
418
+ paint();
419
+ }
420
+
421
+ iframe.addEventListener("load", () => void attach());
422
+
423
+ // 페이지가 떠나기 시작한 시각 — Vite 가 이미 새로 고치는 중이면 다시 고치지 않는다
424
+ let lastUnload = 0;
425
+ let changedAt: number | null = null;
426
+ let fallback = 0;
427
+ new EventSource("/__kf/events").onmessage = (e) => {
428
+ const m = JSON.parse(e.data) as { ev: string; message?: string; where?: string; frame?: string };
429
+ if (m.ev === "error") {
430
+ showError(`프로젝트 코드 오류(고쳐 저장하면 다시 붙는다)\n${m.message}\n${m.where}\n${m.frame}`.trim());
431
+ return;
432
+ }
433
+ changedAt ??= performance.now();
434
+ clearTimeout(fallback);
435
+ fallback = window.setTimeout(() => {
436
+ // SSE 가 Vite 의 새로 고침보다 조금 늦게 닿기도 해서 바로 앞의 새로 고침도 이번 변경을 반영한 것으로 본다
437
+ if (changedAt !== null && lastUnload < changedAt - 400) iframe.contentWindow?.location.reload();
438
+ changedAt = null;
439
+ }, 700);
440
+ };
441
+
442
+ // ---- 조작 ----
443
+
444
+ function boundaries(): number[] {
445
+ const d = S.data;
446
+ if (!d) return [0, dur()];
447
+ return [0, dur(), ...d.scenes.map((s) => s.from), ...d.sections.map((s) => s.t), ...d.drops.map((m) => m.t)].sort((a, b) => a - b);
448
+ }
449
+
450
+ function jump(list: number[], dir: 1 | -1) {
451
+ const t = S.t;
452
+ const eps = 0.5 / fps();
453
+ const target = dir > 0 ? list.find((x) => x > t + eps) : [...list].reverse().find((x) => x < t - eps);
454
+ if (target !== undefined) setTime(target, true);
455
+ }
456
+
457
+ function act(name: string) {
458
+ const f = 1 / fps();
459
+ if (name === "play") S.playing ? pause() : play();
460
+ else if (name === "first") setTime(0);
461
+ else if (name === "last") setTime(dur());
462
+ else if (name === "prev") {
463
+ pause();
464
+ setTime(S.t - f);
465
+ } else if (name === "next") {
466
+ pause();
467
+ setTime(S.t + f);
468
+ } else if (name === "captions") {
469
+ // iframe 만 다시 연다 — attach 가 보던 시각으로 돌아간다
470
+ captions = capBtn.getAttribute("aria-pressed") === "true" ? "off" : "on";
471
+ const u = new URL(location.href);
472
+ u.searchParams.set("captions", captions);
473
+ history.replaceState(null, "", u);
474
+ iframe.src = `/index.html?captions=${captions}`;
475
+ } else if (name === "loop") {
476
+ S.loop = !S.loop;
477
+ loopBtn.setAttribute("aria-pressed", String(S.loop));
478
+ }
479
+ }
480
+
481
+ app.querySelectorAll<HTMLButtonElement>("[data-act]").forEach((b) => b.addEventListener("click", () => act(b.dataset.act!)));
482
+ stageBox.addEventListener("click", () => act("play"));
483
+
484
+ window.addEventListener("keydown", (e) => {
485
+ if (e.metaKey || e.ctrlKey) return;
486
+ const f = 1 / fps();
487
+ const k = e.key;
488
+ if (k === " ") act("play");
489
+ else if (k === "ArrowLeft" || k === "ArrowRight") {
490
+ const dir = k === "ArrowRight" ? 1 : -1;
491
+ pause();
492
+ if (e.altKey) jump(S.data?.beats ?? [], dir);
493
+ else setTime(S.t + dir * (e.shiftKey ? 1 : f));
494
+ } else if (k === "ArrowUp" || k === "ArrowDown") jump(boundaries(), k === "ArrowDown" ? 1 : -1);
495
+ else if (k === "Home") setTime(0);
496
+ else if (k === "End") setTime(dur());
497
+ else if (k === "l" || k === "L") act("loop");
498
+ else if ((k === "c" || k === "C") && !capBtn.hidden) act("captions");
499
+ else return;
500
+ e.preventDefault();
501
+ });
502
+
503
+ requestAnimationFrame(tick);
@@ -0,0 +1,165 @@
1
+ :root {
2
+ --bg: #0c0d10;
3
+ --panel: #14161b;
4
+ --raised: #1b1e25;
5
+ --line: #262a33;
6
+ --ink: #e9ebf0;
7
+ --dim: #8b91a0;
8
+ --faint: #5a606d;
9
+ --accent: #ff6b9a;
10
+ --sky: #7cc4ff;
11
+ --danger: #ff6b6b;
12
+ --mono: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, "Apple SD Gothic Neo", monospace;
13
+ --sans: "Pretendard", "Apple SD Gothic Neo", system-ui, sans-serif;
14
+ color-scheme: dark;
15
+ }
16
+
17
+ * { box-sizing: border-box; }
18
+
19
+ html, body {
20
+ margin: 0;
21
+ height: 100%;
22
+ background: var(--bg);
23
+ color: var(--ink);
24
+ font: 13px/1.4 var(--sans);
25
+ overflow: hidden;
26
+ -webkit-font-smoothing: antialiased;
27
+ }
28
+
29
+ #app {
30
+ display: grid;
31
+ grid-template-rows: auto 1fr auto auto;
32
+ height: 100%;
33
+ }
34
+
35
+ .top {
36
+ display: flex;
37
+ align-items: baseline;
38
+ gap: 16px;
39
+ padding: 10px 16px;
40
+ border-bottom: 1px solid var(--line);
41
+ white-space: nowrap;
42
+ overflow: hidden;
43
+ }
44
+ .top .name { font-weight: 600; letter-spacing: -0.01em; }
45
+ .top .meta { color: var(--dim); font-family: var(--mono); font-size: 12px; }
46
+ .top .grow { flex: 1; }
47
+ .top .keys { color: var(--faint); font-family: var(--mono); font-size: 11px; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
48
+ .top .status { font-family: var(--mono); font-size: 12px; color: var(--dim); }
49
+ .top .status[data-state="ok"]::before,
50
+ .top .status[data-state="wait"]::before,
51
+ .top .status[data-state="err"]::before {
52
+ content: "";
53
+ display: inline-block;
54
+ width: 7px;
55
+ height: 7px;
56
+ margin-right: 7px;
57
+ border-radius: 50%;
58
+ vertical-align: 1px;
59
+ background: var(--sky);
60
+ }
61
+ .top .status[data-state="wait"]::before { background: var(--faint); }
62
+ .top .status[data-state="err"]::before { background: var(--danger); }
63
+
64
+ .error {
65
+ position: absolute;
66
+ z-index: 2;
67
+ top: 0;
68
+ left: 0;
69
+ right: 0;
70
+ display: none;
71
+ padding: 8px 16px;
72
+ background: rgba(42, 20, 24, 0.94);
73
+ color: #ffc9cf;
74
+ border-bottom: 1px solid #4a1f27;
75
+ font-family: var(--mono);
76
+ font-size: 12px;
77
+ white-space: pre-wrap;
78
+ max-height: 40%;
79
+ overflow: auto;
80
+ }
81
+ .error.on { display: block; }
82
+
83
+ .stage {
84
+ position: relative;
85
+ min-height: 0;
86
+ display: grid;
87
+ place-items: center;
88
+ background:
89
+ linear-gradient(45deg, #101216 25%, transparent 25%, transparent 75%, #101216 75%) 0 0 / 24px 24px,
90
+ linear-gradient(45deg, #101216 25%, transparent 25%, transparent 75%, #101216 75%) 12px 12px / 24px 24px,
91
+ var(--bg);
92
+ overflow: hidden;
93
+ }
94
+ .frame {
95
+ position: relative;
96
+ overflow: hidden;
97
+ background: #000;
98
+ box-shadow: 0 0 0 1px var(--line);
99
+ }
100
+ .frame iframe {
101
+ position: absolute;
102
+ left: 0;
103
+ top: 0;
104
+ border: 0;
105
+ transform-origin: 0 0;
106
+ background: #000;
107
+ }
108
+
109
+ .bar {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 4px;
113
+ padding: 8px 12px;
114
+ border-top: 1px solid var(--line);
115
+ background: var(--panel);
116
+ white-space: nowrap;
117
+ overflow: hidden;
118
+ }
119
+ .bar button {
120
+ display: inline-grid;
121
+ place-items: center;
122
+ width: 30px;
123
+ height: 30px;
124
+ padding: 0;
125
+ border: 0;
126
+ border-radius: 6px;
127
+ background: transparent;
128
+ color: var(--ink);
129
+ cursor: pointer;
130
+ }
131
+ .bar button:hover { background: var(--raised); }
132
+ .bar button.play { width: 38px; background: var(--raised); }
133
+ .bar button[aria-pressed="true"] { color: var(--sky); }
134
+ .bar button svg { width: 16px; height: 16px; fill: currentColor; }
135
+ .bar .time {
136
+ margin-left: 10px;
137
+ font-family: var(--mono);
138
+ font-size: 15px;
139
+ font-variant-numeric: tabular-nums;
140
+ letter-spacing: -0.02em;
141
+ }
142
+ .bar .frameno, .bar .info {
143
+ font-family: var(--mono);
144
+ font-size: 12px;
145
+ color: var(--dim);
146
+ font-variant-numeric: tabular-nums;
147
+ }
148
+ .bar .frameno { margin-left: 8px; }
149
+ .bar .info { margin-left: 16px; overflow: hidden; text-overflow: ellipsis; }
150
+ .bar .info b { color: var(--ink); font-weight: 500; }
151
+ .bar .info .drop { color: var(--accent); }
152
+
153
+ .timeline {
154
+ position: relative;
155
+ height: 156px;
156
+ border-top: 1px solid var(--line);
157
+ background: var(--panel);
158
+ cursor: crosshair;
159
+ touch-action: none;
160
+ }
161
+ .timeline canvas { display: block; width: 100%; height: 100%; }
162
+
163
+ @media (max-width: 1100px) {
164
+ .top .keys { display: none; }
165
+ }