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
@@ -1,20 +1,31 @@
1
- import { applyStyle, clamp, emit, ease, progress, safeTime, type Style } from "@kasaframe/core";
1
+ import { applyStyle, bezier, clamp, cue, emit, ease, progress, safeTime, type SfxRole, type Style } from "@kasaframe/core";
2
2
  import { typeText } from "@kasaframe/motion";
3
3
 
4
4
  // 비유: 계측기 화면. 실제 호출이 한 줄 나가고, 원자료 로그가 흐르고, 거기서 숫자가 뽑혀 점·막대·큰 숫자로 쌓인다.
5
5
  // 화면의 숫자는 늘 재 온 값(measure.json 같은 원자료)에서 온다 — 효과는 숫자를 지어내지 않고, 들어온 시각에 보여 줄 뿐이다.
6
- // 화면은 0.1초도 쉬지 않는다 — 카메라는 늘 조금씩 다가가며 흐르고, 박마다 글자·줄·점·막대 중 하나가 크게 움직인다.
6
+ // 겉모습은 OpenGateway 스타일가이드(design.md·globals.css)를 따른다: 상자에 가두지 않고, 칩은 속을 칠하지 않으며,
7
+ // 모서리는 역할(알약·면 12px·각짐)이고, 움직임은 150/220/300ms 감속·스프링과 6px 나타남뿐이다(커졌다 줄기·흔들기 없음).
7
8
 
8
9
  const MONO = "var(--kf-font-mono)";
9
- const BAR = "rgba(100, 116, 139, 0.28)";
10
- const snappy = ease.snappy;
10
+ const BAR = "var(--kf-bar)";
11
+ // globals.css --motion-fast/base/slow·--motion-rise·--ease-out·--ease-spring
12
+ const FAST = 0.15;
13
+ const BASE = 0.22;
14
+ const SLOW = 0.3;
15
+ const RISE = 6;
16
+ const out = ease.snappy;
17
+ const springy = bezier(0.34, 1.3, 0.64, 1);
11
18
 
12
- // OG 움직임 결: 220ms 감속 + 진입 거리
13
- function rise(t: number, at: number, dur = 0.22, px = 14): Style {
14
- const p = snappy(progress(t, at, dur));
19
+ // 서는 순간 소리 하나 — 같은 시각에 함께 서는 칸들은 한 번만(자리가 같으면 한 줄). 칸마다 나는 나타남·째깍·타자는 잔소리, 끝 도장만 기본으로
20
+ function rise(t: number, at: number, dur = BASE, px = RISE, role: SfxRole = "appear"): Style {
21
+ cue(role, t - at, { source: `rise:${role}@${at.toFixed(3)}`, minor: role !== "end" });
22
+ const p = out(progress(t, at, dur));
15
23
  return { opacity: p, y: px * (1 - p) };
16
24
  }
17
25
 
26
+ // 마스터 로고 원문은 루트 <svg> 에 width·height(1140×650)가 박혀 있다 — 루트에서만 걷고 상자를 채우게(안쪽 rect 의 width 는 그대로)
27
+ const fitSvg = (svg: string) => svg.replace(/<svg\b[^>]*>/, (tag) => tag.replace(/\s(width|height)="[^"]*"/g, "").replace("<svg", '<svg width="100%" height="100%"'));
28
+
18
29
  function box(doc: Document, css: string, text = ""): HTMLElement {
19
30
  const d = doc.createElement("div");
20
31
  d.style.cssText = css;
@@ -22,27 +33,33 @@ function box(doc: Document, css: string, text = ""): HTMLElement {
22
33
  return d;
23
34
  }
24
35
 
36
+ // 배지 — 흐린 테두리와 글자색만(badge.tsx). 화면 글이 UI 의 서너 배라 선도 그만큼 굵게
37
+ type Tone = "accent" | "plain" | "soft" | "success";
38
+ const edge = (size: number) => Math.max(2, Math.round(size * 0.05));
39
+ function badgeCss(tone: Tone, size: number): string {
40
+ const line = tone === "accent" ? "color-mix(in srgb, var(--kf-accent) 45%, transparent)" : tone === "success" ? "color-mix(in srgb, var(--kf-success) 45%, transparent)" : "var(--kf-line)";
41
+ const ink = tone === "accent" ? "var(--kf-accent-text)" : tone === "success" ? "var(--kf-success)" : tone === "soft" ? "var(--kf-dim)" : "var(--kf-ink)";
42
+ return `border:${edge(size)}px solid ${line};border-radius:var(--kf-radius-pill);background:transparent;color:${ink}`;
43
+ }
44
+
25
45
  /**
26
- * 측정 바탕 — 흰 모눈이 천천히 흐르고 옅은 파란 주사선이 반 주기 간격으로 둘 지나간다. 주사선은 카드 위로도 지나간다 —
27
- * 카메라가 서 있는 사이 화면을 꽉 채운 카드 밑에 숨으면 박 사이가 멈춘 프레임이 된다. 구도는 안 바꾼다
46
+ * 측정 바탕 — 바탕색 위에 구분선 색의 옅은 모눈이 천천히 흐른다. 그라데이션·빛 번짐 없이 모눈 하나뿐이고 구도는 안 바꾼다.
47
+ * 박 사이를 메우는 몫은 바탕이 아니라 내용(카운터·로그·막대·읽는 칸)이 진다
28
48
  */
29
- export function graphPaper(el: HTMLElement, o: { cell?: number; period?: number; phase?: number } = {}): (t: number) => void {
49
+ export function graphPaper(el: HTMLElement, o: { cell?: number; phase?: number } = {}): (t: number) => void {
30
50
  const doc = el.ownerDocument;
31
51
  const cell = o.cell ?? 56;
52
+ const grid = "color-mix(in srgb, var(--kf-line) 45%, transparent)";
32
53
  const bg = box(
33
54
  doc,
34
55
  `position:absolute;inset:0;background:var(--kf-bg);overflow:hidden;` +
35
- `background-image:linear-gradient(rgba(203,213,225,.55) 1px,transparent 1px),linear-gradient(90deg,rgba(203,213,225,.55) 1px,transparent 1px);` +
56
+ `background-image:linear-gradient(${grid} 1px,transparent 1px),linear-gradient(90deg,${grid} 1px,transparent 1px);` +
36
57
  `background-size:${cell}px ${cell}px`,
37
58
  );
38
- const scans = [0, 0.5].map((k) => ({ k, el: box(doc, "position:absolute;top:0;bottom:0;width:120px;z-index:5;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(0,111,214,.07),transparent)") }));
39
- for (const s of scans) bg.append(s.el);
40
59
  el.append(bg);
41
- const period = o.period ?? 4;
42
60
  return (t: number) => {
43
61
  const u = safeTime(t) + (o.phase ?? 0);
44
62
  bg.style.backgroundPosition = `${(u * 18) % cell}px ${(u * 9) % cell}px`;
45
- for (const s of scans) s.el.style.transform = `translateX(${((((u / period + s.k) % 1) + 1) % 1) * ((el.clientWidth || 1920) + 240) - 120}px)`;
46
63
  };
47
64
  }
48
65
 
@@ -52,47 +69,41 @@ export interface CamShot {
52
69
  x?: number;
53
70
  y?: number;
54
71
  zoom?: number;
55
- /** 샷이 바뀌는 순간 배율을 이만큼 더했다 걷는다(툭) */
72
+ /** 샷이 서는 순간 이만큼 가까이에서 시작해 300ms 감속으로 물러나 앉는다 */
56
73
  kick?: number;
57
74
  }
58
75
 
59
76
  /**
60
- * 계측 카메라 — 샷이 바뀌면 220ms 감속으로 옮기며 툭(kick) 치고, 샷 사이에는 선다. 샷은 드러냄·결과처럼 이유가 있을 때만 —
61
- * 쉬지 않는 몫은 카운터·로그 한 줄·막대가 진다(늘 흐르는 카메라는 kf check camera.wander). drift·pan 을 주면 샷 사이에도 흐르지만 이벤트가 안 남는다.
77
+ * 계측 카메라 — 샷이 바뀌면 300ms(자리 옮김) 스프링으로 옮기고 샷 사이에는 선다. 스프링이 살짝 넘었다 돌아오는 것이 「툭」이다.
78
+ * 샷은 드러냄·결과처럼 이유가 있을 때만 — 쉬지 않는 몫은 카운터·로그 한 줄·막대가 진다(늘 흐르는 카메라는 kf check camera.wander).
62
79
  * 긴 로그 줄이 반쯤 잘리지 않게 크게 들어가지 않는다(배율 1–1.1). 무대 상자(transform-origin 0 0)에 붙인다.
63
80
  */
64
- export function trackCam(t: number, o: { shots: CamShot[]; drift?: number; pan?: number; w?: number; h?: number; scene?: string }): Style {
81
+ export function trackCam(t: number, o: { shots: CamShot[]; w?: number; h?: number; scene?: string }): Style {
65
82
  t = safeTime(t);
66
83
  const W = o.w ?? 1920;
67
84
  const H = o.h ?? 1080;
68
- const drift = o.drift ?? 0;
69
- const pan = o.pan ?? 0;
70
85
  const shots = o.shots.length ? o.shots : [{ at: 0 }];
71
- const frame = (i: number, u: number) => {
72
- const s = shots[i];
73
- const e = Math.max(0, u - s.at);
74
- const dir = i % 2 ? -1 : 1;
75
- return { x: (s.x ?? W / 2) + dir * pan * e, y: (s.y ?? H / 2) + pan * 0.4 * e, z: (s.zoom ?? 1) * (1 + drift * e) };
76
- };
86
+ const frame = (i: number) => ({ x: shots[i].x ?? W / 2, y: shots[i].y ?? H / 2, z: shots[i].zoom ?? 1 });
77
87
  let i = 0;
78
88
  for (let k = 0; k < shots.length; k++) if (shots[k].at <= t) i = k;
79
- let f = frame(i, t);
89
+ let f = frame(i);
80
90
  const since = t - shots[i].at;
81
- if (i > 0 && since < 0.22) {
82
- const p = snappy(since / 0.22);
83
- const a = frame(i - 1, shots[i].at);
91
+ if (i > 0) cue("whoosh", since, { source: `trackCam@${shots[i].at.toFixed(3)}`, scene: o.scene });
92
+ if (i > 0 && since < SLOW) {
93
+ const p = springy(since / SLOW);
94
+ const a = frame(i - 1);
84
95
  f = { x: a.x + (f.x - a.x) * p, y: a.y + (f.y - a.y) * p, z: a.z + (f.z - a.z) * p };
85
- emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round((1 - p) * 1000) });
96
+ emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round((1 - clamp(since / SLOW)) * 1000) });
86
97
  }
87
- const kick = since >= 0 ? (shots[i].kick ?? 0) * Math.exp(-since / 0.12) : 0;
88
- if (since >= 0.22 && kick > 0.0005) emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round(kick * 1000) });
98
+ const kick = since >= 0 && since < SLOW ? (shots[i].kick ?? 0) * (1 - out(since / SLOW)) : 0;
99
+ if (kick > 0.0005) emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round(kick * 1000) });
89
100
  const z = f.z * (1 + kick);
90
101
  return { x: W / 2 - z * f.x, y: H / 2 - z * f.y, scale: z };
91
102
  }
92
103
 
93
104
  /**
94
- * 실제 호출 한 줄 — 주소가 박마다 한 조각씩 붙고, 모델·보낼 글이 찍히고, sendAt 부터 경과 시간이 실제 속도로 굴러
95
- * 잰 값(ms)에 멈추며 상태·첫 토큰·받은 글이 붙는다. 카운터는 지어낸 값이 아니라 원자료의 ms 까지만 센다.
105
+ * 실제 호출 한 줄 — 응답 패널(틀이 곧 뜻인 면, 12px). 주소가 박마다 한 조각씩 붙고, 모델·보낼 글이 찍히고, sendAt 부터 경과 시간이
106
+ * 실제 속도로 굴러 잰 값(ms)에 멈추며 상태·첫 토큰·받은 글이 붙는다. 카운터는 지어낸 값이 아니라 원자료의 ms 까지만 센다.
96
107
  */
97
108
  export function callLine(
98
109
  el: HTMLElement,
@@ -117,13 +128,13 @@ export function callLine(
117
128
  method?: string;
118
129
  size?: number;
119
130
  },
120
- ): (t: number) => void {
131
+ ): ((t: number) => void) & { parts: HTMLElement[]; timer: HTMLElement } {
121
132
  const doc = el.ownerDocument;
122
133
  const size = o.size ?? 56;
123
- const card = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${size * 0.7}px ${size * 0.85}px;border-radius:12px;background:var(--kf-surface);font-family:${MONO};font-size:${size}px;line-height:1.45;color:var(--kf-ink);font-variant-numeric:tabular-nums`);
134
+ const card = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${size * 0.7}px ${size * 0.85}px;border-radius:var(--kf-radius-md);background:var(--kf-surface);font-family:${MONO};font-size:${size}px;line-height:1.45;color:var(--kf-ink);font-variant-numeric:tabular-nums`);
124
135
  const row = (css = "") => box(doc, `display:flex;align-items:center;gap:${size * 0.35}px;white-space:nowrap;${css}`);
125
136
  const r1 = row();
126
- const chip = box(doc, `padding:0 ${size * 0.3}px;border-radius:999px;background:var(--kf-accent);color:var(--kf-on-accent);font-size:${size * 0.8}px;font-weight:700`, o.method ?? "POST");
137
+ const chip = box(doc, `padding:0 ${size * 0.3}px;font-size:${size * 0.8}px;font-weight:600;line-height:1.3;${badgeCss("accent", size * 0.8)}`, o.method ?? "POST");
127
138
  const urlBox = box(doc, "display:flex");
128
139
  const parts = o.url.map((u) => {
129
140
  const s = doc.createElement("span");
@@ -136,9 +147,9 @@ export function callLine(
136
147
  const r2 = row("color:var(--kf-dim)");
137
148
  const r3 = row("color:var(--kf-dim)");
138
149
  const r4 = row(`margin-top:${size * 0.4}px`);
139
- const timer = box(doc, `font-family:var(--kf-font-display);font-weight:800;font-size:${size * 1.9}px;letter-spacing:-0.03em;line-height:1;transform-origin:0 50%`);
140
- const ok = box(doc, `padding:0 ${size * 0.3}px;border-radius:999px;background:var(--kf-success);color:var(--kf-ink);font-size:${size * 0.8}px;font-weight:700`, String(o.status ?? ""));
141
- const what = box(doc, "color:var(--kf-accent-text);font-weight:700", "첫 토큰");
150
+ const timer = box(doc, `font-family:var(--kf-font-display);font-weight:800;font-size:${size * 1.9}px;letter-spacing:-0.055em;line-height:1`);
151
+ const ok = box(doc, `padding:0 ${size * 0.3}px;font-size:${size * 0.8}px;font-weight:600;line-height:1.3;${badgeCss("success", size * 0.8)}`, String(o.status ?? ""));
152
+ const what = box(doc, "color:var(--kf-accent-text);font-weight:600", "첫 토큰");
142
153
  const ans = box(doc, "color:var(--kf-ink)");
143
154
  r4.append(timer, ok, what, ans);
144
155
  const r5 = row(`color:var(--kf-dim);font-size:${size * 0.7}px`);
@@ -146,12 +157,12 @@ export function callLine(
146
157
  el.append(card);
147
158
  const ms = o.ms ?? 0;
148
159
  const doneAt = o.sendAt === undefined ? Infinity : o.sendAt + ms / 1000;
149
- return (t: number) => {
150
- applyStyle(card, rise(t, o.at, 0.22, 40));
160
+ const paint = (t: number) => {
161
+ applyStyle(card, rise(t, o.at));
151
162
  parts.forEach((s, i) => {
152
163
  const at = o.urlAt[i] ?? o.at;
153
164
  s.style.visibility = t >= at ? "visible" : "hidden";
154
- applyStyle(s, t >= at ? { ...rise(t, at, 0.18, 0), scale: 1 + 0.25 * (1 - snappy(progress(t, at, 0.18))) } : {});
165
+ applyStyle(s, t >= at ? rise(t, at) : {});
155
166
  });
156
167
  r2.textContent = o.model ? typeText(`model: ${o.model}`, t, { at: o.modelAt ?? o.at, cps: 90, jitter: 0, ime: false }).text : "";
157
168
  r2.style.display = o.model ? "" : "none";
@@ -161,21 +172,24 @@ export function callLine(
161
172
  r4.style.display = o.sendAt === undefined ? "none" : "";
162
173
  r4.style.visibility = o.sendAt !== undefined && t >= o.sendAt ? "visible" : "hidden";
163
174
  timer.textContent = o.sendAt === undefined ? "" : `${Math.min(ms, Math.max(0, Math.floor((t - o.sendAt) * 1000)))}ms`;
164
- timer.style.color = done ? "var(--kf-accent-text)" : "var(--kf-ink)";
175
+ // 색만 150ms 로 바뀐다 — 크기는 그대로(누름·도착에 커졌다 줄기 없음)
176
+ const c = done ? Math.round(out(progress(t, doneAt, FAST)) * 100) : 0;
177
+ timer.style.color = `color-mix(in srgb, var(--kf-accent-text) ${c}%, var(--kf-ink))`;
165
178
  for (const e of [ok, what, ans]) {
166
179
  e.style.display = done ? "" : "none";
167
180
  applyStyle(e, rise(t, doneAt));
168
181
  }
169
- applyStyle(timer, done ? { scale: 1 + 0.12 * Math.exp(-(t - doneAt) / 0.1) } : {});
170
182
  ans.textContent = done ? typeText(o.answer ?? "", t, { at: doneAt + 0.08, cps: 22, jitter: 0, ime: false }).text : "";
171
183
  r5.textContent = o.usage && o.usageAt !== undefined && t >= o.usageAt ? o.usage : "";
172
184
  r5.style.display = o.usage ? "" : "none";
173
185
  applyStyle(r5, o.usageAt !== undefined ? rise(t, o.usageAt) : {});
174
186
  };
187
+ return Object.assign(paint, { parts, timer });
175
188
  }
176
189
 
177
190
  /**
178
- * 원자료 로그 — 줄이 들어온 시각에 맨 아래 붙고 한 줄씩 위로 밀려 오른다. pick 정규식이 잡은 숫자는 그 시각부터 파란 칸.
191
+ * 원자료 로그 — 코드 블록(틀이 곧 뜻인 면, 12px). 줄이 들어온 시각에 맨 아래 붙고 한 줄씩 위로 밀려 오른다.
192
+ * pick 정규식이 잡은 숫자는 그 시각부터 고름 칠(파랑 /10 알약)과 파란 글자.
179
193
  * 줄 자리는 size·lineHeight·pad 로 정해지니 pickFly 출발점을 장면이 같은 값으로 셀 수 있다(새 줄 = 맨 아래 칸).
180
194
  */
181
195
  export function rawLog(
@@ -187,7 +201,7 @@ export function rawLog(
187
201
  const lh = o.lineHeight ?? Math.round(size * 1.5);
188
202
  const rows = o.rows ?? 9;
189
203
  const pad = o.pad ?? 28;
190
- const panel = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${pad}px;border-radius:12px;background:var(--kf-surface);font-family:${MONO};font-size:${size}px;line-height:${lh}px;color:var(--kf-ink-soft);font-variant-numeric:tabular-nums`);
204
+ const panel = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${pad}px;border-radius:var(--kf-radius-md);background:var(--kf-surface);font-family:${MONO};font-size:${size}px;line-height:${lh}px;color:var(--kf-ink-soft);font-variant-numeric:tabular-nums`);
191
205
  // 밀려 오르는 줄이 안쪽 여백에 반쯤 걸쳐 보이지 않게 줄 칸만큼만 보이는 창을 따로 둔다
192
206
  const view = box(doc, `position:relative;height:${rows * lh}px;overflow:hidden`);
193
207
  const strip = box(doc, "position:relative");
@@ -203,28 +217,28 @@ export function rawLog(
203
217
  const start = m.index + m[0].indexOf(g);
204
218
  num = doc.createElement("span");
205
219
  num.textContent = g;
206
- num.style.cssText = "border-radius:6px;padding:0 4px;margin:0 -4px";
220
+ num.style.cssText = "border-radius:var(--kf-radius-pill);padding:0 0.3em;margin:0 -0.3em";
207
221
  row.append(doc.createTextNode(line.slice(0, start)), num, doc.createTextNode(line.slice(start + g.length)));
208
222
  } else row.textContent = line;
209
223
  strip.append(row);
210
224
  return { row, num };
211
225
  });
212
226
  return (t: number) => {
213
- applyStyle(panel, rise(t, o.at ?? 0, 0.22, 40));
227
+ applyStyle(panel, rise(t, o.at ?? 0));
214
228
  let shown = 0;
215
229
  let slide = 1;
216
230
  o.times.forEach((at, i) => {
217
231
  if (t >= at) {
218
232
  shown = i + 1;
219
- slide = snappy(progress(t, at, 0.2));
233
+ slide = out(progress(t, at, BASE));
220
234
  }
221
- applyStyle(items[i].row, t >= at ? rise(t, at, 0.2, 0) : { opacity: 0 });
235
+ applyStyle(items[i].row, t >= at ? rise(t, at, BASE, 0, "type") : { opacity: 0 });
222
236
  const n = items[i].num;
223
237
  if (n) {
224
238
  const on = t >= at;
225
- n.style.background = on ? "rgba(0,111,214,.16)" : "transparent";
239
+ n.style.background = on ? "var(--kf-tint)" : "transparent";
226
240
  n.style.color = on ? "var(--kf-accent-text)" : "";
227
- n.style.fontWeight = on ? "700" : "";
241
+ n.style.fontWeight = on ? "600" : "";
228
242
  }
229
243
  });
230
244
  const over = Math.max(0, shown - rows);
@@ -232,37 +246,143 @@ export function rawLog(
232
246
  };
233
247
  }
234
248
 
235
- /** 뽑힌 숫자 — 로그 줄의 숫자가 그 자리에서 떠올라 호를 그리며 점 그림 자리로 날아가 꽂힌다(박마다 큰 움직임) */
249
+ type Pt = { x: number; y: number };
250
+ /**
251
+ * 읽는 칸이 닿을 자리 — 무대 좌표 가운데(x·y)와 크기, 아니면 target 요소(효과가 돌려준 .el)를 그 자리에서 잰다. 요소 여럿이면 그 전부를 덮는 한 칸.
252
+ * pad 는 글 둘레 여백(배지는 0 — 배지 테두리에 딱 맞는다)
253
+ */
254
+ export interface ReadStop {
255
+ at: number;
256
+ x?: number;
257
+ y?: number;
258
+ w?: number;
259
+ h?: number;
260
+ target?: HTMLElement | null | (HTMLElement | null)[];
261
+ pad?: number;
262
+ }
263
+ type Box = Pt & { w: number; h: number };
264
+
265
+ // SlidingPillNav 처럼 offsetTop·offsetLeft 를 무대까지 더해 잰다 — 나타남의 6px 올라옴(transform)은 자리에 넣지 않는다
266
+ function measure(target: HTMLElement, stage: HTMLElement): { l: number; t: number; r: number; b: number } | null {
267
+ let x = 0;
268
+ let y = 0;
269
+ let n: HTMLElement | null = target;
270
+ for (; n && n !== stage; n = n.offsetParent as HTMLElement | null) {
271
+ x += n.offsetLeft;
272
+ y += n.offsetTop;
273
+ }
274
+ if (n !== stage || !target.offsetWidth) return null;
275
+ return { l: x, t: y, r: x + target.offsetWidth, b: y + target.offsetHeight };
276
+ }
277
+
278
+ function measureAll(targets: (HTMLElement | null)[], stage: HTMLElement, pad: number): Box | null {
279
+ const rs = targets.map((el) => el && measure(el, stage)).filter((r) => r !== null);
280
+ if (!rs.length) return null;
281
+ const l = Math.min(...rs.map((r) => r.l)) - pad;
282
+ const t = Math.min(...rs.map((r) => r.t)) - pad;
283
+ const r = Math.max(...rs.map((q) => q.r)) + pad;
284
+ const b = Math.max(...rs.map((q) => q.b)) + pad;
285
+ return { x: (l + r) / 2, y: (t + b) / 2, w: r - l, h: b - t };
286
+ }
287
+
288
+ // 짚은 값 위에 서 있는 최소 시간 — 다음 값이 박에 바로 와도 칸이 한 번은 그 값에 딱 맞게 앉는다
289
+ const REST = 0.03;
290
+ const smooth = (p: number) => p * p * (3 - 2 * p);
291
+
292
+ // 옮겨 가는 동안의 칸 — 떠나는 값에서 아래 테두리 한 줄로 내려앉고, 그 한 줄이 다음 값 아래로 미끄러진 뒤, 그 값의 칸만큼 다시 차오른다.
293
+ // 칸 크기 그대로 날아가면 지나는 길의 글을 통째로 덮어 고장처럼 보였다. a 가 없으면(첫 값) 그 자리에서 차오르기만.
294
+ // 미끄러지는 몫은 거리만큼 — 600px 넘게 가면 옮김의 40%, 같은 칸을 다시 짚으면 0(한 줄로 서 있는 동안 화면이 멈춘다)
295
+ function between(a: Box | null, b: Box, u: number, line: number): Box {
296
+ const from = a ?? b;
297
+ const sink = (1 - 0.4 * clamp(Math.hypot(b.x - from.x, b.y + b.h / 2 - from.y - from.h / 2) / 600)) / 2;
298
+ const go = a ? (sink < 0.5 ? smooth(clamp((u - sink) / (1 - 2 * sink))) : u < 0.5 ? 0 : 1) : 1;
299
+ const full = a ? (u < 0.5 ? 1 - clamp(u / sink) : clamp((u - 1 + sink) / sink)) : clamp(u);
300
+ const hBase = a && u < 0.5 ? a.h : b.h;
301
+ const h = line + (hBase - line) * full;
302
+ const bottom = from.y + from.h / 2 + (b.y + b.h / 2 - from.y - from.h / 2) * go;
303
+ return { x: from.x + (b.x - from.x) * go, y: bottom - h / 2, w: from.w + (b.w - from.w) * go, h };
304
+ }
305
+
306
+ // 바탕보다 밝은 글은 「밝게 섞기」(어두운 바탕), 어두운 글은 「어둡게 섞기」(밝은 바탕)로 그대로 남는다 — 위에 그려도 글 뒤에 깔린 것처럼 보인다.
307
+ // 응답 패널·로그의 면 안쪽 값도 짚어야 해서 쌓는 차례로는 글 뒤에 둘 수 없다
308
+ function underText(el: HTMLElement): string {
309
+ const m = /#([0-9a-f]{6})/i.exec(getComputedStyle(el).getPropertyValue("--kf-bg"));
310
+ const [r, g, b] = m ? [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16)) : [0, 0, 0];
311
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 128 ? "screen" : "multiply";
312
+ }
313
+
314
+ /**
315
+ * 뽑힌 숫자·읽는 칸 — 고름 칠(파랑 /20, 정본의 bg-primary/20) 알약에 스타일가이드의 미끄러지는 알약 테두리(SlidingPillNav, 화면 글 크기에 맞춰 파랑 /55)를 두른 한 장.
316
+ * from·to 를 주면 로그 줄의 숫자가 알약째 떠올라 호를 그리며 점 그림 자리로 300ms 감속으로 날아가 꽂힌다(박마다 큰 움직임).
317
+ * stops 를 주면 요점(주인공 값·핵심 사실·그 순간 자막이 말하는 값)이 바뀔 때만 그 값으로 옮겨 가 짚는다 — 메뉴의 알약처럼 글 뒤에 깔리고,
318
+ * 옮겨 가는 동안은 아래 테두리 한 줄로 얇아져 지나가며, 닿으면 그 값의 칸만큼 차올라 다음 요점까지 딱 맞게 선다(옮김 300ms 안).
319
+ * 나타나는 항목을 차례로 훑는 정지점은 두지 않는다 — 화면을 살려 두는 몫은 내용이 진다
320
+ */
236
321
  export function pickFly(
237
322
  el: HTMLElement,
238
- o: { items: { at: number; text: string; from: { x: number; y: number }; to: { x: number; y: number } }[]; dur?: number; size?: number },
323
+ o: {
324
+ items: ({ at: number; text: string; from: Pt; to: Pt } | { text?: string; stops: ReadStop[] })[];
325
+ dur?: number;
326
+ size?: number;
327
+ },
239
328
  ): (t: number) => void {
240
329
  const doc = el.ownerDocument;
241
- const dur = o.dur ?? 0.3;
330
+ const dur = o.dur ?? SLOW;
242
331
  const size = o.size ?? 44;
243
- const spans = o.items.map((it) => {
244
- const s = box(doc, `position:absolute;left:0;top:0;font-family:${MONO};font-size:${size}px;font-weight:800;color:var(--kf-accent-text);background:rgba(0,111,214,.14);border-radius:8px;padding:0 8px;white-space:nowrap;opacity:0;z-index:7;font-variant-numeric:tabular-nums`, it.text);
332
+ const border = Math.max(3, Math.round(size * 0.08));
333
+ let blend = "";
334
+ const pills = o.items.map((it) => {
335
+ const s = box(doc, `position:absolute;left:0;top:0;box-sizing:border-box;display:flex;align-items:center;justify-content:center;font-family:${MONO};font-size:${size}px;font-weight:600;color:var(--kf-accent-text);background:color-mix(in srgb, var(--kf-accent) 20%, transparent);border:${border}px solid color-mix(in srgb, var(--kf-accent) 55%, transparent);border-radius:var(--kf-radius-pill);padding:0 0.3em;white-space:nowrap;opacity:0;z-index:7;pointer-events:none;font-variant-numeric:tabular-nums`, it.text ?? "");
245
336
  el.append(s);
246
337
  return s;
247
338
  });
339
+ const place = (s: HTMLElement, p: Box, opacity: number) => {
340
+ s.style.opacity = String(opacity);
341
+ s.style.width = `${p.w.toFixed(1)}px`;
342
+ s.style.height = `${p.h.toFixed(1)}px`;
343
+ s.style.transform = `translate(${(p.x - p.w / 2).toFixed(1)}px, ${(p.y - p.h / 2).toFixed(1)}px)`;
344
+ };
248
345
  return (t: number) => {
249
346
  o.items.forEach((it, i) => {
250
- const s = spans[i];
251
- const u = (t - it.at) / dur;
252
- if (u < 0 || u > 1) {
347
+ const s = pills[i];
348
+ if (!("stops" in it)) {
349
+ const u = (t - it.at) / dur;
350
+ cue("land", t - it.at - dur, { source: `pickFly@${it.at.toFixed(3)}` });
351
+ if (u < 0 || u > 1) {
352
+ s.style.opacity = "0";
353
+ return;
354
+ }
355
+ const p = out(u);
356
+ s.style.width = s.style.height = "";
357
+ s.style.opacity = String(1 - clamp((u - 0.85) / 0.15));
358
+ s.style.transform = `translate(${it.from.x + (it.to.x - it.from.x) * p}px, ${it.from.y + (it.to.y - it.from.y) * p - Math.sin(Math.PI * p) * 180}px) translate(-50%, -50%)`;
359
+ return;
360
+ }
361
+ blend ||= underText(el);
362
+ s.style.mixBlendMode = blend;
363
+ const stops = [...it.stops].sort((a, b) => a.at - b.at);
364
+ if (!stops.length || t < stops[0].at) {
253
365
  s.style.opacity = "0";
254
366
  return;
255
367
  }
256
- const p = ease.inOutCubic(u);
257
- const x = it.from.x + (it.to.x - it.from.x) * p;
258
- const y = it.from.y + (it.to.y - it.from.y) * p - Math.sin(Math.PI * p) * 180;
259
- s.style.opacity = String(1 - clamp((u - 0.85) / 0.15));
260
- s.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%) scale(${1 + 0.5 * Math.sin(Math.PI * p)})`;
368
+ const boxOf = (st: ReadStop): Box => (st.target && measureAll([st.target].flat(), el, st.pad ?? 0)) || { x: st.x ?? 0, y: st.y ?? 0, w: st.w ?? size * 3, h: st.h ?? size * 1.6 };
369
+ let k = 0;
370
+ while (k + 1 < stops.length && stops[k + 1].at <= t) k++;
371
+ // 값이 나타나는 순간 떠나 300ms 안에 닿는다 — 다음 값이 가까우면 그 앞에 앉도록 더 빨리
372
+ const next = stops[k + 1];
373
+ const move = Math.max(0.1, Math.min(SLOW, (next ? next.at - stops[k].at : Infinity) - REST));
374
+ const u = clamp((t - stops[k].at) / move);
375
+ if (k > 0) cue("select", t - stops[k].at, { source: `pickFly:read@${stops[k].at.toFixed(3)}` });
376
+ const line = border * 2;
377
+ const p = between(k ? boxOf(stops[k - 1]) : null, boxOf(stops[k]), u, line);
378
+ // 얇을수록 진한 파랑 — 탭 아래 줄처럼 얇아도 지나가는 게 보이게. 글 한 줄 반 높이를 넘으면 옅은 칠로(큰 숫자 아래 두꺼운 파란 띠가 되지 않게)
379
+ s.style.background = `color-mix(in srgb, var(--kf-accent) ${Math.round(20 + 80 * clamp(1 - (p.h - line) / (size * 1.5)))}%, transparent)`;
380
+ place(s, p, out(progress(t, stops[0].at, BASE)));
261
381
  });
262
382
  };
263
383
  }
264
384
 
265
- /** 샘플 점 그림 — 모델마다 재 온 값이 들어온 시각에 점으로 꽂히고, 지금까지의 가운데값 눈금·숫자가 따라 미끄러진다 */
385
+ /** 샘플 점 그림 — 모델마다 재 온 값이 들어온 시각에 점으로 꽂히고, 지금까지의 가운데값 눈금·숫자가 300ms 로 따라 미끄러진다. 눈금·막대는 각짐 */
266
386
  export function samplePlot(
267
387
  el: HTMLElement,
268
388
  o: { x: number; y: number; w: number; rows: { label: string; samples: { t: number; v: number }[] }[]; min: number; max: number; unit?: string; decimals?: number; rowH?: number; at?: number; ticks?: number[]; best?: number; size?: number },
@@ -282,10 +402,10 @@ export function samplePlot(
282
402
  const rows = o.rows.map((r, i) => {
283
403
  const top = i * rowH;
284
404
  const label = box(doc, `position:absolute;left:0;top:${top}px;font-size:${size}px;color:var(--kf-ink);white-space:nowrap`, r.label);
285
- const track = box(doc, `position:absolute;left:0;right:0;top:${top + trackY}px;height:10px;border-radius:5px;background:${BAR}`);
405
+ const track = box(doc, `position:absolute;left:0;right:0;top:${top + trackY}px;height:10px;background:${BAR}`);
286
406
  const dots = r.samples.map(() => box(doc, `position:absolute;top:${top + trackY - 19}px;width:48px;height:48px;margin-left:-24px;border-radius:50%;background:var(--kf-accent);opacity:0`));
287
- const mark = box(doc, `position:absolute;top:${top + trackY - 40}px;width:8px;height:90px;margin-left:-4px;border-radius:4px;background:var(--kf-ink);opacity:0`);
288
- const val = box(doc, `position:absolute;top:${top + trackY + 30}px;right:0;font-size:${size * 1.35}px;font-weight:700;color:var(--kf-ink);opacity:0;transform-origin:100% 50%`);
407
+ const mark = box(doc, `position:absolute;top:${top + trackY - 40}px;width:8px;height:90px;margin-left:-4px;background:var(--kf-ink);opacity:0`);
408
+ const val = box(doc, `position:absolute;top:${top + trackY + 30}px;right:0;font-size:${size * 1.35}px;font-weight:600;color:var(--kf-ink);opacity:0`);
289
409
  wrap.append(label, track, ...dots, mark, val);
290
410
  return { r, dots, mark, val };
291
411
  });
@@ -295,32 +415,29 @@ export function samplePlot(
295
415
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
296
416
  };
297
417
  return (t: number) => {
298
- applyStyle(wrap, rise(t, o.at ?? 0, 0.22, 40));
418
+ applyStyle(wrap, rise(t, o.at ?? 0));
299
419
  rows.forEach(({ r, dots, mark, val }, i) => {
300
420
  const got = r.samples.filter((s) => t >= s.t);
301
421
  r.samples.forEach((s, k) => {
302
- const p = snappy(progress(t, s.t, 0.2));
303
422
  dots[k].style.left = `${X(s.v)}px`;
304
- dots[k].style.opacity = String(0.6 * p);
305
- dots[k].style.transform = `scale(${t >= s.t ? 1.8 - 0.8 * p : 0})`;
423
+ applyStyle(dots[k], t >= s.t ? { ...rise(t, s.t, BASE, RISE, "tick"), opacity: 0.6 * out(progress(t, s.t, BASE)) } : { opacity: 0 });
306
424
  });
307
425
  if (!got.length) {
308
426
  mark.style.opacity = "0";
309
427
  val.style.opacity = "0";
310
428
  return;
311
429
  }
312
- // 가운데값은 새 점이 올 때마다 그 자리로 미끄러진다 — 앞 값과 새 값을 220ms 로 잇는다
430
+ // 가운데값은 새 점이 올 때마다 그 자리로 미끄러진다 — 앞 값과 새 값을 300ms(자리 옮김)로 잇는다
313
431
  const last = got[got.length - 1];
314
432
  const prev = got.length > 1 ? median(got.slice(0, -1).map((s) => s.v)) : last.v;
315
433
  const now = median(got.map((s) => s.v));
316
- const k = snappy(progress(t, last.t, 0.22));
434
+ const k = out(progress(t, last.t, SLOW));
317
435
  const m = prev + (now - prev) * k;
318
436
  mark.style.left = `${X(m)}px`;
319
437
  mark.style.opacity = "1";
320
438
  mark.style.background = o.best === i ? "var(--kf-accent)" : "var(--kf-ink)";
321
- val.style.opacity = "1";
439
+ applyStyle(val, rise(t, got[0].t));
322
440
  val.style.color = o.best === i ? "var(--kf-accent-text)" : "var(--kf-ink)";
323
- val.style.transform = `scale(${1 + 0.15 * (1 - k)})`;
324
441
  val.textContent = `${m.toFixed(o.decimals ?? 1)}${o.unit ?? ""}`;
325
442
  });
326
443
  };
@@ -333,24 +450,25 @@ export function meter(t: number, o: { to: number; at: number; dur?: number; deci
333
450
  const n = target.replace(/\D/g, "").length || 1;
334
451
  const dur = o.dur ?? 0.9;
335
452
  let k = 0;
336
- let out = "";
453
+ let s = "";
337
454
  for (const ch of target) {
338
455
  if (!/\d/.test(ch)) {
339
- out += ch;
456
+ s += ch;
340
457
  continue;
341
458
  }
342
459
  const settle = o.at + (dur * (k + 1)) / n;
343
- out += t >= settle ? ch : String(Math.floor((t * 26 + k * 3.7) % 10 + 10) % 10);
460
+ cue("tick", t - settle, { source: `meter@${settle.toFixed(3)}`, minor: true });
461
+ s += t >= settle ? ch : String(Math.floor((t * 26 + k * 3.7) % 10 + 10) % 10);
344
462
  k++;
345
463
  }
346
- return `${out}${o.unit ?? ""}`;
464
+ return `${s}${o.unit ?? ""}`;
347
465
  }
348
466
 
349
- /** 막대 — 굵은 막대가 박마다 하나씩 값만큼 자란다. best 하나만 OG 파랑, 나머지는 회색. 값은 막대 끝, delta 를 주면 그 박에 차이 칩 */
467
+ /** 막대 — 각진 막대가 박마다 하나씩 값만큼 300ms 로 자란다. best 만 브랜드 파랑(차트 칠), 나머지는 회색. 값은 막대 끝, delta 를 주면 그 박에 테두리 배지 */
350
468
  export function rankBars(
351
469
  el: HTMLElement,
352
470
  o: { x: number; y: number; w: number; items: { label: string; value: number; at: number; delta?: string; deltaAt?: number }[]; max: number; best?: number | number[]; prefix?: string; unit?: string; decimals?: number; barH?: number; gap?: number; size?: number },
353
- ): (t: number) => void {
471
+ ): ((t: number) => void) & { vals: HTMLElement[]; deltas: (HTMLElement | null)[] } {
354
472
  const doc = el.ownerDocument;
355
473
  const barH = o.barH ?? 76;
356
474
  const size = o.size ?? 38;
@@ -363,16 +481,16 @@ export function rankBars(
363
481
  const top = i * step;
364
482
  const label = box(doc, `position:absolute;left:0;top:${top}px;font-size:${size}px;color:var(--kf-ink-soft);white-space:nowrap`, it.label);
365
483
  const bar = box(doc, `position:absolute;left:0;top:${top + size * 1.4}px;height:${barH}px;background:${bests.includes(i) ? "var(--kf-accent)" : BAR};transform-origin:0 50%`);
366
- const val = box(doc, `position:absolute;top:${top + size * 1.4}px;line-height:${barH}px;font-size:${size * 1.2}px;font-weight:700;white-space:nowrap;color:${bests.includes(i) ? "var(--kf-accent-text)" : "var(--kf-ink)"}`);
484
+ const val = box(doc, `position:absolute;top:${top + size * 1.4}px;line-height:${barH}px;font-size:${size * 1.2}px;font-weight:600;white-space:nowrap;color:${bests.includes(i) ? "var(--kf-accent-text)" : "var(--kf-ink)"}`);
367
485
  const delta = it.delta
368
- ? box(doc, `position:absolute;top:${top + size * 1.4 + barH / 2}px;padding:4px 16px;border-radius:999px;background:var(--kf-raised);color:var(--kf-ink);font-size:${size}px;font-weight:700;white-space:nowrap;transform-origin:0 50%`, it.delta)
486
+ ? box(doc, `position:absolute;top:${top + size * 1.4 + barH / 2}px;padding:2px 16px;font-size:${size}px;font-weight:600;white-space:nowrap;${badgeCss("plain", size)}`, it.delta)
369
487
  : null;
370
488
  wrap.append(label, bar, val, ...(delta ? [delta] : []));
371
489
  return { it, label, bar, val, delta };
372
490
  });
373
- return (t: number) => {
491
+ const paint = (t: number) => {
374
492
  rows.forEach(({ it, label, bar, val, delta }) => {
375
- const p = snappy(progress(t, it.at, 0.3));
493
+ const p = out(progress(t, it.at, SLOW));
376
494
  const w = (it.value / o.max) * (o.w - 440);
377
495
  applyStyle(label, rise(t, it.at));
378
496
  bar.style.width = `${w}px`;
@@ -382,51 +500,54 @@ export function rankBars(
382
500
  val.textContent = `${o.prefix ?? ""}${it.value.toFixed(o.decimals ?? 1)}${o.unit ?? ""}`;
383
501
  if (delta) {
384
502
  const at = it.deltaAt ?? it.at + 0.35;
385
- const d = snappy(progress(t, at, 0.22));
386
503
  delta.style.left = `${w + 24 + size * 6}px`;
387
- delta.style.opacity = String(d);
388
- delta.style.transform = `translateY(-50%) scale(${t >= at ? 1.3 - 0.3 * d : 0})`;
504
+ applyStyle(delta, t >= at ? rise(t, at) : { opacity: 0 });
505
+ delta.style.translate = "0 -50%";
389
506
  }
390
507
  });
391
508
  };
509
+ return Object.assign(paint, { vals: rows.map((r) => r.val), deltas: rows.map((r) => r.delta) });
392
510
  }
393
511
 
394
- /** 끝 도장 — 「측정 날짜 · 호출 N번 · $비용 · 키 하나」 한 줄과 OG 로고가 쾅 찍힌다. 비용은 반올림해 0 으로 만들지 않고 첫 유효 자리까지 */
395
- export function stamp(el: HTMLElement, o: { x: number; y: number; at: number; date: string; calls: number; cost: number; logo?: string; size?: number }): (t: number) => void {
512
+ /**
513
+ * 끝 도장 — 구분선 한 줄 위에 OG 로고와 「측정 날짜 · 호출 N번 · $비용 · 키 하나」가 선다(상자 없이). 비용은 반올림해 0 으로 만들지 않고 첫 유효 자리까지.
514
+ * logo 는 마스터 로고 SVG 원문(currentColor) — 한 색으로 칠한다. 어두운 바탕 흰색·밝은 바탕 검정(logoInk)
515
+ */
516
+ export function stamp(el: HTMLElement, o: { x: number; y: number; at: number; date: string; calls: number; cost: number; logo?: string; logoInk?: string; size?: number }): (t: number) => void {
396
517
  const doc = el.ownerDocument;
397
518
  const size = o.size ?? 36;
398
- const card = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;display:flex;align-items:center;gap:${size}px;padding:${size * 0.6}px ${size}px;border-radius:12px;background:var(--kf-raised);font-family:${MONO};font-size:${size}px;color:var(--kf-ink);white-space:nowrap;font-variant-numeric:tabular-nums`);
519
+ const row = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;display:flex;align-items:center;gap:${size}px;padding-top:${size * 0.6}px;border-top:${edge(size)}px solid var(--kf-line);font-family:${MONO};font-size:${size}px;color:var(--kf-ink);white-space:nowrap;font-variant-numeric:tabular-nums`);
399
520
  if (o.logo) {
400
- const img = doc.createElement("img");
401
- img.src = o.logo;
402
- img.style.cssText = `height:${size * 1.25}px;width:auto;display:block`;
403
- card.append(img);
521
+ const mark = box(doc, `height:${size * 1.25}px;width:${(size * 1.25 * 1140) / 650}px;color:${o.logoInk ?? "var(--kf-ink)"};display:flex`);
522
+ mark.innerHTML = fitSvg(o.logo);
523
+ row.append(mark);
404
524
  }
405
525
  const digits = o.cost > 0 ? Math.max(2, -Math.floor(Math.log10(o.cost))) : 2;
406
- card.append(box(doc, "", `측정 ${o.date} · 호출 ${o.calls}번 · $${o.cost.toFixed(digits)} · 키 하나`));
407
- el.append(card);
526
+ row.append(box(doc, "", `측정 ${o.date} · 호출 ${o.calls}번 · $${o.cost.toFixed(digits)} · 키 하나`));
527
+ el.append(row);
408
528
  return (t: number) => {
409
- const p = snappy(progress(t, o.at, 0.22));
410
- card.style.opacity = String(p);
411
- card.style.transform = `translateX(-50%) translateY(${20 * (1 - p)}px) scale(${1.25 - 0.25 * p})`;
529
+ applyStyle(row, rise(t, o.at, BASE, RISE, "end"));
530
+ row.style.translate = "-50% 0";
412
531
  };
413
532
  }
414
533
 
415
- /** 칩 — 짧은 낱말·모델 ID·잰 값 하나(「실측」·「openai/ 41」). 220ms 로 크게 들어오고 until 에 오그라들며 빠진다 */
416
- export function flag(el: HTMLElement, o: { x: number; y: number; text: string; at: number; until?: number; tone?: "accent" | "soft" | "plain"; size?: number; mono?: boolean }): (t: number) => void {
534
+ /**
535
+ * 배지 — 짧은 낱말·모델 ID·잰 값 하나(「실측」·「openai/ 41」). 속을 칠하지 않고 흐린 테두리와 글자색만(badge.tsx 의 default·outline·secondary).
536
+ * 6px 올라오며 220ms 로 나타나고 until 에 150ms 로 빠진다
537
+ */
538
+ export function flag(el: HTMLElement, o: { x: number; y: number; text: string; at: number; until?: number; tone?: "accent" | "soft" | "plain"; size?: number; mono?: boolean }): ((t: number) => void) & { el: HTMLElement } {
417
539
  const doc = el.ownerDocument;
418
- const soft = o.tone === "soft";
540
+ const size = o.size ?? 38;
419
541
  const chip = box(
420
542
  doc,
421
- `position:absolute;left:${o.x}px;top:${o.y}px;padding:6px 20px;border-radius:999px;font-family:${o.mono ? MONO : "var(--kf-font-sans)"};font-size:${o.size ?? 38}px;font-weight:700;white-space:nowrap;transform-origin:0 50%;font-variant-numeric:tabular-nums;` +
422
- (o.tone === "plain" ? "background:var(--kf-surface);color:var(--kf-ink)" : soft ? "background:var(--kf-tint);color:var(--kf-accent-text)" : "background:var(--kf-accent);color:var(--kf-on-accent)"),
543
+ `position:absolute;left:${o.x}px;top:${o.y}px;padding:4px 0.5em;font-family:${o.mono ? MONO : "var(--kf-font-sans)"};font-size:${size}px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums;${badgeCss(o.tone ?? "accent", size)}`,
423
544
  o.text,
424
545
  );
425
546
  el.append(chip);
426
- return (t: number) => {
427
- const p = snappy(progress(t, o.at, 0.22));
428
- const gone = o.until === undefined ? 0 : snappy(progress(t, o.until, 0.12));
429
- chip.style.opacity = String(p * (1 - gone));
430
- chip.style.transform = `scale(${t >= o.at ? (1.4 - 0.4 * p) * (1 - 0.6 * gone) : 0.6})`;
547
+ const paint = (t: number) => {
548
+ const gone = o.until === undefined ? 0 : out(progress(t, o.until, FAST));
549
+ const s = rise(t, o.at);
550
+ applyStyle(chip, t >= o.at ? { ...s, opacity: (s.opacity ?? 1) * (1 - gone) } : { opacity: 0 });
431
551
  };
552
+ return Object.assign(paint, { el: chip });
432
553
  }