kasaframe 0.1.4 → 0.1.6

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 (374) hide show
  1. package/NOTICE +311 -0
  2. package/README.md +18 -5
  3. package/docs/AGENTS.md +2 -1
  4. package/package.json +1 -1
  5. package/packages/adapters/src/project.ts +2 -0
  6. package/packages/check/package.json +1 -0
  7. package/packages/check/src/check.ts +8 -2
  8. package/packages/check/src/rules/frames.ts +4 -5
  9. package/packages/cli/src/commands/doctor.ts +15 -2
  10. package/packages/cli/src/commands/fonts.ts +118 -0
  11. package/packages/cli/src/commands/make.ts +15 -5
  12. package/packages/cli/src/commands/render.ts +34 -7
  13. package/packages/cli/src/commands/update.ts +66 -0
  14. package/packages/cli/src/fonts.ts +405 -0
  15. package/packages/cli/src/kf.ts +5 -0
  16. package/packages/cli/src/mix.ts +66 -4
  17. package/packages/cli/src/segments.ts +1 -1
  18. package/packages/cli/src/sfx.ts +429 -0
  19. package/packages/cli/src/update.ts +250 -0
  20. package/packages/core/src/frame.ts +7 -4
  21. package/packages/core/src/index.ts +3 -1
  22. package/packages/core/src/project.ts +19 -0
  23. package/packages/core/src/sfx.ts +70 -0
  24. package/packages/director/src/assemble.ts +7 -4
  25. package/packages/director/src/index.ts +1 -1
  26. package/packages/director/src/pattern.ts +2 -0
  27. package/packages/director/src/scene.ts +14 -5
  28. package/packages/director/src/transitions.ts +21 -8
  29. package/packages/motion/src/enter.ts +7 -1
  30. package/packages/motion/src/impact.ts +5 -2
  31. package/packages/motion/src/moves/dress.ts +15 -2
  32. package/packages/motion/src/moves/menu.ts +11 -5
  33. package/packages/motion/src/transition.ts +3 -1
  34. package/packages/motion/src/transition3d/index.ts +3 -1
  35. package/packages/motion/src/transition3d/live.ts +5 -3
  36. package/packages/patterns/src/bars/rank.ts +6 -7
  37. package/packages/patterns/src/before-after/shrink.ts +3 -4
  38. package/packages/patterns/src/before-after/strike.ts +4 -5
  39. package/packages/patterns/src/code/terminal.ts +25 -16
  40. package/packages/patterns/src/end/lockup.ts +13 -10
  41. package/packages/patterns/src/end/slam.ts +7 -5
  42. package/packages/patterns/src/hook-number/counter.ts +15 -9
  43. package/packages/patterns/src/hook-number/slam.ts +6 -8
  44. package/packages/patterns/src/kit.ts +281 -58
  45. package/packages/patterns/src/limit/gauge.ts +16 -11
  46. package/packages/patterns/src/quote/karaoke.ts +17 -12
  47. package/packages/patterns/src/sfx.ts +80 -0
  48. package/packages/patterns/src/term-reveal/chips.ts +24 -10
  49. package/packages/patterns/src/term-reveal/expand.ts +17 -13
  50. package/packages/patterns/src/title/mask.ts +9 -8
  51. package/packages/patterns/src/title/slam.ts +1 -1
  52. package/packages/patterns/src/types.ts +1 -0
  53. package/packages/plan/src/asks.ts +1 -1
  54. package/packages/plan/src/brief.ts +5 -2
  55. package/packages/plan/src/themes.ts +8 -2
  56. package/packages/themes/kit/THEME-MAKE.md +2 -2
  57. package/packages/themes/kit/fonts/display.json +3517 -0
  58. package/packages/themes/kit/fonts/sans.json +5155 -0
  59. package/packages/themes/kit/fonts/serif.json +5229 -0
  60. package/packages/themes/kit/fonts.json +243 -79
  61. package/packages/themes/package.json +3 -1
  62. package/packages/themes/src/fontfile.ts +183 -0
  63. package/packages/themes/src/fonts.ts +296 -0
  64. package/packages/themes/src/index.ts +32 -2
  65. package/packages/themes/src/look.ts +34 -8
  66. package/packages/themes/src/slots.ts +32 -2
  67. package/packages/themes/src/types.ts +33 -0
  68. package/packages/themes/src/vary.ts +35 -34
  69. package/presets/kawaii-pastel.json +2 -2
  70. package/sfx/README.md +82 -0
  71. package/sfx/click/appear.wav +0 -0
  72. package/sfx/click/burst.wav +0 -0
  73. package/sfx/click/drop.wav +0 -0
  74. package/sfx/click/end.wav +0 -0
  75. package/sfx/click/hop.wav +0 -0
  76. package/sfx/click/land.wav +0 -0
  77. package/sfx/click/select.wav +0 -0
  78. package/sfx/click/stick.wav +0 -0
  79. package/sfx/click/tick.wav +0 -0
  80. package/sfx/click/type.wav +0 -0
  81. package/sfx/click/whoosh.wav +0 -0
  82. package/sfx/pop/appear.wav +0 -0
  83. package/sfx/pop/burst.wav +0 -0
  84. package/sfx/pop/drop.wav +0 -0
  85. package/sfx/pop/end.wav +0 -0
  86. package/sfx/pop/hop.wav +0 -0
  87. package/sfx/pop/land.wav +0 -0
  88. package/sfx/pop/select.wav +0 -0
  89. package/sfx/pop/stick.wav +0 -0
  90. package/sfx/pop/tick.wav +0 -0
  91. package/sfx/pop/type.wav +0 -0
  92. package/sfx/pop/whoosh.wav +0 -0
  93. package/sfx/tags.json +545 -0
  94. package/sfx/ui/appear.wav +0 -0
  95. package/sfx/ui/burst.wav +0 -0
  96. package/sfx/ui/drop.wav +0 -0
  97. package/sfx/ui/end.wav +0 -0
  98. package/sfx/ui/hop.wav +0 -0
  99. package/sfx/ui/land.wav +0 -0
  100. package/sfx/ui/select.wav +0 -0
  101. package/sfx/ui/stick.wav +0 -0
  102. package/sfx/ui/tick.wav +0 -0
  103. package/sfx/ui/type.wav +0 -0
  104. package/sfx/ui/whoosh.wav +0 -0
  105. package/skills/kasaframe/SKILL.md +1 -0
  106. package/themes/blueprint/sample/style.json +5 -5
  107. package/themes/blueprint/style.json +5 -5
  108. package/themes/blueprint/theme.json +3 -1
  109. package/themes/blueprint/variants/deep-rose-suit.json +1 -1
  110. package/themes/blueprint/variants/navy-coral-2.json +1 -1
  111. package/themes/blueprint/variants/navy-coral-suit.json +1 -1
  112. package/themes/blueprint/variants/navy-orange-2.json +1 -1
  113. package/themes/blueprint/variants/navy-orange-suit.json +1 -1
  114. package/themes/cartography/sample/style.json +2 -2
  115. package/themes/cartography/style.json +2 -2
  116. package/themes/cartography/theme.json +3 -1
  117. package/themes/cartography/variants/deep-plum.json +1 -1
  118. package/themes/cartography/variants/frost-teal.json +1 -1
  119. package/themes/cartography/variants/navy-green.json +1 -1
  120. package/themes/circuit-board/sample/style.json +2 -2
  121. package/themes/circuit-board/style.json +2 -2
  122. package/themes/circuit-board/theme.json +3 -1
  123. package/themes/circuit-board/variants/dusk-gold-pretendard.json +1 -1
  124. package/themes/circuit-board/variants/navy-gold-grain.json +1 -1
  125. package/themes/circuit-board/variants/wine-gold-pretendard.json +1 -1
  126. package/themes/clay-toy/sample/style.json +2 -2
  127. package/themes/clay-toy/style.json +2 -2
  128. package/themes/clay-toy/theme.json +3 -1
  129. package/themes/clay-toy/variants/blush-coral-grain.json +1 -1
  130. package/themes/clay-toy/variants/chalk-teal.json +1 -1
  131. package/themes/clay-toy/variants/frost-sky.json +1 -1
  132. package/themes/clay-toy/variants/frost-teal.json +1 -1
  133. package/themes/clay-toy/variants/lilac-plum-round.json +1 -1
  134. package/themes/clay-toy/variants/lilac-plum.json +1 -1
  135. package/themes/clay-toy/variants/sage-indigo.json +1 -1
  136. package/themes/conveyor-factory/sample/style.json +3 -3
  137. package/themes/conveyor-factory/style.json +3 -3
  138. package/themes/conveyor-factory/theme.json +3 -1
  139. package/themes/conveyor-factory/variants/cocoa-gold.json +1 -1
  140. package/themes/conveyor-factory/variants/navy-gold.json +1 -1
  141. package/themes/conveyor-factory/variants/slate-amber-2.json +1 -1
  142. package/themes/conveyor-factory/variants/slate-amber-suit.json +1 -1
  143. package/themes/conveyor-factory/variants/slate-gold-bold.json +1 -1
  144. package/themes/data-journal/theme.json +3 -1
  145. package/themes/data-journal/variants/deep-coral.json +1 -1
  146. package/themes/data-journal/variants/navy-lime.json +1 -1
  147. package/themes/data-journal/variants/wine-indigo.json +1 -1
  148. package/themes/debug-hud/sample/style.json +1 -1
  149. package/themes/debug-hud/style.json +1 -1
  150. package/themes/debug-hud/theme.json +2 -1
  151. package/themes/debug-hud/variants/cocoa-teal.json +2 -2
  152. package/themes/debug-hud/variants/deep-teal-scan.json +2 -2
  153. package/themes/debug-hud/variants/dusk-plum.json +2 -2
  154. package/themes/debug-hud/variants/forest-lime.json +2 -2
  155. package/themes/debug-hud/variants/slate-green.json +2 -2
  156. package/themes/debug-hud/variants/wine-gold.json +2 -2
  157. package/themes/depth-flight/sample/style.json +1 -1
  158. package/themes/depth-flight/style.json +1 -1
  159. package/themes/depth-flight/theme.json +3 -1
  160. package/themes/depth-flight/variants/cocoa-cyan.json +1 -1
  161. package/themes/depth-flight/variants/cocoa-violet.json +1 -1
  162. package/themes/depth-flight/variants/deep-sky.json +1 -1
  163. package/themes/depth-flight/variants/forest-blue.json +1 -1
  164. package/themes/depth-flight/variants/navy-sky-grain.json +1 -1
  165. package/themes/depth-flight/variants/navy-sky.json +1 -1
  166. package/themes/depth-flight/variants/night-teal.json +1 -1
  167. package/themes/depth-flight/variants/slate-indigo.json +1 -1
  168. package/themes/depth-flight/variants/slate-violet.json +1 -1
  169. package/themes/film-reel/sample/style.json +2 -2
  170. package/themes/film-reel/style.json +2 -2
  171. package/themes/film-reel/theme.json +3 -1
  172. package/themes/film-reel/variants/chalk-pink.json +1 -1
  173. package/themes/film-reel/variants/lilac-gold.json +1 -1
  174. package/themes/film-reel/variants/lilac-violet.json +1 -1
  175. package/themes/glass-light/sample/style.json +1 -1
  176. package/themes/glass-light/style.json +1 -1
  177. package/themes/glass-light/theme.json +3 -1
  178. package/themes/glass-light/variants/blush-pink.json +1 -1
  179. package/themes/glass-light/variants/blush-plum.json +1 -1
  180. package/themes/glass-light/variants/cream-violet.json +1 -1
  181. package/themes/glass-light/variants/frost-sky-suit.json +1 -1
  182. package/themes/glass-light/variants/frost-sky.json +1 -1
  183. package/themes/glass-light/variants/sage-green.json +1 -1
  184. package/themes/ink-wash/sample/style.json +3 -3
  185. package/themes/ink-wash/style.json +3 -3
  186. package/themes/ink-wash/theme.json +3 -1
  187. package/themes/iso-diorama/sample/style.json +1 -1
  188. package/themes/iso-diorama/style.json +1 -1
  189. package/themes/iso-diorama/theme.json +3 -1
  190. package/themes/iso-diorama/variants/chalk-plum.json +1 -1
  191. package/themes/iso-diorama/variants/cocoa-indigo.json +1 -1
  192. package/themes/iso-diorama/variants/dusk-blue.json +1 -1
  193. package/themes/iso-diorama/variants/dusk-coral.json +1 -1
  194. package/themes/iso-diorama/variants/dusk-jade.json +1 -1
  195. package/themes/iso-diorama/variants/forest-cyan.json +1 -1
  196. package/themes/iso-diorama/variants/frost-green.json +1 -1
  197. package/themes/iso-diorama/variants/frost-teal.json +1 -1
  198. package/themes/iso-diorama/variants/navy-amber.json +1 -1
  199. package/themes/isotype/sample/style.json +2 -2
  200. package/themes/isotype/style.json +2 -2
  201. package/themes/isotype/theme.json +2 -1
  202. package/themes/isotype/variants/blush-rose.json +1 -1
  203. package/themes/isotype/variants/chalk-coral.json +1 -1
  204. package/themes/isotype/variants/chalk-orange.json +1 -1
  205. package/themes/isotype/variants/frost-amber.json +1 -1
  206. package/themes/isotype/variants/frost-coral.json +1 -1
  207. package/themes/isotype/variants/frost-orange.json +1 -1
  208. package/themes/isotype/variants/sand-amber.json +1 -1
  209. package/themes/kawaii-sticker/effects.ts +15 -3
  210. package/themes/kawaii-sticker/sample/style.json +2 -2
  211. package/themes/kawaii-sticker/style.json +2 -2
  212. package/themes/kawaii-sticker/theme.json +4 -1
  213. package/themes/kawaii-sticker/variants/cream-gold.json +1 -1
  214. package/themes/kawaii-sticker/variants/dusk-green.json +1 -1
  215. package/themes/kawaii-sticker/variants/dusk-indigo.json +1 -1
  216. package/themes/kawaii-sticker/variants/dusk-sky.json +1 -1
  217. package/themes/kawaii-sticker/variants/dusk-teal.json +1 -1
  218. package/themes/kawaii-sticker/variants/frost-blue.json +1 -1
  219. package/themes/kawaii-sticker/variants/frost-plum.json +1 -1
  220. package/themes/kawaii-sticker/variants/lilac-violet.json +1 -1
  221. package/themes/kawaii-sticker/variants/navy-gold.json +1 -1
  222. package/themes/kawaii-sticker/variants/navy-plum.json +1 -1
  223. package/themes/lab-notebook/sample/style.json +3 -3
  224. package/themes/lab-notebook/style.json +3 -3
  225. package/themes/lab-notebook/theme.json +2 -1
  226. package/themes/lab-notebook/variants/blush-orange.json +1 -1
  227. package/themes/lab-notebook/variants/chalk-coral.json +1 -1
  228. package/themes/lab-notebook/variants/chalk-orange.json +1 -1
  229. package/themes/lab-notebook/variants/chalk-rose.json +1 -1
  230. package/themes/lab-notebook/variants/lilac-rose.json +1 -1
  231. package/themes/lab-notebook/variants/sage-rose.json +1 -1
  232. package/themes/lab-notebook/variants/sand-orange.json +1 -1
  233. package/themes/lab-notebook/variants/snow-orange.json +1 -1
  234. package/themes/lab-notebook/variants/snow-rose.json +1 -1
  235. package/themes/manga-panel/sample/style.json +3 -3
  236. package/themes/manga-panel/style.json +3 -3
  237. package/themes/manga-panel/theme.json +3 -1
  238. package/themes/manga-panel/variants/chalk-cyan-dots.json +1 -1
  239. package/themes/manga-panel/variants/chalk-rose.json +1 -1
  240. package/themes/manga-panel/variants/snow-cyan.json +1 -1
  241. package/themes/manga-panel/variants/snow-magenta-dots.json +1 -1
  242. package/themes/marble-run/sample/style.json +2 -2
  243. package/themes/marble-run/style.json +2 -2
  244. package/themes/marble-run/theme.json +3 -1
  245. package/themes/marble-run/variants/slate-orange.json +1 -1
  246. package/themes/neon-sign/sample/style.json +2 -2
  247. package/themes/neon-sign/style.json +2 -2
  248. package/themes/neon-sign/theme.json +2 -1
  249. package/themes/neon-sign/variants/dusk-plum.json +1 -1
  250. package/themes/neon-sign/variants/forest-magenta.json +1 -1
  251. package/themes/neon-sign/variants/night-pink.json +1 -1
  252. package/themes/neon-sign/variants/night-violet.json +1 -1
  253. package/themes/neon-sign/variants/slate-violet.json +1 -1
  254. package/themes/newspaper/sample/style.json +2 -2
  255. package/themes/newspaper/style.json +2 -2
  256. package/themes/newspaper/theme.json +2 -1
  257. package/themes/newspaper/variants/blush-amber.json +1 -1
  258. package/themes/newspaper/variants/chalk-magenta.json +1 -1
  259. package/themes/newspaper/variants/frost-sky.json +1 -1
  260. package/themes/newspaper/variants/lilac-green.json +1 -1
  261. package/themes/newspaper/variants/sage-lime.json +1 -1
  262. package/themes/og-measure/THEME.md +41 -6
  263. package/themes/og-measure/effects.ts +236 -115
  264. package/themes/og-measure/sample/assets/brand/og-logo.svg +11 -11
  265. package/themes/og-measure/sample/lib/data.ts +6 -0
  266. package/themes/og-measure/sample/scenes/end.ts +14 -10
  267. package/themes/og-measure/sample/scenes/gather.ts +4 -1
  268. package/themes/og-measure/sample/scenes/models.ts +5 -2
  269. package/themes/og-measure/sample/scenes/one.ts +9 -6
  270. package/themes/og-measure/sample/scenes/price.ts +8 -3
  271. package/themes/og-measure/sample/style.json +30 -35
  272. package/themes/og-measure/slots.ts +54 -30
  273. package/themes/og-measure/still.png +0 -0
  274. package/themes/og-measure/style.json +30 -35
  275. package/themes/og-measure/theme.json +72 -49
  276. package/themes/og-measure/variants/snow-blue.json +90 -0
  277. package/themes/orbit-system/sample/style.json +3 -3
  278. package/themes/orbit-system/style.json +3 -3
  279. package/themes/orbit-system/theme.json +3 -1
  280. package/themes/orbit-system/variants/cocoa-amber.json +1 -1
  281. package/themes/orbit-system/variants/cocoa-cyan.json +1 -1
  282. package/themes/orbit-system/variants/dusk-magenta.json +1 -1
  283. package/themes/orbit-system/variants/dusk-violet.json +1 -1
  284. package/themes/orbit-system/variants/forest-coral.json +1 -1
  285. package/themes/orbit-system/variants/wine-amber.json +1 -1
  286. package/themes/orbit-system/variants/wine-magenta.json +1 -1
  287. package/themes/origami-fold/sample/style.json +1 -1
  288. package/themes/origami-fold/style.json +1 -1
  289. package/themes/origami-fold/theme.json +3 -1
  290. package/themes/origami-fold/variants/deep-amber-sunflower.json +1 -1
  291. package/themes/origami-fold/variants/deep-orange.json +1 -1
  292. package/themes/origami-fold/variants/forest-coral.json +1 -1
  293. package/themes/origami-fold/variants/forest-pink.json +1 -1
  294. package/themes/origami-fold/variants/frost-amber-paper.json +1 -1
  295. package/themes/origami-fold/variants/sage-coral-sunflower.json +1 -1
  296. package/themes/origami-fold/variants/sage-coral.json +1 -1
  297. package/themes/origami-fold/variants/sage-pink-round.json +1 -1
  298. package/themes/origami-fold/variants/sage-pink.json +1 -1
  299. package/themes/paper-cutout/sample/style.json +2 -2
  300. package/themes/paper-cutout/style.json +2 -2
  301. package/themes/paper-cutout/theme.json +3 -1
  302. package/themes/paper-cutout/variants/blush-violet.json +1 -1
  303. package/themes/paper-cutout/variants/frost-lime.json +1 -1
  304. package/themes/paper-cutout/variants/lilac-gold.json +1 -1
  305. package/themes/paper-cutout/variants/sage-cyan.json +1 -1
  306. package/themes/paper-cutout/variants/sage-green.json +1 -1
  307. package/themes/paper-cutout/variants/sage-pink.json +1 -1
  308. package/themes/paper-cutout/variants/sand-blue.json +1 -1
  309. package/themes/pixel-quest/theme.json +2 -1
  310. package/themes/receipt/sample/style.json +2 -2
  311. package/themes/receipt/style.json +2 -2
  312. package/themes/riso-zine/sample/style.json +2 -2
  313. package/themes/riso-zine/style.json +2 -2
  314. package/themes/riso-zine/theme.json +3 -1
  315. package/themes/riso-zine/variants/chalk-lime.json +1 -1
  316. package/themes/riso-zine/variants/frost-green.json +1 -1
  317. package/themes/riso-zine/variants/lilac-plum.json +1 -1
  318. package/themes/riso-zine/variants/lilac-violet.json +1 -1
  319. package/themes/server-hall/sample/style.json +2 -2
  320. package/themes/server-hall/style.json +2 -2
  321. package/themes/server-hall/theme.json +3 -1
  322. package/themes/server-hall/variants/night-indigo.json +1 -1
  323. package/themes/server-hall/variants/wine-plum.json +1 -1
  324. package/themes/subway-map/sample/style.json +2 -2
  325. package/themes/subway-map/style.json +2 -2
  326. package/themes/subway-map/theme.json +3 -1
  327. package/themes/subway-map/variants/deep-green.json +1 -1
  328. package/themes/subway-map/variants/deep-teal.json +1 -1
  329. package/themes/subway-map/variants/frost-jade.json +1 -1
  330. package/themes/subway-map/variants/frost-teal.json +1 -1
  331. package/themes/subway-map/variants/slate-green.json +1 -1
  332. package/themes/subway-map/variants/snow-jade.json +1 -1
  333. package/themes/swiss-poster/sample/style.json +2 -2
  334. package/themes/swiss-poster/style.json +2 -2
  335. package/themes/swiss-poster/theme.json +3 -1
  336. package/themes/swiss-poster/variants/chalk-plum.json +1 -1
  337. package/themes/swiss-poster/variants/cocoa-cyan.json +1 -1
  338. package/themes/swiss-poster/variants/dusk-magenta.json +1 -1
  339. package/themes/swiss-poster/variants/frost-indigo.json +1 -1
  340. package/themes/swiss-poster/variants/slate-orange.json +1 -1
  341. package/themes/water-works/sample/style.json +1 -1
  342. package/themes/water-works/style.json +1 -1
  343. package/themes/water-works/theme.json +3 -1
  344. package/themes/water-works/variants/chalk-cyan.json +1 -1
  345. package/themes/water-works/variants/frost-indigo.json +1 -1
  346. package/themes/water-works/variants/sand-amber.json +1 -1
  347. package/themes/water-works/variants/sand-plum.json +1 -1
  348. package/themes/whiteboard/sample/style.json +2 -2
  349. package/themes/whiteboard/style.json +2 -2
  350. package/themes/whiteboard/theme.json +3 -1
  351. package/themes/whiteboard/variants/chalk-coral.json +1 -1
  352. package/themes/whiteboard/variants/cream-lime.json +1 -1
  353. package/themes/whiteboard/variants/frost-teal.json +1 -1
  354. package/themes/whiteboard/variants/lilac-indigo.json +1 -1
  355. package/themes/whiteboard/variants/sage-plum.json +1 -1
  356. package/themes/whiteboard/variants/snow-cyan.json +1 -1
  357. package/themes/whiteboard/variants/snow-green.json +1 -1
  358. package/themes/whiteboard/variants/snow-magenta.json +1 -1
  359. package/themes/og-measure/sample/assets/brand/og-logo-full.svg +0 -12
  360. package/themes/og-measure/variants/chalk-orange.json +0 -102
  361. package/themes/og-measure/variants/cocoa-amber.json +0 -138
  362. package/themes/og-measure/variants/cocoa-cyan.json +0 -138
  363. package/themes/og-measure/variants/cocoa-gold.json +0 -129
  364. package/themes/og-measure/variants/cream-lime.json +0 -137
  365. package/themes/og-measure/variants/deep-jade.json +0 -100
  366. package/themes/og-measure/variants/dusk-violet.json +0 -133
  367. package/themes/og-measure/variants/forest-cyan.json +0 -123
  368. package/themes/og-measure/variants/frost-amber.json +0 -129
  369. package/themes/og-measure/variants/frost-cyan.json +0 -133
  370. package/themes/og-measure/variants/lilac-plum.json +0 -127
  371. package/themes/og-measure/variants/lilac-violet.json +0 -139
  372. package/themes/og-measure/variants/sage-green.json +0 -137
  373. package/themes/og-measure/variants/wine-jade.json +0 -127
  374. package/themes/og-measure/variants/wine-plum.json +0 -127
@@ -0,0 +1,80 @@
1
+ import { cue } from "@kasaframe/core";
2
+ import type { ThemeSlots } from "./types.ts";
3
+
4
+ // 패턴이 부르는 테마 슬롯에 소리 자리를 단다 — 테마 슬롯도 패턴 기본 슬롯도 같은 자리에서 소리가 나고, 장면·테마 코드는 그대로다
5
+
6
+ /** 글 찍기 소리 간격(초) — 글자마다 내면 1초에 열 번이 넘어 밀도 상한에 다 잘린다 */
7
+ const TYPE_EVERY = 0.09;
8
+ const TYPE_MAX = 10;
9
+
10
+ export function withSfx<S extends ThemeSlots>(s: S): S {
11
+ const out: ThemeSlots = { ...s };
12
+ if (s.panel)
13
+ out.panel = (el, o) => {
14
+ const paint = s.panel!(el, o);
15
+ return (t) => {
16
+ cue("appear", t - o.at, { source: `panel:${o.seed}@${o.at.toFixed(3)}`, scene: o.ctx.id, weight: 0.8, minor: true });
17
+ paint(t);
18
+ };
19
+ };
20
+ if (s.land)
21
+ out.land = (el, o) => {
22
+ const style = s.land!(el, o);
23
+ return (t) => {
24
+ cue("land", t - o.at, { source: `land:${o.seed}@${o.at.toFixed(3)}`, scene: o.ctx.id });
25
+ return style(t);
26
+ };
27
+ };
28
+ if (s.write)
29
+ out.write = (el, o) => {
30
+ const paint = s.write!(el, o);
31
+ const n = Math.min(TYPE_MAX, Math.max(1, Math.floor(o.dur / TYPE_EVERY)));
32
+ return (t) => {
33
+ const k = Math.floor((t - o.at) / TYPE_EVERY);
34
+ for (const j of [k - 1, k]) if (j >= 0 && j < n) cue("type", t - o.at - j * TYPE_EVERY, { source: `write:${o.seed}:${j}@${o.at.toFixed(3)}`, scene: o.ctx.id, minor: true });
35
+ paint(t);
36
+ };
37
+ };
38
+ if (s.drop)
39
+ out.drop = (el, o) => {
40
+ const burst = s.drop!(el, o);
41
+ const at = o.ctx.beats.drop(o.drop);
42
+ return (t) => {
43
+ cue("drop", t - at, { source: `slot-drop:${o.drop}`, scene: o.ctx.id });
44
+ return burst(t);
45
+ };
46
+ };
47
+ return out as S;
48
+ }
49
+
50
+ /** 쾅 앉는 순간 — kit slam 이 부른다 */
51
+ export const slamCue = (t: number, at: number): void => cue("land", t - at, { source: `slam@${at.toFixed(3)}` });
52
+
53
+ /** 짚개가 요점(핵심 값)으로 옮겨 가는 순간 딸깍 — 요점만 짚으니 잔소리가 아니다 */
54
+ export const keyCue = (t: number, at: number): void => cue("select", t - at, { source: `glider@${at.toFixed(3)}`, weight: 0.8 });
55
+
56
+ /** 박마다 훑는 짚개가 주인공(핵심 값)에 닿는 순간 딸깍 — 곁가지에 닿는 것은 잔소리. 첫 정지점은 짚개가 나타나는 자리라 빼고 */
57
+ export function stopCues(t: number, stops: readonly number[], key: (k: number) => boolean = () => true): void {
58
+ for (let j = 1; j < stops.length - 1; j++)
59
+ if (t >= stops[j] && t < stops[j] + 0.2) cue("select", t - stops[j], { source: `glider@${stops[j].toFixed(3)}`, weight: 0.8, minor: !key(j) });
60
+ }
61
+
62
+ /**
63
+ * 숫자 굴림이 째깍거리는 시각 — 진행도 p(x)(감속)가 k/n 을 지나는 x 에. 빨리 구르는 앞은 촘촘, 앉기 전은 띄엄띄엄(슬롯 머신처럼).
64
+ * p 는 단조라 이분법으로 거꾸로 푼다
65
+ */
66
+ export function rollCues(t: number, o: { at: number; dur: number; p: (x: number) => number; n?: number }): void {
67
+ if (t < o.at || t > o.at + o.dur + 0.2) return;
68
+ const n = o.n ?? Math.max(3, Math.min(10, Math.round(o.dur / 0.12)));
69
+ for (let k = 1; k <= n; k++) {
70
+ let lo = 0;
71
+ let hi = 1;
72
+ for (let i = 0; i < 24; i++) {
73
+ const mid = (lo + hi) / 2;
74
+ if (o.p(mid) < k / n) lo = mid;
75
+ else hi = mid;
76
+ }
77
+ const at = o.at + hi * o.dur;
78
+ cue(k === n ? "land" : "tick", t - at, { source: `roll:${k}@${o.at.toFixed(3)}`, weight: k === n ? 0.8 : 0.7, minor: k !== n });
79
+ }
80
+ }
@@ -1,12 +1,13 @@
1
1
  import { applyStyle, clamp, spring } from "@kasaframe/core";
2
2
  import { fitSize, textEm } from "@kasaframe/motion/moves";
3
- import { ACCENT_TEXT, area, inverseOver, moving, node, slotsOf } from "../kit.ts";
3
+ import { ACCENT_TEXT, area, dwells, followSpoken, glider, inverseOver, markOf, moving, node, sayTimes, slotsOf } from "../kit.ts";
4
4
  import type { TermContent } from "./expand.ts";
5
5
  import type { Pattern } from "../types.ts";
6
6
 
7
7
  /**
8
8
  * 용어 해부 — 약자가 큰 판 위에 서고, 풀이 낱말이 박마다 하나씩 판 아래 칩으로 톡 붙는다(첫 글자는 강조색 — 약자 글자와 같은 글자).
9
9
  * 다 붙으면 한국어 이름이 판 아래에 찍히고, 판이 아래부터 강조색으로 차올라 장면 끝까지 그 용어를 짚는다.
10
+ * 테마가 짚는 칸 모양을 정했으면 판을 칠하지 않는다 — 짚개가 약자부터 짚는다(머무는 테마는 목소리가 풀이·한국어 이름을 말할 때만 그리로).
10
11
  */
11
12
  const pattern: Pattern<TermContent> = {
12
13
  id: "term-reveal.chips",
@@ -42,11 +43,15 @@ const pattern: Pattern<TermContent> = {
42
43
  const col = node(host, `display:flex;flex-direction:column;align-items:center;gap:${Math.round(h * 0.035)}px`);
43
44
  const board = node(col, `position:relative;width:${boardW}px;height:${boardH}px`);
44
45
  const panel = slots.panel(board, { ctx, at: 0, seed: `${kit.seed}:board` });
45
- const fill = node(board, "position:absolute;inset:0;background:var(--kf-accent);border-radius:var(--kf-radius-md)");
46
+ const mark = markOf(kit, ctx);
47
+ const fill = mark ? null : node(board, "position:absolute;inset:0;background:var(--kf-accent);border-radius:var(--kf-radius-md)");
46
48
  const face = (lit: boolean) =>
47
49
  node(board, `position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--kf-font-display);font-size:${termPx}px;font-weight:${ctx.style.type?.weightDisplay ?? 800};letter-spacing:0.04em;color:${lit ? "var(--kf-on-accent)" : "var(--kf-ink)"}`, c.term);
48
- face(false);
49
- const lit = face(true);
50
+ const plain = face(false);
51
+ const lit = mark ? null : face(true);
52
+ // 짚개가 잴 약자 글 상자 — 판 전체가 아니라 글자에 칸이 앉게
53
+ const termText = mark ? node(plain, "position:relative", c.term) : null;
54
+ if (termText) plain.firstChild!.remove();
50
55
  kit.stage?.hero(board);
51
56
  const row = words.length ? node(col, `display:flex;flex-direction:${vertical ? "column" : "row"};align-items:center;gap:${Math.round(chipPx * 0.35)}px;min-height:${Math.round(chipPx * 1.5)}px`) : null;
52
57
  const chips = words.map((wd) => {
@@ -57,17 +62,25 @@ const pattern: Pattern<TermContent> = {
57
62
  });
58
63
  const koBox = c.ko ? node(col, `position:relative;font-family:var(--kf-font-sans);font-size:${koPx}px;font-weight:700;white-space:nowrap;line-height:1.3;height:1.3em`) : null;
59
64
  const ko = koBox ? node(koBox, "color:var(--kf-ink)") : null;
60
- const koMark = koBox ? inverseOver(koBox, c.ko!) : null;
65
+ const koMark = koBox && !mark ? inverseOver(koBox, c.ko!) : null;
66
+ // 짚개가 이름 자리를 찍히기 전부터 짚는다 — 빈 상자는 폭이 0 이라 칸이 점으로 줄어든다
67
+ if (koBox && mark) koBox.style.cssText += `;min-width:${Math.round(textEm(c.ko!) * koPx)}px;text-align:center`;
61
68
 
62
- const hits = kit.hits.length ? kit.hits : [0];
63
69
  const boardAt = kit.first ? -0.3 : 0;
64
- const chipAt = words.map((_, i) => hits.find((b) => b >= 0.12 + i * 0.3) ?? 0.2 + i * 0.3);
65
- const koAt = (chipAt[chipAt.length - 1] ?? 0.2) + 0.3;
70
+ // 머무는 짚개 테마는 풀이 칩·한국어 이름을 목소리가 그것을 말할 때 붙인다(못 들으면 판이 열린 뒤부터 장면 끝 무렵까지 박에 붙여 고르게) —
71
+ // 처음에 몰아 띄우면 남은 시간이 빈다. 붙을 때마다 카메라가 그쪽으로 다가간다. 훑는 테마는 0.3초 간격 박에 붙인다(판 차오름이 끝까지 간다)
72
+ const hits = kit.hits.length ? kit.hits : [0];
73
+ const times = dwells(kit, ctx) ? sayTimes(kit, [...words, c.ko], 0.12, Math.max(0.6, ctx.dur - 0.9)) : null;
74
+ const chipAt = words.map((_, i) => times?.[i] ?? hits.find((b) => b >= 0.12 + i * 0.3) ?? 0.2 + i * 0.3);
75
+ const koAt = c.ko && times ? times[words.length] : (chipAt[chipAt.length - 1] ?? 0.2) + 0.3;
76
+ followSpoken(kit, ctx, [...chips.map((el, i) => ({ at: chipAt[i], el })), ...(koBox ? [{ at: koAt, el: koBox }] : [])]);
66
77
  const koDur = c.ko ? clamp([...c.ko].length * 0.05, 0.25, 0.6) : 0;
67
78
  // 판이 열리는 것만으로는 밝은 바탕에서 화면이 바뀐 것으로 안 읽힌다 — 차오름을 곧바로 시작한다
68
79
  const fillFrom = 0;
69
80
  const writeKo = ko ? slots.write(ko, { ctx, text: c.ko!, at: koAt, dur: koDur, seed: `${kit.seed}:ko` }) : null;
70
81
  const lands = chips.map((ch, i) => slots.land(ch, { ctx, at: chipAt[i], seed: `${kit.seed}:c${i}` }));
82
+ // 판이 열리면 짚개가 약자부터 짚고, 머무는 테마는 목소리가 풀이·한국어 이름을 말할 때만 그리로 옮긴다 — 칠한 판이 차오르던 몫
83
+ const point = termText ? glider(host, [{ el: termText, text: c.term }, ...(row ? [{ el: row, text: words.join(" ") }] : []), ...(koBox ? [{ el: koBox, text: c.ko }] : [])], { kit, ctx, from: boardAt + 0.2 }) : null;
71
84
 
72
85
  return (t: number) => {
73
86
  moving("term-reveal.chips", ctx);
@@ -81,8 +94,9 @@ const pattern: Pattern<TermContent> = {
81
94
  // 판은 칩이 붙기 시작할 때부터 장면 끝까지 차오른다 — 용어를 짚는 몫
82
95
  const k = clamp((t - fillFrom) / Math.max(0.3, ctx.dur - fillFrom - 0.05));
83
96
  const clip = `inset(${((1 - k) * 100).toFixed(2)}% 0 0 0)`;
84
- fill.style.clipPath = clip;
85
- lit.style.clipPath = clip;
97
+ if (fill) fill.style.clipPath = clip;
98
+ if (lit) lit.style.clipPath = clip;
99
+ point?.(t);
86
100
  writeKo?.(t);
87
101
  koMark?.((t - koAt - koDur) / Math.max(0.2, ctx.dur - koAt - koDur - 0.05));
88
102
  };
@@ -1,6 +1,6 @@
1
1
  import { applyStyle, clamp, ease, progress } from "@kasaframe/core";
2
2
  import { fitSize, textEm } from "@kasaframe/motion/moves";
3
- import { ACCENT_TEXT, area, beatStops, fitOnce, glider, moving, node, slotsOf } from "../kit.ts";
3
+ import { ACCENT_TEXT, area, dwells, fitOnce, followSpoken, glider, moving, node, sayTimes, slotsOf } from "../kit.ts";
4
4
  import type { Pattern } from "../types.ts";
5
5
 
6
6
  export interface TermContent {
@@ -21,12 +21,12 @@ function spell(term: string, full: string | undefined): string[] | null {
21
21
 
22
22
  /**
23
23
  * 용어 공개 — 약자가 크게 서고, 약자 글자마다 풀이 낱말이 그 글자에서 아래로 펴져 나온다(글자 → 낱말이 같은 첫 글자로 이어진다).
24
- * 풀이가 약자와 안 맞으면 풀이 한 줄이 펴진다. 그다음 한국어 이름이 찍히고, 짚개가 약자와 풀이 낱말을 번갈아 박마다 짚는다.
24
+ * 풀이가 약자와 안 맞으면 풀이 한 줄이 펴진다. 그다음 한국어 이름이 찍히고, 짚개가 약자부터 짚는다(머무는 테마는 목소리가 풀이 낱말·이름을 말할 때만 그리로).
25
25
  */
26
26
  const pattern: Pattern<TermContent> = {
27
27
  id: "term-reveal.expand",
28
28
  type: "term-reveal",
29
- does: "약자가 서고 글자마다 풀이 낱말이 펴진다, 한국어 이름이 찍히고 짚개가 약자와 풀이를 번갈아 짚는다",
29
+ does: "약자가 서고 글자마다 풀이 낱말이 펴진다, 한국어 이름이 찍히고 짚개가 약자부터 짚는다",
30
30
  energy: ["mid"],
31
31
  family: "mask",
32
32
  camera: ["none"],
@@ -83,17 +83,21 @@ const pattern: Pattern<TermContent> = {
83
83
  // 펴기는 약자가 서자마자 낱말마다 이어서 — 사이에 빈 틈이 있으면 화면이 선 것으로 읽힌다
84
84
  const unfoldAt = termAt + 0.06 * Math.max(0, chars.length - 1) + 0.2;
85
85
  const each = parts.length > 1 ? 0.3 : 0;
86
- const unfoldEnd = unfoldAt + each * Math.max(0, parts.length - 1) + 0.35;
87
- const koAt = parts.length ? unfoldEnd : termAt + 0.2;
88
86
  const koDur = c.ko ? clamp([...c.ko].length * 0.05, 0.25, 0.6) : 0;
89
- const meanAt = koAt + koDur;
87
+ // 머무는 짚개 테마는 풀이 낱말·한국어 이름·뜻을 목소리가 그것을 말할 때 펴고 찍는다(못 들으면 약자가 선 뒤부터 장면 끝 무렵까지 박에 붙여 고르게).
88
+ // 펴지고 찍힐 때마다 카메라가 그쪽으로 다가간다. 훑는 테마는 약자가 서자마자 낱말마다 0.3초 간격으로 이어 편다
89
+ const said = dwells(kit, ctx) ? sayTimes(kit, [...parts.map((_, i) => (words ?? [fullText])[i]), c.ko, c.meaning], unfoldAt, Math.max(unfoldAt + 0.3, ctx.dur - 0.9)) : null;
90
+ const partAt = parts.map((_, i) => said?.[i] ?? unfoldAt + i * each);
91
+ const unfoldEnd = unfoldAt + each * Math.max(0, parts.length - 1) + 0.35;
92
+ const koAt = said && (parts.length || kit.say.length || kit.lines.length) ? said[parts.length] : parts.length ? unfoldEnd : termAt + 0.2;
93
+ const meanAt = Math.max(koAt + koDur, said?.[parts.length + 1] ?? 0);
94
+ followSpoken(kit, ctx, [...parts.map((p, i) => ({ at: partAt[i], el: p.box })), ...(ko ? [{ at: koAt, el: ko }] : []), ...(meaning ? [{ at: meanAt, el: meaning }] : [])]);
90
95
  const meanDur = meaning ? clamp([...c.meaning!].length * 0.035, 0.4, 1.2) : 0;
91
96
  // 짚음은 첫 낱말이 다 펴지자마자 — 펴기와 겹쳐 장면 끝까지 이어진다
92
97
  // 풀이 낱말이 없으면(약자가 아닌 용어) 펼 것이 없다 — 용어가 서자마자 짚기 시작한다
93
- const markFrom = parts.length ? unfoldAt + Math.max(each, 0.3) : termAt + 0.06 * Math.max(0, chars.length - 1) + 0.12;
94
- // 짚개 — 약자와 풀이 낱말·한국어 이름을 번갈아 짚는다(약자 → 낱말 → 약자 → 다음 낱말 …). 빠르기는 박이 정한다
95
- const point = glider(host, [termEl, ...parts.map((p) => p.box), ...(ko ? [ko] : [])], { hub: true });
96
- const stops = beatStops(kit.hits, markFrom, ctx.dur);
98
+ const markFrom = parts.length ? partAt[0] + Math.max(each, 0.3) : termAt + 0.06 * Math.max(0, chars.length - 1) + 0.12;
99
+ // 짚개 — 약자부터 짚고, 머무는 테마는 목소리가 풀이 낱말·한국어 이름(풀이·이름이 없으면 뜻)을 말할 때만 그리로 옮긴다
100
+ const point = glider(host, [{ el: termEl, text: c.term }, ...parts.map((p, i) => ({ el: p.box, text: (words ?? [fullText])[i] })), ...(ko ? [{ el: ko, text: c.ko }] : []), ...(meaning && !parts.length && !ko ? [{ el: meaning, text: c.meaning }] : [])], { kit, ctx, from: markFrom });
97
101
 
98
102
  const writeKo = ko ? slots.write(ko, { ctx, text: c.ko!, at: koAt, dur: koDur, seed: `${kit.seed}:ko` }) : null;
99
103
  const writeMeaning = meaning ? slots.write(meaning, { ctx, text: c.meaning!, at: meanAt, dur: meanDur, seed: `${kit.seed}:m` }) : null;
@@ -106,18 +110,18 @@ const pattern: Pattern<TermContent> = {
106
110
  const p = ease.outExpo(progress(t, termAt + i * 0.06, 0.3));
107
111
  applyStyle(ch, { y: (1 - p) * termPx * 0.4, opacity: p });
108
112
  // 약자 글자는 제 낱말이 펴질 때 강조색으로 — 글자와 낱말이 이어진다
109
- const k = words ? clamp((t - (unfoldAt + i * each)) / 0.2) : 0;
113
+ const k = words ? clamp((t - ((partAt[i] ?? unfoldAt))) / 0.2) : 0;
110
114
  ch.style.color = k > 0.5 ? ACCENT_TEXT : "var(--kf-ink)";
111
115
  });
112
116
  applyStyle(termEl, landed(t));
113
117
  parts.forEach((pt, i) => {
114
118
  // 고른 빠르기로 편다 — 감속 끝자락이 길면 그 사이가 멈춘 화면이 된다
115
- const p = progress(t, unfoldAt + i * each, 0.3);
119
+ const p = progress(t, (partAt[i] ?? unfoldAt), 0.3);
116
120
  pt.box.style.width = `${(p * pt.em * fullPx * 1.02).toFixed(1)}px`;
117
121
  pt.box.style.visibility = p > 0 ? "visible" : "hidden";
118
122
  });
119
123
  // 풀이를 낱말 차례로 반전해 짚는다 — 펴기가 끝난 뒤 장면 끝까지
120
- point(t, stops);
124
+ point(t);
121
125
  writeKo?.(t);
122
126
  writeMeaning?.(t);
123
127
  };
@@ -1,7 +1,7 @@
1
1
  import { applyStyle, clamp } from "@kasaframe/core";
2
2
  import { sweep } from "@kasaframe/motion";
3
3
  import { fitSize } from "@kasaframe/motion/moves";
4
- import { ACCENT_TEXT, area, beatStops, cameraOk, glider, node, riseName, slotsOf, moving } from "../kit.ts";
4
+ import { ACCENT_TEXT, area, beatStops, cameraOk, dwells, glider, moving, node, reread, riseName, slotsOf } from "../kit.ts";
5
5
  import type { Pattern } from "../types.ts";
6
6
 
7
7
  export interface TitleContent {
@@ -12,12 +12,12 @@ export interface TitleContent {
12
12
 
13
13
  /**
14
14
  * 이름이 글자마다 아래에서 가림막을 뚫고 올라와 서고(드러냄), 카메라가 그 이름으로 한 번 다가간다 — 다가감의 이유가 드러냄이다.
15
- * 다가가는 동안 밑줄·부제가 서고, 짚개가 이름과 작은 글을 박마다 번갈아 짚는다.
15
+ * 다가가는 동안 밑줄·부제가 서고, 작은 글이 있으면 짚개가 이름부터 짚는다(머무는 테마는 목소리가 작은 글을 말할 때만 그리로).
16
16
  */
17
17
  const pattern: Pattern<TitleContent> = {
18
18
  id: "title.mask",
19
19
  type: "title",
20
- does: "이름이 글자마다 가림막을 뚫고 서고, 다가가는 동안 짚개가 이름과 작은 글을 번갈아 짚는다",
20
+ does: "이름이 글자마다 가림막을 뚫고 서고, 다가가는 동안 짚개가 이름부터 짚는다",
21
21
  energy: ["mid"],
22
22
  family: "mask",
23
23
  camera: ["pushIn"],
@@ -61,17 +61,18 @@ const pattern: Pattern<TitleContent> = {
61
61
  const subAt = sweepAt + 0.3;
62
62
  const subDur = Math.max(0.3, ctx.dur - subAt - 0.08);
63
63
  if (cameraOk(ctx)) kit.stage?.camera.pushIn(settle, { to: name, scale: 1.07, dur: Math.max(0.6, ctx.dur - settle) });
64
- // 이름이 선 뒤엔 짚개가 이름 ↔ 위 작은 글·부제를 박마다 번갈아 짚는다(카메라를 끈 영상에서도 쉬지 않는 몫, 장면이 길어도 박이 빠르기를 정한다)
65
- const point = glider(host, [name, ...(kicker ? [kicker] : []), ...(sub ? [sub] : [])], { hub: true });
66
- const stops = beatStops(kit.hits, Math.min(settle, nameAt + 0.35), ctx.dur);
64
+ // 이름이 서면 짚개가 이름과 위 작은 글·부제를 짚는다(짚기 방식은 테마 짚음). 작은 글이 없고 훑는 테마면 이름 글자를 박마다 다시 짚는다
65
+ const markFrom = Math.min(settle, nameAt + 0.35);
66
+ const point = glider(host, [{ el: name, text: c.title }, ...(kicker ? [{ el: kicker, text: c.kicker }] : []), ...(sub ? [{ el: sub, text: c.sub }] : [])], { kit, ctx, from: markFrom });
67
+ const again = !kicker && !sub && !dwells(kit, ctx) ? beatStops(kit.hits, markFrom, ctx.dur) : null;
67
68
  const writeKicker = kicker ? slots.write(kicker, { ctx, text: c.kicker!, at: 0, dur: Math.min(0.35, nameEnd * 0.6), seed: `${kit.seed}:kicker` }) : null;
68
69
  const writeSub = sub ? slots.write(sub, { ctx, text: c.sub!, at: subAt, dur: subDur, seed: `${kit.seed}:sub` }) : null;
69
70
  const landed = slots.land(name, { ctx, at: nameEnd, seed: `${kit.seed}:land` });
70
71
 
71
72
  return (t: number) => {
72
73
  moving("title.mask", ctx);
73
- paintName(t, { riseAt: nameAt, each, fill: 0 });
74
- point(t, stops);
74
+ paintName(t, { riseAt: nameAt, each, fill: 0, up: again ? reread(t, again) : undefined });
75
+ point(t);
75
76
  applyStyle(name, landed(t));
76
77
 
77
78
  applyStyle(bar, sweep(t, { at: sweepAt, dur: 0.3 }));
@@ -7,7 +7,7 @@ const pattern: Pattern<TitleContent> = {
7
7
  ...endSlam,
8
8
  id: "title.slam",
9
9
  type: "title",
10
- does: "옅은 자리표 이름이 먼저 서고 드롭에 꽉 찬 이름이 쾅 내려앉는다, 부제·작은 글이 붙고 짚개가 번갈아 짚는다",
10
+ does: "옅은 자리표 이름이 먼저 서고 드롭에 꽉 찬 이름이 쾅 내려앉는다, 부제·작은 글이 붙고 짚개가 이름부터 짚는다",
11
11
  purposes: { prefer: ["promo", "launch", "ad", "mv"] },
12
12
  fits(c, info) {
13
13
  if (typeof c?.title !== "string" || !c.title.trim() || [...c.title].length > 16) return "title 이 비었거나 16자를 넘는다";
@@ -40,6 +40,7 @@ export interface Kit {
40
40
  seed: string;
41
41
  hits: number[];
42
42
  say: SpokenWord[];
43
+ lines: SpokenWord[];
43
44
  slots: ThemeSlots;
44
45
  stage: StageLike;
45
46
  policy: TextPolicy;
@@ -153,7 +153,7 @@ function voicePart(plan: Plan, caps: CapabilityReport): { q?: Question; auto?: s
153
153
  };
154
154
  }
155
155
 
156
- // 테마 — 시리즈가 정했거나 요청이 「리소 느낌」·「og-measure/night-amber」처럼 짚었으면 묻지 않는다(kf new 의 auto 가 그대로 짚은 판을 쓴다).
156
+ // 테마 — 시리즈가 정했거나 요청이 「리소 느낌」·「og-measure/snow-blue」처럼 짚었으면 묻지 않는다(kf new 의 auto 가 그대로 짚은 판을 쓴다).
157
157
  // 보기는 「알아서」(= auto, 권장)와 요청·용도·분위기 낱말(사람이 준 한 마디·레퍼런스까지)에 맞는 approved 세계·변주 넷 — 수백 판을 다 늘어놓지 않는다.
158
158
  // 사람이 보기 밖으로 분위기 한 마디나 그림·주소를 주면 그걸로 넷을 다시 고른다(kf make --theme-mood·--theme-ref)
159
159
  export const THEME_PICKS = 4;
@@ -1,8 +1,9 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { freezeLimits } from "@kasaframe/core";
2
3
  import { captionMode } from "@kasaframe/motion/captions";
3
4
  import { bundleFor, MENU, type Dress } from "@kasaframe/motion/menu";
4
5
  import { questionsMarkdown, type Question } from "@kasaframe/gen";
5
- import { loadTheme } from "@kasaframe/themes";
6
+ import { loadTheme, themeFreezeSec } from "@kasaframe/themes";
6
7
  import type { GenSpec, ShotSpec } from "./types.ts";
7
8
  import { join } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
@@ -125,6 +126,8 @@ export function projectBrief(dir: string): string {
125
126
  const rules = purposeRules(kf.template);
126
127
  const tplFile = join(repo, "templates", kf.template, "template.json");
127
128
  const tplMotion = existsSync(tplFile) ? (JSON.parse(readFileSync(tplFile, "utf8")).motion ?? {}) : {};
129
+ // kf check 와 같은 기준 — 틀이 style.json 을 덮고, 테마는 오류 기준을 늘리기만 한다(늘린 만큼은 경고로 남는다)
130
+ const freeze = freezeLimits({ ...(style.motion ?? {}), ...tplMotion }.maxFreezeSec, themeFreezeSec(kf.theme));
128
131
  const colors = Object.keys(style.colors ?? {}).map((k: string) => `--kf-${k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`);
129
132
  const core = signatures("core");
130
133
  const motion = signatures("motion");
@@ -135,7 +138,7 @@ export function projectBrief(dir: string): string {
135
138
  ...questionsMarkdown(plan.asks ?? [], (flags) => `npx kf make --answer ${dir} ${flags}`),
136
139
  ...narrationSection(dir, kf, plan),
137
140
  `**이 영상의 규칙 — 용도 ${kf.template}(틀이 기본값을 덮는다, kf check 가 같은 값으로 잡는다)**`,
138
- `1. 화면은 ${rules.maxFreezeSec}초(60fps 6프레임) 넘게 멈추지 않는다 — 끝 카드·큰 숫자·로고도 예외 없이 화면 안 움직임(숫자 굴러가기·칸 채우기·반짝임·글자 흐름·바탕 흐름)으로 계속 살아 있게.`,
141
+ `1. 화면은 ${freeze.error}초(60fps ${Math.round(freeze.error * 60)}프레임) 넘게 멈추지 않는다${freeze.error > freeze.warn ? `(테마 ${kf.theme} 가 늘린 기준 — ${freeze.warn}초 넘는 멈춤도 경고로 남는다)` : ""} — 끝 카드·큰 숫자·로고도 예외 없이 화면 안 움직임(숫자 굴러가기·칸 채우기·반짝임·글자 흐름·바탕 흐름)으로 계속 살아 있게.`,
139
142
  `2. 같은 구도는 ${rules.staticSec.warn}초 넘기지 않는다(${rules.staticSec.error}초 넘으면 오류) — 새 요소·칸 채우기·숫자 키우기·컷·전환으로 바꾼다.`,
140
143
  rules.beatMove === "every-beat"
141
144
  ? "3. 미세하게 흔들리는 것만으로 때우지 않는다 — 눈에 보이는 큰 움직임이 박마다 하나 이상."
@@ -38,14 +38,20 @@ export function suggestThemes(request: string, purpose: Purpose, aspect?: Aspect
38
38
  const lookOn = hasLook(want) || !!look.ref;
39
39
  const pool = listThemes({ status: "approved", purpose, ...(aspect ? { aspect } : {}), variants: lookOn });
40
40
  const ask = new Set(requestWords(text));
41
+ const tagsOf = (t: Theme) => (t.tags?.use ?? []).map((u) => u.toLowerCase());
42
+ // 드물게 적힌 낱말일수록 그 세계를 짚는다 — 「소개」처럼 여러 세계가 적은 낱말보다 「OpenGateway」처럼 한 세계만 적은 낱말이 무겁다
43
+ const tagShare = (w: string) => pool.filter((t) => tagsOf(t).some((u) => u.includes(w))).length / Math.max(1, pool.length);
41
44
  const scored = pool.map((t) => {
42
45
  const hay = `${t.name} ${t.metaphor} ${t.guide} ${(t.tags?.use ?? []).join(" ")} ${(t.tags?.mood ?? []).join(" ")} ${t.variant?.desc ?? ""}`.toLowerCase();
43
46
  const hit = [...ask].filter((w) => hay.includes(w));
47
+ // 세계가 tags.use 에 일부러 적은 낱말은 본문에 우연히 나온 낱말보다 무겁다 — 공통 견본 브리프(「OpenGateway 소개」)의 낱말은 모든 세계 THEME.md 에 있어
48
+ // 본문 맞음만으로는 그 제품 전용 세계가 다른 세계와 동점이 된다
49
+ const tagged = hit.filter((w) => tagsOf(t).some((u) => u.includes(w)));
44
50
  const l = lookOn ? lookScore(t, want, look.ref) : { score: 0, why: [] };
45
51
  const used = use.filter((w) => (t.tags?.use ?? []).some((u) => u.includes(w)));
46
52
  const fit = use.length ? (0.9 * used.length) / use.length : 0;
47
53
  // 같은 점수면 세계 기본 판이 앞 — 바란 생김새가 없을 때 변주를 굳이 고르지 않게
48
- return { t, score: hit.length + fit + l.score + (t.variant ? 0 : 0.3), hit, used, why: l.why, tie: tieKey(text, t.id) };
54
+ return { t, score: hit.length + tagged.reduce((s, w) => s + 0.5 * (1 - tagShare(w)), 0) + fit + l.score + (t.variant ? 0 : 0.3), hit, used, why: l.why, tie: tieKey(text, t.id) };
49
55
  });
50
56
  scored.sort((a, b) => b.score - a.score || a.tie - b.tie || a.t.id.localeCompare(b.t.id));
51
57
  const picked: typeof scored = [];
@@ -72,7 +78,7 @@ export function suggestThemes(request: string, purpose: Purpose, aspect?: Aspect
72
78
  // 사람이 짚은 것이라 review 까지 받는다(draft 는 빼고)
73
79
  export function themeFromRequest(request: string, purpose?: Purpose): ThemePick | null {
74
80
  const t = request.toLowerCase();
75
- // 변주를 이름으로 짚었으면(「og-measure/night-amber 로」) 그 판
81
+ // 변주를 이름으로 짚었으면(「og-measure/snow-blue 로」) 그 판
76
82
  const full = /([a-z0-9-]+)\/([a-z0-9-]+)/.exec(t);
77
83
  if (full) {
78
84
  try {
@@ -23,9 +23,9 @@
23
23
  ## 3. 비유·스타일 — `THEME.md` 앞부분 · `style.json` · `theme.json`
24
24
 
25
25
  - 비유 한 줄: 「무엇이 무엇처럼 되며 화면이 찍힌다/드러난다」. 내용(정보가 화면에 오르는 방식)에서 나온 개념이어야 한다 — 장식 이름이 아니라 동사가 있는 문장.
26
- - `style.json`: 프리셋과 같은 KfStyle 꼴 통째, `name` = `{{id}}`. 잉크 2–3색 + 그 어두운 단(`colors`), 바탕 `bg`, 글 `ink`, 강조 `accent`. 글꼴 셋(`sans`·`display`·`mono`)은 **한글이 서는 글꼴로 끝나게**(Pretendard 를 마지막에), `faces` 는 **한 파일 주소**(jsdelivr `gh/google/fonts@main/ofl/<이름>/<파일>.ttf` 나 `npm/pretendard@1.3.9/.../PretendardVariable.woff2`). 구글 폰트 CSS(unicode-range 조각)는 글이 투명하게 찍히니 쓰지 않는다. `theme` 칸에 `inks`·`texture`·`movement` 를 글로.
26
+ - `style.json`: 프리셋과 같은 KfStyle 꼴 통째, `name` = `{{id}}`. 잉크 2–3색 + 그 어두운 단(`colors`), 바탕 `bg`, 글 `ink`, 강조 `accent`. 글꼴 셋(`sans`·`display`·`mono`)은 **한글이 서는 글꼴로 끝나게**(Pretendard 를 마지막에), `faces` 는 **판을 박은 한 파일 주소**(jsdelivr `gh/google/fonts@<커밋 40자>/ofl/<이름>/<파일>.ttf` 나 `npm/pretendard@1.3.9/.../PretendardVariable.woff2` — `@main`·`@latest`·`@5` 처럼 바뀌는 판은 같은 영상이 다르게 구워진다). 구글 폰트 CSS(unicode-range 조각)는 글이 투명하게 찍히니 쓰지 않는다. 글꼴은 상업·영상·웹 임베딩이 허용된 OFL·Apache 만, 한 굵기뿐인 글꼴을 더 굵게 부르면 가짜 굵기가 된다 — 짝 목록에 있는 글꼴이면 `kf fonts list`·`kf fonts sheet` 에서 고르는 게 가장 빠르다. `theme` 칸에 `inks`·`texture`·`movement` 를 글로.
27
27
  - `theme.json`: `name`·`metaphor`·`purposes`·`aspect`. status 는 draft 그대로.
28
- - `theme.json` 의 `vary`·`tags`(SPEC 9 「변주」): 이 세계가 approved 되면 `kf theme vary {{id}}` 가 겉모습만 바꾼 판 15개를 뽑는다. 비유를 깨지 않는 축과 범위만 적는다 — 밝기(`scheme`), 강조 색상 범위(`hue`, OKLCH 도), 글꼴 짝(`fonts` — `packages/themes/kit/fonts.json` 의 id·`tag:<태그>`·`base`), 질감(`texture`), 선 굵기(`stroke` — 슬롯 `trace` 가 `slotkit.traceCanvas` 를 쓰면 실린다), 모서리(`radius`), 빠르기(`tempo`), 밀도(`density` — 효과가 `ctx.style.look?.density` 를 읽을 때만). 금지에 적은 것과 부딪히는 축은 빼고(「밝은 바탕 금지」면 `scheme: ["dark"]`), 재료처럼 색상이 뜻인 색 열쇠는 `fixedHue`, 늘 같은 색은 `keep`. `tags` 는 `{ mood, light, use, dims }`. 효과·슬롯 색은 `var(--kf-*)`·`ctx.style.colors` 로 읽어야 변주 색이 실린다 — 색을 코드에 박지 않는다.
28
+ - `theme.json` 의 `vary`·`tags`(SPEC 9 「변주」): 이 세계가 approved 되면 `kf theme vary {{id}}` 가 겉모습만 바꾼 판 15개를 뽑는다. 비유를 깨지 않는 축과 범위만 적는다 — 밝기(`scheme`), 강조 색상 범위(`hue`, OKLCH 도), 글꼴 짝(`fonts` — 짝 id·`tag:<태그>`·`tag:<태그>+<태그>`(둘 다 가진 짝 — 짝이 수백이라 결이 다른 무리가 딸려 오지 않게)·`base`. 목록은 `kf fonts list`(`packages/themes/kit/fonts.json`·`kit/fonts/<무리>.json`), 견본은 `kf fonts sheet`), 질감(`texture`), 선 굵기(`stroke` — 슬롯 `trace` 가 `slotkit.traceCanvas` 를 쓰면 실린다), 모서리(`radius`), 빠르기(`tempo`), 밀도(`density` — 효과가 `ctx.style.look?.density` 를 읽을 때만). 금지에 적은 것과 부딪히는 축은 빼고(「밝은 바탕 금지」면 `scheme: ["dark"]`), 재료처럼 색상이 뜻인 색 열쇠는 `fixedHue`, 늘 같은 색은 `keep`. 브랜드 가이드가 라이트·다크 두 벌을 따로 정해 뒀으면 다른 밝기 한 벌을 `palettes.<light|dark>` 에(그 밝기 판은 뒤집어 옮기지 않고 그 벌에서 시작한다). `tags` 는 `{ mood, light, use, dims }` — `use` 에는 그 세계가 맞는 제품·주제 이름도(공통 견본 브리프 낱말은 모든 THEME.md 에 있어 `use` 가 고르기를 가른다). 효과·슬롯 색은 `var(--kf-*)`·`ctx.style.colors` 로 읽어야 변주 색이 실린다 — 색을 코드에 박지 않는다.
29
29
  - 글자 대비는 찍힌 픽셀로 3:1 이상 — 옅은 잉크로 작은 글을 쓰지 않는다.
30
30
 
31
31
  ## 4. 효과 — `effects.ts` (5–10개)