@reshotdev/screenshot 0.0.1-beta.23 → 0.0.1-beta.25

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.
@@ -1,1997 +0,0 @@
1
- import { createRequire as __reshotCreateRequire } from 'module'; const require = __reshotCreateRequire(import.meta.url);
2
-
3
- // src/jsx-runtime.ts
4
- var Fragment = /* @__PURE__ */ Symbol("Fragment");
5
- function normalizeChildren(value) {
6
- if (value == null || value === false || value === true) return [];
7
- if (Array.isArray(value)) return value.flatMap(normalizeChildren);
8
- return [value];
9
- }
10
- function jsx(type, props, key) {
11
- const nextProps = props ?? {};
12
- const { children, ...rest } = nextProps;
13
- return {
14
- $$compose: true,
15
- type,
16
- props: rest,
17
- children: normalizeChildren(children),
18
- key
19
- };
20
- }
21
- function jsxs(type, props, key) {
22
- return jsx(type, props, key);
23
- }
24
- var jsxDEV = jsx;
25
-
26
- // src/timeline/context.ts
27
- var WorkflowContextProvider = /* @__PURE__ */ Symbol(
28
- "WorkflowContextProvider"
29
- );
30
- var workflowStack = [];
31
- function createWorkflowProvider(value, children) {
32
- return {
33
- $$compose: true,
34
- type: WorkflowContextProvider,
35
- props: { value },
36
- children: normalizeChildren(children)
37
- };
38
- }
39
- function withWorkflowContext(value, fn) {
40
- workflowStack.push(value);
41
- try {
42
- return fn();
43
- } finally {
44
- workflowStack.pop();
45
- }
46
- }
47
- function getWorkflowContext() {
48
- return workflowStack[workflowStack.length - 1];
49
- }
50
- function useWorkflowContext() {
51
- const context = getWorkflowContext();
52
- if (!context) {
53
- throw new Error(
54
- "Compose primitive used outside <Composition>. Wrap the tree in <Composition workflow={...}>."
55
- );
56
- }
57
- return context;
58
- }
59
-
60
- // src/compile/jsx-to-html.ts
61
- var VOID_ELEMENTS = /* @__PURE__ */ new Set([
62
- "area",
63
- "base",
64
- "br",
65
- "col",
66
- "embed",
67
- "hr",
68
- "img",
69
- "input",
70
- "link",
71
- "meta",
72
- "param",
73
- "source",
74
- "track",
75
- "wbr"
76
- ]);
77
- var UNITLESS_CSS = /* @__PURE__ */ new Set([
78
- "aspectRatio",
79
- "columnCount",
80
- "flex",
81
- "flexGrow",
82
- "flexShrink",
83
- "fontWeight",
84
- "lineHeight",
85
- "opacity",
86
- "order",
87
- "scale",
88
- "zIndex",
89
- "zoom"
90
- ]);
91
- function compile(node) {
92
- const context = { styles: [] };
93
- return {
94
- html: renderNode(node, context),
95
- styles: context.styles
96
- };
97
- }
98
- function compileToHtml(node) {
99
- return compile(node).html;
100
- }
101
- function isComposeElement(value) {
102
- return typeof value === "object" && value !== null && value.$$compose === true;
103
- }
104
- function childrenProp(children) {
105
- if (children.length === 0) return void 0;
106
- if (children.length === 1) return children[0];
107
- return children;
108
- }
109
- function renderNode(node, context) {
110
- if (node == null || node === false || node === true) return "";
111
- if (typeof node === "string") return escapeText(node);
112
- if (typeof node === "number") return escapeText(String(node));
113
- if (Array.isArray(node)) {
114
- return node.map((child) => renderNode(child, context)).join("");
115
- }
116
- if (!isComposeElement(node)) return "";
117
- if (node.type === Fragment) {
118
- return node.children.map((child) => renderNode(child, context)).join("");
119
- }
120
- if (node.type === WorkflowContextProvider) {
121
- const value = node.props.value;
122
- return withWorkflowContext(
123
- value,
124
- () => node.children.map((child) => renderNode(child, context)).join("")
125
- );
126
- }
127
- if (typeof node.type === "function") {
128
- const component = node.type;
129
- const rendered = component({
130
- ...node.props,
131
- children: childrenProp(node.children)
132
- });
133
- return renderNode(rendered, context);
134
- }
135
- if (typeof node.type !== "string") return "";
136
- const attrs = serializeAttrs(node.props);
137
- if (VOID_ELEMENTS.has(node.type)) {
138
- return `<${node.type}${attrs} />`;
139
- }
140
- if (node.type === "style") {
141
- const css = rawTextContent(node.children);
142
- context.styles.push(css);
143
- return `<style${attrs}>${css}</style>`;
144
- }
145
- if (node.type === "script") {
146
- return `<script${attrs}>${rawTextContent(node.children)}</script>`;
147
- }
148
- const inner = node.children.map((child) => renderNode(child, context)).join("");
149
- return `<${node.type}${attrs}>${inner}</${node.type}>`;
150
- }
151
- function serializeAttrs(props) {
152
- const attrs = [];
153
- for (const [rawName, rawValue] of Object.entries(props)) {
154
- if (rawName === "children" || rawName === "key" || rawName === "ref") {
155
- continue;
156
- }
157
- if (rawValue == null || rawValue === false) continue;
158
- const name = attrName(rawName);
159
- if (rawValue === true) {
160
- attrs.push(name);
161
- continue;
162
- }
163
- if (name === "style" && typeof rawValue === "object") {
164
- const css = styleObjectToString(rawValue);
165
- if (css) attrs.push(`style="${escapeAttr(css)}"`);
166
- continue;
167
- }
168
- attrs.push(`${name}="${escapeAttr(String(rawValue))}"`);
169
- }
170
- return attrs.length === 0 ? "" : ` ${attrs.join(" ")}`;
171
- }
172
- function attrName(name) {
173
- switch (name) {
174
- case "charSet":
175
- return "charset";
176
- case "className":
177
- return "class";
178
- case "crossOrigin":
179
- return "crossorigin";
180
- case "autoPlay":
181
- return "autoplay";
182
- case "htmlFor":
183
- return "for";
184
- case "playsInline":
185
- return "playsinline";
186
- default:
187
- return name;
188
- }
189
- }
190
- function styleObjectToString(style) {
191
- const parts = [];
192
- for (const [key, value] of Object.entries(style)) {
193
- if (value == null || value === false) continue;
194
- const cssName = key.startsWith("--") ? key : camelToKebab(key);
195
- const cssValue = typeof value === "number" && !UNITLESS_CSS.has(key) ? `${value}px` : String(value);
196
- parts.push(`${cssName}: ${cssValue}`);
197
- }
198
- return parts.join("; ");
199
- }
200
- function rawTextContent(nodes) {
201
- return nodes.map((node) => {
202
- if (node == null || node === false || node === true) return "";
203
- if (typeof node === "string" || typeof node === "number") {
204
- return String(node);
205
- }
206
- if (Array.isArray(node)) return rawTextContent(node);
207
- return "";
208
- }).join("");
209
- }
210
- function camelToKebab(value) {
211
- return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
212
- }
213
- function escapeText(value) {
214
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
215
- }
216
- function escapeAttr(value) {
217
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
218
- }
219
-
220
- // src/primitives/Composition.tsx
221
- var BASE_STAGE_CSS = `
222
- *, *::before, *::after { box-sizing: border-box; }
223
- html, body { margin: 0; padding: 0; width: 100%; min-height: 100%; background: transparent; }
224
- body.stage {
225
- margin: 0;
226
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
227
- color: #111827;
228
- -webkit-font-smoothing: antialiased;
229
- text-rendering: optimizeLegibility;
230
- }
231
- .reshot-compose-stage {
232
- width: 100vw;
233
- height: 100vh;
234
- position: relative;
235
- overflow: hidden;
236
- display: flex;
237
- align-items: center;
238
- justify-content: center;
239
- background: #ffffff;
240
- }
241
- .reshot-frame {
242
- --reshot-frame-chrome-height: 0px;
243
- width: 100vw;
244
- height: 100vh;
245
- position: relative;
246
- overflow: hidden;
247
- background: #ffffff;
248
- border: 0;
249
- border-radius: 0;
250
- box-shadow: none;
251
- }
252
- .reshot-frame--minimal,
253
- .reshot-frame--browser-light,
254
- .reshot-frame--browser-dark {
255
- --reshot-frame-chrome-height: 32px;
256
- width: min(calc(100vw - 48px), 1440px);
257
- height: min(calc(100vh - 48px), 900px);
258
- border: 1px solid rgba(15, 23, 42, 0.16);
259
- border-radius: 10px;
260
- }
261
- .reshot-frame__chrome {
262
- height: var(--reshot-frame-chrome-height);
263
- display: flex;
264
- align-items: center;
265
- gap: 10px;
266
- padding: 0 12px;
267
- border-bottom: 1px solid rgba(15, 23, 42, 0.1);
268
- background: #f8fafc;
269
- }
270
- .reshot-frame--browser-dark .reshot-frame__chrome {
271
- border-bottom-color: rgba(255, 255, 255, 0.12);
272
- background: rgba(17, 24, 39, 0.92);
273
- }
274
- .reshot-frame__traffic {
275
- display: flex;
276
- gap: 7px;
277
- }
278
- .reshot-frame__traffic span {
279
- width: 11px;
280
- height: 11px;
281
- border-radius: 999px;
282
- display: block;
283
- }
284
- .reshot-frame__traffic span:nth-child(1) { background: #ff5f57; }
285
- .reshot-frame__traffic span:nth-child(2) { background: #febc2e; }
286
- .reshot-frame__traffic span:nth-child(3) { background: #28c840; }
287
- .reshot-frame__url {
288
- flex: 1;
289
- min-width: 0;
290
- display: flex;
291
- align-items: center;
292
- justify-content: flex-start;
293
- color: #475569;
294
- font: 500 11px ui-monospace, SFMono-Regular, Menlo, monospace;
295
- overflow: hidden;
296
- text-overflow: ellipsis;
297
- white-space: nowrap;
298
- }
299
- .reshot-frame--browser-dark .reshot-frame__url {
300
- color: #cbd5e1;
301
- }
302
- .reshot-frame__viewport {
303
- position: absolute;
304
- top: var(--reshot-frame-chrome-height);
305
- left: 0;
306
- right: 0;
307
- bottom: 0;
308
- overflow: hidden;
309
- background: #ffffff;
310
- }
311
- .reshot-frame__media-layer {
312
- position: absolute;
313
- inset: 0;
314
- transform-origin: top left;
315
- will-change: transform;
316
- }
317
- .reshot-frame__media {
318
- width: 100%;
319
- height: 100%;
320
- display: block;
321
- object-fit: contain;
322
- background: #ffffff;
323
- }
324
- .reshot-frame__media--cover {
325
- object-fit: cover;
326
- }
327
- .reshot-frame__overlays {
328
- position: absolute;
329
- inset: 0;
330
- pointer-events: none;
331
- transform-origin: top left;
332
- will-change: transform;
333
- }
334
- .reshot-product-film {
335
- width: 100%;
336
- height: 100%;
337
- }
338
- .reshot-annotation {
339
- position: absolute;
340
- z-index: 4;
341
- opacity: 0;
342
- pointer-events: none;
343
- color: #0f172a;
344
- }
345
- .reshot-annotation__ring {
346
- position: absolute;
347
- inset: 0;
348
- border: 2px solid currentColor;
349
- border-radius: 8px;
350
- box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.82);
351
- }
352
- .reshot-annotation__label {
353
- position: absolute;
354
- left: 0;
355
- top: 100%;
356
- margin-top: 8px;
357
- max-width: 260px;
358
- padding: 6px 8px;
359
- border-radius: 6px;
360
- background: rgba(255, 255, 255, 0.96);
361
- border: 1px solid rgba(15, 23, 42, 0.14);
362
- box-shadow: 0 6px 18px rgba(15, 23, 42, 0.12);
363
- font: 600 12px/1.25 -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
364
- }
365
- .reshot-annotation--success { color: #147a3d; }
366
- .reshot-annotation--warning { color: #9a5b00; }
367
- .reshot-annotation--danger { color: #b4232d; }
368
- .reshot-annotation--edge {
369
- width: auto !important;
370
- height: auto !important;
371
- max-width: 320px;
372
- }
373
- .reshot-annotation--edge .reshot-annotation__ring {
374
- display: none;
375
- }
376
- .reshot-annotation--edge .reshot-annotation__label {
377
- position: static;
378
- margin: 0;
379
- }
380
- @keyframes annotationIn {
381
- 0% { opacity: 0; transform: translateY(4px); }
382
- 100% { opacity: 1; transform: translateY(0); }
383
- }
384
- @keyframes annotationOut {
385
- 0% { opacity: 1; transform: translateY(0); }
386
- 100% { opacity: 0; transform: translateY(-4px); }
387
- }
388
- `;
389
- function Composition({
390
- workflow,
391
- slug = "composition",
392
- capturePath,
393
- durationMs,
394
- children
395
- }) {
396
- const resolvedCapturePath = capturePath ?? (typeof workflow.capturePath === "string" ? workflow.capturePath : void 0);
397
- const resolvedDuration = durationMs ?? workflow.durationMs;
398
- return createWorkflowProvider(
399
- { workflow, slug, capturePath: resolvedCapturePath },
400
- /* @__PURE__ */ jsxs("html", { lang: "en", children: [
401
- /* @__PURE__ */ jsxs("head", { children: [
402
- /* @__PURE__ */ jsx("meta", { charSet: "utf-8" }),
403
- /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
404
- /* @__PURE__ */ jsx("title", { children: slug }),
405
- /* @__PURE__ */ jsx("style", { children: BASE_STAGE_CSS })
406
- ] }),
407
- /* @__PURE__ */ jsxs("body", { className: "stage", children: [
408
- /* @__PURE__ */ jsx(
409
- "main",
410
- {
411
- className: "reshot-compose-stage",
412
- "data-slug": slug,
413
- "data-duration-ms": resolvedDuration,
414
- children
415
- }
416
- ),
417
- /* @__PURE__ */ jsx("script", { children: "window.__RESHOT_COMPOSE_READY__ = true;" })
418
- ] })
419
- ] })
420
- );
421
- }
422
-
423
- // src/primitives/Frame.tsx
424
- function Frame({
425
- chrome = "none",
426
- url = "app.reshot.dev",
427
- src,
428
- fit = "contain",
429
- children
430
- }) {
431
- const context = useWorkflowContext();
432
- const mediaSrc = src ?? context.capturePath ?? "";
433
- const isStillImage = /\.(?:png|jpe?g|webp|gif|avif|svg)(?:[?#].*)?$/i.test(
434
- mediaSrc
435
- );
436
- const showChrome = chrome !== "none";
437
- const mediaClassName = `reshot-frame__media${fit === "cover" ? " reshot-frame__media--cover" : ""}`;
438
- return /* @__PURE__ */ jsxs("div", { className: `reshot-frame reshot-frame--${chrome}`, "data-product-surface": "true", children: [
439
- showChrome ? /* @__PURE__ */ jsxs("div", { className: "reshot-frame__chrome", children: [
440
- /* @__PURE__ */ jsxs("div", { className: "reshot-frame__traffic", "aria-hidden": "true", children: [
441
- /* @__PURE__ */ jsx("span", {}),
442
- /* @__PURE__ */ jsx("span", {}),
443
- /* @__PURE__ */ jsx("span", {})
444
- ] }),
445
- /* @__PURE__ */ jsx("div", { className: "reshot-frame__url", children: url })
446
- ] }) : null,
447
- /* @__PURE__ */ jsxs("div", { className: "reshot-frame__viewport", children: [
448
- /* @__PURE__ */ jsx("div", { className: "reshot-frame__media-layer", children: isStillImage ? /* @__PURE__ */ jsx(
449
- "img",
450
- {
451
- className: mediaClassName,
452
- src: mediaSrc,
453
- alt: "",
454
- "aria-hidden": "true"
455
- }
456
- ) : /* @__PURE__ */ jsx(
457
- "video",
458
- {
459
- className: mediaClassName,
460
- src: mediaSrc,
461
- autoPlay: true,
462
- muted: true,
463
- playsInline: true,
464
- preload: "auto"
465
- }
466
- ) }),
467
- /* @__PURE__ */ jsx("div", { className: "reshot-frame__overlays", children })
468
- ] })
469
- ] });
470
- }
471
-
472
- // src/render/scene-driver.ts
473
- var SCENE_ROOT_CLASS = "reshot-scene-root";
474
- function mountSceneInPage() {
475
- const w = window;
476
- const payload = w.__RESHOT_SCENE__;
477
- if (!payload || w.__RESHOT_SCENE_MOUNTED__) return;
478
- const root = document.querySelector(
479
- "[data-reshot-scene='1'].reshot-scene-root"
480
- );
481
- if (!root) return;
482
- const doc = new DOMParser().parseFromString(payload.html, "text/html");
483
- const dir = doc.documentElement.getAttribute("dir");
484
- const scopeSel = "[data-reshot-scene='1'].reshot-scene-root";
485
- const scopeRule = (rule) => {
486
- if (rule instanceof CSSFontFaceRule) return rule.cssText;
487
- if (rule instanceof CSSStyleRule) {
488
- const selectors = rule.selectorText.split(",").map((sel) => {
489
- const s = sel.trim();
490
- if (/^(html|body|:root)$/i.test(s)) return scopeSel;
491
- const stripped = s.replace(/^\s*(html|body|:root)\b\s*/i, "");
492
- return `${scopeSel} ${stripped || "*"}`.trim();
493
- });
494
- return `${selectors.join(", ")}{${rule.style.cssText}}`;
495
- }
496
- return rule.cssText;
497
- };
498
- const collected = [];
499
- for (const styleEl of Array.from(doc.head.querySelectorAll("style"))) {
500
- const probe = document.createElement("style");
501
- probe.textContent = styleEl.textContent || "";
502
- document.head.appendChild(probe);
503
- const sheet = probe.sheet;
504
- try {
505
- if (sheet) {
506
- for (const rule of Array.from(sheet.cssRules)) collected.push(scopeRule(rule));
507
- } else {
508
- collected.push(styleEl.textContent || "");
509
- }
510
- } finally {
511
- probe.remove();
512
- }
513
- }
514
- const scopedStyle = document.createElement("style");
515
- scopedStyle.setAttribute("data-reshot-scene-styles", "1");
516
- scopedStyle.textContent = collected.join("\n");
517
- document.head.appendChild(scopedStyle);
518
- if (payload.viewport) {
519
- root.style.width = `${payload.viewport.width}px`;
520
- root.style.height = `${payload.viewport.height}px`;
521
- } else {
522
- root.style.width = "100%";
523
- root.style.height = "100%";
524
- }
525
- root.style.position = "absolute";
526
- root.style.top = "0";
527
- root.style.left = "0";
528
- root.style.overflow = "hidden";
529
- if (dir) root.setAttribute("dir", dir);
530
- const bodyWrap = document.createElement("div");
531
- bodyWrap.setAttribute("data-reshot-scene-body", "1");
532
- bodyWrap.style.position = "absolute";
533
- bodyWrap.style.inset = "0";
534
- while (doc.body && doc.body.firstChild) {
535
- bodyWrap.appendChild(document.adoptNode(doc.body.firstChild));
536
- }
537
- root.appendChild(bodyWrap);
538
- for (const s of payload.scrolls || []) {
539
- if (s.sel === ":root") {
540
- bodyWrap.scrollLeft = s.x;
541
- bodyWrap.scrollTop = s.y;
542
- continue;
543
- }
544
- const el = bodyWrap.querySelector(s.sel);
545
- if (el) {
546
- el.scrollLeft = s.x;
547
- el.scrollTop = s.y;
548
- }
549
- }
550
- w.__RESHOT_SCENE_MOUNTED__ = true;
551
- }
552
- var MOUNT_SCENE_SOURCE = `(${mountSceneInPage.toString()})();`;
553
- function sceneCameraInPage() {
554
- const w = window;
555
- const cfg = w.__RESHOT_SCENE_CAMERA__;
556
- if (!cfg || cfg.keyframes.length === 0) return;
557
- const layer = document.querySelector(
558
- ".reshot-frame__media-layer--scene"
559
- );
560
- if (!layer) return;
561
- const kfs = cfg.keyframes.slice().sort((a, b) => a.tMs - b.tMs);
562
- const applyViewport = (x, y, zoom) => {
563
- const rect = layer.getBoundingClientRect();
564
- const scaleX = cfg.source.width > 0 ? rect.width / cfg.source.width : 1;
565
- const scaleY = cfg.source.height > 0 ? rect.height / cfg.source.height : 1;
566
- const tx = Math.round(-x * scaleX * zoom * 100) / 100;
567
- const ty = Math.round(-y * scaleY * zoom * 100) / 100;
568
- const z = Math.round(zoom * 1e3) / 1e3;
569
- layer.style.transform = `translate(${tx}px, ${ty}px) scale(${z})`;
570
- };
571
- const first = kfs[0];
572
- const lastKf = kfs[kfs.length - 1];
573
- const viewportAt = (t) => {
574
- if (t <= first.tMs) return first;
575
- if (t >= lastKf.tMs) return lastKf;
576
- for (let i = 1; i < kfs.length; i++) {
577
- const b = kfs[i];
578
- if (t <= b.tMs) {
579
- const a = kfs[i - 1];
580
- if (b.isHardCut) return a;
581
- const span = b.tMs - a.tMs;
582
- const f = span > 0 ? (t - a.tMs) / span : 1;
583
- return {
584
- x: a.x + (b.x - a.x) * f,
585
- y: a.y + (b.y - a.y) * f,
586
- zoom: a.zoom + (b.zoom - a.zoom) * f
587
- };
588
- }
589
- }
590
- return lastKf;
591
- };
592
- applyViewport(first.x, first.y, first.zoom);
593
- const tick = () => {
594
- const t = w.performance.now();
595
- const v = viewportAt(t);
596
- applyViewport(v.x, v.y, v.zoom);
597
- w.requestAnimationFrame(tick);
598
- };
599
- w.requestAnimationFrame(tick);
600
- }
601
- var SCENE_CAMERA_SOURCE = `(${sceneCameraInPage.toString()})();`;
602
- function buildSceneCameraScript(config) {
603
- if (!config || config.keyframes.length === 0) return "";
604
- const json = JSON.stringify(config).replace(/<\/(script)/gi, "<\\/$1");
605
- return `window.__RESHOT_SCENE_CAMERA__ = ${json};
606
- ${SCENE_CAMERA_SOURCE}`;
607
- }
608
- function sceneMotionInPage() {
609
- const w = window;
610
- const cfg = w.__RESHOT_SCENE_MOTION__;
611
- if (!cfg || !cfg.instructions || cfg.instructions.length === 0) return;
612
- const layer = document.querySelector(".reshot-frame__media-layer--scene");
613
- if (!layer) return;
614
- const ease = (x) => {
615
- const c = x < 0 ? 0 : x > 1 ? 1 : x;
616
- return 1 - Math.pow(1 - c, 3);
617
- };
618
- const q8 = (a) => String(Math.round((a < 0 ? 0 : a > 1 ? 1 : a) * 255) / 255);
619
- const rectRel = (el, lr) => {
620
- const r = el.getBoundingClientRect();
621
- return { top: r.top - lr.top, left: r.left - lr.left, width: r.width, height: r.height };
622
- };
623
- const setBox = (el, top, left, width, height) => {
624
- el.style.top = Math.round(top) + "px";
625
- el.style.left = Math.round(left) + "px";
626
- el.style.width = Math.round(width) + "px";
627
- el.style.height = Math.round(height) + "px";
628
- };
629
- const resolveButton = (body) => {
630
- const cands = body.querySelectorAll("button, [role='button'], a[href]");
631
- let best = null;
632
- let bestScore = 0;
633
- for (let i = 0; i < cands.length; i++) {
634
- const r = cands[i].getBoundingClientRect();
635
- if (r.width < 40 || r.height < 18 || r.width > 480) continue;
636
- const score = r.width * r.height + r.left + (2e3 - r.top);
637
- if (score > bestScore) {
638
- bestScore = score;
639
- best = cands[i];
640
- }
641
- }
642
- return best;
643
- };
644
- const resolveRows = (body, target, max) => {
645
- let container = null;
646
- if (target && target !== "auto") {
647
- container = body.querySelector(target);
648
- } else {
649
- let best = null;
650
- let bestScore = 0;
651
- let fallback = null;
652
- let fallbackKids = 0;
653
- const all = body.getElementsByTagName("*");
654
- for (let i = 0; i < all.length; i++) {
655
- const el = all[i];
656
- const kids2 = el.children;
657
- if (kids2.length > fallbackKids) {
658
- fallbackKids = kids2.length;
659
- fallback = el;
660
- }
661
- const rect = el.getBoundingClientRect();
662
- if (rect.width < 200 || rect.height < 80) continue;
663
- let rowCount = 0;
664
- for (let j = 0; j < kids2.length; j++) {
665
- const kr = kids2[j].getBoundingClientRect();
666
- if (kr.height >= 24 && kr.height <= 320 && kr.width >= rect.width * 0.5) rowCount++;
667
- }
668
- if (rowCount >= 3) {
669
- const score = rowCount * rect.width * Math.min(rect.height, 1200);
670
- if (score > bestScore) {
671
- bestScore = score;
672
- best = el;
673
- }
674
- }
675
- }
676
- container = best || (fallbackKids >= 3 ? fallback : null);
677
- }
678
- if (!container) return [];
679
- const out = [];
680
- const kids = container.children;
681
- for (let i = 0; i < kids.length && out.length < max; i++) {
682
- const el = kids[i];
683
- const r = el.getBoundingClientRect();
684
- if (r.height >= 16 && r.width >= 60) out.push(el);
685
- }
686
- return out;
687
- };
688
- const resolveInput = (body, target) => {
689
- if (target && target !== "auto") return body.querySelector(target);
690
- const cands = body.querySelectorAll(
691
- "input[type='text'], input[type='search'], input:not([type]), textarea, [contenteditable='true']"
692
- );
693
- for (let i = 0; i < cands.length; i++) {
694
- const r = cands[i].getBoundingClientRect();
695
- if (r.width >= 80 && r.height >= 16) return cands[i];
696
- }
697
- return null;
698
- };
699
- const parseNum = (s) => {
700
- const m = (s || "").replace(/[^0-9.\-]/g, "");
701
- if (!m || m === "-" || m === ".") return null;
702
- const n = Number(m);
703
- return Number.isFinite(n) ? n : null;
704
- };
705
- const resolveNumber = (body, target) => {
706
- if (target && target !== "auto") return body.querySelector(target);
707
- let best = null;
708
- let bestScore = 0;
709
- const all = body.getElementsByTagName("*");
710
- for (let i = 0; i < all.length; i++) {
711
- const el = all[i];
712
- if (el.children.length !== 0) continue;
713
- const txt = (el.textContent || "").trim();
714
- if (!/^[$£€]?\s?-?\d[\d,]*(\.\d+)?%?$/.test(txt)) continue;
715
- if (parseNum(txt) === null) continue;
716
- const r = el.getBoundingClientRect();
717
- if (r.width < 12 || r.height < 12) continue;
718
- const score = r.width * r.height;
719
- if (score > bestScore) {
720
- bestScore = score;
721
- best = el;
722
- }
723
- }
724
- return best;
725
- };
726
- const groupInt = (n) => {
727
- const neg = n < 0;
728
- let s = String(Math.abs(Math.round(n)));
729
- let out = "";
730
- while (s.length > 3) {
731
- out = "," + s.slice(-3) + out;
732
- s = s.slice(0, -3);
733
- }
734
- return (neg ? "-" : "") + s + out;
735
- };
736
- const fmtNum = (n, format) => {
737
- if (format === "currency") return "$" + groupInt(n);
738
- if (format === "percent") return groupInt(n) + "%";
739
- return groupInt(n);
740
- };
741
- let states = null;
742
- const makeOverlay = (css) => {
743
- const el = document.createElement("div");
744
- el.setAttribute("data-reshot-motion", "1");
745
- el.style.cssText = "position:absolute;pointer-events:none;z-index:9;opacity:0;" + css;
746
- layer.appendChild(el);
747
- return el;
748
- };
749
- const buildStates = (body) => {
750
- const out = [];
751
- const lr = layer.getBoundingClientRect();
752
- for (const inst of cfg.instructions) {
753
- if (inst.type === "reveal") {
754
- const rows = resolveRows(body, inst.target || "auto", inst.max || 12);
755
- if (rows.length === 0) continue;
756
- const lastStart = inst.startMs + (rows.length - 1) * (inst.stagger || 80);
757
- const perRowDur = Math.max(160, Math.min(600, inst.endMs - lastStart));
758
- for (const row of rows) {
759
- row.style.opacity = "0";
760
- row.style.transform = "translateY(" + Math.round(inst.distancePx || 12) + "px)";
761
- }
762
- out.push({ type: "reveal", inst, rows, perRowDur });
763
- } else if (inst.type === "highlight") {
764
- const rows = resolveRows(body, inst.target || "auto", inst.rows || 8);
765
- if (rows.length === 0) continue;
766
- const geom = rows.map((r) => rectRel(r, lr));
767
- const el = makeOverlay("border-radius:8px;background:rgba(88,101,242,0.12);border:2px solid rgb(88,101,242);");
768
- out.push({ type: "highlight", inst, geom, el });
769
- } else if (inst.type === "cursor") {
770
- const targetEl = typeof inst.to === "string" && inst.to !== "auto" ? body.querySelector(inst.to) : resolveButton(body);
771
- if (!targetEl) continue;
772
- const tr = rectRel(targetEl, lr);
773
- const target = { x: Math.round(tr.left + tr.width / 2), y: Math.round(tr.top + tr.height / 2) };
774
- const start = { x: Math.round(lr.width * (inst.fromXFrac ?? 0.5)), y: Math.round(lr.height * (inst.fromYFrac ?? 0.92)) };
775
- const cursorEl = makeOverlay("width:24px;height:24px;");
776
- cursorEl.innerHTML = "<svg width='24' height='24' viewBox='0 0 24 24'><path d='M4 2 L4 19 L8.4 14.6 L11.6 21 L14 19.9 L10.8 13.6 L17 13.6 Z' fill='#0b1220' stroke='#fff' stroke-width='1.3' stroke-linejoin='round'/></svg>";
777
- const ringEl = inst.click ? makeOverlay("border:2px solid rgba(88,101,242,0.9);border-radius:50%;") : null;
778
- out.push({ type: "cursor", inst, start, target, cursorEl, ringEl });
779
- } else if (inst.type === "type") {
780
- const el = resolveInput(body, inst.target || "auto");
781
- if (!el) continue;
782
- const isInput = el.tagName === "INPUT" || el.tagName === "TEXTAREA";
783
- if (isInput) el.value = "";
784
- else el.textContent = "";
785
- out.push({ type: "type", inst, el, isInput });
786
- } else if (inst.type === "countUp") {
787
- const el = resolveNumber(body, inst.target || "auto");
788
- if (!el) continue;
789
- const original = el.textContent || "";
790
- const parsed = parseNum(original);
791
- const to = typeof inst.to === "number" ? inst.to : parsed;
792
- if (to === null || to === void 0) continue;
793
- out.push({ type: "countUp", inst, el, to, original });
794
- } else if (inst.type === "scrollTo") {
795
- const content = inst.target && inst.target !== "auto" ? body.querySelector(inst.target) : body;
796
- if (!content) continue;
797
- const cTop = content.getBoundingClientRect().top;
798
- let offset;
799
- if (typeof inst.to === "string" && inst.to !== "auto") {
800
- const toEl = content.querySelector(inst.to);
801
- if (!toEl) continue;
802
- offset = Math.max(0, Math.round(toEl.getBoundingClientRect().top - cTop));
803
- } else {
804
- offset = Math.max(0, Math.round(content.scrollHeight - layer.getBoundingClientRect().height));
805
- }
806
- if (offset <= 0) continue;
807
- out.push({ type: "scrollTo", inst, el: content, offset });
808
- } else if (inst.type === "populate") {
809
- const rows = resolveRows(body, inst.target || "auto", inst.max || 12);
810
- if (rows.length === 0) continue;
811
- const lastStart = inst.startMs + (rows.length - 1) * (inst.stagger || 80);
812
- const perRowDur = Math.max(160, Math.min(600, inst.endMs - lastStart));
813
- for (const row of rows) {
814
- row.style.opacity = "0";
815
- row.style.transform = "translateX(" + Math.round(inst.distancePx || 16) + "px)";
816
- }
817
- out.push({ type: "populate", inst, rows, perRowDur });
818
- }
819
- }
820
- return out;
821
- };
822
- const applyReveal = (s, t) => {
823
- for (let i = 0; i < s.rows.length; i++) {
824
- const p = (t - (s.inst.startMs + i * (s.inst.stagger || 80))) / s.perRowDur;
825
- if (p >= 1) {
826
- s.rows[i].style.opacity = "";
827
- s.rows[i].style.transform = "";
828
- } else {
829
- const e = ease(p);
830
- s.rows[i].style.opacity = q8(e);
831
- s.rows[i].style.transform = "translateY(" + Math.round((1 - e) * (s.inst.distancePx || 12)) + "px)";
832
- }
833
- }
834
- };
835
- const applyHighlight = (s, t) => {
836
- const inst = s.inst, g = s.geom, pad = inst.padPx ?? 6;
837
- if (t < inst.startMs || t > inst.endMs) {
838
- s.el.style.opacity = "0";
839
- return;
840
- }
841
- const steps = Math.min(g.length - 1, (inst.rows || 8) - 1);
842
- let a = g[0], b = g[0], frac = 0;
843
- if (inst.walk && steps > 0) {
844
- const fpos = Math.min(1, (t - inst.startMs) / Math.max(1, inst.endMs - inst.startMs)) * steps;
845
- const idx = Math.min(steps, Math.floor(fpos));
846
- frac = ease(fpos - idx);
847
- a = g[idx];
848
- b = g[Math.min(steps, idx + 1)];
849
- }
850
- const top = a.top + (b.top - a.top) * frac;
851
- const left = Math.min(a.left, b.left);
852
- const width = Math.max(a.width, b.width);
853
- const height = a.height + (b.height - a.height) * frac;
854
- setBox(s.el, top - pad, left - pad, width + pad * 2, height + pad * 2);
855
- const fin = Math.min(1, (t - inst.startMs) / 150);
856
- const fout = Math.min(1, (inst.endMs - t) / 150);
857
- s.el.style.opacity = q8(0.9 * Math.max(0, Math.min(fin, fout)));
858
- };
859
- const applyCursor = (s, t) => {
860
- const inst = s.inst;
861
- if (t < inst.startMs || t > inst.endMs) {
862
- s.cursorEl.style.opacity = "0";
863
- if (s.ringEl) s.ringEl.style.opacity = "0";
864
- return;
865
- }
866
- const moveEnd = inst.click ? inst.startMs + (inst.endMs - inst.startMs) * 0.7 : inst.endMs;
867
- const e = ease(Math.min(1, (t - inst.startMs) / Math.max(1, moveEnd - inst.startMs)));
868
- const x = Math.round(s.start.x + (s.target.x - s.start.x) * e);
869
- const y = Math.round(s.start.y + (s.target.y - s.start.y) * e);
870
- s.cursorEl.style.left = x + "px";
871
- s.cursorEl.style.top = y + "px";
872
- s.cursorEl.style.opacity = q8(0.97 * Math.min(1, (t - inst.startMs) / 120));
873
- if (s.ringEl) {
874
- if (t >= moveEnd) {
875
- const cp = Math.min(1, (t - moveEnd) / Math.max(1, inst.endMs - moveEnd));
876
- const r = Math.round(10 + cp * 26);
877
- setBox(s.ringEl, s.target.y - r, s.target.x - r, r * 2, r * 2);
878
- s.ringEl.style.opacity = q8((1 - cp) * 0.8);
879
- } else {
880
- s.ringEl.style.opacity = "0";
881
- }
882
- }
883
- };
884
- const applyType = (s, t) => {
885
- const inst = s.inst;
886
- const text = inst.text || "";
887
- let shown;
888
- if (t < inst.startMs) shown = "";
889
- else if (t >= inst.endMs) shown = text;
890
- else {
891
- const n = Math.round(text.length * Math.min(1, (t - inst.startMs) / Math.max(1, inst.endMs - inst.startMs)));
892
- shown = text.slice(0, n);
893
- }
894
- const typing = t >= inst.startMs && t < inst.endMs;
895
- const caret = inst.caret !== false && typing && Math.floor(t / 250) % 2 === 0 ? "|" : "";
896
- if (s.isInput) s.el.value = shown + (caret ? caret : "");
897
- else s.el.textContent = shown + caret;
898
- };
899
- const applyCountUp = (s, t) => {
900
- const inst = s.inst;
901
- if (t >= inst.endMs) {
902
- s.el.textContent = s.original;
903
- return;
904
- }
905
- if (t < inst.startMs) {
906
- s.el.textContent = fmtNum(inst.from || 0, inst.format || "number");
907
- return;
908
- }
909
- const from = inst.from || 0;
910
- const v = from + (s.to - from) * ease(Math.min(1, (t - inst.startMs) / Math.max(1, inst.endMs - inst.startMs)));
911
- s.el.textContent = fmtNum(v, inst.format || "number");
912
- };
913
- const applyScrollTo = (s, t) => {
914
- const inst = s.inst;
915
- const p = t <= inst.startMs ? 0 : t >= inst.endMs ? 1 : ease((t - inst.startMs) / Math.max(1, inst.endMs - inst.startMs));
916
- const y = Math.round(s.offset * p);
917
- s.el.style.transform = y === 0 ? "" : "translateY(" + -y + "px)";
918
- };
919
- const applyPopulate = (s, t) => {
920
- for (let i = 0; i < s.rows.length; i++) {
921
- const p = (t - (s.inst.startMs + i * (s.inst.stagger || 80))) / s.perRowDur;
922
- if (p >= 1) {
923
- s.rows[i].style.opacity = "";
924
- s.rows[i].style.transform = "";
925
- } else {
926
- const e = ease(p);
927
- s.rows[i].style.opacity = q8(e);
928
- s.rows[i].style.transform = "translateX(" + Math.round((1 - e) * (s.inst.distancePx || 16)) + "px)";
929
- }
930
- }
931
- };
932
- const apply = (t) => {
933
- if (!states) return;
934
- for (const s of states) {
935
- if (s.type === "reveal") applyReveal(s, t);
936
- else if (s.type === "highlight") applyHighlight(s, t);
937
- else if (s.type === "cursor") applyCursor(s, t);
938
- else if (s.type === "type") applyType(s, t);
939
- else if (s.type === "countUp") applyCountUp(s, t);
940
- else if (s.type === "scrollTo") applyScrollTo(s, t);
941
- else applyPopulate(s, t);
942
- }
943
- };
944
- const tick = () => {
945
- if (states === null) {
946
- const body = document.querySelector("[data-reshot-scene-body='1']");
947
- if (!body) {
948
- w.requestAnimationFrame(tick);
949
- return;
950
- }
951
- states = buildStates(body);
952
- }
953
- apply(w.performance.now());
954
- w.requestAnimationFrame(tick);
955
- };
956
- w.requestAnimationFrame(tick);
957
- }
958
- var SCENE_MOTION_SOURCE = `(${sceneMotionInPage.toString()})();`;
959
- function buildSceneMotionScript(config) {
960
- if (!config || config.instructions.length === 0) return "";
961
- const json = JSON.stringify(config).replace(/<\/(script)/gi, "<\\/$1");
962
- return `window.__RESHOT_SCENE_MOTION__ = ${json};
963
- ${SCENE_MOTION_SOURCE}`;
964
- }
965
- function buildSceneMountScript(artifact) {
966
- const payload = {
967
- html: artifact.html,
968
- scrolls: artifact.scrolls ?? [],
969
- viewport: artifact.viewport
970
- };
971
- const json = JSON.stringify(payload).replace(/<\/(script)/gi, "<\\/$1");
972
- return `window.__RESHOT_SCENE__ = ${json};
973
- ${MOUNT_SCENE_SOURCE}`;
974
- }
975
-
976
- // ../motion-core/dist/index.js
977
- var CLUSTER_THRESHOLD_PX = 500;
978
- var MAX_ZOOM = 1.8;
979
- var MIN_ZOOM = 1;
980
- var DEFAULT_PADDING = 2.5;
981
- var SAFE_FRAME_ASPECT_RATIO = 16 / 9;
982
- var IMAGE_EDGE_MARGIN_PX = 4;
983
- var MIN_VISIBLE_RATIO = 0.4;
984
- var ZOOM_LEVELS = [1, 1.15, 1.3, 1.5];
985
- var DEFAULT_SPRING_TENSION = 120;
986
- var DEFAULT_SPRING_FRICTION = 20;
987
- var SIMULATION_TIMESTEP_MS = 1;
988
- var SPRING_EPSILON = 0.01;
989
- var DEFAULT_MOTION_SETTINGS = {
990
- mode: "cinematic",
991
- damping: 0.5,
992
- padding: DEFAULT_PADDING
993
- };
994
- function dampingToSpringParams(damping) {
995
- const clampedDamping = Math.max(0.1, Math.min(1, damping));
996
- const tension = 40 + (200 - 40) * clampedDamping;
997
- const FRICTION_RATIO = DEFAULT_SPRING_FRICTION / (2 * Math.sqrt(DEFAULT_SPRING_TENSION));
998
- const friction = 2 * Math.sqrt(tension) * FRICTION_RATIO;
999
- return { tension, friction };
1000
- }
1001
- function distanceBetweenCenters(a, b) {
1002
- const aCenterX = a.x + a.width / 2;
1003
- const aCenterY = a.y + a.height / 2;
1004
- const bCenterX = b.x + b.width / 2;
1005
- const bCenterY = b.y + b.height / 2;
1006
- return Math.sqrt(
1007
- Math.pow(aCenterX - bCenterX, 2) + Math.pow(aCenterY - bCenterY, 2)
1008
- );
1009
- }
1010
- function getUnionRect(rects) {
1011
- if (rects.length === 0) {
1012
- return { x: 0, y: 0, width: 0, height: 0 };
1013
- }
1014
- let minX = Infinity;
1015
- let minY = Infinity;
1016
- let maxX = -Infinity;
1017
- let maxY = -Infinity;
1018
- for (const r of rects) {
1019
- minX = Math.min(minX, r.x);
1020
- minY = Math.min(minY, r.y);
1021
- maxX = Math.max(maxX, r.x + r.width);
1022
- maxY = Math.max(maxY, r.y + r.height);
1023
- }
1024
- return {
1025
- x: minX,
1026
- y: minY,
1027
- width: maxX - minX,
1028
- height: maxY - minY
1029
- };
1030
- }
1031
- function isNavigationStep(step) {
1032
- return step.action === "navigate";
1033
- }
1034
- function isUserOverrideStep(step) {
1035
- const { strategy } = step.camera;
1036
- return strategy === "manual" || strategy === "wide" || strategy === "fix";
1037
- }
1038
- function generateCameraShots(steps, _settings) {
1039
- if (steps.length === 0) return [];
1040
- const shots = [];
1041
- let clusterBounds = [];
1042
- let clusterStartIndex = 0;
1043
- let clusterStartTimeMs = 0;
1044
- let currentTimeMs = 0;
1045
- for (let i = 0; i < steps.length; i++) {
1046
- const currentStep = steps[i];
1047
- const nextStep = steps[i + 1];
1048
- clusterBounds.push(currentStep.containerBounds ?? currentStep.bounds);
1049
- const stepEndTimeMs = currentTimeMs + currentStep.durationMs + currentStep.transitionDurationMs;
1050
- const shouldBreak = (
1051
- // End of timeline
1052
- !nextStep || // URL changed → hard cut required
1053
- nextStep && isNavigationStep(nextStep) || // Current step is a navigation → it should be its own shot
1054
- isNavigationStep(currentStep) || // Next step has a user override → it needs its own shot
1055
- nextStep && isUserOverrideStep(nextStep) || // Current step has a user override → it should be its own shot
1056
- isUserOverrideStep(currentStep) || // Scroll events force cluster breaks (camera must travel large distance)
1057
- currentStep.action === "scroll" || nextStep && nextStep.action === "scroll" || // Target is too far away from the cluster
1058
- nextStep && distanceBetweenCenters(
1059
- getUnionRect(clusterBounds),
1060
- nextStep.containerBounds ?? nextStep.bounds
1061
- ) > CLUSTER_THRESHOLD_PX
1062
- );
1063
- if (shouldBreak) {
1064
- const unionBounds = getUnionRect(clusterBounds);
1065
- shots.push({
1066
- targetBounds: unionBounds,
1067
- startStepIndex: clusterStartIndex,
1068
- endStepIndex: i,
1069
- startTimeMs: clusterStartTimeMs,
1070
- endTimeMs: stepEndTimeMs,
1071
- isUserOverride: isUserOverrideStep(currentStep) && clusterBounds.length === 1
1072
- });
1073
- clusterBounds = [];
1074
- clusterStartIndex = i + 1;
1075
- clusterStartTimeMs = stepEndTimeMs;
1076
- }
1077
- currentTimeMs = stepEndTimeMs;
1078
- }
1079
- return shots;
1080
- }
1081
- function calculateSafeFrame(shot, steps, settings, ctx) {
1082
- const { imageWidth, imageHeight } = ctx;
1083
- if (shot.isUserOverride) {
1084
- const step = steps[shot.startStepIndex];
1085
- if (step?.camera.viewport) {
1086
- return clampToImage(step.camera.viewport, imageWidth, imageHeight);
1087
- }
1088
- if (step?.camera.strategy === "fix") {
1089
- const prevStep = shot.startStepIndex > 0 ? steps[shot.startStepIndex - 1] : null;
1090
- if (prevStep?.camera.viewport) {
1091
- return clampToImage(prevStep.camera.viewport, imageWidth, imageHeight);
1092
- }
1093
- if (prevStep) {
1094
- const prevBounds = prevStep.bounds;
1095
- return calculateSafeFrameFromBounds(prevBounds, settings, ctx);
1096
- }
1097
- return { x: 0, y: 0, zoom: MIN_ZOOM };
1098
- }
1099
- if (step?.camera.strategy === "wide") {
1100
- return { x: 0, y: 0, zoom: MIN_ZOOM };
1101
- }
1102
- }
1103
- let { targetBounds } = shot;
1104
- if (shot.startStepIndex === shot.endStepIndex) {
1105
- const step = steps[shot.startStepIndex];
1106
- if (step?.containerBounds) {
1107
- targetBounds = step.containerBounds;
1108
- }
1109
- }
1110
- const paddingMultiplier = settings.padding ?? 2;
1111
- const paddedWidth = targetBounds.width * paddingMultiplier;
1112
- const paddedHeight = targetBounds.height * paddingMultiplier;
1113
- const paddedCenterX = targetBounds.x + targetBounds.width / 2;
1114
- const paddedCenterY = targetBounds.y + targetBounds.height / 2;
1115
- const targetAspect = SAFE_FRAME_ASPECT_RATIO;
1116
- const paddedAspect = paddedWidth / Math.max(paddedHeight, 1);
1117
- let frameWidth;
1118
- let frameHeight;
1119
- if (paddedAspect > targetAspect) {
1120
- frameWidth = paddedWidth;
1121
- frameHeight = paddedWidth / targetAspect;
1122
- } else {
1123
- frameHeight = paddedHeight;
1124
- frameWidth = paddedHeight * targetAspect;
1125
- }
1126
- const floorApplied = frameWidth < imageWidth * MIN_VISIBLE_RATIO || frameHeight < imageHeight * MIN_VISIBLE_RATIO;
1127
- frameWidth = Math.max(frameWidth, imageWidth * MIN_VISIBLE_RATIO);
1128
- frameHeight = Math.max(frameHeight, imageHeight * MIN_VISIBLE_RATIO);
1129
- const zoomX = imageWidth / Math.max(frameWidth, 1);
1130
- const zoomY = imageHeight / Math.max(frameHeight, 1);
1131
- const rawZoom = Math.min(zoomX, zoomY);
1132
- const clampedZoom = floorApplied ? MIN_ZOOM : quantizeZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, rawZoom)));
1133
- const viewportWidth = imageWidth / clampedZoom;
1134
- const viewportHeight = imageHeight / clampedZoom;
1135
- const x = paddedCenterX - viewportWidth / 2;
1136
- const y = paddedCenterY - viewportHeight / 2;
1137
- return clampToImage({ x, y, zoom: clampedZoom }, imageWidth, imageHeight);
1138
- }
1139
- function calculateSafeFrameFromBounds(bounds, settings, ctx) {
1140
- const { imageWidth, imageHeight } = ctx;
1141
- const paddingMultiplier = settings.padding ?? 2;
1142
- const paddedWidth = bounds.width * paddingMultiplier;
1143
- const paddedHeight = bounds.height * paddingMultiplier;
1144
- const centerX = bounds.x + bounds.width / 2;
1145
- const centerY = bounds.y + bounds.height / 2;
1146
- const targetAspect = SAFE_FRAME_ASPECT_RATIO;
1147
- const paddedAspect = paddedWidth / Math.max(paddedHeight, 1);
1148
- let frameWidth = paddedWidth;
1149
- let frameHeight = paddedHeight;
1150
- if (paddedAspect > targetAspect) {
1151
- frameHeight = paddedWidth / targetAspect;
1152
- } else {
1153
- frameWidth = paddedHeight * targetAspect;
1154
- }
1155
- const floorApplied = frameWidth < imageWidth * MIN_VISIBLE_RATIO || frameHeight < imageHeight * MIN_VISIBLE_RATIO;
1156
- frameWidth = Math.max(frameWidth, imageWidth * MIN_VISIBLE_RATIO);
1157
- frameHeight = Math.max(frameHeight, imageHeight * MIN_VISIBLE_RATIO);
1158
- const zoomX = imageWidth / Math.max(frameWidth, 1);
1159
- const zoomY = imageHeight / Math.max(frameHeight, 1);
1160
- const rawZoom = Math.min(zoomX, zoomY);
1161
- const clampedZoom = floorApplied ? MIN_ZOOM : quantizeZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, rawZoom)));
1162
- const viewportWidth = imageWidth / clampedZoom;
1163
- const viewportHeight = imageHeight / clampedZoom;
1164
- return clampToImage(
1165
- { x: centerX - viewportWidth / 2, y: centerY - viewportHeight / 2, zoom: clampedZoom },
1166
- imageWidth,
1167
- imageHeight
1168
- );
1169
- }
1170
- function quantizeZoom(zoom) {
1171
- let quantized = ZOOM_LEVELS[0];
1172
- for (const level of ZOOM_LEVELS) {
1173
- if (level <= zoom) {
1174
- quantized = level;
1175
- } else {
1176
- break;
1177
- }
1178
- }
1179
- return quantized;
1180
- }
1181
- function clampToImage(viewport, imageWidth, imageHeight) {
1182
- const zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, viewport.zoom));
1183
- const viewportWidth = imageWidth / zoom;
1184
- const viewportHeight = imageHeight / zoom;
1185
- const maxX = imageWidth - viewportWidth - IMAGE_EDGE_MARGIN_PX;
1186
- const maxY = imageHeight - viewportHeight - IMAGE_EDGE_MARGIN_PX;
1187
- const x = Math.max(IMAGE_EDGE_MARGIN_PX, Math.min(maxX, viewport.x));
1188
- const y = Math.max(IMAGE_EDGE_MARGIN_PX, Math.min(maxY, viewport.y));
1189
- return { x, y, zoom };
1190
- }
1191
- function simulateSpring(current, target, tension, friction, deltaMs) {
1192
- let state = { ...current };
1193
- const steps = Math.ceil(deltaMs / SIMULATION_TIMESTEP_MS);
1194
- const dt = SIMULATION_TIMESTEP_MS / 1e3;
1195
- for (let i = 0; i < steps; i++) {
1196
- const forceX = -tension * (state.x - target.x) - friction * state.vx;
1197
- const forceY = -tension * (state.y - target.y) - friction * state.vy;
1198
- const forceZoom = -tension * (state.zoom - target.zoom) - friction * state.vZoom;
1199
- state = {
1200
- vx: state.vx + forceX * dt,
1201
- vy: state.vy + forceY * dt,
1202
- vZoom: state.vZoom + forceZoom * dt,
1203
- x: state.x + (state.vx + forceX * dt) * dt,
1204
- y: state.y + (state.vy + forceY * dt) * dt,
1205
- zoom: state.zoom + (state.vZoom + forceZoom * dt) * dt
1206
- };
1207
- if (state.zoom < MIN_ZOOM) {
1208
- state.zoom = MIN_ZOOM;
1209
- state.vZoom = Math.max(0, state.vZoom);
1210
- } else if (state.zoom > MAX_ZOOM) {
1211
- state.zoom = MAX_ZOOM;
1212
- state.vZoom = Math.min(0, state.vZoom);
1213
- }
1214
- }
1215
- if (isConverged(state, target)) {
1216
- state = {
1217
- x: target.x,
1218
- y: target.y,
1219
- zoom: target.zoom,
1220
- vx: 0,
1221
- vy: 0,
1222
- vZoom: 0
1223
- };
1224
- }
1225
- return state;
1226
- }
1227
- function springFromViewport(viewport) {
1228
- return {
1229
- x: viewport.x,
1230
- y: viewport.y,
1231
- zoom: viewport.zoom,
1232
- vx: 0,
1233
- vy: 0,
1234
- vZoom: 0
1235
- };
1236
- }
1237
- function viewportFromSpring(state) {
1238
- return {
1239
- x: state.x,
1240
- y: state.y,
1241
- zoom: state.zoom
1242
- };
1243
- }
1244
- function hardCutSpring(target) {
1245
- return springFromViewport(target);
1246
- }
1247
- function isConverged(state, target) {
1248
- const posDelta = Math.abs(state.x - target.x) + Math.abs(state.y - target.y) + Math.abs(state.zoom - target.zoom);
1249
- const velocity = Math.abs(state.vx) + Math.abs(state.vy) + Math.abs(state.vZoom);
1250
- return posDelta < SPRING_EPSILON && velocity < SPRING_EPSILON;
1251
- }
1252
- var SOCIAL_CROP_ASPECT = 9 / 16;
1253
- function solveCameraPath(options) {
1254
- const {
1255
- steps,
1256
- settings = DEFAULT_MOTION_SETTINGS,
1257
- imageWidth,
1258
- imageHeight,
1259
- sampleIntervalMs
1260
- } = options;
1261
- if (steps.length === 0) return [];
1262
- switch (settings.mode) {
1263
- case "static":
1264
- return solveStatic(steps, imageWidth, imageHeight);
1265
- case "manual":
1266
- return solveManual(steps, settings, imageWidth, imageHeight);
1267
- case "cinematic":
1268
- default:
1269
- return solveCinematic(steps, settings, imageWidth, imageHeight, sampleIntervalMs);
1270
- }
1271
- }
1272
- function solveStatic(steps, _imageWidth, _imageHeight) {
1273
- const staticViewport = { x: 0, y: 0, zoom: 1 };
1274
- const keyframes = [];
1275
- let timeMs = 0;
1276
- for (const step of steps) {
1277
- keyframes.push({
1278
- timeMs,
1279
- viewport: { ...staticViewport },
1280
- isHardCut: false
1281
- });
1282
- timeMs += step.durationMs + step.transitionDurationMs;
1283
- }
1284
- return keyframes;
1285
- }
1286
- function solveManual(steps, settings, imageWidth, imageHeight) {
1287
- const keyframes = [];
1288
- let timeMs = 0;
1289
- const ctx = { imageWidth, imageHeight };
1290
- for (let i = 0; i < steps.length; i++) {
1291
- const step = steps[i];
1292
- const singleShot = {
1293
- targetBounds: step.bounds,
1294
- startStepIndex: i,
1295
- endStepIndex: i,
1296
- startTimeMs: timeMs,
1297
- endTimeMs: timeMs + step.durationMs + step.transitionDurationMs,
1298
- isUserOverride: step.camera.strategy !== "auto"
1299
- };
1300
- const viewport = calculateSafeFrame(singleShot, steps, settings, ctx);
1301
- keyframes.push({
1302
- timeMs,
1303
- viewport,
1304
- isHardCut: step.action === "navigate"
1305
- });
1306
- timeMs += step.durationMs + step.transitionDurationMs;
1307
- }
1308
- return keyframes;
1309
- }
1310
- function solveCinematic(steps, settings, imageWidth, imageHeight, sampleIntervalMs) {
1311
- const ctx = { imageWidth, imageHeight };
1312
- const { tension, friction } = dampingToSpringParams(settings.damping ?? 0.5);
1313
- const shots = generateCameraShots(steps, settings);
1314
- const shotViewports = shots.map(
1315
- (shot) => calculateSafeFrame(shot, steps, settings, ctx)
1316
- );
1317
- stabilizeZoomLevels(shotViewports, shots, imageWidth, imageHeight);
1318
- const totalDurationMs = steps.reduce(
1319
- (sum, s) => sum + s.durationMs + s.transitionDurationMs,
1320
- 0
1321
- );
1322
- const keyframes = [];
1323
- if (shots.length === 0 || shotViewports.length === 0) return keyframes;
1324
- let spring = springFromViewport(shotViewports[0]);
1325
- let currentShotIdx = 0;
1326
- if (sampleIntervalMs && sampleIntervalMs > 0) {
1327
- return solveCinematicUniform(
1328
- steps,
1329
- shots,
1330
- shotViewports,
1331
- settings,
1332
- tension,
1333
- friction,
1334
- totalDurationMs,
1335
- sampleIntervalMs
1336
- );
1337
- }
1338
- let timeMs = 0;
1339
- for (let i = 0; i < steps.length; i++) {
1340
- const step = steps[i];
1341
- while (currentShotIdx < shots.length - 1 && i > shots[currentShotIdx].endStepIndex) {
1342
- currentShotIdx++;
1343
- }
1344
- const target = shotViewports[currentShotIdx];
1345
- const isHardCut = step.action === "navigate";
1346
- if (isHardCut) {
1347
- const navViewport = { x: 0, y: 0, zoom: 1 };
1348
- spring = hardCutSpring(navViewport);
1349
- keyframes.push({
1350
- timeMs,
1351
- viewport: { ...navViewport },
1352
- isHardCut: true
1353
- });
1354
- } else {
1355
- const deltaMs = i === 0 ? 0 : steps[i - 1].durationMs + steps[i - 1].transitionDurationMs;
1356
- if (deltaMs > 0) {
1357
- spring = simulateSpring(spring, target, tension, friction, deltaMs);
1358
- }
1359
- keyframes.push({
1360
- timeMs,
1361
- viewport: viewportFromSpring(spring),
1362
- isHardCut: false
1363
- });
1364
- }
1365
- timeMs += step.durationMs + step.transitionDurationMs;
1366
- }
1367
- return keyframes;
1368
- }
1369
- function solveCinematicUniform(steps, shots, shotViewports, _settings, tension, friction, totalDurationMs, intervalMs) {
1370
- const keyframes = [];
1371
- let spring = springFromViewport(shotViewports[0]);
1372
- let currentShotIdx = 0;
1373
- const stepBoundaries = buildStepTimeBoundaries(steps);
1374
- for (let t = 0; t <= totalDurationMs; t += intervalMs) {
1375
- while (currentShotIdx < shots.length - 1 && t >= shots[currentShotIdx + 1].startTimeMs) {
1376
- currentShotIdx++;
1377
- }
1378
- const target = shotViewports[currentShotIdx];
1379
- const isHardCut = isNavigationBoundaryInRange(
1380
- stepBoundaries,
1381
- t - intervalMs,
1382
- t
1383
- );
1384
- if (isHardCut) {
1385
- const navViewport = { x: 0, y: 0, zoom: 1 };
1386
- spring = hardCutSpring(navViewport);
1387
- keyframes.push({
1388
- timeMs: t,
1389
- viewport: { ...navViewport },
1390
- isHardCut: true
1391
- });
1392
- } else {
1393
- spring = simulateSpring(spring, target, tension, friction, intervalMs);
1394
- keyframes.push({
1395
- timeMs: t,
1396
- viewport: viewportFromSpring(spring),
1397
- isHardCut: false
1398
- });
1399
- }
1400
- }
1401
- return keyframes;
1402
- }
1403
- function stabilizeZoomLevels(shotViewports, shots, imageWidth, imageHeight) {
1404
- if (shotViewports.length <= 2) return;
1405
- for (let i = 1; i < shotViewports.length - 1; i++) {
1406
- const prev = shotViewports[i - 1];
1407
- const curr = shotViewports[i];
1408
- const next = shotViewports[i + 1];
1409
- if (shots[i].isUserOverride) continue;
1410
- if (curr.zoom === prev.zoom || curr.zoom === next.zoom) continue;
1411
- if (i - 2 < 0 || i + 2 >= shotViewports.length) continue;
1412
- const prevPrev = shotViewports[i - 2];
1413
- const nextNext = shotViewports[i + 2];
1414
- if (prev.zoom !== prevPrev.zoom || next.zoom !== nextNext.zoom) continue;
1415
- const targetBounds = shots[i].targetBounds;
1416
- const centerX = targetBounds.x + targetBounds.width / 2;
1417
- const centerY = targetBounds.y + targetBounds.height / 2;
1418
- const viewportWidth = imageWidth / prev.zoom;
1419
- const viewportHeight = imageHeight / prev.zoom;
1420
- let x = centerX - viewportWidth / 2;
1421
- let y = centerY - viewportHeight / 2;
1422
- x = Math.max(0, Math.min(imageWidth - viewportWidth, x));
1423
- y = Math.max(0, Math.min(imageHeight - viewportHeight, y));
1424
- shotViewports[i] = { x, y, zoom: prev.zoom };
1425
- }
1426
- }
1427
- function buildStepTimeBoundaries(steps) {
1428
- const boundaries = [];
1429
- let timeMs = 0;
1430
- for (const step of steps) {
1431
- boundaries.push({
1432
- timeMs,
1433
- isNavigation: step.action === "navigate"
1434
- });
1435
- timeMs += step.durationMs + step.transitionDurationMs;
1436
- }
1437
- return boundaries;
1438
- }
1439
- function isNavigationBoundaryInRange(boundaries, fromMs, toMs) {
1440
- return boundaries.some(
1441
- (b) => b.isNavigation && b.timeMs > fromMs && b.timeMs <= toMs
1442
- );
1443
- }
1444
-
1445
- // src/timeline/beat-timing.ts
1446
- var DEFAULT_DURATION_MS = 1800;
1447
- function resolveBeatTiming(at, until, timeline) {
1448
- const startIndex = findTimelineIndex(at, timeline);
1449
- if (startIndex === -1) {
1450
- throw new Error(`Timeline event "${at}" was not found.`);
1451
- }
1452
- const startEvent = timeline[startIndex];
1453
- if (!startEvent) {
1454
- throw new Error(`Timeline event "${at}" was not found.`);
1455
- }
1456
- const startMs = eventTimeMs(startEvent, at);
1457
- const endMs = until ? eventTimeMs(findTimelineEvent(until, timeline), until) : nextEventTimeMs(startIndex, timeline) ?? startMs + DEFAULT_DURATION_MS;
1458
- if (endMs <= startMs) {
1459
- throw new Error(
1460
- `Timeline event "${until ?? "next"}" must be after "${at}".`
1461
- );
1462
- }
1463
- return {
1464
- startMs,
1465
- endMs,
1466
- durationMs: endMs - startMs
1467
- };
1468
- }
1469
- function findTimelineEvent(key, timeline) {
1470
- const event = timeline[findTimelineIndex(key, timeline)];
1471
- if (!event) {
1472
- throw new Error(`Timeline event "${key}" was not found.`);
1473
- }
1474
- return event;
1475
- }
1476
- function findTimelineIndex(key, timeline) {
1477
- return timeline.findIndex((event) => eventMatches(event, key));
1478
- }
1479
- function eventMatches(event, key) {
1480
- return ["id", "name", "type", "key", "label", "slug"].some(
1481
- (field) => event[field] === key
1482
- );
1483
- }
1484
- function nextEventTimeMs(startIndex, timeline) {
1485
- for (const event of timeline.slice(startIndex + 1)) {
1486
- const time = maybeEventTimeMs(event);
1487
- if (typeof time === "number") return time;
1488
- }
1489
- return void 0;
1490
- }
1491
- function eventTimeMs(event, key) {
1492
- const time = maybeEventTimeMs(event);
1493
- if (typeof time !== "number") {
1494
- throw new Error(`Timeline event "${key}" is missing a numeric tMs value.`);
1495
- }
1496
- return time;
1497
- }
1498
- function maybeEventTimeMs(event) {
1499
- for (const field of ["tMs", "timestampMs", "timeMs", "ms"]) {
1500
- const value = event[field];
1501
- if (typeof value === "number" && Number.isFinite(value)) return value;
1502
- }
1503
- return void 0;
1504
- }
1505
-
1506
- // src/timeline/source-to-frame.ts
1507
- var DEFAULT_SOURCE = { width: 1440, height: 900 };
1508
- var DEFAULT_FRAME_VIDEO = { width: 1280, height: 800 };
1509
- var DEFAULT_FRAME_BAR_H = 36;
1510
- function sourceToFrame(rect, source = DEFAULT_SOURCE, frame = DEFAULT_FRAME_VIDEO, barH = DEFAULT_FRAME_BAR_H) {
1511
- const scaleX = frame.width / source.width;
1512
- const scaleY = frame.height / source.height;
1513
- return {
1514
- x: rect.x * scaleX,
1515
- y: barH + rect.y * scaleY,
1516
- w: rect.w * scaleX,
1517
- h: rect.h * scaleY
1518
- };
1519
- }
1520
-
1521
- // src/primitives/product-film-utils.ts
1522
- var DEFAULT_STAGE = { width: 1440, height: 900 };
1523
- function timingFor(at, until, timeline) {
1524
- return resolveBeatTiming(at, until, timeline);
1525
- }
1526
- function readTargetRect(workflow, target) {
1527
- const value = workflow.targets?.[target];
1528
- if (!isRecord(value)) {
1529
- throw new Error(`Annotation target "${target}" was not found.`);
1530
- }
1531
- const rect = {
1532
- x: numberField(value, "x"),
1533
- y: numberField(value, "y"),
1534
- width: numberField(value, "width") ?? numberField(value, "w"),
1535
- height: numberField(value, "height") ?? numberField(value, "h")
1536
- };
1537
- if (typeof rect.x !== "number" || typeof rect.y !== "number" || typeof rect.width !== "number" || typeof rect.height !== "number") {
1538
- throw new Error(
1539
- `Annotation target "${target}" must include numeric x, y, width/height fields.`
1540
- );
1541
- }
1542
- return rect;
1543
- }
1544
- function readSourceSize(workflow) {
1545
- return sizeFromUnknown(workflow.source) ?? sizeFromUnknown(workflow.captureSize) ?? DEFAULT_SOURCE;
1546
- }
1547
- function frameRectFor(rect, source, chrome = "none") {
1548
- const frame = chrome === "none" ? DEFAULT_STAGE : DEFAULT_FRAME_VIDEO;
1549
- return sourceToFrame(
1550
- { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
1551
- source,
1552
- frame,
1553
- chrome === "none" ? 0 : DEFAULT_FRAME_BAR_H
1554
- );
1555
- }
1556
- function fullStageScale(source) {
1557
- return {
1558
- scaleX: DEFAULT_STAGE.width / source.width,
1559
- scaleY: DEFAULT_STAGE.height / source.height
1560
- };
1561
- }
1562
- function cssPx(value) {
1563
- return `${Math.round(value * 100) / 100}px`;
1564
- }
1565
- function stableClassPart(value) {
1566
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1567
- }
1568
- function sizeFromUnknown(value) {
1569
- if (!isRecord(value)) return void 0;
1570
- const width = numberField(value, "width");
1571
- const height = numberField(value, "height");
1572
- if (typeof width !== "number" || typeof height !== "number") return void 0;
1573
- return { width, height };
1574
- }
1575
- function numberField(value, field) {
1576
- const candidate = value[field];
1577
- return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : void 0;
1578
- }
1579
- function isRecord(value) {
1580
- return typeof value === "object" && value !== null;
1581
- }
1582
-
1583
- // src/primitives/camera-solver.ts
1584
- var DEFAULT_CAMERA_SOLVER_SETTINGS = {
1585
- mode: "cinematic",
1586
- damping: 0.55,
1587
- padding: 2.4
1588
- };
1589
- function solveProductFilmCameraPath(steps, workflow, options = {}) {
1590
- const source = readSourceSize(workflow);
1591
- const timedSteps = steps.map((step) => ({
1592
- step,
1593
- timing: timingFor(step.at, step.until, workflow.timeline ?? [])
1594
- }));
1595
- if (timedSteps.length === 0) {
1596
- return { source, timedSteps, solved: [] };
1597
- }
1598
- const solverInput = timedSteps.map(({ step, timing }) => ({
1599
- id: step.id,
1600
- action: step.camera === "wide" ? "navigate" : step.id,
1601
- bounds: targetBoundsForStep(step, workflow, source, options),
1602
- durationMs: Math.max(1, timing.durationMs),
1603
- transitionDurationMs: 0,
1604
- camera: { strategy: cameraStrategy(step.camera) }
1605
- }));
1606
- const solved = solveCameraPath({
1607
- steps: solverInput,
1608
- imageWidth: source.width,
1609
- imageHeight: source.height,
1610
- settings: {
1611
- mode: options.settings?.mode ?? DEFAULT_CAMERA_SOLVER_SETTINGS.mode,
1612
- damping: options.settings?.damping ?? DEFAULT_CAMERA_SOLVER_SETTINGS.damping,
1613
- padding: options.settings?.padding ?? DEFAULT_CAMERA_SOLVER_SETTINGS.padding
1614
- },
1615
- sampleIntervalMs: options.sampleIntervalMs
1616
- });
1617
- return { source, timedSteps, solved };
1618
- }
1619
- function cameraStrategy(camera) {
1620
- if (camera === "wide") return "wide";
1621
- if (camera === "hold") return "fix";
1622
- return "auto";
1623
- }
1624
- function targetBoundsForStep(step, workflow, source, options) {
1625
- const target = step.target;
1626
- const shouldReadTarget = typeof target === "string" && step.camera !== "wide" && (step.camera !== "hold" || options.includeHoldTargetBounds === true);
1627
- if (shouldReadTarget) {
1628
- return readTargetRect(workflow, target);
1629
- }
1630
- return { x: 0, y: 0, width: source.width, height: source.height };
1631
- }
1632
-
1633
- // src/primitives/scene-camera.ts
1634
- function solveSceneCameraPath(steps, workflow, options = {}) {
1635
- const { source, timedSteps, solved } = solveProductFilmCameraPath(
1636
- steps,
1637
- workflow,
1638
- options
1639
- );
1640
- if (timedSteps.length === 0) {
1641
- return { keyframes: [], source };
1642
- }
1643
- const firstStartMs = timedSteps[0]?.timing.startMs ?? 0;
1644
- const keyframes = solved.map((frame) => ({
1645
- tMs: firstStartMs + frame.timeMs,
1646
- x: frame.viewport.x,
1647
- y: frame.viewport.y,
1648
- zoom: frame.viewport.zoom,
1649
- isHardCut: frame.isHardCut
1650
- }));
1651
- return { keyframes, source };
1652
- }
1653
- function solveSceneCameraFromContext(steps, options) {
1654
- const { workflow } = useWorkflowContext();
1655
- return solveSceneCameraPath(steps, workflow, {
1656
- ...options,
1657
- sampleIntervalMs: options?.sampleIntervalMs ?? 1e3 / 60
1658
- });
1659
- }
1660
-
1661
- // src/primitives/scene-motion.ts
1662
- function solveSceneMotion(steps, workflow, options = {}) {
1663
- const timeline = workflow.timeline ?? [];
1664
- const instructions = steps.map((step) => {
1665
- const timing = timingFor(step.at, step.until, timeline);
1666
- if (step.type === "highlight") {
1667
- return {
1668
- type: "highlight",
1669
- target: step.target ?? "auto",
1670
- startMs: timing.startMs,
1671
- endMs: timing.endMs,
1672
- walk: step.walk ?? true,
1673
- rows: Math.max(1, step.rows ?? 8),
1674
- padPx: Math.max(0, step.padPx ?? 6)
1675
- };
1676
- }
1677
- if (step.type === "cursor") {
1678
- return {
1679
- type: "cursor",
1680
- to: step.to ?? "auto",
1681
- startMs: timing.startMs,
1682
- endMs: timing.endMs,
1683
- click: step.click ?? true,
1684
- fromXFrac: step.from?.x ?? 0.5,
1685
- fromYFrac: step.from?.y ?? 0.92
1686
- };
1687
- }
1688
- if (step.type === "type") {
1689
- return {
1690
- type: "type",
1691
- target: step.target ?? "auto",
1692
- startMs: timing.startMs,
1693
- endMs: timing.endMs,
1694
- text: step.text ?? "",
1695
- caret: step.caret ?? true
1696
- };
1697
- }
1698
- if (step.type === "countUp") {
1699
- return {
1700
- type: "countUp",
1701
- target: step.target ?? "auto",
1702
- startMs: timing.startMs,
1703
- endMs: timing.endMs,
1704
- from: step.from ?? 0,
1705
- to: step.to ?? null,
1706
- format: step.format ?? "number"
1707
- };
1708
- }
1709
- if (step.type === "scrollTo") {
1710
- return {
1711
- type: "scrollTo",
1712
- target: step.target ?? "auto",
1713
- to: step.to ?? null,
1714
- startMs: timing.startMs,
1715
- endMs: timing.endMs
1716
- };
1717
- }
1718
- if (step.type === "populate") {
1719
- return {
1720
- type: "populate",
1721
- target: step.target ?? "auto",
1722
- startMs: timing.startMs,
1723
- endMs: timing.endMs,
1724
- stagger: Math.max(0, step.stagger ?? 80),
1725
- distancePx: Math.max(0, step.distancePx ?? 16),
1726
- max: Math.max(1, step.max ?? 12)
1727
- };
1728
- }
1729
- return {
1730
- type: "reveal",
1731
- target: step.target ?? "auto",
1732
- startMs: timing.startMs,
1733
- endMs: timing.endMs,
1734
- stagger: Math.max(0, step.stagger ?? options.defaultStagger ?? 80),
1735
- distancePx: Math.max(0, step.distancePx ?? options.defaultDistancePx ?? 12),
1736
- max: Math.max(1, step.max ?? options.defaultMax ?? 12)
1737
- };
1738
- });
1739
- return { instructions };
1740
- }
1741
- function solveSceneMotionFromContext(steps, options) {
1742
- const { workflow } = useWorkflowContext();
1743
- return solveSceneMotion(steps, workflow, options);
1744
- }
1745
-
1746
- // src/primitives/Scene.tsx
1747
- function Scene({
1748
- artifact,
1749
- chrome = "none",
1750
- url = "app.reshot.dev",
1751
- camera,
1752
- cameraOptions,
1753
- motion,
1754
- motionOptions,
1755
- children
1756
- }) {
1757
- const showChrome = chrome !== "none";
1758
- const mountScript = buildSceneMountScript(artifact);
1759
- const cameraPath = camera && camera.length > 0 ? solveSceneCameraFromContext(camera, cameraOptions) : void 0;
1760
- const cameraScript = buildSceneCameraScript(cameraPath);
1761
- const motionConfig = motion && motion.length > 0 ? solveSceneMotionFromContext(motion, motionOptions) : void 0;
1762
- const motionScript = buildSceneMotionScript(motionConfig);
1763
- return /* @__PURE__ */ jsxs("div", { className: `reshot-frame reshot-frame--${chrome}`, "data-product-surface": "true", children: [
1764
- showChrome ? /* @__PURE__ */ jsxs("div", { className: "reshot-frame__chrome", children: [
1765
- /* @__PURE__ */ jsxs("div", { className: "reshot-frame__traffic", "aria-hidden": "true", children: [
1766
- /* @__PURE__ */ jsx("span", {}),
1767
- /* @__PURE__ */ jsx("span", {}),
1768
- /* @__PURE__ */ jsx("span", {})
1769
- ] }),
1770
- /* @__PURE__ */ jsx("div", { className: "reshot-frame__url", children: url })
1771
- ] }) : null,
1772
- /* @__PURE__ */ jsxs("div", { className: "reshot-frame__viewport", children: [
1773
- /* @__PURE__ */ jsx("style", { children: ".reshot-frame__media-layer--scene{will-change:auto}" }),
1774
- /* @__PURE__ */ jsxs("div", { className: "reshot-frame__media-layer reshot-frame__media-layer--scene", children: [
1775
- /* @__PURE__ */ jsx(
1776
- "div",
1777
- {
1778
- className: SCENE_ROOT_CLASS,
1779
- "data-reshot-scene": "1",
1780
- "aria-hidden": "true"
1781
- }
1782
- ),
1783
- /* @__PURE__ */ jsx("script", { children: mountScript }),
1784
- cameraScript ? /* @__PURE__ */ jsx("script", { children: cameraScript }) : null,
1785
- motionScript ? /* @__PURE__ */ jsx("script", { children: motionScript }) : null
1786
- ] }),
1787
- /* @__PURE__ */ jsx("div", { className: "reshot-frame__overlays", children })
1788
- ] })
1789
- ] });
1790
- }
1791
-
1792
- // src/primitives/Annotation.tsx
1793
- var ANNOTATION_IN_MS = 180;
1794
- var ANNOTATION_OUT_MS = 140;
1795
- function Annotation({
1796
- at,
1797
- until,
1798
- target,
1799
- edge,
1800
- chrome = "none",
1801
- tone = "neutral",
1802
- label,
1803
- children
1804
- }) {
1805
- if (!target && !edge) {
1806
- throw new Error("Annotation requires either a target or an explicit edge placement.");
1807
- }
1808
- const { workflow } = useWorkflowContext();
1809
- const timing = timingFor(at, until, workflow.timeline ?? []);
1810
- const className = `reshot-annotation-timing-${stableClassPart(
1811
- target ?? edge ?? at
1812
- )}-${stableClassPart(at)}`;
1813
- const content = children ?? label;
1814
- const timingStyle = /* @__PURE__ */ jsx("style", { children: `
1815
- .${className} {
1816
- animation:
1817
- annotationIn ${ANNOTATION_IN_MS}ms ${timing.startMs}ms ease-out forwards,
1818
- annotationOut ${ANNOTATION_OUT_MS}ms ${timing.endMs}ms ease-in forwards;
1819
- }
1820
- ` });
1821
- if (target) {
1822
- const source = readSourceSize(workflow);
1823
- const frameRect = frameRectFor(readTargetRect(workflow, target), source, chrome);
1824
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1825
- timingStyle,
1826
- /* @__PURE__ */ jsxs(
1827
- "div",
1828
- {
1829
- className: `reshot-annotation reshot-annotation--${tone} ${className}`,
1830
- "data-annotation-target": target,
1831
- style: {
1832
- left: cssPx(frameRect.x),
1833
- top: cssPx(frameRect.y),
1834
- width: cssPx(frameRect.w),
1835
- height: cssPx(frameRect.h)
1836
- },
1837
- children: [
1838
- /* @__PURE__ */ jsx("span", { className: "reshot-annotation__ring" }),
1839
- content ? /* @__PURE__ */ jsx("span", { className: "reshot-annotation__label", children: content }) : null
1840
- ]
1841
- }
1842
- )
1843
- ] });
1844
- }
1845
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1846
- timingStyle,
1847
- /* @__PURE__ */ jsx(
1848
- "div",
1849
- {
1850
- className: `reshot-annotation reshot-annotation--edge reshot-annotation--${tone} ${className}`,
1851
- "data-annotation-edge": edge,
1852
- style: edgeStyle(edge),
1853
- children: content ? /* @__PURE__ */ jsx("span", { className: "reshot-annotation__label", children: content }) : null
1854
- }
1855
- )
1856
- ] });
1857
- }
1858
- function edgeStyle(edge) {
1859
- const inset = "20px";
1860
- switch (edge) {
1861
- case "top-right":
1862
- return { top: inset, right: inset };
1863
- case "bottom-left":
1864
- return { bottom: inset, left: inset };
1865
- case "bottom-right":
1866
- return { bottom: inset, right: inset };
1867
- case "top-left":
1868
- default:
1869
- return { top: inset, left: inset };
1870
- }
1871
- }
1872
-
1873
- // src/primitives/FocusPath.tsx
1874
- function FocusPath({ steps, className }) {
1875
- const { workflow, slug } = useWorkflowContext();
1876
- const focusClass = className ?? `reshot-focus-${stableClassPart(slug)}-${steps.length}`;
1877
- const { source, timedSteps, solved } = solveProductFilmCameraPath(
1878
- steps,
1879
- workflow,
1880
- { includeHoldTargetBounds: true }
1881
- );
1882
- if (timedSteps.length === 0) {
1883
- return /* @__PURE__ */ jsx("div", { "data-focus-path": focusClass, hidden: true });
1884
- }
1885
- const firstStartMs = timedSteps[0]?.timing.startMs ?? 0;
1886
- const totalMs = Math.max(
1887
- 1,
1888
- timedSteps[timedSteps.length - 1]?.timing.endMs ?? workflow.durationMs ?? 1
1889
- );
1890
- const keyframes = solved.map((frame) => {
1891
- const absoluteMs = firstStartMs + frame.timeMs;
1892
- const percent = Math.max(0, Math.min(100, absoluteMs / totalMs * 100));
1893
- return `${percent.toFixed(3)}% { transform: ${transformFor(
1894
- frame.viewport.x,
1895
- frame.viewport.y,
1896
- frame.viewport.zoom,
1897
- source.width,
1898
- source.height
1899
- )}; }`;
1900
- }).join("\n");
1901
- const firstTransform = solved[0] ? transformFor(
1902
- solved[0].viewport.x,
1903
- solved[0].viewport.y,
1904
- solved[0].viewport.zoom,
1905
- source.width,
1906
- source.height
1907
- ) : "translate(0px, 0px) scale(1)";
1908
- const last = solved[solved.length - 1];
1909
- const lastTransform = last ? transformFor(
1910
- last.viewport.x,
1911
- last.viewport.y,
1912
- last.viewport.zoom,
1913
- source.width,
1914
- source.height
1915
- ) : firstTransform;
1916
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1917
- /* @__PURE__ */ jsx("style", { children: `
1918
- .${focusClass} .reshot-frame__media-layer,
1919
- .${focusClass} .reshot-frame__overlays {
1920
- transform: ${firstTransform};
1921
- animation: ${focusClass}-camera ${totalMs}ms linear forwards;
1922
- }
1923
- @keyframes ${focusClass}-camera {
1924
- 0% { transform: ${firstTransform}; }
1925
- ${keyframes}
1926
- 100% { transform: ${lastTransform}; }
1927
- }
1928
- ` }),
1929
- /* @__PURE__ */ jsx("div", { "data-focus-path": focusClass, hidden: true })
1930
- ] });
1931
- }
1932
- function transformFor(x, y, zoom, sourceWidth, sourceHeight) {
1933
- const { scaleX, scaleY } = fullStageScale({
1934
- width: sourceWidth,
1935
- height: sourceHeight
1936
- });
1937
- const tx = Math.round(-x * scaleX * zoom * 100) / 100;
1938
- const ty = Math.round(-y * scaleY * zoom * 100) / 100;
1939
- const roundedZoom = Math.round(zoom * 1e3) / 1e3;
1940
- return `translate(${tx}px, ${ty}px) scale(${roundedZoom})`;
1941
- }
1942
-
1943
- // src/primitives/ProductFilm.tsx
1944
- function ProductFilm({
1945
- src,
1946
- url,
1947
- chrome = "none",
1948
- fit = "contain",
1949
- steps,
1950
- children
1951
- }) {
1952
- const { slug } = useWorkflowContext();
1953
- const focusClass = `reshot-product-film-${stableClassPart(slug)}`;
1954
- const annotations = steps.filter((step) => step.target && step.label).map((step) => /* @__PURE__ */ jsx(
1955
- Annotation,
1956
- {
1957
- at: step.at,
1958
- until: step.until,
1959
- target: step.target,
1960
- chrome,
1961
- tone: step.tone,
1962
- children: step.label
1963
- }
1964
- ));
1965
- return /* @__PURE__ */ jsx("div", { className: `reshot-product-film ${focusClass}`, children: /* @__PURE__ */ jsxs(Frame, { chrome, url, src, fit, children: [
1966
- /* @__PURE__ */ jsx(FocusPath, { steps, className: focusClass }),
1967
- annotations,
1968
- children
1969
- ] }) });
1970
- }
1971
- export {
1972
- Annotation,
1973
- Composition,
1974
- DEFAULT_FRAME_BAR_H,
1975
- DEFAULT_FRAME_VIDEO,
1976
- DEFAULT_SOURCE,
1977
- FocusPath,
1978
- Fragment,
1979
- Frame,
1980
- ProductFilm,
1981
- Scene,
1982
- buildSceneCameraScript,
1983
- buildSceneMotionScript,
1984
- compile,
1985
- compileToHtml,
1986
- getWorkflowContext,
1987
- jsx,
1988
- jsxDEV,
1989
- jsxs,
1990
- resolveBeatTiming,
1991
- solveSceneCameraFromContext,
1992
- solveSceneCameraPath,
1993
- solveSceneMotion,
1994
- solveSceneMotionFromContext,
1995
- sourceToFrame,
1996
- useWorkflowContext
1997
- };