kasaframe 0.0.0-stage → 0.1.1

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 (338) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +146 -0
  3. package/README.md +77 -2
  4. package/app/package.json +12 -0
  5. package/app/server/engine.ts +415 -0
  6. package/app/server/main.ts +205 -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 +1013 -0
  11. package/app/web/style.css +430 -0
  12. package/bin/hooks.mjs +51 -0
  13. package/bin/kf.mjs +14 -0
  14. package/docs/AGENTS.md +238 -0
  15. package/music/README.md +13 -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 +273 -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 +42 -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 +135 -0
  43. package/packages/check/src/rules/layout.ts +257 -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 +29 -0
  52. package/packages/cli/src/blur.ts +68 -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/blender.ts +270 -0
  58. package/packages/cli/src/commands/check.ts +25 -0
  59. package/packages/cli/src/commands/doctor.ts +22 -0
  60. package/packages/cli/src/commands/gen.ts +173 -0
  61. package/packages/cli/src/commands/look.ts +39 -0
  62. package/packages/cli/src/commands/lyrics.ts +111 -0
  63. package/packages/cli/src/commands/make.ts +542 -0
  64. package/packages/cli/src/commands/new.ts +176 -0
  65. package/packages/cli/src/commands/preview.ts +43 -0
  66. package/packages/cli/src/commands/render.ts +216 -0
  67. package/packages/cli/src/commands/setup.ts +63 -0
  68. package/packages/cli/src/commands/sheet.ts +114 -0
  69. package/packages/cli/src/commands/skill.ts +43 -0
  70. package/packages/cli/src/commands/still.ts +40 -0
  71. package/packages/cli/src/commands/theme.ts +85 -0
  72. package/packages/cli/src/commands/transitions.ts +40 -0
  73. package/packages/cli/src/deps.ts +122 -0
  74. package/packages/cli/src/ffmpeg.ts +81 -0
  75. package/packages/cli/src/index.ts +8 -0
  76. package/packages/cli/src/kf.ts +53 -0
  77. package/packages/cli/src/lock.ts +72 -0
  78. package/packages/cli/src/out.ts +13 -0
  79. package/packages/cli/src/serve.ts +124 -0
  80. package/packages/clock/package.json +14 -0
  81. package/packages/clock/src/index.ts +25 -0
  82. package/packages/clock/src/install.ts +351 -0
  83. package/packages/clock/src/types.ts +45 -0
  84. package/packages/core/package.json +9 -0
  85. package/packages/core/src/beats.ts +283 -0
  86. package/packages/core/src/ease.ts +87 -0
  87. package/packages/core/src/frame.ts +99 -0
  88. package/packages/core/src/index.ts +27 -0
  89. package/packages/core/src/project.ts +146 -0
  90. package/packages/core/src/rand.ts +56 -0
  91. package/packages/core/src/spring.ts +52 -0
  92. package/packages/core/src/style.ts +100 -0
  93. package/packages/core/src/time.ts +88 -0
  94. package/packages/gen/PROVIDERS.md +55 -0
  95. package/packages/gen/package.json +9 -0
  96. package/packages/gen/src/generate.ts +204 -0
  97. package/packages/gen/src/image/og.ts +133 -0
  98. package/packages/gen/src/index.ts +18 -0
  99. package/packages/gen/src/keys.ts +60 -0
  100. package/packages/gen/src/media.ts +150 -0
  101. package/packages/gen/src/plan.ts +106 -0
  102. package/packages/gen/src/registry.ts +51 -0
  103. package/packages/gen/src/seam.ts +103 -0
  104. package/packages/gen/src/types.ts +86 -0
  105. package/packages/gen/src/video/fal.ts +140 -0
  106. package/packages/gen/src/video/h3.ts +107 -0
  107. package/packages/gen/src/video/http.ts +44 -0
  108. package/packages/gen/src/video/ltx.ts +60 -0
  109. package/packages/gen/src/video/minimax.ts +55 -0
  110. package/packages/gen/src/video/veo.ts +71 -0
  111. package/packages/jizura/package.json +16 -0
  112. package/packages/jizura/src/deny.ts +73 -0
  113. package/packages/jizura/src/index.ts +7 -0
  114. package/packages/jizura/src/install.ts +226 -0
  115. package/packages/jizura/src/layer.ts +282 -0
  116. package/packages/jizura/src/lyrics.ts +81 -0
  117. package/packages/jizura/src/pin.ts +5 -0
  118. package/packages/jizura/src/vite.ts +36 -0
  119. package/packages/motion/README.md +142 -0
  120. package/packages/motion/package.json +29 -0
  121. package/packages/motion/src/camera.ts +97 -0
  122. package/packages/motion/src/captions.ts +479 -0
  123. package/packages/motion/src/enter.ts +128 -0
  124. package/packages/motion/src/hangul.ts +129 -0
  125. package/packages/motion/src/impact.ts +100 -0
  126. package/packages/motion/src/index.ts +17 -0
  127. package/packages/motion/src/moves/brushWipe.ts +90 -0
  128. package/packages/motion/src/moves/cloneArray.ts +111 -0
  129. package/packages/motion/src/moves/curtains.ts +81 -0
  130. package/packages/motion/src/moves/echoStack.ts +70 -0
  131. package/packages/motion/src/moves/fit.ts +34 -0
  132. package/packages/motion/src/moves/flood.ts +61 -0
  133. package/packages/motion/src/moves/ghostStack.ts +79 -0
  134. package/packages/motion/src/moves/gridFloor.ts +100 -0
  135. package/packages/motion/src/moves/imageSwarm.ts +109 -0
  136. package/packages/motion/src/moves/index.ts +19 -0
  137. package/packages/motion/src/moves/irisBlades.ts +113 -0
  138. package/packages/motion/src/moves/karaokeLine.ts +94 -0
  139. package/packages/motion/src/moves/ledBoard.ts +94 -0
  140. package/packages/motion/src/moves/liquidTab.ts +72 -0
  141. package/packages/motion/src/moves/markerWrite.ts +63 -0
  142. package/packages/motion/src/moves/morphBox.ts +96 -0
  143. package/packages/motion/src/moves/neonSign.ts +77 -0
  144. package/packages/motion/src/moves/panelPop.ts +78 -0
  145. package/packages/motion/src/moves/paperTear.ts +75 -0
  146. package/packages/motion/src/moves/path.ts +64 -0
  147. package/packages/motion/src/moves/pathText.ts +107 -0
  148. package/packages/motion/src/moves/pointSphere.ts +109 -0
  149. package/packages/motion/src/moves/ransomLetters.ts +73 -0
  150. package/packages/motion/src/moves/shapeMorph.ts +143 -0
  151. package/packages/motion/src/moves/slamWords.ts +89 -0
  152. package/packages/motion/src/moves/sparkTrail.ts +114 -0
  153. package/packages/motion/src/moves/speedLines.ts +91 -0
  154. package/packages/motion/src/moves/splitFlap.ts +147 -0
  155. package/packages/motion/src/moves/stepWords.ts +117 -0
  156. package/packages/motion/src/moves/strikeOut.ts +98 -0
  157. package/packages/motion/src/moves/sunburst.ts +58 -0
  158. package/packages/motion/src/moves/tickerRows.ts +87 -0
  159. package/packages/motion/src/moves/tokenPicker.ts +114 -0
  160. package/packages/motion/src/moves/truchet.ts +85 -0
  161. package/packages/motion/src/moves/wordWall.ts +76 -0
  162. package/packages/motion/src/moves/worldRoll.ts +31 -0
  163. package/packages/motion/src/particles.ts +102 -0
  164. package/packages/motion/src/react/index.tsx +21 -0
  165. package/packages/motion/src/react/parts.tsx +431 -0
  166. package/packages/motion/src/react/time.tsx +97 -0
  167. package/packages/motion/src/text.ts +103 -0
  168. package/packages/motion/src/transition.ts +82 -0
  169. package/packages/motion/src/transition3d/cards.ts +65 -0
  170. package/packages/motion/src/transition3d/chain.ts +84 -0
  171. package/packages/motion/src/transition3d/dive.ts +181 -0
  172. package/packages/motion/src/transition3d/fold.ts +110 -0
  173. package/packages/motion/src/transition3d/gl.ts +219 -0
  174. package/packages/motion/src/transition3d/index.ts +228 -0
  175. package/packages/motion/src/transition3d/live.ts +70 -0
  176. package/packages/motion/src/transition3d/portal.ts +115 -0
  177. package/packages/motion/src/transition3d/react.tsx +29 -0
  178. package/packages/motion/src/transition3d/rig.ts +502 -0
  179. package/packages/motion/src/transition3d/shatter.ts +101 -0
  180. package/packages/motion/src/transition3d/whip.ts +65 -0
  181. package/packages/motion/src/transition3d/world.ts +683 -0
  182. package/packages/motion/src/transition3d/zoomOut.ts +114 -0
  183. package/packages/motion/src/transitions2d/catalog.ts +127 -0
  184. package/packages/motion/src/transitions2d/css.d.ts +2 -0
  185. package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
  186. package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
  187. package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
  188. package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
  189. package/packages/motion/src/transitions2d/index.ts +2 -0
  190. package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
  191. package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
  192. package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
  193. package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
  194. package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
  195. package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
  196. package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
  197. package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
  198. package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
  199. package/packages/motion/src/transitions2d/react.ts +13 -0
  200. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
  201. package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
  202. package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
  203. package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
  204. package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
  205. package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
  206. package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
  207. package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
  208. package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
  209. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
  210. package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
  211. package/packages/motion/src/type.ts +131 -0
  212. package/packages/motion/src/ui.ts +172 -0
  213. package/packages/plan/package.json +15 -0
  214. package/packages/plan/src/brief.ts +254 -0
  215. package/packages/plan/src/captions.ts +75 -0
  216. package/packages/plan/src/detect.ts +93 -0
  217. package/packages/plan/src/index.ts +23 -0
  218. package/packages/plan/src/layout.ts +176 -0
  219. package/packages/plan/src/llm.ts +110 -0
  220. package/packages/plan/src/look.ts +220 -0
  221. package/packages/plan/src/plan.ts +125 -0
  222. package/packages/plan/src/request.ts +55 -0
  223. package/packages/plan/src/studio.ts +53 -0
  224. package/packages/plan/src/templates.ts +42 -0
  225. package/packages/plan/src/themes.ts +48 -0
  226. package/packages/plan/src/types.ts +106 -0
  227. package/packages/studio/package.json +12 -0
  228. package/packages/studio/src/index.ts +1 -0
  229. package/packages/studio/src/plugin.ts +236 -0
  230. package/packages/studio/src/ui/main.ts +503 -0
  231. package/packages/studio/src/ui/studio.css +165 -0
  232. package/packages/studio/src/ui/timeline.ts +317 -0
  233. package/packages/themes/kit/THEME-MAKE.md +68 -0
  234. package/packages/themes/kit/THEME.md +30 -0
  235. package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
  236. package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
  237. package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
  238. package/packages/themes/kit/effects.ts +5 -0
  239. package/packages/themes/kit/sample/index.html +11 -0
  240. package/packages/themes/kit/sample/main.ts +9 -0
  241. package/packages/themes/kit/sample/scenes/scene.ts +15 -0
  242. package/packages/themes/package.json +16 -0
  243. package/packages/themes/src/factory.ts +296 -0
  244. package/packages/themes/src/fx.ts +14 -0
  245. package/packages/themes/src/index.ts +105 -0
  246. package/packages/themes/src/stage3d.ts +177 -0
  247. package/packages/themes/src/types.ts +58 -0
  248. package/presets/kawaii-pastel.json +94 -0
  249. package/presets/neutral-dark.json +82 -0
  250. package/presets/og.json +98 -0
  251. package/skills/kasaframe/SKILL.md +32 -0
  252. package/templates/_shared/project/index.html +11 -0
  253. package/templates/_shared/project/lib/react.ts +22 -0
  254. package/templates/_shared/project/lib/stage.ts +272 -0
  255. package/templates/_shared/project/main.ts +9 -0
  256. package/templates/_shared/scenes/default.ts +18 -0
  257. package/templates/_shared/scenes/drop.ts +30 -0
  258. package/templates/_shared/scenes/end.ts +23 -0
  259. package/templates/_shared/scenes/hook.ts +16 -0
  260. package/templates/_shared/scenes/seam.ts +41 -0
  261. package/templates/_shared/scenes/step.ts +40 -0
  262. package/templates/ad/preview.png +0 -0
  263. package/templates/ad/template.json +132 -0
  264. package/templates/demo/preview.png +0 -0
  265. package/templates/demo/template.json +130 -0
  266. package/templates/explainer/preview.png +0 -0
  267. package/templates/explainer/template.json +142 -0
  268. package/templates/launch/preview.png +0 -0
  269. package/templates/launch/template.json +130 -0
  270. package/templates/mv/preview.png +0 -0
  271. package/templates/mv/scenes/default.ts +95 -0
  272. package/templates/mv/template.json +176 -0
  273. package/templates/promo/preview.png +0 -0
  274. package/templates/promo/template.json +144 -0
  275. package/templates/shorts/preview.png +0 -0
  276. package/templates/shorts/scenes/hook.ts +34 -0
  277. package/templates/shorts/template.json +127 -0
  278. package/templates/square/preview.png +0 -0
  279. package/templates/square/template.json +113 -0
  280. package/themes/debug-hud/THEME.md +39 -0
  281. package/themes/debug-hud/effects.ts +389 -0
  282. package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
  283. package/themes/debug-hud/sample/beats.json +1 -0
  284. package/themes/debug-hud/sample/index.html +11 -0
  285. package/themes/debug-hud/sample/kf.json +59 -0
  286. package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
  287. package/themes/debug-hud/sample/lib/og.ts +29 -0
  288. package/themes/debug-hud/sample/main.ts +9 -0
  289. package/themes/debug-hud/sample/scenes/end.ts +62 -0
  290. package/themes/debug-hud/sample/scenes/gather.ts +67 -0
  291. package/themes/debug-hud/sample/scenes/models.ts +70 -0
  292. package/themes/debug-hud/sample/scenes/price.ts +79 -0
  293. package/themes/debug-hud/sample/style.json +91 -0
  294. package/themes/debug-hud/still.png +0 -0
  295. package/themes/debug-hud/style.json +91 -0
  296. package/themes/debug-hud/theme.json +68 -0
  297. package/themes/og-measure/THEME.md +47 -0
  298. package/themes/og-measure/effects.ts +432 -0
  299. package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
  300. package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
  301. package/themes/og-measure/sample/assets/data/measure.json +353 -0
  302. package/themes/og-measure/sample/beats.json +1 -0
  303. package/themes/og-measure/sample/index.html +11 -0
  304. package/themes/og-measure/sample/kf.json +66 -0
  305. package/themes/og-measure/sample/lib/data.ts +62 -0
  306. package/themes/og-measure/sample/main.ts +9 -0
  307. package/themes/og-measure/sample/measure.ts +78 -0
  308. package/themes/og-measure/sample/scenes/end.ts +47 -0
  309. package/themes/og-measure/sample/scenes/gather.ts +51 -0
  310. package/themes/og-measure/sample/scenes/models.ts +34 -0
  311. package/themes/og-measure/sample/scenes/one.ts +39 -0
  312. package/themes/og-measure/sample/scenes/price.ts +56 -0
  313. package/themes/og-measure/sample/style.json +97 -0
  314. package/themes/og-measure/still.png +0 -0
  315. package/themes/og-measure/style.json +97 -0
  316. package/themes/og-measure/theme.json +79 -0
  317. package/themes/riso-zine/THEME.md +43 -0
  318. package/themes/riso-zine/effects.ts +321 -0
  319. package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
  320. package/themes/riso-zine/sample/beats.json +1 -0
  321. package/themes/riso-zine/sample/index.html +11 -0
  322. package/themes/riso-zine/sample/kf.json +59 -0
  323. package/themes/riso-zine/sample/lib/og.ts +29 -0
  324. package/themes/riso-zine/sample/lib/shot.ts +86 -0
  325. package/themes/riso-zine/sample/lib/zine.ts +42 -0
  326. package/themes/riso-zine/sample/main.ts +9 -0
  327. package/themes/riso-zine/sample/scenes/end.ts +61 -0
  328. package/themes/riso-zine/sample/scenes/gather.ts +62 -0
  329. package/themes/riso-zine/sample/scenes/models.ts +52 -0
  330. package/themes/riso-zine/sample/scenes/price.ts +82 -0
  331. package/themes/riso-zine/sample/style.json +99 -0
  332. package/themes/riso-zine/still.png +0 -0
  333. package/themes/riso-zine/style.json +99 -0
  334. package/themes/riso-zine/theme.json +75 -0
  335. package/tools/beats/beats.py +372 -0
  336. package/tools/beats/requirements.txt +3 -0
  337. package/tools/lyrics/align.py +164 -0
  338. package/tools/lyrics/requirements.txt +9 -0
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "kawaii-pastel",
3
+ "label": "카와이 파스텔 — 퓨처베이스",
4
+ "scheme": "light",
5
+ "colors": {
6
+ "bg": "#FFF6FB",
7
+ "surface": "#FFFFFF",
8
+ "raised": "#FDE8F3",
9
+ "line": "#F6CFE4",
10
+ "ink": "#4A2B4F",
11
+ "inkSoft": "#6B4A70",
12
+ "dim": "#7E5F84",
13
+ "accent": "#FF7EB6",
14
+ "accentSoft": "#FFD1E6",
15
+ "accentText": "#C2387A",
16
+ "onAccent": "#4A2B4F",
17
+ "mint": "#7FD8C3",
18
+ "lavender": "#B9A7FF",
19
+ "sky": "#8FD3FF",
20
+ "lemon": "#FFE48A",
21
+ "peach": "#FFB59E",
22
+ "success": "#3CBF9A",
23
+ "danger": "#E5537A",
24
+ "warning": "#F2A93B"
25
+ },
26
+ "fonts": {
27
+ "sans": "\"Pretendard Variable\", Pretendard, \"Noto Sans KR\", \"Apple SD Gothic Neo\", sans-serif",
28
+ "display": "\"Jua\", \"Pretendard Variable\", \"Noto Sans KR\", \"Apple SD Gothic Neo\", sans-serif",
29
+ "mono": "\"JetBrains Mono\", ui-monospace, \"Pretendard Variable\", \"Noto Sans KR\", \"Apple SD Gothic Neo\", monospace",
30
+ "faces": [
31
+ {
32
+ "family": "Pretendard Variable",
33
+ "src": "https://cdn.jsdelivr.net/npm/pretendard@1.3.9/dist/web/variable/woff2/PretendardVariable.woff2",
34
+ "weight": "45 920"
35
+ },
36
+ {
37
+ "family": "JetBrains Mono",
38
+ "src": "https://cdn.jsdelivr.net/npm/@fontsource-variable/jetbrains-mono@5/files/jetbrains-mono-latin-wght-normal.woff2",
39
+ "weight": "100 800"
40
+ },
41
+ {
42
+ "family": "Jua",
43
+ "src": "https://cdn.jsdelivr.net/gh/google/fonts@main/ofl/jua/Jua-Regular.ttf",
44
+ "weight": "400"
45
+ }
46
+ ]
47
+ },
48
+ "type": {
49
+ "hero": 136,
50
+ "title": 80,
51
+ "body": 40,
52
+ "caption": 32,
53
+ "weightDisplay": 400,
54
+ "weightBody": 600,
55
+ "tracking": "-0.01em"
56
+ },
57
+ "radius": {
58
+ "sm": 16,
59
+ "md": 28,
60
+ "lg": 44,
61
+ "pill": 999
62
+ },
63
+ "shadow": {
64
+ "soft": "0 10px 30px rgba(255, 126, 182, 0.22)",
65
+ "lift": "0 6px 0 rgba(74, 43, 79, 0.12)"
66
+ },
67
+ "motion": {
68
+ "beatPulse": "drops-only",
69
+ "shake": {
70
+ "on": [
71
+ "drop"
72
+ ],
73
+ "maxPerPhrase": 1,
74
+ "maxPx": 10
75
+ },
76
+ "cameraMove": "once-per-phrase",
77
+ "repeatSameMove": false,
78
+ "maxFreezeSec": 0.5,
79
+ "enter": "pop",
80
+ "ease": "outBack",
81
+ "durations": {
82
+ "fast": 0.18,
83
+ "base": 0.36,
84
+ "slow": 0.7
85
+ }
86
+ },
87
+ "notes": [
88
+ "퓨처베이스 곡에 맞춘 밝은 파스텔. 분홍 강조 + 민트·라벤더·하늘·레몬을 보조로 한두 개만.",
89
+ "제목은 Jua(구글 폰트, OFL) — 굵기 한 벌뿐이라 weightDisplay 400. 본문은 Pretendard 600. 글꼴은 한 파일로 받는다(구글 CSS 의 unicode-range 조각은 안 쓴 글자 조각을 늦게 받아 font-display:block 동안 글이 투명하게 찍힌다).",
90
+ "accent 분홍은 바탕 위 글자로 2.2:1 — 글자는 accentText, 분홍은 면·반짝이·도형에.",
91
+ "등장은 pop + outBack 으로 통통 튀게. 그래도 박 펄스는 드롭에서만, 흔들림은 드롭에 한 번.",
92
+ "반짝이·파티클은 시드 고정(rand)으로 — 탭 나눠 찍어도 같은 자리."
93
+ ]
94
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "neutral-dark",
3
+ "label": "뉴트럴 다크",
4
+ "scheme": "dark",
5
+ "colors": {
6
+ "bg": "#0E0F12",
7
+ "surface": "#16181D",
8
+ "raised": "#1E2128",
9
+ "line": "#2C3038",
10
+ "ink": "#F2F3F5",
11
+ "inkSoft": "#C9CCD3",
12
+ "dim": "#8B909A",
13
+ "accent": "#8AB4FF",
14
+ "accentSoft": "rgba(138, 180, 255, 0.24)",
15
+ "accentText": "#8AB4FF",
16
+ "onAccent": "#0E0F12",
17
+ "success": "#4ADE80",
18
+ "danger": "#F87171",
19
+ "warning": "#FBBF24"
20
+ },
21
+ "fonts": {
22
+ "sans": "\"Pretendard Variable\", Pretendard, \"Noto Sans KR\", \"Apple SD Gothic Neo\", sans-serif",
23
+ "display": "\"Pretendard Variable\", Pretendard, \"Noto Sans KR\", \"Apple SD Gothic Neo\", sans-serif",
24
+ "mono": "\"Geist Mono\", ui-monospace, \"Pretendard Variable\", \"Noto Sans KR\", \"Apple SD Gothic Neo\", monospace",
25
+ "faces": [
26
+ {
27
+ "family": "Pretendard Variable",
28
+ "src": "https://cdn.jsdelivr.net/npm/pretendard@1.3.9/dist/web/variable/woff2/PretendardVariable.woff2",
29
+ "weight": "45 920"
30
+ },
31
+ {
32
+ "family": "Geist Mono",
33
+ "src": "https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/fonts/geist-mono/GeistMono-Variable.woff2",
34
+ "weight": "100 900"
35
+ }
36
+ ]
37
+ },
38
+ "type": {
39
+ "hero": 124,
40
+ "title": 72,
41
+ "body": 38,
42
+ "caption": 30,
43
+ "weightDisplay": 720,
44
+ "weightBody": 450,
45
+ "tracking": "-0.025em"
46
+ },
47
+ "radius": {
48
+ "sm": 10,
49
+ "md": 16,
50
+ "lg": 24,
51
+ "pill": 999
52
+ },
53
+ "shadow": {
54
+ "soft": "0 12px 32px rgba(0, 0, 0, 0.45)",
55
+ "lift": "0 0 0 1px rgba(255, 255, 255, 0.06), 0 16px 40px rgba(0, 0, 0, 0.55)"
56
+ },
57
+ "motion": {
58
+ "beatPulse": "drops-only",
59
+ "shake": {
60
+ "on": [
61
+ "drop"
62
+ ],
63
+ "maxPerPhrase": 1,
64
+ "maxPx": 10
65
+ },
66
+ "cameraMove": "once-per-phrase",
67
+ "repeatSameMove": false,
68
+ "maxFreezeSec": 0.5,
69
+ "enter": "rise",
70
+ "ease": "snappy",
71
+ "durations": {
72
+ "fast": 0.15,
73
+ "base": 0.3,
74
+ "slow": 0.6
75
+ }
76
+ },
77
+ "notes": [
78
+ "어디에나 무난한 어두운 판. 무채색 면 세 층 + 차가운 파랑 강조 하나.",
79
+ "글은 ink·inkSoft 로, dim 은 보조 한 줄에만(5.9:1).",
80
+ "브랜드가 정해지지 않은 설명·시연 영상의 기본값."
81
+ ]
82
+ }
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "og",
3
+ "label": "OpenGateway — 브랜드 파랑 한 색",
4
+ "scheme": "light",
5
+ "colors": {
6
+ "bg": "#FFFFFF",
7
+ "surface": "#F8FAFC",
8
+ "raised": "#F1F5F9",
9
+ "line": "#E2E8F0",
10
+ "ink": "#020817",
11
+ "inkSoft": "#1E293B",
12
+ "dim": "#64748B",
13
+ "accent": "#006FD6",
14
+ "accentSoft": "rgba(0, 111, 214, 0.45)",
15
+ "accentText": "#01509D",
16
+ "onAccent": "#FFFFFF",
17
+ "tint": "rgba(0, 111, 214, 0.08)",
18
+ "success": "#22C55E",
19
+ "danger": "#EF4444",
20
+ "warning": "#F59E0B",
21
+ "darkBg": "#090A0C",
22
+ "darkSurface": "#13151B",
23
+ "darkLine": "#39404F",
24
+ "darkInk": "#C6CAD6",
25
+ "darkAccent": "#0085FF"
26
+ },
27
+ "fonts": {
28
+ "sans": "\"Pretendard Variable\", Pretendard, \"Noto Sans KR\", \"Apple SD Gothic Neo\", sans-serif",
29
+ "display": "\"Geist\", \"Pretendard Variable\", \"Noto Sans KR\", \"Apple SD Gothic Neo\", sans-serif",
30
+ "mono": "\"Geist Mono\", ui-monospace, \"Pretendard Variable\", \"Noto Sans KR\", \"Apple SD Gothic Neo\", monospace",
31
+ "faces": [
32
+ {
33
+ "family": "Pretendard Variable",
34
+ "src": "https://cdn.jsdelivr.net/npm/pretendard@1.3.9/dist/web/variable/woff2/PretendardVariable.woff2",
35
+ "weight": "45 920"
36
+ },
37
+ {
38
+ "family": "Geist",
39
+ "src": "https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/fonts/geist-sans/Geist-Variable.woff2",
40
+ "weight": "100 900"
41
+ },
42
+ {
43
+ "family": "Geist Mono",
44
+ "src": "https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/fonts/geist-mono/GeistMono-Variable.woff2",
45
+ "weight": "100 900"
46
+ }
47
+ ]
48
+ },
49
+ "type": {
50
+ "hero": 132,
51
+ "title": 72,
52
+ "body": 38,
53
+ "caption": 30,
54
+ "weightDisplay": 800,
55
+ "weightBody": 500,
56
+ "tracking": "-0.055em"
57
+ },
58
+ "radius": {
59
+ "sm": 0,
60
+ "md": 12,
61
+ "lg": 12,
62
+ "pill": 999
63
+ },
64
+ "shadow": {
65
+ "soft": "none",
66
+ "lift": "none"
67
+ },
68
+ "motion": {
69
+ "beatPulse": "drops-only",
70
+ "shake": {
71
+ "on": [
72
+ "drop"
73
+ ],
74
+ "maxPerPhrase": 1,
75
+ "maxPx": 8
76
+ },
77
+ "cameraMove": "once-per-phrase",
78
+ "repeatSameMove": false,
79
+ "maxFreezeSec": 0.5,
80
+ "enter": "rise",
81
+ "ease": "snappy",
82
+ "durations": {
83
+ "fast": 0.15,
84
+ "base": 0.22,
85
+ "slow": 0.3
86
+ }
87
+ },
88
+ "notes": [
89
+ "출처: opengateway/cp-fe-og-styleguide/design.md + globals.css(:root 라이트), 쇼츠 mla-swa-remotion/src/mlahost/look.ts.",
90
+ "강조는 브랜드 파랑 하나, 빨강(danger)은 오류에만. 파랑 글자는 accentText(--on-primary).",
91
+ "면은 테두리 없이 색으로 가른다(bg 100% → surface 98% → raised 96%). 면 위에 border 를 겹쳐 그리지 않는다. 그림자 없음.",
92
+ "모서리는 세 칸뿐: 면 12px(md·lg), 그 밖 0(sm), 누르는 것 알약(pill). 영상엔 누르는 것이 거의 없다.",
93
+ "영문 대제목·큰 숫자는 Geist 800 자간 −0.055em(워드마크 결). Geist 에 한글이 없어 한글은 Pretendard 로 떨어진다 — 한글 제목은 700·자간 −0.045em·한 단계 작게.",
94
+ "코드·모델 ID·시각은 Geist Mono, 숫자는 tabular-nums.",
95
+ "움직임은 150/220/300ms 감속 곡선 + 6px 진입. 차트 선을 매번 다시 그리지 않는다.",
96
+ "다크 판이 필요하면 dark* 색(.dark 토큰)을 쓴다."
97
+ ]
98
+ }
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: kasaframe
3
+ description: 영상을 만들 때 쓴다 — 홍보 영상·쇼츠·릴스·설명 영상·제품 공개·시연·광고·피드·뮤직비디오·모션그래픽·카운트다운·로고 애니메이션. 말로만 해도(「카사텀 홍보 영상 1분, 리소 느낌으로」) kasaframe 이 용도·테마·곡을 알아서 고르고 장면 지시서를 낸다. 영상 만드는 길은 이것 하나다. 한국어 트리거: 영상 만들어, 쇼츠 만들어, 홍보 영상, 뮤비, 모션그래픽, 설명 영상, 시연 영상, 광고 영상. English: make a video, promo video, shorts, music video, motion graphics, explainer.
4
+ version: 0.1.0
5
+ user-invocable: true
6
+ argument-hint: "[만들 영상 한 줄]"
7
+ ---
8
+
9
+ # kasaframe — 말 한 줄로 영상까지
10
+
11
+ 명령은 `kf <명령>` — `npm i -g kasaframe` 로 깔았으면 어느 폴더에서나. `kf` 가 PATH 에 없고 이 스킬 폴더가 레포 안의 링크면(실제 경로의 두 단계 위가 레포 뿌리) 그 뿌리에서 `npx kf <명령>`. 둘 다 아니면 사람에게 `npm i -g kasaframe && kf setup` 을 알린다. 처음이면 `kf doctor` 로 ffmpeg·크롬·librosa·OG 키를 본다.
12
+ 규칙·장면 쓰는 법·고치는 법의 정본은 **`docs/AGENTS.md`**(이 스킬 폴더 `kf skill path` 의 두 단계 위) — 장면을 쓰기 전에 0절과 3절을 읽는다.
13
+
14
+ ## 순서
15
+
16
+ 1. **요청을 그대로 넘긴다** — 묻지 말고 시작한다.
17
+ ```
18
+ kf make "<사람이 한 말 그대로>"
19
+ ```
20
+ 결과 폴더는 지금 폴더 아래 생긴다(`--out` 으로 바꾼다). 용도(홍보·쇼츠·설명·공개·시연·광고·피드·뮤비)·테마(「~ 느낌·풍」으로 짚으면 그 테마, 아니면 내용에 맞는 승인 테마)·곡(`--music`, 없으면 곡 모음 `KF_MUSIC_DIR` 에서)을 알아서 고른다. **종료 코드 3** 이면 확신이 낮다는 뜻 — 찍힌 보기 중 맞는 것을 `--purpose` 로 다시(그래도 모르겠으면 그때 사람에게 묻는다).
21
+ 2. 찍힌 `<폴더>/MAKE.md`(장면 지시서 — 용도별 규칙·장면별 시각·할 일·박·테마 효과·만들 그림)를 읽고 `scenes/*.ts` 를 채운다. 무대 `lib/stage.ts` 를 겉에 씌우면 「0.1초도 쉬지 않기·같은 구도 오래 두지 않기(용도별 기준)·화면 채우기」가 처음부터 선다.
22
+ 3. `kf look <폴더>` 가 찍는 장면 모음을 Read 로 직접 보고 장면별 확인표(`out/look.md`)대로 고친다. **그림이 안 보이면**(OG 로 붙인 모델 등)만 `kf look <폴더> --vision-model google/gemini-3.5-flash` — 그림 보는 모델이 장면마다 글로 알려 준다.
23
+ 4. `kf make --finish <폴더>` — 렌더·검사·표지·자막. 검사 오류가 0 이 될 때까지 장면을 고쳐 다시(렌더는 고칠 것을 모아 한 번에 — 한 기계에서 하나씩 돈다).
24
+ 5. 결과 `out/video.mp4`·`out/cover.png` 를 사람이 보는 공유 폴더에 넣어 보여 준다.
25
+
26
+ 작업실(`kf app`, :5150)이 떠 있으면 `kf make` 가 거기에 등록하고 주소를 찍는다 — 사람은 페이지에서 실시간 미리보기·지난 작업을 본다.
27
+
28
+ ## 하지 않는 것
29
+
30
+ - 돈이 드는 그림·영상 생성을 묻지 않고 부르지 않는다 — 지시서의 「만들 그림」은 받은 자산·실제 화면·도형으로 안 될 때만 구체적인 프롬프트로 `kf gen img`. 영상 생성은 사람이 요청에 적었을 때만.
31
+ - 슬로건·감성 카피를 화면에 쓰지 않는다(홍보 틀). 이모지 금지.
32
+ - 렌더·검사를 잠금 없이 여러 개 동시에 돌리지 않는다.
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>{{title}}</title>
6
+ </head>
7
+ <body>
8
+ <div id="stage"></div>
9
+ <script type="module" src="./main.ts"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,22 @@
1
+ import { createElement, type ComponentType } from "react";
2
+ import { flushSync } from "react-dom";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+
5
+ // 이미 있는 React 컴포넌트(geono-hub bits 같은)를 고치지 않고 장면에 얹는다.
6
+ // build 에서 바로 붙이면 장면이 숨어 있는 동안(앞 장면 차례) 등장 애니메이션이 다 끝나 버린다 — 장면이 처음 그려질 때 붙인다.
7
+ // 자기 시계(rAF·motion 스프링·타이머)로 도는 컴포넌트면 kf.json 에 "mode": "step"(0 초부터 차례로 돈다).
8
+ // 컴포넌트가 따로 쓰는 npm 패키지는 프로젝트 폴더에 package.json 을 두고 `npm i <패키지> --legacy-peer-deps` —
9
+ // react·react-dom 은 레포 것 한 벌로 묶이니 프로젝트에 또 깔지 않는다(두 벌이면 훅이 깨진다).
10
+ //
11
+ // const crowd = bit(host, CrowdCanvas);
12
+ // return (t) => { crowd({ count: 12 }, "once"); ... } // key 가 같으면 다시 그리지 않는다
13
+ export function bit<P extends object>(host: HTMLElement, Comp: ComponentType<P>): (props: P, key?: string) => void {
14
+ let root: Root | null = null;
15
+ let last: string | undefined;
16
+ return (props, key = JSON.stringify(props)) => {
17
+ root ??= createRoot(host);
18
+ if (key === last) return;
19
+ last = key;
20
+ flushSync(() => root!.render(createElement(Comp, props)));
21
+ };
22
+ }
@@ -0,0 +1,272 @@
1
+ import { applyStyle, ease, emit, lerp, progress, rand, type SceneCtx, type Style } from "@kasaframe/core";
2
+
3
+ // 장면 뼈대가 같이 쓰는 무대. 카메라는 가만히 있고 주인공은 안전 영역 가운데에 선다 — 이유 없이 화면 전체를 흔들면
4
+ // 보는 사람이 주인공을 놓친다(카메라 드리프트·박마다 자세를 겹친 판이 쇼츠마다 기울고 가장자리로 밀렸다).
5
+ // 「단 0.1초도 쉬지 않는다」는 화면 안 움직임이 맡는다 — 바탕 층의 빛 방울 흐름과, 박마다 모양을 바꾸는 바탕 판.
6
+ // 카메라는 장면이 이유를 댈 때만 움직인다: stage.camera.pushIn·pullOut·follow·impact. 움직이는 동안만 __kf.events 에 남아
7
+ // kf check 의 camera.wander 가 「이유 없는 이동」과 가른다. 장면을 채울 때 그대로 써도, 지워도 된다.
8
+
9
+ export interface Stage {
10
+ box: HTMLDivElement; // 바탕 층(장면 전체) — 늘 흐르는 것은 여기에만
11
+ cam: HTMLDivElement; // 카메라 층 — 주 요소는 여기에. 가만히 있다가 camera 가 시킬 때만 움직인다
12
+ camera: Camera;
13
+ safeW: number;
14
+ safeH: number;
15
+ vertical: boolean;
16
+ /** 주인공 표시 — kf check 의 layout.edge 가 이 상자가 안전 영역 가장자리에 붙는지 본다(bigWord 는 스스로 단다) */
17
+ hero(el: HTMLElement): HTMLElement;
18
+ /** 바탕 흐름 + 카메라. 카메라가 요소를 겨누면(to·follow) 내용을 칠한 뒤 맨 끝에 부른다 — 그 프레임의 자리를 잰다 */
19
+ tick(t: number): void;
20
+ }
21
+
22
+ /** 카메라 층 안 좌표(px, 변환 전) — w·h 를 주면 그 상자가 화면에 들어오게 배율을 줄인다 */
23
+ export interface Spot {
24
+ x: number;
25
+ y: number;
26
+ w?: number;
27
+ h?: number;
28
+ }
29
+ export type Target = HTMLElement | Spot | ((t: number) => Spot);
30
+
31
+ export interface Camera {
32
+ /** 드러냄에 다가가기 — to 가 없으면 화면 가운데로 */
33
+ pushIn(at: number, o?: { to?: Target; scale?: number; dur?: number }): Camera;
34
+ /** 비교 전체를 보이게 물러나기 — 기본은 처음 구도(배율 1)로 돌아온다. 더 넓게는 scale: 0.9 */
35
+ pullOut(at: number, o?: { to?: Target; scale?: number; dur?: number }): Camera;
36
+ /** from–to 동안 주인공 상자를 가운데에 두고 따라간다. 끝나면 앞 구도로 돌아와 선다 */
37
+ follow(target: Target, from: number, to: number, o?: { scale?: number }): Camera;
38
+ /** 한 번 툭 — 드롭·결과가 서는 순간에. 배율만 잠깐 키웠다 돌아온다 */
39
+ impact(at: number, o?: { amount?: number; dur?: number }): Camera;
40
+ /** 그 시각 카메라 층 Style — 다른 움직임과 combine 해서 쓸 때 */
41
+ pose(t: number): Style;
42
+ apply(t: number): void;
43
+ }
44
+
45
+ interface Pose {
46
+ s: number;
47
+ fx: number;
48
+ fy: number;
49
+ }
50
+
51
+ type Move =
52
+ | { kind: "push" | "pull"; at: number; dur: number; to?: Target; scale?: number }
53
+ | { kind: "follow"; at: number; end: number; to: Target; scale?: number }
54
+ | { kind: "impact"; at: number; dur: number; amount: number };
55
+
56
+ const FOLLOW_IN = 0.5;
57
+ const FOLLOW_OUT = 0.6;
58
+
59
+ /**
60
+ * 층 하나에 거는 카메라. frame 은 층 안 좌표로 본 「화면에 보일 자리」(보통 안전 영역) — 기본은 층 전체.
61
+ * 주인공을 frame 가운데로 데려오고, 배율은 상자가 frame 의 86% 안에 들어오게 누른다. 회전·원근은 쓰지 않는다.
62
+ */
63
+ export function makeCamera(layer: HTMLElement, ctx: SceneCtx, o: { frame?: { x: number; y: number; w: number; h: number } } = {}): Camera {
64
+ const frame = o.frame;
65
+ const cx = () => (frame ? frame.x + frame.w / 2 : layer.offsetWidth / 2);
66
+ const cy = () => (frame ? frame.y + frame.h / 2 : layer.offsetHeight / 2);
67
+ const fw = () => frame?.w ?? layer.offsetWidth;
68
+ const fh = () => frame?.h ?? layer.offsetHeight;
69
+ const moves: Move[] = [];
70
+ const add = (m: Move) => {
71
+ moves.push(m);
72
+ moves.sort((a, b) => a.at - b.at);
73
+ };
74
+
75
+ // 변환을 걷어 낸 층 안 좌표 — 층 상자와 요소 상자를 같은 화면에서 재고 층 배율로 나눈다(조상 배율까지 함께 나뉜다)
76
+ function spotOf(target: Target, t: number): Spot | null {
77
+ if (typeof target === "function") return target(t);
78
+ if (!(target instanceof HTMLElement)) return target;
79
+ const r = target.getBoundingClientRect();
80
+ if (r.width < 1 || r.height < 1) return null;
81
+ const lr = layer.getBoundingClientRect();
82
+ const k = lr.width / (layer.offsetWidth || 1) || 1;
83
+ return { x: (r.left + r.width / 2 - lr.left) / k, y: (r.top + r.height / 2 - lr.top) / k, w: r.width / k, h: r.height / k };
84
+ }
85
+
86
+ function goal(to: Target | undefined, scale: number | undefined, fallback: number, t: number): Pose {
87
+ const s = to ? spotOf(to, t) : null;
88
+ if (!s) return { s: scale ?? fallback, fx: cx(), fy: cy() };
89
+ const fit = Math.min(s.w ? (fw() * 0.86) / s.w : Infinity, s.h ? (fh() * 0.86) / s.h : Infinity);
90
+ return { s: Math.min(scale ?? fallback, fit), fx: s.x, fy: s.y };
91
+ }
92
+
93
+ const mix = (a: Pose, b: Pose, p: number): Pose => ({ s: lerp(a.s, b.s, p), fx: lerp(a.fx, b.fx, p), fy: lerp(a.fy, b.fy, p) });
94
+
95
+ // 앞 움직임이 다 끝났으면 그 끝 구도에서, 겹치면 지금 구도에서 이어 간다 — 늘 t 하나로 정해진다
96
+ function at(t: number): { p: Pose; kick: number } {
97
+ let p: Pose = { s: 1, fx: cx(), fy: cy() };
98
+ let kick = 0;
99
+ for (const m of moves) {
100
+ if (t < m.at) break;
101
+ if (m.kind === "impact") {
102
+ const e = t - m.at;
103
+ if (e < m.dur) {
104
+ kick += m.amount * Math.exp(-e / (m.dur * 0.3)) * (1 - e / m.dur);
105
+ emit({ kind: "camera", source: "stage:impact", scene: ctx.id, value: kick * 1000 });
106
+ }
107
+ continue;
108
+ }
109
+ if (m.kind === "follow") {
110
+ const w = ease.inOutCubic(progress(t, m.at, FOLLOW_IN)) * (1 - ease.inOutCubic(progress(t, m.end, FOLLOW_OUT)));
111
+ if (w > 0) {
112
+ p = mix(p, goal(m.to, m.scale, p.s, t), w);
113
+ emit({ kind: "camera", source: "stage:follow", scene: ctx.id, value: 1 });
114
+ }
115
+ continue;
116
+ }
117
+ const raw = progress(t, m.at, m.dur);
118
+ const target = goal(m.to, m.scale, m.kind === "push" ? 1.15 : 1, t);
119
+ p = mix(p, target, ease.inOutCubic(raw));
120
+ if (raw > 0 && raw < 1) emit({ kind: "camera", source: `stage:${m.kind}`, scene: ctx.id, value: Math.abs(target.s - p.s) * 1000 + Math.hypot(target.fx - p.fx, target.fy - p.fy) });
121
+ }
122
+ return { p, kick };
123
+ }
124
+
125
+ const cam: Camera = {
126
+ pushIn(t, x = {}) {
127
+ add({ kind: "push", at: t, dur: x.dur ?? 1, to: x.to, scale: x.scale });
128
+ return cam;
129
+ },
130
+ pullOut(t, x = {}) {
131
+ add({ kind: "pull", at: t, dur: x.dur ?? 0.9, to: x.to, scale: x.scale });
132
+ return cam;
133
+ },
134
+ follow(target, from, to, x = {}) {
135
+ add({ kind: "follow", at: from, end: to, to: target, scale: x.scale });
136
+ return cam;
137
+ },
138
+ impact(t, x = {}) {
139
+ add({ kind: "impact", at: t, dur: x.dur ?? 0.35, amount: x.amount ?? 0.04 });
140
+ return cam;
141
+ },
142
+ pose(t) {
143
+ if (!moves.length) return {};
144
+ const { p, kick } = at(t);
145
+ const s = p.s * (1 + kick);
146
+ return { x: -s * (p.fx - cx()), y: -s * (p.fy - cy()), scale: s };
147
+ },
148
+ apply(t) {
149
+ layer.style.transformOrigin = `${cx()}px ${cy()}px`;
150
+ applyStyle(layer, cam.pose(t));
151
+ },
152
+ };
153
+ return cam;
154
+ }
155
+
156
+ // 컷 시각 — 잰 박 목록을 그대로 쓴다(BPM 을 곱하지 않는다). 빠른 곡은 박 몇 개를 묶어 0.6초 넘는 간격으로.
157
+ // 박이 끊긴 곳(곡 앞뒤·잘라 쓴 끝)은 1.2초 간격으로 메워 장면 처음부터 끝까지 컷이 이어진다.
158
+ // 화면 안 내용을 바꾸는 시각이다(새 요소·숫자·칸 채우기) — 카메라를 여기마다 옮기지 않는다
159
+ export function cutTimes(ctx: SceneCtx): number[] {
160
+ const b = ctx.beats.beats.filter((x) => x >= 0 && x < ctx.dur);
161
+ let picked: number[] = [];
162
+ if (b.length >= 2) {
163
+ const gap = (b[b.length - 1] - b[0]) / (b.length - 1);
164
+ const every = Math.max(1, Math.round(0.6 / gap));
165
+ picked = b.filter((_, i) => i % every === 0);
166
+ }
167
+ const anchors = [0];
168
+ for (const t of picked) if (t - anchors[anchors.length - 1] >= 0.3) anchors.push(t);
169
+ anchors.push(ctx.dur);
170
+ const out: number[] = [];
171
+ for (let i = 0; i < anchors.length - 1; i++) {
172
+ const n = Math.max(1, Math.ceil((anchors[i + 1] - anchors[i]) / 1.2));
173
+ for (let k = 0; k < n; k++) out.push(anchors[i] + ((anchors[i + 1] - anchors[i]) * k) / n);
174
+ }
175
+ return out;
176
+ }
177
+
178
+ // 바탕 판의 꼴(안전 영역에 대한 비율) — 컷에서 컷까지 다음 꼴로 쉬지 않고 옮겨 가 같은 구도가 1.5초를 넘지 않고 0.1초도 멈추지 않는다.
179
+ // 빛 방울만으로는 작은 그림의 멈춤 판정을 늘 넘지 못한다(방울 자리에 따라 장면마다 들쭉날쭉). 주인공은 그대로 선다
180
+ const PANELS = [
181
+ { x: 0.07, y: 0.15, w: 0.86, h: 0.7, r: 0.12 },
182
+ { x: 0, y: 0.55, w: 1, h: 0.45, r: 0.04 },
183
+ { x: 0.18, y: 0.05, w: 0.64, h: 0.9, r: 0.5 },
184
+ { x: 0, y: 0, w: 0.5, h: 1, r: 0.04 },
185
+ { x: 0.05, y: 0.3, w: 0.9, h: 0.4, r: 0.2 },
186
+ ];
187
+
188
+ export function makeStage(root: HTMLElement, ctx: SceneCtx, o: { panel?: boolean } = {}): Stage {
189
+ const { top, right, bottom, left } = ctx.safe;
190
+ const { width, height } = ctx.meta;
191
+ // root 의 display 는 mountProject 가 장면을 켜고 끌 때 쓴다 — 배치는 안쪽 상자에
192
+ const box = document.createElement("div");
193
+ box.style.cssText = "position:absolute;inset:0;overflow:hidden;background:var(--kf-bg)";
194
+ const safeW = width - left - right;
195
+ const safeH = height - top - bottom;
196
+ // 글 한 줄만 있는 화면도 비어 보이지 않게 — 안전 영역을 크게 채우는 판 하나. 바탕 층이라 카메라를 따라가지 않는다.
197
+ // 빛 방울은 판 위로 흐른다 — 판 밑에 깔리면 판이 덮은 만큼 흐림이 죽어 그 장면이 멈춘 프레임이 된다
198
+ let panel: HTMLDivElement | null = null;
199
+ if (o.panel !== false) {
200
+ panel = document.createElement("div");
201
+ panel.style.cssText = `position:absolute;left:${left}px;top:${top}px;width:${safeW}px;height:${safeH}px;transform-origin:0 0;background:var(--kf-accent-soft);opacity:.55`;
202
+ box.append(panel);
203
+ }
204
+ // 바탕 패럴랙스 — 큰 빛 방울 여럿이 서로 다른 빠르기로 흐른다. 카메라가 서 있으니 0.1초 멈춤 판정을 이 흐름이 넘겨야 한다 —
205
+ // 연한 accent-soft 만으로는 작은 그림에서 안 바뀐 것으로 읽혀 accent 를 섞는다. 잔무늬는 작은 그림에 안 잡혀 「멈춤」으로 읽힌다
206
+ const orbs = Array.from({ length: 9 }, (_, i) => {
207
+ const el = document.createElement("div");
208
+ const r = Math.round(Math.min(width, height) * (0.12 + 0.18 * rand(ctx.id, i)));
209
+ el.style.cssText = `position:absolute;left:0;top:0;width:${r * 2}px;height:${r * 2}px;border-radius:50%;background:radial-gradient(circle, color-mix(in srgb, var(--kf-accent) 30%, var(--kf-accent-soft)) 0%, transparent 70%);opacity:${(0.6 + 0.35 * rand(ctx.id, i + 20)).toFixed(2)}`;
210
+ box.append(el);
211
+ // 빠르기는 70–190px/초 — 거의 서 있는 방울이 섞이면 그 사이가 멈춘 프레임이 된다
212
+ const a = rand(ctx.id, i + 80) * Math.PI * 2;
213
+ const v = 70 + 120 * rand(ctx.id, i + 100);
214
+ return { el, r, x0: rand(ctx.id, i + 40) * width, y0: rand(ctx.id, i + 60) * height, vx: v * Math.cos(a), vy: v * Math.sin(a) * 0.7 };
215
+ });
216
+ const cam = document.createElement("div");
217
+ cam.style.cssText = `position:absolute;left:${left}px;top:${top}px;right:${right}px;bottom:${bottom}px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:28px`;
218
+ box.append(cam);
219
+ root.append(box);
220
+ const camera = makeCamera(cam, ctx, { frame: { x: 0, y: 0, w: safeW, h: safeH } });
221
+ const cuts = cutTimes(ctx);
222
+ return {
223
+ box,
224
+ cam,
225
+ camera,
226
+ safeW,
227
+ safeH,
228
+ vertical: height > width,
229
+ hero(el) {
230
+ el.dataset.kfHero = "";
231
+ return el;
232
+ },
233
+ tick(t: number) {
234
+ for (const o of orbs) {
235
+ const x = ((o.x0 + o.vx * t) % (width + o.r * 2) + width + o.r * 2) % (width + o.r * 2) - o.r * 2;
236
+ const y = ((o.y0 + o.vy * t) % (height + o.r * 2) + height + o.r * 2) % (height + o.r * 2) - o.r * 2;
237
+ o.el.style.transform = `translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
238
+ }
239
+ if (panel) {
240
+ let k = 0;
241
+ while (k < cuts.length && cuts[k] <= t) k++;
242
+ const from = k > 0 ? cuts[k - 1] : 0;
243
+ const p = progress(t, from, Math.max(0.3, (cuts[k] ?? from + 1.2) - from));
244
+ const a = PANELS[Math.max(0, k - 2) % PANELS.length];
245
+ const b = PANELS[Math.max(0, k - 1) % PANELS.length];
246
+ const f = (key: "x" | "y" | "w" | "h" | "r") => lerp(a[key], b[key], p);
247
+ panel.style.transform = `translate(${(f("x") * safeW).toFixed(1)}px, ${(f("y") * safeH).toFixed(1)}px) scale(${f("w").toFixed(4)}, ${f("h").toFixed(4)})`;
248
+ panel.style.borderRadius = `${(f("r") * Math.min(safeW, safeH)) / f("w")}px / ${(f("r") * Math.min(safeW, safeH)) / f("h")}px`;
249
+ }
250
+ camera.apply(t);
251
+ },
252
+ };
253
+ }
254
+
255
+ // 렌더가 마지막으로 찍는 장면 안 시각 — 되돌이(끝 화면이 첫 화면으로 이어지는 쇼츠·피드)는 ctx.dur 이 아니라 여기서 움직임을 끝낸다
256
+ export const lastT = (ctx: SceneCtx): number => ctx.dur - 1 / ctx.meta.fps;
257
+
258
+ // 글자 수로 크기를 정한다(렌더 전엔 잴 수 없다) — 한글·한자는 1em, 라틴은 0.62em 으로 어림해 안전 영역 폭의 몫에 맞춘다
259
+ export function fitPx(text: string, width: number, o: { share?: number; max?: number; min?: number } = {}): number {
260
+ const em = [...text].reduce((a, c) => a + (/[ᄀ-ᇿ㄰-㆏가-힯一-鿿]/.test(c) ? 1 : c === " " ? 0.3 : 0.62), 0) || 1;
261
+ return Math.round(Math.max(o.min ?? 48, Math.min(o.max ?? 420, (width * (o.share ?? 0.7)) / em)));
262
+ }
263
+
264
+ // 큰 낱말 하나 — 화면 채움 규칙에 맞는 크기로, 주인공 표시를 달아 카메라 층 가운데에
265
+ export function bigWord(text: string, stage: Stage, o: { color?: string; weight?: number; share?: number } = {}): HTMLDivElement {
266
+ const el = document.createElement("div");
267
+ el.textContent = text;
268
+ const px = fitPx(text, stage.safeW, { share: o.share ?? (stage.vertical ? 0.72 : 0.62), max: stage.vertical ? 360 : 420 });
269
+ el.style.cssText = `position:relative;font-family:var(--kf-font-display);font-size:${px}px;font-weight:${o.weight ?? 800};line-height:1.02;color:${o.color ?? "var(--kf-ink)"};letter-spacing:var(--kf-tracking);text-align:center;word-break:keep-all;white-space:nowrap`;
270
+ stage.cam.append(stage.hero(el));
271
+ return el;
272
+ }
@@ -0,0 +1,9 @@
1
+ import { mountProject } from "@kasaframe/adapters";
2
+ import kf from "./kf.json";
3
+ import style from "./style.json";
4
+
5
+ // 장면 파일을 늘리거나 빼도 이 파일은 그대로 — 시간표는 kf.json 한 곳. 글꼴·색 변수는 mountProject 가 건다
6
+ const scenes = import.meta.glob("./scenes/*.ts", { eager: true });
7
+ const beats = Object.values(import.meta.glob("./beats.json", { eager: true, import: "default" }))[0] ?? null;
8
+
9
+ mountProject({ kf, scenes, beats, style, stage: document.getElementById("stage")! });
@@ -0,0 +1,18 @@
1
+ import { applyStyle, type SceneCtx } from "@kasaframe/core";
2
+ import { enter, type MoveName } from "@kasaframe/motion";
3
+ import { bigWord, makeStage } from "../lib/stage.ts";
4
+
5
+ const WORD = "{{word}}";
6
+ // 이어지는 두 장면이 같은 등장을 쓰면 검사가 잡는다(repeatSameMove) — kf new 가 장면 순번으로 돌려 넣는다
7
+ const MOVE: MoveName = "{{move}}";
8
+
9
+ export default {
10
+ build(root: HTMLElement, ctx: SceneCtx) {
11
+ const stage = makeStage(root, ctx);
12
+ const word = bigWord(WORD, stage);
13
+ return (t: number) => {
14
+ stage.tick(t);
15
+ applyStyle(word, enter(MOVE, t, { at: 0, scene: ctx.id }));
16
+ };
17
+ },
18
+ };