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
|
@@ -2,6 +2,8 @@ import { clamp, ease, progress, spring, type SceneCtx, type Style } from "@kasaf
|
|
|
2
2
|
import { mark } from "@kasaframe/motion/moves";
|
|
3
3
|
import { typeDuration, typeText } from "@kasaframe/motion";
|
|
4
4
|
import { partial } from "@kasaframe/themes/slotkit";
|
|
5
|
+
import type { MarkBox, MarkPaint } from "@kasaframe/themes/slots";
|
|
6
|
+
import { keyCue, rollCues, slamCue, stopCues, withSfx } from "./sfx.ts";
|
|
5
7
|
import type { Kit, ThemeSlots } from "./types.ts";
|
|
6
8
|
|
|
7
9
|
// 패턴들이 같이 쓰는 몫. 모듈 맨 위에서 DOM 을 만지지 않는다 — 감독이 Node 에서 meta·fits 를 읽는다.
|
|
@@ -58,12 +60,40 @@ export function inkLike(ctx: SceneCtx): boolean {
|
|
|
58
60
|
return !Number.isFinite(a) || !Number.isFinite(b) || (hi + 0.05) / (lo + 0.05) < 3;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
// 글을 짚는 겹의 칠 — 강조색 글. 강조색 글이 본문 글과 같은 테마(HUD)면 반전 칸으로(같은 색으로 채우면 아무것도 안 바뀐다)
|
|
64
|
+
const readCss = (ctx: SceneCtx): string => (inkLike(ctx) ? "background:var(--kf-accent);color:var(--kf-on-accent)" : `color:${ACCENT_TEXT}`);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 다시 짚기 진행도 — 정지점 사이마다 0→1→0 으로 박마다 한 번 차올랐다 빠진다. 마지막 사이(장면 끝)는 차오르기만 해 끝 프레임에 다 짚혀 있다.
|
|
68
|
+
* 첫 정지점 전엔 0. 훑는 짚개를 쓰는 테마에서 짚을 대상이 하나뿐인 장면(짚개 없음)이 그 글을 박마다 강조로 채웠다 비워 쉬지 않는 몫
|
|
69
|
+
*/
|
|
70
|
+
export function reread(t: number, stops: readonly number[]): number {
|
|
71
|
+
if (stops.length < 2 || t < stops[0]) return 0;
|
|
72
|
+
let j = 0;
|
|
73
|
+
while (j + 2 < stops.length && stops[j + 1] <= t) j++;
|
|
74
|
+
const p = clamp((t - stops[j]) / Math.max(1e-3, stops[j + 1] - stops[j]));
|
|
75
|
+
return j === stops.length - 2 ? p : 1 - Math.abs(2 * p - 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 글 상자 el(position:relative) 위에 같은 글의 강조 겹(readCss) — p 만큼 아래부터 글 전체에 차오른다(reread 로 다시 짚기).
|
|
80
|
+
* 왼쪽부터 짚으면 빈칸·가는 글자(1·i)를 지나는 동안 화면이 거의 안 바뀌어 멈춘 화면이 된다 — 차오름은 모든 글자를 함께 지난다
|
|
81
|
+
*/
|
|
82
|
+
export function readOver(el: HTMLElement, text: string, ctx: SceneCtx): (p: number) => void {
|
|
83
|
+
const hi = node(el, `position:absolute;inset:0;${readCss(ctx)};white-space:inherit;text-align:inherit;visibility:hidden;pointer-events:none`, text);
|
|
84
|
+
return (p) => {
|
|
85
|
+
const k = clamp(p);
|
|
86
|
+
hi.style.visibility = k > 0 ? "visible" : "hidden";
|
|
87
|
+
hi.style.clipPath = `inset(${((1 - k) * 100).toFixed(2)}% 0 0 0)`;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
/**
|
|
62
|
-
* 큰 이름 — 글자마다 가림막 아래에서 올라와 서고(riseAt 부터 each 간격), fill 진행도(0..1)만큼 강조가 왼쪽부터 글자를
|
|
63
|
-
*
|
|
92
|
+
* 큰 이름 — 글자마다 가림막 아래에서 올라와 서고(riseAt 부터 each 간격), fill 진행도(0..1)만큼 강조가 왼쪽부터 글자를 짚는다(readCss).
|
|
93
|
+
* up 을 주면 강조가 모든 글자에 아래부터 그만큼 차오른다(다시 짚기 — readOver 와 같은 결)
|
|
64
94
|
*/
|
|
65
|
-
export function riseName(el: HTMLElement, text: string, ctx: SceneCtx): (t: number, o: { riseAt: number; each: number; fill: number }) => void {
|
|
66
|
-
const hiCss =
|
|
95
|
+
export function riseName(el: HTMLElement, text: string, ctx: SceneCtx): (t: number, o: { riseAt: number; each: number; fill: number; up?: number }) => void {
|
|
96
|
+
const hiCss = readCss(ctx);
|
|
67
97
|
odoStyle(el);
|
|
68
98
|
const glyphs = Array.from(text).map((ch) => {
|
|
69
99
|
const clip = node(el, "display:inline-block;overflow:hidden;padding:0.06em 0 0.12em;margin:-0.06em 0 -0.12em");
|
|
@@ -87,9 +117,9 @@ export function riseName(el: HTMLElement, text: string, ctx: SceneCtx): (t: numb
|
|
|
87
117
|
g.firstChild.remove();
|
|
88
118
|
}
|
|
89
119
|
if (!up) g.dataset.kfOdo = g.dataset.ch ?? "";
|
|
90
|
-
const k = clamp(fp - i);
|
|
120
|
+
const k = clamp(o.up ?? fp - i);
|
|
91
121
|
hi.style.visibility = k > 0 ? "visible" : "hidden";
|
|
92
|
-
hi.style.clipPath = `inset(0 ${((1 - k) * 100).toFixed(2)}% 0 0)`;
|
|
122
|
+
hi.style.clipPath = o.up === undefined ? `inset(0 ${((1 - k) * 100).toFixed(2)}% 0 0)` : `inset(${((1 - k) * 100).toFixed(2)}% 0 0 0)`;
|
|
93
123
|
});
|
|
94
124
|
};
|
|
95
125
|
}
|
|
@@ -151,16 +181,68 @@ export function odometer(el: HTMLElement, o: { template: string; decimals: numbe
|
|
|
151
181
|
};
|
|
152
182
|
}
|
|
153
183
|
|
|
184
|
+
/** 테마 짚음 슬롯을 이 장면에 묶은 것 — 칸 요소를 받아 칠하고 자리 함수를 준다 */
|
|
185
|
+
export type Marker = (el: HTMLElement) => MarkPaint;
|
|
186
|
+
|
|
187
|
+
/** 테마가 짚는 칸 모양(mark 슬롯)을 채웠으면 그것, 아니면 null — 짚개·반전 짚음이 패턴 기본(강조색 꽉 찬 칸)으로 그린다 */
|
|
188
|
+
export const markOf = (kit: Kit, ctx: SceneCtx): Marker | null => {
|
|
189
|
+
const slot = kit.slots.mark;
|
|
190
|
+
return slot ? (el) => slot(el, { ctx, seed: `${kit.seed}:mark` }) : null;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// 짚는 칸 요소 — 대상 글 뒤에 깔린다(놓는 쪽 상자에 isolation). 칠은 테마 슬롯이나 plainMark 가 한다
|
|
194
|
+
const MARK_CSS = "position:absolute;left:0;top:0;width:0;height:0;z-index:-1;visibility:hidden;pointer-events:none";
|
|
195
|
+
|
|
196
|
+
// 테마가 비운 짚음 — 강조색 꽉 찬 칸이 크기 그대로 미끄러진다(직선 7 + smoothstep 3 — 훑는 동안 정지점에 닿아 서면 그 사이가 멈춘 화면)
|
|
197
|
+
function plainMark(el: HTMLElement): MarkPaint {
|
|
198
|
+
el.style.background = "var(--kf-accent)";
|
|
199
|
+
el.style.borderRadius = "var(--kf-radius-sm)";
|
|
200
|
+
return {
|
|
201
|
+
ink: "var(--kf-on-accent)",
|
|
202
|
+
at(from, to, u) {
|
|
203
|
+
const a = from ?? to;
|
|
204
|
+
const e = 0.7 * u + 0.3 * u * u * (3 - 2 * u);
|
|
205
|
+
const mix = (p: number, q: number) => p + (q - p) * e;
|
|
206
|
+
return { x: mix(a.x, to.x), y: mix(a.y, to.y), w: mix(a.w, to.w), h: mix(a.h, to.h) };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function place(el: HTMLElement, b: MarkBox): void {
|
|
212
|
+
el.style.transform = `translate(${b.x.toFixed(1)}px, ${b.y.toFixed(1)}px)`;
|
|
213
|
+
el.style.width = `${b.w.toFixed(1)}px`;
|
|
214
|
+
el.style.height = `${b.h.toFixed(1)}px`;
|
|
215
|
+
}
|
|
216
|
+
|
|
154
217
|
/**
|
|
155
218
|
* 반전 짚음 — el(글 상자, position:relative — 쓰기 슬롯이 글을 갈아 끼우는 요소면 그 부모에 건다) 위에 강조색 면 + onAccent 글 겹을 깔고 p 만큼 왼쪽부터 드러낸다.
|
|
156
|
-
* 옅은 형광펜(accentSoft)은 밝은 바탕에서 화면이 바뀐 것으로 안 읽혀 이쪽이 쉬지 않는 몫을
|
|
219
|
+
* 옅은 형광펜(accentSoft)은 밝은 바탕에서 화면이 바뀐 것으로 안 읽혀 이쪽이 쉬지 않는 몫을 진다.
|
|
220
|
+
* 테마 짚음(mark)이 있으면 그 칸이 글 뒤에 p 만큼 나타나고(모양은 테마 몫), 칸 위 글은 테마 ink 로
|
|
157
221
|
*/
|
|
158
|
-
export function inverseOver(el: HTMLElement, text: string): (p: number) => void {
|
|
159
|
-
|
|
222
|
+
export function inverseOver(el: HTMLElement, text: string, mark: Marker | null = null): (p: number) => void {
|
|
223
|
+
if (!mark) {
|
|
224
|
+
const hi = node(el, "position:absolute;inset:0;background:var(--kf-accent);color:var(--kf-on-accent);white-space:inherit;text-align:inherit;visibility:hidden;pointer-events:none", text);
|
|
225
|
+
return (p) => {
|
|
226
|
+
const k = clamp(p);
|
|
227
|
+
hi.style.visibility = k > 0 ? "visible" : "hidden";
|
|
228
|
+
hi.style.clipPath = `inset(0 ${((1 - k) * 100).toFixed(2)}% 0 0)`;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
el.style.isolation = "isolate";
|
|
232
|
+
const block = node(el, MARK_CSS);
|
|
233
|
+
const shape = mark(block);
|
|
234
|
+
const hi = shape.ink ? node(el, `position:absolute;inset:0;color:${shape.ink};white-space:inherit;text-align:inherit;visibility:hidden;pointer-events:none`, text) : null;
|
|
160
235
|
return (p) => {
|
|
161
236
|
const k = clamp(p);
|
|
237
|
+
const w = el.offsetWidth;
|
|
238
|
+
const h = el.offsetHeight;
|
|
239
|
+
const pad = (parseFloat(getComputedStyle(el).fontSize) || 20) * 0.12;
|
|
240
|
+
const b = shape.at(null, { x: -pad, y: -pad * 0.5, w: w + pad * 2, h: h + pad }, k);
|
|
241
|
+
block.style.visibility = k > 0 ? "visible" : "hidden";
|
|
242
|
+
place(block, b);
|
|
243
|
+
if (!hi) return;
|
|
162
244
|
hi.style.visibility = k > 0 ? "visible" : "hidden";
|
|
163
|
-
hi.style.clipPath = `inset(
|
|
245
|
+
hi.style.clipPath = `inset(${b.y.toFixed(1)}px ${(w - b.x - b.w).toFixed(1)}px ${(h - b.y - b.h).toFixed(1)}px ${b.x.toFixed(1)}px)`;
|
|
164
246
|
};
|
|
165
247
|
}
|
|
166
248
|
|
|
@@ -175,6 +257,7 @@ export function sceneDrop(ctx: SceneCtx): number | null {
|
|
|
175
257
|
* 등장 전엔 opacity 0 이라 그 사이를 다른 움직임이 채워야 한다
|
|
176
258
|
*/
|
|
177
259
|
export function slam(t: number, at: number, from = 1.9): Style {
|
|
260
|
+
slamCue(t, at);
|
|
178
261
|
const lead = 0.16;
|
|
179
262
|
if (t < at - lead) return { opacity: 0 };
|
|
180
263
|
if (t < at) {
|
|
@@ -210,9 +293,9 @@ export function fitOnce(el: HTMLElement, maxW: number, maxPx: number, text?: str
|
|
|
210
293
|
}
|
|
211
294
|
|
|
212
295
|
/**
|
|
213
|
-
*
|
|
214
|
-
* 끝 프레임에서
|
|
215
|
-
* 장면이 길어도
|
|
296
|
+
* 박 정지점 — from 뒤의 박을 간격 gap 넘게 골라 쓰고(빈 곳은 고르게 메운다), 마지막 정지점은 장면 끝(to)이다.
|
|
297
|
+
* 끝 프레임에서 옮겨 다니는 틀이 대상에 거의 앉아 있어야 표지·견본 스틸에 반쯤 걸친 틀이 안 찍힌다.
|
|
298
|
+
* 장면이 길어도 빠르기는 박이 정한다 — 장면 길이에 비례해 느려지는 차오름은 긴 장면에서 멈춘 화면이 된다
|
|
216
299
|
*/
|
|
217
300
|
export function beatStops(hits: readonly number[], from: number, to: number, gap = 0.4): number[] {
|
|
218
301
|
const out = [from];
|
|
@@ -225,18 +308,13 @@ export function beatStops(hits: readonly number[], from: number, to: number, gap
|
|
|
225
308
|
return out;
|
|
226
309
|
}
|
|
227
310
|
|
|
228
|
-
/**
|
|
229
|
-
* 짚개 — 강조색 칸이 대상 사이를 정지점마다 옮겨 다니며 「지금 볼 항」을 짚는다. 정지점 사이를 내내 움직인다(직선 7 + smoothstep 3 —
|
|
230
|
-
* 닿아서 서면 그 사이가 멈춘 화면). 칸은 대상 글 뒤에 깔리고(host 에 isolation), 칸이 앉은 대상 글은 onAccent 로 바뀐다.
|
|
231
|
-
* 대상 자리는 그 프레임에 잰다 — 대상이 굴러가거나 내려앉아도 따라간다
|
|
232
|
-
*/
|
|
233
311
|
// 글자를 두 색으로 — over 사각형(글 상자 안 좌표) 안은 띠 위 글색, 밖은 그 글의 원래 색. 글자 모양으로 오려 칠하니 반쯤 겹쳐도 겹친 글자 부분만 바뀐다.
|
|
234
312
|
// 글색(color)은 건드리지 않아 원래 색을 늘 다시 읽을 수 있다. 칠하기 전 인라인 값을 그대로 되돌린다 — 테마 슬롯이 글에 칠한 배경을 지우면
|
|
235
313
|
// 한 번 짚고 지나간 뒤로 그림이 바뀌어 같은 시각이 다르게 그려졌다(frame.nondeterministic)
|
|
236
314
|
const TONE_PROPS = ["-webkit-text-fill-color", "background-image", "background-color", "background-repeat", "background-size", "background-position", "-webkit-background-clip", "background-clip"];
|
|
237
315
|
const toneSaved = new WeakMap<HTMLElement, [string, string, string][]>();
|
|
238
316
|
|
|
239
|
-
function paintTone(el: HTMLElement, over:
|
|
317
|
+
function paintTone(el: HTMLElement, over: MarkBox | null, ink = "var(--kf-on-accent)"): void {
|
|
240
318
|
const s = el.style;
|
|
241
319
|
if (!over) {
|
|
242
320
|
const saved = toneSaved.get(el);
|
|
@@ -250,7 +328,7 @@ function paintTone(el: HTMLElement, over: { x: number; y: number; w: number; h:
|
|
|
250
328
|
el.dataset.kfTone = "";
|
|
251
329
|
s.setProperty("-webkit-text-fill-color", "transparent");
|
|
252
330
|
s.backgroundColor = "currentColor";
|
|
253
|
-
s.backgroundImage =
|
|
331
|
+
s.backgroundImage = `linear-gradient(${ink}, ${ink})`;
|
|
254
332
|
s.backgroundRepeat = "no-repeat";
|
|
255
333
|
s.backgroundSize = `${over.w.toFixed(1)}px ${over.h.toFixed(1)}px`;
|
|
256
334
|
s.backgroundPosition = `${over.x.toFixed(1)}px ${over.y.toFixed(1)}px`;
|
|
@@ -258,17 +336,22 @@ function paintTone(el: HTMLElement, over: { x: number; y: number; w: number; h:
|
|
|
258
336
|
s.setProperty("background-clip", "text");
|
|
259
337
|
}
|
|
260
338
|
|
|
261
|
-
// 대상 안에
|
|
339
|
+
// 대상 안에 글 뒤를 가리는 판이 있나 — 바탕이 칠해졌고(투명·알파 0 이면 없는 것) 대상 상자의 절반 넘게 덮는 것만.
|
|
340
|
+
// 판 위 구분선·모서리 눈금처럼 가는 장식(og-measure 의 위 구분선 3px)을 판으로 읽으면 띠는 숫자 뒤에 훤히 보이는데 글은 원래 색으로 남았다.
|
|
341
|
+
// 넓이는 변환 전 크기로 잰다 — 판이 내려앉는 중(scale)이어도 같은 답이 나오게
|
|
262
342
|
function hasPlate(root: HTMLElement): boolean {
|
|
343
|
+
const area = (el: HTMLElement) => el.offsetWidth * el.offsetHeight;
|
|
344
|
+
const whole = area(root);
|
|
263
345
|
const els = [root, ...Array.from(root.querySelectorAll<HTMLElement>("*"))];
|
|
264
346
|
return els.some((el) => {
|
|
265
347
|
// 짚개가 글에 칠한 배경(표식 data-kf-tone)은 판이 아니다 — 앞 프레임에 칠했는지와 상관없이 같은 답이 나오게
|
|
266
348
|
if (el.dataset.kfTone !== undefined) return false;
|
|
267
349
|
const cs = getComputedStyle(el);
|
|
268
|
-
if (cs.backgroundImage !== "none") return true;
|
|
269
350
|
const m = /rgba?\(([^)]+)\)/.exec(cs.backgroundColor);
|
|
270
|
-
const
|
|
271
|
-
|
|
351
|
+
const painted = cs.backgroundImage !== "none" || (m ? Number(m[1].split(",")[3] ?? 1) : 0) > 0.05;
|
|
352
|
+
if (!painted) return false;
|
|
353
|
+
// SVG 처럼 레이아웃 크기가 없는 것과 아직 자리를 안 잡은 대상은 칠했으면 판으로 둔다
|
|
354
|
+
return !(el instanceof HTMLElement) || !whole || area(el) >= whole * 0.5;
|
|
272
355
|
});
|
|
273
356
|
}
|
|
274
357
|
|
|
@@ -287,9 +370,145 @@ function leavesOf(root: HTMLElement): HTMLElement[] {
|
|
|
287
370
|
return out;
|
|
288
371
|
}
|
|
289
372
|
|
|
290
|
-
|
|
373
|
+
/** 요점 — 그 시각부터 짚개가 내려앉아 머무는 대상 */
|
|
374
|
+
export interface Focus {
|
|
375
|
+
at: number;
|
|
376
|
+
el: HTMLElement;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 낱말·글을 견줄 꼴 — 문장부호를 떼고 소문자로
|
|
380
|
+
const bare = (s: string) => s.toLowerCase().replace(/[^\p{L}\p{N}.]+/gu, " ").trim();
|
|
381
|
+
|
|
382
|
+
// 목소리 낱말 w 가 글 text 를 말했나 — 낱말이 글의 한 마디이거나 거기에 조사·어미가 붙은 꼴(「캐시가」 = 캐시). 두 글자 아래 마디는 숫자만
|
|
383
|
+
function said(w: string, text: string): boolean {
|
|
384
|
+
const word = bare(w);
|
|
385
|
+
if (!word) return false;
|
|
386
|
+
return bare(text)
|
|
387
|
+
.split(" ")
|
|
388
|
+
.some((tk) => (tk.length >= 2 || /^\d/.test(tk)) && (word === tk || (word.startsWith(tk) && word.length - tk.length <= 3) || (tk.startsWith(word) && word.length >= 2)));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 짚을 요점과 그 시각 — 첫 대상(장면이 주인공·강조로 세운 것)에 from 에 내려앉고, 그 뒤엔 목소리가 다른 대상의 글을 말하는 낱말 시각에만
|
|
393
|
+
* 그리로 옮긴다(주인공 글을 다시 말하면 돌아온다). 나타나는 차례로 훑지 않는다 — 요점이 바뀌지 않으면 머문다.
|
|
394
|
+
* 대상 글은 다 찍힌 뒤의 글(패턴이 아는 내용)로 견준다 — 찍히는 중인 화면 글로 견주면 시각마다 답이 달라진다.
|
|
395
|
+
* 짚을 것이 하나뿐이면 빈 목록 — 하나뿐인 것을 짚는 칸은 「지금 볼 것」을 가리키지 못하고 의미 없는 네모로 남는다
|
|
396
|
+
*/
|
|
397
|
+
export function keyPoints(say: readonly { text: string; from: number }[], from: number, points: readonly { el: HTMLElement; text?: string }[]): Focus[] {
|
|
398
|
+
if (points.length < 2) return [];
|
|
399
|
+
const out: Focus[] = [{ at: from, el: points[0].el }];
|
|
400
|
+
for (const w of say) {
|
|
401
|
+
if (w.from < from) continue;
|
|
402
|
+
const hit = points.find((p) => p.text && said(w.text, p.text));
|
|
403
|
+
if (hit && hit.el !== out[out.length - 1].el) out.push({ at: w.from, el: hit.el });
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** 글 text 가 [from, to) 안에서 말해지는 시각 — 목소리 낱말이 있으면 그 글을 말하는 첫 낱말, 없으면 자막 줄 안에서 그 글이 놓인 자리만큼. 못 찾으면 null */
|
|
409
|
+
export function heardAt(kit: Kit, text: string, from: number, to: number): number | null {
|
|
410
|
+
if (kit.say.length) return kit.say.find((w) => w.from >= from - 0.05 && w.from < to && said(w.text, text))?.from ?? null;
|
|
411
|
+
const key = bare(text).split(" ").find((tk) => tk.length >= 2 || /^\d/.test(tk));
|
|
412
|
+
for (const l of key ? kit.lines : []) {
|
|
413
|
+
const i = bare(l.text).indexOf(key!);
|
|
414
|
+
const at = l.from + ((l.to - l.from) * i) / Math.max(1, bare(l.text).length);
|
|
415
|
+
if (i >= 0 && at >= from - 0.05 && at < to) return at;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 항목이 말해지는 시각 — 목소리 낱말(kit.say)이 그 글을 말하는 첫 시각, 낱말 시각 없이 자막 줄(kit.lines)만 있으면 줄 안에서 그 글이 놓인 자리만큼.
|
|
422
|
+
* 못 찾은 항목은 앞뒤로 찾은 항목 사이(처음은 from, 끝은 to)를 고르게 메우고, 하나도 없으면 [from, to] 를 고르게 — 셋 다 박에 붙인다(0.15초 안).
|
|
423
|
+
* 항목 차례대로 늦어지기만 한다. 감독 장면은 목소리가 길이를 정해 4–9초인데 내용은 2–3초 안에 다 서 — 항목을 말하는 순간에 나눠 세워 그 사이를 메운다
|
|
424
|
+
*/
|
|
425
|
+
export function sayTimes(kit: Kit, texts: readonly (string | undefined)[], from: number, to: number): number[] {
|
|
426
|
+
const found = texts.map((tx) => (tx ? heardAt(kit, tx, from, to) : null));
|
|
427
|
+
const out = found.map((x) => (x === null ? NaN : clamp(x, from, to)));
|
|
428
|
+
// 못 찾은 줄 — 앞 닻(없으면 from)과 뒤 닻(없으면 to) 사이를 고르게
|
|
429
|
+
for (let i = 0; i < out.length; ) {
|
|
430
|
+
if (!Number.isNaN(out[i])) {
|
|
431
|
+
i++;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
let j = i;
|
|
435
|
+
while (j < out.length && Number.isNaN(out[j])) j++;
|
|
436
|
+
const a = i > 0 ? out[i - 1] : from;
|
|
437
|
+
const b = j < out.length ? out[j] : to;
|
|
438
|
+
const lead = i === 0 ? 0 : 1;
|
|
439
|
+
const span = j - i + lead + (j < out.length ? 1 : 0) - 1;
|
|
440
|
+
for (let k = i; k < j; k++) out[k] = a + ((b - a) * (k - i + lead)) / Math.max(1, span);
|
|
441
|
+
i = j;
|
|
442
|
+
}
|
|
443
|
+
let last = -Infinity;
|
|
444
|
+
return out.map((x, i) => {
|
|
445
|
+
const near = kit.hits.reduce((best, h) => (Math.abs(h - x) < Math.abs(best - x) ? h : best), Infinity);
|
|
446
|
+
const snapped = found[i] === null && Math.abs(near - x) < 0.15 ? near : x;
|
|
447
|
+
last = Math.max(snapped, last);
|
|
448
|
+
return last;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 지금 말하는 항목 쪽으로 다가가는 카메라 — 장면 첫 프레임부터 첫 항목 쪽으로, 항목이 말해질 때마다 그쪽으로 천천히 다가가 다음 항목까지,
|
|
454
|
+
* 마지막 항목 뒤엔 장면 끝까지 한 번의 움직임으로 잇는다. 내용이 다 선 뒤 쉬는 구간을 뜻 있는 움직임으로 메운다(다가감의 이유가 그 순간 말하는 것).
|
|
455
|
+
* 다가갈수록 조금씩 가까이(배율 1.02 씩, 1.08 까지). 머무는 짚개(dwell) 테마만 — 훑는 테마는 짚개가 쉬지 않는 몫을 진다. 카메라를 끈 영상엔 안 건다
|
|
456
|
+
*/
|
|
457
|
+
export function followSpoken(kit: Kit, ctx: SceneCtx, shots: readonly Focus[]): void {
|
|
458
|
+
if (!cameraOk(ctx) || !kit.stage || !dwells(kit, ctx)) return;
|
|
459
|
+
const sorted = [...shots].filter((s) => s.at < ctx.dur - 0.2).sort((a, b) => a.at - b.at);
|
|
460
|
+
if (!sorted.length) return;
|
|
461
|
+
// 첫 프레임부터 주인공 쪽으로 — 앞 장면의 다가감과 끊김 없이 이어져 한 번으로 센다(틀이 카메라를 프레이즈당 한 번 허락한다). 0 에서 시작하면
|
|
462
|
+
// 첫 프레임엔 아직 안 움직여 사이가 뜬다. 아직 안 찍힌(빈) 항목 쪽으로 먼저 가면 화면이 비켜 내용이 안전 영역 밖으로 밀린다
|
|
463
|
+
const hero = kit.stage.cam.querySelector<HTMLElement>("[data-kf-hero]") ?? sorted[0].el;
|
|
464
|
+
const list = [{ at: -0.05, el: hero }, ...sorted.filter((s) => s.at > 0.05)];
|
|
465
|
+
// 다음 다가감과 조금 겹쳐 잇는다 — 사이에 한 프레임이라도 서면 검사가 카메라를 두 번 움직인 것으로 센다
|
|
466
|
+
list.forEach((s, i) => {
|
|
467
|
+
const end = list[i + 1] ? list[i + 1].at + 0.1 : ctx.dur + 0.1;
|
|
468
|
+
kit.stage.camera.pushIn(s.at, { to: s.el, scale: Math.min(1.08, 1 + 0.02 * (i + 1)), dur: Math.max(0.4, end - s.at) });
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 요점 사이 옮김 — 스타일가이드 자리 옮김 300ms. 다음 요점이 그보다 가까우면 닿을 틈을 남기고 더 빨리
|
|
473
|
+
const MOVE = 0.3;
|
|
474
|
+
const REST = 0.03;
|
|
475
|
+
|
|
476
|
+
/** 짚을 대상 — text 는 다 찍힌 뒤의 글(목소리 낱말과 견준다) */
|
|
477
|
+
export interface Point {
|
|
478
|
+
el: HTMLElement;
|
|
479
|
+
text?: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** 테마 짚음이 요점에 머무는 방식(mark 의 dwell)인가 — 칸 모양만 묻는다(붙이지 않는 빈 칸에 칠하게 하고 읽는다) */
|
|
483
|
+
export const dwells = (kit: Kit, ctx: SceneCtx): boolean => !!markOf(kit, ctx)?.(document.createElement("div")).dwell;
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 짚개 — 칸이 「지금 볼 것」을 짚는다. 짚기 방식은 테마 짚음(mark)이 고른다:
|
|
487
|
+
* - 머묾(dwell): 첫 대상(장면이 주인공·강조로 세운 것)에 from 에 내려앉아 머물고, 목소리가 다른 대상의 글을 말할 때만 0.3초에 옮긴다(keyPoints).
|
|
488
|
+
* hub 가 아닌 목록(차례로 늘어선 낱말·줄)은 주인공이 없어 짚지 않는다
|
|
489
|
+
* - 훑기(비운 테마): from 부터 박 정지점(beatStops — to 까지)마다 대상 사이를 내내 옮긴다. hub 면 주인공과 나머지를 번갈아 —
|
|
490
|
+
* 작은 글 사이를 옮겨 다니면 칸이 조금밖에 안 움직여 멈춘 화면으로 읽힌다. 마지막 정지점(장면 끝)이 주인공이 되게 차례를 맞춘다
|
|
491
|
+
* 짚을 것이 하나뿐이면 짚지 않는다(의미 없는 네모). until 부터는 걷는다. 칸 모양·옮겨 가는 길·칸 위 글색도 테마 짚음(비우면 강조색 꽉 찬 칸),
|
|
492
|
+
* 칸은 대상 글 뒤에 깔리고(host 에 isolation) 칸이 덮은 글은 그 짚음의 ink 로 바뀐다. 대상 자리는 그 프레임에 잰다 — 대상이 굴러가거나 내려앉아도 따라간다
|
|
493
|
+
*/
|
|
494
|
+
export function glider(host: HTMLElement, points: readonly Point[], o: { kit: Kit; ctx: SceneCtx; from: number; to?: number; until?: number; hub?: boolean; pad?: number }): (t: number) => void {
|
|
495
|
+
const hub = o.hub ?? true;
|
|
496
|
+
if (points.length < 2) return () => {};
|
|
291
497
|
host.style.isolation = "isolate";
|
|
292
|
-
const block = node(host,
|
|
498
|
+
const block = node(host, MARK_CSS);
|
|
499
|
+
const shape = markOf(o.kit, o.ctx)?.(block) ?? plainMark(block);
|
|
500
|
+
const focus = shape.dwell ? (hub ? keyPoints(o.kit.say, o.from, points) : []) : null;
|
|
501
|
+
if (focus && !focus.length) {
|
|
502
|
+
block.remove();
|
|
503
|
+
return () => {};
|
|
504
|
+
}
|
|
505
|
+
const targets = focus ? [...new Set(focus.map((f) => f.el))] : points.map((p) => p.el);
|
|
506
|
+
const stops = focus ? [] : beatStops(o.kit.hits, o.from, o.to ?? o.ctx.dur);
|
|
507
|
+
const until = o.until ?? Infinity;
|
|
508
|
+
const n = targets.length;
|
|
509
|
+
// 훑기 차례 — 마지막 정지점(장면 끝)이 주인공이 되게 맞춘다. 끝 프레임(표지·견본)에 곁가지 글이 아니라 주인공이 짚혀 있다
|
|
510
|
+
const base = hub ? (stops.length - 1) % 2 : 0;
|
|
511
|
+
const order = (k: number) => (hub ? ((k + base) % 2 === 0 ? 0 : 1 + (((k + base - 1) / 2) % (n - 1))) : k % n);
|
|
293
512
|
let toned = new Set<HTMLElement>();
|
|
294
513
|
const pad = o.pad ?? 0.12;
|
|
295
514
|
// 프레임마다 잰다 — 처음 그린 프레임에서 한 번만 재면 탭마다 첫 프레임이 달라 같은 시각이 다르게 그려졌다(frame.nondeterministic)
|
|
@@ -306,50 +525,52 @@ export function glider(host: HTMLElement, targets: HTMLElement[], o: { pad?: num
|
|
|
306
525
|
const fs = parseFloat(getComputedStyle(el).fontSize) || 20;
|
|
307
526
|
return { x: b.x - fs * pad, y: b.y - fs * pad * 0.5, w: b.w + fs * pad * 2, h: b.h + fs * pad };
|
|
308
527
|
};
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return;
|
|
528
|
+
// 이 프레임 칸 자리 — 아직 안 나타났거나 걷었으면 null
|
|
529
|
+
const where = (t: number): MarkBox | null => {
|
|
530
|
+
if (t >= until) return null;
|
|
531
|
+
if (focus) {
|
|
532
|
+
let k = -1;
|
|
533
|
+
while (k + 1 < focus.length && focus[k + 1].at <= t) k++;
|
|
534
|
+
if (k < 0) return null;
|
|
535
|
+
keyCue(t, focus[k].at);
|
|
536
|
+
const next = focus[k + 1];
|
|
537
|
+
const move = Math.max(0.1, Math.min(MOVE, (next ? next.at - focus[k].at : Infinity) - REST));
|
|
538
|
+
return shape.at(k ? rect(focus[k - 1].el) : null, rect(focus[k].el), clamp((t - focus[k].at) / move));
|
|
316
539
|
}
|
|
540
|
+
if (t < stops[0]) return null;
|
|
541
|
+
stopCues(t, stops, (k) => order(k) === 0);
|
|
317
542
|
let j = 0;
|
|
318
543
|
while (j + 1 < stops.length && stops[j + 1] <= t) j++;
|
|
319
544
|
const a = stops[j];
|
|
320
545
|
const b = stops[j + 1] ?? a + 1;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
546
|
+
return shape.at(rect(targets[order(j)]), rect(targets[order(j + 1)]), clamp((t - a) / Math.max(1e-3, b - a)));
|
|
547
|
+
};
|
|
548
|
+
return (t) => {
|
|
549
|
+
const box = where(t);
|
|
550
|
+
if (!box) {
|
|
551
|
+
block.style.visibility = "hidden";
|
|
552
|
+
for (const el of toned) paintTone(el, null);
|
|
553
|
+
toned = new Set();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
330
556
|
block.style.visibility = "visible";
|
|
331
|
-
block
|
|
332
|
-
block.style.width = `${mix(from.w, to.w).toFixed(1)}px`;
|
|
333
|
-
block.style.height = `${mix(from.h, to.h).toFixed(1)}px`;
|
|
334
|
-
// 띠와 겹친 글자 부분만 흰 글로 — 글자에 「겹친 사각형만 on-accent, 나머지는 원래 글색」인 배경을 깔고 글자 모양으로 오린다.
|
|
335
|
-
// 통째로 덮였을 때만 글색을 바꾸면, 쉬지 않고 옮겨 가는 띠는 거의 늘 반쯤 걸쳐 있어 띠 위 글이 검은 채로 남았다
|
|
336
|
-
const bx = mix(from.x, to.x);
|
|
337
|
-
const by = mix(from.y, to.y);
|
|
338
|
-
const bw = mix(from.w, to.w);
|
|
339
|
-
const bh = mix(from.h, to.h);
|
|
557
|
+
place(block, box);
|
|
340
558
|
const now = new Set<HTMLElement>();
|
|
559
|
+
// 띠와 겹친 글자 부분만 띠 위 글색으로 — 글자에 「겹친 사각형만 ink, 나머지는 원래 글색」인 배경을 깔고 글자 모양으로 오린다.
|
|
560
|
+
// 통째로 덮였을 때만 글색을 바꾸면, 옮겨 가는 동안 반쯤 걸친 띠 위 글이 원래 색으로 남았다.
|
|
341
561
|
// 불투명한 판을 품은 대상(테마 panel 이 깔린 숫자판·계기 판)은 띠가 그 판 뒤로 가려 안 보인다 — 거기 글을 흰 글로 칠하면 흰 판 위 흰 글이 된다
|
|
342
|
-
|
|
562
|
+
const ink = shape.ink;
|
|
563
|
+
for (const el of ink ? targets.filter((x) => !opaqueOf(x)).flatMap(leavesOf) : []) {
|
|
343
564
|
const r = boxOf(el);
|
|
344
|
-
const x0 = Math.max(
|
|
345
|
-
const y0 = Math.max(
|
|
346
|
-
const x1 = Math.min(
|
|
347
|
-
const y1 = Math.min(
|
|
565
|
+
const x0 = Math.max(box.x, r.x);
|
|
566
|
+
const y0 = Math.max(box.y, r.y);
|
|
567
|
+
const x1 = Math.min(box.x + box.w, r.x + r.w);
|
|
568
|
+
const y1 = Math.min(box.y + box.h, r.y + r.h);
|
|
348
569
|
if (x1 - x0 < 1 || y1 - y0 < 1) continue;
|
|
349
570
|
// 글 상자에 변환(배율)이 걸려 있으면 배경 좌표는 변환 전 크기로
|
|
350
571
|
const sx = (el.offsetWidth || r.w) / (r.w || 1);
|
|
351
572
|
const sy = (el.offsetHeight || r.h) / (r.h || 1);
|
|
352
|
-
paintTone(el, { x: (x0 - r.x) * sx, y: (y0 - r.y) * sy, w: (x1 - x0) * sx, h: (y1 - y0) * sy });
|
|
573
|
+
paintTone(el, { x: (x0 - r.x) * sx, y: (y0 - r.y) * sy, w: (x1 - x0) * sx, h: (y1 - y0) * sy }, ink);
|
|
353
574
|
now.add(el);
|
|
354
575
|
}
|
|
355
576
|
for (const el of toned) if (!now.has(el)) paintTone(el, null);
|
|
@@ -400,6 +621,7 @@ export function factRoll(t: number, o: { from: number; to: number; at: number; d
|
|
|
400
621
|
const dec = o.decimals ?? decimalsOf(o.to);
|
|
401
622
|
const x = progress(t, o.at, o.dur);
|
|
402
623
|
const p = 0.75 * ease.outCubic(x) + 0.25 * x;
|
|
624
|
+
rollCues(t, { at: o.at, dur: o.dur, p: (u) => 0.75 * ease.outCubic(u) + 0.25 * u });
|
|
403
625
|
const lo = Math.min(o.from, o.to);
|
|
404
626
|
const hi = Math.max(o.from, o.to);
|
|
405
627
|
const value = p >= 1 ? o.to : clamp(o.from + (o.to - o.from) * p, lo, hi);
|
|
@@ -446,5 +668,6 @@ const DEFAULT_SLOTS: Required<Pick<ThemeSlots, "panel" | "land" | "write" | "tra
|
|
|
446
668
|
|
|
447
669
|
/** 테마 슬롯 + 비운 자리는 기본 */
|
|
448
670
|
export function slotsOf(kit: Kit): ThemeSlots & typeof DEFAULT_SLOTS {
|
|
449
|
-
|
|
671
|
+
// 소리 자리는 감싸개가 단다 — 테마 슬롯이든 기본이든 같은 자리에서
|
|
672
|
+
return withSfx({ ...DEFAULT_SLOTS, ...kit.slots }) as ThemeSlots & typeof DEFAULT_SLOTS;
|
|
450
673
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyStyle, clamp, spring } from "@kasaframe/core";
|
|
2
2
|
import { fitSize, textEm } from "@kasaframe/motion/moves";
|
|
3
|
-
import { area,
|
|
3
|
+
import { area, decimalsOf, dwells, factRoll, fmt, followSpoken, glider, moving, node, sayTimes, slotsOf } from "../kit.ts";
|
|
4
4
|
import type { Pattern } from "../types.ts";
|
|
5
5
|
|
|
6
6
|
export interface LimitContent {
|
|
@@ -12,12 +12,12 @@ export interface LimitContent {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* 한계 — 굵은 계기가 차올라 끝의 위험 칸(한계)에 부딪힌다. 값이 있으면 계기와 같은 진행도로 굴러 그 값에 닿는다.
|
|
15
|
-
* 부딪힌 뒤 치르는 것 한 낱말이 딱지로 붙고, 짚개가 한계
|
|
15
|
+
* 부딪힌 뒤 치르는 것 한 낱말이 딱지로 붙고, 짚개가 한계 문장부터 짚는다(머무는 테마는 목소리가 계기 값·치르는 것을 말할 때만 그리로).
|
|
16
16
|
*/
|
|
17
17
|
const pattern: Pattern<LimitContent> = {
|
|
18
18
|
id: "limit.gauge",
|
|
19
19
|
type: "limit",
|
|
20
|
-
does: "계기가 차올라 끝의 위험 칸에 부딪히고, 치르는 것이 붙은 뒤 짚개가
|
|
20
|
+
does: "계기가 차올라 끝의 위험 칸에 부딪히고, 치르는 것이 붙은 뒤 짚개가 한계부터 짚는다",
|
|
21
21
|
energy: ["low", "mid"],
|
|
22
22
|
family: "grow",
|
|
23
23
|
camera: ["none"],
|
|
@@ -78,24 +78,29 @@ const pattern: Pattern<LimitContent> = {
|
|
|
78
78
|
const hits = kit.hits.length ? kit.hits : [0];
|
|
79
79
|
const labelDur = clamp([...c.label].length * 0.03, 0.3, 0.7);
|
|
80
80
|
const fillAt = labelDur * 0.5;
|
|
81
|
-
//
|
|
81
|
+
// 머무는 짚개 테마는 목소리가 한계를 말하는 순간 벽에 닿는다 — 그때까지 고른 빠르기로 차오르고, 치르는 것은 그것을 말할 때 붙고,
|
|
82
|
+
// 닿은 뒤엔 카메라가 한계·치르는 것 쪽으로 다가간다. 훑는 테마·목소리 없는 영상은 1–2.2초 안 박에 닿는다
|
|
83
|
+
const tail = Math.max(fillAt + 1, ctx.dur - 0.6);
|
|
84
|
+
const heard = dwells(kit, ctx) && (kit.say.length > 0 || kit.lines.length > 0);
|
|
85
|
+
const [heardLabel, heardCost] = heard ? sayTimes(kit, [c.label, c.cost], fillAt + 0.9, tail) : [0, 0];
|
|
82
86
|
const hitWant = clamp(ctx.dur * 0.45, fillAt + 0.9, fillAt + 2.2);
|
|
83
|
-
const hitAt = hits.reduce((best, b) => (Math.abs(b - hitWant) < Math.abs(best - hitWant) && b > fillAt + 0.6 ? b : best), hitWant);
|
|
84
|
-
const costAt = hitAt + 0.1;
|
|
87
|
+
const hitAt = heard ? heardLabel : hits.reduce((best, b) => (Math.abs(b - hitWant) < Math.abs(best - hitWant) && b > fillAt + 0.6 ? b : best), hitWant);
|
|
88
|
+
const costAt = Math.max(hitAt + 0.1, heardCost);
|
|
85
89
|
const markFrom = hitAt + 0.2;
|
|
90
|
+
followSpoken(kit, ctx, [{ at: hitAt, el: labelBox }, ...(cost ? [{ at: costAt, el: cost }] : [])]);
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
// 한계 문장은 목소리가 말하는 순간 찍힌다(그 밖엔 처음부터)
|
|
93
|
+
const writeLabel = slots.write(label, { ctx, text: c.label, at: heard ? Math.max(0, hitAt - labelDur) : 0, dur: labelDur, seed: `${kit.seed}:l` });
|
|
88
94
|
const writeCost = cost ? slots.write(cost, { ctx, text: c.cost!, at: costAt, dur: 0.25, seed: `${kit.seed}:c` }) : null;
|
|
89
95
|
const landed = slots.land(wallEl, { ctx, at: hitAt, seed: `${kit.seed}:wall` });
|
|
90
|
-
// 벽에
|
|
96
|
+
// 벽에 닿으면 짚개가 한계 문장부터 짚는다 — 머무는 테마는 목소리가 치르는 것을 말할 때만 그리로
|
|
91
97
|
// 세로면 계기 판은 짚지 않는다 — 띠가 판 뒤로 가려 그 사이가 멈춘 화면이 된다. 한계 문장 ↔ 대가를 오간다
|
|
92
|
-
const point = glider(host, vertical && cost ? [labelBox, cost] : [labelBox, gauge, ...(cost ? [cost] : [])], {
|
|
93
|
-
const stops = beatStops(kit.hits, markFrom, ctx.dur);
|
|
98
|
+
const point = glider(host, vertical && cost ? [{ el: labelBox, text: c.label }, { el: cost, text: c.cost }] : [{ el: labelBox, text: c.label }, { el: gauge }, ...(cost ? [{ el: cost, text: c.cost }] : [])], { kit, ctx, from: markFrom });
|
|
94
99
|
|
|
95
100
|
return (t: number) => {
|
|
96
101
|
moving("limit.gauge", ctx);
|
|
97
102
|
writeLabel(t);
|
|
98
|
-
point(t
|
|
103
|
+
point(t);
|
|
99
104
|
// 고른 빠르기로 차올라 벽에 부딪힌다 — 감속하면 부딪힘이 없고 끝자락이 멈춘 화면이 된다
|
|
100
105
|
const p = clamp((t - fillAt) / (hitAt - fillAt));
|
|
101
106
|
fill.style.transform = vertical ? `scaleY(${(p * wall).toFixed(4)})` : `scaleX(${(p * wall).toFixed(4)})`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyStyle, clamp, spring } from "@kasaframe/core";
|
|
2
2
|
import { fitSize, textEm } from "@kasaframe/motion/moves";
|
|
3
|
-
import { ACCENT_TEXT, area,
|
|
3
|
+
import { ACCENT_TEXT, area, glider, inverseOver, markOf, moving, node, slotsOf } from "../kit.ts";
|
|
4
4
|
import type { Pattern, SpokenWord } from "../types.ts";
|
|
5
5
|
|
|
6
6
|
export interface QuoteContent {
|
|
@@ -80,14 +80,23 @@ const pattern: Pattern<QuoteContent> = {
|
|
|
80
80
|
const mark = node(col, `font-family:var(--kf-font-display);font-size:${Math.round(px * 1.4)}px;line-height:.6;height:${Math.round(px * 0.6)}px;font-weight:900;color:${ACCENT_TEXT}`, "“");
|
|
81
81
|
const body = node(col, `font-family:var(--kf-font-display);font-size:${px}px;font-weight:${ctx.style.type?.weightDisplay ?? 800};line-height:1.3;color:var(--kf-ink);word-break:keep-all`);
|
|
82
82
|
kit.stage?.hero(body);
|
|
83
|
+
const shape = markOf(kit, ctx);
|
|
83
84
|
const spans = words.map((wd, i) => {
|
|
84
85
|
const sp = body.ownerDocument.createElement("span");
|
|
85
86
|
sp.style.cssText = "position:relative;white-space:nowrap";
|
|
86
87
|
sp.textContent = wd;
|
|
87
|
-
const hi = node(sp, "position:absolute;left:-0.08em;right:-0.08em;top:0.02em;bottom:0.02em;padding:0 0.08em;background:var(--kf-accent);color:var(--kf-on-accent);white-space:nowrap;visibility:hidden", wd);
|
|
88
88
|
body.append(sp);
|
|
89
89
|
if (i < words.length - 1) body.append(body.ownerDocument.createTextNode(" "));
|
|
90
|
-
|
|
90
|
+
// 읽는 낱말 반전 칸 — 테마가 짚는 칸 모양을 정했으면 그 칸(반전 짚음)
|
|
91
|
+
if (shape) return { sp, paint: inverseOver(sp, wd, shape) };
|
|
92
|
+
const hi = node(sp, "position:absolute;left:-0.08em;right:-0.08em;top:0.02em;bottom:0.02em;padding:0 0.08em;background:var(--kf-accent);color:var(--kf-on-accent);white-space:nowrap;visibility:hidden", wd);
|
|
93
|
+
return {
|
|
94
|
+
sp,
|
|
95
|
+
paint: (k: number) => {
|
|
96
|
+
hi.style.visibility = k > 0 ? "visible" : "hidden";
|
|
97
|
+
hi.style.clipPath = `inset(0 ${((1 - k) * 100).toFixed(2)}% 0 0)`;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
91
100
|
});
|
|
92
101
|
const whoText = [c.who, c.role].filter(Boolean).join(" · ");
|
|
93
102
|
const who = whoText ? node(col, `align-self:flex-end;font-family:var(--kf-font-sans);font-size:${whoPx}px;font-weight:600;color:var(--kf-ink-soft, var(--kf-ink));white-space:nowrap;line-height:1.3;height:1.3em`) : null;
|
|
@@ -101,24 +110,20 @@ const pattern: Pattern<QuoteContent> = {
|
|
|
101
110
|
const times = kit.say.length ? timing(words, kit.say, readFrom, ctx.dur - 0.05) : even(words, readFrom, readTo);
|
|
102
111
|
const readEnd = times[times.length - 1][1];
|
|
103
112
|
const whoAt = Math.min(Math.max(readFrom, ctx.dur - whoDur - 0.6), readEnd);
|
|
104
|
-
// 다
|
|
105
|
-
const point = glider(host, who ? [body, who] : spans.map((x) => x.sp), { hub: !!who, pad: 0.06 });
|
|
106
|
-
const stops = beatStops(kit.hits, readEnd + 0.05, ctx.dur);
|
|
113
|
+
// 다 읽으면 짚개가 문장부터 짚는다(머무는 테마는 목소리가 누가를 말할 때만 그리로). 누가가 없으면 훑는 테마만 낱말을 차례로 — 머무는 테마엔 주인공이 없다
|
|
114
|
+
const point = glider(host, who ? [{ el: body, text: c.text }, { el: who, text: whoText }] : spans.map((x) => ({ el: x.sp })), { kit, ctx, from: readEnd + 0.05, hub: !!who, pad: 0.06 });
|
|
107
115
|
const writeWho = who ? slots.write(who, { ctx, text: whoText, at: whoAt, dur: whoDur, seed: `${kit.seed}:who` }) : null;
|
|
108
116
|
|
|
109
117
|
return (t: number) => {
|
|
110
118
|
moving("quote.karaoke", ctx);
|
|
111
119
|
applyStyle(mark, { scale: spring(t, { from: 0.4, to: 1, stiffness: 260, damping: 14 }) });
|
|
120
|
+
// 짚는 동안은 낱말 반전 칸을 걷는다 — 두 강조가 겹치면 무엇을 짚는지 흐려진다
|
|
112
121
|
spans.forEach((s, i) => {
|
|
113
122
|
const [a, b] = times[i];
|
|
114
|
-
|
|
115
|
-
s.hi.style.visibility = k > 0 ? "visible" : "hidden";
|
|
116
|
-
s.hi.style.clipPath = `inset(0 ${((1 - k) * 100).toFixed(2)}% 0 0)`;
|
|
123
|
+
s.paint(t >= readEnd + 0.05 ? 0 : clamp((t - a) / Math.max(0.05, b - a)));
|
|
117
124
|
});
|
|
118
125
|
writeWho?.(t);
|
|
119
|
-
|
|
120
|
-
if (t >= readEnd + 0.05) spans.forEach((x) => (x.hi.style.visibility = "hidden"));
|
|
121
|
-
point(t, stops);
|
|
126
|
+
point(t);
|
|
122
127
|
};
|
|
123
128
|
},
|
|
124
129
|
};
|