@kitsra/kavio-core 0.1.0
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/LICENSE +94 -0
- package/dist/index.d.ts +329 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1455 -0
- package/package.json +30 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
export const MAX_COMPOSITION_FRAMES = 216_000;
|
|
2
|
+
export const MAX_CANVAS_WIDTH = 3_840;
|
|
3
|
+
export const MAX_CANVAS_HEIGHT = 2_160;
|
|
4
|
+
export const MAX_LAYERS = 512;
|
|
5
|
+
export const MAX_ASSETS = 64;
|
|
6
|
+
export const MAX_PROP_STRING_LENGTH = 4_096;
|
|
7
|
+
export const MAX_ASSET_BYTES = 500 * 1024 * 1024;
|
|
8
|
+
export const MAX_SOURCE_WIDTH = 3_840;
|
|
9
|
+
export const MAX_SOURCE_HEIGHT = 2_160;
|
|
10
|
+
export const MAX_BLUR_RADIUS = 48;
|
|
11
|
+
export const MAX_FILTERED_LAYERS = 16;
|
|
12
|
+
export const MAX_MASKED_LAYERS = 32;
|
|
13
|
+
export const MAX_MASK_SOURCE_WIDTH = 2_048;
|
|
14
|
+
export const MAX_MASK_SOURCE_HEIGHT = 2_048;
|
|
15
|
+
export const MAX_TEXT_MOTION_FRAGMENTS = 1_000;
|
|
16
|
+
export const MAX_PROCEDURAL_MASK_PIXELS = 2_073_600;
|
|
17
|
+
export const MAX_TRANSITION_DURATION_FRAMES = 180;
|
|
18
|
+
export const DEFAULT_RESOURCE_LIMITS = {
|
|
19
|
+
maxFrames: MAX_COMPOSITION_FRAMES,
|
|
20
|
+
maxWidth: MAX_CANVAS_WIDTH,
|
|
21
|
+
maxHeight: MAX_CANVAS_HEIGHT,
|
|
22
|
+
maxLayers: MAX_LAYERS,
|
|
23
|
+
maxAssets: MAX_ASSETS,
|
|
24
|
+
maxPropStringLength: MAX_PROP_STRING_LENGTH,
|
|
25
|
+
maxAssetBytes: MAX_ASSET_BYTES,
|
|
26
|
+
maxSourceWidth: MAX_SOURCE_WIDTH,
|
|
27
|
+
maxSourceHeight: MAX_SOURCE_HEIGHT,
|
|
28
|
+
maxBlurRadius: MAX_BLUR_RADIUS,
|
|
29
|
+
maxFilteredLayers: MAX_FILTERED_LAYERS,
|
|
30
|
+
maxMaskedLayers: MAX_MASKED_LAYERS,
|
|
31
|
+
maxMaskSourceWidth: MAX_MASK_SOURCE_WIDTH,
|
|
32
|
+
maxMaskSourceHeight: MAX_MASK_SOURCE_HEIGHT,
|
|
33
|
+
maxTextMotionFragments: MAX_TEXT_MOTION_FRAGMENTS,
|
|
34
|
+
maxProceduralMaskPixels: MAX_PROCEDURAL_MASK_PIXELS,
|
|
35
|
+
maxTransitionDurationFrames: MAX_TRANSITION_DURATION_FRAMES
|
|
36
|
+
};
|
|
37
|
+
const TEMPLATE_PATTERN = /{{\s*([A-Za-z0-9_.-]+)\s*}}/g;
|
|
38
|
+
const WHOLE_TEMPLATE_PATTERN = /^{{\s*([A-Za-z0-9_.-]+)\s*}}$/;
|
|
39
|
+
const PERCENT_PATTERN = /^\s*(-?(?:\d+|\d*\.\d+))%(w|h)?\s*$/;
|
|
40
|
+
const CUBIC_BEZIER_PATTERN = /^cubic-bezier\(\s*(-?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?)\s*,\s*(-?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?)\s*,\s*(-?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?)\s*,\s*(-?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?)\s*\)$/i;
|
|
41
|
+
const BACK_OVERSHOOT = 1.70158;
|
|
42
|
+
const ELASTIC_PERIOD = (2 * Math.PI) / 3;
|
|
43
|
+
export function resolveDocumentProps(document, values = {}) {
|
|
44
|
+
const declarations = isRecord(document.props) ? document.props : {};
|
|
45
|
+
const propValues = mergePropValues(declarations, values);
|
|
46
|
+
return resolveTemplateProps(document, propValues, { skipRootProps: true });
|
|
47
|
+
}
|
|
48
|
+
export function applyExportPreset(document, presetInput) {
|
|
49
|
+
const preset = resolveExportPreset(document, presetInput);
|
|
50
|
+
const layerOverrides = preset.layerOverrides ?? {};
|
|
51
|
+
const exportDocument = cloneJsonValue(document);
|
|
52
|
+
return {
|
|
53
|
+
...exportDocument,
|
|
54
|
+
composition: {
|
|
55
|
+
...exportDocument.composition,
|
|
56
|
+
width: preset.width,
|
|
57
|
+
height: preset.height
|
|
58
|
+
},
|
|
59
|
+
layers: exportDocument.layers.map((layer) => applyLayerOverride(layer, layerOverrides[layer.id])),
|
|
60
|
+
exports: [cloneJsonValue(preset)]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export const createExportView = applyExportPreset;
|
|
64
|
+
export function resolveExportPreset(document, presetInput) {
|
|
65
|
+
if (typeof presetInput !== "string") {
|
|
66
|
+
return presetInput;
|
|
67
|
+
}
|
|
68
|
+
const preset = document.exports.find((candidate) => candidate.name === presetInput);
|
|
69
|
+
if (!preset) {
|
|
70
|
+
throw new Error(`Unknown export preset "${presetInput}".`);
|
|
71
|
+
}
|
|
72
|
+
return preset;
|
|
73
|
+
}
|
|
74
|
+
export function resolveTemplateProps(value, values, options = {}) {
|
|
75
|
+
const errors = [];
|
|
76
|
+
const resolved = resolveUnknown(value, values, errors, "", options.skipRootProps === true, true);
|
|
77
|
+
return {
|
|
78
|
+
ok: errors.length === 0,
|
|
79
|
+
value: resolved,
|
|
80
|
+
errors
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function isLayerActive(layer, frame) {
|
|
84
|
+
return frame >= layer.startFrame && frame < layer.startFrame + layer.durationFrames;
|
|
85
|
+
}
|
|
86
|
+
export function getLocalFrame(layer, frame) {
|
|
87
|
+
return frame - layer.startFrame;
|
|
88
|
+
}
|
|
89
|
+
export function evaluateLayer(layer, frame, dimensions) {
|
|
90
|
+
const localFrame = getLocalFrame(layer, frame);
|
|
91
|
+
const visible = isLayerActive(layer, frame);
|
|
92
|
+
const position = resolvePoint(layer.position, dimensions);
|
|
93
|
+
const keyframes = layer.keyframes ?? {};
|
|
94
|
+
const transition = evaluateLayerTransitions(layer, localFrame, dimensions);
|
|
95
|
+
const x = evaluateKeyframes(keyframes.x, localFrame, position.x) + transition.offset.x;
|
|
96
|
+
const y = evaluateKeyframes(keyframes.y, localFrame, position.y) + transition.offset.y;
|
|
97
|
+
const size = resolveSize(layer.size, dimensions);
|
|
98
|
+
const anchor = resolveAnchor(layer.anchor);
|
|
99
|
+
const anchorOffset = {
|
|
100
|
+
x: size.width === null ? null : size.width * anchor.x,
|
|
101
|
+
y: size.height === null ? null : size.height * anchor.y
|
|
102
|
+
};
|
|
103
|
+
const evaluated = {
|
|
104
|
+
id: layer.id,
|
|
105
|
+
type: layer.type,
|
|
106
|
+
localFrame,
|
|
107
|
+
visible,
|
|
108
|
+
position: { x, y },
|
|
109
|
+
anchor,
|
|
110
|
+
size,
|
|
111
|
+
anchorOffset,
|
|
112
|
+
topLeft: {
|
|
113
|
+
x: anchorOffset.x === null ? null : x - anchorOffset.x,
|
|
114
|
+
y: anchorOffset.y === null ? null : y - anchorOffset.y
|
|
115
|
+
},
|
|
116
|
+
opacity: clamp01(evaluateKeyframes(keyframes.opacity, localFrame, layer.opacity ?? 1) * transition.opacity),
|
|
117
|
+
rotation: evaluateKeyframes(keyframes.rotation, localFrame, layer.rotation ?? 0) + transition.rotation,
|
|
118
|
+
scale: evaluateKeyframes(keyframes.scale, localFrame, layer.scale ?? 1) * transition.scale
|
|
119
|
+
};
|
|
120
|
+
if (transition.reveal) {
|
|
121
|
+
evaluated.reveal = transition.reveal;
|
|
122
|
+
}
|
|
123
|
+
if (transition.revealShape) {
|
|
124
|
+
evaluated.revealShape = transition.revealShape;
|
|
125
|
+
}
|
|
126
|
+
if (transition.revealPattern) {
|
|
127
|
+
evaluated.revealPattern = transition.revealPattern;
|
|
128
|
+
}
|
|
129
|
+
if (transition.wash && transition.wash.opacity > 0) {
|
|
130
|
+
evaluated.wash = transition.wash;
|
|
131
|
+
}
|
|
132
|
+
if (transition.filter && transition.filter.blur !== undefined && transition.filter.blur > 0) {
|
|
133
|
+
evaluated.filter = transition.filter;
|
|
134
|
+
}
|
|
135
|
+
if (!isIdentityTransitionTransform(transition.transform)) {
|
|
136
|
+
evaluated.transform = transition.transform;
|
|
137
|
+
}
|
|
138
|
+
if (layer.mask) {
|
|
139
|
+
evaluated.mask = cloneJsonValue(layer.mask);
|
|
140
|
+
}
|
|
141
|
+
if (isCaptionTimelineLayer(layer)) {
|
|
142
|
+
evaluated.caption = visible
|
|
143
|
+
? evaluateCaptionState(layer.source, localFrame, { highlightMode: layer.style?.highlight?.mode })
|
|
144
|
+
: emptyCaptionState(layer.source.kind, localFrame, layer.style?.highlight?.mode ?? "none");
|
|
145
|
+
}
|
|
146
|
+
return evaluated;
|
|
147
|
+
}
|
|
148
|
+
export function evaluateActiveLayers(layers, frame, dimensions) {
|
|
149
|
+
return layers
|
|
150
|
+
.filter((layer) => isLayerActive(layer, frame))
|
|
151
|
+
.map((layer) => evaluateLayer(layer, frame, dimensions));
|
|
152
|
+
}
|
|
153
|
+
export function compileTransitionOverlapWindows(tracks) {
|
|
154
|
+
if (tracks === undefined) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
const windows = [];
|
|
158
|
+
for (const track of tracks) {
|
|
159
|
+
for (let index = 1; index < track.clips.length; index += 1) {
|
|
160
|
+
const nextClip = track.clips[index];
|
|
161
|
+
const previousClip = track.clips[index - 1];
|
|
162
|
+
if (!nextClip || !previousClip || nextClip.transitionFromPrevious === undefined) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const transition = normalizeTransitionSeriesDefinition(nextClip.transitionFromPrevious);
|
|
166
|
+
const startFrame = nextClip.startFrame;
|
|
167
|
+
const endFrame = startFrame + transition.durationFrames;
|
|
168
|
+
const previousEndFrame = previousClip.startFrame + previousClip.durationFrames;
|
|
169
|
+
const nextEndFrame = nextClip.startFrame + nextClip.durationFrames;
|
|
170
|
+
if (endFrame > previousEndFrame || endFrame > nextEndFrame) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
windows.push({
|
|
174
|
+
trackId: track.id,
|
|
175
|
+
previousClipId: previousClip.id,
|
|
176
|
+
previousLayerId: previousClip.layerId,
|
|
177
|
+
nextClipId: nextClip.id,
|
|
178
|
+
nextLayerId: nextClip.layerId,
|
|
179
|
+
startFrame,
|
|
180
|
+
endFrame,
|
|
181
|
+
durationFrames: transition.durationFrames,
|
|
182
|
+
transition
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return windows.sort((left, right) => left.startFrame - right.startFrame || left.endFrame - right.endFrame);
|
|
187
|
+
}
|
|
188
|
+
export function evaluateTransitionSeries(document, frame, dimensions = getCanvasDimensions(document.composition)) {
|
|
189
|
+
const windows = compileTransitionOverlapWindows(document.tracks);
|
|
190
|
+
const activeWindows = windows.filter((window) => frame >= window.startFrame && frame < window.endFrame);
|
|
191
|
+
if (activeWindows.length === 0) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const layersById = new Map(document.layers.map((layer) => [layer.id, layer]));
|
|
195
|
+
const clipsByWindowKey = transitionSeriesClipIndex(document.tracks);
|
|
196
|
+
const evaluated = [];
|
|
197
|
+
for (const window of activeWindows) {
|
|
198
|
+
const key = transitionWindowKey(window.trackId, window.nextClipId);
|
|
199
|
+
const clips = clipsByWindowKey.get(key);
|
|
200
|
+
const previousLayer = layersById.get(window.previousLayerId);
|
|
201
|
+
const nextLayer = layersById.get(window.nextLayerId);
|
|
202
|
+
if (clips === undefined || previousLayer === undefined || nextLayer === undefined) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const previous = evaluateTransitionSeriesClip(previousLayer, clips.previous, frame, dimensions, window, "previous");
|
|
206
|
+
const next = evaluateTransitionSeriesClip(nextLayer, clips.next, frame, dimensions, window, "next");
|
|
207
|
+
const progress = window.durationFrames === 1 ? 1 : (frame - window.startFrame) / (window.durationFrames - 1);
|
|
208
|
+
evaluated.push({
|
|
209
|
+
trackId: window.trackId,
|
|
210
|
+
previous,
|
|
211
|
+
next,
|
|
212
|
+
startFrame: window.startFrame,
|
|
213
|
+
endFrame: window.endFrame,
|
|
214
|
+
durationFrames: window.durationFrames,
|
|
215
|
+
progress,
|
|
216
|
+
easedProgress: evaluateEasing(window.transition.easing ?? "linear", progress),
|
|
217
|
+
transition: window.transition
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return evaluated;
|
|
221
|
+
}
|
|
222
|
+
export function normalizeTransitionSeriesDefinition(definition) {
|
|
223
|
+
const transition = {
|
|
224
|
+
type: definition.presentation.type,
|
|
225
|
+
durationFrames: definition.timing.durationFrames
|
|
226
|
+
};
|
|
227
|
+
if (definition.presentation.direction !== undefined) {
|
|
228
|
+
transition.direction = definition.presentation.direction;
|
|
229
|
+
}
|
|
230
|
+
if (definition.presentation.axis !== undefined) {
|
|
231
|
+
transition.axis = definition.presentation.axis;
|
|
232
|
+
}
|
|
233
|
+
if (definition.presentation.shape !== undefined) {
|
|
234
|
+
transition.shape = definition.presentation.shape;
|
|
235
|
+
}
|
|
236
|
+
if (definition.presentation.color !== undefined) {
|
|
237
|
+
transition.color = definition.presentation.color;
|
|
238
|
+
}
|
|
239
|
+
if (definition.presentation.amount !== undefined) {
|
|
240
|
+
transition.amount = definition.presentation.amount;
|
|
241
|
+
}
|
|
242
|
+
if (definition.presentation.intensity !== undefined) {
|
|
243
|
+
transition.intensity = definition.presentation.intensity;
|
|
244
|
+
}
|
|
245
|
+
if (definition.presentation.rows !== undefined) {
|
|
246
|
+
transition.rows = definition.presentation.rows;
|
|
247
|
+
}
|
|
248
|
+
if (definition.presentation.columns !== undefined) {
|
|
249
|
+
transition.columns = definition.presentation.columns;
|
|
250
|
+
}
|
|
251
|
+
if (definition.timing.easing !== undefined) {
|
|
252
|
+
transition.easing = definition.timing.easing;
|
|
253
|
+
}
|
|
254
|
+
return transition;
|
|
255
|
+
}
|
|
256
|
+
function transitionSeriesClipIndex(tracks) {
|
|
257
|
+
const index = new Map();
|
|
258
|
+
if (tracks === undefined) {
|
|
259
|
+
return index;
|
|
260
|
+
}
|
|
261
|
+
for (const track of tracks) {
|
|
262
|
+
for (let clipIndex = 1; clipIndex < track.clips.length; clipIndex += 1) {
|
|
263
|
+
const next = track.clips[clipIndex];
|
|
264
|
+
const previous = track.clips[clipIndex - 1];
|
|
265
|
+
if (previous !== undefined && next !== undefined && next.transitionFromPrevious !== undefined) {
|
|
266
|
+
index.set(transitionWindowKey(track.id, next.id), { previous, next });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return index;
|
|
271
|
+
}
|
|
272
|
+
function transitionWindowKey(trackId, nextClipId) {
|
|
273
|
+
return `${trackId}\u0000${nextClipId}`;
|
|
274
|
+
}
|
|
275
|
+
function evaluateTransitionSeriesClip(layer, clip, frame, dimensions, window, role) {
|
|
276
|
+
const timelineLayer = transitionSeriesTimelineLayer(layer, clip, window, role);
|
|
277
|
+
const evaluated = evaluateLayer(timelineLayer, frame, dimensions);
|
|
278
|
+
return {
|
|
279
|
+
clipId: clip.id,
|
|
280
|
+
layerId: clip.layerId,
|
|
281
|
+
localFrame: frame - clip.startFrame,
|
|
282
|
+
layer: evaluated
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function transitionSeriesTimelineLayer(layer, clip, window, role) {
|
|
286
|
+
const timelineLayer = {
|
|
287
|
+
...layer,
|
|
288
|
+
startFrame: clip.startFrame,
|
|
289
|
+
durationFrames: role === "previous" ? window.endFrame - clip.startFrame : clip.durationFrames
|
|
290
|
+
};
|
|
291
|
+
if (role === "previous") {
|
|
292
|
+
timelineLayer.transitionOut = window.transition;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
timelineLayer.transitionIn = window.transition;
|
|
296
|
+
}
|
|
297
|
+
return timelineLayer;
|
|
298
|
+
}
|
|
299
|
+
export function getCanvasDimensions(composition, exportDimensions) {
|
|
300
|
+
return {
|
|
301
|
+
width: exportDimensions?.width ?? composition.width,
|
|
302
|
+
height: exportDimensions?.height ?? composition.height
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
export function evaluateCaptionLayer(layer, frame) {
|
|
306
|
+
const localFrame = getLocalFrame(layer, frame);
|
|
307
|
+
if (!isLayerActive(layer, frame)) {
|
|
308
|
+
return emptyCaptionState(layer.source.kind, localFrame, layer.style?.highlight?.mode ?? "none");
|
|
309
|
+
}
|
|
310
|
+
return evaluateCaptionState(layer.source, localFrame, { highlightMode: layer.style?.highlight?.mode });
|
|
311
|
+
}
|
|
312
|
+
export function evaluateCaptionState(source, localFrame, options = {}) {
|
|
313
|
+
const highlightMode = options.highlightMode ?? "word";
|
|
314
|
+
if (source.kind !== "inline") {
|
|
315
|
+
return emptyCaptionState(source.kind, localFrame, highlightMode);
|
|
316
|
+
}
|
|
317
|
+
const cueIndex = source.cues.findIndex((cue) => isFrameInRange(localFrame, cue.startFrame, cue.endFrame));
|
|
318
|
+
const cue = cueIndex === -1 ? null : source.cues[cueIndex] ?? null;
|
|
319
|
+
if (!cue) {
|
|
320
|
+
return emptyCaptionState(source.kind, localFrame, highlightMode);
|
|
321
|
+
}
|
|
322
|
+
const words = evaluateCaptionWords(cue.words ?? [], localFrame);
|
|
323
|
+
const activeWord = words.find((word) => word.active) ?? null;
|
|
324
|
+
const activeWordIndex = activeWord?.index ?? null;
|
|
325
|
+
const lineText = cue.text;
|
|
326
|
+
return {
|
|
327
|
+
sourceKind: source.kind,
|
|
328
|
+
localFrame,
|
|
329
|
+
visible: true,
|
|
330
|
+
cueIndex,
|
|
331
|
+
cue,
|
|
332
|
+
lineText,
|
|
333
|
+
lines: splitCaptionLines(lineText),
|
|
334
|
+
words,
|
|
335
|
+
activeWord,
|
|
336
|
+
activeWordIndex,
|
|
337
|
+
highlightMode,
|
|
338
|
+
highlightedWordIndex: highlightMode === "word" ? activeWordIndex : null,
|
|
339
|
+
highlightedLineText: highlightMode === "line" ? lineText : null
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
export function evaluateCaptionWords(words, localFrame) {
|
|
343
|
+
return words.map((word, index) => {
|
|
344
|
+
const active = isFrameInRange(localFrame, word.startFrame, word.endFrame);
|
|
345
|
+
const state = active ? "active" : localFrame < word.startFrame ? "pending" : "completed";
|
|
346
|
+
return {
|
|
347
|
+
index,
|
|
348
|
+
text: word.text,
|
|
349
|
+
startFrame: word.startFrame,
|
|
350
|
+
endFrame: word.endFrame,
|
|
351
|
+
state,
|
|
352
|
+
active
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
export function collectResourceLimitViolations(inputs, limits = DEFAULT_RESOURCE_LIMITS) {
|
|
357
|
+
const violations = [];
|
|
358
|
+
addResourceViolation(violations, "maxFrames", inputs.frames, limits.maxFrames, "composition.durationFrames");
|
|
359
|
+
addResourceViolation(violations, "maxWidth", inputs.width, limits.maxWidth, "composition.width");
|
|
360
|
+
addResourceViolation(violations, "maxHeight", inputs.height, limits.maxHeight, "composition.height");
|
|
361
|
+
addResourceViolation(violations, "maxLayers", inputs.layerCount, limits.maxLayers, "layers");
|
|
362
|
+
addResourceViolation(violations, "maxAssets", inputs.assetCount, limits.maxAssets, "assets");
|
|
363
|
+
addResourceViolation(violations, "maxPropStringLength", inputs.propStringLength, limits.maxPropStringLength, "props");
|
|
364
|
+
addResourceViolation(violations, "maxAssetBytes", inputs.assetBytes, limits.maxAssetBytes, "assets");
|
|
365
|
+
addResourceViolation(violations, "maxSourceWidth", inputs.sourceWidth, limits.maxSourceWidth, "assets");
|
|
366
|
+
addResourceViolation(violations, "maxSourceHeight", inputs.sourceHeight, limits.maxSourceHeight, "assets");
|
|
367
|
+
addResourceViolation(violations, "maxBlurRadius", inputs.blurRadius, limits.maxBlurRadius, "layers");
|
|
368
|
+
addResourceViolation(violations, "maxFilteredLayers", inputs.filteredLayerCount, limits.maxFilteredLayers, "layers");
|
|
369
|
+
addResourceViolation(violations, "maxMaskedLayers", inputs.maskedLayerCount, limits.maxMaskedLayers, "layers");
|
|
370
|
+
addResourceViolation(violations, "maxMaskSourceWidth", inputs.maskSourceWidth, limits.maxMaskSourceWidth, "layers");
|
|
371
|
+
addResourceViolation(violations, "maxMaskSourceHeight", inputs.maskSourceHeight, limits.maxMaskSourceHeight, "layers");
|
|
372
|
+
addResourceViolation(violations, "maxTextMotionFragments", inputs.textMotionFragments, limits.maxTextMotionFragments, "layers");
|
|
373
|
+
addResourceViolation(violations, "maxProceduralMaskPixels", inputs.proceduralMaskPixels, limits.maxProceduralMaskPixels, "layers");
|
|
374
|
+
addResourceViolation(violations, "maxTransitionDurationFrames", inputs.transitionDurationFrames, limits.maxTransitionDurationFrames, "layers");
|
|
375
|
+
return violations;
|
|
376
|
+
}
|
|
377
|
+
export function collectCompositionResourceLimitInputs(document) {
|
|
378
|
+
const inputs = {
|
|
379
|
+
frames: document.composition.durationFrames,
|
|
380
|
+
width: document.composition.width,
|
|
381
|
+
height: document.composition.height,
|
|
382
|
+
layerCount: document.layers.length,
|
|
383
|
+
assetCount: Object.keys(document.assets).length
|
|
384
|
+
};
|
|
385
|
+
let blurRadius = 0;
|
|
386
|
+
let transitionDurationFrames = 0;
|
|
387
|
+
let textMotionFragments = 0;
|
|
388
|
+
let proceduralMaskPixels = 0;
|
|
389
|
+
const filteredLayerEvents = [];
|
|
390
|
+
const maskedLayerEvents = [];
|
|
391
|
+
for (const layer of document.layers) {
|
|
392
|
+
const layerHasFilter = hasFilterEffect(layer) || hasTransitionBlur(layer.transitionIn) || hasTransitionBlur(layer.transitionOut);
|
|
393
|
+
if (layerHasFilter) {
|
|
394
|
+
filteredLayerEvents.push({ frame: layer.startFrame, delta: 1 }, { frame: layer.startFrame + layer.durationFrames, delta: -1 });
|
|
395
|
+
}
|
|
396
|
+
blurRadius = Math.max(blurRadius, maxLayerBlurRadius(layer));
|
|
397
|
+
if (layer.mask) {
|
|
398
|
+
maskedLayerEvents.push({ frame: layer.startFrame, delta: 1 }, { frame: layer.startFrame + layer.durationFrames, delta: -1 });
|
|
399
|
+
const maskResolution = layer.mask.source.kind === "shape" ? undefined : layer.mask.source.resolution;
|
|
400
|
+
inputs.maskSourceWidth = Math.max(inputs.maskSourceWidth ?? 0, maskResolution?.width ?? 0);
|
|
401
|
+
inputs.maskSourceHeight = Math.max(inputs.maskSourceHeight ?? 0, maskResolution?.height ?? 0);
|
|
402
|
+
if (layer.mask.source.kind === "procedural" && maskResolution !== undefined) {
|
|
403
|
+
proceduralMaskPixels = Math.max(proceduralMaskPixels, maskResolution.width * maskResolution.height);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (layer.type === "text" && layer.textMotion !== undefined) {
|
|
407
|
+
textMotionFragments = Math.max(textMotionFragments, estimateTextMotionFragments(layer.text, layer.textMotion.split));
|
|
408
|
+
}
|
|
409
|
+
transitionDurationFrames = Math.max(transitionDurationFrames, transitionDuration(layer.transitionIn), transitionDuration(layer.transitionOut));
|
|
410
|
+
}
|
|
411
|
+
for (const window of compileTransitionOverlapWindows(document.tracks)) {
|
|
412
|
+
transitionDurationFrames = Math.max(transitionDurationFrames, window.durationFrames);
|
|
413
|
+
blurRadius = Math.max(blurRadius, window.transition.type === "blurDissolve" ? transitionAmount(window.transition, 16) : 0);
|
|
414
|
+
}
|
|
415
|
+
inputs.blurRadius = blurRadius;
|
|
416
|
+
inputs.filteredLayerCount = maxSimultaneousLayerCount(filteredLayerEvents);
|
|
417
|
+
inputs.maskedLayerCount = maxSimultaneousLayerCount(maskedLayerEvents);
|
|
418
|
+
inputs.textMotionFragments = textMotionFragments;
|
|
419
|
+
inputs.proceduralMaskPixels = proceduralMaskPixels;
|
|
420
|
+
inputs.transitionDurationFrames = transitionDurationFrames;
|
|
421
|
+
return inputs;
|
|
422
|
+
}
|
|
423
|
+
export function resolveLayout(layer, dimensions) {
|
|
424
|
+
const position = resolvePoint(layer.position, dimensions);
|
|
425
|
+
const size = resolveSize(layer.size, dimensions);
|
|
426
|
+
const anchor = resolveAnchor(layer.anchor);
|
|
427
|
+
const anchorOffset = {
|
|
428
|
+
x: size.width === null ? null : size.width * anchor.x,
|
|
429
|
+
y: size.height === null ? null : size.height * anchor.y
|
|
430
|
+
};
|
|
431
|
+
return {
|
|
432
|
+
position,
|
|
433
|
+
anchor,
|
|
434
|
+
size,
|
|
435
|
+
anchorOffset,
|
|
436
|
+
topLeft: {
|
|
437
|
+
x: anchorOffset.x === null ? null : position.x - anchorOffset.x,
|
|
438
|
+
y: anchorOffset.y === null ? null : position.y - anchorOffset.y
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
export function resolvePoint(point, dimensions) {
|
|
443
|
+
return {
|
|
444
|
+
x: resolveUnit(point?.x ?? 0, "x", dimensions),
|
|
445
|
+
y: resolveUnit(point?.y ?? 0, "y", dimensions)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
export function resolveSize(size, dimensions) {
|
|
449
|
+
return {
|
|
450
|
+
width: resolveOptionalSizeUnit(size?.width, "width", dimensions),
|
|
451
|
+
height: resolveOptionalSizeUnit(size?.height, "height", dimensions)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
export function resolveAnchor(anchor) {
|
|
455
|
+
if (typeof anchor === "string") {
|
|
456
|
+
return anchorKeywordToPoint(anchor);
|
|
457
|
+
}
|
|
458
|
+
if (anchor && Number.isFinite(anchor.x) && Number.isFinite(anchor.y)) {
|
|
459
|
+
return { x: anchor.x, y: anchor.y };
|
|
460
|
+
}
|
|
461
|
+
return { x: 0, y: 0 };
|
|
462
|
+
}
|
|
463
|
+
export function resolveUnit(value, axis, dimensions) {
|
|
464
|
+
if (typeof value === "number") {
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
const match = PERCENT_PATTERN.exec(value);
|
|
468
|
+
if (!match) {
|
|
469
|
+
return Number(value);
|
|
470
|
+
}
|
|
471
|
+
const amount = Number(match[1]);
|
|
472
|
+
const explicitAxis = match[2];
|
|
473
|
+
const basis = explicitAxis === "w"
|
|
474
|
+
? dimensions.width
|
|
475
|
+
: explicitAxis === "h"
|
|
476
|
+
? dimensions.height
|
|
477
|
+
: percentageBasis(axis, dimensions);
|
|
478
|
+
return (amount / 100) * basis;
|
|
479
|
+
}
|
|
480
|
+
export function evaluateLayerTransitions(layer, localFrame, dimensions) {
|
|
481
|
+
const state = {
|
|
482
|
+
opacity: 1,
|
|
483
|
+
offset: { x: 0, y: 0 },
|
|
484
|
+
scale: 1,
|
|
485
|
+
rotation: 0,
|
|
486
|
+
reveal: null,
|
|
487
|
+
revealShape: null,
|
|
488
|
+
revealPattern: null,
|
|
489
|
+
wash: null,
|
|
490
|
+
filter: null,
|
|
491
|
+
transform: identityTransitionTransform()
|
|
492
|
+
};
|
|
493
|
+
applyTransition(state, layer.transitionIn, "in", localFrame, layer.durationFrames, dimensions);
|
|
494
|
+
applyTransition(state, layer.transitionOut, "out", localFrame, layer.durationFrames, dimensions);
|
|
495
|
+
return state;
|
|
496
|
+
}
|
|
497
|
+
function applyTransition(state, transition, phase, localFrame, layerDurationFrames, dimensions) {
|
|
498
|
+
const requestedDurationFrames = transitionDuration(transition);
|
|
499
|
+
if (!transition || layerDurationFrames <= 0 || requestedDurationFrames <= 0) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const durationFrames = Math.min(requestedDurationFrames, layerDurationFrames);
|
|
503
|
+
const startFrame = phase === "in" ? 0 : Math.max(0, layerDurationFrames - durationFrames);
|
|
504
|
+
if (localFrame < startFrame || localFrame >= startFrame + durationFrames) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const rawProgress = durationFrames === 1 ? 1 : (localFrame - startFrame) / (durationFrames - 1);
|
|
508
|
+
const timingFrame = localFrame - startFrame;
|
|
509
|
+
const progress = transition.timing === undefined
|
|
510
|
+
? evaluateEasing(transition.easing ?? defaultTransitionEasing(transition.type, phase), rawProgress)
|
|
511
|
+
: evaluateTiming(transition.timing, timingFrame, durationFrames);
|
|
512
|
+
const hiddenProgress = phase === "in" ? 1 - progress : progress;
|
|
513
|
+
const visibleProgress = phase === "in" ? progress : 1 - progress;
|
|
514
|
+
switch (transition.type) {
|
|
515
|
+
case "fade":
|
|
516
|
+
case "crossfade":
|
|
517
|
+
state.opacity *= visibleProgress;
|
|
518
|
+
break;
|
|
519
|
+
case "slide": {
|
|
520
|
+
const offset = transitionOffset(transition.direction ?? "up", phase, hiddenProgress, progress, dimensions, 0.08);
|
|
521
|
+
state.offset.x += offset.x;
|
|
522
|
+
state.offset.y += offset.y;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case "push": {
|
|
526
|
+
const offset = transitionOffset(transition.direction ?? "left", phase, hiddenProgress, progress, dimensions, 1);
|
|
527
|
+
state.offset.x += offset.x;
|
|
528
|
+
state.offset.y += offset.y;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "wipe":
|
|
532
|
+
state.reveal = mergeRevealInset(state.reveal, transitionReveal(transition.direction ?? "up", phase, progress));
|
|
533
|
+
break;
|
|
534
|
+
case "zoom":
|
|
535
|
+
state.scale *= 1 + transitionAmount(transition, 0.12) * hiddenProgress;
|
|
536
|
+
break;
|
|
537
|
+
case "spin":
|
|
538
|
+
case "rotate":
|
|
539
|
+
state.rotation += transitionRotation(transition, phase, hiddenProgress, progress, transition.type === "spin" ? 180 : 90);
|
|
540
|
+
break;
|
|
541
|
+
case "flip":
|
|
542
|
+
applyFlipTransition(state, transition, phase, hiddenProgress, progress);
|
|
543
|
+
break;
|
|
544
|
+
case "blurDissolve":
|
|
545
|
+
state.opacity *= visibleProgress;
|
|
546
|
+
state.filter = mergeTransitionFilter(state.filter, { blur: transitionAmount(transition, 16) * hiddenProgress });
|
|
547
|
+
break;
|
|
548
|
+
case "colorDissolve":
|
|
549
|
+
state.wash = mergeTransitionWash(state.wash, {
|
|
550
|
+
color: transition.color ?? "#ffffff",
|
|
551
|
+
opacity: clamp01(hiddenProgress * transitionAmount(transition, 1))
|
|
552
|
+
});
|
|
553
|
+
break;
|
|
554
|
+
case "dip":
|
|
555
|
+
state.wash = mergeTransitionWash(state.wash, {
|
|
556
|
+
color: transition.color ?? "#000000",
|
|
557
|
+
opacity: clamp01(hiddenProgress * transitionAmount(transition, 1))
|
|
558
|
+
});
|
|
559
|
+
break;
|
|
560
|
+
case "iris":
|
|
561
|
+
state.revealShape = {
|
|
562
|
+
shape: transition.shape ?? "circle",
|
|
563
|
+
progress: clamp01(visibleProgress)
|
|
564
|
+
};
|
|
565
|
+
break;
|
|
566
|
+
case "stretch":
|
|
567
|
+
applyStretchTransition(state, transition, hiddenProgress, 1);
|
|
568
|
+
break;
|
|
569
|
+
case "squeeze":
|
|
570
|
+
applyStretchTransition(state, transition, hiddenProgress, -1);
|
|
571
|
+
break;
|
|
572
|
+
case "clockWipe":
|
|
573
|
+
state.revealPattern = mergeRevealPattern(state.revealPattern, {
|
|
574
|
+
kind: "clock",
|
|
575
|
+
progress: clamp01(visibleProgress),
|
|
576
|
+
direction: transition.direction ?? "right",
|
|
577
|
+
rows: 1,
|
|
578
|
+
columns: 1
|
|
579
|
+
});
|
|
580
|
+
break;
|
|
581
|
+
case "barWipe":
|
|
582
|
+
state.revealPattern = mergeRevealPattern(state.revealPattern, barWipePattern(transition, visibleProgress));
|
|
583
|
+
break;
|
|
584
|
+
case "gridWipe":
|
|
585
|
+
state.revealPattern = mergeRevealPattern(state.revealPattern, gridRevealPattern(transition, "grid", visibleProgress));
|
|
586
|
+
break;
|
|
587
|
+
case "tileReveal":
|
|
588
|
+
state.revealPattern = mergeRevealPattern(state.revealPattern, gridRevealPattern(transition, "tiles", visibleProgress));
|
|
589
|
+
break;
|
|
590
|
+
case "radialBlur":
|
|
591
|
+
state.opacity *= visibleProgress;
|
|
592
|
+
state.filter = mergeTransitionFilter(state.filter, { blur: transitionAmount(transition, 18) * hiddenProgress });
|
|
593
|
+
state.scale *= 1 + transitionIntensity(transition, 0.04) * hiddenProgress;
|
|
594
|
+
break;
|
|
595
|
+
case "zoomBlur":
|
|
596
|
+
state.opacity *= visibleProgress;
|
|
597
|
+
state.filter = mergeTransitionFilter(state.filter, { blur: transitionAmount(transition, 18) * hiddenProgress });
|
|
598
|
+
state.scale *= 1 + transitionIntensity(transition, 0.14) * hiddenProgress;
|
|
599
|
+
break;
|
|
600
|
+
case "bookFlip":
|
|
601
|
+
applyFlipTransition(state, transition, phase, hiddenProgress, progress, 105);
|
|
602
|
+
break;
|
|
603
|
+
case "pageCurlLite":
|
|
604
|
+
applyPageCurlLiteTransition(state, transition, phase, hiddenProgress, progress);
|
|
605
|
+
break;
|
|
606
|
+
case "skewSlide": {
|
|
607
|
+
const offset = transitionOffset(transition.direction ?? "up", phase, hiddenProgress, progress, dimensions, 0.16);
|
|
608
|
+
state.offset.x += offset.x;
|
|
609
|
+
state.offset.y += offset.y;
|
|
610
|
+
applyDirectionalSkew(state, transition.direction ?? "up", hiddenProgress, transitionIntensity(transition, 12));
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case "expandMask":
|
|
614
|
+
state.revealShape = {
|
|
615
|
+
shape: transition.shape ?? "circle",
|
|
616
|
+
progress: clamp01(visibleProgress)
|
|
617
|
+
};
|
|
618
|
+
break;
|
|
619
|
+
case "letterboxReveal":
|
|
620
|
+
state.reveal = mergeRevealInset(state.reveal, letterboxReveal(transition.axis ?? "y", visibleProgress));
|
|
621
|
+
break;
|
|
622
|
+
case "filmFlash":
|
|
623
|
+
state.wash = mergeTransitionWash(state.wash, {
|
|
624
|
+
color: transition.color ?? "#fff7dd",
|
|
625
|
+
opacity: clamp01(hiddenProgress * transitionAmount(transition, 1))
|
|
626
|
+
});
|
|
627
|
+
break;
|
|
628
|
+
case "cameraWhip": {
|
|
629
|
+
const direction = transition.direction ?? "left";
|
|
630
|
+
const offset = transitionOffset(direction, phase, hiddenProgress, progress, dimensions, 0.72);
|
|
631
|
+
state.offset.x += offset.x;
|
|
632
|
+
state.offset.y += offset.y;
|
|
633
|
+
applyDirectionalSkew(state, direction, hiddenProgress, transitionIntensity(transition, 10));
|
|
634
|
+
state.filter = mergeTransitionFilter(state.filter, { blur: transitionAmount(transition, 14) * hiddenProgress });
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function transitionDuration(transition) {
|
|
640
|
+
if (!transition) {
|
|
641
|
+
return 0;
|
|
642
|
+
}
|
|
643
|
+
const timingDuration = transition.timing === undefined ? undefined : timingDurationFrames(transition.timing);
|
|
644
|
+
return transition.durationFrames ?? timingDuration ?? 0;
|
|
645
|
+
}
|
|
646
|
+
function defaultTransitionEasing(type, phase) {
|
|
647
|
+
if (type === "slide" ||
|
|
648
|
+
type === "push" ||
|
|
649
|
+
type === "zoom" ||
|
|
650
|
+
type === "flip" ||
|
|
651
|
+
type === "stretch" ||
|
|
652
|
+
type === "squeeze" ||
|
|
653
|
+
type === "zoomBlur" ||
|
|
654
|
+
type === "bookFlip" ||
|
|
655
|
+
type === "pageCurlLite" ||
|
|
656
|
+
type === "skewSlide" ||
|
|
657
|
+
type === "cameraWhip") {
|
|
658
|
+
return phase === "in" ? "outCubic" : "inCubic";
|
|
659
|
+
}
|
|
660
|
+
if (type === "spin" || type === "rotate") {
|
|
661
|
+
return phase === "in" ? "outBack" : "inBack";
|
|
662
|
+
}
|
|
663
|
+
return "linear";
|
|
664
|
+
}
|
|
665
|
+
function transitionOffset(direction, phase, inRemainingProgress, outProgress, dimensions, distanceScale) {
|
|
666
|
+
const vector = transitionVector(direction);
|
|
667
|
+
const distance = transitionDistance(direction, dimensions) * distanceScale;
|
|
668
|
+
const multiplier = phase === "in" ? -inRemainingProgress : outProgress;
|
|
669
|
+
return {
|
|
670
|
+
x: vector.x * distance * multiplier,
|
|
671
|
+
y: vector.y * distance * multiplier
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function transitionReveal(direction, phase, progress) {
|
|
675
|
+
const hidden = (phase === "in" ? 1 - progress : progress) * 100;
|
|
676
|
+
const inset = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
677
|
+
switch (direction) {
|
|
678
|
+
case "up":
|
|
679
|
+
inset[phase === "in" ? "bottom" : "top"] = hidden;
|
|
680
|
+
break;
|
|
681
|
+
case "down":
|
|
682
|
+
inset[phase === "in" ? "top" : "bottom"] = hidden;
|
|
683
|
+
break;
|
|
684
|
+
case "left":
|
|
685
|
+
inset[phase === "in" ? "right" : "left"] = hidden;
|
|
686
|
+
break;
|
|
687
|
+
case "right":
|
|
688
|
+
inset[phase === "in" ? "left" : "right"] = hidden;
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
return inset;
|
|
692
|
+
}
|
|
693
|
+
function transitionVector(direction) {
|
|
694
|
+
switch (direction) {
|
|
695
|
+
case "up":
|
|
696
|
+
return { x: 0, y: -1 };
|
|
697
|
+
case "down":
|
|
698
|
+
return { x: 0, y: 1 };
|
|
699
|
+
case "left":
|
|
700
|
+
return { x: -1, y: 0 };
|
|
701
|
+
case "right":
|
|
702
|
+
return { x: 1, y: 0 };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function transitionDistance(direction, dimensions) {
|
|
706
|
+
return direction === "left" || direction === "right" ? dimensions.width : dimensions.height;
|
|
707
|
+
}
|
|
708
|
+
function mergeRevealInset(current, next) {
|
|
709
|
+
if (!current) {
|
|
710
|
+
return next;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
top: Math.max(current.top, next.top),
|
|
714
|
+
right: Math.max(current.right, next.right),
|
|
715
|
+
bottom: Math.max(current.bottom, next.bottom),
|
|
716
|
+
left: Math.max(current.left, next.left)
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function mergeRevealPattern(current, next) {
|
|
720
|
+
if (!current || next.progress <= current.progress) {
|
|
721
|
+
return next;
|
|
722
|
+
}
|
|
723
|
+
return current;
|
|
724
|
+
}
|
|
725
|
+
function barWipePattern(transition, visibleProgress) {
|
|
726
|
+
const direction = transition.direction ?? (transition.axis === "y" ? "down" : "right");
|
|
727
|
+
const horizontal = direction === "left" || direction === "right";
|
|
728
|
+
return {
|
|
729
|
+
kind: "bars",
|
|
730
|
+
progress: clamp01(visibleProgress),
|
|
731
|
+
direction,
|
|
732
|
+
rows: horizontal ? 1 : transitionGridCount(transition.rows, 8),
|
|
733
|
+
columns: horizontal ? transitionGridCount(transition.columns, 8) : 1
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function gridRevealPattern(transition, kind, visibleProgress) {
|
|
737
|
+
return {
|
|
738
|
+
kind,
|
|
739
|
+
progress: clamp01(visibleProgress),
|
|
740
|
+
direction: transition.direction ?? "right",
|
|
741
|
+
rows: transitionGridCount(transition.rows, 4),
|
|
742
|
+
columns: transitionGridCount(transition.columns, 6)
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function transitionGridCount(value, fallback) {
|
|
746
|
+
const candidate = value ?? fallback;
|
|
747
|
+
if (!Number.isFinite(candidate)) {
|
|
748
|
+
return fallback;
|
|
749
|
+
}
|
|
750
|
+
return Math.max(1, Math.min(32, Math.round(candidate)));
|
|
751
|
+
}
|
|
752
|
+
function transitionAmount(transition, fallback) {
|
|
753
|
+
const value = transition.amount ?? transition.intensity ?? fallback;
|
|
754
|
+
return Number.isFinite(value) ? Math.max(0, value) : fallback;
|
|
755
|
+
}
|
|
756
|
+
function transitionIntensity(transition, fallback) {
|
|
757
|
+
const value = transition.intensity ?? fallback;
|
|
758
|
+
return Number.isFinite(value) ? Math.max(0, value) : fallback;
|
|
759
|
+
}
|
|
760
|
+
function transitionRotation(transition, phase, hiddenProgress, outProgress, fallbackAmount) {
|
|
761
|
+
const amount = transitionAmount(transition, fallbackAmount);
|
|
762
|
+
const direction = transition.direction === "left" || transition.direction === "down" ? -1 : 1;
|
|
763
|
+
return direction * (phase === "in" ? -amount * hiddenProgress : amount * outProgress);
|
|
764
|
+
}
|
|
765
|
+
function applyFlipTransition(state, transition, phase, hiddenProgress, outProgress, fallbackAmount = 90) {
|
|
766
|
+
const amount = transitionAmount(transition, fallbackAmount);
|
|
767
|
+
const axis = transition.axis ?? "y";
|
|
768
|
+
const direction = transition.direction === "left" || transition.direction === "down" ? -1 : 1;
|
|
769
|
+
const rotation = direction * (phase === "in" ? -amount * hiddenProgress : amount * outProgress);
|
|
770
|
+
if (axis === "x") {
|
|
771
|
+
state.transform.rotateX += rotation;
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
state.transform.rotateY += rotation;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function applyPageCurlLiteTransition(state, transition, phase, hiddenProgress, outProgress) {
|
|
778
|
+
const amount = transition.amount !== undefined && Number.isFinite(transition.amount) ? Math.max(0, transition.amount) : 70;
|
|
779
|
+
const direction = transition.direction ?? "left";
|
|
780
|
+
const sign = direction === "left" || direction === "down" ? -1 : 1;
|
|
781
|
+
const rotation = sign * (phase === "in" ? -amount * hiddenProgress : amount * outProgress);
|
|
782
|
+
const curl = transitionIntensity(transition, 10) * hiddenProgress * sign;
|
|
783
|
+
if (transition.axis === "x" || direction === "up" || direction === "down") {
|
|
784
|
+
state.transform.rotateX += rotation;
|
|
785
|
+
state.transform.skewX += curl;
|
|
786
|
+
state.transform.scaleY *= Math.max(0.01, 1 - hiddenProgress * 0.08);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
state.transform.rotateY += rotation;
|
|
790
|
+
state.transform.skewY += curl;
|
|
791
|
+
state.transform.scaleX *= Math.max(0.01, 1 - hiddenProgress * 0.08);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function applyDirectionalSkew(state, direction, hiddenProgress, amount) {
|
|
795
|
+
const sign = direction === "left" || direction === "down" ? -1 : 1;
|
|
796
|
+
if (direction === "left" || direction === "right") {
|
|
797
|
+
state.transform.skewY += sign * amount * hiddenProgress;
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
state.transform.skewX += sign * amount * hiddenProgress;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function applyStretchTransition(state, transition, hiddenProgress, direction) {
|
|
804
|
+
const amount = transitionAmount(transition, 0.28) * hiddenProgress;
|
|
805
|
+
const axis = transition.axis ?? "x";
|
|
806
|
+
const primary = 1 + direction * amount;
|
|
807
|
+
const secondary = 1 - direction * amount * 0.35;
|
|
808
|
+
if (axis === "x") {
|
|
809
|
+
state.transform.scaleX *= Math.max(0.01, primary);
|
|
810
|
+
state.transform.scaleY *= Math.max(0.01, secondary);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
state.transform.scaleY *= Math.max(0.01, primary);
|
|
814
|
+
state.transform.scaleX *= Math.max(0.01, secondary);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function letterboxReveal(axis, visibleProgress) {
|
|
818
|
+
const hidden = (1 - clamp01(visibleProgress)) * 50;
|
|
819
|
+
return axis === "x"
|
|
820
|
+
? { top: 0, right: hidden, bottom: 0, left: hidden }
|
|
821
|
+
: { top: hidden, right: 0, bottom: hidden, left: 0 };
|
|
822
|
+
}
|
|
823
|
+
function mergeTransitionFilter(current, next) {
|
|
824
|
+
return {
|
|
825
|
+
blur: Math.max(current?.blur ?? 0, next.blur ?? 0)
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function mergeTransitionWash(current, next) {
|
|
829
|
+
if (!current || next.opacity >= current.opacity) {
|
|
830
|
+
return next;
|
|
831
|
+
}
|
|
832
|
+
return current;
|
|
833
|
+
}
|
|
834
|
+
function identityTransitionTransform() {
|
|
835
|
+
return {
|
|
836
|
+
rotateX: 0,
|
|
837
|
+
rotateY: 0,
|
|
838
|
+
skewX: 0,
|
|
839
|
+
skewY: 0,
|
|
840
|
+
scaleX: 1,
|
|
841
|
+
scaleY: 1
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function isIdentityTransitionTransform(value) {
|
|
845
|
+
return value.rotateX === 0 && value.rotateY === 0 && value.skewX === 0 && value.skewY === 0 && value.scaleX === 1 && value.scaleY === 1;
|
|
846
|
+
}
|
|
847
|
+
export function evaluateKeyframes(keyframes, localFrame, fallback) {
|
|
848
|
+
if (!keyframes || keyframes.length === 0) {
|
|
849
|
+
return fallback;
|
|
850
|
+
}
|
|
851
|
+
const first = keyframes[0];
|
|
852
|
+
if (!first) {
|
|
853
|
+
return fallback;
|
|
854
|
+
}
|
|
855
|
+
if (localFrame <= first.frame) {
|
|
856
|
+
return first.value;
|
|
857
|
+
}
|
|
858
|
+
for (let index = 0; index < keyframes.length - 1; index += 1) {
|
|
859
|
+
const current = keyframes[index];
|
|
860
|
+
const next = keyframes[index + 1];
|
|
861
|
+
if (!current || !next || next.frame <= current.frame) {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (localFrame <= next.frame) {
|
|
865
|
+
const progress = (localFrame - current.frame) / (next.frame - current.frame);
|
|
866
|
+
const easedProgress = current.timing === undefined
|
|
867
|
+
? evaluateEasing(current.easing ?? "linear", progress)
|
|
868
|
+
: evaluateTiming(current.timing, localFrame - current.frame, next.frame - current.frame + 1);
|
|
869
|
+
return lerp(current.value, next.value, easedProgress);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return keyframes[keyframes.length - 1]?.value ?? fallback;
|
|
873
|
+
}
|
|
874
|
+
export function evaluateEasing(easing, progress) {
|
|
875
|
+
const t = clamp01(progress);
|
|
876
|
+
switch (easing) {
|
|
877
|
+
case "linear":
|
|
878
|
+
return t;
|
|
879
|
+
case "inQuad":
|
|
880
|
+
return t * t;
|
|
881
|
+
case "outQuad":
|
|
882
|
+
return 1 - (1 - t) * (1 - t);
|
|
883
|
+
case "inOutQuad":
|
|
884
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
885
|
+
case "inCubic":
|
|
886
|
+
return t * t * t;
|
|
887
|
+
case "outCubic":
|
|
888
|
+
return 1 - Math.pow(1 - t, 3);
|
|
889
|
+
case "inOutCubic":
|
|
890
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
891
|
+
case "inCirc":
|
|
892
|
+
return 1 - Math.sqrt(1 - Math.pow(t, 2));
|
|
893
|
+
case "outCirc":
|
|
894
|
+
return Math.sqrt(1 - Math.pow(t - 1, 2));
|
|
895
|
+
case "inOutCirc":
|
|
896
|
+
return t < 0.5
|
|
897
|
+
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
|
|
898
|
+
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
|
|
899
|
+
case "inExpo":
|
|
900
|
+
return t === 0 ? 0 : Math.pow(2, 10 * t - 10);
|
|
901
|
+
case "outExpo":
|
|
902
|
+
return t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
|
903
|
+
case "inOutExpo":
|
|
904
|
+
if (t === 0 || t === 1) {
|
|
905
|
+
return t;
|
|
906
|
+
}
|
|
907
|
+
return t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2;
|
|
908
|
+
case "anticipate":
|
|
909
|
+
case "back":
|
|
910
|
+
case "inBack":
|
|
911
|
+
return (BACK_OVERSHOOT + 1) * t * t * t - BACK_OVERSHOOT * t * t;
|
|
912
|
+
case "outBack": {
|
|
913
|
+
const shifted = t - 1;
|
|
914
|
+
return 1 + (BACK_OVERSHOOT + 1) * shifted * shifted * shifted + BACK_OVERSHOOT * shifted * shifted;
|
|
915
|
+
}
|
|
916
|
+
case "inOutBack": {
|
|
917
|
+
const overshoot = BACK_OVERSHOOT * 1.525;
|
|
918
|
+
return t < 0.5
|
|
919
|
+
? (Math.pow(2 * t, 2) * ((overshoot + 1) * 2 * t - overshoot)) / 2
|
|
920
|
+
: (Math.pow(2 * t - 2, 2) * ((overshoot + 1) * (t * 2 - 2) + overshoot) + 2) / 2;
|
|
921
|
+
}
|
|
922
|
+
case "inElastic":
|
|
923
|
+
return t === 0 || t === 1 ? t : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * ELASTIC_PERIOD);
|
|
924
|
+
case "outElastic":
|
|
925
|
+
return t === 0 || t === 1 ? t : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * ELASTIC_PERIOD) + 1;
|
|
926
|
+
case "inOutElastic": {
|
|
927
|
+
if (t === 0 || t === 1) {
|
|
928
|
+
return t;
|
|
929
|
+
}
|
|
930
|
+
const period = (2 * Math.PI) / 4.5;
|
|
931
|
+
return t < 0.5
|
|
932
|
+
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * period)) / 2
|
|
933
|
+
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * period)) / 2 + 1;
|
|
934
|
+
}
|
|
935
|
+
case "inBounce":
|
|
936
|
+
return 1 - outBounce(1 - t);
|
|
937
|
+
case "outBounce":
|
|
938
|
+
return outBounce(t);
|
|
939
|
+
case "inOutBounce":
|
|
940
|
+
return t < 0.5 ? (1 - outBounce(1 - 2 * t)) / 2 : (1 + outBounce(2 * t - 1)) / 2;
|
|
941
|
+
default: {
|
|
942
|
+
const bezier = parseCubicBezier(easing);
|
|
943
|
+
if (!bezier) {
|
|
944
|
+
throw new Error(`Unsupported easing: ${easing}`);
|
|
945
|
+
}
|
|
946
|
+
return evaluateCubicBezier(bezier, t);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
export function evaluateTiming(timing, localFrame, fallbackDurationFrames) {
|
|
951
|
+
const durationFrames = Math.max(1, fallbackDurationFrames ?? timingDurationFrames(timing) ?? 1);
|
|
952
|
+
const progress = durationFrames === 1 ? 1 : localFrame / (durationFrames - 1);
|
|
953
|
+
switch (timing.type) {
|
|
954
|
+
case "tween":
|
|
955
|
+
return evaluateEasing(timing.easing ?? "linear", progress);
|
|
956
|
+
case "spring":
|
|
957
|
+
return evaluateSpringTiming(timing, progress);
|
|
958
|
+
case "steps":
|
|
959
|
+
return evaluateStepsTiming(timing, progress);
|
|
960
|
+
case "sequence":
|
|
961
|
+
return evaluateSequenceTiming(timing, Math.max(0, localFrame));
|
|
962
|
+
case "stagger":
|
|
963
|
+
return evaluateStaggerTiming(timing, Math.max(0, localFrame));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
export function timingDurationFrames(timing) {
|
|
967
|
+
switch (timing.type) {
|
|
968
|
+
case "tween":
|
|
969
|
+
case "spring":
|
|
970
|
+
case "steps":
|
|
971
|
+
return timing.durationFrames;
|
|
972
|
+
case "sequence":
|
|
973
|
+
return timing.segments.reduce((total, segment) => total + segment.durationFrames, 0);
|
|
974
|
+
case "stagger": {
|
|
975
|
+
const childDuration = timingDurationFrames(timing.timing) ?? 1;
|
|
976
|
+
const lastOffset = Math.max(0, timing.childCount - 1) * timing.eachFrames;
|
|
977
|
+
return childDuration + lastOffset;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function evaluateStepsTiming(timing, progress) {
|
|
982
|
+
const t = clamp01(progress);
|
|
983
|
+
const steps = Math.max(1, Math.trunc(timing.steps));
|
|
984
|
+
if (t === 0 || t === 1) {
|
|
985
|
+
return t;
|
|
986
|
+
}
|
|
987
|
+
return timing.direction === "start" ? Math.ceil(t * steps) / steps : Math.floor(t * steps) / steps;
|
|
988
|
+
}
|
|
989
|
+
function evaluateSequenceTiming(timing, localFrame) {
|
|
990
|
+
if (timing.segments.length === 0) {
|
|
991
|
+
return 1;
|
|
992
|
+
}
|
|
993
|
+
let cursor = 0;
|
|
994
|
+
let previousTo = 0;
|
|
995
|
+
for (let index = 0; index < timing.segments.length; index += 1) {
|
|
996
|
+
const segment = timing.segments[index];
|
|
997
|
+
if (!segment) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const durationFrames = Math.max(1, segment.durationFrames);
|
|
1001
|
+
const endFrame = cursor + durationFrames;
|
|
1002
|
+
const from = segment.from ?? previousTo;
|
|
1003
|
+
const to = segment.to ?? (index + 1) / timing.segments.length;
|
|
1004
|
+
if (localFrame < endFrame || index === timing.segments.length - 1) {
|
|
1005
|
+
const segmentFrame = Math.min(Math.max(0, localFrame - cursor), Math.max(0, durationFrames - 1));
|
|
1006
|
+
const progress = segment.timing === undefined ? evaluateEasing("linear", durationFrames === 1 ? 1 : segmentFrame / (durationFrames - 1)) : evaluateTiming(segment.timing, segmentFrame, durationFrames);
|
|
1007
|
+
return lerp(from, to, progress);
|
|
1008
|
+
}
|
|
1009
|
+
previousTo = to;
|
|
1010
|
+
cursor = endFrame;
|
|
1011
|
+
}
|
|
1012
|
+
return previousTo;
|
|
1013
|
+
}
|
|
1014
|
+
function evaluateStaggerTiming(timing, localFrame) {
|
|
1015
|
+
const childIndex = Math.min(Math.max(0, timing.childIndex ?? 0), Math.max(0, timing.childCount - 1));
|
|
1016
|
+
const order = staggerOrder(childIndex, timing.childCount, timing.from ?? "start");
|
|
1017
|
+
const childFrame = localFrame - order * timing.eachFrames;
|
|
1018
|
+
if (childFrame < 0) {
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
return evaluateTiming(timing.timing, childFrame, timingDurationFrames(timing.timing));
|
|
1022
|
+
}
|
|
1023
|
+
function staggerOrder(childIndex, childCount, from) {
|
|
1024
|
+
switch (from) {
|
|
1025
|
+
case "end":
|
|
1026
|
+
return Math.max(0, childCount - 1 - childIndex);
|
|
1027
|
+
case "center":
|
|
1028
|
+
return Math.abs(childIndex - (childCount - 1) / 2);
|
|
1029
|
+
case "start":
|
|
1030
|
+
return childIndex;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function evaluateSpringTiming(timing, progress) {
|
|
1034
|
+
const t = clamp01(progress);
|
|
1035
|
+
const snapWindow = clamp(timing.restSpeed ?? 0.01, 0, 1) * 0.1;
|
|
1036
|
+
if (t === 0 || t === 1 || t >= 1 - snapWindow) {
|
|
1037
|
+
return t === 0 ? 0 : 1;
|
|
1038
|
+
}
|
|
1039
|
+
const stiffness = positiveFinite(timing.stiffness, 100);
|
|
1040
|
+
const damping = positiveFinite(timing.damping, 12) * (1 - clamp01(timing.bounce ?? 0) * 0.5);
|
|
1041
|
+
const mass = positiveFinite(timing.mass, 1);
|
|
1042
|
+
const curve = springCurveAt(t, stiffness, damping, mass);
|
|
1043
|
+
const end = springCurveAt(1, stiffness, damping, mass);
|
|
1044
|
+
if (!Number.isFinite(curve) || !Number.isFinite(end) || Math.abs(end) < 1e-6) {
|
|
1045
|
+
return t;
|
|
1046
|
+
}
|
|
1047
|
+
return curve / end;
|
|
1048
|
+
}
|
|
1049
|
+
function springCurveAt(t, stiffness, damping, mass) {
|
|
1050
|
+
const omega0 = Math.sqrt(stiffness / mass);
|
|
1051
|
+
const zeta = damping / (2 * Math.sqrt(stiffness * mass));
|
|
1052
|
+
if (zeta < 1) {
|
|
1053
|
+
const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);
|
|
1054
|
+
return 1 - Math.exp(-zeta * omega0 * t) * (Math.cos(omegaD * t) + (zeta * omega0 * Math.sin(omegaD * t)) / omegaD);
|
|
1055
|
+
}
|
|
1056
|
+
if (zeta === 1) {
|
|
1057
|
+
return 1 - Math.exp(-omega0 * t) * (1 + omega0 * t);
|
|
1058
|
+
}
|
|
1059
|
+
const sqrtTerm = Math.sqrt(zeta * zeta - 1);
|
|
1060
|
+
const r1 = -omega0 * (zeta - sqrtTerm);
|
|
1061
|
+
const r2 = -omega0 * (zeta + sqrtTerm);
|
|
1062
|
+
return 1 - (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r2 - r1);
|
|
1063
|
+
}
|
|
1064
|
+
function outBounce(t) {
|
|
1065
|
+
if (t < 1 / 2.75) {
|
|
1066
|
+
return 7.5625 * t * t;
|
|
1067
|
+
}
|
|
1068
|
+
if (t < 2 / 2.75) {
|
|
1069
|
+
const shifted = t - 1.5 / 2.75;
|
|
1070
|
+
return 7.5625 * shifted * shifted + 0.75;
|
|
1071
|
+
}
|
|
1072
|
+
if (t < 2.5 / 2.75) {
|
|
1073
|
+
const shifted = t - 2.25 / 2.75;
|
|
1074
|
+
return 7.5625 * shifted * shifted + 0.9375;
|
|
1075
|
+
}
|
|
1076
|
+
const shifted = t - 2.625 / 2.75;
|
|
1077
|
+
return 7.5625 * shifted * shifted + 0.984375;
|
|
1078
|
+
}
|
|
1079
|
+
export function parseCubicBezier(easing) {
|
|
1080
|
+
const match = CUBIC_BEZIER_PATTERN.exec(easing);
|
|
1081
|
+
if (!match) {
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
const x1 = Number(match[1]);
|
|
1085
|
+
const y1 = Number(match[2]);
|
|
1086
|
+
const x2 = Number(match[3]);
|
|
1087
|
+
const y2 = Number(match[4]);
|
|
1088
|
+
if (![x1, y1, x2, y2].every(Number.isFinite) || x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
|
|
1089
|
+
return undefined;
|
|
1090
|
+
}
|
|
1091
|
+
return [x1, y1, x2, y2];
|
|
1092
|
+
}
|
|
1093
|
+
function emptyCaptionState(sourceKind, localFrame, highlightMode) {
|
|
1094
|
+
return {
|
|
1095
|
+
sourceKind,
|
|
1096
|
+
localFrame,
|
|
1097
|
+
visible: false,
|
|
1098
|
+
cueIndex: null,
|
|
1099
|
+
cue: null,
|
|
1100
|
+
lineText: "",
|
|
1101
|
+
lines: [],
|
|
1102
|
+
words: [],
|
|
1103
|
+
activeWord: null,
|
|
1104
|
+
activeWordIndex: null,
|
|
1105
|
+
highlightMode,
|
|
1106
|
+
highlightedWordIndex: null,
|
|
1107
|
+
highlightedLineText: null
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function splitCaptionLines(text) {
|
|
1111
|
+
return text.length === 0 ? [] : text.split(/\r\n?|\n/);
|
|
1112
|
+
}
|
|
1113
|
+
function isFrameInRange(frame, startFrame, endFrame) {
|
|
1114
|
+
return frame >= startFrame && frame < endFrame;
|
|
1115
|
+
}
|
|
1116
|
+
function isCaptionTimelineLayer(layer) {
|
|
1117
|
+
if (layer.type !== "caption") {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
const candidate = layer;
|
|
1121
|
+
return isCaptionSource(candidate.source);
|
|
1122
|
+
}
|
|
1123
|
+
function isCaptionSource(value) {
|
|
1124
|
+
if (!isRecord(value) || typeof value.kind !== "string") {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
if (value.kind === "inline") {
|
|
1128
|
+
return Array.isArray(value.cues);
|
|
1129
|
+
}
|
|
1130
|
+
return ((value.kind === "vtt" || value.kind === "srt" || value.kind === "asset") &&
|
|
1131
|
+
typeof value.asset === "string");
|
|
1132
|
+
}
|
|
1133
|
+
function addResourceViolation(violations, resource, actual, limit, path) {
|
|
1134
|
+
if (actual === undefined || !Number.isFinite(actual) || actual <= limit) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
violations.push({
|
|
1138
|
+
code: resourceLimitCode(resource),
|
|
1139
|
+
severity: "error",
|
|
1140
|
+
message: resourceLimitMessage(resource, actual, limit),
|
|
1141
|
+
path,
|
|
1142
|
+
stage: "validation",
|
|
1143
|
+
retryable: false,
|
|
1144
|
+
resource,
|
|
1145
|
+
actual,
|
|
1146
|
+
limit
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
function resourceLimitCode(resource) {
|
|
1150
|
+
switch (resource) {
|
|
1151
|
+
case "maxFrames":
|
|
1152
|
+
return "LIMIT_MAX_FRAMES";
|
|
1153
|
+
case "maxWidth":
|
|
1154
|
+
return "LIMIT_MAX_WIDTH";
|
|
1155
|
+
case "maxHeight":
|
|
1156
|
+
return "LIMIT_MAX_HEIGHT";
|
|
1157
|
+
case "maxLayers":
|
|
1158
|
+
return "LIMIT_MAX_LAYERS";
|
|
1159
|
+
case "maxAssets":
|
|
1160
|
+
return "LIMIT_MAX_ASSETS";
|
|
1161
|
+
case "maxPropStringLength":
|
|
1162
|
+
return "LIMIT_MAX_PROP_STRING_LENGTH";
|
|
1163
|
+
case "maxAssetBytes":
|
|
1164
|
+
return "LIMIT_MAX_ASSET_BYTES";
|
|
1165
|
+
case "maxSourceWidth":
|
|
1166
|
+
return "LIMIT_MAX_SOURCE_WIDTH";
|
|
1167
|
+
case "maxSourceHeight":
|
|
1168
|
+
return "LIMIT_MAX_SOURCE_HEIGHT";
|
|
1169
|
+
case "maxBlurRadius":
|
|
1170
|
+
return "LIMIT_MAX_BLUR_RADIUS";
|
|
1171
|
+
case "maxFilteredLayers":
|
|
1172
|
+
return "LIMIT_MAX_FILTERED_LAYERS";
|
|
1173
|
+
case "maxMaskedLayers":
|
|
1174
|
+
return "LIMIT_MAX_MASKED_LAYERS";
|
|
1175
|
+
case "maxMaskSourceWidth":
|
|
1176
|
+
return "LIMIT_MAX_MASK_SOURCE_WIDTH";
|
|
1177
|
+
case "maxMaskSourceHeight":
|
|
1178
|
+
return "LIMIT_MAX_MASK_SOURCE_HEIGHT";
|
|
1179
|
+
case "maxTextMotionFragments":
|
|
1180
|
+
return "LIMIT_MAX_TEXT_MOTION_FRAGMENTS";
|
|
1181
|
+
case "maxProceduralMaskPixels":
|
|
1182
|
+
return "LIMIT_MAX_PROCEDURAL_MASK_PIXELS";
|
|
1183
|
+
case "maxTransitionDurationFrames":
|
|
1184
|
+
return "LIMIT_MAX_TRANSITION_DURATION";
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function resourceLimitMessage(resource, actual, limit) {
|
|
1188
|
+
switch (resource) {
|
|
1189
|
+
case "maxFrames":
|
|
1190
|
+
return `composition duration ${actual} exceeds max frames limit ${limit}.`;
|
|
1191
|
+
case "maxWidth":
|
|
1192
|
+
return `composition width ${actual} exceeds max width limit ${limit}.`;
|
|
1193
|
+
case "maxHeight":
|
|
1194
|
+
return `composition height ${actual} exceeds max height limit ${limit}.`;
|
|
1195
|
+
case "maxLayers":
|
|
1196
|
+
return `layer count ${actual} exceeds max layers limit ${limit}.`;
|
|
1197
|
+
case "maxAssets":
|
|
1198
|
+
return `asset count ${actual} exceeds max assets limit ${limit}.`;
|
|
1199
|
+
case "maxPropStringLength":
|
|
1200
|
+
return `prop string length ${actual} exceeds max prop string length limit ${limit}.`;
|
|
1201
|
+
case "maxAssetBytes":
|
|
1202
|
+
return `asset size ${actual} bytes exceeds max asset size limit ${limit}.`;
|
|
1203
|
+
case "maxSourceWidth":
|
|
1204
|
+
return `source width ${actual} exceeds max source width limit ${limit}.`;
|
|
1205
|
+
case "maxSourceHeight":
|
|
1206
|
+
return `source height ${actual} exceeds max source height limit ${limit}.`;
|
|
1207
|
+
case "maxBlurRadius":
|
|
1208
|
+
return `motion blur radius ${actual} exceeds max blur radius limit ${limit}.`;
|
|
1209
|
+
case "maxFilteredLayers":
|
|
1210
|
+
return `simultaneous filtered layer count ${actual} exceeds max filtered layers limit ${limit}.`;
|
|
1211
|
+
case "maxMaskedLayers":
|
|
1212
|
+
return `simultaneous masked layer count ${actual} exceeds max masked layers limit ${limit}.`;
|
|
1213
|
+
case "maxMaskSourceWidth":
|
|
1214
|
+
return `mask source width ${actual} exceeds max mask source width limit ${limit}.`;
|
|
1215
|
+
case "maxMaskSourceHeight":
|
|
1216
|
+
return `mask source height ${actual} exceeds max mask source height limit ${limit}.`;
|
|
1217
|
+
case "maxTextMotionFragments":
|
|
1218
|
+
return `text motion fragment count ${actual} exceeds max text motion fragments limit ${limit}.`;
|
|
1219
|
+
case "maxProceduralMaskPixels":
|
|
1220
|
+
return `procedural mask pixel count ${actual} exceeds max procedural mask pixels limit ${limit}.`;
|
|
1221
|
+
case "maxTransitionDurationFrames":
|
|
1222
|
+
return `transition duration ${actual} exceeds max transition duration limit ${limit}.`;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function maxLayerBlurRadius(layer) {
|
|
1226
|
+
let radius = 0;
|
|
1227
|
+
for (const effect of layer.effects ?? []) {
|
|
1228
|
+
if (effect.type === "blur") {
|
|
1229
|
+
radius = Math.max(radius, effect.radius);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (hasTransitionBlur(layer.transitionIn)) {
|
|
1233
|
+
radius = Math.max(radius, transitionAmount(layer.transitionIn, 16));
|
|
1234
|
+
}
|
|
1235
|
+
if (hasTransitionBlur(layer.transitionOut)) {
|
|
1236
|
+
radius = Math.max(radius, transitionAmount(layer.transitionOut, 16));
|
|
1237
|
+
}
|
|
1238
|
+
return radius;
|
|
1239
|
+
}
|
|
1240
|
+
function hasFilterEffect(layer) {
|
|
1241
|
+
return (layer.effects ?? []).some((effect) => effect.type === "blur");
|
|
1242
|
+
}
|
|
1243
|
+
function hasTransitionBlur(transition) {
|
|
1244
|
+
return transition?.type === "blurDissolve" || transition?.type === "radialBlur" || transition?.type === "zoomBlur" || transition?.type === "cameraWhip";
|
|
1245
|
+
}
|
|
1246
|
+
function estimateTextMotionFragments(text, split) {
|
|
1247
|
+
switch (split ?? "none") {
|
|
1248
|
+
case "char":
|
|
1249
|
+
return Array.from(text).length;
|
|
1250
|
+
case "word": {
|
|
1251
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
1252
|
+
return Math.max(1, words.length);
|
|
1253
|
+
}
|
|
1254
|
+
case "line":
|
|
1255
|
+
return Math.max(1, text.split(/\r\n|\r|\n/).length);
|
|
1256
|
+
case "none":
|
|
1257
|
+
return 1;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function maxSimultaneousLayerCount(events) {
|
|
1261
|
+
let current = 0;
|
|
1262
|
+
let maximum = 0;
|
|
1263
|
+
const sorted = [...events].sort((a, b) => a.frame - b.frame || a.delta - b.delta);
|
|
1264
|
+
for (const event of sorted) {
|
|
1265
|
+
current += event.delta;
|
|
1266
|
+
maximum = Math.max(maximum, current);
|
|
1267
|
+
}
|
|
1268
|
+
return maximum;
|
|
1269
|
+
}
|
|
1270
|
+
function applyLayerOverride(layer, override) {
|
|
1271
|
+
if (override === undefined) {
|
|
1272
|
+
return cloneJsonValue(layer);
|
|
1273
|
+
}
|
|
1274
|
+
const merged = mergeOverrideObjects(cloneJsonValue(layer), override);
|
|
1275
|
+
merged.id = layer.id;
|
|
1276
|
+
merged.type = layer.type;
|
|
1277
|
+
return merged;
|
|
1278
|
+
}
|
|
1279
|
+
function mergeOverrideObjects(base, override) {
|
|
1280
|
+
const merged = { ...base };
|
|
1281
|
+
for (const [key, value] of Object.entries(override)) {
|
|
1282
|
+
if (key === "id" || key === "type") {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const baseValue = merged[key];
|
|
1286
|
+
merged[key] =
|
|
1287
|
+
isRecord(baseValue) && isRecord(value)
|
|
1288
|
+
? mergeOverrideObjects(baseValue, value)
|
|
1289
|
+
: cloneJsonValue(value);
|
|
1290
|
+
}
|
|
1291
|
+
return merged;
|
|
1292
|
+
}
|
|
1293
|
+
function cloneJsonValue(value) {
|
|
1294
|
+
if (Array.isArray(value)) {
|
|
1295
|
+
return value.map((item) => cloneJsonValue(item));
|
|
1296
|
+
}
|
|
1297
|
+
if (isRecord(value)) {
|
|
1298
|
+
const clone = {};
|
|
1299
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
1300
|
+
clone[key] = cloneJsonValue(nestedValue);
|
|
1301
|
+
}
|
|
1302
|
+
return clone;
|
|
1303
|
+
}
|
|
1304
|
+
return value;
|
|
1305
|
+
}
|
|
1306
|
+
function mergePropValues(declarations, overrides) {
|
|
1307
|
+
const values = {};
|
|
1308
|
+
for (const [name, declaration] of Object.entries(declarations)) {
|
|
1309
|
+
if (isRecord(declaration) && "default" in declaration) {
|
|
1310
|
+
values[name] = declaration.default;
|
|
1311
|
+
}
|
|
1312
|
+
else if (!isRecord(declaration)) {
|
|
1313
|
+
values[name] = declaration;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return { ...values, ...overrides };
|
|
1317
|
+
}
|
|
1318
|
+
function resolveUnknown(value, values, errors, path, skipRootProps, isRoot) {
|
|
1319
|
+
if (typeof value === "string") {
|
|
1320
|
+
return resolveString(value, values, errors, path);
|
|
1321
|
+
}
|
|
1322
|
+
if (Array.isArray(value)) {
|
|
1323
|
+
return value.map((item, index) => resolveUnknown(item, values, errors, `${path}[${index}]`, skipRootProps, false));
|
|
1324
|
+
}
|
|
1325
|
+
if (isRecord(value)) {
|
|
1326
|
+
const resolved = {};
|
|
1327
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
1328
|
+
if (isRoot && skipRootProps && key === "props") {
|
|
1329
|
+
resolved[key] = nestedValue;
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
resolved[key] = resolveUnknown(nestedValue, values, errors, appendPath(path, key), skipRootProps, false);
|
|
1333
|
+
}
|
|
1334
|
+
return resolved;
|
|
1335
|
+
}
|
|
1336
|
+
return value;
|
|
1337
|
+
}
|
|
1338
|
+
function resolveString(value, values, errors, path) {
|
|
1339
|
+
const wholeMatch = WHOLE_TEMPLATE_PATTERN.exec(value);
|
|
1340
|
+
if (wholeMatch?.[1]) {
|
|
1341
|
+
return resolvePropValue(wholeMatch[1], value, values, errors, path);
|
|
1342
|
+
}
|
|
1343
|
+
return value.replace(TEMPLATE_PATTERN, (token, propName) => {
|
|
1344
|
+
const propValue = resolvePropValue(propName, token, values, errors, path);
|
|
1345
|
+
return stringifyPropValue(propValue);
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
function resolvePropValue(propName, fallback, values, errors, path) {
|
|
1349
|
+
if (Object.hasOwn(values, propName)) {
|
|
1350
|
+
return values[propName];
|
|
1351
|
+
}
|
|
1352
|
+
errors.push({
|
|
1353
|
+
code: "PROP_UNRESOLVED",
|
|
1354
|
+
severity: "error",
|
|
1355
|
+
message: `Missing value for template prop "${propName}".`,
|
|
1356
|
+
path,
|
|
1357
|
+
stage: "validation",
|
|
1358
|
+
hint: "Provide a prop value or a default in the document props declaration.",
|
|
1359
|
+
retryable: false,
|
|
1360
|
+
prop: propName
|
|
1361
|
+
});
|
|
1362
|
+
return fallback;
|
|
1363
|
+
}
|
|
1364
|
+
function stringifyPropValue(value) {
|
|
1365
|
+
if (value === null || value === undefined) {
|
|
1366
|
+
return "";
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof value === "string") {
|
|
1369
|
+
return value;
|
|
1370
|
+
}
|
|
1371
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
1372
|
+
return String(value);
|
|
1373
|
+
}
|
|
1374
|
+
return JSON.stringify(value);
|
|
1375
|
+
}
|
|
1376
|
+
function appendPath(path, key) {
|
|
1377
|
+
return path.length === 0 ? key : `${path}.${key}`;
|
|
1378
|
+
}
|
|
1379
|
+
function resolveOptionalSizeUnit(value, axis, dimensions) {
|
|
1380
|
+
if (value === undefined || value === 0) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
return resolveUnit(value, axis, dimensions);
|
|
1384
|
+
}
|
|
1385
|
+
function anchorKeywordToPoint(anchor) {
|
|
1386
|
+
switch (anchor) {
|
|
1387
|
+
case "top-left":
|
|
1388
|
+
return { x: 0, y: 0 };
|
|
1389
|
+
case "top":
|
|
1390
|
+
return { x: 0.5, y: 0 };
|
|
1391
|
+
case "top-right":
|
|
1392
|
+
return { x: 1, y: 0 };
|
|
1393
|
+
case "left":
|
|
1394
|
+
return { x: 0, y: 0.5 };
|
|
1395
|
+
case "center":
|
|
1396
|
+
return { x: 0.5, y: 0.5 };
|
|
1397
|
+
case "right":
|
|
1398
|
+
return { x: 1, y: 0.5 };
|
|
1399
|
+
case "bottom-left":
|
|
1400
|
+
return { x: 0, y: 1 };
|
|
1401
|
+
case "bottom":
|
|
1402
|
+
return { x: 0.5, y: 1 };
|
|
1403
|
+
case "bottom-right":
|
|
1404
|
+
return { x: 1, y: 1 };
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function percentageBasis(axis, dimensions) {
|
|
1408
|
+
return axis === "x" || axis === "width" ? dimensions.width : dimensions.height;
|
|
1409
|
+
}
|
|
1410
|
+
function lerp(from, to, progress) {
|
|
1411
|
+
return from + (to - from) * progress;
|
|
1412
|
+
}
|
|
1413
|
+
function clamp01(value) {
|
|
1414
|
+
if (value <= 0) {
|
|
1415
|
+
return 0;
|
|
1416
|
+
}
|
|
1417
|
+
if (value >= 1) {
|
|
1418
|
+
return 1;
|
|
1419
|
+
}
|
|
1420
|
+
return value;
|
|
1421
|
+
}
|
|
1422
|
+
function clamp(value, min, max) {
|
|
1423
|
+
return Math.min(max, Math.max(min, value));
|
|
1424
|
+
}
|
|
1425
|
+
function positiveFinite(value, fallback) {
|
|
1426
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1427
|
+
}
|
|
1428
|
+
function evaluateCubicBezier([x1, y1, x2, y2], progress) {
|
|
1429
|
+
let lower = 0;
|
|
1430
|
+
let upper = 1;
|
|
1431
|
+
let parameter = progress;
|
|
1432
|
+
for (let index = 0; index < 24; index += 1) {
|
|
1433
|
+
const x = cubicBezierCoordinate(parameter, x1, x2);
|
|
1434
|
+
if (Math.abs(x - progress) < 1e-7) {
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
if (x < progress) {
|
|
1438
|
+
lower = parameter;
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
upper = parameter;
|
|
1442
|
+
}
|
|
1443
|
+
parameter = (lower + upper) / 2;
|
|
1444
|
+
}
|
|
1445
|
+
return cubicBezierCoordinate(parameter, y1, y2);
|
|
1446
|
+
}
|
|
1447
|
+
function cubicBezierCoordinate(parameter, point1, point2) {
|
|
1448
|
+
const inverse = 1 - parameter;
|
|
1449
|
+
return (3 * inverse * inverse * parameter * point1 +
|
|
1450
|
+
3 * inverse * parameter * parameter * point2 +
|
|
1451
|
+
parameter * parameter * parameter);
|
|
1452
|
+
}
|
|
1453
|
+
function isRecord(value) {
|
|
1454
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1455
|
+
}
|