@vanillaskyai/video 0.10.6 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.10.8
8
+
9
+ - Reassert a requested pause if native video playback starts late, preserving footage during delayed narration and keeping viewer pauses in place.
10
+
11
+ - Distinguish decode errors, frame-readiness timeouts and stalled footage in the local chat diagnostic log without retaining media URLs or user content.
12
+
13
+ - Correct starter guidance for separate footage modes and remove obsolete planner logs that included raw model output.
14
+ - Prepare the next compatible mobile video while the current scene plays, retaining its decoded element across cuts and limiting mounted footage to the active and next scenes.
15
+ - Observe cached video frames on mount so a missed loading event cannot delay genuine stall recovery.
16
+
17
+ ## 0.10.7
18
+
19
+ - Reuse the first presented video frame at scene handoffs so a second readiness observation cannot briefly interrupt continuous narration.
20
+
21
+ - Keep an already-speaking paragraph uninterrupted across brief visual handoffs, while genuinely late footage still pauses narration until it can play.
22
+
23
+ - Keep the final chapter visible through answer completion, including recovery from failed footage.
24
+ - Derive recovery titles from authored titles or subject excerpts instead of a generic placeholder.
25
+
26
+ - Let cold footage finish its bounded initial load before treating it as stalled. Require the actual new source to present a frame before narration starts, and keep source changes from cancelling their own native load.
27
+
7
28
  ## 0.10.6
8
29
 
9
30
  - Initialize the reusable narration audio element during the existing user gesture so delayed first speech can play on Safari.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TitleSceneTemplate
3
- } from "./chunk-EWWCQFTI.js";
3
+ } from "./chunk-7AA2JWHZ.js";
4
4
  import "./chunk-4YM2M62S.js";
5
5
  export {
6
6
  TitleSceneTemplate
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  VideoFrame
3
- } from "./chunk-VWJBIYBM.js";
3
+ } from "./chunk-KUCEOG2L.js";
4
+ import "./chunk-5JBMYQP6.js";
4
5
  import "./chunk-224QNWRA.js";
5
- import "./chunk-OOBT4X46.js";
6
6
  import "./chunk-SPVTJH3F.js";
7
7
  import "./chunk-R3XAOMKP.js";
8
8
  import "./chunk-73NTSFFI.js";
@@ -5,10 +5,11 @@ import {
5
5
 
6
6
  // src/visual-system/scene-templates/chapter-title.tsx
7
7
  import { jsx } from "react/jsx-runtime";
8
- function TitleScene({ variables, width, height, progress }) {
8
+ function TitleScene({ variables, width, height, progress, motionProgress }) {
9
9
  const title = String(variables.title ?? "A different perspective");
10
10
  const unit = Math.min(width, height);
11
- const opacity = fade(progress / 0.22) * (1 - fade((progress - 0.76) / 0.24));
11
+ const presentation = motionProgress ?? progress;
12
+ const opacity = fade(presentation / 0.22) * (1 - fade((presentation - 0.76) / 0.24));
12
13
  return /* @__PURE__ */ jsx("div", { "data-template": "title", "data-title-treatment": "quiet-fade", style: { position: "absolute", inset: 0, background: "#000", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx("div", { "data-title-composition": "centered", style: { width: "76%", textAlign: "center", opacity, fontFamily: editorialFont, fontWeight: 500, fontSize: unit * 0.068, lineHeight: 1.18, letterSpacing: "-.025em", textWrap: "balance", overflowWrap: "anywhere" }, children: title }) });
13
14
  }
14
15
  var TitleSceneTemplate = TitleScene;
@@ -1,13 +1,60 @@
1
1
  import {
2
2
  darken,
3
3
  resolveTokens,
4
+ useExternalVideoBackdrop,
4
5
  useMediaAudio,
5
6
  useMediaFailure,
6
7
  useNarrationPreroll
7
- } from "./chunk-OOBT4X46.js";
8
+ } from "./chunk-5JBMYQP6.js";
9
+ import {
10
+ resolveMediaType
11
+ } from "./chunk-224QNWRA.js";
8
12
 
9
- // src/visual-system/scene-templates/scene-video-backdrop.tsx
10
- import { useCallback, useEffect, useRef, useState } from "react";
13
+ // src/visual-system/motion/curves.ts
14
+ function interpolate(value, inputRange, outputRange, options) {
15
+ const { easing, extrapolateLeft = "extend", extrapolateRight = "extend" } = options ?? {};
16
+ let i = 0;
17
+ for (; i < inputRange.length - 2; i++) {
18
+ if (value < inputRange[i + 1]) break;
19
+ }
20
+ const inputMin = inputRange[i];
21
+ const inputMax = inputRange[i + 1];
22
+ const outputMin = outputRange[i];
23
+ const outputMax = outputRange[i + 1];
24
+ let t = inputMax === inputMin ? 0 : (value - inputMin) / (inputMax - inputMin);
25
+ if (t < 0 && extrapolateLeft === "clamp") t = 0;
26
+ if (t > 1 && extrapolateRight === "clamp") t = 1;
27
+ if (easing && t >= 0 && t <= 1) {
28
+ t = easing(t);
29
+ }
30
+ return outputMin + t * (outputMax - outputMin);
31
+ }
32
+ function spring(progress, config) {
33
+ if (progress <= 0) return 0;
34
+ if (progress >= 1) {
35
+ const { damping: damping2 = 26 } = config ?? {};
36
+ if (damping2 >= 20) return 1;
37
+ }
38
+ const { damping = 26, stiffness = 170, mass = 1 } = config ?? {};
39
+ const omega = Math.sqrt(stiffness / mass);
40
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
41
+ const t = progress * 3.5;
42
+ let value;
43
+ if (zeta < 1) {
44
+ const omegaD = omega * Math.sqrt(1 - zeta * zeta);
45
+ value = 1 - Math.exp(-zeta * omega * t) * (Math.cos(omegaD * t) + zeta * omega / omegaD * Math.sin(omegaD * t));
46
+ } else if (zeta === 1) {
47
+ value = 1 - Math.exp(-omega * t) * (1 + omega * t);
48
+ } else {
49
+ const s1 = -omega * (zeta + Math.sqrt(zeta * zeta - 1));
50
+ const s2 = -omega * (zeta - Math.sqrt(zeta * zeta - 1));
51
+ value = 1 + (s1 * Math.exp(s2 * t) - s2 * Math.exp(s1 * t)) / (s2 - s1);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ // src/visual-system/scene-templates/scene-background.tsx
57
+ import { useEffect as useEffect2, useState as useState2 } from "react";
11
58
 
12
59
  // src/visual-system/scene-templates/color-utils.ts
13
60
  import React from "react";
@@ -83,49 +130,6 @@ var BrandGradientOverlay = ({ style: globalStyle, progress, sceneDuration, seed
83
130
  });
84
131
  };
85
132
 
86
- // src/visual-system/motion/curves.ts
87
- function interpolate(value, inputRange, outputRange, options) {
88
- const { easing, extrapolateLeft = "extend", extrapolateRight = "extend" } = options ?? {};
89
- let i = 0;
90
- for (; i < inputRange.length - 2; i++) {
91
- if (value < inputRange[i + 1]) break;
92
- }
93
- const inputMin = inputRange[i];
94
- const inputMax = inputRange[i + 1];
95
- const outputMin = outputRange[i];
96
- const outputMax = outputRange[i + 1];
97
- let t = inputMax === inputMin ? 0 : (value - inputMin) / (inputMax - inputMin);
98
- if (t < 0 && extrapolateLeft === "clamp") t = 0;
99
- if (t > 1 && extrapolateRight === "clamp") t = 1;
100
- if (easing && t >= 0 && t <= 1) {
101
- t = easing(t);
102
- }
103
- return outputMin + t * (outputMax - outputMin);
104
- }
105
- function spring(progress, config) {
106
- if (progress <= 0) return 0;
107
- if (progress >= 1) {
108
- const { damping: damping2 = 26 } = config ?? {};
109
- if (damping2 >= 20) return 1;
110
- }
111
- const { damping = 26, stiffness = 170, mass = 1 } = config ?? {};
112
- const omega = Math.sqrt(stiffness / mass);
113
- const zeta = damping / (2 * Math.sqrt(stiffness * mass));
114
- const t = progress * 3.5;
115
- let value;
116
- if (zeta < 1) {
117
- const omegaD = omega * Math.sqrt(1 - zeta * zeta);
118
- value = 1 - Math.exp(-zeta * omega * t) * (Math.cos(omegaD * t) + zeta * omega / omegaD * Math.sin(omegaD * t));
119
- } else if (zeta === 1) {
120
- value = 1 - Math.exp(-omega * t) * (1 + omega * t);
121
- } else {
122
- const s1 = -omega * (zeta + Math.sqrt(zeta * zeta - 1));
123
- const s2 = -omega * (zeta - Math.sqrt(zeta * zeta - 1));
124
- value = 1 + (s1 * Math.exp(s2 * t) - s2 * Math.exp(s1 * t)) / (s2 - s1);
125
- }
126
- return value;
127
- }
128
-
129
133
  // src/visual-system/scene-templates/background-effect.ts
130
134
  var DIRECTIONS = ["right", "left", "up", "down"];
131
135
  function getKenBurnsTransform(progress, direction) {
@@ -223,6 +227,7 @@ function resolveMediaPosition(value) {
223
227
  }
224
228
 
225
229
  // src/visual-system/scene-templates/scene-video-backdrop.tsx
230
+ import { useCallback, useEffect, useRef, useState } from "react";
226
231
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
227
232
  var SceneVideoBackdrop = ({
228
233
  mediaUrl,
@@ -237,9 +242,6 @@ var SceneVideoBackdrop = ({
237
242
  muted,
238
243
  volume,
239
244
  playbackId = mediaUrl,
240
- retainPoster = false,
241
- persistent = false,
242
- preparedPoster,
243
245
  onReady,
244
246
  onError
245
247
  }) => {
@@ -255,16 +257,17 @@ var SceneVideoBackdrop = ({
255
257
  const [waitingKey, setWaitingKey] = useState();
256
258
  const [exhaustedKey, setExhaustedKey] = useState();
257
259
  const videoRef = useRef(null);
260
+ const presentedVideoUrl = useRef(void 0);
258
261
  const startedVideoUrl = useRef(void 0);
259
262
  const startedPlaybackId = useRef(void 0);
260
263
  const videoPresentationKey = `${playbackId}\0${mediaUrl}`;
261
264
  const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying });
262
265
  presentationRef.current = { key: videoPresentationKey, playing: isPlaying };
263
- const unavailable = () => {
266
+ const unavailable = (reason = "playback-error") => {
264
267
  if (presentationRef.current.key === videoPresentationKey && presentationRef.current.playing) {
265
268
  setExhaustedKey(videoPresentationKey);
266
269
  onError?.();
267
- reportMediaFailure?.();
270
+ reportMediaFailure?.(reason);
268
271
  }
269
272
  };
270
273
  useEffect(() => {
@@ -274,6 +277,8 @@ var SceneVideoBackdrop = ({
274
277
  }
275
278
  const video = videoRef.current;
276
279
  if (!video) return;
280
+ const expectedSource = video.getAttribute("src") === mediaUrl ? video.src : void 0;
281
+ let awaitingFirstFrame = presentedVideoUrl.current !== mediaUrl || video.currentSrc !== expectedSource;
277
282
  let previousTime = video.currentTime;
278
283
  let forwardFrames = 0;
279
284
  let stopped = false;
@@ -281,8 +286,14 @@ var SceneVideoBackdrop = ({
281
286
  let poll;
282
287
  const observe = (_now, metadata) => {
283
288
  if (stopped) return;
289
+ const currentSource = video.currentSrc === expectedSource;
290
+ if (currentSource && awaitingFirstFrame && (metadata || presentedVideoUrl.current === mediaUrl)) {
291
+ awaitingFirstFrame = false;
292
+ clearTimeout(deadline);
293
+ deadline = setTimeout(fail, 1e3);
294
+ }
284
295
  const time = metadata?.mediaTime ?? video.currentTime;
285
- if (video.seeking || time < previousTime) forwardFrames = 0;
296
+ if (!currentSource || video.seeking || time < previousTime) forwardFrames = 0;
286
297
  else if (time > previousTime + 1e-3) forwardFrames++;
287
298
  previousTime = time;
288
299
  if (forwardFrames >= 2) {
@@ -294,12 +305,13 @@ var SceneVideoBackdrop = ({
294
305
  if (video.requestVideoFrameCallback) frame = video.requestVideoFrameCallback(observe);
295
306
  else poll = setTimeout(observe, 50);
296
307
  };
297
- const deadline = setTimeout(() => {
308
+ const fail = () => {
298
309
  if (!stopped) {
299
310
  stopped = true;
300
- unavailable();
311
+ unavailable(awaitingFirstFrame ? "frame-readiness-timeout" : "stalled-media");
301
312
  }
302
- }, 1e3);
313
+ };
314
+ let deadline = setTimeout(fail, awaitingFirstFrame ? 8e3 : 1e3);
303
315
  observe();
304
316
  return () => {
305
317
  stopped = true;
@@ -308,6 +320,39 @@ var SceneVideoBackdrop = ({
308
320
  if (frame !== void 0) video.cancelVideoFrameCallback?.(frame);
309
321
  };
310
322
  }, [waitingKey, videoPresentationKey, isPlaying]);
323
+ const onReadyRef = useRef(onReady);
324
+ onReadyRef.current = onReady;
325
+ useEffect(() => {
326
+ const video = videoRef.current;
327
+ if (!video) return;
328
+ let stopped = false;
329
+ let frame;
330
+ const markPresented = () => {
331
+ if (stopped || !video.isConnected || presentationRef.current.key !== videoPresentationKey || video.getAttribute("src") !== mediaUrl || video.currentSrc !== video.src) return false;
332
+ presentedVideoUrl.current = mediaUrl;
333
+ video.dispatchEvent(new Event("vanillasky:video-frame-presented", { bubbles: true }));
334
+ onReadyRef.current?.();
335
+ setDecodedVideoUrl(mediaUrl);
336
+ stopped = true;
337
+ return true;
338
+ };
339
+ const observe = () => {
340
+ if (stopped || frame !== void 0) return;
341
+ if (video.requestVideoFrameCallback) {
342
+ frame = video.requestVideoFrameCallback(() => {
343
+ frame = void 0;
344
+ if (!markPresented()) observe();
345
+ });
346
+ } else if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) markPresented();
347
+ };
348
+ video.addEventListener("loadeddata", observe);
349
+ observe();
350
+ return () => {
351
+ stopped = true;
352
+ video.removeEventListener("loadeddata", observe);
353
+ if (frame !== void 0) video.cancelVideoFrameCallback?.(frame);
354
+ };
355
+ }, [mediaUrl, videoPresentationKey]);
311
356
  const fitDuration = useCallback((video) => {
312
357
  video.playbackRate = (resolvedMuted || video.preservesPitch === true) && sceneDuration && Number.isFinite(video.duration) && video.duration > 0 ? Math.max(0.75, Math.min(1, video.duration / (sceneDuration + 0.2))) : 1;
313
358
  }, [resolvedMuted, sceneDuration]);
@@ -321,7 +366,7 @@ var SceneVideoBackdrop = ({
321
366
  return;
322
367
  }
323
368
  video.currentTime = 0;
324
- void video.play().catch(unavailable);
369
+ void video.play().catch(() => unavailable());
325
370
  };
326
371
  useEffect(() => {
327
372
  const video = videoRef.current;
@@ -330,6 +375,10 @@ var SceneVideoBackdrop = ({
330
375
  video.setAttribute("src", mediaUrl);
331
376
  video.load();
332
377
  }
378
+ }, [mediaUrl]);
379
+ useEffect(() => {
380
+ const video = videoRef.current;
381
+ if (!video) return;
333
382
  return () => {
334
383
  video.pause();
335
384
  video.removeAttribute("src");
@@ -337,7 +386,7 @@ var SceneVideoBackdrop = ({
337
386
  startedVideoUrl.current = void 0;
338
387
  startedPlaybackId.current = void 0;
339
388
  };
340
- }, [mediaUrl]);
389
+ }, []);
341
390
  useEffect(() => {
342
391
  const video = videoRef.current;
343
392
  if (video) video.volume = resolvedVolume;
@@ -352,16 +401,21 @@ var SceneVideoBackdrop = ({
352
401
  }
353
402
  if (startedPlaybackId.current === playbackId) {
354
403
  if (video.ended) continueMotion(video);
355
- else void video.play().catch(unavailable);
404
+ else void video.play().catch(() => unavailable());
356
405
  return;
357
406
  }
358
407
  const changingSource = startedVideoUrl.current !== void 0 && startedVideoUrl.current !== mediaUrl;
359
408
  fitDuration(video);
360
409
  if (!changingSource && video.currentTime > 0) video.currentTime = 0;
361
- video.play().catch(unavailable);
410
+ video.play().catch(() => unavailable());
362
411
  startedVideoUrl.current = mediaUrl;
363
412
  startedPlaybackId.current = playbackId;
364
413
  }, [isPlaying, mediaUrl, playbackId, rewindPreroll]);
414
+ const enforceRequestedPause = (event) => {
415
+ if (presentationRef.current.playing) return;
416
+ event.currentTarget.pause();
417
+ if (rewindPreroll && event.currentTarget.currentTime > 0) event.currentTarget.currentTime = 0;
418
+ };
365
419
  const mediaStyle = {
366
420
  position: "absolute",
367
421
  inset: 0,
@@ -370,59 +424,9 @@ var SceneVideoBackdrop = ({
370
424
  objectFit: "cover",
371
425
  objectPosition: resolvedPosition,
372
426
  transform: bgTransform.transform,
373
- transformOrigin: bgTransform.transformOrigin,
374
- zIndex: persistent ? 1 : void 0
427
+ transformOrigin: bgTransform.transformOrigin
375
428
  };
376
- const preparedPosition = preparedPoster ? resolveMediaPosition(preparedPoster.mediaPosition) : resolvedPosition;
377
- const preparedTransform = getBackgroundTransform(preparedPoster?.backgroundEffect, 0, 0);
378
- const posterPlanes = [
379
- ...persistent && mediaPoster ? [{
380
- presentationKey: videoPresentationKey,
381
- mediaPoster,
382
- mediaPosition: resolvedPosition,
383
- transform: bgTransform.transform,
384
- transformOrigin: bgTransform.transformOrigin,
385
- opacity: 1,
386
- zIndex: 0,
387
- role: "current"
388
- }] : [],
389
- ...preparedPoster && preparedPoster.presentationKey !== videoPresentationKey ? [{
390
- presentationKey: preparedPoster.presentationKey,
391
- mediaPoster: preparedPoster.mediaPoster,
392
- mediaPosition: preparedPosition,
393
- transform: preparedTransform.transform,
394
- transformOrigin: preparedTransform.transformOrigin,
395
- opacity: preparedPoster.opacity ?? 0,
396
- zIndex: 2,
397
- role: "prepared"
398
- }] : []
399
- ];
400
429
  return /* @__PURE__ */ jsxs(Fragment, { children: [
401
- posterPlanes.map((posterPlane) => /* @__PURE__ */ jsx(
402
- "img",
403
- {
404
- src: posterPlane.mediaPoster,
405
- alt: "",
406
- "aria-hidden": "true",
407
- draggable: false,
408
- "data-video-poster-plane": posterPlane.role,
409
- "data-video-poster-visible": posterPlane.opacity > 0 ? "true" : "false",
410
- style: {
411
- position: "absolute",
412
- inset: 0,
413
- width: "100%",
414
- height: "100%",
415
- objectFit: "cover",
416
- objectPosition: posterPlane.mediaPosition,
417
- transform: posterPlane.transform,
418
- transformOrigin: posterPlane.transformOrigin,
419
- zIndex: posterPlane.zIndex,
420
- opacity: posterPlane.opacity,
421
- pointerEvents: "none"
422
- }
423
- },
424
- posterPlane.presentationKey
425
- )),
426
430
  exhaustedKey === videoPresentationKey && /* @__PURE__ */ jsx(
427
431
  "div",
428
432
  {
@@ -437,42 +441,218 @@ var SceneVideoBackdrop = ({
437
441
  {
438
442
  ref: videoRef,
439
443
  src: mediaUrl,
440
- poster: retainPoster || decodedVideoUrl !== mediaUrl ? mediaPoster || void 0 : void 0,
444
+ poster: decodedVideoUrl !== mediaUrl ? mediaPoster || void 0 : void 0,
441
445
  muted: resolvedMuted,
442
446
  loop: false,
443
447
  playsInline: true,
444
448
  preload: "auto",
445
449
  onLoadedMetadata: (event) => fitDuration(event.currentTarget),
446
450
  onEnded: (event) => continueMotion(event.currentTarget),
451
+ onPlay: enforceRequestedPause,
452
+ onPlaying: enforceRequestedPause,
447
453
  onWaiting: () => {
448
454
  if (isPlaying) setWaitingKey(videoPresentationKey);
449
455
  },
450
- onLoadedData: (event) => {
451
- const video = event.currentTarget;
452
- const markPresented = () => {
453
- if (!video.isConnected) return;
454
- onReady?.();
455
- if (!retainPoster) setDecodedVideoUrl(mediaUrl);
456
- };
457
- if (video.requestVideoFrameCallback) {
458
- video.requestVideoFrameCallback(markPresented);
459
- return;
460
- }
461
- markPresented();
462
- },
463
456
  onError,
464
457
  "data-media-position": mediaPosition,
465
- "data-video-backdrop": persistent ? "persistent" : "scene",
458
+ "data-video-backdrop": "scene",
466
459
  style: { ...mediaStyle, visibility: exhaustedKey === videoPresentationKey ? "hidden" : void 0 }
467
460
  }
468
461
  )
469
462
  ] });
470
463
  };
471
464
 
465
+ // src/visual-system/scene-templates/scene-background.tsx
466
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
467
+ function resolveMediaTreatment(value) {
468
+ return value === "none" || value === "subtle" || value === "text-safe" ? value : "cinematic";
469
+ }
470
+ var SCRIM_STOP_COUNT = 7;
471
+ function smoothstep(t) {
472
+ return t * t * (3 - 2 * t);
473
+ }
474
+ function easedStops(peakAlpha, start, end, direction) {
475
+ const alphaAt = (t) => {
476
+ const eased = direction === "fade-out" ? 1 - smoothstep(t) : smoothstep(t);
477
+ return `rgba(0,0,0,${Number((peakAlpha * eased).toFixed(3))})`;
478
+ };
479
+ const stops = [];
480
+ if (start > 0) stops.push(`${alphaAt(0)} 0%`);
481
+ for (let i = 0; i < SCRIM_STOP_COUNT; i += 1) {
482
+ const t = i / (SCRIM_STOP_COUNT - 1);
483
+ const position = Number((start + (end - start) * t).toFixed(2));
484
+ stops.push(`${alphaAt(t)} ${position}%`);
485
+ }
486
+ if (end < 100) stops.push(`${alphaAt(1)} 100%`);
487
+ return stops.join(", ");
488
+ }
489
+ function getMediaTreatmentLayers(value, anchor = "full") {
490
+ const treatment = resolveMediaTreatment(value);
491
+ if (treatment === "none") return [];
492
+ const vignette = {
493
+ id: "vignette",
494
+ background: treatment === "subtle" ? `radial-gradient(ellipse at center, ${easedStops(0.28, 45, 100, "fade-in")})` : `radial-gradient(ellipse at center, ${easedStops(0.72, 32, 100, "fade-in")})`
495
+ };
496
+ if (treatment === "subtle") return [vignette];
497
+ const textSafe = treatment === "text-safe";
498
+ const layers = [vignette];
499
+ if (anchor !== "bottom") {
500
+ layers.push({
501
+ id: "center-scrim",
502
+ background: textSafe ? `radial-gradient(ellipse 92% 58% at 50% 50%, ${easedStops(0.46, 34, 90, "fade-out")})` : `radial-gradient(ellipse 88% 52% at 50% 50%, ${easedStops(0.26, 30, 88, "fade-out")})`
503
+ });
504
+ }
505
+ if (anchor !== "center") {
506
+ layers.push({
507
+ id: "bottom-scrim",
508
+ background: `linear-gradient(to top, ${easedStops(textSafe ? 0.64 : 0.5, 8, 100, "fade-out")})`,
509
+ style: { top: "55%" }
510
+ });
511
+ }
512
+ return layers;
513
+ }
514
+ function initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster) {
515
+ if (typeof window === "undefined") return "ready";
516
+ if (!wantsMedia) return "ready";
517
+ if (resolved === "video") return mediaPoster ? "ready" : "pending";
518
+ if (typeof Image === "undefined") return "ready";
519
+ const cached = new Image();
520
+ cached.src = mediaUrl;
521
+ return cached.complete && cached.naturalWidth > 0 ? "ready" : "pending";
522
+ }
523
+ function getMediaBackgroundProps(variables) {
524
+ return {
525
+ mediaUrl: String(variables.mediaUrl || ""),
526
+ mediaType: String(variables.mediaType || "auto"),
527
+ mediaPoster: String(variables.mediaPoster || ""),
528
+ mediaPosition: String(variables.mediaPosition || "center"),
529
+ mediaTreatment: String(variables.mediaTreatment || "cinematic")
530
+ };
531
+ }
532
+ var SceneBackground = ({
533
+ style,
534
+ progress,
535
+ sceneDuration,
536
+ width: _width,
537
+ // accepted for symmetry; not currently used in render
538
+ height: _height,
539
+ mediaUrl = "",
540
+ mediaType = "auto",
541
+ mediaPoster,
542
+ mediaPosition = "center",
543
+ mediaTreatment = "cinematic",
544
+ textAnchor = "full",
545
+ backgroundEffect,
546
+ seed,
547
+ isPlaying = true,
548
+ beatIntensity = 0
549
+ }) => {
550
+ void _width;
551
+ void _height;
552
+ const resolved = resolveMediaType(mediaType, mediaUrl);
553
+ const wantsMedia = resolved !== "gradient" && !!mediaUrl;
554
+ const externalVideoBackdrop = useExternalVideoBackdrop();
555
+ const hasExternalVideoBackdrop = externalVideoBackdrop !== false && resolved === "video";
556
+ const externalVideoFailed = externalVideoBackdrop === "fallback" && resolved === "video";
557
+ const externalVideoReady = externalVideoBackdrop === "ready" && resolved === "video";
558
+ const [mediaPaint, setMediaPaint] = useState2(
559
+ () => initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster)
560
+ );
561
+ useEffect2(() => {
562
+ setMediaPaint(initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster));
563
+ if (!wantsMedia || resolved !== "photo") return;
564
+ if (typeof Image === "undefined") return;
565
+ let cancelled = false;
566
+ const probe = new Image();
567
+ probe.onload = () => {
568
+ if (!cancelled) setMediaPaint("ready");
569
+ };
570
+ probe.onerror = () => {
571
+ if (!cancelled) setMediaPaint("failed");
572
+ };
573
+ probe.src = mediaUrl;
574
+ if (probe.complete) setMediaPaint(probe.naturalWidth > 0 ? "ready" : "failed");
575
+ return () => {
576
+ cancelled = true;
577
+ probe.onload = null;
578
+ probe.onerror = null;
579
+ };
580
+ }, [mediaUrl, mediaPoster, resolved, wantsMedia]);
581
+ const showMedia = wantsMedia && mediaPaint !== "failed";
582
+ const showTreatment = wantsMedia && mediaPaint === "ready";
583
+ const resolvedPosition = resolveMediaPosition(mediaPosition);
584
+ const resolvedTreatment = resolveMediaTreatment(mediaTreatment);
585
+ const treatmentLayers = getMediaTreatmentLayers(resolvedTreatment, textAnchor);
586
+ const gradSeed = typeof seed === "number" ? seed : typeof seed === "string" ? seed.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0) : 0;
587
+ const bgTransform = getBackgroundTransform(
588
+ backgroundEffect,
589
+ progress,
590
+ beatIntensity
591
+ );
592
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
593
+ (!hasExternalVideoBackdrop || externalVideoFailed) && /* @__PURE__ */ jsx2(
594
+ BrandGradientOverlay,
595
+ {
596
+ style,
597
+ progress,
598
+ sceneDuration,
599
+ seed: gradSeed
600
+ }
601
+ ),
602
+ showMedia && !hasExternalVideoBackdrop && (resolved === "video" ? /* @__PURE__ */ jsx2(
603
+ SceneVideoBackdrop,
604
+ {
605
+ mediaUrl,
606
+ mediaPoster,
607
+ mediaPosition,
608
+ backgroundEffect,
609
+ progress,
610
+ sceneDuration,
611
+ beatIntensity,
612
+ isPlaying,
613
+ onReady: () => setMediaPaint("ready"),
614
+ onError: () => setMediaPaint("failed")
615
+ }
616
+ ) : /* @__PURE__ */ jsx2(
617
+ "img",
618
+ {
619
+ src: mediaUrl,
620
+ alt: "",
621
+ "aria-hidden": "true",
622
+ draggable: false,
623
+ "data-media-position": mediaPosition,
624
+ style: {
625
+ position: "absolute",
626
+ inset: 0,
627
+ transform: bgTransform.transform,
628
+ transformOrigin: bgTransform.transformOrigin,
629
+ width: "100%",
630
+ height: "100%",
631
+ objectFit: "cover",
632
+ objectPosition: resolvedPosition
633
+ }
634
+ }
635
+ )),
636
+ (hasExternalVideoBackdrop ? externalVideoReady : showTreatment) && !externalVideoFailed && treatmentLayers.map((layer) => /* @__PURE__ */ jsx2(
637
+ "div",
638
+ {
639
+ "data-media-treatment": resolvedTreatment,
640
+ "data-media-overlay": layer.id,
641
+ style: {
642
+ position: "absolute",
643
+ inset: 0,
644
+ background: layer.background,
645
+ pointerEvents: "none",
646
+ ...layer.style
647
+ }
648
+ },
649
+ layer.id
650
+ ))
651
+ ] });
652
+ };
653
+
472
654
  export {
473
- BrandGradientOverlay,
474
655
  spring,
475
- getBackgroundTransform,
476
- resolveMediaPosition,
477
- SceneVideoBackdrop
656
+ getMediaBackgroundProps,
657
+ SceneBackground
478
658
  };