kasaframe 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +311 -0
- package/README.md +18 -5
- package/docs/AGENTS.md +2 -1
- package/package.json +1 -1
- package/packages/adapters/src/project.ts +2 -0
- package/packages/check/package.json +1 -0
- package/packages/check/src/check.ts +8 -2
- package/packages/check/src/rules/frames.ts +4 -5
- package/packages/cli/src/commands/doctor.ts +15 -2
- package/packages/cli/src/commands/fonts.ts +118 -0
- package/packages/cli/src/commands/make.ts +15 -5
- package/packages/cli/src/commands/render.ts +34 -7
- package/packages/cli/src/commands/update.ts +66 -0
- package/packages/cli/src/fonts.ts +405 -0
- package/packages/cli/src/kf.ts +5 -0
- package/packages/cli/src/mix.ts +66 -4
- package/packages/cli/src/segments.ts +1 -1
- package/packages/cli/src/sfx.ts +429 -0
- package/packages/cli/src/update.ts +250 -0
- package/packages/core/src/frame.ts +7 -4
- package/packages/core/src/index.ts +3 -1
- package/packages/core/src/project.ts +19 -0
- package/packages/core/src/sfx.ts +70 -0
- package/packages/director/src/assemble.ts +7 -4
- package/packages/director/src/index.ts +1 -1
- package/packages/director/src/pattern.ts +2 -0
- package/packages/director/src/scene.ts +14 -5
- package/packages/director/src/transitions.ts +21 -8
- package/packages/motion/src/enter.ts +7 -1
- package/packages/motion/src/impact.ts +5 -2
- package/packages/motion/src/moves/dress.ts +15 -2
- package/packages/motion/src/moves/menu.ts +11 -5
- package/packages/motion/src/transition.ts +3 -1
- package/packages/motion/src/transition3d/index.ts +3 -1
- package/packages/motion/src/transition3d/live.ts +5 -3
- package/packages/patterns/src/bars/rank.ts +6 -7
- package/packages/patterns/src/before-after/shrink.ts +3 -4
- package/packages/patterns/src/before-after/strike.ts +4 -5
- package/packages/patterns/src/code/terminal.ts +25 -16
- package/packages/patterns/src/end/lockup.ts +13 -10
- package/packages/patterns/src/end/slam.ts +7 -5
- package/packages/patterns/src/hook-number/counter.ts +15 -9
- package/packages/patterns/src/hook-number/slam.ts +6 -8
- package/packages/patterns/src/kit.ts +281 -58
- package/packages/patterns/src/limit/gauge.ts +16 -11
- package/packages/patterns/src/quote/karaoke.ts +17 -12
- package/packages/patterns/src/sfx.ts +80 -0
- package/packages/patterns/src/term-reveal/chips.ts +24 -10
- package/packages/patterns/src/term-reveal/expand.ts +17 -13
- package/packages/patterns/src/title/mask.ts +9 -8
- package/packages/patterns/src/title/slam.ts +1 -1
- package/packages/patterns/src/types.ts +1 -0
- package/packages/plan/src/asks.ts +1 -1
- package/packages/plan/src/brief.ts +5 -2
- package/packages/plan/src/themes.ts +8 -2
- package/packages/themes/kit/THEME-MAKE.md +2 -2
- package/packages/themes/kit/fonts/display.json +3517 -0
- package/packages/themes/kit/fonts/sans.json +5155 -0
- package/packages/themes/kit/fonts/serif.json +5229 -0
- package/packages/themes/kit/fonts.json +243 -79
- package/packages/themes/package.json +3 -1
- package/packages/themes/src/fontfile.ts +183 -0
- package/packages/themes/src/fonts.ts +296 -0
- package/packages/themes/src/index.ts +32 -2
- package/packages/themes/src/look.ts +34 -8
- package/packages/themes/src/slots.ts +32 -2
- package/packages/themes/src/types.ts +33 -0
- package/packages/themes/src/vary.ts +35 -34
- package/presets/kawaii-pastel.json +2 -2
- package/sfx/README.md +82 -0
- package/sfx/click/appear.wav +0 -0
- package/sfx/click/burst.wav +0 -0
- package/sfx/click/drop.wav +0 -0
- package/sfx/click/end.wav +0 -0
- package/sfx/click/hop.wav +0 -0
- package/sfx/click/land.wav +0 -0
- package/sfx/click/select.wav +0 -0
- package/sfx/click/stick.wav +0 -0
- package/sfx/click/tick.wav +0 -0
- package/sfx/click/type.wav +0 -0
- package/sfx/click/whoosh.wav +0 -0
- package/sfx/pop/appear.wav +0 -0
- package/sfx/pop/burst.wav +0 -0
- package/sfx/pop/drop.wav +0 -0
- package/sfx/pop/end.wav +0 -0
- package/sfx/pop/hop.wav +0 -0
- package/sfx/pop/land.wav +0 -0
- package/sfx/pop/select.wav +0 -0
- package/sfx/pop/stick.wav +0 -0
- package/sfx/pop/tick.wav +0 -0
- package/sfx/pop/type.wav +0 -0
- package/sfx/pop/whoosh.wav +0 -0
- package/sfx/tags.json +545 -0
- package/sfx/ui/appear.wav +0 -0
- package/sfx/ui/burst.wav +0 -0
- package/sfx/ui/drop.wav +0 -0
- package/sfx/ui/end.wav +0 -0
- package/sfx/ui/hop.wav +0 -0
- package/sfx/ui/land.wav +0 -0
- package/sfx/ui/select.wav +0 -0
- package/sfx/ui/stick.wav +0 -0
- package/sfx/ui/tick.wav +0 -0
- package/sfx/ui/type.wav +0 -0
- package/sfx/ui/whoosh.wav +0 -0
- package/skills/kasaframe/SKILL.md +1 -0
- package/themes/blueprint/sample/style.json +5 -5
- package/themes/blueprint/style.json +5 -5
- package/themes/blueprint/theme.json +3 -1
- package/themes/blueprint/variants/deep-rose-suit.json +1 -1
- package/themes/blueprint/variants/navy-coral-2.json +1 -1
- package/themes/blueprint/variants/navy-coral-suit.json +1 -1
- package/themes/blueprint/variants/navy-orange-2.json +1 -1
- package/themes/blueprint/variants/navy-orange-suit.json +1 -1
- package/themes/cartography/sample/style.json +2 -2
- package/themes/cartography/style.json +2 -2
- package/themes/cartography/theme.json +3 -1
- package/themes/cartography/variants/deep-plum.json +1 -1
- package/themes/cartography/variants/frost-teal.json +1 -1
- package/themes/cartography/variants/navy-green.json +1 -1
- package/themes/circuit-board/sample/style.json +2 -2
- package/themes/circuit-board/style.json +2 -2
- package/themes/circuit-board/theme.json +3 -1
- package/themes/circuit-board/variants/dusk-gold-pretendard.json +1 -1
- package/themes/circuit-board/variants/navy-gold-grain.json +1 -1
- package/themes/circuit-board/variants/wine-gold-pretendard.json +1 -1
- package/themes/clay-toy/sample/style.json +2 -2
- package/themes/clay-toy/style.json +2 -2
- package/themes/clay-toy/theme.json +3 -1
- package/themes/clay-toy/variants/blush-coral-grain.json +1 -1
- package/themes/clay-toy/variants/chalk-teal.json +1 -1
- package/themes/clay-toy/variants/frost-sky.json +1 -1
- package/themes/clay-toy/variants/frost-teal.json +1 -1
- package/themes/clay-toy/variants/lilac-plum-round.json +1 -1
- package/themes/clay-toy/variants/lilac-plum.json +1 -1
- package/themes/clay-toy/variants/sage-indigo.json +1 -1
- package/themes/conveyor-factory/sample/style.json +3 -3
- package/themes/conveyor-factory/style.json +3 -3
- package/themes/conveyor-factory/theme.json +3 -1
- package/themes/conveyor-factory/variants/cocoa-gold.json +1 -1
- package/themes/conveyor-factory/variants/navy-gold.json +1 -1
- package/themes/conveyor-factory/variants/slate-amber-2.json +1 -1
- package/themes/conveyor-factory/variants/slate-amber-suit.json +1 -1
- package/themes/conveyor-factory/variants/slate-gold-bold.json +1 -1
- package/themes/data-journal/theme.json +3 -1
- package/themes/data-journal/variants/deep-coral.json +1 -1
- package/themes/data-journal/variants/navy-lime.json +1 -1
- package/themes/data-journal/variants/wine-indigo.json +1 -1
- package/themes/debug-hud/sample/style.json +1 -1
- package/themes/debug-hud/style.json +1 -1
- package/themes/debug-hud/theme.json +2 -1
- package/themes/debug-hud/variants/cocoa-teal.json +2 -2
- package/themes/debug-hud/variants/deep-teal-scan.json +2 -2
- package/themes/debug-hud/variants/dusk-plum.json +2 -2
- package/themes/debug-hud/variants/forest-lime.json +2 -2
- package/themes/debug-hud/variants/slate-green.json +2 -2
- package/themes/debug-hud/variants/wine-gold.json +2 -2
- package/themes/depth-flight/sample/style.json +1 -1
- package/themes/depth-flight/style.json +1 -1
- package/themes/depth-flight/theme.json +3 -1
- package/themes/depth-flight/variants/cocoa-cyan.json +1 -1
- package/themes/depth-flight/variants/cocoa-violet.json +1 -1
- package/themes/depth-flight/variants/deep-sky.json +1 -1
- package/themes/depth-flight/variants/forest-blue.json +1 -1
- package/themes/depth-flight/variants/navy-sky-grain.json +1 -1
- package/themes/depth-flight/variants/navy-sky.json +1 -1
- package/themes/depth-flight/variants/night-teal.json +1 -1
- package/themes/depth-flight/variants/slate-indigo.json +1 -1
- package/themes/depth-flight/variants/slate-violet.json +1 -1
- package/themes/film-reel/sample/style.json +2 -2
- package/themes/film-reel/style.json +2 -2
- package/themes/film-reel/theme.json +3 -1
- package/themes/film-reel/variants/chalk-pink.json +1 -1
- package/themes/film-reel/variants/lilac-gold.json +1 -1
- package/themes/film-reel/variants/lilac-violet.json +1 -1
- package/themes/glass-light/sample/style.json +1 -1
- package/themes/glass-light/style.json +1 -1
- package/themes/glass-light/theme.json +3 -1
- package/themes/glass-light/variants/blush-pink.json +1 -1
- package/themes/glass-light/variants/blush-plum.json +1 -1
- package/themes/glass-light/variants/cream-violet.json +1 -1
- package/themes/glass-light/variants/frost-sky-suit.json +1 -1
- package/themes/glass-light/variants/frost-sky.json +1 -1
- package/themes/glass-light/variants/sage-green.json +1 -1
- package/themes/ink-wash/sample/style.json +3 -3
- package/themes/ink-wash/style.json +3 -3
- package/themes/ink-wash/theme.json +3 -1
- package/themes/iso-diorama/sample/style.json +1 -1
- package/themes/iso-diorama/style.json +1 -1
- package/themes/iso-diorama/theme.json +3 -1
- package/themes/iso-diorama/variants/chalk-plum.json +1 -1
- package/themes/iso-diorama/variants/cocoa-indigo.json +1 -1
- package/themes/iso-diorama/variants/dusk-blue.json +1 -1
- package/themes/iso-diorama/variants/dusk-coral.json +1 -1
- package/themes/iso-diorama/variants/dusk-jade.json +1 -1
- package/themes/iso-diorama/variants/forest-cyan.json +1 -1
- package/themes/iso-diorama/variants/frost-green.json +1 -1
- package/themes/iso-diorama/variants/frost-teal.json +1 -1
- package/themes/iso-diorama/variants/navy-amber.json +1 -1
- package/themes/isotype/sample/style.json +2 -2
- package/themes/isotype/style.json +2 -2
- package/themes/isotype/theme.json +2 -1
- package/themes/isotype/variants/blush-rose.json +1 -1
- package/themes/isotype/variants/chalk-coral.json +1 -1
- package/themes/isotype/variants/chalk-orange.json +1 -1
- package/themes/isotype/variants/frost-amber.json +1 -1
- package/themes/isotype/variants/frost-coral.json +1 -1
- package/themes/isotype/variants/frost-orange.json +1 -1
- package/themes/isotype/variants/sand-amber.json +1 -1
- package/themes/kawaii-sticker/effects.ts +15 -3
- package/themes/kawaii-sticker/sample/style.json +2 -2
- package/themes/kawaii-sticker/style.json +2 -2
- package/themes/kawaii-sticker/theme.json +4 -1
- package/themes/kawaii-sticker/variants/cream-gold.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-green.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-indigo.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-sky.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-teal.json +1 -1
- package/themes/kawaii-sticker/variants/frost-blue.json +1 -1
- package/themes/kawaii-sticker/variants/frost-plum.json +1 -1
- package/themes/kawaii-sticker/variants/lilac-violet.json +1 -1
- package/themes/kawaii-sticker/variants/navy-gold.json +1 -1
- package/themes/kawaii-sticker/variants/navy-plum.json +1 -1
- package/themes/lab-notebook/sample/style.json +3 -3
- package/themes/lab-notebook/style.json +3 -3
- package/themes/lab-notebook/theme.json +2 -1
- package/themes/lab-notebook/variants/blush-orange.json +1 -1
- package/themes/lab-notebook/variants/chalk-coral.json +1 -1
- package/themes/lab-notebook/variants/chalk-orange.json +1 -1
- package/themes/lab-notebook/variants/chalk-rose.json +1 -1
- package/themes/lab-notebook/variants/lilac-rose.json +1 -1
- package/themes/lab-notebook/variants/sage-rose.json +1 -1
- package/themes/lab-notebook/variants/sand-orange.json +1 -1
- package/themes/lab-notebook/variants/snow-orange.json +1 -1
- package/themes/lab-notebook/variants/snow-rose.json +1 -1
- package/themes/manga-panel/sample/style.json +3 -3
- package/themes/manga-panel/style.json +3 -3
- package/themes/manga-panel/theme.json +3 -1
- package/themes/manga-panel/variants/chalk-cyan-dots.json +1 -1
- package/themes/manga-panel/variants/chalk-rose.json +1 -1
- package/themes/manga-panel/variants/snow-cyan.json +1 -1
- package/themes/manga-panel/variants/snow-magenta-dots.json +1 -1
- package/themes/marble-run/sample/style.json +2 -2
- package/themes/marble-run/style.json +2 -2
- package/themes/marble-run/theme.json +3 -1
- package/themes/marble-run/variants/slate-orange.json +1 -1
- package/themes/neon-sign/sample/style.json +2 -2
- package/themes/neon-sign/style.json +2 -2
- package/themes/neon-sign/theme.json +2 -1
- package/themes/neon-sign/variants/dusk-plum.json +1 -1
- package/themes/neon-sign/variants/forest-magenta.json +1 -1
- package/themes/neon-sign/variants/night-pink.json +1 -1
- package/themes/neon-sign/variants/night-violet.json +1 -1
- package/themes/neon-sign/variants/slate-violet.json +1 -1
- package/themes/newspaper/sample/style.json +2 -2
- package/themes/newspaper/style.json +2 -2
- package/themes/newspaper/theme.json +2 -1
- package/themes/newspaper/variants/blush-amber.json +1 -1
- package/themes/newspaper/variants/chalk-magenta.json +1 -1
- package/themes/newspaper/variants/frost-sky.json +1 -1
- package/themes/newspaper/variants/lilac-green.json +1 -1
- package/themes/newspaper/variants/sage-lime.json +1 -1
- package/themes/og-measure/THEME.md +41 -6
- package/themes/og-measure/effects.ts +236 -115
- package/themes/og-measure/sample/assets/brand/og-logo.svg +11 -11
- package/themes/og-measure/sample/lib/data.ts +6 -0
- package/themes/og-measure/sample/scenes/end.ts +14 -10
- package/themes/og-measure/sample/scenes/gather.ts +4 -1
- package/themes/og-measure/sample/scenes/models.ts +5 -2
- package/themes/og-measure/sample/scenes/one.ts +9 -6
- package/themes/og-measure/sample/scenes/price.ts +8 -3
- package/themes/og-measure/sample/style.json +30 -35
- package/themes/og-measure/slots.ts +54 -30
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +30 -35
- package/themes/og-measure/theme.json +72 -49
- package/themes/og-measure/variants/snow-blue.json +90 -0
- package/themes/orbit-system/sample/style.json +3 -3
- package/themes/orbit-system/style.json +3 -3
- package/themes/orbit-system/theme.json +3 -1
- package/themes/orbit-system/variants/cocoa-amber.json +1 -1
- package/themes/orbit-system/variants/cocoa-cyan.json +1 -1
- package/themes/orbit-system/variants/dusk-magenta.json +1 -1
- package/themes/orbit-system/variants/dusk-violet.json +1 -1
- package/themes/orbit-system/variants/forest-coral.json +1 -1
- package/themes/orbit-system/variants/wine-amber.json +1 -1
- package/themes/orbit-system/variants/wine-magenta.json +1 -1
- package/themes/origami-fold/sample/style.json +1 -1
- package/themes/origami-fold/style.json +1 -1
- package/themes/origami-fold/theme.json +3 -1
- package/themes/origami-fold/variants/deep-amber-sunflower.json +1 -1
- package/themes/origami-fold/variants/deep-orange.json +1 -1
- package/themes/origami-fold/variants/forest-coral.json +1 -1
- package/themes/origami-fold/variants/forest-pink.json +1 -1
- package/themes/origami-fold/variants/frost-amber-paper.json +1 -1
- package/themes/origami-fold/variants/sage-coral-sunflower.json +1 -1
- package/themes/origami-fold/variants/sage-coral.json +1 -1
- package/themes/origami-fold/variants/sage-pink-round.json +1 -1
- package/themes/origami-fold/variants/sage-pink.json +1 -1
- package/themes/paper-cutout/sample/style.json +2 -2
- package/themes/paper-cutout/style.json +2 -2
- package/themes/paper-cutout/theme.json +3 -1
- package/themes/paper-cutout/variants/blush-violet.json +1 -1
- package/themes/paper-cutout/variants/frost-lime.json +1 -1
- package/themes/paper-cutout/variants/lilac-gold.json +1 -1
- package/themes/paper-cutout/variants/sage-cyan.json +1 -1
- package/themes/paper-cutout/variants/sage-green.json +1 -1
- package/themes/paper-cutout/variants/sage-pink.json +1 -1
- package/themes/paper-cutout/variants/sand-blue.json +1 -1
- package/themes/pixel-quest/theme.json +2 -1
- package/themes/receipt/sample/style.json +2 -2
- package/themes/receipt/style.json +2 -2
- package/themes/riso-zine/sample/style.json +2 -2
- package/themes/riso-zine/style.json +2 -2
- package/themes/riso-zine/theme.json +3 -1
- package/themes/riso-zine/variants/chalk-lime.json +1 -1
- package/themes/riso-zine/variants/frost-green.json +1 -1
- package/themes/riso-zine/variants/lilac-plum.json +1 -1
- package/themes/riso-zine/variants/lilac-violet.json +1 -1
- package/themes/server-hall/sample/style.json +2 -2
- package/themes/server-hall/style.json +2 -2
- package/themes/server-hall/theme.json +3 -1
- package/themes/server-hall/variants/night-indigo.json +1 -1
- package/themes/server-hall/variants/wine-plum.json +1 -1
- package/themes/subway-map/sample/style.json +2 -2
- package/themes/subway-map/style.json +2 -2
- package/themes/subway-map/theme.json +3 -1
- package/themes/subway-map/variants/deep-green.json +1 -1
- package/themes/subway-map/variants/deep-teal.json +1 -1
- package/themes/subway-map/variants/frost-jade.json +1 -1
- package/themes/subway-map/variants/frost-teal.json +1 -1
- package/themes/subway-map/variants/slate-green.json +1 -1
- package/themes/subway-map/variants/snow-jade.json +1 -1
- package/themes/swiss-poster/sample/style.json +2 -2
- package/themes/swiss-poster/style.json +2 -2
- package/themes/swiss-poster/theme.json +3 -1
- package/themes/swiss-poster/variants/chalk-plum.json +1 -1
- package/themes/swiss-poster/variants/cocoa-cyan.json +1 -1
- package/themes/swiss-poster/variants/dusk-magenta.json +1 -1
- package/themes/swiss-poster/variants/frost-indigo.json +1 -1
- package/themes/swiss-poster/variants/slate-orange.json +1 -1
- package/themes/water-works/sample/style.json +1 -1
- package/themes/water-works/style.json +1 -1
- package/themes/water-works/theme.json +3 -1
- package/themes/water-works/variants/chalk-cyan.json +1 -1
- package/themes/water-works/variants/frost-indigo.json +1 -1
- package/themes/water-works/variants/sand-amber.json +1 -1
- package/themes/water-works/variants/sand-plum.json +1 -1
- package/themes/whiteboard/sample/style.json +2 -2
- package/themes/whiteboard/style.json +2 -2
- package/themes/whiteboard/theme.json +3 -1
- package/themes/whiteboard/variants/chalk-coral.json +1 -1
- package/themes/whiteboard/variants/cream-lime.json +1 -1
- package/themes/whiteboard/variants/frost-teal.json +1 -1
- package/themes/whiteboard/variants/lilac-indigo.json +1 -1
- package/themes/whiteboard/variants/sage-plum.json +1 -1
- package/themes/whiteboard/variants/snow-cyan.json +1 -1
- package/themes/whiteboard/variants/snow-green.json +1 -1
- package/themes/whiteboard/variants/snow-magenta.json +1 -1
- package/themes/og-measure/sample/assets/brand/og-logo-full.svg +0 -12
- package/themes/og-measure/variants/chalk-orange.json +0 -102
- package/themes/og-measure/variants/cocoa-amber.json +0 -138
- package/themes/og-measure/variants/cocoa-cyan.json +0 -138
- package/themes/og-measure/variants/cocoa-gold.json +0 -129
- package/themes/og-measure/variants/cream-lime.json +0 -137
- package/themes/og-measure/variants/deep-jade.json +0 -100
- package/themes/og-measure/variants/dusk-violet.json +0 -133
- package/themes/og-measure/variants/forest-cyan.json +0 -123
- package/themes/og-measure/variants/frost-amber.json +0 -129
- package/themes/og-measure/variants/frost-cyan.json +0 -133
- package/themes/og-measure/variants/lilac-plum.json +0 -127
- package/themes/og-measure/variants/lilac-violet.json +0 -139
- package/themes/og-measure/variants/sage-green.json +0 -137
- package/themes/og-measure/variants/wine-jade.json +0 -127
- package/themes/og-measure/variants/wine-plum.json +0 -127
|
@@ -39,20 +39,22 @@ export function onFrame(fn: (t: number) => void): () => void {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export type MotionEventKind = "pulse" | "shake" | "camera" | "flash" | "move";
|
|
42
|
+
export type MotionEventKind = "pulse" | "shake" | "camera" | "flash" | "move" | "sfx";
|
|
43
43
|
|
|
44
44
|
/** 검사(kf check)가 style.json 움직임 규칙 위반을 세는 기록. 효과가 0 이 아닌 프레임마다 한 줄 */
|
|
45
45
|
export interface KfMotionEvent {
|
|
46
|
-
/** 전체 시각(초) — 그
|
|
46
|
+
/** 전체 시각(초) — 그 프레임. sfx 는 그 소리가 날 시각 */
|
|
47
47
|
t: number;
|
|
48
48
|
kind: MotionEventKind;
|
|
49
49
|
/** 부품 id */
|
|
50
50
|
source: string;
|
|
51
|
-
/** pulse 0..1, shake 는 px, camera 는 이동량(px 또는 배율 차), flash 0..1, move 1 */
|
|
51
|
+
/** pulse 0..1, shake 는 px, camera 는 이동량(px 또는 배율 차), flash 0..1, move 1, sfx 는 크기(0..1) */
|
|
52
52
|
value: number;
|
|
53
|
-
/** move 일 때 움직임 이름(fade·rise·whip …) — 같은 움직임 되풀이를
|
|
53
|
+
/** move 일 때 움직임 이름(fade·rise·whip …) — 같은 움직임 되풀이를 센다. sfx 면 효과음 역할 */
|
|
54
54
|
name?: string;
|
|
55
55
|
scene?: string;
|
|
56
|
+
/** sfx 만 — 낱낱이 나타남·숫자 째깍·타자 같은 잔소리. 렌더는 기본으로 안 깐다(테마 sfx.chatter 면 깐다) */
|
|
57
|
+
minor?: boolean;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
interface KfEventHost {
|
|
@@ -83,6 +85,7 @@ export function emit(e: Omit<KfMotionEvent, "t"> & { t?: number }): void {
|
|
|
83
85
|
const row: KfMotionEvent = { t, kind: e.kind, source: e.source, value: Math.round(e.value * 1000) / 1000 };
|
|
84
86
|
if (e.name !== undefined) row.name = e.name;
|
|
85
87
|
if (e.scene !== undefined) row.scene = e.scene;
|
|
88
|
+
if (e.minor) row.minor = true;
|
|
86
89
|
sink().push(row);
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -23,5 +23,7 @@ export { combine, toCss, applyStyle } from "./style.ts";
|
|
|
23
23
|
export type { Style, CssStyle } from "./style.ts";
|
|
24
24
|
export { setFrameTime, frameTime, onFrame, emit, motionEvents, resetMotionEvents, setFrameSample, frameSample } from "./frame.ts";
|
|
25
25
|
export type { KfMotionEvent, MotionEventKind, FrameSample } from "./frame.ts";
|
|
26
|
-
export {
|
|
26
|
+
export { SFX_ROLES, SFX_LABEL, cue, cueAt, withoutSfx } from "./sfx.ts";
|
|
27
|
+
export type { SfxRole, CueOpts } from "./sfx.ts";
|
|
28
|
+
export { silentBeats, scaleOf, cssVars, fontCss, MAX_FREEZE_SEC, freezeLimits } from "./project.ts";
|
|
27
29
|
export type { CaptionLook, KfMeta, KfStyle, MotionRules, SafeArea, SceneCtx, FontFace, StyleLook } from "./project.ts";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createBeats, type Beats } from "./beats.ts";
|
|
2
|
+
import type { CueOpts, SfxRole } from "./sfx.ts";
|
|
2
3
|
|
|
3
4
|
export interface KfMeta {
|
|
4
5
|
fps: number;
|
|
@@ -36,6 +37,19 @@ export interface MotionRules {
|
|
|
36
37
|
durations?: { fast: number; base: number; slow: number };
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** 멈춤 기본 기준 — 단 0.1초(60fps 6프레임)도 쉬지 않는다 */
|
|
41
|
+
export const MAX_FREEZE_SEC = 0.1;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 멈춤 기준 — 경고는 틀·style.json 값(틀이 이긴다, 없으면 0.1초), 오류는 그 값과 테마 값(theme.json motion.maxFreezeSec) 중 큰 쪽.
|
|
45
|
+
* 테마는 늘리기만 한다 — 짚음이 요점에 머물러 화면이 쉬는 세계(OG 측정 화면)가 그 쉼으로 장면을 떨어뜨리지 않게 하되,
|
|
46
|
+
* 고정 관찰 화면 틀·스타일이 늘린 값(0.5)을 조이지는 못하고, 늘린 만큼도 경고로는 보인다
|
|
47
|
+
*/
|
|
48
|
+
export function freezeLimits(base: number | undefined, theme?: number | null): { warn: number; error: number } {
|
|
49
|
+
const warn = base ?? MAX_FREEZE_SEC;
|
|
50
|
+
return { warn, error: Math.max(warn, theme ?? 0) };
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export interface FontFace {
|
|
40
54
|
family: string;
|
|
41
55
|
src: string;
|
|
@@ -131,6 +145,11 @@ export interface SceneCtx {
|
|
|
131
145
|
safe: SafeArea;
|
|
132
146
|
/** 프로젝트 assets/ 기준 경로 → 페이지에서 쓸 주소 */
|
|
133
147
|
asset(path: string): string;
|
|
148
|
+
/**
|
|
149
|
+
* 효과음 자리 — 장면 시각 at(초)에 그 역할 소리. build 에서 불러도 draw 에서 불러도 된다(같은 자리는 한 번).
|
|
150
|
+
* 라이브러리 부품(테마 효과·패턴·전환·드롭)은 저절로 남기니, 부품 밖에서 손으로 지은 움직임에만 쓴다
|
|
151
|
+
*/
|
|
152
|
+
sfx(role: SfxRole, at: number, o?: Omit<CueOpts, "scene">): void;
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
/** 곡 없는 프로젝트용. pulse 는 늘 0(드롭이 없다), drop(i) 는 던진다 */
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { emit, frameTime } from "./frame.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 효과음 역할 — 소리 결(테마가 고른 묶음)마다 역할 하나에 파일 하나가 있다. 렌더가 이 이름으로 파일을 고르고, 밀도 상한을 넘으면
|
|
5
|
+
* 앞 역할(드롭·끝·휙…)부터 남긴다
|
|
6
|
+
*/
|
|
7
|
+
export const SFX_ROLES = ["appear", "stick", "land", "hop", "whoosh", "tick", "select", "type", "drop", "burst", "end"] as const;
|
|
8
|
+
|
|
9
|
+
export type SfxRole = (typeof SFX_ROLES)[number];
|
|
10
|
+
|
|
11
|
+
export const SFX_LABEL: Record<SfxRole, string> = {
|
|
12
|
+
appear: "나타남",
|
|
13
|
+
stick: "톡 붙기",
|
|
14
|
+
land: "착지",
|
|
15
|
+
hop: "통 뛰기",
|
|
16
|
+
whoosh: "휙 전환",
|
|
17
|
+
tick: "숫자 째깍",
|
|
18
|
+
select: "짚기 딸깍",
|
|
19
|
+
type: "타자",
|
|
20
|
+
drop: "드롭 쿵",
|
|
21
|
+
burst: "팡 터짐",
|
|
22
|
+
end: "끝 마무리",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface CueOpts {
|
|
26
|
+
/** 부품 id — 같은 시각·같은 부품은 한 번만 남는다 */
|
|
27
|
+
source?: string;
|
|
28
|
+
scene?: string;
|
|
29
|
+
/** 0..1 — 역할 크기에 곱한다(작은 칸은 작게) */
|
|
30
|
+
weight?: number;
|
|
31
|
+
/**
|
|
32
|
+
* 잔소리 — 낱낱이 나타나는 칸·숫자 째깍·타자처럼 자주 나는 것. 렌더가 기본으로 안 깐다(다 깔면 곡에 묻히고 시끄럽다).
|
|
33
|
+
* 장면 바뀜·드롭·주인공 착지·짚개가 핵심 값에 닿는 순간만 minor 없이 남긴다
|
|
34
|
+
*/
|
|
35
|
+
minor?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let muted = 0;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 이 안에서 그린 것은 소리 자리를 안 남긴다 — 이음매가 앞뒤 장면을 한 벌 더 지어 다른 시각으로 그릴 때(진짜 장면이 이미 남겼고,
|
|
42
|
+
* 첫 프레임에 서 있는 뒤 장면은 시작 직후로 보이는 부품이 프레임마다 새 자리를 남긴다)
|
|
43
|
+
*/
|
|
44
|
+
export function withoutSfx<T>(fn: () => T): T {
|
|
45
|
+
muted++;
|
|
46
|
+
try {
|
|
47
|
+
return fn();
|
|
48
|
+
} finally {
|
|
49
|
+
muted--;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 움직임이 시작하고 이만큼 안에 그린 프레임이 그 자리를 남긴다 — 검사(30fps)·렌더(60fps)·모션 블러 하위 프레임 어느 쪽도 한 번은 이 안에 든다
|
|
54
|
+
const WINDOW = 0.12;
|
|
55
|
+
|
|
56
|
+
/** 효과음 자리를 전체 시각 t(초)에 남긴다 — 장면 작성자의 ctx.sfx 가 부르는 자리 */
|
|
57
|
+
export function cueAt(role: SfxRole, t: number, o: CueOpts = {}): void {
|
|
58
|
+
if (muted || !Number.isFinite(t) || t < 0) return;
|
|
59
|
+
emit({ kind: "sfx", t, name: role, source: o.source ?? role, scene: o.scene, value: o.weight ?? 1, ...(o.minor ? { minor: true } : {}) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 움직임 부품이 부른다 — since 는 그 움직임이 시작하고 지난 초(그 프레임의 부품 시각 − 시작 시각). 시작 직후 프레임에서
|
|
64
|
+
* 「지금 프레임 시각 − since」를 소리 자리로 남긴다. 부품이 장면 시각을 얼마나 밀어 받았든 시작 순간의 전체 시각이 나오고,
|
|
65
|
+
* 어느 프레임이 남기든 같은 값이라 탭을 나눠 찍어도 같은 소리다
|
|
66
|
+
*/
|
|
67
|
+
export function cue(role: SfxRole, since: number, o: CueOpts = {}): void {
|
|
68
|
+
if (!(since >= 0 && since < WINDOW)) return;
|
|
69
|
+
cueAt(role, frameTime() - since, o);
|
|
70
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { themeTransitions } from "@kasaframe/themes";
|
|
4
5
|
import { worldOf } from "@kasaframe/themes/name";
|
|
5
6
|
import { dressSeed, type Dress } from "@kasaframe/motion/menu";
|
|
6
7
|
import { loadTemplate, sceneSkeleton, type Template } from "@kasaframe/plan";
|
|
@@ -8,7 +9,7 @@ import { catalog } from "./catalog.ts";
|
|
|
8
9
|
import { TYPES, type Content, type Scene, type SceneType } from "./content.ts";
|
|
9
10
|
import type { AspectName, Energy, TextPolicy } from "./pattern.ts";
|
|
10
11
|
import { type Entry, type Pick, repick, selectAll, type Slot } from "./select.ts";
|
|
11
|
-
import { type BeatsLite, cyclingText, dressAll, energyOf, seamKinds, seamSpots, type Spot } from "./transitions.ts";
|
|
12
|
+
import { type BeatsLite, cyclingText, dressAll, energyOf, seamKinds, seamsAllowed, seamSpots, type Spot } from "./transitions.ts";
|
|
12
13
|
|
|
13
14
|
// content.json → 장면 시간표·장면 파일. 두 단계로 나뉜다 — 그 사이에 kf narrate 가 목소리로 장면 길이를 정한다.
|
|
14
15
|
// 1. layout: 내용 장면 + 이음매를 kf.json 에 놓고 들릴 말을 captions.json 에(길이는 어림)
|
|
@@ -79,7 +80,8 @@ export function layout(dir: string, content: Content, o: { keepTiming?: boolean
|
|
|
79
80
|
const seamSec = tplSeams[0]?.sec ?? 1.2;
|
|
80
81
|
// 투영을 맞춰 구운 3D 샷(kf blender 가 kf.json 장면에 남긴 matched)은 다시 놓아도 잃지 않는다
|
|
81
82
|
const matched = new Set<string>((kf.scenes ?? []).filter((s: KfScene) => s.matched).map((s: KfScene) => s.id));
|
|
82
|
-
|
|
83
|
+
// 테마가 이음매 전환을 하나도 허용하지 않으면(컷·덮기만) 이음매 장면을 안 넣는다
|
|
84
|
+
const spots = seamsAllowed(themeTransitions(kf.theme)) ? seamSpots(content.scenes.map((s) => s.type), content.scenes.map(spaceOf), tplSeams.length, content.scenes.map((s) => matched.has(s.id))) : [];
|
|
83
85
|
const taken = new Set(content.scenes.map((s) => s.id));
|
|
84
86
|
const seamIds: string[] = [];
|
|
85
87
|
for (let k = 0; seamIds.length < spots.length; k++) {
|
|
@@ -274,9 +276,10 @@ export async function build(dir: string, content: Content, o: BuildOpts = {}): P
|
|
|
274
276
|
const b = spaceOf(scenes[i + 1] ?? {});
|
|
275
277
|
return { at: i, bridge: a === b ? false : b === "3d" ? "up" : "down" };
|
|
276
278
|
});
|
|
277
|
-
const
|
|
279
|
+
const allow = themeTransitions(theme);
|
|
280
|
+
const kinds = seamKinds(spots, key, tpl.scenes.find((s) => s.role === "seam")?.transition ?? "dive", Object.fromEntries(seamIdx.map((i, k) => [k, seamFailed[scenes[i].id] ?? []])), allow);
|
|
278
281
|
|
|
279
|
-
const dress = dressAll(key, scenes, tpl.moves, world);
|
|
282
|
+
const dress = dressAll(key, scenes, tpl.moves, world, allow);
|
|
280
283
|
const scenesDir = join(dir, "scenes");
|
|
281
284
|
mkdirSync(scenesDir, { recursive: true });
|
|
282
285
|
const report: SceneReport[] = [];
|
|
@@ -5,7 +5,7 @@ export { definePattern } from "./pattern.ts";
|
|
|
5
5
|
export type { AspectName, CameraKind, Energy, Family, FitInfo, Kit, Pattern, PatternMeta, SpokenWord, StageLike, TextPolicy } from "./pattern.ts";
|
|
6
6
|
export { order, reject, repick, score, selectAll } from "./select.ts";
|
|
7
7
|
export type { Entry, Pick, Ranked, Rejected, SelectCtx, Slot } from "./select.ts";
|
|
8
|
-
export { BRIDGE_KIND, cyclingText, dressAll, energyOf, SEAM_KINDS, seamKinds, seamSpots, STILL_BG } from "./transitions.ts";
|
|
8
|
+
export { BRIDGE_KIND, cyclingText, dressAll, energyOf, SEAM_KINDS, seamKinds, seamPool, seamsAllowed, seamSpots, STILL_BG } from "./transitions.ts";
|
|
9
9
|
export type { Spot } from "./transitions.ts";
|
|
10
10
|
export { catalog } from "./catalog.ts";
|
|
11
11
|
export { build, contentHash, layout, MARK } from "./assemble.ts";
|
|
@@ -71,6 +71,8 @@ export interface Kit {
|
|
|
71
71
|
hits: number[];
|
|
72
72
|
/** 들리는 낱말 시각(장면 안 초) — kf narrate 를 지났을 때만 */
|
|
73
73
|
say: SpokenWord[];
|
|
74
|
+
/** 자막 줄(장면 안 초) — 낱말 시각이 없는 프로젝트(목소리 없이 자막만)에서 항목이 말해지는 자리를 어림한다 */
|
|
75
|
+
lines: SpokenWord[];
|
|
74
76
|
/** 테마 슬롯(themes/<id>/slots.ts) — 테마가 비운 자리는 패턴 기본으로 그린다 */
|
|
75
77
|
slots: ThemeSlots;
|
|
76
78
|
/** 감독이 dress 바탕·경계 덮기를 건 무대. 감싸개가 paint 다음에 stage.tick 을 부르니 패턴은 부르지 않는다 */
|
|
@@ -24,19 +24,27 @@ type MakeStage = (root: HTMLElement, ctx: SceneCtx, o: { panel?: boolean; dress?
|
|
|
24
24
|
|
|
25
25
|
interface Line {
|
|
26
26
|
scene: string;
|
|
27
|
+
text: string;
|
|
28
|
+
from?: number;
|
|
29
|
+
to?: number;
|
|
27
30
|
words?: { text: string; from: number; to: number }[];
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
// 패턴이 박에 붙일 때 쓰는 간격 — 장면 문법 예시와 같은 값이라 예시에서 본 그림이 그대로 나온다
|
|
31
34
|
const HIT_GAP = 0.3;
|
|
32
35
|
|
|
33
|
-
// captions.json 의
|
|
34
|
-
function
|
|
36
|
+
// captions.json 의 시각은 영상 기준 초 — 장면 안 초로 옮긴다
|
|
37
|
+
function linesOf(glob: Record<string, unknown> | undefined, ctx: SceneCtx): Line[] {
|
|
35
38
|
const lines = (Object.values(glob ?? {})[0] ?? []) as Line[];
|
|
36
|
-
|
|
37
|
-
return lines.filter((l) => l.scene === ctx.id).flatMap((l) => (l.words ?? []).map((w) => ({ text: w.text, from: w.from - ctx.from, to: w.to - ctx.from })));
|
|
39
|
+
return Array.isArray(lines) ? lines.filter((l) => l.scene === ctx.id) : [];
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
const wordsOf = (lines: Line[], ctx: SceneCtx): SpokenWord[] => lines.flatMap((l) => (l.words ?? []).map((w) => ({ text: w.text, from: w.from - ctx.from, to: w.to - ctx.from })));
|
|
43
|
+
|
|
44
|
+
// 낱말 시각 없이 줄 시각만 있는 자막(목소리 없이 자막만 단 프로젝트)
|
|
45
|
+
const timedLines = (lines: Line[], ctx: SceneCtx): SpokenWord[] =>
|
|
46
|
+
lines.flatMap((l) => (l.from !== undefined && l.to !== undefined ? [{ text: l.text, from: l.from - ctx.from, to: l.to - ctx.from }] : []));
|
|
47
|
+
|
|
40
48
|
export function directed(pattern: Pattern<any>, d: Directed, deps: { makeStage: MakeStage; captions?: Record<string, unknown> }) {
|
|
41
49
|
return {
|
|
42
50
|
build(root: HTMLElement, ctx: SceneCtx) {
|
|
@@ -50,7 +58,8 @@ export function directed(pattern: Pattern<any>, d: Directed, deps: { makeStage:
|
|
|
50
58
|
stage.box.prepend(layer);
|
|
51
59
|
surface = slots.surface(layer, { ctx, seed: `${d.seed}:surface` });
|
|
52
60
|
}
|
|
53
|
-
const
|
|
61
|
+
const lines = linesOf(deps.captions, ctx);
|
|
62
|
+
const kit: Kit = { seed: d.seed, hits: hitTimes(ctx, HIT_GAP), say: wordsOf(lines, ctx), lines: timedLines(lines, ctx), slots, stage, policy: d.policy, first: d.first, last: d.last, loop: d.loop };
|
|
54
63
|
const paint = pattern.build(stage.cam, ctx, d.content, kit);
|
|
55
64
|
return (t: number) => {
|
|
56
65
|
surface?.(t);
|
|
@@ -6,6 +6,7 @@ import type { Energy } from "./pattern.ts";
|
|
|
6
6
|
// 장면 사이 — 이음매(3D 전환 장면)를 어디에 몇 개 넣을지, 그 종류, 나머지 경계의 덮기, 장면마다의 바탕.
|
|
7
7
|
// 이음매 기본은 dive, 같은 전환을 잇달아 쓰지 않는다. 덮기·바탕은 연출 수 메뉴(pickDress)의 규칙을 그대로 따른다
|
|
8
8
|
// (이음매·드롭으로 들어가는 경계·1.2초 아래 장면엔 안 덮는다, 이어지는 장면은 다른 바탕).
|
|
9
|
+
// 테마가 허용 목록(theme.json transitions — 덮기·이음매 이름)을 주면 그 안에서만 고른다. 허용한 이음매가 없으면 이음매를 안 넣는다(컷).
|
|
9
10
|
|
|
10
11
|
export interface BeatsLite {
|
|
11
12
|
sections?: { t: number; kind: string; strength?: number }[];
|
|
@@ -66,6 +67,16 @@ export function seamSpots(types: readonly string[], spaces: readonly ("2d" | "3d
|
|
|
66
67
|
|
|
67
68
|
export const SEAM_KINDS = ["dive", "portal", "cards", "zoomOut", "fold", "whip"] as const;
|
|
68
69
|
|
|
70
|
+
/** 고를 수 있는 이음매 종류 — 허용 목록이 없으면 SEAM_KINDS, 있으면 그 안의 것(gl:·smooth: 전환도 이름을 적으면 든다) */
|
|
71
|
+
export function seamPool(allow?: readonly string[] | null): string[] {
|
|
72
|
+
if (!allow) return [...SEAM_KINDS];
|
|
73
|
+
return [...SEAM_KINDS, ...allow.filter((k) => /^(gl|smooth):/.test(k))].filter((k) => allow.includes(k));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 이음매를 하나라도 넣을 수 있나 — 공간 잇기(rise·land)도 허용 목록을 따른다 */
|
|
77
|
+
export const seamsAllowed = (allow?: readonly string[] | null): boolean =>
|
|
78
|
+
!allow || seamPool(allow).length > 0 || Object.values(BRIDGE_KIND).some((k) => allow.includes(k));
|
|
79
|
+
|
|
69
80
|
/** 2D↔3D 경계의 공간 잇기 — 2D→3D 는 주인공 상자가 3D 블록으로 일어서고(rise), 3D→2D 는 한 평면에 내려앉는다(land).
|
|
70
81
|
* 이 판의 transition3d 에 아직 없으면 dive 로(장면이 모르는 kind 로 던지지 않게) */
|
|
71
82
|
const known = new Set<string>(TRANSITIONS_3D);
|
|
@@ -75,18 +86,20 @@ export const BRIDGE_KIND = { up: known.has("rise") ? "rise" : "dive", down: know
|
|
|
75
86
|
* 이음매 종류 — 공간 잇기 자리는 BRIDGE_KIND, 나머지 첫째는 틀이 정한 것(기본 dive), 다음부터는 씨앗으로 앞과 다른 것.
|
|
76
87
|
* failed 는 kf check 에서 떨어진 종류(이음매 차례 번호로)
|
|
77
88
|
*/
|
|
78
|
-
export function seamKinds(spots: readonly Spot[], seed: string, first = "dive", failed: Record<number, string[]> = {}): string[] {
|
|
89
|
+
export function seamKinds(spots: readonly Spot[], seed: string, first = "dive", failed: Record<number, string[]> = {}, allow?: readonly string[] | null): string[] {
|
|
90
|
+
const kinds = seamPool(allow);
|
|
91
|
+
const lead = kinds.includes(first) ? first : (kinds[0] ?? first);
|
|
79
92
|
const out: string[] = [];
|
|
80
93
|
let plain = 0;
|
|
81
94
|
for (const [i, sp] of spots.entries()) {
|
|
82
95
|
const bad = new Set([out[i - 1], ...(failed[i] ?? [])]);
|
|
83
|
-
if (sp.bridge && !(failed[i] ?? []).includes(BRIDGE_KIND[sp.bridge])) {
|
|
96
|
+
if (sp.bridge && !(failed[i] ?? []).includes(BRIDGE_KIND[sp.bridge]) && (!allow || allow.includes(BRIDGE_KIND[sp.bridge]))) {
|
|
84
97
|
out.push(BRIDGE_KIND[sp.bridge]);
|
|
85
98
|
continue;
|
|
86
99
|
}
|
|
87
|
-
const pool =
|
|
88
|
-
if (plain++ === 0 && !bad.has(
|
|
89
|
-
else out.push(pool.length ? pool[Math.floor(rand(`${seed}|seam`, i) * pool.length)] :
|
|
100
|
+
const pool = kinds.filter((k) => !bad.has(k));
|
|
101
|
+
if (plain++ === 0 && !bad.has(lead)) out.push(lead);
|
|
102
|
+
else out.push(pool.length ? pool[Math.floor(rand(`${seed}|seam`, i) * pool.length)] : lead);
|
|
90
103
|
}
|
|
91
104
|
return out;
|
|
92
105
|
}
|
|
@@ -100,9 +113,9 @@ export interface Placed {
|
|
|
100
113
|
/** 바탕 정지 — 움직이는 것은 다 내용 하나를 뜻해야 해서 바탕 수(햇살·집중선·모눈·빛 방울)는 감독 장면에 걸지 않는다 */
|
|
101
114
|
export const STILL_BG = "still";
|
|
102
115
|
|
|
103
|
-
/** 장면마다 글 수·경계 덮기 — 연출 수 메뉴의 고름 그대로(같은 씨앗이면 같은 고름), 바탕은
|
|
104
|
-
export function dressAll(seed: string, scenes: readonly Placed[], moves: Bundle | undefined, theme: string | null): Dress[] {
|
|
105
|
-
return pickDress(seed, scenes, bundleFor(moves, theme ?? undefined)).map((d) => ({ ...d, bg: STILL_BG }));
|
|
116
|
+
/** 장면마다 글 수·경계 덮기 — 연출 수 메뉴의 고름 그대로(같은 씨앗이면 같은 고름), 바탕은 정지. allow 는 테마가 허용한 전환 */
|
|
117
|
+
export function dressAll(seed: string, scenes: readonly Placed[], moves: Bundle | undefined, theme: string | null, allow?: readonly string[] | null): Dress[] {
|
|
118
|
+
return pickDress(seed, scenes, bundleFor(moves, theme ?? undefined, allow)).map((d) => ({ ...d, bg: STILL_BG }));
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
// 감독 기본이 쓰는 글 수 — 낱말을 돌려 띄우되 자르지 않고(slamWords 는 긴 낱말이 화면 끝을 넘는다) 겹쳐 흐리지 않는 것(echoStack·ransom 은 겹친 벌이 장식이 된다)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clamp, emit, progress, rand, spring, toEase, type Ease, type EaseName, type Style, safeTime } from "@kasaframe/core";
|
|
1
|
+
import { clamp, cue, emit, progress, rand, spring, toEase, type Ease, type EaseName, type SfxRole, type Style, safeTime } from "@kasaframe/core";
|
|
2
2
|
|
|
3
3
|
export type MoveName = "fade" | "rise" | "drop" | "pop" | "blurIn" | "scaleIn" | "maskWipe" | "slide";
|
|
4
4
|
export type Side = "left" | "right" | "top" | "bottom";
|
|
@@ -72,6 +72,9 @@ function check(name: string): MoveName {
|
|
|
72
72
|
return name as MoveName;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// 등장 소리 — 스며 나오는 것(fade·blurIn)은 소리 없이, 톡 튀어나오는 pop 은 붙기, 위에서 떨어지는 drop 은 착지. 낱낱이 나타나는 것이라 잔소리(minor)
|
|
76
|
+
const ENTER_SFX: Partial<Record<MoveName, SfxRole>> = { rise: "appear", drop: "land", pop: "stick", scaleIn: "appear", maskWipe: "appear", slide: "appear" };
|
|
77
|
+
|
|
75
78
|
export function enter(name: MoveName, t: number, opts: MoveOpts = {}): Style {
|
|
76
79
|
t = safeTime(t);
|
|
77
80
|
const n = check(name);
|
|
@@ -80,6 +83,9 @@ export function enter(name: MoveName, t: number, opts: MoveOpts = {}): Style {
|
|
|
80
83
|
const p = progress(t, at, dur);
|
|
81
84
|
const e = toEase(opts.ease, n === "pop" ? (x) => x : undefined)(p);
|
|
82
85
|
if (opts.emit !== false && p > 0 && p < 1) emit({ kind: "move", source: opts.source ?? n, scene: opts.scene, name: `enter:${n}`, value: 1 });
|
|
86
|
+
const role = ENTER_SFX[n];
|
|
87
|
+
// 같은 부품이 여러 칸을 늦춰 세우면(stagger) 칸마다 자리가 달라 하나씩 남는다
|
|
88
|
+
if (role && opts.emit !== false) cue(role, t - at, { source: `${opts.source ?? n}@${at.toFixed(3)}`, scene: opts.scene, minor: true });
|
|
83
89
|
return shape(n, p, e, t - at, opts);
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clamp, emit, noise1, progress, toEase, type Beats, type MotionRules, type PulseOpts, type Style, safeTime } from "@kasaframe/core";
|
|
1
|
+
import { clamp, cue, emit, noise1, progress, toEase, type Beats, type MotionRules, type PulseOpts, type Style, safeTime } from "@kasaframe/core";
|
|
2
2
|
|
|
3
3
|
interface Tagged {
|
|
4
4
|
source?: string;
|
|
@@ -63,7 +63,10 @@ export function dropBurst(t: number, opts: DropBurstOpts): DropBurstState {
|
|
|
63
63
|
const local = t - at;
|
|
64
64
|
const p = progress(t, at, 0.9);
|
|
65
65
|
const flash = local < 0 ? 0 : local < 0.04 ? 1 : clamp(1 - (local - 0.04) / 0.22) ** 2;
|
|
66
|
-
if (opts.emit !== false)
|
|
66
|
+
if (opts.emit !== false) {
|
|
67
|
+
emit({ kind: "flash", source, scene: opts.scene, value: flash });
|
|
68
|
+
cue("drop", local, { source, scene: opts.scene });
|
|
69
|
+
}
|
|
67
70
|
const rp = toEase("outCubic")(progress(t, at, 0.7));
|
|
68
71
|
const ring = local < 0 ? { scale: 0, opacity: 0 } : { scale: 0.2 + 2.6 * rp, opacity: (1 - rp) * 0.9 };
|
|
69
72
|
const kick = local < 0 ? 0 : Math.exp(-local / 0.12) * Math.cos(local * 28);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { clamp, type SceneCtx } from "@kasaframe/core";
|
|
2
2
|
import { brushWipe } from "./brushWipe.ts";
|
|
3
3
|
import { echoStack } from "./echoStack.ts";
|
|
4
|
-
import { div } from "./fit.ts";
|
|
4
|
+
import { div, mark } from "./fit.ts";
|
|
5
5
|
import { flood } from "./flood.ts";
|
|
6
6
|
import { ghostStack } from "./ghostStack.ts";
|
|
7
7
|
import { gridFloor } from "./gridFloor.ts";
|
|
@@ -164,6 +164,9 @@ export function dressText(name: string, el: HTMLElement, ctx: SceneCtx, o: { wor
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// 짧은 페이드 한쪽 길이 — 경계 양쪽 합쳐 0.3초
|
|
168
|
+
const FADE = 0.15;
|
|
169
|
+
|
|
167
170
|
/**
|
|
168
171
|
* 장면 경계를 걸치는 덮기 — out 은 장면 끝에서 덮고, in 은 다음 장면 처음에 걷힌다. 다 덮인 채 서 있는 시간은 0.05초 안팎 —
|
|
169
172
|
* 한 색 화면이 0.1초를 넘으면 멈춤 검사(frame.freeze)에 걸린다. 조리개는 끝까지 닫지 않고 바늘구멍에서 되돌아 연다. 두 장면이 경계의 영상 시각을 같은 씨앗으로 써서
|
|
@@ -181,6 +184,16 @@ export function dressWipe(name: string, el: HTMLElement, ctx: SceneCtx, side: "i
|
|
|
181
184
|
case "irisBlades":
|
|
182
185
|
// 다 열린 육각 구멍이 화면 모서리까지 덮게 — 변까지 거리가 대각선 반을 넘어야 모서리에 날이 안 남는다
|
|
183
186
|
return irisBlades(el, { w, h, color: ink, seam: "rgba(255,255,255,.3)", radius: (Math.hypot(w, h) / 2 / Math.cos(Math.PI / 6)) * 1.04, stiffness: 260, damping: 32, steps: [{ at: B - 100, open: 1 }, { at: B - 0.32, open: 0 }, { at: B - 0.02, open: 1 }], scene: ctx.id });
|
|
187
|
+
case "fade": {
|
|
188
|
+
// 바탕색 막이 0.15초 짙어졌다 걷힌다 — 끝까지 덮지 않아 한 색 화면이 서지 않는다
|
|
189
|
+
const veil = div(el.ownerDocument, `position:absolute;inset:0;background:${color(ctx, "bg", "#000000")};opacity:0`);
|
|
190
|
+
el.append(veil);
|
|
191
|
+
return (t) => {
|
|
192
|
+
const k = side === "out" ? clamp((t - (B - FADE)) / FADE) : 1 - clamp(t / FADE);
|
|
193
|
+
veil.style.opacity = (0.9 * k).toFixed(3);
|
|
194
|
+
mark("fade", ctx.id, k > 0);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
184
197
|
case "flood": {
|
|
185
198
|
const zero = { x: w / 2, y: h / 2, w: 0, h: 0, radius: 0 };
|
|
186
199
|
return flood(el, { w, h, x: w / 2, y: h / 2, color: accent, steps: [{ at: B - 100, shape: zero }, { at: B - 0.28, shape: "cover" }, { at: B - 0.02, shape: zero }], scene: ctx.id });
|
|
@@ -27,6 +27,7 @@ export const MENU: readonly MenuItem[] = [
|
|
|
27
27
|
{ name: "neonSign", kind: "text", does: "네온 관 글자가 시동 걸리듯 깜빡이다 켜진다", example: "neonSign" },
|
|
28
28
|
{ name: "ledBoard", kind: "text", does: "LED 점 글판에 한 칸씩 찍힌다", example: "ledBoard" },
|
|
29
29
|
{ name: "none", kind: "wipe", does: "덮지 않고 바로 컷" },
|
|
30
|
+
{ name: "fade", kind: "wipe", does: "바탕색으로 잠깐 옅어졌다 돌아온다(경계 양쪽 0.15초씩) — 큰 면이 화면을 쓸지 않는 조용한 경계" },
|
|
30
31
|
{ name: "brushWipe", kind: "wipe", does: "붓 획 띠가 번갈아 덮었다가 꼬리가 빠지며 걷힌다", example: "brushWipe" },
|
|
31
32
|
{ name: "irisBlades", kind: "wipe", does: "여섯 날 조리개가 닫혔다 열린다", example: "irisBlades" },
|
|
32
33
|
{ name: "flood", kind: "wipe", does: "가운데 점이 화면을 넘치게 덮었다가 오므라든다", example: "flood" },
|
|
@@ -51,7 +52,7 @@ const BASE: Required<Pool> = { bg: ["still"], text: ["bigWord", "echoStack", "sp
|
|
|
51
52
|
// 테마마다 비유에 맞는 수 — 테마를 고르면 용도 묶음보다 앞에 둔다(테마 효과와 결이 같은 것만)
|
|
52
53
|
export const THEME_POOLS: Readonly<Record<string, Pool>> = {
|
|
53
54
|
"debug-hud": { text: ["splitFlap", "ledBoard", "ghostStack"], wipe: ["irisBlades", "flood"] },
|
|
54
|
-
"og-measure": { text: ["splitFlap", "ghostStack", "echoStack"], wipe: ["
|
|
55
|
+
"og-measure": { text: ["splitFlap", "ghostStack", "echoStack"], wipe: ["fade"] },
|
|
55
56
|
"kasaterm-cells": { text: ["ledBoard", "splitFlap", "slamWords"], wipe: ["flood", "irisBlades"] },
|
|
56
57
|
"kawaii-sticker": { text: ["ransomLetters", "slamWords", "echoStack"], wipe: ["brushWipe", "flood"] },
|
|
57
58
|
"riso-zine": { text: ["ransomLetters", "echoStack", "slamWords"], wipe: ["brushWipe", "flood"] },
|
|
@@ -62,21 +63,26 @@ export const THEME_POOLS: Readonly<Record<string, Pool>> = {
|
|
|
62
63
|
|
|
63
64
|
const known = (kind: DressKind) => (n: string) => menuItem(n)?.kind === kind;
|
|
64
65
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* 틀 묶음 + 테마 묶음 → 한 벌. 테마 수를 앞에, 메뉴에 없는 이름은 버린다.
|
|
68
|
+
* allow 는 테마가 허용한 경계 전환(theme.json transitions) — 주면 덮기는 그 안에서만, 남는 게 없으면 컷(none)
|
|
69
|
+
*/
|
|
70
|
+
export function bundleFor(tpl?: Bundle, theme?: string, allow?: readonly string[] | null): Required<Bundle> {
|
|
67
71
|
// 변주 이름(<세계>/<변주>)이면 그 세계 묶음
|
|
68
72
|
const th = theme ? THEME_POOLS[theme.split("/")[0]] : undefined;
|
|
69
73
|
const merge = (kind: DressKind, ...lists: (string[] | undefined)[]) => [...new Set(lists.flatMap((l) => l ?? []))].filter(known(kind));
|
|
74
|
+
const allowed = (list: string[]) => (allow ? list.filter((n) => allow.includes(n)) : list);
|
|
70
75
|
const roles: Record<string, Pool> = {};
|
|
71
76
|
for (const [role, p] of Object.entries(tpl?.roles ?? {})) {
|
|
72
77
|
// 역할 몫은 틀이 이유를 대고 고른 것(훅은 첫 프레임부터 꽉 차는 글) — 테마 수는 그 뒤에 섞지 않는다
|
|
73
|
-
roles[role] = { bg: p.bg?.filter(known("bg")), text: p.text?.filter(known("text")), wipe: p.wipe
|
|
78
|
+
roles[role] = { bg: p.bg?.filter(known("bg")), text: p.text?.filter(known("text")), wipe: p.wipe && allowed(p.wipe.filter(known("wipe"))) };
|
|
74
79
|
}
|
|
80
|
+
const wipe = allowed(merge("wipe", th?.wipe, tpl?.wipe ?? BASE.wipe));
|
|
75
81
|
const bg = merge("bg", th?.bg, tpl?.bg ?? BASE.bg);
|
|
76
82
|
return {
|
|
77
83
|
bg: bg.length ? bg : BASE.bg,
|
|
78
84
|
text: merge("text", th?.text, tpl?.text ?? BASE.text),
|
|
79
|
-
wipe:
|
|
85
|
+
wipe: wipe.length || !allow ? wipe : ["none"],
|
|
80
86
|
roles,
|
|
81
87
|
};
|
|
82
88
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clamp, emit, progress, toEase, type Ease, type EaseName, type Style, safeTime } from "@kasaframe/core";
|
|
1
|
+
import { clamp, cue, emit, progress, toEase, type Ease, type EaseName, type Style, safeTime } from "@kasaframe/core";
|
|
2
2
|
|
|
3
3
|
export type TransitionName = "whip" | "zoomThrough" | "slide" | "matchCut" | "crossfade";
|
|
4
4
|
|
|
@@ -43,6 +43,8 @@ export function transition(name: TransitionName, t: number, opts: TransitionOpts
|
|
|
43
43
|
const sx = dir === "left" ? -1 : dir === "right" ? 1 : 0;
|
|
44
44
|
const sy = dir === "up" ? -1 : dir === "down" ? 1 : 0;
|
|
45
45
|
if (opts.emit !== false && raw > 0 && raw < 1) emit({ kind: "move", source: opts.source ?? name, scene: opts.scene, name: `transition:${name}`, value: 1 });
|
|
46
|
+
// 끊어 잇기(matchCut)·겹쳐 바꾸기(crossfade)는 소리 없이 — 휙 지나가는 것만
|
|
47
|
+
if (opts.emit !== false && name !== "matchCut" && name !== "crossfade") cue("whoosh", t - opts.at, { source: opts.source ?? name, scene: opts.scene });
|
|
46
48
|
const both = raw > 0 && raw < 1;
|
|
47
49
|
switch (name) {
|
|
48
50
|
case "whip": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { onSeek } from "@kasaframe/adapters";
|
|
2
|
-
import { clamp, emit, frameSample, rand, safeTime, type BeatGrid, type KfStyle, type SceneCtx } from "@kasaframe/core";
|
|
2
|
+
import { clamp, cue, emit, frameSample, rand, safeTime, type BeatGrid, type KfStyle, type SceneCtx } from "@kasaframe/core";
|
|
3
3
|
import { FIT, TRANSITIONS_3D, arrivalsOn, durOf, hump, rawProgress, resolveTiming, type FoldStyle, type Timing, type TimingOpts, type Transition3dName, type WindowRect } from "./rig.ts";
|
|
4
4
|
import { camera, inPaint, makeScreen, placeholder, sharedRenderer, NO_FX, type CamFx, type Kit, type KindRun, type Palette, type Screen, type Source } from "./world.ts";
|
|
5
5
|
import { liveNeighbors, type LiveProject } from "./live.ts";
|
|
@@ -206,6 +206,8 @@ export function transition3d(root: HTMLElement, ctxOrOpts?: SceneCtx | Transitio
|
|
|
206
206
|
paint(t);
|
|
207
207
|
const u = rawProgress(t, timing);
|
|
208
208
|
if (o.emit !== false && u > 0 && u < 1) emit({ kind: "move", source, scene, name: `transition3d:${kind}`, value: 1 });
|
|
209
|
+
// 넘어가기 시작하는 순간(화면이 여럿이면 화면마다)에 휙 — 장면 앞 한 프레임에서 시작하는 기본 구간은 장면 시작으로
|
|
210
|
+
if (o.emit !== false) for (const a of arrivals.length > 1 ? arrivals.slice(0, -1) : [timing.at]) cue("whoosh", t - Math.max(0, a), { source: `${source}:${Math.max(0, a).toFixed(3)}`, scene });
|
|
209
211
|
}) as Transition3dDraw;
|
|
210
212
|
// seek 의 그리기 단계 뒤 — 영상은 엔진이 시각을 맞춘 뒤, DOM 은 이번 DOM 으로 찍힌 스냅숏(paint 이벤트) 안에서 올리고 같은 t 로 다시 그린다.
|
|
211
213
|
// React 장면은 첫 seek 에서야 지어져 페이지 ready 가 그림을 못 기다리므로 여기서 받는다
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createBeats, silentBeats, type SceneCtx } from "@kasaframe/core";
|
|
1
|
+
import { createBeats, silentBeats, withoutSfx, type SceneCtx } from "@kasaframe/core";
|
|
2
2
|
import type { KfProjectJson, KfSceneEntry, SceneModule } from "@kasaframe/adapters";
|
|
3
3
|
import { reactScene } from "@kasaframe/adapters/react";
|
|
4
4
|
import type { LiveDom, Source } from "./world.ts";
|
|
@@ -42,9 +42,11 @@ export function sceneSource(mod: SceneModule, ctx: SceneCtx, entry: Pick<KfScene
|
|
|
42
42
|
from: entry.from,
|
|
43
43
|
dur: entry.dur,
|
|
44
44
|
beats: ctx.beats?.data ? createBeats(ctx.beats.data, entry.from) : silentBeats(entry.dur),
|
|
45
|
+
sfx: () => {},
|
|
45
46
|
};
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// 한 벌 더 그린 장면은 소리를 안 낸다 — 진짜 장면이 이미 남겼다
|
|
48
|
+
const draw = mod.react ? reactScene(box, mod.react, sub) : mod.build?.(box, sub);
|
|
49
|
+
return draw ? (t: number) => withoutSfx(() => draw(t)) : undefined;
|
|
48
50
|
},
|
|
49
51
|
time,
|
|
50
52
|
still,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyStyle, clamp, combine } from "@kasaframe/core";
|
|
2
2
|
import { fitSize, textEm } from "@kasaframe/motion/moves";
|
|
3
|
-
import { area,
|
|
3
|
+
import { area, decimalsOf, factRoll, glider, moving, node, slotsOf, spaced } from "../kit.ts";
|
|
4
4
|
import { cssColor, objectFor } from "../objects.ts";
|
|
5
5
|
import type { Pattern } from "../types.ts";
|
|
6
6
|
|
|
@@ -12,13 +12,13 @@ export interface BarsContent {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* 막대가 순위대로 박마다 한 줄씩 자라고 값이 같은 진행도로 굴러 오른다(막대 길이 = 값). 다 자라면 짚개가 강조
|
|
15
|
+
* 막대가 순위대로 박마다 한 줄씩 자라고 값이 같은 진행도로 굴러 오른다(막대 길이 = 값). 다 자라면 짚개가 강조 줄부터 짚는다(머무는 테마는 목소리가 다른 줄 이름을 말할 때만 그리로).
|
|
16
16
|
* 단위·이름이 객체 사전의 막대 물체면(지연 ms) 그 색·모서리. 자라는 시각을 장면 끝까지 펴서 쉬지 않는 몫을 막대가 진다.
|
|
17
17
|
*/
|
|
18
18
|
const pattern: Pattern<BarsContent> = {
|
|
19
19
|
id: "bars.rank",
|
|
20
20
|
type: "bars",
|
|
21
|
-
does: "막대가 순위대로 박마다 자라고 값이 굴러 오른다, 다 자라면 짚개가 강조
|
|
21
|
+
does: "막대가 순위대로 박마다 자라고 값이 굴러 오른다, 다 자라면 짚개가 강조 줄부터 짚는다",
|
|
22
22
|
energy: ["mid"],
|
|
23
23
|
family: "grow",
|
|
24
24
|
camera: ["none"],
|
|
@@ -93,10 +93,9 @@ const pattern: Pattern<BarsContent> = {
|
|
|
93
93
|
});
|
|
94
94
|
const hotLine = lines[Math.max(0, hot)];
|
|
95
95
|
kit.stage?.hero(hotLine.val);
|
|
96
|
-
// 다 자라면 짚개가 강조
|
|
97
|
-
const point = glider(host, [hotLine
|
|
96
|
+
// 다 자라면 짚개가 강조 줄부터 짚는다 — 머무는 테마는 목소리가 다른 줄 이름을 말할 때만 그리로
|
|
97
|
+
const point = glider(host, [hotLine, ...lines.filter((L) => L !== hotLine)].map((L) => ({ el: L.row, text: L.r.label })), { kit, ctx, from: hotAt - 0.1, pad: 0.04 });
|
|
98
98
|
const hotLand = slots.land(hotLine.val, { ctx, at: hotAt, seed: `${kit.seed}:hot` });
|
|
99
|
-
const stops = beatStops(kit.hits, hotAt - 0.1, ctx.dur);
|
|
100
99
|
|
|
101
100
|
return (t: number) => {
|
|
102
101
|
moving("bars.rank", ctx);
|
|
@@ -111,7 +110,7 @@ const pattern: Pattern<BarsContent> = {
|
|
|
111
110
|
L.val.style.left = `${labelW + trackW * (L.r.value / max) * roll.p + 16 * u}px`;
|
|
112
111
|
applyStyle(L.val, L.i === hotLine.i ? combine(L.landed(t), hotLand(t)) : L.landed(t));
|
|
113
112
|
}
|
|
114
|
-
point(t
|
|
113
|
+
point(t);
|
|
115
114
|
};
|
|
116
115
|
},
|
|
117
116
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyStyle, clamp, spring } from "@kasaframe/core";
|
|
2
2
|
import { fitSize } from "@kasaframe/motion/moves";
|
|
3
|
-
import { ACCENT_TEXT, area,
|
|
3
|
+
import { ACCENT_TEXT, area, decimalsOf, fmt, glider, moving, node, slotsOf } from "../kit.ts";
|
|
4
4
|
import { cssColor, objectFor } from "../objects.ts";
|
|
5
5
|
import type { BeforeAfter } from "./strike.ts";
|
|
6
6
|
import type { Pattern } from "../types.ts";
|
|
@@ -92,8 +92,7 @@ const pattern: Pattern<BeforeAfter> = {
|
|
|
92
92
|
const moveFrom = fillEnd + 0.15;
|
|
93
93
|
const moveEnd = moveFrom + (count - 1) / rate;
|
|
94
94
|
const landedAt = moveEnd + 0.05;
|
|
95
|
-
const
|
|
96
|
-
const point = glider(host, [cells[0], label, val], { hub: true });
|
|
95
|
+
const point = glider(host, [{ el: cells[0] }, { el: label, text: after.label }, { el: val, text: fmt(after.value!, dec) }], { kit, ctx, from: landedAt + 0.1 });
|
|
97
96
|
const writeA = slots.write(label, { ctx, text: before.label, at: 0, dur: 0.3, seed: `${kit.seed}:a` });
|
|
98
97
|
const landed = slots.land(cells[0], { ctx, at: landedAt, seed: `${kit.seed}:land` });
|
|
99
98
|
|
|
@@ -119,7 +118,7 @@ const pattern: Pattern<BeforeAfter> = {
|
|
|
119
118
|
// 라벨은 전 → 후로 바뀐다
|
|
120
119
|
if (t >= landedAt) label.textContent = after.label;
|
|
121
120
|
else writeA(t);
|
|
122
|
-
point(t
|
|
121
|
+
point(t);
|
|
123
122
|
};
|
|
124
123
|
},
|
|
125
124
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyStyle, clamp, combine, ease, progress, spring } from "@kasaframe/core";
|
|
2
2
|
import { textEm } from "@kasaframe/motion/moves";
|
|
3
|
-
import { ACCENT_TEXT, area,
|
|
3
|
+
import { ACCENT_TEXT, area, cameraOk, decimalsOf, factRoll, glider, moving, node, slotsOf } from "../kit.ts";
|
|
4
4
|
import type { Pattern } from "../types.ts";
|
|
5
5
|
|
|
6
6
|
interface Side {
|
|
@@ -92,9 +92,8 @@ const pattern: Pattern<BeforeAfter> = {
|
|
|
92
92
|
const writeChange = change ? slots.write(change, { ctx, text: c.change!, at: changeAt, dur: 0.3, seed: `${kit.seed}:ch` }) : null;
|
|
93
93
|
const landed = slots.land(afterNum, { ctx, at: land, seed: `${kit.seed}:land` });
|
|
94
94
|
if (cameraOk(ctx)) kit.stage?.camera.pushIn(land, { to: afterBox, scale: 1.1, dur: Math.max(0.6, ctx.dur - land) });
|
|
95
|
-
//
|
|
96
|
-
const point = glider(host, [afterBox, ...(change ? [change] : []), beforeBox], {
|
|
97
|
-
const stops = beatStops(kit.hits, land + 0.1, ctx.dur);
|
|
95
|
+
// 앉으면 짚개가 후 값부터 짚는다 — 머무는 테마는 목소리가 바뀐 정도·전 값을 말할 때만 그리로
|
|
96
|
+
const point = glider(host, [{ el: afterBox, text: afterText }, ...(change ? [{ el: change, text: c.change }] : []), { el: beforeBox, text: beforeText }], { kit, ctx, from: land + 0.1 });
|
|
98
97
|
|
|
99
98
|
return (t: number) => {
|
|
100
99
|
moving("before-after.strike", ctx);
|
|
@@ -116,7 +115,7 @@ const pattern: Pattern<BeforeAfter> = {
|
|
|
116
115
|
afterNum.textContent = shown;
|
|
117
116
|
// 굴리는 동안 긴 글은 같은 폭에 줄여 넣는다 — 상자가 출렁이지 않게
|
|
118
117
|
afterNum.style.fontSize = `${Math.round(bigPx * Math.min(1, textEm(afterText) / textEm(shown)))}px`;
|
|
119
|
-
point(t
|
|
118
|
+
point(t);
|
|
120
119
|
applyStyle(afterNum, combine({ y: (1 - ease.outExpo(progress(t, openAt, 0.35))) * bigPx * 1.1 }, landed(t)));
|
|
121
120
|
};
|
|
122
121
|
},
|