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.
- package/NOTICE +10 -8
- package/README.md +16 -1
- package/app/server/engine.ts +133 -15
- package/app/server/main.ts +7 -3
- package/app/web/main.ts +218 -36
- package/app/web/style.css +37 -4
- package/bin/hooks.mjs +14 -1
- package/bin/kf.mjs +20 -2
- package/docs/AGENTS.md +64 -16
- package/music/README.md +31 -7
- package/package.json +2 -2
- package/packages/adapters/src/project.ts +1 -1
- package/packages/check/src/check.ts +10 -3
- package/packages/check/src/rules/assets.ts +4 -1
- package/packages/check/src/rules/frames.ts +2 -1
- package/packages/check/src/rules/layout.ts +30 -6
- package/packages/cli/package.json +2 -1
- package/packages/cli/src/blur.ts +15 -3
- package/packages/cli/src/commands/bench.ts +123 -0
- package/packages/cli/src/commands/blender.ts +72 -49
- package/packages/cli/src/commands/direct.ts +210 -0
- package/packages/cli/src/commands/doctor.ts +9 -1
- package/packages/cli/src/commands/look.ts +9 -2
- package/packages/cli/src/commands/lyrics.ts +47 -1
- package/packages/cli/src/commands/make.ts +729 -150
- package/packages/cli/src/commands/music.ts +78 -0
- package/packages/cli/src/commands/narrate.ts +238 -0
- package/packages/cli/src/commands/new.ts +14 -5
- package/packages/cli/src/commands/render.ts +94 -60
- package/packages/cli/src/commands/series.ts +110 -0
- package/packages/cli/src/commands/setup.ts +42 -3
- package/packages/cli/src/commands/theme.ts +89 -2
- package/packages/cli/src/deps.ts +6 -6
- package/packages/cli/src/kf.ts +14 -3
- package/packages/cli/src/mix.ts +131 -0
- package/packages/cli/src/pool.ts +34 -0
- package/packages/cli/src/segments.ts +402 -0
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/project.ts +11 -0
- package/packages/director/package.json +23 -0
- package/packages/director/src/assemble.ts +361 -0
- package/packages/director/src/brief.ts +122 -0
- package/packages/director/src/catalog.ts +49 -0
- package/packages/director/src/content.ts +343 -0
- package/packages/director/src/fallback.ts +58 -0
- package/packages/director/src/index.ts +13 -0
- package/packages/director/src/pattern.ts +90 -0
- package/packages/director/src/patterns/fill/cells.ts +115 -0
- package/packages/director/src/patterns/formula/stack.ts +93 -0
- package/packages/director/src/patterns/util.ts +58 -0
- package/packages/director/src/scene.ts +62 -0
- package/packages/director/src/select.ts +163 -0
- package/packages/director/src/transitions.ts +115 -0
- package/packages/gen/PROVIDERS.md +39 -4
- package/packages/gen/src/ask.ts +132 -0
- package/packages/gen/src/audio.ts +91 -0
- package/packages/gen/src/blender.ts +54 -0
- package/packages/gen/src/capabilities.ts +151 -0
- package/packages/gen/src/generate.ts +4 -2
- package/packages/gen/src/image/codex.ts +117 -0
- package/packages/gen/src/index.ts +10 -2
- package/packages/gen/src/keys.ts +26 -0
- package/packages/gen/src/music/elevenlabs.ts +41 -0
- package/packages/gen/src/music/index.ts +120 -0
- package/packages/gen/src/music/library.ts +73 -0
- package/packages/gen/src/music/lyria.ts +52 -0
- package/packages/gen/src/music/types.ts +28 -0
- package/packages/gen/src/registry.ts +6 -3
- package/packages/gen/src/tts/dashscope.ts +64 -0
- package/packages/gen/src/tts/elevenlabs.ts +84 -0
- package/packages/gen/src/tts/gemini.ts +73 -0
- package/packages/gen/src/tts/index.ts +241 -0
- package/packages/gen/src/tts/local.ts +42 -0
- package/packages/gen/src/tts/types.ts +44 -0
- package/packages/gen/src/types.ts +4 -0
- package/packages/gen/src/video/h3.ts +5 -0
- package/packages/gen/src/video/http.ts +1 -1
- package/packages/jizura/src/index.ts +2 -2
- package/packages/jizura/src/install.ts +198 -155
- package/packages/jizura/src/layer.ts +75 -32
- package/packages/jizura/src/pin.ts +4 -1
- package/packages/jizura/src/vendor.ts +140 -0
- package/packages/jizura/src/vite.ts +34 -7
- package/packages/jizura/vendor/JIZURA_LICENSE.txt +21 -0
- package/packages/jizura/vendor/NOTICE +43 -0
- package/packages/jizura/vendor/engine.json +47 -0
- package/packages/jizura/vendor/fonts/BlackHanSans-Regular.woff2 +0 -0
- package/packages/jizura/vendor/fonts/DotGothic16-Regular.woff2 +0 -0
- package/packages/jizura/vendor/fonts/GowunBatang-Bold.woff2 +0 -0
- package/packages/jizura/vendor/fonts/IBMPlexMono-Medium.ttf +0 -0
- package/packages/jizura/vendor/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/packages/jizura/vendor/fonts/NotoSansKR-VF.woff2 +0 -0
- package/packages/jizura/vendor/fonts/NotoSerifKR-VF.woff2 +0 -0
- package/packages/jizura/vendor/fonts/OFL-blackhansans.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-dotgothic16.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-gowunbatang.txt +94 -0
- package/packages/jizura/vendor/fonts/OFL-ibmplexmono.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-notosanskr.txt +93 -0
- package/packages/jizura/vendor/fonts/OFL-notoserifkr.txt +94 -0
- package/packages/jizura/vendor/fonts/fonts.json +52 -0
- package/packages/jizura/vendor/jizura_engine.js +35024 -0
- package/packages/motion/README.md +11 -0
- package/packages/motion/package.json +1 -0
- package/packages/motion/src/captions.ts +32 -24
- package/packages/motion/src/moves/brushWipe.ts +14 -1
- package/packages/motion/src/moves/dress.ts +191 -0
- package/packages/motion/src/moves/echoStack.ts +3 -3
- package/packages/motion/src/moves/fit.ts +36 -0
- package/packages/motion/src/moves/ghostStack.ts +14 -5
- package/packages/motion/src/moves/imageSwarm.ts +8 -4
- package/packages/motion/src/moves/index.ts +23 -4
- package/packages/motion/src/moves/karaokeLine.ts +17 -3
- package/packages/motion/src/moves/menu.ts +138 -0
- package/packages/motion/src/moves/neonSign.ts +2 -1
- package/packages/motion/src/moves/slamWords.ts +14 -7
- package/packages/motion/src/moves/sparkTrail.ts +16 -0
- package/packages/motion/src/moves/splitFlap.ts +4 -2
- package/packages/motion/src/moves/stepWords.ts +19 -8
- package/packages/motion/src/moves/tickerRows.ts +13 -9
- package/packages/motion/src/moves/tokenPicker.ts +4 -3
- package/packages/motion/src/moves/wordWall.ts +23 -21
- package/packages/motion/src/moves/worldRoll.ts +2 -1
- package/packages/motion/src/transition3d/index.ts +10 -3
- package/packages/motion/src/transition3d/rig.ts +107 -9
- package/packages/motion/src/transition3d/shatter.ts +5 -2
- package/packages/motion/src/transition3d/space.ts +173 -0
- package/packages/motion/src/transition3d/whip.ts +19 -6
- package/packages/motion/src/transition3d/world.ts +54 -0
- package/packages/motion/src/transitions2d/catalog.ts +1 -1
- package/packages/patterns/package.json +15 -0
- package/packages/patterns/src/bars/rank.ts +119 -0
- package/packages/patterns/src/before-after/shrink.ts +127 -0
- package/packages/patterns/src/before-after/strike.ts +125 -0
- package/packages/patterns/src/code/terminal.ts +133 -0
- package/packages/patterns/src/compare/bars.ts +142 -0
- package/packages/patterns/src/compare/duel.ts +103 -0
- package/packages/patterns/src/end/lockup.ts +85 -0
- package/packages/patterns/src/end/slam.ts +91 -0
- package/packages/patterns/src/flow/graph.ts +250 -0
- package/packages/patterns/src/hook-number/counter.ts +101 -0
- package/packages/patterns/src/hook-number/roll.ts +179 -0
- package/packages/patterns/src/hook-number/slam.ts +91 -0
- package/packages/patterns/src/index.ts +38 -0
- package/packages/patterns/src/kit.ts +449 -0
- package/packages/patterns/src/limit/gauge.ts +117 -0
- package/packages/patterns/src/list/cards.ts +131 -0
- package/packages/patterns/src/list/cycle.ts +22 -0
- package/packages/patterns/src/media/panels.ts +100 -0
- package/packages/patterns/src/objects.ts +234 -0
- package/packages/patterns/src/quote/karaoke.ts +126 -0
- package/packages/patterns/src/quote/slam.ts +22 -0
- package/packages/patterns/src/screen/focus.ts +85 -0
- package/packages/patterns/src/steps/path.ts +137 -0
- package/packages/patterns/src/steps/stack.ts +111 -0
- package/packages/patterns/src/term-reveal/chips.ts +92 -0
- package/packages/patterns/src/term-reveal/expand.ts +127 -0
- package/packages/patterns/src/timeline/axis.ts +144 -0
- package/packages/patterns/src/title/mask.ts +85 -0
- package/packages/patterns/src/title/slam.ts +23 -0
- package/packages/patterns/src/toggle-sim/switch.ts +92 -0
- package/packages/patterns/src/trend/columns.ts +108 -0
- package/packages/patterns/src/trend/draw.ts +140 -0
- package/packages/patterns/src/types.ts +67 -0
- package/packages/patterns/src/words/moves.ts +42 -0
- package/packages/patterns/src/words/slam.ts +4 -0
- package/packages/plan/src/asks.ts +256 -0
- package/packages/plan/src/brief.ts +136 -7
- package/packages/plan/src/captions.ts +5 -2
- package/packages/plan/src/index.ts +6 -2
- package/packages/plan/src/llm.ts +64 -2
- package/packages/plan/src/look.ts +3 -1
- package/packages/plan/src/plan.ts +67 -13
- package/packages/plan/src/series.ts +76 -0
- package/packages/plan/src/types.ts +53 -2
- package/packages/themes/package.json +5 -2
- package/packages/themes/src/index.ts +9 -0
- package/packages/themes/src/slotkit.ts +79 -0
- package/packages/themes/src/slots.ts +46 -0
- package/packages/themes/src/stage3d.ts +20 -0
- package/series/og-shorts.json +14 -0
- package/skills/kasaframe/SKILL.md +4 -3
- package/templates/_shared/project/lib/stage.ts +70 -50
- package/templates/_shared/scenes/default.ts +10 -7
- package/templates/_shared/scenes/drop.ts +11 -8
- package/templates/_shared/scenes/end.ts +9 -6
- package/templates/_shared/scenes/hook.ts +8 -6
- package/templates/_shared/scenes/step.ts +13 -4
- package/templates/ad/template.json +20 -1
- package/templates/demo/template.json +11 -0
- package/templates/explainer/template.json +54 -3
- package/templates/launch/template.json +34 -1
- package/templates/mv/scenes/default.ts +11 -8
- package/templates/mv/template.json +4 -2
- package/templates/promo/template.json +42 -1
- package/templates/shorts/scenes/hook.ts +8 -5
- package/templates/shorts/template.json +72 -3
- package/templates/square/template.json +21 -1
- package/themes/debug-hud/slots.ts +99 -0
- package/themes/og-measure/slots.ts +88 -0
- 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 {
|
|
2
|
+
import type { Dress } from "@kasaframe/motion/moves";
|
|
3
|
+
import { makeStage } from "../lib/stage.ts";
|
|
3
4
|
|
|
4
|
-
const
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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)`;
|