@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.
- package/.agents/skills/editor-capabilities/SKILL.md +15 -1
- package/.agents/skills/vidfarm-director/SKILL.md +106 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
- package/SKILL.director.md +521 -261
- package/dist/src/cli.js +404 -96
- package/dist/src/devcli/clips.js +15 -10
- package/dist/src/devcli/doctor.js +2 -2
- package/dist/src/devcli/local-backend.js +46 -2
- package/dist/src/services/clip-curation/cost.js +5 -1
- package/dist/src/services/clip-curation/gemini.js +10 -0
- package/package.json +30 -10
- package/SKILL.platform.md +0 -432
- package/demo/README.md +0 -28
- package/demo/dist/app.css +0 -1
- package/demo/dist/app.js +0 -1850
- package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
- package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
- package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
- package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
- package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +0 -7
- package/demo/dist/icons/timeline/captions.svg +0 -5
- package/demo/dist/icons/timeline/composition.svg +0 -12
- package/demo/dist/icons/timeline/image.svg +0 -18
- package/demo/dist/icons/timeline/music.svg +0 -10
- package/demo/dist/icons/timeline/text.svg +0 -3
- package/demo/dist/index.html +0 -15
- package/dist/src/account-pages-legacy.js +0 -9396
- package/dist/src/account-pages.js +0 -61
- package/dist/src/app.js +0 -21602
- package/dist/src/composition-runtime.js +0 -1053
- package/dist/src/config.js +0 -217
- package/dist/src/context.js +0 -447
- package/dist/src/dev-app-legacy.js +0 -739
- package/dist/src/dev-app.js +0 -6
- package/dist/src/devcli/migrate-local.js +0 -140
- package/dist/src/devcli/sync.js +0 -368
- package/dist/src/domain.js +0 -5
- package/dist/src/editor-chat-history.js +0 -82
- package/dist/src/editor-chat.js +0 -756
- package/dist/src/editor-dark-theme.js +0 -1128
- package/dist/src/frontend/debug.js +0 -71
- package/dist/src/frontend/discover-client.js +0 -130
- package/dist/src/frontend/discover-store.js +0 -23
- package/dist/src/frontend/file-directory.js +0 -1018
- package/dist/src/frontend/flockposter-cache-store.js +0 -124
- package/dist/src/frontend/homepage-client.js +0 -446
- package/dist/src/frontend/homepage-shared.js +0 -201
- package/dist/src/frontend/homepage-store.js +0 -66
- package/dist/src/frontend/homepage-view.js +0 -705
- package/dist/src/frontend/page-runtime-client.js +0 -132
- package/dist/src/frontend/page-runtime-store.js +0 -9
- package/dist/src/frontend/sentry.js +0 -42
- package/dist/src/frontend/template-editor-chat.js +0 -4181
- package/dist/src/help-page.js +0 -346
- package/dist/src/homepage.js +0 -1458
- package/dist/src/index.js +0 -16
- package/dist/src/instrument.js +0 -30
- package/dist/src/landing-page.js +0 -384
- package/dist/src/page-runtime.js +0 -2
- package/dist/src/page-shell.js +0 -1452
- package/dist/src/primitive-context.js +0 -416
- package/dist/src/primitive-registry.js +0 -3940
- package/dist/src/primitive-sdk.js +0 -4
- package/dist/src/primitives/hyperframes-media.js +0 -108
- package/dist/src/react-page-shell.js +0 -35
- package/dist/src/ready-post-schedule-component.js +0 -1540
- package/dist/src/registry.js +0 -296
- package/dist/src/reskin/agency-page.js +0 -299
- package/dist/src/reskin/calendar-page.js +0 -568
- package/dist/src/reskin/chat-page.js +0 -942
- package/dist/src/reskin/discover-page.js +0 -1788
- package/dist/src/reskin/document.js +0 -1587
- package/dist/src/reskin/help-page.js +0 -357
- package/dist/src/reskin/index-page.js +0 -62
- package/dist/src/reskin/inpaint-clipper-page.js +0 -890
- package/dist/src/reskin/inpaint-page.js +0 -2554
- package/dist/src/reskin/inpaint-video-page.js +0 -1339
- package/dist/src/reskin/job-runs-page.js +0 -477
- package/dist/src/reskin/library-page.js +0 -1634
- package/dist/src/reskin/login-page.js +0 -262
- package/dist/src/reskin/portfolio-page.js +0 -687
- package/dist/src/reskin/pricing-page.js +0 -390
- package/dist/src/reskin/settings-page.js +0 -732
- package/dist/src/reskin/theme.js +0 -711
- package/dist/src/runtime.js +0 -35
- package/dist/src/services/api-call-history.js +0 -249
- package/dist/src/services/auth.js +0 -152
- package/dist/src/services/billing-pricing.js +0 -39
- package/dist/src/services/billing.js +0 -241
- package/dist/src/services/cast.js +0 -127
- package/dist/src/services/chat-threads.js +0 -92
- package/dist/src/services/clip-records.js +0 -250
- package/dist/src/services/clip-search.js +0 -77
- package/dist/src/services/clip-vectors.js +0 -125
- package/dist/src/services/composition-sanitize.js +0 -124
- package/dist/src/services/composition-watch.js +0 -79
- package/dist/src/services/elevenlabs.js +0 -222
- package/dist/src/services/file-directory.js +0 -117
- package/dist/src/services/fork-access.js +0 -93
- package/dist/src/services/fork-manifest.js +0 -42
- package/dist/src/services/ghostcut.js +0 -179
- package/dist/src/services/hyperframes.js +0 -3517
- package/dist/src/services/job-capacity.js +0 -14
- package/dist/src/services/job-logs.js +0 -197
- package/dist/src/services/jobs.js +0 -136
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +0 -766
- package/dist/src/services/primitive-media-lambda.js +0 -280
- package/dist/src/services/providers.js +0 -2748
- package/dist/src/services/rate-limits.js +0 -262
- package/dist/src/services/scene-annotations.js +0 -32
- package/dist/src/services/serverless-auth.js +0 -382
- package/dist/src/services/serverless-jobs.js +0 -1084
- package/dist/src/services/serverless-provider-keys.js +0 -409
- package/dist/src/services/serverless-records.js +0 -1515
- package/dist/src/services/serverless-template-configs.js +0 -75
- package/dist/src/services/storage.js +0 -461
- package/dist/src/services/swipe-customize.js +0 -437
- package/dist/src/services/template-certification.js +0 -413
- package/dist/src/services/template-loader.js +0 -99
- package/dist/src/services/template-runtime-bundles.js +0 -217
- package/dist/src/services/template-sources.js +0 -1017
- package/dist/src/services/upstream.js +0 -248
- package/dist/src/services/video-normalization.js +0 -2
- package/dist/src/services/webhooks.js +0 -62
- package/dist/src/template-editor-pages.js +0 -2576
- package/dist/src/template-editor-shell.js +0 -2893
- package/dist/src/template-sdk.js +0 -4
- package/dist/src/worker.js +0 -17
- package/public/assets/discover-client-app.js +0 -1
- package/public/assets/file-directory-app.js +0 -3
- package/public/assets/homepage-app.js +0 -54
- package/public/assets/homepage-client-app.js +0 -80
- package/public/assets/page-runtime-client-app.js +0 -94
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
- package/src/assets/SELLING_WITH_HOOKS.md +0 -377
- package/src/assets/SELLING_WITH_VSLS.md +0 -606
- package/src/assets/favicon.ico +0 -0
- 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
|