kasaframe 0.1.1 → 0.1.3

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 (200) hide show
  1. package/NOTICE +10 -8
  2. package/README.md +16 -1
  3. package/app/server/engine.ts +133 -15
  4. package/app/server/main.ts +7 -3
  5. package/app/web/main.ts +218 -36
  6. package/app/web/style.css +37 -4
  7. package/bin/hooks.mjs +14 -1
  8. package/bin/kf.mjs +20 -2
  9. package/docs/AGENTS.md +64 -16
  10. package/music/README.md +31 -7
  11. package/package.json +2 -2
  12. package/packages/adapters/src/project.ts +1 -1
  13. package/packages/check/src/check.ts +10 -3
  14. package/packages/check/src/rules/assets.ts +4 -1
  15. package/packages/check/src/rules/frames.ts +2 -1
  16. package/packages/check/src/rules/layout.ts +30 -6
  17. package/packages/cli/package.json +2 -1
  18. package/packages/cli/src/blur.ts +15 -3
  19. package/packages/cli/src/commands/bench.ts +123 -0
  20. package/packages/cli/src/commands/blender.ts +72 -49
  21. package/packages/cli/src/commands/direct.ts +210 -0
  22. package/packages/cli/src/commands/doctor.ts +9 -1
  23. package/packages/cli/src/commands/look.ts +9 -2
  24. package/packages/cli/src/commands/lyrics.ts +47 -1
  25. package/packages/cli/src/commands/make.ts +729 -150
  26. package/packages/cli/src/commands/music.ts +78 -0
  27. package/packages/cli/src/commands/narrate.ts +238 -0
  28. package/packages/cli/src/commands/new.ts +14 -5
  29. package/packages/cli/src/commands/render.ts +94 -60
  30. package/packages/cli/src/commands/series.ts +110 -0
  31. package/packages/cli/src/commands/setup.ts +42 -3
  32. package/packages/cli/src/commands/theme.ts +89 -2
  33. package/packages/cli/src/deps.ts +6 -6
  34. package/packages/cli/src/kf.ts +14 -3
  35. package/packages/cli/src/mix.ts +131 -0
  36. package/packages/cli/src/pool.ts +34 -0
  37. package/packages/cli/src/segments.ts +402 -0
  38. package/packages/core/src/index.ts +1 -1
  39. package/packages/core/src/project.ts +11 -0
  40. package/packages/director/package.json +23 -0
  41. package/packages/director/src/assemble.ts +361 -0
  42. package/packages/director/src/brief.ts +122 -0
  43. package/packages/director/src/catalog.ts +49 -0
  44. package/packages/director/src/content.ts +343 -0
  45. package/packages/director/src/fallback.ts +58 -0
  46. package/packages/director/src/index.ts +13 -0
  47. package/packages/director/src/pattern.ts +90 -0
  48. package/packages/director/src/patterns/fill/cells.ts +115 -0
  49. package/packages/director/src/patterns/formula/stack.ts +93 -0
  50. package/packages/director/src/patterns/util.ts +58 -0
  51. package/packages/director/src/scene.ts +62 -0
  52. package/packages/director/src/select.ts +163 -0
  53. package/packages/director/src/transitions.ts +115 -0
  54. package/packages/gen/PROVIDERS.md +39 -4
  55. package/packages/gen/src/ask.ts +132 -0
  56. package/packages/gen/src/audio.ts +91 -0
  57. package/packages/gen/src/blender.ts +54 -0
  58. package/packages/gen/src/capabilities.ts +151 -0
  59. package/packages/gen/src/generate.ts +4 -2
  60. package/packages/gen/src/image/codex.ts +117 -0
  61. package/packages/gen/src/index.ts +10 -2
  62. package/packages/gen/src/keys.ts +26 -0
  63. package/packages/gen/src/music/elevenlabs.ts +41 -0
  64. package/packages/gen/src/music/index.ts +120 -0
  65. package/packages/gen/src/music/library.ts +73 -0
  66. package/packages/gen/src/music/lyria.ts +52 -0
  67. package/packages/gen/src/music/types.ts +28 -0
  68. package/packages/gen/src/registry.ts +6 -3
  69. package/packages/gen/src/tts/dashscope.ts +64 -0
  70. package/packages/gen/src/tts/elevenlabs.ts +84 -0
  71. package/packages/gen/src/tts/gemini.ts +73 -0
  72. package/packages/gen/src/tts/index.ts +241 -0
  73. package/packages/gen/src/tts/local.ts +42 -0
  74. package/packages/gen/src/tts/types.ts +44 -0
  75. package/packages/gen/src/types.ts +4 -0
  76. package/packages/gen/src/video/h3.ts +5 -0
  77. package/packages/gen/src/video/http.ts +1 -1
  78. package/packages/jizura/src/index.ts +2 -2
  79. package/packages/jizura/src/install.ts +198 -155
  80. package/packages/jizura/src/layer.ts +75 -32
  81. package/packages/jizura/src/pin.ts +4 -1
  82. package/packages/jizura/src/vendor.ts +140 -0
  83. package/packages/jizura/src/vite.ts +34 -7
  84. package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
  85. package/packages/jizura/vendor/NOTICE +43 -0
  86. package/packages/jizura/vendor/engine.json +47 -0
  87. package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
  88. package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
  89. package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
  90. package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
  91. package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  92. package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
  93. package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
  94. package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
  95. package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
  96. package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
  97. package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
  98. package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
  99. package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
  100. package/packages/jizura/vendor/fonts/fonts.json +52 -0
  101. package/packages/jizura/vendor/jizura_engine.js +35024 -0
  102. package/packages/motion/README.md +11 -0
  103. package/packages/motion/package.json +1 -0
  104. package/packages/motion/src/captions.ts +32 -24
  105. package/packages/motion/src/moves/brushWipe.ts +14 -1
  106. package/packages/motion/src/moves/dress.ts +191 -0
  107. package/packages/motion/src/moves/echoStack.ts +3 -3
  108. package/packages/motion/src/moves/fit.ts +36 -0
  109. package/packages/motion/src/moves/ghostStack.ts +14 -5
  110. package/packages/motion/src/moves/imageSwarm.ts +8 -4
  111. package/packages/motion/src/moves/index.ts +23 -4
  112. package/packages/motion/src/moves/karaokeLine.ts +17 -3
  113. package/packages/motion/src/moves/menu.ts +138 -0
  114. package/packages/motion/src/moves/neonSign.ts +2 -1
  115. package/packages/motion/src/moves/slamWords.ts +14 -7
  116. package/packages/motion/src/moves/sparkTrail.ts +16 -0
  117. package/packages/motion/src/moves/splitFlap.ts +4 -2
  118. package/packages/motion/src/moves/stepWords.ts +19 -8
  119. package/packages/motion/src/moves/tickerRows.ts +13 -9
  120. package/packages/motion/src/moves/tokenPicker.ts +4 -3
  121. package/packages/motion/src/moves/wordWall.ts +23 -21
  122. package/packages/motion/src/moves/worldRoll.ts +2 -1
  123. package/packages/motion/src/transition3d/index.ts +10 -3
  124. package/packages/motion/src/transition3d/rig.ts +107 -9
  125. package/packages/motion/src/transition3d/shatter.ts +5 -2
  126. package/packages/motion/src/transition3d/space.ts +173 -0
  127. package/packages/motion/src/transition3d/whip.ts +19 -6
  128. package/packages/motion/src/transition3d/world.ts +54 -0
  129. package/packages/motion/src/transitions2d/catalog.ts +1 -1
  130. package/packages/patterns/package.json +15 -0
  131. package/packages/patterns/src/bars/rank.ts +119 -0
  132. package/packages/patterns/src/before-after/shrink.ts +127 -0
  133. package/packages/patterns/src/before-after/strike.ts +125 -0
  134. package/packages/patterns/src/code/terminal.ts +133 -0
  135. package/packages/patterns/src/compare/bars.ts +142 -0
  136. package/packages/patterns/src/compare/duel.ts +103 -0
  137. package/packages/patterns/src/end/lockup.ts +85 -0
  138. package/packages/patterns/src/end/slam.ts +91 -0
  139. package/packages/patterns/src/flow/graph.ts +250 -0
  140. package/packages/patterns/src/hook-number/counter.ts +101 -0
  141. package/packages/patterns/src/hook-number/roll.ts +179 -0
  142. package/packages/patterns/src/hook-number/slam.ts +91 -0
  143. package/packages/patterns/src/index.ts +38 -0
  144. package/packages/patterns/src/kit.ts +449 -0
  145. package/packages/patterns/src/limit/gauge.ts +117 -0
  146. package/packages/patterns/src/list/cards.ts +131 -0
  147. package/packages/patterns/src/list/cycle.ts +22 -0
  148. package/packages/patterns/src/media/panels.ts +100 -0
  149. package/packages/patterns/src/objects.ts +234 -0
  150. package/packages/patterns/src/quote/karaoke.ts +126 -0
  151. package/packages/patterns/src/quote/slam.ts +22 -0
  152. package/packages/patterns/src/screen/focus.ts +85 -0
  153. package/packages/patterns/src/steps/path.ts +137 -0
  154. package/packages/patterns/src/steps/stack.ts +111 -0
  155. package/packages/patterns/src/term-reveal/chips.ts +92 -0
  156. package/packages/patterns/src/term-reveal/expand.ts +127 -0
  157. package/packages/patterns/src/timeline/axis.ts +144 -0
  158. package/packages/patterns/src/title/mask.ts +85 -0
  159. package/packages/patterns/src/title/slam.ts +23 -0
  160. package/packages/patterns/src/toggle-sim/switch.ts +92 -0
  161. package/packages/patterns/src/trend/columns.ts +108 -0
  162. package/packages/patterns/src/trend/draw.ts +140 -0
  163. package/packages/patterns/src/types.ts +67 -0
  164. package/packages/patterns/src/words/moves.ts +42 -0
  165. package/packages/patterns/src/words/slam.ts +4 -0
  166. package/packages/plan/src/asks.ts +256 -0
  167. package/packages/plan/src/brief.ts +136 -7
  168. package/packages/plan/src/captions.ts +5 -2
  169. package/packages/plan/src/index.ts +6 -2
  170. package/packages/plan/src/llm.ts +64 -2
  171. package/packages/plan/src/look.ts +3 -1
  172. package/packages/plan/src/plan.ts +67 -13
  173. package/packages/plan/src/series.ts +76 -0
  174. package/packages/plan/src/types.ts +53 -2
  175. package/packages/themes/package.json +5 -2
  176. package/packages/themes/src/index.ts +9 -0
  177. package/packages/themes/src/slotkit.ts +79 -0
  178. package/packages/themes/src/slots.ts +46 -0
  179. package/packages/themes/src/stage3d.ts +20 -0
  180. package/series/og-shorts.json +14 -0
  181. package/skills/kasaframe/SKILL.md +4 -3
  182. package/templates/_shared/project/lib/stage.ts +70 -50
  183. package/templates/_shared/scenes/default.ts +10 -7
  184. package/templates/_shared/scenes/drop.ts +11 -8
  185. package/templates/_shared/scenes/end.ts +9 -6
  186. package/templates/_shared/scenes/hook.ts +8 -6
  187. package/templates/_shared/scenes/step.ts +13 -4
  188. package/templates/ad/template.json +20 -1
  189. package/templates/demo/template.json +11 -0
  190. package/templates/explainer/template.json +54 -3
  191. package/templates/launch/template.json +34 -1
  192. package/templates/mv/scenes/default.ts +11 -8
  193. package/templates/mv/template.json +4 -2
  194. package/templates/promo/template.json +42 -1
  195. package/templates/shorts/scenes/hook.ts +8 -5
  196. package/templates/shorts/template.json +72 -3
  197. package/templates/square/template.json +21 -1
  198. package/themes/debug-hud/slots.ts +99 -0
  199. package/themes/og-measure/slots.ts +88 -0
  200. package/themes/riso-zine/slots.ts +81 -0
@@ -0,0 +1,179 @@
1
+ import { applyStyle, clamp, combine, ease, progress, spring } from "@kasaframe/core";
2
+ import { sweep } from "@kasaframe/motion";
3
+ import { textEm } from "@kasaframe/motion/moves";
4
+ import { ACCENT_TEXT, area, decimalsOf, factRoll, fitOnce, node, odometer, slotsOf, moving, timesSign } from "../kit.ts";
5
+ import type { Pattern } from "../types.ts";
6
+
7
+ export interface HookNumber {
8
+ value: number;
9
+ unit?: string;
10
+ label: string;
11
+ /** 굴리기 시작 값 — 사실이 있을 때만(없으면 0) */
12
+ from?: number;
13
+ decimals?: number;
14
+ /** 숫자 앞 글(「1/」 같은) */
15
+ prefix?: string;
16
+ source?: string;
17
+ }
18
+
19
+ const FIG = "\u2007"; // 숫자 한 칸 폭 빈칸 — 굴리는 동안 자릿수가 모자라도 단위가 제자리에 있게
20
+
21
+ const TICKS = 11;
22
+
23
+ /**
24
+ * 큰 숫자 하나가 사실 숫자 사이로만 굴러 박에 앉는다. 아래 자는 지나온 거리를 같이 짚고, 앉은 뒤엔 끝 값·밑줄·출처가 이어서 선다.
25
+ * 앉는 순간은 테마 land, 글은 테마 write.
26
+ */
27
+ const pattern: Pattern<HookNumber> = {
28
+ id: "hook-number.roll",
29
+ type: "hook-number",
30
+ does: "큰 숫자가 from→value 로 굴러 박에 앉고, 자 눈금이 지나는 값을 짚는다",
31
+ energy: ["mid"],
32
+ family: "grow",
33
+ camera: ["none"],
34
+ bg: "stage",
35
+ aspects: ["16:9", "9:16", "1:1"],
36
+ dur: { min: 2.4, max: 6 },
37
+ themes: { "og-measure": 0.6, "debug-hud": 0.5 },
38
+ fits(c, _info) {
39
+ if (!Number.isFinite(c?.value)) return "value 가 수가 아니다";
40
+ if (c.from !== undefined && !Number.isFinite(c.from)) return "from 이 수가 아니다";
41
+ if (typeof c.label !== "string" || !c.label.trim()) return "label 이 비었다";
42
+ if ([...c.label].length > 24) return "label 이 24자를 넘는다";
43
+ if (c.unit && [...c.unit].length > 6) return "unit 이 6자를 넘는다";
44
+ if (c.prefix && [...c.prefix].length > 4) return "prefix 가 4자를 넘는다";
45
+ return true;
46
+ },
47
+ build(root, ctx, c, kit) {
48
+ const slots = slotsOf(kit);
49
+ const { w, h, vertical, u } = area(ctx, kit);
50
+ const from = c.from ?? 0;
51
+ const dec = c.decimals ?? Math.max(decimalsOf(c.value), decimalsOf(from));
52
+ const finalText = factRoll(Infinity, { from, to: c.value, at: 0, dur: 1, decimals: dec }).text;
53
+ // 자 양끝은 받은 사실 그대로의 자릿수로
54
+ const fromText = factRoll(0, { from, to: c.value, at: 1, dur: 1, decimals: decimalsOf(from) }).text;
55
+ // 올라가는 굴림은 모자란 자릿수를 숫자 폭 빈칸으로 채워 단위가 제자리, 내려가는 굴림은 긴 글을 같은 폭에 줄여 넣는다 — 앉는 순간 원문 그대로
56
+ const finalEm = textEm(finalText);
57
+ const unit = c.unit ?? "";
58
+
59
+ // 글 폭은 글자 수로 어림한다(build 때 장면이 숨어 있어 잴 수 없다)
60
+ const unitK = 0.4;
61
+ const prefix = c.prefix ?? "";
62
+ // 숫자 줄이 폭 80% 안팎. 세로 화면은 라벨·숫자·(단위)·자·출처를 쌓은 높이가 안전 영역 86% 안에 들게 — 단위를 옆에 둘 때와
63
+ // 숫자 아래 줄로 내릴 때 중 숫자가 더 커지는 쪽을 고른다(긴 숫자는 아래로 내려야 폭을 다 쓰고, 짧은 숫자는 옆에 둬야 넓다)
64
+ // 배수 기호(×)는 아래로 내리지 않고 숫자에 바짝 붙인다
65
+ const times = timesSign(unit);
66
+ const unitGap = times ? 0.04 : 0.12;
67
+ const prefixEm = prefix ? 0.12 + unitK * textEm(prefix) : 0;
68
+ const sizeFor = (below: boolean) => Math.min((w * 0.8) / (finalEm + prefixEm + (unit && !below ? unitGap + unitK * textEm(unit) : 0)), vertical ? (h * 0.86) / (2.22 + (below ? 0.55 : 0)) : h * 0.44);
69
+ const unitBelow = vertical && !!unit && !times && sizeFor(true) > sizeFor(false);
70
+ const numPx = Math.round(sizeFor(unitBelow));
71
+ const rowEm = finalEm + prefixEm + (unit && !unitBelow ? unitGap + unitK * textEm(unit) : 0);
72
+ const colW = Math.round(clamp(numPx * rowEm * 1.04, w * 0.7, w * 0.88));
73
+ const labelPx = Math.round(Math.max(52 * u, numPx * 0.16));
74
+ const weight = ctx.style.type?.weightDisplay ?? 800;
75
+
76
+ const host = node(root, "position:absolute;inset:0;display:flex;align-items:center;justify-content:center");
77
+ const col = node(host, `position:relative;width:${colW}px;display:flex;flex-direction:column;align-items:${vertical ? "center" : "stretch"};gap:${Math.round(numPx * 0.08)}px`);
78
+ const label = node(col, `font-family:var(--kf-font-sans);font-size:${labelPx}px;font-weight:600;color:var(--kf-ink-soft, var(--kf-ink));line-height:1.15;white-space:nowrap;min-height:1.15em`);
79
+ const row = node(col, `position:relative;display:flex;align-items:baseline;gap:${Math.round(numPx * 0.12)}px;transform-origin:0 70%;isolation:isolate`);
80
+ if (prefix) node(row, `font-family:var(--kf-font-display);font-size:${Math.round(numPx * unitK)}px;font-weight:${weight};color:var(--kf-dim, var(--kf-ink));white-space:nowrap`, prefix);
81
+ const numWrap = node(row, "position:relative;display:inline-block");
82
+ const num = node(numWrap, `font-family:var(--kf-font-display);font-size:${numPx}px;font-weight:${weight};line-height:${Math.round(numPx * 0.92)}px;height:${Math.round(numPx * 0.92)}px;letter-spacing:-0.03em;font-variant-numeric:tabular-nums;white-space:pre;color:var(--kf-ink)`);
83
+ const unitEl = unit ? node(unitBelow ? col : row, `font-family:var(--kf-font-display);font-size:${Math.round(numPx * (unitBelow ? 0.5 : unitK))}px;font-weight:${weight};color:${ACCENT_TEXT};white-space:nowrap;line-height:1${times ? `;align-self:center;margin-left:${Math.round(numPx * (unitGap - 0.12))}px` : ""}`, unit) : null;
84
+ const bar = node(row, `position:absolute;left:0;right:0;bottom:${-Math.round(numPx * 0.06)}px;height:${Math.round(numPx * 0.07)}px;background:var(--kf-accent);transform-origin:0 50%;z-index:-1`);
85
+ kit.stage?.hero(row);
86
+ const lineH = Math.round(numPx * 0.92);
87
+ // 작은 범위(걸음 400 아래)는 주행계로 — 글 굴림은 값이 몇 번밖에 안 바뀌어 그 사이가 멈춘 화면이 된다
88
+ const steps = Math.abs(c.value - from) * 10 ** dec;
89
+ const template = [...fromText].length > [...finalText].length ? factRoll(0, { from, to: c.value, at: 1, dur: 1, decimals: dec }).text : finalText;
90
+ const odo = steps < 400 && from >= 0 && c.value >= 0 ? odometer(num, { template, decimals: dec, linePx: lineH }) : null;
91
+ const plain = odo ? node(numWrap, `position:absolute;left:0;top:0;font-family:var(--kf-font-display);font-size:${numPx}px;font-weight:${weight};line-height:${lineH}px;letter-spacing:-0.03em;white-space:pre;color:var(--kf-ink);visibility:hidden`, finalText) : null;
92
+ // 앉은 뒤 장면 끝까지 숫자를 짚는다 — 반전 칸이 왼쪽부터 차오른다. 기울거나 굵은 숫자 글꼴은 글자가 글 폭 밖으로 삐져
93
+ // 나와서(7 의 윗끝) 칸을 양옆으로 넉넉히 — 안 그러면 칸 밖으로 원래 색 글자 조각이 남고 칸 끝에 글자가 잘린다
94
+ const hiPad = Math.round(numPx * 0.08);
95
+ const hiBox = node(numWrap, `position:absolute;left:${-hiPad}px;top:0;height:${lineH}px;overflow:hidden;background:var(--kf-accent);visibility:hidden`);
96
+ const hiText = node(hiBox, `margin-left:${hiPad}px;font-family:var(--kf-font-display);font-size:${numPx}px;font-weight:${weight};line-height:${lineH}px;letter-spacing:-0.03em;white-space:pre;color:var(--kf-on-accent)`, finalText);
97
+
98
+ // 자 — 눈금이 from 에서 value 까지, 표시가 지금 값을 짚는다
99
+ // 세로 화면은 자를 높게 — 눈금이 지금 값을 짚는 몫이 화면 아래쪽을 채운다
100
+ const rulerH = Math.round(Math.max(28 * u, numPx * (vertical ? 0.42 : 0.2)));
101
+ const ruler = node(col, `position:relative;align-self:stretch;height:${rulerH}px;margin-top:${Math.round(numPx * 0.1)}px;transform-origin:0 50%`);
102
+ node(ruler, `position:absolute;left:0;right:0;bottom:0;height:${Math.max(2, Math.round(3 * u))}px;background:var(--kf-ink);opacity:.35`);
103
+ const ticks = Array.from({ length: TICKS }, (_, i) =>
104
+ node(ruler, `position:absolute;bottom:0;left:${(i / (TICKS - 1)) * 100}%;width:${Math.max(3, Math.round(5 * u))}px;margin-left:-${Math.max(1, Math.round(2.5 * u))}px;height:${i % 5 === 0 ? 100 : 55}%;background:var(--kf-ink);transform-origin:50% 100%`),
105
+ );
106
+ const marker = node(ruler, `position:absolute;bottom:0;left:0;width:${Math.round(rulerH * 0.5)}px;height:${Math.round(rulerH * 1.35)}px;margin-left:-${Math.round(rulerH * 0.25)}px;background:var(--kf-accent)`);
107
+ const endPx = Math.round(Math.max(26 * u, labelPx * 0.62));
108
+ const ends = node(col, `position:relative;align-self:stretch;height:${Math.round(endPx * 1.3)}px;font-family:var(--kf-font-mono);font-size:${endPx}px;color:var(--kf-dim, var(--kf-ink));font-variant-numeric:tabular-nums`);
109
+ const endFrom = node(ends, "position:absolute;left:0;top:0;white-space:nowrap");
110
+ const endTo = node(ends, `position:absolute;right:0;top:0;white-space:nowrap;color:${ACCENT_TEXT}`);
111
+ // 출처는 숫자 줄 폭 안에서 두 줄까지 — 길면 글자를 줄인다(잘리지 않게)
112
+ const srcPx = c.source ? Math.round(Math.max(22 * u, Math.min(endPx, (colW * 2 * 0.95) / Math.max(1, textEm(`출처 · ${c.source}`) * 1.05)))) : 0;
113
+ const source = c.source ? node(col, `align-self:stretch;font-family:var(--kf-font-mono);font-size:${srcPx}px;line-height:1.3;height:2.6em;color:var(--kf-dim, var(--kf-ink));word-break:keep-all;overflow-wrap:anywhere;text-align:${vertical ? "center" : "left"};margin-top:${Math.round(numPx * 0.04)}px`) : null;
114
+
115
+ // 움직임은 모두 내용에서 — 굴림(값)·자 표시(지나온 거리)·끝 값·밑줄(강조)·출처가 끊김 없이 이어져 장면 끝까지 간다.
116
+ // 앉는 박은 뒤 사슬이 장면 끝에 닿게 거꾸로 잡는다
117
+ const hits = kit.hits.length ? kit.hits : [0];
118
+ const srcText = c.source ? `출처 · ${c.source}` : "";
119
+ const srcDur = srcText ? clamp([...srcText].length * 0.035, 0.3, 0.9) : 0;
120
+ // 테마 글 찍기는 마지막 글자가 나타난 뒤에도 잠깐 앉는다(찍힘·톡 튀기) — 출처가 장면 끝에 딱 맞춰 끝나면 끝 프레임(표지·견본)에
121
+ // 덜 찍힌 글자가 남는다. 그 사이는 반전 칸이 끝까지 차올라 쉬지 않는다
122
+ const settleIn = srcText ? 0.3 : 0;
123
+ const tail = 0.3 + 0.35 + srcDur + settleIn;
124
+ const want = Math.max(0.9, ctx.dur - tail - 0.02);
125
+ const land = [...hits].reverse().find((b) => b <= want && b >= 0.9) ?? want;
126
+ const toAt = land;
127
+ const sweepAt = toAt + 0.3;
128
+ const sourceAt = sweepAt + 0.35;
129
+ const fromAt = Math.min(0.45, land * 0.3);
130
+
131
+ const writeLabel = slots.write(label, { ctx, text: c.label, at: 0, dur: Math.min(0.4, land * 0.3), seed: `${kit.seed}:label` });
132
+ // 라벨은 그 글꼴로 재서 안전 영역 폭 92% 안에(글자 수 어림은 고정폭·넓은 글꼴에서 넘친다)
133
+ const fitLabel = fitOnce(label, w * 0.92, labelPx, c.label);
134
+ const landed = slots.land(row, { ctx, at: land, seed: `${kit.seed}:land` });
135
+ const writeSource = source ? slots.write(source, { ctx, text: srcText, at: sourceAt, dur: srcDur, seed: `${kit.seed}:src` }) : null;
136
+ const writeFrom = slots.write(endFrom, { ctx, text: fromText + (unit ? ` ${unit}` : ""), at: fromAt, dur: 0.3, seed: `${kit.seed}:from` });
137
+ const writeTo = slots.write(endTo, { ctx, text: `${finalText}${unit ? ` ${unit}` : ""}`, at: toAt, dur: 0.3, seed: `${kit.seed}:to` });
138
+
139
+ return (t: number) => {
140
+ moving("hook-number.roll", ctx);
141
+ fitLabel();
142
+ const r = factRoll(t, { from, to: c.value, at: 0.08, dur: land - 0.08, decimals: dec });
143
+ if (odo) {
144
+ odo(r.value);
145
+ num.style.visibility = t >= land ? "hidden" : "visible";
146
+ plain!.style.visibility = t >= land ? "visible" : "hidden";
147
+ } else {
148
+ const shown = t >= land ? finalText : r.text.padStart([...finalText].length, FIG);
149
+ num.textContent = shown;
150
+ num.style.fontSize = `${Math.round(numPx * Math.min(1, finalEm / textEm(shown.replaceAll(FIG, "0"))))}px`;
151
+ }
152
+ num.style.color = t >= land ? ACCENT_TEXT : "var(--kf-ink)";
153
+ const hp = clamp((t - land - 0.05) / Math.max(0.3, ctx.dur - land - 0.12));
154
+ hiBox.style.visibility = hp > 0 ? "visible" : "hidden";
155
+ // 앉은 뒤라 글 폭을 잴 수 있다(장면이 보인다)
156
+ hiBox.style.width = hp > 0 ? `${(hp * (((plain ?? num).offsetWidth || textEm(finalText) * numPx) + 2 * hiPad)).toFixed(1)}px` : "0px";
157
+ if (unitEl) unitEl.style.color = t >= land ? "var(--kf-ink)" : ACCENT_TEXT;
158
+ // 훅 첫 프레임부터 숫자가 서 있다 — 등장은 크게 들어와 내려앉는 것만
159
+ applyStyle(row, combine({ scale: spring(t, { from: 1.08, to: 1, stiffness: 260, damping: 20 }) }, landed(t)));
160
+ applyStyle(bar, { ...sweep(t, { at: sweepAt, dur: 0.35 }), opacity: 0.85 });
161
+ bar.style.display = t >= sweepAt ? "block" : "none";
162
+
163
+ applyStyle(ruler, { scaleX: ease.outCubic(progress(t, 0, 0.3)) });
164
+ marker.style.left = `${(r.p * 100).toFixed(3)}%`;
165
+ ticks.forEach((tick, i) => {
166
+ const passed = i / (TICKS - 1) <= r.p + 1e-6;
167
+ tick.style.background = passed ? "var(--kf-accent)" : "var(--kf-ink)";
168
+ tick.style.opacity = passed ? "1" : "0.45";
169
+ });
170
+
171
+ writeLabel(t);
172
+ writeSource?.(t);
173
+ writeFrom(t);
174
+ writeTo(t);
175
+ };
176
+ },
177
+ };
178
+
179
+ export default pattern;
@@ -0,0 +1,91 @@
1
+ import { applyStyle, clamp, combine } from "@kasaframe/core";
2
+ import { textEm } from "@kasaframe/motion/moves";
3
+ import { area, beatStops, cameraOk, decimalsOf, factRoll, fitOnce, glider, moving, node, sceneDrop, slam, slotsOf } from "../kit.ts";
4
+ import type { HookNumber } from "./roll.ts";
5
+ import type { Pattern } from "../types.ts";
6
+
7
+ /**
8
+ * 결과가 서는 순간 — 숫자가 화면을 꽉 채우며 드롭(없으면 첫 박)에 쾅 내려앉고, 그 박에 테마 드롭이 터진다.
9
+ * 앉기 전엔 라벨이 먼저 찍혀 무엇의 숫자인지 알리고, 앉은 뒤엔 반전 칸이 숫자를 장면 끝까지 짚는다. 굴리지 않는다 — 결과 그 자체.
10
+ */
11
+ const pattern: Pattern<HookNumber> = {
12
+ id: "hook-number.slam",
13
+ type: "hook-number",
14
+ does: "숫자가 화면을 꽉 채우며 드롭에 쾅 내려앉고, 라벨이 먼저 서고 반전 칸이 숫자를 짚는다",
15
+ energy: ["high"],
16
+ family: "slam",
17
+ camera: ["impact"],
18
+ bg: "stage",
19
+ aspects: ["16:9", "9:16", "1:1"],
20
+ dur: { min: 1.6, max: 5 },
21
+ fits(c) {
22
+ if (!Number.isFinite(c?.value)) return "value 가 수가 아니다";
23
+ if (typeof c.label !== "string" || !c.label.trim() || [...c.label].length > 24) return "label 이 비었거나 24자를 넘는다";
24
+ if (c.unit && [...c.unit].length > 6) return "unit 이 6자를 넘는다";
25
+ return true;
26
+ },
27
+ build(root, ctx, c, kit) {
28
+ const slots = slotsOf(kit);
29
+ const { w, h, vertical, u } = area(ctx, kit);
30
+ const dec = c.decimals ?? decimalsOf(c.value);
31
+ const text = `${c.prefix ?? ""}${factRoll(Infinity, { from: 0, to: c.value, at: 0, dur: 1, decimals: dec }).text}`;
32
+ const unit = c.unit ?? "";
33
+ const rowEm = textEm(text) + (unit ? 0.1 + 0.42 * textEm(unit) : 0);
34
+ // 화면을 꽉 — 폭 90%, 세로 화면은 높이 42% 까지
35
+ const numPx = Math.round(Math.min((w * 0.9) / rowEm, h * (vertical ? 0.42 : 0.62)));
36
+ const labelPx = Math.round(Math.max(52 * u, numPx * 0.13));
37
+ const weight = ctx.style.type?.weightDisplay ?? 800;
38
+
39
+ const host = node(root, "position:absolute;inset:0;display:flex;align-items:center;justify-content:center");
40
+ const col = node(host, `position:relative;display:flex;flex-direction:column;align-items:center;gap:${Math.round(numPx * 0.06)}px`);
41
+ const labelBox = node(col, `position:relative;font-family:var(--kf-font-sans);font-size:${labelPx}px;font-weight:700;white-space:nowrap;line-height:1.2;height:1.2em`);
42
+ const label = node(labelBox, "color:var(--kf-ink)");
43
+ const row = node(col, `display:flex;align-items:baseline;gap:${Math.round(numPx * 0.06)}px;transform-origin:50% 60%`);
44
+ // 쾅 전엔 같은 숫자가 외곽선으로 제자리에 먼저 서 있다 — 무엇이 올지 보이고, 앉으면 꽉 찬 숫자가 그 위를 덮는다
45
+ const ghostRow = node(col, `position:absolute;display:flex;align-items:baseline;gap:${Math.round(numPx * 0.06)}px;visibility:hidden`);
46
+ node(ghostRow, `font-family:var(--kf-font-display);font-size:${numPx}px;font-weight:${weight};line-height:.95;letter-spacing:-0.04em;color:transparent;-webkit-text-stroke:${Math.max(2, Math.round(numPx * 0.008))}px var(--kf-ink);white-space:pre;opacity:.5`, text);
47
+ if (unit) node(ghostRow, `font-family:var(--kf-font-display);font-size:${Math.round(numPx * 0.42)}px;font-weight:${weight};color:transparent;white-space:nowrap`, unit);
48
+ const numBox = node(row, "position:relative;color:var(--kf-ink)");
49
+ node(numBox, `font-family:var(--kf-font-display);font-size:${numPx}px;font-weight:${weight};line-height:.95;letter-spacing:-0.04em;white-space:pre`, text);
50
+ if (unit) node(row, `font-family:var(--kf-font-display);font-size:${Math.round(numPx * 0.42)}px;font-weight:${weight};color:var(--kf-accent-text, var(--kf-accent));white-space:nowrap`, unit);
51
+ kit.stage?.hero(row);
52
+ // 드롭 터짐은 화면 전체 층에 — 카메라 층(안전 영역)에 깔면 터짐이 네모 판으로 보인다
53
+ const burst = node(kit.stage?.box ?? root, "position:absolute;inset:0;pointer-events:none;z-index:50");
54
+
55
+ // 앉는 박 — 이 장면에 드롭이 있으면 그 드롭, 없으면 라벨이 다 찍힌 뒤 첫 박
56
+ const di = sceneDrop(ctx);
57
+ const labelDur = clamp([...c.label].length * 0.03, 0.25, 0.5);
58
+ const hits = kit.hits.length ? kit.hits : [0];
59
+ const at = di !== null ? ctx.beats.drop(di) : (hits.find((b) => b >= labelDur + 0.1) ?? labelDur + 0.15);
60
+ const dropPaint = di !== null && slots.drop ? slots.drop(burst, { ctx, drop: di }) : null;
61
+ if (di === null && kit.stage && cameraOk(ctx)) kit.stage.camera.impact(at, { amount: 0.04 });
62
+ const writeLabel = slots.write(label, { ctx, text: c.label, at: 0, dur: labelDur, seed: `${kit.seed}:l` });
63
+ const landed = slots.land(row, { ctx, at, seed: `${kit.seed}:land` });
64
+ // 짚개 — 쾅 전엔 라벨 ↔ 외곽선 자리(무엇의 숫자가 어디 오나), 쾅 뒤엔 숫자 ↔ 라벨을 박마다 번갈아 짚는다
65
+ const before = glider(host, [labelBox, ghostRow], { hub: true });
66
+ const after = glider(host, [numBox, labelBox], { hub: true });
67
+ const preStops = beatStops(kit.hits, labelDur, at - 0.2);
68
+ // 라벨은 그 글꼴로 재서 안전 영역 폭 92% 안에
69
+ const fitLabel = fitOnce(labelBox, w * 0.92, labelPx, c.label);
70
+ const postStops = beatStops(kit.hits, at + 0.25, ctx.dur);
71
+
72
+ return (t: number) => {
73
+ moving("hook-number.slam", ctx);
74
+ fitLabel();
75
+ writeLabel(t);
76
+ const s = dropPaint?.(t);
77
+ applyStyle(row, combine(slam(t, at), landed(t), s?.punch));
78
+ // 외곽선은 숫자 줄과 같은 자리에 — 처음 보일 때 숫자 줄 자리를 잰다(장면이 보인 뒤라 잴 수 있다)
79
+ ghostRow.style.visibility = t < at ? "visible" : "hidden";
80
+ if (t < at) {
81
+ ghostRow.style.left = `${row.offsetLeft}px`;
82
+ ghostRow.style.top = `${row.offsetTop}px`;
83
+ }
84
+ before(t, preStops, at - 0.16);
85
+ after(t, postStops);
86
+ if (s) applyStyle(host, s.shake);
87
+ };
88
+ },
89
+ };
90
+
91
+ export default pattern;
@@ -0,0 +1,38 @@
1
+ import codeTerminal from "./code/terminal.ts";
2
+ import compareBars from "./compare/bars.ts";
3
+ import compareDuel from "./compare/duel.ts";
4
+ import beforeAfterShrink from "./before-after/shrink.ts";
5
+ import beforeAfterStrike from "./before-after/strike.ts";
6
+ import barsRank from "./bars/rank.ts";
7
+ import endLockup from "./end/lockup.ts";
8
+ import endSlam from "./end/slam.ts";
9
+ import flowGraph from "./flow/graph.ts";
10
+ import hookNumberCounter from "./hook-number/counter.ts";
11
+ import hookNumberRoll from "./hook-number/roll.ts";
12
+ import hookNumberSlam from "./hook-number/slam.ts";
13
+ import limitGauge from "./limit/gauge.ts";
14
+ import listCards from "./list/cards.ts";
15
+ import listCycle from "./list/cycle.ts";
16
+ import mediaPanels from "./media/panels.ts";
17
+ import quoteKaraoke from "./quote/karaoke.ts";
18
+ import quoteSlam from "./quote/slam.ts";
19
+ import screenFocus from "./screen/focus.ts";
20
+ import stepsPath from "./steps/path.ts";
21
+ import stepsStack from "./steps/stack.ts";
22
+ import termRevealChips from "./term-reveal/chips.ts";
23
+ import termRevealExpand from "./term-reveal/expand.ts";
24
+ import timelineAxis from "./timeline/axis.ts";
25
+ import toggleSimSwitch from "./toggle-sim/switch.ts";
26
+ import trendColumns from "./trend/columns.ts";
27
+ import trendDraw from "./trend/draw.ts";
28
+ import wordsSlam from "./words/slam.ts";
29
+ import titleMask from "./title/mask.ts";
30
+ import titleSlam from "./title/slam.ts";
31
+ import type { Pattern } from "./types.ts";
32
+
33
+ // 감독이 Node 에서 meta·fits 로 고른다 — 여기 모인 패턴만 후보다. 장면 파일은 @kasaframe/patterns/<type>/<이름> 으로 하나씩 가져간다
34
+ export const PATTERNS: readonly Pattern[] = [hookNumberRoll, hookNumberSlam, listCards, titleMask, barsRank, beforeAfterStrike, flowGraph, endLockup, endSlam, termRevealExpand, termRevealChips, compareBars, compareDuel, stepsPath, stepsStack, limitGauge, timelineAxis, quoteKaraoke, trendDraw, codeTerminal, wordsSlam, toggleSimSwitch, mediaPanels, screenFocus, titleSlam, listCycle, beforeAfterShrink, hookNumberCounter, quoteSlam, trendColumns];
35
+
36
+ export { OBJECTS, objectFor, objectById, objectCss, objectsJson, cssColor, styleColor } from "./objects.ts";
37
+ export type { VisualObject, ColorToken, Shape2d, Shape3d } from "./objects.ts";
38
+ export type * from "./types.ts";