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
@@ -44,6 +44,45 @@
44
44
  },
45
45
  "beatMove": "every-beat"
46
46
  },
47
+ "moves": {
48
+ "text": [
49
+ "slamWords",
50
+ "echoStack",
51
+ "ghostStack",
52
+ "splitFlap",
53
+ "ransomLetters"
54
+ ],
55
+ "wipe": [
56
+ "brushWipe",
57
+ "irisBlades",
58
+ "flood"
59
+ ],
60
+ "roles": {
61
+ "intro": {
62
+ "text": [
63
+ "ghostStack",
64
+ "splitFlap"
65
+ ]
66
+ },
67
+ "drop": {
68
+ "text": [
69
+ "slamWords",
70
+ "echoStack"
71
+ ]
72
+ },
73
+ "end": {
74
+ "text": [
75
+ "splitFlap",
76
+ "neonSign",
77
+ "ledBoard"
78
+ ]
79
+ }
80
+ }
81
+ },
82
+ "render": {
83
+ "motionBlur": 6,
84
+ "shutter": 180
85
+ },
47
86
  "scenes": [
48
87
  {
49
88
  "id": "intro",
@@ -110,7 +149,9 @@
110
149
  "gen": [],
111
150
  "genVideo": [
112
151
  "vid:seam"
113
- ]
152
+ ],
153
+ "shot3d": "drop1",
154
+ "act": "intro"
114
155
  },
115
156
  "deliverables": [
116
157
  "mp4",
@@ -1,7 +1,9 @@
1
1
  import { applyStyle, clip, ease, type SceneCtx } from "@kasaframe/core";
2
- import { bigWord, makeStage } from "../lib/stage.ts";
2
+ import type { Dress } from "@kasaframe/motion/moves";
3
+ import { makeStage } from "../lib/stage.ts";
3
4
 
4
- const WORD = "{{word}}";
5
+ const WORDS = ["{{word}}", "{{title}}"];
6
+ const DRESS: Dress = { bg: "{{bg}}", text: "{{text}}", wipeIn: "{{wipeIn}}", wipeOut: "{{wipeOut}}" };
5
7
 
6
8
  // 쇼츠 훅 — 첫 프레임부터 결과가 보여야 한다. 생성 바탕(assets/gen/hook-bg.png)이 있으면 어둡게 깔고 그 위에 숫자·낱말 하나
7
9
  function exists(url: string): boolean {
@@ -19,14 +21,15 @@ export default {
19
21
  build(root: HTMLElement, ctx: SceneCtx) {
20
22
  const bg = ctx.asset("gen/hook-bg.png");
21
23
  const hasBg = exists(bg);
22
- const stage = makeStage(root, ctx, { panel: !hasBg });
24
+ // 생성 바탕이 있으면 그게 바탕이다 — 바탕 수는 걷고 글 수만
25
+ const stage = makeStage(root, ctx, { panel: !hasBg, dress: hasBg ? { ...DRESS, bg: "orbs" } : DRESS });
23
26
  const img = document.createElement("div");
24
27
  img.style.cssText = `position:absolute;inset:-4%;background-size:cover;background-position:center;background-image:linear-gradient(rgba(0,0,0,.35), rgba(0,0,0,.55)), url("${bg}")`;
25
28
  if (hasBg) stage.box.insertBefore(img, stage.cam);
26
- const word = bigWord(WORD, stage, { color: hasBg ? "#fff" : "var(--kf-accent)", weight: 900 });
29
+ const words = stage.say(WORDS, { color: hasBg ? "#fff" : "var(--kf-accent)", weight: 900, ready: true });
27
30
  return (t: number) => {
31
+ words.paint(t, { scale: 1.12 - 0.12 * ease.outBack(clip(t, 0, 0.5).p) });
28
32
  stage.tick(t);
29
- applyStyle(word, { scale: 1.12 - 0.12 * ease.outBack(clip(t, 0, 0.5).p) });
30
33
  // 바탕은 장면 내내 천천히 다가간다 — 멈춘 화면으로 잡히지 않게
31
34
  applyStyle(img, { scale: 1 + 0.06 * (t / ctx.dur) });
32
35
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "shorts",
3
3
  "label": "쇼츠",
4
- "summary": "세로 쇼츠 — 첫 1.5초에 결과, 화면 글은 전문 낱말만, 말은 자막 트랙으로, 끝이 처음으로 이어진다",
4
+ "summary": "세로 쇼츠 — 첫 1.5초에 결과, 화면 글은 전문 낱말만, 말은 차분한 내레이션과 자막 트랙으로, 깊은 앰비언트 곡을 아래 깔고, 끝이 처음으로 이어진다",
5
5
  "aspect": "9:16",
6
6
  "width": 1080,
7
7
  "height": 1920,
@@ -11,7 +11,29 @@
11
11
  "max": 60,
12
12
  "default": 45
13
13
  },
14
- "music": "optional",
14
+ "music": "required",
15
+ "mood": "deep",
16
+ "narration": {
17
+ "on": true,
18
+ "lang": "ko",
19
+ "style": "차분하고 낮은 다큐멘터리 내레이션. 서두르지 않고 문장 끝을 내려 놓는다",
20
+ "voice": "elevenlabs:George",
21
+ "voices": {
22
+ "elevenlabs": "George",
23
+ "gemini": "Charon",
24
+ "dashscope": "Neil",
25
+ "local": "Sohee"
26
+ },
27
+ "lead": 0.25,
28
+ "tail": 0.5
29
+ },
30
+ "audio": {
31
+ "musicDb": -20,
32
+ "duck": true,
33
+ "lufs": -14,
34
+ "truePeak": -1.5,
35
+ "fadeOut": 1.5
36
+ },
15
37
  "captions": "off",
16
38
  "loop": true,
17
39
  "style": "og",
@@ -42,6 +64,43 @@
42
64
  },
43
65
  "beatMove": "every-beat"
44
66
  },
67
+ "moves": {
68
+ "text": [
69
+ "slamWords",
70
+ "echoStack",
71
+ "splitFlap",
72
+ "ghostStack",
73
+ "ransomLetters"
74
+ ],
75
+ "wipe": [
76
+ "brushWipe",
77
+ "flood",
78
+ "irisBlades"
79
+ ],
80
+ "roles": {
81
+ "hook": {
82
+ "text": [
83
+ "slamWords"
84
+ ]
85
+ },
86
+ "reveal": {
87
+ "text": [
88
+ "slamWords",
89
+ "echoStack"
90
+ ]
91
+ },
92
+ "end": {
93
+ "text": [
94
+ "splitFlap",
95
+ "slamWords"
96
+ ]
97
+ }
98
+ }
99
+ },
100
+ "render": {
101
+ "motionBlur": 6,
102
+ "shutter": 180
103
+ },
45
104
  "scenes": [
46
105
  {
47
106
  "id": "hook",
@@ -57,6 +116,14 @@
57
116
  "snap": "beat",
58
117
  "idea": "조건 하나로 문제를 재현한다 — 보는 사람이 예상하는 쪽을 먼저 보여 준다"
59
118
  },
119
+ {
120
+ "id": "turn",
121
+ "role": "seam",
122
+ "sec": 1.2,
123
+ "snap": "beat",
124
+ "transition": "dive",
125
+ "idea": "이음매 — setup 의 마지막 화면 속으로 다이브해 explain 의 무대로 넘어간다(3D 전환, 틀이 맡는다 — 장면 파일은 그대로 둔다)"
126
+ },
60
127
  {
61
128
  "id": "explain",
62
129
  "role": "explain",
@@ -94,7 +161,9 @@
94
161
  ],
95
162
  "gen": [
96
163
  "img:hook-bg"
97
- ]
164
+ ],
165
+ "shot3d": "explain",
166
+ "act": "setup"
98
167
  },
99
168
  "deliverables": [
100
169
  "mp4",
@@ -42,6 +42,25 @@
42
42
  },
43
43
  "beatMove": "every-beat"
44
44
  },
45
+ "moves": {
46
+ "text": [
47
+ "slamWords",
48
+ "splitFlap",
49
+ "echoStack",
50
+ "ransomLetters"
51
+ ],
52
+ "wipe": [
53
+ "brushWipe",
54
+ "flood"
55
+ ],
56
+ "roles": {
57
+ "hook": {
58
+ "text": [
59
+ "slamWords"
60
+ ]
61
+ }
62
+ }
63
+ },
45
64
  "scenes": [
46
65
  {
47
66
  "id": "hook",
@@ -84,7 +103,8 @@
84
103
  "want": [
85
104
  "logo"
86
105
  ],
87
- "gen": []
106
+ "gen": [],
107
+ "act": "hook"
88
108
  },
89
109
  "deliverables": [
90
110
  "mp4",
@@ -0,0 +1,99 @@
1
+ import { clamp, ease, type Style } from "@kasaframe/core";
2
+ import type { ThemeSlots } from "@kasaframe/themes/slots";
3
+ import { layer, letters, partial, shownCount, strokePath, traceCanvas } from "@kasaframe/themes/slotkit";
4
+ import { dropTrigger } from "./effects.ts";
5
+
6
+ // 장면 문법 패턴이 테마 이름 없이 부르는 자리 — 재는 순간 상자가 서고 값이 찍히는 계측 화면의 결로.
7
+ // 움직임은 미끄러지지 않고 잡고 멈춘다(outExpo 0.18초 한 가지) — effects.ts 와 같은 손
8
+ const SNAP = 0.18;
9
+ const snap = (t: number, at: number, dur = SNAP): number => ease.outExpo(clamp((t - at) / dur));
10
+ const NOISE = "0123456789ABCDEF#%";
11
+
12
+ export const slots: ThemeSlots = {
13
+ // 서 있는 계측판 눈금 — 흐르는 눈금·지나가는 커서는 뜻이 없어 뺐다. 움직이는 것은 재는 상자·값뿐
14
+ surface(el) {
15
+ layer(
16
+ el,
17
+ "inset:0;background:var(--kf-bg);background-image:linear-gradient(var(--kf-line) 2px,transparent 2px),linear-gradient(90deg,var(--kf-line) 2px,transparent 2px);background-size:60px 60px;opacity:.7",
18
+ );
19
+ return () => {};
20
+ },
21
+
22
+ // 와이어 상자 — 모서리 꺾쇠가 먼저 서고 변이 가운데로 뻗는다. 크기를 재지 않게 % 로
23
+ panel(el, { at, tone }) {
24
+ const box = layer(el, `inset:0;background:${tone === "accent" ? "color-mix(in srgb, var(--kf-accent) 18%, transparent)" : "var(--kf-raised)"}`, true);
25
+ const corners = (["top:0;left:0;border-top", "top:0;right:0;border-top", "bottom:0;left:0;border-bottom", "bottom:0;right:0;border-bottom"] as const).map((pos, i) =>
26
+ layer(box, `${pos}:3px solid var(--kf-ink);border-${i % 2 ? "right" : "left"}:3px solid var(--kf-ink);width:0;height:0`),
27
+ );
28
+ const edges = layer(box, "inset:0;border:1px solid var(--kf-dim)");
29
+ return (t) => {
30
+ const p = snap(t, at, 0.3);
31
+ box.style.visibility = t >= at ? "visible" : "hidden";
32
+ box.style.opacity = String(clamp(p * 1.4));
33
+ corners.forEach((c) => {
34
+ c.style.width = `${(22 * p).toFixed(2)}%`;
35
+ c.style.height = `${(22 * p).toFixed(2)}%`;
36
+ });
37
+ const q = snap(t, at + 0.15, 0.3);
38
+ edges.style.clipPath = `inset(0 ${((1 - q) * 50).toFixed(2)}%)`;
39
+ };
40
+ },
41
+
42
+ // 잡는 순간 두 프레임 밝게 튀고 한 번에 선다 — 미끄러지는 스프링 없이
43
+ land(_el, { at }) {
44
+ return (t): Style => {
45
+ const local = t - at;
46
+ if (local < 0 || local > 0.3) return {};
47
+ return { brightness: local < 2 / 60 ? 1.8 : 1, scale: 1 + 0.06 * (1 - snap(t, at, 0.2)) };
48
+ };
49
+ },
50
+
51
+ // 글자 칸이 16진수로 굴다가 앞에서부터 잡힌다
52
+ write(el, { text, at, dur, seed }) {
53
+ const spans = letters(el, text);
54
+ const chars = Array.from(text);
55
+ return (t) => {
56
+ const fixed = shownCount(spans.length, t, at, dur);
57
+ const roll = Math.floor(t * 30);
58
+ spans.forEach((s, i) => {
59
+ if (i < fixed || chars[i] === " " || t >= at + dur) {
60
+ s.textContent = chars[i];
61
+ s.style.visibility = t >= at ? "visible" : "hidden";
62
+ return;
63
+ }
64
+ s.style.visibility = t >= at && i < fixed + 3 ? "visible" : "hidden";
65
+ s.textContent = /[가-힯]/.test(chars[i]) ? chars[i] : NOISE[(roll * 7 + i * 13 + seed.length) % NOISE.length];
66
+ });
67
+ };
68
+ },
69
+
70
+ // 계측 선 — 끝에 십자 커서가 앞서 가고, 다 그은 뒤에도 측정점 하나가 선을 따라 돈다
71
+ trace(el, { points, at, dur, tone }) {
72
+ const { g, clear } = traceCanvas(el, points);
73
+ const css = getComputedStyle(el);
74
+ const color = (tone === "accent" ? css.getPropertyValue("--kf-accent") : css.getPropertyValue("--kf-ink")).trim() || "#C8FF2E";
75
+ return (t) => {
76
+ clear();
77
+ const p = clamp((t - at) / Math.max(0.05, dur));
78
+ if (p <= 0) return;
79
+ g.strokeStyle = color;
80
+ g.lineWidth = 4;
81
+ g.lineJoin = "miter";
82
+ const drawn = partial(points, p);
83
+ strokePath(g, drawn);
84
+ const [hx, hy] = p < 1 ? drawn[drawn.length - 1] : partial(points, ((t - at - dur) * 0.4) % 1).at(-1)!;
85
+ g.lineWidth = 2;
86
+ g.strokeRect(hx - 10, hy - 10, 20, 20);
87
+ g.beginPath();
88
+ g.moveTo(hx - 22, hy);
89
+ g.lineTo(hx + 22, hy);
90
+ g.moveTo(hx, hy - 22);
91
+ g.lineTo(hx, hy + 22);
92
+ g.stroke();
93
+ };
94
+ },
95
+
96
+ drop(el, { ctx, drop }) {
97
+ return dropTrigger(el, { ctx, drop, label: `DROP ${drop + 1}` });
98
+ },
99
+ };
@@ -0,0 +1,88 @@
1
+ import { clamp, ease, progress, type Style } from "@kasaframe/core";
2
+ import { dropBurst } from "@kasaframe/motion";
3
+ import type { ThemeSlots } from "@kasaframe/themes/slots";
4
+ import { layer, letters, partial, shownCount, strokePath, traceCanvas } from "@kasaframe/themes/slotkit";
5
+
6
+ // 장면 문법 패턴이 테마 이름 없이 부르는 자리 — 계측기 화면의 결. 바탕은 서 있는 모눈(뜻 없는 흐름을 두지 않는다),
7
+ // 움직임은 값이 찍히고 막대가 서는 쪽만. 감속은 effects.ts 와 같은 snappy
8
+ const snappy = ease.snappy;
9
+
10
+ export const slots: ThemeSlots = {
11
+ surface(el) {
12
+ layer(
13
+ el,
14
+ "inset:0;background:var(--kf-bg);background-image:linear-gradient(rgba(203,213,225,.55) 1px,transparent 1px),linear-gradient(90deg,rgba(203,213,225,.55) 1px,transparent 1px);background-size:56px 56px",
15
+ );
16
+ return () => {};
17
+ },
18
+
19
+ // 흰 카드 — 회색 1px 테두리·12px 모서리, 강조 판은 OG 파랑. 위에서 짧게 내려와 선다
20
+ panel(el, { at, tone }) {
21
+ const card = layer(
22
+ el,
23
+ `inset:0;border-radius:var(--kf-radius-md);border:1px solid var(--kf-line);box-shadow:0 1px 2px rgba(2,8,23,.06),0 8px 24px rgba(2,8,23,.06);background:${tone === "accent" ? "var(--kf-accent)" : "var(--kf-bg)"}`,
24
+ true,
25
+ );
26
+ return (t) => {
27
+ const p = snappy(progress(t, at, 0.22));
28
+ card.style.visibility = t >= at ? "visible" : "hidden";
29
+ card.style.opacity = String(p);
30
+ card.style.transform = `translateY(${((1 - p) * -14).toFixed(2)}px)`;
31
+ };
32
+ },
33
+
34
+ // 값이 잡히는 순간 — 넘침 없이 한 번 조이고 선다(계측은 출렁이지 않는다)
35
+ land(_el, { at }) {
36
+ return (t): Style => (t < at || t > at + 0.3 ? {} : { scale: 1 + 0.05 * (1 - snappy(progress(t, at, 0.24))) });
37
+ },
38
+
39
+ // 계기판처럼 — 글자는 앞에서부터 서고, 숫자 칸은 서기 전까지 돌고 있다(meter 와 같은 결)
40
+ write(el, { text, at, dur }) {
41
+ const spans = letters(el, text);
42
+ const chars = Array.from(text);
43
+ return (t) => {
44
+ const n = shownCount(spans.length, t, at, dur);
45
+ spans.forEach((s, i) => {
46
+ const on = t >= at && i < Math.min(spans.length, n + 2);
47
+ s.style.visibility = on ? "visible" : "hidden";
48
+ s.textContent = i < n || !/\d/.test(chars[i]) ? chars[i] : String(Math.floor(t * 26 + i * 3.7) % 10);
49
+ });
50
+ };
51
+ },
52
+
53
+ // 표본 선 — 파랑 3px 선이 그어지고 꺾이는 점마다 표본 점이 선다
54
+ trace(el, { points, at, dur, tone }) {
55
+ const { g, clear } = traceCanvas(el, points);
56
+ const css = getComputedStyle(el);
57
+ const color = (tone === "accent" ? css.getPropertyValue("--kf-accent") : css.getPropertyValue("--kf-ink")).trim() || "#006FD6";
58
+ return (t) => {
59
+ clear();
60
+ const p = clamp((t - at) / Math.max(0.05, dur));
61
+ if (p <= 0) return;
62
+ const drawn = partial(points, p);
63
+ g.strokeStyle = color;
64
+ g.lineWidth = 3;
65
+ g.lineJoin = "round";
66
+ strokePath(g, drawn);
67
+ g.fillStyle = color;
68
+ for (const [x, y] of drawn.slice(0, -1)) {
69
+ g.beginPath();
70
+ g.arc(x, y, 6, 0, Math.PI * 2);
71
+ g.fill();
72
+ }
73
+ };
74
+ },
75
+
76
+ // 드롭 — 파랑 판이 한 번 덮였다 걷히고 고리가 퍼진다(흰 바탕이라 흰 섬광은 안 보인다)
77
+ drop(el, { ctx, drop }) {
78
+ const flood = layer(el, "inset:0;background:var(--kf-accent);opacity:0");
79
+ const ring = layer(el, "left:50%;top:50%;width:40vmin;height:40vmin;margin:-20vmin 0 0 -20vmin;border-radius:50%;border:6px solid var(--kf-accent);opacity:0");
80
+ return (t) => {
81
+ const s = dropBurst(t, { beats: ctx.beats, drop, rules: ctx.style.motion, scene: ctx.id, source: "og-drop" });
82
+ flood.style.opacity = String(Math.round(s.flash * 0.85 * 100) / 100);
83
+ ring.style.opacity = String(s.ring.opacity);
84
+ ring.style.transform = `scale(${s.ring.scale.toFixed(3)})`;
85
+ return s;
86
+ };
87
+ },
88
+ };
@@ -0,0 +1,81 @@
1
+ import { clamp, randRange, spring, type Style } from "@kasaframe/core";
2
+ import type { ThemeSlots } from "@kasaframe/themes/slots";
3
+ import { layer, letters, partial, shownCount, strokePath, traceCanvas } from "@kasaframe/themes/slotkit";
4
+ import { dropPlate, misregister, paperGrain, printFrames } from "./effects.ts";
5
+
6
+ // 장면 문법 패턴이 테마 이름 없이 부르는 자리 — 잉크 판을 한 장씩 찍는 결로. 움직임은 printFrames 로 초당 12장에 끊는다
7
+ export const slots: ThemeSlots = {
8
+ surface(el, { ctx, seed }) {
9
+ return paperGrain(el, { ctx, seed });
10
+ },
11
+
12
+ // 판 두 장 — 블루 테두리 판이 먼저, 핑크 면 판이 한 장 늦게 어긋나 찍힌다. 둘 다 곱하기라 겹친 곳이 보라
13
+ panel(el, { at, seed, tone }) {
14
+ const box = layer(el, "inset:0", true);
15
+ const face = layer(box, `inset:0;mix-blend-mode:multiply;background:${tone === "accent" ? "var(--kf-accent)" : "color-mix(in srgb, var(--kf-accent) 22%, transparent)"}`);
16
+ const edge = layer(box, "inset:0;mix-blend-mode:multiply;border:6px solid var(--kf-ink)");
17
+ const late = at + 1 / 12;
18
+ return (t) => {
19
+ box.style.visibility = t >= at ? "visible" : "hidden";
20
+ edge.style.transform = toTransform(misregister(t, { seed: `${seed}:edge`, px: 3, at }));
21
+ face.style.visibility = t >= late ? "visible" : "hidden";
22
+ face.style.transform = toTransform(misregister(t, { seed: `${seed}:face`, px: 7, at: late }));
23
+ };
24
+ },
25
+
26
+ // 판이 크게 어긋나 찍혔다가 두 장 만에 제자리 — 배율도 초당 12장으로 끊어 내려앉는다
27
+ land(_el, { at, seed }) {
28
+ return (t): Style => {
29
+ if (t < at) return {};
30
+ const q = printFrames(t - at);
31
+ if (q > 0.5) return {};
32
+ const m = misregister(t, { seed, px: 10, at, boil: 0 });
33
+ const k = 1 - clamp(q / 0.5);
34
+ return { x: (m.x ?? 0) * k, y: (m.y ?? 0) * k, scale: spring(q, { from: 1.12, to: 1, stiffness: 300, damping: 18 }) };
35
+ };
36
+ },
37
+
38
+ // 글자가 한 장씩 찍히고, 핑크 판 그림자가 어긋나 비친다(앉은 뒤에도 인쇄 결로 끓는다)
39
+ write(el, { text, at, dur, seed }) {
40
+ const spans = letters(el, text);
41
+ return (t) => {
42
+ const n = shownCount(spans.length, printFrames(t), at, dur);
43
+ spans.forEach((s, i) => (s.style.visibility = i < n ? "visible" : "hidden"));
44
+ const m = misregister(t, { seed, px: 3, at });
45
+ el.style.textShadow = `${(m.x ?? 0).toFixed(1)}px ${(m.y ?? 0).toFixed(1)}px 0 color-mix(in srgb, var(--kf-accent) 75%, transparent)`;
46
+ };
47
+ },
48
+
49
+ // 블루 선이 드럼처럼 끊어 그어지고, 핑크 판 선이 어긋나 뒤따른다
50
+ trace(el, { points, at, dur, seed, tone }) {
51
+ const { g, clear } = traceCanvas(el, points);
52
+ const blue = getComputedStyle(el).getPropertyValue("--kf-ink").trim() || "#3255A4";
53
+ const pink = getComputedStyle(el).getPropertyValue("--kf-accent").trim() || "#FF48B0";
54
+ const dx = randRange(seed, 0, -5, 5);
55
+ const dy = randRange(seed, 1, -5, 5);
56
+ return (t) => {
57
+ clear();
58
+ const p = clamp(printFrames(t - at) / Math.max(0.05, dur));
59
+ if (p <= 0) return;
60
+ g.lineCap = "round";
61
+ g.lineJoin = "round";
62
+ g.globalCompositeOperation = "multiply";
63
+ g.lineWidth = 10;
64
+ g.strokeStyle = tone === "accent" ? pink : blue;
65
+ strokePath(g, partial(points, p));
66
+ g.save();
67
+ g.translate(dx, dy);
68
+ g.strokeStyle = tone === "accent" ? blue : pink;
69
+ g.globalAlpha = 0.7;
70
+ strokePath(g, partial(points, clamp(p - 0.08)));
71
+ g.restore();
72
+ g.globalAlpha = 1;
73
+ };
74
+ },
75
+
76
+ drop(el, { ctx, drop }) {
77
+ return dropPlate(el, { ctx, drop });
78
+ },
79
+ };
80
+
81
+ const toTransform = (s: Style): string => `translate(${(s.x ?? 0).toFixed(2)}px, ${(s.y ?? 0).toFixed(2)}px)`;