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,226 @@
1
+ // JIZURA 엔진을 고정 커밋으로 받아 UI 를 뺀 한 파일로 묶고, 가사 언어에 쓸 글꼴을 받아 둔다(노드 쪽).
2
+ // node packages/jizura/src/install.ts [--lang ko,ja] kf setup jizura 가 부른다
3
+ // 결과는 패키지 밖 ~/.cache/kasaframe/jizura 에 둔다 — 설치본은 패키지 폴더(node_modules 안)에 못 쓸 수 있다.
4
+ // 렌더는 오프라인이어도 돌아야 해서 글꼴을 CDN 이 아니라 여기서 낸다(vite.ts 가 /@kf-jizura/ 로 낸다).
5
+ import { execFileSync } from "node:child_process";
6
+ import { createHash } from "node:crypto";
7
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { JIZURA_COMMIT, JIZURA_CREDIT, JIZURA_REPO, JIZURA_VERSION } from "./pin.ts";
12
+
13
+ export const jizuraHome = (): string => process.env.KF_JIZURA_HOME ?? join(homedir(), ".cache", "kasaframe", "jizura");
14
+ export const engineDir = (): string => join(jizuraHome(), "engine");
15
+ export const fontDir = (): string => join(jizuraHome(), "fonts");
16
+ const repoDir = (): string => join(jizuraHome(), "repo");
17
+
18
+ export interface FontFile {
19
+ family: string;
20
+ /** 가변 글꼴은 "100 900" */
21
+ weight: string;
22
+ file: string;
23
+ lang: string;
24
+ }
25
+
26
+ export interface EngineInfo {
27
+ commit: string;
28
+ version: string;
29
+ sha256: string;
30
+ bytes: number;
31
+ files: string[];
32
+ }
33
+
34
+ // JIZURA 의 J.LANG_FACES·J.FONTS 가 그 언어에서 그리는 얼굴(google/fonts ofl 폴더). 한국어 매핑에 없는 키(dot·mono)는 일본어 얼굴을 그대로 쓴다
35
+ interface Family { family: string; dir: string; weights?: number[] }
36
+ const COMMON: Family[] = [
37
+ { family: "IBM Plex Mono", dir: "ibmplexmono", weights: [500, 600] },
38
+ { family: "DotGothic16", dir: "dotgothic16" },
39
+ ];
40
+ const FAMILIES: Record<string, Family[]> = {
41
+ ko: [
42
+ { family: "Noto Sans KR", dir: "notosanskr" },
43
+ { family: "Noto Serif KR", dir: "notoserifkr" },
44
+ { family: "Black Han Sans", dir: "blackhansans" },
45
+ { family: "Jua", dir: "jua" },
46
+ { family: "Do Hyeon", dir: "dohyeon" },
47
+ { family: "Gowun Dodum", dir: "gowundodum" },
48
+ { family: "Gowun Batang", dir: "gowunbatang", weights: [700] },
49
+ { family: "Nanum Brush Script", dir: "nanumbrushscript" },
50
+ { family: "IBM Plex Sans KR", dir: "ibmplexsanskr", weights: [500] },
51
+ ],
52
+ ja: [
53
+ { family: "Noto Sans JP", dir: "notosansjp" },
54
+ { family: "Noto Serif JP", dir: "notoserifjp" },
55
+ { family: "Dela Gothic One", dir: "delagothicone" },
56
+ { family: "Zen Kaku Gothic New", dir: "zenkakugothicnew", weights: [900] },
57
+ { family: "Zen Old Mincho", dir: "zenoldmincho", weights: [900] },
58
+ { family: "Kaisei Tokumin", dir: "kaiseitokumin", weights: [800] },
59
+ { family: "M PLUS Rounded 1c", dir: "mplusrounded1c", weights: [800] },
60
+ { family: "Mochiy Pop One", dir: "mochiypopone" },
61
+ { family: "Yuji Syuku", dir: "yujisyuku" },
62
+ { family: "Reggae One", dir: "reggaeone" },
63
+ { family: "Rampart One", dir: "rampartone" },
64
+ { family: "Potta One", dir: "pottaone" },
65
+ { family: "Kiwi Maru", dir: "kiwimaru", weights: [500] },
66
+ { family: "Klee One", dir: "kleeone", weights: [600] },
67
+ { family: "Shippori Mincho B1", dir: "shipporiminchob1", weights: [800] },
68
+ { family: "IBM Plex Sans JP", dir: "ibmplexsansjp", weights: [500] },
69
+ ],
70
+ };
71
+ export const FONT_LANGS = Object.keys(FAMILIES);
72
+
73
+ const WEIGHT: Record<string, number> = { Thin: 100, ExtraLight: 200, Light: 300, Regular: 400, Medium: 500, SemiBold: 600, Bold: 700, ExtraBold: 800, Black: 900 };
74
+
75
+ const git = (args: string[], cwd?: string) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
76
+
77
+ /** 고정 커밋만 얕게 받는다. KF_JIZURA_SRC 에 클론을 주면 그걸 읽기만 한다 */
78
+ export function fetchSource(log = console.log): string {
79
+ const given = process.env.KF_JIZURA_SRC;
80
+ if (given) {
81
+ const head = git(["rev-parse", "HEAD"], given);
82
+ if (head !== JIZURA_COMMIT) log(` 주의: KF_JIZURA_SRC 가 ${head.slice(0, 7)} — 고정 커밋 ${JIZURA_COMMIT.slice(0, 7)} 이 아니다(거부 목록·색 구성 번호가 어긋날 수 있다)`);
83
+ return given;
84
+ }
85
+ const dir = repoDir();
86
+ if (existsSync(join(dir, ".git")) && git(["rev-parse", "HEAD"], dir) === JIZURA_COMMIT) return dir;
87
+ mkdirSync(dir, { recursive: true });
88
+ if (!existsSync(join(dir, ".git"))) {
89
+ git(["init", "-q"], dir);
90
+ git(["remote", "add", "origin", JIZURA_REPO], dir);
91
+ }
92
+ log(` ${JIZURA_REPO} @ ${JIZURA_COMMIT.slice(0, 7)} 받는 중`);
93
+ git(["fetch", "-q", "--depth", "1", "origin", JIZURA_COMMIT], dir);
94
+ git(["checkout", "-q", "--detach", "FETCH_HEAD"], dir);
95
+ return dir;
96
+ }
97
+
98
+ /** src/*.js 를 업스트림 build.py 와 같은 순서로 잇되 편집기 UI(12_ui.js)는 뺀다. 업스트림 파일은 고치지 않는다 */
99
+ export function buildEngine(src: string): EngineInfo {
100
+ const version = readFileSync(join(src, "VERSION"), "utf8").trim();
101
+ const files = readdirSync(join(src, "src")).filter((f) => f.endsWith(".js") && f !== "12_ui.js").sort();
102
+ const body = files.map((f) => readFileSync(join(src, "src", f), "utf8")).join("\n").split("@VERSION@").join(version);
103
+ const js = `/*! JIZURA 字面 v${version} engine — ${JIZURA_CREDIT}. 라이선스는 옆 JIZURA_LICENSE.txt\n * ${files.join(" ")} */\n${body}`;
104
+ const out = engineDir();
105
+ mkdirSync(out, { recursive: true });
106
+ writeFileSync(join(out, "jizura_engine.js"), js);
107
+ copyFileSync(join(src, "LICENSE"), join(out, "JIZURA_LICENSE.txt"));
108
+ const info: EngineInfo = {
109
+ commit: git(["rev-parse", "HEAD"], src),
110
+ version,
111
+ sha256: createHash("sha256").update(js).digest("hex"),
112
+ bytes: Buffer.byteLength(js),
113
+ files,
114
+ };
115
+ writeFileSync(join(out, "engine.json"), JSON.stringify(info, null, 1));
116
+ return info;
117
+ }
118
+
119
+ async function getJson(url: string): Promise<unknown> {
120
+ const headers: Record<string, string> = { "user-agent": "kasaframe-jizura-fonts" };
121
+ if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
122
+ const r = await fetch(url, { headers });
123
+ if (!r.ok) throw new Error(`${url} → ${r.status}${r.status === 403 ? " (GitHub API 한도 — GITHUB_TOKEN 을 주면 늘어난다)" : ""}`);
124
+ return r.json();
125
+ }
126
+
127
+ async function download(url: string, to: string): Promise<void> {
128
+ for (let i = 0; ; i++) {
129
+ try {
130
+ const r = await fetch(url);
131
+ if (!r.ok) throw new Error(`${r.status}`);
132
+ writeFileSync(`${to}.part`, Buffer.from(await r.arrayBuffer()));
133
+ renameSync(`${to}.part`, to);
134
+ return;
135
+ } catch (err) {
136
+ if (i >= 3) throw new Error(`${url} 를 못 받았다: ${err instanceof Error ? err.message : err}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ /** google/fonts 의 원본(서브셋 아닌 전체) 글꼴. 받은 파일은 크기가 맞으면 건너뛴다 */
142
+ export async function fetchFonts(langs: string[], log = console.log): Promise<FontFile[]> {
143
+ const dir = fontDir();
144
+ mkdirSync(dir, { recursive: true });
145
+ const manifestPath = join(dir, "fonts.json");
146
+ const have: FontFile[] = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : [];
147
+ const want = [...COMMON.map((f) => ({ ...f, lang: "common" })), ...langs.flatMap((l) => (FAMILIES[l] ?? []).map((f) => ({ ...f, lang: l })))];
148
+ const out = have.filter((f) => existsSync(join(dir, f.file)) && !want.some((w) => w.family === f.family));
149
+ for (const fam of want) {
150
+ const list = (await getJson(`https://api.github.com/repos/google/fonts/contents/ofl/${fam.dir}`)) as { name: string; size: number; download_url: string }[];
151
+ const ttf = list.filter((x) => x.name.endsWith(".ttf") && !/Italic/.test(x.name));
152
+ const variable = ttf.find((x) => x.name.includes("["));
153
+ const picks = variable
154
+ ? [{ src: variable, weight: "100 900" }]
155
+ : ttf
156
+ .map((x) => ({ src: x, w: WEIGHT[/-(\w+)\.ttf$/.exec(x.name)?.[1] ?? "Regular"] ?? 400 }))
157
+ .filter((x) => !fam.weights || fam.weights.includes(x.w))
158
+ .map((x) => ({ src: x.src, weight: String(x.w) }));
159
+ if (!picks.length) throw new Error(`${fam.family}: google/fonts ofl/${fam.dir} 에 맞는 ttf 가 없다`);
160
+ for (const p of picks) {
161
+ const file = p.src.name.replace(/\[.*\]/, "-VF");
162
+ const to = join(dir, file);
163
+ if (!existsSync(to) || statSync(to).size !== p.src.size) {
164
+ log(` ${fam.family} ${p.weight} — ${(p.src.size / 1048576).toFixed(1)}MB`);
165
+ await download(p.src.download_url, to);
166
+ }
167
+ out.push({ family: fam.family, weight: p.weight, file, lang: fam.lang });
168
+ }
169
+ const ofl = list.find((x) => x.name === "OFL.txt");
170
+ const oflTo = join(dir, `OFL-${fam.dir}.txt`);
171
+ if (ofl && !existsSync(oflTo)) await download(ofl.download_url, oflTo);
172
+ }
173
+ writeFileSync(manifestPath, JSON.stringify(out, null, 1));
174
+ return out;
175
+ }
176
+
177
+ export function engineInfo(): EngineInfo | null {
178
+ const p = join(engineDir(), "engine.json");
179
+ return existsSync(p) ? (JSON.parse(readFileSync(p, "utf8")) as EngineInfo) : null;
180
+ }
181
+
182
+ export function installedFonts(): FontFile[] {
183
+ const p = join(fontDir(), "fonts.json");
184
+ return existsSync(p) ? (JSON.parse(readFileSync(p, "utf8")) as FontFile[]) : [];
185
+ }
186
+
187
+ /** kf doctor 한 줄 — cli/deps.ts 의 Check 꼴 */
188
+ export function jizuraCheck() {
189
+ const info = engineInfo();
190
+ const langs = [...new Set(installedFonts().map((f) => f.lang))].filter((l) => l !== "common");
191
+ const ok = !!info && info.commit === JIZURA_COMMIT && langs.length > 0;
192
+ return {
193
+ name: "JIZURA 가사 층",
194
+ ok,
195
+ required: false,
196
+ detail: info
197
+ ? `v${info.version} @${info.commit.slice(0, 7)}${info.commit === JIZURA_COMMIT ? "" : ` (고정 ${JIZURA_COMMIT.slice(0, 7)} 이 아니다)`} · 글꼴 ${langs.join("·") || "없음"} — ${jizuraHome()}`
198
+ : "없음 — 뮤비 가사 키네틱(jizuraLayer)만 못 쓴다. 없으면 틀 장면은 DOM 낱말로 대신한다",
199
+ fix: ok ? undefined : "kf setup jizura (일본어 가사도 쓰면 node packages/jizura/src/install.ts --lang ko,ja)",
200
+ };
201
+ }
202
+
203
+ export async function install(langs: string[], log = console.log): Promise<void> {
204
+ const src = fetchSource(log);
205
+ const info = buildEngine(src);
206
+ log(` 엔진 v${info.version} @${info.commit.slice(0, 7)} — ${(info.bytes / 1048576).toFixed(1)}MB, 파일 ${info.files.length}개 → ${engineDir()}`);
207
+ const fonts = await fetchFonts(langs, log);
208
+ log(` 글꼴 ${fonts.length}개(${langs.join("·")}) → ${fontDir()}`);
209
+ }
210
+
211
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
212
+ const i = process.argv.indexOf("--lang");
213
+ const langs = (i > 0 ? process.argv[i + 1] : "ko").split(",").filter(Boolean);
214
+ const bad = langs.filter((l) => !FONT_LANGS.includes(l));
215
+ if (bad.length) {
216
+ console.error(`모르는 언어 ${bad.join(" ")} — ${FONT_LANGS.join(" ")}`);
217
+ process.exit(2);
218
+ }
219
+ install(langs).then(
220
+ () => process.exit(0),
221
+ (err) => {
222
+ console.error(`jizura 설치 실패: ${err instanceof Error ? err.message : err}`);
223
+ process.exit(1);
224
+ },
225
+ );
226
+ }
@@ -0,0 +1,282 @@
1
+ // JIZURA 字面(MIT, (c) 2026 hakoniwa) 엔진을 결정적인 가사 층으로 — 장면 위 캔버스 한 장에 시각 t 의 가사를 그린다.
2
+ // 플랜 하나 = JIZURA 프로젝트 하나(곡 전체 가사·박). 같은 설정을 쓰는 장면끼리는 한 플랜을 나눠 쓰고, 장면마다 다른 플랜을 고른다.
3
+ // 엔진·글꼴은 kf setup jizura 가 ~/.cache/kasaframe/jizura 에 두고 서버가 /@kf-jizura/ 로 낸다(vite.ts).
4
+ // 빌려 온 것: 플랜 만들기·색 구성 고르기(schemes)는 vocaloid-style-mv-pipeline(MIT, NikusonP) 의 jizura_adapter.js 를 옮겼다.
5
+ import { onSeek } from "@kasaframe/adapters";
6
+ import type { KfMeta, SceneCtx } from "@kasaframe/core";
7
+ import { partPool, type JizuraRegistry, type PoolOptions } from "./deny.ts";
8
+ import { parseLyrics, toLrc, type LyricsJson } from "./lyrics.ts";
9
+
10
+ const URL_BASE = "/@kf-jizura/";
11
+
12
+ export type DrawMode = "transparent" | "front" | "back" | "normal";
13
+
14
+ export interface PlanSettings extends PoolOptions {
15
+ /** JIZURA 스타일 — ocean·gold·noir·sakura·sunset·mono·paper·sumi … */
16
+ style?: string;
17
+ /** 'light' = 밝은 바탕 위 어두운 글(낮 장면) · 'dark' = 어두운 바탕 위 밝은 글(밤 장면) · [번호…] = 그 스타일의 색 구성 */
18
+ schemes?: "light" | "dark" | number[];
19
+ /** 가운데를 비우고 양옆(세로 화면은 위아래) 띠에만 — 주인공 얼굴·몸짓이 요점인 장면 */
20
+ centerFree?: boolean;
21
+ centerDir?: "tb" | "lr";
22
+ seed?: number;
23
+ fx?: Record<string, unknown>;
24
+ colors?: Record<string, unknown>;
25
+ /** 그 밖의 JIZURA 프로젝트 칸은 그대로 넘긴다 */
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ export interface LayerOptions {
30
+ ctx: SceneCtx;
31
+ /** jizuraPlans 로 등록한 이름, 또는 설정 그대로(같은 설정이면 한 플랜을 나눠 쓴다) */
32
+ plan?: string | PlanSettings;
33
+ /** 기본 transparent — JIZURA 바탕 없이 글·장식만 */
34
+ mode?: DrawMode;
35
+ /** 기본은 프로젝트 뿌리의 lyrics.json */
36
+ lyrics?: LyricsJson;
37
+ }
38
+
39
+ type J = JizuraRegistry & Record<string, any>;
40
+
41
+ interface PlanEntry {
42
+ plan: any;
43
+ R: any;
44
+ }
45
+
46
+ declare global {
47
+ interface Window {
48
+ J?: J;
49
+ }
50
+ }
51
+
52
+ // mulberry32 — 렌더러가 만들 때 쓰는 Math.random(종이·입자 질감)을 플랜 seed 에 묶는다. 가상 시계가 seek 마다 난수를 다시 심어도 질감이 같다
53
+ function withSeed<T>(seed: number, fn: () => T): T {
54
+ let a = seed >>> 0;
55
+ const rnd = () => {
56
+ a = (a + 0x6d2b79f5) >>> 0;
57
+ let t = a;
58
+ t = Math.imul(t ^ (t >>> 15), t | 1);
59
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
60
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
61
+ };
62
+ const real = Math.random;
63
+ Math.random = rnd;
64
+ try {
65
+ return fn();
66
+ } finally {
67
+ Math.random = real;
68
+ }
69
+ }
70
+
71
+ function loadScript(src: string): Promise<void> {
72
+ return new Promise((resolve, reject) => {
73
+ const s = document.createElement("script");
74
+ s.src = src;
75
+ s.onload = () => resolve();
76
+ s.onerror = () => reject(new Error(`${src} 를 못 받았다`));
77
+ document.head.append(s);
78
+ });
79
+ }
80
+
81
+ // 없는 파일을 Vite 가 SPA 대체로 index.html(200)로 돌려준다 — JSON 이 아니면 없는 것으로 본다. 가사 없는 뮤비 프로젝트가 렌더 중에 던지지 않게
82
+ async function getJson<T>(url: string | URL): Promise<T | null> {
83
+ try {
84
+ const r = await fetch(url);
85
+ if (!r.ok || !(r.headers.get("content-type") ?? "").includes("json")) return null;
86
+ return (await r.json()) as T;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ interface FontFile { family: string; weight: string; file: string; lang: string }
93
+
94
+ // 받아 둔 글꼴이 있는 언어는 CDN 에 묻지 않는다 — 렌더가 오프라인이어도, 늦게 온 조각 글꼴로 첫 프레임이 대체 글꼴이 되는 일도 없다
95
+ async function localFonts(J: J): Promise<void> {
96
+ const list = (await getJson<FontFile[]>(`${URL_BASE}fonts/fonts.json`)) ?? [];
97
+ for (const f of list) document.fonts.add(new FontFace(f.family, `url(${URL_BASE}fonts/${encodeURIComponent(f.file)})`, { weight: f.weight, display: "block" }));
98
+ const langs = new Set(list.map((f) => f.lang));
99
+ const upstream = J.ensureFonts;
100
+ J.ensureFonts = async (text: string, keys: string[] | null) => {
101
+ if (!langs.has(J.lang) || keys?.includes("@var")) return upstream(text, keys);
102
+ const uniq = [...new Set([...text])].join("") || "가";
103
+ const list = (keys ?? Object.keys(J.FONTS)).filter((k: string) => J.FONTS[k]);
104
+ const faces: { family: string; weight: number }[] = list.map((k: string) => J.faceOf(k));
105
+ for (const b of J.langBaseFaces(list)) faces.push({ family: `"${b.family}"`, weight: b.weight });
106
+ const specs = [...new Set(faces.map((f) => `${f.weight} 64px ${f.family}`))];
107
+ await Promise.all(specs.map((s) => document.fonts.load(s, uniq).catch(() => null)));
108
+ J.glyphs.clear();
109
+ J.metrics.clear();
110
+ };
111
+ }
112
+
113
+ let engine: Promise<J | null> | null = null;
114
+
115
+ /** 엔진을 한 번만 싣는다. 깔지 않았으면 null — 부르는 쪽은 대체 화면으로 */
116
+ export function loadJizura(): Promise<J | null> {
117
+ engine ??= (async () => {
118
+ if (window.J?.plan) return window.J;
119
+ if (!(await getJson(`${URL_BASE}engine/engine.json`))) {
120
+ console.warn("[kf] JIZURA 엔진이 없다 — kf setup jizura (가사 층 없이 그린다)");
121
+ return null;
122
+ }
123
+ try {
124
+ await loadScript(`${URL_BASE}engine/jizura_engine.js`);
125
+ const J = window.J as J;
126
+ await localFonts(J);
127
+ return J;
128
+ } catch (err) {
129
+ console.warn(`[kf] JIZURA 엔진을 못 실었다 — 가사 층 없이 그린다: ${err instanceof Error ? err.message : err}`);
130
+ return null;
131
+ }
132
+ })();
133
+ return engine;
134
+ }
135
+
136
+ const named = new Map<string, PlanSettings>();
137
+
138
+ /** 장면들이 이름으로 고를 플랜을 등록한다 — 낮·밤·후렴처럼 */
139
+ export function jizuraPlans(plans: Record<string, PlanSettings>): void {
140
+ for (const [k, v] of Object.entries(plans)) named.set(k, v);
141
+ }
142
+
143
+ let lyricsJob: Promise<LyricsJson | null> | null = null;
144
+ function projectLyrics(): Promise<LyricsJson | null> {
145
+ lyricsJob ??= getJson(new URL("lyrics.json", document.baseURI)).then((raw) => {
146
+ if (raw === null) return null;
147
+ try {
148
+ return parseLyrics(raw);
149
+ } catch (err) {
150
+ console.warn(`[kf] lyrics.json 을 못 읽었다 — 가사 층 없이 그린다: ${err instanceof Error ? err.message : err}`);
151
+ return null;
152
+ }
153
+ });
154
+ return lyricsJob;
155
+ }
156
+
157
+ const ASPECTS: [string, number][] = [["16:9", 16 / 9], ["9:16", 9 / 16], ["1:1", 1], ["4:5", 0.8], ["4:3", 4 / 3], ["3:4", 0.75], ["21:9", 21 / 9]];
158
+ const aspectOf = (m: KfMeta) => ASPECTS.reduce((b, a) => (Math.abs(a[1] - m.width / m.height) < Math.abs(b[1] - m.width / m.height) ? a : b))[0];
159
+
160
+ // 플랜을 짓는 동안 J.lang·J.typeset 같은 전역이 바뀐다 — 한 번에 하나씩
161
+ let queue: Promise<unknown> = Promise.resolve();
162
+ const plans = new Map<string, Promise<PlanEntry | null>>();
163
+
164
+ function buildPlan(J: J, s: PlanSettings, lyrics: LyricsJson, ctx: SceneCtx): Promise<PlanEntry> {
165
+ const base = J.defaultProject();
166
+ const { deny, denyOnly, moods, schemes, fx, ...rest } = s;
167
+ const seed = s.seed ?? 2609;
168
+ const project = {
169
+ ...base,
170
+ lyrics: toLrc(lyrics),
171
+ lang: lyrics.lang ?? "auto",
172
+ fps: ctx.meta.fps,
173
+ res: Math.min(ctx.meta.width, ctx.meta.height),
174
+ aspect: aspectOf(ctx.meta),
175
+ style: "ocean",
176
+ seed,
177
+ // 統一感·文字整列 켬, 공포·和風 세트 끔. 화면 전체 섬광은 kasaframe(dropBurst·섬광 검사)이 한 곳에서 쥔다
178
+ unify: true,
179
+ typeset: true,
180
+ extra: true,
181
+ wa: false,
182
+ horror: false,
183
+ ...rest,
184
+ fx: { ...base.fx, flash: false, hud: "off", glitch: 0.3, chroma: 0.45, ...fx },
185
+ timing: { ...base.timing, ...((rest.timing as object) ?? {}) },
186
+ enabled: partPool(J, { deny, denyOnly, moods }),
187
+ };
188
+ const data = ctx.beats.data;
189
+ const audio = { duration: data.duration, beats: data.beats.slice(), energy: null, energyRate: 0, bpm: data.bpm };
190
+ const job = queue.then(async () => {
191
+ const plan = J.plan(project, audio);
192
+ if (schemes) {
193
+ const pick = (test: (l: number) => boolean) => plan.style.schemes.map((x: { bg: string }, i: number) => (test(J.lum(x.bg)) ? i : -1)).filter((i: number) => i >= 0);
194
+ const ok: number[] = Array.isArray(schemes) ? schemes : schemes === "dark" ? pick((l) => l < 0.25) : pick((l) => l > 0.6);
195
+ if (ok.length) for (const c of plan.cuts) c.scheme = ok[(c.scheme | 0) % ok.length];
196
+ }
197
+ const R = withSeed(seed, () => new J.Renderer());
198
+ withSeed(seed + 1, () => R.paper(plan.W, plan.H));
199
+ await J.ensureFonts(project.lyrics + "0123456789:./-", J.fontsOfPlan(plan));
200
+ J.glyphs.maxRes = plan.H >= 1000 ? 768 : 512;
201
+ return { plan, R };
202
+ });
203
+ queue = job.catch(() => null);
204
+ return job;
205
+ }
206
+
207
+ function planFor(o: LayerOptions): Promise<PlanEntry | null> {
208
+ const settings = typeof o.plan === "string" ? named.get(o.plan) : (o.plan ?? {});
209
+ if (!settings) return Promise.reject(new Error(`jizuraPlans 에 없는 플랜 "${o.plan}"`));
210
+ const key = JSON.stringify([settings, o.lyrics ? "own" : "project", o.ctx.meta.width, o.ctx.meta.height]);
211
+ let job = plans.get(key);
212
+ if (!job) {
213
+ job = (async () => {
214
+ const [J, lyrics] = await Promise.all([loadJizura(), o.lyrics ? Promise.resolve(o.lyrics) : projectLyrics()]);
215
+ if (!J || !lyrics?.lines.length) return null;
216
+ return buildPlan(J, settings, lyrics, o.ctx);
217
+ })();
218
+ plans.set(key, job);
219
+ }
220
+ return job;
221
+ }
222
+
223
+ /** 엔진·가사가 다 있어 이 층이 그릴 수 있는지 — 틀 장면이 대체 화면을 고를 때 */
224
+ export async function jizuraReady(): Promise<boolean> {
225
+ const [J, lyrics] = await Promise.all([loadJizura(), projectLyrics()]);
226
+ return !!J && !!lyrics?.lines.length;
227
+ }
228
+
229
+ /** 컷 목록 — 어느 컷이 화면을 덮는지 찾을 때(layout·enter·hold·exit·decor·cam·trans) */
230
+ export async function jizuraCuts(o: LayerOptions): Promise<object[]> {
231
+ const e = await planFor(o);
232
+ return (e?.plan.cuts ?? []).map((c: any) => ({
233
+ i: c.index, start: +c.start.toFixed(3), end: +c.end.toFixed(3), text: c.text,
234
+ layout: c.layout, enter: c.enter, hold: c.hold, exit: c.exit, cam: c.cam, trans: c.trans ?? null,
235
+ decor: (c.decor ?? []).map((d: any) => d.id ?? d.type ?? d), scheme: c.scheme,
236
+ }));
237
+ }
238
+
239
+ const drawOpts = (mode: DrawMode, scale: number) =>
240
+ mode === "front" || mode === "back" ? { scale, transparent: true, layer: mode } : mode === "transparent" ? { scale, transparent: true } : { scale };
241
+
242
+ export type JizuraPaint = ((t: number) => void) & { canvas: HTMLCanvasElement };
243
+
244
+ /** 장면 build 에서 부른다 — 장면을 덮는 캔버스를 짓고 (t) => 칠하기를 돌려준다. t 는 장면 안 시각.
245
+ * 한 장면에서 플랜을 바꿔 끼우려면(드롭부터 큰 플랜) 층을 둘 짓고 안 쓰는 쪽 .canvas 를 숨긴다 */
246
+ export function jizuraLayer(el: HTMLElement, o: LayerOptions): JizuraPaint {
247
+ const { ctx } = o;
248
+ const mode = o.mode ?? "transparent";
249
+ const canvas = document.createElement("canvas");
250
+ canvas.width = ctx.meta.width;
251
+ canvas.height = ctx.meta.height;
252
+ canvas.dataset.kfLyrics = "jizura";
253
+ canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none";
254
+ el.append(canvas);
255
+ const g = canvas.getContext("2d")!;
256
+ let entry: PlanEntry | null = null;
257
+ const ready = planFor(o).then(
258
+ (e) => (entry = e),
259
+ (err) => {
260
+ console.warn(`[kf] 가사 층(${ctx.id}): ${err instanceof Error ? err.message : err}`);
261
+ return null;
262
+ },
263
+ );
264
+ let want: number | null = null;
265
+ let drawn: number | null = null;
266
+ const paint = (T: number) => {
267
+ if (!entry) return;
268
+ entry.R.frame(g, entry.plan, T, drawOpts(mode, canvas.width / entry.plan.W));
269
+ drawn = T;
270
+ };
271
+ // 플랜이 아직이면 그 seek 을 플랜이 설 때까지 붙든다 — 첫 프레임부터 가사가 찍힌다
272
+ onSeek(async () => {
273
+ if (want === null || drawn === want) return;
274
+ await ready;
275
+ paint(want);
276
+ });
277
+ const draw = (t: number) => {
278
+ want = ctx.from + t;
279
+ paint(want);
280
+ };
281
+ return Object.assign(draw, { canvas });
282
+ }
@@ -0,0 +1,81 @@
1
+ // lyrics.json — 프로젝트 뿌리에 둔다. 시각은 beats.json 과 같은 0(곡 시각 - from).
2
+ // 글자는 공식 가사가 정본이고, 정렬(kf lyrics align)은 시각만 붙인다.
3
+
4
+ export interface LyricWord {
5
+ w: string;
6
+ start: number;
7
+ end: number;
8
+ }
9
+
10
+ export interface LyricLine {
11
+ text: string;
12
+ start: number;
13
+ end: number;
14
+ words?: LyricWord[];
15
+ }
16
+
17
+ export interface LyricsJson {
18
+ source?: string;
19
+ /** 곡에서 잘라 쓴 시작(초) — beats.json 의 from 과 같은 뜻 */
20
+ from?: number;
21
+ lang?: string;
22
+ lines: LyricLine[];
23
+ }
24
+
25
+ export interface LrcOptions {
26
+ /** 이 시각보다 길게 빈 자리 뒤에는 [間奏] 줄을 넣는다 — 앞 줄이 다음 줄까지 남지 않게 */
27
+ gap?: number;
28
+ /** 줄이 목소리 끝 뒤에 남는 시간 — 정렬은 끝을 이르게 잡는 편이다 */
29
+ tail?: number;
30
+ title?: string;
31
+ }
32
+
33
+ const stamp = (t: number): string => {
34
+ const s = Math.max(0, t);
35
+ const m = Math.floor(s / 60);
36
+ const rest = s - m * 60;
37
+ return `[${String(m).padStart(2, "0")}:${rest.toFixed(2).padStart(5, "0")}]`;
38
+ };
39
+
40
+ // LRC 문법 글자(/ * 끝의 !)가 가사에 있으면 JIZURA 가 컷 나누기·강조로 읽는다 — 정본 글자를 그대로 두려면 비슷한 꼴로 바꾼다
41
+ const plain = (text: string): string => text.replace(/\//g, "/").replace(/\*/g, "*").replace(/!$/, "!").replace(/\|/g, "|");
42
+
43
+ export function toLrc(lyrics: LyricsJson, o: LrcOptions = {}): string {
44
+ const gap = o.gap ?? 2.5;
45
+ const tail = o.tail ?? 0.5;
46
+ const lines = lyrics.lines.filter((l) => l.text.trim() && Number.isFinite(l.start)).slice().sort((a, b) => a.start - b.start);
47
+ const out: string[] = [];
48
+ if (o.title) out.push(`[ti:${o.title}]`);
49
+ lines.forEach((l, i) => {
50
+ out.push(`${stamp(l.start)}${plain(l.text.trim())}`);
51
+ const next = lines[i + 1];
52
+ const end = Number.isFinite(l.end) ? l.end : l.start;
53
+ if (next && next.start - end > gap) out.push(`${stamp(end + tail)}[間奏]`);
54
+ });
55
+ return out.join("\n") + "\n";
56
+ }
57
+
58
+ export function parseLyrics(input: unknown): LyricsJson {
59
+ const o = (input ?? {}) as Partial<LyricsJson>;
60
+ if (!Array.isArray(o.lines)) throw new Error("lyrics.json 에 lines 배열이 없다");
61
+ for (const [i, l] of o.lines.entries()) {
62
+ if (typeof l?.text !== "string" || !Number.isFinite(l.start) || !Number.isFinite(l.end)) {
63
+ throw new Error(`lyrics.json lines[${i}] 에 text·start·end 가 다 있어야 한다`);
64
+ }
65
+ }
66
+ return o as LyricsJson;
67
+ }
68
+
69
+ /** 낱말 시각이 없는 줄은 글자 수 비율로 나눈다 */
70
+ export function wordsOf(l: LyricLine): LyricWord[] {
71
+ if (l.words?.length) return l.words;
72
+ const parts = l.text.split(/\s+/).filter(Boolean);
73
+ const total = parts.reduce((a, p) => a + p.length, 0) || 1;
74
+ let t = l.start;
75
+ return parts.map((w) => {
76
+ const d = ((l.end - l.start) * w.length) / total;
77
+ const out = { w, start: t, end: t + d };
78
+ t += d;
79
+ return out;
80
+ });
81
+ }
@@ -0,0 +1,5 @@
1
+ // 부품 이름·스타일의 색 구성 번호·거부 목록이 이 판에 맞춰져 있다. 판을 올리면 deny.ts 를 다시 보고 견본 스틸을 다시 찍는다 — 같은 seed 라도 컷이 다시 굴려진다
2
+ export const JIZURA_REPO = "https://github.com/852wa/JIZURA.git";
3
+ export const JIZURA_COMMIT = "fc16bfe43ea4a6c25a21f1caf04d17326de14f00";
4
+ export const JIZURA_VERSION = "0.10.1";
5
+ export const JIZURA_CREDIT = "JIZURA 字面 (c) 2026 hakoniwa (github.com/852wa/JIZURA), MIT";
@@ -0,0 +1,36 @@
1
+ // 미리보기·렌더·검사 서버(cli/serve.ts)가 붙이는 플러그인 — ~/.cache/kasaframe/jizura 의 엔진·글꼴을 /@kf-jizura/ 로 낸다.
2
+ // Vite 의 fs.allow 를 넓히지 않고, 엔진은 모듈 변환 없이 원본 그대로(전역 스크립트) 나가게 한다.
3
+ import { createReadStream, existsSync, statSync } from "node:fs";
4
+ import { extname, join, normalize, sep } from "node:path";
5
+ import type { Plugin } from "vite";
6
+ import { engineDir, fontDir } from "./install.ts";
7
+
8
+ export const JIZURA_URL = "/@kf-jizura/";
9
+
10
+ const TYPES: Record<string, string> = {
11
+ ".js": "text/javascript; charset=utf-8",
12
+ ".json": "application/json; charset=utf-8",
13
+ ".ttf": "font/ttf",
14
+ ".txt": "text/plain; charset=utf-8",
15
+ };
16
+
17
+ export function jizuraServe(): Plugin {
18
+ return {
19
+ name: "kf-jizura",
20
+ configureServer(server) {
21
+ server.middlewares.use(JIZURA_URL, (req, res, next) => {
22
+ const [kind, ...rest] = decodeURIComponent((req.url ?? "").split("?")[0]).replace(/^\/+/, "").split("/");
23
+ const base = kind === "engine" ? engineDir() : kind === "fonts" ? fontDir() : null;
24
+ if (!base) return next();
25
+ const file = normalize(join(base, ...rest));
26
+ if (!file.startsWith(base + sep) || !existsSync(file) || !statSync(file).isFile()) {
27
+ res.statusCode = 404;
28
+ return res.end();
29
+ }
30
+ res.setHeader("content-type", TYPES[extname(file)] ?? "application/octet-stream");
31
+ res.setHeader("cache-control", "no-cache");
32
+ createReadStream(file).pipe(res);
33
+ });
34
+ },
35
+ };
36
+ }