kasaframe 0.1.4 → 0.1.5
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 +298 -0
- package/README.md +16 -3
- package/docs/AGENTS.md +1 -1
- package/package.json +1 -1
- package/packages/cli/src/commands/doctor.ts +15 -2
- package/packages/cli/src/commands/fonts.ts +118 -0
- 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/update.ts +250 -0
- package/packages/plan/src/asks.ts +1 -1
- 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 +10 -2
- package/packages/themes/src/look.ts +34 -8
- package/packages/themes/src/types.ts +5 -0
- package/packages/themes/src/vary.ts +35 -34
- package/presets/kawaii-pastel.json +2 -2
- 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/sample/style.json +2 -2
- package/themes/kawaii-sticker/style.json +2 -2
- package/themes/kawaii-sticker/theme.json +3 -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 +32 -6
- package/themes/og-measure/effects.ts +219 -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 +12 -1
- package/themes/og-measure/sample/scenes/models.ts +13 -2
- package/themes/og-measure/sample/scenes/one.ts +8 -6
- package/themes/og-measure/sample/scenes/price.ts +20 -3
- package/themes/og-measure/sample/style.json +30 -35
- package/themes/og-measure/slots.ts +25 -30
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +30 -35
- package/themes/og-measure/theme.json +64 -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,29 @@
|
|
|
1
|
-
import { applyStyle, clamp, emit, ease, progress, safeTime, type Style } from "@kasaframe/core";
|
|
1
|
+
import { applyStyle, bezier, clamp, emit, ease, progress, safeTime, 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
|
-
|
|
14
|
-
const p = snappy(progress(t, at, dur));
|
|
19
|
+
function rise(t: number, at: number, dur = BASE, px = RISE): Style {
|
|
20
|
+
const p = out(progress(t, at, dur));
|
|
15
21
|
return { opacity: p, y: px * (1 - p) };
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
// 마스터 로고 원문은 루트 <svg> 에 width·height(1140×650)가 박혀 있다 — 루트에서만 걷고 상자를 채우게(안쪽 rect 의 width 는 그대로)
|
|
25
|
+
const fitSvg = (svg: string) => svg.replace(/<svg\b[^>]*>/, (tag) => tag.replace(/\s(width|height)="[^"]*"/g, "").replace("<svg", '<svg width="100%" height="100%"'));
|
|
26
|
+
|
|
18
27
|
function box(doc: Document, css: string, text = ""): HTMLElement {
|
|
19
28
|
const d = doc.createElement("div");
|
|
20
29
|
d.style.cssText = css;
|
|
@@ -22,27 +31,33 @@ function box(doc: Document, css: string, text = ""): HTMLElement {
|
|
|
22
31
|
return d;
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
// 배지 — 흐린 테두리와 글자색만(badge.tsx). 화면 글이 UI 의 서너 배라 선도 그만큼 굵게
|
|
35
|
+
type Tone = "accent" | "plain" | "soft" | "success";
|
|
36
|
+
const edge = (size: number) => Math.max(2, Math.round(size * 0.05));
|
|
37
|
+
function badgeCss(tone: Tone, size: number): string {
|
|
38
|
+
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)";
|
|
39
|
+
const ink = tone === "accent" ? "var(--kf-accent-text)" : tone === "success" ? "var(--kf-success)" : tone === "soft" ? "var(--kf-dim)" : "var(--kf-ink)";
|
|
40
|
+
return `border:${edge(size)}px solid ${line};border-radius:var(--kf-radius-pill);background:transparent;color:${ink}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
/**
|
|
26
|
-
* 측정 바탕 —
|
|
27
|
-
*
|
|
44
|
+
* 측정 바탕 — 바탕색 위에 구분선 색의 옅은 모눈이 천천히 흐른다. 그라데이션·빛 번짐 없이 모눈 하나뿐이고 구도는 안 바꾼다.
|
|
45
|
+
* 박 사이를 메우는 몫은 바탕이 아니라 내용(카운터·로그·막대·읽는 칸)이 진다
|
|
28
46
|
*/
|
|
29
|
-
export function graphPaper(el: HTMLElement, o: { cell?: number;
|
|
47
|
+
export function graphPaper(el: HTMLElement, o: { cell?: number; phase?: number } = {}): (t: number) => void {
|
|
30
48
|
const doc = el.ownerDocument;
|
|
31
49
|
const cell = o.cell ?? 56;
|
|
50
|
+
const grid = "color-mix(in srgb, var(--kf-line) 45%, transparent)";
|
|
32
51
|
const bg = box(
|
|
33
52
|
doc,
|
|
34
53
|
`position:absolute;inset:0;background:var(--kf-bg);overflow:hidden;` +
|
|
35
|
-
`background-image:linear-gradient(
|
|
54
|
+
`background-image:linear-gradient(${grid} 1px,transparent 1px),linear-gradient(90deg,${grid} 1px,transparent 1px);` +
|
|
36
55
|
`background-size:${cell}px ${cell}px`,
|
|
37
56
|
);
|
|
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
57
|
el.append(bg);
|
|
41
|
-
const period = o.period ?? 4;
|
|
42
58
|
return (t: number) => {
|
|
43
59
|
const u = safeTime(t) + (o.phase ?? 0);
|
|
44
60
|
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
61
|
};
|
|
47
62
|
}
|
|
48
63
|
|
|
@@ -52,47 +67,40 @@ export interface CamShot {
|
|
|
52
67
|
x?: number;
|
|
53
68
|
y?: number;
|
|
54
69
|
zoom?: number;
|
|
55
|
-
/** 샷이
|
|
70
|
+
/** 샷이 서는 순간 이만큼 가까이에서 시작해 300ms 감속으로 물러나 앉는다 */
|
|
56
71
|
kick?: number;
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
/**
|
|
60
|
-
* 계측 카메라 — 샷이 바뀌면
|
|
61
|
-
* 쉬지 않는 몫은 카운터·로그 한 줄·막대가 진다(늘 흐르는 카메라는 kf check camera.wander).
|
|
75
|
+
* 계측 카메라 — 샷이 바뀌면 300ms(자리 옮김) 스프링으로 옮기고 샷 사이에는 선다. 스프링이 살짝 넘었다 돌아오는 것이 「툭」이다.
|
|
76
|
+
* 샷은 드러냄·결과처럼 이유가 있을 때만 — 쉬지 않는 몫은 카운터·로그 한 줄·막대가 진다(늘 흐르는 카메라는 kf check camera.wander).
|
|
62
77
|
* 긴 로그 줄이 반쯤 잘리지 않게 크게 들어가지 않는다(배율 1–1.1). 무대 상자(transform-origin 0 0)에 붙인다.
|
|
63
78
|
*/
|
|
64
|
-
export function trackCam(t: number, o: { shots: CamShot[];
|
|
79
|
+
export function trackCam(t: number, o: { shots: CamShot[]; w?: number; h?: number; scene?: string }): Style {
|
|
65
80
|
t = safeTime(t);
|
|
66
81
|
const W = o.w ?? 1920;
|
|
67
82
|
const H = o.h ?? 1080;
|
|
68
|
-
const drift = o.drift ?? 0;
|
|
69
|
-
const pan = o.pan ?? 0;
|
|
70
83
|
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
|
-
};
|
|
84
|
+
const frame = (i: number) => ({ x: shots[i].x ?? W / 2, y: shots[i].y ?? H / 2, z: shots[i].zoom ?? 1 });
|
|
77
85
|
let i = 0;
|
|
78
86
|
for (let k = 0; k < shots.length; k++) if (shots[k].at <= t) i = k;
|
|
79
|
-
let f = frame(i
|
|
87
|
+
let f = frame(i);
|
|
80
88
|
const since = t - shots[i].at;
|
|
81
|
-
if (i > 0 && since <
|
|
82
|
-
const p =
|
|
83
|
-
const a = frame(i - 1
|
|
89
|
+
if (i > 0 && since < SLOW) {
|
|
90
|
+
const p = springy(since / SLOW);
|
|
91
|
+
const a = frame(i - 1);
|
|
84
92
|
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 -
|
|
93
|
+
emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round((1 - clamp(since / SLOW)) * 1000) });
|
|
86
94
|
}
|
|
87
|
-
const kick = since >= 0 ? (shots[i].kick ?? 0) *
|
|
88
|
-
if (
|
|
95
|
+
const kick = since >= 0 && since < SLOW ? (shots[i].kick ?? 0) * (1 - out(since / SLOW)) : 0;
|
|
96
|
+
if (kick > 0.0005) emit({ kind: "camera", source: "trackCam", scene: o.scene, value: Math.round(kick * 1000) });
|
|
89
97
|
const z = f.z * (1 + kick);
|
|
90
98
|
return { x: W / 2 - z * f.x, y: H / 2 - z * f.y, scale: z };
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
/**
|
|
94
|
-
* 실제 호출 한 줄 — 주소가 박마다 한 조각씩 붙고, 모델·보낼 글이 찍히고, sendAt 부터 경과 시간이
|
|
95
|
-
* 잰 값(ms)에 멈추며 상태·첫 토큰·받은 글이 붙는다. 카운터는 지어낸 값이 아니라 원자료의 ms 까지만 센다.
|
|
102
|
+
* 실제 호출 한 줄 — 응답 패널(틀이 곧 뜻인 면, 12px). 주소가 박마다 한 조각씩 붙고, 모델·보낼 글이 찍히고, sendAt 부터 경과 시간이
|
|
103
|
+
* 실제 속도로 굴러 잰 값(ms)에 멈추며 상태·첫 토큰·받은 글이 붙는다. 카운터는 지어낸 값이 아니라 원자료의 ms 까지만 센다.
|
|
96
104
|
*/
|
|
97
105
|
export function callLine(
|
|
98
106
|
el: HTMLElement,
|
|
@@ -117,13 +125,13 @@ export function callLine(
|
|
|
117
125
|
method?: string;
|
|
118
126
|
size?: number;
|
|
119
127
|
},
|
|
120
|
-
): (t: number) => void {
|
|
128
|
+
): ((t: number) => void) & { parts: HTMLElement[]; timer: HTMLElement } {
|
|
121
129
|
const doc = el.ownerDocument;
|
|
122
130
|
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:
|
|
131
|
+
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
132
|
const row = (css = "") => box(doc, `display:flex;align-items:center;gap:${size * 0.35}px;white-space:nowrap;${css}`);
|
|
125
133
|
const r1 = row();
|
|
126
|
-
const chip = box(doc, `padding:0 ${size * 0.3}px;
|
|
134
|
+
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
135
|
const urlBox = box(doc, "display:flex");
|
|
128
136
|
const parts = o.url.map((u) => {
|
|
129
137
|
const s = doc.createElement("span");
|
|
@@ -136,9 +144,9 @@ export function callLine(
|
|
|
136
144
|
const r2 = row("color:var(--kf-dim)");
|
|
137
145
|
const r3 = row("color:var(--kf-dim)");
|
|
138
146
|
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:
|
|
147
|
+
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`);
|
|
148
|
+
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 ?? ""));
|
|
149
|
+
const what = box(doc, "color:var(--kf-accent-text);font-weight:600", "첫 토큰");
|
|
142
150
|
const ans = box(doc, "color:var(--kf-ink)");
|
|
143
151
|
r4.append(timer, ok, what, ans);
|
|
144
152
|
const r5 = row(`color:var(--kf-dim);font-size:${size * 0.7}px`);
|
|
@@ -146,12 +154,12 @@ export function callLine(
|
|
|
146
154
|
el.append(card);
|
|
147
155
|
const ms = o.ms ?? 0;
|
|
148
156
|
const doneAt = o.sendAt === undefined ? Infinity : o.sendAt + ms / 1000;
|
|
149
|
-
|
|
150
|
-
applyStyle(card, rise(t, o.at
|
|
157
|
+
const paint = (t: number) => {
|
|
158
|
+
applyStyle(card, rise(t, o.at));
|
|
151
159
|
parts.forEach((s, i) => {
|
|
152
160
|
const at = o.urlAt[i] ?? o.at;
|
|
153
161
|
s.style.visibility = t >= at ? "visible" : "hidden";
|
|
154
|
-
applyStyle(s, t >= at ?
|
|
162
|
+
applyStyle(s, t >= at ? rise(t, at) : {});
|
|
155
163
|
});
|
|
156
164
|
r2.textContent = o.model ? typeText(`model: ${o.model}`, t, { at: o.modelAt ?? o.at, cps: 90, jitter: 0, ime: false }).text : "";
|
|
157
165
|
r2.style.display = o.model ? "" : "none";
|
|
@@ -161,21 +169,24 @@ export function callLine(
|
|
|
161
169
|
r4.style.display = o.sendAt === undefined ? "none" : "";
|
|
162
170
|
r4.style.visibility = o.sendAt !== undefined && t >= o.sendAt ? "visible" : "hidden";
|
|
163
171
|
timer.textContent = o.sendAt === undefined ? "" : `${Math.min(ms, Math.max(0, Math.floor((t - o.sendAt) * 1000)))}ms`;
|
|
164
|
-
|
|
172
|
+
// 색만 150ms 로 바뀐다 — 크기는 그대로(누름·도착에 커졌다 줄기 없음)
|
|
173
|
+
const c = done ? Math.round(out(progress(t, doneAt, FAST)) * 100) : 0;
|
|
174
|
+
timer.style.color = `color-mix(in srgb, var(--kf-accent-text) ${c}%, var(--kf-ink))`;
|
|
165
175
|
for (const e of [ok, what, ans]) {
|
|
166
176
|
e.style.display = done ? "" : "none";
|
|
167
177
|
applyStyle(e, rise(t, doneAt));
|
|
168
178
|
}
|
|
169
|
-
applyStyle(timer, done ? { scale: 1 + 0.12 * Math.exp(-(t - doneAt) / 0.1) } : {});
|
|
170
179
|
ans.textContent = done ? typeText(o.answer ?? "", t, { at: doneAt + 0.08, cps: 22, jitter: 0, ime: false }).text : "";
|
|
171
180
|
r5.textContent = o.usage && o.usageAt !== undefined && t >= o.usageAt ? o.usage : "";
|
|
172
181
|
r5.style.display = o.usage ? "" : "none";
|
|
173
182
|
applyStyle(r5, o.usageAt !== undefined ? rise(t, o.usageAt) : {});
|
|
174
183
|
};
|
|
184
|
+
return Object.assign(paint, { parts, timer });
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
/**
|
|
178
|
-
* 원자료 로그 — 줄이 들어온 시각에 맨 아래 붙고 한 줄씩 위로 밀려 오른다.
|
|
188
|
+
* 원자료 로그 — 코드 블록(틀이 곧 뜻인 면, 12px). 줄이 들어온 시각에 맨 아래 붙고 한 줄씩 위로 밀려 오른다.
|
|
189
|
+
* pick 정규식이 잡은 숫자는 그 시각부터 고름 칠(파랑 /10 알약)과 파란 글자.
|
|
179
190
|
* 줄 자리는 size·lineHeight·pad 로 정해지니 pickFly 출발점을 장면이 같은 값으로 셀 수 있다(새 줄 = 맨 아래 칸).
|
|
180
191
|
*/
|
|
181
192
|
export function rawLog(
|
|
@@ -187,7 +198,7 @@ export function rawLog(
|
|
|
187
198
|
const lh = o.lineHeight ?? Math.round(size * 1.5);
|
|
188
199
|
const rows = o.rows ?? 9;
|
|
189
200
|
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:
|
|
201
|
+
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
202
|
// 밀려 오르는 줄이 안쪽 여백에 반쯤 걸쳐 보이지 않게 줄 칸만큼만 보이는 창을 따로 둔다
|
|
192
203
|
const view = box(doc, `position:relative;height:${rows * lh}px;overflow:hidden`);
|
|
193
204
|
const strip = box(doc, "position:relative");
|
|
@@ -203,28 +214,28 @@ export function rawLog(
|
|
|
203
214
|
const start = m.index + m[0].indexOf(g);
|
|
204
215
|
num = doc.createElement("span");
|
|
205
216
|
num.textContent = g;
|
|
206
|
-
num.style.cssText = "border-radius:
|
|
217
|
+
num.style.cssText = "border-radius:var(--kf-radius-pill);padding:0 0.3em;margin:0 -0.3em";
|
|
207
218
|
row.append(doc.createTextNode(line.slice(0, start)), num, doc.createTextNode(line.slice(start + g.length)));
|
|
208
219
|
} else row.textContent = line;
|
|
209
220
|
strip.append(row);
|
|
210
221
|
return { row, num };
|
|
211
222
|
});
|
|
212
223
|
return (t: number) => {
|
|
213
|
-
applyStyle(panel, rise(t, o.at ?? 0
|
|
224
|
+
applyStyle(panel, rise(t, o.at ?? 0));
|
|
214
225
|
let shown = 0;
|
|
215
226
|
let slide = 1;
|
|
216
227
|
o.times.forEach((at, i) => {
|
|
217
228
|
if (t >= at) {
|
|
218
229
|
shown = i + 1;
|
|
219
|
-
slide =
|
|
230
|
+
slide = out(progress(t, at, BASE));
|
|
220
231
|
}
|
|
221
|
-
applyStyle(items[i].row, t >= at ? rise(t, at,
|
|
232
|
+
applyStyle(items[i].row, t >= at ? rise(t, at, BASE, 0) : { opacity: 0 });
|
|
222
233
|
const n = items[i].num;
|
|
223
234
|
if (n) {
|
|
224
235
|
const on = t >= at;
|
|
225
|
-
n.style.background = on ? "
|
|
236
|
+
n.style.background = on ? "var(--kf-tint)" : "transparent";
|
|
226
237
|
n.style.color = on ? "var(--kf-accent-text)" : "";
|
|
227
|
-
n.style.fontWeight = on ? "
|
|
238
|
+
n.style.fontWeight = on ? "600" : "";
|
|
228
239
|
}
|
|
229
240
|
});
|
|
230
241
|
const over = Math.max(0, shown - rows);
|
|
@@ -232,37 +243,130 @@ export function rawLog(
|
|
|
232
243
|
};
|
|
233
244
|
}
|
|
234
245
|
|
|
235
|
-
|
|
246
|
+
type Pt = { x: number; y: number };
|
|
247
|
+
/** 읽는 칸이 닿을 자리 — 무대 좌표 가운데(x·y)와 크기, 아니면 target 요소(효과가 돌려준 .el)를 그 자리에서 잰다 */
|
|
248
|
+
export interface ReadStop {
|
|
249
|
+
at: number;
|
|
250
|
+
x?: number;
|
|
251
|
+
y?: number;
|
|
252
|
+
w?: number;
|
|
253
|
+
h?: number;
|
|
254
|
+
target?: HTMLElement | null;
|
|
255
|
+
pad?: number;
|
|
256
|
+
}
|
|
257
|
+
type Box = Pt & { w: number; h: number };
|
|
258
|
+
|
|
259
|
+
// SlidingPillNav 처럼 offsetTop·offsetLeft 를 무대까지 더해 잰다 — 나타남의 6px 올라옴(transform)은 자리에 넣지 않는다
|
|
260
|
+
function measure(target: HTMLElement, stage: HTMLElement, pad: number): Box | null {
|
|
261
|
+
let x = 0;
|
|
262
|
+
let y = 0;
|
|
263
|
+
let n: HTMLElement | null = target;
|
|
264
|
+
for (; n && n !== stage; n = n.offsetParent as HTMLElement | null) {
|
|
265
|
+
x += n.offsetLeft;
|
|
266
|
+
y += n.offsetTop;
|
|
267
|
+
}
|
|
268
|
+
if (n !== stage || !target.offsetWidth) return null;
|
|
269
|
+
return { x: x + target.offsetWidth / 2, y: y + target.offsetHeight / 2, w: target.offsetWidth + pad * 2, h: target.offsetHeight + pad * 2 };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 뽑힌 숫자·읽는 칸 — 고름 칠(파랑 /20, 정본의 bg-primary/20) 알약에 스타일가이드의 미끄러지는 알약 테두리(SlidingPillNav, 화면 글 크기에 맞춰 파랑 /55)를 두른 한 장.
|
|
274
|
+
* from·to 를 주면 로그 줄의 숫자가 알약째 떠올라 호를 그리며 점 그림 자리로 300ms 감속으로 날아가 꽂힌다(박마다 큰 움직임).
|
|
275
|
+
* stops 를 주면 지금 재는 값 위로 옮겨 다니며 짚는다 — 메뉴의 알약처럼 값이 나타나 선 뒤(lag, 기본 220ms)에 그 값에 닿고, 그 사이엔 서지 않으며(가까우면 옆으로 휘어 길을 늘린다),
|
|
276
|
+
* 크기는 짚는 값에 맞춰 바뀐다. hold 면 마지막 값 뒤에도 서지 않고 처음 값부터 화면을 다시 짚어 훑는다(holdEvery 초마다 하나)
|
|
277
|
+
*/
|
|
236
278
|
export function pickFly(
|
|
237
279
|
el: HTMLElement,
|
|
238
|
-
o: {
|
|
280
|
+
o: {
|
|
281
|
+
items: ({ at: number; text: string; from: Pt; to: Pt } | { text?: string; stops: ReadStop[]; hold?: boolean; holdEvery?: number; lag?: number })[];
|
|
282
|
+
dur?: number;
|
|
283
|
+
size?: number;
|
|
284
|
+
/** 읽는 칸의 최소 빠르기(px/초) */
|
|
285
|
+
minSpeed?: number;
|
|
286
|
+
},
|
|
239
287
|
): (t: number) => void {
|
|
240
288
|
const doc = el.ownerDocument;
|
|
241
|
-
const dur = o.dur ??
|
|
289
|
+
const dur = o.dur ?? SLOW;
|
|
242
290
|
const size = o.size ?? 44;
|
|
243
|
-
const
|
|
244
|
-
|
|
291
|
+
const vmin = o.minSpeed ?? 320;
|
|
292
|
+
const pills = o.items.map((it) => {
|
|
293
|
+
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:${Math.max(3, Math.round(size * 0.08))}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
294
|
el.append(s);
|
|
246
295
|
return s;
|
|
247
296
|
});
|
|
297
|
+
const glide = (a: Box, b: Box, u: number, gap: number): Box => {
|
|
298
|
+
const d = Math.hypot(b.x - a.x, b.y - a.y);
|
|
299
|
+
// 넓은 알약(줄 하나를 짚는 칸)은 옆으로 휘면 긴 위아래 테두리가 제자리라 곧게 가고, 같은 칸을 다시 짚을 때만 화면 가운데 쪽으로 넘었다 돌아온다.
|
|
300
|
+
// 작은 알약은 곧게 옆으로만 가면 위아래 테두리가 제자리라 늘 조금은 휜다
|
|
301
|
+
const wide = a.w + b.w > 1.5 * (a.h + b.h);
|
|
302
|
+
const want = wide ? (d < 20 ? vmin * gap : d) : Math.max(d * 1.05, vmin * gap);
|
|
303
|
+
const h = Math.sqrt(Math.max(0, want * want - d * d) * (3 / 16));
|
|
304
|
+
const toCenter = (el.offsetHeight || 1080) / 2 < (a.y + b.y) / 2 ? -1 : 1;
|
|
305
|
+
const [nx, ny] = d > 1 && !wide ? [-(b.y - a.y) / d, (b.x - a.x) / d] : [0, toCenter];
|
|
306
|
+
const c = { x: (a.x + b.x) / 2 + nx * h * 2, y: (a.y + b.y) / 2 + ny * h * 2 };
|
|
307
|
+
// 감속 곡선에 곧은 몫을 섞어 닿기 직전에도 빠르기가 0 이 되지 않게 — 감속 곡선은 앞에 몰려 있어 곧은 몫을 더 크게
|
|
308
|
+
const e = out(u) * 0.4 + u * 0.6;
|
|
309
|
+
const v = 1 - e;
|
|
310
|
+
return { x: v * v * a.x + 2 * v * e * c.x + e * e * b.x, y: v * v * a.y + 2 * v * e * c.y + e * e * b.y, w: a.w + (b.w - a.w) * e, h: a.h + (b.h - a.h) * e };
|
|
311
|
+
};
|
|
312
|
+
const place = (s: HTMLElement, p: Box, opacity: number) => {
|
|
313
|
+
s.style.opacity = String(opacity);
|
|
314
|
+
s.style.width = `${p.w.toFixed(1)}px`;
|
|
315
|
+
s.style.height = `${p.h.toFixed(1)}px`;
|
|
316
|
+
s.style.transform = `translate(${(p.x - p.w / 2).toFixed(1)}px, ${(p.y - p.h / 2).toFixed(1)}px)`;
|
|
317
|
+
};
|
|
248
318
|
return (t: number) => {
|
|
249
319
|
o.items.forEach((it, i) => {
|
|
250
|
-
const s =
|
|
251
|
-
|
|
252
|
-
|
|
320
|
+
const s = pills[i];
|
|
321
|
+
if (!("stops" in it)) {
|
|
322
|
+
const u = (t - it.at) / dur;
|
|
323
|
+
if (u < 0 || u > 1) {
|
|
324
|
+
s.style.opacity = "0";
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const p = out(u);
|
|
328
|
+
s.style.width = s.style.height = "";
|
|
329
|
+
s.style.opacity = String(1 - clamp((u - 0.85) / 0.15));
|
|
330
|
+
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%)`;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const lag = it.lag ?? BASE;
|
|
334
|
+
// 첫 값과는 함께 나타나고, 그다음부터 값이 선 뒤에 따라간다
|
|
335
|
+
const stops = [...it.stops].sort((a, b) => a.at - b.at).map((st, i) => ({ ...st, at: i ? st.at + lag : st.at }));
|
|
336
|
+
if (it.hold && stops.length > 1) {
|
|
337
|
+
// 다시 훑기 — 처음 값으로 돌아가 차례대로. 작은 칸끼리 옆으로만 오가면 화면이 거의 안 바뀌어 큰 칸(첫 값)으로 크게 돌아간다. 같은 t 면 같은 차례
|
|
338
|
+
const every = it.holdEvery ?? clamp(stops[stops.length - 1].at - stops[stops.length - 2].at, 0.3, 0.6);
|
|
339
|
+
const n = stops.length;
|
|
340
|
+
const end = stops[n - 1].at;
|
|
341
|
+
for (let j = 1; end + j * every <= t + every; j++) {
|
|
342
|
+
stops.push({ ...stops[(j - 1) % n], at: end + j * every });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (!stops.length || t < stops[0].at) {
|
|
253
346
|
s.style.opacity = "0";
|
|
254
347
|
return;
|
|
255
348
|
}
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
349
|
+
const boxOf = (st: ReadStop): Box => (st.target && measure(st.target, el, st.pad ?? size * 0.25)) || { x: st.x ?? 0, y: st.y ?? 0, w: st.w ?? size * 3, h: st.h ?? size * 1.6 };
|
|
350
|
+
const k = stops.findIndex((st) => st.at > t);
|
|
351
|
+
const shown = out(progress(t, stops[0].at, BASE));
|
|
352
|
+
if (k > 0) {
|
|
353
|
+
const [a, b] = [stops[k - 1], stops[k]];
|
|
354
|
+
place(s, glide(boxOf(a), boxOf(b), clamp((t - a.at) / (b.at - a.at)), b.at - a.at), shown);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const last = boxOf(stops[stops.length - 1]);
|
|
358
|
+
const since = t - stops[stops.length - 1].at;
|
|
359
|
+
if (!it.hold) {
|
|
360
|
+
place(s, last, shown * (1 - out(progress(t, stops[stops.length - 1].at + BASE, FAST))));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const r = 36 * clamp(since / 0.2);
|
|
364
|
+
place(s, { ...last, x: last.x + r * Math.sin(since * 5.5), y: last.y - r * (1 - Math.cos(since * 5.5)) * 0.5 }, shown);
|
|
261
365
|
});
|
|
262
366
|
};
|
|
263
367
|
}
|
|
264
368
|
|
|
265
|
-
/** 샘플 점 그림 — 모델마다 재 온 값이 들어온 시각에 점으로 꽂히고, 지금까지의 가운데값 눈금·숫자가 따라
|
|
369
|
+
/** 샘플 점 그림 — 모델마다 재 온 값이 들어온 시각에 점으로 꽂히고, 지금까지의 가운데값 눈금·숫자가 300ms 로 따라 미끄러진다. 눈금·막대는 각짐 */
|
|
266
370
|
export function samplePlot(
|
|
267
371
|
el: HTMLElement,
|
|
268
372
|
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 +386,10 @@ export function samplePlot(
|
|
|
282
386
|
const rows = o.rows.map((r, i) => {
|
|
283
387
|
const top = i * rowH;
|
|
284
388
|
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;
|
|
389
|
+
const track = box(doc, `position:absolute;left:0;right:0;top:${top + trackY}px;height:10px;background:${BAR}`);
|
|
286
390
|
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:
|
|
391
|
+
const mark = box(doc, `position:absolute;top:${top + trackY - 40}px;width:8px;height:90px;margin-left:-4px;background:var(--kf-ink);opacity:0`);
|
|
392
|
+
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
393
|
wrap.append(label, track, ...dots, mark, val);
|
|
290
394
|
return { r, dots, mark, val };
|
|
291
395
|
});
|
|
@@ -295,32 +399,29 @@ export function samplePlot(
|
|
|
295
399
|
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
296
400
|
};
|
|
297
401
|
return (t: number) => {
|
|
298
|
-
applyStyle(wrap, rise(t, o.at ?? 0
|
|
402
|
+
applyStyle(wrap, rise(t, o.at ?? 0));
|
|
299
403
|
rows.forEach(({ r, dots, mark, val }, i) => {
|
|
300
404
|
const got = r.samples.filter((s) => t >= s.t);
|
|
301
405
|
r.samples.forEach((s, k) => {
|
|
302
|
-
const p = snappy(progress(t, s.t, 0.2));
|
|
303
406
|
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})`;
|
|
407
|
+
applyStyle(dots[k], t >= s.t ? { ...rise(t, s.t), opacity: 0.6 * out(progress(t, s.t, BASE)) } : { opacity: 0 });
|
|
306
408
|
});
|
|
307
409
|
if (!got.length) {
|
|
308
410
|
mark.style.opacity = "0";
|
|
309
411
|
val.style.opacity = "0";
|
|
310
412
|
return;
|
|
311
413
|
}
|
|
312
|
-
// 가운데값은 새 점이 올 때마다 그 자리로 미끄러진다 — 앞 값과 새 값을
|
|
414
|
+
// 가운데값은 새 점이 올 때마다 그 자리로 미끄러진다 — 앞 값과 새 값을 300ms(자리 옮김)로 잇는다
|
|
313
415
|
const last = got[got.length - 1];
|
|
314
416
|
const prev = got.length > 1 ? median(got.slice(0, -1).map((s) => s.v)) : last.v;
|
|
315
417
|
const now = median(got.map((s) => s.v));
|
|
316
|
-
const k =
|
|
418
|
+
const k = out(progress(t, last.t, SLOW));
|
|
317
419
|
const m = prev + (now - prev) * k;
|
|
318
420
|
mark.style.left = `${X(m)}px`;
|
|
319
421
|
mark.style.opacity = "1";
|
|
320
422
|
mark.style.background = o.best === i ? "var(--kf-accent)" : "var(--kf-ink)";
|
|
321
|
-
val
|
|
423
|
+
applyStyle(val, rise(t, got[0].t));
|
|
322
424
|
val.style.color = o.best === i ? "var(--kf-accent-text)" : "var(--kf-ink)";
|
|
323
|
-
val.style.transform = `scale(${1 + 0.15 * (1 - k)})`;
|
|
324
425
|
val.textContent = `${m.toFixed(o.decimals ?? 1)}${o.unit ?? ""}`;
|
|
325
426
|
});
|
|
326
427
|
};
|
|
@@ -333,24 +434,24 @@ export function meter(t: number, o: { to: number; at: number; dur?: number; deci
|
|
|
333
434
|
const n = target.replace(/\D/g, "").length || 1;
|
|
334
435
|
const dur = o.dur ?? 0.9;
|
|
335
436
|
let k = 0;
|
|
336
|
-
let
|
|
437
|
+
let s = "";
|
|
337
438
|
for (const ch of target) {
|
|
338
439
|
if (!/\d/.test(ch)) {
|
|
339
|
-
|
|
440
|
+
s += ch;
|
|
340
441
|
continue;
|
|
341
442
|
}
|
|
342
443
|
const settle = o.at + (dur * (k + 1)) / n;
|
|
343
|
-
|
|
444
|
+
s += t >= settle ? ch : String(Math.floor((t * 26 + k * 3.7) % 10 + 10) % 10);
|
|
344
445
|
k++;
|
|
345
446
|
}
|
|
346
|
-
return `${
|
|
447
|
+
return `${s}${o.unit ?? ""}`;
|
|
347
448
|
}
|
|
348
449
|
|
|
349
|
-
/** 막대 —
|
|
450
|
+
/** 막대 — 각진 막대가 박마다 하나씩 값만큼 300ms 로 자란다. best 만 브랜드 파랑(차트 칠), 나머지는 회색. 값은 막대 끝, delta 를 주면 그 박에 테두리 배지 */
|
|
350
451
|
export function rankBars(
|
|
351
452
|
el: HTMLElement,
|
|
352
453
|
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 {
|
|
454
|
+
): ((t: number) => void) & { vals: HTMLElement[]; deltas: (HTMLElement | null)[] } {
|
|
354
455
|
const doc = el.ownerDocument;
|
|
355
456
|
const barH = o.barH ?? 76;
|
|
356
457
|
const size = o.size ?? 38;
|
|
@@ -363,16 +464,16 @@ export function rankBars(
|
|
|
363
464
|
const top = i * step;
|
|
364
465
|
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
466
|
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:
|
|
467
|
+
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
468
|
const delta = it.delta
|
|
368
|
-
? box(doc, `position:absolute;top:${top + size * 1.4 + barH / 2}px;padding:
|
|
469
|
+
? 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
470
|
: null;
|
|
370
471
|
wrap.append(label, bar, val, ...(delta ? [delta] : []));
|
|
371
472
|
return { it, label, bar, val, delta };
|
|
372
473
|
});
|
|
373
|
-
|
|
474
|
+
const paint = (t: number) => {
|
|
374
475
|
rows.forEach(({ it, label, bar, val, delta }) => {
|
|
375
|
-
const p =
|
|
476
|
+
const p = out(progress(t, it.at, SLOW));
|
|
376
477
|
const w = (it.value / o.max) * (o.w - 440);
|
|
377
478
|
applyStyle(label, rise(t, it.at));
|
|
378
479
|
bar.style.width = `${w}px`;
|
|
@@ -382,51 +483,54 @@ export function rankBars(
|
|
|
382
483
|
val.textContent = `${o.prefix ?? ""}${it.value.toFixed(o.decimals ?? 1)}${o.unit ?? ""}`;
|
|
383
484
|
if (delta) {
|
|
384
485
|
const at = it.deltaAt ?? it.at + 0.35;
|
|
385
|
-
const d = snappy(progress(t, at, 0.22));
|
|
386
486
|
delta.style.left = `${w + 24 + size * 6}px`;
|
|
387
|
-
delta
|
|
388
|
-
delta.style.
|
|
487
|
+
applyStyle(delta, t >= at ? rise(t, at) : { opacity: 0 });
|
|
488
|
+
delta.style.translate = "0 -50%";
|
|
389
489
|
}
|
|
390
490
|
});
|
|
391
491
|
};
|
|
492
|
+
return Object.assign(paint, { vals: rows.map((r) => r.val), deltas: rows.map((r) => r.delta) });
|
|
392
493
|
}
|
|
393
494
|
|
|
394
|
-
/**
|
|
395
|
-
|
|
495
|
+
/**
|
|
496
|
+
* 끝 도장 — 구분선 한 줄 위에 OG 로고와 「측정 날짜 · 호출 N번 · $비용 · 키 하나」가 선다(상자 없이). 비용은 반올림해 0 으로 만들지 않고 첫 유효 자리까지.
|
|
497
|
+
* logo 는 마스터 로고 SVG 원문(currentColor) — 한 색으로 칠한다. 어두운 바탕 흰색·밝은 바탕 검정(logoInk)
|
|
498
|
+
*/
|
|
499
|
+
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
500
|
const doc = el.ownerDocument;
|
|
397
501
|
const size = o.size ?? 36;
|
|
398
|
-
const
|
|
502
|
+
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
503
|
if (o.logo) {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
card.append(img);
|
|
504
|
+
const mark = box(doc, `height:${size * 1.25}px;width:${(size * 1.25 * 1140) / 650}px;color:${o.logoInk ?? "var(--kf-ink)"};display:flex`);
|
|
505
|
+
mark.innerHTML = fitSvg(o.logo);
|
|
506
|
+
row.append(mark);
|
|
404
507
|
}
|
|
405
508
|
const digits = o.cost > 0 ? Math.max(2, -Math.floor(Math.log10(o.cost))) : 2;
|
|
406
|
-
|
|
407
|
-
el.append(
|
|
509
|
+
row.append(box(doc, "", `측정 ${o.date} · 호출 ${o.calls}번 · $${o.cost.toFixed(digits)} · 키 하나`));
|
|
510
|
+
el.append(row);
|
|
408
511
|
return (t: number) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
card.style.transform = `translateX(-50%) translateY(${20 * (1 - p)}px) scale(${1.25 - 0.25 * p})`;
|
|
512
|
+
applyStyle(row, rise(t, o.at));
|
|
513
|
+
row.style.translate = "-50% 0";
|
|
412
514
|
};
|
|
413
515
|
}
|
|
414
516
|
|
|
415
|
-
/**
|
|
416
|
-
|
|
517
|
+
/**
|
|
518
|
+
* 배지 — 짧은 낱말·모델 ID·잰 값 하나(「실측」·「openai/ 41」). 속을 칠하지 않고 흐린 테두리와 글자색만(badge.tsx 의 default·outline·secondary).
|
|
519
|
+
* 6px 올라오며 220ms 로 나타나고 until 에 150ms 로 빠진다
|
|
520
|
+
*/
|
|
521
|
+
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
522
|
const doc = el.ownerDocument;
|
|
418
|
-
const
|
|
523
|
+
const size = o.size ?? 38;
|
|
419
524
|
const chip = box(
|
|
420
525
|
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)"),
|
|
526
|
+
`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
527
|
o.text,
|
|
424
528
|
);
|
|
425
529
|
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})`;
|
|
530
|
+
const paint = (t: number) => {
|
|
531
|
+
const gone = o.until === undefined ? 0 : out(progress(t, o.until, FAST));
|
|
532
|
+
const s = rise(t, o.at);
|
|
533
|
+
applyStyle(chip, t >= o.at ? { ...s, opacity: (s.opacity ?? 1) * (1 - gone) } : { opacity: 0 });
|
|
431
534
|
};
|
|
535
|
+
return Object.assign(paint, { el: chip });
|
|
432
536
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1140 650" width="1140" height="650" fill="none" role="img" aria-label="OpenGateway">
|
|
2
|
+
<title>OpenGateway</title>
|
|
3
|
+
<g id="logo" stroke="currentColor" fill="currentColor">
|
|
4
|
+
<rect id="node-in" x="36" y="36" width="420" height="578" rx="89" fill="none" stroke-width="72"/>
|
|
5
|
+
<rect id="bar-in" x="72" y="272" width="277" height="106" stroke="none"/>
|
|
6
|
+
<path id="node-out" d="M684 213 L684 125 A89 89 0 0 1 773 36 L1015 36 A89 89 0 0 1 1104 125 L1104 525 A89 89 0 0 1 1015 614 L773 614 A89 89 0 0 1 684 525 L684 436" fill="none" stroke-width="72" stroke-linecap="butt"/>
|
|
7
|
+
<g id="arrow">
|
|
8
|
+
<rect id="arrow-shaft" x="420" y="272" width="394" height="106" stroke="none"/>
|
|
9
|
+
<path id="arrow-head" d="M833 220 L970 325 L833 430 Z" stroke-width="38" stroke-linejoin="round"/>
|
|
10
|
+
</g>
|
|
6
11
|
</g>
|
|
7
|
-
|
|
8
|
-
<g id="node-out">
|
|
9
|
-
<path d="M1015,650h-242c-68.9,0-125-56.1-125-125v-89h72v89c0,29.2,23.8,53,53,53h242c29.2,0,53-23.8,53-53V125c0-29.2-23.8-53-53-53h-242c-29.2,0-53,23.8-53,53v88h-72v-88c0-68.9,56.1-125,125-125h242c68.9,0,125,56.1,125,125v400c0,68.9-56.1,125-125,125Z"/>
|
|
10
|
-
</g>
|
|
11
|
-
<path d="M981.6,309.9l-137-105c-5.7-4.4-13.5-5.2-20-2-6.5,3.2-10.6,9.8-10.6,17v52h-394v106h394v52c0,7.2,4.1,13.8,10.6,17,2.7,1.3,5.5,2,8.4,2,4.1,0,8.2-1.3,11.6-3.9l137-105c4.7-3.6,7.4-9.2,7.4-15.1s-2.8-11.5-7.4-15.1Z"/>
|
|
12
|
-
</svg>
|
|
12
|
+
</svg>
|