vibemovie 0.1.0
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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/chunk-HI6AAJID.js +6 -0
- package/dist/chunk-JTRTKTPK.js +729 -0
- package/dist/cli.d.ts +42 -0
- package/dist/cli.js +192 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +10 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +80 -0
- package/dist/scenes-CKgKppfK.d.ts +118 -0
- package/package.json +64 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createCascade, createConsentLedger } from "@pooriaarab/vibe-core";
|
|
3
|
+
import { writeFile } from "fs/promises";
|
|
4
|
+
|
|
5
|
+
// src/scenes.ts
|
|
6
|
+
var RATIOS = ["16:9", "9:16", "1:1"];
|
|
7
|
+
var TEMPLATES = ["documentary", "speedrun", "meme"];
|
|
8
|
+
var MAX_TASK_ROWS = 5;
|
|
9
|
+
var MAX_DIFF_FILES = 4;
|
|
10
|
+
var MAX_TERM_LINES = 5;
|
|
11
|
+
var PACING = {
|
|
12
|
+
documentary: { title: 4200, tasks: 4200, diff: 4200, terminal: 4600, merge: 4600, end: 3400 },
|
|
13
|
+
speedrun: { title: 2500, tasks: 2500, diff: 2500, terminal: 2800, merge: 2800, end: 2200 },
|
|
14
|
+
meme: { title: 3600, tasks: 3600, diff: 3600, terminal: 3900, merge: 3900, end: 3e3 }
|
|
15
|
+
};
|
|
16
|
+
var TRANSITIONS = {
|
|
17
|
+
title: "fade",
|
|
18
|
+
tasks: "slide",
|
|
19
|
+
diff: "scale",
|
|
20
|
+
terminal: "rise",
|
|
21
|
+
merge: "fade",
|
|
22
|
+
end: "fade"
|
|
23
|
+
};
|
|
24
|
+
function isRecord(v) {
|
|
25
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
26
|
+
}
|
|
27
|
+
function num(v) {
|
|
28
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
29
|
+
}
|
|
30
|
+
function str(v) {
|
|
31
|
+
return typeof v === "string" && v.trim().length > 0 ? v : null;
|
|
32
|
+
}
|
|
33
|
+
function strArr(v) {
|
|
34
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
35
|
+
}
|
|
36
|
+
function firstStr(payload, keys) {
|
|
37
|
+
for (const k of keys) {
|
|
38
|
+
const v = str(payload[k]);
|
|
39
|
+
if (v !== null) return v;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function firstNum(payload, keys) {
|
|
44
|
+
for (const k of keys) {
|
|
45
|
+
const v = num(payload[k]);
|
|
46
|
+
if (v !== null) return v;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function normalize(events) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const e of events) {
|
|
53
|
+
if (!isRecord(e)) continue;
|
|
54
|
+
const kind = str(e.kind) ?? str(e.type) ?? "event";
|
|
55
|
+
let ts = 0;
|
|
56
|
+
if (num(e.ts) !== null) ts = num(e.ts);
|
|
57
|
+
else if (typeof e.timestamp === "number" && Number.isFinite(e.timestamp)) ts = e.timestamp;
|
|
58
|
+
else if (typeof e.timestamp === "string") {
|
|
59
|
+
const parsed = Date.parse(e.timestamp);
|
|
60
|
+
if (!Number.isNaN(parsed)) ts = parsed;
|
|
61
|
+
}
|
|
62
|
+
out.push({
|
|
63
|
+
kind,
|
|
64
|
+
ts,
|
|
65
|
+
agent: str(e.agent),
|
|
66
|
+
cwd: str(e.cwd),
|
|
67
|
+
payload: isRecord(e.payload) ? e.payload : {}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out.sort((a, b) => a.ts - b.ts);
|
|
71
|
+
}
|
|
72
|
+
function basename(p) {
|
|
73
|
+
const parts = p.split(/[\\/]/).filter((s) => s.length > 0);
|
|
74
|
+
return parts.length > 0 ? parts[parts.length - 1] : p;
|
|
75
|
+
}
|
|
76
|
+
function formatMinutes(totalMinutes) {
|
|
77
|
+
const h = Math.floor(totalMinutes / 60);
|
|
78
|
+
const m = totalMinutes % 60;
|
|
79
|
+
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
80
|
+
}
|
|
81
|
+
function plural(n, word) {
|
|
82
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
83
|
+
}
|
|
84
|
+
var TASK_KINDS = /* @__PURE__ */ new Set(["task-done", "task", "todo-done", "commit", "milestone"]);
|
|
85
|
+
var PR_OPEN_KINDS = /* @__PURE__ */ new Set(["pr-opened", "pull-request", "pull-request-opened"]);
|
|
86
|
+
var PR_MERGE_KINDS = /* @__PURE__ */ new Set(["pr-merged", "pull-request-merged", "merge"]);
|
|
87
|
+
function collectTasks(events) {
|
|
88
|
+
const rows = [];
|
|
89
|
+
for (const e of events) {
|
|
90
|
+
if (!TASK_KINDS.has(e.kind)) continue;
|
|
91
|
+
const label = firstStr(e.payload, ["label", "task", "title", "summary", "message"]) ?? `Task ${rows.length + 1}`;
|
|
92
|
+
const durationMin = firstNum(e.payload, ["durationMin", "duration_min", "minutes"]);
|
|
93
|
+
rows.push(durationMin !== null ? { label, durationMin } : { label });
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
function collectDiff(events) {
|
|
98
|
+
const files = /* @__PURE__ */ new Set();
|
|
99
|
+
let additions = 0;
|
|
100
|
+
let deletions = 0;
|
|
101
|
+
let filesChanged = 0;
|
|
102
|
+
for (const e of events) {
|
|
103
|
+
for (const f of strArr(e.payload["files"])) files.add(f);
|
|
104
|
+
additions += firstNum(e.payload, ["additions", "adds", "added"]) ?? 0;
|
|
105
|
+
deletions += firstNum(e.payload, ["deletions", "dels", "removed"]) ?? 0;
|
|
106
|
+
const fc = firstNum(e.payload, ["filesChanged", "files_changed"]);
|
|
107
|
+
if (fc !== null) filesChanged = Math.max(filesChanged, fc);
|
|
108
|
+
}
|
|
109
|
+
if (filesChanged === 0) filesChanged = files.size;
|
|
110
|
+
return { filesChanged, additions, deletions, files: [...files].slice(0, MAX_DIFF_FILES) };
|
|
111
|
+
}
|
|
112
|
+
function collectPr(events) {
|
|
113
|
+
let opened = null;
|
|
114
|
+
let merged = null;
|
|
115
|
+
for (const e of events) {
|
|
116
|
+
if (PR_MERGE_KINDS.has(e.kind) && merged === null) merged = e;
|
|
117
|
+
if (PR_OPEN_KINDS.has(e.kind) && opened === null) opened = e;
|
|
118
|
+
}
|
|
119
|
+
const source = merged ?? opened;
|
|
120
|
+
if (source === null) return null;
|
|
121
|
+
return {
|
|
122
|
+
pr: firstNum(source.payload, ["pr", "number", "prNumber", "pr_number"]),
|
|
123
|
+
branch: firstStr(source.payload, ["branch", "base", "into"]) ?? "main",
|
|
124
|
+
reviewers: firstNum(source.payload, ["reviewers", "reviewedBy", "approvals"]),
|
|
125
|
+
merged: merged !== null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function collectTermLines(events, pr) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
const push = (text, cls = "") => {
|
|
131
|
+
if (lines.length < MAX_TERM_LINES) lines.push({ text, cls });
|
|
132
|
+
};
|
|
133
|
+
for (const e of events) {
|
|
134
|
+
if (lines.length >= MAX_TERM_LINES) break;
|
|
135
|
+
if (e.kind === "tests-pass") {
|
|
136
|
+
const passed = firstNum(e.payload, ["passed", "count", "tests"]);
|
|
137
|
+
push("$ npm test");
|
|
138
|
+
push(passed !== null ? `\u2713 ${passed} passed` : "\u2713 tests passed", "ok");
|
|
139
|
+
} else if (e.kind === "tests-fail") {
|
|
140
|
+
const failed = firstNum(e.payload, ["failed", "count"]);
|
|
141
|
+
push("$ npm test");
|
|
142
|
+
push(failed !== null ? `\u2717 ${failed} failing` : "\u2717 tests failing");
|
|
143
|
+
} else if (e.kind === "error") {
|
|
144
|
+
const msg = firstStr(e.payload, ["message", "error", "label"]) ?? "something broke";
|
|
145
|
+
push(`\u2717 ${msg}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (pr !== null && lines.length <= MAX_TERM_LINES - 2) {
|
|
149
|
+
push("$ git push");
|
|
150
|
+
push(`\u2192 pushed to origin/${pr.branch}`, "ok");
|
|
151
|
+
}
|
|
152
|
+
if (lines.length === 0 && events.length > 0) {
|
|
153
|
+
push("$ vibemovie render");
|
|
154
|
+
push(`\u2713 ${plural(events.length, "event")} recapped`, "ok");
|
|
155
|
+
}
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
function captions(template, stats) {
|
|
159
|
+
const { totalMinutes, tasks, diff, pr } = stats;
|
|
160
|
+
const dur = formatMinutes(totalMinutes);
|
|
161
|
+
const prWord = pr !== null ? pr.merged ? "merged" : "opened" : "";
|
|
162
|
+
const prNum = pr?.pr !== null && pr?.pr !== void 0 ? `#${pr.pr} ` : "";
|
|
163
|
+
if (template === "speedrun") {
|
|
164
|
+
return {
|
|
165
|
+
title: totalMinutes > 0 ? `${dur}. go.` : "a session. go.",
|
|
166
|
+
tasks: `${plural(tasks, "task")}. done.`,
|
|
167
|
+
diff: `+${diff.additions} \u2212${diff.deletions} across ${plural(diff.filesChanged, "file")}.`,
|
|
168
|
+
terminal: "green. ship.",
|
|
169
|
+
merge: `${prNum}${prWord}. next.`,
|
|
170
|
+
end: "gg."
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (template === "meme") {
|
|
174
|
+
return {
|
|
175
|
+
title: "touch grass? never heard of it.",
|
|
176
|
+
tasks: `${plural(tasks, "task")} speedrun any%`,
|
|
177
|
+
diff: "number go up",
|
|
178
|
+
terminal: "it compiles. ship it.",
|
|
179
|
+
merge: pr !== null && pr.merged ? "LGTM said the reviewer" : "CI roulette champion",
|
|
180
|
+
end: "same time tomorrow?"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
title: totalMinutes > 0 ? `${dur} in the flow.` : "A session worth replaying.",
|
|
185
|
+
tasks: `${plural(tasks, "task")} landed, clean.`,
|
|
186
|
+
diff: diff.additions >= diff.deletions ? `${plural(diff.filesChanged, "file")} touched \u2014 more added than removed.` : `${plural(diff.filesChanged, "file")} touched \u2014 more removed than added.`,
|
|
187
|
+
terminal: stats.hasTests ? "Tests green. Shipped." : "The commands tell the story.",
|
|
188
|
+
merge: pr !== null ? `Pull request ${prNum}${prWord}.` : "",
|
|
189
|
+
end: "That's the session. Run it back."
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function scene(kind, template, caption, data) {
|
|
193
|
+
return {
|
|
194
|
+
id: `scene-${kind}`,
|
|
195
|
+
kind,
|
|
196
|
+
transition: TRANSITIONS[kind],
|
|
197
|
+
duration: PACING[template][kind],
|
|
198
|
+
caption,
|
|
199
|
+
data
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function buildScenes(events, opts = {}) {
|
|
203
|
+
const template = opts.template ?? "documentary";
|
|
204
|
+
const norm = normalize(events);
|
|
205
|
+
const first = norm[0];
|
|
206
|
+
const last = norm[norm.length - 1];
|
|
207
|
+
const totalMinutes = first !== void 0 && last !== void 0 && last.ts > first.ts ? Math.round((last.ts - first.ts) / 6e4) : 0;
|
|
208
|
+
const sessionName = opts.title ?? (first?.cwd != null ? basename(first.cwd) : "vibe session");
|
|
209
|
+
const agent = first?.agent ?? "agentic";
|
|
210
|
+
const tasks = collectTasks(norm);
|
|
211
|
+
const diff = collectDiff(norm);
|
|
212
|
+
const pr = collectPr(norm);
|
|
213
|
+
const termLines = collectTermLines(norm, pr);
|
|
214
|
+
const hasTests = norm.some((e) => e.kind === "tests-pass");
|
|
215
|
+
const caps = captions(template, { totalMinutes, tasks: tasks.length, diff, pr, hasTests });
|
|
216
|
+
const scenes = [
|
|
217
|
+
scene("title", template, caps.title, {
|
|
218
|
+
sessionName,
|
|
219
|
+
totalMinutes,
|
|
220
|
+
subtitle: `${agent} coding session \xB7 recapped on-device`
|
|
221
|
+
})
|
|
222
|
+
];
|
|
223
|
+
if (tasks.length > 0) {
|
|
224
|
+
scenes.push(
|
|
225
|
+
scene("tasks", template, caps.tasks, { total: tasks.length, tasks: tasks.slice(0, MAX_TASK_ROWS) })
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (diff.filesChanged > 0 || diff.additions + diff.deletions > 0) {
|
|
229
|
+
scenes.push(scene("diff", template, caps.diff, diff));
|
|
230
|
+
}
|
|
231
|
+
if (termLines.length > 0) {
|
|
232
|
+
scenes.push(scene("terminal", template, caps.terminal, { lines: termLines }));
|
|
233
|
+
}
|
|
234
|
+
if (pr !== null) {
|
|
235
|
+
scenes.push(scene("merge", template, caps.merge, pr));
|
|
236
|
+
}
|
|
237
|
+
scenes.push(scene("end", template, caps.end, { tagline: "generated on-device \xB7 hyperframes" }));
|
|
238
|
+
return scenes;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/hyperframes.ts
|
|
242
|
+
function escapeHtml(s) {
|
|
243
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
244
|
+
}
|
|
245
|
+
function embedJson(value) {
|
|
246
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
247
|
+
}
|
|
248
|
+
var TICK_SVG = '<svg class="tick" width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#3a3024" stroke-width="1.4"/><path d="M5.5 10.2l3 3 6-6.4" stroke="#7fae8a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" pathLength="1" stroke-dasharray="1" stroke-dashoffset="1"/></svg>';
|
|
249
|
+
function sceneHtml(scene2) {
|
|
250
|
+
switch (scene2.kind) {
|
|
251
|
+
case "title": {
|
|
252
|
+
const d = scene2.data;
|
|
253
|
+
return `<section class="scene" id="${scene2.id}"><svg class="reel" viewBox="0 0 100 100" fill="none" stroke="currentColor" stroke-width="2"><circle cx="50" cy="50" r="46"/><circle cx="50" cy="20" r="9"/><circle cx="76" cy="65" r="9"/><circle cx="24" cy="65" r="9"/><circle cx="50" cy="50" r="6" fill="currentColor" stroke="none"/></svg><div class="session-title">${escapeHtml(d.sessionName)}</div><div class="session-sub" id="titleCounter">0h 00m</div><div class="session-caption">${escapeHtml(d.subtitle)}</div></section>`;
|
|
254
|
+
}
|
|
255
|
+
case "tasks": {
|
|
256
|
+
const d = scene2.data;
|
|
257
|
+
const rows = d.tasks.map((t) => {
|
|
258
|
+
const dur = t.durationMin !== void 0 ? `<span class="dur">${escapeHtml(formatMinutes(t.durationMin))}</span>` : "";
|
|
259
|
+
return `<div class="task-row">${TICK_SVG}<span class="label">${escapeHtml(t.label)}</span>${dur}</div>`;
|
|
260
|
+
}).join("");
|
|
261
|
+
const more = d.total > d.tasks.length ? `<div class="task-more">+${d.total - d.tasks.length} more</div>` : "";
|
|
262
|
+
return `<section class="scene" id="${scene2.id}"><div class="tasks-head"><b>${d.total}</b> ${d.total === 1 ? "task" : "tasks"} completed</div><div class="task-list">${rows}${more}</div></section>`;
|
|
263
|
+
}
|
|
264
|
+
case "diff": {
|
|
265
|
+
const d = scene2.data;
|
|
266
|
+
const files = d.files.map((f) => `<div class="f">${escapeHtml(f)}</div>`).join("");
|
|
267
|
+
const more = d.filesChanged > d.files.length ? `<div class="f">+${d.filesChanged - d.files.length} more</div>` : "";
|
|
268
|
+
return `<section class="scene" id="${scene2.id}"><div class="diff-head">${d.filesChanged} ${d.filesChanged === 1 ? "file" : "files"} changed</div><div class="diff-bar-wrap"><div class="diff-bar"><div class="add" id="diffAdd"></div><div class="remove" id="diffRemove"></div></div><div class="diff-stats"><span class="plus">+${d.additions}</span><span class="minus">\u2212${d.deletions}</span></div></div><div class="diff-files" id="diffFiles">${files}${more}</div></section>`;
|
|
269
|
+
}
|
|
270
|
+
case "terminal": {
|
|
271
|
+
return `<section class="scene" id="${scene2.id}"><div class="terminal"><div class="terminal-bar"><span></span><span></span><span></span><span class="name">vibe \u2014 zsh</span></div><div class="terminal-body" id="termBody"></div></div></section>`;
|
|
272
|
+
}
|
|
273
|
+
case "merge": {
|
|
274
|
+
const d = scene2.data;
|
|
275
|
+
const head = d.pr !== null ? `PR #${d.pr} ${d.merged ? "merged" : "opened"}` : `PR ${d.merged ? "merged" : "opened"}`;
|
|
276
|
+
const sub = `into ${escapeHtml(d.branch)}` + (d.reviewers !== null ? ` \xB7 reviewed by ${d.reviewers}` : "");
|
|
277
|
+
return `<section class="scene" id="${scene2.id}"><svg class="merge-badge" id="mergeBadge" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><circle cx="14" cy="12" r="4"/><circle cx="14" cy="36" r="4"/><circle cx="34" cy="12" r="4"/><path d="M14 16v12a8 8 0 0 0 8 8h4"/><path d="M34 16v4"/></svg><div class="merge-head">${escapeHtml(head)}</div><div class="merge-sub">${sub}</div><div class="confetti" id="confetti"></div></section>`;
|
|
278
|
+
}
|
|
279
|
+
case "end": {
|
|
280
|
+
const d = scene2.data;
|
|
281
|
+
return `<section class="scene" id="${scene2.id}"><div class="end-mark">vibe<b>movie</b></div><div class="end-sub">${escapeHtml(d.tagline)}</div><svg class="loop-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M17 2l4 4-4 4"/><path d="M3 12v-2a4 4 0 0 1 4-4h14"/><path d="M7 22l-4-4 4-4"/><path d="M21 12v2a4 4 0 0 1-4 4H3"/></svg></section>`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function css(ratio) {
|
|
286
|
+
const screenSizing = ratio === "16:9" ? ".screen{position:relative;aspect-ratio:16/9;overflow:hidden;" : ratio === "9:16" ? ".screen{position:relative;height:min(74vh,760px);width:calc(min(74vh,760px) * 9 / 16);max-width:100%;margin:0 auto;overflow:hidden;" : ".screen{position:relative;height:min(74vh,640px);width:calc(min(74vh,640px));max-width:100%;margin:0 auto;overflow:hidden;";
|
|
287
|
+
return `
|
|
288
|
+
:root{
|
|
289
|
+
--bg: #0a0908; --bg-raised: #14120e; --frame: #050403;
|
|
290
|
+
--ink: #f2ede1; --ink-dim: #a89e8c; --ink-faint: #6b6255;
|
|
291
|
+
--gold: #d9a94e; --add: #7fae8a; --remove: #b25a45; --merge: #a084d6;
|
|
292
|
+
--ease: cubic-bezier(0.16, 1, 0.3, 1); --radius: 14px;
|
|
293
|
+
}
|
|
294
|
+
*{ box-sizing: border-box; }
|
|
295
|
+
html,body{ margin:0; padding:0; min-height:100%; background: var(--bg); color: var(--ink);
|
|
296
|
+
font-family: -apple-system, "SF Pro Text", "Segoe UI", Roboto, system-ui, sans-serif;
|
|
297
|
+
-webkit-font-smoothing: antialiased; }
|
|
298
|
+
body{ padding: 40px 20px 64px; display:flex; justify-content:center; }
|
|
299
|
+
.stage{ width:100%; max-width: 900px; }
|
|
300
|
+
.topbar{ display:flex; align-items:baseline; justify-content:space-between; margin-bottom: 22px; flex-wrap: wrap; gap: 10px; }
|
|
301
|
+
.wordmark{ font-size: 1.35rem; font-weight: 700; letter-spacing: -0.03em; }
|
|
302
|
+
.wordmark b{ font-weight: 700; color: var(--gold); }
|
|
303
|
+
.tagline{ display:block; font-size: 0.78rem; color: var(--ink-faint); margin-top: 2px; }
|
|
304
|
+
.badge-local{ display:inline-flex; align-items:center; gap:7px; font-size: 0.72rem; font-weight: 600;
|
|
305
|
+
color: var(--ink-dim); background: var(--bg-raised); border: 1px solid #262019;
|
|
306
|
+
padding: 6px 12px; border-radius: 999px; white-space: nowrap; }
|
|
307
|
+
.badge-local .dot{ width:7px; height:7px; border-radius:50%; background: #7fc98a; }
|
|
308
|
+
@media (prefers-reduced-motion: no-preference){
|
|
309
|
+
.badge-local .dot{ animation: pulse-dot 2.4s var(--ease) infinite; }
|
|
310
|
+
}
|
|
311
|
+
@keyframes pulse-dot{ 0%{ box-shadow: 0 0 0 0 rgba(127,201,138,0.45);} 70%{ box-shadow: 0 0 0 6px rgba(127,201,138,0);} 100%{ box-shadow: 0 0 0 0 rgba(127,201,138,0);} }
|
|
312
|
+
.player{ position: relative; background: var(--frame); border-radius: var(--radius);
|
|
313
|
+
box-shadow: 0 24px 60px -20px rgba(0,0,0,0.75), 0 2px 0 rgba(255,255,255,0.02) inset;
|
|
314
|
+
overflow: hidden; border: 1px solid #201a12; }
|
|
315
|
+
.filmstrip{ height: 12px; background-color: #171009;
|
|
316
|
+
background-image: radial-gradient(circle 3.2px at 12px 6px, var(--bg) 3.2px, transparent 3.6px);
|
|
317
|
+
background-size: 24px 12px; background-repeat: repeat-x; }
|
|
318
|
+
${screenSizing}
|
|
319
|
+
background: radial-gradient(120% 100% at 50% 0%, #171008 0%, #0a0705 60%, #050302 100%); }
|
|
320
|
+
.screen::before{ content:""; position:absolute; inset:0; pointer-events:none; z-index: 5;
|
|
321
|
+
box-shadow: inset 0 0 90px 20px rgba(0,0,0,0.55); }
|
|
322
|
+
.scene{ position:absolute; inset:0; display:none; flex-direction:column; align-items:center;
|
|
323
|
+
justify-content:center; text-align:center; padding: 8% 9%; will-change: transform, opacity; }
|
|
324
|
+
.scene.is-live{ display:flex; }
|
|
325
|
+
.reel{ position:absolute; top:8%; right:6%; width: 90px; height: 90px; opacity: .16; }
|
|
326
|
+
@media (prefers-reduced-motion: no-preference){ .reel{ animation: spin 26s linear infinite; } }
|
|
327
|
+
@keyframes spin{ to{ transform: rotate(360deg); } }
|
|
328
|
+
.session-title{ font-size: clamp(1.6rem, 4.4vw, 2.7rem); font-weight: 700; letter-spacing: -0.03em; margin: 0 0 10px; }
|
|
329
|
+
.session-sub{ font-family: "SF Mono", "Menlo", monospace; font-size: clamp(1rem, 2.6vw, 1.35rem);
|
|
330
|
+
color: var(--gold); font-weight: 600; letter-spacing: -0.01em; }
|
|
331
|
+
.session-caption{ margin-top: 14px; font-size: 0.82rem; color: var(--ink-faint); }
|
|
332
|
+
.tasks-head{ font-size: clamp(1.3rem, 3.4vw, 2rem); font-weight: 700; letter-spacing: -0.03em; margin-bottom: 22px; }
|
|
333
|
+
.tasks-head b{ color: var(--gold); font-size: 1.25em; }
|
|
334
|
+
.task-list{ width: min(440px, 84%); display:flex; flex-direction:column; gap: 12px; }
|
|
335
|
+
.task-row{ display:flex; align-items:center; gap: 12px; text-align:left; padding: 10px 4px; border-bottom: 1px solid #201a12; }
|
|
336
|
+
.task-row:last-child{ border-bottom: none; }
|
|
337
|
+
.task-row .tick{ flex: none; }
|
|
338
|
+
.task-row .label{ flex:1; font-size: 0.92rem; color: var(--ink); }
|
|
339
|
+
.task-row .dur{ font-family: "SF Mono", Menlo, monospace; font-size: 0.74rem; color: var(--ink-faint); }
|
|
340
|
+
.task-more{ font-size: 0.8rem; color: var(--ink-faint); text-align:left; padding: 6px 4px 0 32px; }
|
|
341
|
+
.diff-head{ font-size: clamp(1.3rem, 3.4vw, 2rem); font-weight: 700; letter-spacing: -0.03em; margin-bottom: 20px; }
|
|
342
|
+
.diff-bar-wrap{ width: min(440px, 84%); }
|
|
343
|
+
.diff-bar{ height: 14px; border-radius: 7px; overflow:hidden; display:flex; background: #1a140d; }
|
|
344
|
+
.diff-bar .add{ background: var(--add); height:100%; width:0%; }
|
|
345
|
+
.diff-bar .remove{ background: var(--remove); height:100%; width:0%; }
|
|
346
|
+
.diff-stats{ margin-top: 10px; font-family: "SF Mono", Menlo, monospace; font-size: 0.9rem; display:flex; gap: 16px; justify-content:center; }
|
|
347
|
+
.diff-stats .plus{ color: var(--add); }
|
|
348
|
+
.diff-stats .minus{ color: var(--remove); }
|
|
349
|
+
.diff-files{ margin-top: 18px; display:flex; flex-direction:column; gap: 6px; align-items:center; }
|
|
350
|
+
.diff-files .f{ font-family: "SF Mono", Menlo, monospace; font-size: 0.74rem; color: var(--ink-dim); opacity: 0; transform: translateY(8px); }
|
|
351
|
+
.terminal{ width: min(460px, 88%); background: #0d0b08; border: 1px solid #241d14; border-radius: 10px;
|
|
352
|
+
overflow:hidden; text-align:left; box-shadow: 0 14px 34px -16px rgba(0,0,0,0.7); }
|
|
353
|
+
.terminal-bar{ display:flex; align-items:center; gap:6px; padding: 8px 10px; background: #161109; border-bottom: 1px solid #241d14; }
|
|
354
|
+
.terminal-bar span{ width:9px; height:9px; border-radius:50%; background:#3a3024; }
|
|
355
|
+
.terminal-bar .name{ margin-left: 8px; font-size: 0.7rem; color: var(--ink-faint); font-family: "SF Mono", Menlo, monospace; }
|
|
356
|
+
.terminal-body{ padding: 14px 16px; font-family: "SF Mono", Menlo, monospace; font-size: 0.78rem;
|
|
357
|
+
line-height: 1.7; min-height: 108px; color: var(--ink-dim); }
|
|
358
|
+
.terminal-body .ok{ color: var(--add); }
|
|
359
|
+
.terminal-body .cursor{ display:inline-block; width: 6px; height: 12px; background: var(--gold); vertical-align: -2px; margin-left: 2px; }
|
|
360
|
+
@media (prefers-reduced-motion: no-preference){ .terminal-body .cursor.blink{ animation: blink 1s steps(1) infinite; } }
|
|
361
|
+
@keyframes blink{ 50%{ opacity: 0; } }
|
|
362
|
+
.merge-badge{ width: 74px; height: 74px; margin-bottom: 14px; color: var(--merge); }
|
|
363
|
+
.merge-head{ font-size: clamp(1.3rem, 3.4vw, 2rem); font-weight:700; letter-spacing:-0.03em; }
|
|
364
|
+
.merge-sub{ margin-top: 6px; font-size: 0.85rem; color: var(--ink-dim); }
|
|
365
|
+
.confetti{ position:absolute; inset:0; pointer-events:none; overflow:hidden; }
|
|
366
|
+
.confetti i{ position:absolute; top:-10%; width: 7px; height: 12px; opacity:0; border-radius: 2px; }
|
|
367
|
+
.confetti.is-firing i{ animation: fall 1.8s ease-in forwards; }
|
|
368
|
+
@keyframes fall{ 0%{ opacity: 1; transform: translateY(0) rotate(0deg);} 100%{ opacity: 0; transform: translateY(220px) rotate(300deg);} }
|
|
369
|
+
.end-mark{ font-size: clamp(1.4rem, 3.6vw, 2.2rem); font-weight:700; letter-spacing:-0.03em; }
|
|
370
|
+
.end-mark b{ color: var(--gold); }
|
|
371
|
+
.end-sub{ margin-top: 8px; font-size: 0.8rem; color: var(--ink-faint); }
|
|
372
|
+
.loop-icon{ width: 26px; height: 26px; margin-top: 16px; color: var(--ink-faint); }
|
|
373
|
+
@media (prefers-reduced-motion: no-preference){ .loop-icon{ animation: spin 3.5s linear infinite; } }
|
|
374
|
+
.scene-title{ position:absolute; left: 20px; bottom: 18px; z-index: 6; font-size: 0.72rem; font-weight: 600;
|
|
375
|
+
letter-spacing: 0.02em; color: var(--ink-dim); background: rgba(5,4,3,0.55); backdrop-filter: blur(6px);
|
|
376
|
+
padding: 6px 11px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.06);
|
|
377
|
+
opacity: 0; transform: translateY(6px); transition: opacity .35s var(--ease), transform .35s var(--ease); }
|
|
378
|
+
.scene-title.is-shown{ opacity: 1; transform: translateY(0); }
|
|
379
|
+
.captions{ position: relative; z-index: 6; background: var(--bg-raised); border-top: 1px solid #201a12;
|
|
380
|
+
color: var(--ink); font-size: 0.86rem; padding: 12px 18px; min-height: 20px; text-align:center; }
|
|
381
|
+
.controls{ display:flex; align-items:center; gap: 12px; padding: 12px 16px;
|
|
382
|
+
background: var(--bg-raised); border-top: 1px solid #201a12; }
|
|
383
|
+
.icon-btn{ flex: none; width: 34px; height: 34px; border-radius: 9px; display:flex; align-items:center;
|
|
384
|
+
justify-content:center; background: #201a12; border: 1px solid #2c2417; color: var(--ink); cursor: pointer;
|
|
385
|
+
transition: background .16s var(--ease); }
|
|
386
|
+
.icon-btn:hover{ background: #2c2417; }
|
|
387
|
+
.icon-btn svg{ width: 15px; height: 15px; }
|
|
388
|
+
.icon-btn:focus-visible{ outline: 2px solid var(--gold); outline-offset: 2px; }
|
|
389
|
+
.time{ font-family: "SF Mono", Menlo, monospace; font-size: 0.76rem; color: var(--ink-faint); flex: none; width: 38px; }
|
|
390
|
+
.time.total{ text-align: right; }
|
|
391
|
+
.scrubber{ flex: 1; position: relative; height: 22px; display:flex; align-items:center; cursor: pointer; }
|
|
392
|
+
.scrubber-track{ position:absolute; left:0; right:0; top: 50%; height: 4px; transform: translateY(-50%);
|
|
393
|
+
background: #241d14; border-radius: 999px; overflow: hidden; }
|
|
394
|
+
.scrubber-fill{ position:absolute; left:0; top:0; bottom:0; width: 0%; background: var(--gold); border-radius: 999px; }
|
|
395
|
+
.scrubber-markers{ position:absolute; inset:0; }
|
|
396
|
+
.scrubber-markers i{ position:absolute; top:50%; width:1px; height: 10px; background: rgba(0,0,0,0.5); transform: translate(0, -50%); }
|
|
397
|
+
.scrubber-thumb{ position:absolute; top:50%; width: 11px; height:11px; border-radius:50%;
|
|
398
|
+
background: var(--ink); transform: translate(-50%, -50%); box-shadow: 0 2px 6px rgba(0,0,0,0.5); }
|
|
399
|
+
.foot{ margin-top: 18px; text-align:center; font-size: 0.74rem; color: var(--ink-faint); }
|
|
400
|
+
@media (max-width: 560px){ .controls{ flex-wrap: wrap; } }
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
function engineJs(scenesJson) {
|
|
404
|
+
return `
|
|
405
|
+
(function(){
|
|
406
|
+
"use strict";
|
|
407
|
+
var SCENES = ${scenesJson};
|
|
408
|
+
var offsets = []; var acc = 0; var i;
|
|
409
|
+
for (i = 0; i < SCENES.length; i++) { offsets.push(acc); acc += SCENES[i].duration; }
|
|
410
|
+
var TOTAL = acc;
|
|
411
|
+
|
|
412
|
+
var els = {};
|
|
413
|
+
for (i = 0; i < SCENES.length; i++) { els[SCENES[i].id] = document.getElementById(SCENES[i].id); }
|
|
414
|
+
|
|
415
|
+
var sceneTitleEl = document.getElementById('sceneTitle');
|
|
416
|
+
var captionsEl = document.getElementById('captions');
|
|
417
|
+
var playBtn = document.getElementById('playBtn');
|
|
418
|
+
var playIcon = document.getElementById('playIcon');
|
|
419
|
+
var scrubber = document.getElementById('scrubber');
|
|
420
|
+
var scrubberFill = document.getElementById('scrubberFill');
|
|
421
|
+
var scrubberThumb= document.getElementById('scrubberThumb');
|
|
422
|
+
var scrubberMarkers = document.getElementById('scrubberMarkers');
|
|
423
|
+
var timeCurrent = document.getElementById('timeCurrent');
|
|
424
|
+
var timeTotal = document.getElementById('timeTotal');
|
|
425
|
+
|
|
426
|
+
for (i = 1; i < SCENES.length; i++) {
|
|
427
|
+
var m = document.createElement('i');
|
|
428
|
+
m.style.left = (offsets[i] / TOTAL * 100) + '%';
|
|
429
|
+
scrubberMarkers.appendChild(m);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function fmt(ms){
|
|
433
|
+
var s = Math.floor(ms/1000);
|
|
434
|
+
var m = Math.floor(s/60); s = s%60;
|
|
435
|
+
return m + ':' + (s<10?'0':'') + s;
|
|
436
|
+
}
|
|
437
|
+
timeTotal.textContent = fmt(TOTAL);
|
|
438
|
+
|
|
439
|
+
function esc(s){
|
|
440
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
441
|
+
}
|
|
442
|
+
function easeOutExpo(x){ return x >= 1 ? 1 : 1 - Math.pow(2, -10*x); }
|
|
443
|
+
function easeOutBack(x){ var c1 = 1.7, c3 = c1+1; return 1 + c3*Math.pow(x-1,3) + c1*Math.pow(x-1,2); }
|
|
444
|
+
function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
|
|
445
|
+
|
|
446
|
+
/* deterministic PRNG so confetti is identical on every play */
|
|
447
|
+
function mulberry32(a){
|
|
448
|
+
return function(){
|
|
449
|
+
a |= 0; a = a + 0x6D2B79F5 | 0;
|
|
450
|
+
var t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
451
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
452
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
var rand = mulberry32(12648430);
|
|
456
|
+
var confettiColors = ['#d9a94e','#7fae8a','#b25a45','#a084d6'];
|
|
457
|
+
function buildConfetti(){
|
|
458
|
+
var c = document.getElementById('confetti');
|
|
459
|
+
if (!c) return;
|
|
460
|
+
c.innerHTML = '';
|
|
461
|
+
for (var j = 0; j < 16; j++) {
|
|
462
|
+
var el = document.createElement('i');
|
|
463
|
+
el.style.left = (8 + rand()*84) + '%';
|
|
464
|
+
el.style.background = confettiColors[j % confettiColors.length];
|
|
465
|
+
el.style.animationDelay = (rand()*0.5) + 's';
|
|
466
|
+
el.style.transform = 'rotate(' + Math.floor(rand()*360) + 'deg)';
|
|
467
|
+
c.appendChild(el);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
buildConfetti();
|
|
471
|
+
|
|
472
|
+
var CPS = 34; /* terminal typing speed, chars per second */
|
|
473
|
+
|
|
474
|
+
var renderers = {
|
|
475
|
+
title: function(scene, localT){
|
|
476
|
+
var el = document.getElementById('titleCounter');
|
|
477
|
+
var totalMin = Math.round(scene.data.totalMinutes * easeOutExpo(clamp(localT/2400, 0, 1)));
|
|
478
|
+
var h = Math.floor(totalMin/60), m = totalMin%60;
|
|
479
|
+
el.textContent = h + 'h ' + (m<10?'0':'') + m + 'm';
|
|
480
|
+
},
|
|
481
|
+
tasks: function(scene, localT){
|
|
482
|
+
var rows = els[scene.id].querySelectorAll('.task-row');
|
|
483
|
+
for (var j = 0; j < rows.length; j++) {
|
|
484
|
+
var start = 450 + j*380;
|
|
485
|
+
var p = easeOutExpo(clamp((localT-start)/420, 0, 1));
|
|
486
|
+
var row = rows[j];
|
|
487
|
+
row.style.opacity = p;
|
|
488
|
+
row.style.transform = 'translateY(' + ((1-p)*14) + 'px)';
|
|
489
|
+
row.querySelector('.tick path').setAttribute('stroke-dashoffset', String(1-p));
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
diff: function(scene, localT){
|
|
493
|
+
var adds = scene.data.additions, dels = scene.data.deletions;
|
|
494
|
+
var total = adds + dels;
|
|
495
|
+
var p = easeOutExpo(clamp((localT-350)/900, 0, 1));
|
|
496
|
+
document.getElementById('diffAdd').style.width = (p * (total > 0 ? adds/total : 0) * 100) + '%';
|
|
497
|
+
document.getElementById('diffRemove').style.width = (p * (total > 0 ? dels/total : 0) * 100) + '%';
|
|
498
|
+
var files = document.getElementById('diffFiles').querySelectorAll('.f');
|
|
499
|
+
for (var j = 0; j < files.length; j++) {
|
|
500
|
+
var start = 500 + j*160;
|
|
501
|
+
var fp = easeOutExpo(clamp((localT-start)/300, 0, 1));
|
|
502
|
+
files[j].style.opacity = fp;
|
|
503
|
+
files[j].style.transform = 'translateY(' + ((1-fp)*8) + 'px)';
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
terminal: function(scene, localT){
|
|
507
|
+
var lines = scene.data.lines;
|
|
508
|
+
var out = '';
|
|
509
|
+
var t0 = 250;
|
|
510
|
+
for (var j = 0; j < lines.length; j++) {
|
|
511
|
+
var elapsed = localT - t0;
|
|
512
|
+
t0 += (lines[j].text.length / CPS) * 1000 + 260;
|
|
513
|
+
if (elapsed <= 0) continue;
|
|
514
|
+
var chars = Math.min(lines[j].text.length, Math.floor(elapsed/1000*CPS));
|
|
515
|
+
var shown = esc(lines[j].text.slice(0, chars));
|
|
516
|
+
var typing = chars < lines[j].text.length;
|
|
517
|
+
var cls = lines[j].cls ? ' class="' + lines[j].cls + '"' : '';
|
|
518
|
+
out += '<div' + cls + '>' + shown + (typing ? '<span class="cursor blink"></span>' : '') + '</div>';
|
|
519
|
+
}
|
|
520
|
+
document.getElementById('termBody').innerHTML = out;
|
|
521
|
+
},
|
|
522
|
+
merge: function(scene, localT, justEntered){
|
|
523
|
+
var p = easeOutBack(clamp(localT/480, 0, 1));
|
|
524
|
+
var badge = document.getElementById('mergeBadge');
|
|
525
|
+
badge.style.transform = 'scale(' + Math.max(0,p) + ') rotate(' + ((1-clamp(localT/480,0,1))*-10) + 'deg)';
|
|
526
|
+
if (justEntered) {
|
|
527
|
+
var c = document.getElementById('confetti');
|
|
528
|
+
c.classList.remove('is-firing');
|
|
529
|
+
void c.offsetWidth;
|
|
530
|
+
c.classList.add('is-firing');
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
end: function(){}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
function sceneIndexAt(time){
|
|
537
|
+
for (var j = SCENES.length-1; j >= 0; j--) { if (time >= offsets[j]) return j; }
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function applyTransition(el, type, localT, dur){
|
|
542
|
+
var inWin = Math.min(560, dur*0.28);
|
|
543
|
+
var outWin = Math.min(560, dur*0.28);
|
|
544
|
+
var opacity = 1, tf = 'none';
|
|
545
|
+
if (localT < inWin) {
|
|
546
|
+
var p = easeOutExpo(localT/inWin);
|
|
547
|
+
if (type==='fade'){ opacity=p; tf = 'scale(' + (0.98+0.02*p) + ')'; }
|
|
548
|
+
else if (type==='slide'){ opacity=p; tf = 'translateX(' + ((1-p)*44) + 'px)'; }
|
|
549
|
+
else if (type==='scale'){ opacity=p; tf = 'scale(' + (0.9+0.1*p) + ')'; }
|
|
550
|
+
else if (type==='rise'){ opacity=p; tf = 'translateY(' + ((1-p)*40) + 'px)'; }
|
|
551
|
+
} else if (localT > dur-outWin) {
|
|
552
|
+
var p2 = easeOutExpo((localT-(dur-outWin))/outWin);
|
|
553
|
+
var q = 1-p2;
|
|
554
|
+
if (type==='fade'){ opacity=q; tf = 'scale(' + (1-0.02*p2) + ')'; }
|
|
555
|
+
else if (type==='slide'){ opacity=q; tf = 'translateX(' + (-p2*44) + 'px)'; }
|
|
556
|
+
else if (type==='scale'){ opacity=q; tf = 'scale(' + (1-0.1*p2) + ')'; }
|
|
557
|
+
else if (type==='rise'){ opacity=q; tf = 'translateY(' + (-p2*40) + 'px)'; }
|
|
558
|
+
}
|
|
559
|
+
el.style.opacity = opacity;
|
|
560
|
+
el.style.transform = tf;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
var isPlaying = true;
|
|
564
|
+
var t = 0;
|
|
565
|
+
var lastSceneIdx = -1;
|
|
566
|
+
var lastFrame = null;
|
|
567
|
+
|
|
568
|
+
function render(){
|
|
569
|
+
var idx = sceneIndexAt(t);
|
|
570
|
+
var scene = SCENES[idx];
|
|
571
|
+
var localT = t - offsets[idx];
|
|
572
|
+
var justEntered = idx !== lastSceneIdx;
|
|
573
|
+
|
|
574
|
+
for (var j = 0; j < SCENES.length; j++) {
|
|
575
|
+
var el = els[SCENES[j].id];
|
|
576
|
+
if (j === idx) {
|
|
577
|
+
el.classList.add('is-live');
|
|
578
|
+
applyTransition(el, SCENES[j].transition, localT, SCENES[j].duration);
|
|
579
|
+
} else {
|
|
580
|
+
el.classList.remove('is-live');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
renderers[scene.kind](scene, localT, justEntered);
|
|
585
|
+
|
|
586
|
+
if (justEntered) {
|
|
587
|
+
sceneTitleEl.textContent = scene.kind.replace(/^\\w/, function(c){ return c.toUpperCase(); });
|
|
588
|
+
sceneTitleEl.classList.add('is-shown');
|
|
589
|
+
captionsEl.textContent = scene.caption;
|
|
590
|
+
}
|
|
591
|
+
lastSceneIdx = idx;
|
|
592
|
+
|
|
593
|
+
var pct = (t/TOTAL*100);
|
|
594
|
+
scrubberFill.style.width = pct + '%';
|
|
595
|
+
scrubberThumb.style.left = pct + '%';
|
|
596
|
+
timeCurrent.textContent = fmt(t);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function loop(ts){
|
|
600
|
+
if (lastFrame === null) lastFrame = ts;
|
|
601
|
+
var dt = ts - lastFrame;
|
|
602
|
+
lastFrame = ts;
|
|
603
|
+
if (isPlaying) { t += dt; if (t >= TOTAL) t = t % TOTAL; }
|
|
604
|
+
render();
|
|
605
|
+
requestAnimationFrame(loop);
|
|
606
|
+
}
|
|
607
|
+
requestAnimationFrame(loop);
|
|
608
|
+
|
|
609
|
+
function setPlaying(v){
|
|
610
|
+
isPlaying = v;
|
|
611
|
+
playBtn.setAttribute('aria-label', v ? 'Pause' : 'Play');
|
|
612
|
+
playIcon.innerHTML = v
|
|
613
|
+
? '<rect x="3" y="2" width="4" height="12" rx="1"/><rect x="9" y="2" width="4" height="12" rx="1"/>'
|
|
614
|
+
: '<path d="M4 2l10 6-10 6z"/>';
|
|
615
|
+
}
|
|
616
|
+
playBtn.addEventListener('click', function(){ setPlaying(!isPlaying); });
|
|
617
|
+
|
|
618
|
+
function seekFromEvent(e){
|
|
619
|
+
var rect = scrubber.getBoundingClientRect();
|
|
620
|
+
var x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
|
|
621
|
+
var p = clamp(x/rect.width, 0, 1);
|
|
622
|
+
t = p * TOTAL;
|
|
623
|
+
lastSceneIdx = -1;
|
|
624
|
+
render();
|
|
625
|
+
}
|
|
626
|
+
var dragging = false;
|
|
627
|
+
scrubber.addEventListener('pointerdown', function(e){
|
|
628
|
+
dragging = true; scrubber.setPointerCapture(e.pointerId); seekFromEvent(e);
|
|
629
|
+
});
|
|
630
|
+
scrubber.addEventListener('pointermove', function(e){ if (dragging) seekFromEvent(e); });
|
|
631
|
+
scrubber.addEventListener('pointerup', function(){ dragging = false; });
|
|
632
|
+
})();
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
function renderHyperframes(scenes, opts = {}) {
|
|
636
|
+
const ratio = opts.ratio ?? "16:9";
|
|
637
|
+
const titleScene = scenes.find((s) => s.kind === "title");
|
|
638
|
+
const docTitle = opts.title ?? titleScene?.data.sessionName ?? "session recap";
|
|
639
|
+
const scenesJson = embedJson(scenes);
|
|
640
|
+
const body = scenes.map(sceneHtml).join("\n ");
|
|
641
|
+
return `<!DOCTYPE html>
|
|
642
|
+
<html lang="en">
|
|
643
|
+
<head>
|
|
644
|
+
<meta charset="UTF-8">
|
|
645
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
646
|
+
<title>vibemovie \u2014 ${escapeHtml(docTitle)}</title>
|
|
647
|
+
<style>${css(ratio)}</style>
|
|
648
|
+
</head>
|
|
649
|
+
<body>
|
|
650
|
+
<div class="stage">
|
|
651
|
+
|
|
652
|
+
<div class="topbar">
|
|
653
|
+
<div>
|
|
654
|
+
<div class="wordmark">vibe<b>movie</b></div>
|
|
655
|
+
<span class="tagline">${escapeHtml(docTitle)}</span>
|
|
656
|
+
</div>
|
|
657
|
+
<div class="badge-local"><span class="dot"></span>local \xB7 no data out</div>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
<div class="player">
|
|
661
|
+
<div class="filmstrip"></div>
|
|
662
|
+
<div class="screen" id="screen">
|
|
663
|
+
${body}
|
|
664
|
+
<div class="scene-title" id="sceneTitle"></div>
|
|
665
|
+
</div>
|
|
666
|
+
<div class="captions" id="captions"></div>
|
|
667
|
+
<div class="controls">
|
|
668
|
+
<button class="icon-btn" id="playBtn" aria-label="Pause">
|
|
669
|
+
<svg id="playIcon" viewBox="0 0 16 16" fill="currentColor"><rect x="3" y="2" width="4" height="12" rx="1"/><rect x="9" y="2" width="4" height="12" rx="1"/></svg>
|
|
670
|
+
</button>
|
|
671
|
+
<div class="time" id="timeCurrent">0:00</div>
|
|
672
|
+
<div class="scrubber" id="scrubber">
|
|
673
|
+
<div class="scrubber-track"><div class="scrubber-fill" id="scrubberFill"></div></div>
|
|
674
|
+
<div class="scrubber-markers" id="scrubberMarkers"></div>
|
|
675
|
+
<div class="scrubber-thumb" id="scrubberThumb"></div>
|
|
676
|
+
</div>
|
|
677
|
+
<div class="time total" id="timeTotal">0:00</div>
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
|
|
681
|
+
<div class="foot">hyperframes \xB7 rendered offline \u2014 no keys, no network, no data out</div>
|
|
682
|
+
|
|
683
|
+
</div>
|
|
684
|
+
<script>${engineJs(scenesJson)}</script>
|
|
685
|
+
</body>
|
|
686
|
+
</html>
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/index.ts
|
|
691
|
+
function createHyperframesRunner() {
|
|
692
|
+
return {
|
|
693
|
+
capability: "video",
|
|
694
|
+
available: () => Promise.resolve(true),
|
|
695
|
+
generate: (req) => {
|
|
696
|
+
const r = req;
|
|
697
|
+
return Promise.resolve(renderHyperframes(r.scenes, { ratio: r.ratio, title: r.title }));
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async function renderMovie(events, opts = {}) {
|
|
702
|
+
const scenes = buildScenes(events, opts);
|
|
703
|
+
const cascade = createCascade({
|
|
704
|
+
consent: createConsentLedger(),
|
|
705
|
+
pickLocal: (capability) => Promise.resolve(capability === "video" ? createHyperframesRunner() : null)
|
|
706
|
+
});
|
|
707
|
+
const resolved = await cascade.resolve({ capability: "video", allowEgress: false });
|
|
708
|
+
const provider = resolved.provider;
|
|
709
|
+
if (resolved.tier !== "local" || !("capability" in provider)) {
|
|
710
|
+
throw new Error("vibemovie v0 renders via the local Hyperframes tier only");
|
|
711
|
+
}
|
|
712
|
+
const req = { scenes };
|
|
713
|
+
if (opts.ratio !== void 0) req.ratio = opts.ratio;
|
|
714
|
+
if (opts.title !== void 0) req.title = opts.title;
|
|
715
|
+
const html = await provider.generate(req);
|
|
716
|
+
if (opts.out !== void 0) {
|
|
717
|
+
await writeFile(opts.out, html, "utf8");
|
|
718
|
+
return { html, path: opts.out };
|
|
719
|
+
}
|
|
720
|
+
return { html };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export {
|
|
724
|
+
RATIOS,
|
|
725
|
+
TEMPLATES,
|
|
726
|
+
buildScenes,
|
|
727
|
+
renderHyperframes,
|
|
728
|
+
renderMovie
|
|
729
|
+
};
|