@vosjs/studio-core 0.18.1 → 0.19.1
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/index.d.ts +276 -3
- package/dist/index.js +577 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -98,6 +98,9 @@ function clipLength(clip) {
|
|
|
98
98
|
return clip.loop ? Math.max(span, clip.loopLen ?? span) : span;
|
|
99
99
|
}
|
|
100
100
|
var MOTION_EASE = "power2.inOut";
|
|
101
|
+
function isKeyedOverlay(c) {
|
|
102
|
+
return c.kind === "image" || c.kind === "video";
|
|
103
|
+
}
|
|
101
104
|
var LAYER_CARD_SHADOW = 0.32;
|
|
102
105
|
var MEDIA_KEY_PREFIX = "media:";
|
|
103
106
|
var OVERLAY_SCALE_MAX = 8;
|
|
@@ -1904,11 +1907,329 @@ function cardPointToScreen(u, v) {
|
|
|
1904
1907
|
|
|
1905
1908
|
// src/overlayText.ts
|
|
1906
1909
|
import {
|
|
1907
|
-
findFontFamily,
|
|
1908
|
-
fontFaceUrl,
|
|
1910
|
+
findFontFamily as findFontFamily2,
|
|
1911
|
+
fontFaceUrl as fontFaceUrl2,
|
|
1909
1912
|
fontStack,
|
|
1910
|
-
nearestFontWeight
|
|
1913
|
+
nearestFontWeight as nearestFontWeight2
|
|
1911
1914
|
} from "@vosjs/shared";
|
|
1915
|
+
|
|
1916
|
+
// src/htmlLayer.ts
|
|
1917
|
+
import { findFontFamily, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
1918
|
+
var DESIGN_FRAME_WIDTH = 1920;
|
|
1919
|
+
var HTML_LAYER_WARN_BYTES = 32e3;
|
|
1920
|
+
var HTML_LAYER_MAX_BYTES = 128e3;
|
|
1921
|
+
function splitTopLevel(value) {
|
|
1922
|
+
const out = [];
|
|
1923
|
+
let depth = 0;
|
|
1924
|
+
let cur = "";
|
|
1925
|
+
for (const ch of value) {
|
|
1926
|
+
if (ch === "(") depth++;
|
|
1927
|
+
else if (ch === ")") depth--;
|
|
1928
|
+
if (ch === "," && depth === 0) {
|
|
1929
|
+
out.push(cur);
|
|
1930
|
+
cur = "";
|
|
1931
|
+
} else cur += ch;
|
|
1932
|
+
}
|
|
1933
|
+
if (cur.trim()) out.push(cur);
|
|
1934
|
+
return out;
|
|
1935
|
+
}
|
|
1936
|
+
var unquote = (s) => s.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim();
|
|
1937
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
1938
|
+
"serif",
|
|
1939
|
+
"sans-serif",
|
|
1940
|
+
"monospace",
|
|
1941
|
+
"cursive",
|
|
1942
|
+
"fantasy",
|
|
1943
|
+
"system-ui",
|
|
1944
|
+
"ui-sans-serif",
|
|
1945
|
+
"ui-serif",
|
|
1946
|
+
"ui-monospace",
|
|
1947
|
+
"ui-rounded",
|
|
1948
|
+
"emoji",
|
|
1949
|
+
"math",
|
|
1950
|
+
"fangsong",
|
|
1951
|
+
"inherit",
|
|
1952
|
+
"initial",
|
|
1953
|
+
"unset"
|
|
1954
|
+
]);
|
|
1955
|
+
function cssFamilies(css) {
|
|
1956
|
+
if (!css) return [];
|
|
1957
|
+
const out = [];
|
|
1958
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1959
|
+
for (const decl of css.match(/font-family\s*:[^;}]+/gi) ?? []) {
|
|
1960
|
+
for (const item of splitTopLevel(decl.replace(/^[^:]*:/, ""))) {
|
|
1961
|
+
const name = unquote(item.replace(/!important/i, ""));
|
|
1962
|
+
if (!name) continue;
|
|
1963
|
+
const lower = name.toLowerCase();
|
|
1964
|
+
if (GENERIC_FAMILIES.has(lower) || lower.startsWith("-apple-")) continue;
|
|
1965
|
+
if (seen.has(lower)) continue;
|
|
1966
|
+
seen.add(lower);
|
|
1967
|
+
out.push(name);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return out;
|
|
1971
|
+
}
|
|
1972
|
+
function cssWeights(css, html = "") {
|
|
1973
|
+
const out = /* @__PURE__ */ new Set([400]);
|
|
1974
|
+
for (const decl of css?.match(/font-weight\s*:[^;}]+/gi) ?? []) {
|
|
1975
|
+
const value = decl.replace(/^[^:]*:/, "").toLowerCase();
|
|
1976
|
+
for (const n of value.match(/\b[1-9]00\b/g) ?? []) out.add(Number(n));
|
|
1977
|
+
if (/\bbold(er)?\b/.test(value)) out.add(700);
|
|
1978
|
+
}
|
|
1979
|
+
if (/<(b|strong|h[1-6])[\s>]/i.test(html)) out.add(700);
|
|
1980
|
+
return [...out].sort((a, b) => a - b);
|
|
1981
|
+
}
|
|
1982
|
+
function htmlLayerFaces(clip) {
|
|
1983
|
+
const faces = [];
|
|
1984
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1985
|
+
const add = (f) => {
|
|
1986
|
+
const key = `${f.family.toLowerCase()}|${f.weight}|${f.style}`;
|
|
1987
|
+
if (seen.has(key)) return;
|
|
1988
|
+
seen.add(key);
|
|
1989
|
+
faces.push(f);
|
|
1990
|
+
};
|
|
1991
|
+
const weights = cssWeights(clip.css, clip.html);
|
|
1992
|
+
for (const family of cssFamilies(clip.css)) {
|
|
1993
|
+
const entry = findFontFamily(family);
|
|
1994
|
+
if (!entry) continue;
|
|
1995
|
+
for (const w of weights)
|
|
1996
|
+
add({
|
|
1997
|
+
family: entry.family,
|
|
1998
|
+
url: fontFaceUrl(entry.slug, nearestFontWeight(entry, w)),
|
|
1999
|
+
weight: nearestFontWeight(entry, w),
|
|
2000
|
+
style: "normal"
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
for (const f of clip.fonts ?? [])
|
|
2004
|
+
add({
|
|
2005
|
+
family: f.family,
|
|
2006
|
+
url: f.url,
|
|
2007
|
+
weight: f.weight ?? 400,
|
|
2008
|
+
style: f.style ?? "normal"
|
|
2009
|
+
});
|
|
2010
|
+
return faces;
|
|
2011
|
+
}
|
|
2012
|
+
function htmlLayerUnhostedFamilies(clip) {
|
|
2013
|
+
const explicit = new Set(
|
|
2014
|
+
(clip.fonts ?? []).map((f) => f.family.toLowerCase())
|
|
2015
|
+
);
|
|
2016
|
+
return cssFamilies(clip.css).filter(
|
|
2017
|
+
(f) => !findFontFamily(f) && !explicit.has(f.toLowerCase())
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
function htmlLayerBleedFor(css) {
|
|
2021
|
+
if (!css) return 0;
|
|
2022
|
+
let worst = 0;
|
|
2023
|
+
const consider = (layer) => {
|
|
2024
|
+
if (/\binset\b/i.test(layer)) return;
|
|
2025
|
+
const lengths = layer.replace(/[a-z-]+\([^)]*\)/gi, " ").replace(/#[0-9a-f]{3,8}/gi, " ").match(/-?\d*\.?\d+/g);
|
|
2026
|
+
if (!lengths) return;
|
|
2027
|
+
const [x = 0, y = 0, blur = 0, spread = 0] = lengths.map(Number);
|
|
2028
|
+
worst = Math.max(
|
|
2029
|
+
worst,
|
|
2030
|
+
Math.abs(x) + blur + spread,
|
|
2031
|
+
Math.abs(y) + blur + spread
|
|
2032
|
+
);
|
|
2033
|
+
};
|
|
2034
|
+
for (const decl of css.match(/box-shadow\s*:[^;}]+/gi) ?? [])
|
|
2035
|
+
for (const layer of splitTopLevel(decl.replace(/^[^:]*:/, "")))
|
|
2036
|
+
consider(layer);
|
|
2037
|
+
for (const fn of css.match(/drop-shadow\(([^)]*)\)/gi) ?? [])
|
|
2038
|
+
consider(fn.replace(/^drop-shadow\(/i, "").replace(/\)$/, ""));
|
|
2039
|
+
return worst ? Math.ceil(worst * 1.15) : 0;
|
|
2040
|
+
}
|
|
2041
|
+
function htmlLayerBleed(clip) {
|
|
2042
|
+
return Math.max(0, Math.round(clip.bleed ?? htmlLayerBleedFor(clip.css)));
|
|
2043
|
+
}
|
|
2044
|
+
function htmlLayerPictureBox(clip) {
|
|
2045
|
+
const bleed = htmlLayerBleed(clip);
|
|
2046
|
+
return {
|
|
2047
|
+
width: clip.box.width + bleed * 2,
|
|
2048
|
+
height: clip.box.height + bleed * 2,
|
|
2049
|
+
bleed
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function htmlLayerWidth(clip) {
|
|
2053
|
+
if (clip.width != null) return clip.width;
|
|
2054
|
+
return Math.min(1, htmlLayerPictureBox(clip).width / DESIGN_FRAME_WIDTH);
|
|
2055
|
+
}
|
|
2056
|
+
var ENTITIES = {
|
|
2057
|
+
amp: "&",
|
|
2058
|
+
lt: "<",
|
|
2059
|
+
gt: ">",
|
|
2060
|
+
quot: '"',
|
|
2061
|
+
apos: "'",
|
|
2062
|
+
nbsp: " "
|
|
2063
|
+
};
|
|
2064
|
+
function htmlLayerText(html) {
|
|
2065
|
+
return html.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]*>/g, " ").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, ref) => {
|
|
2066
|
+
if (ref[0] === "#")
|
|
2067
|
+
return String.fromCodePoint(
|
|
2068
|
+
ref[1] === "x" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1))
|
|
2069
|
+
);
|
|
2070
|
+
return ENTITIES[ref.toLowerCase()] ?? m;
|
|
2071
|
+
}).replace(/\s+/g, " ").trim();
|
|
2072
|
+
}
|
|
2073
|
+
function htmlLayerLabel(clip) {
|
|
2074
|
+
const words = htmlLayerText(clip.html);
|
|
2075
|
+
return words ? words.slice(0, 24) : "HTML";
|
|
2076
|
+
}
|
|
2077
|
+
var VOID_TAGS = [
|
|
2078
|
+
"br",
|
|
2079
|
+
"img",
|
|
2080
|
+
"hr",
|
|
2081
|
+
"input",
|
|
2082
|
+
"source",
|
|
2083
|
+
"col",
|
|
2084
|
+
"area",
|
|
2085
|
+
"base",
|
|
2086
|
+
"wbr",
|
|
2087
|
+
"meta",
|
|
2088
|
+
"link"
|
|
2089
|
+
];
|
|
2090
|
+
var HTML_LAYER_FOREIGN_FIELDS = [
|
|
2091
|
+
"key",
|
|
2092
|
+
"radius",
|
|
2093
|
+
"shadow",
|
|
2094
|
+
"border",
|
|
2095
|
+
"loop",
|
|
2096
|
+
"frame",
|
|
2097
|
+
"fx",
|
|
2098
|
+
"text",
|
|
2099
|
+
"preset"
|
|
2100
|
+
];
|
|
2101
|
+
function htmlLayerProblems(clip) {
|
|
2102
|
+
const html = clip.html ?? "";
|
|
2103
|
+
const css = clip.css;
|
|
2104
|
+
const raw = clip;
|
|
2105
|
+
const problems = [];
|
|
2106
|
+
const push2 = (code, level, message) => problems.push({ code, level, message });
|
|
2107
|
+
if (!html.trim()) push2("empty", "problem", "The layer has no markup.");
|
|
2108
|
+
for (const tag of VOID_TAGS) {
|
|
2109
|
+
const open = new RegExp(`<${tag}(\\s[^>]*?)?>`, "gi");
|
|
2110
|
+
let m;
|
|
2111
|
+
let said = false;
|
|
2112
|
+
while (m = open.exec(html)) {
|
|
2113
|
+
if (!m[0].trimEnd().endsWith("/>") && !said) {
|
|
2114
|
+
said = true;
|
|
2115
|
+
push2(
|
|
2116
|
+
"unclosed-void",
|
|
2117
|
+
"problem",
|
|
2118
|
+
`<${tag}> must close itself as <${tag} /> inside a foreignObject.`
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
const bare = html.match(/&(?!(#\d+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);)/g);
|
|
2124
|
+
if (bare)
|
|
2125
|
+
push2(
|
|
2126
|
+
"bare-ampersand",
|
|
2127
|
+
"problem",
|
|
2128
|
+
`A bare "&" is a parse error; write &. Found ${bare.length}.`
|
|
2129
|
+
);
|
|
2130
|
+
if (/<[a-zA-Z][^>]*?\s[a-zA-Z-]+=(?!["'])[^\s>]+/.test(html))
|
|
2131
|
+
push2(
|
|
2132
|
+
"unquoted-attr",
|
|
2133
|
+
"problem",
|
|
2134
|
+
"Attribute values must be quoted inside a foreignObject."
|
|
2135
|
+
);
|
|
2136
|
+
const bytes = html.length + (css?.length ?? 0);
|
|
2137
|
+
if (bytes > HTML_LAYER_MAX_BYTES)
|
|
2138
|
+
push2(
|
|
2139
|
+
"too-large",
|
|
2140
|
+
"problem",
|
|
2141
|
+
`The layer's source is ${Math.round(bytes / 1e3)} KB; the ceiling is ${HTML_LAYER_MAX_BYTES / 1e3} KB. Inline less, or split the layer.`
|
|
2142
|
+
);
|
|
2143
|
+
else if (bytes > HTML_LAYER_WARN_BYTES)
|
|
2144
|
+
push2(
|
|
2145
|
+
"too-large",
|
|
2146
|
+
"warning",
|
|
2147
|
+
`The layer's source is ${Math.round(bytes / 1e3)} KB. A document carries it twice (the doc and the composed config); keep a layer under ${HTML_LAYER_WARN_BYTES / 1e3} KB.`
|
|
2148
|
+
);
|
|
2149
|
+
for (const field of HTML_LAYER_FOREIGN_FIELDS)
|
|
2150
|
+
if (raw[field] !== void 0)
|
|
2151
|
+
push2(
|
|
2152
|
+
"foreign-field",
|
|
2153
|
+
"problem",
|
|
2154
|
+
`An HTML layer has no "${field}": its CSS is its chrome (shadow, radius, border) and its markup its content.`
|
|
2155
|
+
);
|
|
2156
|
+
const needed = htmlLayerBleedFor(css);
|
|
2157
|
+
if (clip.bleed != null && needed > clip.bleed)
|
|
2158
|
+
push2(
|
|
2159
|
+
"shadow-clipped",
|
|
2160
|
+
"warning",
|
|
2161
|
+
`The CSS paints up to ${needed}px outside the component and bleed is ${clip.bleed}, so the shadow is clipped square at the layer's edge. Set bleed to ${needed}, or leave it out to derive it.`
|
|
2162
|
+
);
|
|
2163
|
+
const external = /\b(src|href)\s*=\s*["']\s*(https?:)?\/\//i.test(html) || /url\(\s*["']?\s*(https?:)?\/\//i.test(css ?? "") || /url\(\s*["']?\s*(https?:)?\/\//i.test(html);
|
|
2164
|
+
if (external)
|
|
2165
|
+
push2(
|
|
2166
|
+
"external-resource",
|
|
2167
|
+
"warning",
|
|
2168
|
+
"The source names a resource by URL. An SVG image fetches nothing, so it will not paint; inline it as a data: URI (faces are inlined for you when the CSS names a hosted family)."
|
|
2169
|
+
);
|
|
2170
|
+
if (/\b(animation|transition)(-[a-z-]+)?\s*:/i.test(css ?? ""))
|
|
2171
|
+
push2(
|
|
2172
|
+
"wall-clock",
|
|
2173
|
+
"warning",
|
|
2174
|
+
"CSS animations inside a layer run on the wall clock, not the timeline: the export will not match the preview. Animate the layer with motion and anim instead."
|
|
2175
|
+
);
|
|
2176
|
+
for (const family of htmlLayerUnhostedFamilies(clip))
|
|
2177
|
+
push2(
|
|
2178
|
+
"unhosted-family",
|
|
2179
|
+
"warning",
|
|
2180
|
+
`The CSS names "${family}", which is not a hosted family (GET https://vos.so/api/fonts); the system stack stands in.`
|
|
2181
|
+
);
|
|
2182
|
+
return problems;
|
|
2183
|
+
}
|
|
2184
|
+
function wellFormedXml(svg, parseXml) {
|
|
2185
|
+
const doc = parseXml(svg);
|
|
2186
|
+
const errors = doc.getElementsByTagName("parsererror");
|
|
2187
|
+
if (!errors.length) return { ok: true };
|
|
2188
|
+
const text = (errors[0].textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2189
|
+
return { ok: false, error: text || "The markup is not well-formed XML." };
|
|
2190
|
+
}
|
|
2191
|
+
function fontFaceCss(faces) {
|
|
2192
|
+
let out = "";
|
|
2193
|
+
for (const f of faces)
|
|
2194
|
+
out += `@font-face{font-family:'${f.family}';src:url(${f.dataUri}) format('woff2');font-weight:${f.weight};font-style:${f.style};font-display:block}`;
|
|
2195
|
+
return out;
|
|
2196
|
+
}
|
|
2197
|
+
function htmlLayerSvg(src, faces = []) {
|
|
2198
|
+
const bleed = Math.max(0, Math.round(src.bleed ?? 0));
|
|
2199
|
+
const boxW = src.box.width + bleed * 2;
|
|
2200
|
+
const boxH = src.box.height + bleed * 2;
|
|
2201
|
+
const wrapper = `.vos-html-layer{width:${boxW}px;height:${boxH}px;box-sizing:border-box;margin:0;padding:${bleed}px;font-family:-apple-system,system-ui,sans-serif;-webkit-font-smoothing:antialiased;text-rendering:geometricPrecision}`;
|
|
2202
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${boxW}" height="${boxH}" viewBox="0 0 ${boxW} ${boxH}"><foreignObject x="0" y="0" width="${boxW}" height="${boxH}"><div xmlns="http://www.w3.org/1999/xhtml" class="vos-html-layer"><style>${fontFaceCss(faces)}${wrapper}${src.css ?? ""}</style>` + src.html + `</div></foreignObject></svg>`;
|
|
2203
|
+
}
|
|
2204
|
+
function fnv1a(s) {
|
|
2205
|
+
let h = 2166136261;
|
|
2206
|
+
for (let i = 0; i < s.length; i++) {
|
|
2207
|
+
h ^= s.charCodeAt(i);
|
|
2208
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
2209
|
+
}
|
|
2210
|
+
return h.toString(36);
|
|
2211
|
+
}
|
|
2212
|
+
function htmlLayerKey(clip, faces, bleed) {
|
|
2213
|
+
const src = clip.html + "\0" + (clip.css ?? "") + "\0" + clip.box.width + "x" + clip.box.height + "\0" + bleed + "\0" + faces.map((f) => `${f.family}:${f.url}:${f.weight}:${f.style}`).join("|");
|
|
2214
|
+
return `html:${clip.id}:${fnv1a(src)}`;
|
|
2215
|
+
}
|
|
2216
|
+
function htmlLayerPayload(clip) {
|
|
2217
|
+
const faces = htmlLayerFaces(clip);
|
|
2218
|
+
const bleed = htmlLayerBleed(clip);
|
|
2219
|
+
return {
|
|
2220
|
+
key: htmlLayerKey(clip, faces, bleed),
|
|
2221
|
+
width: htmlLayerWidth(clip),
|
|
2222
|
+
html: {
|
|
2223
|
+
markup: clip.html,
|
|
2224
|
+
css: clip.css ?? "",
|
|
2225
|
+
box: { width: clip.box.width, height: clip.box.height },
|
|
2226
|
+
bleed,
|
|
2227
|
+
faces
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/overlayText.ts
|
|
1912
2233
|
var OVERLAY_BOX_PAD_X = 0.6;
|
|
1913
2234
|
var OVERLAY_BOX_PAD_Y = 0.35;
|
|
1914
2235
|
var OVERLAY_BOX_RADIUS = 0.25;
|
|
@@ -1954,12 +2275,12 @@ var TEXT_PRESETS = {
|
|
|
1954
2275
|
var OVERLAY_SIZE_MIN = 12;
|
|
1955
2276
|
var OVERLAY_SIZE_MAX = 200;
|
|
1956
2277
|
var OVERLAY_FONT_FACES = [
|
|
1957
|
-
{ family: "Lexend", weight: 400, url:
|
|
1958
|
-
{ family: "Lexend", weight: 600, url:
|
|
2278
|
+
{ family: "Lexend", weight: 400, url: fontFaceUrl2("lexend", 400) },
|
|
2279
|
+
{ family: "Lexend", weight: 600, url: fontFaceUrl2("lexend", 600) },
|
|
1959
2280
|
{
|
|
1960
2281
|
family: "JetBrains Mono",
|
|
1961
2282
|
weight: 400,
|
|
1962
|
-
url:
|
|
2283
|
+
url: fontFaceUrl2("jetbrains-mono", 400)
|
|
1963
2284
|
}
|
|
1964
2285
|
];
|
|
1965
2286
|
function resolveOverlayStyle(clip) {
|
|
@@ -1967,14 +2288,14 @@ function resolveOverlayStyle(clip) {
|
|
|
1967
2288
|
const base = TEXT_PRESETS[presetName];
|
|
1968
2289
|
let stack = base.stack;
|
|
1969
2290
|
let weight = base.weight;
|
|
1970
|
-
const familyEntry =
|
|
2291
|
+
const familyEntry = findFontFamily2(clip.family ?? PRESET_FAMILY[presetName]);
|
|
1971
2292
|
if (clip.family) {
|
|
1972
2293
|
const quoted = clip.family.includes(" ") ? `'${clip.family}'` : clip.family;
|
|
1973
2294
|
stack = familyEntry ? fontStack(familyEntry) : `${quoted}, ${base.stack}`;
|
|
1974
2295
|
}
|
|
1975
2296
|
if (clip.weight !== void 0 || clip.family) {
|
|
1976
2297
|
const wanted = clip.weight ?? base.weight;
|
|
1977
|
-
weight = familyEntry ?
|
|
2298
|
+
weight = familyEntry ? nearestFontWeight2(familyEntry, wanted) : wanted;
|
|
1978
2299
|
}
|
|
1979
2300
|
return {
|
|
1980
2301
|
...base,
|
|
@@ -1996,7 +2317,7 @@ function resolveOverlayStyle(clip) {
|
|
|
1996
2317
|
};
|
|
1997
2318
|
}
|
|
1998
2319
|
function overlayFaceFor(clip) {
|
|
1999
|
-
const entry =
|
|
2320
|
+
const entry = findFontFamily2(
|
|
2000
2321
|
clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
|
|
2001
2322
|
);
|
|
2002
2323
|
if (!entry) return null;
|
|
@@ -2008,7 +2329,7 @@ function overlayFaceFor(clip) {
|
|
|
2008
2329
|
return {
|
|
2009
2330
|
family: entry.family,
|
|
2010
2331
|
weight,
|
|
2011
|
-
url:
|
|
2332
|
+
url: fontFaceUrl2(entry.slug, weight)
|
|
2012
2333
|
};
|
|
2013
2334
|
}
|
|
2014
2335
|
function overlayFontFaces(doc) {
|
|
@@ -2116,6 +2437,11 @@ function overlayRect(clip, measure, frameW, frameH = 1080, mediaAspect) {
|
|
|
2116
2437
|
cy: clip.transform.y * frameH,
|
|
2117
2438
|
rotation: clip.transform.rotation || 0
|
|
2118
2439
|
};
|
|
2440
|
+
if (clip.kind === "html") {
|
|
2441
|
+
const pb = htmlLayerPictureBox(clip);
|
|
2442
|
+
const w2 = htmlLayerWidth(clip) * frameW * scale;
|
|
2443
|
+
return { ...base, w: w2, h: w2 * (pb.height / pb.width) };
|
|
2444
|
+
}
|
|
2119
2445
|
if (clip.kind !== "text") {
|
|
2120
2446
|
const w2 = (clip.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH) * frameW * scale;
|
|
2121
2447
|
return { ...base, w: w2, h: w2 / (mediaAspect || 16 / 9) };
|
|
@@ -2523,6 +2849,19 @@ function studioEntry(data) {
|
|
|
2523
2849
|
onFrame: STUDIO_FRAME
|
|
2524
2850
|
};
|
|
2525
2851
|
}
|
|
2852
|
+
var HTML_LAYER_SVG_CODE = `(src, faces) => {
|
|
2853
|
+
const bleed = Math.max(0, Math.round(src.bleed || 0))
|
|
2854
|
+
const boxW = src.box.width + bleed * 2
|
|
2855
|
+
const boxH = src.box.height + bleed * 2
|
|
2856
|
+
let fontCss = ''
|
|
2857
|
+
for (const f of faces || []) fontCss += "@font-face{font-family:'" + f.family + "';src:url(" + f.dataUri + ") format('woff2');font-weight:" + f.weight + ";font-style:" + f.style + ";font-display:block}"
|
|
2858
|
+
const wrapper = '.vos-html-layer{width:' + boxW + 'px;height:' + boxH + 'px;box-sizing:border-box;margin:0;padding:' + bleed + 'px;font-family:-apple-system,system-ui,sans-serif;-webkit-font-smoothing:antialiased;text-rendering:geometricPrecision}'
|
|
2859
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + boxW + '" height="' + boxH + '" viewBox="0 0 ' + boxW + ' ' + boxH + '">' +
|
|
2860
|
+
'<foreignObject x="0" y="0" width="' + boxW + '" height="' + boxH + '">' +
|
|
2861
|
+
'<div xmlns="http://www.w3.org/1999/xhtml" class="vos-html-layer">' +
|
|
2862
|
+
'<style>' + fontCss + wrapper + (src.css || '') + '</style>' + src.html +
|
|
2863
|
+
'</div></foreignObject></svg>'
|
|
2864
|
+
}`;
|
|
2526
2865
|
var STUDIO_SETUP = `async (ctx) => {
|
|
2527
2866
|
if (!globalThis.__vosTimeline) { ${timelineRuntimeCode} }
|
|
2528
2867
|
const ns = (window.__vos__ = window.__vos__ || {})
|
|
@@ -2589,9 +2928,105 @@ var STUDIO_SETUP = `async (ctx) => {
|
|
|
2589
2928
|
])
|
|
2590
2929
|
} catch (e) { console.warn('[voila] overlay fonts failed to load', e) }
|
|
2591
2930
|
}
|
|
2931
|
+
// An HTML layer arrives as SOURCE, and the picture is built HERE.
|
|
2932
|
+
//
|
|
2933
|
+
// Two rules force this shape. An SVG carrying a <foreignObject> is
|
|
2934
|
+
// origin-clean ONLY as a data: URI (served from a URL it taints the canvas
|
|
2935
|
+
// even same-origin, and a tainted frame cannot be uploaded as a WebGL
|
|
2936
|
+
// texture, so every layer vanishes at once), and an SVG loaded through
|
|
2937
|
+
// <img> may fetch nothing of its own, so a @font-face naming a URL is
|
|
2938
|
+
// silently ignored. So the faces are fetched here by ordinary fetch,
|
|
2939
|
+
// inlined, and what lands in the cache is an inline, vector picture under
|
|
2940
|
+
// the clip's content-addressed key.
|
|
2941
|
+
//
|
|
2942
|
+
// Per-clip state on the namespace: htmlWant (the key a clip wants NOW, so a
|
|
2943
|
+
// build a keystroke made stale is dropped before it decodes), htmlLast (the
|
|
2944
|
+
// last picture built for a clip, drawn while the next rasterizes so an edit
|
|
2945
|
+
// never blinks the layer out), htmlKeys (the key in the cache for a clip,
|
|
2946
|
+
// evicted when a new one lands), htmlErrors (why a clip did not decode).
|
|
2947
|
+
// faceData caches a face's bytes by URL across every layer on the page.
|
|
2948
|
+
const htmlSvg = ${HTML_LAYER_SVG_CODE}
|
|
2949
|
+
ns.htmlWant = ns.htmlWant || {}
|
|
2950
|
+
ns.htmlLast = ns.htmlLast || {}
|
|
2951
|
+
ns.htmlKeys = ns.htmlKeys || {}
|
|
2952
|
+
ns.htmlErrors = ns.htmlErrors || {}
|
|
2953
|
+
const faceData = ns.faceData || (ns.faceData = new Map())
|
|
2954
|
+
const faceOf = (f) => {
|
|
2955
|
+
let p = faceData.get(f.url)
|
|
2956
|
+
if (p) return p
|
|
2957
|
+
p = fetch(f.url).then(async (r) => {
|
|
2958
|
+
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
2959
|
+
const buf = new Uint8Array(await r.arrayBuffer())
|
|
2960
|
+
// Chunked: String.fromCharCode.apply throws on a large spread.
|
|
2961
|
+
let str = ''
|
|
2962
|
+
for (let i = 0; i < buf.length; i += 0x8000) str += String.fromCharCode.apply(null, buf.subarray(i, i + 0x8000))
|
|
2963
|
+
return 'data:font/woff2;base64,' + btoa(str)
|
|
2964
|
+
}).catch((e) => {
|
|
2965
|
+
// Fail open per face, and NOT sticky: the layer paints in the fallback
|
|
2966
|
+
// stack now and the next build asks again.
|
|
2967
|
+
faceData.delete(f.url)
|
|
2968
|
+
console.warn('[voila] html layer face failed to load', f.url, e)
|
|
2969
|
+
return ''
|
|
2970
|
+
})
|
|
2971
|
+
faceData.set(f.url, p)
|
|
2972
|
+
return p
|
|
2973
|
+
}
|
|
2974
|
+
const buildHtmlLayer = async (oc) => {
|
|
2975
|
+
const want = () => ns.htmlWant[oc.id] === oc.key
|
|
2976
|
+
ns.htmlWant[oc.id] = oc.key
|
|
2977
|
+
const h = oc.html
|
|
2978
|
+
const uris = await Promise.all((h.faces || []).map(faceOf))
|
|
2979
|
+
if (!want()) return
|
|
2980
|
+
const faces = []
|
|
2981
|
+
for (let i = 0; i < uris.length; i++) {
|
|
2982
|
+
if (uris[i]) faces.push({ family: h.faces[i].family, weight: h.faces[i].weight, style: h.faces[i].style, dataUri: uris[i] })
|
|
2983
|
+
}
|
|
2984
|
+
const svg = htmlSvg({ html: h.markup, css: h.css, box: h.box, bleed: h.bleed }, faces)
|
|
2985
|
+
const uri = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)))
|
|
2986
|
+
const img = await new Promise((res, rej) => {
|
|
2987
|
+
const i = new Image()
|
|
2988
|
+
i.onload = () => res(i)
|
|
2989
|
+
// foreignObject is XML: malformed markup fails to DECODE, silently, and
|
|
2990
|
+
// the layer would simply never appear. Say so instead.
|
|
2991
|
+
i.onerror = () => rej(new Error('did not decode: is the markup well-formed XML?'))
|
|
2992
|
+
i.src = uri
|
|
2993
|
+
})
|
|
2994
|
+
if (!want()) return
|
|
2995
|
+
// A browser that taints a canvas drawing a foreignObject SVG (WebKit's
|
|
2996
|
+
// history) would lose EVERY layer to one SecurityError at the texture
|
|
2997
|
+
// upload. Probe once on a scratch canvas and, failing, drop the HTML
|
|
2998
|
+
// layers and nothing else.
|
|
2999
|
+
if (ns.htmlSafe === undefined) {
|
|
3000
|
+
try {
|
|
3001
|
+
const pc = document.createElement('canvas')
|
|
3002
|
+
pc.width = pc.height = 1
|
|
3003
|
+
const px = pc.getContext('2d')
|
|
3004
|
+
px.drawImage(img, 0, 0, 1, 1)
|
|
3005
|
+
px.getImageData(0, 0, 1, 1)
|
|
3006
|
+
ns.htmlSafe = true
|
|
3007
|
+
} catch (e) {
|
|
3008
|
+
ns.htmlSafe = false
|
|
3009
|
+
console.error('[voila] html layers are off: this browser taints a canvas that draws a foreignObject SVG', e)
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
const prev = ns.htmlKeys[oc.id]
|
|
3013
|
+
if (prev && prev !== oc.key) cache.delete(prev)
|
|
3014
|
+
cache.set(oc.key, img)
|
|
3015
|
+
ns.htmlKeys[oc.id] = oc.key
|
|
3016
|
+
ns.htmlLast[oc.id] = img
|
|
3017
|
+
delete ns.htmlErrors[oc.id]
|
|
3018
|
+
}
|
|
3019
|
+
// ON_FRAME is its own function string, so the builder is handed over for a
|
|
3020
|
+
// layer that arrives by live edit after setup has run. A failure is
|
|
3021
|
+
// recorded on the clip and said at the level a render page forwards.
|
|
3022
|
+
ns.buildHtmlLayer = (oc) => buildHtmlLayer(oc).catch((e) => {
|
|
3023
|
+
ns.htmlErrors[oc.id] = String((e && e.message) || e)
|
|
3024
|
+
console.error('[voila] html layer ' + oc.id + ' ' + ns.htmlErrors[oc.id])
|
|
3025
|
+
})
|
|
2592
3026
|
// Media overlays (V1b): warm-load through the shared cache so the first
|
|
2593
3027
|
// frame draws complete. Fail-open per clip (a bad key just doesn't draw).
|
|
2594
3028
|
for (const oc of (ctx.data.overlays || [])) {
|
|
3029
|
+
if (oc.html) { await ns.buildHtmlLayer(oc); continue }
|
|
2595
3030
|
if (oc.kind !== 'image' && oc.kind !== 'video') continue
|
|
2596
3031
|
try {
|
|
2597
3032
|
if (oc.kind === 'image') await loadImage(oc.key)
|
|
@@ -2862,6 +3297,8 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
2862
3297
|
for (var oi = 0; oi < ols.length; oi++) {
|
|
2863
3298
|
var ol0 = ols[oi]
|
|
2864
3299
|
if (t < ol0.start || t > ol0.start + ol0.dur) continue
|
|
3300
|
+
// A page that taints on foreignObject draws no HTML layer (setup's probe).
|
|
3301
|
+
if (ol0.html && ns.htmlSafe === false) continue
|
|
2865
3302
|
olVisSig += ol0.id + ':' + (ol0.text || ol0.key) + ':' + ol0.x + ',' + ol0.y + ',' + ol0.scale + ',' + ol0.rot + ',' + (ol0.fs || ol0.w) + ',' + (ol0.color || ol0.radius) + ',' + (ol0.opacity == null ? 1 : ol0.opacity) + (ol0.fx ? ',' + ol0.fx.k + ol0.fx.u + ol0.fx.d + ol0.fx.st : '') + (ol0.mw ? ',w' + ol0.mw : '') + ';'
|
|
2866
3303
|
// fx widens the entrance window to the whole staggered span (tt);
|
|
2867
3304
|
// without fx it is the legacy enter transition window.
|
|
@@ -2873,6 +3310,10 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
2873
3310
|
if (ol0.track) olAnim = true
|
|
2874
3311
|
// A layer CARD is painted on its own plane every frame it is visible.
|
|
2875
3312
|
if (ol0.card) olAnim = true
|
|
3313
|
+
// An HTML layer whose source just changed is rasterizing: keep repainting
|
|
3314
|
+
// until its picture lands, or the edit would not appear until something
|
|
3315
|
+
// else happened to dirty the canvas.
|
|
3316
|
+
if (ol0.html && !(ns.videoCache && ns.videoCache.get(ol0.key))) olAnim = true
|
|
2876
3317
|
// Video overlays advance every frame; images redraw until decoded.
|
|
2877
3318
|
if (ol0.kind === 'video') olAnim = true
|
|
2878
3319
|
else if (ol0.kind === 'image') {
|
|
@@ -2880,6 +3321,20 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
2880
3321
|
if (!(olEl0 && olEl0.complete && olEl0.naturalWidth)) olAnim = true
|
|
2881
3322
|
}
|
|
2882
3323
|
}
|
|
3324
|
+
// A clip that left the document takes its picture with it: the cache
|
|
3325
|
+
// holds decoded images, and a session's edits would otherwise pile up.
|
|
3326
|
+
if (ns.htmlLast) {
|
|
3327
|
+
for (var hlK in ns.htmlLast) {
|
|
3328
|
+
var hlSeen = false
|
|
3329
|
+
for (var hlI = 0; hlI < ols.length; hlI++) if (ols[hlI].id === hlK) { hlSeen = true; break }
|
|
3330
|
+
if (hlSeen) continue
|
|
3331
|
+
if (ns.htmlKeys && ns.htmlKeys[hlK] && ns.videoCache) ns.videoCache.delete(ns.htmlKeys[hlK])
|
|
3332
|
+
delete ns.htmlLast[hlK]
|
|
3333
|
+
if (ns.htmlKeys) delete ns.htmlKeys[hlK]
|
|
3334
|
+
if (ns.htmlWant) delete ns.htmlWant[hlK]
|
|
3335
|
+
if (ns.htmlErrors) delete ns.htmlErrors[hlK]
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
2883
3338
|
var ovSig = W + 'x' + H + '|' + olVisSig
|
|
2884
3339
|
var ovDirty = ov.sig !== ovSig || ov.active || olAnim
|
|
2885
3340
|
// The layer cards visible this frame, with their pose, for the card
|
|
@@ -2891,6 +3346,18 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
2891
3346
|
// picture and the layer card.
|
|
2892
3347
|
function olAcquire(ol, olT) {
|
|
2893
3348
|
var olEl = ns.videoCache ? ns.videoCache.get(ol.key) : null
|
|
3349
|
+
// An HTML layer's key is a CACHE HANDLE, not a URL: never hand it to an
|
|
3350
|
+
// <img>. A miss means setup has not built it (a live edit), so ask once
|
|
3351
|
+
// per key and let a later frame pick it up; a build that failed stays
|
|
3352
|
+
// asked, and the next source edit is a new key.
|
|
3353
|
+
if (ol.html) {
|
|
3354
|
+
if (!olEl && ns.buildHtmlLayer) {
|
|
3355
|
+
var hlB = ns.htmlBuilding || (ns.htmlBuilding = {})
|
|
3356
|
+
if (!hlB[ol.key]) { hlB[ol.key] = 1; ns.buildHtmlLayer(ol) }
|
|
3357
|
+
}
|
|
3358
|
+
// While the edited source rasterizes, keep the clip's previous picture.
|
|
3359
|
+
return olEl || (ns.htmlLast && ns.htmlLast[ol.id]) || null
|
|
3360
|
+
}
|
|
2894
3361
|
if (!olEl && ns.videoCache) {
|
|
2895
3362
|
if (ol.kind === 'image') {
|
|
2896
3363
|
olEl = new Image()
|
|
@@ -2967,6 +3434,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
2967
3434
|
var ol = ols[oj]
|
|
2968
3435
|
var olT = t - ol.start
|
|
2969
3436
|
if (olT < 0 || olT > ol.dur) continue
|
|
3437
|
+
if (ol.html && ns.htmlSafe === false) continue
|
|
2970
3438
|
var olA = 1, olYof = 0, olScl = 1, olBlur = 0
|
|
2971
3439
|
if (ol.fx && ol.fx.u === 'block') {
|
|
2972
3440
|
// fx owns the entrance; block unit = the legacy presets generalized
|
|
@@ -5793,6 +6261,19 @@ function studioLayerData(layers, duration) {
|
|
|
5793
6261
|
return track.keyframes.length ? { track } : {};
|
|
5794
6262
|
})()
|
|
5795
6263
|
};
|
|
6264
|
+
if (o.kind === "html") {
|
|
6265
|
+
const hl = htmlLayerPayload(o);
|
|
6266
|
+
return {
|
|
6267
|
+
...base,
|
|
6268
|
+
kind: "image",
|
|
6269
|
+
key: hl.key,
|
|
6270
|
+
w: round6(hl.width),
|
|
6271
|
+
radius: 0,
|
|
6272
|
+
shadow: "none",
|
|
6273
|
+
opacity: round6(o.opacity ?? 1),
|
|
6274
|
+
html: hl.html
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
5796
6277
|
if (o.kind !== "text") {
|
|
5797
6278
|
const lm = rec ? layerMedia(rec, o.key) : null;
|
|
5798
6279
|
const lmImage = lm ? lm.sourceKind === "image" : o.kind === "image";
|
|
@@ -6704,9 +7185,10 @@ function copyLayout(from, to, opts = {}) {
|
|
|
6704
7185
|
next.duration = Math.max(0.3, Math.round(outDur * 1e3) / 1e3);
|
|
6705
7186
|
if (next.kind === "text") {
|
|
6706
7187
|
if (mine?.kind === "text") next.text = mine.text;
|
|
7188
|
+
} else if (next.kind === "html") {
|
|
6707
7189
|
} else if (opts.keys?.[clip.id]) {
|
|
6708
7190
|
next.key = opts.keys[clip.id];
|
|
6709
|
-
} else if (mine && mine.kind !== "text") {
|
|
7191
|
+
} else if (mine && mine.kind !== "text" && mine.kind !== "html") {
|
|
6710
7192
|
next.key = mine.key;
|
|
6711
7193
|
} else {
|
|
6712
7194
|
skippedMarks.push(clip.id);
|
|
@@ -6767,7 +7249,7 @@ function copyLayout(from, to, opts = {}) {
|
|
|
6767
7249
|
return { doc, notes };
|
|
6768
7250
|
}
|
|
6769
7251
|
function trailingFreeze(doc) {
|
|
6770
|
-
const last = doc.segments
|
|
7252
|
+
const last = doc.segments?.at(-1);
|
|
6771
7253
|
if (!last) return null;
|
|
6772
7254
|
return docFreezes(doc).find((f) => Math.abs(f.at - last.out) < 1e-9) ?? null;
|
|
6773
7255
|
}
|
|
@@ -6833,9 +7315,10 @@ function applyTemplate(template, take, opts) {
|
|
|
6833
7315
|
const word = opts.words?.[clip.id];
|
|
6834
7316
|
if (word !== void 0) next.text = word;
|
|
6835
7317
|
else if (mine?.kind === "text") next.text = mine.text;
|
|
7318
|
+
} else if (next.kind === "html") {
|
|
6836
7319
|
} else if (opts.keys?.[clip.id]) {
|
|
6837
7320
|
next.key = opts.keys[clip.id];
|
|
6838
|
-
} else if (mine && mine.kind !== "text") {
|
|
7321
|
+
} else if (mine && mine.kind !== "text" && mine.kind !== "html") {
|
|
6839
7322
|
next.key = mine.key;
|
|
6840
7323
|
} else if (!/^(https?:|\/\/)/.test(next.key)) {
|
|
6841
7324
|
skipped.push(clip.id);
|
|
@@ -6860,16 +7343,16 @@ function applyTemplate(template, take, opts) {
|
|
|
6860
7343
|
}
|
|
6861
7344
|
if (objects.length || doc.objects) doc.objects = objects;
|
|
6862
7345
|
const audio = doc.audio.filter(
|
|
6863
|
-
(a) => !template.audio.some((t) => t.id === a.id)
|
|
7346
|
+
(a) => !(template.audio ?? []).some((t) => t.id === a.id)
|
|
6864
7347
|
);
|
|
6865
|
-
for (const clip of template.audio) {
|
|
7348
|
+
for (const clip of template.audio ?? []) {
|
|
6866
7349
|
const next = structuredClone(clip);
|
|
6867
7350
|
next.start = Math.max(0, round34(clip.start + shift));
|
|
6868
7351
|
next.from = from;
|
|
6869
7352
|
audio.push(next);
|
|
6870
7353
|
}
|
|
6871
7354
|
doc.audio = audio;
|
|
6872
|
-
if (template.frame
|
|
7355
|
+
if (template.frame?.anim) {
|
|
6873
7356
|
doc.frame = {
|
|
6874
7357
|
...doc.frame,
|
|
6875
7358
|
anim: { ...doc.frame.anim ?? {}, ...template.frame.anim }
|
|
@@ -7581,6 +8064,60 @@ function destinationsForChannel(channel) {
|
|
|
7581
8064
|
return DESTINATIONS.filter((d) => d.channel === channel);
|
|
7582
8065
|
}
|
|
7583
8066
|
|
|
8067
|
+
// src/htmlStarters.ts
|
|
8068
|
+
var WORDS = "font-family:'Inter',-apple-system,system-ui,sans-serif;color:#fafafa;-webkit-font-smoothing:antialiased;";
|
|
8069
|
+
var MONO = "font-family:'JetBrains Mono',ui-monospace,monospace;";
|
|
8070
|
+
var CARD = "background:#0b0b0d;border:1px solid rgba(255,255,255,.08);border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.45);";
|
|
8071
|
+
var HTML_STARTERS = [
|
|
8072
|
+
{
|
|
8073
|
+
id: "callout",
|
|
8074
|
+
name: "Callout",
|
|
8075
|
+
box: { width: 420, height: 132 },
|
|
8076
|
+
html: '<div class="callout"><div class="head"><span class="dot"></span><span class="title">Title</span></div><div class="body">One line that says what this is.</div></div>',
|
|
8077
|
+
css: `.callout{${WORDS}${CARD}box-sizing:border-box;width:420px;height:132px;padding:22px 24px}.head{display:flex;align-items:center;gap:10px}.dot{width:8px;height:8px;border-radius:99px;background:#4ade80;box-shadow:0 0 10px rgba(74,222,128,.9)}.title{font-size:20px;font-weight:600;letter-spacing:-0.01em}.body{margin-top:12px;font-size:15px;line-height:1.5;color:#8b8b94}`
|
|
8078
|
+
},
|
|
8079
|
+
{
|
|
8080
|
+
id: "code",
|
|
8081
|
+
name: "Code",
|
|
8082
|
+
box: { width: 460, height: 168 },
|
|
8083
|
+
html: '<div class="code"><span class="mut">// button.tsx</span>\n<span class="kw">export function</span> <span class="fn">Button</span>({ variant }) {\n <span class="kw">return</span> <span class="tag"><button</span> className={<span class="fn">cn</span>(variant)} <span class="tag">/></span>\n}</div>',
|
|
8084
|
+
css: `.code{${MONO}${CARD}color:#e8e8ec;box-sizing:border-box;width:460px;height:168px;padding:22px 24px;font-size:14px;line-height:1.8;white-space:pre;letter-spacing:-0.01em}.kw{color:#c4b5fd}.fn{color:#7dd3fc}.tag{color:#f0abfc}.mut{color:#5b5b66}`
|
|
8085
|
+
},
|
|
8086
|
+
{
|
|
8087
|
+
id: "terminal",
|
|
8088
|
+
name: "Terminal",
|
|
8089
|
+
box: { width: 460, height: 152 },
|
|
8090
|
+
html: '<div class="term"><span class="prompt">$</span> <span class="cmd">npx vos record https://your.app</span>\n<span class="mut">recording 1920x1080 at 30 fps</span>\n<span class="mut">saved take/</span></div>',
|
|
8091
|
+
css: `.term{${MONO}${CARD}background:#08080a;color:#e8e8ec;box-sizing:border-box;width:460px;height:152px;padding:22px 24px;font-size:14px;line-height:1.8;white-space:pre}.prompt{color:#4ade80}.mut{color:#5b5b66}`
|
|
8092
|
+
},
|
|
8093
|
+
{
|
|
8094
|
+
id: "keys",
|
|
8095
|
+
name: "Keys",
|
|
8096
|
+
box: { width: 220, height: 64 },
|
|
8097
|
+
html: '<div class="keys"><kbd>⌘</kbd><span class="plus">+</span><kbd>K</kbd></div>',
|
|
8098
|
+
css: `.keys{${WORDS}display:flex;align-items:center;justify-content:center;gap:10px;width:220px;height:64px}kbd{display:inline-flex;align-items:center;justify-content:center;min-width:48px;height:48px;padding:0 14px;border-radius:10px;background:#1a1a1f;border:1px solid rgba(255,255,255,.14);border-bottom-width:3px;box-shadow:0 10px 24px rgba(0,0,0,.4);font-size:22px;font-weight:600}.plus{color:#8b8b94;font-size:18px}`
|
|
8099
|
+
},
|
|
8100
|
+
{
|
|
8101
|
+
id: "badge",
|
|
8102
|
+
name: "Badge",
|
|
8103
|
+
box: { width: 200, height: 52 },
|
|
8104
|
+
html: '<div class="badge"><span class="dot"></span>New in 2.0</div>',
|
|
8105
|
+
css: `.badge{${WORDS}display:inline-flex;align-items:center;gap:10px;height:44px;padding:0 20px;border-radius:99px;background:#1a1a1f;border:1px solid rgba(255,255,255,.12);box-shadow:0 12px 30px rgba(0,0,0,.4);font-size:16px;font-weight:600;letter-spacing:-0.01em}.dot{width:8px;height:8px;border-radius:99px;background:#3b82f6;box-shadow:0 0 10px rgba(59,130,246,.9)}`
|
|
8106
|
+
}
|
|
8107
|
+
];
|
|
8108
|
+
function htmlStarterClip(starter, id, start, duration) {
|
|
8109
|
+
return {
|
|
8110
|
+
id,
|
|
8111
|
+
kind: "html",
|
|
8112
|
+
html: starter.html,
|
|
8113
|
+
css: starter.css,
|
|
8114
|
+
box: { ...starter.box },
|
|
8115
|
+
start,
|
|
8116
|
+
duration,
|
|
8117
|
+
transform: { x: 0.5, y: 0.5, scale: 1, rotation: 0 }
|
|
8118
|
+
};
|
|
8119
|
+
}
|
|
8120
|
+
|
|
7584
8121
|
// src/lower/audioPlan.ts
|
|
7585
8122
|
var AUDIO_PLAN_STEP = 1 / 240;
|
|
7586
8123
|
function envelopeAt(env, t, def) {
|
|
@@ -8738,7 +9275,7 @@ var overlaysLane = {
|
|
|
8738
9275
|
kind: "clip",
|
|
8739
9276
|
t: round11(o.start),
|
|
8740
9277
|
duration: round11(o.duration),
|
|
8741
|
-
label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "image" ? "Image" : "Video"
|
|
9278
|
+
label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "html" ? htmlLayerLabel(o) : o.kind === "image" ? "Image" : "Video"
|
|
8742
9279
|
},
|
|
8743
9280
|
// Pose diamonds render after the clips, so they sit on top.
|
|
8744
9281
|
...poseItems(o.id, o.start, o.duration, o.motion)
|
|
@@ -9134,6 +9671,7 @@ export {
|
|
|
9134
9671
|
DEFAULT_TILT_POSE,
|
|
9135
9672
|
DEFAULT_ZOOM_LEVEL,
|
|
9136
9673
|
DEFAULT_ZOOM_STYLE,
|
|
9674
|
+
DESIGN_FRAME_WIDTH,
|
|
9137
9675
|
DESTINATIONS,
|
|
9138
9676
|
DIGEST_CROP_MAX,
|
|
9139
9677
|
DIGEST_FULL_MAX,
|
|
@@ -9155,6 +9693,11 @@ export {
|
|
|
9155
9693
|
FREEZE_SECONDS_MAX,
|
|
9156
9694
|
FREEZE_SECONDS_MIN,
|
|
9157
9695
|
HOUSE_GRADIENT,
|
|
9696
|
+
HTML_LAYER_FOREIGN_FIELDS,
|
|
9697
|
+
HTML_LAYER_MAX_BYTES,
|
|
9698
|
+
HTML_LAYER_SVG_CODE,
|
|
9699
|
+
HTML_LAYER_WARN_BYTES,
|
|
9700
|
+
HTML_STARTERS,
|
|
9158
9701
|
IDLE_KINDS,
|
|
9159
9702
|
LAYER_CARD_SHADOW,
|
|
9160
9703
|
LAYOUT_FRAME_FIELDS,
|
|
@@ -9268,6 +9811,8 @@ export {
|
|
|
9268
9811
|
copyLayout,
|
|
9269
9812
|
copyStyle,
|
|
9270
9813
|
cropBox,
|
|
9814
|
+
cssFamilies,
|
|
9815
|
+
cssWeights,
|
|
9271
9816
|
cursorIdleFade,
|
|
9272
9817
|
deriveViewportCrop,
|
|
9273
9818
|
destinationById,
|
|
@@ -9304,8 +9849,22 @@ export {
|
|
|
9304
9849
|
freezeOutputExtents,
|
|
9305
9850
|
hexToRgbTriplet,
|
|
9306
9851
|
houseLook,
|
|
9852
|
+
htmlLayerBleed,
|
|
9853
|
+
htmlLayerBleedFor,
|
|
9854
|
+
htmlLayerFaces,
|
|
9855
|
+
htmlLayerKey,
|
|
9856
|
+
htmlLayerLabel,
|
|
9857
|
+
htmlLayerPayload,
|
|
9858
|
+
htmlLayerPictureBox,
|
|
9859
|
+
htmlLayerProblems,
|
|
9860
|
+
htmlLayerSvg,
|
|
9861
|
+
htmlLayerText,
|
|
9862
|
+
htmlLayerUnhostedFamilies,
|
|
9863
|
+
htmlLayerWidth,
|
|
9864
|
+
htmlStarterClip,
|
|
9307
9865
|
idleGaps,
|
|
9308
9866
|
idleKey,
|
|
9867
|
+
isKeyedOverlay,
|
|
9309
9868
|
isLookKind,
|
|
9310
9869
|
isMusicBed,
|
|
9311
9870
|
isPlayback,
|
|
@@ -9401,6 +9960,7 @@ export {
|
|
|
9401
9960
|
transitionSeconds,
|
|
9402
9961
|
videoLane,
|
|
9403
9962
|
voiceKey,
|
|
9963
|
+
wellFormedXml,
|
|
9404
9964
|
withBackdrop,
|
|
9405
9965
|
withFreezes,
|
|
9406
9966
|
withHolds,
|