@vanillaskyai/video 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/CHANGELOG.md +45 -0
- package/LICENSE +201 -0
- package/PUBLIC-API.md +276 -0
- package/README.md +152 -0
- package/SECURITY.md +22 -0
- package/SUPPORT.md +28 -0
- package/bin/vanillasky.js +3 -0
- package/dist/bg-confetti-WNQXS7ZS.js +89 -0
- package/dist/bg-emoji-ETI47GLS.js +150 -0
- package/dist/bg-media-SVUZAMGX.js +99 -0
- package/dist/brand-message-F3RD4P3P.js +253 -0
- package/dist/builtin-server-F7YXRVCH.js +8 -0
- package/dist/catalog-types-BIhSpOWK.d.ts +74 -0
- package/dist/chart-bar-CNSC7BNK.js +304 -0
- package/dist/chart-counter-MCRHKN77.js +299 -0
- package/dist/chart-progress-ring-RMI4A7ZK.js +296 -0
- package/dist/check-runtime.d.ts +15 -0
- package/dist/check-runtime.js +102 -0
- package/dist/chunk-2E6T633S.js +27 -0
- package/dist/chunk-352ZZCET.js +83 -0
- package/dist/chunk-73NTSFFI.js +81 -0
- package/dist/chunk-A2X45FET.js +46 -0
- package/dist/chunk-ABPKM4GK.js +187 -0
- package/dist/chunk-ABZACD4G.js +216 -0
- package/dist/chunk-BCRUUJ6A.js +756 -0
- package/dist/chunk-CHLGW22F.js +3551 -0
- package/dist/chunk-CJ7F2CE6.js +104 -0
- package/dist/chunk-CR7FE7BW.js +109 -0
- package/dist/chunk-E7CL7UPB.js +109 -0
- package/dist/chunk-EE6PK6XC.js +18 -0
- package/dist/chunk-ERY4S6QV.js +385 -0
- package/dist/chunk-EVG2ZYIY.js +125 -0
- package/dist/chunk-FNAEQ7QO.js +155 -0
- package/dist/chunk-FQ47WXAJ.js +38 -0
- package/dist/chunk-FSG2PYWG.js +336 -0
- package/dist/chunk-GHJEAP4O.js +504 -0
- package/dist/chunk-GRMUV7QY.js +262 -0
- package/dist/chunk-IFOW65Z6.js +205 -0
- package/dist/chunk-IXCUY3B3.js +786 -0
- package/dist/chunk-L5BTTCZV.js +97 -0
- package/dist/chunk-LMG3GHLL.js +107 -0
- package/dist/chunk-OOPIETLW.js +101 -0
- package/dist/chunk-P22HHFDK.js +43 -0
- package/dist/chunk-QAAO7OAY.js +19 -0
- package/dist/chunk-S7HFLYNN.js +35 -0
- package/dist/chunk-SPVTJH3F.js +24 -0
- package/dist/chunk-W2XJKG6C.js +3177 -0
- package/dist/chunk-XB7LH4J2.js +301 -0
- package/dist/chunk-XGRA2MUV.js +242 -0
- package/dist/chunk-XTOXFMLM.js +105 -0
- package/dist/chunk-YC5MFXA5.js +132 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1681 -0
- package/dist/compose-video-CN3VSWST.js +13 -0
- package/dist/cta-logo-SVA5GP2A.js +209 -0
- package/dist/cta-media-E7VLLK42.js +215 -0
- package/dist/events-_wfjbmEp.d.ts +81 -0
- package/dist/incoming-call-62MFZL3Z.js +320 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/infographic-before-after-AXCDAWND.js +460 -0
- package/dist/infographic-feature-list-GW5GUE6O.js +266 -0
- package/dist/infographic-problem-solution-7NURJANA.js +340 -0
- package/dist/infographic-stat-row-J7IZR6ZN.js +241 -0
- package/dist/infographic-steps-VSZM6F5M.js +400 -0
- package/dist/kit-BMNcHqa9.d.ts +79 -0
- package/dist/prompt-input-LOUCRXCY.js +236 -0
- package/dist/react.d.ts +81 -0
- package/dist/react.js +835 -0
- package/dist/reaction-QG4CYZLQ.js +119 -0
- package/dist/server.d.ts +141 -0
- package/dist/server.js +498 -0
- package/dist/showcase-code-RUTFZR3H.js +351 -0
- package/dist/showcase-phone-6CP6XEU5.js +272 -0
- package/dist/showcase-terminal-MGAEQWPR.js +306 -0
- package/dist/showcase-web-Y4W2HVRJ.js +390 -0
- package/dist/social-conversation-75SD2637.js +695 -0
- package/dist/social-milestone-XI2QE5BO.js +292 -0
- package/dist/social-notification-2UOHQCUY.js +244 -0
- package/dist/social-review-stack-FDYZGZTJ.js +239 -0
- package/dist/social-testimonial-OQ7N52GJ.js +284 -0
- package/dist/social-tweet-5OLCNPPC.js +295 -0
- package/dist/state-DZcKuS32.d.ts +3 -0
- package/dist/system-prompt-A6UXNTPK.js +8 -0
- package/dist/template-catalog.d.ts +24 -0
- package/dist/template-catalog.js +6 -0
- package/dist/templates.d.ts +25 -0
- package/dist/templates.js +19 -0
- package/dist/test.d.ts +149 -0
- package/dist/test.js +272 -0
- package/dist/text-stream-6XHC7J47.js +10 -0
- package/dist/types-B-PZR6G_.d.ts +208 -0
- package/dist/validate-T7GBU2YF.js +8 -0
- package/docs/agent-integration.md +39 -0
- package/docs/architecture.md +84 -0
- package/docs/branding-and-personalization.md +87 -0
- package/docs/concepts.md +106 -0
- package/docs/custom-templates.md +329 -0
- package/docs/customization.md +141 -0
- package/docs/errors.md +70 -0
- package/docs/getting-started.md +111 -0
- package/docs/input-and-first-scene.md +41 -0
- package/docs/integrate-nextjs.md +79 -0
- package/docs/media-and-audio.md +69 -0
- package/docs/motion-and-effects.md +107 -0
- package/docs/persistence.md +95 -0
- package/docs/production.md +122 -0
- package/docs/prompt-and-input.md +185 -0
- package/docs/provider-integration.md +115 -0
- package/docs/reference/protocol.md +96 -0
- package/docs/reference/provider-adapters.md +158 -0
- package/docs/responsive-orientation.md +37 -0
- package/docs/security.md +38 -0
- package/docs/streaming-protocol.md +15 -0
- package/docs/testing.md +137 -0
- package/docs/use-cases.md +59 -0
- package/examples/custom-template/README.md +19 -0
- package/examples/custom-template/minimal-text.tsx +82 -0
- package/examples/custom-template/structured-data.tsx +104 -0
- package/examples/custom-template/supplied-media.tsx +92 -0
- package/examples/nextjs-quickstart/.env.example +2 -0
- package/examples/nextjs-quickstart/README.md +22 -0
- package/examples/nextjs-quickstart/next-env.d.ts +4 -0
- package/examples/nextjs-quickstart/package.json +24 -0
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +22 -0
- package/examples/nextjs-quickstart/src/app/layout.tsx +5 -0
- package/examples/nextjs-quickstart/src/app/page.tsx +31 -0
- package/examples/nextjs-quickstart/tsconfig.json +26 -0
- package/package.json +167 -0
- package/registry/items/backgrounds.json +52 -0
- package/registry/items/barChart.json +237 -0
- package/registry/items/beforeAfter.json +180 -0
- package/registry/items/bigNumber.json +225 -0
- package/registry/items/brandMessage.json +158 -0
- package/registry/items/cardList.json +206 -0
- package/registry/items/chatMessenger.json +151 -0
- package/registry/items/chatWhatsapp.json +151 -0
- package/registry/items/codeEditor.json +184 -0
- package/registry/items/confetti.json +107 -0
- package/registry/items/ctaLogo.json +160 -0
- package/registry/items/ctaMedia.json +193 -0
- package/registry/items/emojiBurst.json +101 -0
- package/registry/items/incomingCall.json +170 -0
- package/registry/items/media.json +179 -0
- package/registry/items/milestone.json +205 -0
- package/registry/items/motion.json +45 -0
- package/registry/items/notification.json +182 -0
- package/registry/items/phoneMockup.json +254 -0
- package/registry/items/problemSolution.json +186 -0
- package/registry/items/progressRing.json +199 -0
- package/registry/items/promptInput.json +148 -0
- package/registry/items/reaction.json +170 -0
- package/registry/items/reviewStack.json +224 -0
- package/registry/items/steps.json +207 -0
- package/registry/items/template-context.json +31 -0
- package/registry/items/terminal.json +212 -0
- package/registry/items/testimonial.json +164 -0
- package/registry/items/theme.json +47 -0
- package/registry/items/tripleStats.json +228 -0
- package/registry/items/tweet.json +203 -0
- package/registry/items/typography.json +47 -0
- package/registry/items/webMockup.json +273 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveDensity
|
|
3
|
+
} from "./chunk-FNAEQ7QO.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveVideoTimeline
|
|
6
|
+
} from "./chunk-SPVTJH3F.js";
|
|
7
|
+
import {
|
|
8
|
+
getTemplateDefaults
|
|
9
|
+
} from "./chunk-73NTSFFI.js";
|
|
10
|
+
|
|
11
|
+
// src/player/video-frame.tsx
|
|
12
|
+
import { createElement, Suspense } from "react";
|
|
13
|
+
|
|
14
|
+
// src/visual-system/layout.ts
|
|
15
|
+
function getDimensions(orientation = "portrait") {
|
|
16
|
+
return orientation === "landscape" ? { width: 1920, height: 1080, aspectRatio: "16 / 9" } : { width: 1080, height: 1920, aspectRatio: "9 / 16" };
|
|
17
|
+
}
|
|
18
|
+
function getSafeZone(orientation = "portrait") {
|
|
19
|
+
return orientation === "landscape" ? { top: 108, bottom: 108, left: 192, right: 192 } : { top: 100, bottom: 100, left: 60, right: 60 };
|
|
20
|
+
}
|
|
21
|
+
function scaleSafeZone(zone, factor) {
|
|
22
|
+
if (factor === 1) return zone;
|
|
23
|
+
return {
|
|
24
|
+
top: Math.round(zone.top * factor),
|
|
25
|
+
bottom: Math.round(zone.bottom * factor),
|
|
26
|
+
left: Math.round(zone.left * factor),
|
|
27
|
+
right: Math.round(zone.right * factor)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/player/video-frame.tsx
|
|
32
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
33
|
+
var SCENE_TRANSITION_SECONDS = 0.3;
|
|
34
|
+
var CONTIGUITY_ULP_FACTOR = 4;
|
|
35
|
+
function clamp01(value) {
|
|
36
|
+
return Math.max(0, Math.min(1, value));
|
|
37
|
+
}
|
|
38
|
+
function supportsContinuousTransition(value) {
|
|
39
|
+
return value === "crossfade" || value === "fade";
|
|
40
|
+
}
|
|
41
|
+
function rangesAreContiguous(left, right) {
|
|
42
|
+
const scale = Math.max(1, Math.abs(left.end), Math.abs(right.start));
|
|
43
|
+
const ulpTolerance = Number.EPSILON * scale * CONTIGUITY_ULP_FACTOR;
|
|
44
|
+
return Math.abs(left.end - right.start) <= ulpTolerance;
|
|
45
|
+
}
|
|
46
|
+
function brandBackground(config) {
|
|
47
|
+
const background = config.style.brand.background;
|
|
48
|
+
return background.type === "solid" ? background.color : `linear-gradient(135deg, ${background.colors[0]}, ${background.colors[1]})`;
|
|
49
|
+
}
|
|
50
|
+
function brandBackgroundFallback(config) {
|
|
51
|
+
const background = config.style.brand.background;
|
|
52
|
+
return background.type === "solid" ? background.color : background.colors[0];
|
|
53
|
+
}
|
|
54
|
+
function sceneBackgroundChanges(left, right) {
|
|
55
|
+
const mediaUrl = (range) => String(range.scene.variables.mediaType || "auto") === "gradient" ? "" : String(range.scene.variables.mediaUrl || "");
|
|
56
|
+
return mediaUrl(left) !== mediaUrl(right);
|
|
57
|
+
}
|
|
58
|
+
function SceneLayer({
|
|
59
|
+
kit,
|
|
60
|
+
config,
|
|
61
|
+
range,
|
|
62
|
+
progress,
|
|
63
|
+
motionProgress,
|
|
64
|
+
width,
|
|
65
|
+
height,
|
|
66
|
+
playing,
|
|
67
|
+
layer,
|
|
68
|
+
opacity,
|
|
69
|
+
interactive,
|
|
70
|
+
zIndex
|
|
71
|
+
}) {
|
|
72
|
+
const template = kit.getTemplate(range.scene.templateId);
|
|
73
|
+
const duration = range.end - range.start;
|
|
74
|
+
return /* @__PURE__ */ jsx(
|
|
75
|
+
"div",
|
|
76
|
+
{
|
|
77
|
+
"data-scene-layer": layer,
|
|
78
|
+
"data-layer-scene-id": range.scene.id,
|
|
79
|
+
"data-layer-template-id": range.scene.templateId,
|
|
80
|
+
"aria-hidden": interactive ? void 0 : true,
|
|
81
|
+
...interactive ? {} : { inert: "inert" },
|
|
82
|
+
style: {
|
|
83
|
+
position: "absolute",
|
|
84
|
+
inset: 0,
|
|
85
|
+
opacity,
|
|
86
|
+
zIndex,
|
|
87
|
+
pointerEvents: interactive ? "auto" : "none",
|
|
88
|
+
// A changed-media transition can expose an incoming template's true
|
|
89
|
+
// initial frame. Values that would be false placeholders at progress
|
|
90
|
+
// zero opt into this inherited guard.
|
|
91
|
+
"--vanillasky-transition-semantic-visibility": layer === "incoming" ? "hidden" : "visible"
|
|
92
|
+
},
|
|
93
|
+
children: template ? /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(
|
|
94
|
+
"div",
|
|
95
|
+
{
|
|
96
|
+
"data-template-loading": range.scene.templateId,
|
|
97
|
+
style: { position: "absolute", inset: 0 }
|
|
98
|
+
}
|
|
99
|
+
), children: createElement(template.component, {
|
|
100
|
+
variables: { ...getTemplateDefaults(template.schema), ...range.scene.variables },
|
|
101
|
+
style: config.style,
|
|
102
|
+
progress,
|
|
103
|
+
motionProgress,
|
|
104
|
+
beatIntensity: 0,
|
|
105
|
+
width,
|
|
106
|
+
height,
|
|
107
|
+
textArchetype: range.scene.textArchetype ?? config.style.defaultTextArchetype,
|
|
108
|
+
backgroundEffect: range.scene.backgroundEffect ?? config.style.defaultBackgroundEffect,
|
|
109
|
+
safeZone: scaleSafeZone(
|
|
110
|
+
getSafeZone(config.orientation),
|
|
111
|
+
resolveDensity(config.style.density).safeZoneScale
|
|
112
|
+
),
|
|
113
|
+
sceneDuration: duration,
|
|
114
|
+
isPlaying: playing
|
|
115
|
+
}) }) : /* @__PURE__ */ jsxs("div", { style: { width, height, background: "#090712", color: "#fff" }, children: [
|
|
116
|
+
"Unsupported template: ",
|
|
117
|
+
range.scene.templateId
|
|
118
|
+
] })
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
function VideoFrame({
|
|
123
|
+
kit,
|
|
124
|
+
config,
|
|
125
|
+
time,
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
playing = false,
|
|
129
|
+
className,
|
|
130
|
+
style
|
|
131
|
+
}) {
|
|
132
|
+
const timeline = resolveVideoTimeline(config);
|
|
133
|
+
const lastRange = timeline.at(-1);
|
|
134
|
+
const foundIndex = timeline.findIndex((range) => time >= range.start && time < range.end);
|
|
135
|
+
const afterEnd = lastRange && time >= lastRange.end;
|
|
136
|
+
const activeIndex = foundIndex >= 0 ? foundIndex : afterEnd ? timeline.length - 1 : -1;
|
|
137
|
+
const active = activeIndex >= 0 ? timeline[activeIndex] : void 0;
|
|
138
|
+
if (!active) {
|
|
139
|
+
return /* @__PURE__ */ jsx(
|
|
140
|
+
"div",
|
|
141
|
+
{
|
|
142
|
+
"data-video-frame": timeline.length === 0 ? "empty" : "gap",
|
|
143
|
+
className,
|
|
144
|
+
style: { width, height, background: brandBackground(config), ...style }
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (!kit.getTemplate(active.scene.templateId)) {
|
|
149
|
+
return /* @__PURE__ */ jsxs(
|
|
150
|
+
"div",
|
|
151
|
+
{
|
|
152
|
+
"data-video-frame": "unsupported",
|
|
153
|
+
"data-template-id": active.scene.templateId,
|
|
154
|
+
className,
|
|
155
|
+
style: { width, height, background: "#090712", color: "#fff", ...style },
|
|
156
|
+
children: [
|
|
157
|
+
"Unsupported template: ",
|
|
158
|
+
active.scene.templateId
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const duration = active.end - active.start;
|
|
164
|
+
const rawProgress = clamp01((time - active.start) / duration);
|
|
165
|
+
const transitionEnabled = supportsContinuousTransition(config.style.defaultTransition);
|
|
166
|
+
const next = activeIndex < timeline.length - 1 ? timeline[activeIndex + 1] : void 0;
|
|
167
|
+
const activeTemplate = kit.getTemplate(active.scene.templateId);
|
|
168
|
+
const nextTemplate = next ? kit.getTemplate(next.scene.templateId) : void 0;
|
|
169
|
+
const contiguousNext = next && rangesAreContiguous(active, next) && activeTemplate.usesGlobalTransition && activeTemplate.transitionTiming && nextTemplate?.usesGlobalTransition && nextTemplate.transitionTiming ? next : void 0;
|
|
170
|
+
const activeTiming = activeTemplate.transitionTiming;
|
|
171
|
+
const nextTiming = nextTemplate?.transitionTiming;
|
|
172
|
+
const eligibleNextTransition = Boolean(
|
|
173
|
+
transitionEnabled && contiguousNext && activeTiming && nextTiming && sceneBackgroundChanges(active, contiguousNext)
|
|
174
|
+
);
|
|
175
|
+
const blendDuration = Math.min(
|
|
176
|
+
SCENE_TRANSITION_SECONDS,
|
|
177
|
+
Math.max(0, duration)
|
|
178
|
+
);
|
|
179
|
+
const blendStart = active.end - blendDuration;
|
|
180
|
+
const blendEnd = blendStart + blendDuration;
|
|
181
|
+
const previewingNext = Boolean(
|
|
182
|
+
eligibleNextTransition && time >= blendStart && time < blendEnd
|
|
183
|
+
);
|
|
184
|
+
const blendProgress = previewingNext && blendDuration > 0 ? Math.round(clamp01((time - blendStart) / blendDuration) * 1e6) / 1e6 : 0;
|
|
185
|
+
const progress = rawProgress;
|
|
186
|
+
const motionProgress = afterEnd && activeTiming ? activeTiming.holdProgress : rawProgress;
|
|
187
|
+
const canvas = getDimensions(config.orientation);
|
|
188
|
+
const scale = Math.min(width / canvas.width, height / canvas.height);
|
|
189
|
+
const canvasLeft = (width - canvas.width * scale) / 2;
|
|
190
|
+
const canvasTop = (height - canvas.height * scale) / 2;
|
|
191
|
+
return /* @__PURE__ */ jsx(
|
|
192
|
+
"div",
|
|
193
|
+
{
|
|
194
|
+
"data-video-frame": "ready",
|
|
195
|
+
"data-scene-id": active.scene.id,
|
|
196
|
+
"data-template-id": active.scene.templateId,
|
|
197
|
+
className,
|
|
198
|
+
style: {
|
|
199
|
+
width,
|
|
200
|
+
height,
|
|
201
|
+
position: "relative",
|
|
202
|
+
overflow: "hidden",
|
|
203
|
+
background: brandBackground(config),
|
|
204
|
+
...style
|
|
205
|
+
},
|
|
206
|
+
children: /* @__PURE__ */ jsxs(
|
|
207
|
+
"div",
|
|
208
|
+
{
|
|
209
|
+
"data-video-canvas": "true",
|
|
210
|
+
style: {
|
|
211
|
+
position: "absolute",
|
|
212
|
+
left: canvasLeft,
|
|
213
|
+
top: canvasTop,
|
|
214
|
+
width: canvas.width,
|
|
215
|
+
height: canvas.height,
|
|
216
|
+
transform: `scale(${scale})`,
|
|
217
|
+
transformOrigin: "top left"
|
|
218
|
+
},
|
|
219
|
+
children: [
|
|
220
|
+
/* @__PURE__ */ jsx(
|
|
221
|
+
"div",
|
|
222
|
+
{
|
|
223
|
+
"data-player-background": "brand",
|
|
224
|
+
"aria-hidden": "true",
|
|
225
|
+
style: {
|
|
226
|
+
position: "absolute",
|
|
227
|
+
inset: 0,
|
|
228
|
+
background: brandBackground(config),
|
|
229
|
+
backgroundColor: brandBackgroundFallback(config),
|
|
230
|
+
pointerEvents: "none"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
),
|
|
234
|
+
previewingNext && contiguousNext && nextTiming ? [
|
|
235
|
+
/* @__PURE__ */ jsx(
|
|
236
|
+
SceneLayer,
|
|
237
|
+
{
|
|
238
|
+
kit,
|
|
239
|
+
config,
|
|
240
|
+
range: active,
|
|
241
|
+
progress,
|
|
242
|
+
motionProgress,
|
|
243
|
+
width: canvas.width,
|
|
244
|
+
height: canvas.height,
|
|
245
|
+
playing,
|
|
246
|
+
layer: "outgoing",
|
|
247
|
+
opacity: 1 - blendProgress,
|
|
248
|
+
interactive: true,
|
|
249
|
+
zIndex: 1
|
|
250
|
+
},
|
|
251
|
+
active.scene.id
|
|
252
|
+
),
|
|
253
|
+
/* @__PURE__ */ jsx(
|
|
254
|
+
SceneLayer,
|
|
255
|
+
{
|
|
256
|
+
kit,
|
|
257
|
+
config,
|
|
258
|
+
range: contiguousNext,
|
|
259
|
+
progress: 0,
|
|
260
|
+
motionProgress: 0,
|
|
261
|
+
width: canvas.width,
|
|
262
|
+
height: canvas.height,
|
|
263
|
+
playing: false,
|
|
264
|
+
layer: "incoming",
|
|
265
|
+
opacity: blendProgress,
|
|
266
|
+
interactive: false,
|
|
267
|
+
zIndex: 2
|
|
268
|
+
},
|
|
269
|
+
contiguousNext.scene.id
|
|
270
|
+
)
|
|
271
|
+
] : [
|
|
272
|
+
/* @__PURE__ */ jsx(
|
|
273
|
+
SceneLayer,
|
|
274
|
+
{
|
|
275
|
+
kit,
|
|
276
|
+
config,
|
|
277
|
+
range: active,
|
|
278
|
+
progress,
|
|
279
|
+
motionProgress,
|
|
280
|
+
width: canvas.width,
|
|
281
|
+
height: canvas.height,
|
|
282
|
+
playing,
|
|
283
|
+
layer: "active",
|
|
284
|
+
opacity: 1,
|
|
285
|
+
interactive: true,
|
|
286
|
+
zIndex: 1
|
|
287
|
+
},
|
|
288
|
+
active.scene.id
|
|
289
|
+
)
|
|
290
|
+
]
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export {
|
|
299
|
+
getDimensions,
|
|
300
|
+
VideoFrame
|
|
301
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/protocol/background.ts
|
|
2
|
+
var BACKGROUND_PRESETS = {
|
|
3
|
+
cosmic: { type: "gradient", colors: ["#8711C1", "#2167E3"] },
|
|
4
|
+
horizon: { type: "gradient", colors: ["#5967C4", "#133A94"] },
|
|
5
|
+
twilight: { type: "gradient", colors: ["#0C1740", "#3D1B66"] },
|
|
6
|
+
meadow: { type: "gradient", colors: ["#348756", "#54B6CA"] },
|
|
7
|
+
velvet: { type: "gradient", colors: ["#76030F", "#121B67"] },
|
|
8
|
+
flamingo: { type: "gradient", colors: ["#C72D50", "#3E3B92"] },
|
|
9
|
+
peach: { type: "gradient", colors: ["#B45A4A", "#AD336D"] },
|
|
10
|
+
saffron: { type: "gradient", colors: ["#F3696E", "#F8A902"] },
|
|
11
|
+
black: { type: "solid", color: "#000000" },
|
|
12
|
+
midnight: { type: "solid", color: "#070B20" },
|
|
13
|
+
aubergine: { type: "solid", color: "#170A2E" },
|
|
14
|
+
coal: { type: "solid", color: "#0A0A0A" },
|
|
15
|
+
navy: { type: "solid", color: "#0A2240" }
|
|
16
|
+
};
|
|
17
|
+
var DEFAULT_BACKGROUND_PRESET = "cosmic";
|
|
18
|
+
var DEFAULT_COLORS = {
|
|
19
|
+
primary: "#00E5A0",
|
|
20
|
+
secondary: "#006BE5",
|
|
21
|
+
foreground: "#FFFFFF",
|
|
22
|
+
surface: "#0A0A14",
|
|
23
|
+
surfaceElevated: "#14152A",
|
|
24
|
+
muted: "#A7A6B0"
|
|
25
|
+
};
|
|
26
|
+
var HEX_COLOR = /^#[0-9a-f]{6}$/i;
|
|
27
|
+
var MINIMUM_TEXT_CONTRAST = 4.5;
|
|
28
|
+
function normalizeColor(value, path) {
|
|
29
|
+
if (!HEX_COLOR.test(value)) throw new Error(`${path} must be a hex color`);
|
|
30
|
+
return value.toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
function resolveBackground(input) {
|
|
33
|
+
if (input == null || typeof input === "string") {
|
|
34
|
+
const preset = BACKGROUND_PRESETS[input ?? DEFAULT_BACKGROUND_PRESET];
|
|
35
|
+
return preset.type === "solid" ? { type: "solid", color: preset.color } : { type: "gradient", colors: [...preset.colors] };
|
|
36
|
+
}
|
|
37
|
+
if ("color" in input) {
|
|
38
|
+
return { type: "solid", color: normalizeColor(input.color, "brand.background.color") };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
type: "gradient",
|
|
42
|
+
colors: [
|
|
43
|
+
normalizeColor(input.colors[0], "brand.background.colors[0]"),
|
|
44
|
+
normalizeColor(input.colors[1], "brand.background.colors[1]")
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function channel(value) {
|
|
49
|
+
const normalized = value / 255;
|
|
50
|
+
return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
|
51
|
+
}
|
|
52
|
+
function luminance(color) {
|
|
53
|
+
return 0.2126 * channel(Number.parseInt(color.slice(1, 3), 16)) + 0.7152 * channel(Number.parseInt(color.slice(3, 5), 16)) + 0.0722 * channel(Number.parseInt(color.slice(5, 7), 16));
|
|
54
|
+
}
|
|
55
|
+
function gradientLuminanceAt(colors, progress) {
|
|
56
|
+
const channels = colors.map((color) => [1, 3, 5].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16)));
|
|
57
|
+
return 0.2126 * channel(channels[0][0] + (channels[1][0] - channels[0][0]) * progress) + 0.7152 * channel(channels[0][1] + (channels[1][1] - channels[0][1]) * progress) + 0.0722 * channel(channels[0][2] + (channels[1][2] - channels[0][2]) * progress);
|
|
58
|
+
}
|
|
59
|
+
function backgroundLuminanceRange(background) {
|
|
60
|
+
if (background.type === "solid") {
|
|
61
|
+
const value = luminance(background.color);
|
|
62
|
+
return [value, value];
|
|
63
|
+
}
|
|
64
|
+
const start = luminance(background.colors[0]);
|
|
65
|
+
const end = luminance(background.colors[1]);
|
|
66
|
+
let left = 0;
|
|
67
|
+
let right = 1;
|
|
68
|
+
for (let iteration = 0; iteration < 64; iteration += 1) {
|
|
69
|
+
const first = left + (right - left) / 3;
|
|
70
|
+
const second = right - (right - left) / 3;
|
|
71
|
+
if (gradientLuminanceAt(background.colors, first) <= gradientLuminanceAt(background.colors, second)) {
|
|
72
|
+
right = second;
|
|
73
|
+
} else {
|
|
74
|
+
left = first;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const minimum = Math.min(start, end, gradientLuminanceAt(background.colors, (left + right) / 2));
|
|
78
|
+
return [minimum, Math.max(start, end)];
|
|
79
|
+
}
|
|
80
|
+
function minimumBackgroundContrast(background, foreground) {
|
|
81
|
+
const foregroundLuminance = luminance(foreground);
|
|
82
|
+
const [minimum, maximum] = backgroundLuminanceRange(background);
|
|
83
|
+
if (foregroundLuminance < minimum) {
|
|
84
|
+
return (minimum + 0.05) / (foregroundLuminance + 0.05);
|
|
85
|
+
}
|
|
86
|
+
if (foregroundLuminance > maximum) {
|
|
87
|
+
return (foregroundLuminance + 0.05) / (maximum + 0.05);
|
|
88
|
+
}
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
function resolveForeground(input, background) {
|
|
92
|
+
const supplied = input?.colors?.foreground;
|
|
93
|
+
if (supplied != null) return normalizeColor(supplied, "brand.colors.foreground");
|
|
94
|
+
const candidates = ["#FFFFFF", "#000000"];
|
|
95
|
+
const foreground = candidates.map((color) => ({ color, contrast: minimumBackgroundContrast(background, color) })).sort((left, right) => right.contrast - left.contrast)[0];
|
|
96
|
+
if (foreground.contrast < MINIMUM_TEXT_CONTRAST) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"brand.colors.foreground is required because neither black nor white contrasts with the entire background; choose a background with one consistently readable foreground"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return foreground.color;
|
|
102
|
+
}
|
|
103
|
+
function validateResolvedBrandContrast(brand, path) {
|
|
104
|
+
const contrast = minimumBackgroundContrast(brand.background, brand.colors.foreground);
|
|
105
|
+
if (contrast < MINIMUM_TEXT_CONTRAST) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${path}.colors.foreground must have at least 4.5:1 contrast across ${path}.background (minimum ${contrast.toFixed(2)}:1); choose a different foreground or background)`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function resolveVideoBrand(input) {
|
|
112
|
+
validateBrandInput(input, "brand");
|
|
113
|
+
const background = resolveBackground(input?.background);
|
|
114
|
+
const colors = Object.fromEntries(Object.entries({
|
|
115
|
+
...DEFAULT_COLORS,
|
|
116
|
+
...input?.colors,
|
|
117
|
+
foreground: resolveForeground(input, background)
|
|
118
|
+
}).map(([name, value]) => [name, normalizeColor(value, `brand.colors.${name}`)]));
|
|
119
|
+
const brand = {
|
|
120
|
+
...input?.name?.trim() ? { name: input.name.trim() } : {},
|
|
121
|
+
...input?.logoUrl?.trim() ? { logoUrl: input.logoUrl.trim() } : {},
|
|
122
|
+
font: input?.font?.trim() || "Inter",
|
|
123
|
+
scriptFont: input?.scriptFont?.trim() || "Caveat",
|
|
124
|
+
background,
|
|
125
|
+
colors
|
|
126
|
+
};
|
|
127
|
+
validateResolvedBrandContrast(brand, "brand");
|
|
128
|
+
return brand;
|
|
129
|
+
}
|
|
130
|
+
function isBackgroundPreset(value) {
|
|
131
|
+
return Object.prototype.hasOwnProperty.call(BACKGROUND_PRESETS, value);
|
|
132
|
+
}
|
|
133
|
+
function validateBrandBackgroundInput(value, path) {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
if (!isBackgroundPreset(value)) throw new Error(`${path} preset is unsupported`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
139
|
+
throw new Error(`${path} must be a preset or custom color object`);
|
|
140
|
+
}
|
|
141
|
+
const background = value;
|
|
142
|
+
const keys = Object.keys(background);
|
|
143
|
+
if (keys.length !== 1 || keys[0] !== "color" && keys[0] !== "colors") {
|
|
144
|
+
throw new Error(`${path} must contain either color or colors`);
|
|
145
|
+
}
|
|
146
|
+
if ("color" in background) {
|
|
147
|
+
if (typeof background.color !== "string") throw new Error(`${path}.color must be a hex color`);
|
|
148
|
+
normalizeColor(background.color, `${path}.color`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(background.colors) || background.colors.length !== 2 || background.colors.some((color) => typeof color !== "string")) {
|
|
152
|
+
throw new Error(`${path}.colors must contain two colors`);
|
|
153
|
+
}
|
|
154
|
+
background.colors.forEach((color, index) => normalizeColor(color, `${path}.colors[${index}]`));
|
|
155
|
+
}
|
|
156
|
+
function validateBrandInput(value, path) {
|
|
157
|
+
if (value == null) return;
|
|
158
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
|
159
|
+
const input = value;
|
|
160
|
+
const allowed = /* @__PURE__ */ new Set(["name", "logoUrl", "font", "scriptFont", "background", "colors"]);
|
|
161
|
+
for (const key of Object.keys(input)) {
|
|
162
|
+
if (!allowed.has(key)) throw new Error(`${path} contains unsupported field ${key}`);
|
|
163
|
+
}
|
|
164
|
+
for (const key of ["name", "logoUrl", "font", "scriptFont"]) {
|
|
165
|
+
if (input[key] != null && (typeof input[key] !== "string" || !input[key].trim())) {
|
|
166
|
+
throw new Error(`${path}.${key} must be a non-empty string`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (input.background != null) validateBrandBackgroundInput(input.background, `${path}.background`);
|
|
170
|
+
if (input.colors != null) {
|
|
171
|
+
if (typeof input.colors !== "object" || Array.isArray(input.colors)) {
|
|
172
|
+
throw new Error(`${path}.colors must be an object`);
|
|
173
|
+
}
|
|
174
|
+
const colors = input.colors;
|
|
175
|
+
const colorNames = new Set(Object.keys(DEFAULT_COLORS));
|
|
176
|
+
for (const [name, color] of Object.entries(colors)) {
|
|
177
|
+
if (!colorNames.has(name)) throw new Error(`${path}.colors contains unsupported field ${name}`);
|
|
178
|
+
if (typeof color !== "string") throw new Error(`${path}.colors.${name} must be a hex color`);
|
|
179
|
+
normalizeColor(color, `${path}.colors.${name}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function validateVideoBrand(value, path) {
|
|
184
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
185
|
+
throw new Error(`${path} must be an object`);
|
|
186
|
+
}
|
|
187
|
+
const brand = value;
|
|
188
|
+
const allowed = /* @__PURE__ */ new Set(["name", "logoUrl", "font", "scriptFont", "background", "colors"]);
|
|
189
|
+
for (const key of Object.keys(brand)) {
|
|
190
|
+
if (!allowed.has(key)) throw new Error(`${path} contains unsupported field ${key}`);
|
|
191
|
+
}
|
|
192
|
+
for (const key of ["font", "scriptFont"]) {
|
|
193
|
+
if (typeof brand[key] !== "string" || !brand[key].trim()) {
|
|
194
|
+
throw new Error(`${path}.${key} must be a non-empty string`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const key of ["name", "logoUrl"]) {
|
|
198
|
+
if (brand[key] != null && (typeof brand[key] !== "string" || !brand[key].trim())) {
|
|
199
|
+
throw new Error(`${path}.${key} must be a non-empty string`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!brand.background || typeof brand.background !== "object" || Array.isArray(brand.background)) {
|
|
203
|
+
throw new Error(`${path}.background must be an object`);
|
|
204
|
+
}
|
|
205
|
+
const background = brand.background;
|
|
206
|
+
if (background.type === "solid") {
|
|
207
|
+
if (Object.keys(background).some((key) => key !== "type" && key !== "color")) {
|
|
208
|
+
throw new Error(`${path}.background contains unsupported fields`);
|
|
209
|
+
}
|
|
210
|
+
if (typeof background.color !== "string") throw new Error(`${path}.background.color must be a hex color`);
|
|
211
|
+
normalizeColor(background.color, `${path}.background.color`);
|
|
212
|
+
} else if (background.type === "gradient") {
|
|
213
|
+
if (Object.keys(background).some((key) => key !== "type" && key !== "colors")) {
|
|
214
|
+
throw new Error(`${path}.background contains unsupported fields`);
|
|
215
|
+
}
|
|
216
|
+
if (!Array.isArray(background.colors) || background.colors.length !== 2 || background.colors.some((color) => typeof color !== "string")) {
|
|
217
|
+
throw new Error(`${path}.background.colors must contain two colors`);
|
|
218
|
+
}
|
|
219
|
+
background.colors.forEach((color, index) => normalizeColor(color, `${path}.background.colors[${index}]`));
|
|
220
|
+
} else {
|
|
221
|
+
throw new Error(`${path}.background.type is unsupported`);
|
|
222
|
+
}
|
|
223
|
+
if (!brand.colors || typeof brand.colors !== "object" || Array.isArray(brand.colors)) {
|
|
224
|
+
throw new Error(`${path}.colors must be an object`);
|
|
225
|
+
}
|
|
226
|
+
const colors = brand.colors;
|
|
227
|
+
const requiredColors = Object.keys(DEFAULT_COLORS);
|
|
228
|
+
for (const name of requiredColors) {
|
|
229
|
+
if (typeof colors[name] !== "string") throw new Error(`${path}.colors.${name} must be a hex color`);
|
|
230
|
+
normalizeColor(colors[name], `${path}.colors.${name}`);
|
|
231
|
+
}
|
|
232
|
+
for (const name of Object.keys(colors)) {
|
|
233
|
+
if (!requiredColors.includes(name)) throw new Error(`${path}.colors contains unsupported field ${name}`);
|
|
234
|
+
}
|
|
235
|
+
validateResolvedBrandContrast(brand, path);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
resolveVideoBrand,
|
|
240
|
+
validateBrandInput,
|
|
241
|
+
validateVideoBrand
|
|
242
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/visual-system/motion/curves.ts
|
|
2
|
+
function cubicBezier(x1, y1, x2, y2) {
|
|
3
|
+
const cx = 3 * x1;
|
|
4
|
+
const bx = 3 * (x2 - x1) - cx;
|
|
5
|
+
const ax = 1 - cx - bx;
|
|
6
|
+
const cy = 3 * y1;
|
|
7
|
+
const by = 3 * (y2 - y1) - cy;
|
|
8
|
+
const ay = 1 - cy - by;
|
|
9
|
+
const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
|
|
10
|
+
const sampleY = (t) => ((ay * t + by) * t + cy) * t;
|
|
11
|
+
const sampleDX = (t) => (3 * ax * t + 2 * bx) * t + cx;
|
|
12
|
+
return (x) => {
|
|
13
|
+
if (x <= 0) return 0;
|
|
14
|
+
if (x >= 1) return 1;
|
|
15
|
+
let t = x;
|
|
16
|
+
for (let i = 0; i < 8; i++) {
|
|
17
|
+
const err = sampleX(t) - x;
|
|
18
|
+
if (Math.abs(err) < 1e-6) return sampleY(t);
|
|
19
|
+
const d = sampleDX(t);
|
|
20
|
+
if (Math.abs(d) < 1e-6) break;
|
|
21
|
+
t -= err / d;
|
|
22
|
+
}
|
|
23
|
+
let lo = 0;
|
|
24
|
+
let hi = 1;
|
|
25
|
+
t = x;
|
|
26
|
+
for (let i = 0; i < 24; i++) {
|
|
27
|
+
const v = sampleX(t);
|
|
28
|
+
if (Math.abs(v - x) < 1e-6) break;
|
|
29
|
+
if (v < x) lo = t;
|
|
30
|
+
else hi = t;
|
|
31
|
+
t = (lo + hi) / 2;
|
|
32
|
+
}
|
|
33
|
+
return sampleY(t);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var Easing = {
|
|
37
|
+
/** Easing.bezier(0.16, 1, 0.3, 1) — the canonical curve syntax from docs/motion-library.md. */
|
|
38
|
+
bezier: cubicBezier,
|
|
39
|
+
// Curves
|
|
40
|
+
linear: (t) => t,
|
|
41
|
+
quad: (t) => t * t,
|
|
42
|
+
cubic: (t) => t * t * t,
|
|
43
|
+
sin: (t) => 1 - Math.cos(t * Math.PI / 2),
|
|
44
|
+
exp: (t) => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
|
|
45
|
+
circle: (t) => 1 - Math.sqrt(1 - t * t),
|
|
46
|
+
// Directional modifiers
|
|
47
|
+
in: (fn) => fn,
|
|
48
|
+
out: (fn) => (t) => 1 - fn(1 - t),
|
|
49
|
+
inOut: (fn) => (t) => t < 0.5 ? fn(t * 2) / 2 : 1 - fn((1 - t) * 2) / 2
|
|
50
|
+
};
|
|
51
|
+
function interpolate(value, inputRange, outputRange, options) {
|
|
52
|
+
const { easing, extrapolateLeft = "extend", extrapolateRight = "extend" } = options ?? {};
|
|
53
|
+
let i = 0;
|
|
54
|
+
for (; i < inputRange.length - 2; i++) {
|
|
55
|
+
if (value < inputRange[i + 1]) break;
|
|
56
|
+
}
|
|
57
|
+
const inputMin = inputRange[i];
|
|
58
|
+
const inputMax = inputRange[i + 1];
|
|
59
|
+
const outputMin = outputRange[i];
|
|
60
|
+
const outputMax = outputRange[i + 1];
|
|
61
|
+
let t = inputMax === inputMin ? 0 : (value - inputMin) / (inputMax - inputMin);
|
|
62
|
+
if (t < 0 && extrapolateLeft === "clamp") t = 0;
|
|
63
|
+
if (t > 1 && extrapolateRight === "clamp") t = 1;
|
|
64
|
+
if (easing && t >= 0 && t <= 1) {
|
|
65
|
+
t = easing(t);
|
|
66
|
+
}
|
|
67
|
+
return outputMin + t * (outputMax - outputMin);
|
|
68
|
+
}
|
|
69
|
+
var SPRING_SMOOTH = { damping: 26, stiffness: 170 };
|
|
70
|
+
var SPRING_SNAPPY = { damping: 20, stiffness: 300 };
|
|
71
|
+
var SPRING_BOUNCY = { damping: 10, stiffness: 180 };
|
|
72
|
+
var SPRING_CRISP = { damping: 14, stiffness: 320 };
|
|
73
|
+
function spring(progress, config) {
|
|
74
|
+
if (progress <= 0) return 0;
|
|
75
|
+
if (progress >= 1) {
|
|
76
|
+
const { damping: damping2 = 26 } = config ?? {};
|
|
77
|
+
if (damping2 >= 20) return 1;
|
|
78
|
+
}
|
|
79
|
+
const { damping = 26, stiffness = 170, mass = 1 } = config ?? {};
|
|
80
|
+
const omega = Math.sqrt(stiffness / mass);
|
|
81
|
+
const zeta = damping / (2 * Math.sqrt(stiffness * mass));
|
|
82
|
+
const t = progress * 3.5;
|
|
83
|
+
let value;
|
|
84
|
+
if (zeta < 1) {
|
|
85
|
+
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
86
|
+
value = 1 - Math.exp(-zeta * omega * t) * (Math.cos(omegaD * t) + zeta * omega / omegaD * Math.sin(omegaD * t));
|
|
87
|
+
} else if (zeta === 1) {
|
|
88
|
+
value = 1 - Math.exp(-omega * t) * (1 + omega * t);
|
|
89
|
+
} else {
|
|
90
|
+
const s1 = -omega * (zeta + Math.sqrt(zeta * zeta - 1));
|
|
91
|
+
const s2 = -omega * (zeta - Math.sqrt(zeta * zeta - 1));
|
|
92
|
+
value = 1 + (s1 * Math.exp(s2 * t) - s2 * Math.exp(s1 * t)) / (s2 - s1);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
Easing,
|
|
99
|
+
interpolate,
|
|
100
|
+
SPRING_SMOOTH,
|
|
101
|
+
SPRING_SNAPPY,
|
|
102
|
+
SPRING_BOUNCY,
|
|
103
|
+
SPRING_CRISP,
|
|
104
|
+
spring
|
|
105
|
+
};
|