clipwise 0.7.2 → 0.9.1

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.
@@ -0,0 +1,288 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ keynote/vignette — Composite Scene 템플릿 (Anthropic 키노트 문법)
4
+
5
+ 녹화 푸티지(프레임 시퀀스)를 HTML 레이어로 합성한다:
6
+ - 고정된 아이보리 무대 위에 떠 있는 카드 속 푸티지
7
+ - 카메라는 CSS가 담당: 푸시인(push), 크롭(crop), 분할(split)
8
+ - __clipwiseSeek(t)가 CSS 애니메이션 + 푸티지 프레임 인덱스를 동시 구동
9
+
10
+ Props (query):
11
+ layout: hero | crop | split
12
+ num, label, caption — 좌상단 넘버/레이블, 하단 캡션
13
+ base, count, fps — 푸티지 프레임 서버 base URL, 프레임 수, fps
14
+ start, rate — 푸티지 시작 오프셋(초), 재생 배속
15
+ cropX, cropY, cropW, cropH — 푸티지 원본(1280×800) 내 크롭 영역
16
+ cardW — 카드 표시 너비(px)
17
+ pushFrom, pushTo — 푸시인 스케일
18
+ accent — 브랜드 악센트
19
+ code — split 레이아웃의 코드 라인들 (||로 구분)
20
+ -->
21
+ <html lang="ko">
22
+ <head>
23
+ <meta charset="utf-8" />
24
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
25
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
26
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..900&family=Space+Grotesk:wght@400..700&family=Fraunces:ital,opsz,wght@1,9..144,400..700&family=JetBrains+Mono:wght@400;600&display=block" rel="stylesheet" />
27
+ <link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" rel="stylesheet" />
28
+ <style>
29
+ /* Anthropic 키노트 팔레트 */
30
+ :root {
31
+ --ivory: #faf9f5; --ink: #141413; --sub: #6e6c64;
32
+ --hairline: #e8e6dc; --accent: #6366f1;
33
+ }
34
+ /* 폰트 프리셋 — kinetic-type.html과 동일한 토큰 체계 */
35
+ :root {
36
+ --sans: "Inter", "Pretendard Variable", -apple-system, sans-serif;
37
+ --em: "Fraunces", Georgia, serif;
38
+ --em-style: italic;
39
+ --mono: "JetBrains Mono", ui-monospace, Menlo, monospace;
40
+ }
41
+ html[data-font="grotesk"] {
42
+ --sans: "Space Grotesk", "Pretendard Variable", -apple-system, sans-serif;
43
+ --em: "Space Grotesk", "Pretendard Variable", sans-serif;
44
+ --em-style: normal;
45
+ }
46
+ html[data-font="system"] {
47
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Pretendard", sans-serif;
48
+ --em: Georgia, "Times New Roman", serif;
49
+ --mono: "SF Mono", ui-monospace, Menlo, monospace;
50
+ }
51
+ * { margin: 0; padding: 0; box-sizing: border-box; }
52
+ html, body { width: 100%; height: 100%; overflow: hidden; background: var(--ivory);
53
+ font-family: var(--sans);
54
+ /* 한글 폴백(Pretendard)에 가짜 기울임/볼드 합성 금지 */
55
+ font-synthesis: none; }
56
+
57
+ /* 종이 그레인 + 비네팅 — kinetic-type.html과 동일한 무대 질감 */
58
+ body::before { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 10;
59
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="220" height="220"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2"/></filter><rect width="220" height="220" filter="url(%23n)" opacity="0.5"/></svg>');
60
+ opacity: 0.05; }
61
+ body::after { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 10;
62
+ background: radial-gradient(ellipse 130% 110% at 50% 42%, transparent 62%, rgba(20,20,19,0.07) 100%); }
63
+
64
+ .stage { width: 100%; height: 100%; display: flex; flex-direction: column;
65
+ align-items: center; justify-content: center; gap: 22px; position: relative; }
66
+
67
+ /* 스레드 — 카드 뒤를 지나는 연결 선 (kinetic-type.html과 동일 경로/문법) */
68
+ .thread { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
69
+ .thread path { fill: none; stroke: var(--accent); stroke-width: 2.5; opacity: 0.55;
70
+ stroke-linecap: round; animation: thread-go linear both; }
71
+ @keyframes thread-go {
72
+ from { stroke-dasharray: var(--tf) 1; }
73
+ to { stroke-dasharray: var(--tt) 1; }
74
+ }
75
+ .thread-dot { position: absolute; width: 9px; height: 9px; border-radius: 50%;
76
+ background: var(--accent); box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 60%, transparent);
77
+ z-index: 1; offset-rotate: 0deg; animation: dot-go linear both; }
78
+ @keyframes dot-go {
79
+ from { offset-distance: calc(var(--tf) * 100%); }
80
+ to { offset-distance: calc(var(--tt) * 100%); }
81
+ }
82
+
83
+ .label-row { display: flex; align-items: baseline; gap: 12px; align-self: center;
84
+ position: relative; z-index: 3;
85
+ opacity: 0; animation: fade-up 700ms cubic-bezier(0.16,1,0.3,1) 150ms both; }
86
+ .num { font-size: 13px; font-weight: 700; letter-spacing: 0.14em; color: var(--accent);
87
+ font-variant-numeric: tabular-nums; }
88
+ .label { font-size: 14px; font-weight: 600; letter-spacing: 0.01em; color: var(--ink); }
89
+
90
+ /* 푸티지 카드 — 아이보리 위에 떠 있는 다크 윈도우 */
91
+ .card {
92
+ position: relative; border-radius: 14px; overflow: hidden;
93
+ box-shadow: 0 1px 0 rgba(20,20,19,0.04), 0 24px 64px -16px rgba(20,20,19,0.28);
94
+ opacity: 0; animation: card-in 800ms cubic-bezier(0.16,1,0.3,1) both;
95
+ background: #0b0d13;
96
+ }
97
+ .titlebar { position: absolute; top: 0; left: 0; right: 0; height: 30px; z-index: 3;
98
+ display: flex; align-items: center; gap: 7px; padding-left: 13px;
99
+ background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); }
100
+ .titlebar i { width: 9px; height: 9px; border-radius: 50%; background: rgba(255,255,255,0.22); }
101
+ .viewport { position: absolute; inset: 0; overflow: hidden; }
102
+ .pusher { position: absolute; inset: 0; transform-origin: 50% 42%;
103
+ animation: push linear both; }
104
+ .footage { position: absolute; transform-origin: 0 0; image-rendering: auto; }
105
+
106
+ /* 캡션은 산세리프 정자 — 세리프 이탤릭은 라틴 디스플레이(키네틱·워드마크) 전용 */
107
+ .caption { font-size: 17.5px; color: var(--sub); letter-spacing: -0.005em;
108
+ font-family: var(--sans); font-weight: 450;
109
+ opacity: 0; animation: fade-up 700ms cubic-bezier(0.16,1,0.3,1) 350ms both; }
110
+ .caption b { color: var(--ink); font-weight: 680; }
111
+ .caption { position: relative; z-index: 3; }
112
+
113
+ /* ── 선 드로잉 주석 레이어 (fx=circle@…;arrow@…) — 카드 위에 그려짐 ── */
114
+ .annotations { position: absolute; inset: 0; z-index: 5; pointer-events: none;
115
+ overflow: visible; }
116
+ .annotations .stroke { stroke: var(--accent); fill: none; stroke-width: 3.5;
117
+ stroke-linecap: round; stroke-linejoin: round;
118
+ stroke-dasharray: 1; stroke-dashoffset: 1;
119
+ animation: draw 600ms cubic-bezier(0.4, 0, 0.2, 1) both;
120
+ filter: drop-shadow(0 1px 4px rgba(20,20,19,0.35)); }
121
+ @keyframes draw { to { stroke-dashoffset: 0; } }
122
+
123
+ /* split — 좌: 코드 카드, 우: 푸티지 */
124
+ .split { display: flex; gap: 36px; align-items: center; position: relative; z-index: 3; }
125
+ .code-card { width: 380px; border-radius: 14px; background: #141413; padding: 22px 24px;
126
+ box-shadow: 0 24px 64px -16px rgba(20,20,19,0.28);
127
+ opacity: 0; animation: card-in 800ms cubic-bezier(0.16,1,0.3,1) both;
128
+ font-family: var(--mono); font-size: 12.5px;
129
+ line-height: 1.85; color: #d8d6cd; }
130
+ .code-card .fname { font-size: 11px; color: #8a887e; margin-bottom: 10px;
131
+ letter-spacing: 0.04em; }
132
+ .code-line { display: block; overflow: hidden; white-space: nowrap; width: 0;
133
+ animation: typeline 420ms steps(28, end) forwards; }
134
+ .code-line .k { color: #8fa0f5; }
135
+ .code-line .c { color: #6e6c64; }
136
+
137
+ @keyframes fade-up { from { opacity: 0; transform: translateY(14px); }
138
+ to { opacity: 1; transform: translateY(0); } }
139
+ @keyframes card-in { from { opacity: 0; transform: translateY(18px) scale(0.985); }
140
+ to { opacity: 1; transform: translateY(0) scale(1); } }
141
+ @keyframes push { from { transform: scale(var(--push-from, 1)); }
142
+ to { transform: scale(var(--push-to, 1)); } }
143
+ @keyframes typeline { from { width: 0; } to { width: 100%; } }
144
+ </style>
145
+ </head>
146
+ <body>
147
+ <div class="stage">
148
+ <div class="label-row"><span class="num">01</span><span class="label">Label</span></div>
149
+ <div class="split" id="content"></div>
150
+ <div class="caption" id="caption">Caption</div>
151
+ </div>
152
+
153
+ <script>
154
+ const P = new URLSearchParams(location.search);
155
+ const num = (k, d) => parseFloat(P.get(k) ?? d);
156
+ const SRC_W = 1280, SRC_H = 800;
157
+
158
+ document.documentElement.style.setProperty("--accent", P.get("accent") || "#6366f1");
159
+ document.documentElement.dataset.font = P.get("font") || "editorial";
160
+ document.querySelector(".num").textContent = P.get("num") || "";
161
+ document.querySelector(".label").textContent = P.get("label") || "";
162
+ // 캡션: *텍스트* → 강조
163
+ const cap = (P.get("caption") || "").split("*");
164
+ document.getElementById("caption").innerHTML =
165
+ cap.map((s, i) => (i % 2 ? `<b>${s}</b>` : s)).join("");
166
+
167
+ // ── 푸티지 카드 구성 ──
168
+ const cardW = num("cardW", 920);
169
+ const cx = num("cropX", 0), cy = num("cropY", 0);
170
+ const cw = num("cropW", SRC_W), ch = num("cropH", SRC_H);
171
+ const cardH = cardW * (ch / cw);
172
+ const scale = cardW / cw;
173
+
174
+ const card = document.createElement("div");
175
+ card.className = "card";
176
+ card.style.width = cardW + "px";
177
+ card.style.height = cardH + "px";
178
+ card.style.setProperty("--push-from", P.get("pushFrom") || "1");
179
+ card.style.setProperty("--push-to", P.get("pushTo") || "1");
180
+ const fullCrop = cw >= SRC_W;
181
+ card.innerHTML = `
182
+ ${fullCrop ? '<div class="titlebar"><i></i><i></i><i></i></div>' : ""}
183
+ <div class="viewport"><div class="pusher" style="animation-duration:${num("dur", 4)}s">
184
+ <img class="footage" id="footage"
185
+ style="width:${SRC_W * scale}px;height:${SRC_H * scale}px;left:${-cx * scale}px;top:${-cy * scale}px" />
186
+ </div></div>`;
187
+
188
+ const content = document.getElementById("content");
189
+ if (P.get("layout") === "split") {
190
+ const codeCard = document.createElement("div");
191
+ codeCard.className = "code-card";
192
+ const lines = (P.get("code") || "").split("||");
193
+ codeCard.innerHTML = `<div class="fname">.clipwise/scenarios/demo.yaml</div>` +
194
+ lines.map((l, i) =>
195
+ `<span class="code-line" style="animation-delay:${500 + i * 260}ms">${l
196
+ .replace(/^(\s*)([\w]+):/, '$1<span class="k">$2</span>:')
197
+ .replace(/(#.*)$/, '<span class="c">$1</span>')}</span>`).join("");
198
+ content.appendChild(codeCard);
199
+ card.style.width = "620px";
200
+ card.style.height = 620 * (ch / cw) + "px";
201
+ const s2 = 620 / cw;
202
+ const img = card.querySelector(".footage");
203
+ img.style.width = SRC_W * s2 + "px";
204
+ img.style.height = SRC_H * s2 + "px";
205
+ img.style.left = -cx * s2 + "px";
206
+ img.style.top = -cy * s2 + "px";
207
+ }
208
+ // 카드를 홀더로 감싼다 — 주석(타원 오버슈트)이 카드의 overflow:hidden에
209
+ // 잘리지 않도록 주석 레이어는 홀더에 부착
210
+ const holder = document.createElement("div");
211
+ holder.style.position = "relative";
212
+ holder.appendChild(card);
213
+ content.appendChild(holder);
214
+
215
+ // ── 선 드로잉 주석 — fx=circle@x,y,w,h@delay;arrow@x1,y1,x2,y2@delay ──
216
+ // 좌표는 푸티지 원본(1280×800) 기준 → 카드 공간으로 변환해 그린다
217
+ const fxSpecs = (P.get("fx") || "").split(";").filter(Boolean);
218
+ if (fxSpecs.length) {
219
+ const effScale = P.get("layout") === "split" ? 620 / cw : scale;
220
+ const toCard = (px, py) => [(px - cx) * effScale, (py - cy) * effScale];
221
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
222
+ svg.setAttribute("class", "annotations");
223
+ let inner = "";
224
+ for (const spec of fxSpecs) {
225
+ const [kind, coords, delay = "0"] = spec.split("@");
226
+ const n = coords.split(",").map(Number);
227
+ if (kind === "circle") {
228
+ // 손그림 느낌: 살짝 큰 타원 + 미세 회전
229
+ const [x0, y0] = toCard(n[0], n[1]);
230
+ const w0 = n[2] * effScale, h0 = n[3] * effScale;
231
+ const cx0 = x0 + w0 / 2, cy0 = y0 + h0 / 2;
232
+ inner += `<ellipse class="stroke" cx="${cx0}" cy="${cy0}"
233
+ rx="${(w0 / 2) * 1.18 + 6}" ry="${(h0 / 2) * 1.45 + 5}" pathLength="1"
234
+ transform="rotate(-4 ${cx0} ${cy0})" style="animation-delay:${delay}ms"/>`;
235
+ } else if (kind === "arrow") {
236
+ const [x1, y1] = toCard(n[0], n[1]);
237
+ const [x2, y2] = toCard(n[2], n[3]);
238
+ // 진행 방향에 수직으로 휘는 곡선 + 화살촉(지연 드로잉)
239
+ const mx = (x1 + x2) / 2 - (y2 - y1) * 0.22;
240
+ const my = (y1 + y2) / 2 + (x2 - x1) * 0.22;
241
+ const ang = Math.atan2(y2 - my, x2 - mx);
242
+ const head = (da) => `${x2 - 13 * Math.cos(ang + da)},${y2 - 13 * Math.sin(ang + da)}`;
243
+ inner += `<path class="stroke" d="M ${x1} ${y1} Q ${mx} ${my} ${x2} ${y2}"
244
+ pathLength="1" style="animation-delay:${delay}ms"/>
245
+ <polyline class="stroke" points="${head(0.45)} ${x2},${y2} ${head(-0.45)}"
246
+ pathLength="1" style="animation-delay:${Number(delay) + 320}ms;animation-duration:250ms"/>`;
247
+ }
248
+ }
249
+ svg.innerHTML = inner;
250
+ // .pusher에 부착 — 푸시인 transform을 푸티지와 함께 타므로
251
+ // 동그라미/화살표가 대상 픽셀에 정확히 앵커링된다 (드리프트 제거)
252
+ card.querySelector(".pusher").appendChild(svg);
253
+ }
254
+
255
+ // ── 푸티지 시킹 — CSS 애니메이션과 동일한 시간축으로 구동 ──
256
+ const BASE = P.get("base"), COUNT = num("count", 1), FPS = num("fps", 30);
257
+ const START = num("start", 0), RATE = num("rate", 1);
258
+ const img = document.getElementById("footage");
259
+
260
+ // ── 스레드 — 모든 신이 공유하는 동일 경로, 구간(threadFrom→threadTo)만 전진 ──
261
+ const THREAD_PATH = "M -6 716 C 150 704, 320 730, 480 714 S 770 724, 940 706 S 1180 728, 1286 712";
262
+ const tf = P.get("threadFrom"), tt = P.get("threadTo");
263
+ if (tf !== null && tt !== null) {
264
+ const durS = parseFloat(P.get("dur") || "4");
265
+ const stage = document.querySelector(".stage");
266
+ const tsvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
267
+ tsvg.setAttribute("class", "thread");
268
+ tsvg.setAttribute("viewBox", "0 0 1280 800");
269
+ tsvg.innerHTML = `<path d="${THREAD_PATH}" pathLength="1"
270
+ style="--tf:${tf};--tt:${tt};animation-duration:${durS}s"/>`;
271
+ stage.appendChild(tsvg);
272
+ const dot = document.createElement("div");
273
+ dot.className = "thread-dot";
274
+ dot.style.cssText = `--tf:${tf};--tt:${tt};offset-path:path('${THREAD_PATH}');animation-duration:${durS}s`;
275
+ stage.appendChild(dot);
276
+ }
277
+
278
+ window.__clipwiseSeek = async (t) => {
279
+ for (const a of document.getAnimations()) { a.pause(); a.currentTime = t; }
280
+ if (BASE) {
281
+ const idx = Math.max(0, Math.min(COUNT - 1, Math.round((START + (t / 1000) * RATE) * FPS)));
282
+ img.src = `${BASE}/${idx}.png`;
283
+ await img.decode();
284
+ }
285
+ };
286
+ </script>
287
+ </body>
288
+ </html>