@vanillaskyai/video 0.3.1 → 0.3.2

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +3 -3
  3. package/dist/{bg-confetti-WNQXS7ZS.js → bg-confetti-KTHLUJPO.js} +3 -3
  4. package/dist/{bg-emoji-ETI47GLS.js → bg-emoji-AW7374DZ.js} +3 -3
  5. package/dist/{bg-media-SVUZAMGX.js → bg-media-KGYE6UVS.js} +9 -4
  6. package/dist/{brand-message-F3RD4P3P.js → brand-message-AY43ZQR2.js} +4 -3
  7. package/dist/{chart-bar-CNSC7BNK.js → chart-bar-Z42RDRQT.js} +8 -4
  8. package/dist/{chart-counter-MCRHKN77.js → chart-counter-X7NAOQ2C.js} +10 -5
  9. package/dist/{chart-progress-ring-RMI4A7ZK.js → chart-progress-ring-7NSNNHL4.js} +8 -4
  10. package/dist/check-runtime.js +2 -2
  11. package/dist/chunk-35LIYILY.js +25 -0
  12. package/dist/{chunk-IFOW65Z6.js → chunk-4AODC6MS.js} +76 -36
  13. package/dist/{chunk-FSG2PYWG.js → chunk-G5JPHBX7.js} +1 -1
  14. package/dist/{chunk-QTMHS7JD.js → chunk-HXSV6RHF.js} +1 -1
  15. package/dist/{chunk-ABZACD4G.js → chunk-IJM4B2ZN.js} +1 -1
  16. package/dist/{chunk-BCRUUJ6A.js → chunk-TCEHK2BW.js} +6 -2
  17. package/dist/{chunk-FNAEQ7QO.js → chunk-XWWLKRNU.js} +3 -1
  18. package/dist/control-visibility-NQOWTKOQ.js +258 -0
  19. package/dist/{cta-logo-SVA5GP2A.js → cta-logo-KNBLXUJU.js} +4 -3
  20. package/dist/{cta-media-E7VLLK42.js → cta-media-NY4ASBGG.js} +8 -4
  21. package/dist/{incoming-call-62MFZL3Z.js → incoming-call-KWOXLGJN.js} +4 -3
  22. package/dist/{infographic-before-after-AXCDAWND.js → infographic-before-after-OEM4DSTZ.js} +3 -3
  23. package/dist/{infographic-feature-list-GW5GUE6O.js → infographic-feature-list-ZZ25JRYQ.js} +8 -4
  24. package/dist/{infographic-problem-solution-7NURJANA.js → infographic-problem-solution-L3RLFDPE.js} +5 -4
  25. package/dist/{infographic-stat-row-J7IZR6ZN.js → infographic-stat-row-3OL3TNSH.js} +8 -4
  26. package/dist/{infographic-steps-VSZM6F5M.js → infographic-steps-LMBSW22P.js} +8 -4
  27. package/dist/preload-media-KIYBFYRH.js +30 -0
  28. package/dist/{prompt-input-LOUCRXCY.js → prompt-input-XCQ6TS2L.js} +4 -3
  29. package/dist/react.js +74 -52
  30. package/dist/{reaction-QG4CYZLQ.js → reaction-YL6YPGPB.js} +5 -4
  31. package/dist/{showcase-code-RUTFZR3H.js → showcase-code-EFVHRJUM.js} +8 -4
  32. package/dist/{showcase-phone-6CP6XEU5.js → showcase-phone-CTHXHEBT.js} +9 -5
  33. package/dist/{showcase-terminal-MGAEQWPR.js → showcase-terminal-XAMPADHN.js} +8 -4
  34. package/dist/{showcase-web-Y4W2HVRJ.js → showcase-web-LAKHYXSD.js} +9 -5
  35. package/dist/{social-milestone-XI2QE5BO.js → social-milestone-EOBEN7CX.js} +6 -4
  36. package/dist/{social-notification-2UOHQCUY.js → social-notification-MELRWIB6.js} +4 -3
  37. package/dist/{social-review-stack-FDYZGZTJ.js → social-review-stack-H3U437VT.js} +4 -3
  38. package/dist/{social-testimonial-OQ7N52GJ.js → social-testimonial-GNNMKMDD.js} +4 -3
  39. package/dist/{social-tweet-5OLCNPPC.js → social-tweet-PBUKKPT3.js} +4 -3
  40. package/docs/getting-started.md +2 -2
  41. package/docs/integrate-nextjs.md +2 -2
  42. package/examples/nextjs-quickstart/package.json +1 -1
  43. package/package.json +1 -1
  44. package/registry/items/barChart.json +9 -3
  45. package/registry/items/beforeAfter.json +1 -1
  46. package/registry/items/bigNumber.json +10 -4
  47. package/registry/items/brandMessage.json +7 -1
  48. package/registry/items/cardList.json +9 -3
  49. package/registry/items/codeEditor.json +9 -3
  50. package/registry/items/confetti.json +1 -1
  51. package/registry/items/ctaLogo.json +7 -1
  52. package/registry/items/ctaMedia.json +9 -3
  53. package/registry/items/emojiBurst.json +1 -1
  54. package/registry/items/incomingCall.json +7 -1
  55. package/registry/items/media.json +9 -3
  56. package/registry/items/milestone.json +8 -2
  57. package/registry/items/notification.json +7 -1
  58. package/registry/items/phoneMockup.json +9 -3
  59. package/registry/items/problemSolution.json +8 -2
  60. package/registry/items/progressRing.json +9 -3
  61. package/registry/items/promptInput.json +7 -1
  62. package/registry/items/reaction.json +8 -2
  63. package/registry/items/reviewStack.json +7 -1
  64. package/registry/items/steps.json +9 -3
  65. package/registry/items/terminal.json +9 -3
  66. package/registry/items/testimonial.json +7 -1
  67. package/registry/items/theme.json +1 -1
  68. package/registry/items/tripleStats.json +9 -3
  69. package/registry/items/tweet.json +7 -1
  70. package/registry/items/webMockup.json +9 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.3.2
8
+
9
+ - Rebuilds text legibility over photo and video backdrops. Scrims now ramp off
10
+ an eased curve instead of a two-stop linear fade, so they no longer leave a
11
+ visible band across the frame, and they are shaped to where each template's
12
+ copy actually sits rather than washing the whole picture. Type over media
13
+ carries its own halo, which lets the scrims stay lighter: the footage keeps
14
+ its contrast and highlights while the headline stays readable.
15
+ - Stops darkening the brand gradient for media that never arrives. A dead,
16
+ blocked, or unresolved `mediaUrl` used to leave the full scrim stack over
17
+ the gradient fallback, so the scene rendered as a muddy, vignetted version
18
+ of the gradient scenes beside it. The fallback is now the clean gradient it
19
+ was always documented to be.
20
+ - Holds a media scene's scrim back until the backdrop actually paints, so a
21
+ photo or video that is still loading shows the clean brand gradient instead
22
+ of a gradient wearing an overlay meant for footage. The scrim and the
23
+ picture now arrive on the same frame.
24
+ - Preloads scene backdrops as soon as their URL is known — including the
25
+ `asset.patch` that carries a resolved stock lookup — so the loading window
26
+ is usually gone before the scene plays. Video posters are warmed; video
27
+ streams deliberately are not.
28
+ - Hides playback controls while a video is playing until the viewer hovers,
29
+ focuses, or taps the player, and keeps paused and completed controls visible.
30
+ - Makes fullscreen usable on mobile browsers through prefixed fullscreen APIs
31
+ and a viewport-filling fallback when native fullscreen is unavailable.
32
+ - Restores configured soundtrack volume and end-of-video fades on iPhone Safari
33
+ by using a Web Audio gain stage for same-origin audio when element volume is
34
+ device-controlled, while preserving direct playback for cross-origin tracks.
35
+
7
36
  ## 0.3.1
8
37
 
9
38
  - Redesigns player controls for each playback state with responsive circular
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Give your AI a video output
2
2
 
3
- ![Version 0.3.1 beta](https://img.shields.io/badge/version-0.3.1_beta-7c3aed)
3
+ ![Version 0.3.2 beta](https://img.shields.io/badge/version-0.3.2_beta-7c3aed)
4
4
 
5
5
  **VanillaSky is the open-source video response layer.** Turn text, structured
6
6
  data, and live application context into personalized video responses that start
@@ -17,7 +17,7 @@ the planning prompt, trusted templates, validation, streaming, and player.
17
17
  For humans:
18
18
 
19
19
  ```bash
20
- npm install @vanillaskyai/video@0.3.1 ai @ai-sdk/openai
20
+ npm install @vanillaskyai/video@0.3.2 ai @ai-sdk/openai
21
21
  ```
22
22
 
23
23
  For coding agents:
@@ -106,7 +106,7 @@ shows each complete, validated scene as soon as it is ready and returns a
106
106
  deterministic `Video` object when generation finishes.
107
107
 
108
108
  A copy-and-run app is in
109
- [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.3.1/examples/nextjs-quickstart).
109
+ [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.3.2/examples/nextjs-quickstart).
110
110
 
111
111
  ## Shape the response
112
112
 
@@ -4,15 +4,15 @@ import {
4
4
  import "./chunk-QAAO7OAY.js";
5
5
  import {
6
6
  TemplateText
7
- } from "./chunk-BCRUUJ6A.js";
7
+ } from "./chunk-TCEHK2BW.js";
8
8
  import "./chunk-L5BTTCZV.js";
9
9
  import {
10
10
  BrandGradientOverlay
11
- } from "./chunk-ABZACD4G.js";
11
+ } from "./chunk-IJM4B2ZN.js";
12
12
  import "./chunk-XTOXFMLM.js";
13
13
  import {
14
14
  resolveTokens
15
- } from "./chunk-FNAEQ7QO.js";
15
+ } from "./chunk-XWWLKRNU.js";
16
16
 
17
17
  // src/visual-system/scene-templates/bg-confetti.tsx
18
18
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -3,17 +3,17 @@ import {
3
3
  } from "./chunk-QAAO7OAY.js";
4
4
  import {
5
5
  TemplateText
6
- } from "./chunk-BCRUUJ6A.js";
6
+ } from "./chunk-TCEHK2BW.js";
7
7
  import {
8
8
  Emoji
9
9
  } from "./chunk-L5BTTCZV.js";
10
10
  import {
11
11
  BrandGradientOverlay
12
- } from "./chunk-ABZACD4G.js";
12
+ } from "./chunk-IJM4B2ZN.js";
13
13
  import "./chunk-XTOXFMLM.js";
14
14
  import {
15
15
  resolveTokens
16
- } from "./chunk-FNAEQ7QO.js";
16
+ } from "./chunk-XWWLKRNU.js";
17
17
 
18
18
  // src/visual-system/scene-templates/bg-emoji.tsx
19
19
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,20 +1,23 @@
1
1
  import {
2
2
  SceneBackground,
3
3
  getMediaBackgroundProps
4
- } from "./chunk-IFOW65Z6.js";
4
+ } from "./chunk-4AODC6MS.js";
5
+ import {
6
+ hasSceneMedia
7
+ } from "./chunk-35LIYILY.js";
5
8
  import {
6
9
  ConfettiLayer
7
10
  } from "./chunk-CJ7F2CE6.js";
8
11
  import "./chunk-QAAO7OAY.js";
9
12
  import {
10
13
  TemplateText
11
- } from "./chunk-BCRUUJ6A.js";
14
+ } from "./chunk-TCEHK2BW.js";
12
15
  import "./chunk-L5BTTCZV.js";
13
- import "./chunk-ABZACD4G.js";
16
+ import "./chunk-IJM4B2ZN.js";
14
17
  import "./chunk-XTOXFMLM.js";
15
18
  import {
16
19
  resolveTokens
17
- } from "./chunk-FNAEQ7QO.js";
20
+ } from "./chunk-XWWLKRNU.js";
18
21
 
19
22
  // src/visual-system/scene-templates/bg-media.tsx
20
23
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -55,6 +58,7 @@ var BgMediaTemplate = ({
55
58
  width,
56
59
  height,
57
60
  ...getMediaBackgroundProps(variables),
61
+ textAnchor: "center",
58
62
  backgroundEffect,
59
63
  seed: String(variables.texts || ""),
60
64
  isPlaying,
@@ -74,6 +78,7 @@ var BgMediaTemplate = ({
74
78
  /* @__PURE__ */ jsx(
75
79
  TemplateText,
76
80
  {
81
+ overMedia: hasSceneMedia(variables),
77
82
  motionProgress,
78
83
  typeTreatment: resolveTokens(style).preset.type,
79
84
  archetype: textArchetype ?? "subtle",
@@ -4,15 +4,16 @@ import {
4
4
  import {
5
5
  SceneBackground,
6
6
  getMediaBackgroundProps
7
- } from "./chunk-IFOW65Z6.js";
7
+ } from "./chunk-4AODC6MS.js";
8
+ import "./chunk-35LIYILY.js";
8
9
  import {
9
10
  renderWithEmoji
10
11
  } from "./chunk-L5BTTCZV.js";
11
- import "./chunk-ABZACD4G.js";
12
+ import "./chunk-IJM4B2ZN.js";
12
13
  import {
13
14
  interpolate
14
15
  } from "./chunk-XTOXFMLM.js";
15
- import "./chunk-FNAEQ7QO.js";
16
+ import "./chunk-XWWLKRNU.js";
16
17
 
17
18
  // src/visual-system/primitives/typography/BrandMessageBubble.tsx
18
19
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1,15 +1,18 @@
1
1
  import {
2
2
  SceneBackground,
3
3
  getMediaBackgroundProps
4
- } from "./chunk-IFOW65Z6.js";
4
+ } from "./chunk-4AODC6MS.js";
5
+ import {
6
+ hasSceneMedia
7
+ } from "./chunk-35LIYILY.js";
5
8
  import {
6
9
  TemplateText
7
- } from "./chunk-BCRUUJ6A.js";
10
+ } from "./chunk-TCEHK2BW.js";
8
11
  import "./chunk-L5BTTCZV.js";
9
12
  import {
10
13
  TOP_TEXT_AREA_RATIO,
11
14
  withOpacity
12
- } from "./chunk-ABZACD4G.js";
15
+ } from "./chunk-IJM4B2ZN.js";
13
16
  import {
14
17
  SPRING_SNAPPY,
15
18
  interpolate,
@@ -17,7 +20,7 @@ import {
17
20
  } from "./chunk-XTOXFMLM.js";
18
21
  import {
19
22
  resolveTokens
20
- } from "./chunk-FNAEQ7QO.js";
23
+ } from "./chunk-XWWLKRNU.js";
21
24
 
22
25
  // src/visual-system/primitives/charts/BarChart.tsx
23
26
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -279,6 +282,7 @@ var ChartBarTemplate = ({
279
282
  /* @__PURE__ */ jsx2("div", { style: { position: "absolute", top: 0, left: 0, right: 0, height: height * TOP_TEXT_AREA_RATIO, overflow: "visible" }, children: /* @__PURE__ */ jsx2(
280
283
  TemplateText,
281
284
  {
285
+ overMedia: hasSceneMedia(variables),
282
286
  motionProgress,
283
287
  typeTreatment: resolveTokens(style).preset.type,
284
288
  archetype: "subtle",
@@ -4,27 +4,31 @@ import {
4
4
  import {
5
5
  SceneBackground,
6
6
  getMediaBackgroundProps
7
- } from "./chunk-IFOW65Z6.js";
7
+ } from "./chunk-4AODC6MS.js";
8
+ import {
9
+ hasSceneMedia
10
+ } from "./chunk-35LIYILY.js";
8
11
  import {
9
12
  ConfettiLayer
10
13
  } from "./chunk-CJ7F2CE6.js";
11
14
  import "./chunk-QAAO7OAY.js";
12
15
  import {
13
16
  TemplateText
14
- } from "./chunk-BCRUUJ6A.js";
17
+ } from "./chunk-TCEHK2BW.js";
15
18
  import "./chunk-L5BTTCZV.js";
16
19
  import {
17
20
  TOP_TEXT_AREA_RATIO,
18
21
  withOpacity
19
- } from "./chunk-ABZACD4G.js";
22
+ } from "./chunk-IJM4B2ZN.js";
20
23
  import {
21
24
  interpolate,
22
25
  spring
23
26
  } from "./chunk-XTOXFMLM.js";
24
27
  import {
28
+ MEDIA_TEXT_SHADOW,
25
29
  TOKEN_DEFAULTS,
26
30
  resolveTokens
27
- } from "./chunk-FNAEQ7QO.js";
31
+ } from "./chunk-XWWLKRNU.js";
28
32
 
29
33
  // src/visual-system/primitives/typography/CountUpNumber.tsx
30
34
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -93,7 +97,7 @@ var CountUpNumber = ({
93
97
  [0, 1],
94
98
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
95
99
  );
96
- const mediaTextShadow = hasMediaShadow ? "0 2px 8px rgba(0,0,0,0.55), 0 6px 16px rgba(0,0,0,0.35)" : void 0;
100
+ const mediaTextShadow = hasMediaShadow ? MEDIA_TEXT_SHADOW : void 0;
97
101
  const finalLabelFontSize = labelFontSize ?? 36 * s * 1.3;
98
102
  return /* @__PURE__ */ jsxs(
99
103
  "div",
@@ -253,6 +257,7 @@ var ChartCounterTemplate = ({
253
257
  /* @__PURE__ */ jsx2("div", { style: { position: "absolute", top: 0, left: 0, right: 0, height: height * TOP_TEXT_AREA_RATIO, overflow: "visible" }, children: /* @__PURE__ */ jsx2(
254
258
  TemplateText,
255
259
  {
260
+ overMedia: hasSceneMedia(variables),
256
261
  motionProgress,
257
262
  typeTreatment: resolveTokens(style).preset.type,
258
263
  archetype: "subtle",
@@ -5,22 +5,25 @@ import {
5
5
  import {
6
6
  SceneBackground,
7
7
  getMediaBackgroundProps
8
- } from "./chunk-IFOW65Z6.js";
8
+ } from "./chunk-4AODC6MS.js";
9
+ import {
10
+ hasSceneMedia
11
+ } from "./chunk-35LIYILY.js";
9
12
  import {
10
13
  TemplateText
11
- } from "./chunk-BCRUUJ6A.js";
14
+ } from "./chunk-TCEHK2BW.js";
12
15
  import "./chunk-L5BTTCZV.js";
13
16
  import {
14
17
  TOP_TEXT_AREA_RATIO,
15
18
  withOpacity
16
- } from "./chunk-ABZACD4G.js";
19
+ } from "./chunk-IJM4B2ZN.js";
17
20
  import {
18
21
  interpolate
19
22
  } from "./chunk-XTOXFMLM.js";
20
23
  import {
21
24
  TOKEN_DEFAULTS,
22
25
  resolveTokens
23
- } from "./chunk-FNAEQ7QO.js";
26
+ } from "./chunk-XWWLKRNU.js";
24
27
 
25
28
  // src/visual-system/primitives/charts/ProgressRing.tsx
26
29
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -254,6 +257,7 @@ var ChartProgressRingTemplate = ({
254
257
  /* @__PURE__ */ jsx2("div", { style: { position: "absolute", top: 0, left: 0, right: 0, height: height * TOP_TEXT_AREA_RATIO, overflow: "visible" }, children: /* @__PURE__ */ jsx2(
255
258
  TemplateText,
256
259
  {
260
+ overMedia: hasSceneMedia(variables),
257
261
  motionProgress,
258
262
  typeTreatment: resolveTokens(style).preset.type,
259
263
  archetype: "subtle",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  VideoFrame
3
- } from "./chunk-QTMHS7JD.js";
4
- import "./chunk-FNAEQ7QO.js";
3
+ } from "./chunk-HXSV6RHF.js";
4
+ import "./chunk-XWWLKRNU.js";
5
5
  import "./chunk-SPVTJH3F.js";
6
6
  import {
7
7
  resolveVideoBrand
@@ -0,0 +1,25 @@
1
+ // src/visual-system/scene-templates/media-source.ts
2
+ var VIDEO_EXTENSIONS = [".mp4", ".webm", ".mov", ".m4v", ".avi"];
3
+ function isVideoUrl(url) {
4
+ try {
5
+ const pathname = new URL(url).pathname.toLowerCase();
6
+ return VIDEO_EXTENSIONS.some((ext) => pathname.endsWith(ext));
7
+ } catch {
8
+ const lower = url.toLowerCase();
9
+ return VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext));
10
+ }
11
+ }
12
+ function resolveMediaType(mediaType, mediaUrl) {
13
+ if (mediaType === "gradient") return "gradient";
14
+ if (mediaType === "video") return "video";
15
+ if (mediaType === "photo") return "photo";
16
+ return mediaUrl && isVideoUrl(mediaUrl) ? "video" : "photo";
17
+ }
18
+ function hasSceneMedia(variables) {
19
+ return String(variables.mediaUrl || "").trim() !== "" && String(variables.mediaType || "auto") !== "gradient";
20
+ }
21
+
22
+ export {
23
+ resolveMediaType,
24
+ hasSceneMedia
25
+ };
@@ -1,21 +1,14 @@
1
+ import {
2
+ resolveMediaType
3
+ } from "./chunk-35LIYILY.js";
1
4
  import {
2
5
  BrandGradientOverlay,
3
6
  getBackgroundTransform
4
- } from "./chunk-ABZACD4G.js";
7
+ } from "./chunk-IJM4B2ZN.js";
5
8
 
6
9
  // src/visual-system/scene-templates/scene-background.tsx
7
- import { useEffect, useRef } from "react";
10
+ import { useEffect, useRef, useState } from "react";
8
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
- var VIDEO_EXTENSIONS = [".mp4", ".webm", ".mov", ".m4v", ".avi"];
10
- function isVideoUrl(url) {
11
- try {
12
- const pathname = new URL(url).pathname.toLowerCase();
13
- return VIDEO_EXTENSIONS.some((ext) => pathname.endsWith(ext));
14
- } catch {
15
- const lower = url.toLowerCase();
16
- return VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext));
17
- }
18
- }
19
12
  var MEDIA_POSITIONS = {
20
13
  center: "center center",
21
14
  top: "center top",
@@ -29,38 +22,57 @@ function resolveMediaPosition(value) {
29
22
  function resolveMediaTreatment(value) {
30
23
  return value === "subtle" || value === "text-safe" ? value : "cinematic";
31
24
  }
32
- function getMediaTreatmentLayers(value) {
25
+ var SCRIM_STOP_COUNT = 7;
26
+ function smoothstep(t) {
27
+ return t * t * (3 - 2 * t);
28
+ }
29
+ function easedStops(peakAlpha, start, end, direction) {
30
+ const alphaAt = (t) => {
31
+ const eased = direction === "fade-out" ? 1 - smoothstep(t) : smoothstep(t);
32
+ return `rgba(0,0,0,${Number((peakAlpha * eased).toFixed(3))})`;
33
+ };
34
+ const stops = [];
35
+ if (start > 0) stops.push(`${alphaAt(0)} 0%`);
36
+ for (let i = 0; i < SCRIM_STOP_COUNT; i += 1) {
37
+ const t = i / (SCRIM_STOP_COUNT - 1);
38
+ const position = Number((start + (end - start) * t).toFixed(2));
39
+ stops.push(`${alphaAt(t)} ${position}%`);
40
+ }
41
+ if (end < 100) stops.push(`${alphaAt(1)} 100%`);
42
+ return stops.join(", ");
43
+ }
44
+ function getMediaTreatmentLayers(value, anchor = "full") {
33
45
  const treatment = resolveMediaTreatment(value);
34
46
  const vignette = {
35
47
  id: "vignette",
36
- background: treatment === "subtle" ? "radial-gradient(ellipse at center, transparent 45%, rgba(0,0,0,0.28) 100%)" : "radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.55) 80%, rgba(0,0,0,0.75) 100%)"
48
+ 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")})`
37
49
  };
38
50
  if (treatment === "subtle") return [vignette];
39
- const cinematic = [
40
- vignette,
41
- {
51
+ const textSafe = treatment === "text-safe";
52
+ const layers = [vignette];
53
+ if (anchor !== "bottom") {
54
+ layers.push({
42
55
  id: "center-scrim",
43
- background: treatment === "text-safe" ? "radial-gradient(ellipse 90% 56% at 50% 50%, rgba(0,0,0,0.36) 0%, rgba(0,0,0,0.18) 55%, transparent 84%)" : "radial-gradient(ellipse 85% 50% at 50% 50%, rgba(0,0,0,0.22) 0%, rgba(0,0,0,0.10) 50%, transparent 80%)"
44
- },
45
- {
56
+ 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")})`
57
+ });
58
+ }
59
+ if (anchor !== "center") {
60
+ layers.push({
46
61
  id: "bottom-scrim",
47
- background: treatment === "text-safe" ? "linear-gradient(to top, rgba(0,0,0,0.68) 0%, transparent 100%)" : "linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%)",
62
+ background: `linear-gradient(to top, ${easedStops(textSafe ? 0.64 : 0.5, 8, 100, "fade-out")})`,
48
63
  style: { top: "55%" }
49
- }
50
- ];
51
- if (treatment === "text-safe") {
52
- cinematic.splice(1, 0, {
53
- id: "full-wash",
54
- background: "rgba(0,0,0,0.30)"
55
64
  });
56
65
  }
57
- return cinematic;
66
+ return layers;
58
67
  }
59
- function resolveMediaType(mediaType, mediaUrl) {
60
- if (mediaType === "gradient") return "gradient";
61
- if (mediaType === "video") return "video";
62
- if (mediaType === "photo") return "photo";
63
- return mediaUrl && isVideoUrl(mediaUrl) ? "video" : "photo";
68
+ function initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster) {
69
+ if (typeof window === "undefined") return "ready";
70
+ if (!wantsMedia) return "ready";
71
+ if (resolved === "video") return mediaPoster ? "ready" : "pending";
72
+ if (typeof Image === "undefined") return "ready";
73
+ const cached = new Image();
74
+ cached.src = mediaUrl;
75
+ return cached.complete && cached.naturalWidth > 0 ? "ready" : "pending";
64
76
  }
65
77
  function getMediaBackgroundProps(variables) {
66
78
  return {
@@ -83,6 +95,7 @@ var SceneBackground = ({
83
95
  mediaPoster,
84
96
  mediaPosition = "center",
85
97
  mediaTreatment = "cinematic",
98
+ textAnchor = "full",
86
99
  backgroundEffect,
87
100
  seed,
88
101
  isPlaying = true,
@@ -91,10 +104,35 @@ var SceneBackground = ({
91
104
  void _width;
92
105
  void _height;
93
106
  const resolved = resolveMediaType(mediaType, mediaUrl);
94
- const showMedia = resolved !== "gradient" && !!mediaUrl;
107
+ const wantsMedia = resolved !== "gradient" && !!mediaUrl;
108
+ const [mediaPaint, setMediaPaint] = useState(
109
+ () => initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster)
110
+ );
111
+ useEffect(() => {
112
+ setMediaPaint(initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster));
113
+ if (!wantsMedia || resolved !== "photo") return;
114
+ if (typeof Image === "undefined") return;
115
+ let cancelled = false;
116
+ const probe = new Image();
117
+ probe.onload = () => {
118
+ if (!cancelled) setMediaPaint("ready");
119
+ };
120
+ probe.onerror = () => {
121
+ if (!cancelled) setMediaPaint("failed");
122
+ };
123
+ probe.src = mediaUrl;
124
+ if (probe.complete) setMediaPaint(probe.naturalWidth > 0 ? "ready" : "failed");
125
+ return () => {
126
+ cancelled = true;
127
+ probe.onload = null;
128
+ probe.onerror = null;
129
+ };
130
+ }, [mediaUrl, mediaPoster, resolved, wantsMedia]);
131
+ const showMedia = wantsMedia && mediaPaint !== "failed";
132
+ const showTreatment = wantsMedia && mediaPaint === "ready";
95
133
  const resolvedPosition = resolveMediaPosition(mediaPosition);
96
134
  const resolvedTreatment = resolveMediaTreatment(mediaTreatment);
97
- const treatmentLayers = getMediaTreatmentLayers(resolvedTreatment);
135
+ const treatmentLayers = getMediaTreatmentLayers(resolvedTreatment, textAnchor);
98
136
  const gradSeed = typeof seed === "number" ? seed : typeof seed === "string" ? seed.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0) : 0;
99
137
  const bgTransform = getBackgroundTransform(
100
138
  backgroundEffect,
@@ -154,6 +192,8 @@ var SceneBackground = ({
154
192
  loop: true,
155
193
  playsInline: true,
156
194
  preload: "auto",
195
+ onLoadedData: () => setMediaPaint("ready"),
196
+ onError: () => setMediaPaint("failed"),
157
197
  "data-media-position": mediaPosition,
158
198
  style: {
159
199
  position: "absolute",
@@ -181,7 +221,7 @@ var SceneBackground = ({
181
221
  }
182
222
  }
183
223
  )),
184
- showMedia && treatmentLayers.map((layer) => /* @__PURE__ */ jsx(
224
+ showTreatment && treatmentLayers.map((layer) => /* @__PURE__ */ jsx(
185
225
  "div",
186
226
  {
187
227
  "data-media-treatment": resolvedTreatment,
@@ -11,7 +11,7 @@ import {
11
11
  } from "./chunk-XTOXFMLM.js";
12
12
  import {
13
13
  TOKEN_DEFAULTS
14
- } from "./chunk-FNAEQ7QO.js";
14
+ } from "./chunk-XWWLKRNU.js";
15
15
 
16
16
  // src/visual-system/primitives/devices/product-surface-config.ts
17
17
  function resolveProductSurfaceMotion(value) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveDensity
3
- } from "./chunk-FNAEQ7QO.js";
3
+ } from "./chunk-XWWLKRNU.js";
4
4
  import {
5
5
  resolveVideoTimeline
6
6
  } from "./chunk-SPVTJH3F.js";
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  darken,
6
6
  resolveTokens
7
- } from "./chunk-FNAEQ7QO.js";
7
+ } from "./chunk-XWWLKRNU.js";
8
8
 
9
9
  // src/visual-system/theme/colors.ts
10
10
  function withOpacity(hex, opacity) {
@@ -9,6 +9,9 @@ import {
9
9
  interpolate,
10
10
  spring
11
11
  } from "./chunk-XTOXFMLM.js";
12
+ import {
13
+ MEDIA_TEXT_SHADOW
14
+ } from "./chunk-XWWLKRNU.js";
12
15
 
13
16
  // src/visual-system/scene-templates/text-archetypes.ts
14
17
  var TEXT_ARCHETYPES = [
@@ -526,7 +529,8 @@ var TemplateText = ({
526
529
  safeZone = DEFAULT_SAFE_ZONE,
527
530
  font = DEFAULT_FONT,
528
531
  color = "#FFFFFF",
529
- beatIntensity = 0
532
+ beatIntensity = 0,
533
+ overMedia = false
530
534
  }) => {
531
535
  const textSafe = typeof textRaw === "string" ? textRaw : "";
532
536
  const isDevelopment = import.meta.env?.DEV;
@@ -568,7 +572,7 @@ var TemplateText = ({
568
572
  } : baseTypo;
569
573
  const presetWeight = tt ? Math.min(900, Math.max(100, fontWeight + tt.weightDelta)) : fontWeight;
570
574
  const presetSize = tt ? fontSize * tt.sizeScale : fontSize;
571
- const textShadow = dropShadowFor(color);
575
+ const textShadow = overMedia ? MEDIA_TEXT_SHADOW : dropShadowFor(color);
572
576
  const containerStyle = {
573
577
  position: "absolute",
574
578
  left: 0,
@@ -144,6 +144,7 @@ function lighten(hex, factor) {
144
144
  const lb = Math.min(255, Math.round(b + (255 - b) * factor));
145
145
  return `#${lr.toString(16).padStart(2, "0")}${lg.toString(16).padStart(2, "0")}${lb.toString(16).padStart(2, "0")}`;
146
146
  }
147
+ var MEDIA_TEXT_SHADOW = "0 2px 8px rgba(0,0,0,0.55), 0 6px 16px rgba(0,0,0,0.35)";
147
148
 
148
149
  export {
149
150
  TOKEN_DEFAULTS,
@@ -151,5 +152,6 @@ export {
151
152
  resolveDensity,
152
153
  resolveTokens,
153
154
  darken,
154
- lighten
155
+ lighten,
156
+ MEDIA_TEXT_SHADOW
155
157
  };