@vosjs/studio-core 0.37.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 +123 -43
- package/dist/index.js +2073 -1848
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1817,1977 +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
|
-
if (name in ZOOM_STYLES) return name;
|
|
1828
|
-
return RETIRED_ZOOM_STYLES[name]?.style ?? null;
|
|
1829
|
-
}
|
|
1830
|
-
var HOLD_DRIFT_MAX = 0.15;
|
|
1831
|
-
var PUMP_REST = 1;
|
|
1832
|
-
function pumpFreeChainGap(p) {
|
|
1833
|
-
return p.rampOut + (p.rampIn - p.rampInOverlap) + PUMP_REST;
|
|
1834
|
-
}
|
|
1835
|
-
var glide = {
|
|
1836
|
-
autoZoom: true,
|
|
1837
|
-
clusterGap: 3,
|
|
1838
|
-
minClusterClicks: 1,
|
|
1839
|
-
targetFill: 0.42,
|
|
1840
|
-
minLevel: 1.3,
|
|
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
|
|
1874
|
-
}
|
|
1875
|
-
};
|
|
1876
|
-
var ZOOM_STYLES = {
|
|
1877
|
-
glide,
|
|
1878
|
-
// Screen Studio "Focused" / Recordly — a block per click, framed and
|
|
1879
|
-
// released, no travel. The spring-settled family ease (near-linear onset,
|
|
1880
|
-
// long settle) at the default's tempo: 0.8 s in, 0.65 s out, and a beat
|
|
1881
|
-
// of 1.5 s after the click so the block is read before it goes. At its
|
|
1882
|
-
// 2026-07 timing (1.4 s in, 1.0 s out around a 1.35 s span) a lone click
|
|
1883
|
-
// spent 74% of its beat moving and every dwell or sped-up click was
|
|
1884
|
-
// shorter than the ramp, which the lowering compressed into a cut.
|
|
1885
|
-
focus: {
|
|
1886
|
-
autoZoom: true,
|
|
1887
|
-
clusterGap: 1.5,
|
|
1888
|
-
minClusterClicks: 1,
|
|
1889
|
-
targetFill: 0.5,
|
|
1890
|
-
minLevel: 1.4,
|
|
1891
|
-
maxLevel: 2,
|
|
1892
|
-
dragLevel: 1.6,
|
|
1893
|
-
lead: 0.35,
|
|
1894
|
-
hold: 1.5,
|
|
1895
|
-
followByDefault: false,
|
|
1896
|
-
typingZoom: true,
|
|
1897
|
-
typingGap: 2,
|
|
1898
|
-
typingHold: 1.3,
|
|
1899
|
-
typingMinLevel: 1.5,
|
|
1900
|
-
rampIn: 0.8,
|
|
1901
|
-
rampInOverlap: 0.25,
|
|
1902
|
-
rampOut: 0.65,
|
|
1903
|
-
chainGap: 2.4,
|
|
1904
|
-
pan: 0.7,
|
|
1905
|
-
ease: "css-bezier(0.28, 0.03, 0.09, 1)",
|
|
1906
|
-
panEase: "css-bezier(0.26, 0.08, 0.2, 1)",
|
|
1907
|
-
followSafeRatio: 0.5,
|
|
1908
|
-
followRecenter: 0.55,
|
|
1909
|
-
followLookahead: 0,
|
|
1910
|
-
tilt: {
|
|
1911
|
-
intensity: "off",
|
|
1912
|
-
rampIn: 0.8,
|
|
1913
|
-
rampOut: 0.65,
|
|
1914
|
-
chainGap: 2.4,
|
|
1915
|
-
pan: 0.7
|
|
1916
|
-
}
|
|
1917
|
-
},
|
|
1918
|
-
// Screen Studio "Smooth" — the long-HOLD style: 1.2 s in, 1.0 s out,
|
|
1919
|
-
// two-second holds, chains across 3.5 s so a slow clip travels rather
|
|
1920
|
-
// than pumps, and the default's level range, because a watched clip
|
|
1921
|
-
// wants the frame, not the magnifier. Its ramps still exceed a dwell or
|
|
1922
|
-
// a sped-up click; the lowering fits them to the room.
|
|
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
|
|
2027
|
-
}
|
|
2028
|
-
},
|
|
2029
|
-
// Auto-zoom off (every competitor ships this switch). Camera params still
|
|
2030
|
-
// apply to MANUAL spans — they move the way the default camera moves
|
|
2031
|
-
// TODAY (spelled as a spread, never a copy that fossilises).
|
|
2032
|
-
none: { ...glide, autoZoom: false }
|
|
2033
|
-
};
|
|
2034
|
-
var DEFAULT_ZOOM_STYLE = "glide";
|
|
2035
|
-
function resolveZoomStyle(name, overrides) {
|
|
2036
|
-
const live = liveZoomStyleName(name) ?? DEFAULT_ZOOM_STYLE;
|
|
2037
|
-
const params = { ...ZOOM_STYLES[live], ...overrides };
|
|
2038
|
-
const floor = pumpFreeChainGap(params);
|
|
2039
|
-
if (params.chainGap < floor) params.chainGap = floor;
|
|
2040
|
-
return params;
|
|
2041
|
-
}
|
|
2042
|
-
var ZOOM_STYLE_OPTIONS = [
|
|
2043
|
-
{
|
|
2044
|
-
name: "glide",
|
|
2045
|
-
label: "Glide",
|
|
2046
|
-
hint: "One steady zoom that travels between clicks"
|
|
2047
|
-
},
|
|
2048
|
-
{
|
|
2049
|
-
name: "focus",
|
|
2050
|
-
label: "Focus",
|
|
2051
|
-
hint: "A zoom per click, framed and released"
|
|
2052
|
-
},
|
|
2053
|
-
{
|
|
2054
|
-
name: "cinema",
|
|
2055
|
-
label: "Cinema",
|
|
2056
|
-
hint: "Slow moves and long holds, for content that is watched"
|
|
2057
|
-
},
|
|
2058
|
-
{ name: "snappy", label: "Snappy", hint: "Quick, energetic zoom cycles" },
|
|
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
|
-
}
|
|
2065
|
-
];
|
|
2066
|
-
|
|
2067
|
-
// src/docVersion.ts
|
|
2068
|
-
var DOC_SCHEMA_VERSION = 5;
|
|
2069
|
-
function migrateHostedDoc(raw) {
|
|
2070
|
-
const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
|
|
2071
|
-
if (version >= DOC_SCHEMA_VERSION) return raw;
|
|
2072
|
-
let doc = raw;
|
|
2073
|
-
if (version < 4 && doc.source && typeof doc.source === "object") {
|
|
2074
|
-
doc = migrateMotion(doc);
|
|
2075
|
-
}
|
|
2076
|
-
if (version < 5) doc = migrateZoomStyle(doc);
|
|
2077
|
-
return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
|
|
2078
|
-
}
|
|
2079
|
-
function migrateZoomStyle(doc) {
|
|
2080
|
-
const name = doc.zoomStyle;
|
|
2081
|
-
if (typeof name !== "string") return doc;
|
|
2082
|
-
const retired = RETIRED_ZOOM_STYLES[name];
|
|
2083
|
-
if (!retired) return doc;
|
|
2084
|
-
return {
|
|
2085
|
-
...doc,
|
|
2086
|
-
zoomStyle: retired.style,
|
|
2087
|
-
tiltStyle: doc.tiltStyle ?? retired.tilt
|
|
2088
|
-
};
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
// src/doc/studioDoc.ts
|
|
2092
|
-
var anchorKindOf = (doc) => "source" in doc ? "recording" : "program";
|
|
2093
|
-
var isRecordingDoc = (doc) => "source" in doc;
|
|
2094
|
-
var isProgramDoc = (doc) => !("source" in doc);
|
|
2095
|
-
function programDuration(doc) {
|
|
2096
|
-
const own = doc.program.duration;
|
|
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;
|
|
2100
|
-
}
|
|
2101
|
-
function anchorSourceDuration(doc) {
|
|
2102
|
-
return isRecordingDoc(doc) ? doc.source.meta.durationMs / 1e3 : programDuration(doc);
|
|
2103
|
-
}
|
|
1820
|
+
// src/overlayText.ts
|
|
1821
|
+
import {
|
|
1822
|
+
findFontFamily as findFontFamily2,
|
|
1823
|
+
fontFaceUrl as fontFaceUrl2,
|
|
1824
|
+
fontStack,
|
|
1825
|
+
nearestFontWeight as nearestFontWeight2
|
|
1826
|
+
} from "@vosjs/shared";
|
|
2104
1827
|
|
|
2105
|
-
// src/
|
|
2106
|
-
|
|
2107
|
-
var
|
|
2108
|
-
var
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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;
|
|
2121
1846
|
}
|
|
2122
|
-
|
|
1847
|
+
if (cur.trim()) out.push(cur);
|
|
1848
|
+
return out;
|
|
2123
1849
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
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 [];
|
|
2133
1871
|
const out = [];
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
const u = clamp2(
|
|
2145
|
-
(t - (ck.t - SNAP_BEFORE)) / SNAP_BEFORE,
|
|
2146
|
-
0,
|
|
2147
|
-
t <= ck.t + SNAP_AFTER ? 1 : 0
|
|
2148
|
-
);
|
|
2149
|
-
const w = u * u * (3 - 2 * u) * SNAP_GAIN;
|
|
2150
|
-
if (w > 0) {
|
|
2151
|
-
cx += (ck.x - cx) * w;
|
|
2152
|
-
cy += (ck.y - cy) * w;
|
|
2153
|
-
}
|
|
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);
|
|
2154
1882
|
}
|
|
2155
|
-
out.push({ t, x: cx, y: cy });
|
|
2156
1883
|
}
|
|
2157
1884
|
return out;
|
|
2158
1885
|
}
|
|
2159
|
-
function
|
|
2160
|
-
|
|
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);
|
|
1892
|
+
}
|
|
1893
|
+
if (/<(b|strong|h[1-6])[\s>]/i.test(html)) out.add(700);
|
|
1894
|
+
return [...out].sort((a, b) => a - b);
|
|
2161
1895
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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) {
|
|
2177
1935
|
const out = [];
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
if (!
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
if (t1 - t0 < DRAG_MIN_S) continue;
|
|
2184
|
-
let travel = 0;
|
|
2185
|
-
for (const e of track) {
|
|
2186
|
-
if (e.t < d.t) continue;
|
|
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;
|
|
1936
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1937
|
+
const add = (u) => {
|
|
1938
|
+
if (!seen.has(u)) {
|
|
1939
|
+
seen.add(u);
|
|
1940
|
+
out.push(u);
|
|
2191
1941
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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]);
|
|
2201
1952
|
return out;
|
|
2202
1953
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
}
|
|
2224
|
-
if (best) {
|
|
2225
|
-
absorbed.add(best);
|
|
2226
|
-
s.start = best.t;
|
|
2227
|
-
s.events = [best, ...s.events];
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
const targets = [];
|
|
2231
|
-
const surfaces = [];
|
|
2232
|
-
for (const c of clicks) {
|
|
2233
|
-
if (absorbed.has(c)) continue;
|
|
2234
|
-
if (isSurfacePress(c, width, height, targetFill)) surfaces.push(c);
|
|
2235
|
-
else targets.push(c);
|
|
2236
|
-
}
|
|
2237
|
-
const chain = (list, spatial) => {
|
|
2238
|
-
const clusters = [];
|
|
2239
|
-
for (const c of list) {
|
|
2240
|
-
const last = clusters.at(-1);
|
|
2241
|
-
const prev = last?.at(-1);
|
|
2242
|
-
const joins = last && prev && c.t - prev.t <= clusterGap && (!spatial || unionFitLevel(unionRect([...last, c]), width, height) >= minLevel);
|
|
2243
|
-
if (joins) last.push(c);
|
|
2244
|
-
else clusters.push([c]);
|
|
2245
|
-
}
|
|
2246
|
-
return clusters;
|
|
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
|
+
);
|
|
2247
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);
|
|
2248
1987
|
return {
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
1988
|
+
width: clip.box.width + bleed * 2,
|
|
1989
|
+
height: clip.box.height + bleed * 2,
|
|
1990
|
+
bleed
|
|
2252
1991
|
};
|
|
2253
1992
|
}
|
|
2254
|
-
function
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
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: " "
|
|
2004
|
+
};
|
|
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();
|
|
2013
|
+
}
|
|
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"
|
|
2031
|
+
];
|
|
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
|
+
}
|
|
2064
|
+
}
|
|
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;
|
|
2143
|
+
}
|
|
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
|
+
);
|
|
2260
2176
|
}
|
|
2261
|
-
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2177
|
+
return out;
|
|
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>`;
|
|
2264
2182
|
}
|
|
2265
|
-
function
|
|
2266
|
-
let
|
|
2267
|
-
let
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
for (const c of clicks) {
|
|
2271
|
-
const r = c.rect ?? { x: c.x, y: c.y, w: 0, h: 0 };
|
|
2272
|
-
x0 = Math.min(x0, r.x);
|
|
2273
|
-
y0 = Math.min(y0, r.y);
|
|
2274
|
-
x1 = Math.max(x1, r.x + r.w);
|
|
2275
|
-
y1 = Math.max(y1, r.y + r.h);
|
|
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;
|
|
2276
2188
|
}
|
|
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);
|
|
2189
|
+
return h.toString(36);
|
|
2283
2190
|
}
|
|
2284
|
-
function
|
|
2285
|
-
|
|
2286
|
-
|
|
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)}`;
|
|
2287
2196
|
}
|
|
2288
|
-
function
|
|
2289
|
-
const
|
|
2290
|
-
|
|
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 } : {} } : {}
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2291
2214
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
clusterGap = style.clusterGap,
|
|
2302
|
-
lead = style.lead,
|
|
2303
|
-
hold = style.hold,
|
|
2304
|
-
minClusterClicks = style.minClusterClicks,
|
|
2305
|
-
dragLevel = style.dragLevel,
|
|
2306
|
-
followByDefault = style.followByDefault,
|
|
2307
|
-
typingZoom = style.typingZoom,
|
|
2308
|
-
typingGap = style.typingGap,
|
|
2309
|
-
typingHold = style.typingHold,
|
|
2310
|
-
typingMinLevel = style.typingMinLevel,
|
|
2311
|
-
duration
|
|
2312
|
-
} = options;
|
|
2313
|
-
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);
|
|
2314
|
-
const grouped = groupTrack(track, {
|
|
2315
|
-
width,
|
|
2316
|
-
height,
|
|
2317
|
-
clusterGap,
|
|
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
|
-
});
|
|
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;
|
|
2363
2224
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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]);
|
|
2368
2233
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
if (
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
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 });
|
|
2260
|
+
}
|
|
2261
|
+
at += r.t.length;
|
|
2262
|
+
if (at >= b) break;
|
|
2263
|
+
}
|
|
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
|
+
}
|
|
2400
2321
|
} else {
|
|
2401
|
-
|
|
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
|
+
}
|
|
2402
2328
|
}
|
|
2329
|
+
line.units = units;
|
|
2403
2330
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
out: round2(s.out),
|
|
2414
|
-
level: clampZoomLevel(s.level),
|
|
2415
|
-
cx: round2(s.cx),
|
|
2416
|
-
cy: round2(s.cy),
|
|
2417
|
-
// Follow styles ride the cursor through the span (entry + dead-zone
|
|
2418
|
-
// recenters baked at lowering); typing/dwell spans stay fixed-focus.
|
|
2419
|
-
...followByDefault ? { focusMode: "auto" } : {},
|
|
2420
|
-
source: "auto"
|
|
2421
|
-
}));
|
|
2422
|
-
const kSpans = typing.map((s, i) => ({
|
|
2423
|
-
id: `k${i}`,
|
|
2424
|
-
in: round2(s.in),
|
|
2425
|
-
out: round2(s.out),
|
|
2426
|
-
level: clampZoomLevel(s.level),
|
|
2427
|
-
cx: round2(s.cx),
|
|
2428
|
-
cy: round2(s.cy),
|
|
2429
|
-
// Never focusMode:'auto': the dot is parked (and fading) while typing —
|
|
2430
|
-
// the FIELD is the anchor, and a follow would be a no-op at best.
|
|
2431
|
-
source: "auto"
|
|
2432
|
-
}));
|
|
2433
|
-
const spans = within([...zSpans, ...kSpans, ...dragSpans]).sort(
|
|
2434
|
-
(a, b) => a.in - b.in
|
|
2435
|
-
);
|
|
2436
|
-
const dwells = dwellSpans(track, width, height, maxLevel, [
|
|
2437
|
-
...spans,
|
|
2438
|
-
...dragReserved
|
|
2439
|
-
]);
|
|
2440
|
-
return within([...spans, ...dwells]).sort((a, b) => a.in - b.in);
|
|
2441
|
-
}
|
|
2442
|
-
function typingSessions(track, width, height, typingGap) {
|
|
2443
|
-
const pings = track.filter((e) => e.type === "key").map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
|
|
2444
|
-
const all = [];
|
|
2445
|
-
let cur = null;
|
|
2446
|
-
for (const p of pings) {
|
|
2447
|
-
const [nx, ny] = fieldCenter(p, width, height);
|
|
2448
|
-
if (cur && p.t - cur.last <= typingGap && Math.hypot(nx - cur.cx, ny - cur.cy) <= TYPING_REFOCUS_FRAC) {
|
|
2449
|
-
cur.events.push(p);
|
|
2450
|
-
cur.last = p.t;
|
|
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);
|
|
2451
2340
|
} else {
|
|
2452
|
-
|
|
2453
|
-
|
|
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);
|
|
2454
2354
|
}
|
|
2355
|
+
base += len + 1;
|
|
2455
2356
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
);
|
|
2459
|
-
}
|
|
2460
|
-
function fieldCenter(c, width, height) {
|
|
2461
|
-
const px = c.rect ? c.rect.x + c.rect.w / 2 : c.x;
|
|
2462
|
-
const py = c.rect ? c.rect.y + c.rect.h / 2 : c.y;
|
|
2463
|
-
return [clamp012(px / width), clamp012(py / height)];
|
|
2357
|
+
if (!lines.length) lines.push({ w: 0, frags: [], a: 0, n: 0 });
|
|
2358
|
+
return { lines, width };
|
|
2464
2359
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
const opening = start === 0;
|
|
2477
|
-
if (!opening && dur >= DWELL_MIN && dur <= DWELL_MAX) {
|
|
2478
|
-
const run = moves.slice(start, i);
|
|
2479
|
-
candidates.push({
|
|
2480
|
-
center: (moves[start].t + endT) / 2,
|
|
2481
|
-
cx: run.reduce((s, p) => s + p.nx, 0) / run.length,
|
|
2482
|
-
cy: run.reduce((s, p) => s + p.ny, 0) / run.length,
|
|
2483
|
-
strength: dur
|
|
2484
|
-
});
|
|
2485
|
-
}
|
|
2486
|
-
start = i;
|
|
2487
|
-
}
|
|
2488
|
-
const sorted = [...candidates].sort((a, b) => b.strength - a.strength);
|
|
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);
|
|
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 };
|
|
2516
2371
|
}
|
|
2517
|
-
|
|
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 };
|
|
2518
2377
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
const n = cluster.length;
|
|
2536
|
-
const cx = clamp012(sx / n / width);
|
|
2537
|
-
const cy = clamp012(sy / n / height);
|
|
2538
|
-
const own = fitLevel({ x: 0, y: 0, w: maxW, h: maxH }, width, height, targetFill) ?? maxLevel;
|
|
2539
|
-
const together = unionFitLevel(unionRect(cluster), width, height);
|
|
2540
|
-
return { cx, cy, level: clamp3(Math.min(own, together), minLevel, maxLevel) };
|
|
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;
|
|
2393
|
+
return out;
|
|
2541
2394
|
}
|
|
2542
|
-
function
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2395
|
+
function runsOf(text) {
|
|
2396
|
+
return typeof text === "string" ? text ? [{ text }] : [] : text;
|
|
2397
|
+
}
|
|
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;
|
|
2412
|
+
const out = [];
|
|
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;
|
|
2422
|
+
return out;
|
|
2423
|
+
}
|
|
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;
|
|
2563
2432
|
}
|
|
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 };
|
|
2433
|
+
return plain.length;
|
|
2572
2434
|
}
|
|
2573
|
-
function
|
|
2574
|
-
|
|
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;
|
|
2444
|
+
}
|
|
2445
|
+
return out;
|
|
2575
2446
|
}
|
|
2576
|
-
function
|
|
2577
|
-
|
|
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
|
+
]);
|
|
2578
2470
|
}
|
|
2579
|
-
function
|
|
2580
|
-
|
|
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] });
|
|
2486
|
+
}
|
|
2487
|
+
return next;
|
|
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
|
+
]);
|
|
2494
|
+
}
|
|
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);
|
|
2499
|
+
}
|
|
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;
|
|
2510
|
+
}
|
|
2511
|
+
return out;
|
|
2512
|
+
}
|
|
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
|
+
};
|
|
2581
2531
|
}
|
|
2582
2532
|
|
|
2583
|
-
// src/
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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
|
+
};
|
|
2547
|
+
}
|
|
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
|
|
2574
|
+
}
|
|
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)
|
|
2585
|
+
}
|
|
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}`;
|
|
2596
|
+
}
|
|
2597
|
+
if (clip.weight !== void 0 || clip.family) {
|
|
2598
|
+
const wanted = clip.weight ?? base.weight;
|
|
2599
|
+
weight = familyEntry ? nearestFontWeight2(familyEntry, wanted) : wanted;
|
|
2641
2600
|
}
|
|
2642
|
-
const sc = Math.min(availW / vw, availH / vh);
|
|
2643
|
-
const dw = vw * sc;
|
|
2644
|
-
const dh = vh * sc;
|
|
2645
|
-
const dx = padL + (availW - dw) / 2;
|
|
2646
|
-
const dy = padT + barH + (availH - dh) / 2;
|
|
2647
2601
|
return {
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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
|
|
2658
2618
|
};
|
|
2659
2619
|
}
|
|
2660
|
-
function
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
const W = Math.max(
|
|
2664
|
-
2,
|
|
2665
|
-
Math.round(H * aspectRatioValue(doc.frame.aspectRatio, meta))
|
|
2620
|
+
function overlayFaceFor(clip) {
|
|
2621
|
+
const entry = findFontFamily2(
|
|
2622
|
+
clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
|
|
2666
2623
|
);
|
|
2667
|
-
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
{
|
|
2672
|
-
width: sm.captureWidth ?? sm.width,
|
|
2673
|
-
height: sm.captureHeight ?? sm.height
|
|
2674
|
-
},
|
|
2675
|
-
W,
|
|
2676
|
-
H
|
|
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
|
|
2677
2628
|
);
|
|
2629
|
+
if (inBase) return null;
|
|
2630
|
+
return {
|
|
2631
|
+
family: entry.family,
|
|
2632
|
+
weight,
|
|
2633
|
+
url: fontFaceUrl2(entry.slug, weight)
|
|
2634
|
+
};
|
|
2678
2635
|
}
|
|
2679
|
-
function
|
|
2680
|
-
const
|
|
2681
|
-
const
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
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 });
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
return faces;
|
|
2654
|
+
}
|
|
2655
|
+
function overlayLines(text) {
|
|
2656
|
+
const lines = text.split("\n");
|
|
2657
|
+
return lines.length ? lines : [""];
|
|
2658
|
+
}
|
|
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);
|
|
2688
2683
|
}
|
|
2689
|
-
return
|
|
2684
|
+
return (h >>> 0).toString(36);
|
|
2690
2685
|
}
|
|
2691
|
-
function
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2694
|
-
const
|
|
2695
|
-
const
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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;
|
|
2699
|
+
}
|
|
2700
|
+
return at;
|
|
2702
2701
|
};
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
if (level <= 1.001) {
|
|
2709
|
-
return { minX: 0.5, maxX: 0.5, minY: 0.5, maxY: 0.5 };
|
|
2710
|
-
}
|
|
2711
|
-
if (camera === "stage") {
|
|
2712
|
-
const x2 = stageAxisBounds(
|
|
2713
|
-
layout.dx,
|
|
2714
|
-
layout.dw,
|
|
2715
|
-
layout.cardX,
|
|
2716
|
-
layout.cardW,
|
|
2717
|
-
layout.W,
|
|
2718
|
-
level
|
|
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
|
|
2719
2707
|
);
|
|
2720
|
-
const
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
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
|
+
});
|
|
2719
|
+
}
|
|
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
2727
|
);
|
|
2728
|
-
|
|
2728
|
+
if (!inBase) {
|
|
2729
|
+
faces.push({
|
|
2730
|
+
f: entry.family,
|
|
2731
|
+
w: font.w,
|
|
2732
|
+
u: fontFaceUrl2(entry.slug, font.w)
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2729
2735
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
level
|
|
2737
|
-
);
|
|
2738
|
-
const y = axisBounds(
|
|
2739
|
-
layout.dy,
|
|
2740
|
-
layout.dh,
|
|
2741
|
-
layout.cardY,
|
|
2742
|
-
layout.cardH,
|
|
2743
|
-
layout.H,
|
|
2744
|
-
level
|
|
2745
|
-
);
|
|
2746
|
-
return { minX: x.min, maxX: x.max, minY: y.min, maxY: y.max };
|
|
2736
|
+
return {
|
|
2737
|
+
l,
|
|
2738
|
+
fs,
|
|
2739
|
+
...faces.length ? { faces } : {},
|
|
2740
|
+
h: hashOf(JSON.stringify([l, fs]))
|
|
2741
|
+
};
|
|
2747
2742
|
}
|
|
2748
|
-
function
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
if (
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
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
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (current) out.push(current);
|
|
2756
2768
|
}
|
|
2757
|
-
return
|
|
2769
|
+
return out.length ? out : [""];
|
|
2758
2770
|
}
|
|
2759
|
-
function
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
hi = mid;
|
|
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);
|
|
2767
2778
|
}
|
|
2768
|
-
return
|
|
2779
|
+
return [...line];
|
|
2769
2780
|
}
|
|
2770
|
-
function
|
|
2771
|
-
const
|
|
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);
|
|
2787
|
+
}
|
|
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;
|
|
2772
2809
|
return {
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
|
2775
2818
|
};
|
|
2776
2819
|
}
|
|
2777
|
-
function
|
|
2778
|
-
|
|
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}`;
|
|
2779
2823
|
}
|
|
2780
|
-
function
|
|
2781
|
-
|
|
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
|
+
)
|
|
2853
|
+
);
|
|
2854
|
+
const laid = layoutText(
|
|
2855
|
+
{ lines: styled.l, maxWidth: clip.maxWidth ? clip.maxWidth * frameW : 0 },
|
|
2856
|
+
(t, f) => measure(t, fonts[f], ls)
|
|
2857
|
+
);
|
|
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;
|
|
2869
|
+
}
|
|
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;
|
|
2874
|
+
return {
|
|
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
|
|
2879
|
+
};
|
|
2782
2880
|
}
|
|
2783
|
-
function
|
|
2784
|
-
|
|
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;
|
|
2785
2892
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
function
|
|
2789
|
-
const
|
|
2790
|
-
|
|
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;
|
|
2923
|
+
}
|
|
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;
|
|
2791
2929
|
}
|
|
2792
|
-
function
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const
|
|
2797
|
-
|
|
2798
|
-
const
|
|
2799
|
-
const
|
|
2800
|
-
|
|
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) };
|
|
2801
2954
|
}
|
|
2802
|
-
function
|
|
2803
|
-
const
|
|
2804
|
-
const
|
|
2805
|
-
|
|
2806
|
-
const my = v.wcy + (layout.H / 2 - v.oy) / v.level;
|
|
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;
|
|
2807
2959
|
return {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
w: size,
|
|
2811
|
-
h: size
|
|
2960
|
+
...doc,
|
|
2961
|
+
overlays: overlays.map((o) => o.kind === "text" ? migrateTextClip(o) : o)
|
|
2812
2962
|
};
|
|
2813
2963
|
}
|
|
2814
|
-
function focusForViewportCentre(cX, cY, level, layout, camera = "card", centring, screen) {
|
|
2815
|
-
const L = Math.max(1, level);
|
|
2816
|
-
const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
|
|
2817
|
-
const a = 1 - (1 - t) / L;
|
|
2818
|
-
if (a <= 1e-6) return { cx: 0.5, cy: 0.5 };
|
|
2819
|
-
const { ox, oy } = screenPoint(layout, camera, screen);
|
|
2820
|
-
const fx = (cX * layout.W - ox * (1 - t) / L - (layout.W / 2 - ox) / L) / a;
|
|
2821
|
-
const fy = (cY * layout.H - oy * (1 - t) / L - (layout.H / 2 - oy) / L) / a;
|
|
2822
|
-
return { cx: (fx - layout.dx) / layout.dw, cy: (fy - layout.dy) / layout.dh };
|
|
2823
|
-
}
|
|
2824
|
-
function levelForFocusFraction(frac) {
|
|
2825
|
-
if (!(frac > 0)) return clampZoomLevel(Number.POSITIVE_INFINITY);
|
|
2826
|
-
return clampZoomLevel(Math.max(1.1, 1 / frac));
|
|
2827
|
-
}
|
|
2828
2964
|
|
|
2829
|
-
// src/
|
|
2830
|
-
var
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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 };
|
|
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;
|
|
2841
2974
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
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;
|
|
2844
2979
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
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
|
|
2872
3019
|
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
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
|
|
2908
3135
|
}
|
|
2909
|
-
}
|
|
2910
|
-
|
|
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;
|
|
2911
3186
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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"
|
|
2918
3209
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
const
|
|
2925
|
-
const
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
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
|
-
});
|
|
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);
|
|
2942
3221
|
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
family: f.family,
|
|
2946
|
-
url: f.url,
|
|
2947
|
-
weight: f.weight ?? 400,
|
|
2948
|
-
style: f.style ?? "normal"
|
|
2949
|
-
});
|
|
2950
|
-
return faces;
|
|
2951
|
-
}
|
|
2952
|
-
function htmlLayerUnhostedFamilies(clip) {
|
|
2953
|
-
const explicit = new Set(
|
|
2954
|
-
(clip.fonts ?? []).map((f) => f.family.toLowerCase())
|
|
2955
|
-
);
|
|
2956
|
-
return cssFamilies(clip.css).filter(
|
|
2957
|
-
(f) => !findFontFamily(f) && !explicit.has(f.toLowerCase())
|
|
2958
|
-
);
|
|
2959
|
-
}
|
|
2960
|
-
function htmlLayerAssetUrls(clip) {
|
|
2961
|
-
const out = [];
|
|
2962
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2963
|
-
const add = (u) => {
|
|
2964
|
-
if (!seen.has(u)) {
|
|
2965
|
-
seen.add(u);
|
|
2966
|
-
out.push(u);
|
|
2967
|
-
}
|
|
2968
|
-
};
|
|
2969
|
-
for (const m of clip.html.matchAll(
|
|
2970
|
-
/\bsrc\s*=\s*["']((?:https?:)?\/\/[^"']+)["']/gi
|
|
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)));
|
|
3222
|
+
if (version < 5) doc = migrateZoomStyle(doc);
|
|
3223
|
+
return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
|
|
3010
3224
|
}
|
|
3011
|
-
function
|
|
3012
|
-
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;
|
|
3013
3230
|
return {
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3231
|
+
...doc,
|
|
3232
|
+
zoomStyle: retired.style,
|
|
3233
|
+
tiltStyle: doc.tiltStyle ?? retired.tilt
|
|
3017
3234
|
};
|
|
3018
3235
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
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;
|
|
3022
3246
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
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();
|
|
3247
|
+
function anchorSourceDuration(doc) {
|
|
3248
|
+
return isRecordingDoc(doc) ? doc.source.meta.durationMs / 1e3 : programDuration(doc);
|
|
3039
3249
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
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 };
|
|
3043
3269
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
"
|
|
3048
|
-
|
|
3049
|
-
"
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
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
|
-
);
|
|
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;
|
|
3088
3299
|
}
|
|
3089
3300
|
}
|
|
3301
|
+
out.push({ t, x: cx, y: cy });
|
|
3090
3302
|
}
|
|
3091
|
-
|
|
3092
|
-
if (bare)
|
|
3093
|
-
push2(
|
|
3094
|
-
"bare-ampersand",
|
|
3095
|
-
"problem",
|
|
3096
|
-
`A bare "&" is a parse error; write &. Found ${bare.length}.`
|
|
3097
|
-
);
|
|
3098
|
-
if (/<[a-zA-Z][^>]*?\s[a-zA-Z-]+=(?!["'])[^\s>]+/.test(html))
|
|
3099
|
-
push2(
|
|
3100
|
-
"unquoted-attr",
|
|
3101
|
-
"problem",
|
|
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.`
|
|
3167
|
-
);
|
|
3168
|
-
return problems;
|
|
3303
|
+
return out;
|
|
3169
3304
|
}
|
|
3170
|
-
function
|
|
3171
|
-
|
|
3172
|
-
const errors = doc.getElementsByTagName("parsererror");
|
|
3173
|
-
if (!errors.length) return { ok: true };
|
|
3174
|
-
const text = (errors[0].textContent ?? "").replace(/\s+/g, " ").trim();
|
|
3175
|
-
return { ok: false, error: text || "The markup is not well-formed XML." };
|
|
3305
|
+
function clamp2(v, lo, hi) {
|
|
3306
|
+
return Math.max(lo, Math.min(hi, v));
|
|
3176
3307
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
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
|
+
}
|
|
3181
3347
|
return out;
|
|
3182
3348
|
}
|
|
3183
|
-
var
|
|
3184
|
-
var
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
const
|
|
3189
|
-
const
|
|
3190
|
-
const
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
);
|
|
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]);
|
|
3202
3391
|
}
|
|
3203
|
-
return
|
|
3392
|
+
return clusters;
|
|
3204
3393
|
};
|
|
3205
|
-
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}` : "";
|
|
3206
|
-
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}`;
|
|
3207
|
-
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>`;
|
|
3208
|
-
}
|
|
3209
|
-
function fnv1a(s) {
|
|
3210
|
-
let h = 2166136261;
|
|
3211
|
-
for (let i = 0; i < s.length; i++) {
|
|
3212
|
-
h ^= s.charCodeAt(i);
|
|
3213
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
3214
|
-
}
|
|
3215
|
-
return h.toString(36);
|
|
3216
|
-
}
|
|
3217
|
-
function htmlLayerKey(clip, faces, bleed, assets = []) {
|
|
3218
|
-
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
|
|
3219
|
-
// moment to this key per frame. The data rides here, the moment there.
|
|
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;
|
|
3227
3394
|
return {
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
markup: clip.html,
|
|
3232
|
-
css: clip.css ?? "",
|
|
3233
|
-
box: { width: clip.box.width, height: clip.box.height },
|
|
3234
|
-
bleed,
|
|
3235
|
-
faces,
|
|
3236
|
-
assets,
|
|
3237
|
-
...clip.live ? { live: true, ...clip.data ? { data: clip.data } : {} } : {}
|
|
3238
|
-
}
|
|
3395
|
+
sessions,
|
|
3396
|
+
clusters: chain(targets, true),
|
|
3397
|
+
surfaces: chain(surfaces, false)
|
|
3239
3398
|
};
|
|
3240
3399
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
return out;
|
|
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);
|
|
3250
3408
|
}
|
|
3251
|
-
return
|
|
3409
|
+
return kept;
|
|
3252
3410
|
}
|
|
3253
|
-
function
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
let
|
|
3257
|
-
|
|
3258
|
-
|
|
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);
|
|
3259
3422
|
}
|
|
3260
|
-
return
|
|
3423
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
3261
3424
|
}
|
|
3262
|
-
function
|
|
3263
|
-
const
|
|
3264
|
-
const
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
};
|
|
3297
|
-
const
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
};
|
|
3322
|
-
const
|
|
3323
|
-
const
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
const
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
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);
|
|
3429
|
+
}
|
|
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);
|
|
3433
|
+
}
|
|
3434
|
+
function isSurfacePress(c, width, height, targetFill) {
|
|
3435
|
+
const fit = fitLevel(c.rect, width, height, targetFill);
|
|
3436
|
+
return fit !== null && fit < DRAG_FIT_LEVEL;
|
|
3437
|
+
}
|
|
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;
|
|
3347
3546
|
} else {
|
|
3348
|
-
|
|
3349
|
-
const gs = graphemesOf(text);
|
|
3350
|
-
for (let i = 0; i < gs.length; i++) {
|
|
3351
|
-
units.push(unitOf(frags, at, at + gs[i].length));
|
|
3352
|
-
at += gs[i].length;
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
line.units = units;
|
|
3356
|
-
}
|
|
3357
|
-
if (x > width) width = x;
|
|
3358
|
-
lines.push(line);
|
|
3359
|
-
};
|
|
3360
|
-
for (let li = 0; li < input.lines.length; li++) {
|
|
3361
|
-
const runs = input.lines[li];
|
|
3362
|
-
let len = 0;
|
|
3363
|
-
for (let i = 0; i < runs.length; i++) len += runs[i].t.length;
|
|
3364
|
-
if (!max || !len || widthOf(slice(runs, 0, len)) <= max) {
|
|
3365
|
-
push2(runs, 0, len);
|
|
3366
|
-
} else {
|
|
3367
|
-
const text = runs.map((r) => r.t).join("");
|
|
3368
|
-
const toks = tokenRanges(text);
|
|
3369
|
-
let a = 0;
|
|
3370
|
-
let b = toks[0][1];
|
|
3371
|
-
for (let i = 1; i < toks.length; i++) {
|
|
3372
|
-
if (widthOf(slice(runs, a, toks[i][1])) <= max) b = toks[i][1];
|
|
3373
|
-
else {
|
|
3374
|
-
push2(runs, a, b);
|
|
3375
|
-
a = toks[i][0];
|
|
3376
|
-
b = toks[i][1];
|
|
3377
|
-
}
|
|
3547
|
+
t.in = z.out;
|
|
3378
3548
|
}
|
|
3379
|
-
push2(runs, a, b);
|
|
3380
3549
|
}
|
|
3381
|
-
base += len + 1;
|
|
3382
3550
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
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;
|
|
3554
|
+
}
|
|
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
|
|
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);
|
|
3385
3587
|
}
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
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);
|
|
3600
|
+
}
|
|
3397
3601
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
y + (kind === "u" ? UNDERLINE_OFFSET_EM : STRIKE_OFFSET_EM) * px
|
|
3602
|
+
return all.filter(
|
|
3603
|
+
(s) => s.events.length >= TYPING_MIN_PINGS && s.last - s.first >= TYPING_MIN_DUR
|
|
3401
3604
|
);
|
|
3402
|
-
return { x: x0, y: top, w: x1 - x0, h: t };
|
|
3403
3605
|
}
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
var OVERLAY_BOX_RADIUS = 0.25;
|
|
3409
|
-
function resolveOverlayBox(clip) {
|
|
3410
|
-
if (!clip.box) return null;
|
|
3411
|
-
const size = resolveOverlayStyle(clip).size;
|
|
3412
|
-
return {
|
|
3413
|
-
color: clip.box.color,
|
|
3414
|
-
opacity: clip.box.opacity ?? 1,
|
|
3415
|
-
padX: (clip.box.paddingX ?? OVERLAY_BOX_PAD_X) * size,
|
|
3416
|
-
padY: (clip.box.paddingY ?? OVERLAY_BOX_PAD_Y) * size,
|
|
3417
|
-
radius: (clip.box.radius ?? OVERLAY_BOX_RADIUS) * size
|
|
3418
|
-
};
|
|
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)];
|
|
3419
3610
|
}
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
weight: 400,
|
|
3443
|
-
size: 22,
|
|
3444
|
-
color: "#e4e4e7",
|
|
3445
|
-
shadow: 0.35
|
|
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
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
start = i;
|
|
3446
3633
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
{
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
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)
|
|
3645
|
+
);
|
|
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}` }));
|
|
3664
|
+
}
|
|
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
|
+
}
|
|
3457
3680
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
const
|
|
3461
|
-
const
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
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) };
|
|
3687
|
+
}
|
|
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;
|
|
3708
|
+
}
|
|
3468
3709
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
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 };
|
|
3718
|
+
}
|
|
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
|
+
};
|
|
3472
3787
|
}
|
|
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;
|
|
3473
3793
|
return {
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
...clip.shadow !== void 0 ? { shadow: Math.min(1, Math.max(0, clip.shadow)) } : {},
|
|
3485
|
-
fontStyle: clip.italic ? "italic" : "normal",
|
|
3486
|
-
align: clip.align ?? "center",
|
|
3487
|
-
letterSpacing: clip.letterSpacing ?? 0,
|
|
3488
|
-
lineHeight: clip.lineHeight ?? OVERLAY_LINE_HEIGHT,
|
|
3489
|
-
stroke: clip.stroke ?? null
|
|
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
|
|
3490
3804
|
};
|
|
3491
3805
|
}
|
|
3492
|
-
function
|
|
3493
|
-
const
|
|
3494
|
-
|
|
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))
|
|
3495
3812
|
);
|
|
3496
|
-
|
|
3497
|
-
const
|
|
3498
|
-
|
|
3499
|
-
(
|
|
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
|
|
3500
3823
|
);
|
|
3501
|
-
if (inBase) return null;
|
|
3502
|
-
return {
|
|
3503
|
-
family: entry.family,
|
|
3504
|
-
weight,
|
|
3505
|
-
url: fontFaceUrl2(entry.slug, weight)
|
|
3506
|
-
};
|
|
3507
3824
|
}
|
|
3508
|
-
function
|
|
3509
|
-
const
|
|
3510
|
-
const
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
};
|
|
3518
|
-
for (const o of doc.overlays ?? []) {
|
|
3519
|
-
if (o.kind !== "text") continue;
|
|
3520
|
-
add(overlayFaceFor(o));
|
|
3521
|
-
add(resolveEmphasis(o)?.face);
|
|
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;
|
|
3522
3834
|
}
|
|
3523
|
-
return
|
|
3524
|
-
}
|
|
3525
|
-
function overlayLines(text) {
|
|
3526
|
-
const lines = text.split("\n");
|
|
3527
|
-
return lines.length ? lines : [""];
|
|
3528
|
-
}
|
|
3529
|
-
var EM_OPEN = "";
|
|
3530
|
-
var EM_CLOSE = "";
|
|
3531
|
-
function parseEmphasis(text) {
|
|
3532
|
-
let any = false;
|
|
3533
|
-
const lines = text.split("\n").map((line) => {
|
|
3534
|
-
const chars = [];
|
|
3535
|
-
const bold = [];
|
|
3536
|
-
const marker = [];
|
|
3537
|
-
for (let i = 0; i < line.length; i++) {
|
|
3538
|
-
if (line[i] === "\\" && line[i + 1] === "*") {
|
|
3539
|
-
chars.push("*");
|
|
3540
|
-
marker.push(false);
|
|
3541
|
-
i++;
|
|
3542
|
-
} else {
|
|
3543
|
-
chars.push(line[i]);
|
|
3544
|
-
marker.push(line[i] === "*");
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
const keep = chars.map(() => true);
|
|
3548
|
-
for (let i = 0; i < chars.length; i++) {
|
|
3549
|
-
if (!marker[i]) continue;
|
|
3550
|
-
let j = i + 1;
|
|
3551
|
-
while (j < chars.length && !marker[j]) j++;
|
|
3552
|
-
if (j >= chars.length) break;
|
|
3553
|
-
const inner = chars.slice(i + 1, j).join("");
|
|
3554
|
-
if (!inner.trim()) {
|
|
3555
|
-
i = j - 1;
|
|
3556
|
-
continue;
|
|
3557
|
-
}
|
|
3558
|
-
keep[i] = false;
|
|
3559
|
-
keep[j] = false;
|
|
3560
|
-
for (let k = i + 1; k < j; k++) bold[k] = true;
|
|
3561
|
-
any = true;
|
|
3562
|
-
i = j;
|
|
3563
|
-
}
|
|
3564
|
-
const kept = [];
|
|
3565
|
-
for (let i = 0; i < chars.length; i++) {
|
|
3566
|
-
if (keep[i])
|
|
3567
|
-
kept.push({ c: chars[i], b: !!bold[i] && !/\s/.test(chars[i]) });
|
|
3568
|
-
}
|
|
3569
|
-
let out = "";
|
|
3570
|
-
for (let i = 0; i < kept.length; i++) {
|
|
3571
|
-
const { c, b } = kept[i];
|
|
3572
|
-
if (b && !(i > 0 && kept[i - 1].b)) out += EM_OPEN;
|
|
3573
|
-
out += c;
|
|
3574
|
-
if (b && !(i + 1 < kept.length && kept[i + 1].b)) out += EM_CLOSE;
|
|
3575
|
-
}
|
|
3576
|
-
return out;
|
|
3577
|
-
});
|
|
3578
|
-
return any ? lines.join("\n") : null;
|
|
3579
|
-
}
|
|
3580
|
-
function overlayDisplayText(clip) {
|
|
3581
|
-
if (!clip.emphasis) return clip.text;
|
|
3582
|
-
return parseEmphasis(clip.text) ?? clip.text;
|
|
3835
|
+
return best;
|
|
3583
3836
|
}
|
|
3584
|
-
function
|
|
3585
|
-
|
|
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;
|
|
3843
|
+
return {
|
|
3844
|
+
x,
|
|
3845
|
+
y,
|
|
3846
|
+
size,
|
|
3847
|
+
radius: cam.shape === "rounded" ? (cam.radius ?? 18) * s : size / 2
|
|
3848
|
+
};
|
|
3586
3849
|
}
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
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 };
|
|
3853
|
+
}
|
|
3854
|
+
if (level <= 1.001) {
|
|
3855
|
+
return { minX: 0.5, maxX: 0.5, minY: 0.5, maxY: 0.5 };
|
|
3856
|
+
}
|
|
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 };
|
|
3875
|
+
}
|
|
3876
|
+
const x = axisBounds(
|
|
3877
|
+
layout.dx,
|
|
3878
|
+
layout.dw,
|
|
3879
|
+
layout.cardX,
|
|
3880
|
+
layout.cardW,
|
|
3881
|
+
layout.W,
|
|
3882
|
+
level
|
|
3593
3883
|
);
|
|
3594
|
-
const
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3884
|
+
const y = axisBounds(
|
|
3885
|
+
layout.dy,
|
|
3886
|
+
layout.dh,
|
|
3887
|
+
layout.cardY,
|
|
3888
|
+
layout.cardH,
|
|
3889
|
+
layout.H,
|
|
3890
|
+
level
|
|
3598
3891
|
);
|
|
3599
|
-
return {
|
|
3600
|
-
weight,
|
|
3601
|
-
...clip.emphasis?.color ? { color: clip.emphasis.color } : {},
|
|
3602
|
-
...entry && !inBase ? {
|
|
3603
|
-
face: {
|
|
3604
|
-
family: entry.family,
|
|
3605
|
-
weight,
|
|
3606
|
-
url: fontFaceUrl2(entry.slug, weight)
|
|
3607
|
-
}
|
|
3608
|
-
} : {}
|
|
3609
|
-
};
|
|
3892
|
+
return { minX: x.min, maxX: x.max, minY: y.min, maxY: y.max };
|
|
3610
3893
|
}
|
|
3611
|
-
function
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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;
|
|
3616
3902
|
}
|
|
3617
|
-
return (
|
|
3903
|
+
return { min: clamp013(lo), max: clamp013(hi) };
|
|
3618
3904
|
}
|
|
3619
|
-
function
|
|
3620
|
-
const
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
};
|
|
3633
|
-
for (const c of line) {
|
|
3634
|
-
if (c === EM_OPEN || c === EM_CLOSE) {
|
|
3635
|
-
flush();
|
|
3636
|
-
bold = c === EM_OPEN;
|
|
3637
|
-
} else run += c;
|
|
3638
|
-
}
|
|
3639
|
-
flush();
|
|
3640
|
-
return runs;
|
|
3641
|
-
});
|
|
3642
|
-
const fs = [{ w: resolveOverlayStyle(clip).weight }, { w: em.weight }];
|
|
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) };
|
|
3915
|
+
}
|
|
3916
|
+
function clampFocus(cx, cy, level, layout, camera = "card", screen) {
|
|
3917
|
+
const b = focusBounds(level, layout, camera, screen);
|
|
3643
3918
|
return {
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
...em.face ? { faces: [{ f: em.face.family, w: em.face.weight, u: em.face.url }] } : {},
|
|
3647
|
-
h: hashOf(JSON.stringify([l, fs]))
|
|
3919
|
+
cx: Math.min(b.maxX, Math.max(b.minX, cx)),
|
|
3920
|
+
cy: Math.min(b.maxY, Math.max(b.minY, cy))
|
|
3648
3921
|
};
|
|
3649
3922
|
}
|
|
3650
|
-
function
|
|
3651
|
-
return
|
|
3923
|
+
function clamp013(v) {
|
|
3924
|
+
return Math.max(0, Math.min(1, v));
|
|
3652
3925
|
}
|
|
3653
|
-
function
|
|
3654
|
-
|
|
3655
|
-
const out = [];
|
|
3656
|
-
for (const line of lines) {
|
|
3657
|
-
if (!line || measure(line) <= maxPx) {
|
|
3658
|
-
out.push(line);
|
|
3659
|
-
continue;
|
|
3660
|
-
}
|
|
3661
|
-
let current = "";
|
|
3662
|
-
for (const token of overlayTokens(line)) {
|
|
3663
|
-
if (!current) {
|
|
3664
|
-
current = token;
|
|
3665
|
-
continue;
|
|
3666
|
-
}
|
|
3667
|
-
if (measure(current + token) <= maxPx) {
|
|
3668
|
-
current += token;
|
|
3669
|
-
} else {
|
|
3670
|
-
out.push(current);
|
|
3671
|
-
current = token;
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
3674
|
-
if (current) out.push(current);
|
|
3675
|
-
}
|
|
3676
|
-
return out.length ? out : [""];
|
|
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 };
|
|
3677
3928
|
}
|
|
3678
|
-
function
|
|
3679
|
-
|
|
3680
|
-
return [
|
|
3681
|
-
...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
|
|
3682
|
-
line
|
|
3683
|
-
)
|
|
3684
|
-
].map((s) => s.segment);
|
|
3685
|
-
}
|
|
3686
|
-
return [...line];
|
|
3929
|
+
function cameraModel(frame) {
|
|
3930
|
+
return frame?.camera === "stage" ? "stage" : "card";
|
|
3687
3931
|
}
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
return lines.map(graphemesOf2);
|
|
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);
|
|
3694
3937
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
const
|
|
3698
|
-
|
|
3699
|
-
const
|
|
3700
|
-
const
|
|
3701
|
-
const
|
|
3702
|
-
const
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
const
|
|
3708
|
-
const
|
|
3709
|
-
const
|
|
3710
|
-
let st = Math.min(2, Math.max(0, spec.stagger ?? defaultStagger));
|
|
3711
|
-
if (upperN > 1) {
|
|
3712
|
-
const maxTotal = Math.max(dur, clipDuration * 0.9);
|
|
3713
|
-
st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
|
|
3714
|
-
}
|
|
3715
|
-
const round14 = (v) => Math.round(v * 1e4) / 1e4;
|
|
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 };
|
|
3947
|
+
}
|
|
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;
|
|
3716
3953
|
return {
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
dur: round14(dur),
|
|
3722
|
-
tt: round14(st * (upperN - 1) + dur),
|
|
3723
|
-
units,
|
|
3724
|
-
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
|
|
3725
3958
|
};
|
|
3726
3959
|
}
|
|
3727
|
-
function
|
|
3728
|
-
const
|
|
3729
|
-
|
|
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 };
|
|
3730
3969
|
}
|
|
3731
|
-
function
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
cx: clip.transform.x * frameW,
|
|
3735
|
-
cy: clip.transform.y * frameH,
|
|
3736
|
-
rotation: clip.transform.rotation || 0
|
|
3737
|
-
};
|
|
3738
|
-
if (clip.kind === "html") {
|
|
3739
|
-
const pb = htmlLayerPictureBox(clip);
|
|
3740
|
-
const w2 = htmlLayerWidth(clip) * frameW * scale;
|
|
3741
|
-
return { ...base, w: w2, h: w2 * (pb.height / pb.width) };
|
|
3742
|
-
}
|
|
3743
|
-
if (clip.kind !== "text") {
|
|
3744
|
-
const w2 = (clip.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH) * frameW * scale;
|
|
3745
|
-
return { ...base, w: w2, h: w2 / (mediaAspect || 16 / 9) };
|
|
3746
|
-
}
|
|
3747
|
-
const style = resolveOverlayStyle(clip);
|
|
3748
|
-
const font = overlayFontString(style, scale, 1);
|
|
3749
|
-
const ls = style.letterSpacing * scale;
|
|
3750
|
-
const styled = styledTextOf(clip);
|
|
3751
|
-
let w = 0;
|
|
3752
|
-
let lineCount = 0;
|
|
3753
|
-
if (styled) {
|
|
3754
|
-
const fonts = styled.fs.map(
|
|
3755
|
-
(f) => overlayFontString({ ...style, weight: f.w }, scale, 1)
|
|
3756
|
-
);
|
|
3757
|
-
const laid = layoutText(
|
|
3758
|
-
{ lines: styled.l, maxWidth: clip.maxWidth ? clip.maxWidth * frameW : 0 },
|
|
3759
|
-
(t, f) => measure(t, fonts[f], ls)
|
|
3760
|
-
);
|
|
3761
|
-
w = laid.width;
|
|
3762
|
-
lineCount = laid.lines.length;
|
|
3763
|
-
} else {
|
|
3764
|
-
const width = (t) => measure(t, font, ls);
|
|
3765
|
-
const lines = clip.maxWidth ? wrapOverlayLines(overlayLines(clip.text), width, clip.maxWidth * frameW) : overlayLines(clip.text);
|
|
3766
|
-
for (const line of lines) w = Math.max(w, width(line));
|
|
3767
|
-
lineCount = lines.length;
|
|
3768
|
-
}
|
|
3769
|
-
const lineH = style.size * scale * style.lineHeight;
|
|
3770
|
-
const box = resolveOverlayBox(clip);
|
|
3771
|
-
const padX = box ? box.padX * scale : 0;
|
|
3772
|
-
const padY = box ? box.padY * scale : 0;
|
|
3773
|
-
return {
|
|
3774
|
-
...base,
|
|
3775
|
-
w: Math.max(w, style.size * scale * 0.6) + padX * 2,
|
|
3776
|
-
// empty text still selectable
|
|
3777
|
-
h: lineCount * lineH + padY * 2
|
|
3778
|
-
};
|
|
3970
|
+
function levelForFocusFraction(frac) {
|
|
3971
|
+
if (!(frac > 0)) return clampZoomLevel(Number.POSITIVE_INFINITY);
|
|
3972
|
+
return clampZoomLevel(Math.max(1.1, 1 / frac));
|
|
3779
3973
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
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 };
|
|
3791
3990
|
}
|
|
3792
3991
|
|
|
3793
3992
|
// src/text3d.ts
|
|
@@ -5402,7 +5601,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5402
5601
|
if (ol.rt) {
|
|
5403
5602
|
var rtFonts = [ovC.font]
|
|
5404
5603
|
for (var rfi = 1; rfi < ol.rt.fs.length; rfi++) {
|
|
5405
|
-
rtFonts.push((ol.
|
|
5604
|
+
rtFonts.push((ol.rt.fs[rfi].i ? 'italic ' : '') + ol.rt.fs[rfi].w + ' ' + olPx + 'px ' + ol.stack)
|
|
5406
5605
|
}
|
|
5407
5606
|
var rtFx = ol.fx && ol.fx.units.length ? ol.fx : null
|
|
5408
5607
|
var rtMax = ol.mw ? ol.mw * W : 0
|
|
@@ -5441,8 +5640,10 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5441
5640
|
ovC.fill()
|
|
5442
5641
|
ovC.restore()
|
|
5443
5642
|
}
|
|
5444
|
-
// Highlights sit behind the words and take no legibility shadow.
|
|
5445
|
-
|
|
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++) {
|
|
5446
5647
|
var rhLine = rtL.lines[rhi]
|
|
5447
5648
|
for (var rhj = 0; rhj < rhLine.frags.length; rhj++) {
|
|
5448
5649
|
var rhF = rhLine.frags[rhj]
|
|
@@ -5460,7 +5661,15 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5460
5661
|
ovC.textAlign = 'left'
|
|
5461
5662
|
// One fragment (or the part of one a unit holds): the underline under
|
|
5462
5663
|
// the glyphs, stroke under fill, the strikethrough over them.
|
|
5463
|
-
var rtPart = function (p, bx, y) {
|
|
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
|
+
}
|
|
5464
5673
|
ovC.font = rtFonts[p.f || 0]
|
|
5465
5674
|
ovC.fillStyle = p.c || ol.color
|
|
5466
5675
|
if (p.u) {
|
|
@@ -5523,7 +5732,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5523
5732
|
ovC.filter = 'blur(' + ((1 - rUu) * olPx * 0.12).toFixed(2) + 'px)'
|
|
5524
5733
|
}
|
|
5525
5734
|
}
|
|
5526
|
-
for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY)
|
|
5735
|
+
for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY, true)
|
|
5527
5736
|
ovC.restore()
|
|
5528
5737
|
}
|
|
5529
5738
|
}
|
|
@@ -8495,7 +8704,8 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
8495
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 }
|
|
8496
8705
|
}
|
|
8497
8706
|
}`;
|
|
8498
|
-
function studioLayerData(
|
|
8707
|
+
function studioLayerData(input, duration, pins) {
|
|
8708
|
+
const layers = migrateText(input);
|
|
8499
8709
|
const rec = "source" in layers ? layers : null;
|
|
8500
8710
|
return {
|
|
8501
8711
|
// Music/SFX clips with their gain envelopes baked (shared truth for the
|
|
@@ -8674,10 +8884,10 @@ function studioLayerData(layers, duration, pins) {
|
|
|
8674
8884
|
const rt = styledTextOf(o);
|
|
8675
8885
|
return {
|
|
8676
8886
|
...base,
|
|
8677
|
-
text: o
|
|
8887
|
+
text: overlayPlainText(o),
|
|
8678
8888
|
// The lines a person reads. A styled layer (rt) is painted from
|
|
8679
8889
|
// its runs; these are then its words, for the signature.
|
|
8680
|
-
lines: overlayLines(
|
|
8890
|
+
lines: overlayLines(overlayPlainText(o)),
|
|
8681
8891
|
// The styled payload, only when the layer is set in more than
|
|
8682
8892
|
// one style: a plain clip's data is unchanged.
|
|
8683
8893
|
...rt ? { rt } : {},
|
|
@@ -8728,7 +8938,7 @@ function studioLayerData(layers, duration, pins) {
|
|
|
8728
8938
|
};
|
|
8729
8939
|
}
|
|
8730
8940
|
function lowerToComposition(input) {
|
|
8731
|
-
const doc = migrateMotion(input);
|
|
8941
|
+
const doc = migrateText(migrateMotion(input));
|
|
8732
8942
|
const rated = ratedSegments(doc);
|
|
8733
8943
|
const transitions = docTransitions(doc);
|
|
8734
8944
|
const footageEnd = durationSec(doc, rated);
|
|
@@ -8937,7 +9147,7 @@ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
|
|
|
8937
9147
|
});
|
|
8938
9148
|
}
|
|
8939
9149
|
function docZoomTrack(input) {
|
|
8940
|
-
const doc = migrateMotion(input);
|
|
9150
|
+
const doc = migrateText(migrateMotion(input));
|
|
8941
9151
|
const rated = ratedSegments(doc);
|
|
8942
9152
|
const transitions = docTransitions(doc);
|
|
8943
9153
|
const layout = docCardLayout(doc);
|
|
@@ -11863,7 +12073,7 @@ var overlaysLane = {
|
|
|
11863
12073
|
kind: "clip",
|
|
11864
12074
|
t: round13(o.start),
|
|
11865
12075
|
duration: round13(o.duration),
|
|
11866
|
-
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"
|
|
11867
12077
|
},
|
|
11868
12078
|
// Pose diamonds render after the clips, so they sit on top.
|
|
11869
12079
|
...poseItems(o.id, o.start, o.duration, o.motion)
|
|
@@ -12276,8 +12486,6 @@ export {
|
|
|
12276
12486
|
DRAG_FIT_LEVEL,
|
|
12277
12487
|
DRAG_MIN_S,
|
|
12278
12488
|
DRAG_MIN_TRAVEL_FRAC,
|
|
12279
|
-
EM_CLOSE,
|
|
12280
|
-
EM_OPEN,
|
|
12281
12489
|
END_CARD_FROM,
|
|
12282
12490
|
END_CARD_RECEDE,
|
|
12283
12491
|
END_CARD_SECONDS,
|
|
@@ -12311,6 +12519,7 @@ export {
|
|
|
12311
12519
|
MEDIA_FRAME_KEYS,
|
|
12312
12520
|
MEDIA_KEY_PREFIX,
|
|
12313
12521
|
MINIMAL_BAR_THEMES,
|
|
12522
|
+
MIXED,
|
|
12314
12523
|
MOTION_DELTA,
|
|
12315
12524
|
MOTION_EASE,
|
|
12316
12525
|
OBJECT_DEFAULT_SCALE,
|
|
@@ -12334,6 +12543,7 @@ export {
|
|
|
12334
12543
|
REJECT_OVERLAP,
|
|
12335
12544
|
REST_TILT_ID,
|
|
12336
12545
|
RETIRED_ZOOM_STYLES,
|
|
12546
|
+
RUN_STYLE_KEYS,
|
|
12337
12547
|
SCENE_MOTION,
|
|
12338
12548
|
SCENE_QUIET,
|
|
12339
12549
|
SEGMENT_ENTER_KINDS,
|
|
@@ -12391,6 +12601,7 @@ export {
|
|
|
12391
12601
|
aspectRatioValue,
|
|
12392
12602
|
audioLane,
|
|
12393
12603
|
backdropMedia,
|
|
12604
|
+
boldWeightFor,
|
|
12394
12605
|
buildDigest,
|
|
12395
12606
|
calloutBox,
|
|
12396
12607
|
calloutClip,
|
|
@@ -12421,9 +12632,11 @@ export {
|
|
|
12421
12632
|
clampSpeedRate,
|
|
12422
12633
|
clampTiltDeg,
|
|
12423
12634
|
clampZoomLevel,
|
|
12635
|
+
clearStyle,
|
|
12424
12636
|
clipEnvelope,
|
|
12425
12637
|
clipLength,
|
|
12426
12638
|
clipsFrom,
|
|
12639
|
+
commonStyle,
|
|
12427
12640
|
computeCardLayout,
|
|
12428
12641
|
computeMicRms,
|
|
12429
12642
|
computePeaks,
|
|
@@ -12442,6 +12655,7 @@ export {
|
|
|
12442
12655
|
deriveViewportCrop,
|
|
12443
12656
|
destinationById,
|
|
12444
12657
|
destinationsForChannel,
|
|
12658
|
+
diffInput,
|
|
12445
12659
|
docCardLayout,
|
|
12446
12660
|
docFreezes,
|
|
12447
12661
|
docMediaList,
|
|
@@ -12517,6 +12731,7 @@ export {
|
|
|
12517
12731
|
lowerProgramDoc,
|
|
12518
12732
|
lowerStudioDoc,
|
|
12519
12733
|
lowerToComposition,
|
|
12734
|
+
marksEmphasis,
|
|
12520
12735
|
mediaAtOutput,
|
|
12521
12736
|
mediaDuration,
|
|
12522
12737
|
mediaFrame,
|
|
@@ -12527,12 +12742,15 @@ export {
|
|
|
12527
12742
|
micLane,
|
|
12528
12743
|
migrateHostedDoc,
|
|
12529
12744
|
migrateMotion,
|
|
12745
|
+
migrateText,
|
|
12746
|
+
migrateTextClip,
|
|
12530
12747
|
migrateZoomStyle,
|
|
12531
12748
|
momentsFromDoc,
|
|
12532
12749
|
motionTrack,
|
|
12533
12750
|
musicBedClip,
|
|
12534
12751
|
nextMediaId,
|
|
12535
12752
|
normalizeCaptureSpace,
|
|
12753
|
+
normalizeRuns,
|
|
12536
12754
|
objectMotionBase,
|
|
12537
12755
|
objectMotionPoseAt,
|
|
12538
12756
|
objectsLane,
|
|
@@ -12541,7 +12759,6 @@ export {
|
|
|
12541
12759
|
outputEnd,
|
|
12542
12760
|
outputRangeToSource,
|
|
12543
12761
|
overlapFraction,
|
|
12544
|
-
overlayDisplayText,
|
|
12545
12762
|
overlayFaceFor,
|
|
12546
12763
|
overlayFontFaces,
|
|
12547
12764
|
overlayFontString,
|
|
@@ -12551,12 +12768,12 @@ export {
|
|
|
12551
12768
|
overlayMotionBase,
|
|
12552
12769
|
overlayMotionPoseAt,
|
|
12553
12770
|
overlayPinPoseAt,
|
|
12771
|
+
overlayPlainText,
|
|
12554
12772
|
overlayRect,
|
|
12555
12773
|
overlaysLane,
|
|
12556
12774
|
ownFreezes,
|
|
12557
12775
|
pageDisplayUrl,
|
|
12558
12776
|
parseCssColour,
|
|
12559
|
-
parseEmphasis,
|
|
12560
12777
|
parsePoseId,
|
|
12561
12778
|
pickStyle,
|
|
12562
12779
|
pinBox,
|
|
@@ -12566,6 +12783,7 @@ export {
|
|
|
12566
12783
|
pinReferent,
|
|
12567
12784
|
pinReferentAt,
|
|
12568
12785
|
placeFreezes,
|
|
12786
|
+
plainText,
|
|
12569
12787
|
planAutoSpeed,
|
|
12570
12788
|
planAutoTilt,
|
|
12571
12789
|
planAutoZoom,
|
|
@@ -12585,7 +12803,7 @@ export {
|
|
|
12585
12803
|
relativeLuminance,
|
|
12586
12804
|
removeSourceRange,
|
|
12587
12805
|
removeSpeedInRange,
|
|
12588
|
-
|
|
12806
|
+
replaceRange,
|
|
12589
12807
|
resolveExportSize,
|
|
12590
12808
|
resolveOverlayBox,
|
|
12591
12809
|
resolveOverlayStyle,
|
|
@@ -12595,6 +12813,7 @@ export {
|
|
|
12595
12813
|
restLeanOf,
|
|
12596
12814
|
restSpansThroughTransitions,
|
|
12597
12815
|
rgbToHex,
|
|
12816
|
+
runsOf,
|
|
12598
12817
|
sameMedia,
|
|
12599
12818
|
sceneChanges,
|
|
12600
12819
|
scrollRuns,
|
|
@@ -12602,17 +12821,23 @@ export {
|
|
|
12602
12821
|
outputLen as segmentOutputLen,
|
|
12603
12822
|
segmentStarts,
|
|
12604
12823
|
setSpeedInRange,
|
|
12824
|
+
setStyle,
|
|
12825
|
+
sliceRuns,
|
|
12605
12826
|
slideSide,
|
|
12606
12827
|
smoothCursor,
|
|
12828
|
+
snapOffset,
|
|
12829
|
+
snapRunWeight,
|
|
12607
12830
|
spanOutputExtent,
|
|
12608
12831
|
speedLane,
|
|
12609
|
-
stripEmphasis,
|
|
12610
12832
|
studioAudioPlan,
|
|
12611
12833
|
studioEntry,
|
|
12612
12834
|
studioLayerData,
|
|
12835
|
+
styleAt,
|
|
12836
|
+
styleOf,
|
|
12613
12837
|
styledTextOf,
|
|
12614
12838
|
tiltLane,
|
|
12615
12839
|
tiltTrackFromDoc,
|
|
12840
|
+
toggleStyle,
|
|
12616
12841
|
tokenRanges,
|
|
12617
12842
|
traceKindOf,
|
|
12618
12843
|
transitionMult,
|