kasaframe 0.1.5 → 0.1.7
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 +13 -0
- package/README.md +2 -2
- package/docs/AGENTS.md +1 -0
- 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/direct.ts +24 -4
- package/packages/cli/src/commands/make.ts +15 -5
- package/packages/cli/src/commands/render.ts +34 -7
- 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/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 +15 -6
- package/packages/director/src/fallback.ts +14 -2
- package/packages/director/src/index.ts +1 -1
- package/packages/director/src/pattern.ts +2 -0
- package/packages/director/src/patterns/fill/cells.ts +5 -2
- package/packages/director/src/scene.ts +17 -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 +14 -8
- 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 +364 -59
- package/packages/patterns/src/limit/gauge.ts +19 -13
- package/packages/patterns/src/quote/karaoke.ts +17 -12
- package/packages/patterns/src/sfx.ts +80 -0
- package/packages/patterns/src/steps/path.ts +15 -7
- package/packages/patterns/src/term-reveal/chips.ts +24 -10
- package/packages/patterns/src/term-reveal/expand.ts +17 -13
- package/packages/patterns/src/timeline/axis.ts +79 -25
- package/packages/patterns/src/title/mask.ts +15 -10
- package/packages/patterns/src/title/slam.ts +1 -1
- package/packages/patterns/src/types.ts +1 -0
- package/packages/plan/src/brief.ts +5 -2
- package/packages/themes/src/index.ts +22 -0
- package/packages/themes/src/slots.ts +32 -2
- package/packages/themes/src/types.ts +28 -0
- 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/themes/kawaii-sticker/effects.ts +15 -3
- package/themes/kawaii-sticker/theme.json +1 -0
- package/themes/og-measure/THEME.md +13 -2
- package/themes/og-measure/effects.ts +76 -59
- package/themes/og-measure/sample/scenes/end.ts +2 -2
- package/themes/og-measure/sample/scenes/gather.ts +2 -10
- package/themes/og-measure/sample/scenes/models.ts +2 -10
- package/themes/og-measure/sample/scenes/one.ts +2 -1
- package/themes/og-measure/sample/scenes/price.ts +4 -16
- package/themes/og-measure/slots.ts +29 -0
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +3 -0
- package/themes/og-measure/theme.json +15 -1
- package/themes/og-measure/variants/snow-blue.json +3 -0
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
## 변주 — 브랜드를 지키는 축만
|
|
30
30
|
|
|
31
|
-
- 강조 파랑은 돌지 않는다(`vary.keep` — accent·accentText·accentSoft·onAccent·tint·의미색). `hue`·`chroma`·`tint` 축이 없다.
|
|
31
|
+
- 강조 파랑은 돌지 않는다(`vary.keep` — accent·accentText·accentSoft·onAccent·tint·칠 토큰 fill·onFill·fillLine·의미색). `hue`·`chroma`·`tint` 축이 없다.
|
|
32
32
|
- 밝기 둘: 다크(세계 기본)·라이트(`vary.palettes.light` — 라이트 판은 다크를 뒤집어 옮기지 않고 정본 라이트 한 벌에서 시작한다. 두 벌이 거울상이 아니라서 옮기면 정본 값이 안 나온다).
|
|
33
33
|
- 바탕은 정본 면 색 사이만(`vary.bg` — 다크 `--background`~`--card`, 라이트 `--card`~`--background`). 그 위의 면·선·흐린 글은 바탕을 따라 자리를 지킨다.
|
|
34
34
|
- 글꼴·모서리·빠르기·질감·선 굵기는 정본이 하나뿐이라 돌지 않는다(스타일가이드의 한글 글꼴 다섯 후보는 「비교용, 고르기 전엔 전역 글꼴을 안 바꾼다」).
|
|
@@ -70,4 +70,15 @@ export default {
|
|
|
70
70
|
- 원자료 로그·축 눈금은 코드 화면이라 글 덩어리가 많다 — 견본은 `kf.json` 의 `text.maxOnScreen` 을 로그 줄·눈금·값을 센 실제 수(30)로 두었다. 로그·차트 밖 글은 셋을 넘기지 않는다.
|
|
71
71
|
- 결과가 서는 순간은 드롭에(`meter` + `trackCam`), 끝 도장은 마지막 마디에.
|
|
72
72
|
- 배지 tone 은 뜻으로 고른다 — 이 장면의 핵심 사실 하나만 accent(파랑 테두리), 나머지 이름·주소는 plain, 덜 중요한 곁말은 soft.
|
|
73
|
-
-
|
|
73
|
+
- 읽는 칸(`pickFly` 의 `stops`)은 **장면의 요점에만** — 주인공 값, 그 장면의 핵심 사실(accent 배지), 그 순간 자막이 말하는 값. 나타나는 항목(공급자 칩·로그 줄·가격 줄)을 박마다 차례로 짚지 않고, 한 번 내려앉으면 다음 요점까지 머문다(다시 훑기 `hold` 를 쓰지 않는다). 칸은 글 뒤에 깔린다 — 옮겨 가는 동안은 아래 테두리 한 줄로 얇아져 지나가고, 값에 닿으면 그 값의 칸만큼 차올라 다음 요점까지 딱 맞게 선다. 배지는 `pad` 없이(배지 테두리에 겹친다), 글은 `pad` 로 둘레를 준다. 도장 줄·로고처럼 지나가면 안 되는 것이 사이에 있으면 정지점을 그 위쪽에서만 고른다(견본 끝 장면).
|
|
74
|
+
- **흐름은 내용이 진다** — 화면을 꽉 채우고(글 56–64px, 화면 전체), 박마다 주소 조각·카운터·로그 한 줄·날아가는 숫자(`pickFly`)·막대·배지 중 하나가 나타나거나 자라 같은 구도가 1.5초를 넘지 않게. 읽는 칸으로 화면을 메우지 않는다 — 1초 안쪽의 쉼은 괜찮다(멈춤 기준 1초). 기다리는 동안도 카운터가 실제 속도로 구른다. `trackCam` 샷은 드러냄·결과가 서는 순간에만 — 구도를 메우려고 샷을 박지 않는다(샷 사이 카메라는 선다).
|
|
75
|
+
|
|
76
|
+
## 감독 패턴에서
|
|
77
|
+
|
|
78
|
+
감독(`kf direct`)의 장면 문법 패턴은 효과 대신 슬롯(`slots.ts`)으로 이 세계를 입는다 — 바탕 모눈(`surface`), 위 구분선 판(`panel`), 6px 앉기(`land`), 계기판 글 찍기(`write`), 3px 추이 선(`trace`), 파란 구분선 드롭(`drop`), 그리고 짚는 칸(`mark`).
|
|
79
|
+
|
|
80
|
+
- **짚는 칸은 고른 상태 알약** — 파랑/10 칠에 파랑/45 테두리, 글 뒤에 깔리고 칸 위 글은 `--on-primary` 파랑. 꽉 칠한 강조색 네모는 서지 않는다(4절). 장면의 요점(주인공 값·그 순간 목소리가 말하는 값)에만 내려앉아 머물고, 다음 요점이 생기면 읽는 칸(`pickFly`)과 같은 길로 아래 테두리 한 줄로 내려앉아 미끄러진 뒤 그 칸만큼 차오른다. 짚을 것이 하나뿐이면 칸이 없다.
|
|
81
|
+
- **칠한 칸도 고른 상태 결** — 진행·점유·지금(차오르는 단계 카드·한계 계기·GPU 칸·시간축 지난 길과 읽음값)은 칠 토큰(`fill`·`onFill`·`fillLine`)으로 파랑/10 면에 파랑/45 안쪽 테두리, 그 위 글은 `--on-primary` 파랑. 「찼다」는 뜻은 남고 꽉 찬 강조색 면은 없다.
|
|
82
|
+
- **시간축 표시는 눈금선** — 축을 가로지르는 가는 눈금이 딱지까지 이어지고(지나면 파랑), 딱지·구간 이름은 오른쪽 한 줄에 겹치지 않게 쌓인다. 글은 눕히지 않는다.
|
|
83
|
+
- **멈춤 오류 기준 1초**(`theme.json` `motion.maxFreezeSec`) — 화면을 살려 두려고 칸이 박마다 항목을 훑지 않는다. 0.1초 넘는 멈춤은 kf check 경고로 보이고(틀 기준), 오류·감독 다시 고르기는 1초부터. 쉬는 구간은 내용이 메운다 — 항목은 목소리가 그것을 말하는 순간에 서고(숫자·계기는 그때까지 구르고 차오른다), 그 사이 카메라가 방금 말한 항목 쪽으로 천천히 다가간다(바탕은 흐르지 않는다).
|
|
84
|
+
- **장면 사이는 컷과 짧은 페이드만**(`theme.json` `transitions`: `none`·`fade`) — 육각 조리개·화면을 쓰는 큰 면·흐린 원 같은 면 전환과 3D 이음매는 쓰지 않는다. 페이드는 바탕색으로 경계 양쪽 0.15초씩.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyStyle, bezier, 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
|
// 비유: 계측기 화면. 실제 호출이 한 줄 나가고, 원자료 로그가 흐르고, 거기서 숫자가 뽑혀 점·막대·큰 숫자로 쌓인다.
|
|
@@ -16,7 +16,9 @@ const RISE = 6;
|
|
|
16
16
|
const out = ease.snappy;
|
|
17
17
|
const springy = bezier(0.34, 1.3, 0.64, 1);
|
|
18
18
|
|
|
19
|
-
|
|
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" });
|
|
20
22
|
const p = out(progress(t, at, dur));
|
|
21
23
|
return { opacity: p, y: px * (1 - p) };
|
|
22
24
|
}
|
|
@@ -86,6 +88,7 @@ export function trackCam(t: number, o: { shots: CamShot[]; w?: number; h?: numbe
|
|
|
86
88
|
for (let k = 0; k < shots.length; k++) if (shots[k].at <= t) i = k;
|
|
87
89
|
let f = frame(i);
|
|
88
90
|
const since = t - shots[i].at;
|
|
91
|
+
if (i > 0) cue("whoosh", since, { source: `trackCam@${shots[i].at.toFixed(3)}`, scene: o.scene });
|
|
89
92
|
if (i > 0 && since < SLOW) {
|
|
90
93
|
const p = springy(since / SLOW);
|
|
91
94
|
const a = frame(i - 1);
|
|
@@ -229,7 +232,7 @@ export function rawLog(
|
|
|
229
232
|
shown = i + 1;
|
|
230
233
|
slide = out(progress(t, at, BASE));
|
|
231
234
|
}
|
|
232
|
-
applyStyle(items[i].row, t >= at ? rise(t, at, BASE, 0) : { opacity: 0 });
|
|
235
|
+
applyStyle(items[i].row, t >= at ? rise(t, at, BASE, 0, "type") : { opacity: 0 });
|
|
233
236
|
const n = items[i].num;
|
|
234
237
|
if (n) {
|
|
235
238
|
const on = t >= at;
|
|
@@ -244,20 +247,23 @@ export function rawLog(
|
|
|
244
247
|
}
|
|
245
248
|
|
|
246
249
|
type Pt = { x: number; y: number };
|
|
247
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* 읽는 칸이 닿을 자리 — 무대 좌표 가운데(x·y)와 크기, 아니면 target 요소(효과가 돌려준 .el)를 그 자리에서 잰다. 요소 여럿이면 그 전부를 덮는 한 칸.
|
|
252
|
+
* pad 는 글 둘레 여백(배지는 0 — 배지 테두리에 딱 맞는다)
|
|
253
|
+
*/
|
|
248
254
|
export interface ReadStop {
|
|
249
255
|
at: number;
|
|
250
256
|
x?: number;
|
|
251
257
|
y?: number;
|
|
252
258
|
w?: number;
|
|
253
259
|
h?: number;
|
|
254
|
-
target?: HTMLElement | null;
|
|
260
|
+
target?: HTMLElement | null | (HTMLElement | null)[];
|
|
255
261
|
pad?: number;
|
|
256
262
|
}
|
|
257
263
|
type Box = Pt & { w: number; h: number };
|
|
258
264
|
|
|
259
265
|
// SlidingPillNav 처럼 offsetTop·offsetLeft 를 무대까지 더해 잰다 — 나타남의 6px 올라옴(transform)은 자리에 넣지 않는다
|
|
260
|
-
function measure(target: HTMLElement, stage: HTMLElement
|
|
266
|
+
function measure(target: HTMLElement, stage: HTMLElement): { l: number; t: number; r: number; b: number } | null {
|
|
261
267
|
let x = 0;
|
|
262
268
|
let y = 0;
|
|
263
269
|
let n: HTMLElement | null = target;
|
|
@@ -266,49 +272,70 @@ function measure(target: HTMLElement, stage: HTMLElement, pad: number): Box | nu
|
|
|
266
272
|
y += n.offsetTop;
|
|
267
273
|
}
|
|
268
274
|
if (n !== stage || !target.offsetWidth) return null;
|
|
269
|
-
return {
|
|
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";
|
|
270
312
|
}
|
|
271
313
|
|
|
272
314
|
/**
|
|
273
315
|
* 뽑힌 숫자·읽는 칸 — 고름 칠(파랑 /20, 정본의 bg-primary/20) 알약에 스타일가이드의 미끄러지는 알약 테두리(SlidingPillNav, 화면 글 크기에 맞춰 파랑 /55)를 두른 한 장.
|
|
274
316
|
* from·to 를 주면 로그 줄의 숫자가 알약째 떠올라 호를 그리며 점 그림 자리로 300ms 감속으로 날아가 꽂힌다(박마다 큰 움직임).
|
|
275
|
-
* stops 를 주면
|
|
276
|
-
*
|
|
317
|
+
* stops 를 주면 요점(주인공 값·핵심 사실·그 순간 자막이 말하는 값)이 바뀔 때만 그 값으로 옮겨 가 짚는다 — 메뉴의 알약처럼 글 뒤에 깔리고,
|
|
318
|
+
* 옮겨 가는 동안은 아래 테두리 한 줄로 얇아져 지나가며, 닿으면 그 값의 칸만큼 차올라 다음 요점까지 딱 맞게 선다(옮김 300ms 안).
|
|
319
|
+
* 나타나는 항목을 차례로 훑는 정지점은 두지 않는다 — 화면을 살려 두는 몫은 내용이 진다
|
|
277
320
|
*/
|
|
278
321
|
export function pickFly(
|
|
279
322
|
el: HTMLElement,
|
|
280
323
|
o: {
|
|
281
|
-
items: ({ at: number; text: string; from: Pt; to: Pt } | { text?: string; stops: ReadStop[]
|
|
324
|
+
items: ({ at: number; text: string; from: Pt; to: Pt } | { text?: string; stops: ReadStop[] })[];
|
|
282
325
|
dur?: number;
|
|
283
326
|
size?: number;
|
|
284
|
-
/** 읽는 칸의 최소 빠르기(px/초) */
|
|
285
|
-
minSpeed?: number;
|
|
286
327
|
},
|
|
287
328
|
): (t: number) => void {
|
|
288
329
|
const doc = el.ownerDocument;
|
|
289
330
|
const dur = o.dur ?? SLOW;
|
|
290
331
|
const size = o.size ?? 44;
|
|
291
|
-
const
|
|
332
|
+
const border = Math.max(3, Math.round(size * 0.08));
|
|
333
|
+
let blend = "";
|
|
292
334
|
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:${
|
|
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 ?? "");
|
|
294
336
|
el.append(s);
|
|
295
337
|
return s;
|
|
296
338
|
});
|
|
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
339
|
const place = (s: HTMLElement, p: Box, opacity: number) => {
|
|
313
340
|
s.style.opacity = String(opacity);
|
|
314
341
|
s.style.width = `${p.w.toFixed(1)}px`;
|
|
@@ -320,6 +347,7 @@ export function pickFly(
|
|
|
320
347
|
const s = pills[i];
|
|
321
348
|
if (!("stops" in it)) {
|
|
322
349
|
const u = (t - it.at) / dur;
|
|
350
|
+
cue("land", t - it.at - dur, { source: `pickFly@${it.at.toFixed(3)}` });
|
|
323
351
|
if (u < 0 || u > 1) {
|
|
324
352
|
s.style.opacity = "0";
|
|
325
353
|
return;
|
|
@@ -330,38 +358,26 @@ export function pickFly(
|
|
|
330
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%)`;
|
|
331
359
|
return;
|
|
332
360
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const stops = [...it.stops].sort((a, b) => a.at - b.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
|
-
}
|
|
361
|
+
blend ||= underText(el);
|
|
362
|
+
s.style.mixBlendMode = blend;
|
|
363
|
+
const stops = [...it.stops].sort((a, b) => a.at - b.at);
|
|
345
364
|
if (!stops.length || t < stops[0].at) {
|
|
346
365
|
s.style.opacity = "0";
|
|
347
366
|
return;
|
|
348
367
|
}
|
|
349
|
-
const boxOf = (st: ReadStop): Box => (st.target &&
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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);
|
|
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)));
|
|
365
381
|
});
|
|
366
382
|
};
|
|
367
383
|
}
|
|
@@ -404,7 +420,7 @@ export function samplePlot(
|
|
|
404
420
|
const got = r.samples.filter((s) => t >= s.t);
|
|
405
421
|
r.samples.forEach((s, k) => {
|
|
406
422
|
dots[k].style.left = `${X(s.v)}px`;
|
|
407
|
-
applyStyle(dots[k], t >= s.t ? { ...rise(t, s.t), opacity: 0.6 * out(progress(t, s.t, BASE)) } : { opacity: 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 });
|
|
408
424
|
});
|
|
409
425
|
if (!got.length) {
|
|
410
426
|
mark.style.opacity = "0";
|
|
@@ -441,6 +457,7 @@ export function meter(t: number, o: { to: number; at: number; dur?: number; deci
|
|
|
441
457
|
continue;
|
|
442
458
|
}
|
|
443
459
|
const settle = o.at + (dur * (k + 1)) / n;
|
|
460
|
+
cue("tick", t - settle, { source: `meter@${settle.toFixed(3)}`, minor: true });
|
|
444
461
|
s += t >= settle ? ch : String(Math.floor((t * 26 + k * 3.7) % 10 + 10) % 10);
|
|
445
462
|
k++;
|
|
446
463
|
}
|
|
@@ -509,7 +526,7 @@ export function stamp(el: HTMLElement, o: { x: number; y: number; at: number; da
|
|
|
509
526
|
row.append(box(doc, "", `측정 ${o.date} · 호출 ${o.calls}번 · $${o.cost.toFixed(digits)} · 키 하나`));
|
|
510
527
|
el.append(row);
|
|
511
528
|
return (t: number) => {
|
|
512
|
-
applyStyle(row, rise(t, o.at));
|
|
529
|
+
applyStyle(row, rise(t, o.at, BASE, RISE, "end"));
|
|
513
530
|
row.style.translate = "-50% 0";
|
|
514
531
|
};
|
|
515
532
|
}
|
|
@@ -27,8 +27,8 @@ export default {
|
|
|
27
27
|
mark.innerHTML = fitSvg(logo);
|
|
28
28
|
stage.append(mark);
|
|
29
29
|
const site = flag(stage, { x: 600, y: 830, text: "opengateway.ai", at: B(3), tone: "plain", size: 80, mono: true });
|
|
30
|
-
//
|
|
31
|
-
const read = pickFly(stage, { items: [{ stops: [{ at:
|
|
30
|
+
// 읽는 칸 — 잰 값 하나에 내려앉아 머문다(도장 줄·로고로는 내려가지 않는다)
|
|
31
|
+
const read = pickFly(stage, { items: [{ stops: [{ at: B(0) + 0.2, target: num, pad: 8 }] }], size: 40 });
|
|
32
32
|
const shots = [
|
|
33
33
|
{ at: 0, x: 960, y: 540, kick: 0.08 },
|
|
34
34
|
{ at: B(2), x: 960, y: 560, zoom: 1.02, kick: 0.05 },
|
|
@@ -35,16 +35,8 @@ export default {
|
|
|
35
35
|
size: 52,
|
|
36
36
|
});
|
|
37
37
|
const card = callLine(stage, { x: 160, y: 430, w: 1600, at: B(6) + 0.2, size: 84, url: [OG.host, "/v1"], urlAt: [B(6) + 0.24, B(7)] });
|
|
38
|
-
// 읽는 칸 —
|
|
39
|
-
const
|
|
40
|
-
const seen = new Set<number>();
|
|
41
|
-
const reads = OG.owners.map((_, i) => {
|
|
42
|
-
const at = i === 0 ? 0 : B(AT[i]);
|
|
43
|
-
const t = seen.has(at) ? at + half : at;
|
|
44
|
-
seen.add(at);
|
|
45
|
-
return { at: t, target: chips[i].el };
|
|
46
|
-
});
|
|
47
|
-
const read = pickFly(stage, { items: [{ stops: [...reads, { at: B(6) + 0.24, target: card.parts[0] }, { at: B(7), target: card.parts[1] }], hold: true }], size: SIZE });
|
|
38
|
+
// 읽는 칸 — 공급자 칩을 차례로 짚지 않는다(나타나는 차례일 뿐 짚을 뜻이 없다). 칩이 모여 주소 한 줄이 다 붙으면 그 요점에 내려앉아 머문다
|
|
39
|
+
const read = pickFly(stage, { items: [{ stops: [{ at: B(7), target: card.parts, pad: 12 }] }], size: SIZE });
|
|
48
40
|
const shots = [
|
|
49
41
|
{ at: 0, x: 960, y: 540 },
|
|
50
42
|
{ at: B(4), x: 920, y: 530, zoom: 1.04, kick: 0.04 },
|
|
@@ -16,17 +16,9 @@ export default {
|
|
|
16
16
|
const lines = OG.requests.map(([path, model]) => `POST ${path.padEnd(24)} ${model}`);
|
|
17
17
|
const times = lines.map((_, i) => (i < 8 ? B(i) : B(8 + Math.floor((i - 8) / 2)) + ((i - 8) % 2) * half));
|
|
18
18
|
const log = rawLog(stage, { x: 90, y: 230, w: 1740, rows: 9, lines, times, pick: /([a-z-]+\/[\w.-]+)$/i, size: 40 });
|
|
19
|
-
// 읽는 칸 — 새로 든 줄 하나를 통째로 짚는다(표 행의 고름 칠). 줄 칸은 rawLog 와 같은 셈(왼쪽 90 + 안쪽 28, 줄 높이 60)
|
|
20
|
-
const read = pickFly(stage, {
|
|
21
|
-
items: [{
|
|
22
|
-
stops: OG.requests.map((_, i) => ({ at: times[i], x: 960, y: 230 + 28 + Math.min(i, 8) * 60 + 30, w: 1740 - 28, h: 58 })),
|
|
23
|
-
hold: true,
|
|
24
|
-
// 줄은 박에 바로 든다 — 기다리지 않고 함께 닿는다
|
|
25
|
-
lag: 0,
|
|
26
|
-
}],
|
|
27
|
-
size: 40,
|
|
28
|
-
});
|
|
29
19
|
const one = flag(stage, { x: 90, y: 920, text: "주소 1개", at: B(4), tone: "accent", size: 48 });
|
|
20
|
+
// 읽는 칸 — 이 장면의 요점(모델이 바뀌어도 주소는 하나)에만 내려앉아 머문다. 줄이 들 때마다 훑지 않는다 — 줄 흐름이 화면을 움직인다
|
|
21
|
+
const read = pickFly(stage, { items: [{ stops: [{ at: B(4), target: one.el }] }], size: 40 });
|
|
30
22
|
const shots = [
|
|
31
23
|
{ at: 0, x: 960, y: 540, kick: 0.05 },
|
|
32
24
|
{ at: B(4), x: 940, y: 530, zoom: 1.03, kick: 0.03 },
|
|
@@ -23,7 +23,8 @@ export default {
|
|
|
23
23
|
const host = flag(stage, { x: 100, y: 660, text: `https://${OG.host}/v1`, at: B(1), tone: "accent", size: 80, mono: true });
|
|
24
24
|
const a = flag(stage, { x: 100, y: 840, text: "OpenAI 형식", at: B(2), tone: "plain", size: 60 });
|
|
25
25
|
const c = flag(stage, { x: 560, y: 840, text: "Anthropic 형식", at: B(3), tone: "plain", size: 60 });
|
|
26
|
-
|
|
26
|
+
// 읽는 칸 — 큰 숫자에 앉았다가 이 장면의 핵심 사실(accent 주소 하나)이 서면 그리로 옮겨 머문다. 형식 배지는 곁말이라 짚지 않는다
|
|
27
|
+
const read = pickFly(stage, { items: [{ stops: [{ at: 0, target: num, pad: 10 }, { at: B(1), target: host.el }] }], size: 60 });
|
|
27
28
|
const shots = [
|
|
28
29
|
{ at: 0, x: 960, y: 540, kick: 0.06 },
|
|
29
30
|
{ at: B(2), x: 950, y: 560, zoom: 1.02, kick: 0.03 },
|
|
@@ -36,22 +36,10 @@ export default {
|
|
|
36
36
|
});
|
|
37
37
|
const glm = flag(stage, { x: 90, y: 840, text: `${P.glm} · 밤 −70%`, at: B(8), tone: "plain", size: 44, mono: true });
|
|
38
38
|
const auto = flag(stage, { x: 90, y: 950, text: "신청 없이 · 같은 모델 ID", at: B(9), tone: "soft", size: 44 });
|
|
39
|
-
// 읽는 칸 —
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ at: 0, target: model.el },
|
|
44
|
-
{ at: B(1), target: win.el },
|
|
45
|
-
...[2, 3, 5, 6].map((k, i) => ({ at: B(k), x: 960, y: 190 + i * (70 + 26 + 56) + 63, w: 1780, h: 140 })),
|
|
46
|
-
{ at: B(8), target: glm.el },
|
|
47
|
-
{ at: B(9), target: auto.el },
|
|
48
|
-
],
|
|
49
|
-
hold: true,
|
|
50
|
-
// 배지·막대는 박에 바로 선다 — 기다리지 않고 함께 닿는다
|
|
51
|
-
lag: 0,
|
|
52
|
-
}],
|
|
53
|
-
size: 44,
|
|
54
|
-
});
|
|
39
|
+
// 읽는 칸 — 요점에만: 할인 시간대(accent 배지) → 출력 할인 값에 차이 배지가 붙을 때 그 줄 → 입력 할인 값 줄. 정가 줄·곁말 배지는 짚지 않는다.
|
|
40
|
+
// 줄 칸은 이름·막대 한 줄을 통째로(표 행의 고름 칠, rankBars 와 같은 셈)
|
|
41
|
+
const row = (k: number) => ({ x: 960, y: 190 + k * (70 + 26 + 56) + 63, w: 1780, h: 140 });
|
|
42
|
+
const read = pickFly(stage, { items: [{ stops: [{ at: B(1), target: win.el }, { at: B(4), ...row(1) }, { at: B(7), ...row(3) }] }], size: 44 });
|
|
55
43
|
const shots = [
|
|
56
44
|
{ at: 0, x: 960, y: 540, kick: 0.05 },
|
|
57
45
|
{ at: B(4), x: 940, y: 530, zoom: 1.03, kick: 0.03 },
|
|
@@ -7,6 +7,9 @@ import { layer, letters, partial, shownCount, strokePath, traceCanvas } from "@k
|
|
|
7
7
|
// 판은 상자가 아니라 구분선(design.md 2절), 움직임은 220ms 감속·6px 나타남과 300ms 자리 옮김뿐(8절). 색은 모두 --kf-* 토큰
|
|
8
8
|
const out = ease.snappy;
|
|
9
9
|
const springy = bezier(0.34, 1.3, 0.64, 1);
|
|
10
|
+
const smooth = (p: number) => p * p * (3 - 2 * p);
|
|
11
|
+
// 짚는 칸 테두리 — 옮겨 가는 동안은 이 두 배 굵기의 한 줄이 된다
|
|
12
|
+
const BORDER = 3;
|
|
10
13
|
|
|
11
14
|
export const slots: ThemeSlots = {
|
|
12
15
|
surface(el) {
|
|
@@ -67,6 +70,32 @@ export const slots: ThemeSlots = {
|
|
|
67
70
|
};
|
|
68
71
|
},
|
|
69
72
|
|
|
73
|
+
// 짚는 칸 — 고른 상태 그대로 파랑/10 알약에 파랑/45 테두리(design.md 4절 — 칠한 배지는 없다). 글 뒤에 깔리고 칸 위 글은 --on-primary 파랑.
|
|
74
|
+
// 다음 요점으로 옮길 땐 pickFly 읽는 칸과 같은 길 — 떠나는 칸에서 아래 테두리 한 줄로 내려앉고, 거리만큼 미끄러진 뒤, 그 칸만큼 차올라 선다
|
|
75
|
+
// (칸 크기 그대로 날면 지나는 길의 글을 덮는다). 처음 짚을 땐 그 자리 아래 줄에서 차오른다
|
|
76
|
+
mark(el) {
|
|
77
|
+
const line = BORDER * 2;
|
|
78
|
+
el.style.boxSizing = "border-box";
|
|
79
|
+
el.style.borderRadius = "var(--kf-radius-pill)";
|
|
80
|
+
el.style.border = `${BORDER}px solid color-mix(in srgb, var(--kf-accent) 45%, transparent)`;
|
|
81
|
+
return {
|
|
82
|
+
ink: "var(--kf-accent-text)",
|
|
83
|
+
dwell: true,
|
|
84
|
+
at(from, to, u) {
|
|
85
|
+
const a = from ?? to;
|
|
86
|
+
// 미끄러지는 몫은 거리만큼 — 600px 넘게 가면 옮김의 40%, 같은 칸을 다시 짚으면 0
|
|
87
|
+
const sink = from ? (1 - 0.4 * clamp(Math.hypot(to.x - a.x, to.y + to.h - a.y - a.h) / 600)) / 2 : 0;
|
|
88
|
+
const go = !from ? 1 : sink < 0.5 ? smooth(clamp((u - sink) / (1 - 2 * sink))) : u < 0.5 ? 0 : 1;
|
|
89
|
+
const full = !from ? clamp(u) : u < 0.5 ? 1 - clamp(u / sink) : clamp((u - 1 + sink) / sink);
|
|
90
|
+
const h = line + ((from && u < 0.5 ? a.h : to.h) - line) * full;
|
|
91
|
+
const bottom = a.y + a.h + (to.y + to.h - a.y - a.h) * go;
|
|
92
|
+
// 얇을수록 진한 파랑 — 탭 아래 줄처럼 얇아도 지나가는 게 보이게, 다 차오르면 고른 상태 /10
|
|
93
|
+
el.style.background = `color-mix(in srgb, var(--kf-accent) ${Math.round(10 + 80 * clamp(1 - (h - line) / 40))}%, transparent)`;
|
|
94
|
+
return u >= 1 ? { ...to } : { x: a.x + (to.x - a.x) * go, y: bottom - h, w: a.w + (to.w - a.w) * go, h };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
|
|
70
99
|
// 드롭 — 섬광·고리 대신 파란 구분선 한 줄이 가운데에서 양옆으로 300ms 스프링으로 그어졌다 220ms 로 빠진다(흔들림·섬광 없음)
|
|
71
100
|
drop(el, { ctx, drop }) {
|
|
72
101
|
const at = ctx.beats.drop(drop);
|
|
Binary file
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"accentText": "#3B99F3",
|
|
16
16
|
"onAccent": "#FFFFFF",
|
|
17
17
|
"tint": "rgba(0, 132, 255, 0.1)",
|
|
18
|
+
"fill": "rgba(0, 132, 255, 0.1)",
|
|
19
|
+
"onFill": "#3B99F3",
|
|
20
|
+
"fillLine": "rgba(0, 132, 255, 0.45)",
|
|
18
21
|
"success": "#10B981",
|
|
19
22
|
"danger": "#EF4343",
|
|
20
23
|
"warning": "#F59E0B",
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
"16:9",
|
|
15
15
|
"9:16"
|
|
16
16
|
],
|
|
17
|
+
"transitions": [
|
|
18
|
+
"none",
|
|
19
|
+
"fade"
|
|
20
|
+
],
|
|
21
|
+
"motion": {
|
|
22
|
+
"maxFreezeSec": 1
|
|
23
|
+
},
|
|
17
24
|
"vary": {
|
|
18
25
|
"scheme": [
|
|
19
26
|
"dark",
|
|
@@ -33,6 +40,9 @@
|
|
|
33
40
|
"accentText": "#01509D",
|
|
34
41
|
"onAccent": "#FFFFFF",
|
|
35
42
|
"tint": "rgba(0, 111, 214, 0.1)",
|
|
43
|
+
"fill": "rgba(0, 111, 214, 0.1)",
|
|
44
|
+
"onFill": "#01509D",
|
|
45
|
+
"fillLine": "rgba(0, 111, 214, 0.45)",
|
|
36
46
|
"success": "#10B981",
|
|
37
47
|
"danger": "#EF4444",
|
|
38
48
|
"warning": "#F59E0B",
|
|
@@ -55,6 +65,9 @@
|
|
|
55
65
|
"accentText",
|
|
56
66
|
"onAccent",
|
|
57
67
|
"tint",
|
|
68
|
+
"fill",
|
|
69
|
+
"onFill",
|
|
70
|
+
"fillLine",
|
|
58
71
|
"success",
|
|
59
72
|
"danger",
|
|
60
73
|
"warning"
|
|
@@ -91,6 +104,7 @@
|
|
|
91
104
|
],
|
|
92
105
|
"dims": "2D"
|
|
93
106
|
},
|
|
107
|
+
"sfx": { "set": "click", "db": -4, "perSec": 0.5 },
|
|
94
108
|
"effects": [
|
|
95
109
|
{
|
|
96
110
|
"name": "graphPaper",
|
|
@@ -119,7 +133,7 @@
|
|
|
119
133
|
{
|
|
120
134
|
"name": "pickFly",
|
|
121
135
|
"kind": "layer",
|
|
122
|
-
"does": "뽑힌 숫자 — 로그 줄의 숫자가 고름 칠 알약째 떠올라 호를 그리며 점 그림 자리로 300ms 감속으로 날아가 꽂힌다(박마다 큰 움직임)",
|
|
136
|
+
"does": "뽑힌 숫자 — 로그 줄의 숫자가 고름 칠 알약째 떠올라 호를 그리며 점 그림 자리로 300ms 감속으로 날아가 꽂힌다(박마다 큰 움직임). stops 면 읽는 칸 — 글 뒤에 깔린 알약이 장면의 요점(주인공 값·핵심 사실·자막이 말하는 값)이 바뀔 때만 아래 테두리 한 줄로 얇아져 옮겨 가고, 그 값의 칸만큼 차올라 딱 맞게 서서 머문다(나타나는 항목을 차례로 훑지 않는다)",
|
|
123
137
|
"use": "const paint = pickFly(stage, { items: [{ at: b[0], text: \"746\", from: { x: 760, y: 200 }, to: { x: 1500, y: 250 } }] })"
|
|
124
138
|
},
|
|
125
139
|
{
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"accentText": "#01509D",
|
|
39
39
|
"onAccent": "#FFFFFF",
|
|
40
40
|
"tint": "rgba(0, 111, 214, 0.1)",
|
|
41
|
+
"fill": "rgba(0, 111, 214, 0.1)",
|
|
42
|
+
"onFill": "#01509D",
|
|
43
|
+
"fillLine": "rgba(0, 111, 214, 0.45)",
|
|
41
44
|
"success": "#10B981",
|
|
42
45
|
"danger": "#EF4444",
|
|
43
46
|
"warning": "#F59E0B",
|