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,283 @@
1
+ import { emit } from "./frame.ts";
2
+
3
+ export type SectionKind = "intro" | "drop" | "main" | "break" | "outro" | (string & {});
4
+
5
+ export interface BeatsJson {
6
+ source?: string;
7
+ /** 곡에서 잘라 쓴 시작(초). 시각은 모두 여기를 0 으로 센다 — 소리를 낼 때만 from + t */
8
+ from?: number;
9
+ duration: number;
10
+ bpm: number;
11
+ beats: number[];
12
+ downbeats?: number[];
13
+ bars?: number[];
14
+ /** 4 마디 묶음 시작 — style.json 의 once-per-phrase·maxPerPhrase 가 세는 단위 */
15
+ phrases?: number[];
16
+ sections?: { t: number; kind: SectionKind; strength?: number }[];
17
+ drops?: number[];
18
+ energy?: { hz: number; low: number[]; mid: number[]; high: number[] };
19
+ }
20
+
21
+ export interface Section {
22
+ index: number;
23
+ kind: SectionKind;
24
+ t: number;
25
+ end: number;
26
+ /** 0..1 — 드롭은 저역 대비, main 은 바뀐 정도. 분석이 안 줬으면 1 */
27
+ strength: number;
28
+ }
29
+
30
+ export interface Phrase {
31
+ index: number;
32
+ t: number;
33
+ end: number;
34
+ }
35
+
36
+ export type BeatGrid = "beat" | "downbeat" | "bar" | "phrase" | "drop" | "section";
37
+
38
+ export interface PulseOpts {
39
+ /** 무엇마다 — 박·첫박·마디·드롭, 또는 n 박마다 */
40
+ every?: BeatGrid | number;
41
+ /** 지수 감쇠 시간 상수(초). 작을수록 짧게 튄다 */
42
+ decay?: number;
43
+ /** 어디서 — 기본은 드롭 구간만(style.json beatPulse 기본 drops-only) */
44
+ within?: "drops" | "all";
45
+ /** 이벤트 기록용 부품 id·장면 id. emit:false 면 기록하지 않는다(화면에 안 쓰는 계산일 때) */
46
+ source?: string;
47
+ scene?: string;
48
+ emit?: boolean;
49
+ }
50
+
51
+ export interface Beats {
52
+ readonly data: BeatsJson;
53
+ /** 이 도구의 0 초가 곡의 몇 초인지 — 장면 ctx 는 장면 시작 */
54
+ readonly offset: number;
55
+ readonly bpm: number;
56
+ readonly duration: number;
57
+ beats: number[];
58
+ downbeats: number[];
59
+ bars: number[];
60
+ drops: number[];
61
+ phrases: number[];
62
+ sections: Section[];
63
+ /** n 번째 박 시각. 소수면 사이를 잇고, 배열 밖이면 끝 박 간격의 중앙값으로 늘린다 */
64
+ beatAt(n: number): number;
65
+ beatIndex(t: number): number;
66
+ nearestBeat(t: number, grid?: BeatGrid): number;
67
+ /** grid 에서 t 이하 가장 최근 시각. 없으면 -Infinity */
68
+ prev(t: number, grid?: BeatGrid): number;
69
+ next(t: number, grid?: BeatGrid): number;
70
+ drop(i: number): number;
71
+ section(t: number): Section;
72
+ phrase(t: number): Phrase;
73
+ inDrop(t: number): boolean;
74
+ pulse(t: number, opts?: PulseOpts): number;
75
+ /** 대역 에너지 0..1(곡 전체 기준). 곡 분석에 energy 가 없으면 0 */
76
+ energy(t: number, band?: "low" | "mid" | "high"): number;
77
+ }
78
+
79
+ const sorted = (a: number[] | undefined): number[] =>
80
+ (a ?? []).filter((v) => Number.isFinite(v)).slice().sort((x, y) => x - y);
81
+
82
+ function median(xs: number[]): number {
83
+ if (xs.length === 0) return 0;
84
+ const s = xs.slice().sort((a, b) => a - b);
85
+ return s[Math.floor(s.length / 2)];
86
+ }
87
+
88
+ /** 이분 탐색 — t 이하인 마지막 인덱스, 없으면 -1 */
89
+ function floorIndex(xs: number[], t: number): number {
90
+ let lo = 0;
91
+ let hi = xs.length - 1;
92
+ let ans = -1;
93
+ while (lo <= hi) {
94
+ const mid = (lo + hi) >> 1;
95
+ if (xs[mid] <= t + 1e-9) {
96
+ ans = mid;
97
+ lo = mid + 1;
98
+ } else hi = mid - 1;
99
+ }
100
+ return ans;
101
+ }
102
+
103
+ /** beats.json 을 검사하고 빈 칸을 채운다. 틀린 값은 렌더 중이 아니라 여기서 바로 던진다 */
104
+ export function parseBeats(input: unknown): BeatsJson {
105
+ const j = (typeof input === "string" ? JSON.parse(input) : input) as Partial<BeatsJson> | null;
106
+ if (!j || typeof j !== "object") throw new Error("beats.json: 객체가 아니다");
107
+ if (!Array.isArray(j.beats) || j.beats.length < 2) throw new Error("beats.json: beats 가 두 개 이상 있어야 한다");
108
+ const beats = sorted(j.beats);
109
+ const duration = Number.isFinite(j.duration) && j.duration! > 0 ? j.duration! : beats[beats.length - 1];
110
+ const bpm = Number.isFinite(j.bpm) && j.bpm! > 0 ? j.bpm! : 60 / median(beats.slice(1).map((b, i) => b - beats[i]));
111
+ const downbeats = j.downbeats?.length ? sorted(j.downbeats) : beats.filter((_, i) => i % 4 === 0);
112
+ const bars = j.bars?.length ? sorted(j.bars) : downbeats;
113
+ const sections = (j.sections ?? [])
114
+ .filter((s) => s && Number.isFinite(s.t))
115
+ .slice()
116
+ .sort((a, b) => a.t - b.t);
117
+ if (sections.length === 0 || sections[0].t > 0) sections.unshift({ t: 0, kind: "intro" });
118
+ // 분석이 phrases 를 안 줬으면 4 마디씩 센다
119
+ const phrases = j.phrases?.length ? sorted(j.phrases) : bars.filter((_, i) => i % 4 === 0);
120
+ if (phrases.length === 0 || phrases[0] > 0) phrases.unshift(0);
121
+ const out: BeatsJson = { duration, bpm, beats, downbeats, bars, phrases, sections, drops: sorted(j.drops) };
122
+ if (j.source !== undefined) out.source = j.source;
123
+ if (Number.isFinite(j.from)) out.from = j.from;
124
+ if (j.energy) out.energy = j.energy;
125
+ return out;
126
+ }
127
+
128
+ export function createBeats(input: BeatsJson | unknown, offset = 0): Beats {
129
+ const data = parseBeats(input);
130
+ const shift = (xs: number[]) => xs.map((v) => v - offset);
131
+ const beats = shift(data.beats);
132
+ const downbeats = shift(data.downbeats!);
133
+ const bars = shift(data.bars!);
134
+ const drops = shift(data.drops!);
135
+ const phrases = shift(data.phrases!);
136
+ const raw = data.sections!;
137
+ const end = data.duration - offset;
138
+ const sections: Section[] = raw.map((s, index) => ({
139
+ index,
140
+ kind: s.kind,
141
+ t: s.t - offset,
142
+ end: index + 1 < raw.length ? raw[index + 1].t - offset : end,
143
+ strength: Number.isFinite(s.strength) ? s.strength! : 1,
144
+ }));
145
+ const hasDropSections = sections.some((s) => s.kind === "drop");
146
+ const gap = median(data.beats.slice(1).map((b, i) => b - data.beats[i])) || 60 / data.bpm;
147
+ const barLen = median(bars.slice(1).map((b, i) => b - bars[i])) || gap * 4;
148
+
149
+ const gridOf = (g: BeatGrid): number[] =>
150
+ g === "beat" ? beats : g === "downbeat" ? downbeats : g === "bar" ? bars : g === "phrase" ? phrases : g === "drop" ? drops : sections.map((s) => s.t);
151
+
152
+ const beatAt = (n: number): number => {
153
+ if (!Number.isFinite(n)) throw new Error(`beatAt(${n}): 박 번호가 수가 아니다`);
154
+ const last = beats.length - 1;
155
+ if (n < 0) return beats[0] + n * gap;
156
+ if (n > last) return beats[last] + (n - last) * gap;
157
+ const i = Math.floor(n);
158
+ const f = n - i;
159
+ return f === 0 || i === last ? beats[i] : beats[i] + (beats[i + 1] - beats[i]) * f;
160
+ };
161
+
162
+ const prev = (t: number, g: BeatGrid = "beat"): number => {
163
+ const xs = gridOf(g);
164
+ const i = floorIndex(xs, t);
165
+ return i < 0 ? -Infinity : xs[i];
166
+ };
167
+
168
+ const next = (t: number, g: BeatGrid = "beat"): number => {
169
+ const xs = gridOf(g);
170
+ const i = floorIndex(xs, t);
171
+ return i + 1 < xs.length ? xs[i + 1] : Infinity;
172
+ };
173
+
174
+ const nearestBeat = (t: number, g: BeatGrid = "beat"): number => {
175
+ const xs = gridOf(g);
176
+ if (xs.length === 0) return t;
177
+ const a = prev(t, g);
178
+ const b = next(t, g);
179
+ if (!Number.isFinite(a)) return b;
180
+ if (!Number.isFinite(b)) return a;
181
+ return t - a <= b - t ? a : b;
182
+ };
183
+
184
+ const sectionStarts = sections.map((s) => s.t);
185
+ const section = (t: number): Section => sections[Math.max(0, floorIndex(sectionStarts, t))];
186
+
187
+ const phrase = (t: number): Phrase => {
188
+ const i = Math.max(0, floorIndex(phrases, t));
189
+ return { index: i, t: phrases[i], end: i + 1 < phrases.length ? phrases[i + 1] : end };
190
+ };
191
+
192
+ const inDrop = (t: number): boolean => {
193
+ if (hasDropSections) return section(t).kind === "drop";
194
+ // 분석이 구간 종류를 못 붙였으면 드롭에서 8 마디(한 프레이즈)를 드롭 구간으로 본다
195
+ const d = prev(t, "drop");
196
+ return Number.isFinite(d) && t - d < barLen * 8;
197
+ };
198
+
199
+ const pulse = (t: number, opts: PulseOpts = {}): number => {
200
+ const every = opts.every ?? "beat";
201
+ const decay = opts.decay ?? 0.16;
202
+ if ((opts.within ?? "drops") === "drops" && !inDrop(t)) return 0;
203
+ let at: number;
204
+ if (typeof every === "number") {
205
+ const i = floorIndex(beats, t);
206
+ if (i < 0) return 0;
207
+ at = beats[i - (i % Math.max(1, Math.round(every)))];
208
+ } else at = prev(t, every);
209
+ if (!Number.isFinite(at)) return 0;
210
+ const dt = t - at;
211
+ const v = dt < 0 || dt > decay * 6 ? 0 : Math.exp(-dt / Math.max(decay, 1e-3));
212
+ if (opts.emit !== false && v > 0.05) emit({ kind: "pulse", source: opts.source ?? "pulse", scene: opts.scene, value: v });
213
+ return v;
214
+ };
215
+
216
+ const drop = (i: number): number => {
217
+ if (!Number.isInteger(i) || i < 0 || i >= drops.length)
218
+ throw new Error(`drop(${i}): 이 곡의 드롭은 ${drops.length}개(0..${drops.length - 1})`);
219
+ return drops[i];
220
+ };
221
+
222
+ // 분석이 곡 전체 기준으로 0..1 을 맞춰 준다 — 여기서 다시 최댓값으로 나누면 조용한 구간이 부푼다
223
+ const energy = (t: number, band: "low" | "mid" | "high" = "low"): number => {
224
+ const e = data.energy;
225
+ const xs = e?.[band];
226
+ if (!e || !xs || xs.length === 0 || !(e.hz > 0)) return 0;
227
+ const x = (t + offset) * e.hz;
228
+ const i = Math.floor(x);
229
+ const v = i < 0 ? xs[0] : i >= xs.length - 1 ? xs[xs.length - 1] : xs[i] + (xs[i + 1] - xs[i]) * (x - i);
230
+ return Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0;
231
+ };
232
+
233
+ return {
234
+ data,
235
+ offset,
236
+ bpm: data.bpm,
237
+ duration: data.duration,
238
+ beats,
239
+ downbeats,
240
+ bars,
241
+ drops,
242
+ phrases,
243
+ sections,
244
+ beatAt,
245
+ beatIndex: (t) => floorIndex(beats, t),
246
+ nearestBeat,
247
+ prev,
248
+ next,
249
+ drop,
250
+ section,
251
+ phrase,
252
+ inDrop,
253
+ pulse,
254
+ energy,
255
+ };
256
+ }
257
+
258
+ let current: Beats | null = null;
259
+
260
+ export function setBeats(input: BeatsJson | unknown): Beats {
261
+ current = createBeats(input);
262
+ return current;
263
+ }
264
+
265
+ export async function loadBeats(url: string): Promise<Beats> {
266
+ const res = await fetch(url);
267
+ if (!res.ok) throw new Error(`beats.json 을 못 받았다: ${url} (${res.status})`);
268
+ return setBeats(await res.json());
269
+ }
270
+
271
+ export function getBeats(): Beats {
272
+ if (!current) throw new Error("beats.json 이 아직 없다 — setBeats(json) 이나 loadBeats(url) 를 먼저 부른다");
273
+ return current;
274
+ }
275
+
276
+ export const hasBeats = (): boolean => current !== null;
277
+
278
+ /** 곡 전체 시각 기준 도구들. 장면 안에서는 ctx.beats(장면 시작 0 기준)를 쓴다 */
279
+ export const beatAt = (n: number): number => getBeats().beatAt(n);
280
+ export const nearestBeat = (t: number, grid?: BeatGrid): number => getBeats().nearestBeat(t, grid);
281
+ export const pulse = (t: number, opts?: PulseOpts): number => getBeats().pulse(t, opts);
282
+ export const drop = (i: number): number => getBeats().drop(i);
283
+ export const section = (t: number): Section => getBeats().section(t);
@@ -0,0 +1,87 @@
1
+ export type Ease = (p: number) => number;
2
+
3
+ const c1 = 1.70158;
4
+ const c3 = c1 + 1;
5
+ const c4 = (2 * Math.PI) / 3;
6
+
7
+ const out = (f: Ease): Ease => (p) => 1 - f(1 - p);
8
+ const inOut = (f: Ease): Ease => (p) => (p < 0.5 ? f(2 * p) / 2 : 1 - f(2 - 2 * p) / 2);
9
+
10
+ const pow = (n: number): Ease => (p) => p ** n;
11
+ const expoIn: Ease = (p) => (p <= 0 ? 0 : 2 ** (10 * p - 10));
12
+ const backIn: Ease = (p) => c3 * p ** 3 - c1 * p * p;
13
+ const circIn: Ease = (p) => 1 - Math.sqrt(1 - p * p);
14
+ const elasticOut: Ease = (p) => (p <= 0 ? 0 : p >= 1 ? 1 : 2 ** (-10 * p) * Math.sin((p * 10 - 0.75) * c4) + 1);
15
+
16
+ /** CSS cubic-bezier 와 같은 곡선. x 로 s 를 뉴턴법 + 이분법으로 찾는다 */
17
+ export function bezier(x1: number, y1: number, x2: number, y2: number): Ease {
18
+ const at = (a: number, b: number, s: number) => 3 * a * s * (1 - s) ** 2 + 3 * b * s * s * (1 - s) + s ** 3;
19
+ const dAt = (a: number, b: number, s: number) => 3 * a * (1 - s) ** 2 + 6 * (b - a) * s * (1 - s) + 3 * (1 - b) * s * s;
20
+ return (p) => {
21
+ if (p <= 0) return 0;
22
+ if (p >= 1) return 1;
23
+ let s = p;
24
+ for (let i = 0; i < 8; i++) {
25
+ const d = dAt(x1, x2, s);
26
+ if (Math.abs(d) < 1e-6) break;
27
+ const next = s - (at(x1, x2, s) - p) / d;
28
+ if (next < 0 || next > 1) break;
29
+ s = next;
30
+ }
31
+ if (Math.abs(at(x1, x2, s) - p) > 1e-5) {
32
+ let lo = 0;
33
+ let hi = 1;
34
+ for (let i = 0; i < 40; i++) {
35
+ s = (lo + hi) / 2;
36
+ if (at(x1, x2, s) < p) lo = s;
37
+ else hi = s;
38
+ }
39
+ }
40
+ return at(y1, y2, s);
41
+ };
42
+ }
43
+
44
+ export const ease = {
45
+ linear: ((p) => p) as Ease,
46
+ inQuad: pow(2),
47
+ outQuad: out(pow(2)),
48
+ inOutQuad: inOut(pow(2)),
49
+ inCubic: pow(3),
50
+ outCubic: out(pow(3)),
51
+ inOutCubic: inOut(pow(3)),
52
+ inQuart: pow(4),
53
+ outQuart: out(pow(4)),
54
+ inOutQuart: inOut(pow(4)),
55
+ inQuint: pow(5),
56
+ outQuint: out(pow(5)),
57
+ inOutQuint: inOut(pow(5)),
58
+ inExpo: expoIn,
59
+ outExpo: out(expoIn),
60
+ inOutExpo: inOut(expoIn),
61
+ inCirc: circIn,
62
+ outCirc: out(circIn),
63
+ inOutCirc: inOut(circIn),
64
+ inBack: backIn,
65
+ outBack: out(backIn),
66
+ inOutBack: inOut(backIn),
67
+ outElastic: elasticOut,
68
+ /** CSS 기본값들 */
69
+ css: bezier(0.25, 0.1, 0.25, 1),
70
+ cssIn: bezier(0.42, 0, 1, 1),
71
+ cssOut: bezier(0, 0, 0.58, 1),
72
+ cssInOut: bezier(0.42, 0, 0.58, 1),
73
+ /** 카사텀·OG 공통 감속 cubic-bezier(0.22, 1, 0.36, 1) */
74
+ snappy: bezier(0.22, 1, 0.36, 1),
75
+ steps: (n: number): Ease => (p) => (p >= 1 ? 1 : Math.floor(p * n) / n),
76
+ } as const;
77
+
78
+ export type EaseName = Exclude<keyof typeof ease, "steps">;
79
+
80
+ /** 이름이나 함수를 받아 함수로. 모르는 이름은 조용히 넘기지 않고 던진다 — 오타가 linear 로 바뀌어 묻히지 않게 */
81
+ export function toEase(e: EaseName | Ease | undefined, fallback: Ease = ease.outCubic): Ease {
82
+ if (e === undefined) return fallback;
83
+ if (typeof e === "function") return e;
84
+ const f = ease[e];
85
+ if (typeof f !== "function") throw new Error(`모르는 이징: ${String(e)}`);
86
+ return f as Ease;
87
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 지금 그리는 프레임의 전체 시각. 페이지의 seek(t) 가 setFrameTime 을 한 번 부르면
3
+ * React(useSyncExternalStore)·바닐라·three 가 onFrame 으로 같은 값을 받는다 — 시간 출처를 하나로 묶는 자리.
4
+ */
5
+ let now = 0;
6
+ const listeners = new Set<(t: number) => void>();
7
+
8
+ export function setFrameTime(t: number): void {
9
+ if (!Number.isFinite(t)) throw new Error(`setFrameTime: 유한한 초가 아니다 (${t})`);
10
+ if (t === now) return;
11
+ now = t;
12
+ for (const fn of listeners) fn(t);
13
+ }
14
+
15
+ export const frameTime = (): number => now;
16
+
17
+ export interface FrameSample {
18
+ /** 0..count-1 */
19
+ index: number;
20
+ count: number;
21
+ }
22
+
23
+ let sample: FrameSample = { index: 0, count: 1 };
24
+
25
+ /**
26
+ * 모션 블러 하위 프레임 — 렌더가 한 프레임을 count 장으로 나눠 찍어 평균할 때 지금이 몇 번째인가.
27
+ * 3D 전환은 이 번호로 카메라를 조리개 위에서 흔들어, 같은 평균에서 피사계 심도까지 얻는다. 평소엔 { 0, 1 }
28
+ */
29
+ export function setFrameSample(index: number, count: number): void {
30
+ sample = count > 1 ? { index, count } : { index: 0, count: 1 };
31
+ }
32
+
33
+ export const frameSample = (): FrameSample => sample;
34
+
35
+ export function onFrame(fn: (t: number) => void): () => void {
36
+ listeners.add(fn);
37
+ return () => {
38
+ listeners.delete(fn);
39
+ };
40
+ }
41
+
42
+ export type MotionEventKind = "pulse" | "shake" | "camera" | "flash" | "move";
43
+
44
+ /** 검사(kf check)가 style.json 움직임 규칙 위반을 세는 기록. 효과가 0 이 아닌 프레임마다 한 줄 */
45
+ export interface KfMotionEvent {
46
+ /** 전체 시각(초) — 그 프레임 */
47
+ t: number;
48
+ kind: MotionEventKind;
49
+ /** 부품 id */
50
+ source: string;
51
+ /** pulse 0..1, shake 는 px, camera 는 이동량(px 또는 배율 차), flash 0..1, move 1 */
52
+ value: number;
53
+ /** move 일 때 움직임 이름(fade·rise·whip …) — 같은 움직임 되풀이를 센다 */
54
+ name?: string;
55
+ scene?: string;
56
+ }
57
+
58
+ interface KfEventHost {
59
+ __kf?: { events?: KfMotionEvent[] };
60
+ }
61
+
62
+ const seen = new Set<string>();
63
+ let fallback: KfMotionEvent[] = [];
64
+
65
+ function sink(): KfMotionEvent[] {
66
+ const g = globalThis as unknown as KfEventHost;
67
+ if (typeof window === "undefined") return fallback;
68
+ g.__kf ??= {};
69
+ g.__kf.events ??= [];
70
+ return g.__kf.events;
71
+ }
72
+
73
+ /**
74
+ * 같은 프레임에서 같은 부품이 여러 번 그려져도(React 재렌더) 한 줄만 남긴다.
75
+ * seek 사이에 비우지 않는다 — 검사는 탭별 배열을 모아 합친다.
76
+ */
77
+ export function emit(e: Omit<KfMotionEvent, "t"> & { t?: number }): void {
78
+ if (!(Math.abs(e.value) > 1e-3)) return;
79
+ const t = Math.round((e.t ?? now) * 1000) / 1000;
80
+ const key = `${t}|${e.kind}|${e.source}|${e.scene ?? ""}`;
81
+ if (seen.has(key)) return;
82
+ seen.add(key);
83
+ const row: KfMotionEvent = { t, kind: e.kind, source: e.source, value: Math.round(e.value * 1000) / 1000 };
84
+ if (e.name !== undefined) row.name = e.name;
85
+ if (e.scene !== undefined) row.scene = e.scene;
86
+ sink().push(row);
87
+ }
88
+
89
+ export function motionEvents(): KfMotionEvent[] {
90
+ return sink();
91
+ }
92
+
93
+ /** 시험·미리보기 다시 그리기용 */
94
+ export function resetMotionEvents(): void {
95
+ seen.clear();
96
+ fallback = [];
97
+ const g = globalThis as unknown as KfEventHost;
98
+ if (typeof window !== "undefined" && g.__kf) g.__kf.events = [];
99
+ }
@@ -0,0 +1,27 @@
1
+ export { clamp, safeTime, lerp, progress, clip, seq, interpolate, toFrame, toTime } from "./time.ts";
2
+ export type { Clip, Seq, SeqItem, SeqSlot } from "./time.ts";
3
+ export { ease, bezier, toEase } from "./ease.ts";
4
+ export type { Ease, EaseName } from "./ease.ts";
5
+ export { spring, springDuration } from "./spring.ts";
6
+ export type { SpringOpts } from "./spring.ts";
7
+ export { rand, randRange, randInt, randNormal, pick, shuffle, noise1 } from "./rand.ts";
8
+ export {
9
+ parseBeats,
10
+ createBeats,
11
+ setBeats,
12
+ loadBeats,
13
+ getBeats,
14
+ hasBeats,
15
+ beatAt,
16
+ nearestBeat,
17
+ pulse,
18
+ drop,
19
+ section,
20
+ } from "./beats.ts";
21
+ export type { Beats, BeatsJson, BeatGrid, PulseOpts, Phrase, Section, SectionKind } from "./beats.ts";
22
+ export { combine, toCss, applyStyle } from "./style.ts";
23
+ export type { Style, CssStyle } from "./style.ts";
24
+ export { setFrameTime, frameTime, onFrame, emit, motionEvents, resetMotionEvents, setFrameSample, frameSample } from "./frame.ts";
25
+ export type { KfMotionEvent, MotionEventKind, FrameSample } from "./frame.ts";
26
+ export { silentBeats, scaleOf, cssVars, fontCss } from "./project.ts";
27
+ export type { KfMeta, KfStyle, MotionRules, SafeArea, SceneCtx, FontFace } from "./project.ts";
@@ -0,0 +1,146 @@
1
+ import { createBeats, type Beats } from "./beats.ts";
2
+
3
+ export interface KfMeta {
4
+ fps: number;
5
+ width: number;
6
+ height: number;
7
+ /** 초 */
8
+ duration: number;
9
+ }
10
+
11
+ export interface SafeArea {
12
+ top: number;
13
+ bottom: number;
14
+ left: number;
15
+ right: number;
16
+ }
17
+
18
+ export interface MotionRules {
19
+ /** drops-only: 드롭 구간에서만 박 펄스 · downbeats: 첫박만 · every-beat · off */
20
+ beatPulse: "drops-only" | "downbeats" | "every-beat" | "off";
21
+ shake: { on: ("drop" | "impact")[]; maxPerPhrase: number; maxPx: number };
22
+ cameraMove: "once-per-phrase" | "free" | "off";
23
+ /** false 면 이어지는 두 장면이 같은 등장 움직임을 쓰지 않는다 */
24
+ repeatSameMove: boolean;
25
+ /** 이보다 오래 화면이 멈추면 검사가 잡는다(초) */
26
+ maxFreezeSec: number;
27
+ /** 아무 1초에 화면 25% 넘게 번쩍이는 섬광이 이보다 많으면 검사 오류(빛 민감 발작 — ITU-R BT.1702 취지, 기본 3) */
28
+ maxFlashPerSec?: number;
29
+ /** 같은 구도가 이 초를 넘으면 검사 경고·오류(기본 1.5·3). 곡 구조를 따르는 뮤비·설명·시연은 틀이 5·8 로 덮는다 */
30
+ staticSec?: { warn: number; error: number };
31
+ /** 눈에 보이는 큰 움직임을 박마다 요구하는 곳 — every-beat 어디서나 · drop-sections 후렴·드롭 구간만(벌스는 화면 안 움직임으로 충분) */
32
+ beatMove?: "every-beat" | "drop-sections";
33
+ /** 기본 등장 움직임 · 이징 · 길이(초) — 숫자를 안 넣은 부품이 이걸 쓴다 */
34
+ enter?: string;
35
+ ease?: string;
36
+ durations?: { fast: number; base: number; slow: number };
37
+ }
38
+
39
+ export interface FontFace {
40
+ family: string;
41
+ src: string;
42
+ weight?: string;
43
+ style?: string;
44
+ }
45
+
46
+ /** style.json — presets/<이름>.json 을 프로젝트에 복사한 것 */
47
+ export interface KfStyle {
48
+ name: string;
49
+ label: string;
50
+ /** light·dark — 검사가 대비를 볼 때 바탕 쪽을 고른다 */
51
+ scheme: "light" | "dark";
52
+ colors: {
53
+ bg: string;
54
+ surface: string;
55
+ raised: string;
56
+ line: string;
57
+ ink: string;
58
+ inkSoft: string;
59
+ dim: string;
60
+ accent: string;
61
+ accentSoft: string;
62
+ onAccent: string;
63
+ success: string;
64
+ danger: string;
65
+ warning: string;
66
+ [k: string]: string;
67
+ };
68
+ /** font-family 값. 셋 다 한글이 서는 글꼴로 끝난다 */
69
+ fonts: {
70
+ sans: string;
71
+ display: string;
72
+ mono: string;
73
+ faces?: FontFace[];
74
+ /**
75
+ * @font-face 묶음을 주는 CSS 주소. 구글 폰트 CSS 처럼 unicode-range 로 쪼갠 것은 피한다 —
76
+ * 화면에 처음 나온 글자 조각을 그때 받느라 그 프레임 글이 투명하게 찍힌다. 되도록 faces 에 한 파일로
77
+ */
78
+ css?: string[];
79
+ };
80
+ type: {
81
+ /** 1920×1080 기준 px. 화면 크기가 다르면 짧은 변 비율로 곱해 쓴다 */
82
+ hero: number;
83
+ title: number;
84
+ body: number;
85
+ caption: number;
86
+ weightDisplay: number;
87
+ weightBody: number;
88
+ tracking: string;
89
+ };
90
+ radius: { sm: number; md: number; lg: number; pill: number };
91
+ shadow: { soft: string; lift: string };
92
+ motion: MotionRules;
93
+ /** 디자인 출처·지킬 점 — 사람과 모델이 읽는다 */
94
+ notes?: string[];
95
+ }
96
+
97
+ export interface SceneCtx {
98
+ id: string;
99
+ /** 영상 전체에서 장면이 시작하는 시각(초) */
100
+ from: number;
101
+ dur: number;
102
+ meta: KfMeta;
103
+ /** 장면 시작을 0 으로 옮긴 박·드롭 시각. 곡이 없으면 드롭 없는 120 BPM 격자 */
104
+ beats: Beats;
105
+ style: KfStyle;
106
+ safe: SafeArea;
107
+ /** 프로젝트 assets/ 기준 경로 → 페이지에서 쓸 주소 */
108
+ asset(path: string): string;
109
+ }
110
+
111
+ /** 곡 없는 프로젝트용. pulse 는 늘 0(드롭이 없다), drop(i) 는 던진다 */
112
+ export function silentBeats(duration: number, offset = 0): Beats {
113
+ const n = Math.max(2, Math.ceil(duration * 2) + 1);
114
+ return createBeats(
115
+ { duration, bpm: 120, beats: Array.from({ length: n }, (_, i) => i * 0.5), sections: [{ t: 0, kind: "intro" }], drops: [] },
116
+ offset,
117
+ );
118
+ }
119
+
120
+ /** 짧은 변 1080 을 1 로 본 배율 — 세로 쇼츠도 같은 type 값으로 맞는 크기가 나온다 */
121
+ export const scaleOf = (meta: Pick<KfMeta, "width" | "height">): number => Math.min(meta.width, meta.height) / 1080;
122
+
123
+ export function cssVars(style: KfStyle, meta?: Pick<KfMeta, "width" | "height">): string {
124
+ const k = meta ? scaleOf(meta) : 1;
125
+ const lines: string[] = [];
126
+ for (const [name, v] of Object.entries(style.colors)) lines.push(`--kf-${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}: ${v};`);
127
+ lines.push(`--kf-font-sans: ${style.fonts.sans};`, `--kf-font-display: ${style.fonts.display};`, `--kf-font-mono: ${style.fonts.mono};`);
128
+ for (const t of ["hero", "title", "body", "caption"] as const) lines.push(`--kf-size-${t}: ${Math.round(style.type[t] * k)}px;`);
129
+ lines.push(`--kf-tracking: ${style.type.tracking};`);
130
+ for (const [n, v] of Object.entries(style.radius)) lines.push(`--kf-radius-${n}: ${v}px;`);
131
+ lines.push(`--kf-shadow-soft: ${style.shadow.soft};`, `--kf-shadow-lift: ${style.shadow.lift};`);
132
+ return `:root {\n ${lines.join("\n ")}\n}`;
133
+ }
134
+
135
+ /** @font-face·@import 묶음. 설치된 글꼴을 먼저 쓰고 없으면 받는다 */
136
+ export function fontCss(style: KfStyle): string {
137
+ const out: string[] = (style.fonts.css ?? []).map((u) => `@import url("${u}");`);
138
+ const formats: Record<string, string> = { woff2: "woff2", woff: "woff", ttf: "truetype", otf: "opentype" };
139
+ for (const f of style.fonts.faces ?? []) {
140
+ const ext = /\.(woff2|woff|ttf|otf)(?:[?#].*)?$/i.exec(f.src)?.[1].toLowerCase() ?? "woff2";
141
+ out.push(
142
+ `@font-face { font-family: "${f.family}"; src: local("${f.family}"), url("${f.src}") format("${formats[ext]}"); font-weight: ${f.weight ?? "100 900"}; font-style: ${f.style ?? "normal"}; font-display: block; }`,
143
+ );
144
+ }
145
+ return out.join("\n");
146
+ }