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,191 @@
1
+ import type { SceneCtx } from "@kasaframe/core";
2
+ import { brushWipe } from "./brushWipe.ts";
3
+ import { echoStack } from "./echoStack.ts";
4
+ import { div } from "./fit.ts";
5
+ import { flood } from "./flood.ts";
6
+ import { ghostStack } from "./ghostStack.ts";
7
+ import { gridFloor } from "./gridFloor.ts";
8
+ import { irisBlades } from "./irisBlades.ts";
9
+ import { ledBoard } from "./ledBoard.ts";
10
+ import { neonSign } from "./neonSign.ts";
11
+ import { ransomLetters } from "./ransomLetters.ts";
12
+ import { slamWords } from "./slamWords.ts";
13
+ import { speedLines } from "./speedLines.ts";
14
+ import { splitFlap } from "./splitFlap.ts";
15
+ import { sunburst } from "./sunburst.ts";
16
+ import { truchet } from "./truchet.ts";
17
+
18
+ // 메뉴(menu.ts)의 이름을 장면에 실제로 건다 — 무대(lib/stage.ts)가 부른다. 수마다 예시 장면에서 검사를 넘긴 값을 기본으로 박아 두어,
19
+ // 뼈대가 고른 수를 그대로 둬도 kf check 를 넘는다. 낱말만 바꿔 쓰면 된다.
20
+
21
+ type Paint = (t: number) => void;
22
+
23
+ const color = (ctx: SceneCtx, key: string, fallback: string): string => {
24
+ const v = ctx.style?.colors?.[key];
25
+ return typeof v === "string" && v ? v : fallback;
26
+ };
27
+
28
+ // 상대휘도(WCAG) — #rgb·#rrggbb 만. 못 읽으면 NaN
29
+ function lum(hex: string): number {
30
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
31
+ if (!m) return NaN;
32
+ const h = m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1];
33
+ const ch = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
34
+ return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
35
+ }
36
+
37
+ // 오려 붙인 글자는 한 색이 글자색(바탕 위)도 되고 색종이(검은 글자 밑)도 된다 — 둘 다 3:1 을 넘는 가운데 밝기(0.12–0.3)만 쓴다.
38
+ // 밝은 테마·어두운 테마 어디서나 읽힌다. 테마 강조색이 그 안이면 앞에 둔다
39
+ const RANSOM = ["#E4572E", "#2E86DE", "#16A085", "#8E44AD", "#C0392B", "#D35400"];
40
+
41
+ /** 장면 안 박 — 빠른 곡은 박을 묶어 간격 gap 넘게. 잰 시각 그대로(BPM 을 곱하지 않는다) */
42
+ export function hitTimes(ctx: SceneCtx, gap = 0.4): number[] {
43
+ const b = ctx.beats.beats.filter((x) => x >= 0 && x < ctx.dur);
44
+ if (b.length < 2) return b.length ? b : [0];
45
+ const step = (b[b.length - 1] - b[0]) / (b.length - 1);
46
+ const every = Math.max(1, Math.ceil(gap / Math.max(step, 1e-3)));
47
+ const out = b.filter((_, i) => i % every === 0);
48
+ // 첫 박이 늦으면 첫 화면이 빈다 — 0 에 하나 세운다
49
+ if (out[0] > 0.12) out.unshift(0);
50
+ return out;
51
+ }
52
+
53
+ const cycle = (words: readonly string[], n: number): string[] => Array.from({ length: n }, (_, i) => words[i % words.length]);
54
+
55
+ // 한 수를 창(박 n 개)마다 새로 세운다 — 박이 쌓일수록 작아지는 수(메아리·네온)를 긴 장면에서도 같은 크기로 되풀이한다
56
+ function windows(host: HTMLElement, hits: readonly number[], per: number, make: (el: HTMLElement, hits: number[], k: number) => Paint): Paint {
57
+ const starts: number[] = [];
58
+ const paints: { el: HTMLElement; paint: Paint }[] = [];
59
+ for (let i = 0, k = 0; i < hits.length; i += per, k++) {
60
+ const el = div(host.ownerDocument, "position:absolute;inset:0");
61
+ host.append(el);
62
+ starts.push(hits[i]);
63
+ paints.push({ el, paint: make(el, hits.slice(i, i + per), k) });
64
+ }
65
+ return (t) => {
66
+ let k = 0;
67
+ while (k + 1 < starts.length && starts[k + 1] <= t) k++;
68
+ paints.forEach((p, i) => {
69
+ p.el.style.display = i === k ? "block" : "none";
70
+ if (i === k) p.paint(t);
71
+ });
72
+ };
73
+ }
74
+
75
+ /** 바탕 수 — el 은 화면 전체. still(기본)은 정지라 그리는 함수가 없다(null). 모르는 이름도 still */
76
+ export function dressBg(name: string, el: HTMLElement, ctx: SceneCtx): Paint | null {
77
+ const { width: w, height: h } = ctx.meta;
78
+ const hits = hitTimes(ctx);
79
+ const bg = color(ctx, "bg", "#0b0b0f");
80
+ const accent = color(ctx, "accent", "#ff5a1f");
81
+ const soft = color(ctx, "accentSoft", accent);
82
+ const line = color(ctx, "line", "#888");
83
+ el.style.background = bg;
84
+ switch (name) {
85
+ case "sunburst":
86
+ // 글 뒤에 깔리니 바탕에 강조색을 조금만 섞은 두 색 — 박마다 툭 도는 줄무늬가 진하면 화면 대부분이 한꺼번에 꺾여 섬광 검사(frame.flash)에 걸린다
87
+ return sunburst(el, { w, h, hits, rays: h > w ? 16 : 20, colors: [`color-mix(in srgb, ${accent} 14%, ${bg})`, bg], glow: "transparent", speed: 10, scene: ctx.id });
88
+ case "speedLines":
89
+ return speedLines(el, { w, h, kind: "radial", color: soft, inner: 0.36, boil: 8, scene: ctx.id });
90
+ case "gridFloor":
91
+ return gridFloor(el, { w, h, color: soft, speed: 1.4, glow: 0, scene: ctx.id });
92
+ case "truchet":
93
+ return truchet(el, { w, h, hits, cell: Math.round(Math.min(w, h) / 9), bg, line, accent: soft, share: 0.3, scene: ctx.id });
94
+ default:
95
+ // 바탕색 위 아주 옅은 점 — 서 있는 질감이라 화면이 비어 보이지 않을 뿐 아무것도 뜻하지 않으니 움직이지 않는다.
96
+ // 바탕색은 무대 상자에 맡긴다(장면이 마디마다 바탕색을 바꿀 수 있게)
97
+ el.style.background = "";
98
+ el.style.backgroundImage = `radial-gradient(circle, color-mix(in srgb, ${line} 55%, transparent) 1.5px, transparent 2px)`;
99
+ el.style.backgroundSize = `${Math.round(Math.min(w, h) / 24)}px ${Math.round(Math.min(w, h) / 24)}px`;
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 글 수 — el 은 안전 영역 상자(w×h). words 를 차례로 돌려 쓴다. bigWord(무대 기본)거나 모르면 null — 무대가 큰 낱말을 세운다.
106
+ * 박마다 내용이 바뀌어(새 낱말·새 벌·판 넘김) 같은 구도 검사(layout.static)를 수가 스스로 넘는다
107
+ */
108
+ export function dressText(name: string, el: HTMLElement, ctx: SceneCtx, o: { words: readonly string[]; w: number; h: number; ready?: boolean }): Paint | null {
109
+ const given = [...new Set(o.words.map((x) => x.trim()).filter(Boolean))];
110
+ const words = given.length ? given : ["kasaframe"];
111
+ const { w, h } = o;
112
+ const hits = hitTimes(ctx);
113
+ // 첫 프레임부터 글이 서 있어야 하는 장면(훅) — 첫 박을 장면 앞으로 당겨 이미 내려앉은 낱말로 시작한다
114
+ if (o.ready && hits[0] <= 0.15) hits[0] = -0.3;
115
+ const scene = ctx.id;
116
+ switch (name) {
117
+ case "slamWords": {
118
+ // 화면 폭을 채운 낱말이 1초에 두 번 바뀌면 섬광 세 번을 넘는다(frame.flash) — 1.2초 넘는 간격마다
119
+ const slams = hitTimes(ctx, 1.2);
120
+ if (o.ready && slams[0] <= 0.15) slams[0] = -0.3;
121
+ // 낱말 판은 가운데 띠(높이 55%)에 옅은 강조색으로 — 화면 전체를 뒤집으면 판이 바뀌는 박마다 화면 대부분이 한꺼번에
122
+ // 밝기가 꺾여 섬광 검사(frame.flash)에 걸린다. 띠가 보여야 긴 낱말 하나뿐인 세로 화면도 비어 보이지 않는다(layout.fill)
123
+ const bh = Math.round(h * 0.55);
124
+ const band = div(el.ownerDocument, `position:absolute;left:0;top:${Math.round((h - bh) / 2)}px;width:${w}px;height:${bh}px`);
125
+ el.append(band);
126
+ return slamWords(band, { words: cycle(words, slams.length), hits: slams, w, h: bh, palettes: [["color-mix(in srgb, var(--kf-accent) 10%, transparent)", "var(--kf-ink)"], ["color-mix(in srgb, var(--kf-accent) 24%, transparent)", "var(--kf-ink)"]], scene });
127
+ }
128
+ case "echoStack":
129
+ return windows(el, hits, 4, (box, hs, k) => echoStack(box, { word: words[k % words.length], hits: hs, dir: k % 2 ? "back" : "down", w, h, scene }));
130
+ case "splitFlap": {
131
+ const times = hits.filter((_, i) => i % 2 === 0);
132
+ const texts = cycle(words, times.length);
133
+ // 판에 같은 글이 이어 오르면 넘어가지 않는다 — 낱말이 하나면 판 넘김 대신 글자판을 비웠다 채운다
134
+ if (words.length === 1) texts.forEach((_, i) => i % 2 && (texts[i] = " ".repeat([...words[0]].length)));
135
+ return splitFlap(el, { texts, times, w, h, scene });
136
+ }
137
+ case "ghostStack": {
138
+ const times = hits.filter((_, i) => i % 2 === 0);
139
+ const lines = cycle(words, Math.max(4, times.length + 2)).map((text, i) => ({ text, at: times[i] ?? ctx.dur + i, hot: [0] }));
140
+ return ghostStack(el, { lines, visible: 3, w, h, scene });
141
+ }
142
+ case "ransomLetters": {
143
+ const accent = color(ctx, "accent", "");
144
+ const l = lum(accent);
145
+ const palette = l >= 0.12 && l <= 0.3 ? [accent, ...RANSOM] : RANSOM;
146
+ return windows(el, hits, 6, (box, hs, k) => ransomLetters(box, { text: words[k % words.length], at: Math.max(0, hs[0] - 0.02), hits: hs, palette, w, h, seed: `${scene}:${k}`, scene }));
147
+ }
148
+ case "neonSign": {
149
+ // 네온은 어두운 판 위에서 산다 — 밝은 테마에서도 간판 판을 깐다
150
+ const plate = div(el.ownerDocument, `position:absolute;inset:0;border-radius:${Math.round(Math.min(w, h) * 0.06)}px;background:#0a090d`);
151
+ el.append(plate);
152
+ const neon = color(ctx, "accent", "#FF4F7A");
153
+ return windows(plate, hits, 6, (box, hs, k) => neonSign(box, { text: words[k % words.length], at: hs[0] + 0.02, color: neon, w, h, seed: `${scene}:${k}`, scene }));
154
+ }
155
+ case "ledBoard": {
156
+ const text = cycle(words, 3).join(" ");
157
+ // 글이 장면 끝에서야 다 지나가는 빠르기 — 일찍 다 지나가면 빈 판이 멈춘 화면이 된다. 판 칸 수 = 폭 ÷ (판 높이/18), 글 폭은 16점 글꼴 0.6em 어림
158
+ const cols = Math.floor((w * 18) / (h * 0.4));
159
+ const speed = (cols + text.length * 16 * 0.6) / (ctx.dur + 0.3);
160
+ return ledBoard(el, { text, mode: "scroll", speed, at: 0, x: 0, y: h * 0.3, w, h: h * 0.4, stageW: w, stageH: h, color: color(ctx, "accent", "#FFB23E"), scene });
161
+ }
162
+ default:
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 장면 경계를 걸치는 덮기 — out 은 장면 끝에서 덮고, in 은 다음 장면 처음에 걷힌다. 다 덮인 채 서 있는 시간은 0.05초 안팎 —
169
+ * 한 색 화면이 0.1초를 넘으면 멈춤 검사(frame.freeze)에 걸린다. 조리개는 끝까지 닫지 않고 바늘구멍에서 되돌아 연다. 두 장면이 경계의 영상 시각을 같은 씨앗으로 써서
170
+ * 앞 장면 마지막 프레임과 뒤 장면 첫 프레임이 같은 덮개로 이어진다. el 은 화면 전체, 맨 위 층
171
+ */
172
+ export function dressWipe(name: string, el: HTMLElement, ctx: SceneCtx, side: "in" | "out"): Paint | null {
173
+ const { width: w, height: h } = ctx.meta;
174
+ const B = side === "out" ? ctx.dur : 0;
175
+ const seed = `${name}@${(ctx.from + B).toFixed(3)}`;
176
+ const accent = color(ctx, "accent", "#5b3fd9");
177
+ const ink = color(ctx, "ink", "#111111");
178
+ switch (name) {
179
+ case "brushWipe":
180
+ return brushWipe(el, { w, h, at: B - 0.35, dur: 0.7, strokes: 5, color: accent, seed, scene: ctx.id });
181
+ case "irisBlades":
182
+ // 다 열린 육각 구멍이 화면 모서리까지 덮게 — 변까지 거리가 대각선 반을 넘어야 모서리에 날이 안 남는다
183
+ return irisBlades(el, { w, h, color: ink, seam: "rgba(255,255,255,.3)", radius: (Math.hypot(w, h) / 2 / Math.cos(Math.PI / 6)) * 1.04, stiffness: 260, damping: 32, steps: [{ at: B - 100, open: 1 }, { at: B - 0.32, open: 0 }, { at: B - 0.02, open: 1 }], scene: ctx.id });
184
+ case "flood": {
185
+ const zero = { x: w / 2, y: h / 2, w: 0, h: 0, radius: 0 };
186
+ return flood(el, { w, h, x: w / 2, y: h / 2, color: accent, steps: [{ at: B - 100, shape: zero }, { at: B - 0.28, shape: "cover" }, { at: B - 0.02, shape: zero }], scene: ctx.id });
187
+ }
188
+ default:
189
+ return null;
190
+ }
191
+ }
@@ -0,0 +1,70 @@
1
+ import { clamp, ease, safeTime } from "@kasaframe/core";
2
+ import { div, fitSize, mark } from "./fit.ts";
3
+
4
+ export interface EchoStackOpts {
5
+ word: string;
6
+ /** 메아리가 한 벌씩 생기는 시각(장면 초) — 첫 시각에 원본 */
7
+ hits: number[];
8
+ /** down: 아래로 줄줄이 · back: 뒤로 작아지며 겹친다 */
9
+ dir?: "down" | "back";
10
+ size?: number;
11
+ w?: number;
12
+ h?: number;
13
+ scene?: string;
14
+ }
15
+
16
+ /**
17
+ * 같은 낱말의 메아리 — 박마다 한 벌씩 앞 벌에서 미끄러져 나와 쌓이고, 멀어질수록 외곽선만 남으며 흐려진다. 새 벌은 잠깐 강조색.
18
+ * (ESCAPE VELOCITY 272.5–274초 「BACK」 다섯 줄 · mexicat 60초 숫자 잔상)
19
+ */
20
+ export function echoStack(el: HTMLElement, o: EchoStackOpts): (t: number) => void {
21
+ const doc = el.ownerDocument;
22
+ const W = o.w ?? 1920;
23
+ const H = o.h ?? 1080;
24
+ const n = o.hits.length;
25
+ const back = o.dir === "back";
26
+ const px = o.size ?? fitSize(o.word, W, H, back ? { share: 0.7, hShare: 0.44 } : { share: 0.62, hShare: 1.12 / Math.max(1, n) });
27
+ // 벌 사이를 촘촘히 — 메아리가 한 덩어리로 읽히게
28
+ const lh = px * 0.74;
29
+ const layer = div(doc, "position:absolute;inset:0;overflow:hidden");
30
+ const block = div(doc, `position:absolute;left:50%;top:50%;width:0;height:0;font-family:var(--kf-font-display);font-weight:900;font-size:${px}px;line-height:${lh}px;letter-spacing:-.03em;white-space:nowrap`);
31
+ layer.append(block);
32
+ el.append(layer);
33
+ const copies = o.hits.map((_, i) => {
34
+ const c = div(doc, `position:absolute;left:0;top:0;transform-origin:50% 50%;z-index:${n - i}`, o.word);
35
+ if (i > 0 && i % 2 === 1) Object.assign(c.style, { color: "transparent", webkitTextStroke: ".025em var(--kf-ink)" });
36
+ block.append(c);
37
+ return c;
38
+ });
39
+
40
+ return (t: number) => {
41
+ t = safeTime(t);
42
+ let moving = false;
43
+ copies.forEach((c, i) => {
44
+ const local = t - o.hits[i];
45
+ const p = ease.outCubic(clamp(local / 0.16));
46
+ if (local < 0) {
47
+ c.style.opacity = "0";
48
+ return;
49
+ }
50
+ if (local < 0.16) moving = true;
51
+ const fade = 1 - (0.8 * i) / Math.max(1, n);
52
+ c.style.opacity = (p * fade).toFixed(3);
53
+ const hotNow = local < 0.25 && i > 0;
54
+ if (i % 2 === 0) c.style.color = hotNow ? "var(--kf-accent)" : "var(--kf-ink)";
55
+ else c.style.webkitTextStroke = `.025em ${hotNow ? "var(--kf-accent)" : "var(--kf-ink)"}`;
56
+ if (back) {
57
+ const s = 0.84 ** (i - 1 + p);
58
+ const y = -(i - 1 + p) * px * 0.2;
59
+ c.style.transform = `translate(-50%,${(-lh / 2 + y).toFixed(1)}px) scale(${(i === 0 ? 1 : s).toFixed(4)})`;
60
+ } else {
61
+ const y = (i - (n - 1) / 2) * lh - (1 - p) * lh * 0.6;
62
+ const x = (i - (n - 1) / 2) * px * 0.06;
63
+ c.style.transform = `translate(calc(-50% + ${x.toFixed(1)}px),${(y - lh / 2).toFixed(1)}px)`;
64
+ }
65
+ });
66
+ // 묶음 전체가 옆으로 흔들리며 흘러 다음 박까지 서지 않는다 — 위아래로 퍼지거나 커지면 끝 벌이 화면 밖으로 잘린다
67
+ block.style.transform = `translate(${(W * 0.07 * Math.sin(t * 1.9)).toFixed(1)}px,0) scale(${(1 + 0.02 * t).toFixed(4)})`;
68
+ mark("echoStack", o.scene, moving);
69
+ };
70
+ }
@@ -0,0 +1,70 @@
1
+ import { emit } from "@kasaframe/core";
2
+
3
+ // 수들이 같이 쓰는 도우미. build 때는 장면이 숨어 있어 글 폭을 잴 수 없다 — 글자 수로 어림한다.
4
+
5
+ /** 글 폭을 글자 크기(em) 단위로 어림 — 한글·한자 1, 라틴 0.62, 빈칸 0.3 */
6
+ export function textEm(text: string): number {
7
+ return [...text].reduce((a, c) => a + (/[ᄀ-ᇿ㄰-㆏가-힯一-鿿]/.test(c) ? 1 : c === " " ? 0.3 : 0.62), 0) || 1;
8
+ }
9
+
10
+ /** 폭 `w` 의 `share` 몫, 높이 `h` 의 `hShare` 몫 안에 드는 글자 크기(px) */
11
+ export function fitSize(text: string, w: number, h: number, o: { share?: number; hShare?: number; max?: number; min?: number } = {}): number {
12
+ const byW = (w * (o.share ?? 0.86)) / textEm(text);
13
+ const byH = h * (o.hShare ?? 0.62);
14
+ return Math.round(Math.max(o.min ?? 24, Math.min(o.max ?? 2000, byW, byH)));
15
+ }
16
+
17
+ /** 시각 목록에서 t 에 켜져 있는 칸 — 첫 시각 전이면 -1 */
18
+ export function lastIndex(times: readonly number[], t: number): number {
19
+ let k = -1;
20
+ for (let i = 0; i < times.length; i++) if (times[i] <= t) k = i;
21
+ return k;
22
+ }
23
+
24
+ export function div(doc: Document, css: string, text?: string): HTMLDivElement {
25
+ const d = doc.createElement("div");
26
+ d.style.cssText = css;
27
+ if (text !== undefined) d.textContent = text;
28
+ return d;
29
+ }
30
+
31
+ /** 움직이는 동안 검사용 기록을 남긴다 — 같은 수를 이어지는 장면에 되풀이하면 motion.repeat 가 잡는다 */
32
+ export function mark(name: string, scene: string | undefined, active: boolean): void {
33
+ if (active) emit({ kind: "move", source: name, scene, name: `move:${name}`, value: 1 });
34
+ }
35
+
36
+ /**
37
+ * 띠 글 — 낱말 조각들(pieces, 한 바퀴)을 이어 붙여 흐르게 하되, 화면 끝에 걸친 조각은 끝에 닿기 전에 흐려져 사라진다(양 끝 흐림).
38
+ * 글이 잘린 채 화면 끝에 머물면 검사가 text.offscreen 으로 잡는다 — 그래서 한 줄 대신 낱말마다 따로 둔다(긴 줄 하나면 늘 끝에 걸려 안 보인다).
39
+ * cycles 는 화면을 덮는 바퀴 수, em 은 조각 폭 어림(px, 처음 보일 때 잰 폭으로 바뀐다).
40
+ * 반환 함수: offset(띠 안 px, 흐른 거리) · toScreen(띠 안 x → 화면 x) · view(화면에서 보이는 [왼쪽, 오른쪽]) · fade(흐려지는 폭 px)
41
+ */
42
+ export function strip(host: HTMLElement, pieces: readonly string[], cycles: number, guess: (piece: string) => number): (offset: number, toScreen: (x: number) => number, view: [number, number], fade: number) => void {
43
+ const doc = host.ownerDocument;
44
+ const spans = Array.from({ length: cycles * pieces.length }, (_, k) => {
45
+ const s = doc.createElement("span");
46
+ s.textContent = pieces[k % pieces.length];
47
+ s.style.cssText = "position:absolute;left:0;top:50%;transform:translateY(-50%);white-space:pre";
48
+ host.append(s);
49
+ return s;
50
+ });
51
+ let widths: number[] | null = null;
52
+ return (offset, toScreen, view, fade) => {
53
+ // 조각 폭은 글꼴이 걸린 뒤 처음 보일 때 잰다(build 때는 장면이 숨어 있다)
54
+ if (!widths && spans[0].offsetWidth) widths = pieces.map((_, i) => spans[i].offsetWidth);
55
+ const ws = widths ?? pieces.map(guess);
56
+ const lap = ws.reduce((a, b) => a + b, 0) || 1;
57
+ const total = lap * cycles;
58
+ let acc = 0;
59
+ spans.forEach((s, k) => {
60
+ const w = ws[k % pieces.length];
61
+ const x = ((((offset + acc) % total) + total) % total) - lap;
62
+ acc += w;
63
+ const a = toScreen(x);
64
+ const b = toScreen(x + w);
65
+ const edge = Math.min(Math.min(a, b) - view[0], view[1] - Math.max(a, b));
66
+ s.style.left = `${x.toFixed(1)}px`;
67
+ s.style.opacity = Math.max(0, Math.min(1, edge / fade)).toFixed(3);
68
+ });
69
+ };
70
+ }
@@ -0,0 +1,61 @@
1
+ import { safeTime } from "@kasaframe/core";
2
+ import { div, mark } from "./fit.ts";
3
+ import { springSteps, type SpringStep } from "./liquidTab.ts";
4
+
5
+ export type FloodShape = "dot" | "cover" | { x: number; y: number; w: number; h: number; radius?: number };
6
+
7
+ export interface FloodOpts {
8
+ /** 점이 생기고 덮기가 퍼져 나가는 자리(px) */
9
+ x?: number;
10
+ y?: number;
11
+ /** 모양이 바뀌는 시각과 모양 — "dot" 작은 점 · "cover" 화면을 넘치게 덮기 · 상자(다음 장면의 액자 자리) */
12
+ steps: { at: number; shape: FloodShape }[];
13
+ color?: string;
14
+ /** 점 지름(px) */
15
+ dot?: number;
16
+ w?: number;
17
+ h?: number;
18
+ stiffness?: number;
19
+ damping?: number;
20
+ scene?: string;
21
+ }
22
+
23
+ /**
24
+ * 덮고 오므리기 — 점 하나가 화면 밖까지 넘치게 퍼져 덮었다가, 다음 장면의 모양(알약·액자)으로 오므라든다.
25
+ * 덮을 때는 대각선의 1.1배까지 0.3초쯤 — 짧으면 한 프레임에 화면 반이 바뀌어 튄다. (twoclipping 공개 영상 1.4·22.6초)
26
+ */
27
+ export function flood(el: HTMLElement, o: FloodOpts): (t: number) => void {
28
+ const W = o.w ?? 1920;
29
+ const H = o.h ?? 1080;
30
+ const ox = o.x ?? W / 2;
31
+ const oy = o.y ?? H / 2;
32
+ const dot = o.dot ?? 28;
33
+ // 퍼지는 점에서 가장 먼 모서리까지의 1.1배 — 어느 자리에서 시작해도 모서리가 남지 않게
34
+ const reach = 1.1 * Math.max(Math.hypot(ox, oy), Math.hypot(W - ox, oy), Math.hypot(ox, H - oy), Math.hypot(W - ox, H - oy));
35
+ const rect = (s: FloodShape) =>
36
+ s === "dot" ? { x: ox, y: oy, w: dot, h: dot, r: dot / 2 } : s === "cover" ? { x: ox, y: oy, w: reach * 2, h: reach * 2, r: reach } : { x: s.x + s.w / 2, y: s.y + s.h / 2, w: s.w, h: s.h, r: s.radius ?? 28 };
37
+ const rects = o.steps.map((s) => ({ at: s.at, ...rect(s.shape) }));
38
+ const track = (k: "x" | "y" | "w" | "h" | "r"): SpringStep[] => rects.map((r) => ({ at: r.at, to: r[k] }));
39
+ const tracks = { x: track("x"), y: track("y"), w: track("w"), h: track("h"), r: track("r") };
40
+ const shape = div(el.ownerDocument, `position:absolute;left:0;top:0;background:${o.color ?? "var(--kf-ink)"};will-change:transform`);
41
+ el.append(shape);
42
+ const first = o.steps[0]?.at ?? 0;
43
+ const sp = { stiffness: o.stiffness ?? 240, damping: o.damping ?? 30, from: 0 };
44
+ return (t: number) => {
45
+ t = safeTime(t);
46
+ if (t < first) {
47
+ shape.style.display = "none";
48
+ return;
49
+ }
50
+ shape.style.display = "block";
51
+ const v = (k: keyof typeof tracks) => springSteps(t, tracks[k], { ...sp, from: tracks[k][0].to });
52
+ const w = Math.max(0, v("w"));
53
+ const h = Math.max(0, v("h"));
54
+ const r = Math.max(0, Math.min(v("r"), w / 2, h / 2));
55
+ shape.style.width = `${w.toFixed(1)}px`;
56
+ shape.style.height = `${h.toFixed(1)}px`;
57
+ shape.style.borderRadius = `${r.toFixed(1)}px`;
58
+ shape.style.transform = `translate(${(v("x") - w / 2).toFixed(1)}px, ${(v("y") - h / 2).toFixed(1)}px)`;
59
+ mark("flood", o.scene, o.steps.some((s) => t >= s.at && t < s.at + 0.45));
60
+ };
61
+ }
@@ -0,0 +1,88 @@
1
+ import { clamp, ease, safeTime } from "@kasaframe/core";
2
+ import { div, fitSize, lastIndex, mark } from "./fit.ts";
3
+
4
+ export interface GhostLine {
5
+ text: string;
6
+ /** 이 줄이 채워지기 시작하는 시각(장면 초) — 보통 박 */
7
+ at: number;
8
+ /** 강조색으로 채울 낱말 번호(빈칸으로 나눈) */
9
+ hot?: number[];
10
+ }
11
+
12
+ export interface GhostStackOpts {
13
+ lines: GhostLine[];
14
+ /** 한 화면에 보이는 줄 수 */
15
+ visible?: number;
16
+ /** 한 줄을 채우는 가장 짧은 초 — 다음 줄 박이 있으면 그 박까지 같은 빠르기로 채운다 */
17
+ fill?: number;
18
+ w?: number;
19
+ h?: number;
20
+ scene?: string;
21
+ }
22
+
23
+ /**
24
+ * 외곽선 유령이 먼저 깔리고 부를 때 채워진다 — 다음 줄은 속 빈 외곽선으로 미리 보이고, 제 박에 왼쪽부터 채워지며 묶음이 한 줄씩 위로 밀린다.
25
+ * (mexicat p(doom) 116.5–118.7초 「BREAKING THROUGH EACH SAFETY FENCE」)
26
+ */
27
+ export function ghostStack(el: HTMLElement, o: GhostStackOpts): (t: number) => void {
28
+ const doc = el.ownerDocument;
29
+ const W = o.w ?? 1920;
30
+ const H = o.h ?? 1080;
31
+ const vis = o.visible ?? 3;
32
+ const fillDur = o.fill ?? 0.22;
33
+ const longest = o.lines.reduce((a, l) => (l.text.length > a.length ? l.text : a), "");
34
+ const px = fitSize(longest, W, H, { share: 0.92, hShare: 0.86 / vis });
35
+ const lh = px * 0.94;
36
+ const layer = div(doc, "position:absolute;inset:0;overflow:hidden");
37
+ const stack = div(doc, `position:absolute;left:${W * 0.04}px;top:0;width:${W * 0.92}px;font-family:var(--kf-font-display);font-weight:900;font-size:${px}px;line-height:${lh}px;letter-spacing:-.035em;white-space:nowrap`);
38
+ layer.append(stack);
39
+ el.append(layer);
40
+ const rows = o.lines.map((l, i) => {
41
+ const row = div(doc, `position:absolute;left:0;top:${i * lh}px;height:${lh}px`);
42
+ // 유령 줄도 읽혀야 한다 — 가는 반투명 외곽선은 밝은 바탕에서 대비 1.2:1 로 검사(text.contrast)에 걸렸다
43
+ const ghost = div(doc, "position:absolute;left:0;top:0;color:transparent;-webkit-text-stroke:.03em var(--kf-ink);opacity:.85");
44
+ const full = div(doc, "position:absolute;left:0;top:0;color:var(--kf-ink)");
45
+ l.text.split(" ").forEach((w, j, all) => {
46
+ const span = doc.createElement("span");
47
+ span.textContent = j < all.length - 1 ? `${w} ` : w;
48
+ if (l.hot?.includes(j)) span.style.color = "var(--kf-accent)";
49
+ full.append(span);
50
+ });
51
+ ghost.textContent = l.text;
52
+ row.append(ghost, full);
53
+ stack.append(row);
54
+ return { row, ghost, full };
55
+ });
56
+ const times = o.lines.map((l) => l.at);
57
+ // 마지막 줄은 다음 박이 없다 — 앞 줄들의 박 간격만큼 채운다
58
+ const gap = times.length > 1 ? (times[times.length - 1] - times[0]) / (times.length - 1) : fillDur;
59
+
60
+ return (t: number) => {
61
+ t = safeTime(t);
62
+ const k = lastIndex(times, t);
63
+ // 채워지는 줄이 아래에서 둘째 칸에 오도록 — 박마다 0.2초 만에 한 줄 밀린다
64
+ const target = (i: number) => Math.max(0, i - (vis - 2));
65
+ const prev = target(Math.max(0, k - 1));
66
+ const cur = target(Math.max(0, k));
67
+ const p = k < 0 ? 1 : ease.outCubic(clamp((t - times[k]) / 0.2));
68
+ const scroll = prev + (cur - prev) * p;
69
+ // 박 사이에도 묶음이 천천히 떠올라 멈추지 않는다
70
+ const drift = 14 * t;
71
+ const top = H / 2 - (vis * lh) / 2 - scroll * lh - drift;
72
+ stack.style.transform = `translateY(${top.toFixed(1)}px)`;
73
+ rows.forEach((r, i) => {
74
+ // 유령은 앞 줄이 채워질 때 나타난다
75
+ const ghostAt = i === 0 ? times[0] - 0.3 : times[i - 1];
76
+ r.ghost.style.opacity = (0.85 * clamp((t - ghostAt) / 0.15)).toFixed(3);
77
+ // 다음 줄 박까지 같은 빠르기로 채운다 — 읽어 나가는 빠르기. 0.2초 만에 다 채우면 다음 박까지 화면이 선다
78
+ const span = Math.max(fillDur, times[i + 1] !== undefined ? times[i + 1] - times[i] : gap);
79
+ const f = clamp((t - times[i]) / span);
80
+ r.full.style.clipPath = `inset(0 ${((1 - f) * 100).toFixed(2)}% 0 0)`;
81
+ r.row.style.display = i < cur - 1 || i > k + 1 ? "none" : "block";
82
+ // 위로 밀려 나가는 줄은 화면 끝에 닿기 전에 흐려진다 — 잘린 글이 남지 않게(검사 text.offscreen)
83
+ const y = top + i * lh;
84
+ r.row.style.opacity = Math.max(0, Math.min(1, y / (lh * 0.3), (H - y - lh) / (lh * 0.3))).toFixed(3);
85
+ });
86
+ mark("ghostStack", o.scene, k >= 0 && t - times[k] < fillDur);
87
+ };
88
+ }
@@ -0,0 +1,100 @@
1
+ import { safeTime } from "@kasaframe/core";
2
+ import { mark } from "./fit.ts";
3
+
4
+ export interface GridFloorOpts {
5
+ /** 지평선 높이(화면 높이의 몫) */
6
+ horizon?: number;
7
+ /** 소실점 가로 자리(px) */
8
+ vx?: number;
9
+ color?: string;
10
+ /** 빛 번짐(px) — 0 이면 없음 */
11
+ glow?: number;
12
+ /** 앞으로 흐르는 속도(칸/초) */
13
+ speed?: number;
14
+ /** 세로선 수(한쪽) */
15
+ columns?: number;
16
+ /** 가로선 수 */
17
+ rows?: number;
18
+ /** 천장에도 같은 모눈 — 터널 */
19
+ tunnel?: boolean;
20
+ lineWidth?: number;
21
+ w?: number;
22
+ h?: number;
23
+ scene?: string;
24
+ }
25
+
26
+ /**
27
+ * 원근 모눈 바닥 — 가로선 간격이 깊이(z)를 따라 좁아지고 z 를 t 로 흘려 앞으로 달린다. tunnel 이면 천장도.
28
+ * 두께 있는 글은 같이 내보내는 extrude() 를 text-shadow 에. (mexicat p(doom) 47.8–49.3초 · Where No Map Goes 14.8·65.4초)
29
+ */
30
+ export function gridFloor(el: HTMLElement, o: GridFloorOpts = {}): (t: number) => void {
31
+ const doc = el.ownerDocument;
32
+ const W = o.w ?? 1920;
33
+ const H = o.h ?? 1080;
34
+ const canvas = doc.createElement("canvas");
35
+ canvas.width = W;
36
+ canvas.height = H;
37
+ canvas.style.cssText = `position:absolute;left:0;top:0;width:${W}px;height:${H}px;pointer-events:none`;
38
+ el.append(canvas);
39
+ const g = canvas.getContext("2d")!;
40
+ const hy = H * (o.horizon ?? 0.42);
41
+ const vx = o.vx ?? W / 2;
42
+ const cols = o.columns ?? 14;
43
+ const rows = o.rows ?? 22;
44
+ const color = o.color ?? "#3BE7FF";
45
+ // 카메라 높이·초점 — 가장 가까운 가로선(z=1)이 화면 아래 끝에 오게
46
+ const near = 1;
47
+ const far = rows;
48
+ const floorSpan = H - hy;
49
+ const ceilSpan = hy;
50
+ return (t: number) => {
51
+ t = safeTime(t);
52
+ g.clearRect(0, 0, W, H);
53
+ g.strokeStyle = color;
54
+ g.lineWidth = o.lineWidth ?? 2.5;
55
+ g.shadowColor = color;
56
+ g.shadowBlur = o.glow ?? 14;
57
+ const scroll = (t * (o.speed ?? 2.4)) % 1;
58
+ const planes: [number, number][] = [[1, floorSpan]];
59
+ if (o.tunnel) planes.push([-1, ceilSpan]);
60
+ for (const [sign, span] of planes) {
61
+ // 가로선 — z 가 near..far, 화면 높이는 1/z 에 비례
62
+ for (let k = 0; k <= rows; k++) {
63
+ const z = near + k - scroll;
64
+ if (z < near * 0.6 || z > far) continue;
65
+ const y = hy + (sign * span * near) / z;
66
+ g.globalAlpha = Math.min(1, 1.6 - z / far);
67
+ g.beginPath();
68
+ g.moveTo(0, y);
69
+ g.lineTo(W, y);
70
+ g.stroke();
71
+ }
72
+ // 세로선 — 소실점에서 화면 끝 너머로 퍼진다
73
+ g.globalAlpha = 0.85;
74
+ for (let c = -cols; c <= cols; c++) {
75
+ const x = vx + (c / cols) * W * 1.6;
76
+ g.beginPath();
77
+ g.moveTo(vx, hy);
78
+ g.lineTo(x, hy + sign * span);
79
+ g.stroke();
80
+ }
81
+ }
82
+ g.globalAlpha = 1;
83
+ g.shadowBlur = 0;
84
+ mark("gridFloor", o.scene, true);
85
+ };
86
+ }
87
+
88
+ /**
89
+ * 두께 있는 글 — text-shadow 를 1px 씩 n 겹, 뒤로 갈수록 어둡게. 원근 바닥 위로 날아가는 글에. `el.style.textShadow = extrude(14, "#ff7a00")`
90
+ */
91
+ export function extrude(n: number, color: string, o: { dx?: number; dy?: number; shade?: number } = {}): string {
92
+ const dx = o.dx ?? 1;
93
+ const dy = o.dy ?? 1;
94
+ const shade = o.shade ?? 55;
95
+ return Array.from({ length: n }, (_, i) => {
96
+ const k = i + 1;
97
+ const dark = Math.round(20 + (shade * k) / n);
98
+ return `${(dx * k).toFixed(1)}px ${(dy * k).toFixed(1)}px 0 color-mix(in srgb, ${color} ${100 - dark}%, black)`;
99
+ }).join(", ");
100
+ }