@vosjs/studio-core 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +275 -42
- package/dist/index.js +2352 -1760
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1817,1777 +1817,2176 @@ function outputEnd(doc, footageEnd) {
|
|
|
1817
1817
|
return Math.max(footageEnd, lastLayerEnd(doc));
|
|
1818
1818
|
}
|
|
1819
1819
|
|
|
1820
|
-
// src/
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1820
|
+
// src/overlayText.ts
|
|
1821
|
+
import {
|
|
1822
|
+
findFontFamily as findFontFamily2,
|
|
1823
|
+
fontFaceUrl as fontFaceUrl2,
|
|
1824
|
+
fontStack,
|
|
1825
|
+
nearestFontWeight as nearestFontWeight2
|
|
1826
|
+
} from "@vosjs/shared";
|
|
1827
|
+
|
|
1828
|
+
// src/htmlLayer.ts
|
|
1829
|
+
import { findFontFamily, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
1830
|
+
var DESIGN_FRAME_WIDTH = 1920;
|
|
1831
|
+
var HTML_LAYER_WARN_BYTES = 32e3;
|
|
1832
|
+
var HTML_LAYER_MAX_BYTES = 128e3;
|
|
1833
|
+
var HTML_LAYER_LIVE_HZ = 60;
|
|
1834
|
+
var HTML_LAYER_ASSET_HOST = "https://assets.vos.so/";
|
|
1835
|
+
function splitTopLevel(value) {
|
|
1836
|
+
const out = [];
|
|
1837
|
+
let depth = 0;
|
|
1838
|
+
let cur = "";
|
|
1839
|
+
for (const ch of value) {
|
|
1840
|
+
if (ch === "(") depth++;
|
|
1841
|
+
else if (ch === ")") depth--;
|
|
1842
|
+
if (ch === "," && depth === 0) {
|
|
1843
|
+
out.push(cur);
|
|
1844
|
+
cur = "";
|
|
1845
|
+
} else cur += ch;
|
|
1846
|
+
}
|
|
1847
|
+
if (cur.trim()) out.push(cur);
|
|
1848
|
+
return out;
|
|
1829
1849
|
}
|
|
1830
|
-
var
|
|
1831
|
-
var
|
|
1832
|
-
|
|
1833
|
-
|
|
1850
|
+
var unquote = (s) => s.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim();
|
|
1851
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
1852
|
+
"serif",
|
|
1853
|
+
"sans-serif",
|
|
1854
|
+
"monospace",
|
|
1855
|
+
"cursive",
|
|
1856
|
+
"fantasy",
|
|
1857
|
+
"system-ui",
|
|
1858
|
+
"ui-sans-serif",
|
|
1859
|
+
"ui-serif",
|
|
1860
|
+
"ui-monospace",
|
|
1861
|
+
"ui-rounded",
|
|
1862
|
+
"emoji",
|
|
1863
|
+
"math",
|
|
1864
|
+
"fangsong",
|
|
1865
|
+
"inherit",
|
|
1866
|
+
"initial",
|
|
1867
|
+
"unset"
|
|
1868
|
+
]);
|
|
1869
|
+
function cssFamilies(css) {
|
|
1870
|
+
if (!css) return [];
|
|
1871
|
+
const out = [];
|
|
1872
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1873
|
+
for (const decl of css.match(/font-family\s*:[^;}]+/gi) ?? []) {
|
|
1874
|
+
for (const item of splitTopLevel(decl.replace(/^[^:]*:/, ""))) {
|
|
1875
|
+
const name = unquote(item.replace(/!important/i, ""));
|
|
1876
|
+
if (!name) continue;
|
|
1877
|
+
const lower = name.toLowerCase();
|
|
1878
|
+
if (GENERIC_FAMILIES.has(lower) || lower.startsWith("-apple-")) continue;
|
|
1879
|
+
if (seen.has(lower)) continue;
|
|
1880
|
+
seen.add(lower);
|
|
1881
|
+
out.push(name);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return out;
|
|
1834
1885
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
maxLevel: 1.8,
|
|
1842
|
-
dragLevel: 1.5,
|
|
1843
|
-
lead: 0.35,
|
|
1844
|
-
hold: 1.2,
|
|
1845
|
-
followByDefault: true,
|
|
1846
|
-
typingZoom: true,
|
|
1847
|
-
typingGap: 2.5,
|
|
1848
|
-
typingHold: 1.1,
|
|
1849
|
-
typingMinLevel: 1.4,
|
|
1850
|
-
rampIn: 0.6,
|
|
1851
|
-
rampInOverlap: 0.2,
|
|
1852
|
-
rampOut: 0.5,
|
|
1853
|
-
chainGap: 3,
|
|
1854
|
-
pan: 0.55,
|
|
1855
|
-
// The reference's corner trace at 60 fps: a quarter of the move by a
|
|
1856
|
-
// fifth of the time, two thirds by half, nine tenths by three
|
|
1857
|
-
// quarters, over ~0.6 s. Fitted against those three points,
|
|
1858
|
-
// (0.36, 0, 0.4, 1) is the closest cubic (15 / 64 / 93 %; half the
|
|
1859
|
-
// move at ~245 ms and 90 % at ~425 ms at 0.6 s): a gentle start and a
|
|
1860
|
-
// long settle. CSS ease (0.25, 0.1, 0.25, 1) reached the midpoint
|
|
1861
|
-
// 70 ms earlier and read a touch fast; (0.16, 1, 0.3, 1) put half the
|
|
1862
|
-
// move in the first sixty milliseconds and read as a twitch.
|
|
1863
|
-
ease: "css-bezier(0.36, 0, 0.4, 1)",
|
|
1864
|
-
panEase: "css-bezier(0.36, 0, 0.4, 1)",
|
|
1865
|
-
followSafeRatio: 0.45,
|
|
1866
|
-
followRecenter: 0.55,
|
|
1867
|
-
followLookahead: 0.4,
|
|
1868
|
-
tilt: {
|
|
1869
|
-
intensity: "off",
|
|
1870
|
-
rampIn: 0.6,
|
|
1871
|
-
rampOut: 0.5,
|
|
1872
|
-
chainGap: 3,
|
|
1873
|
-
pan: 0.55
|
|
1886
|
+
function cssWeights(css, html = "") {
|
|
1887
|
+
const out = /* @__PURE__ */ new Set([400]);
|
|
1888
|
+
for (const decl of css?.match(/font-weight\s*:[^;}]+/gi) ?? []) {
|
|
1889
|
+
const value = decl.replace(/^[^:]*:/, "").toLowerCase();
|
|
1890
|
+
for (const n of value.match(/\b[1-9]00\b/g) ?? []) out.add(Number(n));
|
|
1891
|
+
if (/\bbold(er)?\b/.test(value)) out.add(700);
|
|
1874
1892
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
cinema: {
|
|
1924
|
-
autoZoom: true,
|
|
1925
|
-
clusterGap: 2.4,
|
|
1926
|
-
minClusterClicks: 1,
|
|
1927
|
-
targetFill: 0.42,
|
|
1928
|
-
minLevel: 1.3,
|
|
1929
|
-
maxLevel: 1.8,
|
|
1930
|
-
dragLevel: 1.5,
|
|
1931
|
-
lead: 0.6,
|
|
1932
|
-
hold: 2,
|
|
1933
|
-
followByDefault: true,
|
|
1934
|
-
typingZoom: true,
|
|
1935
|
-
typingGap: 3,
|
|
1936
|
-
typingHold: 1.8,
|
|
1937
|
-
typingMinLevel: 1.4,
|
|
1938
|
-
rampIn: 1.2,
|
|
1939
|
-
rampInOverlap: 0.4,
|
|
1940
|
-
rampOut: 1,
|
|
1941
|
-
chainGap: 3.5,
|
|
1942
|
-
pan: 1.1,
|
|
1943
|
-
ease: "css-bezier(0.33, 0, 0.15, 1)",
|
|
1944
|
-
panEase: "css-bezier(0.33, 0, 0.22, 1)",
|
|
1945
|
-
followSafeRatio: 0.6,
|
|
1946
|
-
followRecenter: 0.9,
|
|
1947
|
-
followLookahead: 0.5,
|
|
1948
|
-
tilt: {
|
|
1949
|
-
intensity: "off",
|
|
1950
|
-
rampIn: 1.2,
|
|
1951
|
-
rampOut: 1,
|
|
1952
|
-
chainGap: 3.5,
|
|
1953
|
-
pan: 1.1
|
|
1954
|
-
}
|
|
1955
|
-
},
|
|
1956
|
-
// The studio's original pace with the instant-velocity onset defect fixed:
|
|
1957
|
-
// 0.4 s cycles on the onset-capped ease, the 2.5× ceiling of the 2026-07
|
|
1958
|
-
// comparison's defect two brought to 2.2, a chain gap past its pump band.
|
|
1959
|
-
snappy: {
|
|
1960
|
-
autoZoom: true,
|
|
1961
|
-
clusterGap: 1.2,
|
|
1962
|
-
minClusterClicks: 1,
|
|
1963
|
-
targetFill: 0.5,
|
|
1964
|
-
minLevel: 1.4,
|
|
1965
|
-
maxLevel: 2.2,
|
|
1966
|
-
dragLevel: 1.6,
|
|
1967
|
-
lead: 0.25,
|
|
1968
|
-
hold: 1.2,
|
|
1969
|
-
followByDefault: false,
|
|
1970
|
-
typingZoom: true,
|
|
1971
|
-
typingGap: 1.6,
|
|
1972
|
-
typingHold: 0.9,
|
|
1973
|
-
typingMinLevel: 1.5,
|
|
1974
|
-
rampIn: 0.4,
|
|
1975
|
-
rampInOverlap: 0.15,
|
|
1976
|
-
rampOut: 0.4,
|
|
1977
|
-
chainGap: 2,
|
|
1978
|
-
pan: 0.5,
|
|
1979
|
-
ease: "css-bezier(0.3, 0.55, 0.2, 1)",
|
|
1980
|
-
panEase: "css-bezier(0.25, 0.1, 0.25, 1)",
|
|
1981
|
-
followSafeRatio: 0.5,
|
|
1982
|
-
followRecenter: 0.45,
|
|
1983
|
-
followLookahead: 0,
|
|
1984
|
-
tilt: {
|
|
1985
|
-
intensity: "off",
|
|
1986
|
-
rampIn: 0.4,
|
|
1987
|
-
rampOut: 0.4,
|
|
1988
|
-
chainGap: 2,
|
|
1989
|
-
pan: 0.5
|
|
1990
|
-
}
|
|
1991
|
-
},
|
|
1992
|
-
// Screen Studio's "instant zoom" — a cut-in (eight frames at 60 fps),
|
|
1993
|
-
// tutorial tempo. Untouched in motion (the one style that never pumped
|
|
1994
|
-
// and never left a ramp unsettled); the ceiling comes down with the
|
|
1995
|
-
// others and the hold gains the beat a cut-in needs to be read.
|
|
1996
|
-
cut: {
|
|
1997
|
-
autoZoom: true,
|
|
1998
|
-
clusterGap: 1.2,
|
|
1999
|
-
minClusterClicks: 1,
|
|
2000
|
-
targetFill: 0.5,
|
|
2001
|
-
minLevel: 1.4,
|
|
2002
|
-
maxLevel: 2,
|
|
2003
|
-
dragLevel: 1.6,
|
|
2004
|
-
lead: 0.2,
|
|
2005
|
-
hold: 1.2,
|
|
2006
|
-
followByDefault: false,
|
|
2007
|
-
typingZoom: true,
|
|
2008
|
-
typingGap: 1.2,
|
|
2009
|
-
typingHold: 0.6,
|
|
2010
|
-
typingMinLevel: 1.5,
|
|
2011
|
-
rampIn: 0.14,
|
|
2012
|
-
rampInOverlap: 0.07,
|
|
2013
|
-
rampOut: 0.14,
|
|
2014
|
-
chainGap: 1.3,
|
|
2015
|
-
pan: 0.35,
|
|
2016
|
-
ease: "css-bezier(0.2, 0, 0.4, 1)",
|
|
2017
|
-
panEase: "css-bezier(0.2, 0, 0.4, 1)",
|
|
2018
|
-
followSafeRatio: 0.5,
|
|
2019
|
-
followRecenter: 0.35,
|
|
2020
|
-
followLookahead: 0,
|
|
2021
|
-
tilt: {
|
|
2022
|
-
intensity: "off",
|
|
2023
|
-
rampIn: 0.3,
|
|
2024
|
-
rampOut: 0.3,
|
|
2025
|
-
chainGap: 1.3,
|
|
2026
|
-
pan: 0.35
|
|
1893
|
+
if (/<(b|strong|h[1-6])[\s>]/i.test(html)) out.add(700);
|
|
1894
|
+
return [...out].sort((a, b) => a - b);
|
|
1895
|
+
}
|
|
1896
|
+
function htmlLayerFaces(clip) {
|
|
1897
|
+
const faces = [];
|
|
1898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1899
|
+
const add = (f) => {
|
|
1900
|
+
const key = `${f.family.toLowerCase()}|${f.weight}|${f.style}`;
|
|
1901
|
+
if (seen.has(key)) return;
|
|
1902
|
+
seen.add(key);
|
|
1903
|
+
faces.push(f);
|
|
1904
|
+
};
|
|
1905
|
+
const weights = cssWeights(clip.css, clip.html);
|
|
1906
|
+
for (const family of cssFamilies(clip.css)) {
|
|
1907
|
+
const entry = findFontFamily(family);
|
|
1908
|
+
if (!entry) continue;
|
|
1909
|
+
for (const w of weights)
|
|
1910
|
+
add({
|
|
1911
|
+
family: entry.family,
|
|
1912
|
+
url: fontFaceUrl(entry.slug, nearestFontWeight(entry, w)),
|
|
1913
|
+
weight: nearestFontWeight(entry, w),
|
|
1914
|
+
style: "normal"
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
for (const f of clip.fonts ?? [])
|
|
1918
|
+
add({
|
|
1919
|
+
family: f.family,
|
|
1920
|
+
url: f.url,
|
|
1921
|
+
weight: f.weight ?? 400,
|
|
1922
|
+
style: f.style ?? "normal"
|
|
1923
|
+
});
|
|
1924
|
+
return faces;
|
|
1925
|
+
}
|
|
1926
|
+
function htmlLayerUnhostedFamilies(clip) {
|
|
1927
|
+
const explicit = new Set(
|
|
1928
|
+
(clip.fonts ?? []).map((f) => f.family.toLowerCase())
|
|
1929
|
+
);
|
|
1930
|
+
return cssFamilies(clip.css).filter(
|
|
1931
|
+
(f) => !findFontFamily(f) && !explicit.has(f.toLowerCase())
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
function htmlLayerAssetUrls(clip) {
|
|
1935
|
+
const out = [];
|
|
1936
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1937
|
+
const add = (u) => {
|
|
1938
|
+
if (!seen.has(u)) {
|
|
1939
|
+
seen.add(u);
|
|
1940
|
+
out.push(u);
|
|
2027
1941
|
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
1942
|
+
};
|
|
1943
|
+
for (const m of clip.html.matchAll(
|
|
1944
|
+
/\bsrc\s*=\s*["']((?:https?:)?\/\/[^"']+)["']/gi
|
|
1945
|
+
))
|
|
1946
|
+
add(m[1]);
|
|
1947
|
+
for (const text of [clip.html, clip.css ?? ""])
|
|
1948
|
+
for (const m of text.matchAll(
|
|
1949
|
+
/url\(\s*["']?((?:https?:)?\/\/[^"')\s]+)["']?\s*\)/gi
|
|
1950
|
+
))
|
|
1951
|
+
add(m[1]);
|
|
1952
|
+
return out;
|
|
1953
|
+
}
|
|
1954
|
+
function htmlLayerAssets(clip) {
|
|
1955
|
+
const inlinable = [];
|
|
1956
|
+
const foreign = [];
|
|
1957
|
+
for (const u of htmlLayerAssetUrls(clip))
|
|
1958
|
+
(u.replace(/^\/\//, "https://").startsWith(HTML_LAYER_ASSET_HOST) ? inlinable : foreign).push(u);
|
|
1959
|
+
return { inlinable, foreign };
|
|
1960
|
+
}
|
|
1961
|
+
function htmlLayerBleedFor(css) {
|
|
1962
|
+
if (!css) return 0;
|
|
1963
|
+
let worst = 0;
|
|
1964
|
+
const consider = (layer) => {
|
|
1965
|
+
if (/\binset\b/i.test(layer)) return;
|
|
1966
|
+
const lengths = layer.replace(/[a-z-]+\([^)]*\)/gi, " ").replace(/#[0-9a-f]{3,8}/gi, " ").match(/-?\d*\.?\d+/g);
|
|
1967
|
+
if (!lengths) return;
|
|
1968
|
+
const [x = 0, y = 0, blur = 0, spread = 0] = lengths.map(Number);
|
|
1969
|
+
worst = Math.max(
|
|
1970
|
+
worst,
|
|
1971
|
+
Math.abs(x) + blur + spread,
|
|
1972
|
+
Math.abs(y) + blur + spread
|
|
1973
|
+
);
|
|
1974
|
+
};
|
|
1975
|
+
for (const decl of css.match(/box-shadow\s*:[^;}]+/gi) ?? [])
|
|
1976
|
+
for (const layer of splitTopLevel(decl.replace(/^[^:]*:/, "")))
|
|
1977
|
+
consider(layer);
|
|
1978
|
+
for (const fn of css.match(/drop-shadow\(([^)]*)\)/gi) ?? [])
|
|
1979
|
+
consider(fn.replace(/^drop-shadow\(/i, "").replace(/\)$/, ""));
|
|
1980
|
+
return worst ? Math.ceil(worst * 1.15) : 0;
|
|
1981
|
+
}
|
|
1982
|
+
function htmlLayerBleed(clip) {
|
|
1983
|
+
return Math.max(0, Math.round(clip.bleed ?? htmlLayerBleedFor(clip.css)));
|
|
1984
|
+
}
|
|
1985
|
+
function htmlLayerPictureBox(clip) {
|
|
1986
|
+
const bleed = htmlLayerBleed(clip);
|
|
1987
|
+
return {
|
|
1988
|
+
width: clip.box.width + bleed * 2,
|
|
1989
|
+
height: clip.box.height + bleed * 2,
|
|
1990
|
+
bleed
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function htmlLayerWidth(clip) {
|
|
1994
|
+
if (clip.width != null) return clip.width;
|
|
1995
|
+
return Math.min(1, htmlLayerPictureBox(clip).width / DESIGN_FRAME_WIDTH);
|
|
1996
|
+
}
|
|
1997
|
+
var ENTITIES = {
|
|
1998
|
+
amp: "&",
|
|
1999
|
+
lt: "<",
|
|
2000
|
+
gt: ">",
|
|
2001
|
+
quot: '"',
|
|
2002
|
+
apos: "'",
|
|
2003
|
+
nbsp: " "
|
|
2033
2004
|
};
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2005
|
+
function htmlLayerText(html) {
|
|
2006
|
+
return html.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]*>/g, " ").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, ref) => {
|
|
2007
|
+
if (ref[0] === "#")
|
|
2008
|
+
return String.fromCodePoint(
|
|
2009
|
+
ref[1] === "x" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1))
|
|
2010
|
+
);
|
|
2011
|
+
return ENTITIES[ref.toLowerCase()] ?? m;
|
|
2012
|
+
}).replace(/\s+/g, " ").trim();
|
|
2041
2013
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
{ name: "cut", label: "Cut", hint: "Instant zooms, no glide" },
|
|
2060
|
-
{
|
|
2061
|
-
name: "none",
|
|
2062
|
-
label: "None",
|
|
2063
|
-
hint: "No automatic zooms, add your own on the timeline"
|
|
2064
|
-
}
|
|
2014
|
+
function htmlLayerLabel(clip) {
|
|
2015
|
+
const words = htmlLayerText(clip.html);
|
|
2016
|
+
return words ? words.slice(0, 24) : "HTML";
|
|
2017
|
+
}
|
|
2018
|
+
var HTML_LAYER_PLACEHOLDER = /\{\{\s*(t|data\.[A-Za-z0-9_.-]+)\s*\}\}/;
|
|
2019
|
+
var VOID_TAGS = [
|
|
2020
|
+
"br",
|
|
2021
|
+
"img",
|
|
2022
|
+
"hr",
|
|
2023
|
+
"input",
|
|
2024
|
+
"source",
|
|
2025
|
+
"col",
|
|
2026
|
+
"area",
|
|
2027
|
+
"base",
|
|
2028
|
+
"wbr",
|
|
2029
|
+
"meta",
|
|
2030
|
+
"link"
|
|
2065
2031
|
];
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2032
|
+
var HTML_LAYER_FOREIGN_FIELDS = [
|
|
2033
|
+
"key",
|
|
2034
|
+
"radius",
|
|
2035
|
+
"shadow",
|
|
2036
|
+
"border",
|
|
2037
|
+
"loop",
|
|
2038
|
+
"frame",
|
|
2039
|
+
"fx",
|
|
2040
|
+
"text",
|
|
2041
|
+
"preset"
|
|
2042
|
+
];
|
|
2043
|
+
function htmlLayerProblems(clip) {
|
|
2044
|
+
const html = clip.html ?? "";
|
|
2045
|
+
const css = clip.css;
|
|
2046
|
+
const raw = clip;
|
|
2047
|
+
const problems = [];
|
|
2048
|
+
const push2 = (code, level, message) => problems.push({ code, level, message });
|
|
2049
|
+
if (!html.trim()) push2("empty", "problem", "The layer has no markup.");
|
|
2050
|
+
for (const tag of VOID_TAGS) {
|
|
2051
|
+
const open = new RegExp(`<${tag}(\\s[^>]*?)?>`, "gi");
|
|
2052
|
+
let m;
|
|
2053
|
+
let said = false;
|
|
2054
|
+
while (m = open.exec(html)) {
|
|
2055
|
+
if (!m[0].trimEnd().endsWith("/>") && !said) {
|
|
2056
|
+
said = true;
|
|
2057
|
+
push2(
|
|
2058
|
+
"unclosed-void",
|
|
2059
|
+
"problem",
|
|
2060
|
+
`<${tag}> must close itself as <${tag} /> inside a foreignObject.`
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2075
2064
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2065
|
+
const bare = html.match(/&(?!(#\d+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);)/g);
|
|
2066
|
+
if (bare)
|
|
2067
|
+
push2(
|
|
2068
|
+
"bare-ampersand",
|
|
2069
|
+
"problem",
|
|
2070
|
+
`A bare "&" is a parse error; write &. Found ${bare.length}.`
|
|
2071
|
+
);
|
|
2072
|
+
if (/<[a-zA-Z][^>]*?\s[a-zA-Z-]+=(?!["'])[^\s>]+/.test(html))
|
|
2073
|
+
push2(
|
|
2074
|
+
"unquoted-attr",
|
|
2075
|
+
"problem",
|
|
2076
|
+
"Attribute values must be quoted inside a foreignObject."
|
|
2077
|
+
);
|
|
2078
|
+
const bytes = html.length + (css?.length ?? 0);
|
|
2079
|
+
if (bytes > HTML_LAYER_MAX_BYTES)
|
|
2080
|
+
push2(
|
|
2081
|
+
"too-large",
|
|
2082
|
+
"problem",
|
|
2083
|
+
`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.`
|
|
2084
|
+
);
|
|
2085
|
+
else if (bytes > HTML_LAYER_WARN_BYTES)
|
|
2086
|
+
push2(
|
|
2087
|
+
"too-large",
|
|
2088
|
+
"warning",
|
|
2089
|
+
`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.`
|
|
2090
|
+
);
|
|
2091
|
+
for (const field of HTML_LAYER_FOREIGN_FIELDS)
|
|
2092
|
+
if (raw[field] !== void 0)
|
|
2093
|
+
push2(
|
|
2094
|
+
"foreign-field",
|
|
2095
|
+
"problem",
|
|
2096
|
+
`An HTML layer has no "${field}": its CSS is its chrome (shadow, radius, border) and its markup its content.`
|
|
2097
|
+
);
|
|
2098
|
+
const needed = htmlLayerBleedFor(css);
|
|
2099
|
+
if (clip.bleed != null && needed > clip.bleed)
|
|
2100
|
+
push2(
|
|
2101
|
+
"shadow-clipped",
|
|
2102
|
+
"warning",
|
|
2103
|
+
`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.`
|
|
2104
|
+
);
|
|
2105
|
+
for (const u of htmlLayerAssets({ html, css }).foreign)
|
|
2106
|
+
push2(
|
|
2107
|
+
"external-resource",
|
|
2108
|
+
"warning",
|
|
2109
|
+
`The source names ${u} by URL. The page inlines only ${HTML_LAYER_ASSET_HOST}\u2026 (the host it may fetch, and the one the fleet reaches), so this will paint blank. Upload it (vos asset upload) or inline it as a data: URI.`
|
|
2110
|
+
);
|
|
2111
|
+
if (/\bhref\s*=\s*["']\s*(https?:)?\/\//i.test(html))
|
|
2112
|
+
push2(
|
|
2113
|
+
"external-resource",
|
|
2114
|
+
"warning",
|
|
2115
|
+
"The source names a stylesheet or a link by URL. An SVG image fetches no href; inline the styles in the CSS."
|
|
2116
|
+
);
|
|
2117
|
+
const animated = /\b(animation|transition)(-[a-z-]+)?\s*:/i.test(css ?? "");
|
|
2118
|
+
if (animated && !clip.live)
|
|
2119
|
+
push2(
|
|
2120
|
+
"wall-clock",
|
|
2121
|
+
"warning",
|
|
2122
|
+
"CSS animations inside a layer run on the wall clock, not the timeline: the export will not match the preview. Set live: true to scrub them to the clip's time, or animate the layer with motion and anim."
|
|
2123
|
+
);
|
|
2124
|
+
if (clip.live && /\banimation-delay\s*:/i.test(css ?? ""))
|
|
2125
|
+
push2(
|
|
2126
|
+
"live-delay",
|
|
2127
|
+
"warning",
|
|
2128
|
+
"A live layer scrubs its keyframes by setting animation-delay to -t, so the authored animation-delay is overridden. Fold the delay into the keyframes instead."
|
|
2129
|
+
);
|
|
2130
|
+
if (!clip.live && (HTML_LAYER_PLACEHOLDER.test(html) || HTML_LAYER_PLACEHOLDER.test(css ?? "")))
|
|
2131
|
+
push2(
|
|
2132
|
+
"placeholder-still",
|
|
2133
|
+
"warning",
|
|
2134
|
+
"The source carries {{t}} or {{data.\u2026}} placeholders, which fill only on a live layer (live: true); a still layer shows them as written."
|
|
2135
|
+
);
|
|
2136
|
+
for (const family of htmlLayerUnhostedFamilies(clip))
|
|
2137
|
+
push2(
|
|
2138
|
+
"unhosted-family",
|
|
2139
|
+
"warning",
|
|
2140
|
+
`The CSS names "${family}", which is not a hosted family (GET https://vos.so/api/fonts); the system stack stands in.`
|
|
2141
|
+
);
|
|
2142
|
+
return problems;
|
|
2078
2143
|
}
|
|
2079
|
-
function
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
return {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2144
|
+
function wellFormedXml(svg, parseXml) {
|
|
2145
|
+
const doc = parseXml(svg);
|
|
2146
|
+
const errors = doc.getElementsByTagName("parsererror");
|
|
2147
|
+
if (!errors.length) return { ok: true };
|
|
2148
|
+
const text = (errors[0].textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2149
|
+
return { ok: false, error: text || "The markup is not well-formed XML." };
|
|
2150
|
+
}
|
|
2151
|
+
function fontFaceCss(faces) {
|
|
2152
|
+
let out = "";
|
|
2153
|
+
for (const f of faces)
|
|
2154
|
+
out += `@font-face{font-family:'${f.family}';src:url(${f.dataUri}) format('woff2');font-weight:${f.weight};font-style:${f.style};font-display:block}`;
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
var fmtT = (t) => String(Math.round(t * 1e3) / 1e3);
|
|
2158
|
+
var escapeXml = (v) => String(v).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2159
|
+
function htmlLayerSvg(src, faces = [], assets = []) {
|
|
2160
|
+
const bleed = Math.max(0, Math.round(src.bleed ?? 0));
|
|
2161
|
+
const boxW = src.box.width + bleed * 2;
|
|
2162
|
+
const boxH = src.box.height + bleed * 2;
|
|
2163
|
+
const t = src.t ?? 0;
|
|
2164
|
+
const fill = (text) => {
|
|
2165
|
+
let out = text;
|
|
2166
|
+
for (const a of assets) out = out.split(a.url).join(a.dataUri);
|
|
2167
|
+
if (src.live) {
|
|
2168
|
+
out = out.replace(/\{\{\s*t\s*\}\}/g, fmtT(t));
|
|
2169
|
+
out = out.replace(
|
|
2170
|
+
/\{\{\s*data\.([A-Za-z0-9_.-]+)\s*\}\}/g,
|
|
2171
|
+
(_m, name) => {
|
|
2172
|
+
const v = src.data ? src.data[name] : void 0;
|
|
2173
|
+
return v == null ? "" : escapeXml(v);
|
|
2174
|
+
}
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
return out;
|
|
2088
2178
|
};
|
|
2179
|
+
const scrub = src.live ? `.vos-html-layer{--vos-t:${fmtT(t)}}.vos-html-layer,.vos-html-layer *{animation-play-state:paused!important;animation-delay:calc(var(--vos-t) * -1s)!important}` : "";
|
|
2180
|
+
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}`;
|
|
2181
|
+
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}${fill(src.css ?? "")}${scrub}</style>` + fill(src.html) + `</div></foreignObject></svg>`;
|
|
2089
2182
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
if (typeof own === "number" && own > 0) return own;
|
|
2098
|
-
const cfg = doc.program.config.duration;
|
|
2099
|
-
return typeof cfg === "number" && cfg > 0 ? cfg : 0;
|
|
2183
|
+
function fnv1a(s) {
|
|
2184
|
+
let h = 2166136261;
|
|
2185
|
+
for (let i = 0; i < s.length; i++) {
|
|
2186
|
+
h ^= s.charCodeAt(i);
|
|
2187
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
2188
|
+
}
|
|
2189
|
+
return h.toString(36);
|
|
2100
2190
|
}
|
|
2101
|
-
function
|
|
2102
|
-
|
|
2191
|
+
function htmlLayerKey(clip, faces, bleed, assets = []) {
|
|
2192
|
+
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("|") + " " + assets.join("|") + // A live layer's picture is also a function of t: ON_FRAME appends the
|
|
2193
|
+
// moment to this key per frame. The data rides here, the moment there.
|
|
2194
|
+
(clip.live ? " live " + JSON.stringify(clip.data ?? null) : "");
|
|
2195
|
+
return `html:${clip.id}:${fnv1a(src)}`;
|
|
2103
2196
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
2197
|
+
function htmlLayerPayload(clip) {
|
|
2198
|
+
const faces = htmlLayerFaces(clip);
|
|
2199
|
+
const bleed = htmlLayerBleed(clip);
|
|
2200
|
+
const assets = htmlLayerAssets(clip).inlinable;
|
|
2201
|
+
return {
|
|
2202
|
+
key: htmlLayerKey(clip, faces, bleed, assets),
|
|
2203
|
+
width: htmlLayerWidth(clip),
|
|
2204
|
+
html: {
|
|
2205
|
+
markup: clip.html,
|
|
2206
|
+
css: clip.css ?? "",
|
|
2207
|
+
box: { width: clip.box.width, height: clip.box.height },
|
|
2208
|
+
bleed,
|
|
2209
|
+
faces,
|
|
2210
|
+
assets,
|
|
2211
|
+
...clip.live ? { live: true, ...clip.data ? { data: clip.data } : {} } : {}
|
|
2120
2212
|
}
|
|
2121
|
-
}
|
|
2122
|
-
return { x: last.x, y: last.y };
|
|
2213
|
+
};
|
|
2123
2214
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
if (
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2215
|
+
|
|
2216
|
+
// src/richText/layout.ts
|
|
2217
|
+
var sameStyle = (a, b) => (a.f || 0) === (b.f || 0) && a.c === b.c && a.u === b.u && a.s === b.s && a.h === b.h;
|
|
2218
|
+
function graphemesOf(text) {
|
|
2219
|
+
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
|
|
2220
|
+
const seg = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2221
|
+
const out = [];
|
|
2222
|
+
for (const s of seg.segment(text)) out.push(s.segment);
|
|
2223
|
+
return out;
|
|
2224
|
+
}
|
|
2225
|
+
return Array.from(text);
|
|
2226
|
+
}
|
|
2227
|
+
function tokenRanges(line) {
|
|
2228
|
+
const out = [];
|
|
2229
|
+
const re = /\S+\s*/g;
|
|
2230
|
+
let m;
|
|
2231
|
+
while ((m = re.exec(line)) !== null) {
|
|
2232
|
+
out.push([m.index, m.index + m[0].length]);
|
|
2233
|
+
}
|
|
2234
|
+
return out.length ? out : [[0, line.length]];
|
|
2235
|
+
}
|
|
2236
|
+
function layoutText(input, measure) {
|
|
2237
|
+
const memo = {};
|
|
2238
|
+
const adv = (t, f) => {
|
|
2239
|
+
if (!t) return 0;
|
|
2240
|
+
const key = f + "\0" + t;
|
|
2241
|
+
let w = memo[key];
|
|
2242
|
+
if (w === void 0) {
|
|
2243
|
+
w = measure(t, f);
|
|
2244
|
+
memo[key] = w;
|
|
2245
|
+
}
|
|
2246
|
+
return w;
|
|
2247
|
+
};
|
|
2248
|
+
const slice = (runs, a, b) => {
|
|
2249
|
+
const out = [];
|
|
2250
|
+
let at = 0;
|
|
2251
|
+
for (let i = 0; i < runs.length; i++) {
|
|
2252
|
+
const r = runs[i];
|
|
2253
|
+
const from = Math.max(a, at);
|
|
2254
|
+
const to = Math.min(b, at + r.t.length);
|
|
2255
|
+
if (to > from) {
|
|
2256
|
+
const t = r.t.slice(from - at, to - at);
|
|
2257
|
+
const last = out[out.length - 1];
|
|
2258
|
+
if (last && sameStyle(last, r)) last.t += t;
|
|
2259
|
+
else out.push({ ...r, t });
|
|
2153
2260
|
}
|
|
2261
|
+
at += r.t.length;
|
|
2262
|
+
if (at >= b) break;
|
|
2154
2263
|
}
|
|
2155
|
-
out
|
|
2156
|
-
}
|
|
2264
|
+
return out;
|
|
2265
|
+
};
|
|
2266
|
+
const widthOf = (runs) => {
|
|
2267
|
+
let w = 0;
|
|
2268
|
+
for (let i = 0; i < runs.length; i++) w += adv(runs[i].t, runs[i].f || 0);
|
|
2269
|
+
return w;
|
|
2270
|
+
};
|
|
2271
|
+
const partsIn = (frags, a, b) => {
|
|
2272
|
+
const out = [];
|
|
2273
|
+
for (let i = 0; i < frags.length; i++) {
|
|
2274
|
+
const fr = frags[i];
|
|
2275
|
+
const from = Math.max(a, fr.o);
|
|
2276
|
+
const to = Math.min(b, fr.o + fr.t.length);
|
|
2277
|
+
if (to <= from) continue;
|
|
2278
|
+
const f = fr.f || 0;
|
|
2279
|
+
const before = adv(fr.t.slice(0, from - fr.o), f);
|
|
2280
|
+
out.push({
|
|
2281
|
+
...fr,
|
|
2282
|
+
t: fr.t.slice(from - fr.o, to - fr.o),
|
|
2283
|
+
x: fr.x + before,
|
|
2284
|
+
w: adv(fr.t.slice(0, to - fr.o), f) - before,
|
|
2285
|
+
o: from
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
return out;
|
|
2289
|
+
};
|
|
2290
|
+
const unitOf = (frags, a, b) => {
|
|
2291
|
+
const parts = partsIn(frags, a, b);
|
|
2292
|
+
if (!parts.length) return { x: 0, w: 0, parts };
|
|
2293
|
+
const last = parts[parts.length - 1];
|
|
2294
|
+
return { x: parts[0].x, w: last.x + last.w - parts[0].x, parts };
|
|
2295
|
+
};
|
|
2296
|
+
const lines = [];
|
|
2297
|
+
const max = input.maxWidth && input.maxWidth > 0 ? input.maxWidth : 0;
|
|
2298
|
+
let width = 0;
|
|
2299
|
+
let base = 0;
|
|
2300
|
+
const push2 = (runs, a, b) => {
|
|
2301
|
+
const cut = slice(runs, a, b);
|
|
2302
|
+
const frags = [];
|
|
2303
|
+
let x = 0;
|
|
2304
|
+
let o = 0;
|
|
2305
|
+
for (let i = 0; i < cut.length; i++) {
|
|
2306
|
+
const w = adv(cut[i].t, cut[i].f || 0);
|
|
2307
|
+
frags.push({ ...cut[i], x, w, o });
|
|
2308
|
+
x += w;
|
|
2309
|
+
o += cut[i].t.length;
|
|
2310
|
+
}
|
|
2311
|
+
const line = { w: x, frags, a: base + a, n: b - a };
|
|
2312
|
+
if (input.unit) {
|
|
2313
|
+
const text = cut.map((r) => r.t).join("");
|
|
2314
|
+
const units = [];
|
|
2315
|
+
if (input.unit === "line") units.push(unitOf(frags, 0, text.length));
|
|
2316
|
+
else if (input.unit === "word") {
|
|
2317
|
+
const toks = tokenRanges(text);
|
|
2318
|
+
for (let i = 0; i < toks.length; i++) {
|
|
2319
|
+
units.push(unitOf(frags, toks[i][0], toks[i][1]));
|
|
2320
|
+
}
|
|
2321
|
+
} else {
|
|
2322
|
+
let at = 0;
|
|
2323
|
+
const gs = graphemesOf(text);
|
|
2324
|
+
for (let i = 0; i < gs.length; i++) {
|
|
2325
|
+
units.push(unitOf(frags, at, at + gs[i].length));
|
|
2326
|
+
at += gs[i].length;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
line.units = units;
|
|
2330
|
+
}
|
|
2331
|
+
if (x > width) width = x;
|
|
2332
|
+
lines.push(line);
|
|
2333
|
+
};
|
|
2334
|
+
for (let li = 0; li < input.lines.length; li++) {
|
|
2335
|
+
const runs = input.lines[li];
|
|
2336
|
+
let len = 0;
|
|
2337
|
+
for (let i = 0; i < runs.length; i++) len += runs[i].t.length;
|
|
2338
|
+
if (!max || !len || widthOf(slice(runs, 0, len)) <= max) {
|
|
2339
|
+
push2(runs, 0, len);
|
|
2340
|
+
} else {
|
|
2341
|
+
const text = runs.map((r) => r.t).join("");
|
|
2342
|
+
const toks = tokenRanges(text);
|
|
2343
|
+
let a = 0;
|
|
2344
|
+
let b = toks[0][1];
|
|
2345
|
+
for (let i = 1; i < toks.length; i++) {
|
|
2346
|
+
if (widthOf(slice(runs, a, toks[i][1])) <= max) b = toks[i][1];
|
|
2347
|
+
else {
|
|
2348
|
+
push2(runs, a, b);
|
|
2349
|
+
a = toks[i][0];
|
|
2350
|
+
b = toks[i][1];
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
push2(runs, a, b);
|
|
2354
|
+
}
|
|
2355
|
+
base += len + 1;
|
|
2356
|
+
}
|
|
2357
|
+
if (!lines.length) lines.push({ w: 0, frags: [], a: 0, n: 0 });
|
|
2358
|
+
return { lines, width };
|
|
2359
|
+
}
|
|
2360
|
+
var UNDERLINE_OFFSET_EM = 0.42;
|
|
2361
|
+
var DECORATION_THICKNESS_EM = 0.065;
|
|
2362
|
+
var STRIKE_OFFSET_EM = 0.02;
|
|
2363
|
+
var HIGHLIGHT_HALF_EM = 0.58;
|
|
2364
|
+
function decorationRect(kind, x, w, y, px) {
|
|
2365
|
+
const x0 = Math.round(x);
|
|
2366
|
+
const x1 = Math.round(x + w);
|
|
2367
|
+
if (kind === "h") {
|
|
2368
|
+
const top2 = Math.round(y - HIGHLIGHT_HALF_EM * px);
|
|
2369
|
+
const bottom = Math.round(y + HIGHLIGHT_HALF_EM * px);
|
|
2370
|
+
return { x: x0, y: top2, w: x1 - x0, h: bottom - top2 };
|
|
2371
|
+
}
|
|
2372
|
+
const t = Math.max(1, Math.round(DECORATION_THICKNESS_EM * px));
|
|
2373
|
+
const top = Math.round(
|
|
2374
|
+
y + (kind === "u" ? UNDERLINE_OFFSET_EM : STRIKE_OFFSET_EM) * px
|
|
2375
|
+
);
|
|
2376
|
+
return { x: x0, y: top, w: x1 - x0, h: t };
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// src/richText/runs.ts
|
|
2380
|
+
var RUN_STYLE_KEYS = [
|
|
2381
|
+
"weight",
|
|
2382
|
+
"italic",
|
|
2383
|
+
"color",
|
|
2384
|
+
"underline",
|
|
2385
|
+
"strike",
|
|
2386
|
+
"highlight"
|
|
2387
|
+
];
|
|
2388
|
+
var MIXED = "mixed";
|
|
2389
|
+
function plainText(text) {
|
|
2390
|
+
if (typeof text === "string") return text;
|
|
2391
|
+
let out = "";
|
|
2392
|
+
for (const r of text) out += r.text;
|
|
2157
2393
|
return out;
|
|
2158
2394
|
}
|
|
2159
|
-
function
|
|
2160
|
-
return
|
|
2395
|
+
function runsOf(text) {
|
|
2396
|
+
return typeof text === "string" ? text ? [{ text }] : [] : text;
|
|
2161
2397
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
var
|
|
2173
|
-
var
|
|
2174
|
-
function
|
|
2175
|
-
if (
|
|
2176
|
-
const downs = dedupePresses(track.filter((e) => e.type === "down"));
|
|
2398
|
+
function styleOf(run) {
|
|
2399
|
+
const out = {};
|
|
2400
|
+
if (typeof run.weight === "number") out.weight = run.weight;
|
|
2401
|
+
if (typeof run.italic === "boolean") out.italic = run.italic;
|
|
2402
|
+
if (run.color) out.color = run.color;
|
|
2403
|
+
if (run.underline) out.underline = true;
|
|
2404
|
+
if (run.strike) out.strike = true;
|
|
2405
|
+
if (run.highlight) out.highlight = run.highlight;
|
|
2406
|
+
return out;
|
|
2407
|
+
}
|
|
2408
|
+
var sameStyle2 = (a, b) => RUN_STYLE_KEYS.every((k) => a[k] === b[k]);
|
|
2409
|
+
var isStyled = (s) => RUN_STYLE_KEYS.some((k) => s[k] !== void 0);
|
|
2410
|
+
function normalizeRuns(text) {
|
|
2411
|
+
if (typeof text === "string") return text;
|
|
2177
2412
|
const out = [];
|
|
2178
|
-
for (const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
const
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
if (e.t > up.t) break;
|
|
2188
|
-
if (e.type !== "move" && e.type !== "up") continue;
|
|
2189
|
-
const dist = Math.hypot(e.x - d.x, e.y - d.y) / width;
|
|
2190
|
-
if (dist > travel) travel = dist;
|
|
2191
|
-
}
|
|
2192
|
-
if (travel < DRAG_MIN_TRAVEL_FRAC) continue;
|
|
2193
|
-
out.push({
|
|
2194
|
-
t0,
|
|
2195
|
-
t1,
|
|
2196
|
-
nx: clamp012(d.x / width),
|
|
2197
|
-
ny: clamp012(d.y / height),
|
|
2198
|
-
travel
|
|
2199
|
-
});
|
|
2200
|
-
}
|
|
2413
|
+
for (const r of text) {
|
|
2414
|
+
if (typeof r.text !== "string" || !r.text) continue;
|
|
2415
|
+
const style = styleOf(r);
|
|
2416
|
+
const last = out[out.length - 1];
|
|
2417
|
+
if (last && sameStyle2(last, style)) last.text += r.text;
|
|
2418
|
+
else out.push({ text: r.text, ...style });
|
|
2419
|
+
}
|
|
2420
|
+
if (!out.length) return "";
|
|
2421
|
+
if (out.length === 1 && !isStyled(out[0])) return out[0].text;
|
|
2201
2422
|
return out;
|
|
2202
2423
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
track.filter((e) => e.type === "down")
|
|
2212
|
-
).map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
|
|
2213
|
-
const sessions = typingZoom ? typingSessions(track, width, height, typingGap) : [];
|
|
2214
|
-
const absorbed = /* @__PURE__ */ new Set();
|
|
2215
|
-
for (const s of sessions) {
|
|
2216
|
-
let best = null;
|
|
2217
|
-
for (const c of clicks) {
|
|
2218
|
-
if (absorbed.has(c)) continue;
|
|
2219
|
-
if (c.t >= s.first || s.first - c.t > TYPING_CLICK_ABSORB) continue;
|
|
2220
|
-
const [nx, ny] = fieldCenter(c, width, height);
|
|
2221
|
-
if (Math.hypot(nx - s.cx, ny - s.cy) > TYPING_REFOCUS_FRAC) continue;
|
|
2222
|
-
if (!best || c.t > best.t) best = c;
|
|
2223
|
-
}
|
|
2224
|
-
if (best) {
|
|
2225
|
-
absorbed.add(best);
|
|
2226
|
-
s.start = best.t;
|
|
2227
|
-
s.events = [best, ...s.events];
|
|
2228
|
-
}
|
|
2424
|
+
function snapOffset(plain, offset, dir) {
|
|
2425
|
+
const o = Math.max(0, Math.min(plain.length, offset));
|
|
2426
|
+
let at = 0;
|
|
2427
|
+
for (const g of graphemesOf(plain)) {
|
|
2428
|
+
const next = at + g.length;
|
|
2429
|
+
if (o === at) return at;
|
|
2430
|
+
if (o < next) return dir < 0 ? at : next;
|
|
2431
|
+
at = next;
|
|
2229
2432
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2433
|
+
return plain.length;
|
|
2434
|
+
}
|
|
2435
|
+
function sliceRuns(text, start, end) {
|
|
2436
|
+
const out = [];
|
|
2437
|
+
let at = 0;
|
|
2438
|
+
for (const r of runsOf(text)) {
|
|
2439
|
+
const from = Math.max(start, at);
|
|
2440
|
+
const to = Math.min(end, at + r.text.length);
|
|
2441
|
+
if (to > from) out.push({ ...r, text: r.text.slice(from - at, to - at) });
|
|
2442
|
+
at += r.text.length;
|
|
2443
|
+
if (at >= end) break;
|
|
2236
2444
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2445
|
+
return out;
|
|
2446
|
+
}
|
|
2447
|
+
function styleOfChar(text, offset) {
|
|
2448
|
+
let at = 0;
|
|
2449
|
+
for (const r of runsOf(text)) {
|
|
2450
|
+
if (offset < at + r.text.length) return styleOf(r);
|
|
2451
|
+
at += r.text.length;
|
|
2452
|
+
}
|
|
2453
|
+
return {};
|
|
2454
|
+
}
|
|
2455
|
+
function styleAt(text, offset) {
|
|
2456
|
+
const len = plainText(text).length;
|
|
2457
|
+
if (!len) return {};
|
|
2458
|
+
return styleOfChar(text, offset > 0 ? Math.min(offset, len) - 1 : 0);
|
|
2459
|
+
}
|
|
2460
|
+
function replaceRange(text, start, end, insert, style) {
|
|
2461
|
+
const len = plainText(text).length;
|
|
2462
|
+
const a = Math.max(0, Math.min(start, end, len));
|
|
2463
|
+
const b = Math.min(len, Math.max(start, end));
|
|
2464
|
+
const set = style ?? (b > a ? styleOfChar(text, a) : styleAt(text, a));
|
|
2465
|
+
return normalizeRuns([
|
|
2466
|
+
...sliceRuns(text, 0, a),
|
|
2467
|
+
...insert ? [{ text: insert, ...styleOf(set) }] : [],
|
|
2468
|
+
...sliceRuns(text, b, len)
|
|
2469
|
+
]);
|
|
2470
|
+
}
|
|
2471
|
+
function rangeOf(text, start, end) {
|
|
2472
|
+
const plain = plainText(text);
|
|
2473
|
+
const a = snapOffset(plain, Math.min(start, end), -1);
|
|
2474
|
+
const b = snapOffset(plain, Math.max(start, end), 1);
|
|
2475
|
+
return b > a ? { plain, a, b } : null;
|
|
2476
|
+
}
|
|
2477
|
+
function setStyle(text, start, end, patch) {
|
|
2478
|
+
const r = rangeOf(text, start, end);
|
|
2479
|
+
if (!r) return normalizeRuns(text);
|
|
2480
|
+
const restyle = (run) => {
|
|
2481
|
+
const next = { ...run };
|
|
2482
|
+
for (const k of RUN_STYLE_KEYS) {
|
|
2483
|
+
if (!(k in patch)) continue;
|
|
2484
|
+
if (patch[k] === void 0) delete next[k];
|
|
2485
|
+
else Object.assign(next, { [k]: patch[k] });
|
|
2245
2486
|
}
|
|
2246
|
-
return
|
|
2247
|
-
};
|
|
2248
|
-
return {
|
|
2249
|
-
sessions,
|
|
2250
|
-
clusters: chain(targets, true),
|
|
2251
|
-
surfaces: chain(surfaces, false)
|
|
2487
|
+
return next;
|
|
2252
2488
|
};
|
|
2489
|
+
return normalizeRuns([
|
|
2490
|
+
...sliceRuns(text, 0, r.a),
|
|
2491
|
+
...sliceRuns(text, r.a, r.b).map(restyle),
|
|
2492
|
+
...sliceRuns(text, r.b, r.plain.length)
|
|
2493
|
+
]);
|
|
2253
2494
|
}
|
|
2254
|
-
function
|
|
2255
|
-
const
|
|
2256
|
-
for (const
|
|
2257
|
-
|
|
2258
|
-
if (prev && e.t - prev.t <= PRESS_ECHO_MS && Math.abs(e.x - prev.x) <= PRESS_ECHO_PX && Math.abs(e.y - prev.y) <= PRESS_ECHO_PX) {
|
|
2259
|
-
continue;
|
|
2260
|
-
}
|
|
2261
|
-
kept.push(e);
|
|
2262
|
-
}
|
|
2263
|
-
return kept;
|
|
2495
|
+
function clearStyle(text, start, end) {
|
|
2496
|
+
const patch = {};
|
|
2497
|
+
for (const k of RUN_STYLE_KEYS) patch[k] = void 0;
|
|
2498
|
+
return setStyle(text, start, end, patch);
|
|
2264
2499
|
}
|
|
2265
|
-
function
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
for (const
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
y1 = Math.max(y1, r.y + r.h);
|
|
2500
|
+
function commonStyle(text, start, end) {
|
|
2501
|
+
const r = rangeOf(text, start, end);
|
|
2502
|
+
const runs = r ? sliceRuns(text, r.a, r.b) : [];
|
|
2503
|
+
const out = {};
|
|
2504
|
+
if (!runs.length) return styleAt(text, start);
|
|
2505
|
+
for (const k of RUN_STYLE_KEYS) {
|
|
2506
|
+
const first = styleOf(runs[0])[k];
|
|
2507
|
+
const agree = runs.every((run) => styleOf(run)[k] === first);
|
|
2508
|
+
if (!agree) out[k] = MIXED;
|
|
2509
|
+
else if (first !== void 0) out[k] = first;
|
|
2276
2510
|
}
|
|
2277
|
-
return
|
|
2278
|
-
}
|
|
2279
|
-
function unionFitLevel(rect, width, height) {
|
|
2280
|
-
const fx = rect.w > 0 ? width * CLUSTER_FILL / rect.w : Infinity;
|
|
2281
|
-
const fy = rect.h > 0 ? height * CLUSTER_FILL / rect.h : Infinity;
|
|
2282
|
-
return Math.min(fx, fy);
|
|
2511
|
+
return out;
|
|
2283
2512
|
}
|
|
2284
|
-
function
|
|
2285
|
-
|
|
2286
|
-
return
|
|
2513
|
+
function toggleStyle(text, start, end, key, value) {
|
|
2514
|
+
const on = commonStyle(text, start, end)[key] === value;
|
|
2515
|
+
return setStyle(text, start, end, { [key]: on ? void 0 : value });
|
|
2516
|
+
}
|
|
2517
|
+
function diffInput(prev, next, caretAfter) {
|
|
2518
|
+
const caret = Math.max(0, Math.min(next.length, caretAfter));
|
|
2519
|
+
const maxTail = Math.min(prev.length, next.length - caret);
|
|
2520
|
+
let tail = 0;
|
|
2521
|
+
while (tail < maxTail && prev[prev.length - 1 - tail] === next[next.length - 1 - tail])
|
|
2522
|
+
tail++;
|
|
2523
|
+
const maxHead = Math.min(prev.length - tail, next.length - tail);
|
|
2524
|
+
let head = 0;
|
|
2525
|
+
while (head < maxHead && prev[head] === next[head]) head++;
|
|
2526
|
+
return {
|
|
2527
|
+
start: head,
|
|
2528
|
+
end: prev.length - tail,
|
|
2529
|
+
insert: next.slice(head, next.length - tail)
|
|
2530
|
+
};
|
|
2287
2531
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2532
|
+
|
|
2533
|
+
// src/overlayText.ts
|
|
2534
|
+
var OVERLAY_BOX_PAD_X = 0.6;
|
|
2535
|
+
var OVERLAY_BOX_PAD_Y = 0.35;
|
|
2536
|
+
var OVERLAY_BOX_RADIUS = 0.25;
|
|
2537
|
+
function resolveOverlayBox(clip) {
|
|
2538
|
+
if (!clip.box) return null;
|
|
2539
|
+
const size = resolveOverlayStyle(clip).size;
|
|
2540
|
+
return {
|
|
2541
|
+
color: clip.box.color,
|
|
2542
|
+
opacity: clip.box.opacity ?? 1,
|
|
2543
|
+
padX: (clip.box.paddingX ?? OVERLAY_BOX_PAD_X) * size,
|
|
2544
|
+
padY: (clip.box.paddingY ?? OVERLAY_BOX_PAD_Y) * size,
|
|
2545
|
+
radius: (clip.box.radius ?? OVERLAY_BOX_RADIUS) * size
|
|
2546
|
+
};
|
|
2291
2547
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
typingGap,
|
|
2319
|
-
typingZoom,
|
|
2320
|
-
targetFill,
|
|
2321
|
-
minLevel
|
|
2322
|
-
});
|
|
2323
|
-
const { sessions } = grouped;
|
|
2324
|
-
const drags = dragsFromTrack(track, width, height);
|
|
2325
|
-
const dragPress = new Set(drags.map((d) => round2(d.t0)));
|
|
2326
|
-
const withoutDrags = (list) => list.map((cl) => cl.filter((c) => !dragPress.has(round2(c.t)))).filter((cl) => cl.length > 0);
|
|
2327
|
-
const clusters = withoutDrags(grouped.clusters);
|
|
2328
|
-
const surfaces = withoutDrags(grouped.surfaces);
|
|
2329
|
-
const dragSpans = drags.map((d, i) => ({
|
|
2330
|
-
id: `g${i}`,
|
|
2331
|
-
in: round2(Math.max(0, d.t0 - lead)),
|
|
2332
|
-
out: round2(d.t1 + hold),
|
|
2333
|
-
level: clampZoomLevel(Math.min(Math.max(dragLevel, 1.1), maxLevel)),
|
|
2334
|
-
cx: round2(d.nx),
|
|
2335
|
-
cy: round2(d.ny),
|
|
2336
|
-
focusMode: "auto",
|
|
2337
|
-
source: "auto"
|
|
2338
|
-
}));
|
|
2339
|
-
const eligible = clusters.filter((c) => c.length >= minClusterClicks);
|
|
2340
|
-
const clickSpans = [];
|
|
2341
|
-
const dragReserved = surfaces.map((cluster, i) => {
|
|
2342
|
-
const f = clusterFocus(cluster, width, height);
|
|
2343
|
-
return {
|
|
2344
|
-
id: `drag${i}`,
|
|
2345
|
-
in: Math.max(0, cluster[0].t - lead),
|
|
2346
|
-
out: cluster[cluster.length - 1].t + hold,
|
|
2347
|
-
level: 1,
|
|
2348
|
-
cx: f.cx,
|
|
2349
|
-
cy: f.cy
|
|
2350
|
-
};
|
|
2351
|
-
});
|
|
2352
|
-
for (const cluster of eligible) {
|
|
2353
|
-
const first = cluster[0];
|
|
2354
|
-
const last = cluster[cluster.length - 1];
|
|
2355
|
-
const f = focusFor(cluster, width, height, targetFill, minLevel, maxLevel);
|
|
2356
|
-
clickSpans.push({
|
|
2357
|
-
in: Math.max(0, first.t - lead),
|
|
2358
|
-
out: last.t + hold,
|
|
2359
|
-
cx: f.cx,
|
|
2360
|
-
cy: f.cy,
|
|
2361
|
-
level: f.level
|
|
2362
|
-
});
|
|
2548
|
+
var PRESET_FAMILY = {
|
|
2549
|
+
title: "Lexend",
|
|
2550
|
+
caption: "Lexend",
|
|
2551
|
+
label: "JetBrains Mono"
|
|
2552
|
+
};
|
|
2553
|
+
var TEXT_PRESETS = {
|
|
2554
|
+
title: {
|
|
2555
|
+
stack: "Lexend, -apple-system, system-ui, sans-serif",
|
|
2556
|
+
weight: 600,
|
|
2557
|
+
size: 64,
|
|
2558
|
+
color: "#fafafa",
|
|
2559
|
+
shadow: 0.45
|
|
2560
|
+
},
|
|
2561
|
+
caption: {
|
|
2562
|
+
stack: "Lexend, -apple-system, system-ui, sans-serif",
|
|
2563
|
+
weight: 400,
|
|
2564
|
+
size: 32,
|
|
2565
|
+
color: "#f4f4f5",
|
|
2566
|
+
shadow: 0.4
|
|
2567
|
+
},
|
|
2568
|
+
label: {
|
|
2569
|
+
stack: "'JetBrains Mono', ui-monospace, SFMono-Regular, monospace",
|
|
2570
|
+
weight: 400,
|
|
2571
|
+
size: 22,
|
|
2572
|
+
color: "#e4e4e7",
|
|
2573
|
+
shadow: 0.35
|
|
2363
2574
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2575
|
+
};
|
|
2576
|
+
var OVERLAY_SIZE_MIN = 12;
|
|
2577
|
+
var OVERLAY_SIZE_MAX = 200;
|
|
2578
|
+
var OVERLAY_FONT_FACES = [
|
|
2579
|
+
{ family: "Lexend", weight: 400, url: fontFaceUrl2("lexend", 400) },
|
|
2580
|
+
{ family: "Lexend", weight: 600, url: fontFaceUrl2("lexend", 600) },
|
|
2581
|
+
{
|
|
2582
|
+
family: "JetBrains Mono",
|
|
2583
|
+
weight: 400,
|
|
2584
|
+
url: fontFaceUrl2("jetbrains-mono", 400)
|
|
2368
2585
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
);
|
|
2380
|
-
return {
|
|
2381
|
-
in: Math.max(0, s.start - lead),
|
|
2382
|
-
out: s.last + typingHold,
|
|
2383
|
-
cx: f.cx,
|
|
2384
|
-
cy: f.cy,
|
|
2385
|
-
level: f.level
|
|
2386
|
-
};
|
|
2387
|
-
});
|
|
2388
|
-
for (const t of typing) {
|
|
2389
|
-
for (const z of clickSpans) {
|
|
2390
|
-
if (z.dead || t.dead) continue;
|
|
2391
|
-
if (t.in >= z.out || t.out <= z.in) continue;
|
|
2392
|
-
if (Math.hypot(t.cx - z.cx, t.cy - z.cy) <= TYPING_REFOCUS_FRAC) {
|
|
2393
|
-
t.in = Math.min(t.in, z.in);
|
|
2394
|
-
t.out = Math.max(t.out, z.out);
|
|
2395
|
-
z.dead = true;
|
|
2396
|
-
} else if (z.in <= t.in && z.out >= t.out) {
|
|
2397
|
-
t.dead = true;
|
|
2398
|
-
} else if (z.in > t.in) {
|
|
2399
|
-
t.out = z.in;
|
|
2400
|
-
} else {
|
|
2401
|
-
t.in = z.out;
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2586
|
+
];
|
|
2587
|
+
function resolveOverlayStyle(clip) {
|
|
2588
|
+
const presetName = clip.preset in TEXT_PRESETS ? clip.preset : "title";
|
|
2589
|
+
const base = TEXT_PRESETS[presetName];
|
|
2590
|
+
let stack = base.stack;
|
|
2591
|
+
let weight = base.weight;
|
|
2592
|
+
const familyEntry = findFontFamily2(clip.family ?? PRESET_FAMILY[presetName]);
|
|
2593
|
+
if (clip.family) {
|
|
2594
|
+
const quoted = clip.family.includes(" ") ? `'${clip.family}'` : clip.family;
|
|
2595
|
+
stack = familyEntry ? fontStack(familyEntry) : `${quoted}, ${base.stack}`;
|
|
2404
2596
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2597
|
+
if (clip.weight !== void 0 || clip.family) {
|
|
2598
|
+
const wanted = clip.weight ?? base.weight;
|
|
2599
|
+
weight = familyEntry ? nearestFontWeight2(familyEntry, wanted) : wanted;
|
|
2408
2600
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
...
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
source: "auto"
|
|
2432
|
-
}));
|
|
2433
|
-
const spans = within([...zSpans, ...kSpans, ...dragSpans]).sort(
|
|
2434
|
-
(a, b) => a.in - b.in
|
|
2601
|
+
return {
|
|
2602
|
+
...base,
|
|
2603
|
+
stack,
|
|
2604
|
+
weight,
|
|
2605
|
+
...clip.size !== void 0 ? {
|
|
2606
|
+
size: Math.min(
|
|
2607
|
+
OVERLAY_SIZE_MAX,
|
|
2608
|
+
Math.max(OVERLAY_SIZE_MIN, clip.size)
|
|
2609
|
+
)
|
|
2610
|
+
} : {},
|
|
2611
|
+
...clip.color ? { color: clip.color } : {},
|
|
2612
|
+
...clip.shadow !== void 0 ? { shadow: Math.min(1, Math.max(0, clip.shadow)) } : {},
|
|
2613
|
+
fontStyle: clip.italic ? "italic" : "normal",
|
|
2614
|
+
align: clip.align ?? "center",
|
|
2615
|
+
letterSpacing: clip.letterSpacing ?? 0,
|
|
2616
|
+
lineHeight: clip.lineHeight ?? OVERLAY_LINE_HEIGHT,
|
|
2617
|
+
stroke: clip.stroke ?? null
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
function overlayFaceFor(clip) {
|
|
2621
|
+
const entry = findFontFamily2(
|
|
2622
|
+
clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
|
|
2435
2623
|
);
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2624
|
+
if (!entry) return null;
|
|
2625
|
+
const weight = resolveOverlayStyle(clip).weight;
|
|
2626
|
+
const inBase = OVERLAY_FONT_FACES.some(
|
|
2627
|
+
(f) => f.family === entry.family && f.weight === weight
|
|
2628
|
+
);
|
|
2629
|
+
if (inBase) return null;
|
|
2630
|
+
return {
|
|
2631
|
+
family: entry.family,
|
|
2632
|
+
weight,
|
|
2633
|
+
url: fontFaceUrl2(entry.slug, weight)
|
|
2634
|
+
};
|
|
2441
2635
|
}
|
|
2442
|
-
function
|
|
2443
|
-
const
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
const
|
|
2448
|
-
if (
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2636
|
+
function overlayFontFaces(doc) {
|
|
2637
|
+
const faces = [...OVERLAY_FONT_FACES];
|
|
2638
|
+
const seen = new Set(faces.map((f) => `${f.family}|${f.weight}`));
|
|
2639
|
+
const add = (face) => {
|
|
2640
|
+
if (!face) return;
|
|
2641
|
+
const key = `${face.family}|${face.weight}`;
|
|
2642
|
+
if (seen.has(key)) return;
|
|
2643
|
+
seen.add(key);
|
|
2644
|
+
faces.push(face);
|
|
2645
|
+
};
|
|
2646
|
+
for (const o of doc.overlays ?? []) {
|
|
2647
|
+
if (o.kind !== "text") continue;
|
|
2648
|
+
add(overlayFaceFor(o));
|
|
2649
|
+
for (const f of styledTextOf(o)?.faces ?? []) {
|
|
2650
|
+
add({ family: f.f, weight: f.w, url: f.u });
|
|
2454
2651
|
}
|
|
2455
2652
|
}
|
|
2456
|
-
return
|
|
2457
|
-
(s) => s.events.length >= TYPING_MIN_PINGS && s.last - s.first >= TYPING_MIN_DUR
|
|
2458
|
-
);
|
|
2653
|
+
return faces;
|
|
2459
2654
|
}
|
|
2460
|
-
function
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
return [clamp012(px / width), clamp012(py / height)];
|
|
2655
|
+
function overlayLines(text) {
|
|
2656
|
+
const lines = text.split("\n");
|
|
2657
|
+
return lines.length ? lines : [""];
|
|
2464
2658
|
}
|
|
2465
|
-
function
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
const sourceDuration = trackEnd;
|
|
2490
|
-
const len = Math.max(1, sourceDuration * 0.05);
|
|
2491
|
-
const taken = [...reserved];
|
|
2492
|
-
const accepted = [];
|
|
2493
|
-
const centers = [];
|
|
2494
|
-
for (const c of sorted) {
|
|
2495
|
-
if (centers.some((t) => Math.abs(t - c.center) < DWELL_SPACING)) continue;
|
|
2496
|
-
const spanIn = Math.max(
|
|
2497
|
-
0,
|
|
2498
|
-
Math.min(c.center - len / 2, sourceDuration - len)
|
|
2499
|
-
);
|
|
2500
|
-
const spanOut = Math.min(sourceDuration, spanIn + len);
|
|
2501
|
-
if (spanOut - spanIn < 0.3) continue;
|
|
2502
|
-
if (taken.some((z) => spanIn < z.out && spanOut > z.in)) continue;
|
|
2503
|
-
const span = {
|
|
2504
|
-
id: `d${accepted.length}`,
|
|
2505
|
-
in: round2(spanIn),
|
|
2506
|
-
out: round2(spanOut),
|
|
2507
|
-
// No element rect on moves → point zoom at the planner ceiling.
|
|
2508
|
-
level: clampZoomLevel(maxLevel),
|
|
2509
|
-
cx: round2(clamp012(c.cx)),
|
|
2510
|
-
cy: round2(clamp012(c.cy)),
|
|
2511
|
-
source: "auto"
|
|
2512
|
-
};
|
|
2513
|
-
accepted.push(span);
|
|
2514
|
-
taken.push(span);
|
|
2515
|
-
centers.push(c.center);
|
|
2659
|
+
function overlayPlainText(clip) {
|
|
2660
|
+
return plainText(clip.text);
|
|
2661
|
+
}
|
|
2662
|
+
function familyEntryOf(clip) {
|
|
2663
|
+
return findFontFamily2(
|
|
2664
|
+
clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
|
|
2665
|
+
) ?? null;
|
|
2666
|
+
}
|
|
2667
|
+
function snapRunWeight(clip, weight) {
|
|
2668
|
+
const entry = familyEntryOf(clip);
|
|
2669
|
+
return entry ? nearestFontWeight2(entry, weight) : weight;
|
|
2670
|
+
}
|
|
2671
|
+
var BOLD_WEIGHT = 700;
|
|
2672
|
+
function boldWeightFor(clip) {
|
|
2673
|
+
return snapRunWeight(
|
|
2674
|
+
clip,
|
|
2675
|
+
Math.max(BOLD_WEIGHT, resolveOverlayStyle(clip).weight)
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
function hashOf(text) {
|
|
2679
|
+
let h = 2166136261;
|
|
2680
|
+
for (let i = 0; i < text.length; i++) {
|
|
2681
|
+
h ^= text.charCodeAt(i);
|
|
2682
|
+
h = Math.imul(h, 16777619);
|
|
2516
2683
|
}
|
|
2517
|
-
return
|
|
2684
|
+
return (h >>> 0).toString(36);
|
|
2518
2685
|
}
|
|
2519
|
-
function
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
sy += c.y;
|
|
2686
|
+
function styledTextOf(clip) {
|
|
2687
|
+
const runs = normalizeRuns(clip.text);
|
|
2688
|
+
if (typeof runs === "string") return null;
|
|
2689
|
+
const style = resolveOverlayStyle(clip);
|
|
2690
|
+
const italic = style.fontStyle === "italic";
|
|
2691
|
+
const fs = [
|
|
2692
|
+
{ w: style.weight, ...italic ? { i: 1 } : {} }
|
|
2693
|
+
];
|
|
2694
|
+
const fontOf = (w, i) => {
|
|
2695
|
+
let at = fs.findIndex((f) => f.w === w && !!f.i === i);
|
|
2696
|
+
if (at < 0) {
|
|
2697
|
+
fs.push({ w, ...i ? { i: 1 } : {} });
|
|
2698
|
+
at = fs.length - 1;
|
|
2533
2699
|
}
|
|
2700
|
+
return at;
|
|
2701
|
+
};
|
|
2702
|
+
const l = [[]];
|
|
2703
|
+
for (const run of runs) {
|
|
2704
|
+
const f = fontOf(
|
|
2705
|
+
run.weight !== void 0 ? snapRunWeight(clip, run.weight) : style.weight,
|
|
2706
|
+
run.italic ?? italic
|
|
2707
|
+
);
|
|
2708
|
+
const set = {
|
|
2709
|
+
...f ? { f } : {},
|
|
2710
|
+
...run.color ? { c: run.color } : {},
|
|
2711
|
+
...run.underline ? { u: 1 } : {},
|
|
2712
|
+
...run.strike ? { s: 1 } : {},
|
|
2713
|
+
...run.highlight ? { h: run.highlight } : {}
|
|
2714
|
+
};
|
|
2715
|
+
run.text.split("\n").forEach((part, i) => {
|
|
2716
|
+
if (i > 0) l.push([]);
|
|
2717
|
+
if (part) l[l.length - 1].push({ t: part, ...set });
|
|
2718
|
+
});
|
|
2534
2719
|
}
|
|
2535
|
-
const
|
|
2536
|
-
const
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2720
|
+
const entry = familyEntryOf(clip);
|
|
2721
|
+
const faces = [];
|
|
2722
|
+
for (const font of fs.slice(1)) {
|
|
2723
|
+
if (!entry || font.w === style.weight) continue;
|
|
2724
|
+
if (faces.some((x) => x.w === font.w)) continue;
|
|
2725
|
+
const inBase = OVERLAY_FONT_FACES.some(
|
|
2726
|
+
(b) => b.family === entry.family && b.weight === font.w
|
|
2727
|
+
);
|
|
2728
|
+
if (!inBase) {
|
|
2729
|
+
faces.push({
|
|
2730
|
+
f: entry.family,
|
|
2731
|
+
w: font.w,
|
|
2732
|
+
u: fontFaceUrl2(entry.slug, font.w)
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return {
|
|
2737
|
+
l,
|
|
2738
|
+
fs,
|
|
2739
|
+
...faces.length ? { faces } : {},
|
|
2740
|
+
h: hashOf(JSON.stringify([l, fs]))
|
|
2741
|
+
};
|
|
2541
2742
|
}
|
|
2542
|
-
function
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2743
|
+
function overlayTokens(line) {
|
|
2744
|
+
return line.match(/\S+\s*/g) ?? [line];
|
|
2745
|
+
}
|
|
2746
|
+
function wrapOverlayLines(lines, measure, maxPx) {
|
|
2747
|
+
if (!(maxPx > 0)) return lines;
|
|
2748
|
+
const out = [];
|
|
2749
|
+
for (const line of lines) {
|
|
2750
|
+
if (!line || measure(line) <= maxPx) {
|
|
2751
|
+
out.push(line);
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
let current = "";
|
|
2755
|
+
for (const token of overlayTokens(line)) {
|
|
2756
|
+
if (!current) {
|
|
2757
|
+
current = token;
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
if (measure(current + token) <= maxPx) {
|
|
2761
|
+
current += token;
|
|
2762
|
+
} else {
|
|
2763
|
+
out.push(current);
|
|
2764
|
+
current = token;
|
|
2765
|
+
}
|
|
2562
2766
|
}
|
|
2767
|
+
if (current) out.push(current);
|
|
2563
2768
|
}
|
|
2564
|
-
|
|
2565
|
-
const rect = rects > 0 ? {
|
|
2566
|
-
x: clamp012(x0 / width),
|
|
2567
|
-
y: clamp012(y0 / height),
|
|
2568
|
-
w: clamp012((x1 - x0) / width),
|
|
2569
|
-
h: clamp012((y1 - y0) / height)
|
|
2570
|
-
} : null;
|
|
2571
|
-
return { cx: clamp012(sx / n / width), cy: clamp012(sy / n / height), rect };
|
|
2769
|
+
return out.length ? out : [""];
|
|
2572
2770
|
}
|
|
2573
|
-
function
|
|
2574
|
-
|
|
2771
|
+
function graphemesOf2(line) {
|
|
2772
|
+
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
|
|
2773
|
+
return [
|
|
2774
|
+
...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
|
|
2775
|
+
line
|
|
2776
|
+
)
|
|
2777
|
+
].map((s) => s.segment);
|
|
2778
|
+
}
|
|
2779
|
+
return [...line];
|
|
2575
2780
|
}
|
|
2576
|
-
function
|
|
2577
|
-
|
|
2781
|
+
function overlaySegments(text, unit) {
|
|
2782
|
+
const lines = overlayLines(text);
|
|
2783
|
+
if (unit === "block") return [];
|
|
2784
|
+
if (unit === "line") return lines.map((l) => [l]);
|
|
2785
|
+
if (unit === "word") return lines.map(overlayTokens);
|
|
2786
|
+
return lines.map(graphemesOf2);
|
|
2578
2787
|
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2788
|
+
var FX_DIR = { forward: 0, reverse: 1, center: 2 };
|
|
2789
|
+
function resolveOverlayFx(clip, clipDuration) {
|
|
2790
|
+
const spec = overlayFxSpec(clip) ?? clip.fx ?? null;
|
|
2791
|
+
if (!spec) return null;
|
|
2792
|
+
const unit = spec.unit ?? "block";
|
|
2793
|
+
const text = overlayPlainText(clip);
|
|
2794
|
+
const units = overlaySegments(text, unit);
|
|
2795
|
+
const n = unit === "block" ? 1 : units.reduce((sum, line) => sum + line.length, 0);
|
|
2796
|
+
const upperN = unit === "line" && clip.maxWidth ? overlayLines(text).reduce(
|
|
2797
|
+
(sum, line) => sum + overlayTokens(line).length,
|
|
2798
|
+
0
|
|
2799
|
+
) : n;
|
|
2800
|
+
const typewriter = spec.fx === "typewriter";
|
|
2801
|
+
const dur = typewriter ? 1e-3 : Math.min(2, Math.max(0.05, spec.duration ?? OVERLAY_TRANSITION_DUR));
|
|
2802
|
+
const defaultStagger = typewriter ? 0.05 : unit === "block" ? 0 : 0.06;
|
|
2803
|
+
let st = Math.min(2, Math.max(0, spec.stagger ?? defaultStagger));
|
|
2804
|
+
if (upperN > 1) {
|
|
2805
|
+
const maxTotal = Math.max(dur, clipDuration * 0.9);
|
|
2806
|
+
st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
|
|
2807
|
+
}
|
|
2808
|
+
const round14 = (v) => Math.round(v * 1e4) / 1e4;
|
|
2809
|
+
return {
|
|
2810
|
+
k: spec.fx,
|
|
2811
|
+
u: unit,
|
|
2812
|
+
d: FX_DIR[spec.direction ?? "forward"] ?? 0,
|
|
2813
|
+
st: round14(st),
|
|
2814
|
+
dur: round14(dur),
|
|
2815
|
+
tt: round14(st * (upperN - 1) + dur),
|
|
2816
|
+
units,
|
|
2817
|
+
n
|
|
2818
|
+
};
|
|
2581
2819
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
const
|
|
2603
|
-
const
|
|
2604
|
-
const
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
const dw2 = vw * sc2;
|
|
2616
|
-
const dh2 = vh * sc2;
|
|
2617
|
-
const follow = frame.focusFollow === "camera" && zoomFocus ? zoomFocus : null;
|
|
2618
|
-
const fcx = clamp013(follow ? follow.cx : frame.focus?.cx ?? 0.5);
|
|
2619
|
-
const fcy = clamp013(follow ? follow.cy : frame.focus?.cy ?? 0.5);
|
|
2620
|
-
const vTop = padT + barH;
|
|
2621
|
-
const dx2 = Math.min(
|
|
2622
|
-
padL,
|
|
2623
|
-
Math.max(padL + availW - dw2, padL + availW / 2 - fcx * dw2)
|
|
2820
|
+
function overlayFontString(style, scale, s) {
|
|
2821
|
+
const italic = style.fontStyle === "italic" ? "italic " : "";
|
|
2822
|
+
return `${italic}${style.weight} ${style.size * scale * s}px ${style.stack}`;
|
|
2823
|
+
}
|
|
2824
|
+
function overlayRect(clip, measure, frameW, frameH = 1080, mediaAspect) {
|
|
2825
|
+
const scale = clip.transform.scale || 1;
|
|
2826
|
+
const base = {
|
|
2827
|
+
cx: clip.transform.x * frameW,
|
|
2828
|
+
cy: clip.transform.y * frameH,
|
|
2829
|
+
rotation: clip.transform.rotation || 0
|
|
2830
|
+
};
|
|
2831
|
+
if (clip.kind === "html") {
|
|
2832
|
+
const pb = htmlLayerPictureBox(clip);
|
|
2833
|
+
const w2 = htmlLayerWidth(clip) * frameW * scale;
|
|
2834
|
+
return { ...base, w: w2, h: w2 * (pb.height / pb.width) };
|
|
2835
|
+
}
|
|
2836
|
+
if (clip.kind !== "text") {
|
|
2837
|
+
const w2 = (clip.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH) * frameW * scale;
|
|
2838
|
+
return { ...base, w: w2, h: w2 / (mediaAspect || 16 / 9) };
|
|
2839
|
+
}
|
|
2840
|
+
const style = resolveOverlayStyle(clip);
|
|
2841
|
+
const font = overlayFontString(style, scale, 1);
|
|
2842
|
+
const ls = style.letterSpacing * scale;
|
|
2843
|
+
const styled = styledTextOf(clip);
|
|
2844
|
+
let w = 0;
|
|
2845
|
+
let lineCount = 0;
|
|
2846
|
+
if (styled) {
|
|
2847
|
+
const fonts = styled.fs.map(
|
|
2848
|
+
(f) => overlayFontString(
|
|
2849
|
+
{ ...style, weight: f.w, fontStyle: f.i ? "italic" : "normal" },
|
|
2850
|
+
scale,
|
|
2851
|
+
1
|
|
2852
|
+
)
|
|
2624
2853
|
);
|
|
2625
|
-
const
|
|
2626
|
-
|
|
2627
|
-
|
|
2854
|
+
const laid = layoutText(
|
|
2855
|
+
{ lines: styled.l, maxWidth: clip.maxWidth ? clip.maxWidth * frameW : 0 },
|
|
2856
|
+
(t, f) => measure(t, fonts[f], ls)
|
|
2628
2857
|
);
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
};
|
|
2858
|
+
w = laid.width;
|
|
2859
|
+
lineCount = laid.lines.length;
|
|
2860
|
+
} else {
|
|
2861
|
+
const width = (t) => measure(t, font, ls);
|
|
2862
|
+
const lines = clip.maxWidth ? wrapOverlayLines(
|
|
2863
|
+
overlayLines(overlayPlainText(clip)),
|
|
2864
|
+
width,
|
|
2865
|
+
clip.maxWidth * frameW
|
|
2866
|
+
) : overlayLines(overlayPlainText(clip));
|
|
2867
|
+
for (const line of lines) w = Math.max(w, width(line));
|
|
2868
|
+
lineCount = lines.length;
|
|
2641
2869
|
}
|
|
2642
|
-
const
|
|
2643
|
-
const
|
|
2644
|
-
const
|
|
2645
|
-
const
|
|
2646
|
-
const dy = padT + barH + (availH - dh) / 2;
|
|
2870
|
+
const lineH = style.size * scale * style.lineHeight;
|
|
2871
|
+
const box = resolveOverlayBox(clip);
|
|
2872
|
+
const padX = box ? box.padX * scale : 0;
|
|
2873
|
+
const padY = box ? box.padY * scale : 0;
|
|
2647
2874
|
return {
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
dw,
|
|
2653
|
-
dh,
|
|
2654
|
-
cardX: dx,
|
|
2655
|
-
cardY: dy - barH,
|
|
2656
|
-
cardW: dw,
|
|
2657
|
-
cardH: dh + barH
|
|
2875
|
+
...base,
|
|
2876
|
+
w: Math.max(w, style.size * scale * 0.6) + padX * 2,
|
|
2877
|
+
// empty text still selectable
|
|
2878
|
+
h: lineCount * lineH + padY * 2
|
|
2658
2879
|
};
|
|
2659
2880
|
}
|
|
2660
|
-
function
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
Math.
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
{
|
|
2672
|
-
width: sm.captureWidth ?? sm.width,
|
|
2673
|
-
height: sm.captureHeight ?? sm.height
|
|
2674
|
-
},
|
|
2675
|
-
W,
|
|
2676
|
-
H
|
|
2677
|
-
);
|
|
2881
|
+
function overlayHit(rect, px, py, padPx = 8) {
|
|
2882
|
+
let dx = px - rect.cx;
|
|
2883
|
+
let dy = py - rect.cy;
|
|
2884
|
+
if (rect.rotation) {
|
|
2885
|
+
const a = -rect.rotation * Math.PI / 180;
|
|
2886
|
+
const rx = dx * Math.cos(a) - dy * Math.sin(a);
|
|
2887
|
+
const ry = dx * Math.sin(a) + dy * Math.cos(a);
|
|
2888
|
+
dx = rx;
|
|
2889
|
+
dy = ry;
|
|
2890
|
+
}
|
|
2891
|
+
return Math.abs(dx) <= rect.w / 2 + padPx && Math.abs(dy) <= rect.h / 2 + padPx;
|
|
2678
2892
|
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2684
|
-
for (
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2893
|
+
|
|
2894
|
+
// src/richText/migrateEmphasis.ts
|
|
2895
|
+
function parseLine(line) {
|
|
2896
|
+
const chars = [];
|
|
2897
|
+
const marker = [];
|
|
2898
|
+
for (let i = 0; i < line.length; i++) {
|
|
2899
|
+
if (line[i] === "\\" && line[i + 1] === "*") {
|
|
2900
|
+
chars.push("*");
|
|
2901
|
+
marker.push(false);
|
|
2902
|
+
i++;
|
|
2903
|
+
} else {
|
|
2904
|
+
chars.push(line[i]);
|
|
2905
|
+
marker.push(line[i] === "*");
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
const keep = chars.map(() => true);
|
|
2909
|
+
const bold = [];
|
|
2910
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2911
|
+
if (!marker[i]) continue;
|
|
2912
|
+
let j = i + 1;
|
|
2913
|
+
while (j < chars.length && !marker[j]) j++;
|
|
2914
|
+
if (j >= chars.length) break;
|
|
2915
|
+
if (!chars.slice(i + 1, j).join("").trim()) {
|
|
2916
|
+
i = j - 1;
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
keep[i] = false;
|
|
2920
|
+
keep[j] = false;
|
|
2921
|
+
for (let k = i + 1; k < j; k++) bold[k] = true;
|
|
2922
|
+
i = j;
|
|
2688
2923
|
}
|
|
2689
|
-
|
|
2924
|
+
const out = [];
|
|
2925
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2926
|
+
if (keep[i]) out.push({ c: chars[i], b: !!bold[i] && !/\s/.test(chars[i]) });
|
|
2927
|
+
}
|
|
2928
|
+
return out;
|
|
2690
2929
|
}
|
|
2691
|
-
function
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
const
|
|
2696
|
-
|
|
2930
|
+
function marksEmphasis(text) {
|
|
2931
|
+
return text.split("\n").some((line) => parseLine(line).some((x) => x.b));
|
|
2932
|
+
}
|
|
2933
|
+
function migrateTextClip(clip) {
|
|
2934
|
+
const legacy = clip;
|
|
2935
|
+
if (!("emphasis" in legacy)) return clip;
|
|
2936
|
+
const { emphasis, ...rest } = legacy;
|
|
2937
|
+
const next = rest;
|
|
2938
|
+
if (!emphasis || typeof legacy.text !== "string") return next;
|
|
2939
|
+
const weight = emphasis.weight !== void 0 ? snapRunWeight(next, emphasis.weight) : boldWeightFor(next);
|
|
2940
|
+
const runs = [];
|
|
2941
|
+
legacy.text.split("\n").forEach((line, li) => {
|
|
2942
|
+
if (li > 0) runs.push({ text: "\n" });
|
|
2943
|
+
for (const { c, b } of parseLine(line)) {
|
|
2944
|
+
runs.push(
|
|
2945
|
+
b ? {
|
|
2946
|
+
text: c,
|
|
2947
|
+
weight,
|
|
2948
|
+
...emphasis.color ? { color: emphasis.color } : {}
|
|
2949
|
+
} : { text: c }
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
});
|
|
2953
|
+
return { ...next, text: normalizeRuns(runs) };
|
|
2954
|
+
}
|
|
2955
|
+
function migrateText(doc) {
|
|
2956
|
+
const overlays = Array.isArray(doc.overlays) ? doc.overlays : [];
|
|
2957
|
+
const legacy = overlays.some((o) => o.kind === "text" && "emphasis" in o);
|
|
2958
|
+
if (!legacy) return doc;
|
|
2697
2959
|
return {
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
size,
|
|
2701
|
-
radius: cam.shape === "rounded" ? (cam.radius ?? 18) * s : size / 2
|
|
2960
|
+
...doc,
|
|
2961
|
+
overlays: overlays.map((o) => o.kind === "text" ? migrateTextClip(o) : o)
|
|
2702
2962
|
};
|
|
2703
2963
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
if (
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2964
|
+
|
|
2965
|
+
// src/zoomStyle.ts
|
|
2966
|
+
var RETIRED_ZOOM_STYLES = {
|
|
2967
|
+
keynote: { style: "glide", tilt: "medium" },
|
|
2968
|
+
drift: { style: "cinema", tilt: "subtle" }
|
|
2969
|
+
};
|
|
2970
|
+
function liveZoomStyleName(name) {
|
|
2971
|
+
if (!name) return null;
|
|
2972
|
+
if (name in ZOOM_STYLES) return name;
|
|
2973
|
+
return RETIRED_ZOOM_STYLES[name]?.style ?? null;
|
|
2974
|
+
}
|
|
2975
|
+
var HOLD_DRIFT_MAX = 0.15;
|
|
2976
|
+
var PUMP_REST = 1;
|
|
2977
|
+
function pumpFreeChainGap(p) {
|
|
2978
|
+
return p.rampOut + (p.rampIn - p.rampInOverlap) + PUMP_REST;
|
|
2979
|
+
}
|
|
2980
|
+
var glide = {
|
|
2981
|
+
autoZoom: true,
|
|
2982
|
+
clusterGap: 3,
|
|
2983
|
+
minClusterClicks: 1,
|
|
2984
|
+
targetFill: 0.42,
|
|
2985
|
+
minLevel: 1.3,
|
|
2986
|
+
maxLevel: 1.8,
|
|
2987
|
+
dragLevel: 1.5,
|
|
2988
|
+
lead: 0.35,
|
|
2989
|
+
hold: 1.2,
|
|
2990
|
+
followByDefault: true,
|
|
2991
|
+
typingZoom: true,
|
|
2992
|
+
typingGap: 2.5,
|
|
2993
|
+
typingHold: 1.1,
|
|
2994
|
+
typingMinLevel: 1.4,
|
|
2995
|
+
rampIn: 0.6,
|
|
2996
|
+
rampInOverlap: 0.2,
|
|
2997
|
+
rampOut: 0.5,
|
|
2998
|
+
chainGap: 3,
|
|
2999
|
+
pan: 0.55,
|
|
3000
|
+
// The reference's corner trace at 60 fps: a quarter of the move by a
|
|
3001
|
+
// fifth of the time, two thirds by half, nine tenths by three
|
|
3002
|
+
// quarters, over ~0.6 s. Fitted against those three points,
|
|
3003
|
+
// (0.36, 0, 0.4, 1) is the closest cubic (15 / 64 / 93 %; half the
|
|
3004
|
+
// move at ~245 ms and 90 % at ~425 ms at 0.6 s): a gentle start and a
|
|
3005
|
+
// long settle. CSS ease (0.25, 0.1, 0.25, 1) reached the midpoint
|
|
3006
|
+
// 70 ms earlier and read a touch fast; (0.16, 1, 0.3, 1) put half the
|
|
3007
|
+
// move in the first sixty milliseconds and read as a twitch.
|
|
3008
|
+
ease: "css-bezier(0.36, 0, 0.4, 1)",
|
|
3009
|
+
panEase: "css-bezier(0.36, 0, 0.4, 1)",
|
|
3010
|
+
followSafeRatio: 0.45,
|
|
3011
|
+
followRecenter: 0.55,
|
|
3012
|
+
followLookahead: 0.4,
|
|
3013
|
+
tilt: {
|
|
3014
|
+
intensity: "off",
|
|
3015
|
+
rampIn: 0.6,
|
|
3016
|
+
rampOut: 0.5,
|
|
3017
|
+
chainGap: 3,
|
|
3018
|
+
pan: 0.55
|
|
2729
3019
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
)
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
3020
|
+
};
|
|
3021
|
+
var ZOOM_STYLES = {
|
|
3022
|
+
glide,
|
|
3023
|
+
// Screen Studio "Focused" / Recordly — a block per click, framed and
|
|
3024
|
+
// released, no travel. The spring-settled family ease (near-linear onset,
|
|
3025
|
+
// long settle) at the default's tempo: 0.8 s in, 0.65 s out, and a beat
|
|
3026
|
+
// of 1.5 s after the click so the block is read before it goes. At its
|
|
3027
|
+
// 2026-07 timing (1.4 s in, 1.0 s out around a 1.35 s span) a lone click
|
|
3028
|
+
// spent 74% of its beat moving and every dwell or sped-up click was
|
|
3029
|
+
// shorter than the ramp, which the lowering compressed into a cut.
|
|
3030
|
+
focus: {
|
|
3031
|
+
autoZoom: true,
|
|
3032
|
+
clusterGap: 1.5,
|
|
3033
|
+
minClusterClicks: 1,
|
|
3034
|
+
targetFill: 0.5,
|
|
3035
|
+
minLevel: 1.4,
|
|
3036
|
+
maxLevel: 2,
|
|
3037
|
+
dragLevel: 1.6,
|
|
3038
|
+
lead: 0.35,
|
|
3039
|
+
hold: 1.5,
|
|
3040
|
+
followByDefault: false,
|
|
3041
|
+
typingZoom: true,
|
|
3042
|
+
typingGap: 2,
|
|
3043
|
+
typingHold: 1.3,
|
|
3044
|
+
typingMinLevel: 1.5,
|
|
3045
|
+
rampIn: 0.8,
|
|
3046
|
+
rampInOverlap: 0.25,
|
|
3047
|
+
rampOut: 0.65,
|
|
3048
|
+
chainGap: 2.4,
|
|
3049
|
+
pan: 0.7,
|
|
3050
|
+
ease: "css-bezier(0.28, 0.03, 0.09, 1)",
|
|
3051
|
+
panEase: "css-bezier(0.26, 0.08, 0.2, 1)",
|
|
3052
|
+
followSafeRatio: 0.5,
|
|
3053
|
+
followRecenter: 0.55,
|
|
3054
|
+
followLookahead: 0,
|
|
3055
|
+
tilt: {
|
|
3056
|
+
intensity: "off",
|
|
3057
|
+
rampIn: 0.8,
|
|
3058
|
+
rampOut: 0.65,
|
|
3059
|
+
chainGap: 2.4,
|
|
3060
|
+
pan: 0.7
|
|
3061
|
+
}
|
|
3062
|
+
},
|
|
3063
|
+
// Screen Studio "Smooth" — the long-HOLD style: 1.2 s in, 1.0 s out,
|
|
3064
|
+
// two-second holds, chains across 3.5 s so a slow clip travels rather
|
|
3065
|
+
// than pumps, and the default's level range, because a watched clip
|
|
3066
|
+
// wants the frame, not the magnifier. Its ramps still exceed a dwell or
|
|
3067
|
+
// a sped-up click; the lowering fits them to the room.
|
|
3068
|
+
cinema: {
|
|
3069
|
+
autoZoom: true,
|
|
3070
|
+
clusterGap: 2.4,
|
|
3071
|
+
minClusterClicks: 1,
|
|
3072
|
+
targetFill: 0.42,
|
|
3073
|
+
minLevel: 1.3,
|
|
3074
|
+
maxLevel: 1.8,
|
|
3075
|
+
dragLevel: 1.5,
|
|
3076
|
+
lead: 0.6,
|
|
3077
|
+
hold: 2,
|
|
3078
|
+
followByDefault: true,
|
|
3079
|
+
typingZoom: true,
|
|
3080
|
+
typingGap: 3,
|
|
3081
|
+
typingHold: 1.8,
|
|
3082
|
+
typingMinLevel: 1.4,
|
|
3083
|
+
rampIn: 1.2,
|
|
3084
|
+
rampInOverlap: 0.4,
|
|
3085
|
+
rampOut: 1,
|
|
3086
|
+
chainGap: 3.5,
|
|
3087
|
+
pan: 1.1,
|
|
3088
|
+
ease: "css-bezier(0.33, 0, 0.15, 1)",
|
|
3089
|
+
panEase: "css-bezier(0.33, 0, 0.22, 1)",
|
|
3090
|
+
followSafeRatio: 0.6,
|
|
3091
|
+
followRecenter: 0.9,
|
|
3092
|
+
followLookahead: 0.5,
|
|
3093
|
+
tilt: {
|
|
3094
|
+
intensity: "off",
|
|
3095
|
+
rampIn: 1.2,
|
|
3096
|
+
rampOut: 1,
|
|
3097
|
+
chainGap: 3.5,
|
|
3098
|
+
pan: 1.1
|
|
3099
|
+
}
|
|
3100
|
+
},
|
|
3101
|
+
// The studio's original pace with the instant-velocity onset defect fixed:
|
|
3102
|
+
// 0.4 s cycles on the onset-capped ease, the 2.5× ceiling of the 2026-07
|
|
3103
|
+
// comparison's defect two brought to 2.2, a chain gap past its pump band.
|
|
3104
|
+
snappy: {
|
|
3105
|
+
autoZoom: true,
|
|
3106
|
+
clusterGap: 1.2,
|
|
3107
|
+
minClusterClicks: 1,
|
|
3108
|
+
targetFill: 0.5,
|
|
3109
|
+
minLevel: 1.4,
|
|
3110
|
+
maxLevel: 2.2,
|
|
3111
|
+
dragLevel: 1.6,
|
|
3112
|
+
lead: 0.25,
|
|
3113
|
+
hold: 1.2,
|
|
3114
|
+
followByDefault: false,
|
|
3115
|
+
typingZoom: true,
|
|
3116
|
+
typingGap: 1.6,
|
|
3117
|
+
typingHold: 0.9,
|
|
3118
|
+
typingMinLevel: 1.5,
|
|
3119
|
+
rampIn: 0.4,
|
|
3120
|
+
rampInOverlap: 0.15,
|
|
3121
|
+
rampOut: 0.4,
|
|
3122
|
+
chainGap: 2,
|
|
3123
|
+
pan: 0.5,
|
|
3124
|
+
ease: "css-bezier(0.3, 0.55, 0.2, 1)",
|
|
3125
|
+
panEase: "css-bezier(0.25, 0.1, 0.25, 1)",
|
|
3126
|
+
followSafeRatio: 0.5,
|
|
3127
|
+
followRecenter: 0.45,
|
|
3128
|
+
followLookahead: 0,
|
|
3129
|
+
tilt: {
|
|
3130
|
+
intensity: "off",
|
|
3131
|
+
rampIn: 0.4,
|
|
3132
|
+
rampOut: 0.4,
|
|
3133
|
+
chainGap: 2,
|
|
3134
|
+
pan: 0.5
|
|
3135
|
+
}
|
|
3136
|
+
},
|
|
3137
|
+
// Screen Studio's "instant zoom" — a cut-in (eight frames at 60 fps),
|
|
3138
|
+
// tutorial tempo. Untouched in motion (the one style that never pumped
|
|
3139
|
+
// and never left a ramp unsettled); the ceiling comes down with the
|
|
3140
|
+
// others and the hold gains the beat a cut-in needs to be read.
|
|
3141
|
+
cut: {
|
|
3142
|
+
autoZoom: true,
|
|
3143
|
+
clusterGap: 1.2,
|
|
3144
|
+
minClusterClicks: 1,
|
|
3145
|
+
targetFill: 0.5,
|
|
3146
|
+
minLevel: 1.4,
|
|
3147
|
+
maxLevel: 2,
|
|
3148
|
+
dragLevel: 1.6,
|
|
3149
|
+
lead: 0.2,
|
|
3150
|
+
hold: 1.2,
|
|
3151
|
+
followByDefault: false,
|
|
3152
|
+
typingZoom: true,
|
|
3153
|
+
typingGap: 1.2,
|
|
3154
|
+
typingHold: 0.6,
|
|
3155
|
+
typingMinLevel: 1.5,
|
|
3156
|
+
rampIn: 0.14,
|
|
3157
|
+
rampInOverlap: 0.07,
|
|
3158
|
+
rampOut: 0.14,
|
|
3159
|
+
chainGap: 1.3,
|
|
3160
|
+
pan: 0.35,
|
|
3161
|
+
ease: "css-bezier(0.2, 0, 0.4, 1)",
|
|
3162
|
+
panEase: "css-bezier(0.2, 0, 0.4, 1)",
|
|
3163
|
+
followSafeRatio: 0.5,
|
|
3164
|
+
followRecenter: 0.35,
|
|
3165
|
+
followLookahead: 0,
|
|
3166
|
+
tilt: {
|
|
3167
|
+
intensity: "off",
|
|
3168
|
+
rampIn: 0.3,
|
|
3169
|
+
rampOut: 0.3,
|
|
3170
|
+
chainGap: 1.3,
|
|
3171
|
+
pan: 0.35
|
|
3172
|
+
}
|
|
3173
|
+
},
|
|
3174
|
+
// Auto-zoom off (every competitor ships this switch). Camera params still
|
|
3175
|
+
// apply to MANUAL spans — they move the way the default camera moves
|
|
3176
|
+
// TODAY (spelled as a spread, never a copy that fossilises).
|
|
3177
|
+
none: { ...glide, autoZoom: false }
|
|
3178
|
+
};
|
|
3179
|
+
var DEFAULT_ZOOM_STYLE = "glide";
|
|
3180
|
+
function resolveZoomStyle(name, overrides) {
|
|
3181
|
+
const live = liveZoomStyleName(name) ?? DEFAULT_ZOOM_STYLE;
|
|
3182
|
+
const params = { ...ZOOM_STYLES[live], ...overrides };
|
|
3183
|
+
const floor = pumpFreeChainGap(params);
|
|
3184
|
+
if (params.chainGap < floor) params.chainGap = floor;
|
|
3185
|
+
return params;
|
|
2747
3186
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
3187
|
+
var ZOOM_STYLE_OPTIONS = [
|
|
3188
|
+
{
|
|
3189
|
+
name: "glide",
|
|
3190
|
+
label: "Glide",
|
|
3191
|
+
hint: "One steady zoom that travels between clicks"
|
|
3192
|
+
},
|
|
3193
|
+
{
|
|
3194
|
+
name: "focus",
|
|
3195
|
+
label: "Focus",
|
|
3196
|
+
hint: "A zoom per click, framed and released"
|
|
3197
|
+
},
|
|
3198
|
+
{
|
|
3199
|
+
name: "cinema",
|
|
3200
|
+
label: "Cinema",
|
|
3201
|
+
hint: "Slow moves and long holds, for content that is watched"
|
|
3202
|
+
},
|
|
3203
|
+
{ name: "snappy", label: "Snappy", hint: "Quick, energetic zoom cycles" },
|
|
3204
|
+
{ name: "cut", label: "Cut", hint: "Instant zooms, no glide" },
|
|
3205
|
+
{
|
|
3206
|
+
name: "none",
|
|
3207
|
+
label: "None",
|
|
3208
|
+
hint: "No automatic zooms, add your own on the timeline"
|
|
2756
3209
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
3210
|
+
];
|
|
3211
|
+
|
|
3212
|
+
// src/docVersion.ts
|
|
3213
|
+
var DOC_SCHEMA_VERSION = 6;
|
|
3214
|
+
function migrateHostedDoc(raw) {
|
|
3215
|
+
const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
|
|
3216
|
+
const read = migrateText(raw);
|
|
3217
|
+
if (version >= DOC_SCHEMA_VERSION) return read;
|
|
3218
|
+
let doc = read;
|
|
3219
|
+
if (version < 4 && doc.source && typeof doc.source === "object") {
|
|
3220
|
+
doc = migrateMotion(doc);
|
|
2767
3221
|
}
|
|
2768
|
-
|
|
3222
|
+
if (version < 5) doc = migrateZoomStyle(doc);
|
|
3223
|
+
return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
|
|
2769
3224
|
}
|
|
2770
|
-
function
|
|
2771
|
-
const
|
|
3225
|
+
function migrateZoomStyle(doc) {
|
|
3226
|
+
const name = doc.zoomStyle;
|
|
3227
|
+
if (typeof name !== "string") return doc;
|
|
3228
|
+
const retired = RETIRED_ZOOM_STYLES[name];
|
|
3229
|
+
if (!retired) return doc;
|
|
2772
3230
|
return {
|
|
2773
|
-
|
|
2774
|
-
|
|
3231
|
+
...doc,
|
|
3232
|
+
zoomStyle: retired.style,
|
|
3233
|
+
tiltStyle: doc.tiltStyle ?? retired.tilt
|
|
2775
3234
|
};
|
|
2776
3235
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
3236
|
+
|
|
3237
|
+
// src/doc/studioDoc.ts
|
|
3238
|
+
var anchorKindOf = (doc) => "source" in doc ? "recording" : "program";
|
|
3239
|
+
var isRecordingDoc = (doc) => "source" in doc;
|
|
3240
|
+
var isProgramDoc = (doc) => !("source" in doc);
|
|
3241
|
+
function programDuration(doc) {
|
|
3242
|
+
const own = doc.program.duration;
|
|
3243
|
+
if (typeof own === "number" && own > 0) return own;
|
|
3244
|
+
const cfg = doc.program.config.duration;
|
|
3245
|
+
return typeof cfg === "number" && cfg > 0 ? cfg : 0;
|
|
2779
3246
|
}
|
|
2780
|
-
function
|
|
2781
|
-
return
|
|
3247
|
+
function anchorSourceDuration(doc) {
|
|
3248
|
+
return isRecordingDoc(doc) ? doc.source.meta.durationMs / 1e3 : programDuration(doc);
|
|
2782
3249
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
3250
|
+
|
|
3251
|
+
// src/planner/smoothing.ts
|
|
3252
|
+
var SNAP_BEFORE = 0.12;
|
|
3253
|
+
var SNAP_AFTER = 0.18;
|
|
3254
|
+
var SNAP_GAIN = 0.5;
|
|
3255
|
+
function sampleAt(points, t) {
|
|
3256
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
3257
|
+
if (t <= points[0].t) return { x: points[0].x, y: points[0].y };
|
|
3258
|
+
const last = points[points.length - 1];
|
|
3259
|
+
if (t >= last.t) return { x: last.x, y: last.y };
|
|
3260
|
+
for (let i = 1; i < points.length; i++) {
|
|
3261
|
+
if (points[i].t >= t) {
|
|
3262
|
+
const a = points[i - 1];
|
|
3263
|
+
const b = points[i];
|
|
3264
|
+
const f = (t - a.t) / (b.t - a.t || 1);
|
|
3265
|
+
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
return { x: last.x, y: last.y };
|
|
2785
3269
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
const
|
|
2790
|
-
|
|
3270
|
+
function smoothCursor(track, options = {}) {
|
|
3271
|
+
const factor = clamp2(options.factor ?? 0.15, 0.01, 1);
|
|
3272
|
+
const fps = options.fps ?? 60;
|
|
3273
|
+
const raw = track.filter((e) => e.type === "move" || e.type === "down" || e.type === "up").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y }));
|
|
3274
|
+
if (raw.length === 0) return [];
|
|
3275
|
+
const clicks = options.clickSnap ? track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y })) : [];
|
|
3276
|
+
const start = raw[0].t;
|
|
3277
|
+
const end = raw[raw.length - 1].t;
|
|
3278
|
+
const step = 1 / fps;
|
|
3279
|
+
const out = [];
|
|
3280
|
+
let cx = raw[0].x;
|
|
3281
|
+
let cy = raw[0].y;
|
|
3282
|
+
let ci = 0;
|
|
3283
|
+
for (let t = start; t <= end + 1e-9; t += step) {
|
|
3284
|
+
const target = sampleAt(raw, t);
|
|
3285
|
+
cx += (target.x - cx) * factor;
|
|
3286
|
+
cy += (target.y - cy) * factor;
|
|
3287
|
+
if (clicks.length) {
|
|
3288
|
+
while (ci < clicks.length - 1 && t > clicks[ci].t + SNAP_AFTER) ci++;
|
|
3289
|
+
const ck = clicks[ci];
|
|
3290
|
+
const u = clamp2(
|
|
3291
|
+
(t - (ck.t - SNAP_BEFORE)) / SNAP_BEFORE,
|
|
3292
|
+
0,
|
|
3293
|
+
t <= ck.t + SNAP_AFTER ? 1 : 0
|
|
3294
|
+
);
|
|
3295
|
+
const w = u * u * (3 - 2 * u) * SNAP_GAIN;
|
|
3296
|
+
if (w > 0) {
|
|
3297
|
+
cx += (ck.x - cx) * w;
|
|
3298
|
+
cy += (ck.y - cy) * w;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
out.push({ t, x: cx, y: cy });
|
|
3302
|
+
}
|
|
3303
|
+
return out;
|
|
2791
3304
|
}
|
|
2792
|
-
function
|
|
2793
|
-
|
|
2794
|
-
const fx = layout.dx + cx * layout.dw;
|
|
2795
|
-
const fy = layout.dy + cy * layout.dh;
|
|
2796
|
-
const { ox, oy } = screenPoint(layout, camera, screen);
|
|
2797
|
-
const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
|
|
2798
|
-
const wcx = fx + (ox - fx) / L * (1 - t);
|
|
2799
|
-
const wcy = fy + (oy - fy) / L * (1 - t);
|
|
2800
|
-
return { level: L, fx, fy, wcx, wcy, ox, oy };
|
|
3305
|
+
function clamp2(v, lo, hi) {
|
|
3306
|
+
return Math.max(lo, Math.min(hi, v));
|
|
2801
3307
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
3308
|
+
|
|
3309
|
+
// src/planner/autoZoom.ts
|
|
3310
|
+
var DWELL_MOVE_FRAC = 0.02;
|
|
3311
|
+
var DWELL_MIN = 0.45;
|
|
3312
|
+
var DWELL_MAX = 2.6;
|
|
3313
|
+
var DWELL_SPACING = 1.8;
|
|
3314
|
+
var CLUSTER_FILL = 0.85;
|
|
3315
|
+
var DRAG_FIT_LEVEL = 1.15;
|
|
3316
|
+
var PRESS_ECHO_MS = 4;
|
|
3317
|
+
var PRESS_ECHO_PX = 2;
|
|
3318
|
+
var DRAG_MIN_TRAVEL_FRAC = 0.015;
|
|
3319
|
+
var DRAG_MIN_S = 0.2;
|
|
3320
|
+
function dragsFromTrack(track, width, height) {
|
|
3321
|
+
if (!(width > 0) || !(height > 0)) return [];
|
|
3322
|
+
const downs = dedupePresses(track.filter((e) => e.type === "down"));
|
|
3323
|
+
const out = [];
|
|
3324
|
+
for (const d of downs) {
|
|
3325
|
+
const up = track.find((e) => e.type === "up" && e.t > d.t);
|
|
3326
|
+
if (!up) continue;
|
|
3327
|
+
const t0 = d.t / 1e3;
|
|
3328
|
+
const t1 = up.t / 1e3;
|
|
3329
|
+
if (t1 - t0 < DRAG_MIN_S) continue;
|
|
3330
|
+
let travel = 0;
|
|
3331
|
+
for (const e of track) {
|
|
3332
|
+
if (e.t < d.t) continue;
|
|
3333
|
+
if (e.t > up.t) break;
|
|
3334
|
+
if (e.type !== "move" && e.type !== "up") continue;
|
|
3335
|
+
const dist = Math.hypot(e.x - d.x, e.y - d.y) / width;
|
|
3336
|
+
if (dist > travel) travel = dist;
|
|
3337
|
+
}
|
|
3338
|
+
if (travel < DRAG_MIN_TRAVEL_FRAC) continue;
|
|
3339
|
+
out.push({
|
|
3340
|
+
t0,
|
|
3341
|
+
t1,
|
|
3342
|
+
nx: clamp012(d.x / width),
|
|
3343
|
+
ny: clamp012(d.y / height),
|
|
3344
|
+
travel
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
return out;
|
|
3348
|
+
}
|
|
3349
|
+
var TYPING_MIN_PINGS = 2;
|
|
3350
|
+
var TYPING_MIN_DUR = 0.5;
|
|
3351
|
+
var TYPING_CLICK_ABSORB = 1.5;
|
|
3352
|
+
var TYPING_REFOCUS_FRAC = 0.08;
|
|
3353
|
+
function groupTrack(track, opts) {
|
|
3354
|
+
const { width, height, clusterGap, typingGap, typingZoom, targetFill } = opts;
|
|
3355
|
+
const minLevel = opts.minLevel ?? resolveZoomStyle().minLevel;
|
|
3356
|
+
const clicks = dedupePresses(
|
|
3357
|
+
track.filter((e) => e.type === "down")
|
|
3358
|
+
).map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
|
|
3359
|
+
const sessions = typingZoom ? typingSessions(track, width, height, typingGap) : [];
|
|
3360
|
+
const absorbed = /* @__PURE__ */ new Set();
|
|
3361
|
+
for (const s of sessions) {
|
|
3362
|
+
let best = null;
|
|
3363
|
+
for (const c of clicks) {
|
|
3364
|
+
if (absorbed.has(c)) continue;
|
|
3365
|
+
if (c.t >= s.first || s.first - c.t > TYPING_CLICK_ABSORB) continue;
|
|
3366
|
+
const [nx, ny] = fieldCenter(c, width, height);
|
|
3367
|
+
if (Math.hypot(nx - s.cx, ny - s.cy) > TYPING_REFOCUS_FRAC) continue;
|
|
3368
|
+
if (!best || c.t > best.t) best = c;
|
|
3369
|
+
}
|
|
3370
|
+
if (best) {
|
|
3371
|
+
absorbed.add(best);
|
|
3372
|
+
s.start = best.t;
|
|
3373
|
+
s.events = [best, ...s.events];
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
const targets = [];
|
|
3377
|
+
const surfaces = [];
|
|
3378
|
+
for (const c of clicks) {
|
|
3379
|
+
if (absorbed.has(c)) continue;
|
|
3380
|
+
if (isSurfacePress(c, width, height, targetFill)) surfaces.push(c);
|
|
3381
|
+
else targets.push(c);
|
|
3382
|
+
}
|
|
3383
|
+
const chain = (list, spatial) => {
|
|
3384
|
+
const clusters = [];
|
|
3385
|
+
for (const c of list) {
|
|
3386
|
+
const last = clusters.at(-1);
|
|
3387
|
+
const prev = last?.at(-1);
|
|
3388
|
+
const joins = last && prev && c.t - prev.t <= clusterGap && (!spatial || unionFitLevel(unionRect([...last, c]), width, height) >= minLevel);
|
|
3389
|
+
if (joins) last.push(c);
|
|
3390
|
+
else clusters.push([c]);
|
|
3391
|
+
}
|
|
3392
|
+
return clusters;
|
|
3393
|
+
};
|
|
2807
3394
|
return {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
h: size
|
|
3395
|
+
sessions,
|
|
3396
|
+
clusters: chain(targets, true),
|
|
3397
|
+
surfaces: chain(surfaces, false)
|
|
2812
3398
|
};
|
|
2813
3399
|
}
|
|
2814
|
-
function
|
|
2815
|
-
const
|
|
2816
|
-
const
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
3400
|
+
function dedupePresses(downs) {
|
|
3401
|
+
const kept = [];
|
|
3402
|
+
for (const e of downs) {
|
|
3403
|
+
const prev = kept.at(-1);
|
|
3404
|
+
if (prev && e.t - prev.t <= PRESS_ECHO_MS && Math.abs(e.x - prev.x) <= PRESS_ECHO_PX && Math.abs(e.y - prev.y) <= PRESS_ECHO_PX) {
|
|
3405
|
+
continue;
|
|
3406
|
+
}
|
|
3407
|
+
kept.push(e);
|
|
3408
|
+
}
|
|
3409
|
+
return kept;
|
|
2823
3410
|
}
|
|
2824
|
-
function
|
|
2825
|
-
|
|
2826
|
-
|
|
3411
|
+
function unionRect(clicks) {
|
|
3412
|
+
let x0 = Infinity;
|
|
3413
|
+
let y0 = Infinity;
|
|
3414
|
+
let x1 = -Infinity;
|
|
3415
|
+
let y1 = -Infinity;
|
|
3416
|
+
for (const c of clicks) {
|
|
3417
|
+
const r = c.rect ?? { x: c.x, y: c.y, w: 0, h: 0 };
|
|
3418
|
+
x0 = Math.min(x0, r.x);
|
|
3419
|
+
y0 = Math.min(y0, r.y);
|
|
3420
|
+
x1 = Math.max(x1, r.x + r.w);
|
|
3421
|
+
y1 = Math.max(y1, r.y + r.h);
|
|
3422
|
+
}
|
|
3423
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
2827
3424
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
var CARD_Z = -4;
|
|
2833
|
-
var BACKGROUND_Z = -6;
|
|
2834
|
-
var CARD_OVERSCAN_STEP = 0.05;
|
|
2835
|
-
var CARD_OVERSCAN_MAX = 4;
|
|
2836
|
-
var CAMERA_NEAR = 0.1;
|
|
2837
|
-
var CAMERA_FAR = 100;
|
|
2838
|
-
function planeSizeAtDepth(distance, fovDeg, aspect) {
|
|
2839
|
-
const height = 2 * Math.abs(distance) * Math.tan(fovDeg * Math.PI / 180 / 2);
|
|
2840
|
-
return { width: height * aspect, height };
|
|
3425
|
+
function unionFitLevel(rect, width, height) {
|
|
3426
|
+
const fx = rect.w > 0 ? width * CLUSTER_FILL / rect.w : Infinity;
|
|
3427
|
+
const fy = rect.h > 0 ? height * CLUSTER_FILL / rect.h : Infinity;
|
|
3428
|
+
return Math.min(fx, fy);
|
|
2841
3429
|
}
|
|
2842
|
-
function
|
|
2843
|
-
|
|
3430
|
+
function fitLevel(rect, width, height, targetFill) {
|
|
3431
|
+
if (!rect || rect.w <= 0 || rect.h <= 0) return null;
|
|
3432
|
+
return Math.min(width * targetFill / rect.w, height * targetFill / rect.h);
|
|
2844
3433
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
findFontFamily as findFontFamily2,
|
|
2849
|
-
fontFaceUrl as fontFaceUrl2,
|
|
2850
|
-
fontStack,
|
|
2851
|
-
nearestFontWeight as nearestFontWeight2
|
|
2852
|
-
} from "@vosjs/shared";
|
|
2853
|
-
|
|
2854
|
-
// src/htmlLayer.ts
|
|
2855
|
-
import { findFontFamily, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
2856
|
-
var DESIGN_FRAME_WIDTH = 1920;
|
|
2857
|
-
var HTML_LAYER_WARN_BYTES = 32e3;
|
|
2858
|
-
var HTML_LAYER_MAX_BYTES = 128e3;
|
|
2859
|
-
var HTML_LAYER_LIVE_HZ = 60;
|
|
2860
|
-
var HTML_LAYER_ASSET_HOST = "https://assets.vos.so/";
|
|
2861
|
-
function splitTopLevel(value) {
|
|
2862
|
-
const out = [];
|
|
2863
|
-
let depth = 0;
|
|
2864
|
-
let cur = "";
|
|
2865
|
-
for (const ch of value) {
|
|
2866
|
-
if (ch === "(") depth++;
|
|
2867
|
-
else if (ch === ")") depth--;
|
|
2868
|
-
if (ch === "," && depth === 0) {
|
|
2869
|
-
out.push(cur);
|
|
2870
|
-
cur = "";
|
|
2871
|
-
} else cur += ch;
|
|
2872
|
-
}
|
|
2873
|
-
if (cur.trim()) out.push(cur);
|
|
2874
|
-
return out;
|
|
3434
|
+
function isSurfacePress(c, width, height, targetFill) {
|
|
3435
|
+
const fit = fitLevel(c.rect, width, height, targetFill);
|
|
3436
|
+
return fit !== null && fit < DRAG_FIT_LEVEL;
|
|
2875
3437
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
const
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3438
|
+
function planAutoZoom(track, options) {
|
|
3439
|
+
const style = resolveZoomStyle(options.style, options.params);
|
|
3440
|
+
if (!style.autoZoom) return [];
|
|
3441
|
+
const {
|
|
3442
|
+
width,
|
|
3443
|
+
height,
|
|
3444
|
+
targetFill = style.targetFill,
|
|
3445
|
+
minLevel = style.minLevel,
|
|
3446
|
+
maxLevel = style.maxLevel,
|
|
3447
|
+
clusterGap = style.clusterGap,
|
|
3448
|
+
lead = style.lead,
|
|
3449
|
+
hold = style.hold,
|
|
3450
|
+
minClusterClicks = style.minClusterClicks,
|
|
3451
|
+
dragLevel = style.dragLevel,
|
|
3452
|
+
followByDefault = style.followByDefault,
|
|
3453
|
+
typingZoom = style.typingZoom,
|
|
3454
|
+
typingGap = style.typingGap,
|
|
3455
|
+
typingHold = style.typingHold,
|
|
3456
|
+
typingMinLevel = style.typingMinLevel,
|
|
3457
|
+
duration
|
|
3458
|
+
} = options;
|
|
3459
|
+
const within = (spans2) => duration === void 0 ? spans2 : spans2.map((z) => ({ ...z, out: Math.min(z.out, round2(duration)) })).filter((z) => z.out - z.in >= 0.3);
|
|
3460
|
+
const grouped = groupTrack(track, {
|
|
3461
|
+
width,
|
|
3462
|
+
height,
|
|
3463
|
+
clusterGap,
|
|
3464
|
+
typingGap,
|
|
3465
|
+
typingZoom,
|
|
3466
|
+
targetFill,
|
|
3467
|
+
minLevel
|
|
3468
|
+
});
|
|
3469
|
+
const { sessions } = grouped;
|
|
3470
|
+
const drags = dragsFromTrack(track, width, height);
|
|
3471
|
+
const dragPress = new Set(drags.map((d) => round2(d.t0)));
|
|
3472
|
+
const withoutDrags = (list) => list.map((cl) => cl.filter((c) => !dragPress.has(round2(c.t)))).filter((cl) => cl.length > 0);
|
|
3473
|
+
const clusters = withoutDrags(grouped.clusters);
|
|
3474
|
+
const surfaces = withoutDrags(grouped.surfaces);
|
|
3475
|
+
const dragSpans = drags.map((d, i) => ({
|
|
3476
|
+
id: `g${i}`,
|
|
3477
|
+
in: round2(Math.max(0, d.t0 - lead)),
|
|
3478
|
+
out: round2(d.t1 + hold),
|
|
3479
|
+
level: clampZoomLevel(Math.min(Math.max(dragLevel, 1.1), maxLevel)),
|
|
3480
|
+
cx: round2(d.nx),
|
|
3481
|
+
cy: round2(d.ny),
|
|
3482
|
+
focusMode: "auto",
|
|
3483
|
+
source: "auto"
|
|
3484
|
+
}));
|
|
3485
|
+
const eligible = clusters.filter((c) => c.length >= minClusterClicks);
|
|
3486
|
+
const clickSpans = [];
|
|
3487
|
+
const dragReserved = surfaces.map((cluster, i) => {
|
|
3488
|
+
const f = clusterFocus(cluster, width, height);
|
|
3489
|
+
return {
|
|
3490
|
+
id: `drag${i}`,
|
|
3491
|
+
in: Math.max(0, cluster[0].t - lead),
|
|
3492
|
+
out: cluster[cluster.length - 1].t + hold,
|
|
3493
|
+
level: 1,
|
|
3494
|
+
cx: f.cx,
|
|
3495
|
+
cy: f.cy
|
|
3496
|
+
};
|
|
3497
|
+
});
|
|
3498
|
+
for (const cluster of eligible) {
|
|
3499
|
+
const first = cluster[0];
|
|
3500
|
+
const last = cluster[cluster.length - 1];
|
|
3501
|
+
const f = focusFor(cluster, width, height, targetFill, minLevel, maxLevel);
|
|
3502
|
+
clickSpans.push({
|
|
3503
|
+
in: Math.max(0, first.t - lead),
|
|
3504
|
+
out: last.t + hold,
|
|
3505
|
+
cx: f.cx,
|
|
3506
|
+
cy: f.cy,
|
|
3507
|
+
level: f.level
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
clickSpans.sort((a, b) => a.in - b.in);
|
|
3511
|
+
for (let i = 0; i + 1 < clickSpans.length; i++) {
|
|
3512
|
+
if (clickSpans[i].out > clickSpans[i + 1].in)
|
|
3513
|
+
clickSpans[i].out = clickSpans[i + 1].in;
|
|
3514
|
+
}
|
|
3515
|
+
for (const z of clickSpans) if (z.out - z.in < 0.3) z.dead = true;
|
|
3516
|
+
const typingFloor = Math.min(Math.max(minLevel, typingMinLevel), maxLevel);
|
|
3517
|
+
let typing = sessions.map((s) => {
|
|
3518
|
+
const f = focusFor(
|
|
3519
|
+
s.events,
|
|
3520
|
+
width,
|
|
3521
|
+
height,
|
|
3522
|
+
targetFill,
|
|
3523
|
+
typingFloor,
|
|
3524
|
+
maxLevel
|
|
3525
|
+
);
|
|
3526
|
+
return {
|
|
3527
|
+
in: Math.max(0, s.start - lead),
|
|
3528
|
+
out: s.last + typingHold,
|
|
3529
|
+
cx: f.cx,
|
|
3530
|
+
cy: f.cy,
|
|
3531
|
+
level: f.level
|
|
3532
|
+
};
|
|
3533
|
+
});
|
|
3534
|
+
for (const t of typing) {
|
|
3535
|
+
for (const z of clickSpans) {
|
|
3536
|
+
if (z.dead || t.dead) continue;
|
|
3537
|
+
if (t.in >= z.out || t.out <= z.in) continue;
|
|
3538
|
+
if (Math.hypot(t.cx - z.cx, t.cy - z.cy) <= TYPING_REFOCUS_FRAC) {
|
|
3539
|
+
t.in = Math.min(t.in, z.in);
|
|
3540
|
+
t.out = Math.max(t.out, z.out);
|
|
3541
|
+
z.dead = true;
|
|
3542
|
+
} else if (z.in <= t.in && z.out >= t.out) {
|
|
3543
|
+
t.dead = true;
|
|
3544
|
+
} else if (z.in > t.in) {
|
|
3545
|
+
t.out = z.in;
|
|
3546
|
+
} else {
|
|
3547
|
+
t.in = z.out;
|
|
3548
|
+
}
|
|
2908
3549
|
}
|
|
2909
3550
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
const out = /* @__PURE__ */ new Set([400]);
|
|
2914
|
-
for (const decl of css?.match(/font-weight\s*:[^;}]+/gi) ?? []) {
|
|
2915
|
-
const value = decl.replace(/^[^:]*:/, "").toLowerCase();
|
|
2916
|
-
for (const n of value.match(/\b[1-9]00\b/g) ?? []) out.add(Number(n));
|
|
2917
|
-
if (/\bbold(er)?\b/.test(value)) out.add(700);
|
|
2918
|
-
}
|
|
2919
|
-
if (/<(b|strong|h[1-6])[\s>]/i.test(html)) out.add(700);
|
|
2920
|
-
return [...out].sort((a, b) => a - b);
|
|
2921
|
-
}
|
|
2922
|
-
function htmlLayerFaces(clip) {
|
|
2923
|
-
const faces = [];
|
|
2924
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2925
|
-
const add = (f) => {
|
|
2926
|
-
const key = `${f.family.toLowerCase()}|${f.weight}|${f.style}`;
|
|
2927
|
-
if (seen.has(key)) return;
|
|
2928
|
-
seen.add(key);
|
|
2929
|
-
faces.push(f);
|
|
2930
|
-
};
|
|
2931
|
-
const weights = cssWeights(clip.css, clip.html);
|
|
2932
|
-
for (const family of cssFamilies(clip.css)) {
|
|
2933
|
-
const entry = findFontFamily(family);
|
|
2934
|
-
if (!entry) continue;
|
|
2935
|
-
for (const w of weights)
|
|
2936
|
-
add({
|
|
2937
|
-
family: entry.family,
|
|
2938
|
-
url: fontFaceUrl(entry.slug, nearestFontWeight(entry, w)),
|
|
2939
|
-
weight: nearestFontWeight(entry, w),
|
|
2940
|
-
style: "normal"
|
|
2941
|
-
});
|
|
3551
|
+
typing = typing.filter((t) => !t.dead).sort((a, b) => a.in - b.in);
|
|
3552
|
+
for (let i = 0; i + 1 < typing.length; i++) {
|
|
3553
|
+
if (typing[i].out > typing[i + 1].in) typing[i].out = typing[i + 1].in;
|
|
2942
3554
|
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
);
|
|
2956
|
-
|
|
2957
|
-
|
|
3555
|
+
typing = typing.filter((t) => t.out - t.in >= 0.3);
|
|
3556
|
+
const zSpans = clickSpans.filter((s) => !s.dead).sort((a, b) => a.in - b.in).map((s, i) => ({
|
|
3557
|
+
id: `z${i}`,
|
|
3558
|
+
in: round2(s.in),
|
|
3559
|
+
out: round2(s.out),
|
|
3560
|
+
level: clampZoomLevel(s.level),
|
|
3561
|
+
cx: round2(s.cx),
|
|
3562
|
+
cy: round2(s.cy),
|
|
3563
|
+
// Follow styles ride the cursor through the span (entry + dead-zone
|
|
3564
|
+
// recenters baked at lowering); typing/dwell spans stay fixed-focus.
|
|
3565
|
+
...followByDefault ? { focusMode: "auto" } : {},
|
|
3566
|
+
source: "auto"
|
|
3567
|
+
}));
|
|
3568
|
+
const kSpans = typing.map((s, i) => ({
|
|
3569
|
+
id: `k${i}`,
|
|
3570
|
+
in: round2(s.in),
|
|
3571
|
+
out: round2(s.out),
|
|
3572
|
+
level: clampZoomLevel(s.level),
|
|
3573
|
+
cx: round2(s.cx),
|
|
3574
|
+
cy: round2(s.cy),
|
|
3575
|
+
// Never focusMode:'auto': the dot is parked (and fading) while typing —
|
|
3576
|
+
// the FIELD is the anchor, and a follow would be a no-op at best.
|
|
3577
|
+
source: "auto"
|
|
3578
|
+
}));
|
|
3579
|
+
const spans = within([...zSpans, ...kSpans, ...dragSpans]).sort(
|
|
3580
|
+
(a, b) => a.in - b.in
|
|
2958
3581
|
);
|
|
3582
|
+
const dwells = dwellSpans(track, width, height, maxLevel, [
|
|
3583
|
+
...spans,
|
|
3584
|
+
...dragReserved
|
|
3585
|
+
]);
|
|
3586
|
+
return within([...spans, ...dwells]).sort((a, b) => a.in - b.in);
|
|
2959
3587
|
}
|
|
2960
|
-
function
|
|
2961
|
-
const
|
|
2962
|
-
const
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3588
|
+
function typingSessions(track, width, height, typingGap) {
|
|
3589
|
+
const pings = track.filter((e) => e.type === "key").map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
|
|
3590
|
+
const all = [];
|
|
3591
|
+
let cur = null;
|
|
3592
|
+
for (const p of pings) {
|
|
3593
|
+
const [nx, ny] = fieldCenter(p, width, height);
|
|
3594
|
+
if (cur && p.t - cur.last <= typingGap && Math.hypot(nx - cur.cx, ny - cur.cy) <= TYPING_REFOCUS_FRAC) {
|
|
3595
|
+
cur.events.push(p);
|
|
3596
|
+
cur.last = p.t;
|
|
3597
|
+
} else {
|
|
3598
|
+
cur = { events: [p], first: p.t, last: p.t, start: p.t, cx: nx, cy: ny };
|
|
3599
|
+
all.push(cur);
|
|
2967
3600
|
}
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
)
|
|
2972
|
-
add(m[1]);
|
|
2973
|
-
for (const text of [clip.html, clip.css ?? ""])
|
|
2974
|
-
for (const m of text.matchAll(
|
|
2975
|
-
/url\(\s*["']?((?:https?:)?\/\/[^"')\s]+)["']?\s*\)/gi
|
|
2976
|
-
))
|
|
2977
|
-
add(m[1]);
|
|
2978
|
-
return out;
|
|
2979
|
-
}
|
|
2980
|
-
function htmlLayerAssets(clip) {
|
|
2981
|
-
const inlinable = [];
|
|
2982
|
-
const foreign = [];
|
|
2983
|
-
for (const u of htmlLayerAssetUrls(clip))
|
|
2984
|
-
(u.replace(/^\/\//, "https://").startsWith(HTML_LAYER_ASSET_HOST) ? inlinable : foreign).push(u);
|
|
2985
|
-
return { inlinable, foreign };
|
|
2986
|
-
}
|
|
2987
|
-
function htmlLayerBleedFor(css) {
|
|
2988
|
-
if (!css) return 0;
|
|
2989
|
-
let worst = 0;
|
|
2990
|
-
const consider = (layer) => {
|
|
2991
|
-
if (/\binset\b/i.test(layer)) return;
|
|
2992
|
-
const lengths = layer.replace(/[a-z-]+\([^)]*\)/gi, " ").replace(/#[0-9a-f]{3,8}/gi, " ").match(/-?\d*\.?\d+/g);
|
|
2993
|
-
if (!lengths) return;
|
|
2994
|
-
const [x = 0, y = 0, blur = 0, spread = 0] = lengths.map(Number);
|
|
2995
|
-
worst = Math.max(
|
|
2996
|
-
worst,
|
|
2997
|
-
Math.abs(x) + blur + spread,
|
|
2998
|
-
Math.abs(y) + blur + spread
|
|
2999
|
-
);
|
|
3000
|
-
};
|
|
3001
|
-
for (const decl of css.match(/box-shadow\s*:[^;}]+/gi) ?? [])
|
|
3002
|
-
for (const layer of splitTopLevel(decl.replace(/^[^:]*:/, "")))
|
|
3003
|
-
consider(layer);
|
|
3004
|
-
for (const fn of css.match(/drop-shadow\(([^)]*)\)/gi) ?? [])
|
|
3005
|
-
consider(fn.replace(/^drop-shadow\(/i, "").replace(/\)$/, ""));
|
|
3006
|
-
return worst ? Math.ceil(worst * 1.15) : 0;
|
|
3007
|
-
}
|
|
3008
|
-
function htmlLayerBleed(clip) {
|
|
3009
|
-
return Math.max(0, Math.round(clip.bleed ?? htmlLayerBleedFor(clip.css)));
|
|
3010
|
-
}
|
|
3011
|
-
function htmlLayerPictureBox(clip) {
|
|
3012
|
-
const bleed = htmlLayerBleed(clip);
|
|
3013
|
-
return {
|
|
3014
|
-
width: clip.box.width + bleed * 2,
|
|
3015
|
-
height: clip.box.height + bleed * 2,
|
|
3016
|
-
bleed
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
function htmlLayerWidth(clip) {
|
|
3020
|
-
if (clip.width != null) return clip.width;
|
|
3021
|
-
return Math.min(1, htmlLayerPictureBox(clip).width / DESIGN_FRAME_WIDTH);
|
|
3022
|
-
}
|
|
3023
|
-
var ENTITIES = {
|
|
3024
|
-
amp: "&",
|
|
3025
|
-
lt: "<",
|
|
3026
|
-
gt: ">",
|
|
3027
|
-
quot: '"',
|
|
3028
|
-
apos: "'",
|
|
3029
|
-
nbsp: " "
|
|
3030
|
-
};
|
|
3031
|
-
function htmlLayerText(html) {
|
|
3032
|
-
return html.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]*>/g, " ").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, ref) => {
|
|
3033
|
-
if (ref[0] === "#")
|
|
3034
|
-
return String.fromCodePoint(
|
|
3035
|
-
ref[1] === "x" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1))
|
|
3036
|
-
);
|
|
3037
|
-
return ENTITIES[ref.toLowerCase()] ?? m;
|
|
3038
|
-
}).replace(/\s+/g, " ").trim();
|
|
3601
|
+
}
|
|
3602
|
+
return all.filter(
|
|
3603
|
+
(s) => s.events.length >= TYPING_MIN_PINGS && s.last - s.first >= TYPING_MIN_DUR
|
|
3604
|
+
);
|
|
3039
3605
|
}
|
|
3040
|
-
function
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3606
|
+
function fieldCenter(c, width, height) {
|
|
3607
|
+
const px = c.rect ? c.rect.x + c.rect.w / 2 : c.x;
|
|
3608
|
+
const py = c.rect ? c.rect.y + c.rect.h / 2 : c.y;
|
|
3609
|
+
return [clamp012(px / width), clamp012(py / height)];
|
|
3043
3610
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
"frame",
|
|
3065
|
-
"fx",
|
|
3066
|
-
"text",
|
|
3067
|
-
"preset"
|
|
3068
|
-
];
|
|
3069
|
-
function htmlLayerProblems(clip) {
|
|
3070
|
-
const html = clip.html ?? "";
|
|
3071
|
-
const css = clip.css;
|
|
3072
|
-
const raw = clip;
|
|
3073
|
-
const problems = [];
|
|
3074
|
-
const push2 = (code, level, message) => problems.push({ code, level, message });
|
|
3075
|
-
if (!html.trim()) push2("empty", "problem", "The layer has no markup.");
|
|
3076
|
-
for (const tag of VOID_TAGS) {
|
|
3077
|
-
const open = new RegExp(`<${tag}(\\s[^>]*?)?>`, "gi");
|
|
3078
|
-
let m;
|
|
3079
|
-
let said = false;
|
|
3080
|
-
while (m = open.exec(html)) {
|
|
3081
|
-
if (!m[0].trimEnd().endsWith("/>") && !said) {
|
|
3082
|
-
said = true;
|
|
3083
|
-
push2(
|
|
3084
|
-
"unclosed-void",
|
|
3085
|
-
"problem",
|
|
3086
|
-
`<${tag}> must close itself as <${tag} /> inside a foreignObject.`
|
|
3087
|
-
);
|
|
3088
|
-
}
|
|
3611
|
+
function dwellSpans(track, width, height, maxLevel, reserved) {
|
|
3612
|
+
const moves = track.filter((e) => e.type === "move").map((e) => ({ t: e.t / 1e3, nx: e.x / width, ny: e.y / height }));
|
|
3613
|
+
if (moves.length < 2) return [];
|
|
3614
|
+
const trackEnd = track[track.length - 1].t / 1e3;
|
|
3615
|
+
const candidates = [];
|
|
3616
|
+
let start = 0;
|
|
3617
|
+
for (let i = 1; i <= moves.length; i++) {
|
|
3618
|
+
const breaks = i === moves.length || Math.hypot(moves[i].nx - moves[i - 1].nx, moves[i].ny - moves[i - 1].ny) > DWELL_MOVE_FRAC;
|
|
3619
|
+
if (!breaks) continue;
|
|
3620
|
+
const endT = i < moves.length ? moves[i].t : trackEnd;
|
|
3621
|
+
const dur = endT - moves[start].t;
|
|
3622
|
+
const opening = start === 0;
|
|
3623
|
+
if (!opening && dur >= DWELL_MIN && dur <= DWELL_MAX) {
|
|
3624
|
+
const run = moves.slice(start, i);
|
|
3625
|
+
candidates.push({
|
|
3626
|
+
center: (moves[start].t + endT) / 2,
|
|
3627
|
+
cx: run.reduce((s, p) => s + p.nx, 0) / run.length,
|
|
3628
|
+
cy: run.reduce((s, p) => s + p.ny, 0) / run.length,
|
|
3629
|
+
strength: dur
|
|
3630
|
+
});
|
|
3089
3631
|
}
|
|
3632
|
+
start = i;
|
|
3090
3633
|
}
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
"Attribute values must be quoted inside a foreignObject."
|
|
3103
|
-
);
|
|
3104
|
-
const bytes = html.length + (css?.length ?? 0);
|
|
3105
|
-
if (bytes > HTML_LAYER_MAX_BYTES)
|
|
3106
|
-
push2(
|
|
3107
|
-
"too-large",
|
|
3108
|
-
"problem",
|
|
3109
|
-
`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.`
|
|
3110
|
-
);
|
|
3111
|
-
else if (bytes > HTML_LAYER_WARN_BYTES)
|
|
3112
|
-
push2(
|
|
3113
|
-
"too-large",
|
|
3114
|
-
"warning",
|
|
3115
|
-
`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.`
|
|
3116
|
-
);
|
|
3117
|
-
for (const field of HTML_LAYER_FOREIGN_FIELDS)
|
|
3118
|
-
if (raw[field] !== void 0)
|
|
3119
|
-
push2(
|
|
3120
|
-
"foreign-field",
|
|
3121
|
-
"problem",
|
|
3122
|
-
`An HTML layer has no "${field}": its CSS is its chrome (shadow, radius, border) and its markup its content.`
|
|
3123
|
-
);
|
|
3124
|
-
const needed = htmlLayerBleedFor(css);
|
|
3125
|
-
if (clip.bleed != null && needed > clip.bleed)
|
|
3126
|
-
push2(
|
|
3127
|
-
"shadow-clipped",
|
|
3128
|
-
"warning",
|
|
3129
|
-
`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.`
|
|
3130
|
-
);
|
|
3131
|
-
for (const u of htmlLayerAssets({ html, css }).foreign)
|
|
3132
|
-
push2(
|
|
3133
|
-
"external-resource",
|
|
3134
|
-
"warning",
|
|
3135
|
-
`The source names ${u} by URL. The page inlines only ${HTML_LAYER_ASSET_HOST}\u2026 (the host it may fetch, and the one the fleet reaches), so this will paint blank. Upload it (vos asset upload) or inline it as a data: URI.`
|
|
3136
|
-
);
|
|
3137
|
-
if (/\bhref\s*=\s*["']\s*(https?:)?\/\//i.test(html))
|
|
3138
|
-
push2(
|
|
3139
|
-
"external-resource",
|
|
3140
|
-
"warning",
|
|
3141
|
-
"The source names a stylesheet or a link by URL. An SVG image fetches no href; inline the styles in the CSS."
|
|
3142
|
-
);
|
|
3143
|
-
const animated = /\b(animation|transition)(-[a-z-]+)?\s*:/i.test(css ?? "");
|
|
3144
|
-
if (animated && !clip.live)
|
|
3145
|
-
push2(
|
|
3146
|
-
"wall-clock",
|
|
3147
|
-
"warning",
|
|
3148
|
-
"CSS animations inside a layer run on the wall clock, not the timeline: the export will not match the preview. Set live: true to scrub them to the clip's time, or animate the layer with motion and anim."
|
|
3149
|
-
);
|
|
3150
|
-
if (clip.live && /\banimation-delay\s*:/i.test(css ?? ""))
|
|
3151
|
-
push2(
|
|
3152
|
-
"live-delay",
|
|
3153
|
-
"warning",
|
|
3154
|
-
"A live layer scrubs its keyframes by setting animation-delay to -t, so the authored animation-delay is overridden. Fold the delay into the keyframes instead."
|
|
3155
|
-
);
|
|
3156
|
-
if (!clip.live && (HTML_LAYER_PLACEHOLDER.test(html) || HTML_LAYER_PLACEHOLDER.test(css ?? "")))
|
|
3157
|
-
push2(
|
|
3158
|
-
"placeholder-still",
|
|
3159
|
-
"warning",
|
|
3160
|
-
"The source carries {{t}} or {{data.\u2026}} placeholders, which fill only on a live layer (live: true); a still layer shows them as written."
|
|
3161
|
-
);
|
|
3162
|
-
for (const family of htmlLayerUnhostedFamilies(clip))
|
|
3163
|
-
push2(
|
|
3164
|
-
"unhosted-family",
|
|
3165
|
-
"warning",
|
|
3166
|
-
`The CSS names "${family}", which is not a hosted family (GET https://vos.so/api/fonts); the system stack stands in.`
|
|
3634
|
+
const sorted = [...candidates].sort((a, b) => b.strength - a.strength);
|
|
3635
|
+
const sourceDuration = trackEnd;
|
|
3636
|
+
const len = Math.max(1, sourceDuration * 0.05);
|
|
3637
|
+
const taken = [...reserved];
|
|
3638
|
+
const accepted = [];
|
|
3639
|
+
const centers = [];
|
|
3640
|
+
for (const c of sorted) {
|
|
3641
|
+
if (centers.some((t) => Math.abs(t - c.center) < DWELL_SPACING)) continue;
|
|
3642
|
+
const spanIn = Math.max(
|
|
3643
|
+
0,
|
|
3644
|
+
Math.min(c.center - len / 2, sourceDuration - len)
|
|
3167
3645
|
);
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3646
|
+
const spanOut = Math.min(sourceDuration, spanIn + len);
|
|
3647
|
+
if (spanOut - spanIn < 0.3) continue;
|
|
3648
|
+
if (taken.some((z) => spanIn < z.out && spanOut > z.in)) continue;
|
|
3649
|
+
const span = {
|
|
3650
|
+
id: `d${accepted.length}`,
|
|
3651
|
+
in: round2(spanIn),
|
|
3652
|
+
out: round2(spanOut),
|
|
3653
|
+
// No element rect on moves → point zoom at the planner ceiling.
|
|
3654
|
+
level: clampZoomLevel(maxLevel),
|
|
3655
|
+
cx: round2(clamp012(c.cx)),
|
|
3656
|
+
cy: round2(clamp012(c.cy)),
|
|
3657
|
+
source: "auto"
|
|
3658
|
+
};
|
|
3659
|
+
accepted.push(span);
|
|
3660
|
+
taken.push(span);
|
|
3661
|
+
centers.push(c.center);
|
|
3662
|
+
}
|
|
3663
|
+
return accepted.sort((a, b) => a.in - b.in).map((z, i) => ({ ...z, id: `d${i}` }));
|
|
3176
3664
|
}
|
|
3177
|
-
function
|
|
3178
|
-
let
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3665
|
+
function focusFor(cluster, width, height, targetFill, minLevel, maxLevel) {
|
|
3666
|
+
let sx = 0;
|
|
3667
|
+
let sy = 0;
|
|
3668
|
+
let maxW = 0;
|
|
3669
|
+
let maxH = 0;
|
|
3670
|
+
for (const c of cluster) {
|
|
3671
|
+
if (c.rect) {
|
|
3672
|
+
sx += c.rect.x + c.rect.w / 2;
|
|
3673
|
+
sy += c.rect.y + c.rect.h / 2;
|
|
3674
|
+
maxW = Math.max(maxW, c.rect.w);
|
|
3675
|
+
maxH = Math.max(maxH, c.rect.h);
|
|
3676
|
+
} else {
|
|
3677
|
+
sx += c.x;
|
|
3678
|
+
sy += c.y;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
const n = cluster.length;
|
|
3682
|
+
const cx = clamp012(sx / n / width);
|
|
3683
|
+
const cy = clamp012(sy / n / height);
|
|
3684
|
+
const own = fitLevel({ x: 0, y: 0, w: maxW, h: maxH }, width, height, targetFill) ?? maxLevel;
|
|
3685
|
+
const together = unionFitLevel(unionRect(cluster), width, height);
|
|
3686
|
+
return { cx, cy, level: clamp3(Math.min(own, together), minLevel, maxLevel) };
|
|
3182
3687
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3688
|
+
function clusterFocus(events, width, height) {
|
|
3689
|
+
let sx = 0;
|
|
3690
|
+
let sy = 0;
|
|
3691
|
+
let x0 = Infinity;
|
|
3692
|
+
let y0 = Infinity;
|
|
3693
|
+
let x1 = -Infinity;
|
|
3694
|
+
let y1 = -Infinity;
|
|
3695
|
+
let rects = 0;
|
|
3696
|
+
for (const c of events) {
|
|
3697
|
+
if (c.rect) {
|
|
3698
|
+
sx += c.rect.x + c.rect.w / 2;
|
|
3699
|
+
sy += c.rect.y + c.rect.h / 2;
|
|
3700
|
+
x0 = Math.min(x0, c.rect.x);
|
|
3701
|
+
y0 = Math.min(y0, c.rect.y);
|
|
3702
|
+
x1 = Math.max(x1, c.rect.x + c.rect.w);
|
|
3703
|
+
y1 = Math.max(y1, c.rect.y + c.rect.h);
|
|
3704
|
+
rects++;
|
|
3705
|
+
} else {
|
|
3706
|
+
sx += c.x;
|
|
3707
|
+
sy += c.y;
|
|
3202
3708
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
const
|
|
3206
|
-
|
|
3207
|
-
|
|
3709
|
+
}
|
|
3710
|
+
const n = Math.max(1, events.length);
|
|
3711
|
+
const rect = rects > 0 ? {
|
|
3712
|
+
x: clamp012(x0 / width),
|
|
3713
|
+
y: clamp012(y0 / height),
|
|
3714
|
+
w: clamp012((x1 - x0) / width),
|
|
3715
|
+
h: clamp012((y1 - y0) / height)
|
|
3716
|
+
} : null;
|
|
3717
|
+
return { cx: clamp012(sx / n / width), cy: clamp012(sy / n / height), rect };
|
|
3208
3718
|
}
|
|
3209
|
-
function
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3719
|
+
function clamp3(v, lo, hi) {
|
|
3720
|
+
return Math.max(lo, Math.min(hi, v));
|
|
3721
|
+
}
|
|
3722
|
+
function clamp012(v) {
|
|
3723
|
+
return clamp3(v, 0, 1);
|
|
3724
|
+
}
|
|
3725
|
+
function round2(v) {
|
|
3726
|
+
return Math.round(v * 1e3) / 1e3;
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
// src/lower/lowerToComposition.ts
|
|
3730
|
+
import {
|
|
3731
|
+
EASINGS,
|
|
3732
|
+
lerpArray as lerpArray2,
|
|
3733
|
+
sample as sample2,
|
|
3734
|
+
segmentRate as segmentRate2,
|
|
3735
|
+
sortKeyframes as sortKeyframes2,
|
|
3736
|
+
sourceToTimeline,
|
|
3737
|
+
splitBySpeed as splitBySpeed3,
|
|
3738
|
+
totalDuration as totalDuration3
|
|
3739
|
+
} from "@vosjs/timeline";
|
|
3740
|
+
import { timelineRuntimeCode as timelineRuntimeCode2 } from "@vosjs/timeline/bundle";
|
|
3741
|
+
|
|
3742
|
+
// src/layout.ts
|
|
3743
|
+
function computeCardLayout(frame, video, W, H, zoomFocus) {
|
|
3744
|
+
const s = H / 1080;
|
|
3745
|
+
const pad = (frame.padding || 0) * s;
|
|
3746
|
+
const ins = frame.inset ?? {};
|
|
3747
|
+
const padL = ins.left == null ? pad : ins.left * W;
|
|
3748
|
+
const padR = ins.right == null ? pad : ins.right * W;
|
|
3749
|
+
const padT = ins.top == null ? pad : ins.top * H;
|
|
3750
|
+
const padB = ins.bottom == null ? pad : ins.bottom * H;
|
|
3751
|
+
const bar = frame.browserBar;
|
|
3752
|
+
const vw = video.width || 16;
|
|
3753
|
+
const vh = video.height || 9;
|
|
3754
|
+
const fitCover = frame.fit === "cover";
|
|
3755
|
+
const cf = fitCover ? 1 : Math.min(1, W / H / (vw / vh));
|
|
3756
|
+
const barH = bar.kind !== "none" ? (bar.height || 44) * s * cf : 0;
|
|
3757
|
+
const availW = Math.max(1, W - padL - padR);
|
|
3758
|
+
const availH = Math.max(1, H - padT - padB - barH);
|
|
3759
|
+
if (fitCover) {
|
|
3760
|
+
const sc2 = Math.max(availW / vw, availH / vh);
|
|
3761
|
+
const dw2 = vw * sc2;
|
|
3762
|
+
const dh2 = vh * sc2;
|
|
3763
|
+
const follow = frame.focusFollow === "camera" && zoomFocus ? zoomFocus : null;
|
|
3764
|
+
const fcx = clamp013(follow ? follow.cx : frame.focus?.cx ?? 0.5);
|
|
3765
|
+
const fcy = clamp013(follow ? follow.cy : frame.focus?.cy ?? 0.5);
|
|
3766
|
+
const vTop = padT + barH;
|
|
3767
|
+
const dx2 = Math.min(
|
|
3768
|
+
padL,
|
|
3769
|
+
Math.max(padL + availW - dw2, padL + availW / 2 - fcx * dw2)
|
|
3770
|
+
);
|
|
3771
|
+
const dy2 = Math.min(
|
|
3772
|
+
vTop,
|
|
3773
|
+
Math.max(vTop + availH - dh2, vTop + availH / 2 - fcy * dh2)
|
|
3774
|
+
);
|
|
3775
|
+
return {
|
|
3776
|
+
W,
|
|
3777
|
+
H,
|
|
3778
|
+
dx: dx2,
|
|
3779
|
+
dy: dy2,
|
|
3780
|
+
dw: dw2,
|
|
3781
|
+
dh: dh2,
|
|
3782
|
+
cardX: padL,
|
|
3783
|
+
cardY: padT,
|
|
3784
|
+
cardW: availW,
|
|
3785
|
+
cardH: availH + barH
|
|
3786
|
+
};
|
|
3214
3787
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
const
|
|
3219
|
-
|
|
3220
|
-
(clip.live ? " live " + JSON.stringify(clip.data ?? null) : "");
|
|
3221
|
-
return `html:${clip.id}:${fnv1a(src)}`;
|
|
3222
|
-
}
|
|
3223
|
-
function htmlLayerPayload(clip) {
|
|
3224
|
-
const faces = htmlLayerFaces(clip);
|
|
3225
|
-
const bleed = htmlLayerBleed(clip);
|
|
3226
|
-
const assets = htmlLayerAssets(clip).inlinable;
|
|
3788
|
+
const sc = Math.min(availW / vw, availH / vh);
|
|
3789
|
+
const dw = vw * sc;
|
|
3790
|
+
const dh = vh * sc;
|
|
3791
|
+
const dx = padL + (availW - dw) / 2;
|
|
3792
|
+
const dy = padT + barH + (availH - dh) / 2;
|
|
3227
3793
|
return {
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
}
|
|
3794
|
+
W,
|
|
3795
|
+
H,
|
|
3796
|
+
dx,
|
|
3797
|
+
dy,
|
|
3798
|
+
dw,
|
|
3799
|
+
dh,
|
|
3800
|
+
cardX: dx,
|
|
3801
|
+
cardY: dy - barH,
|
|
3802
|
+
cardW: dw,
|
|
3803
|
+
cardH: dh + barH
|
|
3239
3804
|
};
|
|
3240
3805
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
const
|
|
3806
|
+
function docCardLayout(doc, media) {
|
|
3807
|
+
const meta = doc.source.meta;
|
|
3808
|
+
const H = 1080;
|
|
3809
|
+
const W = Math.max(
|
|
3810
|
+
2,
|
|
3811
|
+
Math.round(H * aspectRatioValue(doc.frame.aspectRatio, meta))
|
|
3812
|
+
);
|
|
3813
|
+
const src = mediaSource(doc, media) ?? doc.source;
|
|
3814
|
+
const sm = src.meta;
|
|
3815
|
+
return computeCardLayout(
|
|
3816
|
+
mediaFrame(doc, media),
|
|
3817
|
+
{
|
|
3818
|
+
width: sm.captureWidth ?? sm.width,
|
|
3819
|
+
height: sm.captureHeight ?? sm.height
|
|
3820
|
+
},
|
|
3821
|
+
W,
|
|
3822
|
+
H
|
|
3823
|
+
);
|
|
3824
|
+
}
|
|
3825
|
+
function recommendedExportResolution(doc) {
|
|
3826
|
+
const meta = doc.source.meta;
|
|
3827
|
+
const capW = meta.captureWidth ?? meta.width;
|
|
3828
|
+
const video = { width: capW, height: meta.captureHeight ?? meta.height };
|
|
3829
|
+
let best = EXPORT_RESOLUTION_OPTIONS[0];
|
|
3830
|
+
for (const r of EXPORT_RESOLUTION_OPTIONS) {
|
|
3831
|
+
const { width, height } = resolveExportSize(doc, r);
|
|
3832
|
+
if (computeCardLayout(doc.frame, video, width, height).dw > capW) break;
|
|
3833
|
+
best = r;
|
|
3834
|
+
}
|
|
3835
|
+
return best;
|
|
3836
|
+
}
|
|
3837
|
+
function camBubbleRect(cam, W, H = 1080) {
|
|
3838
|
+
const s = H / 1080;
|
|
3839
|
+
const size = Math.max(40, (cam.size || 0.25) * H);
|
|
3840
|
+
const mg = 24 * s;
|
|
3841
|
+
const x = cam.x != null ? cam.x * W - size / 2 : cam.position.includes("right") ? W - mg - size : mg;
|
|
3842
|
+
const y = cam.y != null ? cam.y * H - size / 2 : cam.position.includes("top") ? mg : H - mg - size;
|
|
3249
3843
|
return {
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
radius: (clip.box.radius ?? OVERLAY_BOX_RADIUS) * size
|
|
3844
|
+
x,
|
|
3845
|
+
y,
|
|
3846
|
+
size,
|
|
3847
|
+
radius: cam.shape === "rounded" ? (cam.radius ?? 18) * s : size / 2
|
|
3255
3848
|
};
|
|
3256
3849
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
label: "JetBrains Mono"
|
|
3261
|
-
};
|
|
3262
|
-
var TEXT_PRESETS = {
|
|
3263
|
-
title: {
|
|
3264
|
-
stack: "Lexend, -apple-system, system-ui, sans-serif",
|
|
3265
|
-
weight: 600,
|
|
3266
|
-
size: 64,
|
|
3267
|
-
color: "#fafafa",
|
|
3268
|
-
shadow: 0.45
|
|
3269
|
-
},
|
|
3270
|
-
caption: {
|
|
3271
|
-
stack: "Lexend, -apple-system, system-ui, sans-serif",
|
|
3272
|
-
weight: 400,
|
|
3273
|
-
size: 32,
|
|
3274
|
-
color: "#f4f4f5",
|
|
3275
|
-
shadow: 0.4
|
|
3276
|
-
},
|
|
3277
|
-
label: {
|
|
3278
|
-
stack: "'JetBrains Mono', ui-monospace, SFMono-Regular, monospace",
|
|
3279
|
-
weight: 400,
|
|
3280
|
-
size: 22,
|
|
3281
|
-
color: "#e4e4e7",
|
|
3282
|
-
shadow: 0.35
|
|
3283
|
-
}
|
|
3284
|
-
};
|
|
3285
|
-
var OVERLAY_SIZE_MIN = 12;
|
|
3286
|
-
var OVERLAY_SIZE_MAX = 200;
|
|
3287
|
-
var OVERLAY_FONT_FACES = [
|
|
3288
|
-
{ family: "Lexend", weight: 400, url: fontFaceUrl2("lexend", 400) },
|
|
3289
|
-
{ family: "Lexend", weight: 600, url: fontFaceUrl2("lexend", 600) },
|
|
3290
|
-
{
|
|
3291
|
-
family: "JetBrains Mono",
|
|
3292
|
-
weight: 400,
|
|
3293
|
-
url: fontFaceUrl2("jetbrains-mono", 400)
|
|
3850
|
+
function focusBounds(level, layout, camera = "card", screen) {
|
|
3851
|
+
if (camera === "stage" && screen && level > 1.001) {
|
|
3852
|
+
return { minX: 0, maxX: 1, minY: 0, maxY: 1 };
|
|
3294
3853
|
}
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
const presetName = clip.preset in TEXT_PRESETS ? clip.preset : "title";
|
|
3298
|
-
const base = TEXT_PRESETS[presetName];
|
|
3299
|
-
let stack = base.stack;
|
|
3300
|
-
let weight = base.weight;
|
|
3301
|
-
const familyEntry = findFontFamily2(clip.family ?? PRESET_FAMILY[presetName]);
|
|
3302
|
-
if (clip.family) {
|
|
3303
|
-
const quoted = clip.family.includes(" ") ? `'${clip.family}'` : clip.family;
|
|
3304
|
-
stack = familyEntry ? fontStack(familyEntry) : `${quoted}, ${base.stack}`;
|
|
3854
|
+
if (level <= 1.001) {
|
|
3855
|
+
return { minX: 0.5, maxX: 0.5, minY: 0.5, maxY: 0.5 };
|
|
3305
3856
|
}
|
|
3306
|
-
if (
|
|
3307
|
-
const
|
|
3308
|
-
|
|
3857
|
+
if (camera === "stage") {
|
|
3858
|
+
const x2 = stageAxisBounds(
|
|
3859
|
+
layout.dx,
|
|
3860
|
+
layout.dw,
|
|
3861
|
+
layout.cardX,
|
|
3862
|
+
layout.cardW,
|
|
3863
|
+
layout.W,
|
|
3864
|
+
level
|
|
3865
|
+
);
|
|
3866
|
+
const y2 = stageAxisBounds(
|
|
3867
|
+
layout.dy,
|
|
3868
|
+
layout.dh,
|
|
3869
|
+
layout.cardY,
|
|
3870
|
+
layout.cardH,
|
|
3871
|
+
layout.H,
|
|
3872
|
+
level
|
|
3873
|
+
);
|
|
3874
|
+
return { minX: x2.min, maxX: x2.max, minY: y2.min, maxY: y2.max };
|
|
3309
3875
|
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
Math.max(OVERLAY_SIZE_MIN, clip.size)
|
|
3318
|
-
)
|
|
3319
|
-
} : {},
|
|
3320
|
-
...clip.color ? { color: clip.color } : {},
|
|
3321
|
-
...clip.shadow !== void 0 ? { shadow: Math.min(1, Math.max(0, clip.shadow)) } : {},
|
|
3322
|
-
fontStyle: clip.italic ? "italic" : "normal",
|
|
3323
|
-
align: clip.align ?? "center",
|
|
3324
|
-
letterSpacing: clip.letterSpacing ?? 0,
|
|
3325
|
-
lineHeight: clip.lineHeight ?? OVERLAY_LINE_HEIGHT,
|
|
3326
|
-
stroke: clip.stroke ?? null
|
|
3327
|
-
};
|
|
3328
|
-
}
|
|
3329
|
-
function overlayFaceFor(clip) {
|
|
3330
|
-
const entry = findFontFamily2(
|
|
3331
|
-
clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
|
|
3876
|
+
const x = axisBounds(
|
|
3877
|
+
layout.dx,
|
|
3878
|
+
layout.dw,
|
|
3879
|
+
layout.cardX,
|
|
3880
|
+
layout.cardW,
|
|
3881
|
+
layout.W,
|
|
3882
|
+
level
|
|
3332
3883
|
);
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3884
|
+
const y = axisBounds(
|
|
3885
|
+
layout.dy,
|
|
3886
|
+
layout.dh,
|
|
3887
|
+
layout.cardY,
|
|
3888
|
+
layout.cardH,
|
|
3889
|
+
layout.H,
|
|
3890
|
+
level
|
|
3337
3891
|
);
|
|
3338
|
-
|
|
3339
|
-
return {
|
|
3340
|
-
family: entry.family,
|
|
3341
|
-
weight,
|
|
3342
|
-
url: fontFaceUrl2(entry.slug, weight)
|
|
3343
|
-
};
|
|
3344
|
-
}
|
|
3345
|
-
function overlayFontFaces(doc) {
|
|
3346
|
-
const faces = [...OVERLAY_FONT_FACES];
|
|
3347
|
-
const seen = new Set(faces.map((f) => `${f.family}|${f.weight}`));
|
|
3348
|
-
const add = (face) => {
|
|
3349
|
-
if (!face) return;
|
|
3350
|
-
const key = `${face.family}|${face.weight}`;
|
|
3351
|
-
if (seen.has(key)) return;
|
|
3352
|
-
seen.add(key);
|
|
3353
|
-
faces.push(face);
|
|
3354
|
-
};
|
|
3355
|
-
for (const o of doc.overlays ?? []) {
|
|
3356
|
-
if (o.kind !== "text") continue;
|
|
3357
|
-
add(overlayFaceFor(o));
|
|
3358
|
-
add(resolveEmphasis(o)?.face);
|
|
3359
|
-
}
|
|
3360
|
-
return faces;
|
|
3361
|
-
}
|
|
3362
|
-
function overlayLines(text) {
|
|
3363
|
-
const lines = text.split("\n");
|
|
3364
|
-
return lines.length ? lines : [""];
|
|
3365
|
-
}
|
|
3366
|
-
var EM_OPEN = "";
|
|
3367
|
-
var EM_CLOSE = "";
|
|
3368
|
-
function parseEmphasis(text) {
|
|
3369
|
-
let any = false;
|
|
3370
|
-
const lines = text.split("\n").map((line) => {
|
|
3371
|
-
const chars = [];
|
|
3372
|
-
const bold = [];
|
|
3373
|
-
const marker = [];
|
|
3374
|
-
for (let i = 0; i < line.length; i++) {
|
|
3375
|
-
if (line[i] === "\\" && line[i + 1] === "*") {
|
|
3376
|
-
chars.push("*");
|
|
3377
|
-
marker.push(false);
|
|
3378
|
-
i++;
|
|
3379
|
-
} else {
|
|
3380
|
-
chars.push(line[i]);
|
|
3381
|
-
marker.push(line[i] === "*");
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
const keep = chars.map(() => true);
|
|
3385
|
-
for (let i = 0; i < chars.length; i++) {
|
|
3386
|
-
if (!marker[i]) continue;
|
|
3387
|
-
let j = i + 1;
|
|
3388
|
-
while (j < chars.length && !marker[j]) j++;
|
|
3389
|
-
if (j >= chars.length) break;
|
|
3390
|
-
const inner = chars.slice(i + 1, j).join("");
|
|
3391
|
-
if (!inner.trim()) {
|
|
3392
|
-
i = j - 1;
|
|
3393
|
-
continue;
|
|
3394
|
-
}
|
|
3395
|
-
keep[i] = false;
|
|
3396
|
-
keep[j] = false;
|
|
3397
|
-
for (let k = i + 1; k < j; k++) bold[k] = true;
|
|
3398
|
-
any = true;
|
|
3399
|
-
i = j;
|
|
3400
|
-
}
|
|
3401
|
-
const kept = [];
|
|
3402
|
-
for (let i = 0; i < chars.length; i++) {
|
|
3403
|
-
if (keep[i])
|
|
3404
|
-
kept.push({ c: chars[i], b: !!bold[i] && !/\s/.test(chars[i]) });
|
|
3405
|
-
}
|
|
3406
|
-
let out = "";
|
|
3407
|
-
for (let i = 0; i < kept.length; i++) {
|
|
3408
|
-
const { c, b } = kept[i];
|
|
3409
|
-
if (b && !(i > 0 && kept[i - 1].b)) out += EM_OPEN;
|
|
3410
|
-
out += c;
|
|
3411
|
-
if (b && !(i + 1 < kept.length && kept[i + 1].b)) out += EM_CLOSE;
|
|
3412
|
-
}
|
|
3413
|
-
return out;
|
|
3414
|
-
});
|
|
3415
|
-
return any ? lines.join("\n") : null;
|
|
3892
|
+
return { minX: x.min, maxX: x.max, minY: y.min, maxY: y.max };
|
|
3416
3893
|
}
|
|
3417
|
-
function
|
|
3418
|
-
|
|
3419
|
-
|
|
3894
|
+
function axisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level) {
|
|
3895
|
+
const k = 1 - 1 / level;
|
|
3896
|
+
let lo = (coverOff / k - anchorOff) / anchorSize;
|
|
3897
|
+
let hi = ((coverOff + coverSize - viewport / level) / k - anchorOff) / anchorSize;
|
|
3898
|
+
if (lo > hi) {
|
|
3899
|
+
const mid = (lo + hi) / 2;
|
|
3900
|
+
lo = mid;
|
|
3901
|
+
hi = mid;
|
|
3902
|
+
}
|
|
3903
|
+
return { min: clamp013(lo), max: clamp013(hi) };
|
|
3420
3904
|
}
|
|
3421
|
-
function
|
|
3422
|
-
|
|
3905
|
+
function stageAxisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level) {
|
|
3906
|
+
const b = CAMERA_COVER_MIN / 2 * viewport;
|
|
3907
|
+
let lo = (coverOff + b / level - anchorOff) / anchorSize;
|
|
3908
|
+
let hi = (coverOff + coverSize - b / level - anchorOff) / anchorSize;
|
|
3909
|
+
if (lo > hi) {
|
|
3910
|
+
const mid = (lo + hi) / 2;
|
|
3911
|
+
lo = mid;
|
|
3912
|
+
hi = mid;
|
|
3913
|
+
}
|
|
3914
|
+
return { min: clamp013(lo), max: clamp013(hi) };
|
|
3423
3915
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
if (!clip.emphasis || parseEmphasis(clip.text) === null) return null;
|
|
3427
|
-
const style = resolveOverlayStyle(clip);
|
|
3428
|
-
const entry = findFontFamily2(
|
|
3429
|
-
clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
|
|
3430
|
-
);
|
|
3431
|
-
const wanted = clip.emphasis?.weight ?? Math.max(EMPHASIS_WEIGHT, style.weight);
|
|
3432
|
-
const weight = entry ? nearestFontWeight2(entry, wanted) : wanted;
|
|
3433
|
-
const inBase = OVERLAY_FONT_FACES.some(
|
|
3434
|
-
(f) => entry && f.family === entry.family && f.weight === weight
|
|
3435
|
-
);
|
|
3916
|
+
function clampFocus(cx, cy, level, layout, camera = "card", screen) {
|
|
3917
|
+
const b = focusBounds(level, layout, camera, screen);
|
|
3436
3918
|
return {
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
...entry && !inBase ? {
|
|
3440
|
-
face: {
|
|
3441
|
-
family: entry.family,
|
|
3442
|
-
weight,
|
|
3443
|
-
url: fontFaceUrl2(entry.slug, weight)
|
|
3444
|
-
}
|
|
3445
|
-
} : {}
|
|
3919
|
+
cx: Math.min(b.maxX, Math.max(b.minX, cx)),
|
|
3920
|
+
cy: Math.min(b.maxY, Math.max(b.minY, cy))
|
|
3446
3921
|
};
|
|
3447
3922
|
}
|
|
3448
|
-
function
|
|
3449
|
-
|
|
3450
|
-
let run = "";
|
|
3451
|
-
let b = bold;
|
|
3452
|
-
for (const c of text) {
|
|
3453
|
-
if (c === EM_OPEN || c === EM_CLOSE) {
|
|
3454
|
-
if (run) w += measure(run, b ? emFont : font);
|
|
3455
|
-
run = "";
|
|
3456
|
-
b = c === EM_OPEN;
|
|
3457
|
-
} else run += c;
|
|
3458
|
-
}
|
|
3459
|
-
if (run) w += measure(run, b ? emFont : font);
|
|
3460
|
-
return w;
|
|
3923
|
+
function clamp013(v) {
|
|
3924
|
+
return Math.max(0, Math.min(1, v));
|
|
3461
3925
|
}
|
|
3462
|
-
function
|
|
3463
|
-
return
|
|
3926
|
+
function screenPoint(layout, camera, screen) {
|
|
3927
|
+
return camera === "stage" && screen ? { ox: screen.x * layout.W, oy: screen.y * layout.H } : { ox: layout.W / 2, oy: layout.H / 2 };
|
|
3464
3928
|
}
|
|
3465
|
-
function
|
|
3466
|
-
|
|
3467
|
-
const out = [];
|
|
3468
|
-
for (const line of lines) {
|
|
3469
|
-
if (!line || measure(line) <= maxPx) {
|
|
3470
|
-
out.push(line);
|
|
3471
|
-
continue;
|
|
3472
|
-
}
|
|
3473
|
-
let current = "";
|
|
3474
|
-
for (const token of overlayTokens(line)) {
|
|
3475
|
-
if (!current) {
|
|
3476
|
-
current = token;
|
|
3477
|
-
continue;
|
|
3478
|
-
}
|
|
3479
|
-
if (measure(current + token) <= maxPx) {
|
|
3480
|
-
current += token;
|
|
3481
|
-
} else {
|
|
3482
|
-
out.push(current);
|
|
3483
|
-
current = token;
|
|
3484
|
-
}
|
|
3485
|
-
}
|
|
3486
|
-
if (current) out.push(current);
|
|
3487
|
-
}
|
|
3488
|
-
return out.length ? out : [""];
|
|
3929
|
+
function cameraModel(frame) {
|
|
3930
|
+
return frame?.camera === "stage" ? "stage" : "card";
|
|
3489
3931
|
}
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
)
|
|
3496
|
-
].map((s) => s.segment);
|
|
3497
|
-
}
|
|
3498
|
-
return [...line];
|
|
3932
|
+
var CAMERA_CENTRE_RAMP = 0.3;
|
|
3933
|
+
var CAMERA_COVER_MIN = 0.8;
|
|
3934
|
+
function cameraCentring(level) {
|
|
3935
|
+
const u = clamp013((level - 1) / CAMERA_CENTRE_RAMP);
|
|
3936
|
+
return u * u * (3 - 2 * u);
|
|
3499
3937
|
}
|
|
3500
|
-
function
|
|
3501
|
-
const
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3938
|
+
function zoomView(level, cx, cy, layout, camera = "card", centring, screen) {
|
|
3939
|
+
const L = Math.max(1, level);
|
|
3940
|
+
const fx = layout.dx + cx * layout.dw;
|
|
3941
|
+
const fy = layout.dy + cy * layout.dh;
|
|
3942
|
+
const { ox, oy } = screenPoint(layout, camera, screen);
|
|
3943
|
+
const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
|
|
3944
|
+
const wcx = fx + (ox - fx) / L * (1 - t);
|
|
3945
|
+
const wcy = fy + (oy - fy) / L * (1 - t);
|
|
3946
|
+
return { level: L, fx, fy, wcx, wcy, ox, oy };
|
|
3506
3947
|
}
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
const
|
|
3510
|
-
|
|
3511
|
-
const
|
|
3512
|
-
const text = overlayDisplayText(clip);
|
|
3513
|
-
const units = overlaySegments(text, unit);
|
|
3514
|
-
const n = unit === "block" ? 1 : units.reduce((sum, line) => sum + line.length, 0);
|
|
3515
|
-
const upperN = unit === "line" && clip.maxWidth ? overlayLines(text).reduce(
|
|
3516
|
-
(sum, line) => sum + overlayTokens(line).length,
|
|
3517
|
-
0
|
|
3518
|
-
) : n;
|
|
3519
|
-
const typewriter = spec.fx === "typewriter";
|
|
3520
|
-
const dur = typewriter ? 1e-3 : Math.min(2, Math.max(0.05, spec.duration ?? OVERLAY_TRANSITION_DUR));
|
|
3521
|
-
const defaultStagger = typewriter ? 0.05 : unit === "block" ? 0 : 0.06;
|
|
3522
|
-
let st = Math.min(2, Math.max(0, spec.stagger ?? defaultStagger));
|
|
3523
|
-
if (upperN > 1) {
|
|
3524
|
-
const maxTotal = Math.max(dur, clipDuration * 0.9);
|
|
3525
|
-
st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
|
|
3526
|
-
}
|
|
3527
|
-
const round14 = (v) => Math.round(v * 1e4) / 1e4;
|
|
3948
|
+
function zoomViewport(level, cx, cy, layout, camera = "card", centring, screen) {
|
|
3949
|
+
const v = zoomView(level, cx, cy, layout, camera, centring, screen);
|
|
3950
|
+
const size = 1 / v.level;
|
|
3951
|
+
const mx = v.wcx + (layout.W / 2 - v.ox) / v.level;
|
|
3952
|
+
const my = v.wcy + (layout.H / 2 - v.oy) / v.level;
|
|
3528
3953
|
return {
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
dur: round14(dur),
|
|
3534
|
-
tt: round14(st * (upperN - 1) + dur),
|
|
3535
|
-
units,
|
|
3536
|
-
n
|
|
3954
|
+
x: (mx - layout.W / (2 * v.level)) / layout.W,
|
|
3955
|
+
y: (my - layout.H / (2 * v.level)) / layout.H,
|
|
3956
|
+
w: size,
|
|
3957
|
+
h: size
|
|
3537
3958
|
};
|
|
3538
3959
|
}
|
|
3539
|
-
function
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3960
|
+
function focusForViewportCentre(cX, cY, level, layout, camera = "card", centring, screen) {
|
|
3961
|
+
const L = Math.max(1, level);
|
|
3962
|
+
const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
|
|
3963
|
+
const a = 1 - (1 - t) / L;
|
|
3964
|
+
if (a <= 1e-6) return { cx: 0.5, cy: 0.5 };
|
|
3965
|
+
const { ox, oy } = screenPoint(layout, camera, screen);
|
|
3966
|
+
const fx = (cX * layout.W - ox * (1 - t) / L - (layout.W / 2 - ox) / L) / a;
|
|
3967
|
+
const fy = (cY * layout.H - oy * (1 - t) / L - (layout.H / 2 - oy) / L) / a;
|
|
3968
|
+
return { cx: (fx - layout.dx) / layout.dw, cy: (fy - layout.dy) / layout.dh };
|
|
3542
3969
|
}
|
|
3543
|
-
function
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
cx: clip.transform.x * frameW,
|
|
3547
|
-
cy: clip.transform.y * frameH,
|
|
3548
|
-
rotation: clip.transform.rotation || 0
|
|
3549
|
-
};
|
|
3550
|
-
if (clip.kind === "html") {
|
|
3551
|
-
const pb = htmlLayerPictureBox(clip);
|
|
3552
|
-
const w2 = htmlLayerWidth(clip) * frameW * scale;
|
|
3553
|
-
return { ...base, w: w2, h: w2 * (pb.height / pb.width) };
|
|
3554
|
-
}
|
|
3555
|
-
if (clip.kind !== "text") {
|
|
3556
|
-
const w2 = (clip.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH) * frameW * scale;
|
|
3557
|
-
return { ...base, w: w2, h: w2 / (mediaAspect || 16 / 9) };
|
|
3558
|
-
}
|
|
3559
|
-
const style = resolveOverlayStyle(clip);
|
|
3560
|
-
const font = overlayFontString(style, scale, 1);
|
|
3561
|
-
const ls = style.letterSpacing * scale;
|
|
3562
|
-
const em = resolveEmphasis(clip);
|
|
3563
|
-
const emFont = em ? overlayFontString({ ...style, weight: em.weight }, scale, 1) : "";
|
|
3564
|
-
const width = (t) => em ? measureEmphasized(t, (run, f) => measure(run, f, ls), font, emFont) : measure(t, font, ls);
|
|
3565
|
-
const text = overlayDisplayText(clip);
|
|
3566
|
-
const lines = clip.maxWidth ? wrapOverlayLines(overlayLines(text), width, clip.maxWidth * frameW) : overlayLines(text);
|
|
3567
|
-
let w = 0;
|
|
3568
|
-
for (const line of lines) w = Math.max(w, width(line));
|
|
3569
|
-
const lineH = style.size * scale * style.lineHeight;
|
|
3570
|
-
const box = resolveOverlayBox(clip);
|
|
3571
|
-
const padX = box ? box.padX * scale : 0;
|
|
3572
|
-
const padY = box ? box.padY * scale : 0;
|
|
3573
|
-
return {
|
|
3574
|
-
...base,
|
|
3575
|
-
w: Math.max(w, style.size * scale * 0.6) + padX * 2,
|
|
3576
|
-
// empty text still selectable
|
|
3577
|
-
h: lines.length * lineH + padY * 2
|
|
3578
|
-
};
|
|
3970
|
+
function levelForFocusFraction(frac) {
|
|
3971
|
+
if (!(frac > 0)) return clampZoomLevel(Number.POSITIVE_INFINITY);
|
|
3972
|
+
return clampZoomLevel(Math.max(1.1, 1 / frac));
|
|
3579
3973
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3974
|
+
|
|
3975
|
+
// src/stage.ts
|
|
3976
|
+
var CARD_FOV = 30;
|
|
3977
|
+
var OVERLAY_Z = -2;
|
|
3978
|
+
var CARD_Z = -4;
|
|
3979
|
+
var BACKGROUND_Z = -6;
|
|
3980
|
+
var CARD_OVERSCAN_STEP = 0.05;
|
|
3981
|
+
var CARD_OVERSCAN_MAX = 4;
|
|
3982
|
+
var CAMERA_NEAR = 0.1;
|
|
3983
|
+
var CAMERA_FAR = 100;
|
|
3984
|
+
function planeSizeAtDepth(distance, fovDeg, aspect) {
|
|
3985
|
+
const height = 2 * Math.abs(distance) * Math.tan(fovDeg * Math.PI / 180 / 2);
|
|
3986
|
+
return { width: height * aspect, height };
|
|
3987
|
+
}
|
|
3988
|
+
function cardPointToScreen(u, v) {
|
|
3989
|
+
return { sx: u, sy: v };
|
|
3591
3990
|
}
|
|
3592
3991
|
|
|
3593
3992
|
// src/text3d.ts
|
|
@@ -4317,6 +4716,11 @@ function push(keys, t, a) {
|
|
|
4317
4716
|
|
|
4318
4717
|
// src/lower/studioEntry.ts
|
|
4319
4718
|
import { timelineRuntimeCode } from "@vosjs/timeline/bundle";
|
|
4719
|
+
|
|
4720
|
+
// src/richText/layoutCode.ts
|
|
4721
|
+
var textLayoutCode = '"use strict";var __vosTextLayout=(()=>{var E=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var N=Object.prototype.hasOwnProperty;var W=(n,r)=>{for(var h in r)E(n,h,{get:r[h],enumerable:!0})},j=(n,r,h,l)=>{if(r&&typeof r=="object"||typeof r=="function")for(let f of A(r))!N.call(n,f)&&f!==h&&E(n,f,{get:()=>r[f],enumerable:!(l=k(r,f))||l.enumerable});return n};var C=n=>j(E({},"__esModule",{value:!0}),n);var v={};W(v,{DECORATION_THICKNESS_EM:()=>U,HIGHLIGHT_HALF_EM:()=>I,STRIKE_OFFSET_EM:()=>H,UNDERLINE_OFFSET_EM:()=>O,decorationRect:()=>K,graphemesOf:()=>_,layoutText:()=>G,tokenRanges:()=>F});var D=(n,r)=>(n.f||0)===(r.f||0)&&n.c===r.c&&n.u===r.u&&n.s===r.s&&n.h===r.h;function _(n){if(typeof Intl<"u"&&typeof Intl.Segmenter=="function"){let r=new Intl.Segmenter(void 0,{granularity:"grapheme"}),h=[];for(let l of r.segment(n))h.push(l.segment);return h}return Array.from(n)}function F(n){let r=[],h=/\\S+\\s*/g,l;for(;(l=h.exec(n))!==null;)r.push([l.index,l.index+l[0].length]);return r.length?r:[[0,n.length]]}function G(n,r){let h={},l=(i,u)=>{if(!i)return 0;let s=u+"\\0"+i,t=h[s];return t===void 0&&(t=r(i,u),h[s]=t),t},f=(i,u,s)=>{let t=[],o=0;for(let e=0;e<i.length;e++){let c=i[e],a=Math.max(u,o),m=Math.min(s,o+c.t.length);if(m>a){let g=c.t.slice(a-o,m-o),x=t[t.length-1];x&&D(x,c)?x.t+=g:t.push({...c,t:g})}if(o+=c.t.length,o>=s)break}return t},p=i=>{let u=0;for(let s=0;s<i.length;s++)u+=l(i[s].t,i[s].f||0);return u},R=(i,u,s)=>{let t=[];for(let o=0;o<i.length;o++){let e=i[o],c=Math.max(u,e.o),a=Math.min(s,e.o+e.t.length);if(a<=c)continue;let m=e.f||0,g=l(e.t.slice(0,c-e.o),m);t.push({...e,t:e.t.slice(c-e.o,a-e.o),x:e.x+g,w:l(e.t.slice(0,a-e.o),m)-g,o:c})}return t},L=(i,u,s)=>{let t=R(i,u,s);if(!t.length)return{x:0,w:0,parts:t};let o=t[t.length-1];return{x:t[0].x,w:o.x+o.w-t[0].x,parts:t}},y=[],d=n.maxWidth&&n.maxWidth>0?n.maxWidth:0,w=0,T=0,S=(i,u,s)=>{let t=f(i,u,s),o=[],e=0,c=0;for(let m=0;m<t.length;m++){let g=l(t[m].t,t[m].f||0);o.push({...t[m],x:e,w:g,o:c}),e+=g,c+=t[m].t.length}let a={w:e,frags:o,a:T+u,n:s-u};if(n.unit){let m=t.map(x=>x.t).join(""),g=[];if(n.unit==="line")g.push(L(o,0,m.length));else if(n.unit==="word"){let x=F(m);for(let b=0;b<x.length;b++)g.push(L(o,x[b][0],x[b][1]))}else{let x=0,b=_(m);for(let M=0;M<b.length;M++)g.push(L(o,x,x+b[M].length)),x+=b[M].length}a.units=g}e>w&&(w=e),y.push(a)};for(let i=0;i<n.lines.length;i++){let u=n.lines[i],s=0;for(let t=0;t<u.length;t++)s+=u[t].t.length;if(!d||!s||p(f(u,0,s))<=d)S(u,0,s);else{let t=u.map(a=>a.t).join(""),o=F(t),e=0,c=o[0][1];for(let a=1;a<o.length;a++)p(f(u,e,o[a][1]))<=d||(S(u,e,c),e=o[a][0]),c=o[a][1];S(u,e,c)}T+=s+1}return y.length||y.push({w:0,frags:[],a:0,n:0}),{lines:y,width:w}}var O=.42,U=.065,H=.02,I=.58;function K(n,r,h,l,f){let p=Math.round(r),R=Math.round(r+h);if(n==="h"){let d=Math.round(l-I*f),w=Math.round(l+I*f);return{x:p,y:d,w:R-p,h:w-d}}let L=Math.max(1,Math.round(U*f)),y=Math.round(l+(n==="u"?O:H)*f);return{x:p,y,w:R-p,h:L}}return C(v);})();';
|
|
4722
|
+
|
|
4723
|
+
// src/lower/studioEntry.ts
|
|
4320
4724
|
var STUDIO_ENTRY_ID = "vosso.studio";
|
|
4321
4725
|
function studioEntry(data) {
|
|
4322
4726
|
return {
|
|
@@ -4713,6 +5117,18 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
4713
5117
|
var s = H / 1080 // scale design-px controls to comp px
|
|
4714
5118
|
var ns = window.__vos__ || {}
|
|
4715
5119
|
var playing = ns.isPaused === false
|
|
5120
|
+
// The styled-text layout (richText/layout.ts, generated), installed once:
|
|
5121
|
+
// the page lays a text layer out with the code the host picks with.
|
|
5122
|
+
if (!ns.textLayout) { ${textLayoutCode}; ns.textLayout = __vosTextLayout }
|
|
5123
|
+
var TXL = ns.textLayout
|
|
5124
|
+
// The font epoch counts faces landing. A face changes what text measures
|
|
5125
|
+
// and what it paints, so the epoch is part of every layout's key and of
|
|
5126
|
+
// the layer's signature: a late face repaints a held layer once.
|
|
5127
|
+
var ofBump = function () { ns.fontEpoch = (ns.fontEpoch || 0) + 1 }
|
|
5128
|
+
if (!ns.fontWatch && typeof document !== 'undefined' && document.fonts && document.fonts.addEventListener) {
|
|
5129
|
+
ns.fontWatch = 1
|
|
5130
|
+
document.fonts.addEventListener('loadingdone', ofBump)
|
|
5131
|
+
}
|
|
4716
5132
|
|
|
4717
5133
|
// --- audio clips (music/SFX): Web Audio scheduler against the transport ---
|
|
4718
5134
|
// Buffer sources are one-shot: (re)schedule everything on play / clip-data change
|
|
@@ -4840,7 +5256,19 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
4840
5256
|
if (t < ol0.start || t > ol0.start + ol0.dur) continue
|
|
4841
5257
|
// A page that taints on foreignObject draws no HTML layer (setup's probe).
|
|
4842
5258
|
if (ol0.html && ns.htmlSafe === false) continue
|
|
4843
|
-
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 : '')
|
|
5259
|
+
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 : '')
|
|
5260
|
+
// Everything else a held layer's pixels depend on. The signature is the
|
|
5261
|
+
// ONLY thing that repaints a hold, so a field missing here is an edit
|
|
5262
|
+
// that does not show until something else moves.
|
|
5263
|
+
if (ol0.lines) {
|
|
5264
|
+
olVisSig += '|' + ol0.lines.join('\\n') + '|' + ol0.weight + '|' + ol0.stack + '|' + (ol0.sty || '') + '|' + (ol0.ls || 0) + '|' + (ol0.lh || 0) + '|' + (ol0.align || '') + '|' + ol0.shadow
|
|
5265
|
+
+ (ol0.stroke ? '|s' + ol0.stroke.c + ol0.stroke.w : '')
|
|
5266
|
+
+ (ol0.box ? '|b' + ol0.box.c + ol0.box.o + ',' + ol0.box.px + ',' + ol0.box.py + ',' + ol0.box.r : '')
|
|
5267
|
+
+ (ol0.rt ? '|r' + ol0.rt.h : '')
|
|
5268
|
+
} else {
|
|
5269
|
+
olVisSig += '|' + (ol0.shadow || '') + (ol0.border ? '|' + ol0.border.width + ol0.border.color : '') + (ol0.crop ? '|c' + ol0.crop.x + ',' + ol0.crop.y + ',' + ol0.crop.w + ',' + ol0.crop.h : '')
|
|
5270
|
+
}
|
|
5271
|
+
olVisSig += ';'
|
|
4844
5272
|
// fx widens the entrance window to the whole staggered span (tt);
|
|
4845
5273
|
// without fx it is the legacy enter transition window.
|
|
4846
5274
|
var olEW = ol0.fx ? ol0.fx.tt : olTD
|
|
@@ -4878,8 +5306,20 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
4878
5306
|
if (ns.htmlErrors) delete ns.htmlErrors[hlK]
|
|
4879
5307
|
}
|
|
4880
5308
|
}
|
|
4881
|
-
|
|
4882
|
-
|
|
5309
|
+
// A styled layer's cached layout leaves with its clip.
|
|
5310
|
+
if (ns.textLayouts) {
|
|
5311
|
+
for (var tlK in ns.textLayouts) {
|
|
5312
|
+
var tlSeen = false
|
|
5313
|
+
for (var tlI = 0; tlI < ols.length; tlI++) if (ols[tlI].id === tlK) { tlSeen = true; break }
|
|
5314
|
+
if (!tlSeen) delete ns.textLayouts[tlK]
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
var ovSig = W + 'x' + H + '|' + (ns.fontEpoch || 0) + '|' + (ns.htmlSafe === false ? 0 : 1) + '|' + olVisSig
|
|
5318
|
+
// The layer repaints when what it shows changed (the signature), while
|
|
5319
|
+
// something on it animates, and ONCE more after an animation's last frame:
|
|
5320
|
+
// that frame was drawn a moment short of settled, and a picture that
|
|
5321
|
+
// landed between two frames is drawn by it too. A hold costs nothing.
|
|
5322
|
+
var ovDirty = ov.sig !== ovSig || olAnim || ov.wasAnim
|
|
4883
5323
|
// The layer cards visible this frame, with their pose, for the card
|
|
4884
5324
|
// planes below (the overlay loop collects them instead of painting).
|
|
4885
5325
|
var lcList = []
|
|
@@ -5108,7 +5548,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5108
5548
|
try {
|
|
5109
5549
|
var ofBF = new FontFace(ofB.family, 'url(' + ofB.url + ')', { weight: String(ofB.weight) })
|
|
5110
5550
|
document.fonts.add(ofBF)
|
|
5111
|
-
ofBF.load().
|
|
5551
|
+
ofBF.load().then(ofBump, function () {})
|
|
5112
5552
|
} catch (e) {}
|
|
5113
5553
|
}
|
|
5114
5554
|
if (ol.face) {
|
|
@@ -5118,20 +5558,22 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5118
5558
|
try {
|
|
5119
5559
|
var ofFace = new FontFace(ol.face.f, 'url(' + ol.face.u + ')', { weight: String(ol.face.w) })
|
|
5120
5560
|
document.fonts.add(ofFace)
|
|
5121
|
-
ofFace.load().
|
|
5561
|
+
ofFace.load().then(ofBump, function () {})
|
|
5122
5562
|
} catch (e) {}
|
|
5123
5563
|
}
|
|
5124
5564
|
}
|
|
5125
|
-
//
|
|
5126
|
-
// first
|
|
5127
|
-
if (ol.
|
|
5128
|
-
var
|
|
5129
|
-
|
|
5130
|
-
|
|
5565
|
+
// A styled layer's other faces, the same way (a live edit that styles
|
|
5566
|
+
// its first run paints without a LOAD).
|
|
5567
|
+
if (ol.rt && ol.rt.faces) {
|
|
5568
|
+
for (var rfj = 0; rfj < ol.rt.faces.length; rfj++) {
|
|
5569
|
+
var rtF = ol.rt.faces[rfj]
|
|
5570
|
+
var rtFK = rtF.f + '|' + rtF.w
|
|
5571
|
+
if (ofSet[rtFK]) continue
|
|
5572
|
+
ofSet[rtFK] = 1
|
|
5131
5573
|
try {
|
|
5132
|
-
var
|
|
5133
|
-
document.fonts.add(
|
|
5134
|
-
|
|
5574
|
+
var rtFace = new FontFace(rtF.f, 'url(' + rtF.u + ')', { weight: String(rtF.w) })
|
|
5575
|
+
document.fonts.add(rtFace)
|
|
5576
|
+
rtFace.load().then(ofBump, function () {})
|
|
5135
5577
|
} catch (e) {}
|
|
5136
5578
|
}
|
|
5137
5579
|
}
|
|
@@ -5150,68 +5592,153 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5150
5592
|
ovC.textAlign = 'center'
|
|
5151
5593
|
ovC.textBaseline = 'middle'
|
|
5152
5594
|
ovC.letterSpacing = ((ol.ls || 0) * olMS * s) + 'px'
|
|
5153
|
-
//
|
|
5154
|
-
//
|
|
5155
|
-
//
|
|
5156
|
-
//
|
|
5157
|
-
//
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
var
|
|
5194
|
-
|
|
5595
|
+
// A STYLED layer (ol.rt: lines of runs, each set its own way) is laid
|
|
5596
|
+
// out by the layout module, ONCE: the layout is kept per clip under a
|
|
5597
|
+
// key of everything it was measured with, so a frame that only moves,
|
|
5598
|
+
// fades or reveals the layer measures nothing. Fragments draw left to
|
|
5599
|
+
// right from each line's left edge; a per-unit entrance animates the
|
|
5600
|
+
// layout's units, each a few parts of fragments.
|
|
5601
|
+
if (ol.rt) {
|
|
5602
|
+
var rtFonts = [ovC.font]
|
|
5603
|
+
for (var rfi = 1; rfi < ol.rt.fs.length; rfi++) {
|
|
5604
|
+
rtFonts.push((ol.rt.fs[rfi].i ? 'italic ' : '') + ol.rt.fs[rfi].w + ' ' + olPx + 'px ' + ol.stack)
|
|
5605
|
+
}
|
|
5606
|
+
var rtFx = ol.fx && ol.fx.units.length ? ol.fx : null
|
|
5607
|
+
var rtMax = ol.mw ? ol.mw * W : 0
|
|
5608
|
+
var rtKey = ol.rt.h + '|' + rtFonts[0] + '|' + ((ol.ls || 0) * olMS * s) + '|' + rtMax + '|' + (rtFx ? rtFx.u : '') + '|' + (ns.fontEpoch || 0)
|
|
5609
|
+
var rtAll = ns.textLayouts || (ns.textLayouts = {})
|
|
5610
|
+
var rtHit = rtAll[ol.id]
|
|
5611
|
+
if (!rtHit || rtHit.k !== rtKey) {
|
|
5612
|
+
rtHit = rtAll[ol.id] = {
|
|
5613
|
+
k: rtKey,
|
|
5614
|
+
L: TXL.layoutText(
|
|
5615
|
+
{ lines: ol.rt.l, maxWidth: rtMax, unit: rtFx ? rtFx.u : undefined },
|
|
5616
|
+
function (mt, mf) { ovC.font = rtFonts[mf]; return ovC.measureText(mt).width }
|
|
5617
|
+
)
|
|
5618
|
+
}
|
|
5619
|
+
ovC.font = rtFonts[0]
|
|
5620
|
+
}
|
|
5621
|
+
var rtL = rtHit.L
|
|
5622
|
+
var rtN = rtL.lines.length
|
|
5623
|
+
var rtLH = olPx * (ol.lh || ${OVERLAY_LINE_HEIGHT})
|
|
5624
|
+
var rtY0 = -((rtN - 1) * rtLH) / 2
|
|
5625
|
+
// A line's left edge: lines sit centred on the anchor and align
|
|
5626
|
+
// against the widest, as the plain layer's do.
|
|
5627
|
+
var rtLeft = function (lw) {
|
|
5628
|
+
var xo = ol.align === 'left' ? (lw - rtL.width) / 2 : ol.align === 'right' ? (rtL.width - lw) / 2 : 0
|
|
5629
|
+
return xo - lw / 2
|
|
5630
|
+
}
|
|
5631
|
+
if (ol.box) {
|
|
5632
|
+
var rbPX = ol.box.px * olMS * s
|
|
5633
|
+
var rbPY = ol.box.py * olMS * s
|
|
5634
|
+
var rbW = rtL.width + rbPX * 2
|
|
5635
|
+
var rbH = rtN * rtLH + rbPY * 2
|
|
5636
|
+
ovC.save()
|
|
5637
|
+
ovC.globalAlpha = olA * ol.box.o
|
|
5638
|
+
ovC.fillStyle = ol.box.c
|
|
5639
|
+
rr(-rbW / 2, -rbH / 2, rbW, rbH, Math.min(ol.box.r * olMS * s, rbH / 2), ovC)
|
|
5640
|
+
ovC.fill()
|
|
5641
|
+
ovC.restore()
|
|
5642
|
+
}
|
|
5643
|
+
// Highlights sit behind the words and take no legibility shadow. Under
|
|
5644
|
+
// a per-unit entrance each is drawn with its unit instead (below), so
|
|
5645
|
+
// it arrives with its word.
|
|
5646
|
+
for (var rhi = 0; !rtFx && rhi < rtN; rhi++) {
|
|
5647
|
+
var rhLine = rtL.lines[rhi]
|
|
5648
|
+
for (var rhj = 0; rhj < rhLine.frags.length; rhj++) {
|
|
5649
|
+
var rhF = rhLine.frags[rhj]
|
|
5650
|
+
if (!rhF.h) continue
|
|
5651
|
+
var rhR = TXL.decorationRect('h', rtLeft(rhLine.w) + rhF.x, rhF.w, rtY0 + rhi * rtLH, olPx)
|
|
5652
|
+
ovC.fillStyle = rhF.h
|
|
5653
|
+
ovC.fillRect(rhR.x, rhR.y, rhR.w, rhR.h)
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
if (ol.shadow > 0) {
|
|
5657
|
+
ovC.shadowColor = 'rgba(0,0,0,' + ol.shadow + ')'
|
|
5658
|
+
ovC.shadowBlur = olPx * 0.25
|
|
5659
|
+
ovC.shadowOffsetY = olPx * 0.04
|
|
5660
|
+
}
|
|
5661
|
+
ovC.textAlign = 'left'
|
|
5662
|
+
// One fragment (or the part of one a unit holds): the underline under
|
|
5663
|
+
// the glyphs, stroke under fill, the strikethrough over them.
|
|
5664
|
+
var rtPart = function (p, bx, y, withH) {
|
|
5665
|
+
if (withH && p.h) {
|
|
5666
|
+
var rhU = TXL.decorationRect('h', bx + p.x, p.w, y, olPx)
|
|
5667
|
+
var rhS = ovC.shadowColor
|
|
5668
|
+
ovC.shadowColor = 'rgba(0,0,0,0)'
|
|
5669
|
+
ovC.fillStyle = p.h
|
|
5670
|
+
ovC.fillRect(rhU.x, rhU.y, rhU.w, rhU.h)
|
|
5671
|
+
ovC.shadowColor = rhS
|
|
5672
|
+
}
|
|
5673
|
+
ovC.font = rtFonts[p.f || 0]
|
|
5674
|
+
ovC.fillStyle = p.c || ol.color
|
|
5675
|
+
if (p.u) {
|
|
5676
|
+
var ru = TXL.decorationRect('u', bx + p.x, p.w, y, olPx)
|
|
5677
|
+
ovC.fillRect(ru.x, ru.y, ru.w, ru.h)
|
|
5678
|
+
}
|
|
5195
5679
|
if (ol.stroke) {
|
|
5196
5680
|
ovC.strokeStyle = ol.stroke.c
|
|
5197
5681
|
ovC.lineWidth = ol.stroke.w * olMS * s
|
|
5198
5682
|
ovC.lineJoin = 'round'
|
|
5199
|
-
ovC.strokeText(
|
|
5683
|
+
ovC.strokeText(p.t, bx + p.x, y)
|
|
5684
|
+
}
|
|
5685
|
+
ovC.fillText(p.t, bx + p.x, y)
|
|
5686
|
+
if (p.s) {
|
|
5687
|
+
var rs = TXL.decorationRect('s', bx + p.x, p.w, y, olPx)
|
|
5688
|
+
ovC.fillRect(rs.x, rs.y, rs.w, rs.h)
|
|
5200
5689
|
}
|
|
5201
|
-
ovC.fillText(drun, dx, y)
|
|
5202
|
-
dx += ovC.measureText(drun).width
|
|
5203
|
-
ovC.fillStyle = dfs
|
|
5204
|
-
drun = ''
|
|
5205
5690
|
}
|
|
5206
|
-
|
|
5207
|
-
var
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
}
|
|
5691
|
+
if (!rtFx) {
|
|
5692
|
+
for (var rli = 0; rli < rtN; rli++) {
|
|
5693
|
+
var rLine = rtL.lines[rli]
|
|
5694
|
+
var rBX = rtLeft(rLine.w)
|
|
5695
|
+
for (var rfk = 0; rfk < rLine.frags.length; rfk++) rtPart(rLine.frags[rfk], rBX, rtY0 + rli * rtLH)
|
|
5696
|
+
}
|
|
5697
|
+
} else {
|
|
5698
|
+
// Per-unit entrance, the plain layer's own arithmetic over the
|
|
5699
|
+
// layout's units: delay = order(index) x st, eased over dur.
|
|
5700
|
+
var rtCount = 0
|
|
5701
|
+
for (var rci = 0; rci < rtN; rci++) rtCount += rtL.lines[rci].units.length
|
|
5702
|
+
var rtIdx = 0
|
|
5703
|
+
for (var rui = 0; rui < rtN; rui++) {
|
|
5704
|
+
var rULine = rtL.lines[rui]
|
|
5705
|
+
var rUBX = rtLeft(rULine.w)
|
|
5706
|
+
var rUY = rtY0 + rui * rtLH
|
|
5707
|
+
for (var ruj = 0; ruj < rULine.units.length; ruj++, rtIdx++) {
|
|
5708
|
+
var rU = rULine.units[ruj]
|
|
5709
|
+
var rOrd = rtFx.d === 1 ? (rtCount - 1 - rtIdx) : rtFx.d === 2 ? Math.abs(rtIdx - (rtCount - 1) / 2) : rtIdx
|
|
5710
|
+
var rT2 = olT - rOrd * rtFx.st
|
|
5711
|
+
var rUA = 1, rUu = 1
|
|
5712
|
+
if (rtFx.k === 'typewriter') {
|
|
5713
|
+
rUA = rT2 >= 0 ? 1 : 0
|
|
5714
|
+
} else {
|
|
5715
|
+
rUu = Math.max(0, Math.min(1, rT2 / rtFx.dur))
|
|
5716
|
+
var rUE = 1 - Math.pow(1 - rUu, 3)
|
|
5717
|
+
rUA = rtFx.k === 'pop' ? Math.min(1, rUu * 2) : rUE
|
|
5718
|
+
}
|
|
5719
|
+
if (rUA <= 0.004) continue
|
|
5720
|
+
ovC.save()
|
|
5721
|
+
ovC.globalAlpha = olA * rUA
|
|
5722
|
+
if (rUu < 1) {
|
|
5723
|
+
if (rtFx.k === 'rise') {
|
|
5724
|
+
ovC.translate(0, (1 - (1 - Math.pow(1 - rUu, 3))) * 24 * s)
|
|
5725
|
+
} else if (rtFx.k === 'pop') {
|
|
5726
|
+
var rPS = 1 + 2.70158 * Math.pow(rUu - 1, 3) + 1.70158 * Math.pow(rUu - 1, 2)
|
|
5727
|
+
var rCX = rUBX + rU.x + rU.w / 2
|
|
5728
|
+
ovC.translate(rCX, rUY)
|
|
5729
|
+
ovC.scale(rPS, rPS)
|
|
5730
|
+
ovC.translate(-rCX, -rUY)
|
|
5731
|
+
} else if (rtFx.k === 'blur') {
|
|
5732
|
+
ovC.filter = 'blur(' + ((1 - rUu) * olPx * 0.12).toFixed(2) + 'px)'
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY, true)
|
|
5736
|
+
ovC.restore()
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5212
5739
|
}
|
|
5213
|
-
|
|
5214
|
-
|
|
5740
|
+
ovC.restore()
|
|
5741
|
+
continue
|
|
5215
5742
|
}
|
|
5216
5743
|
var olLines = ol.lines || ['']
|
|
5217
5744
|
// maxWidth wrap (ol.mw = frame-width fraction): greedy over word tokens
|
|
@@ -5224,7 +5751,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5224
5751
|
var olWrapped = []
|
|
5225
5752
|
for (var olwl = 0; olwl < olLines.length; olwl++) {
|
|
5226
5753
|
var olWLine = olLines[olwl]
|
|
5227
|
-
if (!olWLine ||
|
|
5754
|
+
if (!olWLine || ovC.measureText(olWLine).width <= olWMax) {
|
|
5228
5755
|
olWrapped.push(olWLine)
|
|
5229
5756
|
continue
|
|
5230
5757
|
}
|
|
@@ -5232,7 +5759,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5232
5759
|
var olCur = ''
|
|
5233
5760
|
for (var olti = 0; olti < olToks.length; olti++) {
|
|
5234
5761
|
if (!olCur) { olCur = olToks[olti]; continue }
|
|
5235
|
-
if (
|
|
5762
|
+
if (ovC.measureText(olCur + olToks[olti]).width <= olWMax) {
|
|
5236
5763
|
olCur += olToks[olti]
|
|
5237
5764
|
} else {
|
|
5238
5765
|
olWrapped.push(olCur)
|
|
@@ -5282,10 +5809,10 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5282
5809
|
olFx = { k: olFx.k, u: olFx.u, d: olFx.d, st: olFx.st, dur: olFx.dur, tt: olFx.tt, units: olRe, n: olReN }
|
|
5283
5810
|
}
|
|
5284
5811
|
var olLWs = null, olMaxW = 0
|
|
5285
|
-
if (ol.box || ol.align || olFx
|
|
5812
|
+
if (ol.box || ol.align || olFx) {
|
|
5286
5813
|
olLWs = []
|
|
5287
5814
|
for (var olwi = 0; olwi < olLines.length; olwi++) {
|
|
5288
|
-
var olw =
|
|
5815
|
+
var olw = ovC.measureText(olLines[olwi]).width
|
|
5289
5816
|
olLWs.push(olw)
|
|
5290
5817
|
if (olw > olMaxW) olMaxW = olw
|
|
5291
5818
|
}
|
|
@@ -5321,12 +5848,6 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5321
5848
|
: (olMaxW - olLWs[ok]) / 2
|
|
5322
5849
|
}
|
|
5323
5850
|
var olLY = olY0 + ok * olLH
|
|
5324
|
-
if (olEm) {
|
|
5325
|
-
// Runs draw LEFT-aligned from the centred line's left edge.
|
|
5326
|
-
ovC.textAlign = 'left'
|
|
5327
|
-
olDraw(olLines[ok], olXof - olLWs[ok] / 2, olLY, false)
|
|
5328
|
-
continue
|
|
5329
|
-
}
|
|
5330
5851
|
if (ol.stroke) {
|
|
5331
5852
|
ovC.strokeStyle = ol.stroke.c
|
|
5332
5853
|
ovC.lineWidth = ol.stroke.w * olMS * s
|
|
@@ -5363,10 +5884,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5363
5884
|
: olIdx
|
|
5364
5885
|
var olT2 = olT - olOrd * olFx.st
|
|
5365
5886
|
var olNext = olPref + olUs[ou]
|
|
5366
|
-
var olNW =
|
|
5367
|
-
// The emphasis state this unit starts in (a char unit inside a
|
|
5368
|
-
// marked word carries no mark of its own).
|
|
5369
|
-
var olUB = olEm ? olStateAfter(olPref) : false
|
|
5887
|
+
var olNW = ovC.measureText(olNext).width
|
|
5370
5888
|
var olUW = olNW - olPW
|
|
5371
5889
|
var olUX = olXb + olPW
|
|
5372
5890
|
var olUA = 1, olUu = 1
|
|
@@ -5395,11 +5913,6 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5395
5913
|
ovC.filter = 'blur(' + ((1 - olUu) * olPx * 0.12).toFixed(2) + 'px)'
|
|
5396
5914
|
}
|
|
5397
5915
|
}
|
|
5398
|
-
if (olEm) {
|
|
5399
|
-
olDraw(olUnit, olUX, olLY2, olUB)
|
|
5400
|
-
ovC.restore()
|
|
5401
|
-
continue
|
|
5402
|
-
}
|
|
5403
5916
|
if (ol.stroke) {
|
|
5404
5917
|
ovC.strokeStyle = ol.stroke.c
|
|
5405
5918
|
ovC.lineWidth = ol.stroke.w * olMS * s
|
|
@@ -5414,7 +5927,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5414
5927
|
ovC.restore()
|
|
5415
5928
|
}
|
|
5416
5929
|
ov.sig = ovSig
|
|
5417
|
-
ov.
|
|
5930
|
+
ov.wasAnim = olAnim
|
|
5418
5931
|
if (ov.texture) ov.texture.needsUpdate = true
|
|
5419
5932
|
}
|
|
5420
5933
|
|
|
@@ -8191,7 +8704,8 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
8191
8704
|
window.__voilaDebug = { cv: cv, bgCv: bg.canvas, ovCv: ov.canvas, W: W, H: H, glW: gl && gl.width, glH: gl && gl.height, t: t, canvases: document.querySelectorAll('canvas').length }
|
|
8192
8705
|
}
|
|
8193
8706
|
}`;
|
|
8194
|
-
function studioLayerData(
|
|
8707
|
+
function studioLayerData(input, duration, pins) {
|
|
8708
|
+
const layers = migrateText(input);
|
|
8195
8709
|
const rec = "source" in layers ? layers : null;
|
|
8196
8710
|
return {
|
|
8197
8711
|
// Music/SFX clips with their gain envelopes baked (shared truth for the
|
|
@@ -8367,27 +8881,16 @@ function studioLayerData(layers, duration, pins) {
|
|
|
8367
8881
|
}
|
|
8368
8882
|
const st = resolveOverlayStyle(o);
|
|
8369
8883
|
const bx = resolveOverlayBox(o);
|
|
8370
|
-
const
|
|
8884
|
+
const rt = styledTextOf(o);
|
|
8371
8885
|
return {
|
|
8372
8886
|
...base,
|
|
8373
|
-
text: o
|
|
8374
|
-
// The
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
//
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
w: em.weight,
|
|
8381
|
-
...em.color ? { c: em.color } : {},
|
|
8382
|
-
...em.face ? {
|
|
8383
|
-
face: {
|
|
8384
|
-
f: em.face.family,
|
|
8385
|
-
w: em.face.weight,
|
|
8386
|
-
u: em.face.url
|
|
8387
|
-
}
|
|
8388
|
-
} : {}
|
|
8389
|
-
}
|
|
8390
|
-
} : {},
|
|
8887
|
+
text: overlayPlainText(o),
|
|
8888
|
+
// The lines a person reads. A styled layer (rt) is painted from
|
|
8889
|
+
// its runs; these are then its words, for the signature.
|
|
8890
|
+
lines: overlayLines(overlayPlainText(o)),
|
|
8891
|
+
// The styled payload, only when the layer is set in more than
|
|
8892
|
+
// one style: a plain clip's data is unchanged.
|
|
8893
|
+
...rt ? { rt } : {},
|
|
8391
8894
|
fs: st.size,
|
|
8392
8895
|
weight: st.weight,
|
|
8393
8896
|
stack: st.stack,
|
|
@@ -8435,7 +8938,7 @@ function studioLayerData(layers, duration, pins) {
|
|
|
8435
8938
|
};
|
|
8436
8939
|
}
|
|
8437
8940
|
function lowerToComposition(input) {
|
|
8438
|
-
const doc = migrateMotion(input);
|
|
8941
|
+
const doc = migrateText(migrateMotion(input));
|
|
8439
8942
|
const rated = ratedSegments(doc);
|
|
8440
8943
|
const transitions = docTransitions(doc);
|
|
8441
8944
|
const footageEnd = durationSec(doc, rated);
|
|
@@ -8644,7 +9147,7 @@ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
|
|
|
8644
9147
|
});
|
|
8645
9148
|
}
|
|
8646
9149
|
function docZoomTrack(input) {
|
|
8647
|
-
const doc = migrateMotion(input);
|
|
9150
|
+
const doc = migrateText(migrateMotion(input));
|
|
8648
9151
|
const rated = ratedSegments(doc);
|
|
8649
9152
|
const transitions = docTransitions(doc);
|
|
8650
9153
|
const layout = docCardLayout(doc);
|
|
@@ -10520,6 +11023,68 @@ function applyLook(frame, look, size, video, placement) {
|
|
|
10520
11023
|
return out;
|
|
10521
11024
|
}
|
|
10522
11025
|
|
|
11026
|
+
// src/richText/caret.ts
|
|
11027
|
+
function caretStops(line, measure) {
|
|
11028
|
+
const stops = [{ o: line.a, x: 0 }];
|
|
11029
|
+
for (const fr of line.frags) {
|
|
11030
|
+
let at = 0;
|
|
11031
|
+
for (const g of graphemesOf(fr.t)) {
|
|
11032
|
+
at += g.length;
|
|
11033
|
+
stops.push({
|
|
11034
|
+
o: line.a + fr.o + at,
|
|
11035
|
+
x: fr.x + measure(fr.t.slice(0, at), fr.f || 0)
|
|
11036
|
+
});
|
|
11037
|
+
}
|
|
11038
|
+
}
|
|
11039
|
+
return stops;
|
|
11040
|
+
}
|
|
11041
|
+
function lineOfOffset(layout, offset) {
|
|
11042
|
+
const lines = layout.lines;
|
|
11043
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11044
|
+
if (offset >= lines[i].a) return i;
|
|
11045
|
+
}
|
|
11046
|
+
return 0;
|
|
11047
|
+
}
|
|
11048
|
+
function caretAt(layout, offset, measure) {
|
|
11049
|
+
const li = lineOfOffset(layout, offset);
|
|
11050
|
+
const stops = caretStops(layout.lines[li], measure);
|
|
11051
|
+
let x = stops[stops.length - 1].x;
|
|
11052
|
+
for (const s of stops) {
|
|
11053
|
+
if (s.o >= offset) {
|
|
11054
|
+
x = s.x;
|
|
11055
|
+
break;
|
|
11056
|
+
}
|
|
11057
|
+
}
|
|
11058
|
+
return { line: li, x };
|
|
11059
|
+
}
|
|
11060
|
+
function offsetAtX(line, x, measure) {
|
|
11061
|
+
const stops = caretStops(line, measure);
|
|
11062
|
+
let best = stops[0];
|
|
11063
|
+
for (const s of stops) {
|
|
11064
|
+
if (Math.abs(s.x - x) < Math.abs(best.x - x)) best = s;
|
|
11065
|
+
}
|
|
11066
|
+
return best.o;
|
|
11067
|
+
}
|
|
11068
|
+
function rangeRects(layout, start, end, measure, breakTail = 0) {
|
|
11069
|
+
const out = [];
|
|
11070
|
+
if (end <= start) return out;
|
|
11071
|
+
layout.lines.forEach((line, i) => {
|
|
11072
|
+
const lineEnd = line.a + line.n;
|
|
11073
|
+
if (end <= line.a || start > lineEnd) return;
|
|
11074
|
+
const stops = caretStops(line, measure);
|
|
11075
|
+
const xOf = (o) => {
|
|
11076
|
+
for (const s of stops) if (s.o >= o) return s.x;
|
|
11077
|
+
return stops[stops.length - 1].x;
|
|
11078
|
+
};
|
|
11079
|
+
const x0 = xOf(Math.max(start, line.a));
|
|
11080
|
+
const x1 = xOf(Math.min(end, lineEnd));
|
|
11081
|
+
const next = layout.lines[i + 1];
|
|
11082
|
+
const tail = next && next.a > lineEnd && end > lineEnd ? breakTail : 0;
|
|
11083
|
+
if (x1 + tail > x0) out.push({ line: i, x: x0, w: x1 - x0 + tail });
|
|
11084
|
+
});
|
|
11085
|
+
return out;
|
|
11086
|
+
}
|
|
11087
|
+
|
|
10523
11088
|
// src/timeline/lanes.ts
|
|
10524
11089
|
import {
|
|
10525
11090
|
mapTime as mapTime3,
|
|
@@ -11508,7 +12073,7 @@ var overlaysLane = {
|
|
|
11508
12073
|
kind: "clip",
|
|
11509
12074
|
t: round13(o.start),
|
|
11510
12075
|
duration: round13(o.duration),
|
|
11511
|
-
label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "html" ? htmlLayerLabel(o) : o.kind === "image" ? "Image" : "Video"
|
|
12076
|
+
label: o.kind === "text" ? plainText(o.text).split("\n")[0].slice(0, 24) || "Text" : o.kind === "html" ? htmlLayerLabel(o) : o.kind === "image" ? "Image" : "Video"
|
|
11512
12077
|
},
|
|
11513
12078
|
// Pose diamonds render after the clips, so they sit on top.
|
|
11514
12079
|
...poseItems(o.id, o.start, o.duration, o.motion)
|
|
@@ -11900,6 +12465,7 @@ export {
|
|
|
11900
12465
|
CURSOR_IDLE_FADE_OUT,
|
|
11901
12466
|
CURSOR_IDLE_HOLD,
|
|
11902
12467
|
DARK_GROUND,
|
|
12468
|
+
DECORATION_THICKNESS_EM,
|
|
11903
12469
|
DEFAULT_BROWSER_BAR,
|
|
11904
12470
|
DEFAULT_CAM_POSE,
|
|
11905
12471
|
DEFAULT_CAM_STYLE,
|
|
@@ -11920,8 +12486,6 @@ export {
|
|
|
11920
12486
|
DRAG_FIT_LEVEL,
|
|
11921
12487
|
DRAG_MIN_S,
|
|
11922
12488
|
DRAG_MIN_TRAVEL_FRAC,
|
|
11923
|
-
EM_CLOSE,
|
|
11924
|
-
EM_OPEN,
|
|
11925
12489
|
END_CARD_FROM,
|
|
11926
12490
|
END_CARD_RECEDE,
|
|
11927
12491
|
END_CARD_SECONDS,
|
|
@@ -11937,6 +12501,7 @@ export {
|
|
|
11937
12501
|
FREEZE_DEFAULT_SECONDS,
|
|
11938
12502
|
FREEZE_SECONDS_MAX,
|
|
11939
12503
|
FREEZE_SECONDS_MIN,
|
|
12504
|
+
HIGHLIGHT_HALF_EM,
|
|
11940
12505
|
HOLD_DRIFT_MAX,
|
|
11941
12506
|
HOUSE_GRADIENT,
|
|
11942
12507
|
HOUSE_REGISTER,
|
|
@@ -11954,6 +12519,7 @@ export {
|
|
|
11954
12519
|
MEDIA_FRAME_KEYS,
|
|
11955
12520
|
MEDIA_KEY_PREFIX,
|
|
11956
12521
|
MINIMAL_BAR_THEMES,
|
|
12522
|
+
MIXED,
|
|
11957
12523
|
MOTION_DELTA,
|
|
11958
12524
|
MOTION_EASE,
|
|
11959
12525
|
OBJECT_DEFAULT_SCALE,
|
|
@@ -11977,6 +12543,7 @@ export {
|
|
|
11977
12543
|
REJECT_OVERLAP,
|
|
11978
12544
|
REST_TILT_ID,
|
|
11979
12545
|
RETIRED_ZOOM_STYLES,
|
|
12546
|
+
RUN_STYLE_KEYS,
|
|
11980
12547
|
SCENE_MOTION,
|
|
11981
12548
|
SCENE_QUIET,
|
|
11982
12549
|
SEGMENT_ENTER_KINDS,
|
|
@@ -11986,6 +12553,7 @@ export {
|
|
|
11986
12553
|
SPEED_SPAN_MIN,
|
|
11987
12554
|
STAGE_CLIP_PREFIX,
|
|
11988
12555
|
STILL_DEFAULT,
|
|
12556
|
+
STRIKE_OFFSET_EM,
|
|
11989
12557
|
STUDIO_ENTRY_ID,
|
|
11990
12558
|
STYLE_FIELDS,
|
|
11991
12559
|
TEXT3D_DEPTH_DEFAULT,
|
|
@@ -12010,6 +12578,7 @@ export {
|
|
|
12010
12578
|
TRANSITION_SECONDS_MAX,
|
|
12011
12579
|
TRANSITION_SECONDS_MIN,
|
|
12012
12580
|
TRANSITION_SPEED_MULT,
|
|
12581
|
+
UNDERLINE_OFFSET_EM,
|
|
12013
12582
|
WINDOW_FOCUS_MIN,
|
|
12014
12583
|
ZOOM_CHAIN_GAP,
|
|
12015
12584
|
ZOOM_EASE,
|
|
@@ -12032,6 +12601,7 @@ export {
|
|
|
12032
12601
|
aspectRatioValue,
|
|
12033
12602
|
audioLane,
|
|
12034
12603
|
backdropMedia,
|
|
12604
|
+
boldWeightFor,
|
|
12035
12605
|
buildDigest,
|
|
12036
12606
|
calloutBox,
|
|
12037
12607
|
calloutClip,
|
|
@@ -12053,6 +12623,8 @@ export {
|
|
|
12053
12623
|
cardInset,
|
|
12054
12624
|
cardPointToScreen,
|
|
12055
12625
|
cardPoseTrack,
|
|
12626
|
+
caretAt,
|
|
12627
|
+
caretStops,
|
|
12056
12628
|
clampCamFrac,
|
|
12057
12629
|
clampCamSize,
|
|
12058
12630
|
clampFocus,
|
|
@@ -12060,9 +12632,11 @@ export {
|
|
|
12060
12632
|
clampSpeedRate,
|
|
12061
12633
|
clampTiltDeg,
|
|
12062
12634
|
clampZoomLevel,
|
|
12635
|
+
clearStyle,
|
|
12063
12636
|
clipEnvelope,
|
|
12064
12637
|
clipLength,
|
|
12065
12638
|
clipsFrom,
|
|
12639
|
+
commonStyle,
|
|
12066
12640
|
computeCardLayout,
|
|
12067
12641
|
computeMicRms,
|
|
12068
12642
|
computePeaks,
|
|
@@ -12076,10 +12650,12 @@ export {
|
|
|
12076
12650
|
cursorFromRecords,
|
|
12077
12651
|
cursorFromTrace,
|
|
12078
12652
|
cursorIdleFade,
|
|
12653
|
+
decorationRect,
|
|
12079
12654
|
deltaE,
|
|
12080
12655
|
deriveViewportCrop,
|
|
12081
12656
|
destinationById,
|
|
12082
12657
|
destinationsForChannel,
|
|
12658
|
+
diffInput,
|
|
12083
12659
|
docCardLayout,
|
|
12084
12660
|
docFreezes,
|
|
12085
12661
|
docMediaList,
|
|
@@ -12146,14 +12722,16 @@ export {
|
|
|
12146
12722
|
lastLayerEnd,
|
|
12147
12723
|
layerMedia,
|
|
12148
12724
|
layoutOf,
|
|
12725
|
+
layoutText,
|
|
12149
12726
|
levelForFocusFraction,
|
|
12727
|
+
lineOfOffset,
|
|
12150
12728
|
liveZoomStyleName,
|
|
12151
12729
|
lookFromBrand,
|
|
12152
12730
|
lookKindForGround,
|
|
12153
12731
|
lowerProgramDoc,
|
|
12154
12732
|
lowerStudioDoc,
|
|
12155
12733
|
lowerToComposition,
|
|
12156
|
-
|
|
12734
|
+
marksEmphasis,
|
|
12157
12735
|
mediaAtOutput,
|
|
12158
12736
|
mediaDuration,
|
|
12159
12737
|
mediaFrame,
|
|
@@ -12164,20 +12742,23 @@ export {
|
|
|
12164
12742
|
micLane,
|
|
12165
12743
|
migrateHostedDoc,
|
|
12166
12744
|
migrateMotion,
|
|
12745
|
+
migrateText,
|
|
12746
|
+
migrateTextClip,
|
|
12167
12747
|
migrateZoomStyle,
|
|
12168
12748
|
momentsFromDoc,
|
|
12169
12749
|
motionTrack,
|
|
12170
12750
|
musicBedClip,
|
|
12171
12751
|
nextMediaId,
|
|
12172
12752
|
normalizeCaptureSpace,
|
|
12753
|
+
normalizeRuns,
|
|
12173
12754
|
objectMotionBase,
|
|
12174
12755
|
objectMotionPoseAt,
|
|
12175
12756
|
objectsLane,
|
|
12757
|
+
offsetAtX,
|
|
12176
12758
|
outputDurationOf,
|
|
12177
12759
|
outputEnd,
|
|
12178
12760
|
outputRangeToSource,
|
|
12179
12761
|
overlapFraction,
|
|
12180
|
-
overlayDisplayText,
|
|
12181
12762
|
overlayFaceFor,
|
|
12182
12763
|
overlayFontFaces,
|
|
12183
12764
|
overlayFontString,
|
|
@@ -12187,12 +12768,12 @@ export {
|
|
|
12187
12768
|
overlayMotionBase,
|
|
12188
12769
|
overlayMotionPoseAt,
|
|
12189
12770
|
overlayPinPoseAt,
|
|
12771
|
+
overlayPlainText,
|
|
12190
12772
|
overlayRect,
|
|
12191
12773
|
overlaysLane,
|
|
12192
12774
|
ownFreezes,
|
|
12193
12775
|
pageDisplayUrl,
|
|
12194
12776
|
parseCssColour,
|
|
12195
|
-
parseEmphasis,
|
|
12196
12777
|
parsePoseId,
|
|
12197
12778
|
pickStyle,
|
|
12198
12779
|
pinBox,
|
|
@@ -12202,6 +12783,7 @@ export {
|
|
|
12202
12783
|
pinReferent,
|
|
12203
12784
|
pinReferentAt,
|
|
12204
12785
|
placeFreezes,
|
|
12786
|
+
plainText,
|
|
12205
12787
|
planAutoSpeed,
|
|
12206
12788
|
planAutoTilt,
|
|
12207
12789
|
planAutoZoom,
|
|
@@ -12213,6 +12795,7 @@ export {
|
|
|
12213
12795
|
programStillTime,
|
|
12214
12796
|
projectFromArtifact,
|
|
12215
12797
|
pumpFreeChainGap,
|
|
12798
|
+
rangeRects,
|
|
12216
12799
|
ratedSegments,
|
|
12217
12800
|
recommendedExportResolution,
|
|
12218
12801
|
refillAudioBeds,
|
|
@@ -12220,7 +12803,7 @@ export {
|
|
|
12220
12803
|
relativeLuminance,
|
|
12221
12804
|
removeSourceRange,
|
|
12222
12805
|
removeSpeedInRange,
|
|
12223
|
-
|
|
12806
|
+
replaceRange,
|
|
12224
12807
|
resolveExportSize,
|
|
12225
12808
|
resolveOverlayBox,
|
|
12226
12809
|
resolveOverlayStyle,
|
|
@@ -12230,6 +12813,7 @@ export {
|
|
|
12230
12813
|
restLeanOf,
|
|
12231
12814
|
restSpansThroughTransitions,
|
|
12232
12815
|
rgbToHex,
|
|
12816
|
+
runsOf,
|
|
12233
12817
|
sameMedia,
|
|
12234
12818
|
sceneChanges,
|
|
12235
12819
|
scrollRuns,
|
|
@@ -12237,16 +12821,24 @@ export {
|
|
|
12237
12821
|
outputLen as segmentOutputLen,
|
|
12238
12822
|
segmentStarts,
|
|
12239
12823
|
setSpeedInRange,
|
|
12824
|
+
setStyle,
|
|
12825
|
+
sliceRuns,
|
|
12240
12826
|
slideSide,
|
|
12241
12827
|
smoothCursor,
|
|
12828
|
+
snapOffset,
|
|
12829
|
+
snapRunWeight,
|
|
12242
12830
|
spanOutputExtent,
|
|
12243
12831
|
speedLane,
|
|
12244
|
-
stripEmphasis,
|
|
12245
12832
|
studioAudioPlan,
|
|
12246
12833
|
studioEntry,
|
|
12247
12834
|
studioLayerData,
|
|
12835
|
+
styleAt,
|
|
12836
|
+
styleOf,
|
|
12837
|
+
styledTextOf,
|
|
12248
12838
|
tiltLane,
|
|
12249
12839
|
tiltTrackFromDoc,
|
|
12840
|
+
toggleStyle,
|
|
12841
|
+
tokenRanges,
|
|
12250
12842
|
traceKindOf,
|
|
12251
12843
|
transitionMult,
|
|
12252
12844
|
transitionSeconds,
|