onda-engine 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/dist/cinema.js ADDED
@@ -0,0 +1,1687 @@
1
+ import * as Components from 'onda-engine/components';
2
+ import { settleTime, manifestEntry, fitMaxWidth, fitFontSize, measureText, resolvePlacement, isPlacement, defaultTheme } from 'onda-engine/components';
3
+ import { TransitionSeries, linearTiming, Sequence, Group, Rect, Composition, crossFade, useCurrentFrame, useVideoConfig, Camera, lumaWipe, filmBurn, whipPan, zoomBlur, typeMask, morph, gridPixelate, glassWipe, expandMorph, devicePullback, chromaticAberration, blur, none, dipToColor, depthPush, zoom, push, clockWipe, flip, iris, wipe, slide, fade, Scene3D, AbsoluteFill, Text, clipEllipse, clipPath, clipRect } from 'onda-engine/react';
4
+ import { createElement, cloneElement } from 'react';
5
+
6
+ // ../cinema/src/index.tsx
7
+
8
+ // ../cinema/src/props.ts
9
+ var PLACEMENT_COORDS = {
10
+ center: [0.5, 0.5],
11
+ top: [0.5, 0.1],
12
+ bottom: [0.5, 0.9],
13
+ left: [0.1, 0.5],
14
+ right: [0.9, 0.5],
15
+ "top-left": [0.1, 0.1],
16
+ "top-right": [0.9, 0.1],
17
+ "bottom-left": [0.1, 0.9],
18
+ "bottom-right": [0.9, 0.9],
19
+ "upper-third": [0.5, 0.28],
20
+ "lower-third": [0.5, 0.72]
21
+ };
22
+ var SELF_ANCHORING = /* @__PURE__ */ new Set([
23
+ "LowerThird",
24
+ "Callout",
25
+ "BlurReveal",
26
+ "Button",
27
+ "Captions",
28
+ "ChapterCard",
29
+ "CountUp",
30
+ "EndCard",
31
+ "InputField",
32
+ "KineticText",
33
+ "MaskReveal",
34
+ "MatrixDecode",
35
+ "PricingCard",
36
+ "QuoteCard",
37
+ "SlotMachineRoll",
38
+ "StatCard",
39
+ "Terminal",
40
+ "TextAnimator",
41
+ "TitleCard",
42
+ "Typewriter"
43
+ ]);
44
+ function placementOffset(props, w, h) {
45
+ const p = props?.placement;
46
+ let fx = 0.5;
47
+ let fy = 0.5;
48
+ const coords = typeof p === "string" ? PLACEMENT_COORDS[p] : void 0;
49
+ if (coords) [fx, fy] = coords;
50
+ else if (p && typeof p === "object") {
51
+ const o = p;
52
+ if (typeof o.x === "number") fx = o.x;
53
+ if (typeof o.y === "number") fy = o.y;
54
+ }
55
+ return [(fx - 0.5) * w, (fy - 0.5) * h];
56
+ }
57
+ var SIZE_ROLES = {
58
+ hero: 0.15,
59
+ heading: 0.09,
60
+ subheading: 0.052,
61
+ body: 0.03,
62
+ caption: 0.02
63
+ };
64
+ var roleToPx = (role, w, h) => Math.round((SIZE_ROLES[role] ?? 0) * Math.min(w, h));
65
+ var isPxSource = (name) => name === "fontSize" || name.endsWith("FontSize");
66
+ var PROP_ALIASES = {
67
+ TitleCard: {
68
+ titleSize: "titleSize",
69
+ titleFontSize: "titleSize",
70
+ subtitleSize: "subtitleSize",
71
+ subtitleFontSize: "subtitleSize"
72
+ },
73
+ Highlight: { size: "fontSize", fontSize: "fontSize" },
74
+ WordStagger: { size: "fontSize", fontSize: "fontSize" },
75
+ Captions: { size: "fontSize", fontSize: "fontSize" },
76
+ StatCard: {
77
+ numberSize: "valueSize",
78
+ numberFontSize: "valueSize",
79
+ labelSize: "labelSize",
80
+ labelFontSize: "labelSize"
81
+ }
82
+ };
83
+ function adaptProps(component, props, w, h) {
84
+ const out = { ...props ?? {} };
85
+ const aliases = PROP_ALIASES[component];
86
+ if (aliases) {
87
+ const pxValue = {};
88
+ const roleValue = {};
89
+ for (const [src, target] of Object.entries(aliases)) {
90
+ if (!(src in out)) continue;
91
+ const v = out[src];
92
+ if (isPxSource(src)) {
93
+ if (typeof v === "number") pxValue[target] = v;
94
+ } else if (typeof v === "string" && v in SIZE_ROLES) {
95
+ roleValue[target] = roleToPx(v, w, h);
96
+ } else if (typeof v === "number") {
97
+ roleValue[target] = v;
98
+ }
99
+ if (src !== target) delete out[src];
100
+ }
101
+ for (const target of /* @__PURE__ */ new Set([...Object.keys(roleValue), ...Object.keys(pxValue)])) {
102
+ out[target] = pxValue[target] ?? roleValue[target];
103
+ }
104
+ }
105
+ for (const k of Object.keys(out)) {
106
+ const v = out[k];
107
+ if (typeof v === "string" && v in SIZE_ROLES) out[k] = roleToPx(v, w, h);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ // ../cinema/src/timing.ts
113
+ function timeSpecToSeconds(spec, fps) {
114
+ if (spec == null) return 0;
115
+ if (typeof spec === "number") return spec >= 0 ? spec : 0;
116
+ const s = spec.trim();
117
+ if (s === "") return 0;
118
+ if (s.includes(":")) {
119
+ const [m, sec] = s.split(":");
120
+ return (Number(m) || 0) * 60 + (Number(sec) || 0);
121
+ }
122
+ if (s.endsWith("ms")) return (Number(s.slice(0, -2)) || 0) / 1e3;
123
+ if (s.endsWith("f")) return (Number(s.slice(0, -1)) || 0) / fps;
124
+ if (s.endsWith("s")) return Number(s.slice(0, -1)) || 0;
125
+ return Number(s) || 0;
126
+ }
127
+ function toFrames(spec, fps) {
128
+ return Math.round(timeSpecToSeconds(spec, fps) * fps);
129
+ }
130
+ function sceneDurationSeconds(scene, fps) {
131
+ if (scene.for != null) return timeSpecToSeconds(scene.for, fps);
132
+ let max = 0;
133
+ for (const track of scene.tracks) {
134
+ for (const e of track.entries) {
135
+ const end = timeSpecToSeconds(e.at, fps) + timeSpecToSeconds(e.for, fps);
136
+ if (end > max) max = end;
137
+ }
138
+ }
139
+ return max > 0 ? max : 3;
140
+ }
141
+ function sceneDurationFrames(scene, fps) {
142
+ return Math.max(1, Math.round(sceneDurationSeconds(scene, fps) * fps));
143
+ }
144
+ var DEFAULT_TRANSITION_FRAMES = 15;
145
+ function transitionOverlapFrames(prev, scene, fps) {
146
+ if (!prev || !scene.transition) return 0;
147
+ const requested = scene.transition.durationInFrames ?? DEFAULT_TRANSITION_FRAMES;
148
+ const shorter = Math.min(sceneDurationFrames(prev, fps), sceneDurationFrames(scene, fps));
149
+ return Math.max(1, Math.min(requested, Math.floor(shorter / 3)));
150
+ }
151
+ function scenePlacements(scenes, fps) {
152
+ const out = [];
153
+ let offset = 0;
154
+ scenes.forEach((scene, i) => {
155
+ const overlapIn = i > 0 ? transitionOverlapFrames(scenes[i - 1], scene, fps) : 0;
156
+ offset -= overlapIn;
157
+ const durationInFrames = sceneDurationFrames(scene, fps);
158
+ out.push({ start: offset, durationInFrames, overlapIn });
159
+ offset += durationInFrames;
160
+ });
161
+ return out;
162
+ }
163
+ function totalFrames(payload, fps) {
164
+ let total = 0;
165
+ let prev;
166
+ for (const scene of payload.scenes) {
167
+ total -= transitionOverlapFrames(prev, scene, fps);
168
+ total += sceneDurationFrames(scene, fps);
169
+ prev = scene;
170
+ }
171
+ return Math.max(1, total);
172
+ }
173
+
174
+ // ../cinema/src/inspect/constants.ts
175
+ var CONTRAST_MIN_BODY = 4.5;
176
+ var CONTRAST_MIN_LARGE = 3;
177
+ var LARGE_TEXT_PX = 24;
178
+ var LARGE_TEXT_BOLD_PX = 14 * 4 / 3;
179
+ var BOLD_WEIGHT = 700;
180
+ var READ_SECONDS_PER_WORD = 0.25;
181
+ var READ_ORIENTATION_SECONDS = 0.6;
182
+ var READ_MIN_SECONDS = 1.2;
183
+ function readingTimeSeconds(wordCount) {
184
+ return Math.max(READ_MIN_SECONDS, READ_SECONDS_PER_WORD * wordCount + READ_ORIENTATION_SECONDS);
185
+ }
186
+ var v1920 = (px) => px / 1920;
187
+ var h1080 = (px) => px / 1080;
188
+ var SAFE_AREAS = {
189
+ "16:9": { top: 0.05, bottom: 0.05, left: 0.1, right: 0.1 },
190
+ "9:16": { top: v1920(220), bottom: v1920(420), left: h1080(60), right: h1080(164) },
191
+ "1:1": { top: 0.05, bottom: 0.1, left: 0.06, right: 0.06 },
192
+ "4:5": { top: 0.05, bottom: 0.12, left: 0.06, right: 0.06 }
193
+ };
194
+ function inferFormat(width, height) {
195
+ const ratio = width / height;
196
+ const known = [
197
+ ["16:9", 16 / 9],
198
+ ["9:16", 9 / 16],
199
+ ["1:1", 1],
200
+ ["4:5", 4 / 5]
201
+ ];
202
+ let best = "16:9";
203
+ let bestD = Number.POSITIVE_INFINITY;
204
+ for (const [id, r] of known) {
205
+ const d = Math.abs(Math.log(ratio / r));
206
+ if (d < bestD) {
207
+ bestD = d;
208
+ best = id;
209
+ }
210
+ }
211
+ return best;
212
+ }
213
+ var FONT_FLOOR_PX = {
214
+ "16:9": 28,
215
+ "9:16": 40,
216
+ "1:1": 40,
217
+ "4:5": 40
218
+ };
219
+ var FONT_FLOOR_REFERENCE_DIM = 1080;
220
+ function fontFloorPx(format, width, height) {
221
+ return FONT_FLOOR_PX[format] * Math.min(width, height) / FONT_FLOOR_REFERENCE_DIM;
222
+ }
223
+ var FOCAL_COLLISION_WINDOW_SECONDS = 0.25;
224
+ var TRANSITION_BUDGET_SECONDS = 0.6;
225
+ var DENSITY_MAX_NON_AMBIENT = 5;
226
+ var DENSITY_MAX_FOCAL = 1;
227
+
228
+ // ../cinema/src/inspect/collisions.ts
229
+ var secs = (frames, fps) => `${(Math.round(frames / fps * 100) / 100).toString()}s`;
230
+ var checkCollisions = (ctx) => {
231
+ const { resolved } = ctx;
232
+ const { fps } = resolved;
233
+ const violations = [];
234
+ const windowFrames = FOCAL_COLLISION_WINDOW_SECONDS * fps;
235
+ const focal = resolved.entries.filter((e) => e.role === "focal" && e.visibleFrames > 0).sort((a, b) => a.absStart - b.absStart);
236
+ for (let i = 1; i < focal.length; i++) {
237
+ const a = focal[i - 1];
238
+ const b = focal[i];
239
+ if (!a || !b) continue;
240
+ const gap = b.absStart - a.absStart;
241
+ if (gap <= windowFrames) {
242
+ violations.push({
243
+ check: "timing.collisions",
244
+ severity: "warn",
245
+ targetId: b.targetId,
246
+ sceneId: b.sceneId,
247
+ message: `focal entrances collide: "${a.targetId}" and "${b.targetId}" begin ${secs(gap, fps)} apart (\u2264${FOCAL_COLLISION_WINDOW_SECONDS}s \u2014 inside the attention window; stagger them or demote one to 'support')`
248
+ });
249
+ }
250
+ }
251
+ for (const entry of resolved.entries) {
252
+ const settle = settleTime(entry.component, entry.adapted, fps);
253
+ if (settle === null || entry.visibleFrames <= 0) continue;
254
+ if (settle > entry.visibleFrames) {
255
+ const fitsViaClamp = manifestEntry(entry.component)?.props.some((p) => p.name === "fitToClip");
256
+ violations.push({
257
+ check: "timing.collisions",
258
+ severity: "warn",
259
+ targetId: entry.targetId,
260
+ sceneId: entry.sceneId,
261
+ message: `${entry.component}'s entrance settles at ${secs(settle, fps)} but it is only on screen for ${secs(entry.visibleFrames, fps)} \u2014 the move is cut off mid-flight`,
262
+ // Mechanical: the component's own envelope clamp, when it has one.
263
+ fix: fitsViaClamp ? { prop: "fitToClip", suggested: true } : void 0
264
+ });
265
+ }
266
+ }
267
+ const budgetFrames = Math.round(TRANSITION_BUDGET_SECONDS * fps);
268
+ for (const t of resolved.transitions) {
269
+ if (t.durationInFrames > budgetFrames) {
270
+ violations.push({
271
+ check: "timing.collisions",
272
+ severity: "warn",
273
+ targetId: t.sceneId,
274
+ sceneId: t.sceneId,
275
+ message: `"${t.type}" transition into "${t.sceneId}" runs ${secs(t.durationInFrames, fps)} \u2014 over the ${TRANSITION_BUDGET_SECONDS}s budget (both scenes read as mush for the whole overlap)`,
276
+ fix: { prop: "transition.durationInFrames", suggested: budgetFrames }
277
+ });
278
+ }
279
+ }
280
+ return violations;
281
+ };
282
+
283
+ // ../cinema/src/inspect/density.ts
284
+ function sceneDensity(resolved, sceneIndex) {
285
+ const sceneId = resolved.scenes[sceneIndex]?.scene.id ?? `scene-${sceneIndex}`;
286
+ const events = [];
287
+ for (const e of resolved.entries) {
288
+ if (e.sceneIndex !== sceneIndex || e.visibleFrames <= 0) continue;
289
+ const nonAmbient2 = e.role === "ambient" ? 0 : 1;
290
+ const focalDelta = e.role === "focal" ? 1 : 0;
291
+ events.push({ frame: e.localStart, nonAmbient: nonAmbient2, focal: focalDelta });
292
+ events.push({
293
+ frame: e.localStart + e.visibleFrames,
294
+ nonAmbient: -nonAmbient2,
295
+ focal: -focalDelta
296
+ });
297
+ }
298
+ events.sort((a, b) => a.frame - b.frame || a.nonAmbient - b.nonAmbient);
299
+ let nonAmbient = 0;
300
+ let focal = 0;
301
+ let peakNonAmbient = 0;
302
+ let peakFocal = 0;
303
+ let peakFrame = 0;
304
+ for (const ev of events) {
305
+ nonAmbient += ev.nonAmbient;
306
+ focal += ev.focal;
307
+ if (nonAmbient > peakNonAmbient) {
308
+ peakNonAmbient = nonAmbient;
309
+ peakFrame = ev.frame;
310
+ }
311
+ if (focal > peakFocal) peakFocal = focal;
312
+ }
313
+ return { sceneId, peakNonAmbient, peakFocal, peakFrame };
314
+ }
315
+ function densityMetrics(resolved) {
316
+ return resolved.scenes.map((_, i) => sceneDensity(resolved, i));
317
+ }
318
+ var checkDensity = (ctx) => {
319
+ const violations = [];
320
+ for (const d of densityMetrics(ctx.resolved)) {
321
+ if (d.peakNonAmbient > DENSITY_MAX_NON_AMBIENT) {
322
+ violations.push({
323
+ check: "density.score",
324
+ severity: "warn",
325
+ targetId: d.sceneId,
326
+ sceneId: d.sceneId,
327
+ message: `scene "${d.sceneId}" peaks at ${d.peakNonAmbient} concurrently visible non-ambient entries (frame ${d.peakFrame}, scene-local) \u2014 budget is ${DENSITY_MAX_NON_AMBIENT}; cut, stagger, or mark atmosphere 'ambient'`
328
+ });
329
+ }
330
+ if (d.peakFocal > DENSITY_MAX_FOCAL) {
331
+ violations.push({
332
+ check: "density.score",
333
+ severity: "warn",
334
+ targetId: d.sceneId,
335
+ sceneId: d.sceneId,
336
+ message: `scene "${d.sceneId}" shows ${d.peakFocal} focal entries at once \u2014 only ${DENSITY_MAX_FOCAL} thing can be THE thing; demote the rest to 'support'`
337
+ });
338
+ }
339
+ }
340
+ return violations;
341
+ };
342
+
343
+ // ../cinema/src/inspect/frames.ts
344
+ var checkTransitionCapture = (ctx) => {
345
+ const frames = ctx.opts.frames;
346
+ if (!frames || frames.length === 0) return [];
347
+ const { resolved } = ctx;
348
+ const violations = [];
349
+ for (const f of frames) {
350
+ const hit = resolved.transitions.find((t) => f >= t.start && f < t.start + t.durationInFrames);
351
+ if (!hit) continue;
352
+ const before = hit.start - 1;
353
+ const after = hit.start + hit.durationInFrames;
354
+ const candidates = [before, after].filter((c) => c >= 0 && c < resolved.totalFrames);
355
+ const suggested = candidates.length > 0 ? candidates.reduce((best, c) => Math.abs(c - f) < Math.abs(best - f) ? c : best) : f;
356
+ violations.push({
357
+ check: "frames.transitionCapture",
358
+ severity: "warn",
359
+ targetId: hit.sceneId,
360
+ sceneId: hit.sceneId,
361
+ message: `frame ${f} lands inside the "${hit.type}" transition into "${hit.sceneId}" (frames ${hit.start}\u2013${hit.start + hit.durationInFrames - 1}) \u2014 it captures two scenes blended`,
362
+ fix: { prop: "frames", suggested }
363
+ });
364
+ }
365
+ return violations;
366
+ };
367
+
368
+ // ../cinema/src/inspect/color.ts
369
+ function parseColor(value) {
370
+ if (typeof value !== "string") return null;
371
+ const s = value.trim();
372
+ if (!s.startsWith("#")) return null;
373
+ const hex = s.slice(1);
374
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
375
+ if (hex.length === 3) {
376
+ const [r, g, b] = hex;
377
+ return {
378
+ r: Number.parseInt(`${r}${r}`, 16) / 255,
379
+ g: Number.parseInt(`${g}${g}`, 16) / 255,
380
+ b: Number.parseInt(`${b}${b}`, 16) / 255,
381
+ a: 1
382
+ };
383
+ }
384
+ if (hex.length === 6 || hex.length === 8) {
385
+ return {
386
+ r: Number.parseInt(hex.slice(0, 2), 16) / 255,
387
+ g: Number.parseInt(hex.slice(2, 4), 16) / 255,
388
+ b: Number.parseInt(hex.slice(4, 6), 16) / 255,
389
+ a: hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1
390
+ };
391
+ }
392
+ return null;
393
+ }
394
+ var linearize = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
395
+ function relativeLuminance(c) {
396
+ return 0.2126 * linearize(c.r) + 0.7152 * linearize(c.g) + 0.0722 * linearize(c.b);
397
+ }
398
+ function contrastRatio(a, b) {
399
+ const la = relativeLuminance(a);
400
+ const lb = relativeLuminance(b);
401
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
402
+ return (hi + 0.05) / (lo + 0.05);
403
+ }
404
+
405
+ // ../cinema/src/inspect/resolve.ts
406
+ function resolveComposition(payload) {
407
+ const fps = payload.fps > 0 ? payload.fps : 30;
408
+ const { width, height } = payload;
409
+ const scenesIn = payload.scenes ?? [];
410
+ const placements = scenePlacements(scenesIn, fps);
411
+ const total = totalFrames(payload, fps);
412
+ const scenes = scenesIn.map((scene, index) => ({
413
+ scene,
414
+ index,
415
+ start: placements[index]?.start ?? 0,
416
+ durationInFrames: placements[index]?.durationInFrames ?? 1
417
+ }));
418
+ const transitions = [];
419
+ scenesIn.forEach((scene, i) => {
420
+ const overlap = placements[i]?.overlapIn ?? 0;
421
+ if (i > 0 && scene.transition && overlap > 0) {
422
+ transitions.push({
423
+ sceneIndex: i,
424
+ sceneId: scene.id,
425
+ type: scene.transition.type,
426
+ start: placements[i]?.start ?? 0,
427
+ durationInFrames: overlap
428
+ });
429
+ }
430
+ });
431
+ const entries = [];
432
+ for (const { scene, index: si, start: sceneStart, durationInFrames: sceneDur } of scenes) {
433
+ scene.tracks?.forEach((track, ti) => {
434
+ track.entries.forEach((entry, ei) => {
435
+ const path = `scenes[${si}].tracks[${ti}].entries[${ei}]`;
436
+ const localStart = toFrames(entry.at, fps);
437
+ const dur = toFrames(entry.for, fps);
438
+ const visibleEnd = Math.min(localStart + dur, sceneDur);
439
+ entries.push({
440
+ kind: "scene",
441
+ component: entry.component,
442
+ props: entry.props ?? {},
443
+ adapted: adaptProps(entry.component, entry.props, width, height),
444
+ role: entry.role ?? "support",
445
+ targetId: entry.id ?? path,
446
+ path,
447
+ sceneId: scene.id,
448
+ sceneIndex: si,
449
+ trackIndex: ti,
450
+ entryIndex: ei,
451
+ localStart,
452
+ absStart: sceneStart + localStart,
453
+ durationInFrames: dur,
454
+ visibleFrames: Math.max(0, visibleEnd - localStart),
455
+ raw: entry
456
+ });
457
+ });
458
+ });
459
+ }
460
+ const layerEntries = [];
461
+ payload.layers?.forEach((layer, li) => {
462
+ layer.entries.forEach((entry, ei) => {
463
+ const path = `layers[${li}].entries[${ei}]`;
464
+ const from = toFrames(entry.at ?? 0, fps);
465
+ const dur = entry.for != null ? toFrames(entry.for, fps) : Math.max(1, total - from);
466
+ layerEntries.push({
467
+ kind: "layer",
468
+ component: entry.component,
469
+ props: entry.props ?? {},
470
+ adapted: adaptProps(entry.component, entry.props, width, height),
471
+ role: "support",
472
+ targetId: entry.id ?? path,
473
+ path,
474
+ localStart: from,
475
+ absStart: from,
476
+ durationInFrames: dur,
477
+ visibleFrames: Math.max(0, Math.min(from + dur, total) - from),
478
+ under: Boolean(layer.under),
479
+ raw: entry
480
+ });
481
+ });
482
+ });
483
+ return {
484
+ payload,
485
+ fps,
486
+ width,
487
+ height,
488
+ totalFrames: total,
489
+ scenes,
490
+ entries,
491
+ layerEntries,
492
+ transitions
493
+ };
494
+ }
495
+ function windowsOverlap(aStart, aLen, bStart, bLen) {
496
+ return aStart < bStart + bLen && bStart < aStart + aLen;
497
+ }
498
+ function parseDefault(literal) {
499
+ if (literal === void 0) return void 0;
500
+ const s = literal.trim();
501
+ if (s === "true") return true;
502
+ if (s === "false") return false;
503
+ const n = Number(s);
504
+ if (s !== "" && Number.isFinite(n)) return n;
505
+ if (s.startsWith("'") && s.endsWith("'") || s.startsWith('"') && s.endsWith('"') || s.startsWith("`") && s.endsWith("`"))
506
+ return s.slice(1, -1);
507
+ return void 0;
508
+ }
509
+ var wordsOf = (s) => s.split(/\s+/).filter(Boolean).length;
510
+ function totalWords(blocks) {
511
+ return blocks.reduce((sum, b) => sum + wordsOf(b.content), 0);
512
+ }
513
+ function letterSpacingPx(value, fontSize) {
514
+ if (typeof value === "number") return value;
515
+ if (typeof value !== "string") return void 0;
516
+ const n = Number.parseFloat(value.trim());
517
+ if (!Number.isFinite(n)) return void 0;
518
+ return value.trim().endsWith("em") ? n * fontSize : n;
519
+ }
520
+ function propValue(entry, meta, name) {
521
+ const explicit = entry.adapted[name];
522
+ if (explicit !== void 0) return explicit;
523
+ return parseDefault(meta?.default);
524
+ }
525
+ var WRAPPING_COMPONENTS = /* @__PURE__ */ new Set(["Captions"]);
526
+ var NON_READABLE_TEXT_PROPS = /* @__PURE__ */ new Set(["charset"]);
527
+ function sizePropFor(m, textProp) {
528
+ const sizeProps = m.props.filter((p) => p.role === "fontSize");
529
+ const prefixed = sizeProps.find(
530
+ (p) => p.name === `${textProp}Size` || p.name === `${textProp}FontSize`
531
+ );
532
+ if (prefixed) return prefixed;
533
+ if (textProp === "text" || sizeProps.length === 1)
534
+ return sizeProps.find((p) => p.name === "fontSize") ?? sizeProps[0];
535
+ return void 0;
536
+ }
537
+ function colorPropFor(m, textProp) {
538
+ const colorProps = m.props.filter((p) => p.role === "color");
539
+ return colorProps.find((p) => p.name === `${textProp}Color`) ?? (textProp === "text" || colorProps.length === 1 ? colorProps.find((p) => p.name === "color") : void 0);
540
+ }
541
+ var MUTED_HINT = /textmuted|muted|dim\b/i;
542
+ function textBlocks(entry, theme) {
543
+ const m = manifestEntry(entry.component);
544
+ if (!m) return [];
545
+ const blocks = [];
546
+ for (const p of m.props) {
547
+ if (p.role !== "text" || NON_READABLE_TEXT_PROPS.has(p.name)) continue;
548
+ const value = propValue(entry, p, p.name);
549
+ if (typeof value !== "string" || value.trim() === "") continue;
550
+ const sizeMeta = sizePropFor(m, p.name);
551
+ const sizeValue = sizeMeta ? propValue(entry, sizeMeta, sizeMeta.name) : void 0;
552
+ const fontSize = typeof sizeValue === "number" && sizeValue > 0 ? sizeValue : 48;
553
+ const colorMeta = colorPropFor(m, p.name);
554
+ const explicitColor = colorMeta ? entry.adapted[colorMeta.name] : void 0;
555
+ let color;
556
+ let colorExplicit = false;
557
+ if (typeof explicitColor === "string") {
558
+ color = explicitColor;
559
+ colorExplicit = true;
560
+ } else if (colorMeta?.themeable) {
561
+ color = MUTED_HINT.test(colorMeta.description) ? theme.textMuted : theme.text;
562
+ } else {
563
+ color = theme.text;
564
+ }
565
+ const weightMeta = m.props.find((q) => q.name === "fontWeight");
566
+ const weightValue = weightMeta ? propValue(entry, weightMeta, "fontWeight") : void 0;
567
+ const familyValue = entry.adapted.fontFamily;
568
+ const fitValue = entry.adapted.fit;
569
+ const maxWidthValue = entry.adapted.maxWidth;
570
+ blocks.push({
571
+ textProp: p.name,
572
+ content: value,
573
+ fontSize,
574
+ sizeProp: sizeMeta?.name,
575
+ color,
576
+ colorExplicit,
577
+ fontWeight: typeof weightValue === "number" ? weightValue : 400,
578
+ fontFamily: typeof familyValue === "string" ? familyValue : void 0,
579
+ letterSpacing: letterSpacingPx(entry.adapted.letterSpacing, fontSize),
580
+ fit: fitValue === "frame" || fitValue === "none" ? fitValue : void 0,
581
+ maxWidth: typeof maxWidthValue === "number" ? maxWidthValue : void 0
582
+ });
583
+ }
584
+ return blocks;
585
+ }
586
+
587
+ // ../cinema/src/inspect/legibility.ts
588
+ function isCovering(component) {
589
+ if (component === "Scrim") return true;
590
+ const m = manifestEntry(component);
591
+ return m?.occlusion === "full_frame" || m?.sceneRole === "background";
592
+ }
593
+ function isMediaBearing(entry) {
594
+ const m = manifestEntry(entry.component);
595
+ if (!m) return false;
596
+ if (m.category === "Media") return true;
597
+ return m.props.some(
598
+ (p) => p.role === "url" && (p.required || entry.adapted[p.name] !== void 0)
599
+ );
600
+ }
601
+ function fillColors(entry) {
602
+ const m = manifestEntry(entry.component);
603
+ if (!m) return [];
604
+ const out = [];
605
+ for (const p of m.props) {
606
+ if (p.role !== "color") continue;
607
+ const value = entry.adapted[p.name] ?? parseDefault(p.default);
608
+ const candidates = Array.isArray(value) ? value : [value];
609
+ for (const c of candidates) {
610
+ const parsed = parseColor(c);
611
+ if (parsed) out.push(parsed);
612
+ }
613
+ }
614
+ if (out.length === 0 && entry.component === "Scrim") {
615
+ const white = parseColor("#ffffff");
616
+ if (white) out.push(white);
617
+ }
618
+ return out;
619
+ }
620
+ function coverOpacity(entry) {
621
+ const m = manifestEntry(entry.component);
622
+ const meta = m?.props.find((p) => p.name === "opacity");
623
+ const v = entry.adapted.opacity ?? parseDefault(meta?.default);
624
+ return typeof v === "number" && v >= 0 && v <= 1 ? v : 1;
625
+ }
626
+ function over(top, alpha, under) {
627
+ const a = Math.max(0, Math.min(1, alpha * top.a));
628
+ return {
629
+ r: top.r * a + under.r * (1 - a),
630
+ g: top.g * a + under.g * (1 - a),
631
+ b: top.b * a + under.b * (1 - a),
632
+ a: 1
633
+ };
634
+ }
635
+ function backdropFor(target, resolved) {
636
+ const beneath = [];
637
+ const overlapsTarget = (e) => windowsOverlap(e.absStart, e.visibleFrames, target.absStart, target.visibleFrames);
638
+ for (const e of resolved.layerEntries) {
639
+ if (e.under && overlapsTarget(e)) beneath.push(e);
640
+ }
641
+ if (target.kind === "scene") {
642
+ for (const e of resolved.entries) {
643
+ if (e.sceneIndex !== target.sceneIndex || !overlapsTarget(e)) continue;
644
+ const ti = e.trackIndex ?? 0;
645
+ const ei = e.entryIndex ?? 0;
646
+ const tTi = target.trackIndex ?? 0;
647
+ const tEi = target.entryIndex ?? 0;
648
+ if (ti < tTi || ti === tTi && ei < tEi) beneath.push(e);
649
+ }
650
+ } else {
651
+ for (const e of resolved.entries) if (overlapsTarget(e)) beneath.push(e);
652
+ }
653
+ const veils = [];
654
+ for (let i = beneath.length - 1; i >= 0; i--) {
655
+ const e = beneath[i];
656
+ if (!e || !isCovering(e.component)) continue;
657
+ if (isMediaBearing(e)) return { kind: "media" };
658
+ const colors = fillColors(e);
659
+ if (colors.length === 0) return { kind: "unknown" };
660
+ const alpha = coverOpacity(e);
661
+ const opaque = alpha >= 0.95 && colors.every((c) => c.a >= 0.95);
662
+ if (opaque) return { kind: "colors", colors: composite(colors, veils) };
663
+ veils.push({ colors, alpha });
664
+ }
665
+ const bg = parseColor(resolveBackground(resolved.payload.brand));
666
+ if (!bg) return { kind: "unknown" };
667
+ return { kind: "colors", colors: composite([bg], veils) };
668
+ }
669
+ function resolveBackground(brand) {
670
+ return brand?.bg ?? "#08080a";
671
+ }
672
+ function composite(base, veils) {
673
+ let result = base;
674
+ for (let i = veils.length - 1; i >= 0; i--) {
675
+ const veil = veils[i];
676
+ if (!veil) continue;
677
+ const next = [];
678
+ for (const under of result)
679
+ for (const top of veil.colors) next.push(over(top, veil.alpha, under));
680
+ result = next;
681
+ }
682
+ return result;
683
+ }
684
+ function requiredContrast(fontSize, fontWeight) {
685
+ const large = fontSize >= LARGE_TEXT_PX || fontWeight >= BOLD_WEIGHT && fontSize >= LARGE_TEXT_BOLD_PX;
686
+ return large ? CONTRAST_MIN_LARGE : CONTRAST_MIN_BODY;
687
+ }
688
+ var fmt = (n) => (Math.round(n * 100) / 100).toString();
689
+ var checkLegibility = (ctx) => {
690
+ const { resolved, theme, format } = ctx;
691
+ const floor = fontFloorPx(format, resolved.width, resolved.height);
692
+ const violations = [];
693
+ for (const entry of [...resolved.entries, ...resolved.layerEntries]) {
694
+ const blocks = textBlocks(entry, theme);
695
+ if (blocks.length === 0) continue;
696
+ for (const b of blocks) {
697
+ if (b.fontSize < floor) {
698
+ violations.push({
699
+ check: "text.legibility",
700
+ severity: "warn",
701
+ targetId: entry.targetId,
702
+ sceneId: entry.sceneId,
703
+ message: `${entry.component} "${b.textProp}" is ${fmt(b.fontSize)}px \u2014 below the ${fmt(floor)}px ${format} floor (body text on a phone-scale canvas needs \u2265${fmt(floor)}px; legibility.info video-text rules)`,
704
+ fix: { prop: b.sizeProp ?? "fontSize", suggested: Math.ceil(floor) }
705
+ });
706
+ }
707
+ }
708
+ const backdrop = backdropFor(entry, resolved);
709
+ if (backdrop.kind === "media") {
710
+ violations.push({
711
+ check: "text.legibility",
712
+ severity: "info",
713
+ targetId: entry.targetId,
714
+ sceneId: entry.sceneId,
715
+ message: `${entry.component} sits over image/video \u2014 contrast unverifiable analytically (verify on a rendered frame, or put a Scrim behind it)`
716
+ });
717
+ continue;
718
+ }
719
+ if (backdrop.kind === "unknown") {
720
+ violations.push({
721
+ check: "text.legibility",
722
+ severity: "info",
723
+ targetId: entry.targetId,
724
+ sceneId: entry.sceneId,
725
+ message: `${entry.component}'s backdrop color can't be resolved analytically \u2014 contrast unverified`
726
+ });
727
+ continue;
728
+ }
729
+ for (const b of blocks) {
730
+ const textColor = parseColor(b.color);
731
+ if (!textColor) continue;
732
+ const required = requiredContrast(b.fontSize, b.fontWeight);
733
+ let worst = Number.POSITIVE_INFINITY;
734
+ for (const c of backdrop.colors) worst = Math.min(worst, contrastRatio(textColor, c));
735
+ if (worst < required) {
736
+ violations.push({
737
+ check: "text.legibility",
738
+ severity: "error",
739
+ targetId: entry.targetId,
740
+ sceneId: entry.sceneId,
741
+ message: `${entry.component} "${b.textProp}" contrast is ${fmt(worst)}:1 against its backdrop \u2014 below the WCAG ${fmt(required)}:1 minimum for ${fmt(b.fontSize)}px text (SC 1.4.3)`
742
+ });
743
+ }
744
+ }
745
+ }
746
+ return violations;
747
+ };
748
+ var checkOverflow = (ctx) => {
749
+ const { resolved, safe, theme, format } = ctx;
750
+ const { width, height } = resolved;
751
+ const safeRect = {
752
+ left: safe.left * width,
753
+ top: safe.top * height,
754
+ right: width - safe.right * width,
755
+ bottom: height - safe.bottom * height
756
+ };
757
+ const violations = [];
758
+ for (const entry of [...resolved.entries, ...resolved.layerEntries]) {
759
+ if (WRAPPING_COMPONENTS.has(entry.component)) continue;
760
+ const blocks = textBlocks(entry, theme);
761
+ for (const b of blocks) {
762
+ if (b.content.includes("\n")) continue;
763
+ const measureOpts = {
764
+ fontFamily: b.fontFamily,
765
+ fontWeight: b.fontWeight,
766
+ letterSpacing: b.letterSpacing
767
+ };
768
+ const cap = fitMaxWidth({ fit: b.fit, maxWidth: b.maxWidth }, width);
769
+ const size = cap !== void 0 ? fitFontSize(b.content, b.fontSize, cap, measureOpts) : b.fontSize;
770
+ const m = measureText(b.content, size, measureOpts);
771
+ if (m.width <= 0) continue;
772
+ const placement = entry.adapted.placement;
773
+ const box = resolvePlacement(
774
+ isPlacement(placement) ? placement : void 0,
775
+ { width, height },
776
+ { width: m.width, height: m.height }
777
+ );
778
+ const x0 = box.originX;
779
+ const y0 = box.originY;
780
+ const x1 = x0 + m.width;
781
+ const y1 = y0 + m.height;
782
+ const escapesFrame = x0 < 0 || y0 < 0 || x1 > width || y1 > height;
783
+ const escapesSafe = x0 < safeRect.left || y0 < safeRect.top || x1 > safeRect.right || y1 > safeRect.bottom;
784
+ if (!escapesFrame && !escapesSafe) continue;
785
+ const boxFitsSafe = (fs) => {
786
+ const fm = measureText(b.content, fs, measureOpts);
787
+ const fb = resolvePlacement(
788
+ isPlacement(placement) ? placement : void 0,
789
+ { width, height },
790
+ { width: fm.width, height: fm.height }
791
+ );
792
+ return fb.originX >= safeRect.left && fb.originY >= safeRect.top && fb.originX + fm.width <= safeRect.right && fb.originY + fm.height <= safeRect.bottom;
793
+ };
794
+ let lo = 1;
795
+ let hi = Math.floor(size);
796
+ let fitted = 0;
797
+ while (lo <= hi) {
798
+ const mid = lo + hi >> 1;
799
+ if (boxFitsSafe(mid)) {
800
+ fitted = mid;
801
+ lo = mid + 1;
802
+ } else {
803
+ hi = mid - 1;
804
+ }
805
+ }
806
+ const fix = b.sizeProp && fitted > 0 && fitted < size ? { prop: b.sizeProp, suggested: fitted } : void 0;
807
+ violations.push({
808
+ check: "layout.overflow",
809
+ severity: escapesFrame ? "error" : "warn",
810
+ targetId: entry.targetId,
811
+ sceneId: entry.sceneId,
812
+ message: escapesFrame ? `${entry.component} "${b.textProp}" measures ${Math.round(m.width)}px wide at ${Math.round(size)}px \u2014 it escapes the ${width}\xD7${height} frame` : `${entry.component} "${b.textProp}" measures ${Math.round(m.width)}px wide at ${Math.round(size)}px \u2014 outside the ${format} safe area (platform UI / title-safe band)`,
813
+ fix
814
+ });
815
+ }
816
+ }
817
+ return violations;
818
+ };
819
+
820
+ // ../cinema/src/inspect/reading.ts
821
+ var checkReadingTime = (ctx) => {
822
+ const { resolved, theme } = ctx;
823
+ const violations = [];
824
+ for (const entry of [...resolved.entries, ...resolved.layerEntries]) {
825
+ if (entry.role === "ambient") continue;
826
+ const words = totalWords(textBlocks(entry, theme));
827
+ if (words === 0) continue;
828
+ const needed = readingTimeSeconds(words);
829
+ const visible = entry.visibleFrames / resolved.fps;
830
+ if (visible >= needed) continue;
831
+ const suggested = Math.ceil(needed * 10) / 10;
832
+ violations.push({
833
+ check: "timing.readingTime",
834
+ severity: "warn",
835
+ targetId: entry.targetId,
836
+ sceneId: entry.sceneId,
837
+ message: `${entry.component} shows ${words} word${words === 1 ? "" : "s"} for ${(Math.round(visible * 100) / 100).toString()}s \u2014 ${suggested}s needed to read it (0.25s/word + 0.6s orientation, \u22651.2s)`,
838
+ fix: { prop: "for", suggested: `${suggested}s` }
839
+ });
840
+ }
841
+ return violations;
842
+ };
843
+
844
+ // ../cinema/src/inspect/index.ts
845
+ var CHECKS = {
846
+ "text.legibility": checkLegibility,
847
+ "layout.overflow": checkOverflow,
848
+ "timing.readingTime": checkReadingTime,
849
+ "timing.collisions": checkCollisions,
850
+ "density.score": checkDensity,
851
+ "frames.transitionCapture": checkTransitionCapture
852
+ };
853
+ function inspect(payload, opts = {}) {
854
+ const resolved = resolveComposition(payload);
855
+ const format = opts.format ?? inferFormat(resolved.width, resolved.height);
856
+ const ctx = {
857
+ payload,
858
+ resolved,
859
+ format,
860
+ safe: SAFE_AREAS[format],
861
+ theme: {
862
+ text: payload.brand?.text ?? defaultTheme.text,
863
+ textMuted: payload.brand?.dim ?? defaultTheme.textMuted,
864
+ background: payload.brand?.bg ?? "#08080a"
865
+ },
866
+ opts
867
+ };
868
+ const violations = [];
869
+ for (const check of Object.values(CHECKS)) violations.push(...check(ctx));
870
+ const summary = { error: 0, warn: 0, info: 0 };
871
+ for (const v of violations) summary[v.severity]++;
872
+ const density = densityMetrics(resolved);
873
+ return {
874
+ violations,
875
+ summary,
876
+ format,
877
+ fps: resolved.fps,
878
+ totalFrames: resolved.totalFrames,
879
+ density
880
+ };
881
+ }
882
+
883
+ // ../cinema/src/index.tsx
884
+ var DUR = Components.DURATION;
885
+ var num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
886
+ var asDir = (v) => v === "down" || v === "left" || v === "right" || v === "up" ? v : "up";
887
+ var exitStart = (p, dur, exitDur) => Math.max(0, dur - exitDur - num(p.delay, 0));
888
+ var CHOREOGRAPHY = {
889
+ // Direct-manipulation keyframe animation (the Studio editor). Shares the sampler
890
+ // with the Keyframes component + Studio preview, so interpolation is identical.
891
+ // Position is an OFFSET from placement (additive); opacity/scale absolute;
892
+ // rotation in degrees.
893
+ keyframes: (frame, _fps, p) => {
894
+ const k = Components.sampleKeyframes(p, frame);
895
+ return {
896
+ opacity: k.opacity,
897
+ x: k.x,
898
+ y: k.y,
899
+ scaleX: k.scale,
900
+ scaleY: k.scale,
901
+ rotation: k.rotation
902
+ };
903
+ },
904
+ entryFade: (frame, fps, p) => Components.entryFade({
905
+ frame,
906
+ fps,
907
+ delay: num(p.delay, 0),
908
+ durationInFrames: num(p.durationInFrames, DUR.base)
909
+ }),
910
+ entryFadeRise: (frame, fps, p) => Components.entryFadeRise({
911
+ frame,
912
+ fps,
913
+ delay: num(p.delay, 0),
914
+ durationInFrames: num(p.durationInFrames, DUR.base),
915
+ travelPx: num(p.travelPx, 12)
916
+ }),
917
+ entrySlide: (frame, fps, p) => Components.entrySlide({
918
+ frame,
919
+ fps,
920
+ delay: num(p.delay, 0),
921
+ durationInFrames: num(p.durationInFrames, DUR.base),
922
+ direction: asDir(p.direction),
923
+ distance: num(p.distance, 12)
924
+ }),
925
+ entryScale: (frame, fps, p) => Components.entryScale({
926
+ frame,
927
+ fps,
928
+ delay: num(p.delay, 0),
929
+ durationInFrames: num(p.durationInFrames, DUR.base),
930
+ from: num(p.from, 0.9)
931
+ }),
932
+ heroReveal: (frame, fps, p) => Components.heroReveal({
933
+ frame,
934
+ fps,
935
+ delay: num(p.delay, 0),
936
+ durationInFrames: num(p.durationInFrames, DUR.slow),
937
+ travelPx: num(p.travelPx, 16)
938
+ }),
939
+ exitFade: (frame, fps, p, dur) => {
940
+ const ed = num(p.durationInFrames, DUR.fast);
941
+ return Components.exitFade({ frame, fps, delay: exitStart(p, dur, ed), durationInFrames: ed });
942
+ },
943
+ exitFadeFall: (frame, fps, p, dur) => {
944
+ const ed = num(p.durationInFrames, DUR.fast);
945
+ return Components.exitFadeFall({
946
+ frame,
947
+ fps,
948
+ delay: exitStart(p, dur, ed),
949
+ durationInFrames: ed,
950
+ travelPx: num(p.travelPx, 8)
951
+ });
952
+ },
953
+ exitSlide: (frame, fps, p, dur) => {
954
+ const ed = num(p.durationInFrames, DUR.fast);
955
+ return Components.exitSlide({
956
+ frame,
957
+ fps,
958
+ delay: exitStart(p, dur, ed),
959
+ durationInFrames: ed,
960
+ direction: asDir(p.direction),
961
+ distance: num(p.distance, 12)
962
+ });
963
+ },
964
+ exitScale: (frame, fps, p, dur) => {
965
+ const ed = num(p.durationInFrames, DUR.fast);
966
+ return Components.exitScale({ frame, fps, delay: exitStart(p, dur, ed), durationInFrames: ed });
967
+ }
968
+ };
969
+ var REST = { opacity: 1, x: 0, y: 0, scaleX: 1, scaleY: 1 };
970
+ function composeMotion(animate, frame, fps, dur) {
971
+ let m = REST;
972
+ for (const a of animate ?? []) {
973
+ const fn = CHOREOGRAPHY[a.pattern];
974
+ if (!fn) continue;
975
+ const r = fn(frame, fps, a.params ?? {}, dur);
976
+ m = {
977
+ opacity: m.opacity * r.opacity,
978
+ x: m.x + r.x,
979
+ y: m.y + r.y,
980
+ scaleX: m.scaleX * r.scaleX,
981
+ scaleY: m.scaleY * r.scaleY,
982
+ rotation: (m.rotation ?? 0) + (r.rotation ?? 0)
983
+ };
984
+ }
985
+ return m;
986
+ }
987
+ var TRANSITIONS = {
988
+ "cross-fade": () => crossFade(),
989
+ fade: () => fade(),
990
+ slide: (o) => slide(o),
991
+ wipe: (o) => wipe(o),
992
+ iris: () => iris(),
993
+ flip: () => flip(),
994
+ "clock-wipe": () => clockWipe(),
995
+ push: (o) => push(o),
996
+ zoom: (o) => zoom(o),
997
+ "depth-push": () => depthPush(),
998
+ "dip-to-color": (o) => dipToColor(o),
999
+ none: () => none(),
1000
+ // Effect transitions — approximated in the presentation layer (no engine blur).
1001
+ blur: () => blur(),
1002
+ "chromatic-aberration": () => chromaticAberration(),
1003
+ "device-pullback": () => devicePullback(),
1004
+ "expand-morph": () => expandMorph(),
1005
+ "glass-wipe": (o) => glassWipe(o),
1006
+ "grid-pixelate": (o) => gridPixelate(o),
1007
+ morph: () => morph(),
1008
+ "type-mask": (o) => typeMask(o),
1009
+ "zoom-blur": (o) => zoomBlur(o),
1010
+ "whip-pan": (o) => whipPan(o),
1011
+ "film-burn": (o) => filmBurn(o),
1012
+ "luma-wipe": () => lumaWipe()
1013
+ };
1014
+ var presentationFor = (type, options) => (TRANSITIONS[type] ?? crossFade)(options);
1015
+ var isComponent = (v) => typeof v === "function";
1016
+ var NAME_ALIASES = {
1017
+ RgbGlitchText: "RgbGlitch"
1018
+ // ondajs `rgb-glitch-text` → @onda `RgbGlitch`
1019
+ };
1020
+ function defaultRegistry() {
1021
+ const reg = {};
1022
+ for (const [name, value] of Object.entries(Components)) {
1023
+ if (/^[A-Z]/.test(name) && isComponent(value)) {
1024
+ reg[name] = value;
1025
+ }
1026
+ }
1027
+ for (const [alias, target] of Object.entries(NAME_ALIASES)) {
1028
+ if (reg[target] && !reg[alias]) reg[alias] = reg[target];
1029
+ }
1030
+ return reg;
1031
+ }
1032
+ function errorPlaceholder(name) {
1033
+ return createElement(
1034
+ AbsoluteFill,
1035
+ { justify: "center", align: "center" },
1036
+ createElement(
1037
+ Text,
1038
+ { fontSize: 32, color: "#ff6b6b", fontWeight: 600 },
1039
+ `\u26A0 unknown component: ${name}`
1040
+ )
1041
+ );
1042
+ }
1043
+ function matteClipProps(matte, clip, registry, width, height) {
1044
+ const out = {};
1045
+ if (matte) {
1046
+ const Comp = registry[matte.component];
1047
+ if (Comp) {
1048
+ out.matte = createElement(Comp, adaptProps(matte.component, matte.props, width, height));
1049
+ out.matteMode = matte.mode ?? "alpha";
1050
+ }
1051
+ }
1052
+ if (clip) {
1053
+ out.clip = clip.shape === "ellipse" ? clipEllipse(clip.width ?? width, clip.height ?? height) : clip.shape === "path" && clip.data ? clipPath(clip.data) : clipRect(clip.width ?? width, clip.height ?? height, clip.cornerRadius);
1054
+ }
1055
+ return out;
1056
+ }
1057
+ function AnimatedEntry({
1058
+ component,
1059
+ props,
1060
+ animate,
1061
+ effects,
1062
+ depth,
1063
+ transform3d,
1064
+ matte,
1065
+ clip,
1066
+ durationInFrames,
1067
+ registry
1068
+ }) {
1069
+ const frame = useCurrentFrame();
1070
+ const { fps, width, height } = useVideoConfig();
1071
+ const m = composeMotion(animate, frame, fps, durationInFrames);
1072
+ const Comp = registry[component];
1073
+ const base = Comp ? createElement(Comp, adaptProps(component, props, width, height)) : errorPlaceholder(component);
1074
+ const fxChild = effects || typeof depth === "number" ? createElement(
1075
+ Group,
1076
+ { ...effects ?? {}, ...typeof depth === "number" ? { depth } : {} },
1077
+ base
1078
+ ) : base;
1079
+ const matteChild = matte || clip ? createElement(
1080
+ Group,
1081
+ matteClipProps(matte, clip, registry, width, height),
1082
+ fxChild
1083
+ ) : fxChild;
1084
+ const child = transform3d && Object.keys(transform3d).length > 0 ? createElement(
1085
+ Scene3D,
1086
+ {},
1087
+ createElement(
1088
+ Group,
1089
+ { ...transform3d },
1090
+ matteChild
1091
+ )
1092
+ ) : matteChild;
1093
+ const cx = width / 2;
1094
+ const cy = height / 2;
1095
+ const [px, py] = SELF_ANCHORING.has(component) ? [0, 0] : placementOffset(props, width, height);
1096
+ return createElement(
1097
+ Group,
1098
+ { x: m.x + px, y: m.y + py, opacity: m.opacity },
1099
+ createElement(
1100
+ Group,
1101
+ { x: cx, y: cy },
1102
+ createElement(
1103
+ Group,
1104
+ { scaleX: m.scaleX, scaleY: m.scaleY, rotation: m.rotation },
1105
+ createElement(Group, { x: -cx, y: -cy }, child)
1106
+ )
1107
+ )
1108
+ );
1109
+ }
1110
+ function MorphSuppressGate({
1111
+ suppress,
1112
+ children
1113
+ }) {
1114
+ const frame = useCurrentFrame();
1115
+ if (frame >= suppress.from && frame < suppress.to) return null;
1116
+ return children;
1117
+ }
1118
+ function EntrySlot({
1119
+ entry,
1120
+ registry,
1121
+ suppress
1122
+ }) {
1123
+ const { fps } = useVideoConfig();
1124
+ const animated = createElement(AnimatedEntry, {
1125
+ component: entry.component,
1126
+ props: entry.props,
1127
+ animate: entry.animate,
1128
+ effects: entry.effects,
1129
+ depth: entry.depth,
1130
+ transform3d: entry.transform3d,
1131
+ matte: entry.matte,
1132
+ clip: entry.clip,
1133
+ durationInFrames: toFrames(entry.for, fps),
1134
+ registry
1135
+ });
1136
+ return createElement(
1137
+ Sequence,
1138
+ { from: toFrames(entry.at, fps), durationInFrames: toFrames(entry.for, fps) },
1139
+ // biome-ignore lint/correctness/noChildrenProp: raw createElement props object, not JSX
1140
+ suppress ? createElement(MorphSuppressGate, { suppress, children: animated }) : animated
1141
+ );
1142
+ }
1143
+ function AnimatedCamera({
1144
+ move,
1145
+ durationInFrames,
1146
+ children
1147
+ }) {
1148
+ const frame = useCurrentFrame();
1149
+ const { width, height } = useVideoConfig();
1150
+ const p = durationInFrames > 1 ? Math.min(1, Math.max(0, frame / (durationInFrames - 1))) : 1;
1151
+ const e = p * p * (3 - 2 * p);
1152
+ const from = move.from ?? {};
1153
+ const to = move.to ?? {};
1154
+ const lerp = (a, b, d) => {
1155
+ const av = a ?? d;
1156
+ const bv = b ?? d;
1157
+ return av + (bv - av) * e;
1158
+ };
1159
+ return createElement(
1160
+ Camera,
1161
+ {
1162
+ focusX: lerp(from.x, to.x, 0.5) * width,
1163
+ focusY: lerp(from.y, to.y, 0.5) * height,
1164
+ zoom: lerp(from.zoom, to.zoom, 1),
1165
+ rotate: lerp(from.rotate, to.rotate, 0)
1166
+ },
1167
+ children
1168
+ );
1169
+ }
1170
+ function SceneTracks({
1171
+ scene,
1172
+ registry,
1173
+ suppress,
1174
+ responsive
1175
+ }) {
1176
+ return createElement(
1177
+ Group,
1178
+ null,
1179
+ ...scene.tracks.map(
1180
+ (track, ti) => createElement(
1181
+ Group,
1182
+ { key: track.id ?? `track-${ti}` },
1183
+ ...track.entries.map((entry, ei) => {
1184
+ const key = entry.id ?? `entry-${ei}`;
1185
+ const slot = createElement(EntrySlot, { entry, registry, suppress: suppress?.get(entry) });
1186
+ if (!responsive) return cloneElement(slot, { key });
1187
+ const t = Components.responsiveEntryTransform(
1188
+ Components.entryDesignAnchor(entry.props),
1189
+ responsive.design,
1190
+ responsive.out
1191
+ );
1192
+ if (t.x === 0 && t.y === 0 && t.scale === 1) return cloneElement(slot, { key });
1193
+ return createElement(
1194
+ Group,
1195
+ { key, x: t.x, y: t.y, scaleX: t.scale, scaleY: t.scale },
1196
+ slot
1197
+ );
1198
+ })
1199
+ )
1200
+ )
1201
+ );
1202
+ }
1203
+ function entryTransform(entry, w, h) {
1204
+ const [px, py] = SELF_ANCHORING.has(entry.component) ? [0, 0] : placementOffset(entry.props, w, h);
1205
+ const s = entry.props?.scale;
1206
+ return { x: px, y: py, scale: typeof s === "number" && Number.isFinite(s) ? s : 1 };
1207
+ }
1208
+ function morphEntriesAtTail(scene, fps, sceneDur, overlap) {
1209
+ const m = /* @__PURE__ */ new Map();
1210
+ const cutStart = sceneDur - overlap;
1211
+ for (const track of scene.tracks) {
1212
+ for (const e of track.entries) {
1213
+ if (!e.morphKey) continue;
1214
+ const start = toFrames(e.at, fps);
1215
+ const end = start + toFrames(e.for, fps);
1216
+ if (start < sceneDur && end > cutStart) m.set(e.morphKey, e);
1217
+ }
1218
+ }
1219
+ return m;
1220
+ }
1221
+ function morphEntriesAtHead(scene, fps, overlap) {
1222
+ const m = /* @__PURE__ */ new Map();
1223
+ for (const track of scene.tracks) {
1224
+ for (const e of track.entries) {
1225
+ if (!e.morphKey) continue;
1226
+ const start = toFrames(e.at, fps);
1227
+ const end = start + toFrames(e.for, fps);
1228
+ if (start < overlap && end > 0) m.set(e.morphKey, e);
1229
+ }
1230
+ }
1231
+ return m;
1232
+ }
1233
+ function MorphLayer({ pair, registry }) {
1234
+ const frame = useCurrentFrame();
1235
+ const { width, height } = useVideoConfig();
1236
+ const n = pair.overlapFrames;
1237
+ const p = n > 1 ? Math.min(1, Math.max(0, frame / (n - 1))) : 1;
1238
+ const e = p * p * (3 - 2 * p);
1239
+ const lerp = (a, b) => a + (b - a) * e;
1240
+ const x = lerp(pair.from.x, pair.toT.x);
1241
+ const y = lerp(pair.from.y, pair.toT.y);
1242
+ const scale = lerp(pair.from.scale, pair.toT.scale);
1243
+ const Comp = registry[pair.to.component];
1244
+ const base = Comp ? createElement(Comp, adaptProps(pair.to.component, pair.to.props, width, height)) : errorPlaceholder(pair.to.component);
1245
+ const cx = width / 2;
1246
+ const cy = height / 2;
1247
+ return createElement(
1248
+ Group,
1249
+ { x, y },
1250
+ createElement(
1251
+ Group,
1252
+ { x: cx, y: cy },
1253
+ createElement(
1254
+ Group,
1255
+ { scaleX: scale, scaleY: scale },
1256
+ createElement(Group, { x: -cx, y: -cy }, base)
1257
+ )
1258
+ )
1259
+ );
1260
+ }
1261
+ function planMorphs(scenes, fps, w, h, sceneStart, sceneDur) {
1262
+ const pairs = [];
1263
+ const suppress = /* @__PURE__ */ new Map();
1264
+ const addSuppress = (sceneIdx, entry, win) => {
1265
+ let m = suppress.get(sceneIdx);
1266
+ if (!m) {
1267
+ m = /* @__PURE__ */ new Map();
1268
+ suppress.set(sceneIdx, m);
1269
+ }
1270
+ m.set(entry, win);
1271
+ };
1272
+ for (let i = 1; i < scenes.length; i++) {
1273
+ const prev = scenes[i - 1];
1274
+ const cur = scenes[i];
1275
+ if (!prev || !cur || !cur.transition) continue;
1276
+ const overlap = transitionOverlapFrames(prev, cur, fps);
1277
+ if (overlap <= 0) continue;
1278
+ const prevDur = sceneDur[i - 1] ?? 0;
1279
+ const aMap = morphEntriesAtTail(prev, fps, prevDur, overlap);
1280
+ if (aMap.size === 0) continue;
1281
+ const bMap = morphEntriesAtHead(cur, fps, overlap);
1282
+ if (bMap.size === 0) continue;
1283
+ const overlapStart = sceneStart[i] ?? 0;
1284
+ for (const [key, aEntry] of aMap) {
1285
+ const bEntry = bMap.get(key);
1286
+ if (!bEntry) continue;
1287
+ pairs.push({
1288
+ to: bEntry,
1289
+ from: entryTransform(aEntry, w, h),
1290
+ toT: entryTransform(bEntry, w, h),
1291
+ overlapStart,
1292
+ overlapFrames: overlap
1293
+ });
1294
+ addSuppress(i - 1, aEntry, { from: prevDur - overlap, to: prevDur });
1295
+ addSuppress(i, bEntry, { from: 0, to: overlap });
1296
+ }
1297
+ }
1298
+ return { pairs, suppress };
1299
+ }
1300
+ function brandToTheme(brand) {
1301
+ const t = {};
1302
+ if (brand.accent) t.accent = brand.accent;
1303
+ if (brand.accentSoft) t.accentSoft = brand.accentSoft;
1304
+ if (brand.text) t.text = brand.text;
1305
+ if (brand.dim) t.textMuted = brand.dim;
1306
+ if (brand.bg) t.background = brand.bg;
1307
+ if (brand.surface) t.surface = brand.surface;
1308
+ if (brand.border) t.border = brand.border;
1309
+ if (brand.fontBody) t.fontFamily = brand.fontBody;
1310
+ if (brand.fontDisplay) t.headingFamily = brand.fontDisplay;
1311
+ return t;
1312
+ }
1313
+ function fitGroup(scene, width, height, child) {
1314
+ const dw = scene.designWidth;
1315
+ const dh = scene.designHeight;
1316
+ const fit = scene.fit;
1317
+ if (!dw || !dh || !fit || fit === "responsive" || dw === width && dh === height) return child;
1318
+ const scale = fit === "contain" ? Math.min(width / dw, height / dh) : Math.max(width / dw, height / dh);
1319
+ const x = (width - dw * scale) / 2;
1320
+ const y = (height - dh * scale) / 2;
1321
+ return createElement(Group, { x, y, scaleX: scale, scaleY: scale }, child);
1322
+ }
1323
+ function sceneResponsive(scene, width, height) {
1324
+ const dw = scene.designWidth;
1325
+ const dh = scene.designHeight;
1326
+ if (scene.fit !== "responsive" || !dw || !dh || dw === width && dh === height) return void 0;
1327
+ return { design: { width: dw, height: dh }, out: { width, height } };
1328
+ }
1329
+ function buildComposition(payload, opts = {}) {
1330
+ const {
1331
+ width,
1332
+ height,
1333
+ fps,
1334
+ scenes,
1335
+ layers = [],
1336
+ brand,
1337
+ linear,
1338
+ finish,
1339
+ motionBlur,
1340
+ dof
1341
+ } = payload;
1342
+ const registry = opts.registry ?? defaultRegistry();
1343
+ const total = totalFrames(payload, fps);
1344
+ const placements = scenePlacements(scenes, fps);
1345
+ const sceneDur = placements.map((p) => p.durationInFrames);
1346
+ const sceneStart = placements.map((p) => p.start);
1347
+ const morphPlan = planMorphs(scenes, fps, width, height, sceneStart, sceneDur);
1348
+ const seriesChildren = [];
1349
+ scenes.forEach((scene, i) => {
1350
+ if (i > 0 && scene.transition) {
1351
+ seriesChildren.push(
1352
+ createElement(TransitionSeries.Transition, {
1353
+ key: `transition-${i}`,
1354
+ presentation: presentationFor(scene.transition.type, scene.transition.options),
1355
+ timing: linearTiming({
1356
+ durationInFrames: transitionOverlapFrames(scenes[i - 1], scene, fps)
1357
+ })
1358
+ })
1359
+ );
1360
+ }
1361
+ const sceneSuppress = morphPlan.suppress.get(i);
1362
+ const responsive = sceneResponsive(scene, width, height);
1363
+ seriesChildren.push(
1364
+ createElement(
1365
+ TransitionSeries.Sequence,
1366
+ { key: scene.id, durationInFrames: sceneDur[i] ?? sceneDurationFrames(scene, fps) },
1367
+ scene.camera ? createElement(AnimatedCamera, {
1368
+ move: scene.camera,
1369
+ durationInFrames: sceneDur[i] ?? sceneDurationFrames(scene, fps),
1370
+ // biome-ignore lint/correctness/noChildrenProp: raw createElement props object, not JSX
1371
+ children: fitGroup(
1372
+ scene,
1373
+ width,
1374
+ height,
1375
+ createElement(SceneTracks, {
1376
+ scene,
1377
+ registry,
1378
+ suppress: sceneSuppress,
1379
+ responsive
1380
+ })
1381
+ )
1382
+ }) : fitGroup(
1383
+ scene,
1384
+ width,
1385
+ height,
1386
+ createElement(SceneTracks, { scene, registry, suppress: sceneSuppress, responsive })
1387
+ )
1388
+ )
1389
+ );
1390
+ });
1391
+ const morphLayers = morphPlan.pairs.map(
1392
+ (pair, i) => createElement(
1393
+ Sequence,
1394
+ {
1395
+ key: `morph-${i}`,
1396
+ from: pair.overlapStart,
1397
+ durationInFrames: pair.overlapFrames
1398
+ },
1399
+ createElement(MorphLayer, { pair, registry })
1400
+ )
1401
+ );
1402
+ const layerEls = (under) => layers.filter((l) => Boolean(l.under) === under).flatMap(
1403
+ (layer, li) => layer.entries.map((entry, ei) => {
1404
+ const from = toFrames(entry.at ?? 0, fps);
1405
+ const dur = entry.for != null ? toFrames(entry.for, fps) : Math.max(1, total - from);
1406
+ return createElement(
1407
+ Sequence,
1408
+ { key: `layer-${under ? "u" : "o"}-${li}-${ei}`, from, durationInFrames: dur },
1409
+ createElement(AnimatedEntry, {
1410
+ component: entry.component,
1411
+ props: entry.props,
1412
+ animate: entry.animate,
1413
+ effects: entry.effects,
1414
+ depth: entry.depth,
1415
+ transform3d: entry.transform3d,
1416
+ matte: entry.matte,
1417
+ clip: entry.clip,
1418
+ durationInFrames: dur,
1419
+ registry
1420
+ })
1421
+ );
1422
+ })
1423
+ );
1424
+ const root = createElement(
1425
+ Group,
1426
+ null,
1427
+ createElement(Rect, { width, height, fill: brand?.bg ?? "#08080a" }),
1428
+ ...layerEls(true),
1429
+ createElement(TransitionSeries, null, ...seriesChildren),
1430
+ ...morphLayers,
1431
+ ...layerEls(false)
1432
+ );
1433
+ const content = brand ? createElement(Components.ThemeProvider, { theme: brandToTheme(brand) }, root) : root;
1434
+ return createElement(
1435
+ Composition,
1436
+ { width, height, fps, durationInFrames: total, linear, finish, motionBlur, dof },
1437
+ content
1438
+ );
1439
+ }
1440
+ function editDistance(a, b) {
1441
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
1442
+ for (let i = 1; i <= a.length; i++) {
1443
+ const cur = [i];
1444
+ for (let j = 1; j <= b.length; j++) {
1445
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1446
+ cur[j] = Math.min((prev[j] ?? 0) + 1, (cur[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
1447
+ }
1448
+ prev = cur;
1449
+ }
1450
+ return prev[b.length] ?? 0;
1451
+ }
1452
+ function closestComponent(name, registry) {
1453
+ let best = null;
1454
+ let bestD = Number.POSITIVE_INFINITY;
1455
+ for (const k of Object.keys(registry)) {
1456
+ const d = editDistance(name.toLowerCase(), k.toLowerCase());
1457
+ if (d < bestD) {
1458
+ bestD = d;
1459
+ best = k;
1460
+ }
1461
+ }
1462
+ return best && bestD <= Math.max(2, Math.floor(name.length / 3)) ? best : null;
1463
+ }
1464
+ function isValidTimeSpec(v) {
1465
+ if (typeof v === "number") return Number.isFinite(v);
1466
+ const s = v.trim();
1467
+ if (s === "") return false;
1468
+ const numOk = (x) => x.trim() !== "" && Number.isFinite(Number(x));
1469
+ if (s.includes(":")) {
1470
+ const [m, sec] = s.split(":");
1471
+ return numOk(m ?? "") && numOk(sec ?? "");
1472
+ }
1473
+ if (s.endsWith("ms")) return numOk(s.slice(0, -2));
1474
+ if (s.endsWith("f")) return numOk(s.slice(0, -1));
1475
+ if (s.endsWith("s")) return numOk(s.slice(0, -1));
1476
+ return numOk(s);
1477
+ }
1478
+ var BRIDGE_PROPS = ["placement", "scale"];
1479
+ function knownPropsFor(component) {
1480
+ const m = Components.manifestEntry(component);
1481
+ if (!m) return null;
1482
+ const known = new Set(m.props.map((p) => p.name));
1483
+ const shape = m.schema.shape;
1484
+ if (shape && typeof shape === "object") for (const k of Object.keys(shape)) known.add(k);
1485
+ for (const src of Object.keys(PROP_ALIASES[component] ?? {})) known.add(src);
1486
+ for (const k of BRIDGE_PROPS) known.add(k);
1487
+ return known;
1488
+ }
1489
+ function validateComposition(payload, opts = {}) {
1490
+ const registry = opts.registry ?? defaultRegistry();
1491
+ const fidelity = Components.COMPONENT_FIDELITY;
1492
+ const diags = [];
1493
+ if (!(payload.fps > 0)) diags.push({ level: "error", path: "fps", message: "fps must be > 0" });
1494
+ if (!(payload.width > 0 && payload.height > 0))
1495
+ diags.push({ level: "error", path: "size", message: "width/height must be > 0" });
1496
+ if (!payload.scenes?.length)
1497
+ diags.push({ level: "error", path: "scenes", message: "composition has no scenes" });
1498
+ const fps = payload.fps > 0 ? payload.fps : 30;
1499
+ const checkTime = (v, path, required) => {
1500
+ if (v === void 0) {
1501
+ if (required) diags.push({ level: "error", path, message: "missing required time" });
1502
+ return;
1503
+ }
1504
+ if (!isValidTimeSpec(v)) {
1505
+ diags.push({
1506
+ level: "error",
1507
+ path,
1508
+ message: `invalid time ${JSON.stringify(v)} \u2014 use seconds (e.g. 2) or a spec string ("2s", "500ms", "0:02", "90f")`
1509
+ });
1510
+ return;
1511
+ }
1512
+ const secs2 = typeof v === "number" ? v : timeSpecToSeconds(v, fps);
1513
+ if (secs2 < 0)
1514
+ diags.push({ level: "error", path, message: `time ${JSON.stringify(v)} is negative` });
1515
+ };
1516
+ const checkEntry = (e, path, timed) => {
1517
+ if (e.role !== void 0 && e.role !== "focal" && e.role !== "support" && e.role !== "ambient")
1518
+ diags.push({
1519
+ level: "error",
1520
+ path: `${path}.role`,
1521
+ message: `invalid role ${JSON.stringify(e.role)} \u2014 use 'focal' | 'support' | 'ambient' (absent = 'support')`
1522
+ });
1523
+ if (!registry[e.component]) {
1524
+ const guess = closestComponent(e.component, registry);
1525
+ diags.push({
1526
+ level: "error",
1527
+ path: `${path}.component`,
1528
+ message: `unknown component "${e.component}"${guess ? ` \u2014 did you mean "${guess}"?` : ""}`
1529
+ });
1530
+ } else {
1531
+ const f = fidelity?.[e.component];
1532
+ if (f?.fidelity === "apes_remotion")
1533
+ diags.push({
1534
+ level: "warning",
1535
+ path: `${path}.component`,
1536
+ message: `"${e.component}" imitates a browser-only effect the engine doesn't do natively \u2014 it renders a stylized approximation; avoid for hero moments.`
1537
+ });
1538
+ else if (f?.fidelity === "degraded")
1539
+ diags.push({
1540
+ level: "info",
1541
+ path: `${path}.component`,
1542
+ message: `"${e.component}" renders an approximation until the engine gains "${f.needsFeature}".`
1543
+ });
1544
+ if (f?.backend === "gpu_only")
1545
+ diags.push({
1546
+ level: "warning",
1547
+ path: `${path}.component`,
1548
+ message: `"${e.component}" needs the GPU (Vello) backend \u2014 it won't render correctly on the CPU reference (e.g. a CPU-verified or no-GPU export).`
1549
+ });
1550
+ const known = knownPropsFor(e.component);
1551
+ if (known && e.props) {
1552
+ for (const k of Object.keys(e.props)) {
1553
+ if (!known.has(k))
1554
+ diags.push({
1555
+ level: "warning",
1556
+ path: `${path}.props.${k}`,
1557
+ message: `unknown prop "${k}" on ${e.component} \u2014 passed through (the component ignores props it doesn't declare)`
1558
+ });
1559
+ }
1560
+ }
1561
+ }
1562
+ checkTime(e.at, `${path}.at`, timed);
1563
+ checkTime(e.for, `${path}.for`, timed);
1564
+ (e.animate ?? []).forEach((a, i) => {
1565
+ if (!CHOREOGRAPHY[a.pattern])
1566
+ diags.push({
1567
+ level: "warning",
1568
+ path: `${path}.animate[${i}]`,
1569
+ message: `unknown choreography pattern "${a.pattern}" (ignored)`
1570
+ });
1571
+ });
1572
+ };
1573
+ payload.scenes?.forEach((scene, si) => {
1574
+ if (scene.transition && !TRANSITIONS[scene.transition.type])
1575
+ diags.push({
1576
+ level: "warning",
1577
+ path: `scenes[${si}].transition`,
1578
+ message: `transition "${scene.transition.type}" not in the engine yet \u2014 falls back to cross-fade`
1579
+ });
1580
+ if (!scene.tracks?.length)
1581
+ diags.push({ level: "warning", path: `scenes[${si}].tracks`, message: "scene has no tracks" });
1582
+ scene.tracks?.forEach(
1583
+ (track, ti) => track.entries.forEach(
1584
+ (e, ei) => checkEntry(e, `scenes[${si}].tracks[${ti}].entries[${ei}]`, true)
1585
+ )
1586
+ );
1587
+ });
1588
+ payload.layers?.forEach(
1589
+ (layer, li) => layer.entries.forEach((e, ei) => checkEntry(e, `layers[${li}].entries[${ei}]`, false))
1590
+ );
1591
+ return diags;
1592
+ }
1593
+ //! The Studio→engine PROP vocabulary — size-role tokens, prop-name aliases,
1594
+ //! placement coords, and the self-anchoring list. Extracted (verbatim) from the
1595
+ //! renderer in `index.tsx` so the inspector can resolve an entry's effective
1596
+ //! props/placement the SAME way `buildComposition` does, without importing the
1597
+ //! React renderer. Behavior is identical to the pre-extraction code.
1598
+ //! Time/frame math — mirrors Studio's `composition.ts` helpers so scene windows
1599
+ //! and audio offsets line up with what actually plays.
1600
+ //! The timeline composition payload — the document an agent (ONDA Studio) emits
1601
+ //! and `buildComposition` turns into an @onda-engine/react scene. Structural mirror of
1602
+ //! Studio's `composition.ts` schema (kept as plain types; validation is
1603
+ //! `validateComposition`).
1604
+ //! Inspector constants — every threshold the checks measure against, with its
1605
+ //! source. Research-backed values cite the standard/paper; the rest are marked
1606
+ //! PRODUCT DECISION (ours to tune, no external authority).
1607
+ //! `timing.collisions` — attention can't be in two places at once.
1608
+ //!
1609
+ //! Three measurements:
1610
+ //! 1. Two FOCAL entrances beginning within the 250ms attention window
1611
+ //! (attentional blink: a second target 200–500ms after a first is routinely
1612
+ //! missed — Raymond, Shapiro & Arnell 1992; see constants.ts).
1613
+ //! 2. An entrance whose settle (the `@onda-engine/components` settleTime registry —
1614
+ //! the same formulas the components run) outlives the entry's visible
1615
+ //! window: the move is cut off mid-flight.
1616
+ //! 3. A scene transition longer than the 0.6s budget.
1617
+ //! `density.score` — how much is on screen at once, per scene?
1618
+ //! An event sweep over entry visible windows finds each scene's PEAK count of
1619
+ //! concurrently visible non-ambient entries (and focal entries). Budgets:
1620
+ //! ≤5 non-ambient, ≤1 focal (see constants.ts — product decisions). The peaks
1621
+ //! are always reported on the InspectReport; violations fire over budget.
1622
+ //! `frames.transitionCapture` — don't thumbnail a frame that's mid-transition.
1623
+ //! Given `opts.frames` (the indices a consumer intends to capture), flag any
1624
+ //! that land inside a transition's overlap window — those frames show two
1625
+ //! scenes blended. The fix is mechanical: the nearest frame outside the window.
1626
+ //! WCAG color math — hex parsing, relative luminance, contrast ratio.
1627
+ //! Formulas from WCAG 2.x: relative luminance per
1628
+ //! https://www.w3.org/WAI/GL/wiki/Relative_luminance (sRGB linearization), and
1629
+ //! contrast ratio `(L1 + 0.05) / (L2 + 0.05)` per
1630
+ //! https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum (SC 1.4.3).
1631
+ //! Engine colors are hex (`#rgb`, `#rrggbb`, `#rrggbbaa`) — anything else is
1632
+ //! "unparseable" and the caller treats the contrast as unverifiable.
1633
+ //! Resolve a composition payload to the FRAME timeline the checks measure on —
1634
+ //! the same resolution `buildComposition` performs (shared `scenePlacements` +
1635
+ //! `toFrames` + `adaptProps`), minus the React tree. Every entry gets absolute
1636
+ //! start/visible frames, its effective (size-role-resolved) props, and its
1637
+ //! attention role.
1638
+ //! Text extraction — what does this entry SAY, at what size, in what color?
1639
+ //! Driven by the `@onda-engine/components` manifest (per-prop semantic roles), so the
1640
+ //! inspector knows each component's text props + defaults without hardcoding
1641
+ //! eighty dialects. A `TextBlock` is one string a viewer reads: content +
1642
+ //! resolved px font size + resolved color + the measurement options needed to
1643
+ //! reproduce the component's own metrics.
1644
+ //! `text.legibility` — is every readable string big enough and contrasty enough?
1645
+ //! Two measurements per text block:
1646
+ //! 1. Font size vs the per-format research floor (legibility.info: 40px body
1647
+ //! minimum for full-HD phone-first formats — see constants.ts).
1648
+ //! 2. WCAG 2.x contrast of the text color vs what's BEHIND its placement box,
1649
+ //! found by walking the z-order beneath the entry: solid fills and gradient
1650
+ //! stops are checked analytically (worst stop wins, translucent veils are
1651
+ //! alpha-composited); an image/video behind yields an `info` — contrast is
1652
+ //! unverifiable analytically.
1653
+ //! `layout.overflow` — does measured text stay inside the frame and the
1654
+ //! format's safe area?
1655
+ //! Width comes from the engine's own text metrics (`measureText`, cosmic-text
1656
+ //! via wasm — warm it with `preloadTextMetrics()` in Node for real numbers; the
1657
+ //! glyph-count estimate is the documented fallback). Placement resolves through
1658
+ //! the shared `resolvePlacement` contract with the measured element size, so
1659
+ //! the checked box is where the component actually sits. Entries whose own
1660
+ //! `fit`/`maxWidth` auto-fit caps the line are measured at the FITTED size.
1661
+ //! `timing.readingTime` — does every text stay up long enough to be read?
1662
+ //! Needed time = `max(1.2s, 0.25s × words + 0.6s)` — see constants.ts for the
1663
+ //! research trail (BBC subtitle guidelines, Brysbaert 2019). Measured against
1664
+ //! the entry's VISIBLE window (its duration clamped to the scene cut).
1665
+ //! The INSPECTOR — deterministic quality metrics over a composition payload.
1666
+ //! `inspect(payload, opts?)` measures the SAME document `validateComposition`
1667
+ //! checks and `buildComposition` renders, resolved to frames with the same
1668
+ //! helpers, and returns structured violations: legibility (font floors + WCAG
1669
+ //! contrast), layout overflow vs per-platform safe areas, reading time, focal
1670
+ //! entrance/settle/transition collisions, per-scene density, and
1671
+ //! transition-window thumbnail capture. Every threshold lives in
1672
+ //! `constants.ts` with its source.
1673
+ //! Where `validateComposition` answers "will this render?", `inspect` answers
1674
+ //! "will this read?" — both deterministic, both agent-correctable.
1675
+ //! Text widths use the engine's cosmic-text metrics; call
1676
+ //! `preloadTextMetrics()` (from `@onda-engine/components`) before inspecting in Node
1677
+ //! for shaped widths instead of the glyph-count estimate.
1678
+ //! `@onda-engine/cinema` — turn a timeline composition payload into an `@onda-engine/react`
1679
+ //! scene. This is the spec→engine renderer ONDA Studio uses in place of its
1680
+ //! Remotion `composition-renderer`: scenes play through a `<TransitionSeries>`,
1681
+ //! tracks layer as `<AbsoluteFill>`s, and each entry is a registry component
1682
+ //! wrapped in its choreography — applied as numeric `Motion` on a `<Group>`
1683
+ //! (the engine transform), not CSS.
1684
+
1685
+ export { CHECKS, CONTRAST_MIN_BODY, CONTRAST_MIN_LARGE, DENSITY_MAX_FOCAL, DENSITY_MAX_NON_AMBIENT, FOCAL_COLLISION_WINDOW_SECONDS, FONT_FLOOR_PX, READ_MIN_SECONDS, READ_ORIENTATION_SECONDS, READ_SECONDS_PER_WORD, SAFE_AREAS, TRANSITION_BUDGET_SECONDS, buildComposition, contrastRatio, fontFloorPx, inferFormat, inspect, parseColor, readingTimeSeconds, relativeLuminance, resolveComposition, scenePlacements, textBlocks, timeSpecToSeconds, toFrames, totalFrames, totalWords, validateComposition };
1686
+ //# sourceMappingURL=cinema.js.map
1687
+ //# sourceMappingURL=cinema.js.map