gitframes 1.2.35
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/LICENCE +202 -0
- package/README.md +77 -0
- package/dist/audio/index.d.mts +2 -0
- package/dist/audio/index.mjs +6 -0
- package/dist/audio-C0YslYEG.d.mts +32 -0
- package/dist/audio-C0YslYEG.d.mts.map +1 -0
- package/dist/audio-C4kiPbrV.mjs +1866 -0
- package/dist/audio-C4kiPbrV.mjs.map +1 -0
- package/dist/audio-CiXUMGyk.mjs +578 -0
- package/dist/audio-CiXUMGyk.mjs.map +1 -0
- package/dist/chunk-GMkBE123.mjs +47 -0
- package/dist/client-Dyypr8tt.mjs +66 -0
- package/dist/client-Dyypr8tt.mjs.map +1 -0
- package/dist/dist-C37594c6.mjs +27195 -0
- package/dist/dist-C37594c6.mjs.map +1 -0
- package/dist/dist-CMF9eNLB.mjs +4062 -0
- package/dist/dist-CMF9eNLB.mjs.map +1 -0
- package/dist/dist-cjs--KIl7Gqy.mjs +6 -0
- package/dist/dist-cjs-B-_4TVjP.mjs +66 -0
- package/dist/dist-cjs-B-_4TVjP.mjs.map +1 -0
- package/dist/dist-cjs-B3TIyZ3K.mjs +437 -0
- package/dist/dist-cjs-B3TIyZ3K.mjs.map +1 -0
- package/dist/dist-cjs-Bsz2eGzl.mjs +9069 -0
- package/dist/dist-cjs-Bsz2eGzl.mjs.map +1 -0
- package/dist/dist-cjs-BuJ2FXob.mjs +181 -0
- package/dist/dist-cjs-BuJ2FXob.mjs.map +1 -0
- package/dist/dist-cjs-C-FMsqmo.mjs +1091 -0
- package/dist/dist-cjs-C-FMsqmo.mjs.map +1 -0
- package/dist/dist-cjs-C4b9TCXd.mjs +48 -0
- package/dist/dist-cjs-C4b9TCXd.mjs.map +1 -0
- package/dist/dist-cjs-C817PNLc.mjs +51 -0
- package/dist/dist-cjs-C817PNLc.mjs.map +1 -0
- package/dist/dist-cjs-CBnnS4k8.mjs +43 -0
- package/dist/dist-cjs-CBnnS4k8.mjs.map +1 -0
- package/dist/dist-cjs-CWbeD0Mu.mjs +64 -0
- package/dist/dist-cjs-CWbeD0Mu.mjs.map +1 -0
- package/dist/dist-cjs-ChXEJ1ro.mjs +85 -0
- package/dist/dist-cjs-ChXEJ1ro.mjs.map +1 -0
- package/dist/dist-cjs-CslF8Dju.mjs +153 -0
- package/dist/dist-cjs-CslF8Dju.mjs.map +1 -0
- package/dist/dist-cjs-D52jYyEm.mjs +98 -0
- package/dist/dist-cjs-D52jYyEm.mjs.map +1 -0
- package/dist/dist-cjs-DhWYgNfa.mjs +209 -0
- package/dist/dist-cjs-DhWYgNfa.mjs.map +1 -0
- package/dist/dist-cjs-DssYqayv.mjs +5015 -0
- package/dist/dist-cjs-DssYqayv.mjs.map +1 -0
- package/dist/dist-cjs-Du6DnSjo.mjs +137 -0
- package/dist/dist-cjs-Du6DnSjo.mjs.map +1 -0
- package/dist/dist-cjs-b-oBQaxe.mjs +323 -0
- package/dist/dist-cjs-b-oBQaxe.mjs.map +1 -0
- package/dist/effects/index.d.mts +2 -0
- package/dist/effects/index.mjs +4 -0
- package/dist/effects-4t4qk-BR.mjs +21130 -0
- package/dist/effects-4t4qk-BR.mjs.map +1 -0
- package/dist/event-streams-B0YUBtyM.mjs +204 -0
- package/dist/event-streams-B0YUBtyM.mjs.map +1 -0
- package/dist/fonts/index.d.mts +2 -0
- package/dist/fonts/index.mjs +3 -0
- package/dist/fonts-Dn18r_ST.mjs +188 -0
- package/dist/fonts-Dn18r_ST.mjs.map +1 -0
- package/dist/index-B1557AUi.d.mts +928 -0
- package/dist/index-B1557AUi.d.mts.map +1 -0
- package/dist/index-DSaMXGoA.d.mts +65 -0
- package/dist/index-DSaMXGoA.d.mts.map +1 -0
- package/dist/index-FICUHo4P.d.mts +69 -0
- package/dist/index-FICUHo4P.d.mts.map +1 -0
- package/dist/index-YZsTjqut.d.mts +147 -0
- package/dist/index-YZsTjqut.d.mts.map +1 -0
- package/dist/index.d.mts +736 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +7135 -0
- package/dist/index.mjs.map +1 -0
- package/dist/package-5RsA21g-.mjs +6 -0
- package/dist/package-5RsA21g-.mjs.map +1 -0
- package/dist/react/index.d.mts +3 -0
- package/dist/react/index.mjs +4 -0
- package/dist/renderer/index.d.mts +2 -0
- package/dist/renderer/index.mjs +6 -0
- package/dist/renderer-DVi31fSv.mjs +23274 -0
- package/dist/renderer-DVi31fSv.mjs.map +1 -0
- package/dist/renderers-CLJQRPVU-CMZnHNK0.mjs +49 -0
- package/dist/renderers-CLJQRPVU-CMZnHNK0.mjs.map +1 -0
- package/dist/signals/index.d.mts +9 -0
- package/dist/signals/index.mjs +8 -0
- package/dist/signals-BB_kdndF.mjs +227 -0
- package/dist/signals-BB_kdndF.mjs.map +1 -0
- package/dist/signin-CVUAIuED.mjs +865 -0
- package/dist/signin-CVUAIuED.mjs.map +1 -0
- package/dist/sso-oidc-D1GnNQYW.mjs +1051 -0
- package/dist/sso-oidc-D1GnNQYW.mjs.map +1 -0
- package/dist/sts-Bz-e3kNW.mjs +6 -0
- package/dist/sts-I4KhuCBs.mjs +1395 -0
- package/dist/sts-I4KhuCBs.mjs.map +1 -0
- package/package.json +174 -0
|
@@ -0,0 +1,4062 @@
|
|
|
1
|
+
import { A as drawCaptionNode, C as SlugGeometry, F as drawParagraphNode, I as drawSignalNode, J as lutStore, L as drawSvgNode, M as drawGifNode, N as drawImageNode, O as clearVideoNodeState, P as drawLottieNode, R as drawVideoNode, S as SlugFontCache, W as inputStore, Y as mediaDecoderCache, at as GetFontAssetUrl, ct as resolveMediaSourceUrl, et as registerHeadlessFont, it as useRenderContext, nt as signalRegistry, st as getEnv, tt as shaderStore } from "./dist-C37594c6.mjs";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import z$1, { z } from "zod";
|
|
4
|
+
import { AudioSample, AudioSampleSink, AudioSampleSource, Mp3OutputFormat, Mp4OutputFormat, Output, QUALITY_VERY_HIGH, VideoSample, VideoSampleSource, WebMOutputFormat, canEncodeAudio } from "mediabunny";
|
|
5
|
+
import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { registerMp3Encoder } from "@mediabunny/mp3-encoder";
|
|
8
|
+
|
|
9
|
+
//#region ../core/dist/index.mjs
|
|
10
|
+
var ProgrammaticSignal = class ProgrammaticSignal$1 {
|
|
11
|
+
gpuBinding;
|
|
12
|
+
label;
|
|
13
|
+
_evaluator;
|
|
14
|
+
_samples;
|
|
15
|
+
_fps;
|
|
16
|
+
_durationFrames;
|
|
17
|
+
_cachedFrame = -1;
|
|
18
|
+
_cachedValue = 0;
|
|
19
|
+
_seekedFrame = -1;
|
|
20
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
21
|
+
_children = /* @__PURE__ */ new Set();
|
|
22
|
+
get fps() {
|
|
23
|
+
return this._fps;
|
|
24
|
+
}
|
|
25
|
+
constructor(source, options = {}) {
|
|
26
|
+
this._fps = options.fps ?? 24;
|
|
27
|
+
this._durationFrames = options.durationFrames;
|
|
28
|
+
this.gpuBinding = options.gpuBinding;
|
|
29
|
+
this.label = options.label;
|
|
30
|
+
if (typeof source === "number") {
|
|
31
|
+
const constVal = source;
|
|
32
|
+
this._evaluator = () => constVal;
|
|
33
|
+
this._cachedValue = constVal;
|
|
34
|
+
} else if (typeof source === "function") this._evaluator = source;
|
|
35
|
+
else if (isSignal(source)) this._evaluator = (ctx) => Number(source.get(ctx)) || 0;
|
|
36
|
+
else {
|
|
37
|
+
const arr = source;
|
|
38
|
+
this._samples = arr instanceof Float32Array ? arr : new Float32Array(Array.from(arr));
|
|
39
|
+
this._durationFrames = this._samples.length;
|
|
40
|
+
this._cachedValue = this._samples.length > 0 ? this._samples[0] : 0;
|
|
41
|
+
this._evaluator = (ctx) => {
|
|
42
|
+
if (!this._samples || this._samples.length === 0) return 0;
|
|
43
|
+
const compFps = ctx.fps ?? this._fps;
|
|
44
|
+
const targetFrame = compFps !== this._fps && compFps > 0 ? ctx.frame / compFps * this._fps : ctx.frame;
|
|
45
|
+
const idx = Math.max(0, Math.min(this._samples.length - 1, Math.round(targetFrame)));
|
|
46
|
+
return this._samples[idx];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
get value() {
|
|
51
|
+
return this.get();
|
|
52
|
+
}
|
|
53
|
+
get(ctx) {
|
|
54
|
+
registerSignalDependency(this._subscribers);
|
|
55
|
+
frameSignal.get();
|
|
56
|
+
const currentFrame = ctx?.frame ?? (this._seekedFrame >= 0 ? this._seekedFrame : frameSignal.value);
|
|
57
|
+
if (this._cachedFrame === currentFrame) return this._cachedValue;
|
|
58
|
+
const fps = ctx?.fps ?? this._fps;
|
|
59
|
+
const time = ctx?.time ?? (fps > 0 ? currentFrame / fps : 0);
|
|
60
|
+
const duration = ctx?.duration ?? this._durationFrames ?? 100;
|
|
61
|
+
const resolvedCtx = {
|
|
62
|
+
frame: currentFrame,
|
|
63
|
+
fps,
|
|
64
|
+
time,
|
|
65
|
+
duration,
|
|
66
|
+
durationMs: ctx?.durationMs ?? duration / fps * 1e3,
|
|
67
|
+
progress: duration > 0 ? currentFrame / duration : 0,
|
|
68
|
+
deltaTime: ctx?.deltaTime ?? (fps > 0 ? 1 / fps : .0416)
|
|
69
|
+
};
|
|
70
|
+
this._cachedFrame = currentFrame;
|
|
71
|
+
this._cachedValue = this._evaluator(resolvedCtx);
|
|
72
|
+
return this._cachedValue;
|
|
73
|
+
}
|
|
74
|
+
peek() {
|
|
75
|
+
return this._cachedValue;
|
|
76
|
+
}
|
|
77
|
+
registerChild(child) {
|
|
78
|
+
this._children.add(child);
|
|
79
|
+
}
|
|
80
|
+
_derive(evaluator, options, other) {
|
|
81
|
+
const child = new ProgrammaticSignal$1(evaluator, {
|
|
82
|
+
fps: this._fps,
|
|
83
|
+
durationFrames: this._durationFrames,
|
|
84
|
+
...options
|
|
85
|
+
});
|
|
86
|
+
this._children.add(child);
|
|
87
|
+
if (other instanceof ProgrammaticSignal$1) other.registerChild(child);
|
|
88
|
+
return child;
|
|
89
|
+
}
|
|
90
|
+
seekFrame(frame) {
|
|
91
|
+
this._seekedFrame = frame;
|
|
92
|
+
this._cachedFrame = -1;
|
|
93
|
+
for (const sub of this._subscribers) sub.markDirty();
|
|
94
|
+
for (const child of this._children) child.seekFrame(frame);
|
|
95
|
+
}
|
|
96
|
+
invalidate() {
|
|
97
|
+
this._cachedFrame = -1;
|
|
98
|
+
for (const sub of this._subscribers) sub.markDirty();
|
|
99
|
+
for (const child of this._children) child.invalidate();
|
|
100
|
+
}
|
|
101
|
+
valueOf() {
|
|
102
|
+
return this.get();
|
|
103
|
+
}
|
|
104
|
+
[Symbol.toPrimitive](hint) {
|
|
105
|
+
if (hint === "number") return Number(this.get());
|
|
106
|
+
if (hint === "string") return String(this.get());
|
|
107
|
+
return this.get();
|
|
108
|
+
}
|
|
109
|
+
smooth(windowFrames) {
|
|
110
|
+
const window$1 = Math.max(1, Math.round(windowFrames));
|
|
111
|
+
if (window$1 <= 1) return this;
|
|
112
|
+
return this._derive((ctx) => {
|
|
113
|
+
let sum = 0;
|
|
114
|
+
let count = 0;
|
|
115
|
+
const startF = Math.max(0, ctx.frame - window$1 + 1);
|
|
116
|
+
for (let f = startF; f <= ctx.frame; f++) {
|
|
117
|
+
sum += this.get({
|
|
118
|
+
...ctx,
|
|
119
|
+
frame: f,
|
|
120
|
+
time: f / ctx.fps
|
|
121
|
+
});
|
|
122
|
+
count++;
|
|
123
|
+
}
|
|
124
|
+
return count > 0 ? sum / count : 0;
|
|
125
|
+
}, { label: `${this.label ?? "sig"}_smoothed(${window$1})` });
|
|
126
|
+
}
|
|
127
|
+
remap(inMin, inMax, outMin, outMax, clamp = false) {
|
|
128
|
+
const inRange = inMax - inMin;
|
|
129
|
+
const outRange = outMax - outMin;
|
|
130
|
+
return this._derive((ctx) => {
|
|
131
|
+
const val = this.get(ctx);
|
|
132
|
+
const remapped = outMin + (inRange !== 0 ? (val - inMin) / inRange : 0) * outRange;
|
|
133
|
+
if (clamp) {
|
|
134
|
+
const lo = Math.min(outMin, outMax);
|
|
135
|
+
const hi = Math.max(outMin, outMax);
|
|
136
|
+
return Math.max(lo, Math.min(hi, remapped));
|
|
137
|
+
}
|
|
138
|
+
return remapped;
|
|
139
|
+
}, { label: `${this.label ?? "sig"}_remap` });
|
|
140
|
+
}
|
|
141
|
+
clamp(min, max) {
|
|
142
|
+
const lo = Math.min(min, max);
|
|
143
|
+
const hi = Math.max(min, max);
|
|
144
|
+
return this._derive((ctx) => Math.max(lo, Math.min(hi, this.get(ctx))), { label: `${this.label ?? "sig"}_clamped` });
|
|
145
|
+
}
|
|
146
|
+
gate(threshold, options = {}) {
|
|
147
|
+
const mode = options.mode ?? "gate";
|
|
148
|
+
const debounce = Math.max(1, options.debounceFrames ?? 2);
|
|
149
|
+
const hold = Math.max(1, options.holdFrames ?? 4);
|
|
150
|
+
const invert = Boolean(options.invert);
|
|
151
|
+
return this._derive((ctx) => {
|
|
152
|
+
const currentF = ctx.frame;
|
|
153
|
+
let out = 0;
|
|
154
|
+
if (mode === "gate") out = this.get(ctx) >= threshold ? 1 : 0;
|
|
155
|
+
else if (mode === "trigger") {
|
|
156
|
+
let lastTrigger = -Infinity;
|
|
157
|
+
const startF = Math.max(0, currentF - 120);
|
|
158
|
+
for (let f = startF; f <= currentF; f++) {
|
|
159
|
+
const v = this.get({
|
|
160
|
+
...ctx,
|
|
161
|
+
frame: f,
|
|
162
|
+
time: f / ctx.fps
|
|
163
|
+
});
|
|
164
|
+
const prev = f > 0 ? this.get({
|
|
165
|
+
...ctx,
|
|
166
|
+
frame: f - 1,
|
|
167
|
+
time: (f - 1) / ctx.fps
|
|
168
|
+
}) : 0;
|
|
169
|
+
if (v >= threshold && prev < threshold && f - lastTrigger >= debounce) lastTrigger = f;
|
|
170
|
+
}
|
|
171
|
+
out = currentF >= lastTrigger && currentF < lastTrigger + hold ? 1 : 0;
|
|
172
|
+
} else if (mode === "toggle") {
|
|
173
|
+
let toggleCount = 0;
|
|
174
|
+
let lastTrigger = -Infinity;
|
|
175
|
+
for (let f = 0; f <= currentF; f++) {
|
|
176
|
+
const v = this.get({
|
|
177
|
+
...ctx,
|
|
178
|
+
frame: f,
|
|
179
|
+
time: f / ctx.fps
|
|
180
|
+
});
|
|
181
|
+
const prev = f > 0 ? this.get({
|
|
182
|
+
...ctx,
|
|
183
|
+
frame: f - 1,
|
|
184
|
+
time: (f - 1) / ctx.fps
|
|
185
|
+
}) : 0;
|
|
186
|
+
if (v >= threshold && prev < threshold && f - lastTrigger >= debounce) {
|
|
187
|
+
toggleCount++;
|
|
188
|
+
lastTrigger = f;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
out = toggleCount % 2 === 1 ? 1 : 0;
|
|
192
|
+
}
|
|
193
|
+
return invert ? 1 - out : out;
|
|
194
|
+
}, { label: `${this.label ?? "sig"}_gate(${threshold})` });
|
|
195
|
+
}
|
|
196
|
+
velocity(dt = 1) {
|
|
197
|
+
const step = Math.max(1, Math.round(dt));
|
|
198
|
+
return this._derive((ctx) => {
|
|
199
|
+
const curr = this.get(ctx);
|
|
200
|
+
const prevF = Math.max(0, ctx.frame - step);
|
|
201
|
+
const prev = this.get({
|
|
202
|
+
...ctx,
|
|
203
|
+
frame: prevF,
|
|
204
|
+
time: prevF / ctx.fps
|
|
205
|
+
});
|
|
206
|
+
const deltaSec = step / ctx.fps;
|
|
207
|
+
return deltaSec > 0 ? (curr - prev) / deltaSec : curr - prev;
|
|
208
|
+
}, { label: `${this.label ?? "sig"}_velocity` });
|
|
209
|
+
}
|
|
210
|
+
delay(frames) {
|
|
211
|
+
const shift = Math.max(0, Math.round(frames));
|
|
212
|
+
return this._derive((ctx) => {
|
|
213
|
+
const delayedF = Math.max(0, ctx.frame - shift);
|
|
214
|
+
return this.get({
|
|
215
|
+
...ctx,
|
|
216
|
+
frame: delayedF,
|
|
217
|
+
time: delayedF / ctx.fps
|
|
218
|
+
});
|
|
219
|
+
}, { label: `${this.label ?? "sig"}_delay(${shift})` });
|
|
220
|
+
}
|
|
221
|
+
spring(stiffness = 150, damping = 12) {
|
|
222
|
+
return this._derive((ctx) => {
|
|
223
|
+
const targetFrame = ctx.frame;
|
|
224
|
+
if (targetFrame <= 0) return this.get(ctx);
|
|
225
|
+
let pos = this.get({
|
|
226
|
+
...ctx,
|
|
227
|
+
frame: 0,
|
|
228
|
+
time: 0
|
|
229
|
+
});
|
|
230
|
+
let vel = 0;
|
|
231
|
+
const dt = 1 / Math.max(1, ctx.fps);
|
|
232
|
+
for (let f = 1; f <= targetFrame; f++) {
|
|
233
|
+
const targetVal = this.get({
|
|
234
|
+
...ctx,
|
|
235
|
+
frame: f,
|
|
236
|
+
time: f / ctx.fps
|
|
237
|
+
});
|
|
238
|
+
const displacement = pos - targetVal;
|
|
239
|
+
const force = -stiffness * displacement - damping * vel;
|
|
240
|
+
vel += force * dt;
|
|
241
|
+
pos += vel * dt;
|
|
242
|
+
}
|
|
243
|
+
return pos;
|
|
244
|
+
}, { label: `${this.label ?? "sig"}_spring` });
|
|
245
|
+
}
|
|
246
|
+
add(other) {
|
|
247
|
+
if (typeof other === "number") return this._derive((ctx) => this.get(ctx) + other);
|
|
248
|
+
return this._derive((ctx) => this.get(ctx) + (Number(other.get(ctx)) || 0), void 0, other);
|
|
249
|
+
}
|
|
250
|
+
subtract(other) {
|
|
251
|
+
if (typeof other === "number") return this._derive((ctx) => this.get(ctx) - other);
|
|
252
|
+
return this._derive((ctx) => this.get(ctx) - (Number(other.get(ctx)) || 0), void 0, other);
|
|
253
|
+
}
|
|
254
|
+
multiply(factor) {
|
|
255
|
+
if (typeof factor === "number") return this._derive((ctx) => this.get(ctx) * factor);
|
|
256
|
+
return this._derive((ctx) => this.get(ctx) * (Number(factor.get(ctx)) || 0), void 0, factor);
|
|
257
|
+
}
|
|
258
|
+
invert() {
|
|
259
|
+
return this._derive((ctx) => 1 - this.get(ctx), { label: `${this.label ?? "sig"}_inverted` });
|
|
260
|
+
}
|
|
261
|
+
pow(exponent) {
|
|
262
|
+
return this._derive((ctx) => this.get(ctx) ** exponent);
|
|
263
|
+
}
|
|
264
|
+
map(fn) {
|
|
265
|
+
return this._derive((ctx) => fn(this.get(ctx), ctx));
|
|
266
|
+
}
|
|
267
|
+
toFrameArray(totalFrames, fps) {
|
|
268
|
+
const count = Math.max(1, totalFrames);
|
|
269
|
+
const f = fps ?? this._fps;
|
|
270
|
+
const out = new Float32Array(count);
|
|
271
|
+
for (let i = 0; i < count; i++) out[i] = this.get({
|
|
272
|
+
frame: i,
|
|
273
|
+
fps: f,
|
|
274
|
+
time: i / f,
|
|
275
|
+
duration: count,
|
|
276
|
+
durationMs: count / f * 1e3,
|
|
277
|
+
progress: i / count,
|
|
278
|
+
deltaTime: 1 / f
|
|
279
|
+
});
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function programmaticSignal(source, options) {
|
|
284
|
+
return new ProgrammaticSignal(source, options);
|
|
285
|
+
}
|
|
286
|
+
let activeComputed = null;
|
|
287
|
+
function registerSignalDependency(subscribers) {
|
|
288
|
+
if (activeComputed !== null) {
|
|
289
|
+
subscribers.add(activeComputed);
|
|
290
|
+
activeComputed.deps.add(subscribers);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function isSignal(val) {
|
|
294
|
+
return val !== null && typeof val === "object" && "get" in val && typeof val.get === "function" && "value" in val;
|
|
295
|
+
}
|
|
296
|
+
var StateSignalImpl = class {
|
|
297
|
+
_value;
|
|
298
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
299
|
+
constructor(initialValue) {
|
|
300
|
+
this._value = initialValue;
|
|
301
|
+
}
|
|
302
|
+
get value() {
|
|
303
|
+
return this.get();
|
|
304
|
+
}
|
|
305
|
+
set value(nextValue) {
|
|
306
|
+
this.set(nextValue);
|
|
307
|
+
}
|
|
308
|
+
get() {
|
|
309
|
+
if (activeComputed !== null) {
|
|
310
|
+
this._subscribers.add(activeComputed);
|
|
311
|
+
activeComputed.deps.add(this._subscribers);
|
|
312
|
+
}
|
|
313
|
+
return this._value;
|
|
314
|
+
}
|
|
315
|
+
peek() {
|
|
316
|
+
return this._value;
|
|
317
|
+
}
|
|
318
|
+
set(nextValue) {
|
|
319
|
+
if (Object.is(this._value, nextValue)) return;
|
|
320
|
+
this._value = nextValue;
|
|
321
|
+
this.notifySubscribers();
|
|
322
|
+
}
|
|
323
|
+
update(fn) {
|
|
324
|
+
this.set(fn(this._value));
|
|
325
|
+
}
|
|
326
|
+
notifySubscribers() {
|
|
327
|
+
if (this._subscribers.size === 0) return;
|
|
328
|
+
const toNotify = Array.from(this._subscribers);
|
|
329
|
+
for (const sub of toNotify) sub.markDirty();
|
|
330
|
+
}
|
|
331
|
+
valueOf() {
|
|
332
|
+
return this.get();
|
|
333
|
+
}
|
|
334
|
+
[Symbol.toPrimitive](hint) {
|
|
335
|
+
if (hint === "number") return Number(this.get());
|
|
336
|
+
if (hint === "string") return String(this.get());
|
|
337
|
+
return this.get();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
var ComputedImpl = class {
|
|
341
|
+
_fn;
|
|
342
|
+
_value;
|
|
343
|
+
_dirty = true;
|
|
344
|
+
_evaluating = false;
|
|
345
|
+
deps = /* @__PURE__ */ new Set();
|
|
346
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
347
|
+
constructor(fn) {
|
|
348
|
+
this._fn = fn;
|
|
349
|
+
}
|
|
350
|
+
get value() {
|
|
351
|
+
return this.get();
|
|
352
|
+
}
|
|
353
|
+
get() {
|
|
354
|
+
if (activeComputed !== null && activeComputed !== this) {
|
|
355
|
+
this._subscribers.add(activeComputed);
|
|
356
|
+
activeComputed.deps.add(this._subscribers);
|
|
357
|
+
}
|
|
358
|
+
if (this._dirty) this.compute();
|
|
359
|
+
return this._value;
|
|
360
|
+
}
|
|
361
|
+
peek() {
|
|
362
|
+
if (this._dirty) this.compute();
|
|
363
|
+
return this._value;
|
|
364
|
+
}
|
|
365
|
+
markDirty() {
|
|
366
|
+
if (this._dirty) return;
|
|
367
|
+
this._dirty = true;
|
|
368
|
+
const toNotify = Array.from(this._subscribers);
|
|
369
|
+
for (const sub of toNotify) sub.markDirty();
|
|
370
|
+
}
|
|
371
|
+
compute() {
|
|
372
|
+
if (this._evaluating) throw new Error("Cyclic dependency detected in computed signal");
|
|
373
|
+
for (const depSubSet of this.deps) depSubSet.delete(this);
|
|
374
|
+
this.deps.clear();
|
|
375
|
+
const prevActive = activeComputed;
|
|
376
|
+
activeComputed = this;
|
|
377
|
+
this._evaluating = true;
|
|
378
|
+
try {
|
|
379
|
+
this._value = this._fn();
|
|
380
|
+
this._dirty = false;
|
|
381
|
+
} finally {
|
|
382
|
+
this._evaluating = false;
|
|
383
|
+
activeComputed = prevActive;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
valueOf() {
|
|
387
|
+
return this.get();
|
|
388
|
+
}
|
|
389
|
+
[Symbol.toPrimitive](hint) {
|
|
390
|
+
if (hint === "number") return Number(this.get());
|
|
391
|
+
if (hint === "string") return String(this.get());
|
|
392
|
+
return this.get();
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
var FrameArraySignalImpl = class {
|
|
396
|
+
gpuBinding;
|
|
397
|
+
fps;
|
|
398
|
+
_samples;
|
|
399
|
+
_cachedFrame = -1;
|
|
400
|
+
_cachedValue = 0;
|
|
401
|
+
_seekedFrame = -1;
|
|
402
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
403
|
+
constructor(samples, fps = 24, gpuBinding) {
|
|
404
|
+
this._samples = samples;
|
|
405
|
+
this.fps = fps;
|
|
406
|
+
this.gpuBinding = gpuBinding;
|
|
407
|
+
if (samples.length > 0) this._cachedValue = samples[0];
|
|
408
|
+
}
|
|
409
|
+
get value() {
|
|
410
|
+
return this.get();
|
|
411
|
+
}
|
|
412
|
+
get(ctx) {
|
|
413
|
+
if (activeComputed !== null) {
|
|
414
|
+
this._subscribers.add(activeComputed);
|
|
415
|
+
activeComputed.deps.add(this._subscribers);
|
|
416
|
+
}
|
|
417
|
+
frameSignal.get();
|
|
418
|
+
const currentFrame = ctx?.frame ?? (this._seekedFrame >= 0 ? this._seekedFrame : frameSignal.value);
|
|
419
|
+
if (this._cachedFrame === currentFrame) return this._cachedValue;
|
|
420
|
+
this._cachedFrame = currentFrame;
|
|
421
|
+
if (!this._samples || this._samples.length === 0) {
|
|
422
|
+
this._cachedValue = 0;
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
const compFps = ctx?.fps ?? this.fps;
|
|
426
|
+
const targetFrame = compFps !== this.fps && compFps > 0 ? currentFrame / compFps * this.fps : currentFrame;
|
|
427
|
+
const idx = Math.max(0, Math.min(this._samples.length - 1, Math.round(targetFrame)));
|
|
428
|
+
this._cachedValue = Number(this._samples[idx]) || 0;
|
|
429
|
+
return this._cachedValue;
|
|
430
|
+
}
|
|
431
|
+
peek() {
|
|
432
|
+
return this._cachedValue;
|
|
433
|
+
}
|
|
434
|
+
seekFrame(frame) {
|
|
435
|
+
this._seekedFrame = frame;
|
|
436
|
+
this._cachedFrame = frame;
|
|
437
|
+
if (!this._samples || this._samples.length === 0) this._cachedValue = 0;
|
|
438
|
+
else {
|
|
439
|
+
const idx = Math.max(0, Math.min(this._samples.length - 1, Math.round(frame)));
|
|
440
|
+
this._cachedValue = Number(this._samples[idx]) || 0;
|
|
441
|
+
}
|
|
442
|
+
for (const sub of this._subscribers) sub.markDirty();
|
|
443
|
+
}
|
|
444
|
+
valueOf() {
|
|
445
|
+
return this.get();
|
|
446
|
+
}
|
|
447
|
+
[Symbol.toPrimitive](hint) {
|
|
448
|
+
if (hint === "number") return Number(this.get());
|
|
449
|
+
if (hint === "string") return String(this.get());
|
|
450
|
+
return this.get();
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function signal(initialValue) {
|
|
454
|
+
return new StateSignalImpl(initialValue);
|
|
455
|
+
}
|
|
456
|
+
function computed(fn) {
|
|
457
|
+
return new ComputedImpl(fn);
|
|
458
|
+
}
|
|
459
|
+
function frameArraySignal(samples, fps = 24, gpuBinding) {
|
|
460
|
+
return new FrameArraySignalImpl(samples, fps, gpuBinding);
|
|
461
|
+
}
|
|
462
|
+
const frameSignal = signal(0);
|
|
463
|
+
const timeSignal = signal(0);
|
|
464
|
+
const progressSignal = signal(0);
|
|
465
|
+
function updateClockSignals(frame, fps = 24, durationMs) {
|
|
466
|
+
frameSignal.set(frame);
|
|
467
|
+
const timeSec = fps > 0 ? frame / fps : 0;
|
|
468
|
+
timeSignal.set(timeSec);
|
|
469
|
+
if (durationMs && durationMs > 0) {
|
|
470
|
+
const durSec = durationMs / 1e3;
|
|
471
|
+
progressSignal.set(Math.max(0, Math.min(1, timeSec / durSec)));
|
|
472
|
+
} else progressSignal.set(0);
|
|
473
|
+
}
|
|
474
|
+
var ProceduralSignalImpl = class {
|
|
475
|
+
gpuBinding;
|
|
476
|
+
_config;
|
|
477
|
+
constructor(config) {
|
|
478
|
+
this._config = {
|
|
479
|
+
type: "sine",
|
|
480
|
+
frequency: 1,
|
|
481
|
+
amplitude: 1,
|
|
482
|
+
offset: 0,
|
|
483
|
+
phase: 0,
|
|
484
|
+
...config
|
|
485
|
+
};
|
|
486
|
+
this.gpuBinding = config.gpuBinding;
|
|
487
|
+
}
|
|
488
|
+
get value() {
|
|
489
|
+
return this.get();
|
|
490
|
+
}
|
|
491
|
+
get(ctx) {
|
|
492
|
+
frameSignal.get();
|
|
493
|
+
const frame = ctx?.frame ?? frameSignal.value;
|
|
494
|
+
const fps = ctx?.fps ?? 24;
|
|
495
|
+
const time = ctx?.time ?? (fps > 0 ? frame / fps : 0);
|
|
496
|
+
if (this._config.fn) return this._config.fn(ctx ?? {
|
|
497
|
+
frame,
|
|
498
|
+
fps,
|
|
499
|
+
time,
|
|
500
|
+
duration: 0,
|
|
501
|
+
durationMs: 0,
|
|
502
|
+
progress: 0,
|
|
503
|
+
deltaTime: fps > 0 ? 1 / fps : 0
|
|
504
|
+
});
|
|
505
|
+
const freq = this._config.frequency ?? 1;
|
|
506
|
+
const amp = this._config.amplitude ?? 1;
|
|
507
|
+
const off = this._config.offset ?? 0;
|
|
508
|
+
const phase = this._config.phase ?? 0;
|
|
509
|
+
switch (this._config.type ?? "sine") {
|
|
510
|
+
case "sine": return off + amp * Math.sin(2 * Math.PI * freq * time + phase);
|
|
511
|
+
case "triangle": return off + amp * (2 * Math.abs(2 * (time * freq + phase / (2 * Math.PI) - Math.floor(time * freq + phase / (2 * Math.PI) + .5))) - 1);
|
|
512
|
+
case "sawtooth": return off + amp * (2 * (time * freq + phase / (2 * Math.PI) - Math.floor(time * freq + phase / (2 * Math.PI) + .5)));
|
|
513
|
+
case "square": return off + (Math.sin(2 * Math.PI * freq * time + phase) >= 0 ? amp : -amp);
|
|
514
|
+
case "bounce": {
|
|
515
|
+
const cycle = time * freq % 1;
|
|
516
|
+
return off + amp * Math.abs(Math.sin(Math.PI * cycle));
|
|
517
|
+
}
|
|
518
|
+
default: return off + amp * Math.sin(2 * Math.PI * freq * time + phase);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
peek() {
|
|
522
|
+
return this.get();
|
|
523
|
+
}
|
|
524
|
+
valueOf() {
|
|
525
|
+
return this.get();
|
|
526
|
+
}
|
|
527
|
+
[Symbol.toPrimitive](hint) {
|
|
528
|
+
if (hint === "number") return Number(this.get());
|
|
529
|
+
if (hint === "string") return String(this.get());
|
|
530
|
+
return this.get();
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
function signalBuilder(config) {
|
|
534
|
+
return new ProceduralSignalImpl(config);
|
|
535
|
+
}
|
|
536
|
+
function sineSignal(options = {}) {
|
|
537
|
+
const min = options.min;
|
|
538
|
+
const max = options.max;
|
|
539
|
+
let amplitude = options.amplitude ?? 1;
|
|
540
|
+
let offset = options.offset ?? 0;
|
|
541
|
+
if (min !== void 0 && max !== void 0) {
|
|
542
|
+
offset = (min + max) / 2;
|
|
543
|
+
amplitude = (max - min) / 2;
|
|
544
|
+
} else if (min !== void 0) offset = min + amplitude;
|
|
545
|
+
else if (max !== void 0) offset = max - amplitude;
|
|
546
|
+
const period = options.period;
|
|
547
|
+
const frequency = options.frequency;
|
|
548
|
+
const phase = options.phase ?? 0;
|
|
549
|
+
return signalBuilder({
|
|
550
|
+
type: "custom",
|
|
551
|
+
fn: (ctx) => {
|
|
552
|
+
let angle;
|
|
553
|
+
if (period !== void 0 && period > 0) angle = 2 * Math.PI * ctx.frame / period + phase;
|
|
554
|
+
else {
|
|
555
|
+
const freq = frequency ?? 1;
|
|
556
|
+
angle = 2 * Math.PI * freq * ctx.time + phase;
|
|
557
|
+
}
|
|
558
|
+
return offset + amplitude * Math.sin(angle);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function cosineSignal(options = {}) {
|
|
563
|
+
const min = options.min;
|
|
564
|
+
const max = options.max;
|
|
565
|
+
let amplitude = options.amplitude ?? 1;
|
|
566
|
+
let offset = options.offset ?? 0;
|
|
567
|
+
if (min !== void 0 && max !== void 0) {
|
|
568
|
+
offset = (min + max) / 2;
|
|
569
|
+
amplitude = (max - min) / 2;
|
|
570
|
+
} else if (min !== void 0) offset = min + amplitude;
|
|
571
|
+
else if (max !== void 0) offset = max - amplitude;
|
|
572
|
+
const period = options.period;
|
|
573
|
+
const frequency = options.frequency;
|
|
574
|
+
const phase = options.phase ?? 0;
|
|
575
|
+
return signalBuilder({
|
|
576
|
+
type: "custom",
|
|
577
|
+
fn: (ctx) => {
|
|
578
|
+
let angle;
|
|
579
|
+
if (period !== void 0 && period > 0) angle = 2 * Math.PI * ctx.frame / period + phase;
|
|
580
|
+
else {
|
|
581
|
+
const freq = frequency ?? 1;
|
|
582
|
+
angle = 2 * Math.PI * freq * ctx.time + phase;
|
|
583
|
+
}
|
|
584
|
+
return offset + amplitude * Math.cos(angle);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const Signal = {
|
|
589
|
+
state: signal,
|
|
590
|
+
computed,
|
|
591
|
+
builder: signalBuilder,
|
|
592
|
+
fromArray: frameArraySignal,
|
|
593
|
+
programmatic: programmaticSignal,
|
|
594
|
+
frame: frameSignal,
|
|
595
|
+
time: timeSignal,
|
|
596
|
+
progress: progressSignal,
|
|
597
|
+
sine: sineSignal,
|
|
598
|
+
cosine: cosineSignal
|
|
599
|
+
};
|
|
600
|
+
var Effect = class {
|
|
601
|
+
signals;
|
|
602
|
+
config;
|
|
603
|
+
_frameHooks = [];
|
|
604
|
+
constructor(initialConfig) {
|
|
605
|
+
this.config = { ...initialConfig };
|
|
606
|
+
this.signals = {};
|
|
607
|
+
for (const [key, val] of Object.entries(initialConfig)) if (isSignal(val)) this.signals[key] = val;
|
|
608
|
+
else this.signals[key] = signal(val);
|
|
609
|
+
return new Proxy(this, {
|
|
610
|
+
get(target, prop, receiver) {
|
|
611
|
+
if (typeof prop === "string" && prop in target.signals) {
|
|
612
|
+
const sig = target.signals[prop];
|
|
613
|
+
return sig ? sig.value : void 0;
|
|
614
|
+
}
|
|
615
|
+
return Reflect.get(target, prop, receiver);
|
|
616
|
+
},
|
|
617
|
+
set(target, prop, val, receiver) {
|
|
618
|
+
if (typeof prop === "string" && prop in target.signals) {
|
|
619
|
+
const currentSig = target.signals[prop];
|
|
620
|
+
if (isSignal(val)) {
|
|
621
|
+
target.signals[prop] = val;
|
|
622
|
+
target.config[prop] = val;
|
|
623
|
+
} else if (currentSig && typeof currentSig.set === "function") {
|
|
624
|
+
currentSig.set(val);
|
|
625
|
+
target.config[prop] = val;
|
|
626
|
+
} else {
|
|
627
|
+
target.signals[prop] = signal(val);
|
|
628
|
+
target.config[prop] = val;
|
|
629
|
+
}
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
return Reflect.set(target, prop, val, receiver);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Game-engine per-frame lifecycle hook.
|
|
638
|
+
* Invoked before rendering each frame so scripts or agents can mutate properties.
|
|
639
|
+
*/
|
|
640
|
+
onRequestFrame(callback) {
|
|
641
|
+
this._frameHooks.push(callback);
|
|
642
|
+
return this;
|
|
643
|
+
}
|
|
644
|
+
notifyFrame(ctx) {
|
|
645
|
+
for (const hook of this._frameHooks) hook(ctx);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Samples concrete scalar / config values for the current frame.
|
|
649
|
+
*/
|
|
650
|
+
resolveUniforms(ctx) {
|
|
651
|
+
const out = {};
|
|
652
|
+
for (const [k, sig] of Object.entries(this.signals)) out[k] = sig.get(ctx);
|
|
653
|
+
return out;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Returns WebGPU texture bindings attached to any property signal.
|
|
657
|
+
*/
|
|
658
|
+
resolveTextures() {
|
|
659
|
+
const out = {};
|
|
660
|
+
for (const [k, sig] of Object.entries(this.signals)) {
|
|
661
|
+
const gpu = sig.gpuBinding;
|
|
662
|
+
if (gpu?.textureView) out[`${k}TextureView`] = gpu.textureView;
|
|
663
|
+
if (gpu?.texture) out[`${k}Texture`] = gpu.texture;
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Converts the effect into an operation node for the VirtualMediaData AST.
|
|
669
|
+
*/
|
|
670
|
+
toOperation() {
|
|
671
|
+
const opPayload = {
|
|
672
|
+
op: this.op,
|
|
673
|
+
effect: this,
|
|
674
|
+
onRequestFrame: (ctx) => this.notifyFrame(ctx),
|
|
675
|
+
signals: this.signals,
|
|
676
|
+
...this.config
|
|
677
|
+
};
|
|
678
|
+
for (const [k, sig] of Object.entries(this.signals)) {
|
|
679
|
+
const orig = this.config[k];
|
|
680
|
+
if (!isSignal(orig) && (typeof orig === "string" || typeof orig === "boolean")) opPayload[k] = sig.value;
|
|
681
|
+
else opPayload[k] = sig;
|
|
682
|
+
}
|
|
683
|
+
return opPayload;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
var ModerationError = class extends Error {
|
|
687
|
+
constructor(message) {
|
|
688
|
+
super(message);
|
|
689
|
+
this.name = "ModerationError";
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
var ModerationUnrefundableError = class extends Error {
|
|
693
|
+
constructor(message) {
|
|
694
|
+
super(message);
|
|
695
|
+
this.name = "ModerationUnrefundableError";
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
/**
|
|
699
|
+
* Generates a unique ID using nanoid.
|
|
700
|
+
* This function is cross-platform compatible (Node.js and Browser).
|
|
701
|
+
*
|
|
702
|
+
* @param size - Optional size of the ID (default is 21)
|
|
703
|
+
* @returns A unique string ID
|
|
704
|
+
*/
|
|
705
|
+
const generateId = (size) => {
|
|
706
|
+
return nanoid(size);
|
|
707
|
+
};
|
|
708
|
+
const HandleDefinitionSchema = z$1.object({
|
|
709
|
+
dataTypes: z$1.custom(),
|
|
710
|
+
label: z$1.string(),
|
|
711
|
+
required: z$1.boolean().optional(),
|
|
712
|
+
order: z$1.number(),
|
|
713
|
+
description: z$1.string().optional()
|
|
714
|
+
});
|
|
715
|
+
const ConfigHandleSchema = z$1.object({
|
|
716
|
+
configKey: z$1.string(),
|
|
717
|
+
dataTypes: z$1.custom(),
|
|
718
|
+
label: z$1.string(),
|
|
719
|
+
description: z$1.string().optional()
|
|
720
|
+
});
|
|
721
|
+
/**
|
|
722
|
+
* Metadata defining the interface and identity of a node.
|
|
723
|
+
* This is safe to import in any environment.
|
|
724
|
+
*/
|
|
725
|
+
const NodeMetadataSchema = z$1.object({
|
|
726
|
+
type: z$1.string().min(1),
|
|
727
|
+
version: z$1.number().int().positive().optional(),
|
|
728
|
+
baseType: z$1.string().optional(),
|
|
729
|
+
displayName: z$1.string().min(1),
|
|
730
|
+
description: z$1.string().optional(),
|
|
731
|
+
showInQuickAccess: z$1.boolean().optional(),
|
|
732
|
+
showInSidebar: z$1.boolean().optional(),
|
|
733
|
+
advanced: z$1.boolean().optional(),
|
|
734
|
+
category: z$1.string().min(1),
|
|
735
|
+
subcategory: z$1.string().optional(),
|
|
736
|
+
defaultDimensions: z$1.object({
|
|
737
|
+
width: z$1.number().positive().optional(),
|
|
738
|
+
height: z$1.number().positive().optional()
|
|
739
|
+
}).optional(),
|
|
740
|
+
handles: z$1.object({
|
|
741
|
+
inputs: z$1.array(HandleDefinitionSchema),
|
|
742
|
+
outputs: z$1.array(HandleDefinitionSchema)
|
|
743
|
+
}),
|
|
744
|
+
variableInputs: z$1.discriminatedUnion("enabled", [z$1.object({
|
|
745
|
+
enabled: z$1.literal(true),
|
|
746
|
+
dataTypes: z$1.custom()
|
|
747
|
+
}), z$1.object({
|
|
748
|
+
enabled: z$1.literal(false),
|
|
749
|
+
dataTypes: z$1.custom().optional()
|
|
750
|
+
})]).optional(),
|
|
751
|
+
variableOutputs: z$1.discriminatedUnion("enabled", [z$1.object({
|
|
752
|
+
enabled: z$1.literal(true),
|
|
753
|
+
dataTypes: z$1.custom()
|
|
754
|
+
}), z$1.object({
|
|
755
|
+
enabled: z$1.literal(false),
|
|
756
|
+
dataTypes: z$1.custom().optional()
|
|
757
|
+
})]).optional(),
|
|
758
|
+
isTerminal: z$1.boolean().optional(),
|
|
759
|
+
isTransient: z$1.boolean().optional(),
|
|
760
|
+
configSchema: z$1.custom().optional(),
|
|
761
|
+
defaultConfig: z$1.record(z$1.string(), z$1.unknown()).optional(),
|
|
762
|
+
validation: z$1.any().optional(),
|
|
763
|
+
resultSchema: z$1.custom().optional(),
|
|
764
|
+
configHandles: z$1.array(ConfigHandleSchema).optional()
|
|
765
|
+
});
|
|
766
|
+
const DataTypeVal = {
|
|
767
|
+
Text: "Text",
|
|
768
|
+
Number: "Number",
|
|
769
|
+
Boolean: "Boolean",
|
|
770
|
+
Image: "Image",
|
|
771
|
+
Video: "Video",
|
|
772
|
+
Audio: "Audio",
|
|
773
|
+
SVG: "SVG",
|
|
774
|
+
Caption: "Caption",
|
|
775
|
+
Lottie: "Lottie",
|
|
776
|
+
ThreeD: "ThreeD",
|
|
777
|
+
GIF: "GIF",
|
|
778
|
+
Signal: "Signal",
|
|
779
|
+
LUT: "LUT"
|
|
780
|
+
};
|
|
781
|
+
const DataType = DataTypeVal;
|
|
782
|
+
const DataTypes = Object.values(DataTypeVal);
|
|
783
|
+
const HandleTypeVal = {
|
|
784
|
+
Input: "Input",
|
|
785
|
+
Output: "Output"
|
|
786
|
+
};
|
|
787
|
+
const JsonValueSchema = z$1.lazy(() => z$1.union([
|
|
788
|
+
z$1.string(),
|
|
789
|
+
z$1.number(),
|
|
790
|
+
z$1.boolean(),
|
|
791
|
+
z$1.null(),
|
|
792
|
+
z$1.array(JsonValueSchema),
|
|
793
|
+
z$1.record(z$1.string(), JsonValueSchema.optional())
|
|
794
|
+
]));
|
|
795
|
+
/**
|
|
796
|
+
* Zod schema for the FileAsset model
|
|
797
|
+
* Matches the Prisma schema definitions for types and nullability.
|
|
798
|
+
*/
|
|
799
|
+
const FileAssetSchema = z$1.object({
|
|
800
|
+
id: z$1.cuid2(),
|
|
801
|
+
name: z$1.string(),
|
|
802
|
+
createdAt: z$1.coerce.date(),
|
|
803
|
+
updatedAt: z$1.coerce.date(),
|
|
804
|
+
userId: z$1.cuid2().nullable(),
|
|
805
|
+
width: z$1.number().int().nullable(),
|
|
806
|
+
height: z$1.number().int().nullable(),
|
|
807
|
+
bucket: z$1.string(),
|
|
808
|
+
size: z$1.number().int(),
|
|
809
|
+
mimeType: z$1.string(),
|
|
810
|
+
key: z$1.string(),
|
|
811
|
+
isUploaded: z$1.boolean().default(true),
|
|
812
|
+
duration: z$1.number().int().nullable(),
|
|
813
|
+
sampleRate: z$1.number().int().nullable().optional(),
|
|
814
|
+
channels: z$1.number().int().nullable().optional(),
|
|
815
|
+
bitDepth: z$1.number().int().nullable().optional(),
|
|
816
|
+
audioCodec: z$1.string().nullable().optional(),
|
|
817
|
+
audioBitrate: z$1.number().int().nullable().optional(),
|
|
818
|
+
fps: z$1.number().int().nullable(),
|
|
819
|
+
fingerprint: z$1.string().nullable()
|
|
820
|
+
});
|
|
821
|
+
const NodeVisualConfigSchema = z$1.object({
|
|
822
|
+
position: z$1.object({
|
|
823
|
+
x: z$1.number(),
|
|
824
|
+
y: z$1.number()
|
|
825
|
+
}).optional(),
|
|
826
|
+
width: z$1.number().nullish(),
|
|
827
|
+
height: z$1.number().nullish(),
|
|
828
|
+
locked: z$1.boolean().optional().default(false),
|
|
829
|
+
collapsed: z$1.boolean().optional().default(false)
|
|
830
|
+
}).passthrough();
|
|
831
|
+
const dataTypeColors = {
|
|
832
|
+
Text: {
|
|
833
|
+
bg: "bg-blue-500",
|
|
834
|
+
stroke: "stroke-blue-500",
|
|
835
|
+
hex: "#3b82f6",
|
|
836
|
+
text: "text-blue-500",
|
|
837
|
+
border: "border-blue-500"
|
|
838
|
+
},
|
|
839
|
+
Image: {
|
|
840
|
+
bg: "bg-purple-500",
|
|
841
|
+
stroke: "stroke-purple-500",
|
|
842
|
+
hex: "#a855f7",
|
|
843
|
+
text: "text-purple-500",
|
|
844
|
+
border: "border-purple-500"
|
|
845
|
+
},
|
|
846
|
+
Video: {
|
|
847
|
+
bg: "bg-teal-500",
|
|
848
|
+
stroke: "stroke-teal-500",
|
|
849
|
+
hex: "oklch(60% 0.118 184.704)",
|
|
850
|
+
text: "text-teal-500",
|
|
851
|
+
border: "border-teal-500"
|
|
852
|
+
},
|
|
853
|
+
Audio: {
|
|
854
|
+
bg: "bg-orange-500",
|
|
855
|
+
stroke: "stroke-orange-500",
|
|
856
|
+
hex: "#f97316",
|
|
857
|
+
text: "text-orange-500",
|
|
858
|
+
border: "border-orange-500"
|
|
859
|
+
},
|
|
860
|
+
Number: {
|
|
861
|
+
bg: "bg-green-600",
|
|
862
|
+
stroke: "stroke-green-600",
|
|
863
|
+
hex: "#16a34a",
|
|
864
|
+
text: "text-green-600",
|
|
865
|
+
border: "border-green-600"
|
|
866
|
+
},
|
|
867
|
+
Boolean: {
|
|
868
|
+
bg: "bg-amber-600",
|
|
869
|
+
stroke: "stroke-amber-600",
|
|
870
|
+
hex: "#d97706",
|
|
871
|
+
text: "text-amber-600",
|
|
872
|
+
border: "border-amber-600"
|
|
873
|
+
},
|
|
874
|
+
SVG: {
|
|
875
|
+
bg: "bg-lime-600",
|
|
876
|
+
stroke: "stroke-lime-600",
|
|
877
|
+
hex: "#65a30d",
|
|
878
|
+
text: "text-lime-600",
|
|
879
|
+
border: "border-lime-600"
|
|
880
|
+
},
|
|
881
|
+
Caption: {
|
|
882
|
+
bg: "bg-rose-500",
|
|
883
|
+
stroke: "stroke-rose-500",
|
|
884
|
+
hex: "#f43f5e",
|
|
885
|
+
text: "text-rose-500",
|
|
886
|
+
border: "border-rose-500"
|
|
887
|
+
},
|
|
888
|
+
GIF: {
|
|
889
|
+
bg: "bg-pink-500",
|
|
890
|
+
stroke: "stroke-pink-500",
|
|
891
|
+
hex: "#ec4899",
|
|
892
|
+
text: "text-pink-500",
|
|
893
|
+
border: "border-pink-500"
|
|
894
|
+
},
|
|
895
|
+
Lottie: {
|
|
896
|
+
bg: "bg-indigo-500",
|
|
897
|
+
stroke: "stroke-indigo-500",
|
|
898
|
+
hex: "#6366f1",
|
|
899
|
+
text: "text-indigo-500",
|
|
900
|
+
border: "border-indigo-500"
|
|
901
|
+
},
|
|
902
|
+
Signal: {
|
|
903
|
+
bg: "bg-fuchsia-500",
|
|
904
|
+
stroke: "stroke-fuchsia-500",
|
|
905
|
+
hex: "#d946ef",
|
|
906
|
+
text: "text-fuchsia-500",
|
|
907
|
+
border: "border-fuchsia-500"
|
|
908
|
+
},
|
|
909
|
+
LUT: {
|
|
910
|
+
bg: "bg-cyan-600",
|
|
911
|
+
stroke: "stroke-cyan-600",
|
|
912
|
+
hex: "#0891b2",
|
|
913
|
+
text: "text-cyan-600",
|
|
914
|
+
border: "border-cyan-600"
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
const DATA_TYPE_EXTENSIONS = {
|
|
918
|
+
Image: "png",
|
|
919
|
+
Video: "mp4",
|
|
920
|
+
Audio: "mp3",
|
|
921
|
+
Text: "txt",
|
|
922
|
+
Number: "txt",
|
|
923
|
+
Boolean: "txt",
|
|
924
|
+
SVG: "svg",
|
|
925
|
+
Caption: "srt",
|
|
926
|
+
Lottie: "json",
|
|
927
|
+
ThreeD: "glb",
|
|
928
|
+
GIF: "gif",
|
|
929
|
+
Signal: "txt",
|
|
930
|
+
LUT: "cube"
|
|
931
|
+
};
|
|
932
|
+
const MIME_TYPES = {
|
|
933
|
+
png: "image/png",
|
|
934
|
+
jpg: "image/jpeg",
|
|
935
|
+
jpeg: "image/jpeg",
|
|
936
|
+
gif: "image/gif",
|
|
937
|
+
webp: "image/webp",
|
|
938
|
+
svg: "image/svg+xml",
|
|
939
|
+
mp4: "video/mp4",
|
|
940
|
+
webm: "video/webm",
|
|
941
|
+
mov: "video/quicktime",
|
|
942
|
+
mp3: "audio/mpeg",
|
|
943
|
+
wav: "audio/wav",
|
|
944
|
+
ogg: "audio/ogg",
|
|
945
|
+
pdf: "application/pdf",
|
|
946
|
+
json: "application/json",
|
|
947
|
+
txt: "text/plain",
|
|
948
|
+
srt: "text/srt",
|
|
949
|
+
aac: "audio/aac",
|
|
950
|
+
flac: "audio/flac"
|
|
951
|
+
};
|
|
952
|
+
const COMPOSITE_OPERATIONS$1 = [
|
|
953
|
+
"source-over",
|
|
954
|
+
"source-in",
|
|
955
|
+
"source-out",
|
|
956
|
+
"source-atop",
|
|
957
|
+
"destination-over",
|
|
958
|
+
"destination-in",
|
|
959
|
+
"destination-out",
|
|
960
|
+
"destination-atop",
|
|
961
|
+
"lighter",
|
|
962
|
+
"copy",
|
|
963
|
+
"xor",
|
|
964
|
+
"multiply",
|
|
965
|
+
"screen",
|
|
966
|
+
"overlay",
|
|
967
|
+
"darken",
|
|
968
|
+
"lighten",
|
|
969
|
+
"color-dodge",
|
|
970
|
+
"color-burn",
|
|
971
|
+
"hard-light",
|
|
972
|
+
"soft-light",
|
|
973
|
+
"difference",
|
|
974
|
+
"exclusion",
|
|
975
|
+
"hue",
|
|
976
|
+
"saturation",
|
|
977
|
+
"color",
|
|
978
|
+
"luminosity"
|
|
979
|
+
];
|
|
980
|
+
const GlobalCompositeOperation$1 = z.enum(COMPOSITE_OPERATIONS$1);
|
|
981
|
+
var ConfigSchemaRegistry = class ConfigSchemaRegistry$1 {
|
|
982
|
+
static schemas = /* @__PURE__ */ new Map();
|
|
983
|
+
static register(type, schema) {
|
|
984
|
+
ConfigSchemaRegistry$1.schemas.set(type, schema);
|
|
985
|
+
}
|
|
986
|
+
static get(type) {
|
|
987
|
+
return ConfigSchemaRegistry$1.schemas.get(type);
|
|
988
|
+
}
|
|
989
|
+
static clear() {
|
|
990
|
+
ConfigSchemaRegistry$1.schemas.clear();
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
const COMPOSITOR_DEFAULTS = {
|
|
994
|
+
FONT_FAMILY: "Inter",
|
|
995
|
+
FONT_SIZE: 64,
|
|
996
|
+
FILL: "#ffffff",
|
|
997
|
+
LINE_HEIGHT: 1.1,
|
|
998
|
+
ALIGN: "left",
|
|
999
|
+
VERTICAL_ALIGN: "top",
|
|
1000
|
+
LETTER_SPACING: 0,
|
|
1001
|
+
STROKE_WIDTH: 0,
|
|
1002
|
+
STROKE: "#000000",
|
|
1003
|
+
CORNER_RADIUS: 0,
|
|
1004
|
+
PADDING: 0
|
|
1005
|
+
};
|
|
1006
|
+
const FileDataSchema = z$1.object({ entity: z$1.custom().optional() });
|
|
1007
|
+
const SignalStatsSchema = z$1.object({
|
|
1008
|
+
min: z$1.number().optional(),
|
|
1009
|
+
max: z$1.number().optional(),
|
|
1010
|
+
mean: z$1.number().optional(),
|
|
1011
|
+
rms: z$1.number().optional(),
|
|
1012
|
+
peak: z$1.number().optional(),
|
|
1013
|
+
beats: z$1.array(z$1.number()).optional(),
|
|
1014
|
+
tempo: z$1.number().optional()
|
|
1015
|
+
});
|
|
1016
|
+
const SignalChannelMetaSchema = z$1.object({
|
|
1017
|
+
id: z$1.string(),
|
|
1018
|
+
name: z$1.string(),
|
|
1019
|
+
index: z$1.number(),
|
|
1020
|
+
stats: SignalStatsSchema.optional()
|
|
1021
|
+
});
|
|
1022
|
+
const GeneratorSignalDataSchema = z$1.object({
|
|
1023
|
+
v: z$1.literal(2).optional(),
|
|
1024
|
+
type: z$1.literal("generator"),
|
|
1025
|
+
nodeId: z$1.string().optional(),
|
|
1026
|
+
baseType: z$1.string().optional(),
|
|
1027
|
+
func: z$1.enum([
|
|
1028
|
+
"sine",
|
|
1029
|
+
"triangle",
|
|
1030
|
+
"sawtooth",
|
|
1031
|
+
"square",
|
|
1032
|
+
"custom"
|
|
1033
|
+
]).or(z$1.string()).default("sine"),
|
|
1034
|
+
frequency: z$1.number().default(1),
|
|
1035
|
+
amplitude: z$1.number().default(1),
|
|
1036
|
+
phase: z$1.number().default(0),
|
|
1037
|
+
offset: z$1.number().default(0),
|
|
1038
|
+
waveform: z$1.string().optional(),
|
|
1039
|
+
bpm: z$1.number().optional(),
|
|
1040
|
+
syncToBpm: z$1.boolean().optional(),
|
|
1041
|
+
fmFrequency: z$1.number().optional(),
|
|
1042
|
+
fmDepth: z$1.number().optional(),
|
|
1043
|
+
envelope: z$1.string().optional(),
|
|
1044
|
+
fnBody: z$1.string().optional(),
|
|
1045
|
+
fnParams: z$1.array(z$1.record(z$1.string(), z$1.unknown())).optional(),
|
|
1046
|
+
customWGSL: z$1.string().optional(),
|
|
1047
|
+
signalFnName: z$1.string().optional(),
|
|
1048
|
+
outputType: z$1.string().optional(),
|
|
1049
|
+
fps: z$1.number().optional(),
|
|
1050
|
+
durationMs: z$1.number().optional(),
|
|
1051
|
+
stats: SignalStatsSchema.optional()
|
|
1052
|
+
}).passthrough();
|
|
1053
|
+
const AudioExtractorSignalDataSchema = z$1.object({
|
|
1054
|
+
v: z$1.literal(2).optional(),
|
|
1055
|
+
type: z$1.literal("audio_extractor"),
|
|
1056
|
+
nodeId: z$1.string().optional(),
|
|
1057
|
+
sourceUrl: z$1.string().optional(),
|
|
1058
|
+
channel: z$1.enum([
|
|
1059
|
+
"primary",
|
|
1060
|
+
"beat",
|
|
1061
|
+
"bass",
|
|
1062
|
+
"energy",
|
|
1063
|
+
"spectral_flux"
|
|
1064
|
+
]).or(z$1.string()).optional(),
|
|
1065
|
+
channelIndex: z$1.number().optional(),
|
|
1066
|
+
channels: z$1.array(SignalChannelMetaSchema).optional(),
|
|
1067
|
+
extractionMode: z$1.string().optional(),
|
|
1068
|
+
attackMs: z$1.number().optional(),
|
|
1069
|
+
releaseMs: z$1.number().optional(),
|
|
1070
|
+
sensitivity: z$1.number().optional(),
|
|
1071
|
+
noiseFloorDb: z$1.number().optional(),
|
|
1072
|
+
dynamicRangeDb: z$1.number().optional(),
|
|
1073
|
+
autoRange: z$1.boolean().optional(),
|
|
1074
|
+
smoothing: z$1.number().optional(),
|
|
1075
|
+
curve: z$1.string().optional(),
|
|
1076
|
+
beatThreshold: z$1.number().optional(),
|
|
1077
|
+
beatDecayMs: z$1.number().optional(),
|
|
1078
|
+
previewMode: z$1.string().optional(),
|
|
1079
|
+
samples: z$1.union([z$1.array(z$1.number()), z$1.instanceof(Float32Array)]).optional(),
|
|
1080
|
+
fps: z$1.number().optional(),
|
|
1081
|
+
durationMs: z$1.number().optional(),
|
|
1082
|
+
stats: SignalStatsSchema.optional(),
|
|
1083
|
+
state: z$1.enum([
|
|
1084
|
+
"pending",
|
|
1085
|
+
"ready",
|
|
1086
|
+
"failed"
|
|
1087
|
+
]).optional(),
|
|
1088
|
+
error: z$1.string().optional(),
|
|
1089
|
+
virtualMedia: z$1.record(z$1.string(), z$1.unknown()).optional(),
|
|
1090
|
+
signalFnName: z$1.string().optional(),
|
|
1091
|
+
customWGSL: z$1.string().optional()
|
|
1092
|
+
}).passthrough();
|
|
1093
|
+
const SignalMathDataSchema = z$1.object({
|
|
1094
|
+
v: z$1.literal(2).optional(),
|
|
1095
|
+
type: z$1.literal("signal_math"),
|
|
1096
|
+
nodeId: z$1.string().optional(),
|
|
1097
|
+
op: z$1.string().optional(),
|
|
1098
|
+
operation: z$1.string().optional(),
|
|
1099
|
+
signalA: z$1.unknown().optional(),
|
|
1100
|
+
signalB: z$1.unknown().optional(),
|
|
1101
|
+
bValue: z$1.number().optional(),
|
|
1102
|
+
inMin: z$1.number().optional(),
|
|
1103
|
+
inMax: z$1.number().optional(),
|
|
1104
|
+
outMin: z$1.number().optional(),
|
|
1105
|
+
outMax: z$1.number().optional(),
|
|
1106
|
+
clampMin: z$1.number().optional(),
|
|
1107
|
+
clampMax: z$1.number().optional(),
|
|
1108
|
+
exponent: z$1.number().optional(),
|
|
1109
|
+
edge0: z$1.number().optional(),
|
|
1110
|
+
edge1: z$1.number().optional(),
|
|
1111
|
+
customWGSL: z$1.string().optional(),
|
|
1112
|
+
stats: SignalStatsSchema.optional()
|
|
1113
|
+
}).passthrough();
|
|
1114
|
+
const SignalGateDataSchema = z$1.object({
|
|
1115
|
+
v: z$1.literal(2).optional(),
|
|
1116
|
+
type: z$1.literal("gate"),
|
|
1117
|
+
nodeId: z$1.string().optional(),
|
|
1118
|
+
sourceSignal: z$1.unknown().optional(),
|
|
1119
|
+
threshold: z$1.number().optional(),
|
|
1120
|
+
debounceMs: z$1.number().optional(),
|
|
1121
|
+
mode: z$1.enum([
|
|
1122
|
+
"gate",
|
|
1123
|
+
"trigger",
|
|
1124
|
+
"toggle"
|
|
1125
|
+
]).optional(),
|
|
1126
|
+
holdFrames: z$1.number().optional(),
|
|
1127
|
+
invert: z$1.boolean().optional(),
|
|
1128
|
+
stats: SignalStatsSchema.optional()
|
|
1129
|
+
}).passthrough();
|
|
1130
|
+
const SignalDataSchema = z$1.union([
|
|
1131
|
+
GeneratorSignalDataSchema,
|
|
1132
|
+
AudioExtractorSignalDataSchema,
|
|
1133
|
+
SignalMathDataSchema,
|
|
1134
|
+
SignalGateDataSchema,
|
|
1135
|
+
z$1.object({ type: z$1.string() }).passthrough()
|
|
1136
|
+
]);
|
|
1137
|
+
function serializeSignalData(data) {
|
|
1138
|
+
return JSON.stringify(data, (_key, value) => {
|
|
1139
|
+
if (value instanceof Float32Array) return Array.from(value);
|
|
1140
|
+
return value;
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
function deserializeSignalData(raw) {
|
|
1144
|
+
if (typeof raw === "string") try {
|
|
1145
|
+
return SignalDataSchema.parse(JSON.parse(raw));
|
|
1146
|
+
} catch {
|
|
1147
|
+
return { type: "unknown" };
|
|
1148
|
+
}
|
|
1149
|
+
return SignalDataSchema.parse(raw);
|
|
1150
|
+
}
|
|
1151
|
+
const OutputItemSchema = z$1.object({
|
|
1152
|
+
type: z$1.custom(),
|
|
1153
|
+
data: z$1.any(),
|
|
1154
|
+
outputHandleId: z$1.string().optional()
|
|
1155
|
+
});
|
|
1156
|
+
const createOutputItemSchema = (type, dataSchema) => {
|
|
1157
|
+
return z$1.object({
|
|
1158
|
+
type,
|
|
1159
|
+
data: dataSchema,
|
|
1160
|
+
outputHandleId: z$1.string().optional()
|
|
1161
|
+
});
|
|
1162
|
+
};
|
|
1163
|
+
const SingleOutputGenericSchema = (outputItemSchema) => z$1.object({
|
|
1164
|
+
selectedOutputIndex: z$1.literal(0),
|
|
1165
|
+
outputs: z$1.tuple([z$1.object({ items: z$1.tuple([outputItemSchema]) })])
|
|
1166
|
+
});
|
|
1167
|
+
const MultiOutputGenericSchema = (outputItemSchema) => z$1.object({
|
|
1168
|
+
selectedOutputIndex: z$1.number(),
|
|
1169
|
+
outputs: z$1.array(z$1.object({ items: z$1.array(outputItemSchema) })),
|
|
1170
|
+
sourceFingerprint: z$1.string().optional()
|
|
1171
|
+
});
|
|
1172
|
+
const AnyOutputUnionSchema = z$1.object({
|
|
1173
|
+
type: z$1.custom(),
|
|
1174
|
+
data: z$1.any(),
|
|
1175
|
+
outputHandleId: z$1.string().optional()
|
|
1176
|
+
});
|
|
1177
|
+
const NodeResultSchema = z$1.object({
|
|
1178
|
+
selectedOutputIndex: z$1.number(),
|
|
1179
|
+
outputs: z$1.array(z$1.object({ items: z$1.array(AnyOutputUnionSchema) }))
|
|
1180
|
+
});
|
|
1181
|
+
const ExportResultSchema = MultiOutputGenericSchema(AnyOutputUnionSchema);
|
|
1182
|
+
const AnimationTypeSchema = z.enum([
|
|
1183
|
+
"fade-in",
|
|
1184
|
+
"fade-out",
|
|
1185
|
+
"slide-in-left",
|
|
1186
|
+
"slide-in-right",
|
|
1187
|
+
"slide-in-top",
|
|
1188
|
+
"slide-in-bottom",
|
|
1189
|
+
"zoom-in",
|
|
1190
|
+
"zoom-out",
|
|
1191
|
+
"rotate-cw",
|
|
1192
|
+
"rotate-ccw",
|
|
1193
|
+
"bounce",
|
|
1194
|
+
"shake"
|
|
1195
|
+
]);
|
|
1196
|
+
const VideoAnimationSchema = z.object({
|
|
1197
|
+
id: z.string(),
|
|
1198
|
+
type: AnimationTypeSchema,
|
|
1199
|
+
value: z.number()
|
|
1200
|
+
});
|
|
1201
|
+
const VideoSpatialPropsSchema = z.object({
|
|
1202
|
+
x: z.number().default(0),
|
|
1203
|
+
y: z.number().default(0),
|
|
1204
|
+
width: z.number().optional(),
|
|
1205
|
+
height: z.number().optional(),
|
|
1206
|
+
rotation: z.number().default(0),
|
|
1207
|
+
scale: z.number().min(0).default(1),
|
|
1208
|
+
opacity: z.number().min(0).max(1).default(1),
|
|
1209
|
+
zIndex: z.number().int().optional(),
|
|
1210
|
+
blendMode: GlobalCompositeOperation$1.optional()
|
|
1211
|
+
});
|
|
1212
|
+
const VideoTimingPropsSchema = z.object({
|
|
1213
|
+
startFrame: z.number().int().min(0).default(0),
|
|
1214
|
+
durationInMS: z.number().int().min(1).optional(),
|
|
1215
|
+
trimStart: z.number().int().min(0).optional(),
|
|
1216
|
+
trimEnd: z.number().int().min(0).optional(),
|
|
1217
|
+
speed: z.number().min(.25).max(4).optional()
|
|
1218
|
+
});
|
|
1219
|
+
const VideoTextPropsSchema = z.object({
|
|
1220
|
+
text: z.string().optional(),
|
|
1221
|
+
fontSize: z.number().optional(),
|
|
1222
|
+
fontFamily: z.string().optional(),
|
|
1223
|
+
fontStyle: z.string().optional(),
|
|
1224
|
+
fontWeight: z.union([z.number(), z.string()]).optional(),
|
|
1225
|
+
fill: z.string().optional(),
|
|
1226
|
+
align: z.string().optional(),
|
|
1227
|
+
verticalAlign: z.string().optional(),
|
|
1228
|
+
letterSpacing: z.number().optional(),
|
|
1229
|
+
lineHeight: z.number().optional(),
|
|
1230
|
+
padding: z.number().optional(),
|
|
1231
|
+
stroke: z.string().optional(),
|
|
1232
|
+
strokeWidth: z.number().optional(),
|
|
1233
|
+
strokeAlign: z.enum([
|
|
1234
|
+
"inside",
|
|
1235
|
+
"center",
|
|
1236
|
+
"outside"
|
|
1237
|
+
]).optional(),
|
|
1238
|
+
textShadow: z.string().optional(),
|
|
1239
|
+
textBackgroundColor: z.string().optional(),
|
|
1240
|
+
shadows: z.array(z.object({
|
|
1241
|
+
color: z.string(),
|
|
1242
|
+
offset: z.object({
|
|
1243
|
+
x: z.number(),
|
|
1244
|
+
y: z.number()
|
|
1245
|
+
}).optional(),
|
|
1246
|
+
blurRadius: z.number().optional()
|
|
1247
|
+
})).optional(),
|
|
1248
|
+
textAnimation: z.any().optional(),
|
|
1249
|
+
bottomPadding: z.number().optional(),
|
|
1250
|
+
maxWidth: z.number().optional()
|
|
1251
|
+
});
|
|
1252
|
+
const VideoStylePropsSchema = z.object({
|
|
1253
|
+
backgroundColor: z.string().optional(),
|
|
1254
|
+
borderColor: z.string().optional(),
|
|
1255
|
+
borderWidth: z.number().optional(),
|
|
1256
|
+
strokeRadius: z.number().optional(),
|
|
1257
|
+
autoDimensions: z.boolean().optional()
|
|
1258
|
+
});
|
|
1259
|
+
const VideoLayerTypeSchema = z.enum([
|
|
1260
|
+
"Video",
|
|
1261
|
+
"Image",
|
|
1262
|
+
"Audio",
|
|
1263
|
+
"Text",
|
|
1264
|
+
"Caption",
|
|
1265
|
+
"SVG",
|
|
1266
|
+
"GIF",
|
|
1267
|
+
"Lottie"
|
|
1268
|
+
]);
|
|
1269
|
+
const BaseVideoLayerPropsSchema = VideoSpatialPropsSchema.merge(VideoTimingPropsSchema).merge(VideoTextPropsSchema).merge(VideoStylePropsSchema).extend({
|
|
1270
|
+
type: VideoLayerTypeSchema.optional(),
|
|
1271
|
+
animations: z.array(VideoAnimationSchema).optional()
|
|
1272
|
+
});
|
|
1273
|
+
const TransitionTypeSchema = z.enum([
|
|
1274
|
+
"none",
|
|
1275
|
+
"crossfade",
|
|
1276
|
+
"wipe-left",
|
|
1277
|
+
"wipe-right",
|
|
1278
|
+
"slide-up",
|
|
1279
|
+
"slide-down"
|
|
1280
|
+
]);
|
|
1281
|
+
const TransitionSchema = z.object({
|
|
1282
|
+
type: TransitionTypeSchema,
|
|
1283
|
+
durationFrames: z.number().min(1)
|
|
1284
|
+
});
|
|
1285
|
+
const MediaSourceSchema = z.object({ entity: FileAssetSchema.optional() });
|
|
1286
|
+
const MediaMetadataSchema = z.object({
|
|
1287
|
+
width: z.number().optional().nullable(),
|
|
1288
|
+
height: z.number().optional().nullable(),
|
|
1289
|
+
fps: z.number().optional().nullable(),
|
|
1290
|
+
durationMs: z.number().optional().nullable(),
|
|
1291
|
+
sampleRate: z.number().optional().nullable(),
|
|
1292
|
+
channels: z.number().optional().nullable(),
|
|
1293
|
+
bitDepth: z.number().optional().nullable(),
|
|
1294
|
+
audioCodec: z.string().optional().nullable(),
|
|
1295
|
+
audioBitrate: z.number().optional().nullable()
|
|
1296
|
+
});
|
|
1297
|
+
const TimelineSegmentSchema = z.object({
|
|
1298
|
+
startSec: z.number(),
|
|
1299
|
+
endSec: z.number().optional()
|
|
1300
|
+
});
|
|
1301
|
+
const TimelineTransformSchema = z.object({
|
|
1302
|
+
startFrame: z.number().optional(),
|
|
1303
|
+
segments: z.array(TimelineSegmentSchema).optional()
|
|
1304
|
+
});
|
|
1305
|
+
const BaseMediaOperationPropsSchema = z.object({
|
|
1306
|
+
volume: z.number().optional(),
|
|
1307
|
+
opacity: z.number().optional(),
|
|
1308
|
+
startFrame: z.number().optional(),
|
|
1309
|
+
timeline: TimelineTransformSchema.optional()
|
|
1310
|
+
});
|
|
1311
|
+
/** Original source file (leaf node) */
|
|
1312
|
+
const SourceOperationSchema = BaseMediaOperationPropsSchema.extend({
|
|
1313
|
+
op: z.literal("source"),
|
|
1314
|
+
source: MediaSourceSchema,
|
|
1315
|
+
sourceMeta: MediaMetadataSchema,
|
|
1316
|
+
dataType: z.enum(Object.values(DataTypeVal))
|
|
1317
|
+
});
|
|
1318
|
+
/** Text content (leaf node) */
|
|
1319
|
+
const TextOperationSchema = BaseMediaOperationPropsSchema.extend({
|
|
1320
|
+
op: z.literal("text"),
|
|
1321
|
+
text: z.string(),
|
|
1322
|
+
metadata: MediaMetadataSchema.optional(),
|
|
1323
|
+
dataType: z.enum(Object.values(DataTypeVal))
|
|
1324
|
+
}).passthrough();
|
|
1325
|
+
const MediaOperationSchema = z.union([
|
|
1326
|
+
SourceOperationSchema,
|
|
1327
|
+
TextOperationSchema,
|
|
1328
|
+
BaseMediaOperationPropsSchema.extend({
|
|
1329
|
+
op: z.string(),
|
|
1330
|
+
metadata: MediaMetadataSchema.optional(),
|
|
1331
|
+
dataType: z.enum(Object.values(DataTypeVal))
|
|
1332
|
+
}).passthrough()
|
|
1333
|
+
]);
|
|
1334
|
+
const VirtualMediaDataSchema = z.lazy(() => z.object({
|
|
1335
|
+
metadata: MediaMetadataSchema,
|
|
1336
|
+
operation: MediaOperationSchema,
|
|
1337
|
+
children: z.array(VirtualMediaDataSchema).default([])
|
|
1338
|
+
}));
|
|
1339
|
+
const ExtendedLayerSchema = BaseVideoLayerPropsSchema.extend({
|
|
1340
|
+
id: z.string(),
|
|
1341
|
+
name: z.string().optional(),
|
|
1342
|
+
type: VideoLayerTypeSchema,
|
|
1343
|
+
virtualMedia: VirtualMediaDataSchema.optional(),
|
|
1344
|
+
transitionIn: TransitionSchema.optional(),
|
|
1345
|
+
transitionOut: TransitionSchema.optional(),
|
|
1346
|
+
durationInMS: z.number(),
|
|
1347
|
+
maxDurationInMS: z.number().optional(),
|
|
1348
|
+
src: z.string().optional(),
|
|
1349
|
+
volume: z.number().default(1),
|
|
1350
|
+
inputHandleId: z.string().optional(),
|
|
1351
|
+
isPlaceholder: z.boolean().optional(),
|
|
1352
|
+
lockAspect: z.boolean().optional(),
|
|
1353
|
+
lockRatio: z.boolean().optional(),
|
|
1354
|
+
zIndex: z.number().optional()
|
|
1355
|
+
}).strict();
|
|
1356
|
+
const DEFAULT_FPS = 60;
|
|
1357
|
+
const DEFAULT_DURATION_MS = 3e3;
|
|
1358
|
+
const DEFAULT_MEDIA_DIMENSION = 1080;
|
|
1359
|
+
/** Default styling applied to new Text layers. */
|
|
1360
|
+
const TEXT_LAYER_DEFAULTS = {
|
|
1361
|
+
fontSize: 60,
|
|
1362
|
+
fontFamily: "Inter",
|
|
1363
|
+
fill: "#ffffff",
|
|
1364
|
+
fontStyle: "normal",
|
|
1365
|
+
align: "start",
|
|
1366
|
+
padding: 0
|
|
1367
|
+
};
|
|
1368
|
+
/** Default styling applied to new Caption layers. */
|
|
1369
|
+
const CAPTION_LAYER_DEFAULTS = {
|
|
1370
|
+
fontSize: 48,
|
|
1371
|
+
fontFamily: "Inter",
|
|
1372
|
+
fill: "#ffffff",
|
|
1373
|
+
align: "center",
|
|
1374
|
+
verticalAlign: "bottom",
|
|
1375
|
+
padding: 0,
|
|
1376
|
+
lineHeight: 1.1,
|
|
1377
|
+
textAnimation: {
|
|
1378
|
+
in: "none",
|
|
1379
|
+
out: "none",
|
|
1380
|
+
entranceMs: 0,
|
|
1381
|
+
exitMs: 0,
|
|
1382
|
+
kinetic: "none",
|
|
1383
|
+
applyBy: "word",
|
|
1384
|
+
smoothing: true
|
|
1385
|
+
},
|
|
1386
|
+
bottomPadding: 60
|
|
1387
|
+
};
|
|
1388
|
+
/**
|
|
1389
|
+
* Identify the actual leaf media type (Video, Image, etc) by walking down the tree.
|
|
1390
|
+
* This can differ from getMediaType() if an operation (like Ken Burns) upgrades
|
|
1391
|
+
* an Image to a Video.
|
|
1392
|
+
*/
|
|
1393
|
+
function getLeafMediaType(vv) {
|
|
1394
|
+
if (!vv) return void 0;
|
|
1395
|
+
if (vv.operation?.op === "source") return vv.operation.dataType;
|
|
1396
|
+
if (vv.children && vv.children.length > 0) return getLeafMediaType(vv.children[0]);
|
|
1397
|
+
return vv.operation.dataType;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Resolve the MIME type of the leaf source media.
|
|
1401
|
+
*/
|
|
1402
|
+
function resolveMediaMimeType(vv) {
|
|
1403
|
+
if (!vv) return void 0;
|
|
1404
|
+
if (vv.operation?.op === "source") {
|
|
1405
|
+
const op = vv.operation;
|
|
1406
|
+
if (op.mimeType) return op.mimeType;
|
|
1407
|
+
if (op.sourceMeta?.mimeType) return op.sourceMeta.mimeType;
|
|
1408
|
+
if (op.source?.entity?.mimeType) return op.source.entity.mimeType;
|
|
1409
|
+
const url = typeof op.source === "string" ? op.source : op.url ?? op.source?.url;
|
|
1410
|
+
if (typeof url === "string") {
|
|
1411
|
+
const ext = url.split(".").pop()?.toLowerCase();
|
|
1412
|
+
if (ext === "mp4") return "video/mp4";
|
|
1413
|
+
if (ext === "webm") return "video/webm";
|
|
1414
|
+
if (ext === "mov") return "video/quicktime";
|
|
1415
|
+
if (ext === "png") return "image/png";
|
|
1416
|
+
if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
|
|
1417
|
+
if (ext === "webp") return "image/webp";
|
|
1418
|
+
if (ext === "svg") return "image/svg+xml";
|
|
1419
|
+
if (ext === "mp3") return "audio/mpeg";
|
|
1420
|
+
if (ext === "wav") return "audio/wav";
|
|
1421
|
+
if (ext === "aac") return "audio/aac";
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (vv.children && vv.children.length > 0) return resolveMediaMimeType(vv.children[0]);
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Create a VirtualMediaData from a FileData source or Text.
|
|
1428
|
+
* Used by Import, VideoGen, and Text nodes to wrap concrete content.
|
|
1429
|
+
*/
|
|
1430
|
+
function createVirtualMedia(source, type = "Video") {
|
|
1431
|
+
if (source && typeof source === "object" && "operation" in source && "metadata" in source) return source;
|
|
1432
|
+
if (typeof source === "string") {
|
|
1433
|
+
const url = source;
|
|
1434
|
+
const sourceMeta$1 = {
|
|
1435
|
+
width: void 0,
|
|
1436
|
+
height: void 0,
|
|
1437
|
+
durationMs: type === "Image" || /\.(png|jpg|jpeg|webp|gif|svg)$/i.test(url) ? DEFAULT_DURATION_MS : void 0,
|
|
1438
|
+
fps: void 0
|
|
1439
|
+
};
|
|
1440
|
+
return {
|
|
1441
|
+
metadata: sourceMeta$1,
|
|
1442
|
+
operation: {
|
|
1443
|
+
op: "source",
|
|
1444
|
+
url,
|
|
1445
|
+
source: url,
|
|
1446
|
+
sourceMeta: sourceMeta$1,
|
|
1447
|
+
dataType: type
|
|
1448
|
+
},
|
|
1449
|
+
children: []
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
if (typeof source === "object" && source !== null && ("url" in source || "path" in source)) {
|
|
1453
|
+
const desc = source;
|
|
1454
|
+
const url = desc.url ?? desc.path;
|
|
1455
|
+
const isImage = type === "Image" || /\.(png|jpg|jpeg|webp|gif|svg)$/i.test(url);
|
|
1456
|
+
const durationMs = desc.durationMs ?? (isImage ? DEFAULT_DURATION_MS : void 0);
|
|
1457
|
+
const sourceMeta$1 = {
|
|
1458
|
+
width: desc.width,
|
|
1459
|
+
height: desc.height,
|
|
1460
|
+
durationMs,
|
|
1461
|
+
fps: desc.fps,
|
|
1462
|
+
sampleRate: desc.sampleRate,
|
|
1463
|
+
channels: desc.channels,
|
|
1464
|
+
bitDepth: desc.bitDepth,
|
|
1465
|
+
audioCodec: desc.audioCodec,
|
|
1466
|
+
audioBitrate: desc.audioBitrate
|
|
1467
|
+
};
|
|
1468
|
+
return {
|
|
1469
|
+
metadata: sourceMeta$1,
|
|
1470
|
+
operation: {
|
|
1471
|
+
op: "source",
|
|
1472
|
+
url,
|
|
1473
|
+
source: url,
|
|
1474
|
+
sourceMeta: sourceMeta$1,
|
|
1475
|
+
dataType: type
|
|
1476
|
+
},
|
|
1477
|
+
children: []
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const sourceMeta = getFileDataMetadata(source) || {
|
|
1481
|
+
width: void 0,
|
|
1482
|
+
height: void 0,
|
|
1483
|
+
durationMs: void 0,
|
|
1484
|
+
fps: void 0
|
|
1485
|
+
};
|
|
1486
|
+
return {
|
|
1487
|
+
metadata: sourceMeta,
|
|
1488
|
+
operation: {
|
|
1489
|
+
op: "source",
|
|
1490
|
+
source: { entity: source?.entity },
|
|
1491
|
+
sourceMeta,
|
|
1492
|
+
dataType: type
|
|
1493
|
+
},
|
|
1494
|
+
children: []
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Identify if a VirtualMediaData node is intended to be Video, Audio, Image, or Text.
|
|
1499
|
+
*/
|
|
1500
|
+
function getMediaType(vv) {
|
|
1501
|
+
if (!vv) throw new Error("No media data provided");
|
|
1502
|
+
return vv.operation.dataType;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Append an operation to an existing VirtualMediaData (recursive).
|
|
1506
|
+
* This creates a new parent node wrapping the current one as a child.
|
|
1507
|
+
*/
|
|
1508
|
+
function appendOperation(vv, operation) {
|
|
1509
|
+
return {
|
|
1510
|
+
metadata: computeNextMetadata(getActiveMediaMetadata(vv) ?? {}, operation),
|
|
1511
|
+
operation,
|
|
1512
|
+
children: [vv]
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Helper to compute the metadata of the NEXT node in the operator tree.
|
|
1517
|
+
*/
|
|
1518
|
+
function computeNextMetadata(baseMeta, op) {
|
|
1519
|
+
let { width, height, durationMs, fps, sampleRate, channels, bitDepth, audioCodec, audioBitrate } = baseMeta;
|
|
1520
|
+
if ("metadata" in op && op.metadata) {
|
|
1521
|
+
width = op.metadata.width ?? width;
|
|
1522
|
+
height = op.metadata.height ?? height;
|
|
1523
|
+
durationMs = op.metadata.durationMs ?? durationMs;
|
|
1524
|
+
fps = op.metadata.fps ?? fps;
|
|
1525
|
+
sampleRate = op.metadata.sampleRate ?? sampleRate;
|
|
1526
|
+
channels = op.metadata.channels ?? channels;
|
|
1527
|
+
bitDepth = op.metadata.bitDepth ?? bitDepth;
|
|
1528
|
+
audioCodec = op.metadata.audioCodec ?? audioCodec;
|
|
1529
|
+
audioBitrate = op.metadata.audioBitrate ?? audioBitrate;
|
|
1530
|
+
}
|
|
1531
|
+
if (op.dataType === "Audio" || op.dataType === "Text" || op.dataType === "Caption") {
|
|
1532
|
+
width = void 0;
|
|
1533
|
+
height = void 0;
|
|
1534
|
+
fps = void 0;
|
|
1535
|
+
}
|
|
1536
|
+
return {
|
|
1537
|
+
width,
|
|
1538
|
+
height,
|
|
1539
|
+
durationMs,
|
|
1540
|
+
fps,
|
|
1541
|
+
sampleRate,
|
|
1542
|
+
channels,
|
|
1543
|
+
bitDepth,
|
|
1544
|
+
audioCodec,
|
|
1545
|
+
audioBitrate
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function getFileDataMetadata(filedata) {
|
|
1549
|
+
if (!filedata) return null;
|
|
1550
|
+
const { entity } = filedata;
|
|
1551
|
+
const width = entity?.width;
|
|
1552
|
+
const height = entity?.height;
|
|
1553
|
+
let durationMs = entity?.duration;
|
|
1554
|
+
const fps = entity?.fps;
|
|
1555
|
+
const sampleRate = entity?.sampleRate;
|
|
1556
|
+
const channels = entity?.channels;
|
|
1557
|
+
const bitDepth = entity?.bitDepth;
|
|
1558
|
+
const audioCodec = entity?.audioCodec;
|
|
1559
|
+
const audioBitrate = entity?.audioBitrate;
|
|
1560
|
+
if (durationMs === void 0 || durationMs === null) {
|
|
1561
|
+
const mimeType = entity?.mimeType ?? "";
|
|
1562
|
+
if (mimeType.startsWith("image/") || mimeType === "image/svg+xml") durationMs = DEFAULT_DURATION_MS;
|
|
1563
|
+
}
|
|
1564
|
+
if (width === void 0 && height === void 0 && durationMs === void 0 && sampleRate === void 0 && channels === void 0) return null;
|
|
1565
|
+
return {
|
|
1566
|
+
width,
|
|
1567
|
+
height,
|
|
1568
|
+
durationMs: durationMs === 0 ? void 0 : durationMs,
|
|
1569
|
+
fps,
|
|
1570
|
+
sampleRate,
|
|
1571
|
+
channels,
|
|
1572
|
+
bitDepth,
|
|
1573
|
+
audioCodec,
|
|
1574
|
+
audioBitrate
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Get the active metadata from the VirtualMediaData node.
|
|
1579
|
+
* Simply returns the metadata property of the node.
|
|
1580
|
+
* Supports legacy formats (sourceMeta) and extracts from source if needed.
|
|
1581
|
+
*/
|
|
1582
|
+
function getActiveMediaMetadata(vv) {
|
|
1583
|
+
if (!vv) return null;
|
|
1584
|
+
let width = vv.metadata?.width;
|
|
1585
|
+
let height = vv.metadata?.height;
|
|
1586
|
+
let durationMs = vv.metadata?.durationMs;
|
|
1587
|
+
let fps = vv.metadata?.fps;
|
|
1588
|
+
let sampleRate = vv.metadata?.sampleRate;
|
|
1589
|
+
let channels = vv.metadata?.channels;
|
|
1590
|
+
let bitDepth = vv.metadata?.bitDepth;
|
|
1591
|
+
let audioCodec = vv.metadata?.audioCodec;
|
|
1592
|
+
let audioBitrate = vv.metadata?.audioBitrate;
|
|
1593
|
+
const op = vv.operation;
|
|
1594
|
+
if (op?.op === "source") {
|
|
1595
|
+
const sm = op.sourceMeta || {};
|
|
1596
|
+
const fdm = op.source ? getFileDataMetadata(op.source) : null;
|
|
1597
|
+
width = width ?? sm.width ?? fdm?.width;
|
|
1598
|
+
height = height ?? sm.height ?? fdm?.height;
|
|
1599
|
+
durationMs = durationMs ?? sm.durationMs ?? fdm?.durationMs;
|
|
1600
|
+
fps = fps ?? sm.fps ?? fdm?.fps;
|
|
1601
|
+
sampleRate = sampleRate ?? sm.sampleRate ?? fdm?.sampleRate;
|
|
1602
|
+
channels = channels ?? sm.channels ?? fdm?.channels;
|
|
1603
|
+
bitDepth = bitDepth ?? sm.bitDepth ?? fdm?.bitDepth;
|
|
1604
|
+
audioCodec = audioCodec ?? sm.audioCodec ?? fdm?.audioCodec;
|
|
1605
|
+
audioBitrate = audioBitrate ?? sm.audioBitrate ?? fdm?.audioBitrate;
|
|
1606
|
+
}
|
|
1607
|
+
if (width === void 0 && height === void 0 && durationMs === void 0 && sampleRate === void 0 && channels === void 0) return null;
|
|
1608
|
+
return {
|
|
1609
|
+
width,
|
|
1610
|
+
height,
|
|
1611
|
+
durationMs: durationMs === 0 ? void 0 : durationMs,
|
|
1612
|
+
fps,
|
|
1613
|
+
sampleRate,
|
|
1614
|
+
channels,
|
|
1615
|
+
bitDepth,
|
|
1616
|
+
audioCodec,
|
|
1617
|
+
audioBitrate
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Compute the dimensions and offsets for rendering a cropped video.
|
|
1622
|
+
* Traverses the VirtualMediaData tree to find nested crops and source dimensions.
|
|
1623
|
+
*/
|
|
1624
|
+
function computeVideoCropRenderProps(virtualMedia) {
|
|
1625
|
+
let totalOffsetX = 0;
|
|
1626
|
+
let totalOffsetY = 0;
|
|
1627
|
+
let hasCrop = false;
|
|
1628
|
+
let sourceMetaFound = null;
|
|
1629
|
+
let currentNode = virtualMedia;
|
|
1630
|
+
while (currentNode) {
|
|
1631
|
+
const op = currentNode.operation;
|
|
1632
|
+
if (op.op === "Crop") {
|
|
1633
|
+
hasCrop = true;
|
|
1634
|
+
const anyOp = op;
|
|
1635
|
+
const inputMeta = currentNode.children[0]?.metadata;
|
|
1636
|
+
const inputW = inputMeta?.width ?? 0;
|
|
1637
|
+
const inputH = inputMeta?.height ?? 0;
|
|
1638
|
+
if (inputW > 0 && inputH > 0) {
|
|
1639
|
+
const cropLeftPx = anyOp.leftPercentage / 100 * inputW;
|
|
1640
|
+
const cropTopPx = anyOp.topPercentage / 100 * inputH;
|
|
1641
|
+
totalOffsetX += cropLeftPx;
|
|
1642
|
+
totalOffsetY += cropTopPx;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (op.op === "source") sourceMetaFound = op.sourceMeta ?? currentNode.metadata;
|
|
1646
|
+
currentNode = currentNode.children[0];
|
|
1647
|
+
}
|
|
1648
|
+
if (!hasCrop || !sourceMetaFound) return null;
|
|
1649
|
+
const sourceW = sourceMetaFound.width ?? 1;
|
|
1650
|
+
const sourceH = sourceMetaFound.height ?? 1;
|
|
1651
|
+
return {
|
|
1652
|
+
videoNaturalWidth: sourceW,
|
|
1653
|
+
videoNaturalHeight: sourceH,
|
|
1654
|
+
cropTranslatePercentageX: -(totalOffsetX / sourceW) * 100,
|
|
1655
|
+
cropTranslatePercentageY: -(totalOffsetY / sourceH) * 100
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Performs a deep comparison between two values to determine if they are equivalent.
|
|
1660
|
+
* Handles primitives, objects, arrays, Dates, and NaNs.
|
|
1661
|
+
*/
|
|
1662
|
+
function isEqual(a, b) {
|
|
1663
|
+
if (a === b) return true;
|
|
1664
|
+
if (typeof a === "number" && typeof b === "number" && Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
1665
|
+
if (a && b && typeof a === "object" && typeof b === "object") {
|
|
1666
|
+
if (a.constructor !== b.constructor) return false;
|
|
1667
|
+
if (Array.isArray(a)) {
|
|
1668
|
+
if (a.length !== b.length) return false;
|
|
1669
|
+
for (let i = 0; i < a.length; i++) if (!isEqual(a[i], b[i])) return false;
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
if (a instanceof Date) return a.getTime() === b.getTime();
|
|
1673
|
+
if (a instanceof RegExp) return a.toString() === b.toString();
|
|
1674
|
+
const keys = Object.keys(a);
|
|
1675
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
1676
|
+
for (const key of keys) {
|
|
1677
|
+
if (!Object.hasOwn(b, key)) return false;
|
|
1678
|
+
if (!isEqual(a[key], b[key])) return false;
|
|
1679
|
+
}
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Gets a stable object for hashing/fingerprinting by sorting keys and skipping volatile fields.
|
|
1686
|
+
*/
|
|
1687
|
+
function getStableData(data) {
|
|
1688
|
+
if (typeof data !== "object" || data === null) return data;
|
|
1689
|
+
if (Array.isArray(data)) return data.map(getStableData);
|
|
1690
|
+
const result = {};
|
|
1691
|
+
const keys = Object.keys(data).sort();
|
|
1692
|
+
for (const key of keys) {
|
|
1693
|
+
const val = data[key];
|
|
1694
|
+
if (key === "createdAt" || key === "updatedAt" || key === "dataUrl" || key === "filePath") continue;
|
|
1695
|
+
if (typeof val === "string" && val.startsWith("blob:")) continue;
|
|
1696
|
+
if (key === "entity" && val && typeof val === "object" && val.id) {
|
|
1697
|
+
result.entity = { id: val.id };
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
result[key] = getStableData(val);
|
|
1701
|
+
}
|
|
1702
|
+
return result;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Generates a fingerprint for the given data.
|
|
1706
|
+
*/
|
|
1707
|
+
function getFingerprint(data) {
|
|
1708
|
+
const stableData = getStableData(data);
|
|
1709
|
+
const str = JSON.stringify(stableData);
|
|
1710
|
+
if (str.length === 0) return "0";
|
|
1711
|
+
const getHash = (seed) => {
|
|
1712
|
+
let h1 = 3735928559 ^ seed;
|
|
1713
|
+
let h2 = 1103547991 ^ seed;
|
|
1714
|
+
for (let i = 0; i < str.length; i++) {
|
|
1715
|
+
const ch = str.charCodeAt(i);
|
|
1716
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
1717
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
1718
|
+
}
|
|
1719
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822519);
|
|
1720
|
+
h1 = Math.imul(h1 ^ h1 >>> 13, 3266489917);
|
|
1721
|
+
h1 = h1 ^ h1 >>> 16;
|
|
1722
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822519);
|
|
1723
|
+
h2 = Math.imul(h2 ^ h2 >>> 13, 3266489917);
|
|
1724
|
+
h2 = h2 ^ h2 >>> 16;
|
|
1725
|
+
return (h1 >>> 0).toString(36).padStart(7, "0") + (h2 >>> 0).toString(36).padStart(7, "0");
|
|
1726
|
+
};
|
|
1727
|
+
return getHash(0) + getHash(1);
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Generates a version-aware fingerprint for render caching.
|
|
1731
|
+
* Includes app version so cached renders are invalidated on version bumps.
|
|
1732
|
+
*/
|
|
1733
|
+
function getRenderFingerprint(data, appVersion, extra) {
|
|
1734
|
+
return getFingerprint({
|
|
1735
|
+
__v: appVersion,
|
|
1736
|
+
data,
|
|
1737
|
+
...extra
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Checks if data is VirtualMediaData.
|
|
1742
|
+
*/
|
|
1743
|
+
function isVirtualMediaData(data) {
|
|
1744
|
+
return typeof data === "object" && data !== null && "metadata" in data && "operation" in data && "children" in data;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Checks if the virtual media is just a raw source without any significant operations
|
|
1748
|
+
* that would require a rendering step (like transformations, trimming, or effects).
|
|
1749
|
+
*/
|
|
1750
|
+
function hasOnlySingleSource(data) {
|
|
1751
|
+
if (!isVirtualMediaData(data)) return false;
|
|
1752
|
+
if (data.operation.op === "source") {
|
|
1753
|
+
if (data.children && data.children.length > 0) return false;
|
|
1754
|
+
const op = data.operation;
|
|
1755
|
+
return !(op.volume !== void 0 && op.volume !== 1 || op.opacity !== void 0 && op.opacity !== 1 || op.startFrame !== void 0 && op.startFrame !== 0 || op.timeline && (op.timeline.startFrame !== void 0 && op.timeline.startFrame !== 0 || op.timeline.segments && op.timeline.segments.length > 0));
|
|
1756
|
+
}
|
|
1757
|
+
if (data.operation.op === "compose") return false;
|
|
1758
|
+
if (data.operation.op === "layer" && data.children.length === 1) {
|
|
1759
|
+
const op = data.operation;
|
|
1760
|
+
if (!(op.x !== void 0 && op.x !== 0 || op.y !== void 0 && op.y !== 0 || op.rotation !== void 0 && op.rotation !== 0 || op.scale !== void 0 && op.scale !== 1 || op.opacity !== void 0 && op.opacity !== 1 || op.volume !== void 0 && op.volume !== 1 || op.zIndex !== void 0 || op.blendMode !== void 0 || op.filters !== void 0 || op.transitionIn !== void 0 || op.transitionOut !== void 0 || op.animations && op.animations.length > 0 || op.trimStart !== void 0 || op.trimEnd !== void 0 || op.speed !== void 0 || op.backgroundColor !== void 0 || op.borderColor !== void 0 || op.borderWidth !== void 0 || op.strokeRadius !== void 0)) return hasOnlySingleSource(data.children[0]);
|
|
1761
|
+
}
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Finds the source asset in a virtual media tree.
|
|
1766
|
+
*/
|
|
1767
|
+
function findSourceAsset(vm) {
|
|
1768
|
+
if (vm.operation.op === "source") return vm.operation.source;
|
|
1769
|
+
if (vm.children && vm.children.length > 0) return findSourceAsset(vm.children[0]);
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Checks if a cached node result can be reused based on source fingerprint.
|
|
1774
|
+
* Returns the valid cached result, or null if the cache is stale.
|
|
1775
|
+
*/
|
|
1776
|
+
function checkNodeResultCache(currentResult, input) {
|
|
1777
|
+
if (!currentResult?.sourceFingerprint) return null;
|
|
1778
|
+
const inputFingerprint = getFingerprint(input);
|
|
1779
|
+
if (currentResult.sourceFingerprint !== inputFingerprint) return null;
|
|
1780
|
+
return currentResult;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Creates a fresh result with a source fingerprint stamp.
|
|
1784
|
+
*/
|
|
1785
|
+
function stampResult(result, input) {
|
|
1786
|
+
return {
|
|
1787
|
+
...result,
|
|
1788
|
+
sourceFingerprint: getFingerprint(input)
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Attempts to parse the width and height of an SVG from its buffer.
|
|
1793
|
+
* Falls back to viewBox ratios if explicitly defined sizes are missing.
|
|
1794
|
+
*/
|
|
1795
|
+
function extractSvgDimensions(buffer) {
|
|
1796
|
+
try {
|
|
1797
|
+
const match = buffer.toString("utf-8").match(/<svg[^>]*>/i);
|
|
1798
|
+
if (!match) return null;
|
|
1799
|
+
const svgTag = match[0];
|
|
1800
|
+
const getAttr = (name) => {
|
|
1801
|
+
const attrMatch = svgTag.match(new RegExp(`${name}=["']([^"']+)["']`, "i"));
|
|
1802
|
+
if (!attrMatch) return null;
|
|
1803
|
+
const val = parseFloat(attrMatch[1].replace(/[a-z%]/gi, ""));
|
|
1804
|
+
return isNaN(val) ? null : val;
|
|
1805
|
+
};
|
|
1806
|
+
let w = getAttr("width");
|
|
1807
|
+
let h = getAttr("height");
|
|
1808
|
+
const viewBoxMatch = svgTag.match(/viewBox=["']([^"']+)["']/i);
|
|
1809
|
+
if (viewBoxMatch) {
|
|
1810
|
+
const parts = viewBoxMatch[1].split(/[\s,]+/).map(parseFloat);
|
|
1811
|
+
if (parts.length >= 4 && !isNaN(parts[2]) && !isNaN(parts[3])) {
|
|
1812
|
+
const vbW = parts[2];
|
|
1813
|
+
const vbH = parts[3];
|
|
1814
|
+
if (w == null && h == null) {
|
|
1815
|
+
w = vbW;
|
|
1816
|
+
h = vbH;
|
|
1817
|
+
} else if (w != null && h == null) h = w * (vbH / vbW);
|
|
1818
|
+
else if (h != null && w == null) w = h * (vbW / vbH);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (w != null && h != null && w > 0 && h > 0) return {
|
|
1822
|
+
w,
|
|
1823
|
+
h
|
|
1824
|
+
};
|
|
1825
|
+
return null;
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
console.error("Error parsing SVG dimensions", error);
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
//#endregion
|
|
1833
|
+
//#region ../node-sdk/dist/renderer.mjs
|
|
1834
|
+
function defineRenderer(plugin) {
|
|
1835
|
+
return Object.freeze(plugin);
|
|
1836
|
+
}
|
|
1837
|
+
const GLOBAL_WEBGPU_REGISTRY_KEY = Symbol.for("gitframes.webgpuRegistry");
|
|
1838
|
+
var WebGPURegistry = class {
|
|
1839
|
+
renderers = /* @__PURE__ */ new Map();
|
|
1840
|
+
register(op, renderer) {
|
|
1841
|
+
this.renderers.set(op, renderer);
|
|
1842
|
+
}
|
|
1843
|
+
get(op) {
|
|
1844
|
+
return this.renderers.get(op);
|
|
1845
|
+
}
|
|
1846
|
+
has(op) {
|
|
1847
|
+
return this.renderers.has(op);
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
const webgpuRegistry = (() => {
|
|
1851
|
+
const g = globalThis;
|
|
1852
|
+
if (!g[GLOBAL_WEBGPU_REGISTRY_KEY]) g[GLOBAL_WEBGPU_REGISTRY_KEY] = new WebGPURegistry();
|
|
1853
|
+
return g[GLOBAL_WEBGPU_REGISTRY_KEY];
|
|
1854
|
+
})();
|
|
1855
|
+
function registerWebGPURenderer(op, renderer) {
|
|
1856
|
+
webgpuRegistry.register(op, renderer);
|
|
1857
|
+
}
|
|
1858
|
+
var AudioProcessorRegistry = class {
|
|
1859
|
+
processors = /* @__PURE__ */ new Map();
|
|
1860
|
+
register(opType, processor) {
|
|
1861
|
+
this.processors.set(opType, processor);
|
|
1862
|
+
}
|
|
1863
|
+
get(opType) {
|
|
1864
|
+
return this.processors.get(opType) || null;
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
const GLOBAL_AUDIO_KEY = Symbol.for("gitframes.audioRegistry");
|
|
1868
|
+
const audioRegistry = (() => {
|
|
1869
|
+
const g = globalThis;
|
|
1870
|
+
if (!g[GLOBAL_AUDIO_KEY]) g[GLOBAL_AUDIO_KEY] = new AudioProcessorRegistry();
|
|
1871
|
+
return g[GLOBAL_AUDIO_KEY];
|
|
1872
|
+
})();
|
|
1873
|
+
|
|
1874
|
+
//#endregion
|
|
1875
|
+
//#region ../node-sdk/dist/index.mjs
|
|
1876
|
+
var ConfigBuilder = class ConfigBuilder$1 {
|
|
1877
|
+
constructor(shape = {}, options = { strict: true }) {
|
|
1878
|
+
this.shape = shape;
|
|
1879
|
+
this.options = options;
|
|
1880
|
+
}
|
|
1881
|
+
field(key, schema, options) {
|
|
1882
|
+
const withMeta = options ? schema.meta({ ...options }) : schema;
|
|
1883
|
+
return new ConfigBuilder$1({
|
|
1884
|
+
...this.shape,
|
|
1885
|
+
[key]: withMeta
|
|
1886
|
+
}, this.options);
|
|
1887
|
+
}
|
|
1888
|
+
build() {
|
|
1889
|
+
const base = z.object(this.shape);
|
|
1890
|
+
return { schema: this.options.strict !== false ? base.strict() : base };
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
function configBuilder(options = { strict: true }) {
|
|
1894
|
+
return new ConfigBuilder({}, options);
|
|
1895
|
+
}
|
|
1896
|
+
const COMPOSITE_OPERATIONS = [
|
|
1897
|
+
"source-over",
|
|
1898
|
+
"source-in",
|
|
1899
|
+
"source-out",
|
|
1900
|
+
"source-atop",
|
|
1901
|
+
"destination-over",
|
|
1902
|
+
"destination-in",
|
|
1903
|
+
"destination-out",
|
|
1904
|
+
"destination-atop",
|
|
1905
|
+
"lighter",
|
|
1906
|
+
"copy",
|
|
1907
|
+
"xor",
|
|
1908
|
+
"multiply",
|
|
1909
|
+
"screen",
|
|
1910
|
+
"overlay",
|
|
1911
|
+
"darken",
|
|
1912
|
+
"lighten",
|
|
1913
|
+
"color-dodge",
|
|
1914
|
+
"color-burn",
|
|
1915
|
+
"hard-light",
|
|
1916
|
+
"soft-light",
|
|
1917
|
+
"difference",
|
|
1918
|
+
"exclusion",
|
|
1919
|
+
"hue",
|
|
1920
|
+
"saturation",
|
|
1921
|
+
"color",
|
|
1922
|
+
"luminosity"
|
|
1923
|
+
];
|
|
1924
|
+
const GlobalCompositeOperation = z.enum(COMPOSITE_OPERATIONS);
|
|
1925
|
+
const ColorSchema = z.string().optional();
|
|
1926
|
+
const PercentageSchema = z.number().min(0).max(100);
|
|
1927
|
+
const DimensionSchema = z.number().min(0).optional();
|
|
1928
|
+
const AnimationSchema = z.object({ animations: z.array(z.object({
|
|
1929
|
+
id: z.string(),
|
|
1930
|
+
type: z.enum([
|
|
1931
|
+
"fade-in",
|
|
1932
|
+
"fade-out",
|
|
1933
|
+
"slide-in-left",
|
|
1934
|
+
"slide-in-right",
|
|
1935
|
+
"slide-in-top",
|
|
1936
|
+
"slide-in-bottom",
|
|
1937
|
+
"zoom-in",
|
|
1938
|
+
"zoom-out",
|
|
1939
|
+
"rotate-cw",
|
|
1940
|
+
"rotate-ccw",
|
|
1941
|
+
"bounce",
|
|
1942
|
+
"shake"
|
|
1943
|
+
]),
|
|
1944
|
+
value: z.number()
|
|
1945
|
+
})).optional() });
|
|
1946
|
+
const StrokeSchema = z.object({
|
|
1947
|
+
stroke: ColorSchema,
|
|
1948
|
+
strokeWidth: z.number().min(0).optional(),
|
|
1949
|
+
strokeAlign: z.enum([
|
|
1950
|
+
"inside",
|
|
1951
|
+
"center",
|
|
1952
|
+
"outside"
|
|
1953
|
+
]).optional().default("inside"),
|
|
1954
|
+
strokeRadius: z.number().min(0).optional()
|
|
1955
|
+
});
|
|
1956
|
+
const ImageResultSchema = SingleOutputGenericSchema(createOutputItemSchema(z.literal("Image"), FileDataSchema));
|
|
1957
|
+
const VideoResultSchema = SingleOutputGenericSchema(createOutputItemSchema(z.literal("Video"), VirtualMediaDataSchema));
|
|
1958
|
+
const AudioResultSchema = SingleOutputGenericSchema(createOutputItemSchema(z.literal("Audio"), VirtualMediaDataSchema));
|
|
1959
|
+
const TextResultSchema = SingleOutputGenericSchema(createOutputItemSchema(z.literal("Text"), z.string()));
|
|
1960
|
+
const NumberResultSchema = SingleOutputGenericSchema(createOutputItemSchema(z.literal("Number"), z.number()));
|
|
1961
|
+
const SignalResultSchema = SingleOutputGenericSchema(createOutputItemSchema(z.literal("Signal"), z.unknown()));
|
|
1962
|
+
|
|
1963
|
+
//#endregion
|
|
1964
|
+
//#region ../renderers/dist/index.mjs
|
|
1965
|
+
const mapBlendModeToSkia = (mode) => {
|
|
1966
|
+
if (!mode) return "srcOver";
|
|
1967
|
+
switch (mode) {
|
|
1968
|
+
case "source-over": return "srcOver";
|
|
1969
|
+
case "source-in": return "srcIn";
|
|
1970
|
+
case "source-out": return "srcOut";
|
|
1971
|
+
case "source-atop": return "srcATop";
|
|
1972
|
+
case "destination-over": return "dstOver";
|
|
1973
|
+
case "destination-in": return "dstIn";
|
|
1974
|
+
case "destination-out": return "dstOut";
|
|
1975
|
+
case "destination-atop": return "dstATop";
|
|
1976
|
+
case "copy": return "src";
|
|
1977
|
+
case "xor": return "xor";
|
|
1978
|
+
case "multiply": return "multiply";
|
|
1979
|
+
case "screen": return "screen";
|
|
1980
|
+
case "overlay": return "overlay";
|
|
1981
|
+
case "darken": return "darken";
|
|
1982
|
+
case "lighten":
|
|
1983
|
+
case "lighter": return "lighten";
|
|
1984
|
+
case "color-dodge": return "colorDodge";
|
|
1985
|
+
case "color-burn": return "colorBurn";
|
|
1986
|
+
case "hard-light": return "hardLight";
|
|
1987
|
+
case "soft-light": return "softLight";
|
|
1988
|
+
case "difference": return "difference";
|
|
1989
|
+
case "exclusion": return "exclusion";
|
|
1990
|
+
case "hue": return "hue";
|
|
1991
|
+
case "saturation": return "saturation";
|
|
1992
|
+
case "color": return "color";
|
|
1993
|
+
case "luminosity": return "luminosity";
|
|
1994
|
+
default: return "srcOver";
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
const requiresIsolation = (mode) => {
|
|
1998
|
+
return mode === "destination-in" || mode === "source-in";
|
|
1999
|
+
};
|
|
2000
|
+
function computeStrokeRect(w, h, cornerRadius, strokeWidth, align = "outside") {
|
|
2001
|
+
if (align === "inside") {
|
|
2002
|
+
const half = strokeWidth / 2;
|
|
2003
|
+
return {
|
|
2004
|
+
x: half,
|
|
2005
|
+
y: half,
|
|
2006
|
+
width: Math.max(0, w - strokeWidth),
|
|
2007
|
+
height: Math.max(0, h - strokeWidth),
|
|
2008
|
+
radius: cornerRadius === 0 ? 0 : Math.max(0, cornerRadius - half)
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
if (align === "outside") {
|
|
2012
|
+
const half = strokeWidth / 2;
|
|
2013
|
+
return {
|
|
2014
|
+
x: -half,
|
|
2015
|
+
y: -half,
|
|
2016
|
+
width: w + strokeWidth,
|
|
2017
|
+
height: h + strokeWidth,
|
|
2018
|
+
radius: cornerRadius === 0 ? 0 : cornerRadius + half
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
return {
|
|
2022
|
+
x: 0,
|
|
2023
|
+
y: 0,
|
|
2024
|
+
width: w,
|
|
2025
|
+
height: h,
|
|
2026
|
+
radius: cornerRadius
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
const { cbrt, sqrt, PI: π } = Math;
|
|
2030
|
+
const x2t = (x, a, b, c, d) => {
|
|
2031
|
+
const q = a + b * x;
|
|
2032
|
+
const s = q ** 2 + c;
|
|
2033
|
+
if (s > 0) {
|
|
2034
|
+
const root = sqrt(s);
|
|
2035
|
+
return cbrt(q + root) + cbrt(q - root) - d;
|
|
2036
|
+
}
|
|
2037
|
+
const l = cbrt(sqrt(q * q - s));
|
|
2038
|
+
const angle = q ? Math.atan(sqrt(-s) / q) : -π / 2;
|
|
2039
|
+
let φ;
|
|
2040
|
+
if (b < 0) φ = (q > 0 ? 2 * π : π) - angle;
|
|
2041
|
+
else if (d < 0) φ = (q > 0 ? 2 * π : -3 * π) + angle;
|
|
2042
|
+
else φ = (q > 0 ? 0 : π) + angle;
|
|
2043
|
+
return 2 * l * Math.cos(φ / 3) - d;
|
|
2044
|
+
};
|
|
2045
|
+
const Y = (t, ay, by, cy) => ((ay * t + 3 * by) * t + cy) * t;
|
|
2046
|
+
const linearEasing = (x) => x;
|
|
2047
|
+
function bezier(mX1, mY1, mX2, mY2) {
|
|
2048
|
+
if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) throw new Error("bezier x values must be in [0, 1] range");
|
|
2049
|
+
if (mX1 === mY1 && mX2 === mY2) return linearEasing;
|
|
2050
|
+
const a = 6 * (3 * mX1 - 3 * mX2 + 1);
|
|
2051
|
+
const b = 6 * (mX2 - 2 * mX1);
|
|
2052
|
+
const c = 3 * mX1;
|
|
2053
|
+
const a2 = a * a;
|
|
2054
|
+
const b2 = b * b;
|
|
2055
|
+
const d = b / a;
|
|
2056
|
+
const e = 3 * b * c / a2 - b2 * b / (a2 * a);
|
|
2057
|
+
const w1 = 2 * c / a - b2 / a2;
|
|
2058
|
+
const w = w1 * w1 * w1;
|
|
2059
|
+
const o = 3 / a;
|
|
2060
|
+
const ay = 3 * mY1 - 3 * mY2 + 1;
|
|
2061
|
+
const by = mY2 - 2 * mY1;
|
|
2062
|
+
const cy = 3 * mY1;
|
|
2063
|
+
const X2T = a ? x2t : linearEasing;
|
|
2064
|
+
return function BezierEasing(x) {
|
|
2065
|
+
if (x === 0 || x === 1) return x;
|
|
2066
|
+
return Y(X2T(x, e, o, w, d), ay, by, cy);
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const Easing = {
|
|
2070
|
+
linear: linearEasing,
|
|
2071
|
+
ease: bezier(.25, .1, .25, 1),
|
|
2072
|
+
quad: (t) => t * t,
|
|
2073
|
+
cubic: (t) => t * t * t,
|
|
2074
|
+
poly: (n) => (t) => t ** n,
|
|
2075
|
+
sin: (t) => 1 - Math.cos(t * Math.PI / 2),
|
|
2076
|
+
circle: (t) => 1 - Math.sqrt(1 - t * t),
|
|
2077
|
+
exp: (t) => 2 ** (10 * (t - 1)),
|
|
2078
|
+
elastic: (bounciness = 1) => {
|
|
2079
|
+
const p = bounciness * Math.PI;
|
|
2080
|
+
return (t) => 1 - Math.cos(t * Math.PI / 2) ** 3 * Math.cos(t * p);
|
|
2081
|
+
},
|
|
2082
|
+
back: (s = 1.70158) => (t) => t * t * ((s + 1) * t - s),
|
|
2083
|
+
bounce: (t) => {
|
|
2084
|
+
if (t < 1 / 2.75) return 7.5625 * t * t;
|
|
2085
|
+
if (t < 2 / 2.75) {
|
|
2086
|
+
const t2$1 = t - 1.5 / 2.75;
|
|
2087
|
+
return 7.5625 * t2$1 * t2$1 + .75;
|
|
2088
|
+
}
|
|
2089
|
+
if (t < 2.5 / 2.75) {
|
|
2090
|
+
const t2$1 = t - 2.25 / 2.75;
|
|
2091
|
+
return 7.5625 * t2$1 * t2$1 + .9375;
|
|
2092
|
+
}
|
|
2093
|
+
const t2 = t - 2.625 / 2.75;
|
|
2094
|
+
return 7.5625 * t2 * t2 + .984375;
|
|
2095
|
+
},
|
|
2096
|
+
bezier,
|
|
2097
|
+
in: (fn) => fn,
|
|
2098
|
+
out: (fn) => (t) => 1 - fn(1 - t),
|
|
2099
|
+
inOut: (fn) => (t) => t < .5 ? fn(t * 2) / 2 : 1 - fn((1 - t) * 2) / 2
|
|
2100
|
+
};
|
|
2101
|
+
function interpolate(value, inputRange, outputRange, options) {
|
|
2102
|
+
const extrapolateLeft = options?.extrapolateLeft ?? "extend";
|
|
2103
|
+
const extrapolateRight = options?.extrapolateRight ?? "extend";
|
|
2104
|
+
if (inputRange.length !== outputRange.length) throw new Error("inputRange and outputRange must have the same length");
|
|
2105
|
+
if (inputRange.length < 2) throw new Error("inputRange must have at least 2 elements");
|
|
2106
|
+
const firstInput = inputRange[0];
|
|
2107
|
+
const lastInput = inputRange[inputRange.length - 1];
|
|
2108
|
+
if (value < firstInput) {
|
|
2109
|
+
if (extrapolateLeft === "clamp") return outputRange[0];
|
|
2110
|
+
if (extrapolateLeft === "identity") return value;
|
|
2111
|
+
}
|
|
2112
|
+
if (value > lastInput) {
|
|
2113
|
+
if (extrapolateRight === "clamp") return outputRange[outputRange.length - 1];
|
|
2114
|
+
if (extrapolateRight === "identity") return value;
|
|
2115
|
+
}
|
|
2116
|
+
let i = 1;
|
|
2117
|
+
for (; i < inputRange.length - 1; i++) if (value < inputRange[i]) break;
|
|
2118
|
+
const x0 = inputRange[i - 1];
|
|
2119
|
+
const x1 = inputRange[i];
|
|
2120
|
+
const y0 = outputRange[i - 1];
|
|
2121
|
+
const y1 = outputRange[i];
|
|
2122
|
+
if (x0 === x1) return y0;
|
|
2123
|
+
let progress = (value - x0) / (x1 - x0);
|
|
2124
|
+
if (options?.easing) progress = options.easing(progress);
|
|
2125
|
+
return y0 + progress * (y1 - y0);
|
|
2126
|
+
}
|
|
2127
|
+
const Vector3Math = {
|
|
2128
|
+
create(x = 0, y = 0, z$2 = 0) {
|
|
2129
|
+
return [
|
|
2130
|
+
x,
|
|
2131
|
+
y,
|
|
2132
|
+
z$2
|
|
2133
|
+
];
|
|
2134
|
+
},
|
|
2135
|
+
copy(v) {
|
|
2136
|
+
return [
|
|
2137
|
+
v[0],
|
|
2138
|
+
v[1],
|
|
2139
|
+
v[2]
|
|
2140
|
+
];
|
|
2141
|
+
},
|
|
2142
|
+
set(out, x, y, z$2) {
|
|
2143
|
+
out[0] = x;
|
|
2144
|
+
out[1] = y;
|
|
2145
|
+
out[2] = z$2;
|
|
2146
|
+
return out;
|
|
2147
|
+
},
|
|
2148
|
+
add(a, b) {
|
|
2149
|
+
return [
|
|
2150
|
+
a[0] + b[0],
|
|
2151
|
+
a[1] + b[1],
|
|
2152
|
+
a[2] + b[2]
|
|
2153
|
+
];
|
|
2154
|
+
},
|
|
2155
|
+
subtract(a, b) {
|
|
2156
|
+
return [
|
|
2157
|
+
a[0] - b[0],
|
|
2158
|
+
a[1] - b[1],
|
|
2159
|
+
a[2] - b[2]
|
|
2160
|
+
];
|
|
2161
|
+
},
|
|
2162
|
+
multiplyScalar(v, s) {
|
|
2163
|
+
return [
|
|
2164
|
+
v[0] * s,
|
|
2165
|
+
v[1] * s,
|
|
2166
|
+
v[2] * s
|
|
2167
|
+
];
|
|
2168
|
+
},
|
|
2169
|
+
dot(a, b) {
|
|
2170
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
2171
|
+
},
|
|
2172
|
+
cross(a, b) {
|
|
2173
|
+
return [
|
|
2174
|
+
a[1] * b[2] - a[2] * b[1],
|
|
2175
|
+
a[2] * b[0] - a[0] * b[2],
|
|
2176
|
+
a[0] * b[1] - a[1] * b[0]
|
|
2177
|
+
];
|
|
2178
|
+
},
|
|
2179
|
+
lengthSq(v) {
|
|
2180
|
+
return v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
|
|
2181
|
+
},
|
|
2182
|
+
length(v) {
|
|
2183
|
+
return Math.hypot(v[0], v[1], v[2]);
|
|
2184
|
+
},
|
|
2185
|
+
distance(a, b) {
|
|
2186
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2187
|
+
},
|
|
2188
|
+
normalize(v) {
|
|
2189
|
+
const len = Math.hypot(v[0], v[1], v[2]);
|
|
2190
|
+
if (len === 0) return [
|
|
2191
|
+
0,
|
|
2192
|
+
0,
|
|
2193
|
+
0
|
|
2194
|
+
];
|
|
2195
|
+
const inv = 1 / len;
|
|
2196
|
+
return [
|
|
2197
|
+
v[0] * inv,
|
|
2198
|
+
v[1] * inv,
|
|
2199
|
+
v[2] * inv
|
|
2200
|
+
];
|
|
2201
|
+
},
|
|
2202
|
+
lerp(a, b, t) {
|
|
2203
|
+
return [
|
|
2204
|
+
a[0] + (b[0] - a[0]) * t,
|
|
2205
|
+
a[1] + (b[1] - a[1]) * t,
|
|
2206
|
+
a[2] + (b[2] - a[2]) * t
|
|
2207
|
+
];
|
|
2208
|
+
},
|
|
2209
|
+
equals(a, b, epsilon = 1e-6) {
|
|
2210
|
+
return Math.abs(a[0] - b[0]) <= epsilon && Math.abs(a[1] - b[1]) <= epsilon && Math.abs(a[2] - b[2]) <= epsilon;
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
/**
|
|
2214
|
+
* Column-major 4x4 matrix mathematics for WebGPU 3D rendering and camera projections.
|
|
2215
|
+
* Memory layout matches WGSL `mat4x4<f32>`:
|
|
2216
|
+
* [ 0, 4, 8, 12 ]
|
|
2217
|
+
* [ 1, 5, 9, 13 ]
|
|
2218
|
+
* [ 2, 6, 10, 14 ]
|
|
2219
|
+
* [ 3, 7, 11, 15 ]
|
|
2220
|
+
*/
|
|
2221
|
+
const Matrix4Math = {
|
|
2222
|
+
identity() {
|
|
2223
|
+
return [
|
|
2224
|
+
1,
|
|
2225
|
+
0,
|
|
2226
|
+
0,
|
|
2227
|
+
0,
|
|
2228
|
+
0,
|
|
2229
|
+
1,
|
|
2230
|
+
0,
|
|
2231
|
+
0,
|
|
2232
|
+
0,
|
|
2233
|
+
0,
|
|
2234
|
+
1,
|
|
2235
|
+
0,
|
|
2236
|
+
0,
|
|
2237
|
+
0,
|
|
2238
|
+
0,
|
|
2239
|
+
1
|
|
2240
|
+
];
|
|
2241
|
+
},
|
|
2242
|
+
copy(m) {
|
|
2243
|
+
return [...m];
|
|
2244
|
+
},
|
|
2245
|
+
multiply(a, b) {
|
|
2246
|
+
const out = new Array(16);
|
|
2247
|
+
for (let row = 0; row < 4; row++) for (let col = 0; col < 4; col++) out[col * 4 + row] = a[0 + row] * b[col * 4 + 0] + a[4 + row] * b[col * 4 + 1] + a[8 + row] * b[col * 4 + 2] + a[12 + row] * b[col * 4 + 3];
|
|
2248
|
+
return out;
|
|
2249
|
+
},
|
|
2250
|
+
translate(tx, ty, tz = 0) {
|
|
2251
|
+
return [
|
|
2252
|
+
1,
|
|
2253
|
+
0,
|
|
2254
|
+
0,
|
|
2255
|
+
0,
|
|
2256
|
+
0,
|
|
2257
|
+
1,
|
|
2258
|
+
0,
|
|
2259
|
+
0,
|
|
2260
|
+
0,
|
|
2261
|
+
0,
|
|
2262
|
+
1,
|
|
2263
|
+
0,
|
|
2264
|
+
tx,
|
|
2265
|
+
ty,
|
|
2266
|
+
tz,
|
|
2267
|
+
1
|
|
2268
|
+
];
|
|
2269
|
+
},
|
|
2270
|
+
scale(sx, sy, sz = 1) {
|
|
2271
|
+
return [
|
|
2272
|
+
sx,
|
|
2273
|
+
0,
|
|
2274
|
+
0,
|
|
2275
|
+
0,
|
|
2276
|
+
0,
|
|
2277
|
+
sy,
|
|
2278
|
+
0,
|
|
2279
|
+
0,
|
|
2280
|
+
0,
|
|
2281
|
+
0,
|
|
2282
|
+
sz,
|
|
2283
|
+
0,
|
|
2284
|
+
0,
|
|
2285
|
+
0,
|
|
2286
|
+
0,
|
|
2287
|
+
1
|
|
2288
|
+
];
|
|
2289
|
+
},
|
|
2290
|
+
rotateX(rad) {
|
|
2291
|
+
const c = Math.cos(rad);
|
|
2292
|
+
const s = Math.sin(rad);
|
|
2293
|
+
return [
|
|
2294
|
+
1,
|
|
2295
|
+
0,
|
|
2296
|
+
0,
|
|
2297
|
+
0,
|
|
2298
|
+
0,
|
|
2299
|
+
c,
|
|
2300
|
+
s,
|
|
2301
|
+
0,
|
|
2302
|
+
0,
|
|
2303
|
+
-s,
|
|
2304
|
+
c,
|
|
2305
|
+
0,
|
|
2306
|
+
0,
|
|
2307
|
+
0,
|
|
2308
|
+
0,
|
|
2309
|
+
1
|
|
2310
|
+
];
|
|
2311
|
+
},
|
|
2312
|
+
rotateY(rad) {
|
|
2313
|
+
const c = Math.cos(rad);
|
|
2314
|
+
const s = Math.sin(rad);
|
|
2315
|
+
return [
|
|
2316
|
+
c,
|
|
2317
|
+
0,
|
|
2318
|
+
-s,
|
|
2319
|
+
0,
|
|
2320
|
+
0,
|
|
2321
|
+
1,
|
|
2322
|
+
0,
|
|
2323
|
+
0,
|
|
2324
|
+
s,
|
|
2325
|
+
0,
|
|
2326
|
+
c,
|
|
2327
|
+
0,
|
|
2328
|
+
0,
|
|
2329
|
+
0,
|
|
2330
|
+
0,
|
|
2331
|
+
1
|
|
2332
|
+
];
|
|
2333
|
+
},
|
|
2334
|
+
rotateZ(rad) {
|
|
2335
|
+
const c = Math.cos(rad);
|
|
2336
|
+
const s = Math.sin(rad);
|
|
2337
|
+
return [
|
|
2338
|
+
c,
|
|
2339
|
+
s,
|
|
2340
|
+
0,
|
|
2341
|
+
0,
|
|
2342
|
+
-s,
|
|
2343
|
+
c,
|
|
2344
|
+
0,
|
|
2345
|
+
0,
|
|
2346
|
+
0,
|
|
2347
|
+
0,
|
|
2348
|
+
1,
|
|
2349
|
+
0,
|
|
2350
|
+
0,
|
|
2351
|
+
0,
|
|
2352
|
+
0,
|
|
2353
|
+
1
|
|
2354
|
+
];
|
|
2355
|
+
},
|
|
2356
|
+
rotateAxis(axis, rad) {
|
|
2357
|
+
const norm = Vector3Math.normalize(axis);
|
|
2358
|
+
const x = norm[0];
|
|
2359
|
+
const y = norm[1];
|
|
2360
|
+
const z$2 = norm[2];
|
|
2361
|
+
const c = Math.cos(rad);
|
|
2362
|
+
const s = Math.sin(rad);
|
|
2363
|
+
const t = 1 - c;
|
|
2364
|
+
return [
|
|
2365
|
+
t * x * x + c,
|
|
2366
|
+
t * x * y + s * z$2,
|
|
2367
|
+
t * x * z$2 - s * y,
|
|
2368
|
+
0,
|
|
2369
|
+
t * x * y - s * z$2,
|
|
2370
|
+
t * y * y + c,
|
|
2371
|
+
t * y * z$2 + s * x,
|
|
2372
|
+
0,
|
|
2373
|
+
t * x * z$2 + s * y,
|
|
2374
|
+
t * y * z$2 - s * x,
|
|
2375
|
+
t * z$2 * z$2 + c,
|
|
2376
|
+
0,
|
|
2377
|
+
0,
|
|
2378
|
+
0,
|
|
2379
|
+
0,
|
|
2380
|
+
1
|
|
2381
|
+
];
|
|
2382
|
+
},
|
|
2383
|
+
lookAt(eye, target, up = [
|
|
2384
|
+
0,
|
|
2385
|
+
-1,
|
|
2386
|
+
0
|
|
2387
|
+
]) {
|
|
2388
|
+
let f = Vector3Math.subtract(target, eye);
|
|
2389
|
+
if (Vector3Math.lengthSq(f) === 0) f = [
|
|
2390
|
+
0,
|
|
2391
|
+
0,
|
|
2392
|
+
1
|
|
2393
|
+
];
|
|
2394
|
+
else f = Vector3Math.normalize(f);
|
|
2395
|
+
let r = Vector3Math.cross(f, up);
|
|
2396
|
+
if (Vector3Math.lengthSq(r) === 0) r = [
|
|
2397
|
+
1,
|
|
2398
|
+
0,
|
|
2399
|
+
0
|
|
2400
|
+
];
|
|
2401
|
+
else r = Vector3Math.normalize(r);
|
|
2402
|
+
const u = Vector3Math.cross(r, f);
|
|
2403
|
+
return [
|
|
2404
|
+
r[0],
|
|
2405
|
+
u[0],
|
|
2406
|
+
f[0],
|
|
2407
|
+
0,
|
|
2408
|
+
r[1],
|
|
2409
|
+
u[1],
|
|
2410
|
+
f[1],
|
|
2411
|
+
0,
|
|
2412
|
+
r[2],
|
|
2413
|
+
u[2],
|
|
2414
|
+
f[2],
|
|
2415
|
+
0,
|
|
2416
|
+
-Vector3Math.dot(r, eye),
|
|
2417
|
+
-Vector3Math.dot(u, eye),
|
|
2418
|
+
-Vector3Math.dot(f, eye),
|
|
2419
|
+
1
|
|
2420
|
+
];
|
|
2421
|
+
},
|
|
2422
|
+
perspective(fovYRad, aspect, near, far) {
|
|
2423
|
+
const f = 1 / Math.tan(fovYRad / 2);
|
|
2424
|
+
const rangeInv = 1 / (far - near);
|
|
2425
|
+
return [
|
|
2426
|
+
f / aspect,
|
|
2427
|
+
0,
|
|
2428
|
+
0,
|
|
2429
|
+
0,
|
|
2430
|
+
0,
|
|
2431
|
+
f,
|
|
2432
|
+
0,
|
|
2433
|
+
0,
|
|
2434
|
+
0,
|
|
2435
|
+
0,
|
|
2436
|
+
far * rangeInv,
|
|
2437
|
+
1,
|
|
2438
|
+
0,
|
|
2439
|
+
0,
|
|
2440
|
+
-near * far * rangeInv,
|
|
2441
|
+
0
|
|
2442
|
+
];
|
|
2443
|
+
},
|
|
2444
|
+
orthographic(left, right, bottom, top, near, far) {
|
|
2445
|
+
const lr = 1 / (right - left);
|
|
2446
|
+
const bt = 1 / (top - bottom);
|
|
2447
|
+
const nf = 1 / (far - near);
|
|
2448
|
+
return [
|
|
2449
|
+
2 * lr,
|
|
2450
|
+
0,
|
|
2451
|
+
0,
|
|
2452
|
+
0,
|
|
2453
|
+
0,
|
|
2454
|
+
2 * bt,
|
|
2455
|
+
0,
|
|
2456
|
+
0,
|
|
2457
|
+
0,
|
|
2458
|
+
0,
|
|
2459
|
+
nf,
|
|
2460
|
+
0,
|
|
2461
|
+
-(right + left) * lr,
|
|
2462
|
+
-(top + bottom) * bt,
|
|
2463
|
+
-near * nf,
|
|
2464
|
+
1
|
|
2465
|
+
];
|
|
2466
|
+
},
|
|
2467
|
+
invert(m) {
|
|
2468
|
+
const inv = new Array(16);
|
|
2469
|
+
inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
|
|
2470
|
+
inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10];
|
|
2471
|
+
inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9];
|
|
2472
|
+
inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9];
|
|
2473
|
+
inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10];
|
|
2474
|
+
inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10];
|
|
2475
|
+
inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9];
|
|
2476
|
+
inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9];
|
|
2477
|
+
inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6];
|
|
2478
|
+
inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6];
|
|
2479
|
+
inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5];
|
|
2480
|
+
inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5];
|
|
2481
|
+
inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6];
|
|
2482
|
+
inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6];
|
|
2483
|
+
inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
|
|
2484
|
+
inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
|
|
2485
|
+
let det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
|
|
2486
|
+
if (Math.abs(det) < 1e-12) return null;
|
|
2487
|
+
det = 1 / det;
|
|
2488
|
+
for (let i = 0; i < 16; i++) inv[i] *= det;
|
|
2489
|
+
return inv;
|
|
2490
|
+
},
|
|
2491
|
+
transpose(m) {
|
|
2492
|
+
return [
|
|
2493
|
+
m[0],
|
|
2494
|
+
m[4],
|
|
2495
|
+
m[8],
|
|
2496
|
+
m[12],
|
|
2497
|
+
m[1],
|
|
2498
|
+
m[5],
|
|
2499
|
+
m[9],
|
|
2500
|
+
m[13],
|
|
2501
|
+
m[2],
|
|
2502
|
+
m[6],
|
|
2503
|
+
m[10],
|
|
2504
|
+
m[14],
|
|
2505
|
+
m[3],
|
|
2506
|
+
m[7],
|
|
2507
|
+
m[11],
|
|
2508
|
+
m[15]
|
|
2509
|
+
];
|
|
2510
|
+
},
|
|
2511
|
+
projectPoint(m, p) {
|
|
2512
|
+
const x = p[0];
|
|
2513
|
+
const y = p[1];
|
|
2514
|
+
const z$2 = p[2];
|
|
2515
|
+
const X = m[0] * x + m[4] * y + m[8] * z$2 + m[12];
|
|
2516
|
+
const Y$1 = m[1] * x + m[5] * y + m[9] * z$2 + m[13];
|
|
2517
|
+
const Z = m[2] * x + m[6] * y + m[10] * z$2 + m[14];
|
|
2518
|
+
const W = m[3] * x + m[7] * y + m[11] * z$2 + m[15];
|
|
2519
|
+
const wInv = Math.abs(W) > 1e-6 ? 1 / W : 1;
|
|
2520
|
+
return [
|
|
2521
|
+
X * wInv,
|
|
2522
|
+
Y$1 * wInv,
|
|
2523
|
+
Z * wInv
|
|
2524
|
+
];
|
|
2525
|
+
},
|
|
2526
|
+
projectRectangle(m, width, height) {
|
|
2527
|
+
return [
|
|
2528
|
+
Matrix4Math.projectPoint(m, [
|
|
2529
|
+
0,
|
|
2530
|
+
0,
|
|
2531
|
+
0
|
|
2532
|
+
]),
|
|
2533
|
+
Matrix4Math.projectPoint(m, [
|
|
2534
|
+
width,
|
|
2535
|
+
0,
|
|
2536
|
+
0
|
|
2537
|
+
]),
|
|
2538
|
+
Matrix4Math.projectPoint(m, [
|
|
2539
|
+
width,
|
|
2540
|
+
height,
|
|
2541
|
+
0
|
|
2542
|
+
]),
|
|
2543
|
+
Matrix4Math.projectPoint(m, [
|
|
2544
|
+
0,
|
|
2545
|
+
height,
|
|
2546
|
+
0
|
|
2547
|
+
])
|
|
2548
|
+
];
|
|
2549
|
+
},
|
|
2550
|
+
toFloat32Array(m) {
|
|
2551
|
+
return new Float32Array(m);
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
/**
|
|
2555
|
+
* WebGPU 3D Lighting & Material Shading engine.
|
|
2556
|
+
* Supports Ambient, Directional, Point, and Spot light sources,
|
|
2557
|
+
* normal matrix computation for non-uniform surface scaling,
|
|
2558
|
+
* and binary uniform buffer packing for WGSL shaders.
|
|
2559
|
+
*/
|
|
2560
|
+
const MAX_LIGHTS = 8;
|
|
2561
|
+
const LIGHT_UNIFORM_SIZE_BYTES = 544;
|
|
2562
|
+
/**
|
|
2563
|
+
* Parses CSS / hex / rgb color strings, arrays, or color objects into normalized linear [r, g, b] floats in [0, 1].
|
|
2564
|
+
*/
|
|
2565
|
+
function parseColorToRgb(color) {
|
|
2566
|
+
if (!color) return [
|
|
2567
|
+
1,
|
|
2568
|
+
1,
|
|
2569
|
+
1
|
|
2570
|
+
];
|
|
2571
|
+
if (Array.isArray(color)) {
|
|
2572
|
+
const r = color[0] ?? 1;
|
|
2573
|
+
const g = color[1] ?? 1;
|
|
2574
|
+
const b = color[2] ?? 1;
|
|
2575
|
+
return [
|
|
2576
|
+
r > 1 ? r / 255 : r,
|
|
2577
|
+
g > 1 ? g / 255 : g,
|
|
2578
|
+
b > 1 ? b / 255 : b
|
|
2579
|
+
];
|
|
2580
|
+
}
|
|
2581
|
+
if (typeof color === "object" && color !== null) {
|
|
2582
|
+
const r = color.r ?? 1;
|
|
2583
|
+
const g = color.g ?? 1;
|
|
2584
|
+
const b = color.b ?? 1;
|
|
2585
|
+
return [
|
|
2586
|
+
r > 1 ? r / 255 : r,
|
|
2587
|
+
g > 1 ? g / 255 : g,
|
|
2588
|
+
b > 1 ? b / 255 : b
|
|
2589
|
+
];
|
|
2590
|
+
}
|
|
2591
|
+
if (typeof color !== "string") return [
|
|
2592
|
+
1,
|
|
2593
|
+
1,
|
|
2594
|
+
1
|
|
2595
|
+
];
|
|
2596
|
+
const str = color.trim().toLowerCase();
|
|
2597
|
+
if (str.startsWith("#")) {
|
|
2598
|
+
const hex = str.slice(1);
|
|
2599
|
+
if (hex.length === 3) return [
|
|
2600
|
+
Number.parseInt(hex[0] + hex[0], 16) / 255,
|
|
2601
|
+
Number.parseInt(hex[1] + hex[1], 16) / 255,
|
|
2602
|
+
Number.parseInt(hex[2] + hex[2], 16) / 255
|
|
2603
|
+
];
|
|
2604
|
+
if (hex.length >= 6) return [
|
|
2605
|
+
Number.parseInt(hex.slice(0, 2), 16) / 255,
|
|
2606
|
+
Number.parseInt(hex.slice(2, 4), 16) / 255,
|
|
2607
|
+
Number.parseInt(hex.slice(4, 6), 16) / 255
|
|
2608
|
+
];
|
|
2609
|
+
}
|
|
2610
|
+
const rgbMatch = str.match(/rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/);
|
|
2611
|
+
if (rgbMatch) {
|
|
2612
|
+
const r = Number.parseFloat(rgbMatch[1]) / 255;
|
|
2613
|
+
const g = Number.parseFloat(rgbMatch[2]) / 255;
|
|
2614
|
+
const b = Number.parseFloat(rgbMatch[3]) / 255;
|
|
2615
|
+
return [
|
|
2616
|
+
Math.max(0, Math.min(1, r)),
|
|
2617
|
+
Math.max(0, Math.min(1, g)),
|
|
2618
|
+
Math.max(0, Math.min(1, b))
|
|
2619
|
+
];
|
|
2620
|
+
}
|
|
2621
|
+
const hslMatch = str.match(/hsla?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?/);
|
|
2622
|
+
if (hslMatch) return hslToRgb(Number.parseFloat(hslMatch[1]) / 360, Number.parseFloat(hslMatch[2]) / 100, Number.parseFloat(hslMatch[3]) / 100);
|
|
2623
|
+
return [
|
|
2624
|
+
1,
|
|
2625
|
+
1,
|
|
2626
|
+
1
|
|
2627
|
+
];
|
|
2628
|
+
}
|
|
2629
|
+
function hslToRgb(h, s, l) {
|
|
2630
|
+
if (s === 0) return [
|
|
2631
|
+
l,
|
|
2632
|
+
l,
|
|
2633
|
+
l
|
|
2634
|
+
];
|
|
2635
|
+
const hue2rgb = (p$1, q$1, t) => {
|
|
2636
|
+
let tt = t;
|
|
2637
|
+
if (tt < 0) tt += 1;
|
|
2638
|
+
if (tt > 1) tt -= 1;
|
|
2639
|
+
if (tt < 1 / 6) return p$1 + (q$1 - p$1) * 6 * tt;
|
|
2640
|
+
if (tt < 1 / 2) return q$1;
|
|
2641
|
+
if (tt < 2 / 3) return p$1 + (q$1 - p$1) * (2 / 3 - tt) * 6;
|
|
2642
|
+
return p$1;
|
|
2643
|
+
};
|
|
2644
|
+
const q = l < .5 ? l * (1 + s) : l + s - l * s;
|
|
2645
|
+
const p = 2 * l - q;
|
|
2646
|
+
return [
|
|
2647
|
+
hue2rgb(p, q, h + 1 / 3),
|
|
2648
|
+
hue2rgb(p, q, h),
|
|
2649
|
+
hue2rgb(p, q, h - 1 / 3)
|
|
2650
|
+
];
|
|
2651
|
+
}
|
|
2652
|
+
const PlaybackContext = createContext({
|
|
2653
|
+
frame: 0,
|
|
2654
|
+
fps: 24,
|
|
2655
|
+
isPlaying: false
|
|
2656
|
+
});
|
|
2657
|
+
const PlaybackProvider = PlaybackContext.Provider;
|
|
2658
|
+
const useCurrentFrame = () => {
|
|
2659
|
+
return useContext(PlaybackContext).frame;
|
|
2660
|
+
};
|
|
2661
|
+
const useVideoConfig = () => {
|
|
2662
|
+
return {
|
|
2663
|
+
fps: useContext(PlaybackContext).fps,
|
|
2664
|
+
width: 1920,
|
|
2665
|
+
height: 1080
|
|
2666
|
+
};
|
|
2667
|
+
};
|
|
2668
|
+
const defaultSpringConfig = {
|
|
2669
|
+
damping: 10,
|
|
2670
|
+
mass: 1,
|
|
2671
|
+
stiffness: 100,
|
|
2672
|
+
overshootClamping: false
|
|
2673
|
+
};
|
|
2674
|
+
const advanceCache = {};
|
|
2675
|
+
function advance({ animation, now, config }) {
|
|
2676
|
+
const { toValue, lastTimestamp, current, velocity } = animation;
|
|
2677
|
+
const deltaTime = Math.min(now - lastTimestamp, 64);
|
|
2678
|
+
if (config.damping <= 0) throw new Error("Spring damping must be greater than 0, otherwise the spring() animation will never end, causing an infinite loop.");
|
|
2679
|
+
if (config.stiffness <= 0) throw new Error("Spring stiffness must be greater than 0.");
|
|
2680
|
+
if (config.mass <= 0) throw new Error("Spring mass must be greater than 0.");
|
|
2681
|
+
const c = config.damping;
|
|
2682
|
+
const m = config.mass;
|
|
2683
|
+
const k = config.stiffness;
|
|
2684
|
+
const cacheKey = [
|
|
2685
|
+
toValue,
|
|
2686
|
+
lastTimestamp,
|
|
2687
|
+
current,
|
|
2688
|
+
velocity,
|
|
2689
|
+
c,
|
|
2690
|
+
m,
|
|
2691
|
+
k,
|
|
2692
|
+
now
|
|
2693
|
+
].join("-");
|
|
2694
|
+
if (advanceCache[cacheKey]) return advanceCache[cacheKey];
|
|
2695
|
+
const v0 = -velocity;
|
|
2696
|
+
const x0 = toValue - current;
|
|
2697
|
+
const zeta = c / (2 * Math.sqrt(k * m));
|
|
2698
|
+
const omega0 = Math.sqrt(k / m);
|
|
2699
|
+
const omega1 = omega0 * Math.sqrt(1 - zeta ** 2);
|
|
2700
|
+
const t = deltaTime / 1e3;
|
|
2701
|
+
const sin1 = Math.sin(omega1 * t);
|
|
2702
|
+
const cos1 = Math.cos(omega1 * t);
|
|
2703
|
+
const underDampedEnvelope = Math.exp(-zeta * omega0 * t);
|
|
2704
|
+
const underDampedFrag1 = underDampedEnvelope * (sin1 * ((v0 + zeta * omega0 * x0) / omega1) + x0 * cos1);
|
|
2705
|
+
const underDampedPosition = toValue - underDampedFrag1;
|
|
2706
|
+
const underDampedVelocity = zeta * omega0 * underDampedFrag1 - underDampedEnvelope * (cos1 * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin1);
|
|
2707
|
+
const criticallyDampedEnvelope = Math.exp(-omega0 * t);
|
|
2708
|
+
const criticallyDampedPosition = toValue - criticallyDampedEnvelope * (x0 + (v0 + omega0 * x0) * t);
|
|
2709
|
+
const criticallyDampedVelocity = criticallyDampedEnvelope * (v0 * (t * omega0 - 1) + t * x0 * omega0 * omega0);
|
|
2710
|
+
const animationNode = {
|
|
2711
|
+
toValue,
|
|
2712
|
+
prevPosition: current,
|
|
2713
|
+
lastTimestamp: now,
|
|
2714
|
+
current: zeta < 1 ? underDampedPosition : criticallyDampedPosition,
|
|
2715
|
+
velocity: zeta < 1 ? underDampedVelocity : criticallyDampedVelocity
|
|
2716
|
+
};
|
|
2717
|
+
advanceCache[cacheKey] = animationNode;
|
|
2718
|
+
return animationNode;
|
|
2719
|
+
}
|
|
2720
|
+
const calculationCache = {};
|
|
2721
|
+
function springCalculation({ frame, fps, config = {} }) {
|
|
2722
|
+
const from = 0;
|
|
2723
|
+
const to = 1;
|
|
2724
|
+
const cacheKey = [
|
|
2725
|
+
frame,
|
|
2726
|
+
fps,
|
|
2727
|
+
config.damping,
|
|
2728
|
+
config.mass,
|
|
2729
|
+
config.overshootClamping,
|
|
2730
|
+
config.stiffness
|
|
2731
|
+
].join("-");
|
|
2732
|
+
if (calculationCache[cacheKey]) return calculationCache[cacheKey];
|
|
2733
|
+
let animation = {
|
|
2734
|
+
lastTimestamp: 0,
|
|
2735
|
+
current: from,
|
|
2736
|
+
toValue: to,
|
|
2737
|
+
velocity: 0,
|
|
2738
|
+
prevPosition: 0
|
|
2739
|
+
};
|
|
2740
|
+
const frameClamped = Math.max(0, frame);
|
|
2741
|
+
const unevenRest = frameClamped % 1;
|
|
2742
|
+
for (let f = 0; f <= Math.floor(frameClamped); f++) {
|
|
2743
|
+
if (f === Math.floor(frameClamped)) f += unevenRest;
|
|
2744
|
+
const time = f / fps * 1e3;
|
|
2745
|
+
animation = advance({
|
|
2746
|
+
animation,
|
|
2747
|
+
now: time,
|
|
2748
|
+
config: {
|
|
2749
|
+
...defaultSpringConfig,
|
|
2750
|
+
...config
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
calculationCache[cacheKey] = animation;
|
|
2755
|
+
return animation;
|
|
2756
|
+
}
|
|
2757
|
+
const measureCache = /* @__PURE__ */ new Map();
|
|
2758
|
+
function measureSpring({ fps, config = {}, threshold = .005 }) {
|
|
2759
|
+
if (typeof threshold !== "number") throw new TypeError(`threshold must be a number, got ${threshold}`);
|
|
2760
|
+
if (threshold === 0) return Infinity;
|
|
2761
|
+
if (threshold === 1) return 0;
|
|
2762
|
+
if (Number.isNaN(threshold)) throw new TypeError("Threshold is NaN");
|
|
2763
|
+
if (!Number.isFinite(threshold)) throw new TypeError("Threshold is not finite");
|
|
2764
|
+
if (threshold < 0) throw new TypeError("Threshold is below 0");
|
|
2765
|
+
const cacheKey = [
|
|
2766
|
+
fps,
|
|
2767
|
+
config.damping,
|
|
2768
|
+
config.mass,
|
|
2769
|
+
config.overshootClamping,
|
|
2770
|
+
config.stiffness,
|
|
2771
|
+
threshold
|
|
2772
|
+
].join("-");
|
|
2773
|
+
if (measureCache.has(cacheKey)) return measureCache.get(cacheKey);
|
|
2774
|
+
let frame = 0;
|
|
2775
|
+
let finishedFrame = 0;
|
|
2776
|
+
const calc = () => {
|
|
2777
|
+
return springCalculation({
|
|
2778
|
+
fps,
|
|
2779
|
+
frame,
|
|
2780
|
+
config
|
|
2781
|
+
});
|
|
2782
|
+
};
|
|
2783
|
+
let animation = calc();
|
|
2784
|
+
const calcDifference = () => {
|
|
2785
|
+
return Math.abs(animation.current - animation.toValue);
|
|
2786
|
+
};
|
|
2787
|
+
let difference = calcDifference();
|
|
2788
|
+
while (difference >= threshold) {
|
|
2789
|
+
frame++;
|
|
2790
|
+
animation = calc();
|
|
2791
|
+
difference = calcDifference();
|
|
2792
|
+
}
|
|
2793
|
+
finishedFrame = frame;
|
|
2794
|
+
for (let i = 0; i < 20; i++) {
|
|
2795
|
+
frame++;
|
|
2796
|
+
animation = calc();
|
|
2797
|
+
difference = calcDifference();
|
|
2798
|
+
if (difference >= threshold) {
|
|
2799
|
+
i = 0;
|
|
2800
|
+
finishedFrame = frame + 1;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
measureCache.set(cacheKey, finishedFrame);
|
|
2804
|
+
return finishedFrame;
|
|
2805
|
+
}
|
|
2806
|
+
function spring({ frame: passedFrame, fps, config = {}, from = 0, to = 1, durationInFrames: passedDurationInFrames, durationRestThreshold, delay = 0, reverse = false }) {
|
|
2807
|
+
const needsToCalculateNaturalDuration = reverse || typeof passedDurationInFrames !== "undefined";
|
|
2808
|
+
const naturalDuration = needsToCalculateNaturalDuration ? measureSpring({
|
|
2809
|
+
fps,
|
|
2810
|
+
config,
|
|
2811
|
+
threshold: durationRestThreshold
|
|
2812
|
+
}) : void 0;
|
|
2813
|
+
const naturalDurationGetter = needsToCalculateNaturalDuration ? { get: () => naturalDuration } : { get: () => {
|
|
2814
|
+
throw new Error("did not calculate natural duration");
|
|
2815
|
+
} };
|
|
2816
|
+
const delayProcessed = (reverse ? (passedDurationInFrames ?? naturalDurationGetter.get()) - passedFrame : passedFrame) + (reverse ? delay : -delay);
|
|
2817
|
+
const durationProcessed = passedDurationInFrames === void 0 ? delayProcessed : delayProcessed / (passedDurationInFrames / naturalDurationGetter.get());
|
|
2818
|
+
if (passedDurationInFrames && delayProcessed > passedDurationInFrames) return to;
|
|
2819
|
+
const spr = springCalculation({
|
|
2820
|
+
fps,
|
|
2821
|
+
frame: durationProcessed,
|
|
2822
|
+
config
|
|
2823
|
+
});
|
|
2824
|
+
const inner = config.overshootClamping ? to >= from ? Math.min(spr.current, to) : Math.max(spr.current, to) : spr.current;
|
|
2825
|
+
return from === 0 && to === 1 ? inner : interpolate(inner, [0, 1], [from, to]);
|
|
2826
|
+
}
|
|
2827
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2828
|
+
function getCacheKey(text, style) {
|
|
2829
|
+
return JSON.stringify({
|
|
2830
|
+
text,
|
|
2831
|
+
...style
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Shared text measurer that uses SlugGeometry.measure for all environments.
|
|
2836
|
+
*/
|
|
2837
|
+
function measureText(text, style) {
|
|
2838
|
+
const safeText = text ? String(text) : " ";
|
|
2839
|
+
const cacheKey = getCacheKey(safeText, style);
|
|
2840
|
+
const cached = cache.get(cacheKey);
|
|
2841
|
+
if (cached) return cached;
|
|
2842
|
+
const fontSize = style.fontSize ?? 48;
|
|
2843
|
+
let fontFamily = style.fontFamily ?? "Inter";
|
|
2844
|
+
if (SlugFontCache.isFailed(fontFamily)) fontFamily = "Inter";
|
|
2845
|
+
const letterSpacing = style.letterSpacing ?? 0;
|
|
2846
|
+
const calculatedLineHeight = style.lineHeight !== void 0 && style.lineHeight < 10 ? style.lineHeight * fontSize : style.lineHeight ?? fontSize * 1.2;
|
|
2847
|
+
const slugFont = SlugFontCache.getFont(fontFamily, style.fontWeight, null);
|
|
2848
|
+
const padding = style.padding ?? 0;
|
|
2849
|
+
if (!slugFont) {
|
|
2850
|
+
if (!SlugFontCache.isFailed(fontFamily)) {
|
|
2851
|
+
const fontUrl = GetFontAssetUrl(fontFamily);
|
|
2852
|
+
SlugFontCache.preloadSlugFont(null, fontFamily, fontUrl).catch(() => {});
|
|
2853
|
+
}
|
|
2854
|
+
const lines = safeText.split("\n");
|
|
2855
|
+
let totalWrappedLines = 0;
|
|
2856
|
+
const maxW = style.width !== void 0 ? Math.max(10, style.width - padding * 2) : 0;
|
|
2857
|
+
for (const line of lines) if (maxW > 0) {
|
|
2858
|
+
const estLineWidth = line.length * fontSize * .55 + letterSpacing * line.length;
|
|
2859
|
+
const wrapCount = Math.max(1, Math.ceil(estLineWidth / maxW));
|
|
2860
|
+
totalWrappedLines += wrapCount;
|
|
2861
|
+
} else totalWrappedLines += 1;
|
|
2862
|
+
const result$1 = {
|
|
2863
|
+
width: style.width !== void 0 ? style.width : Math.ceil(safeText.length * fontSize * .6 + letterSpacing * safeText.length) + 2 + padding * 2,
|
|
2864
|
+
height: style.height !== void 0 ? style.height : Math.ceil(totalWrappedLines * calculatedLineHeight) + 2 + padding * 2
|
|
2865
|
+
};
|
|
2866
|
+
cache.set(cacheKey, result$1);
|
|
2867
|
+
return result$1;
|
|
2868
|
+
}
|
|
2869
|
+
const measured = SlugGeometry.measure(safeText, slugFont, fontSize, letterSpacing, calculatedLineHeight, style.width !== void 0 ? Math.max(0, style.width - padding * 2) : void 0, style.spans);
|
|
2870
|
+
const result = {
|
|
2871
|
+
width: style.width !== void 0 && !style.keepNaturalWidth ? style.width : Math.ceil(measured.width) + padding * 2 + 8,
|
|
2872
|
+
height: style.height !== void 0 ? style.height : Math.ceil(measured.height) + padding * 2
|
|
2873
|
+
};
|
|
2874
|
+
cache.set(cacheKey, result);
|
|
2875
|
+
return result;
|
|
2876
|
+
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Async variant of measureText. Preloads the Slug font and yields to the browser.
|
|
2879
|
+
*/
|
|
2880
|
+
function measureTextAsync(text, style) {
|
|
2881
|
+
let fontFamily = style.fontFamily ?? "Inter";
|
|
2882
|
+
if (SlugFontCache.isFailed(fontFamily)) fontFamily = "Inter";
|
|
2883
|
+
if (SlugFontCache.isFailed(fontFamily)) return Promise.resolve(measureText(text, style));
|
|
2884
|
+
const fontUrl = GetFontAssetUrl(fontFamily);
|
|
2885
|
+
return SlugFontCache.preloadSlugFont(null, fontFamily, fontUrl).then(() => measureText(text, style)).catch(() => measureText(text, style));
|
|
2886
|
+
}
|
|
2887
|
+
if (typeof globalThis !== "undefined") globalThis.__GATEWAI_DELAYS__ = globalThis.__GATEWAI_DELAYS__ || /* @__PURE__ */ new Set();
|
|
2888
|
+
const hasDelayRender = () => {
|
|
2889
|
+
return useMemo(() => {
|
|
2890
|
+
let nextHandle = 0;
|
|
2891
|
+
return {
|
|
2892
|
+
delayRender: (_msg) => {
|
|
2893
|
+
const h = nextHandle++;
|
|
2894
|
+
if (typeof globalThis !== "undefined") globalThis.__GATEWAI_DELAYS__.add(h);
|
|
2895
|
+
return h;
|
|
2896
|
+
},
|
|
2897
|
+
continueRender: (h) => {
|
|
2898
|
+
if (typeof globalThis !== "undefined") globalThis.__GATEWAI_DELAYS__.delete(h);
|
|
2899
|
+
}
|
|
2900
|
+
};
|
|
2901
|
+
}, []);
|
|
2902
|
+
};
|
|
2903
|
+
/**
|
|
2904
|
+
* Preloads a font into the browser or headless environment.
|
|
2905
|
+
*/
|
|
2906
|
+
async function preloadFont(family, url) {
|
|
2907
|
+
if (typeof window !== "undefined" && typeof FontFace !== "undefined") try {
|
|
2908
|
+
const loadedFace = await new FontFace(family, `url(${url})`).load();
|
|
2909
|
+
document.fonts.add(loadedFace);
|
|
2910
|
+
await document.fonts.ready;
|
|
2911
|
+
} catch (e) {
|
|
2912
|
+
console.warn(`[preloadFont] Failed to load font "${family}" from ${url}:`, e);
|
|
2913
|
+
}
|
|
2914
|
+
else await registerHeadlessFont(family, url);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
//#endregion
|
|
2918
|
+
//#region ../compositions/dist/layer-logic-B4xSPDvq.mjs
|
|
2919
|
+
const BASE_URL = getEnv("BASE_URL");
|
|
2920
|
+
const CDN_DOMAIN = getEnv("R2_CUSTOM_DOMAIN");
|
|
2921
|
+
/**
|
|
2922
|
+
* Compute render parameters for the CURRENT node only — does NOT recurse into
|
|
2923
|
+
* children. Each node in the VirtualMedia tree is responsible for exactly its
|
|
2924
|
+
* own operation; `SingleClipComposition` drives traversal via React recursion.
|
|
2925
|
+
*
|
|
2926
|
+
* This is the correct function to use inside the renderer.
|
|
2927
|
+
*/
|
|
2928
|
+
function computeRenderParams(vv) {
|
|
2929
|
+
const op = vv.operation;
|
|
2930
|
+
const baseMeta = getActiveMediaMetadata(vv);
|
|
2931
|
+
const params = {
|
|
2932
|
+
sourceUrl: void 0,
|
|
2933
|
+
trimStartSec: 0,
|
|
2934
|
+
trimEndSec: null,
|
|
2935
|
+
speed: 1,
|
|
2936
|
+
cropRegion: null,
|
|
2937
|
+
flipH: false,
|
|
2938
|
+
flipV: false,
|
|
2939
|
+
rotation: 0,
|
|
2940
|
+
effectiveDurationSec: 0,
|
|
2941
|
+
mimeType: resolveMediaMimeType(vv) || null
|
|
2942
|
+
};
|
|
2943
|
+
switch (op.op) {
|
|
2944
|
+
case "text":
|
|
2945
|
+
params.sourceUrl = void 0;
|
|
2946
|
+
break;
|
|
2947
|
+
default:
|
|
2948
|
+
params.sourceUrl = resolveMediaSourceUrl(vv);
|
|
2949
|
+
break;
|
|
2950
|
+
}
|
|
2951
|
+
const sourceDurationSec = (baseMeta?.durationMs ?? 0) / 1e3;
|
|
2952
|
+
params.effectiveDurationSec = ((params.trimEndSec ?? sourceDurationSec) - params.trimStartSec) / params.speed;
|
|
2953
|
+
return params;
|
|
2954
|
+
}
|
|
2955
|
+
function secondsToFrames(seconds, fps) {
|
|
2956
|
+
return Math.round(seconds * fps);
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
//#endregion
|
|
2960
|
+
//#region ../compositions/dist/index.mjs
|
|
2961
|
+
/**
|
|
2962
|
+
* normalizeTimeline recurses through the virtual media tree and "absorbs"
|
|
2963
|
+
* generic timeline-shifting operations into standard properties
|
|
2964
|
+
* of their children.
|
|
2965
|
+
*/
|
|
2966
|
+
function normalizeTimeline(vv, fps, context = { timeOffsetSec: 0 }) {
|
|
2967
|
+
if (!vv) return vv;
|
|
2968
|
+
const { operation, metadata = {}, children = [] } = vv;
|
|
2969
|
+
const { timeOffsetSec, durationLimitMs } = context;
|
|
2970
|
+
if (!operation) return {
|
|
2971
|
+
...vv,
|
|
2972
|
+
children: (children || []).map((child) => normalizeTimeline(child, fps, context))
|
|
2973
|
+
};
|
|
2974
|
+
const timeline = operation.timeline;
|
|
2975
|
+
const segments = timeline?.segments || [];
|
|
2976
|
+
let totalDurationMs = getActiveMediaMetadata(vv)?.durationMs ?? 0;
|
|
2977
|
+
if (segments.length > 0) {
|
|
2978
|
+
const totalSegmentsFrames = segments.reduce((acc, seg) => {
|
|
2979
|
+
const durSec = seg.endSec != null ? seg.endSec - seg.startSec : 0;
|
|
2980
|
+
return acc + Math.max(1, Math.round(durSec * fps));
|
|
2981
|
+
}, 0);
|
|
2982
|
+
if (totalSegmentsFrames > 0) totalDurationMs = totalSegmentsFrames / fps * 1e3;
|
|
2983
|
+
}
|
|
2984
|
+
const newOperation = { ...operation };
|
|
2985
|
+
const rawStartFrame = timeline?.startFrame ?? newOperation.startFrame;
|
|
2986
|
+
if (typeof rawStartFrame === "number") newOperation.startFrame = rawStartFrame - secondsToFrames(timeOffsetSec, fps);
|
|
2987
|
+
const newMetadata = {
|
|
2988
|
+
...metadata,
|
|
2989
|
+
durationMs: totalDurationMs
|
|
2990
|
+
};
|
|
2991
|
+
const childContext = typeof rawStartFrame === "number" || newOperation.op === "compose" ? {
|
|
2992
|
+
timeOffsetSec: 0,
|
|
2993
|
+
durationLimitMs: void 0
|
|
2994
|
+
} : {
|
|
2995
|
+
timeOffsetSec,
|
|
2996
|
+
durationLimitMs
|
|
2997
|
+
};
|
|
2998
|
+
return {
|
|
2999
|
+
metadata: newMetadata,
|
|
3000
|
+
operation: newOperation,
|
|
3001
|
+
children: (children || []).map((child) => normalizeTimeline(child, fps, childContext))
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
let mediaLogger = {
|
|
3005
|
+
info: (msg, ...args) => {
|
|
3006
|
+
if (typeof msg === "object" && msg !== null) console.info("[AudioExtractor]", msg, ...args);
|
|
3007
|
+
else console.info(`[AudioExtractor] ${msg}`, ...args);
|
|
3008
|
+
},
|
|
3009
|
+
warn: (msg, ...args) => {
|
|
3010
|
+
if (typeof msg === "object" && msg !== null) console.warn("[AudioExtractor]", msg, ...args);
|
|
3011
|
+
else console.warn(`[AudioExtractor] ${msg}`, ...args);
|
|
3012
|
+
},
|
|
3013
|
+
error: (msg, ...args) => {
|
|
3014
|
+
if (typeof msg === "object" && msg !== null) console.error("[AudioExtractor]", msg, ...args);
|
|
3015
|
+
else console.error(`[AudioExtractor] ${msg}`, ...args);
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
if (typeof window === "undefined" || typeof process !== "undefined" && process.versions?.node) import(
|
|
3019
|
+
/* webpackIgnore: true */
|
|
3020
|
+
/* @vite-ignore */
|
|
3021
|
+
"@gitframes/server-utils"
|
|
3022
|
+
).then((m) => {
|
|
3023
|
+
if (m.mediaLogger) mediaLogger = m.mediaLogger;
|
|
3024
|
+
}).catch(() => {});
|
|
3025
|
+
const yieldToMain$1 = () => {
|
|
3026
|
+
if (typeof window === "undefined") return Promise.resolve();
|
|
3027
|
+
const sched = globalThis.scheduler;
|
|
3028
|
+
return typeof sched?.yield === "function" ? sched.yield() : new Promise((resolve) => setTimeout(resolve, 0));
|
|
3029
|
+
};
|
|
3030
|
+
const waitForDelayRender$1 = async (timeoutMs = 3e4) => {
|
|
3031
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3032
|
+
const start = Date.now();
|
|
3033
|
+
return new Promise((resolve) => {
|
|
3034
|
+
const check = () => {
|
|
3035
|
+
const browserHandles = typeof window !== "undefined" && window.renderer_delayRenderHandles ? window.renderer_delayRenderHandles : [];
|
|
3036
|
+
const globalDelays = typeof globalThis !== "undefined" ? globalThis.__GATEWAI_DELAYS__ : null;
|
|
3037
|
+
const hasBrowserDelays = browserHandles.length > 0;
|
|
3038
|
+
const hasGlobalDelays = globalDelays instanceof Set && globalDelays.size > 0;
|
|
3039
|
+
if (!hasBrowserDelays && !hasGlobalDelays) {
|
|
3040
|
+
resolve();
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
if (Date.now() - start > timeoutMs) {
|
|
3044
|
+
mediaLogger.warn(`[AudioExtractor] waitForDelayRender timed out waiting for handles: ${JSON.stringify({
|
|
3045
|
+
browserHandles,
|
|
3046
|
+
globalDelays: globalDelays ? Array.from(globalDelays) : []
|
|
3047
|
+
})}`);
|
|
3048
|
+
resolve();
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
setTimeout(check, 50);
|
|
3052
|
+
};
|
|
3053
|
+
check();
|
|
3054
|
+
});
|
|
3055
|
+
};
|
|
3056
|
+
/**
|
|
3057
|
+
* Decodes all audio samples from a given source URL into raw Float32 mono/stereo channel buffers.
|
|
3058
|
+
*/
|
|
3059
|
+
async function decodeAudioSource(url) {
|
|
3060
|
+
mediaLogger.info(`[AudioExtractor] decodeAudioSource started for url: ${url}`);
|
|
3061
|
+
try {
|
|
3062
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined" && !(typeof process !== "undefined" && process.versions && process.versions.node)) {
|
|
3063
|
+
mediaLogger.info(`[AudioExtractor] Browser detected. Fetching arrayBuffer for ${url}...`);
|
|
3064
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
3065
|
+
if (!AudioContextClass) throw new Error("AudioContext is not supported in this browser");
|
|
3066
|
+
const audioCtx = new AudioContextClass();
|
|
3067
|
+
try {
|
|
3068
|
+
const response = await fetch(url);
|
|
3069
|
+
mediaLogger.info(`[AudioExtractor] Fetch response status for ${url}: ${response.status} (${response.statusText})`);
|
|
3070
|
+
if (!response.ok) throw new Error(`Failed to fetch audio source: ${response.statusText}`);
|
|
3071
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
3072
|
+
mediaLogger.info(`[AudioExtractor] Fetched ${arrayBuffer.byteLength} bytes. Decoding audio data...`);
|
|
3073
|
+
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
|
|
3074
|
+
mediaLogger.info(`[AudioExtractor] Decoded successfully: ${audioBuffer.numberOfChannels} channels, sampleRate: ${audioBuffer.sampleRate}, duration: ${audioBuffer.duration}s`);
|
|
3075
|
+
const channels = [];
|
|
3076
|
+
for (let c = 0; c < audioBuffer.numberOfChannels; c++) channels.push(new Float32Array(audioBuffer.getChannelData(c)));
|
|
3077
|
+
return {
|
|
3078
|
+
channels,
|
|
3079
|
+
sampleRate: audioBuffer.sampleRate
|
|
3080
|
+
};
|
|
3081
|
+
} finally {
|
|
3082
|
+
audioCtx.close();
|
|
3083
|
+
mediaLogger.info(`[AudioExtractor] AudioContext closed for ${url}`);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
let inputAcquired = false;
|
|
3087
|
+
try {
|
|
3088
|
+
mediaLogger.info(`[AudioExtractor] Server / Node.js detected. Acquiring from inputStore for ${url}...`);
|
|
3089
|
+
const input = await inputStore.acquire(url);
|
|
3090
|
+
inputAcquired = true;
|
|
3091
|
+
mediaLogger.info(`[AudioExtractor] inputStore acquired. Retrieving primary audio track...`);
|
|
3092
|
+
const track = await input.getPrimaryAudioTrack();
|
|
3093
|
+
if (!track) {
|
|
3094
|
+
mediaLogger.warn(`[AudioExtractor] No primary audio track found in input for ${url}`);
|
|
3095
|
+
return null;
|
|
3096
|
+
}
|
|
3097
|
+
const generator = new AudioSampleSink(track).samples(0);
|
|
3098
|
+
const decodedPlanes = [];
|
|
3099
|
+
let totalFrames = 0;
|
|
3100
|
+
let sampleRate = 48e3;
|
|
3101
|
+
let numChannels = 1;
|
|
3102
|
+
mediaLogger.info(`[AudioExtractor] Starting mediabunny generator loop for ${url}...`);
|
|
3103
|
+
for await (const sample of generator) {
|
|
3104
|
+
sampleRate = sample.sampleRate;
|
|
3105
|
+
numChannels = sample.numberOfChannels;
|
|
3106
|
+
const sampleFrames = sample.numberOfFrames;
|
|
3107
|
+
if (decodedPlanes.length === 0) for (let c = 0; c < numChannels; c++) decodedPlanes.push([]);
|
|
3108
|
+
for (let c = 0; c < numChannels; c++) {
|
|
3109
|
+
let planeBuffer;
|
|
3110
|
+
try {
|
|
3111
|
+
const allocSize = sample.allocationSize({
|
|
3112
|
+
planeIndex: c,
|
|
3113
|
+
format: "f32-planar"
|
|
3114
|
+
});
|
|
3115
|
+
planeBuffer = new Float32Array(allocSize / 4);
|
|
3116
|
+
sample.copyTo(planeBuffer, {
|
|
3117
|
+
planeIndex: c,
|
|
3118
|
+
format: "f32-planar"
|
|
3119
|
+
});
|
|
3120
|
+
} catch {
|
|
3121
|
+
const allocSize = sample.allocationSize({ planeIndex: c });
|
|
3122
|
+
if (sample.format === "s16" || sample.format === "s16-planar") {
|
|
3123
|
+
const int16Buffer = new Int16Array(allocSize / 2);
|
|
3124
|
+
sample.copyTo(int16Buffer, { planeIndex: c });
|
|
3125
|
+
planeBuffer = new Float32Array(int16Buffer.length);
|
|
3126
|
+
for (let i = 0; i < int16Buffer.length; i++) planeBuffer[i] = (int16Buffer[i] ?? 0) / 32768;
|
|
3127
|
+
} else if (sample.format === "s32" || sample.format === "s32-planar") {
|
|
3128
|
+
const int32Buffer = new Int32Array(allocSize / 4);
|
|
3129
|
+
sample.copyTo(int32Buffer, { planeIndex: c });
|
|
3130
|
+
planeBuffer = new Float32Array(int32Buffer.length);
|
|
3131
|
+
for (let i = 0; i < int32Buffer.length; i++) planeBuffer[i] = (int32Buffer[i] ?? 0) / 2147483648;
|
|
3132
|
+
} else if (sample.format === "u8" || sample.format === "u8-planar") {
|
|
3133
|
+
const u8Buffer = new Uint8Array(allocSize);
|
|
3134
|
+
sample.copyTo(u8Buffer, { planeIndex: c });
|
|
3135
|
+
planeBuffer = new Float32Array(u8Buffer.length);
|
|
3136
|
+
for (let i = 0; i < u8Buffer.length; i++) planeBuffer[i] = ((u8Buffer[i] ?? 128) - 128) / 128;
|
|
3137
|
+
} else {
|
|
3138
|
+
planeBuffer = new Float32Array(allocSize / 4);
|
|
3139
|
+
sample.copyTo(planeBuffer, { planeIndex: c });
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
decodedPlanes[c].push(planeBuffer);
|
|
3143
|
+
}
|
|
3144
|
+
totalFrames += sampleFrames;
|
|
3145
|
+
sample.close();
|
|
3146
|
+
}
|
|
3147
|
+
mediaLogger.info(`[AudioExtractor] Generator loop finished. Decoded total frames: ${totalFrames}`);
|
|
3148
|
+
if (totalFrames === 0 || decodedPlanes.length === 0) return null;
|
|
3149
|
+
return {
|
|
3150
|
+
channels: decodedPlanes.map((planes) => {
|
|
3151
|
+
const merged = new Float32Array(totalFrames);
|
|
3152
|
+
let offset = 0;
|
|
3153
|
+
for (const plane of planes) {
|
|
3154
|
+
merged.set(plane, offset);
|
|
3155
|
+
offset += plane.length;
|
|
3156
|
+
}
|
|
3157
|
+
return merged;
|
|
3158
|
+
}),
|
|
3159
|
+
sampleRate
|
|
3160
|
+
};
|
|
3161
|
+
} catch (err) {
|
|
3162
|
+
if (typeof process !== "undefined" && process.versions?.node) try {
|
|
3163
|
+
const { execSync } = await import("node:child_process");
|
|
3164
|
+
const stdout = execSync(`ffmpeg -v error -i "${url}" -f f32le -ar 48000 -ac 2 pipe:1`, { maxBuffer: 100 * 1024 * 1024 });
|
|
3165
|
+
const totalSamples = stdout.byteLength / 4;
|
|
3166
|
+
const numFrames = totalSamples / 2;
|
|
3167
|
+
const interleaved = new Float32Array(stdout.buffer, stdout.byteOffset, totalSamples);
|
|
3168
|
+
const left = new Float32Array(numFrames);
|
|
3169
|
+
const right = new Float32Array(numFrames);
|
|
3170
|
+
for (let i = 0; i < numFrames; i++) {
|
|
3171
|
+
left[i] = interleaved[i * 2];
|
|
3172
|
+
right[i] = interleaved[i * 2 + 1];
|
|
3173
|
+
}
|
|
3174
|
+
return {
|
|
3175
|
+
channels: [left, right],
|
|
3176
|
+
sampleRate: 48e3
|
|
3177
|
+
};
|
|
3178
|
+
} catch (ffmpegErr) {
|
|
3179
|
+
mediaLogger.warn({ ffmpegErr }, `[AudioExtractor] ffmpeg fallback decode failed for ${url}`);
|
|
3180
|
+
}
|
|
3181
|
+
mediaLogger.error({ err }, `[AudioExtractor] Failed to decode source ${url}`);
|
|
3182
|
+
return null;
|
|
3183
|
+
} finally {
|
|
3184
|
+
if (inputAcquired) inputStore.release(url);
|
|
3185
|
+
}
|
|
3186
|
+
} catch (err) {
|
|
3187
|
+
mediaLogger.error({ err }, `[AudioExtractor] Failed to decode source ${url}`);
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Resamples a PCM buffer using linear interpolation to target sample rate & speed.
|
|
3193
|
+
*/
|
|
3194
|
+
async function resampleAndStretchPCM(inputChannel, sourceSampleRate, targetSampleRate, speed, outputLength, trimOffsetSamples) {
|
|
3195
|
+
const output = new Float32Array(outputLength);
|
|
3196
|
+
const factor = sourceSampleRate / targetSampleRate * speed;
|
|
3197
|
+
const YIELD_INTERVAL = 1e5;
|
|
3198
|
+
for (let i = 0; i < outputLength; i++) {
|
|
3199
|
+
if (i > 0 && i % YIELD_INTERVAL === 0) await yieldToMain$1();
|
|
3200
|
+
const srcIndex = trimOffsetSamples + i * factor;
|
|
3201
|
+
const indexFloor = Math.floor(srcIndex);
|
|
3202
|
+
const indexCeil = Math.ceil(srcIndex);
|
|
3203
|
+
if (indexFloor < 0) {
|
|
3204
|
+
output[i] = 0;
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
if (indexFloor >= inputChannel.length) break;
|
|
3208
|
+
const weight = srcIndex - indexFloor;
|
|
3209
|
+
const sampleFloor = inputChannel[indexFloor] ?? 0;
|
|
3210
|
+
const val = sampleFloor + weight * ((indexCeil >= 0 && indexCeil < inputChannel.length ? inputChannel[indexCeil] ?? sampleFloor : sampleFloor) - sampleFloor);
|
|
3211
|
+
output[i] = Number.isFinite(val) ? val : 0;
|
|
3212
|
+
}
|
|
3213
|
+
return output;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Recursively parses the VirtualMediaData tree and mixes all active audio tracks
|
|
3217
|
+
* into a single unified stereo PCM stream (Float32Array channels).
|
|
3218
|
+
*/
|
|
3219
|
+
async function mixAudioTracks(virtualMedia, fps, targetSampleRate = 48e3, device, renderId) {
|
|
3220
|
+
mediaLogger.info(`[AudioExtractor] mixAudioTracks started. FPS: ${fps}, targetSampleRate: ${targetSampleRate}`);
|
|
3221
|
+
await waitForDelayRender$1();
|
|
3222
|
+
shaderStore.registerSignalsFromComposition(renderId, virtualMedia);
|
|
3223
|
+
const durationMs = getActiveMediaMetadata(virtualMedia)?.durationMs || 1e3;
|
|
3224
|
+
const totalDurationSec = durationMs / 1e3;
|
|
3225
|
+
const totalTargetSamples = Math.ceil(totalDurationSec * targetSampleRate);
|
|
3226
|
+
mediaLogger.info(`[AudioExtractor] Composition total duration: ${durationMs}ms (${totalDurationSec}s). Total mixed samples to allocate: ${totalTargetSamples}`);
|
|
3227
|
+
const outputLeft = new Float32Array(totalTargetSamples);
|
|
3228
|
+
const outputRight = new Float32Array(totalTargetSamples);
|
|
3229
|
+
const mixedChannels = [outputLeft, outputRight];
|
|
3230
|
+
const decodedCache = /* @__PURE__ */ new Map();
|
|
3231
|
+
async function traverse(node, inheritedSeekSec, inheritedClockFrames, inheritedVolume, inheritedSpeed, inheritedDurationFrames) {
|
|
3232
|
+
if (!node) return null;
|
|
3233
|
+
const op = node.operation;
|
|
3234
|
+
if (!op) {
|
|
3235
|
+
const durationMs$1 = getActiveMediaMetadata(node)?.durationMs ?? 0;
|
|
3236
|
+
const durationFrames = Math.round(durationMs$1 / 1e3 * fps);
|
|
3237
|
+
const actualDurationFrames = inheritedDurationFrames !== null ? durationFrames > 0 ? Math.min(inheritedDurationFrames, durationFrames) : inheritedDurationFrames : durationFrames;
|
|
3238
|
+
const localSamplesCount$1 = Math.ceil(actualDurationFrames / fps * targetSampleRate);
|
|
3239
|
+
if (localSamplesCount$1 <= 0) return null;
|
|
3240
|
+
const localChannels$1 = [new Float32Array(localSamplesCount$1), new Float32Array(localSamplesCount$1)];
|
|
3241
|
+
for (const child of node.children) {
|
|
3242
|
+
const childChannels = await traverse(child, inheritedSeekSec, inheritedClockFrames, inheritedVolume, inheritedSpeed, actualDurationFrames);
|
|
3243
|
+
if (childChannels) {
|
|
3244
|
+
const childOp = child.operation;
|
|
3245
|
+
const childStartFrame = childOp?.startFrame ?? childOp?.timeline?.startFrame ?? 0;
|
|
3246
|
+
const childStartSample = Math.round(childStartFrame / fps * targetSampleRate);
|
|
3247
|
+
for (let c = 0; c < 2; c++) {
|
|
3248
|
+
const dest = localChannels$1[c];
|
|
3249
|
+
const src = childChannels[c];
|
|
3250
|
+
const len = Math.min(src.length, dest.length - childStartSample);
|
|
3251
|
+
for (let i = 0; i < len; i++) if (childStartSample + i >= 0) dest[childStartSample + i] += src[i];
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
return localChannels$1;
|
|
3256
|
+
}
|
|
3257
|
+
const volume = inheritedVolume * (op.volume ?? 1) * (op.opacity === 0 ? 0 : 1) * (op.muted ? 0 : 1);
|
|
3258
|
+
const speedMultiplier = inheritedSpeed;
|
|
3259
|
+
const localClockFrames = inheritedClockFrames + (op.startFrame ?? op.timeline?.startFrame ?? 0);
|
|
3260
|
+
const segments = op.timeline?.segments || [];
|
|
3261
|
+
const nodeDurationMs = getActiveMediaMetadata(node)?.durationMs ?? 0;
|
|
3262
|
+
const nodeDurationFrames = Math.round(nodeDurationMs / 1e3 * fps);
|
|
3263
|
+
const nodeActualDurationFrames = inheritedDurationFrames !== null ? nodeDurationFrames > 0 ? Math.min(inheritedDurationFrames, nodeDurationFrames) : inheritedDurationFrames : nodeDurationFrames;
|
|
3264
|
+
const localSamplesCount = Math.ceil(nodeActualDurationFrames / fps * targetSampleRate);
|
|
3265
|
+
if (localSamplesCount <= 0) return null;
|
|
3266
|
+
const localLeft = new Float32Array(localSamplesCount);
|
|
3267
|
+
const localRight = new Float32Array(localSamplesCount);
|
|
3268
|
+
const localChannels = [localLeft, localRight];
|
|
3269
|
+
const processSegment = async (seg, windowStartFrameOffset) => {
|
|
3270
|
+
const totalSeekSec = inheritedSeekSec + (seg ? seg.startSec : 0);
|
|
3271
|
+
const totalClockFrames = localClockFrames + windowStartFrameOffset;
|
|
3272
|
+
let segDurationFrames = 0;
|
|
3273
|
+
if (seg) segDurationFrames = Math.max(1, Math.round(((seg.endSec ?? 0) - seg.startSec) * fps));
|
|
3274
|
+
else segDurationFrames = nodeDurationFrames;
|
|
3275
|
+
const actualDurationFrames = inheritedDurationFrames !== null ? Math.min(inheritedDurationFrames, segDurationFrames) : segDurationFrames;
|
|
3276
|
+
const segmentStartSample = Math.round(windowStartFrameOffset / fps * targetSampleRate);
|
|
3277
|
+
if (op.op === "source") {
|
|
3278
|
+
const mediaType = getMediaType(node);
|
|
3279
|
+
const params = computeRenderParams(node);
|
|
3280
|
+
if (params.sourceUrl && (mediaType === "Audio" || mediaType === "Video") && volume > 0) {
|
|
3281
|
+
const finalSpeed = Math.max(.5, speedMultiplier * (Number(params.speed) || 1));
|
|
3282
|
+
const effectiveTrimSec = Math.max(0, (Number(params.trimStartSec) || 0) + totalSeekSec);
|
|
3283
|
+
let decoded = decodedCache.get(params.sourceUrl);
|
|
3284
|
+
if (decoded === void 0) {
|
|
3285
|
+
decoded = await decodeAudioSource(params.sourceUrl);
|
|
3286
|
+
decodedCache.set(params.sourceUrl, decoded);
|
|
3287
|
+
}
|
|
3288
|
+
if (decoded) {
|
|
3289
|
+
const clipDurationSec = actualDurationFrames / fps;
|
|
3290
|
+
const clipSamplesCount = Math.min(localSamplesCount - segmentStartSample, Math.ceil(clipDurationSec * targetSampleRate));
|
|
3291
|
+
if (clipSamplesCount > 0 && segmentStartSample < localSamplesCount) {
|
|
3292
|
+
const trimOffsetSamples = Math.round(effectiveTrimSec * decoded.sampleRate);
|
|
3293
|
+
const srcLeft = decoded.channels[0];
|
|
3294
|
+
const srcRight = decoded.channels.length > 1 ? decoded.channels[1] : decoded.channels[0];
|
|
3295
|
+
const stretchedLeft = await resampleAndStretchPCM(srcLeft, decoded.sampleRate, targetSampleRate, finalSpeed, clipSamplesCount, trimOffsetSamples);
|
|
3296
|
+
const stretchedRight = await resampleAndStretchPCM(srcRight, decoded.sampleRate, targetSampleRate, finalSpeed, clipSamplesCount, trimOffsetSamples);
|
|
3297
|
+
for (let i = 0; i < clipSamplesCount; i++) {
|
|
3298
|
+
const outIndex = segmentStartSample + i;
|
|
3299
|
+
localLeft[outIndex] += stretchedLeft[i] * volume;
|
|
3300
|
+
localRight[outIndex] += stretchedRight[i] * volume;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
await yieldToMain$1();
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
if (node.children) for (const child of node.children) {
|
|
3308
|
+
const childChannels = await traverse(child, totalSeekSec, totalClockFrames, volume, speedMultiplier, actualDurationFrames);
|
|
3309
|
+
if (childChannels) {
|
|
3310
|
+
const childOp = child.operation;
|
|
3311
|
+
const childStartFrame = childOp?.startFrame ?? childOp?.timeline?.startFrame ?? 0;
|
|
3312
|
+
const destStartSample = segmentStartSample + Math.round(childStartFrame / fps * targetSampleRate);
|
|
3313
|
+
for (let c = 0; c < 2; c++) {
|
|
3314
|
+
const dest = localChannels[c];
|
|
3315
|
+
const src = childChannels[c];
|
|
3316
|
+
const len = Math.min(src.length, dest.length - destStartSample);
|
|
3317
|
+
for (let i = 0; i < len; i++) if (destStartSample + i >= 0) dest[destStartSample + i] += src[i];
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
const pluginProcessor = audioRegistry.get(op.op);
|
|
3322
|
+
if (pluginProcessor) try {
|
|
3323
|
+
const clipDurationSec = actualDurationFrames / fps;
|
|
3324
|
+
const clipSamplesCount = Math.min(localSamplesCount - segmentStartSample, Math.ceil(clipDurationSec * targetSampleRate));
|
|
3325
|
+
if (clipSamplesCount > 0 && segmentStartSample < localSamplesCount && segmentStartSample >= 0) await pluginProcessor(localChannels.map((ch) => ch.subarray(segmentStartSample, segmentStartSample + clipSamplesCount)), targetSampleRate, node, {
|
|
3326
|
+
device,
|
|
3327
|
+
frame: totalClockFrames,
|
|
3328
|
+
fps,
|
|
3329
|
+
renderId,
|
|
3330
|
+
elapsedMs: (totalClockFrames / fps + totalSeekSec) * 1e3,
|
|
3331
|
+
durationMs: getActiveMediaMetadata(node)?.durationMs ?? void 0
|
|
3332
|
+
});
|
|
3333
|
+
} catch (pluginErr) {
|
|
3334
|
+
mediaLogger.error({ err: pluginErr }, `[AudioExtractor] Custom plugin for operation '${op.op}' failed`);
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
if (segments.length > 0) {
|
|
3338
|
+
let currentStart = 0;
|
|
3339
|
+
for (const seg of segments) {
|
|
3340
|
+
const durSec = (seg.endSec ?? 0) - seg.startSec;
|
|
3341
|
+
const durFrames = Math.max(1, Math.round(durSec * fps));
|
|
3342
|
+
await processSegment(seg, currentStart);
|
|
3343
|
+
currentStart += durFrames;
|
|
3344
|
+
}
|
|
3345
|
+
} else await processSegment(null, 0);
|
|
3346
|
+
return localChannels;
|
|
3347
|
+
}
|
|
3348
|
+
const rootChannels = await traverse(normalizeTimeline(virtualMedia, fps), 0, 0, 1, 1, null);
|
|
3349
|
+
if (rootChannels) for (let c = 0; c < 2; c++) {
|
|
3350
|
+
const dest = mixedChannels[c];
|
|
3351
|
+
const src = rootChannels[c];
|
|
3352
|
+
const len = Math.min(src.length, dest.length);
|
|
3353
|
+
for (let i = 0; i < len; i++) dest[i] = src[i];
|
|
3354
|
+
}
|
|
3355
|
+
decodedCache.clear();
|
|
3356
|
+
let mixPeak = 0;
|
|
3357
|
+
for (let i = 0; i < totalTargetSamples; i++) {
|
|
3358
|
+
const l = Math.abs(outputLeft[i] ?? 0);
|
|
3359
|
+
const r = Math.abs(outputRight[i] ?? 0);
|
|
3360
|
+
if (l > mixPeak) mixPeak = l;
|
|
3361
|
+
if (r > mixPeak) mixPeak = r;
|
|
3362
|
+
}
|
|
3363
|
+
const autoGain = mixPeak > .95 ? .92 / mixPeak : 1;
|
|
3364
|
+
for (let i = 0; i < totalTargetSamples; i++) {
|
|
3365
|
+
const l = (outputLeft[i] ?? 0) * autoGain;
|
|
3366
|
+
const r = (outputRight[i] ?? 0) * autoGain;
|
|
3367
|
+
outputLeft[i] = Number.isFinite(l) ? Math.tanh(l) : 0;
|
|
3368
|
+
outputRight[i] = Number.isFinite(r) ? Math.tanh(r) : 0;
|
|
3369
|
+
}
|
|
3370
|
+
return {
|
|
3371
|
+
channels: mixedChannels,
|
|
3372
|
+
sampleRate: targetSampleRate
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
const RenderTimelineContext = createContext({
|
|
3376
|
+
accumulatedSeekOffsetSec: 0,
|
|
3377
|
+
accumulatedClockOffsetSec: 0
|
|
3378
|
+
});
|
|
3379
|
+
const RenderTimelineProvider = RenderTimelineContext.Provider;
|
|
3380
|
+
const AudioProcessorContext = React.createContext({ processors: [] });
|
|
3381
|
+
const DEFAULT_STATE = {
|
|
3382
|
+
frame: 0,
|
|
3383
|
+
fps: 30,
|
|
3384
|
+
isPlaying: false
|
|
3385
|
+
};
|
|
3386
|
+
var CompositionStateStore = class {
|
|
3387
|
+
states = /* @__PURE__ */ new Map();
|
|
3388
|
+
listeners = /* @__PURE__ */ new Map();
|
|
3389
|
+
setState(key, frame, fps, isPlaying) {
|
|
3390
|
+
const current = this.states.get(key);
|
|
3391
|
+
const resolvedPlaying = isPlaying !== void 0 ? isPlaying : current?.isPlaying ?? false;
|
|
3392
|
+
if (current?.frame === frame && current?.fps === fps && current?.isPlaying === resolvedPlaying) return;
|
|
3393
|
+
this.states.set(key, {
|
|
3394
|
+
frame,
|
|
3395
|
+
fps,
|
|
3396
|
+
isPlaying: resolvedPlaying
|
|
3397
|
+
});
|
|
3398
|
+
this.notify(key);
|
|
3399
|
+
}
|
|
3400
|
+
getState(key) {
|
|
3401
|
+
if (this.states.has(key)) return this.states.get(key);
|
|
3402
|
+
for (const [storeKey, value] of this.states.entries()) if (key.startsWith(storeKey)) return value;
|
|
3403
|
+
return DEFAULT_STATE;
|
|
3404
|
+
}
|
|
3405
|
+
subscribe(key, listener) {
|
|
3406
|
+
if (!this.listeners.has(key)) this.listeners.set(key, /* @__PURE__ */ new Set());
|
|
3407
|
+
this.listeners.get(key).add(listener);
|
|
3408
|
+
return () => {
|
|
3409
|
+
const set = this.listeners.get(key);
|
|
3410
|
+
set?.delete(listener);
|
|
3411
|
+
if (set?.size === 0) {
|
|
3412
|
+
this.listeners.delete(key);
|
|
3413
|
+
this.states.delete(key);
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
clearKey(key) {
|
|
3418
|
+
this.states.delete(key);
|
|
3419
|
+
}
|
|
3420
|
+
notify(key) {
|
|
3421
|
+
for (const [listenerKey, set] of this.listeners.entries()) if (listenerKey === key || listenerKey.startsWith(key)) set.forEach((l) => l());
|
|
3422
|
+
}
|
|
3423
|
+
};
|
|
3424
|
+
const compositionStateStore = new CompositionStateStore();
|
|
3425
|
+
const useCompositionState = (renderId) => {
|
|
3426
|
+
if (typeof globalThis !== "undefined" && globalThis.__IS_HEADLESS_RENDERER__ === true) return compositionStateStore.getState(renderId);
|
|
3427
|
+
return useSyncExternalStore(useCallback((onStoreChange) => compositionStateStore.subscribe(renderId, onStoreChange), [renderId]), () => compositionStateStore.getState(renderId), () => DEFAULT_STATE);
|
|
3428
|
+
};
|
|
3429
|
+
const segmentRangeCache = /* @__PURE__ */ new WeakMap();
|
|
3430
|
+
function getCachedSegmentRanges(segments, fps) {
|
|
3431
|
+
const cached = segmentRangeCache.get(segments);
|
|
3432
|
+
if (cached && cached.fps === fps) return cached.ranges;
|
|
3433
|
+
let currentStart = 0;
|
|
3434
|
+
const ranges = segments.map((seg) => {
|
|
3435
|
+
const durSec = (seg.endSec ?? 0) - seg.startSec;
|
|
3436
|
+
const durFrames = Math.max(1, Math.round(durSec * fps));
|
|
3437
|
+
const start = currentStart;
|
|
3438
|
+
currentStart += durFrames;
|
|
3439
|
+
return {
|
|
3440
|
+
seg,
|
|
3441
|
+
start,
|
|
3442
|
+
duration: durFrames
|
|
3443
|
+
};
|
|
3444
|
+
});
|
|
3445
|
+
segmentRangeCache.set(segments, {
|
|
3446
|
+
fps,
|
|
3447
|
+
ranges
|
|
3448
|
+
});
|
|
3449
|
+
return ranges;
|
|
3450
|
+
}
|
|
3451
|
+
const frameCtxPool = [];
|
|
3452
|
+
function acquireFrameContext() {
|
|
3453
|
+
return frameCtxPool.pop() ?? {};
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Effect operations carry every config field as a signal. Hand node renderers
|
|
3457
|
+
* the plain value for this frame (nested objects/arrays included), keeping only
|
|
3458
|
+
* GPU-bound signals intact since those are consumed as textures.
|
|
3459
|
+
*/
|
|
3460
|
+
function resolveSignalOperation(virtualMedia, frameCtx) {
|
|
3461
|
+
const op = virtualMedia.operation;
|
|
3462
|
+
const resolved = { ...op };
|
|
3463
|
+
for (const [key, sig] of Object.entries(op.signals)) if (isSignal(sig) && !sig.gpuBinding) resolved[key] = sig.get(frameCtx);
|
|
3464
|
+
return {
|
|
3465
|
+
...virtualMedia,
|
|
3466
|
+
operation: resolved
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
async function drawCompositionTree(ctx, encoder, targetView, targetTexture, targetWidth, targetHeight, virtualMedia, props, inheritedSeekOffset = 0, inheritedClockOffset = 0) {
|
|
3470
|
+
const fps = props.fps ?? 24;
|
|
3471
|
+
const op = virtualMedia.operation;
|
|
3472
|
+
if (!op) return;
|
|
3473
|
+
const isCompositor = op.op === "Compositor";
|
|
3474
|
+
const frame = props.frame ?? 0;
|
|
3475
|
+
const segments = op.timeline?.segments || [];
|
|
3476
|
+
const localFrame = frame - (op.startFrame ?? op.timeline?.startFrame ?? 0);
|
|
3477
|
+
const isTimelineProvider = op.timeline !== void 0 || op.startFrame !== void 0 || op.op === "compose";
|
|
3478
|
+
const resolvedDurationMs = isTimelineProvider ? getActiveMediaMetadata(virtualMedia)?.durationMs ?? props.durationMs ?? void 0 : props.durationMs ?? getActiveMediaMetadata(virtualMedia)?.durationMs ?? void 0;
|
|
3479
|
+
if (!(op.op === "source" || op.op === "text" || op.op === "caption")) updateClockSignals(frame, fps, resolvedDurationMs);
|
|
3480
|
+
const frameCtx = acquireFrameContext();
|
|
3481
|
+
frameCtx.frame = frame;
|
|
3482
|
+
frameCtx.fps = fps;
|
|
3483
|
+
frameCtx.time = fps > 0 ? frame / fps : 0;
|
|
3484
|
+
frameCtx.duration = (resolvedDurationMs ?? 0) / 1e3;
|
|
3485
|
+
frameCtx.durationMs = resolvedDurationMs ?? 0;
|
|
3486
|
+
frameCtx.progress = resolvedDurationMs && resolvedDurationMs > 0 ? Math.max(0, Math.min(1, frame / fps * 1e3 / resolvedDurationMs)) : 0;
|
|
3487
|
+
frameCtx.deltaTime = fps > 0 ? 1 / fps : 0;
|
|
3488
|
+
const resolvedElapsedMs = isTimelineProvider ? (localFrame !== void 0 && fps !== void 0 ? localFrame / fps * 1e3 : void 0) ?? props.elapsedMs : props.elapsedMs ?? (localFrame !== void 0 && fps !== void 0 ? localFrame / fps * 1e3 : void 0);
|
|
3489
|
+
const effectiveLocalFrame = localFrame + Math.round(inheritedSeekOffset * fps);
|
|
3490
|
+
let activeSeg = null;
|
|
3491
|
+
let windowStartFrame = 0;
|
|
3492
|
+
if (segments.length > 0) {
|
|
3493
|
+
const active = getCachedSegmentRanges(segments, fps).find(({ start, duration }) => effectiveLocalFrame >= start && effectiveLocalFrame < start + duration);
|
|
3494
|
+
if (active) {
|
|
3495
|
+
activeSeg = active.seg;
|
|
3496
|
+
windowStartFrame = active.start;
|
|
3497
|
+
} else return;
|
|
3498
|
+
}
|
|
3499
|
+
const segmentLocalFrame = localFrame - windowStartFrame;
|
|
3500
|
+
const accumulatedSeekOffsetSec = inheritedSeekOffset + (activeSeg ? activeSeg.startSec - windowStartFrame / fps : 0);
|
|
3501
|
+
const accumulatedClockOffsetSec = inheritedClockOffset + windowStartFrame / fps;
|
|
3502
|
+
if (typeof op.onRequestFrame === "function") op.onRequestFrame(frameCtx);
|
|
3503
|
+
else if (op.effect && typeof op.effect.notifyFrame === "function") op.effect.notifyFrame(frameCtx);
|
|
3504
|
+
const CustomRenderer = webgpuRegistry.get(op.op);
|
|
3505
|
+
if (CustomRenderer) {
|
|
3506
|
+
if (op.signals) virtualMedia = resolveSignalOperation(virtualMedia, frameCtx);
|
|
3507
|
+
const inputs = op?.inputs;
|
|
3508
|
+
if (inputs) {
|
|
3509
|
+
for (const entry of Object.values(inputs)) if (entry?.connectionValid && entry?.outputItem?.type === "LUT") {
|
|
3510
|
+
const lutVirtualMedia = entry.outputItem.data;
|
|
3511
|
+
if (lutVirtualMedia && typeof lutVirtualMedia === "object" && lutVirtualMedia.operation) {
|
|
3512
|
+
const lutW = lutVirtualMedia.metadata?.width ?? targetWidth;
|
|
3513
|
+
const lutH = lutVirtualMedia.metadata?.height ?? targetHeight;
|
|
3514
|
+
const dummyTex = ctx.renderer.getTemporaryTexture(lutW, lutH);
|
|
3515
|
+
await drawCompositionTree(ctx, encoder, dummyTex.createView(), dummyTex, lutW, lutH, lutVirtualMedia, {
|
|
3516
|
+
...props,
|
|
3517
|
+
virtualMedia: lutVirtualMedia
|
|
3518
|
+
}, accumulatedSeekOffsetSec, accumulatedClockOffsetSec);
|
|
3519
|
+
}
|
|
3520
|
+
} else if (entry?.connectionValid && entry?.outputItem?.type === "Signal") {
|
|
3521
|
+
const sigData = entry.outputItem.data;
|
|
3522
|
+
if (sigData && typeof sigData === "object") await signalRegistry.ensureAudioSourcesExtracted(ctx.device, sigData.signalConfig ?? sigData, fps, props.renderId);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
const pass = ctx.renderer.beginFrame(encoder, targetView, {
|
|
3526
|
+
r: 0,
|
|
3527
|
+
g: 0,
|
|
3528
|
+
b: 0,
|
|
3529
|
+
a: 0
|
|
3530
|
+
}, targetWidth, targetHeight, "load");
|
|
3531
|
+
let passEnded = false;
|
|
3532
|
+
const originalEnd = pass.end.bind(pass);
|
|
3533
|
+
pass.end = () => {
|
|
3534
|
+
if (!passEnded) {
|
|
3535
|
+
originalEnd();
|
|
3536
|
+
passEnded = true;
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
const renderArgs = {
|
|
3540
|
+
ctx,
|
|
3541
|
+
encoder,
|
|
3542
|
+
pass,
|
|
3543
|
+
targetView,
|
|
3544
|
+
targetTexture,
|
|
3545
|
+
targetWidth,
|
|
3546
|
+
targetHeight,
|
|
3547
|
+
props: {
|
|
3548
|
+
...props,
|
|
3549
|
+
virtualMedia,
|
|
3550
|
+
inheritedSeekOffset: accumulatedSeekOffsetSec,
|
|
3551
|
+
inheritedClockOffset: accumulatedClockOffsetSec,
|
|
3552
|
+
durationMs: resolvedDurationMs,
|
|
3553
|
+
elapsedMs: resolvedElapsedMs
|
|
3554
|
+
},
|
|
3555
|
+
drawChild: async (child, overrides, targetViewOverride, targetTextureOverride, targetWidthOverride, targetHeightOverride) => {
|
|
3556
|
+
await drawCompositionTree(ctx, encoder, targetViewOverride ?? targetView, targetTextureOverride ?? targetTexture, targetWidthOverride ?? targetWidth, targetHeightOverride ?? targetHeight, child, {
|
|
3557
|
+
...props,
|
|
3558
|
+
durationMs: resolvedDurationMs,
|
|
3559
|
+
elapsedMs: resolvedElapsedMs,
|
|
3560
|
+
virtualMedia: child,
|
|
3561
|
+
...overrides,
|
|
3562
|
+
excludeTextures: [
|
|
3563
|
+
...props.excludeTextures || [],
|
|
3564
|
+
...overrides?.excludeTextures || [],
|
|
3565
|
+
targetTextureOverride ?? targetTexture
|
|
3566
|
+
]
|
|
3567
|
+
}, isCompositor ? 0 : accumulatedSeekOffsetSec, accumulatedClockOffsetSec);
|
|
3568
|
+
}
|
|
3569
|
+
};
|
|
3570
|
+
await CustomRenderer(renderArgs);
|
|
3571
|
+
renderArgs.pass.end();
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
if (op.op === "LUT") return;
|
|
3575
|
+
if (op.op === "Signal" || op.op === "SignalMath" || op.dataType === "Signal") {
|
|
3576
|
+
const pass = ctx.renderer.beginFrame(encoder, targetView, {
|
|
3577
|
+
r: 0,
|
|
3578
|
+
g: 0,
|
|
3579
|
+
b: 0,
|
|
3580
|
+
a: 0
|
|
3581
|
+
}, targetWidth, targetHeight, "load");
|
|
3582
|
+
await drawSignalNode(ctx, encoder, pass, {
|
|
3583
|
+
nodeId: op.nodeId || op.signalConfig?.nodeId || props.renderId,
|
|
3584
|
+
func: op.func,
|
|
3585
|
+
amplitude: Number(op.amplitude ?? 1),
|
|
3586
|
+
frequency: Number(op.frequency ?? 1),
|
|
3587
|
+
phase: Number(op.phase ?? 0),
|
|
3588
|
+
offset: Number(op.offset ?? 0),
|
|
3589
|
+
signalConfig: {
|
|
3590
|
+
...op.signalConfig,
|
|
3591
|
+
...op
|
|
3592
|
+
},
|
|
3593
|
+
frame,
|
|
3594
|
+
fps,
|
|
3595
|
+
elapsedMs: resolvedElapsedMs,
|
|
3596
|
+
durationMs: resolvedDurationMs ?? 0,
|
|
3597
|
+
width: targetWidth,
|
|
3598
|
+
height: targetHeight,
|
|
3599
|
+
drawChild: async (child, childProps, childView, childTex, childW, childH) => {
|
|
3600
|
+
await drawCompositionTree(ctx, encoder, childView, childTex, childW, childH, child, {
|
|
3601
|
+
...props,
|
|
3602
|
+
durationMs: resolvedDurationMs,
|
|
3603
|
+
elapsedMs: resolvedElapsedMs,
|
|
3604
|
+
...childProps,
|
|
3605
|
+
excludeTextures: [
|
|
3606
|
+
...props.excludeTextures || [],
|
|
3607
|
+
...childProps?.excludeTextures || [],
|
|
3608
|
+
childTex
|
|
3609
|
+
]
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
});
|
|
3613
|
+
pass.end();
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
if (op.op === "source" || op.op === "text" || op.op === "caption") {
|
|
3617
|
+
const pass = ctx.renderer.beginFrame(encoder, targetView, {
|
|
3618
|
+
r: 0,
|
|
3619
|
+
g: 0,
|
|
3620
|
+
b: 0,
|
|
3621
|
+
a: 0
|
|
3622
|
+
}, targetWidth, targetHeight, "load");
|
|
3623
|
+
if (op.op === "source") {
|
|
3624
|
+
const mediaType = getMediaType(virtualMedia);
|
|
3625
|
+
const params = computeRenderParams(virtualMedia);
|
|
3626
|
+
if (params.sourceUrl) {
|
|
3627
|
+
const containerWidth = props.containerWidth ?? ctx.surface.width;
|
|
3628
|
+
const containerHeight = props.containerHeight ?? ctx.surface.height;
|
|
3629
|
+
if (mediaType === "Image") await drawImageNode(ctx, pass, {
|
|
3630
|
+
src: params.sourceUrl,
|
|
3631
|
+
dstRect: {
|
|
3632
|
+
x: 0,
|
|
3633
|
+
y: 0,
|
|
3634
|
+
width: containerWidth,
|
|
3635
|
+
height: containerHeight
|
|
3636
|
+
},
|
|
3637
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
3638
|
+
isHeadless: props.isHeadless
|
|
3639
|
+
});
|
|
3640
|
+
else if (mediaType === "SVG") await drawSvgNode(ctx, pass, {
|
|
3641
|
+
src: params.sourceUrl,
|
|
3642
|
+
dstRect: {
|
|
3643
|
+
x: 0,
|
|
3644
|
+
y: 0,
|
|
3645
|
+
width: containerWidth,
|
|
3646
|
+
height: containerHeight
|
|
3647
|
+
},
|
|
3648
|
+
opacity: Number(op.opacity ?? 1),
|
|
3649
|
+
isHeadless: props.isHeadless
|
|
3650
|
+
});
|
|
3651
|
+
else if (mediaType === "Lottie") await drawLottieNode(ctx, pass, {
|
|
3652
|
+
src: params.sourceUrl,
|
|
3653
|
+
dstRect: {
|
|
3654
|
+
x: 0,
|
|
3655
|
+
y: 0,
|
|
3656
|
+
width: containerWidth,
|
|
3657
|
+
height: containerHeight
|
|
3658
|
+
},
|
|
3659
|
+
frame: segmentLocalFrame + Math.round(accumulatedSeekOffsetSec * fps),
|
|
3660
|
+
fps,
|
|
3661
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
3662
|
+
isHeadless: props.isHeadless
|
|
3663
|
+
});
|
|
3664
|
+
else if (mediaType === "GIF") await drawGifNode(ctx, pass, {
|
|
3665
|
+
src: params.sourceUrl,
|
|
3666
|
+
dstRect: {
|
|
3667
|
+
x: 0,
|
|
3668
|
+
y: 0,
|
|
3669
|
+
width: containerWidth,
|
|
3670
|
+
height: containerHeight
|
|
3671
|
+
},
|
|
3672
|
+
frame: segmentLocalFrame + Math.round(accumulatedSeekOffsetSec * fps),
|
|
3673
|
+
fps,
|
|
3674
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
3675
|
+
isHeadless: props.isHeadless
|
|
3676
|
+
});
|
|
3677
|
+
else if (mediaType === "Text") {
|
|
3678
|
+
const opText = { ...op };
|
|
3679
|
+
drawParagraphNode(ctx, pass, {
|
|
3680
|
+
text: opText.text || "",
|
|
3681
|
+
dstRect: {
|
|
3682
|
+
x: Number(opText.x ?? 0),
|
|
3683
|
+
y: Number(opText.y ?? 0),
|
|
3684
|
+
width: Number(opText.width ?? containerWidth),
|
|
3685
|
+
height: Number(opText.height ?? containerHeight)
|
|
3686
|
+
},
|
|
3687
|
+
fontFamily: opText.fontFamily,
|
|
3688
|
+
fontSize: Number(opText.fontSize ?? 48),
|
|
3689
|
+
color: opText.fill ?? opText.color ?? "white",
|
|
3690
|
+
width: opText.width ? Number(opText.width) : void 0,
|
|
3691
|
+
height: opText.height ? Number(opText.height) : void 0,
|
|
3692
|
+
lineHeight: Number(opText.lineHeight ?? 1.2),
|
|
3693
|
+
align: opText.align,
|
|
3694
|
+
fontWeight: opText.fontWeight,
|
|
3695
|
+
fontStyle: opText.fontStyle,
|
|
3696
|
+
letterSpacing: opText.letterSpacing,
|
|
3697
|
+
shadows: opText.shadows,
|
|
3698
|
+
textBackgroundColor: opText.textBackgroundColor,
|
|
3699
|
+
borderRadius: opText.borderRadius != null ? Number(opText.borderRadius) : void 0,
|
|
3700
|
+
padding: opText.padding != null ? Number(opText.padding) : void 0,
|
|
3701
|
+
stroke: opText.stroke,
|
|
3702
|
+
strokeWidth: opText.strokeWidth ?? opText.strokeThickness ? Number(opText.strokeWidth ?? opText.strokeThickness) : void 0,
|
|
3703
|
+
strokeAlign: opText.strokeAlign,
|
|
3704
|
+
opacity: Number(opText.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
3705
|
+
isHeadless: props.isHeadless,
|
|
3706
|
+
animation: opText.animation ?? opText.textAnimation,
|
|
3707
|
+
animators: opText.animators,
|
|
3708
|
+
pathOptions: opText.pathOptions,
|
|
3709
|
+
typewriter: opText.typewriter,
|
|
3710
|
+
marquee: opText.marquee,
|
|
3711
|
+
textProgress: opText.textProgress != null ? Number(opText.textProgress) : void 0,
|
|
3712
|
+
signals: props.signals ?? opText.signals,
|
|
3713
|
+
firstMargin: opText.firstMargin != null ? Number(opText.firstMargin) : void 0,
|
|
3714
|
+
lastMargin: opText.lastMargin != null ? Number(opText.lastMargin) : void 0,
|
|
3715
|
+
baselineShift: opText.baselineShift != null ? Number(opText.baselineShift) : void 0,
|
|
3716
|
+
frame: localFrame,
|
|
3717
|
+
fps,
|
|
3718
|
+
durationMs: resolvedDurationMs,
|
|
3719
|
+
elapsedMs: resolvedElapsedMs,
|
|
3720
|
+
isVideoMode: props.isVideoMode
|
|
3721
|
+
});
|
|
3722
|
+
} else if (mediaType === "Video") {
|
|
3723
|
+
const finalPlaybackRate = Math.max(.01, (Number(props.playbackRateOverride) || 1) * (Number(params.speed) || 1));
|
|
3724
|
+
const effectiveTrimSec = Math.max(0, (Number(props.trimStartOverride) || 0) + (Number(params.trimStartSec) || 0) + accumulatedSeekOffsetSec);
|
|
3725
|
+
const rawTimestampSec = segmentLocalFrame / fps * finalPlaybackRate + effectiveTrimSec;
|
|
3726
|
+
const timestampSec = Math.max(0, Number.isFinite(rawTimestampSec) ? rawTimestampSec : 0);
|
|
3727
|
+
await drawVideoNode(ctx, pass, {
|
|
3728
|
+
nodeId: props.renderId,
|
|
3729
|
+
frameKey: `${props.renderId ?? "video"}-${params.sourceUrl}-${Math.round(timestampSec * fps)}`,
|
|
3730
|
+
sourceUrl: params.sourceUrl,
|
|
3731
|
+
timestampSec,
|
|
3732
|
+
dstRect: {
|
|
3733
|
+
x: 0,
|
|
3734
|
+
y: 0,
|
|
3735
|
+
width: containerWidth,
|
|
3736
|
+
height: containerHeight
|
|
3737
|
+
},
|
|
3738
|
+
opacity: Number(op.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
3739
|
+
isHeadless: props.isHeadless,
|
|
3740
|
+
isPlaying: props.isPlaying,
|
|
3741
|
+
forceWait: props.forceWait
|
|
3742
|
+
});
|
|
3743
|
+
} else if (mediaType === "Caption") {
|
|
3744
|
+
const opCap = {
|
|
3745
|
+
...op,
|
|
3746
|
+
...props
|
|
3747
|
+
};
|
|
3748
|
+
await drawCaptionNode(ctx, pass, {
|
|
3749
|
+
renderId: props.renderId,
|
|
3750
|
+
src: params.sourceUrl,
|
|
3751
|
+
dstRect: {
|
|
3752
|
+
x: 0,
|
|
3753
|
+
y: 0,
|
|
3754
|
+
width: containerWidth,
|
|
3755
|
+
height: containerHeight
|
|
3756
|
+
},
|
|
3757
|
+
frame: localFrame,
|
|
3758
|
+
fps,
|
|
3759
|
+
fontFamily: opCap.fontFamily,
|
|
3760
|
+
fontSize: Number(opCap.fontSize ?? 48),
|
|
3761
|
+
color: opCap.fill ?? opCap.color ?? "white",
|
|
3762
|
+
lineHeight: Number(opCap.lineHeight ?? 1.2),
|
|
3763
|
+
align: opCap.align === "start" ? "left" : opCap.align === "end" ? "right" : opCap.align ?? "center",
|
|
3764
|
+
textAlignVertical: opCap.verticalAlign ?? opCap.textAlignVertical,
|
|
3765
|
+
fontWeight: opCap.fontWeight,
|
|
3766
|
+
fontStyle: opCap.fontStyle,
|
|
3767
|
+
letterSpacing: opCap.letterSpacing,
|
|
3768
|
+
shadows: opCap.shadows,
|
|
3769
|
+
textBackgroundColor: opCap.background ?? opCap.textBackgroundColor,
|
|
3770
|
+
borderRadius: (opCap.borderRadius ?? opCap.strokeRadius) != null ? Number(opCap.borderRadius ?? opCap.strokeRadius) : void 0,
|
|
3771
|
+
strokeRadius: (opCap.strokeRadius ?? opCap.borderRadius) != null ? Number(opCap.strokeRadius ?? opCap.borderRadius) : void 0,
|
|
3772
|
+
stroke: opCap.stroke,
|
|
3773
|
+
strokeWidth: (opCap.strokeWidth ?? opCap.strokeThickness) != null ? Number(opCap.strokeWidth ?? opCap.strokeThickness) : void 0,
|
|
3774
|
+
maxWidth: opCap.maxWidth,
|
|
3775
|
+
padding: opCap.padding != null ? Number(opCap.padding) : void 0,
|
|
3776
|
+
opacity: Number(opCap.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
3777
|
+
isHeadless: props.isHeadless,
|
|
3778
|
+
isVideoMode: props.isVideoMode
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
} else if (op.op === "text") {
|
|
3783
|
+
const opText = { ...op };
|
|
3784
|
+
const containerWidth = props.containerWidth ?? ctx.surface.width;
|
|
3785
|
+
const containerHeight = props.containerHeight ?? ctx.surface.height;
|
|
3786
|
+
drawParagraphNode(ctx, pass, {
|
|
3787
|
+
renderId: props.renderId,
|
|
3788
|
+
text: opText.text || "",
|
|
3789
|
+
dstRect: {
|
|
3790
|
+
x: Number(opText.x ?? 0),
|
|
3791
|
+
y: Number(opText.y ?? 0),
|
|
3792
|
+
width: Number(opText.width ?? containerWidth),
|
|
3793
|
+
height: Number(opText.height ?? containerHeight)
|
|
3794
|
+
},
|
|
3795
|
+
spans: opText.spans,
|
|
3796
|
+
fontFamily: opText.fontFamily,
|
|
3797
|
+
fontSize: Number(opText.fontSize ?? 48),
|
|
3798
|
+
color: opText.fill ?? opText.color ?? "white",
|
|
3799
|
+
width: opText.width ? Number(opText.width) : void 0,
|
|
3800
|
+
height: opText.height ? Number(opText.height) : void 0,
|
|
3801
|
+
lineHeight: Number(opText.lineHeight ?? 1.2),
|
|
3802
|
+
align: opText.align,
|
|
3803
|
+
fontWeight: opText.fontWeight,
|
|
3804
|
+
fontStyle: opText.fontStyle,
|
|
3805
|
+
letterSpacing: opText.letterSpacing,
|
|
3806
|
+
shadows: opText.shadows,
|
|
3807
|
+
textBackgroundColor: opText.textBackgroundColor,
|
|
3808
|
+
borderRadius: (opText.borderRadius ?? opText.strokeRadius) != null ? Number(opText.borderRadius ?? opText.strokeRadius) : void 0,
|
|
3809
|
+
strokeRadius: (opText.strokeRadius ?? opText.borderRadius) != null ? Number(opText.strokeRadius ?? opText.borderRadius) : void 0,
|
|
3810
|
+
padding: opText.padding != null ? Number(opText.padding) : void 0,
|
|
3811
|
+
stroke: opText.stroke,
|
|
3812
|
+
strokeWidth: opText.strokeWidth ?? opText.strokeThickness ? Number(opText.strokeWidth ?? opText.strokeThickness) : void 0,
|
|
3813
|
+
strokeAlign: opText.strokeAlign,
|
|
3814
|
+
opacity: Number(opText.opacity ?? 1) * Number(props.opacity ?? 1),
|
|
3815
|
+
isHeadless: props.isHeadless,
|
|
3816
|
+
animation: opText.animation ?? opText.textAnimation,
|
|
3817
|
+
animators: opText.animators,
|
|
3818
|
+
pathOptions: opText.pathOptions,
|
|
3819
|
+
typewriter: opText.typewriter,
|
|
3820
|
+
marquee: opText.marquee,
|
|
3821
|
+
signals: props.signals ?? opText.signals,
|
|
3822
|
+
firstMargin: opText.firstMargin != null ? Number(opText.firstMargin) : void 0,
|
|
3823
|
+
lastMargin: opText.lastMargin != null ? Number(opText.lastMargin) : void 0,
|
|
3824
|
+
baselineShift: opText.baselineShift != null ? Number(opText.baselineShift) : void 0,
|
|
3825
|
+
frame: localFrame,
|
|
3826
|
+
fps,
|
|
3827
|
+
durationMs: resolvedDurationMs,
|
|
3828
|
+
elapsedMs: resolvedElapsedMs,
|
|
3829
|
+
isVideoMode: props.isVideoMode
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
pass.end();
|
|
3833
|
+
}
|
|
3834
|
+
if (virtualMedia.children && virtualMedia.children.length > 0) for (let i = 0; i < virtualMedia.children.length; i++) {
|
|
3835
|
+
const child = virtualMedia.children[i];
|
|
3836
|
+
if (child) await drawCompositionTree(ctx, encoder, targetView, targetTexture, targetWidth, targetHeight, child, {
|
|
3837
|
+
...props,
|
|
3838
|
+
durationMs: resolvedDurationMs,
|
|
3839
|
+
elapsedMs: resolvedElapsedMs,
|
|
3840
|
+
virtualMedia: child,
|
|
3841
|
+
renderId: `${props.renderId}-c${i}`
|
|
3842
|
+
}, isCompositor ? 0 : accumulatedSeekOffsetSec, accumulatedClockOffsetSec);
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
const CanvasComposition = memo((props) => {
|
|
3846
|
+
const ctx = useRenderContext();
|
|
3847
|
+
const { renderId, virtualMedia: rawVirtualMedia } = props;
|
|
3848
|
+
const state = useCompositionState(renderId);
|
|
3849
|
+
const { virtualMedia, fps } = React.useMemo(() => {
|
|
3850
|
+
const fpsValue = props.fps ?? state.fps;
|
|
3851
|
+
return {
|
|
3852
|
+
virtualMedia: normalizeTimeline(rawVirtualMedia, fpsValue),
|
|
3853
|
+
fps: fpsValue
|
|
3854
|
+
};
|
|
3855
|
+
}, [
|
|
3856
|
+
rawVirtualMedia,
|
|
3857
|
+
state.fps,
|
|
3858
|
+
props.fps
|
|
3859
|
+
]);
|
|
3860
|
+
const frame = props.frame ?? state.frame;
|
|
3861
|
+
const isMountedRef = React.useRef(false);
|
|
3862
|
+
const latestTaskRef = React.useRef(null);
|
|
3863
|
+
const isRenderingRef = React.useRef(false);
|
|
3864
|
+
const [fontUpdateTick, forceUpdate] = React.useState(0);
|
|
3865
|
+
React.useEffect(() => {
|
|
3866
|
+
return SlugFontCache.addListener(() => {
|
|
3867
|
+
if (isMountedRef.current) forceUpdate((t) => t + 1);
|
|
3868
|
+
});
|
|
3869
|
+
}, []);
|
|
3870
|
+
React.useEffect(() => {
|
|
3871
|
+
const cdnDomain = getEnv("R2_CUSTOM_DOMAIN");
|
|
3872
|
+
if (!cdnDomain) {
|
|
3873
|
+
console.error("[CanvasComposition] R2_CUSTOM_DOMAIN is not defined.");
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
const url = `https://${cdnDomain}/static/NotoColorEmoji.ttf`;
|
|
3877
|
+
SlugFontCache.preloadEmojiFontFace(url).catch((err) => {
|
|
3878
|
+
console.error("[CanvasComposition] Failed to preload emoji font:", err);
|
|
3879
|
+
});
|
|
3880
|
+
}, []);
|
|
3881
|
+
const [lutUpdateTick, setLutUpdateTick] = React.useState(0);
|
|
3882
|
+
React.useEffect(() => {
|
|
3883
|
+
return lutStore.onChange(() => {
|
|
3884
|
+
if (isMountedRef.current) setLutUpdateTick((t) => t + 1);
|
|
3885
|
+
});
|
|
3886
|
+
}, []);
|
|
3887
|
+
React.useLayoutEffect(() => {
|
|
3888
|
+
isMountedRef.current = true;
|
|
3889
|
+
return () => {
|
|
3890
|
+
isMountedRef.current = false;
|
|
3891
|
+
mediaDecoderCache.clearNode(renderId);
|
|
3892
|
+
clearVideoNodeState(renderId, ctx?.device);
|
|
3893
|
+
if (typeof globalThis !== "undefined" && globalThis.__GATEWAI_DELAYS__) {
|
|
3894
|
+
const delays = globalThis.__GATEWAI_DELAYS__;
|
|
3895
|
+
if (delays instanceof Set) {
|
|
3896
|
+
for (const handle of delays) if (typeof handle === "string" && handle.startsWith(renderId)) delays.delete(handle);
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
if (typeof window !== "undefined" && window.renderer_delayRenderHandles) {
|
|
3900
|
+
const handles = window.renderer_delayRenderHandles;
|
|
3901
|
+
if (Array.isArray(handles)) {
|
|
3902
|
+
const filtered = handles.filter((handle) => !(typeof handle === "string" && handle.startsWith(renderId)));
|
|
3903
|
+
window.renderer_delayRenderHandles = filtered;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
};
|
|
3907
|
+
}, [renderId, ctx]);
|
|
3908
|
+
React.useLayoutEffect(() => {
|
|
3909
|
+
if (!ctx || !virtualMedia) return;
|
|
3910
|
+
latestTaskRef.current = {
|
|
3911
|
+
ctx,
|
|
3912
|
+
virtualMedia,
|
|
3913
|
+
frame,
|
|
3914
|
+
fps,
|
|
3915
|
+
props
|
|
3916
|
+
};
|
|
3917
|
+
const triggerRender = async () => {
|
|
3918
|
+
if (isRenderingRef.current) return;
|
|
3919
|
+
isRenderingRef.current = true;
|
|
3920
|
+
while (latestTaskRef.current && isMountedRef.current) {
|
|
3921
|
+
const task = latestTaskRef.current;
|
|
3922
|
+
if (task.ctx !== ctx) break;
|
|
3923
|
+
latestTaskRef.current = null;
|
|
3924
|
+
try {
|
|
3925
|
+
const { device, surface, renderer } = task.ctx;
|
|
3926
|
+
const targetW = surface.width;
|
|
3927
|
+
const targetH = surface.height;
|
|
3928
|
+
if (targetW === 0 || targetH === 0) continue;
|
|
3929
|
+
const encoder = device.createCommandEncoder();
|
|
3930
|
+
const stableTex = renderer.getTemporaryTexture(targetW, targetH);
|
|
3931
|
+
const stableView = stableTex.createView();
|
|
3932
|
+
renderer.beginFrame(encoder, stableView, {
|
|
3933
|
+
r: 0,
|
|
3934
|
+
g: 0,
|
|
3935
|
+
b: 0,
|
|
3936
|
+
a: 0
|
|
3937
|
+
}, targetW, targetH, "clear").end();
|
|
3938
|
+
await drawCompositionTree(task.ctx, encoder, stableView, stableTex, targetW, targetH, task.virtualMedia, {
|
|
3939
|
+
...task.props,
|
|
3940
|
+
frame: task.frame,
|
|
3941
|
+
fps: task.fps,
|
|
3942
|
+
isPlaying: task.props.isPlaying ?? state.isPlaying,
|
|
3943
|
+
isVideoMode: task.virtualMedia.operation.dataType === "Video",
|
|
3944
|
+
excludeTextures: [stableTex]
|
|
3945
|
+
});
|
|
3946
|
+
if (latestTaskRef.current !== null) continue;
|
|
3947
|
+
if (isMountedRef.current && !renderer.isDestroyed) {
|
|
3948
|
+
const blitW = surface.width;
|
|
3949
|
+
const blitH = surface.height;
|
|
3950
|
+
if (blitW > 0 && blitH > 0) {
|
|
3951
|
+
const canvasView = surface.getCurrentTextureView();
|
|
3952
|
+
const blitPass = renderer.beginFrame(encoder, canvasView, {
|
|
3953
|
+
r: 0,
|
|
3954
|
+
g: 0,
|
|
3955
|
+
b: 0,
|
|
3956
|
+
a: 0
|
|
3957
|
+
}, blitW, blitH, "clear");
|
|
3958
|
+
renderer.drawTexture(blitPass, stableTex, {
|
|
3959
|
+
x: 0,
|
|
3960
|
+
y: 0,
|
|
3961
|
+
width: blitW,
|
|
3962
|
+
height: blitH
|
|
3963
|
+
});
|
|
3964
|
+
blitPass.end();
|
|
3965
|
+
}
|
|
3966
|
+
device.queue.submit([encoder.finish()]);
|
|
3967
|
+
surface.present();
|
|
3968
|
+
const key = `${task.props.renderId}-${task.frame}`;
|
|
3969
|
+
if (!window.__frameRenderResolvers) window.__frameRenderResolvers = /* @__PURE__ */ new Map();
|
|
3970
|
+
const status = window.__frameRenderResolvers.get(key);
|
|
3971
|
+
if (status) {
|
|
3972
|
+
status.rendered = true;
|
|
3973
|
+
if (status.resolver) status.resolver();
|
|
3974
|
+
} else window.__frameRenderResolvers.set(key, { rendered: true });
|
|
3975
|
+
}
|
|
3976
|
+
} catch (err) {
|
|
3977
|
+
console.error("Error drawing WebGPU composition tree:", err);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
isRenderingRef.current = false;
|
|
3981
|
+
if (latestTaskRef.current && isMountedRef.current) triggerRender();
|
|
3982
|
+
};
|
|
3983
|
+
triggerRender();
|
|
3984
|
+
}, [
|
|
3985
|
+
ctx,
|
|
3986
|
+
virtualMedia,
|
|
3987
|
+
frame,
|
|
3988
|
+
fps,
|
|
3989
|
+
props,
|
|
3990
|
+
fontUpdateTick,
|
|
3991
|
+
lutUpdateTick
|
|
3992
|
+
]);
|
|
3993
|
+
return null;
|
|
3994
|
+
});
|
|
3995
|
+
/**
|
|
3996
|
+
* browser-video-renderer.tsx
|
|
3997
|
+
*
|
|
3998
|
+
* Architecture
|
|
3999
|
+
* ────────────
|
|
4000
|
+
* ┌─────────────────────────────────────────────────────────────────┐
|
|
4001
|
+
* │ AudioMuxer (no React deps) │
|
|
4002
|
+
* │ · subscribe / getSnapshot → useSyncExternalStore contract │
|
|
4003
|
+
* │ · run() → mixing + encoding with cooperative yielding │
|
|
4004
|
+
* │ · abort() → AbortSignal cancellation │
|
|
4005
|
+
* └─────────────────────────────────────────────────────────────────┘
|
|
4006
|
+
* ▲ useSyncExternalStore
|
|
4007
|
+
* ┌─────────────────────────────────────────────────────────────────┐
|
|
4008
|
+
* │ BrowserVideoRendererInner │
|
|
4009
|
+
* │ Effect 1: muxer init (output, sources, OPFS target) │
|
|
4010
|
+
* │ Effect 2: video frame loop → setVideoFramesDone(true) │
|
|
4011
|
+
* │ Effect 3: audio kick-off when videoFramesDone │
|
|
4012
|
+
* │ Effect 4: finalization when audioState.phase === "done" │
|
|
4013
|
+
* │ Effect 5: progress bridge audioState → onProgress prop │
|
|
4014
|
+
* │ Effect 6: error bridge audioState → onError prop │
|
|
4015
|
+
* └─────────────────────────────────────────────────────────────────┘
|
|
4016
|
+
*
|
|
4017
|
+
* Why FPS dropped to 2-3 during audio processing (and how it's fixed)
|
|
4018
|
+
* ────────────────────────────────────────────────────────────────────
|
|
4019
|
+
*
|
|
4020
|
+
* ROOT CAUSE 1 — Main-thread monopoly
|
|
4021
|
+
* The old audio chunk loop ran entirely on the JS main thread. Each
|
|
4022
|
+
* iteration allocated a Float32Array, interleaved channel data, and
|
|
4023
|
+
* called `await audioSource.add()`. Consecutive microtask continuations
|
|
4024
|
+
* never returned control to the browser's rendering pipeline; the
|
|
4025
|
+
* compositor thread was starved waiting for a JS task to complete before
|
|
4026
|
+
* it could issue a new paint frame.
|
|
4027
|
+
*
|
|
4028
|
+
* ROOT CAUSE 2 — React scheduler saturation
|
|
4029
|
+
* `onProgress("audio", pct)` was called on every chunk (up to ~100×/sec
|
|
4030
|
+
* for long clips). Each call triggered a parent setState, enqueuing a
|
|
4031
|
+
* React reconciliation. When updates arrived faster than the reconciler
|
|
4032
|
+
* could drain them, React degraded to synchronous rendering to clear the
|
|
4033
|
+
* backlog — which blocked the main thread even longer.
|
|
4034
|
+
*
|
|
4035
|
+
* ROOT CAUSE 3 — AudioEncoder backpressure
|
|
4036
|
+
* mediabunny's AudioSampleSource.add() awaits the encoder's internal
|
|
4037
|
+
* queue. Under load it parks the caller in a microtask continuation and
|
|
4038
|
+
* immediately resumes on the same task. The browser task scheduler never
|
|
4039
|
+
* saw an idle slot to run its rendering pipeline.
|
|
4040
|
+
*
|
|
4041
|
+
* ROOT CAUSE 4 — Entangled concerns
|
|
4042
|
+
* Audio encoding lived inside the video frame useEffect. The render loop
|
|
4043
|
+
* and the encoder competed for the same task slot and the same React
|
|
4044
|
+
* reconciliation pass.
|
|
4045
|
+
*
|
|
4046
|
+
* FIX — yieldToMain() between every audio chunk
|
|
4047
|
+
* `scheduler.yield()` (Chrome 115+) is the correct primitive: it
|
|
4048
|
+
* explicitly yields to the browser's task queue at "user-blocking"
|
|
4049
|
+
* priority, guaranteeing a paint opportunity between chunks. The
|
|
4050
|
+
* `setTimeout(0)` fallback provides equivalent semantics on older
|
|
4051
|
+
* browsers (>4 ms clamped, sufficient for ~60 FPS).
|
|
4052
|
+
* Progress notifications are throttled to ≤12 updates/sec to avoid
|
|
4053
|
+
* overwhelming the React scheduler.
|
|
4054
|
+
*/
|
|
4055
|
+
const isNode = typeof process !== "undefined" && Boolean(process.versions?.node);
|
|
4056
|
+
if (typeof window !== "undefined" && !isNode) canEncodeAudio("mp3").then((supported) => {
|
|
4057
|
+
if (!supported) registerMp3Encoder();
|
|
4058
|
+
}).catch(() => {});
|
|
4059
|
+
|
|
4060
|
+
//#endregion
|
|
4061
|
+
export { DataTypes as $, frameArraySignal as $t, ImageResultSchema as A, TextOperationSchema as At, BaseMediaOperationPropsSchema as B, VideoTimingPropsSchema as Bt, preloadFont as C, SignalDataSchema as Ct, useCurrentFrame as D, SingleOutputGenericSchema as Dt, springCalculation as E, SignalStatsSchema as Et, defineRenderer as F, VideoAnimationSchema as Ft, ConfigHandleSchema as G, computed as Gt, CAPTION_LAYER_DEFAULTS as H, appendOperation as Ht, registerWebGPURenderer as I, VideoLayerTypeSchema as It, DEFAULT_DURATION_MS as J, createVirtualMedia as Jt, ConfigSchemaRegistry as K, cosineSignal as Kt, AnimationTypeSchema as L, VideoSpatialPropsSchema as Lt, VideoResultSchema as M, TimelineTransformSchema as Mt, configBuilder as N, TransitionSchema as Nt, useVideoConfig as O, SourceOperationSchema as Ot, audioRegistry as P, TransitionTypeSchema as Pt, DataTypeVal as Q, findSourceAsset as Qt, AnyOutputUnionSchema as R, VideoStylePropsSchema as Rt, parseColorToRgb as S, updateClockSignals as Sn, SignalChannelMetaSchema as St, spring as T, SignalMathDataSchema as Tt, COMPOSITE_OPERATIONS$1 as U, checkNodeResultCache as Ut, BaseVideoLayerPropsSchema as V, VirtualMediaDataSchema as Vt, COMPOSITOR_DEFAULTS as W, computeVideoCropRenderProps as Wt, DEFAULT_MEDIA_DIMENSION as X, deserializeSignalData as Xt, DEFAULT_FPS as Y, dataTypeColors as Yt, DataType as Z, extractSvgDimensions as Zt, interpolate as _, signal as _n, NodeResultSchema as _t, mixAudioTracks as a, getLeafMediaType as an, GeneratorSignalDataSchema as at, measureText as b, stampResult as bn, ProgrammaticSignal as bt, LIGHT_UNIFORM_SIZE_BYTES as c, hasOnlySingleSource as cn, JsonValueSchema as ct, PlaybackContext as d, isVirtualMediaData as dn, MediaOperationSchema as dt, frameSignal as en, Effect as et, PlaybackProvider as f, programmaticSignal as fn, MediaSourceSchema as ft, hasDelayRender as g, serializeSignalData as gn, NodeMetadataSchema as gt, computeStrokeRect as h, resolveMediaMimeType as hn, MultiOutputGenericSchema as ht, drawCompositionTree as i, getFingerprint as in, FileDataSchema as it, SignalResultSchema as j, TimelineSegmentSchema as jt, ColorSchema as k, TEXT_LAYER_DEFAULTS as kt, MAX_LIGHTS as l, isEqual as ln, MIME_TYPES as lt, bezier as m, registerSignalDependency as mn, ModerationUnrefundableError as mt, compositionStateStore as n, getActiveMediaMetadata as nn, ExtendedLayerSchema as nt, useCompositionState as o, getMediaType as on, GlobalCompositeOperation$1 as ot, Vector3Math as p, progressSignal as pn, ModerationError as pt, DATA_TYPE_EXTENSIONS as q, createOutputItemSchema as qt, decodeAudioSource as r, getFileDataMetadata as rn, FileAssetSchema as rt, Easing as s, getRenderFingerprint as sn, HandleTypeVal as st, CanvasComposition as t, generateId as tn, ExportResultSchema as tt, Matrix4Math as u, isSignal as un, MediaMetadataSchema as ut, mapBlendModeToSkia as v, signalBuilder as vn, NodeVisualConfigSchema as vt, requiresIsolation as w, SignalGateDataSchema as wt, measureTextAsync as x, timeSignal as xn, Signal as xt, measureSpring as y, sineSignal as yn, OutputItemSchema as yt, AudioExtractorSignalDataSchema as z, VideoTextPropsSchema as zt };
|
|
4062
|
+
//# sourceMappingURL=dist-CMF9eNLB.mjs.map
|