hyperframes 0.3.0 → 0.3.2

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.
Files changed (33) hide show
  1. package/dist/cli.js +7159 -6411
  2. package/dist/docs/{templates.md → examples.md} +2 -2
  3. package/dist/skills/gsap/SKILL.md +5 -16
  4. package/dist/skills/gsap/references/effects.md +7 -14
  5. package/dist/skills/hyperframes/SKILL.md +124 -43
  6. package/dist/skills/hyperframes/house-style.md +34 -93
  7. package/dist/skills/hyperframes/references/captions.md +2 -2
  8. package/dist/skills/hyperframes/references/css-patterns.md +36 -34
  9. package/dist/skills/hyperframes/references/transitions/catalog.md +4 -19
  10. package/dist/skills/hyperframes/references/transitions/css-other.md +0 -11
  11. package/dist/skills/hyperframes/references/transitions.md +53 -37
  12. package/dist/skills/hyperframes/references/{fonts.md → typography.md} +73 -32
  13. package/dist/skills/hyperframes/scripts/animation-map.mjs +596 -0
  14. package/dist/skills/hyperframes/scripts/contrast-report.mjs +335 -0
  15. package/dist/skills/hyperframes-cli/SKILL.md +1 -1
  16. package/dist/studio/assets/hyperframes-player-C6QOH12J.js +198 -0
  17. package/dist/studio/assets/{index-QlToZFln.js → index-BLIJTYAJ.js} +21 -21
  18. package/dist/studio/assets/index-DZEa45DQ.css +1 -0
  19. package/dist/studio/index.html +2 -2
  20. package/dist/templates/_shared/AGENTS.md +58 -0
  21. package/dist/templates/_shared/CLAUDE.md +8 -9
  22. package/package.json +1 -1
  23. package/dist/skills/gsap/references/frameworks.md +0 -56
  24. package/dist/skills/gsap/references/plugins.md +0 -194
  25. package/dist/skills/gsap/references/react.md +0 -80
  26. package/dist/skills/gsap/references/scrolltrigger.md +0 -147
  27. package/dist/skills/gsap/references/utils.md +0 -91
  28. package/dist/skills/hyperframes/references/examples.md +0 -146
  29. package/dist/skills/hyperframes/references/marker-highlight.md +0 -158
  30. package/dist/skills/hyperframes/references/transitions/shader-setup.md +0 -463
  31. package/dist/skills/hyperframes/references/transitions/shader-transitions.md +0 -329
  32. package/dist/studio/assets/hyperframes-player-Ba4c3ztZ.js +0 -198
  33. package/dist/studio/assets/index-DHr9yo58.css +0 -1
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+ // contrast-report.mjs — HyperFrames contrast audit
3
+ //
4
+ // Reads a composition, seeks to N sample timestamps, walks the DOM for text
5
+ // elements, measures the WCAG 2.1 contrast ratio between each element's
6
+ // declared foreground color and the pixels behind it, and emits:
7
+ //
8
+ // - contrast-report.json (machine-readable, one entry per text element × sample)
9
+ // - contrast-overlay.png (sprite grid; magenta=fail AA, yellow=pass AA only, green=AAA)
10
+ //
11
+ // Usage:
12
+ // node skills/hyperframes/scripts/contrast-report.mjs <composition-dir> \
13
+ // [--samples N] [--out <dir>] [--width W] [--height H] [--fps N]
14
+ //
15
+ // The composition directory must contain an index.html. Raw authoring HTML
16
+ // works — the producer's file server auto-injects the runtime at serve time.
17
+ // Exits 1 if any text element fails WCAG AA.
18
+
19
+ import { mkdir, writeFile } from "node:fs/promises";
20
+ import { resolve } from "node:path";
21
+
22
+ import sharp from "sharp";
23
+
24
+ // Use the producer's file server — it auto-injects the HyperFrames runtime
25
+ // and render-seek bridge, so raw authoring HTML works without a build step.
26
+ import {
27
+ createFileServer,
28
+ createCaptureSession,
29
+ initializeSession,
30
+ closeCaptureSession,
31
+ captureFrameToBuffer,
32
+ getCompositionDuration,
33
+ } from "@hyperframes/producer";
34
+
35
+ // ─── CLI ─────────────────────────────────────────────────────────────────────
36
+
37
+ const args = parseArgs(process.argv.slice(2));
38
+ if (!args.composition) die("missing <composition-dir>");
39
+
40
+ const SAMPLES = Number(args.samples ?? 10);
41
+ const OUT_DIR = resolve(args.out ?? ".hyperframes/contrast");
42
+ const WIDTH = Number(args.width ?? 1920);
43
+ const HEIGHT = Number(args.height ?? 1080);
44
+ const FPS = Number(args.fps ?? 30);
45
+ const COMP_DIR = resolve(args.composition);
46
+
47
+ // ─── Main ────────────────────────────────────────────────────────────────────
48
+
49
+ await mkdir(OUT_DIR, { recursive: true });
50
+
51
+ const server = await createFileServer({ projectDir: COMP_DIR, port: 0 });
52
+ const session = await createCaptureSession(
53
+ server.url,
54
+ OUT_DIR,
55
+ { width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
56
+ null,
57
+ );
58
+ await initializeSession(session);
59
+
60
+ try {
61
+ const duration = await getCompositionDuration(session);
62
+ const times = Array.from(
63
+ { length: SAMPLES },
64
+ (_, i) => +(((i + 0.5) / SAMPLES) * duration).toFixed(3),
65
+ );
66
+
67
+ const allEntries = [];
68
+ const overlayFrames = [];
69
+
70
+ for (let i = 0; i < times.length; i++) {
71
+ const t = times[i];
72
+ const { buffer: pngBuf } = await captureFrameToBuffer(session, i, t);
73
+ const elements = await probeTextElements(session, t);
74
+ const annotated = await annotateFrame(pngBuf, elements);
75
+ overlayFrames.push({ t, png: annotated });
76
+ for (const el of elements) allEntries.push({ time: t, ...el });
77
+ }
78
+
79
+ const report = {
80
+ composition: COMP_DIR,
81
+ width: WIDTH,
82
+ height: HEIGHT,
83
+ duration,
84
+ samples: times,
85
+ entries: allEntries,
86
+ summary: summarize(allEntries),
87
+ };
88
+
89
+ await writeFile(resolve(OUT_DIR, "contrast-report.json"), JSON.stringify(report, null, 2));
90
+ await writeOverlaySprite(overlayFrames, resolve(OUT_DIR, "contrast-overlay.png"));
91
+
92
+ printSummary(report);
93
+ process.exitCode = report.summary.failAA > 0 ? 1 : 0;
94
+ } finally {
95
+ await closeCaptureSession(session).catch(() => {});
96
+ server.close();
97
+ }
98
+
99
+ // ─── DOM probe (runs in the page) ────────────────────────────────────────────
100
+
101
+ async function probeTextElements(session, _t) {
102
+ // `session.page` is the Puppeteer Page owned by the capture session.
103
+ // We pass a pure function to `evaluate`: it walks the DOM and returns
104
+ // enough info for us to compute a ratio in Node using the frame buffer.
105
+ return await session.page.evaluate(() => {
106
+ /** @type {Array<{selector: string, text: string, fg: [number,number,number,number], fontSize: number, fontWeight: number, bbox: {x:number,y:number,w:number,h:number}}>} */
107
+ const out = [];
108
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
109
+ const parseColor = (c) => {
110
+ const m = c.match(/rgba?\(([^)]+)\)/);
111
+ if (!m) return [0, 0, 0, 1];
112
+ const parts = m[1].split(",").map((s) => parseFloat(s.trim()));
113
+ return [parts[0], parts[1], parts[2], parts[3] ?? 1];
114
+ };
115
+ const selectorOf = (el) => {
116
+ if (el.id) return `#${el.id}`;
117
+ const cls = [...el.classList].slice(0, 2).join(".");
118
+ return cls ? `${el.tagName.toLowerCase()}.${cls}` : el.tagName.toLowerCase();
119
+ };
120
+ let el;
121
+ while ((el = walker.nextNode())) {
122
+ // must have direct text
123
+ const direct = [...el.childNodes].some(
124
+ (n) => n.nodeType === 3 && n.textContent.trim().length,
125
+ );
126
+ if (!direct) continue;
127
+ const cs = getComputedStyle(el);
128
+ if (cs.visibility === "hidden" || cs.display === "none") continue;
129
+ if (parseFloat(cs.opacity) <= 0.01) continue;
130
+ const rect = el.getBoundingClientRect();
131
+ if (rect.width < 8 || rect.height < 8) continue;
132
+ out.push({
133
+ selector: selectorOf(el),
134
+ text: el.textContent.trim().slice(0, 60),
135
+ fg: parseColor(cs.color),
136
+ fontSize: parseFloat(cs.fontSize),
137
+ fontWeight: Number(cs.fontWeight) || 400,
138
+ bbox: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
139
+ });
140
+ }
141
+ return out;
142
+ });
143
+ }
144
+
145
+ // ─── Pixel sampling + WCAG math ──────────────────────────────────────────────
146
+
147
+ async function annotateFrame(pngBuf, elements) {
148
+ const img = sharp(pngBuf);
149
+ const meta = await img.metadata();
150
+ const { width, height } = meta;
151
+ const raw = await img.ensureAlpha().raw().toBuffer();
152
+ const channels = 4;
153
+
154
+ const measured = [];
155
+ for (const el of elements) {
156
+ const bg = sampleRingMedian(raw, width, height, channels, el.bbox);
157
+ const fg = compositeOver(el.fg, bg); // flatten any alpha against measured bg
158
+ const ratio = wcagRatio(fg, bg);
159
+ const large = isLargeText(el.fontSize, el.fontWeight);
160
+ el.bg = bg;
161
+ el.ratio = +ratio.toFixed(2);
162
+ el.wcagAA = large ? ratio >= 3 : ratio >= 4.5;
163
+ el.wcagAALarge = ratio >= 3;
164
+ el.wcagAAA = large ? ratio >= 4.5 : ratio >= 7;
165
+ measured.push(el);
166
+ }
167
+
168
+ // Draw boxes + ratio labels as an SVG overlay (sharp composite).
169
+ const svg = buildOverlaySVG(measured, width, height);
170
+ return await sharp(pngBuf)
171
+ .composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
172
+ .png()
173
+ .toBuffer();
174
+ }
175
+
176
+ function sampleRingMedian(raw, width, height, channels, bbox) {
177
+ // 4-px ring immediately outside the element bbox. Median of each channel.
178
+ const r = [],
179
+ g = [],
180
+ b = [];
181
+ const x0 = Math.max(0, Math.floor(bbox.x) - 4);
182
+ const x1 = Math.min(width - 1, Math.ceil(bbox.x + bbox.w) + 4);
183
+ const y0 = Math.max(0, Math.floor(bbox.y) - 4);
184
+ const y1 = Math.min(height - 1, Math.ceil(bbox.y + bbox.h) + 4);
185
+ const pushPixel = (x, y) => {
186
+ const i = (y * width + x) * channels;
187
+ r.push(raw[i]);
188
+ g.push(raw[i + 1]);
189
+ b.push(raw[i + 2]);
190
+ };
191
+ for (let x = x0; x <= x1; x++) {
192
+ pushPixel(x, y0);
193
+ pushPixel(x, y1);
194
+ }
195
+ for (let y = y0; y <= y1; y++) {
196
+ pushPixel(x0, y);
197
+ pushPixel(x1, y);
198
+ }
199
+ return [median(r), median(g), median(b), 1];
200
+ }
201
+
202
+ function median(arr) {
203
+ const s = [...arr].sort((a, b) => a - b);
204
+ return s[Math.floor(s.length / 2)];
205
+ }
206
+
207
+ function compositeOver([fr, fg, fb, fa], [br, bg, bb]) {
208
+ return [
209
+ Math.round(fr * fa + br * (1 - fa)),
210
+ Math.round(fg * fa + bg * (1 - fa)),
211
+ Math.round(fb * fa + bb * (1 - fa)),
212
+ 1,
213
+ ];
214
+ }
215
+
216
+ function relLum([r, g, b]) {
217
+ const ch = (v) => {
218
+ const s = v / 255;
219
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
220
+ };
221
+ return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b);
222
+ }
223
+
224
+ function wcagRatio(a, b) {
225
+ const la = relLum(a);
226
+ const lb = relLum(b);
227
+ const [L1, L2] = la > lb ? [la, lb] : [lb, la];
228
+ return (L1 + 0.05) / (L2 + 0.05);
229
+ }
230
+
231
+ function isLargeText(fontSize, fontWeight) {
232
+ return fontSize >= 24 || (fontSize >= 19 && fontWeight >= 700);
233
+ }
234
+
235
+ // ─── Overlay rendering ───────────────────────────────────────────────────────
236
+
237
+ function buildOverlaySVG(elements, w, h) {
238
+ const rects = elements
239
+ .map((el) => {
240
+ const color = !el.wcagAA ? "#ff00aa" : !el.wcagAAA ? "#ffcc00" : "#00e08a";
241
+ const { x, y, w: bw, h: bh } = el.bbox;
242
+ return `
243
+ <rect x="${x}" y="${y}" width="${bw}" height="${bh}"
244
+ fill="none" stroke="${color}" stroke-width="3"/>
245
+ <rect x="${x}" y="${y - 18}" width="${48}" height="16" fill="${color}"/>
246
+ <text x="${x + 4}" y="${y - 5}" font-family="monospace" font-size="12" fill="#000">
247
+ ${el.ratio.toFixed(1)}:1
248
+ </text>`;
249
+ })
250
+ .join("");
251
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">${rects}</svg>`;
252
+ }
253
+
254
+ async function writeOverlaySprite(frames, outPath) {
255
+ if (!frames.length) return;
256
+ const cols = Math.min(frames.length, 5);
257
+ const rows = Math.ceil(frames.length / cols);
258
+ const { width, height } = await sharp(frames[0].png).metadata();
259
+ const scale = 0.25;
260
+ const cellW = Math.round(width * scale);
261
+ const cellH = Math.round(height * scale);
262
+
263
+ const cells = await Promise.all(
264
+ frames.map(async (f) => ({
265
+ input: await sharp(f.png).resize(cellW, cellH).png().toBuffer(),
266
+ time: f.t,
267
+ })),
268
+ );
269
+
270
+ const composites = cells.map((c, i) => ({
271
+ input: c.input,
272
+ top: Math.floor(i / cols) * cellH,
273
+ left: (i % cols) * cellW,
274
+ }));
275
+
276
+ await sharp({
277
+ create: {
278
+ width: cols * cellW,
279
+ height: rows * cellH,
280
+ channels: 3,
281
+ background: { r: 16, g: 16, b: 20 },
282
+ },
283
+ })
284
+ .composite(composites)
285
+ .png()
286
+ .toFile(outPath);
287
+ }
288
+
289
+ // ─── Summary ────────────────────────────────────────────────────────────────
290
+
291
+ function summarize(entries) {
292
+ const total = entries.length;
293
+ const failAA = entries.filter((e) => !e.wcagAA).length;
294
+ const passAAonly = entries.filter((e) => e.wcagAA && !e.wcagAAA).length;
295
+ const passAAA = entries.filter((e) => e.wcagAAA).length;
296
+ return { total, failAA, passAAonly, passAAA };
297
+ }
298
+
299
+ function printSummary({ summary, entries }) {
300
+ const { total, failAA, passAAonly, passAAA } = summary;
301
+ console.log(`\nContrast report: ${total} text-element samples`);
302
+ console.log(` fail WCAG AA: ${failAA}`);
303
+ console.log(` pass AA, not AAA: ${passAAonly}`);
304
+ console.log(` pass AAA: ${passAAA}`);
305
+ if (failAA) {
306
+ console.log("\nFailures:");
307
+ for (const e of entries.filter((x) => !x.wcagAA)) {
308
+ console.log(` t=${e.time}s ${e.selector.padEnd(24)} ${e.ratio.toFixed(2)}:1 "${e.text}"`);
309
+ }
310
+ }
311
+ }
312
+
313
+ // ─── Utilities ──────────────────────────────────────────────────────────────
314
+
315
+ function parseArgs(argv) {
316
+ const out = {};
317
+ let positional = 0;
318
+ for (let i = 0; i < argv.length; i++) {
319
+ const a = argv[i];
320
+ if (a.startsWith("--")) {
321
+ const k = a.slice(2);
322
+ const v = argv[i + 1]?.startsWith("--") ? true : argv[++i];
323
+ out[k] = v;
324
+ } else if (positional === 0) {
325
+ out.composition = a;
326
+ positional++;
327
+ }
328
+ }
329
+ return out;
330
+ }
331
+
332
+ function die(msg) {
333
+ console.error(`contrast-report: ${msg}`);
334
+ process.exit(2);
335
+ }
@@ -21,7 +21,7 @@ Lint before preview — catches missing `data-composition-id`, overlapping track
21
21
 
22
22
  ```bash
23
23
  npx hyperframes init my-video # interactive wizard
24
- npx hyperframes init my-video --template warm-grain # pick a template
24
+ npx hyperframes init my-video --example warm-grain # pick an example
25
25
  npx hyperframes init my-video --video clip.mp4 # with video file
26
26
  npx hyperframes init my-video --audio track.mp3 # with audio file
27
27
  npx hyperframes init my-video --non-interactive # skip prompts (CI/agents)
@@ -0,0 +1,198 @@
1
+ var T=Object.defineProperty;var N=(p,e,t)=>e in p?T(p,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):p[e]=t;var h=(p,e,t)=>N(p,typeof e!="symbol"?e+"":e,t);var R=`
2
+ :host {
3
+ display: block;
4
+ position: relative;
5
+ overflow: hidden;
6
+ background: #000;
7
+ contain: layout style;
8
+ }
9
+
10
+ .hfp-container {
11
+ position: absolute;
12
+ inset: 0;
13
+ overflow: hidden;
14
+ pointer-events: none;
15
+ }
16
+
17
+
18
+ .hfp-iframe {
19
+ position: absolute;
20
+ top: 50%;
21
+ left: 50%;
22
+ border: none;
23
+ pointer-events: none;
24
+ }
25
+
26
+ .hfp-poster {
27
+ position: absolute;
28
+ inset: 0;
29
+ object-fit: contain;
30
+ z-index: 1;
31
+ pointer-events: none;
32
+ }
33
+
34
+ /* ── Theming via CSS custom properties ──
35
+ *
36
+ * Override from outside the shadow DOM:
37
+ * hyperframes-player {
38
+ * --hfp-controls-bg: linear-gradient(transparent, rgba(0,0,0,0.9));
39
+ * --hfp-accent: #ff6b6b;
40
+ * --hfp-font: "Inter", sans-serif;
41
+ * }
42
+ */
43
+
44
+ .hfp-controls {
45
+ position: absolute;
46
+ bottom: 0;
47
+ left: 0;
48
+ right: 0;
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--hfp-controls-gap, 12px);
52
+ padding: var(--hfp-controls-padding, 8px 16px);
53
+ background: var(--hfp-controls-bg, linear-gradient(transparent, rgba(0, 0, 0, 0.7)));
54
+ color: var(--hfp-color, #fff);
55
+ font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
56
+ font-size: var(--hfp-font-size, 13px);
57
+ z-index: 10;
58
+ pointer-events: auto;
59
+ opacity: 1;
60
+ transition: opacity 0.3s ease;
61
+ user-select: none;
62
+ }
63
+
64
+ .hfp-controls.hfp-hidden {
65
+ opacity: 0;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .hfp-play-btn {
70
+ background: none;
71
+ border: none;
72
+ color: var(--hfp-color, #fff);
73
+ cursor: pointer;
74
+ padding: 8px;
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: 40px;
79
+ height: 40px;
80
+ flex-shrink: 0;
81
+ z-index: 10;
82
+ }
83
+
84
+ .hfp-play-btn:hover {
85
+ opacity: 0.8;
86
+ }
87
+
88
+ .hfp-play-btn svg,
89
+ .hfp-play-btn svg * {
90
+ pointer-events: none;
91
+ }
92
+
93
+ .hfp-scrubber {
94
+ flex: 1;
95
+ height: var(--hfp-scrubber-height, 4px);
96
+ background: var(--hfp-scrubber-bg, rgba(255, 255, 255, 0.3));
97
+ border-radius: var(--hfp-scrubber-radius, 2px);
98
+ cursor: pointer;
99
+ position: relative;
100
+ }
101
+
102
+ .hfp-scrubber:hover {
103
+ height: var(--hfp-scrubber-height-hover, 6px);
104
+ }
105
+
106
+ .hfp-progress {
107
+ position: absolute;
108
+ top: 0;
109
+ left: 0;
110
+ height: 100%;
111
+ background: var(--hfp-accent, #fff);
112
+ border-radius: var(--hfp-scrubber-radius, 2px);
113
+ pointer-events: none;
114
+ }
115
+
116
+ .hfp-time {
117
+ flex-shrink: 0;
118
+ font-variant-numeric: tabular-nums;
119
+ opacity: 0.9;
120
+ }
121
+
122
+ .hfp-speed-wrap {
123
+ position: relative;
124
+ flex-shrink: 0;
125
+ }
126
+
127
+ .hfp-speed-btn {
128
+ background: var(--hfp-speed-btn-bg, rgba(255, 255, 255, 0.15));
129
+ border: none;
130
+ border-radius: var(--hfp-speed-btn-radius, 4px);
131
+ color: var(--hfp-color, #fff);
132
+ cursor: pointer;
133
+ font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
134
+ font-size: 12px;
135
+ font-variant-numeric: tabular-nums;
136
+ font-weight: 600;
137
+ padding: 4px 8px;
138
+ min-width: 40px;
139
+ text-align: center;
140
+ transition: background 0.15s ease;
141
+ }
142
+
143
+ .hfp-speed-btn:hover {
144
+ background: var(--hfp-speed-btn-bg-hover, rgba(255, 255, 255, 0.3));
145
+ }
146
+
147
+ .hfp-speed-menu {
148
+ position: absolute;
149
+ bottom: calc(100% + 8px);
150
+ right: 0;
151
+ background: var(--hfp-menu-bg, rgba(20, 20, 20, 0.95));
152
+ backdrop-filter: blur(12px);
153
+ -webkit-backdrop-filter: blur(12px);
154
+ border: 1px solid var(--hfp-menu-border, rgba(255, 255, 255, 0.1));
155
+ border-radius: var(--hfp-menu-radius, 8px);
156
+ padding: 4px;
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: 2px;
160
+ min-width: 80px;
161
+ opacity: 0;
162
+ visibility: hidden;
163
+ transform: translateY(4px);
164
+ transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
165
+ box-shadow: var(--hfp-menu-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
166
+ }
167
+
168
+ .hfp-speed-menu.hfp-open {
169
+ opacity: 1;
170
+ visibility: visible;
171
+ transform: translateY(0);
172
+ }
173
+
174
+ .hfp-speed-option {
175
+ background: none;
176
+ border: none;
177
+ border-radius: 4px;
178
+ color: var(--hfp-menu-color, rgba(255, 255, 255, 0.7));
179
+ cursor: pointer;
180
+ font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
181
+ font-size: 13px;
182
+ font-variant-numeric: tabular-nums;
183
+ padding: 6px 12px;
184
+ text-align: left;
185
+ transition: background 0.1s ease, color 0.1s ease;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ .hfp-speed-option:hover {
190
+ background: var(--hfp-menu-hover-bg, rgba(255, 255, 255, 0.1));
191
+ color: var(--hfp-color, #fff);
192
+ }
193
+
194
+ .hfp-speed-option.hfp-active {
195
+ color: var(--hfp-accent, #fff);
196
+ font-weight: 600;
197
+ }
198
+ `,I='<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><polygon points="4,2 16,9 4,16"/></svg>',F='<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><rect x="3" y="2" width="4" height="14"/><rect x="11" y="2" width="4" height="14"/></svg>',O=[.25,.5,1,1.5,2,4];function y(p){return Number.isInteger(p)?`${p}x`:`${p}x`}function S(p){if(!Number.isFinite(p)||p<0)return"0:00";let e=Math.floor(p),t=Math.floor(e/60),s=e%60;return`${t}:${s.toString().padStart(2,"0")}`}function j(p,e,t={}){let s=t.speedPresets??O,i=document.createElement("div");i.className="hfp-controls",i.addEventListener("click",r=>{r.stopPropagation()});let a=document.createElement("button");a.className="hfp-play-btn",a.type="button",a.innerHTML=I,a.setAttribute("aria-label","Play");let o=document.createElement("div");o.className="hfp-scrubber";let d=document.createElement("div");d.className="hfp-progress",d.style.width="0%",o.appendChild(d);let u=document.createElement("span");u.className="hfp-time",u.textContent="0:00 / 0:00";let m=document.createElement("div");m.className="hfp-speed-wrap";let l=document.createElement("button");l.className="hfp-speed-btn",l.type="button",l.textContent="1x",l.setAttribute("aria-label","Playback speed");let c=document.createElement("div");c.className="hfp-speed-menu",c.setAttribute("role","menu");for(let r of s){let n=document.createElement("button");n.className="hfp-speed-option",n.type="button",n.setAttribute("role","menuitem"),n.dataset.speed=String(r),n.textContent=y(r),r===1&&n.classList.add("hfp-active"),c.appendChild(n)}m.appendChild(c),m.appendChild(l),i.appendChild(a),i.appendChild(o),i.appendChild(u),i.appendChild(m),p.appendChild(i);let v=!1,b=null;s.indexOf(1),a.addEventListener("click",r=>{r.stopPropagation(),v?e.onPause():e.onPlay()});let x=r=>{for(let n of c.querySelectorAll(".hfp-speed-option"))n.classList.toggle("hfp-active",n.dataset.speed===String(r))};l.addEventListener("click",r=>{r.stopPropagation();let n=c.classList.toggle("hfp-open");l.setAttribute("aria-expanded",String(n))}),c.addEventListener("click",r=>{r.stopPropagation();let n=r.target.closest(".hfp-speed-option");if(!n)return;let f=parseFloat(n.dataset.speed);s.indexOf(f),l.textContent=y(f),x(f),c.classList.remove("hfp-open"),l.setAttribute("aria-expanded","false"),e.onSpeedChange(f)});let w=()=>{c.classList.remove("hfp-open"),l.setAttribute("aria-expanded","false")};document.addEventListener("click",w);let _=r=>{let n=o.getBoundingClientRect(),f=Math.max(0,Math.min(1,(r-n.left)/n.width));e.onSeek(f)},g=!1;o.addEventListener("mousedown",r=>{r.stopPropagation(),g=!0,_(r.clientX)});let k=r=>{g&&_(r.clientX)},A=()=>{g=!1};document.addEventListener("mousemove",k),document.addEventListener("mouseup",A),o.addEventListener("touchstart",r=>{g=!0;let n=r.touches[0];n&&_(n.clientX)},{passive:!0});let C=r=>{if(g){let n=r.touches[0];n&&_(n.clientX)}},P=()=>{g=!1};document.addEventListener("touchmove",C,{passive:!0}),document.addEventListener("touchend",P);let M=()=>{b&&clearTimeout(b),b=setTimeout(()=>{v&&i.classList.add("hfp-hidden")},3e3)},L=p instanceof ShadowRoot?p.host:p;return L.addEventListener("mousemove",()=>{i.classList.remove("hfp-hidden"),M()}),L.addEventListener("mouseleave",()=>{v&&i.classList.add("hfp-hidden")}),{updateTime(r,n){let f=n>0?r/n*100:0;d.style.width=`${f}%`,u.textContent=`${S(r)} / ${S(n)}`},updatePlaying(r){v=r,a.innerHTML=r?F:I,a.setAttribute("aria-label",r?"Pause":"Play"),r?M():i.classList.remove("hfp-hidden")},updateSpeed(r){s.indexOf(r),l.textContent=y(r),x(r)},show(){i.style.display=""},hide(){i.style.display="none"},destroy(){document.removeEventListener("mousemove",k),document.removeEventListener("mouseup",A),document.removeEventListener("touchmove",C),document.removeEventListener("touchend",P),document.removeEventListener("click",w),b&&clearTimeout(b)}}}var E=30,z="https://cdn.jsdelivr.net/npm/@hyperframes/core/dist/hyperframe.runtime.iife.js",D=class extends HTMLElement{constructor(){super();h(this,"shadow");h(this,"container");h(this,"iframe");h(this,"posterEl",null);h(this,"controlsApi",null);h(this,"resizeObserver");h(this,"_ready",!1);h(this,"_duration",0);h(this,"_currentTime",0);h(this,"_paused",!0);h(this,"_compositionWidth",1920);h(this,"_compositionHeight",1080);h(this,"_probeInterval",null);h(this,"_lastUpdateMs",0);h(this,"_parentMedia",[]);h(this,"_runtimeInjected",!1);this.shadow=this.attachShadow({mode:"open"});let e=document.createElement("style");e.textContent=R,this.shadow.appendChild(e),this.container=document.createElement("div"),this.container.className="hfp-container",this.iframe=document.createElement("iframe"),this.iframe.className="hfp-iframe",this.iframe.sandbox.add("allow-scripts","allow-same-origin"),this.iframe.allow="autoplay; fullscreen",this.iframe.referrerPolicy="no-referrer",this.iframe.title="HyperFrames Composition",this.container.appendChild(this.iframe),this.shadow.appendChild(this.container),this.addEventListener("click",t=>{this._isControlsClick(t)||(this._paused?this.play():this.pause())}),this.resizeObserver=new ResizeObserver(()=>this._updateScale()),this._onMessage=this._onMessage.bind(this),this._onIframeLoad=this._onIframeLoad.bind(this)}static get observedAttributes(){return["src","width","height","controls","muted","poster","playback-rate","audio-src"]}connectedCallback(){this.resizeObserver.observe(this),window.addEventListener("message",this._onMessage),this.iframe.addEventListener("load",this._onIframeLoad),this.hasAttribute("controls")&&this._setupControls(),this.hasAttribute("poster")&&this._setupPoster(),this.hasAttribute("audio-src")&&this._setupParentAudioFromUrl(this.getAttribute("audio-src")),this.hasAttribute("src")&&(this.iframe.src=this.getAttribute("src"))}disconnectedCallback(){var e;this.resizeObserver.disconnect(),window.removeEventListener("message",this._onMessage),this.iframe.removeEventListener("load",this._onIframeLoad),this._probeInterval&&clearInterval(this._probeInterval),(e=this.controlsApi)==null||e.destroy();for(let t of this._parentMedia)t.el.pause(),t.el.src="";this._parentMedia=[]}attributeChangedCallback(e,t,s){var i,a;switch(e){case"src":s&&(this._ready=!1,this.iframe.src=s);break;case"width":this._compositionWidth=parseInt(s||"1920",10),this._updateScale();break;case"height":this._compositionHeight=parseInt(s||"1080",10),this._updateScale();break;case"controls":s!==null?this._setupControls():((i=this.controlsApi)==null||i.destroy(),this.controlsApi=null);break;case"poster":this._setupPoster();break;case"playback-rate":{let o=parseFloat(s||"1");for(let d of this._parentMedia)d.el.playbackRate=o;this._sendControl("set-playback-rate",{playbackRate:o}),(a=this.controlsApi)==null||a.updateSpeed(o),this.dispatchEvent(new Event("ratechange"));break}case"muted":for(let o of this._parentMedia)o.el.muted=s!==null;this._sendControl("set-muted",{muted:s!==null});break;case"audio-src":s&&this._setupParentAudioFromUrl(s);break}}get iframeElement(){return this.iframe}play(){var e;this._hidePoster(),this._playParentMedia(),this._sendControl("play"),this._paused=!1,(e=this.controlsApi)==null||e.updatePlaying(!0),this.dispatchEvent(new Event("play"))}pause(){var e;this._pauseParentMedia(),this._sendControl("pause"),this._paused=!0,(e=this.controlsApi)==null||e.updatePlaying(!1),this.dispatchEvent(new Event("pause"))}seek(e){var s,i;let t=Math.round(e*E);this._sendControl("seek",{frame:t}),this._currentTime=e;for(let a of this._parentMedia){let o=e-a.start;o>=0&&o<a.duration&&(a.el.currentTime=o)}this._paused=!0,(s=this.controlsApi)==null||s.updatePlaying(!1),(i=this.controlsApi)==null||i.updateTime(this._currentTime,this._duration)}get currentTime(){return this._currentTime}set currentTime(e){this.seek(e)}get duration(){return this._duration}get paused(){return this._paused}get ready(){return this._ready}get playbackRate(){return parseFloat(this.getAttribute("playback-rate")||"1")}set playbackRate(e){this.setAttribute("playback-rate",String(e))}get muted(){return this.hasAttribute("muted")}set muted(e){e?this.setAttribute("muted",""):this.removeAttribute("muted")}get loop(){return this.hasAttribute("loop")}set loop(e){e?this.setAttribute("loop",""):this.removeAttribute("loop")}_sendControl(e,t={}){var s;try{(s=this.iframe.contentWindow)==null||s.postMessage({source:"hf-parent",type:"control",action:e,...t},"*")}catch{}}_isControlsClick(e){return e.composedPath().some(t=>t instanceof HTMLElement&&t.classList.contains("hfp-controls"))}_onMessage(e){var s,i,a,o;if(e.source!==this.iframe.contentWindow)return;let t=e.data;if(!(!t||t.source!=="hf-preview")){if(t.type==="state"){this._currentTime=(t.frame??0)/E;let d=!this._paused;this._paused=!t.isPlaying,d&&this._paused?this._pauseParentMedia():!d&&!this._paused&&this._playParentMedia();let u=performance.now();(u-this._lastUpdateMs>100||this._paused!==d)&&(this._lastUpdateMs=u,(s=this.controlsApi)==null||s.updateTime(this._currentTime,this._duration),(i=this.controlsApi)==null||i.updatePlaying(!this._paused),this.dispatchEvent(new CustomEvent("timeupdate",{detail:{currentTime:this._currentTime}}))),this._currentTime>=this._duration&&!this._paused&&(this._pauseParentMedia(),this.loop?(this.seek(0),this.play()):(this._paused=!0,(a=this.controlsApi)==null||a.updatePlaying(!1),this.dispatchEvent(new Event("ended"))))}t.type==="timeline"&&t.durationInFrames>0&&Number.isFinite(t.durationInFrames)&&(this._duration=t.durationInFrames/E,(o=this.controlsApi)==null||o.updateTime(this._currentTime,this._duration)),t.type==="stage-size"&&t.width>0&&t.height>0&&(this._compositionWidth=t.width,this._compositionHeight=t.height,this._updateScale())}}_onIframeLoad(){let e=0;this._runtimeInjected=!1,this._probeInterval&&clearInterval(this._probeInterval),this._probeInterval=setInterval(()=>{var t,s;e++;try{let i=this.iframe.contentWindow;if(!i)return;let a=!!(i.__hf||i.__player),o=!!(i.__timelines&&Object.keys(i.__timelines).length>0);if(!a&&o&&!this._runtimeInjected&&e>=5){this._injectRuntime();return}if(this._runtimeInjected&&!a)return;let d=(()=>{var u,m;if(i.__player&&typeof i.__player.getDuration=="function")return i.__player;if(i.__timelines){let l=Object.keys(i.__timelines);if(l.length>0){let c=(m=(u=this.iframe.contentDocument)==null?void 0:u.querySelector("[data-composition-id]"))==null?void 0:m.getAttribute("data-composition-id"),v=c&&c in i.__timelines?c:l[l.length-1],b=i.__timelines[v];return{getDuration:()=>b.duration()}}}return null})();if(d&&d.getDuration()>0){clearInterval(this._probeInterval),this._duration=d.getDuration(),this._ready=!0,(t=this.controlsApi)==null||t.updateTime(0,this._duration),this.dispatchEvent(new CustomEvent("ready",{detail:{duration:this._duration}}));let u=(s=this.iframe.contentDocument)==null?void 0:s.querySelector("[data-composition-id]");if(u){let m=parseInt(u.getAttribute("data-width")||"0",10),l=parseInt(u.getAttribute("data-height")||"0",10);m>0&&l>0&&(this._compositionWidth=m,this._compositionHeight=l,this._updateScale())}this._setupParentMedia(),this.hasAttribute("autoplay")&&this.play();return}}catch{}e>=40&&(clearInterval(this._probeInterval),this.dispatchEvent(new CustomEvent("error",{detail:{message:"Composition timeline not found after 8s"}})))},200)}_injectRuntime(){this._runtimeInjected=!0;try{let e=this.iframe.contentDocument;if(!e)return;let t=e.createElement("script");t.src=z,t.onload=()=>{},t.onerror=()=>{},(e.head||e.documentElement).appendChild(t)}catch{}}_updateScale(){let e=this.getBoundingClientRect();if(e.width===0||e.height===0)return;let t=Math.min(e.width/this._compositionWidth,e.height/this._compositionHeight);this.iframe.style.width=`${this._compositionWidth}px`,this.iframe.style.height=`${this._compositionHeight}px`,this.iframe.style.transform=`translate(-50%, -50%) scale(${t})`}_setupControls(){if(this.controlsApi)return;let e={onPlay:()=>this.play(),onPause:()=>this.pause(),onSeek:i=>this.seek(i*this._duration),onSpeedChange:i=>{this.playbackRate=i}},t=this.getAttribute("speed-presets"),s=t?t.split(",").map(Number).filter(i=>!isNaN(i)&&i>0):void 0;this.controlsApi=j(this.shadow,e,{speedPresets:s})}_setupPoster(){var t;let e=this.getAttribute("poster");if(!e){(t=this.posterEl)==null||t.remove(),this.posterEl=null;return}this.posterEl||(this.posterEl=document.createElement("img"),this.posterEl.className="hfp-poster",this.shadow.appendChild(this.posterEl)),this.posterEl.src=e}_playParentMedia(){for(let e of this._parentMedia)e.el.src&&e.el.play().then(()=>{this._muteIframeMedia()}).catch(()=>{})}_muteIframeMedia(){try{let e=this.iframe.contentDocument;if(!e)return;let t=e.querySelectorAll("audio[data-start], video[data-start]");for(let s of t)s.volume=0}catch{}}_pauseParentMedia(){for(let e of this._parentMedia)e.el.pause()}_createParentMedia(e,t,s,i){if(this._parentMedia.some(o=>o.el.src===e))return;let a=t==="video"?document.createElement("video"):new Audio;a.preload="auto",a.src=e,a.load(),a.muted=this.muted,this.playbackRate!==1&&(a.playbackRate=this.playbackRate),this._parentMedia.push({el:a,start:s,duration:i})}_setupParentAudioFromUrl(e){this._createParentMedia(e,"audio",0,1/0)}_setupParentMedia(){var e;try{let t=this.iframe.contentDocument;if(!t)return;let s=t.querySelectorAll("audio[data-start], video[data-start]");for(let i of s){let a=i.getAttribute("src")||((e=i.querySelector("source"))==null?void 0:e.src);if(!a)continue;let o=parseFloat(i.getAttribute("data-start")||"0"),d=parseFloat(i.getAttribute("data-duration")||"Infinity"),u=i.tagName==="VIDEO"?"video":"audio";this._createParentMedia(a,u,o,d)}}catch{}}_hidePoster(){var e;(e=this.posterEl)==null||e.remove(),this.posterEl=null}};customElements.get("hyperframes-player")||customElements.define("hyperframes-player",D);export{D as HyperframesPlayer,O as SPEED_PRESETS,y as formatSpeed,S as formatTime};