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
|
@@ -17,8 +17,8 @@ argument-hint: "[만들 영상 한 줄]"
|
|
|
17
17
|
```
|
|
18
18
|
kf make "<사람이 한 말 그대로>"
|
|
19
19
|
```
|
|
20
|
-
결과 폴더는 지금 폴더 아래 생긴다(`--out` 으로 바꾼다).
|
|
21
|
-
2. 찍힌 `<폴더>/MAKE.md`(장면 지시서 — 용도별 규칙·장면별 시각·할 일·박·테마 효과·만들
|
|
20
|
+
결과 폴더는 지금 폴더 아래 생긴다(`--out` 으로 바꾼다). 시리즈(「OG 쇼츠」처럼 `kf series list` 의 묶음을 짚으면 그 묶음이 다 정한다)·용도(홍보·쇼츠·설명·공개·시연·광고·피드·뮤비)·테마(「~ 느낌·풍」으로 짚으면 그 테마, 아니면 맨 위 질문으로 묻는다 — 답 전엔 내용에 맞는 승인 테마로 깔린다)·곡(`--music`, 없으면 곡 모음 `KF_MUSIC_DIR` 에서)을 알아서 고른다. **종료 코드 3** 이면 확신이 낮다는 뜻 — 찍힌 보기 중 맞는 것을 `--purpose` 로 다시(그래도 모르겠으면 그때 사람에게 묻는다).
|
|
21
|
+
2. 찍힌 `<폴더>/MAKE.md`(장면 지시서 — 용도별 규칙·장면별 시각·할 일·박·테마 효과·만들 그림·주인공 3D 장면)를 읽는다. **맨 위에 「사용자에게 먼저 물을 것」이 있으면** 장면을 쓰기 전에 AskUserQuestion 한 번으로 묻고(적힌 header·보기 그대로, 첫째가 권장) 답을 `kf make --answer <폴더> --<플래그> <값> …` 로 넣는다 — 테마·목소리와 돈이 드는 생성·없는 능력이 올라온다(테마는 견본 그림 경로가 보기마다 붙는다 — 사람에게 보여 주고 묻는다). **없는 능력(「넣는 법·다른 엔진·빼기」 보기)은 네가 대신 고르지 말고 꼭 사람에게** 묻는다. `kf make` 가 종료 코드 3 으로 보기를 찍고 멈추면(박자·가사 도구처럼 뼈대 전에 필요한 것) 그 보기를 그대로 물어 플래그로 다시 부른다. **쇼츠·설명은 내레이션 먼저** — 지시서의 「내레이션 먼저」대로 `captions.json` 에 장면별 문장을 쓰고 `kf narrate <폴더>` 로 장면 길이를 목소리에 맞춘 뒤(곡이 있으면 박에) 장면을 쓴다. 그다음 고른 공급자로 그림(`kf gen img`)·3D 샷(`kf blender`)·영상 컷(`kf gen vid`)을 만들고 `scenes/*.ts` 를 채운다. 무대 `lib/stage.ts` 를 겉에 씌우면 「0.1초도 쉬지 않기·같은 구도 오래 두지 않기(용도별 기준)·화면 채우기」가 처음부터 선다.
|
|
22
22
|
3. `kf look <폴더>` 가 찍는 장면 모음을 Read 로 직접 보고 장면별 확인표(`out/look.md`)대로 고친다. **그림이 안 보이면**(OG 로 붙인 모델 등)만 `kf look <폴더> --vision-model google/gemini-3.5-flash` — 그림 보는 모델이 장면마다 글로 알려 준다.
|
|
23
23
|
4. `kf make --finish <폴더>` — 렌더·검사·표지·자막. 검사 오류가 0 이 될 때까지 장면을 고쳐 다시(렌더는 고칠 것을 모아 한 번에 — 한 기계에서 하나씩 돈다).
|
|
24
24
|
5. 결과 `out/video.mp4`·`out/cover.png` 를 사람이 보는 공유 폴더에 넣어 보여 준다.
|
|
@@ -27,6 +27,7 @@ argument-hint: "[만들 영상 한 줄]"
|
|
|
27
27
|
|
|
28
28
|
## 하지 않는 것
|
|
29
29
|
|
|
30
|
-
- 돈이 드는 그림·영상 생성을
|
|
30
|
+
- 돈이 드는 그림·영상 생성을 묻기 전에 부르지 않는다 — 사람이 고른 답(또는 `--yes`·`--allow`)이 있을 때만. 「만들 그림」은 받은 자산·실제 화면·도형으로 안 될 때만 구체적인 프롬프트로.
|
|
31
|
+
- 지역 제한이 걸린 공급자(사내 H3)를 회사·바깥 결과물에 쓰지 않는다 — 보기의 경고를 그대로 사람에게 보인다.
|
|
31
32
|
- 슬로건·감성 카피를 화면에 쓰지 않는다(홍보 틀). 이모지 금지.
|
|
32
33
|
- 렌더·검사를 잠금 없이 여러 개 동시에 돌리지 않는다.
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { applyStyle, ease, emit, lerp, progress,
|
|
1
|
+
import { applyStyle, combine, ease, emit, lerp, progress, type SceneCtx, type Style } from "@kasaframe/core";
|
|
2
|
+
import { enter, type MoveName } from "@kasaframe/motion";
|
|
3
|
+
import { dressBg, dressText, dressWipe, type Dress } from "@kasaframe/motion/moves";
|
|
2
4
|
|
|
3
5
|
// 장면 뼈대가 같이 쓰는 무대. 카메라는 가만히 있고 주인공은 안전 영역 가운데에 선다 — 이유 없이 화면 전체를 흔들면
|
|
4
6
|
// 보는 사람이 주인공을 놓친다(카메라 드리프트·박마다 자세를 겹친 판이 쇼츠마다 기울고 가장자리로 밀렸다).
|
|
5
|
-
// 「단 0.1초도 쉬지 않는다」는
|
|
7
|
+
// 「단 0.1초도 쉬지 않는다」는 내용의 움직임이 맡는다 — 글·숫자 수(dress.text)가 박마다 바뀌고, 글 뒤 강조 띠가 박마다 차오른다.
|
|
8
|
+
// 바탕은 정지(still) — 뜻 없이 떠도는 빛 방울·옮겨 가는 판은 보는 사람이 무엇인지 묻게 만들어 걷었다.
|
|
6
9
|
// 카메라는 장면이 이유를 댈 때만 움직인다: stage.camera.pushIn·pullOut·follow·impact. 움직이는 동안만 __kf.events 에 남아
|
|
7
10
|
// kf check 의 camera.wander 가 「이유 없는 이동」과 가른다. 장면을 채울 때 그대로 써도, 지워도 된다.
|
|
11
|
+
// 연출 수(dress)를 주면 바탕·글·경계 덮기를 수 메뉴(@kasaframe/motion/moves menu)에서 건다 — kf new 가 장면마다 시드로 골라 넣는다.
|
|
8
12
|
|
|
9
13
|
export interface Stage {
|
|
10
14
|
box: HTMLDivElement; // 바탕 층(장면 전체) — 늘 흐르는 것은 여기에만
|
|
@@ -15,7 +19,12 @@ export interface Stage {
|
|
|
15
19
|
vertical: boolean;
|
|
16
20
|
/** 주인공 표시 — kf check 의 layout.edge 가 이 상자가 안전 영역 가장자리에 붙는지 본다(bigWord 는 스스로 단다) */
|
|
17
21
|
hero(el: HTMLElement): HTMLElement;
|
|
18
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* 낱말을 고른 글 수(dress.text)로 띄운다 — bigWord 면 큰 낱말 + 등장 움직임. paint 를 매 프레임 부른다(extra 는 그 위에 얹을 흔들림·펀치).
|
|
24
|
+
* ready 면 첫 프레임부터 글이 서 있다(훅·썸네일)
|
|
25
|
+
*/
|
|
26
|
+
say(words: readonly string[], o?: { enter?: MoveName; color?: string; weight?: number; ready?: boolean }): { el: HTMLElement; paint: (t: number, extra?: Style) => void };
|
|
27
|
+
/** 경계 덮기 + 카메라(+ 이름으로 부른 움직이는 바탕). 카메라가 요소를 겨누면(to·follow) 내용을 칠한 뒤 맨 끝에 부른다 — 그 프레임의 자리를 잰다 */
|
|
19
28
|
tick(t: number): void;
|
|
20
29
|
}
|
|
21
30
|
|
|
@@ -175,17 +184,7 @@ export function cutTimes(ctx: SceneCtx): number[] {
|
|
|
175
184
|
return out;
|
|
176
185
|
}
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
// 빛 방울만으로는 작은 그림의 멈춤 판정을 늘 넘지 못한다(방울 자리에 따라 장면마다 들쭉날쭉). 주인공은 그대로 선다
|
|
180
|
-
const PANELS = [
|
|
181
|
-
{ x: 0.07, y: 0.15, w: 0.86, h: 0.7, r: 0.12 },
|
|
182
|
-
{ x: 0, y: 0.55, w: 1, h: 0.45, r: 0.04 },
|
|
183
|
-
{ x: 0.18, y: 0.05, w: 0.64, h: 0.9, r: 0.5 },
|
|
184
|
-
{ x: 0, y: 0, w: 0.5, h: 1, r: 0.04 },
|
|
185
|
-
{ x: 0.05, y: 0.3, w: 0.9, h: 0.4, r: 0.2 },
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
export function makeStage(root: HTMLElement, ctx: SceneCtx, o: { panel?: boolean } = {}): Stage {
|
|
187
|
+
export function makeStage(root: HTMLElement, ctx: SceneCtx, o: { panel?: boolean; dress?: Dress } = {}): Stage {
|
|
189
188
|
const { top, right, bottom, left } = ctx.safe;
|
|
190
189
|
const { width, height } = ctx.meta;
|
|
191
190
|
// root 의 display 는 mountProject 가 장면을 켜고 끌 때 쓴다 — 배치는 안쪽 상자에
|
|
@@ -193,33 +192,30 @@ export function makeStage(root: HTMLElement, ctx: SceneCtx, o: { panel?: boolean
|
|
|
193
192
|
box.style.cssText = "position:absolute;inset:0;overflow:hidden;background:var(--kf-bg)";
|
|
194
193
|
const safeW = width - left - right;
|
|
195
194
|
const safeH = height - top - bottom;
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
panel.style.cssText = `position:absolute;left:${left}px;top:${top}px;width:${safeW}px;height:${safeH}px;transform-origin:0 0;background:var(--kf-accent-soft);opacity:.55`;
|
|
202
|
-
box.append(panel);
|
|
203
|
-
}
|
|
204
|
-
// 바탕 패럴랙스 — 큰 빛 방울 여럿이 서로 다른 빠르기로 흐른다. 카메라가 서 있으니 0.1초 멈춤 판정을 이 흐름이 넘겨야 한다 —
|
|
205
|
-
// 연한 accent-soft 만으로는 작은 그림에서 안 바뀐 것으로 읽혀 accent 를 섞는다. 잔무늬는 작은 그림에 안 잡혀 「멈춤」으로 읽힌다
|
|
206
|
-
const orbs = Array.from({ length: 9 }, (_, i) => {
|
|
207
|
-
const el = document.createElement("div");
|
|
208
|
-
const r = Math.round(Math.min(width, height) * (0.12 + 0.18 * rand(ctx.id, i)));
|
|
209
|
-
el.style.cssText = `position:absolute;left:0;top:0;width:${r * 2}px;height:${r * 2}px;border-radius:50%;background:radial-gradient(circle, color-mix(in srgb, var(--kf-accent) 30%, var(--kf-accent-soft)) 0%, transparent 70%);opacity:${(0.6 + 0.35 * rand(ctx.id, i + 20)).toFixed(2)}`;
|
|
210
|
-
box.append(el);
|
|
211
|
-
// 빠르기는 70–190px/초 — 거의 서 있는 방울이 섞이면 그 사이가 멈춘 프레임이 된다
|
|
212
|
-
const a = rand(ctx.id, i + 80) * Math.PI * 2;
|
|
213
|
-
const v = 70 + 120 * rand(ctx.id, i + 100);
|
|
214
|
-
return { el, r, x0: rand(ctx.id, i + 40) * width, y0: rand(ctx.id, i + 60) * height, vx: v * Math.cos(a), vy: v * Math.sin(a) * 0.7 };
|
|
215
|
-
});
|
|
195
|
+
// 바탕은 정지(still) — 패턴이 제 바탕을 그리면(panel: false) 바탕색만
|
|
196
|
+
const bgLayer = document.createElement("div");
|
|
197
|
+
bgLayer.style.cssText = "position:absolute;inset:0";
|
|
198
|
+
const bg = o.panel === false ? null : dressBg(o.dress?.bg ?? "still", bgLayer, ctx);
|
|
199
|
+
if (o.panel !== false) box.append(bgLayer);
|
|
216
200
|
const cam = document.createElement("div");
|
|
217
201
|
cam.style.cssText = `position:absolute;left:${left}px;top:${top}px;right:${right}px;bottom:${bottom}px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:28px`;
|
|
218
202
|
box.append(cam);
|
|
203
|
+
// 경계 덮기는 맨 위 층 — 앞 장면 끝을 덮은 덮개가 다음 장면 첫 프레임에서 그대로 걷힌다
|
|
204
|
+
const wipes = (["in", "out"] as const)
|
|
205
|
+
.map((side) => {
|
|
206
|
+
const name = side === "in" ? o.dress?.wipeIn : o.dress?.wipeOut;
|
|
207
|
+
if (!name || name === "none") return null;
|
|
208
|
+
const layer = document.createElement("div");
|
|
209
|
+
layer.style.cssText = "position:absolute;inset:0;pointer-events:none;z-index:60";
|
|
210
|
+
const paint = dressWipe(name, layer, ctx, side);
|
|
211
|
+
if (paint) box.append(layer);
|
|
212
|
+
return paint;
|
|
213
|
+
})
|
|
214
|
+
.filter((p): p is (t: number) => void => !!p);
|
|
219
215
|
root.append(box);
|
|
220
216
|
const camera = makeCamera(cam, ctx, { frame: { x: 0, y: 0, w: safeW, h: safeH } });
|
|
221
217
|
const cuts = cutTimes(ctx);
|
|
222
|
-
|
|
218
|
+
const stage: Stage = {
|
|
223
219
|
box,
|
|
224
220
|
cam,
|
|
225
221
|
camera,
|
|
@@ -230,26 +226,50 @@ export function makeStage(root: HTMLElement, ctx: SceneCtx, o: { panel?: boolean
|
|
|
230
226
|
el.dataset.kfHero = "";
|
|
231
227
|
return el;
|
|
232
228
|
},
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
229
|
+
say(words, x = {}) {
|
|
230
|
+
// 글 뒤 강조 띠 — 컷(박)마다 글 폭만큼 왼쪽부터 차오른다(형광펜으로 지금 읽힐 자리를 칠하는 강조).
|
|
231
|
+
// 글 수가 박에 바뀐 뒤 다음 박까지 서 있는 사이에도 내용이 움직인다 — 0.1초 멈춤 규칙을 이 띠가 넘긴다
|
|
232
|
+
const mark = document.createElement("div");
|
|
233
|
+
mark.style.cssText = `position:absolute;left:${Math.round(safeW * 0.1)}px;top:${Math.round(safeH * 0.37)}px;width:${Math.round(safeW * 0.8)}px;height:${Math.round(safeH * 0.26)}px;border-radius:${Math.round(safeH * 0.03)}px;background:color-mix(in srgb, var(--kf-accent) 50%, transparent);transform-origin:0 50%`;
|
|
234
|
+
stage.cam.append(mark);
|
|
235
|
+
const sweep = (t: number) => {
|
|
240
236
|
let k = 0;
|
|
241
|
-
while (k < cuts.length && cuts[k] <= t) k++;
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
237
|
+
while (k + 1 < cuts.length && cuts[k + 1] <= t) k++;
|
|
238
|
+
const a = cuts[k] ?? 0;
|
|
239
|
+
const b = cuts[k + 1] ?? ctx.dur;
|
|
240
|
+
mark.style.transform = `scaleX(${progress(t, a, Math.max(0.05, b - a)).toFixed(4)})`;
|
|
241
|
+
};
|
|
242
|
+
const host = document.createElement("div");
|
|
243
|
+
host.style.cssText = "position:absolute;inset:0";
|
|
244
|
+
const move = o.dress ? dressText(o.dress.text, host, ctx, { words, w: safeW, h: safeH, ready: x.ready }) : null;
|
|
245
|
+
if (move) {
|
|
246
|
+
// 수가 안전 영역 전체를 쓰니 그 상자를 주인공으로 짚으면 늘 가장자리에 붙은 것으로 잡힌다 — 글은 수가 가운데에 세운다
|
|
247
|
+
stage.cam.append(host);
|
|
248
|
+
return {
|
|
249
|
+
el: host,
|
|
250
|
+
paint(t, extra) {
|
|
251
|
+
sweep(t);
|
|
252
|
+
move(t);
|
|
253
|
+
if (extra) applyStyle(host, extra);
|
|
254
|
+
},
|
|
255
|
+
};
|
|
249
256
|
}
|
|
257
|
+
const word = bigWord(words[0] ?? "", stage, { color: x.color, weight: x.weight });
|
|
258
|
+
return {
|
|
259
|
+
el: word,
|
|
260
|
+
paint(t, extra) {
|
|
261
|
+
sweep(t);
|
|
262
|
+
applyStyle(word, combine(x.enter ? enter(x.enter, t, { at: 0, scene: ctx.id }) : {}, extra));
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
tick(t: number) {
|
|
267
|
+
bg?.(t);
|
|
268
|
+
for (const w of wipes) w(t);
|
|
250
269
|
camera.apply(t);
|
|
251
270
|
},
|
|
252
271
|
};
|
|
272
|
+
return stage;
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
// 렌더가 마지막으로 찍는 장면 안 시각 — 되돌이(끝 화면이 첫 화면으로 이어지는 쇼츠·피드)는 ctx.dur 이 아니라 여기서 움직임을 끝낸다
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { SceneCtx } from "@kasaframe/core";
|
|
2
|
+
import type { MoveName } from "@kasaframe/motion";
|
|
3
|
+
import type { Dress } from "@kasaframe/motion/moves";
|
|
4
|
+
import { makeStage } from "../lib/stage.ts";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const WORDS = ["{{word}}", "{{title}}"];
|
|
6
7
|
// 이어지는 두 장면이 같은 등장을 쓰면 검사가 잡는다(repeatSameMove) — kf new 가 장면 순번으로 돌려 넣는다
|
|
7
8
|
const MOVE: MoveName = "{{move}}";
|
|
9
|
+
// 연출 수 — kf new 가 용도·테마 묶음에서 시드로 골랐다(같은 요청이면 같은 수). 바꾸려면 MAKE.md 「연출 수」 메뉴의 이름으로
|
|
10
|
+
const DRESS: Dress = { bg: "{{bg}}", text: "{{text}}", wipeIn: "{{wipeIn}}", wipeOut: "{{wipeOut}}" };
|
|
8
11
|
|
|
9
12
|
export default {
|
|
10
13
|
build(root: HTMLElement, ctx: SceneCtx) {
|
|
11
|
-
const stage = makeStage(root, ctx);
|
|
12
|
-
const
|
|
14
|
+
const stage = makeStage(root, ctx, { dress: DRESS });
|
|
15
|
+
const words = stage.say(WORDS, { enter: MOVE });
|
|
13
16
|
return (t: number) => {
|
|
17
|
+
words.paint(t);
|
|
14
18
|
stage.tick(t);
|
|
15
|
-
applyStyle(word, enter(MOVE, t, { at: 0, scene: ctx.id }));
|
|
16
19
|
};
|
|
17
20
|
},
|
|
18
21
|
};
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import { applyStyle, combine, type SceneCtx } from "@kasaframe/core";
|
|
2
|
-
import { beatPulse, dropBurst,
|
|
3
|
-
import {
|
|
2
|
+
import { beatPulse, dropBurst, type MoveName } from "@kasaframe/motion";
|
|
3
|
+
import type { Dress } from "@kasaframe/motion/moves";
|
|
4
|
+
import { makeStage } from "../lib/stage.ts";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const WORDS = ["{{word}}", "{{title}}"];
|
|
6
7
|
const MOVE: MoveName = "{{move}}";
|
|
8
|
+
const DRESS: Dress = { bg: "{{bg}}", text: "{{text}}", wipeIn: "{{wipeIn}}", wipeOut: "{{wipeOut}}" };
|
|
7
9
|
|
|
8
10
|
// 장면 시작이 드롭에 붙어 있다(template anchor drop:k). 섬광·충격파·흔들림은 dropBurst 가 style.json 규칙(드롭 전용·maxPx)대로 내고
|
|
9
11
|
// window.__kf.events 에 남겨 검사가 센다 — 손으로 짜면 규칙 위반을 못 잡는다
|
|
10
12
|
export default {
|
|
11
13
|
build(root: HTMLElement, ctx: SceneCtx) {
|
|
12
|
-
const stage = makeStage(root, ctx);
|
|
14
|
+
const stage = makeStage(root, ctx, { dress: DRESS });
|
|
13
15
|
const ring = document.createElement("div");
|
|
14
16
|
ring.style.cssText = "position:absolute;left:50%;top:50%;width:600px;height:600px;margin:-300px 0 0 -300px;border-radius:50%;border:12px solid var(--kf-accent)";
|
|
15
17
|
stage.box.insertBefore(ring, stage.cam);
|
|
16
|
-
const
|
|
18
|
+
const words = stage.say(WORDS, { enter: MOVE, weight: 900, ready: true });
|
|
17
19
|
const flash = document.createElement("div");
|
|
18
20
|
flash.style.cssText = "position:absolute;inset:0;background:var(--kf-accent)";
|
|
19
21
|
stage.box.append(flash);
|
|
20
22
|
const drop = ctx.beats.drops.findIndex((d) => d >= -0.05 && d < ctx.dur);
|
|
21
23
|
return (t: number) => {
|
|
22
|
-
stage.tick(t);
|
|
23
24
|
const b = drop >= 0 ? dropBurst(t, { beats: ctx.beats, drop, rules: ctx.style.motion, scene: ctx.id }) : null;
|
|
24
25
|
flash.style.opacity = String((b?.flash ?? 0) * 0.85);
|
|
25
26
|
applyStyle(ring, b ? { scale: b.ring.scale, opacity: b.ring.opacity } : { opacity: 0 });
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// 박 펄스는 큰 낱말에만 — 글 수는 스스로 박에 터지고, 수가 깐 판 전체를 박마다 키우면 가장자리가 깜빡여 섬광 검사에 걸린다
|
|
28
|
+
const pulse = DRESS.text === "bigWord" ? beatPulse(t, { beats: ctx.beats, rules: ctx.style.motion, scene: ctx.id, source: "drop-word" }) : undefined;
|
|
29
|
+
words.paint(t, combine(b?.shake, b?.punch, pulse));
|
|
30
|
+
stage.tick(t);
|
|
28
31
|
};
|
|
29
32
|
},
|
|
30
33
|
};
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { applyStyle, type SceneCtx } from "@kasaframe/core";
|
|
2
2
|
import { enter, type MoveName } from "@kasaframe/motion";
|
|
3
|
-
import {
|
|
3
|
+
import type { Dress } from "@kasaframe/motion/moves";
|
|
4
|
+
import { fitPx, makeStage } from "../lib/stage.ts";
|
|
4
5
|
|
|
5
6
|
const NAME = "{{title}}";
|
|
6
7
|
const LINE = "{{line}}";
|
|
7
8
|
const MOVE: MoveName = "{{move}}";
|
|
9
|
+
const DRESS: Dress = { bg: "{{bg}}", text: "{{text}}", wipeIn: "{{wipeIn}}", wipeOut: "{{wipeOut}}" };
|
|
8
10
|
|
|
9
11
|
export default {
|
|
10
12
|
build(root: HTMLElement, ctx: SceneCtx) {
|
|
11
|
-
const stage = makeStage(root, ctx);
|
|
12
|
-
const name =
|
|
13
|
+
const stage = makeStage(root, ctx, { dress: DRESS });
|
|
14
|
+
const name = stage.say([NAME], { enter: MOVE });
|
|
13
15
|
const line = document.createElement("div");
|
|
14
16
|
line.textContent = LINE;
|
|
15
|
-
|
|
17
|
+
// 이름 수가 안전 영역을 다 쓰니 한 줄은 아래 띠에 — 이름과 겹치지 않는다
|
|
18
|
+
line.style.cssText = `position:absolute;left:0;right:0;bottom:4%;z-index:5;font-family:var(--kf-font-sans);font-size:${fitPx(LINE, stage.safeW, { share: 0.6, max: 64, min: 40 })}px;color:var(--kf-dim);word-break:keep-all;text-align:center`;
|
|
16
19
|
if (LINE) stage.cam.append(line);
|
|
17
20
|
return (t: number) => {
|
|
18
|
-
|
|
19
|
-
applyStyle(name, enter(MOVE, t, { at: 0, scene: ctx.id }));
|
|
21
|
+
name.paint(t);
|
|
20
22
|
applyStyle(line, enter("fade", t, { at: 0.25, scene: ctx.id }));
|
|
23
|
+
stage.tick(t);
|
|
21
24
|
};
|
|
22
25
|
},
|
|
23
26
|
};
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { clip, ease, type SceneCtx } from "@kasaframe/core";
|
|
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
|
// 훅은 첫 프레임부터 내용이 있어야 한다 — 페이드로 시작하면 썸네일·첫 화면이 빈다
|
|
7
9
|
export default {
|
|
8
10
|
build(root: HTMLElement, ctx: SceneCtx) {
|
|
9
|
-
const stage = makeStage(root, ctx);
|
|
10
|
-
const
|
|
11
|
+
const stage = makeStage(root, ctx, { dress: DRESS });
|
|
12
|
+
const words = stage.say(WORDS, { color: "var(--kf-accent)", weight: 900, ready: true });
|
|
11
13
|
return (t: number) => {
|
|
14
|
+
words.paint(t, { scale: 1.12 - 0.12 * ease.outBack(clip(t, 0, 0.5).p) });
|
|
12
15
|
stage.tick(t);
|
|
13
|
-
applyStyle(word, { scale: 1.12 - 0.12 * ease.outBack(clip(t, 0, 0.5).p) });
|
|
14
16
|
};
|
|
15
17
|
},
|
|
16
18
|
};
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { applyStyle, type SceneCtx } from "@kasaframe/core";
|
|
2
2
|
import { cursor, enter, terminal, type MoveName } from "@kasaframe/motion";
|
|
3
|
+
import type { Dress } from "@kasaframe/motion/moves";
|
|
3
4
|
import { makeStage } from "../lib/stage.ts";
|
|
4
5
|
|
|
5
6
|
const STEP = "{{word}}";
|
|
6
7
|
const MOVE: MoveName = "{{move}}";
|
|
7
8
|
const COMMANDS = [{ cmd: "kf new demo --template demo", out: ["장면 6개 · 60초"] }];
|
|
9
|
+
// 창이 주인공이라 글 수는 안 쓰고 바탕·경계 덮기만 건다
|
|
10
|
+
const DRESS: Dress = { bg: "{{bg}}", text: "bigWord", wipeIn: "{{wipeIn}}", wipeOut: "{{wipeOut}}" };
|
|
8
11
|
|
|
9
12
|
// 시연 한 단계 — 창(터미널) 하나 + 누를 자리로 다가가는 카메라 + 커서 + 단계 번호. 화면 녹화가 있으면 창 안을 <video data-kf-from> 로 바꾼다
|
|
10
13
|
export default {
|
|
11
14
|
build(root: HTMLElement, ctx: SceneCtx) {
|
|
12
15
|
// 흐르는 바탕·컷마다 바뀌는 판이 「쉬지 않기」를 진다 — 창은 가만히 서고, 카메라는 누른 뒤 한 번만 다가간다
|
|
13
|
-
const stage = makeStage(root, ctx);
|
|
16
|
+
const stage = makeStage(root, ctx, { dress: DRESS });
|
|
14
17
|
const cam = document.createElement("div");
|
|
15
18
|
// 창이 바탕을 다 덮으면 흐르는 바탕이 안 보여 멈춘 화면이 된다 — 둘레를 남긴다
|
|
16
19
|
cam.style.cssText = "position:relative;width:min(1100px, 70%)";
|
|
@@ -25,12 +28,18 @@ export default {
|
|
|
25
28
|
cam.append(win, badge, pointer);
|
|
26
29
|
stage.cam.append(cam);
|
|
27
30
|
const click = Math.min(1.2, ctx.dur * 0.3);
|
|
31
|
+
const typed = 0.5 + COMMANDS[0].cmd.length / 16 + 0.3;
|
|
32
|
+
const FRAMES = Math.round(ctx.dur * ctx.meta.fps);
|
|
28
33
|
stage.camera.pushIn(click, { to: () => ({ x: cam.offsetLeft + 0.42 * cam.offsetWidth, y: cam.offsetTop + 0.45 * cam.offsetHeight }), scale: 1.12, dur: 1.2 });
|
|
29
34
|
return (t: number) => {
|
|
30
35
|
const st = terminal(t, COMMANDS, { at: 0.5, cps: 16 });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// 명령을 친 뒤에는 그 일의 진행이 구른다 — 프레임 수가 장면 끝까지 차오른다(쉬지 않기를 뜻 있는 숫자가 진다)
|
|
37
|
+
const done = Math.round(FRAMES * Math.min(1, Math.max(0, (t - typed) / Math.max(0.5, ctx.dur - typed))));
|
|
38
|
+
const run = t > typed ? `<div style="color:var(--kf-dim)">프레임 ${String(done).padStart(4, " ")} / ${FRAMES}</div>` : "";
|
|
39
|
+
win.innerHTML =
|
|
40
|
+
st.lines
|
|
41
|
+
.map((l) => `<div>${l.kind === "cmd" ? `<span style="color:var(--kf-accent)">${l.prompt} </span>` : ""}${l.text.replace(/</g, "<")}${l.caret ? '<span style="display:inline-block;width:.55em;height:1.1em;vertical-align:-.2em;background:var(--kf-accent)"></span>' : ""}</div>`)
|
|
42
|
+
.join("") + run;
|
|
34
43
|
const c = cursor(t, [{ t: 0, x: 900, y: 520 }, { t: click, x: 300, y: 120, click: true }, { t: ctx.dur, x: 360, y: 160 }]);
|
|
35
44
|
applyStyle(pointer, { x: c.x, y: c.y, scale: c.pressed ? 0.9 : 1 });
|
|
36
45
|
applyStyle(cam, enter(MOVE, t, { at: 0, scene: ctx.id }));
|
|
@@ -69,6 +69,24 @@
|
|
|
69
69
|
},
|
|
70
70
|
"beatMove": "every-beat"
|
|
71
71
|
},
|
|
72
|
+
"moves": {
|
|
73
|
+
"text": [
|
|
74
|
+
"slamWords",
|
|
75
|
+
"splitFlap",
|
|
76
|
+
"ransomLetters"
|
|
77
|
+
],
|
|
78
|
+
"wipe": [
|
|
79
|
+
"brushWipe",
|
|
80
|
+
"flood"
|
|
81
|
+
],
|
|
82
|
+
"roles": {
|
|
83
|
+
"brand": {
|
|
84
|
+
"text": [
|
|
85
|
+
"slamWords"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
72
90
|
"scenes": [
|
|
73
91
|
{
|
|
74
92
|
"id": "brand",
|
|
@@ -101,7 +119,8 @@
|
|
|
101
119
|
],
|
|
102
120
|
"gen": [
|
|
103
121
|
"img:message-hero"
|
|
104
|
-
]
|
|
122
|
+
],
|
|
123
|
+
"act": "message"
|
|
105
124
|
},
|
|
106
125
|
"deliverables": [
|
|
107
126
|
"mp4",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "explainer",
|
|
3
3
|
"label": "설명",
|
|
4
|
-
"summary": "원리 설명 — 도식 하나를 단계마다 키워 가며, 화면 글은 전문 낱말만,
|
|
4
|
+
"summary": "원리 설명 — 도식 하나를 단계마다 키워 가며, 화면 글은 전문 낱말만, 차분한 내레이션·자막 트랙·깊은 앰비언트 곡",
|
|
5
5
|
"aspect": "16:9",
|
|
6
6
|
"width": 1920,
|
|
7
7
|
"height": 1080,
|
|
@@ -23,7 +23,29 @@
|
|
|
23
23
|
"max": 120,
|
|
24
24
|
"default": 90
|
|
25
25
|
},
|
|
26
|
-
"music": "
|
|
26
|
+
"music": "required",
|
|
27
|
+
"mood": "deep",
|
|
28
|
+
"narration": {
|
|
29
|
+
"on": true,
|
|
30
|
+
"lang": "ko",
|
|
31
|
+
"style": "차분하고 낮은 다큐멘터리 내레이션. 서두르지 않고 문장 끝을 내려 놓는다",
|
|
32
|
+
"voice": "elevenlabs:George",
|
|
33
|
+
"voices": {
|
|
34
|
+
"elevenlabs": "George",
|
|
35
|
+
"gemini": "Charon",
|
|
36
|
+
"dashscope": "Neil",
|
|
37
|
+
"local": "Sohee"
|
|
38
|
+
},
|
|
39
|
+
"lead": 0.25,
|
|
40
|
+
"tail": 0.5
|
|
41
|
+
},
|
|
42
|
+
"audio": {
|
|
43
|
+
"musicDb": -20,
|
|
44
|
+
"duck": true,
|
|
45
|
+
"lufs": -14,
|
|
46
|
+
"truePeak": -1.5,
|
|
47
|
+
"fadeOut": 1.5
|
|
48
|
+
},
|
|
27
49
|
"captions": "off",
|
|
28
50
|
"loop": false,
|
|
29
51
|
"style": "og",
|
|
@@ -54,6 +76,25 @@
|
|
|
54
76
|
},
|
|
55
77
|
"beatMove": "drop-sections"
|
|
56
78
|
},
|
|
79
|
+
"moves": {
|
|
80
|
+
"text": [
|
|
81
|
+
"bigWord",
|
|
82
|
+
"ghostStack",
|
|
83
|
+
"splitFlap"
|
|
84
|
+
],
|
|
85
|
+
"wipe": [
|
|
86
|
+
"irisBlades",
|
|
87
|
+
"flood",
|
|
88
|
+
"none"
|
|
89
|
+
],
|
|
90
|
+
"roles": {
|
|
91
|
+
"hook": {
|
|
92
|
+
"text": [
|
|
93
|
+
"slamWords"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
57
98
|
"scenes": [
|
|
58
99
|
{
|
|
59
100
|
"id": "hook",
|
|
@@ -69,6 +110,14 @@
|
|
|
69
110
|
"snap": "beat",
|
|
70
111
|
"idea": "질문 하나 — 왜 그런가. 낱말 하나와 도식의 빈자리"
|
|
71
112
|
},
|
|
113
|
+
{
|
|
114
|
+
"id": "turn",
|
|
115
|
+
"role": "seam",
|
|
116
|
+
"sec": 1.5,
|
|
117
|
+
"snap": "beat",
|
|
118
|
+
"transition": "portal",
|
|
119
|
+
"idea": "이음매 — question 의 빈자리가 문이 되어 mechanism 도식 안으로 들어간다(3D 전환, 틀이 맡는다 — 장면 파일은 그대로 둔다)"
|
|
120
|
+
},
|
|
72
121
|
{
|
|
73
122
|
"id": "mechanism",
|
|
74
123
|
"role": "explain",
|
|
@@ -104,7 +153,9 @@
|
|
|
104
153
|
"data",
|
|
105
154
|
"logo"
|
|
106
155
|
],
|
|
107
|
-
"gen": []
|
|
156
|
+
"gen": [],
|
|
157
|
+
"shot3d": "mechanism",
|
|
158
|
+
"act": "example"
|
|
108
159
|
},
|
|
109
160
|
"deliverables": [
|
|
110
161
|
"mp4",
|
|
@@ -42,6 +42,37 @@
|
|
|
42
42
|
},
|
|
43
43
|
"beatMove": "every-beat"
|
|
44
44
|
},
|
|
45
|
+
"moves": {
|
|
46
|
+
"text": [
|
|
47
|
+
"ghostStack",
|
|
48
|
+
"splitFlap",
|
|
49
|
+
"slamWords",
|
|
50
|
+
"echoStack"
|
|
51
|
+
],
|
|
52
|
+
"wipe": [
|
|
53
|
+
"irisBlades",
|
|
54
|
+
"flood",
|
|
55
|
+
"brushWipe"
|
|
56
|
+
],
|
|
57
|
+
"roles": {
|
|
58
|
+
"tease": {
|
|
59
|
+
"text": [
|
|
60
|
+
"ghostStack"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"reveal": {
|
|
64
|
+
"text": [
|
|
65
|
+
"slamWords"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"end": {
|
|
69
|
+
"text": [
|
|
70
|
+
"neonSign",
|
|
71
|
+
"splitFlap"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
45
76
|
"scenes": [
|
|
46
77
|
{
|
|
47
78
|
"id": "tease",
|
|
@@ -98,7 +129,9 @@
|
|
|
98
129
|
],
|
|
99
130
|
"gen": [
|
|
100
131
|
"img:reveal-hero"
|
|
101
|
-
]
|
|
132
|
+
],
|
|
133
|
+
"shot3d": "reveal",
|
|
134
|
+
"act": "tease"
|
|
102
135
|
},
|
|
103
136
|
"deliverables": [
|
|
104
137
|
"mp4",
|
|
@@ -20,14 +20,11 @@ function plans(ctx: SceneCtx) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// 뮤비 한 구간 — 바탕은 잰 마디마다(드롭 구간은 박마다) 색·자리를 바꾸고, 가사는 JIZURA 층이 부른 시각에 그린다.
|
|
23
|
-
//
|
|
23
|
+
// lyrics.json 이 없거나 사람이 낱말로를 골랐으면(kf lyrics use <폴더> words) 부른 낱말을 DOM 으로 띄운다
|
|
24
24
|
export default {
|
|
25
25
|
build(root: HTMLElement, ctx: SceneCtx) {
|
|
26
|
-
//
|
|
26
|
+
// 「쉬지 않기」는 가사가 진다 — 가사 층, 없으면 부르는 낱말이 왼쪽부터 차오른다. 바탕은 정지(마디마다 색만 바뀐다)
|
|
27
27
|
const st = makeStage(root, ctx);
|
|
28
|
-
const dots = document.createElement("div");
|
|
29
|
-
dots.style.cssText = "position:absolute;inset:-120px;background-image:radial-gradient(circle, var(--kf-line) 3px, transparent 3.6px);background-size:60px 60px";
|
|
30
|
-
st.box.insertBefore(dots, st.box.firstChild);
|
|
31
28
|
const stage = document.createElement("div");
|
|
32
29
|
stage.style.cssText = "position:absolute;inset:0;display:flex;flex-wrap:wrap;align-content:center;justify-content:center;padding:8% 10%;box-sizing:border-box;container-type:size";
|
|
33
30
|
st.cam.append(stage);
|
|
@@ -39,7 +36,6 @@ export default {
|
|
|
39
36
|
const decided = jizuraReady().then((ok) => (jz = ok));
|
|
40
37
|
|
|
41
38
|
const lines = (lyrics?.lines ?? []).filter((l) => l.end > ctx.from && l.start < ctx.from + ctx.dur).map((l) => ({ ...l, ws: wordsOf(l) }));
|
|
42
|
-
// accent-soft 바탕에는 같은 계열인 빛 방울(늘 흐르는 몫)이 묻혀 가사가 서 있는 동안 0.1초 멈춤이 된다 — 빼 둔다
|
|
43
39
|
const tones = ["var(--kf-bg)", "var(--kf-surface)", "var(--kf-raised)"];
|
|
44
40
|
const spots = ["center", "flex-start", "flex-end"];
|
|
45
41
|
let shown = "";
|
|
@@ -62,7 +58,15 @@ export default {
|
|
|
62
58
|
});
|
|
63
59
|
}
|
|
64
60
|
const pulse = beatPulse(t, { beats: ctx.beats, rules: ctx.style.motion, scene: ctx.id, source: "lyric" });
|
|
65
|
-
spans.forEach((s, i) =>
|
|
61
|
+
spans.forEach((s, i) => {
|
|
62
|
+
applyStyle(s, combine(enter("pop", T, { at: ws[i]?.start ?? 0, scene: ctx.id }), pulse));
|
|
63
|
+
// 부르는 동안 왼쪽부터 강조색이 찬다 — 부른 데까지가 보인다
|
|
64
|
+
const w = ws[i];
|
|
65
|
+
const p = w ? Math.min(1, Math.max(0, (T - w.start) / Math.max(0.05, w.end - w.start))) : 1;
|
|
66
|
+
s.style.backgroundImage = `linear-gradient(90deg, var(--kf-accent) ${(p * 100).toFixed(2)}%, var(--kf-ink) ${(p * 100).toFixed(2)}%)`;
|
|
67
|
+
s.style.backgroundClip = "text";
|
|
68
|
+
s.style.color = "transparent";
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
let last = 0;
|
|
@@ -70,7 +74,6 @@ export default {
|
|
|
70
74
|
const draw = (t: number) => {
|
|
71
75
|
last = t;
|
|
72
76
|
undecided = jz === null;
|
|
73
|
-
dots.style.transform = `translate(${((t * 24) % 60).toFixed(2)}px, ${((t * 36) % 60).toFixed(2)}px)`;
|
|
74
77
|
const inDrop = ctx.beats.inDrop(t);
|
|
75
78
|
const grid = inDrop ? ctx.beats.beats : ctx.beats.bars;
|
|
76
79
|
const cut = Math.max(0, grid.filter((g) => g <= t).length - 1);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lyrics": {
|
|
29
29
|
"mode": "kinetic",
|
|
30
30
|
"align": "kf lyrics align <곡> --text 공식가사.txt --lang ko — 보컬 분리 + whisper 강제 정렬로 줄·어절 시각 → lyrics.json(공식 가사가 글자 정본, MMS 는 비상업 가중치라 안 씀)",
|
|
31
|
-
"layer": "JIZURA 가사 층(@kasaframe/jizura jizuraLayer) —
|
|
31
|
+
"layer": "JIZURA 가사 층(@kasaframe/jizura jizuraLayer) — 엔진·자주 쓰는 한글 글꼴은 실려 있다, lyrics.json 이 없으면 DOM 낱말",
|
|
32
32
|
"lipsync": "rhubarb — 캐릭터 입 모양을 보컬에 맞출 자리(선택)"
|
|
33
33
|
},
|
|
34
34
|
"loop": false,
|
|
@@ -144,7 +144,9 @@
|
|
|
144
144
|
"img:character",
|
|
145
145
|
"img:world-bg"
|
|
146
146
|
],
|
|
147
|
-
"genVideo": []
|
|
147
|
+
"genVideo": [],
|
|
148
|
+
"shot3d": "drop",
|
|
149
|
+
"act": "main"
|
|
148
150
|
},
|
|
149
151
|
"deliverables": [
|
|
150
152
|
"mp4",
|