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
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import { applyStyle, clamp, emit, ease, progress, safeTime, type Style } from "@kasaframe/core";
|
|
1
|
+
import { applyStyle, bezier, clamp, cue, emit, ease, progress, safeTime, type SfxRole, type Style } from "@kasaframe/core";
|
|
2
2
|
import { typeText } from "@kasaframe/motion";
|
|
3
3
|
|
|
4
4
|
// 비유: 계측기 화면. 실제 호출이 한 줄 나가고, 원자료 로그가 흐르고, 거기서 숫자가 뽑혀 점·막대·큰 숫자로 쌓인다.
|
|
5
5
|
// 화면의 숫자는 늘 재 온 값(measure.json 같은 원자료)에서 온다 — 효과는 숫자를 지어내지 않고, 들어온 시각에 보여 줄 뿐이다.
|
|
6
|
-
//
|
|
6
|
+
// 겉모습은 OpenGateway 스타일가이드(design.md·globals.css)를 따른다: 상자에 가두지 않고, 칩은 속을 칠하지 않으며,
|
|
7
|
+
// 모서리는 역할(알약·면 12px·각짐)이고, 움직임은 150/220/300ms 감속·스프링과 6px 나타남뿐이다(커졌다 줄기·흔들기 없음).
|
|
7
8
|
|
|
8
9
|
const MONO = "var(--kf-font-mono)";
|
|
9
|
-
const BAR = "
|
|
10
|
-
|
|
10
|
+
const BAR = "var(--kf-bar)";
|
|
11
|
+
// globals.css --motion-fast/base/slow·--motion-rise·--ease-out·--ease-spring
|
|
12
|
+
const FAST = 0.15;
|
|
13
|
+
const BASE = 0.22;
|
|
14
|
+
const SLOW = 0.3;
|
|
15
|
+
const RISE = 6;
|
|
16
|
+
const out = ease.snappy;
|
|
17
|
+
const springy = bezier(0.34, 1.3, 0.64, 1);
|
|
11
18
|
|
|
12
|
-
//
|
|
13
|
-
function rise(t: number, at: number, dur =
|
|
14
|
-
|
|
19
|
+
// 서는 순간 소리 하나 — 같은 시각에 함께 서는 칸들은 한 번만(자리가 같으면 한 줄). 칸마다 나는 나타남·째깍·타자는 잔소리, 끝 도장만 기본으로
|
|
20
|
+
function rise(t: number, at: number, dur = BASE, px = RISE, role: SfxRole = "appear"): Style {
|
|
21
|
+
cue(role, t - at, { source: `rise:${role}@${at.toFixed(3)}`, minor: role !== "end" });
|
|
22
|
+
const p = out(progress(t, at, dur));
|
|
15
23
|
return { opacity: p, y: px * (1 - p) };
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
// 마스터 로고 원문은 루트 <svg> 에 width·height(1140×650)가 박혀 있다 — 루트에서만 걷고 상자를 채우게(안쪽 rect 의 width 는 그대로)
|
|
27
|
+
const fitSvg = (svg: string) => svg.replace(/<svg\b[^>]*>/, (tag) => tag.replace(/\s(width|height)="[^"]*"/g, "").replace("<svg", '<svg width="100%" height="100%"'));
|
|
28
|
+
|
|
18
29
|
function box(doc: Document, css: string, text = ""): HTMLElement {
|
|
19
30
|
const d = doc.createElement("div");
|
|
20
31
|
d.style.cssText = css;
|
|
@@ -22,27 +33,33 @@ function box(doc: Document, css: string, text = ""): HTMLElement {
|
|
|
22
33
|
return d;
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
// 배지 — 흐린 테두리와 글자색만(badge.tsx). 화면 글이 UI 의 서너 배라 선도 그만큼 굵게
|
|
37
|
+
type Tone = "accent" | "plain" | "soft" | "success";
|
|
38
|
+
const edge = (size: number) => Math.max(2, Math.round(size * 0.05));
|
|
39
|
+
function badgeCss(tone: Tone, size: number): string {
|
|
40
|
+
const line = tone === "accent" ? "color-mix(in srgb, var(--kf-accent) 45%, transparent)" : tone === "success" ? "color-mix(in srgb, var(--kf-success) 45%, transparent)" : "var(--kf-line)";
|
|
41
|
+
const ink = tone === "accent" ? "var(--kf-accent-text)" : tone === "success" ? "var(--kf-success)" : tone === "soft" ? "var(--kf-dim)" : "var(--kf-ink)";
|
|
42
|
+
return `border:${edge(size)}px solid ${line};border-radius:var(--kf-radius-pill);background:transparent;color:${ink}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
/**
|
|
26
|
-
* 측정 바탕 —
|
|
27
|
-
*
|
|
46
|
+
* 측정 바탕 — 바탕색 위에 구분선 색의 옅은 모눈이 천천히 흐른다. 그라데이션·빛 번짐 없이 모눈 하나뿐이고 구도는 안 바꾼다.
|
|
47
|
+
* 박 사이를 메우는 몫은 바탕이 아니라 내용(카운터·로그·막대·읽는 칸)이 진다
|
|
28
48
|
*/
|
|
29
|
-
export function graphPaper(el: HTMLElement, o: { cell?: number;
|
|
49
|
+
export function graphPaper(el: HTMLElement, o: { cell?: number; phase?: number } = {}): (t: number) => void {
|
|
30
50
|
const doc = el.ownerDocument;
|
|
31
51
|
const cell = o.cell ?? 56;
|
|
52
|
+
const grid = "color-mix(in srgb, var(--kf-line) 45%, transparent)";
|
|
32
53
|
const bg = box(
|
|
33
54
|
doc,
|
|
34
55
|
`position:absolute;inset:0;background:var(--kf-bg);overflow:hidden;` +
|
|
35
|
-
`background-image:linear-gradient(
|
|
56
|
+
`background-image:linear-gradient(${grid} 1px,transparent 1px),linear-gradient(90deg,${grid} 1px,transparent 1px);` +
|
|
36
57
|
`background-size:${cell}px ${cell}px`,
|
|
37
58
|
);
|
|
38
|
-
const scans = [0, 0.5].map((k) => ({ k, el: box(doc, "position:absolute;top:0;bottom:0;width:120px;z-index:5;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(0,111,214,.07),transparent)") }));
|
|
39
|
-
for (const s of scans) bg.append(s.el);
|
|
40
59
|
el.append(bg);
|
|
41
|
-
const period = o.period ?? 4;
|
|
42
60
|
return (t: number) => {
|
|
43
61
|
const u = safeTime(t) + (o.phase ?? 0);
|
|
44
62
|
bg.style.backgroundPosition = `${(u * 18) % cell}px ${(u * 9) % cell}px`;
|
|
45
|
-
for (const s of scans) s.el.style.transform = `translateX(${((((u / period + s.k) % 1) + 1) % 1) * ((el.clientWidth || 1920) + 240) - 120}px)`;
|
|
46
63
|
};
|
|
47
64
|
}
|
|
48
65
|
|
|
@@ -52,47 +69,41 @@ export interface CamShot {
|
|
|
52
69
|
x?: number;
|
|
53
70
|
y?: number;
|
|
54
71
|
zoom?: number;
|
|
55
|
-
/** 샷이
|
|
72
|
+
/** 샷이 서는 순간 이만큼 가까이에서 시작해 300ms 감속으로 물러나 앉는다 */
|
|
56
73
|
kick?: number;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
/**
|
|
60
|
-
* 계측 카메라 — 샷이 바뀌면
|
|
61
|
-
* 쉬지 않는 몫은 카운터·로그 한 줄·막대가 진다(늘 흐르는 카메라는 kf check camera.wander).
|
|
77
|
+
* 계측 카메라 — 샷이 바뀌면 300ms(자리 옮김) 스프링으로 옮기고 샷 사이에는 선다. 스프링이 살짝 넘었다 돌아오는 것이 「툭」이다.
|
|
78
|
+
* 샷은 드러냄·결과처럼 이유가 있을 때만 — 쉬지 않는 몫은 카운터·로그 한 줄·막대가 진다(늘 흐르는 카메라는 kf check camera.wander).
|
|
62
79
|
* 긴 로그 줄이 반쯤 잘리지 않게 크게 들어가지 않는다(배율 1–1.1). 무대 상자(transform-origin 0 0)에 붙인다.
|
|
63
80
|
*/
|
|
64
|
-
export function trackCam(t: number, o: { shots: CamShot[];
|
|
81
|
+
export function trackCam(t: number, o: { shots: CamShot[]; w?: number; h?: number; scene?: string }): Style {
|
|
65
82
|
t = safeTime(t);
|
|
66
83
|
const W = o.w ?? 1920;
|
|
67
84
|
const H = o.h ?? 1080;
|
|
68
|
-
const drift = o.drift ?? 0;
|
|
69
|
-
const pan = o.pan ?? 0;
|
|
70
85
|
const shots = o.shots.length ? o.shots : [{ at: 0 }];
|
|
71
|
-
const frame = (i: number,
|
|
72
|
-
const s = shots[i];
|
|
73
|
-
const e = Math.max(0, u - s.at);
|
|
74
|
-
const dir = i % 2 ? -1 : 1;
|
|
75
|
-
return { x: (s.x ?? W / 2) + dir * pan * e, y: (s.y ?? H / 2) + pan * 0.4 * e, z: (s.zoom ?? 1) * (1 + drift * e) };
|
|
76
|
-
};
|
|
86
|
+
const frame = (i: number) => ({ x: shots[i].x ?? W / 2, y: shots[i].y ?? H / 2, z: shots[i].zoom ?? 1 });
|
|
77
87
|
let i = 0;
|
|
78
88
|
for (let k = 0; k < shots.length; k++) if (shots[k].at <= t) i = k;
|
|
79
|
-
let f = frame(i
|
|
89
|
+
let f = frame(i);
|
|
80
90
|
const since = t - shots[i].at;
|
|
81
|
-
if (i > 0
|
|
82
|
-
|
|
83
|
-
const
|
|
91
|
+
if (i > 0) cue("whoosh", since, { source: `trackCam@${shots[i].at.toFixed(3)}`, scene: o.scene });
|
|
92
|
+
if (i > 0 && since < SLOW) {
|
|
93
|
+
const p = springy(since / SLOW);
|
|
94
|
+
const a = frame(i - 1);
|
|
84
95
|
f = { x: a.x + (f.x - a.x) * p, y: a.y + (f.y - a.y) * p, z: a.z + (f.z - a.z) * p };
|
|
85
|
-
emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round((1 -
|
|
96
|
+
emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round((1 - clamp(since / SLOW)) * 1000) });
|
|
86
97
|
}
|
|
87
|
-
const kick = since >= 0 ? (shots[i].kick ?? 0) *
|
|
88
|
-
if (
|
|
98
|
+
const kick = since >= 0 && since < SLOW ? (shots[i].kick ?? 0) * (1 - out(since / SLOW)) : 0;
|
|
99
|
+
if (kick > 0.0005) emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round(kick * 1000) });
|
|
89
100
|
const z = f.z * (1 + kick);
|
|
90
101
|
return { x: W / 2 - z * f.x, y: H / 2 - z * f.y, scale: z };
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
/**
|
|
94
|
-
* 실제 호출 한 줄 — 주소가 박마다 한 조각씩 붙고, 모델·보낼 글이 찍히고, sendAt 부터 경과 시간이
|
|
95
|
-
* 잰 값(ms)에 멈추며 상태·첫 토큰·받은 글이 붙는다. 카운터는 지어낸 값이 아니라 원자료의 ms 까지만 센다.
|
|
105
|
+
* 실제 호출 한 줄 — 응답 패널(틀이 곧 뜻인 면, 12px). 주소가 박마다 한 조각씩 붙고, 모델·보낼 글이 찍히고, sendAt 부터 경과 시간이
|
|
106
|
+
* 실제 속도로 굴러 잰 값(ms)에 멈추며 상태·첫 토큰·받은 글이 붙는다. 카운터는 지어낸 값이 아니라 원자료의 ms 까지만 센다.
|
|
96
107
|
*/
|
|
97
108
|
export function callLine(
|
|
98
109
|
el: HTMLElement,
|
|
@@ -117,13 +128,13 @@ export function callLine(
|
|
|
117
128
|
method?: string;
|
|
118
129
|
size?: number;
|
|
119
130
|
},
|
|
120
|
-
): (t: number) => void {
|
|
131
|
+
): ((t: number) => void) & { parts: HTMLElement[]; timer: HTMLElement } {
|
|
121
132
|
const doc = el.ownerDocument;
|
|
122
133
|
const size = o.size ?? 56;
|
|
123
|
-
const card = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${size * 0.7}px ${size * 0.85}px;border-radius:
|
|
134
|
+
const card = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${size * 0.7}px ${size * 0.85}px;border-radius:var(--kf-radius-md);background:var(--kf-surface);font-family:${MONO};font-size:${size}px;line-height:1.45;color:var(--kf-ink);font-variant-numeric:tabular-nums`);
|
|
124
135
|
const row = (css = "") => box(doc, `display:flex;align-items:center;gap:${size * 0.35}px;white-space:nowrap;${css}`);
|
|
125
136
|
const r1 = row();
|
|
126
|
-
const chip = box(doc, `padding:0 ${size * 0.3}px;
|
|
137
|
+
const chip = box(doc, `padding:0 ${size * 0.3}px;font-size:${size * 0.8}px;font-weight:600;line-height:1.3;${badgeCss("accent", size * 0.8)}`, o.method ?? "POST");
|
|
127
138
|
const urlBox = box(doc, "display:flex");
|
|
128
139
|
const parts = o.url.map((u) => {
|
|
129
140
|
const s = doc.createElement("span");
|
|
@@ -136,9 +147,9 @@ export function callLine(
|
|
|
136
147
|
const r2 = row("color:var(--kf-dim)");
|
|
137
148
|
const r3 = row("color:var(--kf-dim)");
|
|
138
149
|
const r4 = row(`margin-top:${size * 0.4}px`);
|
|
139
|
-
const timer = box(doc, `font-family:var(--kf-font-display);font-weight:800;font-size:${size * 1.9}px;letter-spacing:-0.
|
|
140
|
-
const ok = box(doc, `padding:0 ${size * 0.3}px;
|
|
141
|
-
const what = box(doc, "color:var(--kf-accent-text);font-weight:
|
|
150
|
+
const timer = box(doc, `font-family:var(--kf-font-display);font-weight:800;font-size:${size * 1.9}px;letter-spacing:-0.055em;line-height:1`);
|
|
151
|
+
const ok = box(doc, `padding:0 ${size * 0.3}px;font-size:${size * 0.8}px;font-weight:600;line-height:1.3;${badgeCss("success", size * 0.8)}`, String(o.status ?? ""));
|
|
152
|
+
const what = box(doc, "color:var(--kf-accent-text);font-weight:600", "첫 토큰");
|
|
142
153
|
const ans = box(doc, "color:var(--kf-ink)");
|
|
143
154
|
r4.append(timer, ok, what, ans);
|
|
144
155
|
const r5 = row(`color:var(--kf-dim);font-size:${size * 0.7}px`);
|
|
@@ -146,12 +157,12 @@ export function callLine(
|
|
|
146
157
|
el.append(card);
|
|
147
158
|
const ms = o.ms ?? 0;
|
|
148
159
|
const doneAt = o.sendAt === undefined ? Infinity : o.sendAt + ms / 1000;
|
|
149
|
-
|
|
150
|
-
applyStyle(card, rise(t, o.at
|
|
160
|
+
const paint = (t: number) => {
|
|
161
|
+
applyStyle(card, rise(t, o.at));
|
|
151
162
|
parts.forEach((s, i) => {
|
|
152
163
|
const at = o.urlAt[i] ?? o.at;
|
|
153
164
|
s.style.visibility = t >= at ? "visible" : "hidden";
|
|
154
|
-
applyStyle(s, t >= at ?
|
|
165
|
+
applyStyle(s, t >= at ? rise(t, at) : {});
|
|
155
166
|
});
|
|
156
167
|
r2.textContent = o.model ? typeText(`model: ${o.model}`, t, { at: o.modelAt ?? o.at, cps: 90, jitter: 0, ime: false }).text : "";
|
|
157
168
|
r2.style.display = o.model ? "" : "none";
|
|
@@ -161,21 +172,24 @@ export function callLine(
|
|
|
161
172
|
r4.style.display = o.sendAt === undefined ? "none" : "";
|
|
162
173
|
r4.style.visibility = o.sendAt !== undefined && t >= o.sendAt ? "visible" : "hidden";
|
|
163
174
|
timer.textContent = o.sendAt === undefined ? "" : `${Math.min(ms, Math.max(0, Math.floor((t - o.sendAt) * 1000)))}ms`;
|
|
164
|
-
|
|
175
|
+
// 색만 150ms 로 바뀐다 — 크기는 그대로(누름·도착에 커졌다 줄기 없음)
|
|
176
|
+
const c = done ? Math.round(out(progress(t, doneAt, FAST)) * 100) : 0;
|
|
177
|
+
timer.style.color = `color-mix(in srgb, var(--kf-accent-text) ${c}%, var(--kf-ink))`;
|
|
165
178
|
for (const e of [ok, what, ans]) {
|
|
166
179
|
e.style.display = done ? "" : "none";
|
|
167
180
|
applyStyle(e, rise(t, doneAt));
|
|
168
181
|
}
|
|
169
|
-
applyStyle(timer, done ? { scale: 1 + 0.12 * Math.exp(-(t - doneAt) / 0.1) } : {});
|
|
170
182
|
ans.textContent = done ? typeText(o.answer ?? "", t, { at: doneAt + 0.08, cps: 22, jitter: 0, ime: false }).text : "";
|
|
171
183
|
r5.textContent = o.usage && o.usageAt !== undefined && t >= o.usageAt ? o.usage : "";
|
|
172
184
|
r5.style.display = o.usage ? "" : "none";
|
|
173
185
|
applyStyle(r5, o.usageAt !== undefined ? rise(t, o.usageAt) : {});
|
|
174
186
|
};
|
|
187
|
+
return Object.assign(paint, { parts, timer });
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
/**
|
|
178
|
-
* 원자료 로그 — 줄이 들어온 시각에 맨 아래 붙고 한 줄씩 위로 밀려 오른다.
|
|
191
|
+
* 원자료 로그 — 코드 블록(틀이 곧 뜻인 면, 12px). 줄이 들어온 시각에 맨 아래 붙고 한 줄씩 위로 밀려 오른다.
|
|
192
|
+
* pick 정규식이 잡은 숫자는 그 시각부터 고름 칠(파랑 /10 알약)과 파란 글자.
|
|
179
193
|
* 줄 자리는 size·lineHeight·pad 로 정해지니 pickFly 출발점을 장면이 같은 값으로 셀 수 있다(새 줄 = 맨 아래 칸).
|
|
180
194
|
*/
|
|
181
195
|
export function rawLog(
|
|
@@ -187,7 +201,7 @@ export function rawLog(
|
|
|
187
201
|
const lh = o.lineHeight ?? Math.round(size * 1.5);
|
|
188
202
|
const rows = o.rows ?? 9;
|
|
189
203
|
const pad = o.pad ?? 28;
|
|
190
|
-
const panel = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${pad}px;border-radius:
|
|
204
|
+
const panel = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;width:${o.w}px;box-sizing:border-box;padding:${pad}px;border-radius:var(--kf-radius-md);background:var(--kf-surface);font-family:${MONO};font-size:${size}px;line-height:${lh}px;color:var(--kf-ink-soft);font-variant-numeric:tabular-nums`);
|
|
191
205
|
// 밀려 오르는 줄이 안쪽 여백에 반쯤 걸쳐 보이지 않게 줄 칸만큼만 보이는 창을 따로 둔다
|
|
192
206
|
const view = box(doc, `position:relative;height:${rows * lh}px;overflow:hidden`);
|
|
193
207
|
const strip = box(doc, "position:relative");
|
|
@@ -203,28 +217,28 @@ export function rawLog(
|
|
|
203
217
|
const start = m.index + m[0].indexOf(g);
|
|
204
218
|
num = doc.createElement("span");
|
|
205
219
|
num.textContent = g;
|
|
206
|
-
num.style.cssText = "border-radius:
|
|
220
|
+
num.style.cssText = "border-radius:var(--kf-radius-pill);padding:0 0.3em;margin:0 -0.3em";
|
|
207
221
|
row.append(doc.createTextNode(line.slice(0, start)), num, doc.createTextNode(line.slice(start + g.length)));
|
|
208
222
|
} else row.textContent = line;
|
|
209
223
|
strip.append(row);
|
|
210
224
|
return { row, num };
|
|
211
225
|
});
|
|
212
226
|
return (t: number) => {
|
|
213
|
-
applyStyle(panel, rise(t, o.at ?? 0
|
|
227
|
+
applyStyle(panel, rise(t, o.at ?? 0));
|
|
214
228
|
let shown = 0;
|
|
215
229
|
let slide = 1;
|
|
216
230
|
o.times.forEach((at, i) => {
|
|
217
231
|
if (t >= at) {
|
|
218
232
|
shown = i + 1;
|
|
219
|
-
slide =
|
|
233
|
+
slide = out(progress(t, at, BASE));
|
|
220
234
|
}
|
|
221
|
-
applyStyle(items[i].row, t >= at ? rise(t, at, 0
|
|
235
|
+
applyStyle(items[i].row, t >= at ? rise(t, at, BASE, 0, "type") : { opacity: 0 });
|
|
222
236
|
const n = items[i].num;
|
|
223
237
|
if (n) {
|
|
224
238
|
const on = t >= at;
|
|
225
|
-
n.style.background = on ? "
|
|
239
|
+
n.style.background = on ? "var(--kf-tint)" : "transparent";
|
|
226
240
|
n.style.color = on ? "var(--kf-accent-text)" : "";
|
|
227
|
-
n.style.fontWeight = on ? "
|
|
241
|
+
n.style.fontWeight = on ? "600" : "";
|
|
228
242
|
}
|
|
229
243
|
});
|
|
230
244
|
const over = Math.max(0, shown - rows);
|
|
@@ -232,37 +246,143 @@ export function rawLog(
|
|
|
232
246
|
};
|
|
233
247
|
}
|
|
234
248
|
|
|
235
|
-
|
|
249
|
+
type Pt = { x: number; y: number };
|
|
250
|
+
/**
|
|
251
|
+
* 읽는 칸이 닿을 자리 — 무대 좌표 가운데(x·y)와 크기, 아니면 target 요소(효과가 돌려준 .el)를 그 자리에서 잰다. 요소 여럿이면 그 전부를 덮는 한 칸.
|
|
252
|
+
* pad 는 글 둘레 여백(배지는 0 — 배지 테두리에 딱 맞는다)
|
|
253
|
+
*/
|
|
254
|
+
export interface ReadStop {
|
|
255
|
+
at: number;
|
|
256
|
+
x?: number;
|
|
257
|
+
y?: number;
|
|
258
|
+
w?: number;
|
|
259
|
+
h?: number;
|
|
260
|
+
target?: HTMLElement | null | (HTMLElement | null)[];
|
|
261
|
+
pad?: number;
|
|
262
|
+
}
|
|
263
|
+
type Box = Pt & { w: number; h: number };
|
|
264
|
+
|
|
265
|
+
// SlidingPillNav 처럼 offsetTop·offsetLeft 를 무대까지 더해 잰다 — 나타남의 6px 올라옴(transform)은 자리에 넣지 않는다
|
|
266
|
+
function measure(target: HTMLElement, stage: HTMLElement): { l: number; t: number; r: number; b: number } | null {
|
|
267
|
+
let x = 0;
|
|
268
|
+
let y = 0;
|
|
269
|
+
let n: HTMLElement | null = target;
|
|
270
|
+
for (; n && n !== stage; n = n.offsetParent as HTMLElement | null) {
|
|
271
|
+
x += n.offsetLeft;
|
|
272
|
+
y += n.offsetTop;
|
|
273
|
+
}
|
|
274
|
+
if (n !== stage || !target.offsetWidth) return null;
|
|
275
|
+
return { l: x, t: y, r: x + target.offsetWidth, b: y + target.offsetHeight };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function measureAll(targets: (HTMLElement | null)[], stage: HTMLElement, pad: number): Box | null {
|
|
279
|
+
const rs = targets.map((el) => el && measure(el, stage)).filter((r) => r !== null);
|
|
280
|
+
if (!rs.length) return null;
|
|
281
|
+
const l = Math.min(...rs.map((r) => r.l)) - pad;
|
|
282
|
+
const t = Math.min(...rs.map((r) => r.t)) - pad;
|
|
283
|
+
const r = Math.max(...rs.map((q) => q.r)) + pad;
|
|
284
|
+
const b = Math.max(...rs.map((q) => q.b)) + pad;
|
|
285
|
+
return { x: (l + r) / 2, y: (t + b) / 2, w: r - l, h: b - t };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 짚은 값 위에 서 있는 최소 시간 — 다음 값이 박에 바로 와도 칸이 한 번은 그 값에 딱 맞게 앉는다
|
|
289
|
+
const REST = 0.03;
|
|
290
|
+
const smooth = (p: number) => p * p * (3 - 2 * p);
|
|
291
|
+
|
|
292
|
+
// 옮겨 가는 동안의 칸 — 떠나는 값에서 아래 테두리 한 줄로 내려앉고, 그 한 줄이 다음 값 아래로 미끄러진 뒤, 그 값의 칸만큼 다시 차오른다.
|
|
293
|
+
// 칸 크기 그대로 날아가면 지나는 길의 글을 통째로 덮어 고장처럼 보였다. a 가 없으면(첫 값) 그 자리에서 차오르기만.
|
|
294
|
+
// 미끄러지는 몫은 거리만큼 — 600px 넘게 가면 옮김의 40%, 같은 칸을 다시 짚으면 0(한 줄로 서 있는 동안 화면이 멈춘다)
|
|
295
|
+
function between(a: Box | null, b: Box, u: number, line: number): Box {
|
|
296
|
+
const from = a ?? b;
|
|
297
|
+
const sink = (1 - 0.4 * clamp(Math.hypot(b.x - from.x, b.y + b.h / 2 - from.y - from.h / 2) / 600)) / 2;
|
|
298
|
+
const go = a ? (sink < 0.5 ? smooth(clamp((u - sink) / (1 - 2 * sink))) : u < 0.5 ? 0 : 1) : 1;
|
|
299
|
+
const full = a ? (u < 0.5 ? 1 - clamp(u / sink) : clamp((u - 1 + sink) / sink)) : clamp(u);
|
|
300
|
+
const hBase = a && u < 0.5 ? a.h : b.h;
|
|
301
|
+
const h = line + (hBase - line) * full;
|
|
302
|
+
const bottom = from.y + from.h / 2 + (b.y + b.h / 2 - from.y - from.h / 2) * go;
|
|
303
|
+
return { x: from.x + (b.x - from.x) * go, y: bottom - h / 2, w: from.w + (b.w - from.w) * go, h };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 바탕보다 밝은 글은 「밝게 섞기」(어두운 바탕), 어두운 글은 「어둡게 섞기」(밝은 바탕)로 그대로 남는다 — 위에 그려도 글 뒤에 깔린 것처럼 보인다.
|
|
307
|
+
// 응답 패널·로그의 면 안쪽 값도 짚어야 해서 쌓는 차례로는 글 뒤에 둘 수 없다
|
|
308
|
+
function underText(el: HTMLElement): string {
|
|
309
|
+
const m = /#([0-9a-f]{6})/i.exec(getComputedStyle(el).getPropertyValue("--kf-bg"));
|
|
310
|
+
const [r, g, b] = m ? [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16)) : [0, 0, 0];
|
|
311
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 128 ? "screen" : "multiply";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 뽑힌 숫자·읽는 칸 — 고름 칠(파랑 /20, 정본의 bg-primary/20) 알약에 스타일가이드의 미끄러지는 알약 테두리(SlidingPillNav, 화면 글 크기에 맞춰 파랑 /55)를 두른 한 장.
|
|
316
|
+
* from·to 를 주면 로그 줄의 숫자가 알약째 떠올라 호를 그리며 점 그림 자리로 300ms 감속으로 날아가 꽂힌다(박마다 큰 움직임).
|
|
317
|
+
* stops 를 주면 요점(주인공 값·핵심 사실·그 순간 자막이 말하는 값)이 바뀔 때만 그 값으로 옮겨 가 짚는다 — 메뉴의 알약처럼 글 뒤에 깔리고,
|
|
318
|
+
* 옮겨 가는 동안은 아래 테두리 한 줄로 얇아져 지나가며, 닿으면 그 값의 칸만큼 차올라 다음 요점까지 딱 맞게 선다(옮김 300ms 안).
|
|
319
|
+
* 나타나는 항목을 차례로 훑는 정지점은 두지 않는다 — 화면을 살려 두는 몫은 내용이 진다
|
|
320
|
+
*/
|
|
236
321
|
export function pickFly(
|
|
237
322
|
el: HTMLElement,
|
|
238
|
-
o: {
|
|
323
|
+
o: {
|
|
324
|
+
items: ({ at: number; text: string; from: Pt; to: Pt } | { text?: string; stops: ReadStop[] })[];
|
|
325
|
+
dur?: number;
|
|
326
|
+
size?: number;
|
|
327
|
+
},
|
|
239
328
|
): (t: number) => void {
|
|
240
329
|
const doc = el.ownerDocument;
|
|
241
|
-
const dur = o.dur ??
|
|
330
|
+
const dur = o.dur ?? SLOW;
|
|
242
331
|
const size = o.size ?? 44;
|
|
243
|
-
const
|
|
244
|
-
|
|
332
|
+
const border = Math.max(3, Math.round(size * 0.08));
|
|
333
|
+
let blend = "";
|
|
334
|
+
const pills = o.items.map((it) => {
|
|
335
|
+
const s = box(doc, `position:absolute;left:0;top:0;box-sizing:border-box;display:flex;align-items:center;justify-content:center;font-family:${MONO};font-size:${size}px;font-weight:600;color:var(--kf-accent-text);background:color-mix(in srgb, var(--kf-accent) 20%, transparent);border:${border}px solid color-mix(in srgb, var(--kf-accent) 55%, transparent);border-radius:var(--kf-radius-pill);padding:0 0.3em;white-space:nowrap;opacity:0;z-index:7;pointer-events:none;font-variant-numeric:tabular-nums`, it.text ?? "");
|
|
245
336
|
el.append(s);
|
|
246
337
|
return s;
|
|
247
338
|
});
|
|
339
|
+
const place = (s: HTMLElement, p: Box, opacity: number) => {
|
|
340
|
+
s.style.opacity = String(opacity);
|
|
341
|
+
s.style.width = `${p.w.toFixed(1)}px`;
|
|
342
|
+
s.style.height = `${p.h.toFixed(1)}px`;
|
|
343
|
+
s.style.transform = `translate(${(p.x - p.w / 2).toFixed(1)}px, ${(p.y - p.h / 2).toFixed(1)}px)`;
|
|
344
|
+
};
|
|
248
345
|
return (t: number) => {
|
|
249
346
|
o.items.forEach((it, i) => {
|
|
250
|
-
const s =
|
|
251
|
-
|
|
252
|
-
|
|
347
|
+
const s = pills[i];
|
|
348
|
+
if (!("stops" in it)) {
|
|
349
|
+
const u = (t - it.at) / dur;
|
|
350
|
+
cue("land", t - it.at - dur, { source: `pickFly@${it.at.toFixed(3)}` });
|
|
351
|
+
if (u < 0 || u > 1) {
|
|
352
|
+
s.style.opacity = "0";
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const p = out(u);
|
|
356
|
+
s.style.width = s.style.height = "";
|
|
357
|
+
s.style.opacity = String(1 - clamp((u - 0.85) / 0.15));
|
|
358
|
+
s.style.transform = `translate(${it.from.x + (it.to.x - it.from.x) * p}px, ${it.from.y + (it.to.y - it.from.y) * p - Math.sin(Math.PI * p) * 180}px) translate(-50%, -50%)`;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
blend ||= underText(el);
|
|
362
|
+
s.style.mixBlendMode = blend;
|
|
363
|
+
const stops = [...it.stops].sort((a, b) => a.at - b.at);
|
|
364
|
+
if (!stops.length || t < stops[0].at) {
|
|
253
365
|
s.style.opacity = "0";
|
|
254
366
|
return;
|
|
255
367
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
368
|
+
const boxOf = (st: ReadStop): Box => (st.target && measureAll([st.target].flat(), el, st.pad ?? 0)) || { x: st.x ?? 0, y: st.y ?? 0, w: st.w ?? size * 3, h: st.h ?? size * 1.6 };
|
|
369
|
+
let k = 0;
|
|
370
|
+
while (k + 1 < stops.length && stops[k + 1].at <= t) k++;
|
|
371
|
+
// 값이 나타나는 순간 떠나 300ms 안에 닿는다 — 다음 값이 가까우면 그 앞에 앉도록 더 빨리
|
|
372
|
+
const next = stops[k + 1];
|
|
373
|
+
const move = Math.max(0.1, Math.min(SLOW, (next ? next.at - stops[k].at : Infinity) - REST));
|
|
374
|
+
const u = clamp((t - stops[k].at) / move);
|
|
375
|
+
if (k > 0) cue("select", t - stops[k].at, { source: `pickFly:read@${stops[k].at.toFixed(3)}` });
|
|
376
|
+
const line = border * 2;
|
|
377
|
+
const p = between(k ? boxOf(stops[k - 1]) : null, boxOf(stops[k]), u, line);
|
|
378
|
+
// 얇을수록 진한 파랑 — 탭 아래 줄처럼 얇아도 지나가는 게 보이게. 글 한 줄 반 높이를 넘으면 옅은 칠로(큰 숫자 아래 두꺼운 파란 띠가 되지 않게)
|
|
379
|
+
s.style.background = `color-mix(in srgb, var(--kf-accent) ${Math.round(20 + 80 * clamp(1 - (p.h - line) / (size * 1.5)))}%, transparent)`;
|
|
380
|
+
place(s, p, out(progress(t, stops[0].at, BASE)));
|
|
261
381
|
});
|
|
262
382
|
};
|
|
263
383
|
}
|
|
264
384
|
|
|
265
|
-
/** 샘플 점 그림 — 모델마다 재 온 값이 들어온 시각에 점으로 꽂히고, 지금까지의 가운데값 눈금·숫자가 따라
|
|
385
|
+
/** 샘플 점 그림 — 모델마다 재 온 값이 들어온 시각에 점으로 꽂히고, 지금까지의 가운데값 눈금·숫자가 300ms 로 따라 미끄러진다. 눈금·막대는 각짐 */
|
|
266
386
|
export function samplePlot(
|
|
267
387
|
el: HTMLElement,
|
|
268
388
|
o: { x: number; y: number; w: number; rows: { label: string; samples: { t: number; v: number }[] }[]; min: number; max: number; unit?: string; decimals?: number; rowH?: number; at?: number; ticks?: number[]; best?: number; size?: number },
|
|
@@ -282,10 +402,10 @@ export function samplePlot(
|
|
|
282
402
|
const rows = o.rows.map((r, i) => {
|
|
283
403
|
const top = i * rowH;
|
|
284
404
|
const label = box(doc, `position:absolute;left:0;top:${top}px;font-size:${size}px;color:var(--kf-ink);white-space:nowrap`, r.label);
|
|
285
|
-
const track = box(doc, `position:absolute;left:0;right:0;top:${top + trackY}px;height:10px;
|
|
405
|
+
const track = box(doc, `position:absolute;left:0;right:0;top:${top + trackY}px;height:10px;background:${BAR}`);
|
|
286
406
|
const dots = r.samples.map(() => box(doc, `position:absolute;top:${top + trackY - 19}px;width:48px;height:48px;margin-left:-24px;border-radius:50%;background:var(--kf-accent);opacity:0`));
|
|
287
|
-
const mark = box(doc, `position:absolute;top:${top + trackY - 40}px;width:8px;height:90px;margin-left:-4px;
|
|
288
|
-
const val = box(doc, `position:absolute;top:${top + trackY + 30}px;right:0;font-size:${size * 1.35}px;font-weight:
|
|
407
|
+
const mark = box(doc, `position:absolute;top:${top + trackY - 40}px;width:8px;height:90px;margin-left:-4px;background:var(--kf-ink);opacity:0`);
|
|
408
|
+
const val = box(doc, `position:absolute;top:${top + trackY + 30}px;right:0;font-size:${size * 1.35}px;font-weight:600;color:var(--kf-ink);opacity:0`);
|
|
289
409
|
wrap.append(label, track, ...dots, mark, val);
|
|
290
410
|
return { r, dots, mark, val };
|
|
291
411
|
});
|
|
@@ -295,32 +415,29 @@ export function samplePlot(
|
|
|
295
415
|
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
296
416
|
};
|
|
297
417
|
return (t: number) => {
|
|
298
|
-
applyStyle(wrap, rise(t, o.at ?? 0
|
|
418
|
+
applyStyle(wrap, rise(t, o.at ?? 0));
|
|
299
419
|
rows.forEach(({ r, dots, mark, val }, i) => {
|
|
300
420
|
const got = r.samples.filter((s) => t >= s.t);
|
|
301
421
|
r.samples.forEach((s, k) => {
|
|
302
|
-
const p = snappy(progress(t, s.t, 0.2));
|
|
303
422
|
dots[k].style.left = `${X(s.v)}px`;
|
|
304
|
-
dots[k].
|
|
305
|
-
dots[k].style.transform = `scale(${t >= s.t ? 1.8 - 0.8 * p : 0})`;
|
|
423
|
+
applyStyle(dots[k], t >= s.t ? { ...rise(t, s.t, BASE, RISE, "tick"), opacity: 0.6 * out(progress(t, s.t, BASE)) } : { opacity: 0 });
|
|
306
424
|
});
|
|
307
425
|
if (!got.length) {
|
|
308
426
|
mark.style.opacity = "0";
|
|
309
427
|
val.style.opacity = "0";
|
|
310
428
|
return;
|
|
311
429
|
}
|
|
312
|
-
// 가운데값은 새 점이 올 때마다 그 자리로 미끄러진다 — 앞 값과 새 값을
|
|
430
|
+
// 가운데값은 새 점이 올 때마다 그 자리로 미끄러진다 — 앞 값과 새 값을 300ms(자리 옮김)로 잇는다
|
|
313
431
|
const last = got[got.length - 1];
|
|
314
432
|
const prev = got.length > 1 ? median(got.slice(0, -1).map((s) => s.v)) : last.v;
|
|
315
433
|
const now = median(got.map((s) => s.v));
|
|
316
|
-
const k =
|
|
434
|
+
const k = out(progress(t, last.t, SLOW));
|
|
317
435
|
const m = prev + (now - prev) * k;
|
|
318
436
|
mark.style.left = `${X(m)}px`;
|
|
319
437
|
mark.style.opacity = "1";
|
|
320
438
|
mark.style.background = o.best === i ? "var(--kf-accent)" : "var(--kf-ink)";
|
|
321
|
-
val
|
|
439
|
+
applyStyle(val, rise(t, got[0].t));
|
|
322
440
|
val.style.color = o.best === i ? "var(--kf-accent-text)" : "var(--kf-ink)";
|
|
323
|
-
val.style.transform = `scale(${1 + 0.15 * (1 - k)})`;
|
|
324
441
|
val.textContent = `${m.toFixed(o.decimals ?? 1)}${o.unit ?? ""}`;
|
|
325
442
|
});
|
|
326
443
|
};
|
|
@@ -333,24 +450,25 @@ export function meter(t: number, o: { to: number; at: number; dur?: number; deci
|
|
|
333
450
|
const n = target.replace(/\D/g, "").length || 1;
|
|
334
451
|
const dur = o.dur ?? 0.9;
|
|
335
452
|
let k = 0;
|
|
336
|
-
let
|
|
453
|
+
let s = "";
|
|
337
454
|
for (const ch of target) {
|
|
338
455
|
if (!/\d/.test(ch)) {
|
|
339
|
-
|
|
456
|
+
s += ch;
|
|
340
457
|
continue;
|
|
341
458
|
}
|
|
342
459
|
const settle = o.at + (dur * (k + 1)) / n;
|
|
343
|
-
|
|
460
|
+
cue("tick", t - settle, { source: `meter@${settle.toFixed(3)}`, minor: true });
|
|
461
|
+
s += t >= settle ? ch : String(Math.floor((t * 26 + k * 3.7) % 10 + 10) % 10);
|
|
344
462
|
k++;
|
|
345
463
|
}
|
|
346
|
-
return `${
|
|
464
|
+
return `${s}${o.unit ?? ""}`;
|
|
347
465
|
}
|
|
348
466
|
|
|
349
|
-
/** 막대 —
|
|
467
|
+
/** 막대 — 각진 막대가 박마다 하나씩 값만큼 300ms 로 자란다. best 만 브랜드 파랑(차트 칠), 나머지는 회색. 값은 막대 끝, delta 를 주면 그 박에 테두리 배지 */
|
|
350
468
|
export function rankBars(
|
|
351
469
|
el: HTMLElement,
|
|
352
470
|
o: { x: number; y: number; w: number; items: { label: string; value: number; at: number; delta?: string; deltaAt?: number }[]; max: number; best?: number | number[]; prefix?: string; unit?: string; decimals?: number; barH?: number; gap?: number; size?: number },
|
|
353
|
-
): (t: number) => void {
|
|
471
|
+
): ((t: number) => void) & { vals: HTMLElement[]; deltas: (HTMLElement | null)[] } {
|
|
354
472
|
const doc = el.ownerDocument;
|
|
355
473
|
const barH = o.barH ?? 76;
|
|
356
474
|
const size = o.size ?? 38;
|
|
@@ -363,16 +481,16 @@ export function rankBars(
|
|
|
363
481
|
const top = i * step;
|
|
364
482
|
const label = box(doc, `position:absolute;left:0;top:${top}px;font-size:${size}px;color:var(--kf-ink-soft);white-space:nowrap`, it.label);
|
|
365
483
|
const bar = box(doc, `position:absolute;left:0;top:${top + size * 1.4}px;height:${barH}px;background:${bests.includes(i) ? "var(--kf-accent)" : BAR};transform-origin:0 50%`);
|
|
366
|
-
const val = box(doc, `position:absolute;top:${top + size * 1.4}px;line-height:${barH}px;font-size:${size * 1.2}px;font-weight:
|
|
484
|
+
const val = box(doc, `position:absolute;top:${top + size * 1.4}px;line-height:${barH}px;font-size:${size * 1.2}px;font-weight:600;white-space:nowrap;color:${bests.includes(i) ? "var(--kf-accent-text)" : "var(--kf-ink)"}`);
|
|
367
485
|
const delta = it.delta
|
|
368
|
-
? box(doc, `position:absolute;top:${top + size * 1.4 + barH / 2}px;padding:
|
|
486
|
+
? box(doc, `position:absolute;top:${top + size * 1.4 + barH / 2}px;padding:2px 16px;font-size:${size}px;font-weight:600;white-space:nowrap;${badgeCss("plain", size)}`, it.delta)
|
|
369
487
|
: null;
|
|
370
488
|
wrap.append(label, bar, val, ...(delta ? [delta] : []));
|
|
371
489
|
return { it, label, bar, val, delta };
|
|
372
490
|
});
|
|
373
|
-
|
|
491
|
+
const paint = (t: number) => {
|
|
374
492
|
rows.forEach(({ it, label, bar, val, delta }) => {
|
|
375
|
-
const p =
|
|
493
|
+
const p = out(progress(t, it.at, SLOW));
|
|
376
494
|
const w = (it.value / o.max) * (o.w - 440);
|
|
377
495
|
applyStyle(label, rise(t, it.at));
|
|
378
496
|
bar.style.width = `${w}px`;
|
|
@@ -382,51 +500,54 @@ export function rankBars(
|
|
|
382
500
|
val.textContent = `${o.prefix ?? ""}${it.value.toFixed(o.decimals ?? 1)}${o.unit ?? ""}`;
|
|
383
501
|
if (delta) {
|
|
384
502
|
const at = it.deltaAt ?? it.at + 0.35;
|
|
385
|
-
const d = snappy(progress(t, at, 0.22));
|
|
386
503
|
delta.style.left = `${w + 24 + size * 6}px`;
|
|
387
|
-
delta
|
|
388
|
-
delta.style.
|
|
504
|
+
applyStyle(delta, t >= at ? rise(t, at) : { opacity: 0 });
|
|
505
|
+
delta.style.translate = "0 -50%";
|
|
389
506
|
}
|
|
390
507
|
});
|
|
391
508
|
};
|
|
509
|
+
return Object.assign(paint, { vals: rows.map((r) => r.val), deltas: rows.map((r) => r.delta) });
|
|
392
510
|
}
|
|
393
511
|
|
|
394
|
-
/**
|
|
395
|
-
|
|
512
|
+
/**
|
|
513
|
+
* 끝 도장 — 구분선 한 줄 위에 OG 로고와 「측정 날짜 · 호출 N번 · $비용 · 키 하나」가 선다(상자 없이). 비용은 반올림해 0 으로 만들지 않고 첫 유효 자리까지.
|
|
514
|
+
* logo 는 마스터 로고 SVG 원문(currentColor) — 한 색으로 칠한다. 어두운 바탕 흰색·밝은 바탕 검정(logoInk)
|
|
515
|
+
*/
|
|
516
|
+
export function stamp(el: HTMLElement, o: { x: number; y: number; at: number; date: string; calls: number; cost: number; logo?: string; logoInk?: string; size?: number }): (t: number) => void {
|
|
396
517
|
const doc = el.ownerDocument;
|
|
397
518
|
const size = o.size ?? 36;
|
|
398
|
-
const
|
|
519
|
+
const row = box(doc, `position:absolute;left:${o.x}px;top:${o.y}px;display:flex;align-items:center;gap:${size}px;padding-top:${size * 0.6}px;border-top:${edge(size)}px solid var(--kf-line);font-family:${MONO};font-size:${size}px;color:var(--kf-ink);white-space:nowrap;font-variant-numeric:tabular-nums`);
|
|
399
520
|
if (o.logo) {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
card.append(img);
|
|
521
|
+
const mark = box(doc, `height:${size * 1.25}px;width:${(size * 1.25 * 1140) / 650}px;color:${o.logoInk ?? "var(--kf-ink)"};display:flex`);
|
|
522
|
+
mark.innerHTML = fitSvg(o.logo);
|
|
523
|
+
row.append(mark);
|
|
404
524
|
}
|
|
405
525
|
const digits = o.cost > 0 ? Math.max(2, -Math.floor(Math.log10(o.cost))) : 2;
|
|
406
|
-
|
|
407
|
-
el.append(
|
|
526
|
+
row.append(box(doc, "", `측정 ${o.date} · 호출 ${o.calls}번 · $${o.cost.toFixed(digits)} · 키 하나`));
|
|
527
|
+
el.append(row);
|
|
408
528
|
return (t: number) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
card.style.transform = `translateX(-50%) translateY(${20 * (1 - p)}px) scale(${1.25 - 0.25 * p})`;
|
|
529
|
+
applyStyle(row, rise(t, o.at, BASE, RISE, "end"));
|
|
530
|
+
row.style.translate = "-50% 0";
|
|
412
531
|
};
|
|
413
532
|
}
|
|
414
533
|
|
|
415
|
-
/**
|
|
416
|
-
|
|
534
|
+
/**
|
|
535
|
+
* 배지 — 짧은 낱말·모델 ID·잰 값 하나(「실측」·「openai/ 41」). 속을 칠하지 않고 흐린 테두리와 글자색만(badge.tsx 의 default·outline·secondary).
|
|
536
|
+
* 6px 올라오며 220ms 로 나타나고 until 에 150ms 로 빠진다
|
|
537
|
+
*/
|
|
538
|
+
export function flag(el: HTMLElement, o: { x: number; y: number; text: string; at: number; until?: number; tone?: "accent" | "soft" | "plain"; size?: number; mono?: boolean }): ((t: number) => void) & { el: HTMLElement } {
|
|
417
539
|
const doc = el.ownerDocument;
|
|
418
|
-
const
|
|
540
|
+
const size = o.size ?? 38;
|
|
419
541
|
const chip = box(
|
|
420
542
|
doc,
|
|
421
|
-
`position:absolute;left:${o.x}px;top:${o.y}px;padding:
|
|
422
|
-
(o.tone === "plain" ? "background:var(--kf-surface);color:var(--kf-ink)" : soft ? "background:var(--kf-tint);color:var(--kf-accent-text)" : "background:var(--kf-accent);color:var(--kf-on-accent)"),
|
|
543
|
+
`position:absolute;left:${o.x}px;top:${o.y}px;padding:4px 0.5em;font-family:${o.mono ? MONO : "var(--kf-font-sans)"};font-size:${size}px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums;${badgeCss(o.tone ?? "accent", size)}`,
|
|
423
544
|
o.text,
|
|
424
545
|
);
|
|
425
546
|
el.append(chip);
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
chip.
|
|
430
|
-
chip.style.transform = `scale(${t >= o.at ? (1.4 - 0.4 * p) * (1 - 0.6 * gone) : 0.6})`;
|
|
547
|
+
const paint = (t: number) => {
|
|
548
|
+
const gone = o.until === undefined ? 0 : out(progress(t, o.until, FAST));
|
|
549
|
+
const s = rise(t, o.at);
|
|
550
|
+
applyStyle(chip, t >= o.at ? { ...s, opacity: (s.opacity ?? 1) * (1 - gone) } : { opacity: 0 });
|
|
431
551
|
};
|
|
552
|
+
return Object.assign(paint, { el: chip });
|
|
432
553
|
}
|