@mevdragon/vidfarm-devcli 0.20.5 → 0.20.8

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 (154) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
  13. package/SKILL.director.md +521 -261
  14. package/dist/src/cli.js +404 -96
  15. package/dist/src/devcli/clips.js +15 -10
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21602
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -756
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3517
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,1053 +0,0 @@
1
- // Runtime script injected into composition iframes so the studio editor can
2
- // see a `__player` adapter and drive playback. The script lives in src/ so the
3
- // API lambda can inject it into both draft and fork composition HTML before
4
- // serving it to the editor iframe.
5
- //
6
- // IMPORTANT: this body is executed inside the iframe, NOT in the Node API
7
- // runtime. Keep it browser-only (no node imports, no top-level await).
8
- export const COMPOSITION_RUNTIME_SCRIPT_BODY = `
9
- (() => {
10
- const root = document.querySelector("[data-composition-id]");
11
- let declaredDuration = Number(root?.getAttribute("data-duration") || 0);
12
- let duration = declaredDuration;
13
- let time = 0;
14
- let playing = false;
15
- let playbackRate = 1;
16
- let previewMuted = false;
17
- let raf = 0;
18
- let last = 0;
19
- const backingVideo = document.querySelector("[data-vf-playback-source]");
20
- const initialMuted = new WeakMap();
21
- if (backingVideo instanceof HTMLMediaElement) {
22
- initialMuted.set(backingVideo, backingVideo.muted || backingVideo.hasAttribute("muted"));
23
- }
24
- let clips = [];
25
- let activeVideoProxyKey = null;
26
- let activeVideoProxyNode = null;
27
- let hasVideoProxies = false;
28
- const activeMedia = new WeakSet();
29
- const mediaStateKeys = new WeakMap();
30
- const visibilityStates = new WeakMap();
31
-
32
- function clampTime(nextTime) {
33
- const safe = Number(nextTime);
34
- if (!Number.isFinite(safe)) return 0;
35
- return Math.max(0, Math.min(duration, safe));
36
- }
37
-
38
- function readNumberAttr(node, names, fallback = 0) {
39
- for (const name of names) {
40
- const raw = node.getAttribute(name);
41
- if (raw !== null) {
42
- const value = Number(raw);
43
- return Number.isFinite(value) ? value : fallback;
44
- }
45
- }
46
- return fallback;
47
- }
48
-
49
- function readTiming(clip) {
50
- const start = readNumberAttr(clip, ["data-start"], 0);
51
- const clipDuration = readNumberAttr(clip, ["data-duration"], 0);
52
- const mediaStart = readNumberAttr(clip, ["data-playback-start", "data-media-start"], 0);
53
- return { start, duration: clipDuration, end: start + clipDuration, mediaStart };
54
- }
55
-
56
- function refreshClips() {
57
- // The parent editor can live-patch the composition DOM. Re-read the root's
58
- // declared duration so a patched data-duration followed by refreshClips()
59
- // never leaves the player clamped to a stale duration.
60
- declaredDuration = Number(root?.getAttribute("data-duration") || 0);
61
- duration = declaredDuration;
62
- clips = Array.from(document.querySelectorAll("[data-start]")).filter((node) => node !== root);
63
- for (const clip of clips) {
64
- if (clip instanceof HTMLMediaElement && !initialMuted.has(clip)) {
65
- initialMuted.set(clip, clip.muted || clip.hasAttribute("muted"));
66
- }
67
- if (clip instanceof HTMLMediaElement) {
68
- clip.preload = "auto";
69
- if (clip.readyState === 0) {
70
- try { clip.load(); } catch {}
71
- }
72
- }
73
- }
74
- if (backingVideo instanceof HTMLMediaElement) {
75
- backingVideo.preload = "auto";
76
- if (backingVideo.readyState === 0) {
77
- try { backingVideo.load(); } catch {}
78
- }
79
- }
80
- activeVideoProxyKey = null;
81
- activeVideoProxyNode = null;
82
- hasVideoProxies = clips.some((clip) => clip instanceof Element && clip.getAttribute("data-vf-timeline-proxy") === "video");
83
- discoverGeneralCssAnimations();
84
- apply(time, { forceSeek: true });
85
- }
86
-
87
- function applyMediaState(clip) {
88
- const volume = Number(clip.getAttribute("data-volume") || 1);
89
- const nextMuted = previewMuted || initialMuted.get(clip) === true;
90
- const stateKey = playbackRate + "|" + volume + "|" + nextMuted;
91
- if (mediaStateKeys.get(clip) === stateKey) return;
92
- mediaStateKeys.set(clip, stateKey);
93
- if (Number.isFinite(volume)) clip.volume = Math.max(0, Math.min(1, volume));
94
- clip.muted = nextMuted;
95
- try { clip.playbackRate = playbackRate; } catch {}
96
- try { clip.defaultPlaybackRate = playbackRate; } catch {}
97
- try {
98
- if ("preservesPitch" in clip) clip.preservesPitch = true;
99
- if ("mozPreservesPitch" in clip) clip.mozPreservesPitch = true;
100
- if ("webkitPreservesPitch" in clip) clip.webkitPreservesPitch = true;
101
- } catch {}
102
- }
103
-
104
- function setVisibility(node, active) {
105
- const next = active ? "visible" : "hidden";
106
- if (visibilityStates.get(node) === next) return;
107
- visibilityStates.set(node, next);
108
- node.style.visibility = next;
109
- }
110
-
111
- function setPlaybackRate(nextRate) {
112
- const next = Number(nextRate);
113
- playbackRate = Number.isFinite(next) && next > 0 ? Math.max(0.05, Math.min(8, next)) : 1;
114
- for (const clip of clips) {
115
- if (clip instanceof HTMLMediaElement) applyMediaState(clip);
116
- }
117
- if (backingVideo instanceof HTMLMediaElement) applyMediaState(backingVideo);
118
- }
119
-
120
- function setMuted(nextMuted) {
121
- previewMuted = Boolean(nextMuted);
122
- for (const clip of clips) {
123
- if (clip instanceof HTMLMediaElement) applyMediaState(clip);
124
- }
125
- if (backingVideo instanceof HTMLMediaElement) applyMediaState(backingVideo);
126
- }
127
-
128
- function clipKey(clip) {
129
- return clip.getAttribute("data-hf-id") || clip.id || clip.getAttribute("data-label") || "";
130
- }
131
-
132
- function ensurePlaying(media) {
133
- if (!playing) {
134
- if (!media.paused) media.pause();
135
- activeMedia.delete(media);
136
- return;
137
- }
138
- if (!activeMedia.has(media) || media.paused) {
139
- activeMedia.add(media);
140
- void media.play().catch(() => {});
141
- }
142
- }
143
-
144
- function pauseInactiveMedia(media) {
145
- activeMedia.delete(media);
146
- if (!media.paused) media.pause();
147
- }
148
-
149
- function seekMedia(media, target, forceSeek, driftLimit) {
150
- if (!Number.isFinite(target)) return;
151
- if (forceSeek || Math.abs(media.currentTime - target) > driftLimit) {
152
- try { media.currentTime = Math.max(0, target); } catch {}
153
- }
154
- }
155
-
156
- // Runtime-adapter bridge — mirrors the official @hyperframes/producer
157
- // deterministic adapters so compositions driven by a registered GSAP
158
- // timeline (window.__timelines), Anime.js (window.__hfAnime), Lottie
159
- // (window.__hfLottie), or Three.js/TypeGPU (window.__hfThreeTime/
160
- // __hfTypegpuTime + the hf-seek CustomEvent) scrub with the player instead
161
- // of running on the wall clock. Everything is duck-typed and try/caught so
162
- // a broken composition script never kills the apply loop.
163
- function compositionTimeline() {
164
- let registry = null;
165
- try { registry = window.__timelines; } catch { return null; }
166
- if (!registry || typeof registry !== "object") return null;
167
- const compId = root?.getAttribute("data-composition-id") || "";
168
- let timeline = compId ? registry[compId] : null;
169
- if (!timeline) {
170
- const keys = Object.keys(registry);
171
- if (keys.length === 1) timeline = registry[keys[0]];
172
- }
173
- if (!timeline || typeof timeline.pause !== "function") return null;
174
- if (typeof timeline.totalTime !== "function" && typeof timeline.seek !== "function") return null;
175
- return timeline;
176
- }
177
-
178
- function timelineDuration(timeline) {
179
- try {
180
- const value = typeof timeline.duration === "function" ? Number(timeline.duration()) : Number(timeline.duration);
181
- if (Number.isFinite(value) && value > 0) return value;
182
- } catch {}
183
- return 0;
184
- }
185
-
186
- function syncTimeline() {
187
- const timeline = compositionTimeline();
188
- if (!timeline) return;
189
- try {
190
- timeline.pause();
191
- // Official gsap adapter seek: nudge to time+1ms with suppressed events,
192
- // then land exactly, so gsap re-renders even when the target equals the
193
- // timeline's current time.
194
- if (typeof timeline.totalTime === "function") {
195
- timeline.totalTime(time + 0.001, true);
196
- timeline.totalTime(time, false);
197
- } else {
198
- timeline.seek(time, false);
199
- }
200
- } catch {}
201
- }
202
-
203
- function syncAnimeInstances() {
204
- let instances = null;
205
- try { instances = window.__hfAnime; } catch { return; }
206
- if (!Array.isArray(instances) || instances.length === 0) return;
207
- const ms = Math.max(0, time) * 1000;
208
- for (const instance of instances) {
209
- try {
210
- if (instance && typeof instance.seek === "function") instance.seek(ms);
211
- } catch {}
212
- }
213
- }
214
-
215
- function syncLottiePlayers() {
216
- let players = null;
217
- try { players = window.__hfLottie; } catch { return; }
218
- if (!Array.isArray(players) || players.length === 0) return;
219
- const safe = Math.max(0, time);
220
- for (const player of players) {
221
- try {
222
- if (!player || typeof player !== "object") continue;
223
- if (typeof player.goToAndStop === "function") {
224
- player.goToAndStop(safe * 1000, false);
225
- } else if (typeof player.pause === "function" && ("totalFrames" in player || "duration" in player)) {
226
- if (typeof player.setCurrentRawFrameValue === "function") {
227
- const totalFrames = player.totalFrames ?? 0;
228
- const frameRate = player.frameRate ?? 30;
229
- if (totalFrames > 0) player.setCurrentRawFrameValue(Math.min(safe * frameRate, totalFrames - 1));
230
- } else if (typeof player.seek === "function") {
231
- player.seek(Math.min(100, (safe / (player.duration ?? 1)) * 100));
232
- }
233
- }
234
- } catch {}
235
- }
236
- }
237
-
238
- let lastDispatchedSeekTime = -1;
239
- function syncSeekGlobals() {
240
- const safe = Math.max(0, time);
241
- try { window.__hfThreeTime = safe; } catch {}
242
- try { window.__hfTypegpuTime = safe; } catch {}
243
- // The official three/typegpu adapters dedupe the hf-seek dispatch on the
244
- // last dispatched time.
245
- if (safe === lastDispatchedSeekTime) return;
246
- lastDispatchedSeekTime = safe;
247
- try { window.dispatchEvent(new CustomEvent("hf-seek", { detail: { time: safe } })); } catch {}
248
- }
249
-
250
- function syncRuntimeAdapters() {
251
- syncTimeline();
252
- syncAnimeInstances();
253
- syncLottiePlayers();
254
- syncSeekGlobals();
255
- }
256
-
257
- // data-duration on the root is authoritative when present. Arbitrary
258
- // compositions may omit it, so infer the duration from clip extents,
259
- // preferring a registered GSAP timeline that reports a longer duration.
260
- // Recomputed on every apply so a timeline registered after load still
261
- // corrects the duration (and seek clamping) lazily.
262
- function ensureDuration() {
263
- if (declaredDuration > 0) {
264
- duration = declaredDuration;
265
- return;
266
- }
267
- let inferred = 0;
268
- for (const clip of clips) {
269
- const timing = readTiming(clip);
270
- if (timing.end > inferred) inferred = timing.end;
271
- }
272
- const timeline = compositionTimeline();
273
- if (timeline) {
274
- const reported = timelineDuration(timeline);
275
- if (reported > inferred) inferred = reported;
276
- }
277
- duration = inferred;
278
- }
279
-
280
- // Ken Burns image clips carry a CSS transform animation (vf-kb-* keyframes)
281
- // directly on the [data-kenburns] <img>. CSS animations run on the wall clock,
282
- // not player time, so the runtime scrubs them: pause + set currentTime from
283
- // normalized clip progress. Normalizing against data-duration (instead of
284
- // trusting the inline --vf-kb-dur) keeps the preview correct even while a
285
- // timeline resize has not yet re-synced the inline duration.
286
- function syncKenBurnsClip(clip, timing) {
287
- if (typeof clip.getAnimations !== "function") return;
288
- const progress = timing.duration > 0
289
- ? Math.max(0, Math.min(1, (time - timing.start) / timing.duration))
290
- : 0;
291
- let anims = [];
292
- try { anims = clip.getAnimations({ subtree: true }); } catch { return; }
293
- for (const anim of anims) {
294
- if (typeof anim.animationName !== "string" || anim.animationName.indexOf("vf-kb-") !== 0) continue;
295
- let total = 0;
296
- try { total = Number(anim.effect?.getComputedTiming?.().duration) || 0; } catch {}
297
- if (!total) continue;
298
- try {
299
- anim.pause();
300
- anim.currentTime = progress * total;
301
- } catch {}
302
- }
303
- }
304
-
305
- // Scene transitions: a [data-transition] clip animates in (vf-tr-*
306
- // keyframes) over its first data-transition-duration seconds, ON TOP of the
307
- // previous clip on its track — so that previous clip must stay active for
308
- // the transition window. Publish materializes this overlap into
309
- // data-duration (normalizePublishHtml); the preview grants it live so
310
- // butt-cut drafts preview exactly like the render. A [data-transition-out]
311
- // clip additionally animates AWAY (vf-tr-out-* keyframes) over its last
312
- // data-transition-out-duration seconds — no hold needed, the exit window
313
- // lives inside the clip's own span.
314
- function collectTransitionWindows() {
315
- const windows = [];
316
- for (const clip of clips) {
317
- if (!(clip instanceof HTMLElement) || clip instanceof HTMLAudioElement) continue;
318
- if (!clip.hasAttribute("data-transition")) continue;
319
- const timing = readTiming(clip);
320
- let transitionDuration = Number(clip.getAttribute("data-transition-duration"));
321
- if (!Number.isFinite(transitionDuration) || transitionDuration <= 0) transitionDuration = 0.5;
322
- windows.push({
323
- track: Number(clip.getAttribute("data-track-index")) || 0,
324
- start: timing.start,
325
- duration: Math.min(transitionDuration, timing.duration)
326
- });
327
- }
328
- return windows;
329
- }
330
-
331
- function transitionHoldFor(clip, timing, windows) {
332
- if (!windows.length) return 0;
333
- if (!(clip instanceof HTMLElement) || clip instanceof HTMLAudioElement) return 0;
334
- const track = Number(clip.getAttribute("data-track-index")) || 0;
335
- let hold = 0;
336
- for (const window of windows) {
337
- if (window.track !== track) continue;
338
- // Nominal-end adjacency: already-materialized holds (published HTML)
339
- // shift this clip's end past the next start, fail the check, and are
340
- // simply not re-granted — no double extension either way.
341
- if (Math.abs(window.start - timing.end) > 0.05) continue;
342
- if (window.duration > hold) hold = window.duration;
343
- }
344
- return hold;
345
- }
346
-
347
- function syncTransitionClip(clip, timing, hold) {
348
- if (typeof clip.getAnimations !== "function") return;
349
- // Exit animations sit at the clip's tail via the inline --vf-tr-out-delay
350
- // var. Timeline edits touch data-duration only, so re-derive the delay
351
- // (duration + any live hold − out duration) before scrubbing; publish
352
- // materializes the same value for the render producer.
353
- if (clip.hasAttribute("data-transition-out")) {
354
- let outDuration = Number(clip.getAttribute("data-transition-out-duration"));
355
- if (!Number.isFinite(outDuration) || outDuration <= 0) outDuration = 0.5;
356
- outDuration = Math.min(outDuration, timing.duration);
357
- const expectedDelay = Math.max(0, timing.duration + (hold || 0) - outDuration);
358
- try {
359
- if (Math.abs((parseFloat(clip.style.getPropertyValue("--vf-tr-out-dur")) || 0) - outDuration) > 0.002) {
360
- clip.style.setProperty("--vf-tr-out-dur", outDuration.toFixed(3) + "s");
361
- }
362
- if (Math.abs((parseFloat(clip.style.getPropertyValue("--vf-tr-out-delay")) || 0) - expectedDelay) > 0.002) {
363
- clip.style.setProperty("--vf-tr-out-delay", expectedDelay.toFixed(3) + "s");
364
- }
365
- } catch {}
366
- }
367
- const offsetMs = Math.max(0, (time - timing.start)) * 1000;
368
- let anims = [];
369
- try { anims = clip.getAnimations({ subtree: true }); } catch { return; }
370
- for (const anim of anims) {
371
- if (typeof anim.animationName !== "string" || anim.animationName.indexOf("vf-tr-") !== 0) continue;
372
- try {
373
- anim.pause();
374
- anim.currentTime = offsetMs;
375
- } catch {}
376
- }
377
- }
378
-
379
- // Animated caption words carry vf-cap-* keyframes whose inline delay/
380
- // duration ARE the word windows relative to the layer start, so unlike Ken
381
- // Burns there is nothing to normalize — the scrub is simply the absolute
382
- // offset into the layer. Paused-by-stylesheet + scrubbed-here matches the
383
- // render producer's CSS adapter exactly.
384
- //
385
- // Stability: Chrome PERMANENTLY drops a paused fill:none CSS animation the
386
- // moment its currentTime is pushed past the effect end, and never re-creates
387
- // it while the computed animation-name is unchanged — so one forward scrub
388
- // across a word's window would leave that word stuck in its resting state
389
- // (invisible for word-pop/stack-up) on every later frame. Past the end, a
390
- // parked fill:none animation looks identical to the resting styles, so park
391
- // at 0 instead of overshooting. fill:forwards presets (karaoke-fill,
392
- // cumulative-reveal) must overshoot to hold their final keyframe — finished
393
- // + forwards animations keep applying their effect and survive. If a word's
394
- // animation is missing anyway (dropped before this guard, or an innerHTML
395
- // morph replaced the spans mid-scrub), restart it declaratively by cycling
396
- // animation-name through none.
397
- function captionWordAnimationFor(word) {
398
- let anims = [];
399
- try { anims = word.getAnimations(); } catch { return null; }
400
- for (const anim of anims) {
401
- if (typeof anim.animationName === "string" && anim.animationName.indexOf("vf-cap-") === 0) return anim;
402
- }
403
- return null;
404
- }
405
-
406
- function syncCaptionClip(clip, timing) {
407
- if (typeof clip.getAnimations !== "function") return;
408
- const offsetMs = Math.max(0, (time - timing.start)) * 1000;
409
- let words = [];
410
- try { words = Array.from(clip.querySelectorAll('[data-cap-word="true"]')); } catch { return; }
411
- for (const word of words) {
412
- if (typeof word.getAnimations !== "function") continue;
413
- let anim = captionWordAnimationFor(word);
414
- if (!anim) {
415
- const inlineEndMs =
416
- ((parseFloat(word.style.animationDelay) || 0) + (parseFloat(word.style.animationDuration) || 0)) * 1000;
417
- let needed = offsetMs <= inlineEndMs;
418
- if (!needed) {
419
- try { needed = getComputedStyle(word).animationFillMode.indexOf("forward") !== -1 || getComputedStyle(word).animationFillMode.indexOf("both") !== -1; } catch {}
420
- }
421
- if (!needed) continue;
422
- try {
423
- word.style.animationName = "none";
424
- void word.offsetWidth;
425
- word.style.removeProperty("animation-name");
426
- } catch { continue; }
427
- anim = captionWordAnimationFor(word);
428
- if (!anim) continue;
429
- }
430
- let target = offsetMs;
431
- try {
432
- const computed = anim.effect && typeof anim.effect.getComputedTiming === "function"
433
- ? anim.effect.getComputedTiming()
434
- : null;
435
- if (computed && typeof computed.endTime === "number" && offsetMs >= computed.endTime
436
- && computed.fill !== "forwards" && computed.fill !== "both") {
437
- target = 0;
438
- }
439
- } catch {}
440
- try {
441
- anim.pause();
442
- anim.currentTime = target;
443
- } catch {}
444
- }
445
- }
446
-
447
- // Generalized CSS keyframe scrub — the preview twin of the official
448
- // producer CSS adapter: every element whose computed animation-name is set
449
- // gets its animations paused and scrubbed to the player time relative to
450
- // the nearest ancestor-or-self [data-start] (0 when none). vf-* animations
451
- // (Ken Burns, transitions, captions) keep their dedicated machinery above.
452
- // Discovery is cached per refreshClips like the official adapter's discover
453
- // pass; elements added by live patches are picked up on the next refresh.
454
- // Each entry also caches its clip-relative start at discovery so the
455
- // per-frame sync skips the ancestor walk; a re-parented or retimed element
456
- // is likewise re-resolved on the next refreshClips discovery.
457
- let generalCssEntries = [];
458
-
459
- function discoverGeneralCssAnimations() {
460
- generalCssEntries = [];
461
- let nodes = [];
462
- try { nodes = document.querySelectorAll("*"); } catch { return; }
463
- for (const node of nodes) {
464
- if (!(node instanceof HTMLElement) || typeof node.getAnimations !== "function") continue;
465
- let names = "";
466
- try { names = getComputedStyle(node).animationName || ""; } catch { continue; }
467
- if (!names || names === "none") continue;
468
- const expected = [];
469
- for (const name of names.split(",")) {
470
- const trimmed = name.trim();
471
- if (!trimmed || trimmed === "none" || trimmed.indexOf("vf-") === 0) continue;
472
- expected.push(trimmed);
473
- }
474
- if (expected.length) generalCssEntries.push({ el: node, names: expected, start: generalClipStartFor(node) });
475
- }
476
- }
477
-
478
- function hasAnimationNamed(anims, name) {
479
- for (const anim of anims) {
480
- if (anim.animationName === name) return true;
481
- }
482
- return false;
483
- }
484
-
485
- function generalClipStartFor(node) {
486
- const holder = node.closest("[data-start]");
487
- return holder ? readNumberAttr(holder, ["data-start"], 0) : 0;
488
- }
489
-
490
- // Cycling animation-name through none re-creates dropped CSS animations
491
- // declaratively (same trick as the caption resurrection above), preserving
492
- // any authored inline animation-name.
493
- function resurrectCssAnimations(node) {
494
- try {
495
- const inline = node.style.animationName;
496
- node.style.animationName = "none";
497
- void node.offsetWidth;
498
- if (inline && inline !== "none") {
499
- node.style.animationName = inline;
500
- } else {
501
- node.style.removeProperty("animation-name");
502
- }
503
- } catch {}
504
- }
505
-
506
- function scrubGeneralAnimation(anim, offsetMs) {
507
- let target = offsetMs;
508
- try {
509
- const computed = anim.effect && typeof anim.effect.getComputedTiming === "function"
510
- ? anim.effect.getComputedTiming()
511
- : null;
512
- const endTime = computed && typeof computed.endTime === "number" ? computed.endTime : NaN;
513
- if (Number.isFinite(endTime) && offsetMs >= endTime) {
514
- // Same Chrome guard as the caption scrub: paused fill:none/backwards
515
- // animations pushed past their effect end are destroyed permanently,
516
- // so park them at 0; fill:forwards/both hold their final keyframe at
517
- // the clamped end. Infinite-iteration animations (endTime Infinity)
518
- // take the raw clip-relative offset like the official CSS adapter —
519
- // the browser resolves the iteration phase.
520
- target = computed.fill === "forwards" || computed.fill === "both" ? endTime : 0;
521
- }
522
- } catch {}
523
- try {
524
- anim.pause();
525
- anim.currentTime = target;
526
- } catch {}
527
- }
528
-
529
- function syncGeneralCssAnimations() {
530
- for (const entry of generalCssEntries) {
531
- const node = entry.el;
532
- if (!node.isConnected) continue;
533
- const offsetMs = Math.max(0, time - entry.start) * 1000;
534
- let anims = [];
535
- try { anims = node.getAnimations(); } catch { continue; }
536
- let missing = null;
537
- for (const name of entry.names) {
538
- if (!hasAnimationNamed(anims, name)) (missing || (missing = [])).push(name);
539
- }
540
- if (missing) {
541
- resurrectCssAnimations(node);
542
- try { anims = node.getAnimations(); } catch { anims = []; }
543
- // Names that still have no live animation (e.g. a display:none
544
- // subtree) are dropped until the next discovery so a hidden element
545
- // is not forced through a reflow every frame.
546
- entry.names = entry.names.filter((name) => missing.indexOf(name) === -1 || hasAnimationNamed(anims, name));
547
- }
548
- for (const anim of anims) {
549
- if (typeof anim.animationName !== "string" || anim.animationName.indexOf("vf-") === 0) continue;
550
- scrubGeneralAnimation(anim, offsetMs);
551
- }
552
- }
553
- }
554
-
555
- function apply(nextTime, options = {}) {
556
- const forceSeek = Boolean(options.forceSeek);
557
- const fromClock = Boolean(options.fromClock);
558
- ensureDuration();
559
- time = clampTime(nextTime);
560
- let activeVideoProxy = null;
561
- let activeVideoProxyStart = -Infinity;
562
- const transitionWindows = collectTransitionWindows();
563
- for (const clip of clips) {
564
- const timing = readTiming(clip);
565
- const hold = transitionHoldFor(clip, timing, transitionWindows);
566
- // hyperframes-core clip windows are inclusive of both ends — the final
567
- // frame holds.
568
- const active = time >= timing.start && time <= timing.end + hold;
569
- if (clip instanceof HTMLElement && !(clip instanceof HTMLAudioElement)) {
570
- if (clip.getAttribute("data-vf-timeline-proxy") === "video") {
571
- // Clip windows are inclusive of both ends, so at an exact butt-cut
572
- // the outgoing AND incoming proxies are both active. Prefer the
573
- // greatest data-start (timeline order — the incoming clip wins);
574
- // DOM order only breaks ties between equal starts.
575
- if (active && timing.start >= activeVideoProxyStart) {
576
- activeVideoProxy = clip;
577
- activeVideoProxyStart = timing.start;
578
- }
579
- } else {
580
- setVisibility(clip, active);
581
- }
582
- if (clip.hasAttribute("data-kenburns")) syncKenBurnsClip(clip, timing);
583
- if (clip.hasAttribute("data-caption-animation")) syncCaptionClip(clip, timing);
584
- if (clip.hasAttribute("data-transition") || clip.hasAttribute("data-transition-out")) syncTransitionClip(clip, timing, hold);
585
- }
586
- if (clip instanceof HTMLMediaElement) {
587
- applyMediaState(clip);
588
- if (active) {
589
- const target = Math.max(0, timing.mediaStart + time - timing.start);
590
- const enteringClip = !activeMedia.has(clip);
591
- seekMedia(clip, target, forceSeek || enteringClip, playing ? 0.75 : 0.08);
592
- if (playing) {
593
- ensurePlaying(clip);
594
- } else {
595
- pauseInactiveMedia(clip);
596
- }
597
- } else {
598
- pauseInactiveMedia(clip);
599
- }
600
- }
601
- }
602
- syncGeneralCssAnimations();
603
- syncRuntimeAdapters();
604
- if (backingVideo instanceof HTMLVideoElement) {
605
- applyMediaState(backingVideo);
606
- const shouldShowBackingVideo = !hasVideoProxies || Boolean(activeVideoProxy);
607
- backingVideo.style.visibility = shouldShowBackingVideo ? "visible" : "hidden";
608
- if (activeVideoProxy) {
609
- const nextProxyKey = clipKey(activeVideoProxy);
610
- const proxyChanged = nextProxyKey !== activeVideoProxyKey;
611
- activeVideoProxyKey = nextProxyKey;
612
- activeVideoProxyNode = activeVideoProxy;
613
- const timing = readTiming(activeVideoProxy);
614
- const target = Math.max(0, timing.mediaStart + time - timing.start);
615
- const driftLimit = playing ? (fromClock ? 0.75 : 2.0) : 0.08;
616
- const drifting = !backingVideo.seeking && Math.abs(backingVideo.currentTime - target) > driftLimit;
617
- if (forceSeek || proxyChanged || !playing || drifting) {
618
- seekMedia(backingVideo, target, true, 0.08);
619
- }
620
- } else {
621
- activeVideoProxyKey = null;
622
- activeVideoProxyNode = null;
623
- if (!hasVideoProxies && !fromClock) seekMedia(backingVideo, time, forceSeek, playing ? 1.25 : 0.08);
624
- }
625
- if (playing && shouldShowBackingVideo) {
626
- ensurePlaying(backingVideo);
627
- } else {
628
- pauseInactiveMedia(backingVideo);
629
- }
630
- }
631
- }
632
-
633
- function tick(now) {
634
- if (!playing) return;
635
- if (!last) last = now;
636
- const delta = ((now - last) / 1000) * playbackRate;
637
- const next = time + delta;
638
- last = now;
639
- apply(next >= duration ? duration : next, { fromClock: true });
640
- if (time >= duration) {
641
- playing = false;
642
- for (const clip of clips) {
643
- if (clip instanceof HTMLMediaElement) pauseInactiveMedia(clip);
644
- }
645
- if (backingVideo instanceof HTMLMediaElement) pauseInactiveMedia(backingVideo);
646
- return;
647
- }
648
- raf = requestAnimationFrame(tick);
649
- }
650
-
651
- window.__player = {
652
- play() {
653
- if (playing) return;
654
- if (time >= duration) time = 0;
655
- playing = true;
656
- last = 0;
657
- apply(time, { forceSeek: true });
658
- cancelAnimationFrame(raf);
659
- raf = requestAnimationFrame(tick);
660
- },
661
- pause() {
662
- playing = false;
663
- cancelAnimationFrame(raf);
664
- for (const clip of clips) {
665
- if (clip instanceof HTMLMediaElement) pauseInactiveMedia(clip);
666
- }
667
- if (backingVideo instanceof HTMLMediaElement) pauseInactiveMedia(backingVideo);
668
- },
669
- seek(nextTime, options = {}) {
670
- const shouldKeepPlaying = playing || options.keepPlaying === true;
671
- playing = shouldKeepPlaying;
672
- if (!shouldKeepPlaying) cancelAnimationFrame(raf);
673
- apply(nextTime, { forceSeek: true });
674
- if (shouldKeepPlaying) {
675
- last = 0;
676
- cancelAnimationFrame(raf);
677
- raf = requestAnimationFrame(tick);
678
- }
679
- },
680
- getTime() {
681
- return time;
682
- },
683
- getDuration() {
684
- return duration;
685
- },
686
- isPlaying() {
687
- return playing;
688
- },
689
- setPlaybackRate,
690
- getPlaybackRate() {
691
- return playbackRate;
692
- },
693
- setMuted,
694
- refreshClips,
695
- // Re-scrub caption word animations at the current playhead without a full
696
- // apply(). The editor calls this after live-patching a caption layer's
697
- // DOM (style/text edits rebuild the word spans, whose fresh CSS-paused
698
- // animations sit at currentTime 0 — resting/invisible — until scrubbed).
699
- syncCaptions() {
700
- for (const clip of Array.from(document.querySelectorAll("[data-caption-animation]"))) {
701
- if (clip instanceof HTMLElement) syncCaptionClip(clip, readTiming(clip));
702
- }
703
- }
704
- };
705
-
706
- refreshClips();
707
- if (backingVideo instanceof HTMLMediaElement) {
708
- backingVideo.addEventListener("loadedmetadata", () => {
709
- if (!playing) apply(time, { forceSeek: true });
710
- });
711
- }
712
- window.addEventListener("load", () => {
713
- // Composition scripts may register adapter instances (GSAP timelines,
714
- // Lottie players, ...) or start CSS animations on the load event —
715
- // re-discover and re-scrub so late registrations preview correctly.
716
- discoverGeneralCssAnimations();
717
- if (!playing) apply(time, { forceSeek: true });
718
- });
719
-
720
- // Cache the parent frame's origin from the first accepted control message
721
- // and pin all outbound postMessages to it. Falls back to document.referrer's
722
- // origin so drag/select events still reach the editor before any control
723
- // message has been received. Without pinning, sending "*" leaks preview
724
- // frame coordinates and layer ids to any embedding third-party page.
725
- let trustedParentOrigin = null;
726
- try {
727
- if (document.referrer) {
728
- trustedParentOrigin = new URL(document.referrer).origin;
729
- }
730
- } catch {}
731
- function parentTargetOrigin() {
732
- return trustedParentOrigin || "*";
733
- }
734
- function postToParent(payload) {
735
- if (!window.parent || window.parent === window) return;
736
- try {
737
- window.parent.postMessage(payload, parentTargetOrigin());
738
- } catch {}
739
- }
740
-
741
- window.addEventListener("message", (event) => {
742
- // Only accept messages from the direct parent frame. Blocks sibling
743
- // iframes and popups that could otherwise spoof play/pause/seek by
744
- // forging data.source.
745
- if (event.source !== window.parent) return;
746
- const data = event.data || {};
747
- if (data.type !== "control" && data.source !== "hf-parent") return;
748
- if (event.origin && event.origin !== "null") {
749
- trustedParentOrigin = event.origin;
750
- }
751
- if (data.action === "play") {
752
- window.__player.play();
753
- }
754
- if (data.action === "pause" || data.action === "stop-media") {
755
- window.__player.pause();
756
- }
757
- if (data.action === "seek") {
758
- const nextTime = Number.isFinite(Number(data.time))
759
- ? Number(data.time)
760
- : Number.isFinite(Number(data.frame))
761
- ? Number(data.frame) / 30
762
- : Number.isFinite(Number(data.payload?.time))
763
- ? Number(data.payload.time)
764
- : time;
765
- window.__player.seek(nextTime);
766
- }
767
- if (data.action === "set-playback-rate") {
768
- setPlaybackRate(data.playbackRate ?? data.payload?.playbackRate);
769
- }
770
- if (data.action === "set-muted") {
771
- setMuted(data.muted ?? data.payload?.muted);
772
- }
773
- });
774
-
775
- function selectTargetFromEvent(event) {
776
- const handle = event.target instanceof Element
777
- ? event.target.closest("[data-vf-frame-handle]")
778
- : null;
779
- const handleTargetId = handle?.getAttribute("data-vf-target-id") || null;
780
- if (handleTargetId) {
781
- return document.querySelector("[data-hf-id='" + CSS.escape(handleTargetId) + "'], #" + CSS.escape(handleTargetId));
782
- }
783
- return event.target instanceof Element
784
- ? event.target.closest("[data-start]")
785
- : null;
786
- }
787
-
788
- function postSelectedLayer(event) {
789
- const target = selectTargetFromEvent(event);
790
- const rawId = target?.getAttribute("data-hf-id") || target?.id || null;
791
- const id = rawId === "source-video" ? null : rawId;
792
- postToParent({
793
- source: "vidfarm-composition",
794
- action: "select-layer",
795
- id
796
- });
797
- }
798
-
799
- function postPreviewContextMenu(event) {
800
- event.preventDefault();
801
- const root = document.querySelector("[data-composition-id]");
802
- const rect = root instanceof HTMLElement ? root.getBoundingClientRect() : null;
803
- const target = selectTargetFromEvent(event);
804
- const rawId = target?.getAttribute("data-hf-id") || target?.id || null;
805
- const id = rawId === "source-video" ? null : rawId;
806
- const x = rect ? ((event.clientX - rect.left) / Math.max(1, rect.width)) * 100 : 12;
807
- const y = rect ? ((event.clientY - rect.top) / Math.max(1, rect.height)) * 100 : 70;
808
- postToParent({
809
- source: "vidfarm-composition",
810
- action: "preview-context-menu",
811
- id,
812
- clientX: event.clientX,
813
- clientY: event.clientY,
814
- time,
815
- frame: {
816
- x: Math.max(0, Math.min(95, x)),
817
- y: Math.max(0, Math.min(95, y)),
818
- width: 42,
819
- height: 12
820
- }
821
- });
822
- }
823
-
824
- function frameFromNode(node) {
825
- return {
826
- x: Number.parseFloat(node.style.left || "0") || 0,
827
- y: Number.parseFloat(node.style.top || "0") || 0,
828
- width: Number.parseFloat(node.style.width || "100") || 100,
829
- height: Number.parseFloat(node.style.height || "100") || 100,
830
- // NaN (not 18) when the wrapper has no inline font-size — text layers keep
831
- // their real size on inner spans, so fabricating 18 here would make a plain
832
- // MOVE collapse the text to 18px. Downstream apply/stage all guard
833
- // Number.isFinite, so an absent size is simply left untouched on a move.
834
- fontSize: Number.parseFloat(node.style.fontSize)
835
- };
836
- }
837
-
838
- function isVisualFrameTarget(node) {
839
- if (!(node instanceof HTMLElement) || node instanceof HTMLAudioElement) return false;
840
- const id = node.getAttribute("data-hf-id") || node.id || "";
841
- return id !== "source-video";
842
- }
843
-
844
- function isTextFrameTarget(node) {
845
- return node instanceof HTMLElement &&
846
- !(node instanceof HTMLAudioElement) &&
847
- !(node instanceof HTMLVideoElement) &&
848
- !(node instanceof HTMLImageElement);
849
- }
850
-
851
- function syncRuntimeFrameControls(node) {
852
- if (!isVisualFrameTarget(node)) return;
853
- const root = document.querySelector("[data-composition-id]");
854
- if (!(root instanceof HTMLElement)) return;
855
- const targetId = node.getAttribute("data-hf-id") || node.id || "";
856
- if (!targetId) return;
857
- let ring = document.querySelector("[data-vf-selection-ring='true']");
858
- if (!(ring instanceof HTMLElement)) {
859
- ring = document.createElement("div");
860
- ring.setAttribute("data-vf-selection-ring", "true");
861
- root.append(ring);
862
- }
863
- ring.style.display = "block";
864
- ring.style.position = "absolute";
865
- ring.style.left = node.style.left || "0%";
866
- ring.style.top = node.style.top || "0%";
867
- ring.style.width = node.style.width || "100%";
868
- ring.style.height = node.style.height || "100%";
869
- ring.style.borderRadius = node.style.borderRadius || "0";
870
- ring.style.zIndex = "9999";
871
- ring.style.pointerEvents = "auto";
872
- ring.style.cursor = "grab";
873
- ring.style.border = "1px dashed #F2CD46";
874
- ring.style.boxShadow = "0 0 0 2px #118df2";
875
- ring.setAttribute("data-vf-frame-handle", "move");
876
- ring.setAttribute("data-vf-target-id", targetId);
877
- for (const handle of Array.from(ring.querySelectorAll("[data-vf-frame-handle]"))) {
878
- if (handle !== ring) handle.remove();
879
- }
880
- for (const [mode, cssText] of [
881
- ["top-left", "left:-7px;top:-7px;cursor:nwse-resize;"],
882
- ["top", "top:-7px;left:50%;transform:translateX(-50%);cursor:ns-resize;"],
883
- ["top-right", "right:-7px;top:-7px;cursor:nesw-resize;"],
884
- ["right", "right:-7px;top:50%;transform:translateY(-50%);cursor:ew-resize;"],
885
- ["bottom-right", "right:-7px;bottom:-7px;cursor:nwse-resize;"],
886
- ["bottom", "bottom:-7px;left:50%;transform:translateX(-50%);cursor:ns-resize;"],
887
- ["bottom-left", "left:-7px;bottom:-7px;cursor:nesw-resize;"],
888
- ["left", "left:-7px;top:50%;transform:translateY(-50%);cursor:ew-resize;"]
889
- ]) {
890
- const handle = document.createElement("span");
891
- handle.setAttribute("data-vf-frame-handle", mode);
892
- handle.setAttribute("data-vf-target-id", targetId);
893
- handle.style.cssText = [
894
- "position:absolute",
895
- "display:block",
896
- "width:14px",
897
- "height:14px",
898
- "box-sizing:border-box",
899
- "border-radius:999px",
900
- "border:2px solid #0b0d0a",
901
- "background:#F2CD46",
902
- "box-shadow:0 0 0 2px rgba(242,205,70,0.32),0 1px 4px rgba(0,0,0,0.38)",
903
- "pointer-events:auto",
904
- "touch-action:none",
905
- cssText
906
- ].join(";");
907
- ring.append(handle);
908
- }
909
- }
910
-
911
- function applyFrameToNode(node, frame) {
912
- node.style.left = frame.x + "%";
913
- node.style.top = frame.y + "%";
914
- node.style.width = frame.width + "%";
915
- node.style.height = frame.height + "%";
916
- if (Number.isFinite(frame.fontSize)) {
917
- node.style.fontSize = frame.fontSize + "px";
918
- }
919
- syncRuntimeFrameControls(node);
920
- }
921
-
922
- function postFrameDraft(node, phase) {
923
- const id = node.getAttribute("data-hf-id") || node.id || null;
924
- if (!id) return;
925
- postToParent({
926
- source: "vidfarm-composition",
927
- action: "visual-frame-draft",
928
- phase,
929
- id,
930
- frame: frameFromNode(node)
931
- });
932
- }
933
-
934
- let visualFrameDrag = null;
935
-
936
- function beginVisualFrameDrag(event) {
937
- if (event.button !== 0) return;
938
- const handle = event.target instanceof Element
939
- ? event.target.closest("[data-vf-frame-handle]")
940
- : null;
941
- const handleTargetId = handle?.getAttribute("data-vf-target-id") || null;
942
- const target = handleTargetId
943
- ? document.querySelector("[data-hf-id='" + CSS.escape(handleTargetId) + "'], #" + CSS.escape(handleTargetId))
944
- : event.target instanceof Element
945
- ? event.target.closest("[data-start]")
946
- : null;
947
- if (!isVisualFrameTarget(target)) return;
948
- const root = document.querySelector("[data-composition-id]");
949
- if (!(root instanceof HTMLElement)) return;
950
- const rootRect = root.getBoundingClientRect();
951
- event.preventDefault();
952
- event.stopPropagation();
953
- postSelectedLayer(event);
954
- visualFrameDrag = {
955
- node: target,
956
- mode: handle?.getAttribute("data-vf-frame-handle") || "move",
957
- startX: event.clientX,
958
- startY: event.clientY,
959
- rootWidth: Math.max(1, rootRect.width),
960
- rootHeight: Math.max(1, rootRect.height),
961
- frame: frameFromNode(target),
962
- moved: false
963
- };
964
- syncRuntimeFrameControls(target);
965
- target.style.cursor = "grabbing";
966
- postFrameDraft(target, "start");
967
- }
968
-
969
- function updateVisualFrameDrag(event) {
970
- if (!visualFrameDrag) return;
971
- const drag = visualFrameDrag;
972
- const dx = ((event.clientX - drag.startX) / drag.rootWidth) * 100;
973
- const dy = ((event.clientY - drag.startY) / drag.rootHeight) * 100;
974
- if (!drag.moved && Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) < 2) return;
975
- drag.moved = true;
976
- event.preventDefault();
977
- const next = { ...drag.frame };
978
- if (drag.mode === "move") {
979
- next.x = Math.max(0, Math.min(100 - drag.frame.width, drag.frame.x + dx));
980
- next.y = Math.max(0, Math.min(100 - drag.frame.height, drag.frame.y + dy));
981
- }
982
- if (drag.mode === "left" || drag.mode === "top-left" || drag.mode === "bottom-left") {
983
- const nextX = Math.max(0, Math.min(drag.frame.x + drag.frame.width - 1, drag.frame.x + dx));
984
- next.x = nextX;
985
- next.width = Math.max(1, Math.min(100 - nextX, drag.frame.width + (drag.frame.x - nextX)));
986
- }
987
- if (drag.mode === "right" || drag.mode === "top-right" || drag.mode === "bottom-right") {
988
- next.width = Math.max(1, Math.min(100 - drag.frame.x, drag.frame.width + dx));
989
- }
990
- if (drag.mode === "top" || drag.mode === "top-left" || drag.mode === "top-right") {
991
- const nextY = Math.max(0, Math.min(drag.frame.y + drag.frame.height - 1, drag.frame.y + dy));
992
- next.y = nextY;
993
- next.height = Math.max(1, Math.min(100 - nextY, drag.frame.height + (drag.frame.y - nextY)));
994
- }
995
- if (drag.mode === "bottom" || drag.mode === "bottom-left" || drag.mode === "bottom-right") {
996
- next.height = Math.max(1, Math.min(100 - drag.frame.y, drag.frame.height + dy));
997
- }
998
- if (
999
- isTextFrameTarget(drag.node) &&
1000
- ["top-left", "top-right", "bottom-left", "bottom-right"].includes(drag.mode)
1001
- ) {
1002
- const widthScale = next.width / Math.max(1, drag.frame.width);
1003
- const heightScale = next.height / Math.max(1, drag.frame.height);
1004
- const scale = Math.sqrt(Math.max(0.05, widthScale * heightScale));
1005
- next.fontSize = Math.max(8, Math.min(180, (drag.frame.fontSize || 18) * scale));
1006
- }
1007
- applyFrameToNode(drag.node, next);
1008
- postFrameDraft(drag.node, "move");
1009
- }
1010
-
1011
- function endVisualFrameDrag() {
1012
- if (!visualFrameDrag) return;
1013
- visualFrameDrag.node.style.cursor = "";
1014
- postFrameDraft(visualFrameDrag.node, visualFrameDrag.moved ? "end" : "tap");
1015
- visualFrameDrag = null;
1016
- }
1017
-
1018
- function hoverVisualFrameTarget(event) {
1019
- const handle = event.target instanceof Element
1020
- ? event.target.closest("[data-vf-frame-handle]")
1021
- : null;
1022
- const handleTargetId = handle?.getAttribute("data-vf-target-id") || null;
1023
- const target = handleTargetId
1024
- ? document.querySelector("[data-hf-id='" + CSS.escape(handleTargetId) + "'], #" + CSS.escape(handleTargetId))
1025
- : event.target instanceof Element
1026
- ? event.target.closest("[data-start]")
1027
- : null;
1028
- if (!isVisualFrameTarget(target)) return;
1029
- target.style.cursor = "grab";
1030
- syncRuntimeFrameControls(target);
1031
- }
1032
-
1033
- document.addEventListener("pointerover", hoverVisualFrameTarget, true);
1034
- document.addEventListener("pointerdown", beginVisualFrameDrag, true);
1035
- document.addEventListener("pointermove", updateVisualFrameDrag, { passive: false });
1036
- document.addEventListener("pointerup", endVisualFrameDrag);
1037
- document.addEventListener("pointercancel", endVisualFrameDrag);
1038
- document.addEventListener("pointerdown", postSelectedLayer);
1039
- document.addEventListener("click", postSelectedLayer);
1040
- document.addEventListener("contextmenu", postPreviewContextMenu, true);
1041
-
1042
- try {
1043
- console.info("[hf-runtime] ready", {
1044
- compositionId: root?.getAttribute("data-composition-id"),
1045
- duration,
1046
- clipCount: document.querySelectorAll("[data-start]").length
1047
- });
1048
- } catch {}
1049
-
1050
- apply(0);
1051
- })();
1052
- `;
1053
- //# sourceMappingURL=composition-runtime.js.map