@vosjs/studio-core 0.37.0 → 0.39.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 +131 -43
- package/dist/index.js +2153 -2153
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1817,1977 +1817,2127 @@ function outputEnd(doc, footageEnd) {
|
|
|
1817
1817
|
return Math.max(footageEnd, lastLayerEnd(doc));
|
|
1818
1818
|
}
|
|
1819
1819
|
|
|
1820
|
-
// src/
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
}
|
|
1830
|
-
var
|
|
1831
|
-
var
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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
|
-
}
|
|
2104
|
-
|
|
2105
|
-
// src/planner/smoothing.ts
|
|
2106
|
-
var SNAP_BEFORE = 0.12;
|
|
2107
|
-
var SNAP_AFTER = 0.18;
|
|
2108
|
-
var SNAP_GAIN = 0.5;
|
|
2109
|
-
function sampleAt(points, t) {
|
|
2110
|
-
if (points.length === 0) return { x: 0, y: 0 };
|
|
2111
|
-
if (t <= points[0].t) return { x: points[0].x, y: points[0].y };
|
|
2112
|
-
const last = points[points.length - 1];
|
|
2113
|
-
if (t >= last.t) return { x: last.x, y: last.y };
|
|
2114
|
-
for (let i = 1; i < points.length; i++) {
|
|
2115
|
-
if (points[i].t >= t) {
|
|
2116
|
-
const a = points[i - 1];
|
|
2117
|
-
const b = points[i];
|
|
2118
|
-
const f = (t - a.t) / (b.t - a.t || 1);
|
|
2119
|
-
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
return { x: last.x, y: last.y };
|
|
2123
|
-
}
|
|
2124
|
-
function smoothCursor(track, options = {}) {
|
|
2125
|
-
const factor = clamp2(options.factor ?? 0.15, 0.01, 1);
|
|
2126
|
-
const fps = options.fps ?? 60;
|
|
2127
|
-
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 }));
|
|
2128
|
-
if (raw.length === 0) return [];
|
|
2129
|
-
const clicks = options.clickSnap ? track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y })) : [];
|
|
2130
|
-
const start = raw[0].t;
|
|
2131
|
-
const end = raw[raw.length - 1].t;
|
|
2132
|
-
const step = 1 / fps;
|
|
2133
|
-
const out = [];
|
|
2134
|
-
let cx = raw[0].x;
|
|
2135
|
-
let cy = raw[0].y;
|
|
2136
|
-
let ci = 0;
|
|
2137
|
-
for (let t = start; t <= end + 1e-9; t += step) {
|
|
2138
|
-
const target = sampleAt(raw, t);
|
|
2139
|
-
cx += (target.x - cx) * factor;
|
|
2140
|
-
cy += (target.y - cy) * factor;
|
|
2141
|
-
if (clicks.length) {
|
|
2142
|
-
while (ci < clicks.length - 1 && t > clicks[ci].t + SNAP_AFTER) ci++;
|
|
2143
|
-
const ck = clicks[ci];
|
|
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
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
out.push({ t, x: cx, y: cy });
|
|
2156
|
-
}
|
|
2157
|
-
return out;
|
|
2158
|
-
}
|
|
2159
|
-
function clamp2(v, lo, hi) {
|
|
2160
|
-
return Math.max(lo, Math.min(hi, v));
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
// src/planner/autoZoom.ts
|
|
2164
|
-
var DWELL_MOVE_FRAC = 0.02;
|
|
2165
|
-
var DWELL_MIN = 0.45;
|
|
2166
|
-
var DWELL_MAX = 2.6;
|
|
2167
|
-
var DWELL_SPACING = 1.8;
|
|
2168
|
-
var CLUSTER_FILL = 0.85;
|
|
2169
|
-
var DRAG_FIT_LEVEL = 1.15;
|
|
2170
|
-
var PRESS_ECHO_MS = 4;
|
|
2171
|
-
var PRESS_ECHO_PX = 2;
|
|
2172
|
-
var DRAG_MIN_TRAVEL_FRAC = 0.015;
|
|
2173
|
-
var DRAG_MIN_S = 0.2;
|
|
2174
|
-
function dragsFromTrack(track, width, height) {
|
|
2175
|
-
if (!(width > 0) || !(height > 0)) return [];
|
|
2176
|
-
const downs = dedupePresses(track.filter((e) => e.type === "down"));
|
|
2177
|
-
const out = [];
|
|
2178
|
-
for (const d of downs) {
|
|
2179
|
-
const up = track.find((e) => e.type === "up" && e.t > d.t);
|
|
2180
|
-
if (!up) continue;
|
|
2181
|
-
const t0 = d.t / 1e3;
|
|
2182
|
-
const t1 = up.t / 1e3;
|
|
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;
|
|
2191
|
-
}
|
|
2192
|
-
if (travel < DRAG_MIN_TRAVEL_FRAC) continue;
|
|
2193
|
-
out.push({
|
|
2194
|
-
t0,
|
|
2195
|
-
t1,
|
|
2196
|
-
nx: clamp012(d.x / width),
|
|
2197
|
-
ny: clamp012(d.y / height),
|
|
2198
|
-
travel
|
|
2199
|
-
});
|
|
2200
|
-
}
|
|
2201
|
-
return out;
|
|
2202
|
-
}
|
|
2203
|
-
var TYPING_MIN_PINGS = 2;
|
|
2204
|
-
var TYPING_MIN_DUR = 0.5;
|
|
2205
|
-
var TYPING_CLICK_ABSORB = 1.5;
|
|
2206
|
-
var TYPING_REFOCUS_FRAC = 0.08;
|
|
2207
|
-
function groupTrack(track, opts) {
|
|
2208
|
-
const { width, height, clusterGap, typingGap, typingZoom, targetFill } = opts;
|
|
2209
|
-
const minLevel = opts.minLevel ?? resolveZoomStyle().minLevel;
|
|
2210
|
-
const clicks = dedupePresses(
|
|
2211
|
-
track.filter((e) => e.type === "down")
|
|
2212
|
-
).map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
|
|
2213
|
-
const sessions = typingZoom ? typingSessions(track, width, height, typingGap) : [];
|
|
2214
|
-
const absorbed = /* @__PURE__ */ new Set();
|
|
2215
|
-
for (const s of sessions) {
|
|
2216
|
-
let best = null;
|
|
2217
|
-
for (const c of clicks) {
|
|
2218
|
-
if (absorbed.has(c)) continue;
|
|
2219
|
-
if (c.t >= s.first || s.first - c.t > TYPING_CLICK_ABSORB) continue;
|
|
2220
|
-
const [nx, ny] = fieldCenter(c, width, height);
|
|
2221
|
-
if (Math.hypot(nx - s.cx, ny - s.cy) > TYPING_REFOCUS_FRAC) continue;
|
|
2222
|
-
if (!best || c.t > best.t) best = c;
|
|
2223
|
-
}
|
|
2224
|
-
if (best) {
|
|
2225
|
-
absorbed.add(best);
|
|
2226
|
-
s.start = best.t;
|
|
2227
|
-
s.events = [best, ...s.events];
|
|
2228
|
-
}
|
|
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;
|
|
2247
|
-
};
|
|
2248
|
-
return {
|
|
2249
|
-
sessions,
|
|
2250
|
-
clusters: chain(targets, true),
|
|
2251
|
-
surfaces: chain(surfaces, false)
|
|
2252
|
-
};
|
|
2253
|
-
}
|
|
2254
|
-
function dedupePresses(downs) {
|
|
2255
|
-
const kept = [];
|
|
2256
|
-
for (const e of downs) {
|
|
2257
|
-
const prev = kept.at(-1);
|
|
2258
|
-
if (prev && e.t - prev.t <= PRESS_ECHO_MS && Math.abs(e.x - prev.x) <= PRESS_ECHO_PX && Math.abs(e.y - prev.y) <= PRESS_ECHO_PX) {
|
|
2259
|
-
continue;
|
|
2260
|
-
}
|
|
2261
|
-
kept.push(e);
|
|
2262
|
-
}
|
|
2263
|
-
return kept;
|
|
2264
|
-
}
|
|
2265
|
-
function unionRect(clicks) {
|
|
2266
|
-
let x0 = Infinity;
|
|
2267
|
-
let y0 = Infinity;
|
|
2268
|
-
let x1 = -Infinity;
|
|
2269
|
-
let y1 = -Infinity;
|
|
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);
|
|
2276
|
-
}
|
|
2277
|
-
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
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);
|
|
2283
|
-
}
|
|
2284
|
-
function fitLevel(rect, width, height, targetFill) {
|
|
2285
|
-
if (!rect || rect.w <= 0 || rect.h <= 0) return null;
|
|
2286
|
-
return Math.min(width * targetFill / rect.w, height * targetFill / rect.h);
|
|
2287
|
-
}
|
|
2288
|
-
function isSurfacePress(c, width, height, targetFill) {
|
|
2289
|
-
const fit = fitLevel(c.rect, width, height, targetFill);
|
|
2290
|
-
return fit !== null && fit < DRAG_FIT_LEVEL;
|
|
2291
|
-
}
|
|
2292
|
-
function planAutoZoom(track, options) {
|
|
2293
|
-
const style = resolveZoomStyle(options.style, options.params);
|
|
2294
|
-
if (!style.autoZoom) return [];
|
|
2295
|
-
const {
|
|
2296
|
-
width,
|
|
2297
|
-
height,
|
|
2298
|
-
targetFill = style.targetFill,
|
|
2299
|
-
minLevel = style.minLevel,
|
|
2300
|
-
maxLevel = style.maxLevel,
|
|
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
|
-
});
|
|
2363
|
-
}
|
|
2364
|
-
clickSpans.sort((a, b) => a.in - b.in);
|
|
2365
|
-
for (let i = 0; i + 1 < clickSpans.length; i++) {
|
|
2366
|
-
if (clickSpans[i].out > clickSpans[i + 1].in)
|
|
2367
|
-
clickSpans[i].out = clickSpans[i + 1].in;
|
|
2368
|
-
}
|
|
2369
|
-
for (const z of clickSpans) if (z.out - z.in < 0.3) z.dead = true;
|
|
2370
|
-
const typingFloor = Math.min(Math.max(minLevel, typingMinLevel), maxLevel);
|
|
2371
|
-
let typing = sessions.map((s) => {
|
|
2372
|
-
const f = focusFor(
|
|
2373
|
-
s.events,
|
|
2374
|
-
width,
|
|
2375
|
-
height,
|
|
2376
|
-
targetFill,
|
|
2377
|
-
typingFloor,
|
|
2378
|
-
maxLevel
|
|
2379
|
-
);
|
|
2380
|
-
return {
|
|
2381
|
-
in: Math.max(0, s.start - lead),
|
|
2382
|
-
out: s.last + typingHold,
|
|
2383
|
-
cx: f.cx,
|
|
2384
|
-
cy: f.cy,
|
|
2385
|
-
level: f.level
|
|
2386
|
-
};
|
|
2387
|
-
});
|
|
2388
|
-
for (const t of typing) {
|
|
2389
|
-
for (const z of clickSpans) {
|
|
2390
|
-
if (z.dead || t.dead) continue;
|
|
2391
|
-
if (t.in >= z.out || t.out <= z.in) continue;
|
|
2392
|
-
if (Math.hypot(t.cx - z.cx, t.cy - z.cy) <= TYPING_REFOCUS_FRAC) {
|
|
2393
|
-
t.in = Math.min(t.in, z.in);
|
|
2394
|
-
t.out = Math.max(t.out, z.out);
|
|
2395
|
-
z.dead = true;
|
|
2396
|
-
} else if (z.in <= t.in && z.out >= t.out) {
|
|
2397
|
-
t.dead = true;
|
|
2398
|
-
} else if (z.in > t.in) {
|
|
2399
|
-
t.out = z.in;
|
|
2400
|
-
} else {
|
|
2401
|
-
t.in = z.out;
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
typing = typing.filter((t) => !t.dead).sort((a, b) => a.in - b.in);
|
|
2406
|
-
for (let i = 0; i + 1 < typing.length; i++) {
|
|
2407
|
-
if (typing[i].out > typing[i + 1].in) typing[i].out = typing[i + 1].in;
|
|
1820
|
+
// src/overlayText.ts
|
|
1821
|
+
import {
|
|
1822
|
+
findFontFamily as findFontFamily2,
|
|
1823
|
+
fontFaceUrl as fontFaceUrl2,
|
|
1824
|
+
fontStack,
|
|
1825
|
+
nearestFontWeight as nearestFontWeight2
|
|
1826
|
+
} from "@vosjs/shared";
|
|
1827
|
+
|
|
1828
|
+
// src/htmlLayer.ts
|
|
1829
|
+
import { findFontFamily, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
1830
|
+
var DESIGN_FRAME_WIDTH = 1920;
|
|
1831
|
+
var HTML_LAYER_WARN_BYTES = 32e3;
|
|
1832
|
+
var HTML_LAYER_MAX_BYTES = 128e3;
|
|
1833
|
+
var HTML_LAYER_LIVE_HZ = 60;
|
|
1834
|
+
var HTML_LAYER_ASSET_HOST = "https://assets.vos.so/";
|
|
1835
|
+
function splitTopLevel(value) {
|
|
1836
|
+
const out = [];
|
|
1837
|
+
let depth = 0;
|
|
1838
|
+
let cur = "";
|
|
1839
|
+
for (const ch of value) {
|
|
1840
|
+
if (ch === "(") depth++;
|
|
1841
|
+
else if (ch === ")") depth--;
|
|
1842
|
+
if (ch === "," && depth === 0) {
|
|
1843
|
+
out.push(cur);
|
|
1844
|
+
cur = "";
|
|
1845
|
+
} else cur += ch;
|
|
2408
1846
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
id: `z${i}`,
|
|
2412
|
-
in: round2(s.in),
|
|
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);
|
|
1847
|
+
if (cur.trim()) out.push(cur);
|
|
1848
|
+
return out;
|
|
2441
1849
|
}
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
1850
|
+
var unquote = (s) => s.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim();
|
|
1851
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
1852
|
+
"serif",
|
|
1853
|
+
"sans-serif",
|
|
1854
|
+
"monospace",
|
|
1855
|
+
"cursive",
|
|
1856
|
+
"fantasy",
|
|
1857
|
+
"system-ui",
|
|
1858
|
+
"ui-sans-serif",
|
|
1859
|
+
"ui-serif",
|
|
1860
|
+
"ui-monospace",
|
|
1861
|
+
"ui-rounded",
|
|
1862
|
+
"emoji",
|
|
1863
|
+
"math",
|
|
1864
|
+
"fangsong",
|
|
1865
|
+
"inherit",
|
|
1866
|
+
"initial",
|
|
1867
|
+
"unset"
|
|
1868
|
+
]);
|
|
1869
|
+
function cssFamilies(css) {
|
|
1870
|
+
if (!css) return [];
|
|
1871
|
+
const out = [];
|
|
1872
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1873
|
+
for (const decl of css.match(/font-family\s*:[^;}]+/gi) ?? []) {
|
|
1874
|
+
for (const item of splitTopLevel(decl.replace(/^[^:]*:/, ""))) {
|
|
1875
|
+
const name = unquote(item.replace(/!important/i, ""));
|
|
1876
|
+
if (!name) continue;
|
|
1877
|
+
const lower = name.toLowerCase();
|
|
1878
|
+
if (GENERIC_FAMILIES.has(lower) || lower.startsWith("-apple-")) continue;
|
|
1879
|
+
if (seen.has(lower)) continue;
|
|
1880
|
+
seen.add(lower);
|
|
1881
|
+
out.push(name);
|
|
2454
1882
|
}
|
|
2455
1883
|
}
|
|
2456
|
-
return
|
|
2457
|
-
(s) => s.events.length >= TYPING_MIN_PINGS && s.last - s.first >= TYPING_MIN_DUR
|
|
2458
|
-
);
|
|
1884
|
+
return out;
|
|
2459
1885
|
}
|
|
2460
|
-
function
|
|
2461
|
-
const
|
|
2462
|
-
const
|
|
2463
|
-
|
|
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);
|
|
2464
1895
|
}
|
|
2465
|
-
function
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
const
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
const
|
|
2477
|
-
if (!
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
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"
|
|
2484
1915
|
});
|
|
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);
|
|
2516
1916
|
}
|
|
2517
|
-
|
|
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;
|
|
2518
1925
|
}
|
|
2519
|
-
function
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
1926
|
+
function htmlLayerUnhostedFamilies(clip) {
|
|
1927
|
+
const explicit = new Set(
|
|
1928
|
+
(clip.fonts ?? []).map((f) => f.family.toLowerCase())
|
|
1929
|
+
);
|
|
1930
|
+
return cssFamilies(clip.css).filter(
|
|
1931
|
+
(f) => !findFontFamily(f) && !explicit.has(f.toLowerCase())
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
function htmlLayerAssetUrls(clip) {
|
|
1935
|
+
const out = [];
|
|
1936
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1937
|
+
const add = (u) => {
|
|
1938
|
+
if (!seen.has(u)) {
|
|
1939
|
+
seen.add(u);
|
|
1940
|
+
out.push(u);
|
|
2533
1941
|
}
|
|
2534
|
-
}
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
const
|
|
2540
|
-
|
|
1942
|
+
};
|
|
1943
|
+
for (const m of clip.html.matchAll(
|
|
1944
|
+
/\bsrc\s*=\s*["']((?:https?:)?\/\/[^"']+)["']/gi
|
|
1945
|
+
))
|
|
1946
|
+
add(m[1]);
|
|
1947
|
+
for (const text of [clip.html, clip.css ?? ""])
|
|
1948
|
+
for (const m of text.matchAll(
|
|
1949
|
+
/url\(\s*["']?((?:https?:)?\/\/[^"')\s]+)["']?\s*\)/gi
|
|
1950
|
+
))
|
|
1951
|
+
add(m[1]);
|
|
1952
|
+
return out;
|
|
2541
1953
|
}
|
|
2542
|
-
function
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
1954
|
+
function htmlLayerAssets(clip) {
|
|
1955
|
+
const inlinable = [];
|
|
1956
|
+
const foreign = [];
|
|
1957
|
+
for (const u of htmlLayerAssetUrls(clip))
|
|
1958
|
+
(u.replace(/^\/\//, "https://").startsWith(HTML_LAYER_ASSET_HOST) ? inlinable : foreign).push(u);
|
|
1959
|
+
return { inlinable, foreign };
|
|
1960
|
+
}
|
|
1961
|
+
function htmlLayerBleedFor(css) {
|
|
1962
|
+
if (!css) return 0;
|
|
1963
|
+
let worst = 0;
|
|
1964
|
+
const consider = (layer) => {
|
|
1965
|
+
if (/\binset\b/i.test(layer)) return;
|
|
1966
|
+
const lengths = layer.replace(/[a-z-]+\([^)]*\)/gi, " ").replace(/#[0-9a-f]{3,8}/gi, " ").match(/-?\d*\.?\d+/g);
|
|
1967
|
+
if (!lengths) return;
|
|
1968
|
+
const [x = 0, y = 0, blur = 0, spread = 0] = lengths.map(Number);
|
|
1969
|
+
worst = Math.max(
|
|
1970
|
+
worst,
|
|
1971
|
+
Math.abs(x) + blur + spread,
|
|
1972
|
+
Math.abs(y) + blur + spread
|
|
1973
|
+
);
|
|
1974
|
+
};
|
|
1975
|
+
for (const decl of css.match(/box-shadow\s*:[^;}]+/gi) ?? [])
|
|
1976
|
+
for (const layer of splitTopLevel(decl.replace(/^[^:]*:/, "")))
|
|
1977
|
+
consider(layer);
|
|
1978
|
+
for (const fn of css.match(/drop-shadow\(([^)]*)\)/gi) ?? [])
|
|
1979
|
+
consider(fn.replace(/^drop-shadow\(/i, "").replace(/\)$/, ""));
|
|
1980
|
+
return worst ? Math.ceil(worst * 1.15) : 0;
|
|
1981
|
+
}
|
|
1982
|
+
function htmlLayerBleed(clip) {
|
|
1983
|
+
return Math.max(0, Math.round(clip.bleed ?? htmlLayerBleedFor(clip.css)));
|
|
1984
|
+
}
|
|
1985
|
+
function htmlLayerPictureBox(clip) {
|
|
1986
|
+
const bleed = htmlLayerBleed(clip);
|
|
1987
|
+
return {
|
|
1988
|
+
width: clip.box.width + bleed * 2,
|
|
1989
|
+
height: clip.box.height + bleed * 2,
|
|
1990
|
+
bleed
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function htmlLayerWidth(clip) {
|
|
1994
|
+
if (clip.width != null) return clip.width;
|
|
1995
|
+
return Math.min(1, htmlLayerPictureBox(clip).width / DESIGN_FRAME_WIDTH);
|
|
1996
|
+
}
|
|
1997
|
+
var ENTITIES = {
|
|
1998
|
+
amp: "&",
|
|
1999
|
+
lt: "<",
|
|
2000
|
+
gt: ">",
|
|
2001
|
+
quot: '"',
|
|
2002
|
+
apos: "'",
|
|
2003
|
+
nbsp: " "
|
|
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
|
+
}
|
|
2562
2063
|
}
|
|
2563
2064
|
}
|
|
2564
|
-
const
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
} : null;
|
|
2571
|
-
return { cx: clamp012(sx / n / width), cy: clamp012(sy / n / height), rect };
|
|
2572
|
-
}
|
|
2573
|
-
function clamp3(v, lo, hi) {
|
|
2574
|
-
return Math.max(lo, Math.min(hi, v));
|
|
2575
|
-
}
|
|
2576
|
-
function clamp012(v) {
|
|
2577
|
-
return clamp3(v, 0, 1);
|
|
2578
|
-
}
|
|
2579
|
-
function round2(v) {
|
|
2580
|
-
return Math.round(v * 1e3) / 1e3;
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
// src/lower/lowerToComposition.ts
|
|
2584
|
-
import {
|
|
2585
|
-
EASINGS,
|
|
2586
|
-
lerpArray as lerpArray2,
|
|
2587
|
-
sample as sample2,
|
|
2588
|
-
segmentRate as segmentRate2,
|
|
2589
|
-
sortKeyframes as sortKeyframes2,
|
|
2590
|
-
sourceToTimeline,
|
|
2591
|
-
splitBySpeed as splitBySpeed3,
|
|
2592
|
-
totalDuration as totalDuration3
|
|
2593
|
-
} from "@vosjs/timeline";
|
|
2594
|
-
import { timelineRuntimeCode as timelineRuntimeCode2 } from "@vosjs/timeline/bundle";
|
|
2595
|
-
|
|
2596
|
-
// src/layout.ts
|
|
2597
|
-
function computeCardLayout(frame, video, W, H, zoomFocus) {
|
|
2598
|
-
const s = H / 1080;
|
|
2599
|
-
const pad = (frame.padding || 0) * s;
|
|
2600
|
-
const ins = frame.inset ?? {};
|
|
2601
|
-
const padL = ins.left == null ? pad : ins.left * W;
|
|
2602
|
-
const padR = ins.right == null ? pad : ins.right * W;
|
|
2603
|
-
const padT = ins.top == null ? pad : ins.top * H;
|
|
2604
|
-
const padB = ins.bottom == null ? pad : ins.bottom * H;
|
|
2605
|
-
const bar = frame.browserBar;
|
|
2606
|
-
const vw = video.width || 16;
|
|
2607
|
-
const vh = video.height || 9;
|
|
2608
|
-
const fitCover = frame.fit === "cover";
|
|
2609
|
-
const cf = fitCover ? 1 : Math.min(1, W / H / (vw / vh));
|
|
2610
|
-
const barH = bar.kind !== "none" ? (bar.height || 44) * s * cf : 0;
|
|
2611
|
-
const availW = Math.max(1, W - padL - padR);
|
|
2612
|
-
const availH = Math.max(1, H - padT - padB - barH);
|
|
2613
|
-
if (fitCover) {
|
|
2614
|
-
const sc2 = Math.max(availW / vw, availH / vh);
|
|
2615
|
-
const dw2 = vw * sc2;
|
|
2616
|
-
const dh2 = vh * sc2;
|
|
2617
|
-
const follow = frame.focusFollow === "camera" && zoomFocus ? zoomFocus : null;
|
|
2618
|
-
const fcx = clamp013(follow ? follow.cx : frame.focus?.cx ?? 0.5);
|
|
2619
|
-
const fcy = clamp013(follow ? follow.cy : frame.focus?.cy ?? 0.5);
|
|
2620
|
-
const vTop = padT + barH;
|
|
2621
|
-
const dx2 = Math.min(
|
|
2622
|
-
padL,
|
|
2623
|
-
Math.max(padL + availW - dw2, padL + availW / 2 - fcx * dw2)
|
|
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}.`
|
|
2624
2071
|
);
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
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."
|
|
2628
2077
|
);
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
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;
|
|
2659
2143
|
}
|
|
2660
|
-
function
|
|
2661
|
-
const
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
);
|
|
2667
|
-
const src = mediaSource(doc, media) ?? doc.source;
|
|
2668
|
-
const sm = src.meta;
|
|
2669
|
-
return computeCardLayout(
|
|
2670
|
-
mediaFrame(doc, media),
|
|
2671
|
-
{
|
|
2672
|
-
width: sm.captureWidth ?? sm.width,
|
|
2673
|
-
height: sm.captureHeight ?? sm.height
|
|
2674
|
-
},
|
|
2675
|
-
W,
|
|
2676
|
-
H
|
|
2677
|
-
);
|
|
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." };
|
|
2678
2150
|
}
|
|
2679
|
-
function
|
|
2680
|
-
|
|
2681
|
-
const
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2151
|
+
function fontFaceCss(faces) {
|
|
2152
|
+
let out = "";
|
|
2153
|
+
for (const f of faces)
|
|
2154
|
+
out += `@font-face{font-family:'${f.family}';src:url(${f.dataUri}) format('woff2');font-weight:${f.weight};font-style:${f.style};font-display:block}`;
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
var fmtT = (t) => String(Math.round(t * 1e3) / 1e3);
|
|
2158
|
+
var escapeXml = (v) => String(v).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2159
|
+
function htmlLayerSvg(src, faces = [], assets = []) {
|
|
2160
|
+
const bleed = Math.max(0, Math.round(src.bleed ?? 0));
|
|
2161
|
+
const boxW = src.box.width + bleed * 2;
|
|
2162
|
+
const boxH = src.box.height + bleed * 2;
|
|
2163
|
+
const t = src.t ?? 0;
|
|
2164
|
+
const fill = (text) => {
|
|
2165
|
+
let out = text;
|
|
2166
|
+
for (const a of assets) out = out.split(a.url).join(a.dataUri);
|
|
2167
|
+
if (src.live) {
|
|
2168
|
+
out = out.replace(/\{\{\s*t\s*\}\}/g, fmtT(t));
|
|
2169
|
+
out = out.replace(
|
|
2170
|
+
/\{\{\s*data\.([A-Za-z0-9_.-]+)\s*\}\}/g,
|
|
2171
|
+
(_m, name) => {
|
|
2172
|
+
const v = src.data ? src.data[name] : void 0;
|
|
2173
|
+
return v == null ? "" : escapeXml(v);
|
|
2174
|
+
}
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
return out;
|
|
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>`;
|
|
2182
|
+
}
|
|
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;
|
|
2688
2188
|
}
|
|
2689
|
-
return
|
|
2189
|
+
return h.toString(36);
|
|
2690
2190
|
}
|
|
2691
|
-
function
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
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)}`;
|
|
2196
|
+
}
|
|
2197
|
+
function htmlLayerPayload(clip) {
|
|
2198
|
+
const faces = htmlLayerFaces(clip);
|
|
2199
|
+
const bleed = htmlLayerBleed(clip);
|
|
2200
|
+
const assets = htmlLayerAssets(clip).inlinable;
|
|
2697
2201
|
return {
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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
|
+
}
|
|
2702
2213
|
};
|
|
2703
2214
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
if (
|
|
2709
|
-
|
|
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;
|
|
2710
2224
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
);
|
|
2720
|
-
const y2 = stageAxisBounds(
|
|
2721
|
-
layout.dy,
|
|
2722
|
-
layout.dh,
|
|
2723
|
-
layout.cardY,
|
|
2724
|
-
layout.cardH,
|
|
2725
|
-
layout.H,
|
|
2726
|
-
level
|
|
2727
|
-
);
|
|
2728
|
-
return { minX: x2.min, maxX: x2.max, minY: y2.min, maxY: y2.max };
|
|
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]);
|
|
2729
2233
|
}
|
|
2730
|
-
|
|
2731
|
-
layout.dx,
|
|
2732
|
-
layout.dw,
|
|
2733
|
-
layout.cardX,
|
|
2734
|
-
layout.cardW,
|
|
2735
|
-
layout.W,
|
|
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 };
|
|
2234
|
+
return out.length ? out : [[0, line.length]];
|
|
2747
2235
|
}
|
|
2748
|
-
function
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
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
|
+
}
|
|
2321
|
+
} else {
|
|
2322
|
+
let at = 0;
|
|
2323
|
+
const gs = graphemesOf(text);
|
|
2324
|
+
for (let i = 0; i < gs.length; i++) {
|
|
2325
|
+
units.push(unitOf(frags, at, at + gs[i].length));
|
|
2326
|
+
at += gs[i].length;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
line.units = units;
|
|
2330
|
+
}
|
|
2331
|
+
if (x > width) width = x;
|
|
2332
|
+
lines.push(line);
|
|
2333
|
+
};
|
|
2334
|
+
for (let li = 0; li < input.lines.length; li++) {
|
|
2335
|
+
const runs = input.lines[li];
|
|
2336
|
+
let len = 0;
|
|
2337
|
+
for (let i = 0; i < runs.length; i++) len += runs[i].t.length;
|
|
2338
|
+
if (!max || !len || widthOf(slice(runs, 0, len)) <= max) {
|
|
2339
|
+
push2(runs, 0, len);
|
|
2340
|
+
} else {
|
|
2341
|
+
const text = runs.map((r) => r.t).join("");
|
|
2342
|
+
const toks = tokenRanges(text);
|
|
2343
|
+
let a = 0;
|
|
2344
|
+
let b = toks[0][1];
|
|
2345
|
+
for (let i = 1; i < toks.length; i++) {
|
|
2346
|
+
if (widthOf(slice(runs, a, toks[i][1])) <= max) b = toks[i][1];
|
|
2347
|
+
else {
|
|
2348
|
+
push2(runs, a, b);
|
|
2349
|
+
a = toks[i][0];
|
|
2350
|
+
b = toks[i][1];
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
push2(runs, a, b);
|
|
2354
|
+
}
|
|
2355
|
+
base += len + 1;
|
|
2756
2356
|
}
|
|
2757
|
-
|
|
2357
|
+
if (!lines.length) lines.push({ w: 0, frags: [], a: 0, n: 0 });
|
|
2358
|
+
return { lines, width };
|
|
2758
2359
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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 };
|
|
2767
2371
|
}
|
|
2768
|
-
|
|
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 };
|
|
2769
2377
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
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;
|
|
2776
2394
|
}
|
|
2777
|
-
function
|
|
2778
|
-
return
|
|
2395
|
+
function runsOf(text) {
|
|
2396
|
+
return typeof text === "string" ? text ? [{ text }] : [] : text;
|
|
2779
2397
|
}
|
|
2780
|
-
function
|
|
2781
|
-
|
|
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;
|
|
2782
2407
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
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;
|
|
2785
2423
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
const
|
|
2790
|
-
|
|
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;
|
|
2432
|
+
}
|
|
2433
|
+
return plain.length;
|
|
2791
2434
|
}
|
|
2792
|
-
function
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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;
|
|
2801
2446
|
}
|
|
2802
|
-
function
|
|
2803
|
-
|
|
2804
|
-
const
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
};
|
|
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
|
+
]);
|
|
2813
2470
|
}
|
|
2814
|
-
function
|
|
2815
|
-
const
|
|
2816
|
-
const
|
|
2817
|
-
const
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
const
|
|
2822
|
-
|
|
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
|
+
]);
|
|
2823
2494
|
}
|
|
2824
|
-
function
|
|
2825
|
-
|
|
2826
|
-
|
|
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);
|
|
2827
2499
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
return { width: height * aspect, height };
|
|
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;
|
|
2841
2512
|
}
|
|
2842
|
-
function
|
|
2843
|
-
|
|
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
|
+
};
|
|
2844
2531
|
}
|
|
2845
2532
|
|
|
2846
2533
|
// src/overlayText.ts
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
var HTML_LAYER_ASSET_HOST = "https://assets.vos.so/";
|
|
2861
|
-
function splitTopLevel(value) {
|
|
2862
|
-
const out = [];
|
|
2863
|
-
let depth = 0;
|
|
2864
|
-
let cur = "";
|
|
2865
|
-
for (const ch of value) {
|
|
2866
|
-
if (ch === "(") depth++;
|
|
2867
|
-
else if (ch === ")") depth--;
|
|
2868
|
-
if (ch === "," && depth === 0) {
|
|
2869
|
-
out.push(cur);
|
|
2870
|
-
cur = "";
|
|
2871
|
-
} else cur += ch;
|
|
2872
|
-
}
|
|
2873
|
-
if (cur.trim()) out.push(cur);
|
|
2874
|
-
return out;
|
|
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
|
+
};
|
|
2875
2547
|
}
|
|
2876
|
-
var
|
|
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
|
-
if (!name) continue;
|
|
2903
|
-
const lower = name.toLowerCase();
|
|
2904
|
-
if (GENERIC_FAMILIES.has(lower) || lower.startsWith("-apple-")) continue;
|
|
2905
|
-
if (seen.has(lower)) continue;
|
|
2906
|
-
seen.add(lower);
|
|
2907
|
-
out.push(name);
|
|
2908
|
-
}
|
|
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
|
|
2909
2574
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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)
|
|
2918
2585
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
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;
|
|
2600
|
+
}
|
|
2601
|
+
return {
|
|
2602
|
+
...base,
|
|
2603
|
+
stack,
|
|
2604
|
+
weight,
|
|
2605
|
+
...clip.size !== void 0 ? {
|
|
2606
|
+
size: Math.min(
|
|
2607
|
+
OVERLAY_SIZE_MAX,
|
|
2608
|
+
Math.max(OVERLAY_SIZE_MIN, clip.size)
|
|
2609
|
+
)
|
|
2610
|
+
} : {},
|
|
2611
|
+
...clip.color ? { color: clip.color } : {},
|
|
2612
|
+
...clip.shadow !== void 0 ? { shadow: Math.min(1, Math.max(0, clip.shadow)) } : {},
|
|
2613
|
+
fontStyle: clip.italic ? "italic" : "normal",
|
|
2614
|
+
align: clip.align ?? "center",
|
|
2615
|
+
letterSpacing: clip.letterSpacing ?? 0,
|
|
2616
|
+
lineHeight: clip.lineHeight ?? OVERLAY_LINE_HEIGHT,
|
|
2617
|
+
stroke: clip.stroke ?? null
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
function overlayFaceFor(clip) {
|
|
2621
|
+
const entry = findFontFamily2(
|
|
2622
|
+
clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
|
|
2623
|
+
);
|
|
2624
|
+
if (!entry) return null;
|
|
2625
|
+
const weight = resolveOverlayStyle(clip).weight;
|
|
2626
|
+
const inBase = OVERLAY_FONT_FACES.some(
|
|
2627
|
+
(f) => f.family === entry.family && f.weight === weight
|
|
2628
|
+
);
|
|
2629
|
+
if (inBase) return null;
|
|
2630
|
+
return {
|
|
2631
|
+
family: entry.family,
|
|
2632
|
+
weight,
|
|
2633
|
+
url: fontFaceUrl2(entry.slug, weight)
|
|
2634
|
+
};
|
|
2921
2635
|
}
|
|
2922
|
-
function
|
|
2923
|
-
const faces = [];
|
|
2924
|
-
const seen =
|
|
2925
|
-
const add = (
|
|
2926
|
-
|
|
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}`;
|
|
2927
2642
|
if (seen.has(key)) return;
|
|
2928
2643
|
seen.add(key);
|
|
2929
|
-
faces.push(
|
|
2644
|
+
faces.push(face);
|
|
2930
2645
|
};
|
|
2931
|
-
const
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
family: entry.family,
|
|
2938
|
-
url: fontFaceUrl(entry.slug, nearestFontWeight(entry, w)),
|
|
2939
|
-
weight: nearestFontWeight(entry, w),
|
|
2940
|
-
style: "normal"
|
|
2941
|
-
});
|
|
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
|
+
}
|
|
2942
2652
|
}
|
|
2943
|
-
for (const f of clip.fonts ?? [])
|
|
2944
|
-
add({
|
|
2945
|
-
family: f.family,
|
|
2946
|
-
url: f.url,
|
|
2947
|
-
weight: f.weight ?? 400,
|
|
2948
|
-
style: f.style ?? "normal"
|
|
2949
|
-
});
|
|
2950
2653
|
return faces;
|
|
2951
2654
|
}
|
|
2952
|
-
function
|
|
2953
|
-
const
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
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)
|
|
2958
2676
|
);
|
|
2959
2677
|
}
|
|
2960
|
-
function
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
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);
|
|
2683
|
+
}
|
|
2684
|
+
return (h >>> 0).toString(36);
|
|
2685
|
+
}
|
|
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;
|
|
2967
2699
|
}
|
|
2700
|
+
return at;
|
|
2968
2701
|
};
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
if (
|
|
2992
|
-
const
|
|
2993
|
-
|
|
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
|
|
2702
|
+
const l = [[]];
|
|
2703
|
+
for (const run of runs) {
|
|
2704
|
+
const f = fontOf(
|
|
2705
|
+
run.weight !== void 0 ? snapRunWeight(clip, run.weight) : style.weight,
|
|
2706
|
+
run.italic ?? italic
|
|
2707
|
+
);
|
|
2708
|
+
const set = {
|
|
2709
|
+
...f ? { f } : {},
|
|
2710
|
+
...run.color ? { c: run.color } : {},
|
|
2711
|
+
...run.underline ? { u: 1 } : {},
|
|
2712
|
+
...run.strike ? { s: 1 } : {},
|
|
2713
|
+
...run.highlight ? { h: run.highlight } : {}
|
|
2714
|
+
};
|
|
2715
|
+
run.text.split("\n").forEach((part, i) => {
|
|
2716
|
+
if (i > 0) l.push([]);
|
|
2717
|
+
if (part) l[l.length - 1].push({ t: part, ...set });
|
|
2718
|
+
});
|
|
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
|
|
2999
2727
|
);
|
|
2728
|
+
if (!inBase) {
|
|
2729
|
+
faces.push({
|
|
2730
|
+
f: entry.family,
|
|
2731
|
+
w: font.w,
|
|
2732
|
+
u: fontFaceUrl2(entry.slug, font.w)
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return {
|
|
2737
|
+
l,
|
|
2738
|
+
fs,
|
|
2739
|
+
...faces.length ? { faces } : {},
|
|
2740
|
+
h: hashOf(JSON.stringify([l, fs]))
|
|
3000
2741
|
};
|
|
3001
|
-
for (const decl of css.match(/box-shadow\s*:[^;}]+/gi) ?? [])
|
|
3002
|
-
for (const layer of splitTopLevel(decl.replace(/^[^:]*:/, "")))
|
|
3003
|
-
consider(layer);
|
|
3004
|
-
for (const fn of css.match(/drop-shadow\(([^)]*)\)/gi) ?? [])
|
|
3005
|
-
consider(fn.replace(/^drop-shadow\(/i, "").replace(/\)$/, ""));
|
|
3006
|
-
return worst ? Math.ceil(worst * 1.15) : 0;
|
|
3007
|
-
}
|
|
3008
|
-
function htmlLayerBleed(clip) {
|
|
3009
|
-
return Math.max(0, Math.round(clip.bleed ?? htmlLayerBleedFor(clip.css)));
|
|
3010
2742
|
}
|
|
3011
|
-
function
|
|
3012
|
-
const
|
|
2743
|
+
function textRunsOf(clip) {
|
|
2744
|
+
const styled = styledTextOf(clip);
|
|
2745
|
+
if (styled) return styled;
|
|
2746
|
+
const style = resolveOverlayStyle(clip);
|
|
3013
2747
|
return {
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
2748
|
+
l: overlayLines(overlayPlainText(clip)).map((t) => t ? [{ t }] : []),
|
|
2749
|
+
fs: [
|
|
2750
|
+
{
|
|
2751
|
+
w: style.weight,
|
|
2752
|
+
...style.fontStyle === "italic" ? { i: 1 } : {}
|
|
2753
|
+
}
|
|
2754
|
+
]
|
|
3017
2755
|
};
|
|
3018
2756
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
2757
|
+
var FX_DIR = { forward: 0, reverse: 1, center: 2 };
|
|
2758
|
+
function resolveOverlayFx(clip, clipDuration) {
|
|
2759
|
+
const spec = overlayFxSpec(clip) ?? clip.fx ?? null;
|
|
2760
|
+
if (!spec) return null;
|
|
2761
|
+
const unit = spec.unit ?? "block";
|
|
2762
|
+
const lines = overlayLines(overlayPlainText(clip));
|
|
2763
|
+
const count = (per) => lines.reduce((sum, line) => sum + per(line), 0);
|
|
2764
|
+
const words = () => count((line) => tokenRanges(line).length);
|
|
2765
|
+
const n = unit === "block" ? 1 : unit === "line" ? lines.length : unit === "word" ? words() : count((line) => graphemesOf(line).length);
|
|
2766
|
+
const upperN = unit === "line" && clip.maxWidth ? words() : n;
|
|
2767
|
+
const typewriter = spec.fx === "typewriter";
|
|
2768
|
+
const dur = typewriter ? 1e-3 : Math.min(2, Math.max(0.05, spec.duration ?? OVERLAY_TRANSITION_DUR));
|
|
2769
|
+
const defaultStagger = typewriter ? 0.05 : unit === "block" ? 0 : 0.06;
|
|
2770
|
+
let st = Math.min(2, Math.max(0, spec.stagger ?? defaultStagger));
|
|
2771
|
+
if (upperN > 1) {
|
|
2772
|
+
const maxTotal = Math.max(dur, clipDuration * 0.9);
|
|
2773
|
+
st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
|
|
2774
|
+
}
|
|
2775
|
+
const round14 = (v) => Math.round(v * 1e4) / 1e4;
|
|
2776
|
+
return {
|
|
2777
|
+
k: spec.fx,
|
|
2778
|
+
u: unit,
|
|
2779
|
+
d: FX_DIR[spec.direction ?? "forward"] ?? 0,
|
|
2780
|
+
st: round14(st),
|
|
2781
|
+
dur: round14(dur),
|
|
2782
|
+
tt: round14(st * (upperN - 1) + dur)
|
|
2783
|
+
};
|
|
3022
2784
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
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();
|
|
2785
|
+
function overlayFontString(style, scale, s) {
|
|
2786
|
+
const italic = style.fontStyle === "italic" ? "italic " : "";
|
|
2787
|
+
return `${italic}${style.weight} ${style.size * scale * s}px ${style.stack}`;
|
|
3039
2788
|
}
|
|
3040
|
-
function
|
|
3041
|
-
const
|
|
3042
|
-
|
|
2789
|
+
function overlayRect(clip, measure, frameW, frameH = 1080, mediaAspect) {
|
|
2790
|
+
const scale = clip.transform.scale || 1;
|
|
2791
|
+
const base = {
|
|
2792
|
+
cx: clip.transform.x * frameW,
|
|
2793
|
+
cy: clip.transform.y * frameH,
|
|
2794
|
+
rotation: clip.transform.rotation || 0
|
|
2795
|
+
};
|
|
2796
|
+
if (clip.kind === "html") {
|
|
2797
|
+
const pb = htmlLayerPictureBox(clip);
|
|
2798
|
+
const w2 = htmlLayerWidth(clip) * frameW * scale;
|
|
2799
|
+
return { ...base, w: w2, h: w2 * (pb.height / pb.width) };
|
|
2800
|
+
}
|
|
2801
|
+
if (clip.kind !== "text") {
|
|
2802
|
+
const w2 = (clip.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH) * frameW * scale;
|
|
2803
|
+
return { ...base, w: w2, h: w2 / (mediaAspect || 16 / 9) };
|
|
2804
|
+
}
|
|
2805
|
+
const style = resolveOverlayStyle(clip);
|
|
2806
|
+
const ls = style.letterSpacing * scale;
|
|
2807
|
+
const runs = textRunsOf(clip);
|
|
2808
|
+
const fonts = runs.fs.map(
|
|
2809
|
+
(f) => overlayFontString(
|
|
2810
|
+
{ ...style, weight: f.w, fontStyle: f.i ? "italic" : "normal" },
|
|
2811
|
+
scale,
|
|
2812
|
+
1
|
|
2813
|
+
)
|
|
2814
|
+
);
|
|
2815
|
+
const laid = layoutText(
|
|
2816
|
+
{ lines: runs.l, maxWidth: clip.maxWidth ? clip.maxWidth * frameW : 0 },
|
|
2817
|
+
(t, f) => measure(t, fonts[f], ls)
|
|
2818
|
+
);
|
|
2819
|
+
const w = laid.width;
|
|
2820
|
+
const lineCount = laid.lines.length;
|
|
2821
|
+
const lineH = style.size * scale * style.lineHeight;
|
|
2822
|
+
const box = resolveOverlayBox(clip);
|
|
2823
|
+
const padX = box ? box.padX * scale : 0;
|
|
2824
|
+
const padY = box ? box.padY * scale : 0;
|
|
2825
|
+
return {
|
|
2826
|
+
...base,
|
|
2827
|
+
w: Math.max(w, style.size * scale * 0.6) + padX * 2,
|
|
2828
|
+
// empty text still selectable
|
|
2829
|
+
h: lineCount * lineH + padY * 2
|
|
2830
|
+
};
|
|
3043
2831
|
}
|
|
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
|
-
const
|
|
3072
|
-
const
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
if (!m[0].trimEnd().endsWith("/>") && !said) {
|
|
3082
|
-
said = true;
|
|
3083
|
-
push2(
|
|
3084
|
-
"unclosed-void",
|
|
3085
|
-
"problem",
|
|
3086
|
-
`<${tag}> must close itself as <${tag} /> inside a foreignObject.`
|
|
3087
|
-
);
|
|
3088
|
-
}
|
|
2832
|
+
function overlayHit(rect, px, py, padPx = 8) {
|
|
2833
|
+
let dx = px - rect.cx;
|
|
2834
|
+
let dy = py - rect.cy;
|
|
2835
|
+
if (rect.rotation) {
|
|
2836
|
+
const a = -rect.rotation * Math.PI / 180;
|
|
2837
|
+
const rx = dx * Math.cos(a) - dy * Math.sin(a);
|
|
2838
|
+
const ry = dx * Math.sin(a) + dy * Math.cos(a);
|
|
2839
|
+
dx = rx;
|
|
2840
|
+
dy = ry;
|
|
2841
|
+
}
|
|
2842
|
+
return Math.abs(dx) <= rect.w / 2 + padPx && Math.abs(dy) <= rect.h / 2 + padPx;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
// src/richText/migrateEmphasis.ts
|
|
2846
|
+
function parseLine(line) {
|
|
2847
|
+
const chars = [];
|
|
2848
|
+
const marker = [];
|
|
2849
|
+
for (let i = 0; i < line.length; i++) {
|
|
2850
|
+
if (line[i] === "\\" && line[i + 1] === "*") {
|
|
2851
|
+
chars.push("*");
|
|
2852
|
+
marker.push(false);
|
|
2853
|
+
i++;
|
|
2854
|
+
} else {
|
|
2855
|
+
chars.push(line[i]);
|
|
2856
|
+
marker.push(line[i] === "*");
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
const keep = chars.map(() => true);
|
|
2860
|
+
const bold = [];
|
|
2861
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2862
|
+
if (!marker[i]) continue;
|
|
2863
|
+
let j = i + 1;
|
|
2864
|
+
while (j < chars.length && !marker[j]) j++;
|
|
2865
|
+
if (j >= chars.length) break;
|
|
2866
|
+
if (!chars.slice(i + 1, j).join("").trim()) {
|
|
2867
|
+
i = j - 1;
|
|
2868
|
+
continue;
|
|
3089
2869
|
}
|
|
2870
|
+
keep[i] = false;
|
|
2871
|
+
keep[j] = false;
|
|
2872
|
+
for (let k = i + 1; k < j; k++) bold[k] = true;
|
|
2873
|
+
i = j;
|
|
3090
2874
|
}
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
const
|
|
3105
|
-
if (
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
)
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
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.`
|
|
2875
|
+
const out = [];
|
|
2876
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2877
|
+
if (keep[i]) out.push({ c: chars[i], b: !!bold[i] && !/\s/.test(chars[i]) });
|
|
2878
|
+
}
|
|
2879
|
+
return out;
|
|
2880
|
+
}
|
|
2881
|
+
function marksEmphasis(text) {
|
|
2882
|
+
return text.split("\n").some((line) => parseLine(line).some((x) => x.b));
|
|
2883
|
+
}
|
|
2884
|
+
function migrateTextClip(clip) {
|
|
2885
|
+
const legacy = clip;
|
|
2886
|
+
if (!("emphasis" in legacy)) return clip;
|
|
2887
|
+
const { emphasis, ...rest } = legacy;
|
|
2888
|
+
const next = rest;
|
|
2889
|
+
if (!emphasis || typeof legacy.text !== "string") return next;
|
|
2890
|
+
const weight = emphasis.weight !== void 0 ? snapRunWeight(next, emphasis.weight) : boldWeightFor(next);
|
|
2891
|
+
const runs = [];
|
|
2892
|
+
legacy.text.split("\n").forEach((line, li) => {
|
|
2893
|
+
if (li > 0) runs.push({ text: "\n" });
|
|
2894
|
+
for (const { c, b } of parseLine(line)) {
|
|
2895
|
+
runs.push(
|
|
2896
|
+
b ? {
|
|
2897
|
+
text: c,
|
|
2898
|
+
weight,
|
|
2899
|
+
...emphasis.color ? { color: emphasis.color } : {}
|
|
2900
|
+
} : { text: c }
|
|
3123
2901
|
);
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
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;
|
|
2902
|
+
}
|
|
2903
|
+
});
|
|
2904
|
+
return { ...next, text: normalizeRuns(runs) };
|
|
3169
2905
|
}
|
|
3170
|
-
function
|
|
3171
|
-
const
|
|
3172
|
-
const
|
|
3173
|
-
if (!
|
|
3174
|
-
|
|
3175
|
-
|
|
2906
|
+
function migrateText(doc) {
|
|
2907
|
+
const overlays = Array.isArray(doc.overlays) ? doc.overlays : [];
|
|
2908
|
+
const legacy = overlays.some((o) => o.kind === "text" && "emphasis" in o);
|
|
2909
|
+
if (!legacy) return doc;
|
|
2910
|
+
return {
|
|
2911
|
+
...doc,
|
|
2912
|
+
overlays: overlays.map((o) => o.kind === "text" ? migrateTextClip(o) : o)
|
|
2913
|
+
};
|
|
3176
2914
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
2915
|
+
|
|
2916
|
+
// src/zoomStyle.ts
|
|
2917
|
+
var RETIRED_ZOOM_STYLES = {
|
|
2918
|
+
keynote: { style: "glide", tilt: "medium" },
|
|
2919
|
+
drift: { style: "cinema", tilt: "subtle" }
|
|
2920
|
+
};
|
|
2921
|
+
function liveZoomStyleName(name) {
|
|
2922
|
+
if (!name) return null;
|
|
2923
|
+
if (name in ZOOM_STYLES) return name;
|
|
2924
|
+
return RETIRED_ZOOM_STYLES[name]?.style ?? null;
|
|
3182
2925
|
}
|
|
3183
|
-
var
|
|
3184
|
-
var
|
|
3185
|
-
function
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
2926
|
+
var HOLD_DRIFT_MAX = 0.15;
|
|
2927
|
+
var PUMP_REST = 1;
|
|
2928
|
+
function pumpFreeChainGap(p) {
|
|
2929
|
+
return p.rampOut + (p.rampIn - p.rampInOverlap) + PUMP_REST;
|
|
2930
|
+
}
|
|
2931
|
+
var glide = {
|
|
2932
|
+
autoZoom: true,
|
|
2933
|
+
clusterGap: 3,
|
|
2934
|
+
minClusterClicks: 1,
|
|
2935
|
+
targetFill: 0.42,
|
|
2936
|
+
minLevel: 1.3,
|
|
2937
|
+
maxLevel: 1.8,
|
|
2938
|
+
dragLevel: 1.5,
|
|
2939
|
+
lead: 0.35,
|
|
2940
|
+
hold: 1.2,
|
|
2941
|
+
followByDefault: true,
|
|
2942
|
+
typingZoom: true,
|
|
2943
|
+
typingGap: 2.5,
|
|
2944
|
+
typingHold: 1.1,
|
|
2945
|
+
typingMinLevel: 1.4,
|
|
2946
|
+
rampIn: 0.6,
|
|
2947
|
+
rampInOverlap: 0.2,
|
|
2948
|
+
rampOut: 0.5,
|
|
2949
|
+
chainGap: 3,
|
|
2950
|
+
pan: 0.55,
|
|
2951
|
+
// The reference's corner trace at 60 fps: a quarter of the move by a
|
|
2952
|
+
// fifth of the time, two thirds by half, nine tenths by three
|
|
2953
|
+
// quarters, over ~0.6 s. Fitted against those three points,
|
|
2954
|
+
// (0.36, 0, 0.4, 1) is the closest cubic (15 / 64 / 93 %; half the
|
|
2955
|
+
// move at ~245 ms and 90 % at ~425 ms at 0.6 s): a gentle start and a
|
|
2956
|
+
// long settle. CSS ease (0.25, 0.1, 0.25, 1) reached the midpoint
|
|
2957
|
+
// 70 ms earlier and read a touch fast; (0.16, 1, 0.3, 1) put half the
|
|
2958
|
+
// move in the first sixty milliseconds and read as a twitch.
|
|
2959
|
+
ease: "css-bezier(0.36, 0, 0.4, 1)",
|
|
2960
|
+
panEase: "css-bezier(0.36, 0, 0.4, 1)",
|
|
2961
|
+
followSafeRatio: 0.45,
|
|
2962
|
+
followRecenter: 0.55,
|
|
2963
|
+
followLookahead: 0.4,
|
|
2964
|
+
tilt: {
|
|
2965
|
+
intensity: "off",
|
|
2966
|
+
rampIn: 0.6,
|
|
2967
|
+
rampOut: 0.5,
|
|
2968
|
+
chainGap: 3,
|
|
2969
|
+
pan: 0.55
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
var ZOOM_STYLES = {
|
|
2973
|
+
glide,
|
|
2974
|
+
// Screen Studio "Focused" / Recordly — a block per click, framed and
|
|
2975
|
+
// released, no travel. The spring-settled family ease (near-linear onset,
|
|
2976
|
+
// long settle) at the default's tempo: 0.8 s in, 0.65 s out, and a beat
|
|
2977
|
+
// of 1.5 s after the click so the block is read before it goes. At its
|
|
2978
|
+
// 2026-07 timing (1.4 s in, 1.0 s out around a 1.35 s span) a lone click
|
|
2979
|
+
// spent 74% of its beat moving and every dwell or sped-up click was
|
|
2980
|
+
// shorter than the ramp, which the lowering compressed into a cut.
|
|
2981
|
+
focus: {
|
|
2982
|
+
autoZoom: true,
|
|
2983
|
+
clusterGap: 1.5,
|
|
2984
|
+
minClusterClicks: 1,
|
|
2985
|
+
targetFill: 0.5,
|
|
2986
|
+
minLevel: 1.4,
|
|
2987
|
+
maxLevel: 2,
|
|
2988
|
+
dragLevel: 1.6,
|
|
2989
|
+
lead: 0.35,
|
|
2990
|
+
hold: 1.5,
|
|
2991
|
+
followByDefault: false,
|
|
2992
|
+
typingZoom: true,
|
|
2993
|
+
typingGap: 2,
|
|
2994
|
+
typingHold: 1.3,
|
|
2995
|
+
typingMinLevel: 1.5,
|
|
2996
|
+
rampIn: 0.8,
|
|
2997
|
+
rampInOverlap: 0.25,
|
|
2998
|
+
rampOut: 0.65,
|
|
2999
|
+
chainGap: 2.4,
|
|
3000
|
+
pan: 0.7,
|
|
3001
|
+
ease: "css-bezier(0.28, 0.03, 0.09, 1)",
|
|
3002
|
+
panEase: "css-bezier(0.26, 0.08, 0.2, 1)",
|
|
3003
|
+
followSafeRatio: 0.5,
|
|
3004
|
+
followRecenter: 0.55,
|
|
3005
|
+
followLookahead: 0,
|
|
3006
|
+
tilt: {
|
|
3007
|
+
intensity: "off",
|
|
3008
|
+
rampIn: 0.8,
|
|
3009
|
+
rampOut: 0.65,
|
|
3010
|
+
chainGap: 2.4,
|
|
3011
|
+
pan: 0.7
|
|
3012
|
+
}
|
|
3013
|
+
},
|
|
3014
|
+
// Screen Studio "Smooth" — the long-HOLD style: 1.2 s in, 1.0 s out,
|
|
3015
|
+
// two-second holds, chains across 3.5 s so a slow clip travels rather
|
|
3016
|
+
// than pumps, and the default's level range, because a watched clip
|
|
3017
|
+
// wants the frame, not the magnifier. Its ramps still exceed a dwell or
|
|
3018
|
+
// a sped-up click; the lowering fits them to the room.
|
|
3019
|
+
cinema: {
|
|
3020
|
+
autoZoom: true,
|
|
3021
|
+
clusterGap: 2.4,
|
|
3022
|
+
minClusterClicks: 1,
|
|
3023
|
+
targetFill: 0.42,
|
|
3024
|
+
minLevel: 1.3,
|
|
3025
|
+
maxLevel: 1.8,
|
|
3026
|
+
dragLevel: 1.5,
|
|
3027
|
+
lead: 0.6,
|
|
3028
|
+
hold: 2,
|
|
3029
|
+
followByDefault: true,
|
|
3030
|
+
typingZoom: true,
|
|
3031
|
+
typingGap: 3,
|
|
3032
|
+
typingHold: 1.8,
|
|
3033
|
+
typingMinLevel: 1.4,
|
|
3034
|
+
rampIn: 1.2,
|
|
3035
|
+
rampInOverlap: 0.4,
|
|
3036
|
+
rampOut: 1,
|
|
3037
|
+
chainGap: 3.5,
|
|
3038
|
+
pan: 1.1,
|
|
3039
|
+
ease: "css-bezier(0.33, 0, 0.15, 1)",
|
|
3040
|
+
panEase: "css-bezier(0.33, 0, 0.22, 1)",
|
|
3041
|
+
followSafeRatio: 0.6,
|
|
3042
|
+
followRecenter: 0.9,
|
|
3043
|
+
followLookahead: 0.5,
|
|
3044
|
+
tilt: {
|
|
3045
|
+
intensity: "off",
|
|
3046
|
+
rampIn: 1.2,
|
|
3047
|
+
rampOut: 1,
|
|
3048
|
+
chainGap: 3.5,
|
|
3049
|
+
pan: 1.1
|
|
3202
3050
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3051
|
+
},
|
|
3052
|
+
// The studio's original pace with the instant-velocity onset defect fixed:
|
|
3053
|
+
// 0.4 s cycles on the onset-capped ease, the 2.5× ceiling of the 2026-07
|
|
3054
|
+
// comparison's defect two brought to 2.2, a chain gap past its pump band.
|
|
3055
|
+
snappy: {
|
|
3056
|
+
autoZoom: true,
|
|
3057
|
+
clusterGap: 1.2,
|
|
3058
|
+
minClusterClicks: 1,
|
|
3059
|
+
targetFill: 0.5,
|
|
3060
|
+
minLevel: 1.4,
|
|
3061
|
+
maxLevel: 2.2,
|
|
3062
|
+
dragLevel: 1.6,
|
|
3063
|
+
lead: 0.25,
|
|
3064
|
+
hold: 1.2,
|
|
3065
|
+
followByDefault: false,
|
|
3066
|
+
typingZoom: true,
|
|
3067
|
+
typingGap: 1.6,
|
|
3068
|
+
typingHold: 0.9,
|
|
3069
|
+
typingMinLevel: 1.5,
|
|
3070
|
+
rampIn: 0.4,
|
|
3071
|
+
rampInOverlap: 0.15,
|
|
3072
|
+
rampOut: 0.4,
|
|
3073
|
+
chainGap: 2,
|
|
3074
|
+
pan: 0.5,
|
|
3075
|
+
ease: "css-bezier(0.3, 0.55, 0.2, 1)",
|
|
3076
|
+
panEase: "css-bezier(0.25, 0.1, 0.25, 1)",
|
|
3077
|
+
followSafeRatio: 0.5,
|
|
3078
|
+
followRecenter: 0.45,
|
|
3079
|
+
followLookahead: 0,
|
|
3080
|
+
tilt: {
|
|
3081
|
+
intensity: "off",
|
|
3082
|
+
rampIn: 0.4,
|
|
3083
|
+
rampOut: 0.4,
|
|
3084
|
+
chainGap: 2,
|
|
3085
|
+
pan: 0.5
|
|
3086
|
+
}
|
|
3087
|
+
},
|
|
3088
|
+
// Screen Studio's "instant zoom" — a cut-in (eight frames at 60 fps),
|
|
3089
|
+
// tutorial tempo. Untouched in motion (the one style that never pumped
|
|
3090
|
+
// and never left a ramp unsettled); the ceiling comes down with the
|
|
3091
|
+
// others and the hold gains the beat a cut-in needs to be read.
|
|
3092
|
+
cut: {
|
|
3093
|
+
autoZoom: true,
|
|
3094
|
+
clusterGap: 1.2,
|
|
3095
|
+
minClusterClicks: 1,
|
|
3096
|
+
targetFill: 0.5,
|
|
3097
|
+
minLevel: 1.4,
|
|
3098
|
+
maxLevel: 2,
|
|
3099
|
+
dragLevel: 1.6,
|
|
3100
|
+
lead: 0.2,
|
|
3101
|
+
hold: 1.2,
|
|
3102
|
+
followByDefault: false,
|
|
3103
|
+
typingZoom: true,
|
|
3104
|
+
typingGap: 1.2,
|
|
3105
|
+
typingHold: 0.6,
|
|
3106
|
+
typingMinLevel: 1.5,
|
|
3107
|
+
rampIn: 0.14,
|
|
3108
|
+
rampInOverlap: 0.07,
|
|
3109
|
+
rampOut: 0.14,
|
|
3110
|
+
chainGap: 1.3,
|
|
3111
|
+
pan: 0.35,
|
|
3112
|
+
ease: "css-bezier(0.2, 0, 0.4, 1)",
|
|
3113
|
+
panEase: "css-bezier(0.2, 0, 0.4, 1)",
|
|
3114
|
+
followSafeRatio: 0.5,
|
|
3115
|
+
followRecenter: 0.35,
|
|
3116
|
+
followLookahead: 0,
|
|
3117
|
+
tilt: {
|
|
3118
|
+
intensity: "off",
|
|
3119
|
+
rampIn: 0.3,
|
|
3120
|
+
rampOut: 0.3,
|
|
3121
|
+
chainGap: 1.3,
|
|
3122
|
+
pan: 0.35
|
|
3123
|
+
}
|
|
3124
|
+
},
|
|
3125
|
+
// Auto-zoom off (every competitor ships this switch). Camera params still
|
|
3126
|
+
// apply to MANUAL spans — they move the way the default camera moves
|
|
3127
|
+
// TODAY (spelled as a spread, never a copy that fossilises).
|
|
3128
|
+
none: { ...glide, autoZoom: false }
|
|
3129
|
+
};
|
|
3130
|
+
var DEFAULT_ZOOM_STYLE = "glide";
|
|
3131
|
+
function resolveZoomStyle(name, overrides) {
|
|
3132
|
+
const live = liveZoomStyleName(name) ?? DEFAULT_ZOOM_STYLE;
|
|
3133
|
+
const params = { ...ZOOM_STYLES[live], ...overrides };
|
|
3134
|
+
const floor = pumpFreeChainGap(params);
|
|
3135
|
+
if (params.chainGap < floor) params.chainGap = floor;
|
|
3136
|
+
return params;
|
|
3208
3137
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3138
|
+
var ZOOM_STYLE_OPTIONS = [
|
|
3139
|
+
{
|
|
3140
|
+
name: "glide",
|
|
3141
|
+
label: "Glide",
|
|
3142
|
+
hint: "One steady zoom that travels between clicks"
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
name: "focus",
|
|
3146
|
+
label: "Focus",
|
|
3147
|
+
hint: "A zoom per click, framed and released"
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
name: "cinema",
|
|
3151
|
+
label: "Cinema",
|
|
3152
|
+
hint: "Slow moves and long holds, for content that is watched"
|
|
3153
|
+
},
|
|
3154
|
+
{ name: "snappy", label: "Snappy", hint: "Quick, energetic zoom cycles" },
|
|
3155
|
+
{ name: "cut", label: "Cut", hint: "Instant zooms, no glide" },
|
|
3156
|
+
{
|
|
3157
|
+
name: "none",
|
|
3158
|
+
label: "None",
|
|
3159
|
+
hint: "No automatic zooms, add your own on the timeline"
|
|
3214
3160
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3161
|
+
];
|
|
3162
|
+
|
|
3163
|
+
// src/docVersion.ts
|
|
3164
|
+
var DOC_SCHEMA_VERSION = 6;
|
|
3165
|
+
function migrateHostedDoc(raw) {
|
|
3166
|
+
const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
|
|
3167
|
+
const read = migrateText(raw);
|
|
3168
|
+
if (version >= DOC_SCHEMA_VERSION) return read;
|
|
3169
|
+
let doc = read;
|
|
3170
|
+
if (version < 4 && doc.source && typeof doc.source === "object") {
|
|
3171
|
+
doc = migrateMotion(doc);
|
|
3172
|
+
}
|
|
3173
|
+
if (version < 5) doc = migrateZoomStyle(doc);
|
|
3174
|
+
return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
|
|
3222
3175
|
}
|
|
3223
|
-
function
|
|
3224
|
-
const
|
|
3225
|
-
|
|
3226
|
-
const
|
|
3176
|
+
function migrateZoomStyle(doc) {
|
|
3177
|
+
const name = doc.zoomStyle;
|
|
3178
|
+
if (typeof name !== "string") return doc;
|
|
3179
|
+
const retired = RETIRED_ZOOM_STYLES[name];
|
|
3180
|
+
if (!retired) return doc;
|
|
3227
3181
|
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
|
-
}
|
|
3182
|
+
...doc,
|
|
3183
|
+
zoomStyle: retired.style,
|
|
3184
|
+
tiltStyle: doc.tiltStyle ?? retired.tilt
|
|
3239
3185
|
};
|
|
3240
3186
|
}
|
|
3241
3187
|
|
|
3242
|
-
// src/
|
|
3243
|
-
var
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3188
|
+
// src/doc/studioDoc.ts
|
|
3189
|
+
var anchorKindOf = (doc) => "source" in doc ? "recording" : "program";
|
|
3190
|
+
var isRecordingDoc = (doc) => "source" in doc;
|
|
3191
|
+
var isProgramDoc = (doc) => !("source" in doc);
|
|
3192
|
+
function programDuration(doc) {
|
|
3193
|
+
const own = doc.program.duration;
|
|
3194
|
+
if (typeof own === "number" && own > 0) return own;
|
|
3195
|
+
const cfg = doc.program.config.duration;
|
|
3196
|
+
return typeof cfg === "number" && cfg > 0 ? cfg : 0;
|
|
3197
|
+
}
|
|
3198
|
+
function anchorSourceDuration(doc) {
|
|
3199
|
+
return isRecordingDoc(doc) ? doc.source.meta.durationMs / 1e3 : programDuration(doc);
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// src/planner/smoothing.ts
|
|
3203
|
+
var SNAP_BEFORE = 0.12;
|
|
3204
|
+
var SNAP_AFTER = 0.18;
|
|
3205
|
+
var SNAP_GAIN = 0.5;
|
|
3206
|
+
function sampleAt(points, t) {
|
|
3207
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
3208
|
+
if (t <= points[0].t) return { x: points[0].x, y: points[0].y };
|
|
3209
|
+
const last = points[points.length - 1];
|
|
3210
|
+
if (t >= last.t) return { x: last.x, y: last.y };
|
|
3211
|
+
for (let i = 1; i < points.length; i++) {
|
|
3212
|
+
if (points[i].t >= t) {
|
|
3213
|
+
const a = points[i - 1];
|
|
3214
|
+
const b = points[i];
|
|
3215
|
+
const f = (t - a.t) / (b.t - a.t || 1);
|
|
3216
|
+
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
3217
|
+
}
|
|
3250
3218
|
}
|
|
3251
|
-
return
|
|
3219
|
+
return { x: last.x, y: last.y };
|
|
3252
3220
|
}
|
|
3253
|
-
function
|
|
3221
|
+
function smoothCursor(track, options = {}) {
|
|
3222
|
+
const factor = clamp2(options.factor ?? 0.15, 0.01, 1);
|
|
3223
|
+
const fps = options.fps ?? 60;
|
|
3224
|
+
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 }));
|
|
3225
|
+
if (raw.length === 0) return [];
|
|
3226
|
+
const clicks = options.clickSnap ? track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y })) : [];
|
|
3227
|
+
const start = raw[0].t;
|
|
3228
|
+
const end = raw[raw.length - 1].t;
|
|
3229
|
+
const step = 1 / fps;
|
|
3230
|
+
const out = [];
|
|
3231
|
+
let cx = raw[0].x;
|
|
3232
|
+
let cy = raw[0].y;
|
|
3233
|
+
let ci = 0;
|
|
3234
|
+
for (let t = start; t <= end + 1e-9; t += step) {
|
|
3235
|
+
const target = sampleAt(raw, t);
|
|
3236
|
+
cx += (target.x - cx) * factor;
|
|
3237
|
+
cy += (target.y - cy) * factor;
|
|
3238
|
+
if (clicks.length) {
|
|
3239
|
+
while (ci < clicks.length - 1 && t > clicks[ci].t + SNAP_AFTER) ci++;
|
|
3240
|
+
const ck = clicks[ci];
|
|
3241
|
+
const u = clamp2(
|
|
3242
|
+
(t - (ck.t - SNAP_BEFORE)) / SNAP_BEFORE,
|
|
3243
|
+
0,
|
|
3244
|
+
t <= ck.t + SNAP_AFTER ? 1 : 0
|
|
3245
|
+
);
|
|
3246
|
+
const w = u * u * (3 - 2 * u) * SNAP_GAIN;
|
|
3247
|
+
if (w > 0) {
|
|
3248
|
+
cx += (ck.x - cx) * w;
|
|
3249
|
+
cy += (ck.y - cy) * w;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
out.push({ t, x: cx, y: cy });
|
|
3253
|
+
}
|
|
3254
|
+
return out;
|
|
3255
|
+
}
|
|
3256
|
+
function clamp2(v, lo, hi) {
|
|
3257
|
+
return Math.max(lo, Math.min(hi, v));
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// src/planner/autoZoom.ts
|
|
3261
|
+
var DWELL_MOVE_FRAC = 0.02;
|
|
3262
|
+
var DWELL_MIN = 0.45;
|
|
3263
|
+
var DWELL_MAX = 2.6;
|
|
3264
|
+
var DWELL_SPACING = 1.8;
|
|
3265
|
+
var CLUSTER_FILL = 0.85;
|
|
3266
|
+
var DRAG_FIT_LEVEL = 1.15;
|
|
3267
|
+
var PRESS_ECHO_MS = 4;
|
|
3268
|
+
var PRESS_ECHO_PX = 2;
|
|
3269
|
+
var DRAG_MIN_TRAVEL_FRAC = 0.015;
|
|
3270
|
+
var DRAG_MIN_S = 0.2;
|
|
3271
|
+
function dragsFromTrack(track, width, height) {
|
|
3272
|
+
if (!(width > 0) || !(height > 0)) return [];
|
|
3273
|
+
const downs = dedupePresses(track.filter((e) => e.type === "down"));
|
|
3254
3274
|
const out = [];
|
|
3255
|
-
const
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3275
|
+
for (const d of downs) {
|
|
3276
|
+
const up = track.find((e) => e.type === "up" && e.t > d.t);
|
|
3277
|
+
if (!up) continue;
|
|
3278
|
+
const t0 = d.t / 1e3;
|
|
3279
|
+
const t1 = up.t / 1e3;
|
|
3280
|
+
if (t1 - t0 < DRAG_MIN_S) continue;
|
|
3281
|
+
let travel = 0;
|
|
3282
|
+
for (const e of track) {
|
|
3283
|
+
if (e.t < d.t) continue;
|
|
3284
|
+
if (e.t > up.t) break;
|
|
3285
|
+
if (e.type !== "move" && e.type !== "up") continue;
|
|
3286
|
+
const dist = Math.hypot(e.x - d.x, e.y - d.y) / width;
|
|
3287
|
+
if (dist > travel) travel = dist;
|
|
3288
|
+
}
|
|
3289
|
+
if (travel < DRAG_MIN_TRAVEL_FRAC) continue;
|
|
3290
|
+
out.push({
|
|
3291
|
+
t0,
|
|
3292
|
+
t1,
|
|
3293
|
+
nx: clamp012(d.x / width),
|
|
3294
|
+
ny: clamp012(d.y / height),
|
|
3295
|
+
travel
|
|
3296
|
+
});
|
|
3259
3297
|
}
|
|
3260
|
-
return out
|
|
3298
|
+
return out;
|
|
3261
3299
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3300
|
+
var TYPING_MIN_PINGS = 2;
|
|
3301
|
+
var TYPING_MIN_DUR = 0.5;
|
|
3302
|
+
var TYPING_CLICK_ABSORB = 1.5;
|
|
3303
|
+
var TYPING_REFOCUS_FRAC = 0.08;
|
|
3304
|
+
function groupTrack(track, opts) {
|
|
3305
|
+
const { width, height, clusterGap, typingGap, typingZoom, targetFill } = opts;
|
|
3306
|
+
const minLevel = opts.minLevel ?? resolveZoomStyle().minLevel;
|
|
3307
|
+
const clicks = dedupePresses(
|
|
3308
|
+
track.filter((e) => e.type === "down")
|
|
3309
|
+
).map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
|
|
3310
|
+
const sessions = typingZoom ? typingSessions(track, width, height, typingGap) : [];
|
|
3311
|
+
const absorbed = /* @__PURE__ */ new Set();
|
|
3312
|
+
for (const s of sessions) {
|
|
3313
|
+
let best = null;
|
|
3314
|
+
for (const c of clicks) {
|
|
3315
|
+
if (absorbed.has(c)) continue;
|
|
3316
|
+
if (c.t >= s.first || s.first - c.t > TYPING_CLICK_ABSORB) continue;
|
|
3317
|
+
const [nx, ny] = fieldCenter(c, width, height);
|
|
3318
|
+
if (Math.hypot(nx - s.cx, ny - s.cy) > TYPING_REFOCUS_FRAC) continue;
|
|
3319
|
+
if (!best || c.t > best.t) best = c;
|
|
3271
3320
|
}
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
let at = 0;
|
|
3277
|
-
for (let i = 0; i < runs.length; i++) {
|
|
3278
|
-
const r = runs[i];
|
|
3279
|
-
const from = Math.max(a, at);
|
|
3280
|
-
const to = Math.min(b, at + r.t.length);
|
|
3281
|
-
if (to > from) {
|
|
3282
|
-
const t = r.t.slice(from - at, to - at);
|
|
3283
|
-
const last = out[out.length - 1];
|
|
3284
|
-
if (last && sameStyle(last, r)) last.t += t;
|
|
3285
|
-
else out.push({ ...r, t });
|
|
3286
|
-
}
|
|
3287
|
-
at += r.t.length;
|
|
3288
|
-
if (at >= b) break;
|
|
3321
|
+
if (best) {
|
|
3322
|
+
absorbed.add(best);
|
|
3323
|
+
s.start = best.t;
|
|
3324
|
+
s.events = [best, ...s.events];
|
|
3289
3325
|
}
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
const
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
const
|
|
3302
|
-
const
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
out.push({
|
|
3307
|
-
...fr,
|
|
3308
|
-
t: fr.t.slice(from - fr.o, to - fr.o),
|
|
3309
|
-
x: fr.x + before,
|
|
3310
|
-
w: adv(fr.t.slice(0, to - fr.o), f) - before,
|
|
3311
|
-
o: from
|
|
3312
|
-
});
|
|
3326
|
+
}
|
|
3327
|
+
const targets = [];
|
|
3328
|
+
const surfaces = [];
|
|
3329
|
+
for (const c of clicks) {
|
|
3330
|
+
if (absorbed.has(c)) continue;
|
|
3331
|
+
if (isSurfacePress(c, width, height, targetFill)) surfaces.push(c);
|
|
3332
|
+
else targets.push(c);
|
|
3333
|
+
}
|
|
3334
|
+
const chain = (list, spatial) => {
|
|
3335
|
+
const clusters = [];
|
|
3336
|
+
for (const c of list) {
|
|
3337
|
+
const last = clusters.at(-1);
|
|
3338
|
+
const prev = last?.at(-1);
|
|
3339
|
+
const joins = last && prev && c.t - prev.t <= clusterGap && (!spatial || unionFitLevel(unionRect([...last, c]), width, height) >= minLevel);
|
|
3340
|
+
if (joins) last.push(c);
|
|
3341
|
+
else clusters.push([c]);
|
|
3313
3342
|
}
|
|
3314
|
-
return
|
|
3343
|
+
return clusters;
|
|
3315
3344
|
};
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
return { x: parts[0].x, w: last.x + last.w - parts[0].x, parts };
|
|
3345
|
+
return {
|
|
3346
|
+
sessions,
|
|
3347
|
+
clusters: chain(targets, true),
|
|
3348
|
+
surfaces: chain(surfaces, false)
|
|
3321
3349
|
};
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
let x = 0;
|
|
3330
|
-
let o = 0;
|
|
3331
|
-
for (let i = 0; i < cut.length; i++) {
|
|
3332
|
-
const w = adv(cut[i].t, cut[i].f || 0);
|
|
3333
|
-
frags.push({ ...cut[i], x, w, o });
|
|
3334
|
-
x += w;
|
|
3335
|
-
o += cut[i].t.length;
|
|
3350
|
+
}
|
|
3351
|
+
function dedupePresses(downs) {
|
|
3352
|
+
const kept = [];
|
|
3353
|
+
for (const e of downs) {
|
|
3354
|
+
const prev = kept.at(-1);
|
|
3355
|
+
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) {
|
|
3356
|
+
continue;
|
|
3336
3357
|
}
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3358
|
+
kept.push(e);
|
|
3359
|
+
}
|
|
3360
|
+
return kept;
|
|
3361
|
+
}
|
|
3362
|
+
function unionRect(clicks) {
|
|
3363
|
+
let x0 = Infinity;
|
|
3364
|
+
let y0 = Infinity;
|
|
3365
|
+
let x1 = -Infinity;
|
|
3366
|
+
let y1 = -Infinity;
|
|
3367
|
+
for (const c of clicks) {
|
|
3368
|
+
const r = c.rect ?? { x: c.x, y: c.y, w: 0, h: 0 };
|
|
3369
|
+
x0 = Math.min(x0, r.x);
|
|
3370
|
+
y0 = Math.min(y0, r.y);
|
|
3371
|
+
x1 = Math.max(x1, r.x + r.w);
|
|
3372
|
+
y1 = Math.max(y1, r.y + r.h);
|
|
3373
|
+
}
|
|
3374
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
3375
|
+
}
|
|
3376
|
+
function unionFitLevel(rect, width, height) {
|
|
3377
|
+
const fx = rect.w > 0 ? width * CLUSTER_FILL / rect.w : Infinity;
|
|
3378
|
+
const fy = rect.h > 0 ? height * CLUSTER_FILL / rect.h : Infinity;
|
|
3379
|
+
return Math.min(fx, fy);
|
|
3380
|
+
}
|
|
3381
|
+
function fitLevel(rect, width, height, targetFill) {
|
|
3382
|
+
if (!rect || rect.w <= 0 || rect.h <= 0) return null;
|
|
3383
|
+
return Math.min(width * targetFill / rect.w, height * targetFill / rect.h);
|
|
3384
|
+
}
|
|
3385
|
+
function isSurfacePress(c, width, height, targetFill) {
|
|
3386
|
+
const fit = fitLevel(c.rect, width, height, targetFill);
|
|
3387
|
+
return fit !== null && fit < DRAG_FIT_LEVEL;
|
|
3388
|
+
}
|
|
3389
|
+
function planAutoZoom(track, options) {
|
|
3390
|
+
const style = resolveZoomStyle(options.style, options.params);
|
|
3391
|
+
if (!style.autoZoom) return [];
|
|
3392
|
+
const {
|
|
3393
|
+
width,
|
|
3394
|
+
height,
|
|
3395
|
+
targetFill = style.targetFill,
|
|
3396
|
+
minLevel = style.minLevel,
|
|
3397
|
+
maxLevel = style.maxLevel,
|
|
3398
|
+
clusterGap = style.clusterGap,
|
|
3399
|
+
lead = style.lead,
|
|
3400
|
+
hold = style.hold,
|
|
3401
|
+
minClusterClicks = style.minClusterClicks,
|
|
3402
|
+
dragLevel = style.dragLevel,
|
|
3403
|
+
followByDefault = style.followByDefault,
|
|
3404
|
+
typingZoom = style.typingZoom,
|
|
3405
|
+
typingGap = style.typingGap,
|
|
3406
|
+
typingHold = style.typingHold,
|
|
3407
|
+
typingMinLevel = style.typingMinLevel,
|
|
3408
|
+
duration
|
|
3409
|
+
} = options;
|
|
3410
|
+
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);
|
|
3411
|
+
const grouped = groupTrack(track, {
|
|
3412
|
+
width,
|
|
3413
|
+
height,
|
|
3414
|
+
clusterGap,
|
|
3415
|
+
typingGap,
|
|
3416
|
+
typingZoom,
|
|
3417
|
+
targetFill,
|
|
3418
|
+
minLevel
|
|
3419
|
+
});
|
|
3420
|
+
const { sessions } = grouped;
|
|
3421
|
+
const drags = dragsFromTrack(track, width, height);
|
|
3422
|
+
const dragPress = new Set(drags.map((d) => round2(d.t0)));
|
|
3423
|
+
const withoutDrags = (list) => list.map((cl) => cl.filter((c) => !dragPress.has(round2(c.t)))).filter((cl) => cl.length > 0);
|
|
3424
|
+
const clusters = withoutDrags(grouped.clusters);
|
|
3425
|
+
const surfaces = withoutDrags(grouped.surfaces);
|
|
3426
|
+
const dragSpans = drags.map((d, i) => ({
|
|
3427
|
+
id: `g${i}`,
|
|
3428
|
+
in: round2(Math.max(0, d.t0 - lead)),
|
|
3429
|
+
out: round2(d.t1 + hold),
|
|
3430
|
+
level: clampZoomLevel(Math.min(Math.max(dragLevel, 1.1), maxLevel)),
|
|
3431
|
+
cx: round2(d.nx),
|
|
3432
|
+
cy: round2(d.ny),
|
|
3433
|
+
focusMode: "auto",
|
|
3434
|
+
source: "auto"
|
|
3435
|
+
}));
|
|
3436
|
+
const eligible = clusters.filter((c) => c.length >= minClusterClicks);
|
|
3437
|
+
const clickSpans = [];
|
|
3438
|
+
const dragReserved = surfaces.map((cluster, i) => {
|
|
3439
|
+
const f = clusterFocus(cluster, width, height);
|
|
3440
|
+
return {
|
|
3441
|
+
id: `drag${i}`,
|
|
3442
|
+
in: Math.max(0, cluster[0].t - lead),
|
|
3443
|
+
out: cluster[cluster.length - 1].t + hold,
|
|
3444
|
+
level: 1,
|
|
3445
|
+
cx: f.cx,
|
|
3446
|
+
cy: f.cy
|
|
3447
|
+
};
|
|
3448
|
+
});
|
|
3449
|
+
for (const cluster of eligible) {
|
|
3450
|
+
const first = cluster[0];
|
|
3451
|
+
const last = cluster[cluster.length - 1];
|
|
3452
|
+
const f = focusFor(cluster, width, height, targetFill, minLevel, maxLevel);
|
|
3453
|
+
clickSpans.push({
|
|
3454
|
+
in: Math.max(0, first.t - lead),
|
|
3455
|
+
out: last.t + hold,
|
|
3456
|
+
cx: f.cx,
|
|
3457
|
+
cy: f.cy,
|
|
3458
|
+
level: f.level
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
clickSpans.sort((a, b) => a.in - b.in);
|
|
3462
|
+
for (let i = 0; i + 1 < clickSpans.length; i++) {
|
|
3463
|
+
if (clickSpans[i].out > clickSpans[i + 1].in)
|
|
3464
|
+
clickSpans[i].out = clickSpans[i + 1].in;
|
|
3465
|
+
}
|
|
3466
|
+
for (const z of clickSpans) if (z.out - z.in < 0.3) z.dead = true;
|
|
3467
|
+
const typingFloor = Math.min(Math.max(minLevel, typingMinLevel), maxLevel);
|
|
3468
|
+
let typing = sessions.map((s) => {
|
|
3469
|
+
const f = focusFor(
|
|
3470
|
+
s.events,
|
|
3471
|
+
width,
|
|
3472
|
+
height,
|
|
3473
|
+
targetFill,
|
|
3474
|
+
typingFloor,
|
|
3475
|
+
maxLevel
|
|
3476
|
+
);
|
|
3477
|
+
return {
|
|
3478
|
+
in: Math.max(0, s.start - lead),
|
|
3479
|
+
out: s.last + typingHold,
|
|
3480
|
+
cx: f.cx,
|
|
3481
|
+
cy: f.cy,
|
|
3482
|
+
level: f.level
|
|
3483
|
+
};
|
|
3484
|
+
});
|
|
3485
|
+
for (const t of typing) {
|
|
3486
|
+
for (const z of clickSpans) {
|
|
3487
|
+
if (z.dead || t.dead) continue;
|
|
3488
|
+
if (t.in >= z.out || t.out <= z.in) continue;
|
|
3489
|
+
if (Math.hypot(t.cx - z.cx, t.cy - z.cy) <= TYPING_REFOCUS_FRAC) {
|
|
3490
|
+
t.in = Math.min(t.in, z.in);
|
|
3491
|
+
t.out = Math.max(t.out, z.out);
|
|
3492
|
+
z.dead = true;
|
|
3493
|
+
} else if (z.in <= t.in && z.out >= t.out) {
|
|
3494
|
+
t.dead = true;
|
|
3495
|
+
} else if (z.in > t.in) {
|
|
3496
|
+
t.out = z.in;
|
|
3347
3497
|
} 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
|
-
}
|
|
3498
|
+
t.in = z.out;
|
|
3378
3499
|
}
|
|
3379
|
-
push2(runs, a, b);
|
|
3380
3500
|
}
|
|
3381
|
-
base += len + 1;
|
|
3382
3501
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3502
|
+
typing = typing.filter((t) => !t.dead).sort((a, b) => a.in - b.in);
|
|
3503
|
+
for (let i = 0; i + 1 < typing.length; i++) {
|
|
3504
|
+
if (typing[i].out > typing[i + 1].in) typing[i].out = typing[i + 1].in;
|
|
3505
|
+
}
|
|
3506
|
+
typing = typing.filter((t) => t.out - t.in >= 0.3);
|
|
3507
|
+
const zSpans = clickSpans.filter((s) => !s.dead).sort((a, b) => a.in - b.in).map((s, i) => ({
|
|
3508
|
+
id: `z${i}`,
|
|
3509
|
+
in: round2(s.in),
|
|
3510
|
+
out: round2(s.out),
|
|
3511
|
+
level: clampZoomLevel(s.level),
|
|
3512
|
+
cx: round2(s.cx),
|
|
3513
|
+
cy: round2(s.cy),
|
|
3514
|
+
// Follow styles ride the cursor through the span (entry + dead-zone
|
|
3515
|
+
// recenters baked at lowering); typing/dwell spans stay fixed-focus.
|
|
3516
|
+
...followByDefault ? { focusMode: "auto" } : {},
|
|
3517
|
+
source: "auto"
|
|
3518
|
+
}));
|
|
3519
|
+
const kSpans = typing.map((s, i) => ({
|
|
3520
|
+
id: `k${i}`,
|
|
3521
|
+
in: round2(s.in),
|
|
3522
|
+
out: round2(s.out),
|
|
3523
|
+
level: clampZoomLevel(s.level),
|
|
3524
|
+
cx: round2(s.cx),
|
|
3525
|
+
cy: round2(s.cy),
|
|
3526
|
+
// Never focusMode:'auto': the dot is parked (and fading) while typing —
|
|
3527
|
+
// the FIELD is the anchor, and a follow would be a no-op at best.
|
|
3528
|
+
source: "auto"
|
|
3529
|
+
}));
|
|
3530
|
+
const spans = within([...zSpans, ...kSpans, ...dragSpans]).sort(
|
|
3531
|
+
(a, b) => a.in - b.in
|
|
3532
|
+
);
|
|
3533
|
+
const dwells = dwellSpans(track, width, height, maxLevel, [
|
|
3534
|
+
...spans,
|
|
3535
|
+
...dragReserved
|
|
3536
|
+
]);
|
|
3537
|
+
return within([...spans, ...dwells]).sort((a, b) => a.in - b.in);
|
|
3385
3538
|
}
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3539
|
+
function typingSessions(track, width, height, typingGap) {
|
|
3540
|
+
const pings = track.filter((e) => e.type === "key").map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
|
|
3541
|
+
const all = [];
|
|
3542
|
+
let cur = null;
|
|
3543
|
+
for (const p of pings) {
|
|
3544
|
+
const [nx, ny] = fieldCenter(p, width, height);
|
|
3545
|
+
if (cur && p.t - cur.last <= typingGap && Math.hypot(nx - cur.cx, ny - cur.cy) <= TYPING_REFOCUS_FRAC) {
|
|
3546
|
+
cur.events.push(p);
|
|
3547
|
+
cur.last = p.t;
|
|
3548
|
+
} else {
|
|
3549
|
+
cur = { events: [p], first: p.t, last: p.t, start: p.t, cx: nx, cy: ny };
|
|
3550
|
+
all.push(cur);
|
|
3551
|
+
}
|
|
3397
3552
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
y + (kind === "u" ? UNDERLINE_OFFSET_EM : STRIKE_OFFSET_EM) * px
|
|
3553
|
+
return all.filter(
|
|
3554
|
+
(s) => s.events.length >= TYPING_MIN_PINGS && s.last - s.first >= TYPING_MIN_DUR
|
|
3401
3555
|
);
|
|
3402
|
-
return { x: x0, y: top, w: x1 - x0, h: t };
|
|
3403
3556
|
}
|
|
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
|
-
};
|
|
3557
|
+
function fieldCenter(c, width, height) {
|
|
3558
|
+
const px = c.rect ? c.rect.x + c.rect.w / 2 : c.x;
|
|
3559
|
+
const py = c.rect ? c.rect.y + c.rect.h / 2 : c.y;
|
|
3560
|
+
return [clamp012(px / width), clamp012(py / height)];
|
|
3419
3561
|
}
|
|
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
|
|
3562
|
+
function dwellSpans(track, width, height, maxLevel, reserved) {
|
|
3563
|
+
const moves = track.filter((e) => e.type === "move").map((e) => ({ t: e.t / 1e3, nx: e.x / width, ny: e.y / height }));
|
|
3564
|
+
if (moves.length < 2) return [];
|
|
3565
|
+
const trackEnd = track[track.length - 1].t / 1e3;
|
|
3566
|
+
const candidates = [];
|
|
3567
|
+
let start = 0;
|
|
3568
|
+
for (let i = 1; i <= moves.length; i++) {
|
|
3569
|
+
const breaks = i === moves.length || Math.hypot(moves[i].nx - moves[i - 1].nx, moves[i].ny - moves[i - 1].ny) > DWELL_MOVE_FRAC;
|
|
3570
|
+
if (!breaks) continue;
|
|
3571
|
+
const endT = i < moves.length ? moves[i].t : trackEnd;
|
|
3572
|
+
const dur = endT - moves[start].t;
|
|
3573
|
+
const opening = start === 0;
|
|
3574
|
+
if (!opening && dur >= DWELL_MIN && dur <= DWELL_MAX) {
|
|
3575
|
+
const run = moves.slice(start, i);
|
|
3576
|
+
candidates.push({
|
|
3577
|
+
center: (moves[start].t + endT) / 2,
|
|
3578
|
+
cx: run.reduce((s, p) => s + p.nx, 0) / run.length,
|
|
3579
|
+
cy: run.reduce((s, p) => s + p.ny, 0) / run.length,
|
|
3580
|
+
strength: dur
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
start = i;
|
|
3446
3584
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
{
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3585
|
+
const sorted = [...candidates].sort((a, b) => b.strength - a.strength);
|
|
3586
|
+
const sourceDuration = trackEnd;
|
|
3587
|
+
const len = Math.max(1, sourceDuration * 0.05);
|
|
3588
|
+
const taken = [...reserved];
|
|
3589
|
+
const accepted = [];
|
|
3590
|
+
const centers = [];
|
|
3591
|
+
for (const c of sorted) {
|
|
3592
|
+
if (centers.some((t) => Math.abs(t - c.center) < DWELL_SPACING)) continue;
|
|
3593
|
+
const spanIn = Math.max(
|
|
3594
|
+
0,
|
|
3595
|
+
Math.min(c.center - len / 2, sourceDuration - len)
|
|
3596
|
+
);
|
|
3597
|
+
const spanOut = Math.min(sourceDuration, spanIn + len);
|
|
3598
|
+
if (spanOut - spanIn < 0.3) continue;
|
|
3599
|
+
if (taken.some((z) => spanIn < z.out && spanOut > z.in)) continue;
|
|
3600
|
+
const span = {
|
|
3601
|
+
id: `d${accepted.length}`,
|
|
3602
|
+
in: round2(spanIn),
|
|
3603
|
+
out: round2(spanOut),
|
|
3604
|
+
// No element rect on moves → point zoom at the planner ceiling.
|
|
3605
|
+
level: clampZoomLevel(maxLevel),
|
|
3606
|
+
cx: round2(clamp012(c.cx)),
|
|
3607
|
+
cy: round2(clamp012(c.cy)),
|
|
3608
|
+
source: "auto"
|
|
3609
|
+
};
|
|
3610
|
+
accepted.push(span);
|
|
3611
|
+
taken.push(span);
|
|
3612
|
+
centers.push(c.center);
|
|
3613
|
+
}
|
|
3614
|
+
return accepted.sort((a, b) => a.in - b.in).map((z, i) => ({ ...z, id: `d${i}` }));
|
|
3615
|
+
}
|
|
3616
|
+
function focusFor(cluster, width, height, targetFill, minLevel, maxLevel) {
|
|
3617
|
+
let sx = 0;
|
|
3618
|
+
let sy = 0;
|
|
3619
|
+
let maxW = 0;
|
|
3620
|
+
let maxH = 0;
|
|
3621
|
+
for (const c of cluster) {
|
|
3622
|
+
if (c.rect) {
|
|
3623
|
+
sx += c.rect.x + c.rect.w / 2;
|
|
3624
|
+
sy += c.rect.y + c.rect.h / 2;
|
|
3625
|
+
maxW = Math.max(maxW, c.rect.w);
|
|
3626
|
+
maxH = Math.max(maxH, c.rect.h);
|
|
3627
|
+
} else {
|
|
3628
|
+
sx += c.x;
|
|
3629
|
+
sy += c.y;
|
|
3630
|
+
}
|
|
3457
3631
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
const
|
|
3461
|
-
const
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3632
|
+
const n = cluster.length;
|
|
3633
|
+
const cx = clamp012(sx / n / width);
|
|
3634
|
+
const cy = clamp012(sy / n / height);
|
|
3635
|
+
const own = fitLevel({ x: 0, y: 0, w: maxW, h: maxH }, width, height, targetFill) ?? maxLevel;
|
|
3636
|
+
const together = unionFitLevel(unionRect(cluster), width, height);
|
|
3637
|
+
return { cx, cy, level: clamp3(Math.min(own, together), minLevel, maxLevel) };
|
|
3638
|
+
}
|
|
3639
|
+
function clusterFocus(events, width, height) {
|
|
3640
|
+
let sx = 0;
|
|
3641
|
+
let sy = 0;
|
|
3642
|
+
let x0 = Infinity;
|
|
3643
|
+
let y0 = Infinity;
|
|
3644
|
+
let x1 = -Infinity;
|
|
3645
|
+
let y1 = -Infinity;
|
|
3646
|
+
let rects = 0;
|
|
3647
|
+
for (const c of events) {
|
|
3648
|
+
if (c.rect) {
|
|
3649
|
+
sx += c.rect.x + c.rect.w / 2;
|
|
3650
|
+
sy += c.rect.y + c.rect.h / 2;
|
|
3651
|
+
x0 = Math.min(x0, c.rect.x);
|
|
3652
|
+
y0 = Math.min(y0, c.rect.y);
|
|
3653
|
+
x1 = Math.max(x1, c.rect.x + c.rect.w);
|
|
3654
|
+
y1 = Math.max(y1, c.rect.y + c.rect.h);
|
|
3655
|
+
rects++;
|
|
3656
|
+
} else {
|
|
3657
|
+
sx += c.x;
|
|
3658
|
+
sy += c.y;
|
|
3659
|
+
}
|
|
3468
3660
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3661
|
+
const n = Math.max(1, events.length);
|
|
3662
|
+
const rect = rects > 0 ? {
|
|
3663
|
+
x: clamp012(x0 / width),
|
|
3664
|
+
y: clamp012(y0 / height),
|
|
3665
|
+
w: clamp012((x1 - x0) / width),
|
|
3666
|
+
h: clamp012((y1 - y0) / height)
|
|
3667
|
+
} : null;
|
|
3668
|
+
return { cx: clamp012(sx / n / width), cy: clamp012(sy / n / height), rect };
|
|
3669
|
+
}
|
|
3670
|
+
function clamp3(v, lo, hi) {
|
|
3671
|
+
return Math.max(lo, Math.min(hi, v));
|
|
3672
|
+
}
|
|
3673
|
+
function clamp012(v) {
|
|
3674
|
+
return clamp3(v, 0, 1);
|
|
3675
|
+
}
|
|
3676
|
+
function round2(v) {
|
|
3677
|
+
return Math.round(v * 1e3) / 1e3;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
// src/lower/lowerToComposition.ts
|
|
3681
|
+
import {
|
|
3682
|
+
EASINGS,
|
|
3683
|
+
lerpArray as lerpArray2,
|
|
3684
|
+
sample as sample2,
|
|
3685
|
+
segmentRate as segmentRate2,
|
|
3686
|
+
sortKeyframes as sortKeyframes2,
|
|
3687
|
+
sourceToTimeline,
|
|
3688
|
+
splitBySpeed as splitBySpeed3,
|
|
3689
|
+
totalDuration as totalDuration3
|
|
3690
|
+
} from "@vosjs/timeline";
|
|
3691
|
+
import { timelineRuntimeCode as timelineRuntimeCode2 } from "@vosjs/timeline/bundle";
|
|
3692
|
+
|
|
3693
|
+
// src/layout.ts
|
|
3694
|
+
function computeCardLayout(frame, video, W, H, zoomFocus) {
|
|
3695
|
+
const s = H / 1080;
|
|
3696
|
+
const pad = (frame.padding || 0) * s;
|
|
3697
|
+
const ins = frame.inset ?? {};
|
|
3698
|
+
const padL = ins.left == null ? pad : ins.left * W;
|
|
3699
|
+
const padR = ins.right == null ? pad : ins.right * W;
|
|
3700
|
+
const padT = ins.top == null ? pad : ins.top * H;
|
|
3701
|
+
const padB = ins.bottom == null ? pad : ins.bottom * H;
|
|
3702
|
+
const bar = frame.browserBar;
|
|
3703
|
+
const vw = video.width || 16;
|
|
3704
|
+
const vh = video.height || 9;
|
|
3705
|
+
const fitCover = frame.fit === "cover";
|
|
3706
|
+
const cf = fitCover ? 1 : Math.min(1, W / H / (vw / vh));
|
|
3707
|
+
const barH = bar.kind !== "none" ? (bar.height || 44) * s * cf : 0;
|
|
3708
|
+
const availW = Math.max(1, W - padL - padR);
|
|
3709
|
+
const availH = Math.max(1, H - padT - padB - barH);
|
|
3710
|
+
if (fitCover) {
|
|
3711
|
+
const sc2 = Math.max(availW / vw, availH / vh);
|
|
3712
|
+
const dw2 = vw * sc2;
|
|
3713
|
+
const dh2 = vh * sc2;
|
|
3714
|
+
const follow = frame.focusFollow === "camera" && zoomFocus ? zoomFocus : null;
|
|
3715
|
+
const fcx = clamp013(follow ? follow.cx : frame.focus?.cx ?? 0.5);
|
|
3716
|
+
const fcy = clamp013(follow ? follow.cy : frame.focus?.cy ?? 0.5);
|
|
3717
|
+
const vTop = padT + barH;
|
|
3718
|
+
const dx2 = Math.min(
|
|
3719
|
+
padL,
|
|
3720
|
+
Math.max(padL + availW - dw2, padL + availW / 2 - fcx * dw2)
|
|
3721
|
+
);
|
|
3722
|
+
const dy2 = Math.min(
|
|
3723
|
+
vTop,
|
|
3724
|
+
Math.max(vTop + availH - dh2, vTop + availH / 2 - fcy * dh2)
|
|
3725
|
+
);
|
|
3726
|
+
return {
|
|
3727
|
+
W,
|
|
3728
|
+
H,
|
|
3729
|
+
dx: dx2,
|
|
3730
|
+
dy: dy2,
|
|
3731
|
+
dw: dw2,
|
|
3732
|
+
dh: dh2,
|
|
3733
|
+
cardX: padL,
|
|
3734
|
+
cardY: padT,
|
|
3735
|
+
cardW: availW,
|
|
3736
|
+
cardH: availH + barH
|
|
3737
|
+
};
|
|
3472
3738
|
}
|
|
3739
|
+
const sc = Math.min(availW / vw, availH / vh);
|
|
3740
|
+
const dw = vw * sc;
|
|
3741
|
+
const dh = vh * sc;
|
|
3742
|
+
const dx = padL + (availW - dw) / 2;
|
|
3743
|
+
const dy = padT + barH + (availH - dh) / 2;
|
|
3473
3744
|
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
|
|
3745
|
+
W,
|
|
3746
|
+
H,
|
|
3747
|
+
dx,
|
|
3748
|
+
dy,
|
|
3749
|
+
dw,
|
|
3750
|
+
dh,
|
|
3751
|
+
cardX: dx,
|
|
3752
|
+
cardY: dy - barH,
|
|
3753
|
+
cardW: dw,
|
|
3754
|
+
cardH: dh + barH
|
|
3490
3755
|
};
|
|
3491
3756
|
}
|
|
3492
|
-
function
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3757
|
+
function docCardLayout(doc, media) {
|
|
3758
|
+
const meta = doc.source.meta;
|
|
3759
|
+
const H = 1080;
|
|
3760
|
+
const W = Math.max(
|
|
3761
|
+
2,
|
|
3762
|
+
Math.round(H * aspectRatioValue(doc.frame.aspectRatio, meta))
|
|
3495
3763
|
);
|
|
3496
|
-
|
|
3497
|
-
const
|
|
3498
|
-
|
|
3499
|
-
(
|
|
3764
|
+
const src = mediaSource(doc, media) ?? doc.source;
|
|
3765
|
+
const sm = src.meta;
|
|
3766
|
+
return computeCardLayout(
|
|
3767
|
+
mediaFrame(doc, media),
|
|
3768
|
+
{
|
|
3769
|
+
width: sm.captureWidth ?? sm.width,
|
|
3770
|
+
height: sm.captureHeight ?? sm.height
|
|
3771
|
+
},
|
|
3772
|
+
W,
|
|
3773
|
+
H
|
|
3500
3774
|
);
|
|
3501
|
-
if (inBase) return null;
|
|
3502
|
-
return {
|
|
3503
|
-
family: entry.family,
|
|
3504
|
-
weight,
|
|
3505
|
-
url: fontFaceUrl2(entry.slug, weight)
|
|
3506
|
-
};
|
|
3507
3775
|
}
|
|
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);
|
|
3776
|
+
function recommendedExportResolution(doc) {
|
|
3777
|
+
const meta = doc.source.meta;
|
|
3778
|
+
const capW = meta.captureWidth ?? meta.width;
|
|
3779
|
+
const video = { width: capW, height: meta.captureHeight ?? meta.height };
|
|
3780
|
+
let best = EXPORT_RESOLUTION_OPTIONS[0];
|
|
3781
|
+
for (const r of EXPORT_RESOLUTION_OPTIONS) {
|
|
3782
|
+
const { width, height } = resolveExportSize(doc, r);
|
|
3783
|
+
if (computeCardLayout(doc.frame, video, width, height).dw > capW) break;
|
|
3784
|
+
best = r;
|
|
3522
3785
|
}
|
|
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;
|
|
3786
|
+
return best;
|
|
3583
3787
|
}
|
|
3584
|
-
function
|
|
3585
|
-
|
|
3788
|
+
function camBubbleRect(cam, W, H = 1080) {
|
|
3789
|
+
const s = H / 1080;
|
|
3790
|
+
const size = Math.max(40, (cam.size || 0.25) * H);
|
|
3791
|
+
const mg = 24 * s;
|
|
3792
|
+
const x = cam.x != null ? cam.x * W - size / 2 : cam.position.includes("right") ? W - mg - size : mg;
|
|
3793
|
+
const y = cam.y != null ? cam.y * H - size / 2 : cam.position.includes("top") ? mg : H - mg - size;
|
|
3794
|
+
return {
|
|
3795
|
+
x,
|
|
3796
|
+
y,
|
|
3797
|
+
size,
|
|
3798
|
+
radius: cam.shape === "rounded" ? (cam.radius ?? 18) * s : size / 2
|
|
3799
|
+
};
|
|
3586
3800
|
}
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3801
|
+
function focusBounds(level, layout, camera = "card", screen) {
|
|
3802
|
+
if (camera === "stage" && screen && level > 1.001) {
|
|
3803
|
+
return { minX: 0, maxX: 1, minY: 0, maxY: 1 };
|
|
3804
|
+
}
|
|
3805
|
+
if (level <= 1.001) {
|
|
3806
|
+
return { minX: 0.5, maxX: 0.5, minY: 0.5, maxY: 0.5 };
|
|
3807
|
+
}
|
|
3808
|
+
if (camera === "stage") {
|
|
3809
|
+
const x2 = stageAxisBounds(
|
|
3810
|
+
layout.dx,
|
|
3811
|
+
layout.dw,
|
|
3812
|
+
layout.cardX,
|
|
3813
|
+
layout.cardW,
|
|
3814
|
+
layout.W,
|
|
3815
|
+
level
|
|
3816
|
+
);
|
|
3817
|
+
const y2 = stageAxisBounds(
|
|
3818
|
+
layout.dy,
|
|
3819
|
+
layout.dh,
|
|
3820
|
+
layout.cardY,
|
|
3821
|
+
layout.cardH,
|
|
3822
|
+
layout.H,
|
|
3823
|
+
level
|
|
3824
|
+
);
|
|
3825
|
+
return { minX: x2.min, maxX: x2.max, minY: y2.min, maxY: y2.max };
|
|
3826
|
+
}
|
|
3827
|
+
const x = axisBounds(
|
|
3828
|
+
layout.dx,
|
|
3829
|
+
layout.dw,
|
|
3830
|
+
layout.cardX,
|
|
3831
|
+
layout.cardW,
|
|
3832
|
+
layout.W,
|
|
3833
|
+
level
|
|
3593
3834
|
);
|
|
3594
|
-
const
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3835
|
+
const y = axisBounds(
|
|
3836
|
+
layout.dy,
|
|
3837
|
+
layout.dh,
|
|
3838
|
+
layout.cardY,
|
|
3839
|
+
layout.cardH,
|
|
3840
|
+
layout.H,
|
|
3841
|
+
level
|
|
3598
3842
|
);
|
|
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
|
-
};
|
|
3843
|
+
return { minX: x.min, maxX: x.max, minY: y.min, maxY: y.max };
|
|
3610
3844
|
}
|
|
3611
|
-
function
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3845
|
+
function axisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level) {
|
|
3846
|
+
const k = 1 - 1 / level;
|
|
3847
|
+
let lo = (coverOff / k - anchorOff) / anchorSize;
|
|
3848
|
+
let hi = ((coverOff + coverSize - viewport / level) / k - anchorOff) / anchorSize;
|
|
3849
|
+
if (lo > hi) {
|
|
3850
|
+
const mid = (lo + hi) / 2;
|
|
3851
|
+
lo = mid;
|
|
3852
|
+
hi = mid;
|
|
3616
3853
|
}
|
|
3617
|
-
return (
|
|
3854
|
+
return { min: clamp013(lo), max: clamp013(hi) };
|
|
3618
3855
|
}
|
|
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 }];
|
|
3856
|
+
function stageAxisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level) {
|
|
3857
|
+
const b = CAMERA_COVER_MIN / 2 * viewport;
|
|
3858
|
+
let lo = (coverOff + b / level - anchorOff) / anchorSize;
|
|
3859
|
+
let hi = (coverOff + coverSize - b / level - anchorOff) / anchorSize;
|
|
3860
|
+
if (lo > hi) {
|
|
3861
|
+
const mid = (lo + hi) / 2;
|
|
3862
|
+
lo = mid;
|
|
3863
|
+
hi = mid;
|
|
3864
|
+
}
|
|
3865
|
+
return { min: clamp013(lo), max: clamp013(hi) };
|
|
3866
|
+
}
|
|
3867
|
+
function clampFocus(cx, cy, level, layout, camera = "card", screen) {
|
|
3868
|
+
const b = focusBounds(level, layout, camera, screen);
|
|
3643
3869
|
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]))
|
|
3870
|
+
cx: Math.min(b.maxX, Math.max(b.minX, cx)),
|
|
3871
|
+
cy: Math.min(b.maxY, Math.max(b.minY, cy))
|
|
3648
3872
|
};
|
|
3649
3873
|
}
|
|
3650
|
-
function
|
|
3651
|
-
return
|
|
3874
|
+
function clamp013(v) {
|
|
3875
|
+
return Math.max(0, Math.min(1, v));
|
|
3652
3876
|
}
|
|
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 : [""];
|
|
3877
|
+
function screenPoint(layout, camera, screen) {
|
|
3878
|
+
return camera === "stage" && screen ? { ox: screen.x * layout.W, oy: screen.y * layout.H } : { ox: layout.W / 2, oy: layout.H / 2 };
|
|
3677
3879
|
}
|
|
3678
|
-
function
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
return [...line];
|
|
3880
|
+
function cameraModel(frame) {
|
|
3881
|
+
return frame?.camera === "stage" ? "stage" : "card";
|
|
3882
|
+
}
|
|
3883
|
+
var CAMERA_CENTRE_RAMP = 0.3;
|
|
3884
|
+
var CAMERA_COVER_MIN = 0.8;
|
|
3885
|
+
function cameraCentring(level) {
|
|
3886
|
+
const u = clamp013((level - 1) / CAMERA_CENTRE_RAMP);
|
|
3887
|
+
return u * u * (3 - 2 * u);
|
|
3687
3888
|
}
|
|
3688
|
-
function
|
|
3689
|
-
const
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3889
|
+
function zoomView(level, cx, cy, layout, camera = "card", centring, screen) {
|
|
3890
|
+
const L = Math.max(1, level);
|
|
3891
|
+
const fx = layout.dx + cx * layout.dw;
|
|
3892
|
+
const fy = layout.dy + cy * layout.dh;
|
|
3893
|
+
const { ox, oy } = screenPoint(layout, camera, screen);
|
|
3894
|
+
const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
|
|
3895
|
+
const wcx = fx + (ox - fx) / L * (1 - t);
|
|
3896
|
+
const wcy = fy + (oy - fy) / L * (1 - t);
|
|
3897
|
+
return { level: L, fx, fy, wcx, wcy, ox, oy };
|
|
3694
3898
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
const
|
|
3698
|
-
|
|
3699
|
-
const
|
|
3700
|
-
const text = stripEmphasis(overlayDisplayText(clip));
|
|
3701
|
-
const units = overlaySegments(text, unit);
|
|
3702
|
-
const n = unit === "block" ? 1 : units.reduce((sum, line) => sum + line.length, 0);
|
|
3703
|
-
const upperN = unit === "line" && clip.maxWidth ? overlayLines(text).reduce(
|
|
3704
|
-
(sum, line) => sum + overlayTokens(line).length,
|
|
3705
|
-
0
|
|
3706
|
-
) : n;
|
|
3707
|
-
const typewriter = spec.fx === "typewriter";
|
|
3708
|
-
const dur = typewriter ? 1e-3 : Math.min(2, Math.max(0.05, spec.duration ?? OVERLAY_TRANSITION_DUR));
|
|
3709
|
-
const defaultStagger = typewriter ? 0.05 : unit === "block" ? 0 : 0.06;
|
|
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;
|
|
3899
|
+
function zoomViewport(level, cx, cy, layout, camera = "card", centring, screen) {
|
|
3900
|
+
const v = zoomView(level, cx, cy, layout, camera, centring, screen);
|
|
3901
|
+
const size = 1 / v.level;
|
|
3902
|
+
const mx = v.wcx + (layout.W / 2 - v.ox) / v.level;
|
|
3903
|
+
const my = v.wcy + (layout.H / 2 - v.oy) / v.level;
|
|
3716
3904
|
return {
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
dur: round14(dur),
|
|
3722
|
-
tt: round14(st * (upperN - 1) + dur),
|
|
3723
|
-
units,
|
|
3724
|
-
n
|
|
3905
|
+
x: (mx - layout.W / (2 * v.level)) / layout.W,
|
|
3906
|
+
y: (my - layout.H / (2 * v.level)) / layout.H,
|
|
3907
|
+
w: size,
|
|
3908
|
+
h: size
|
|
3725
3909
|
};
|
|
3726
3910
|
}
|
|
3727
|
-
function
|
|
3728
|
-
const
|
|
3729
|
-
|
|
3911
|
+
function focusForViewportCentre(cX, cY, level, layout, camera = "card", centring, screen) {
|
|
3912
|
+
const L = Math.max(1, level);
|
|
3913
|
+
const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
|
|
3914
|
+
const a = 1 - (1 - t) / L;
|
|
3915
|
+
if (a <= 1e-6) return { cx: 0.5, cy: 0.5 };
|
|
3916
|
+
const { ox, oy } = screenPoint(layout, camera, screen);
|
|
3917
|
+
const fx = (cX * layout.W - ox * (1 - t) / L - (layout.W / 2 - ox) / L) / a;
|
|
3918
|
+
const fy = (cY * layout.H - oy * (1 - t) / L - (layout.H / 2 - oy) / L) / a;
|
|
3919
|
+
return { cx: (fx - layout.dx) / layout.dw, cy: (fy - layout.dy) / layout.dh };
|
|
3730
3920
|
}
|
|
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
|
-
};
|
|
3921
|
+
function levelForFocusFraction(frac) {
|
|
3922
|
+
if (!(frac > 0)) return clampZoomLevel(Number.POSITIVE_INFINITY);
|
|
3923
|
+
return clampZoomLevel(Math.max(1.1, 1 / frac));
|
|
3779
3924
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3925
|
+
|
|
3926
|
+
// src/stage.ts
|
|
3927
|
+
var CARD_FOV = 30;
|
|
3928
|
+
var OVERLAY_Z = -2;
|
|
3929
|
+
var CARD_Z = -4;
|
|
3930
|
+
var BACKGROUND_Z = -6;
|
|
3931
|
+
var CARD_OVERSCAN_STEP = 0.05;
|
|
3932
|
+
var CARD_OVERSCAN_MAX = 4;
|
|
3933
|
+
var CAMERA_NEAR = 0.1;
|
|
3934
|
+
var CAMERA_FAR = 100;
|
|
3935
|
+
function planeSizeAtDepth(distance, fovDeg, aspect) {
|
|
3936
|
+
const height = 2 * Math.abs(distance) * Math.tan(fovDeg * Math.PI / 180 / 2);
|
|
3937
|
+
return { width: height * aspect, height };
|
|
3938
|
+
}
|
|
3939
|
+
function cardPointToScreen(u, v) {
|
|
3940
|
+
return { sx: u, sy: v };
|
|
3791
3941
|
}
|
|
3792
3942
|
|
|
3793
3943
|
// src/text3d.ts
|
|
@@ -5393,324 +5543,155 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
5393
5543
|
ovC.textAlign = 'center'
|
|
5394
5544
|
ovC.textBaseline = 'middle'
|
|
5395
5545
|
ovC.letterSpacing = ((ol.ls || 0) * olMS * s) + 'px'
|
|
5396
|
-
//
|
|
5397
|
-
//
|
|
5398
|
-
//
|
|
5399
|
-
//
|
|
5400
|
-
//
|
|
5401
|
-
//
|
|
5546
|
+
// Every text layer is laid out by the layout module, ONCE: a styled one
|
|
5547
|
+
// from its runs (ol.rt: lines of runs, each set its own way), a plain
|
|
5548
|
+
// one from its lines, each a single run in the layer's own style. The
|
|
5549
|
+
// layout is kept per clip under a key of everything it was measured
|
|
5550
|
+
// with, so a frame that only moves, fades or reveals the layer measures
|
|
5551
|
+
// nothing. Fragments draw left to right from each line's left edge; a
|
|
5552
|
+
// per-unit entrance animates the layout's units, each a few parts of
|
|
5553
|
+
// fragments.
|
|
5554
|
+
var rtFonts = [ovC.font]
|
|
5402
5555
|
if (ol.rt) {
|
|
5403
|
-
var rtFonts = [ovC.font]
|
|
5404
5556
|
for (var rfi = 1; rfi < ol.rt.fs.length; rfi++) {
|
|
5405
|
-
rtFonts.push((ol.
|
|
5406
|
-
}
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
var rtY0 = -((rtN - 1) * rtLH) / 2
|
|
5426
|
-
// A line's left edge: lines sit centred on the anchor and align
|
|
5427
|
-
// against the widest, as the plain layer's do.
|
|
5428
|
-
var rtLeft = function (lw) {
|
|
5429
|
-
var xo = ol.align === 'left' ? (lw - rtL.width) / 2 : ol.align === 'right' ? (rtL.width - lw) / 2 : 0
|
|
5430
|
-
return xo - lw / 2
|
|
5431
|
-
}
|
|
5432
|
-
if (ol.box) {
|
|
5433
|
-
var rbPX = ol.box.px * olMS * s
|
|
5434
|
-
var rbPY = ol.box.py * olMS * s
|
|
5435
|
-
var rbW = rtL.width + rbPX * 2
|
|
5436
|
-
var rbH = rtN * rtLH + rbPY * 2
|
|
5437
|
-
ovC.save()
|
|
5438
|
-
ovC.globalAlpha = olA * ol.box.o
|
|
5439
|
-
ovC.fillStyle = ol.box.c
|
|
5440
|
-
rr(-rbW / 2, -rbH / 2, rbW, rbH, Math.min(ol.box.r * olMS * s, rbH / 2), ovC)
|
|
5441
|
-
ovC.fill()
|
|
5442
|
-
ovC.restore()
|
|
5443
|
-
}
|
|
5444
|
-
// Highlights sit behind the words and take no legibility shadow.
|
|
5445
|
-
for (var rhi = 0; rhi < rtN; rhi++) {
|
|
5446
|
-
var rhLine = rtL.lines[rhi]
|
|
5447
|
-
for (var rhj = 0; rhj < rhLine.frags.length; rhj++) {
|
|
5448
|
-
var rhF = rhLine.frags[rhj]
|
|
5449
|
-
if (!rhF.h) continue
|
|
5450
|
-
var rhR = TXL.decorationRect('h', rtLeft(rhLine.w) + rhF.x, rhF.w, rtY0 + rhi * rtLH, olPx)
|
|
5451
|
-
ovC.fillStyle = rhF.h
|
|
5452
|
-
ovC.fillRect(rhR.x, rhR.y, rhR.w, rhR.h)
|
|
5453
|
-
}
|
|
5454
|
-
}
|
|
5455
|
-
if (ol.shadow > 0) {
|
|
5456
|
-
ovC.shadowColor = 'rgba(0,0,0,' + ol.shadow + ')'
|
|
5457
|
-
ovC.shadowBlur = olPx * 0.25
|
|
5458
|
-
ovC.shadowOffsetY = olPx * 0.04
|
|
5459
|
-
}
|
|
5460
|
-
ovC.textAlign = 'left'
|
|
5461
|
-
// One fragment (or the part of one a unit holds): the underline under
|
|
5462
|
-
// the glyphs, stroke under fill, the strikethrough over them.
|
|
5463
|
-
var rtPart = function (p, bx, y) {
|
|
5464
|
-
ovC.font = rtFonts[p.f || 0]
|
|
5465
|
-
ovC.fillStyle = p.c || ol.color
|
|
5466
|
-
if (p.u) {
|
|
5467
|
-
var ru = TXL.decorationRect('u', bx + p.x, p.w, y, olPx)
|
|
5468
|
-
ovC.fillRect(ru.x, ru.y, ru.w, ru.h)
|
|
5469
|
-
}
|
|
5470
|
-
if (ol.stroke) {
|
|
5471
|
-
ovC.strokeStyle = ol.stroke.c
|
|
5472
|
-
ovC.lineWidth = ol.stroke.w * olMS * s
|
|
5473
|
-
ovC.lineJoin = 'round'
|
|
5474
|
-
ovC.strokeText(p.t, bx + p.x, y)
|
|
5475
|
-
}
|
|
5476
|
-
ovC.fillText(p.t, bx + p.x, y)
|
|
5477
|
-
if (p.s) {
|
|
5478
|
-
var rs = TXL.decorationRect('s', bx + p.x, p.w, y, olPx)
|
|
5479
|
-
ovC.fillRect(rs.x, rs.y, rs.w, rs.h)
|
|
5480
|
-
}
|
|
5481
|
-
}
|
|
5482
|
-
if (!rtFx) {
|
|
5483
|
-
for (var rli = 0; rli < rtN; rli++) {
|
|
5484
|
-
var rLine = rtL.lines[rli]
|
|
5485
|
-
var rBX = rtLeft(rLine.w)
|
|
5486
|
-
for (var rfk = 0; rfk < rLine.frags.length; rfk++) rtPart(rLine.frags[rfk], rBX, rtY0 + rli * rtLH)
|
|
5487
|
-
}
|
|
5488
|
-
} else {
|
|
5489
|
-
// Per-unit entrance, the plain layer's own arithmetic over the
|
|
5490
|
-
// layout's units: delay = order(index) x st, eased over dur.
|
|
5491
|
-
var rtCount = 0
|
|
5492
|
-
for (var rci = 0; rci < rtN; rci++) rtCount += rtL.lines[rci].units.length
|
|
5493
|
-
var rtIdx = 0
|
|
5494
|
-
for (var rui = 0; rui < rtN; rui++) {
|
|
5495
|
-
var rULine = rtL.lines[rui]
|
|
5496
|
-
var rUBX = rtLeft(rULine.w)
|
|
5497
|
-
var rUY = rtY0 + rui * rtLH
|
|
5498
|
-
for (var ruj = 0; ruj < rULine.units.length; ruj++, rtIdx++) {
|
|
5499
|
-
var rU = rULine.units[ruj]
|
|
5500
|
-
var rOrd = rtFx.d === 1 ? (rtCount - 1 - rtIdx) : rtFx.d === 2 ? Math.abs(rtIdx - (rtCount - 1) / 2) : rtIdx
|
|
5501
|
-
var rT2 = olT - rOrd * rtFx.st
|
|
5502
|
-
var rUA = 1, rUu = 1
|
|
5503
|
-
if (rtFx.k === 'typewriter') {
|
|
5504
|
-
rUA = rT2 >= 0 ? 1 : 0
|
|
5505
|
-
} else {
|
|
5506
|
-
rUu = Math.max(0, Math.min(1, rT2 / rtFx.dur))
|
|
5507
|
-
var rUE = 1 - Math.pow(1 - rUu, 3)
|
|
5508
|
-
rUA = rtFx.k === 'pop' ? Math.min(1, rUu * 2) : rUE
|
|
5509
|
-
}
|
|
5510
|
-
if (rUA <= 0.004) continue
|
|
5511
|
-
ovC.save()
|
|
5512
|
-
ovC.globalAlpha = olA * rUA
|
|
5513
|
-
if (rUu < 1) {
|
|
5514
|
-
if (rtFx.k === 'rise') {
|
|
5515
|
-
ovC.translate(0, (1 - (1 - Math.pow(1 - rUu, 3))) * 24 * s)
|
|
5516
|
-
} else if (rtFx.k === 'pop') {
|
|
5517
|
-
var rPS = 1 + 2.70158 * Math.pow(rUu - 1, 3) + 1.70158 * Math.pow(rUu - 1, 2)
|
|
5518
|
-
var rCX = rUBX + rU.x + rU.w / 2
|
|
5519
|
-
ovC.translate(rCX, rUY)
|
|
5520
|
-
ovC.scale(rPS, rPS)
|
|
5521
|
-
ovC.translate(-rCX, -rUY)
|
|
5522
|
-
} else if (rtFx.k === 'blur') {
|
|
5523
|
-
ovC.filter = 'blur(' + ((1 - rUu) * olPx * 0.12).toFixed(2) + 'px)'
|
|
5524
|
-
}
|
|
5525
|
-
}
|
|
5526
|
-
for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY)
|
|
5527
|
-
ovC.restore()
|
|
5528
|
-
}
|
|
5529
|
-
}
|
|
5557
|
+
rtFonts.push((ol.rt.fs[rfi].i ? 'italic ' : '') + ol.rt.fs[rfi].w + ' ' + olPx + 'px ' + ol.stack)
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
var rtFx = ol.fx && ol.fx.u !== 'block' ? ol.fx : null
|
|
5561
|
+
var rtMax = ol.mw ? ol.mw * W : 0
|
|
5562
|
+
var rtKey = (ol.rt ? 'r' + ol.rt.h : 'p' + (ol.lines || ['']).join('\\n')) + '|' + rtFonts[0] + '|' + ((ol.ls || 0) * olMS * s) + '|' + rtMax + '|' + (rtFx ? rtFx.u : '') + '|' + (ns.fontEpoch || 0)
|
|
5563
|
+
var rtAll = ns.textLayouts || (ns.textLayouts = {})
|
|
5564
|
+
var rtHit = rtAll[ol.id]
|
|
5565
|
+
if (!rtHit || rtHit.k !== rtKey) {
|
|
5566
|
+
var rtIn = ol.rt ? ol.rt.l : []
|
|
5567
|
+
if (!ol.rt) {
|
|
5568
|
+
var rtPl = ol.lines || ['']
|
|
5569
|
+
for (var rpi = 0; rpi < rtPl.length; rpi++) rtIn.push(rtPl[rpi] ? [{ t: rtPl[rpi] }] : [])
|
|
5570
|
+
}
|
|
5571
|
+
rtHit = rtAll[ol.id] = {
|
|
5572
|
+
k: rtKey,
|
|
5573
|
+
L: TXL.layoutText(
|
|
5574
|
+
{ lines: rtIn, maxWidth: rtMax, unit: rtFx ? rtFx.u : undefined },
|
|
5575
|
+
function (mt, mf) { ovC.font = rtFonts[mf]; return ovC.measureText(mt).width }
|
|
5576
|
+
)
|
|
5530
5577
|
}
|
|
5531
|
-
ovC.
|
|
5532
|
-
continue
|
|
5578
|
+
ovC.font = rtFonts[0]
|
|
5533
5579
|
}
|
|
5534
|
-
var
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
//
|
|
5539
|
-
//
|
|
5540
|
-
|
|
5541
|
-
var
|
|
5542
|
-
|
|
5543
|
-
for (var olwl = 0; olwl < olLines.length; olwl++) {
|
|
5544
|
-
var olWLine = olLines[olwl]
|
|
5545
|
-
if (!olWLine || ovC.measureText(olWLine).width <= olWMax) {
|
|
5546
|
-
olWrapped.push(olWLine)
|
|
5547
|
-
continue
|
|
5548
|
-
}
|
|
5549
|
-
var olToks = olWLine.match(/\\S+\\s*/g) || [olWLine]
|
|
5550
|
-
var olCur = ''
|
|
5551
|
-
for (var olti = 0; olti < olToks.length; olti++) {
|
|
5552
|
-
if (!olCur) { olCur = olToks[olti]; continue }
|
|
5553
|
-
if (ovC.measureText(olCur + olToks[olti]).width <= olWMax) {
|
|
5554
|
-
olCur += olToks[olti]
|
|
5555
|
-
} else {
|
|
5556
|
-
olWrapped.push(olCur)
|
|
5557
|
-
olCur = olToks[olti]
|
|
5558
|
-
}
|
|
5559
|
-
}
|
|
5560
|
-
if (olCur) olWrapped.push(olCur)
|
|
5561
|
-
}
|
|
5562
|
-
olLines = olWrapped.length ? olWrapped : ['']
|
|
5563
|
-
}
|
|
5564
|
-
var olLH = olPx * (ol.lh || ${OVERLAY_LINE_HEIGHT})
|
|
5565
|
-
var olY0 = -((olLines.length - 1) * olLH) / 2
|
|
5566
|
-
// Per-line widths: needed by the pill, by left/right alignment (lines
|
|
5567
|
-
// draw centered; alignment is an x offset against the widest line), and
|
|
5568
|
-
// by per-unit fx (units place by prefix advance from the line's left edge).
|
|
5569
|
-
var olFx = ol.fx && ol.fx.units.length ? ol.fx : null
|
|
5570
|
-
// With wrap active, baked per-line unit arrays regroup onto the WRAPPED
|
|
5571
|
-
// lines. Wrapping never reorders: word/char units consume in flat order
|
|
5572
|
-
// by string length (wrapped lines are token concatenations); 'line'
|
|
5573
|
-
// units become one per wrapped line. Flat delay order is unchanged.
|
|
5574
|
-
if (olFx && ol.mw) {
|
|
5575
|
-
var olFlat = []
|
|
5576
|
-
for (var olfi = 0; olfi < olFx.units.length; olfi++) {
|
|
5577
|
-
for (var olfj = 0; olfj < olFx.units[olfi].length; olfj++) {
|
|
5578
|
-
olFlat.push(olFx.units[olfi][olfj])
|
|
5579
|
-
}
|
|
5580
|
-
}
|
|
5581
|
-
var olRe = []
|
|
5582
|
-
if (olFx.u === 'line') {
|
|
5583
|
-
for (var olri = 0; olri < olLines.length; olri++) olRe.push([olLines[olri]])
|
|
5584
|
-
} else {
|
|
5585
|
-
var olFk = 0
|
|
5586
|
-
for (var olri2 = 0; olri2 < olLines.length; olri2++) {
|
|
5587
|
-
var olNeed = olLines[olri2].length
|
|
5588
|
-
var olArr = []
|
|
5589
|
-
var olGot = 0
|
|
5590
|
-
while (olFk < olFlat.length && olGot < olNeed) {
|
|
5591
|
-
olArr.push(olFlat[olFk])
|
|
5592
|
-
olGot += olFlat[olFk].length
|
|
5593
|
-
olFk++
|
|
5594
|
-
}
|
|
5595
|
-
olRe.push(olArr)
|
|
5596
|
-
}
|
|
5597
|
-
}
|
|
5598
|
-
var olReN = 0
|
|
5599
|
-
for (var olrn = 0; olrn < olRe.length; olrn++) olReN += olRe[olrn].length
|
|
5600
|
-
olFx = { k: olFx.k, u: olFx.u, d: olFx.d, st: olFx.st, dur: olFx.dur, tt: olFx.tt, units: olRe, n: olReN }
|
|
5601
|
-
}
|
|
5602
|
-
var olLWs = null, olMaxW = 0
|
|
5603
|
-
if (ol.box || ol.align || olFx) {
|
|
5604
|
-
olLWs = []
|
|
5605
|
-
for (var olwi = 0; olwi < olLines.length; olwi++) {
|
|
5606
|
-
var olw = ovC.measureText(olLines[olwi]).width
|
|
5607
|
-
olLWs.push(olw)
|
|
5608
|
-
if (olw > olMaxW) olMaxW = olw
|
|
5609
|
-
}
|
|
5580
|
+
var rtL = rtHit.L
|
|
5581
|
+
var rtN = rtL.lines.length
|
|
5582
|
+
var rtLH = olPx * (ol.lh || ${OVERLAY_LINE_HEIGHT})
|
|
5583
|
+
var rtY0 = -((rtN - 1) * rtLH) / 2
|
|
5584
|
+
// A line's left edge: lines sit centred on the anchor and align
|
|
5585
|
+
// against the widest.
|
|
5586
|
+
var rtLeft = function (lw) {
|
|
5587
|
+
var xo = ol.align === 'left' ? (lw - rtL.width) / 2 : ol.align === 'right' ? (rtL.width - lw) / 2 : 0
|
|
5588
|
+
return xo - lw / 2
|
|
5610
5589
|
}
|
|
5611
|
-
// Background pill (ol.box, baked design px at fs): drawn BEFORE the text
|
|
5612
|
-
// and before the legibility shadow config, so the pill never inherits the
|
|
5613
|
-
// text shadow. Geometry mirrors overlayRect's inflation \u2014 change together.
|
|
5614
5590
|
if (ol.box) {
|
|
5615
|
-
var
|
|
5616
|
-
var
|
|
5617
|
-
var
|
|
5618
|
-
var
|
|
5619
|
-
var obR = Math.min(ol.box.r * olMS * s, obFullH / 2)
|
|
5591
|
+
var rbPX = ol.box.px * olMS * s
|
|
5592
|
+
var rbPY = ol.box.py * olMS * s
|
|
5593
|
+
var rbW = rtL.width + rbPX * 2
|
|
5594
|
+
var rbH = rtN * rtLH + rbPY * 2
|
|
5620
5595
|
ovC.save()
|
|
5621
5596
|
ovC.globalAlpha = olA * ol.box.o
|
|
5622
5597
|
ovC.fillStyle = ol.box.c
|
|
5623
|
-
rr(-
|
|
5598
|
+
rr(-rbW / 2, -rbH / 2, rbW, rbH, Math.min(ol.box.r * olMS * s, rbH / 2), ovC)
|
|
5624
5599
|
ovC.fill()
|
|
5625
5600
|
ovC.restore()
|
|
5626
5601
|
}
|
|
5602
|
+
// Highlights sit behind the words and take no legibility shadow. Under
|
|
5603
|
+
// a per-unit entrance each is drawn with its unit instead (below), so
|
|
5604
|
+
// it arrives with its word.
|
|
5605
|
+
for (var rhi = 0; !rtFx && rhi < rtN; rhi++) {
|
|
5606
|
+
var rhLine = rtL.lines[rhi]
|
|
5607
|
+
for (var rhj = 0; rhj < rhLine.frags.length; rhj++) {
|
|
5608
|
+
var rhF = rhLine.frags[rhj]
|
|
5609
|
+
if (!rhF.h) continue
|
|
5610
|
+
var rhR = TXL.decorationRect('h', rtLeft(rhLine.w) + rhF.x, rhF.w, rtY0 + rhi * rtLH, olPx)
|
|
5611
|
+
ovC.fillStyle = rhF.h
|
|
5612
|
+
ovC.fillRect(rhR.x, rhR.y, rhR.w, rhR.h)
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5627
5615
|
if (ol.shadow > 0) {
|
|
5628
5616
|
ovC.shadowColor = 'rgba(0,0,0,' + ol.shadow + ')'
|
|
5629
5617
|
ovC.shadowBlur = olPx * 0.25
|
|
5630
5618
|
ovC.shadowOffsetY = olPx * 0.04
|
|
5631
5619
|
}
|
|
5632
|
-
ovC.
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
ovC.
|
|
5620
|
+
ovC.textAlign = 'left'
|
|
5621
|
+
// One fragment (or the part of one a unit holds): the underline under
|
|
5622
|
+
// the glyphs, stroke under fill, the strikethrough over them.
|
|
5623
|
+
var rtPart = function (p, bx, y, withH) {
|
|
5624
|
+
if (withH && p.h) {
|
|
5625
|
+
var rhU = TXL.decorationRect('h', bx + p.x, p.w, y, olPx)
|
|
5626
|
+
var rhS = ovC.shadowColor
|
|
5627
|
+
ovC.shadowColor = 'rgba(0,0,0,0)'
|
|
5628
|
+
ovC.fillStyle = p.h
|
|
5629
|
+
ovC.fillRect(rhU.x, rhU.y, rhU.w, rhU.h)
|
|
5630
|
+
ovC.shadowColor = rhS
|
|
5631
|
+
}
|
|
5632
|
+
ovC.font = rtFonts[p.f || 0]
|
|
5633
|
+
ovC.fillStyle = p.c || ol.color
|
|
5634
|
+
if (p.u) {
|
|
5635
|
+
var ru = TXL.decorationRect('u', bx + p.x, p.w, y, olPx)
|
|
5636
|
+
ovC.fillRect(ru.x, ru.y, ru.w, ru.h)
|
|
5637
|
+
}
|
|
5638
|
+
if (ol.stroke) {
|
|
5639
|
+
ovC.strokeStyle = ol.stroke.c
|
|
5640
|
+
ovC.lineWidth = ol.stroke.w * olMS * s
|
|
5641
|
+
ovC.lineJoin = 'round'
|
|
5642
|
+
ovC.strokeText(p.t, bx + p.x, y)
|
|
5643
|
+
}
|
|
5644
|
+
ovC.fillText(p.t, bx + p.x, y)
|
|
5645
|
+
if (p.s) {
|
|
5646
|
+
var rs = TXL.decorationRect('s', bx + p.x, p.w, y, olPx)
|
|
5647
|
+
ovC.fillRect(rs.x, rs.y, rs.w, rs.h)
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
if (!rtFx) {
|
|
5651
|
+
for (var rli = 0; rli < rtN; rli++) {
|
|
5652
|
+
var rLine = rtL.lines[rli]
|
|
5653
|
+
var rBX = rtLeft(rLine.w)
|
|
5654
|
+
for (var rfk = 0; rfk < rLine.frags.length; rfk++) rtPart(rLine.frags[rfk], rBX, rtY0 + rli * rtLH)
|
|
5649
5655
|
}
|
|
5650
5656
|
} else {
|
|
5651
|
-
// Per-unit entrance
|
|
5652
|
-
//
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
var
|
|
5660
|
-
var
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
}
|
|
5668
|
-
var olXb = olXof2 - olLW2 / 2
|
|
5669
|
-
var olPref = '', olPW = 0
|
|
5670
|
-
for (var ou = 0; ou < olUs.length; ou++, olIdx++) {
|
|
5671
|
-
var olOrd = olFx.d === 1
|
|
5672
|
-
? (olFx.n - 1 - olIdx)
|
|
5673
|
-
: olFx.d === 2
|
|
5674
|
-
? Math.abs(olIdx - (olFx.n - 1) / 2)
|
|
5675
|
-
: olIdx
|
|
5676
|
-
var olT2 = olT - olOrd * olFx.st
|
|
5677
|
-
var olNext = olPref + olUs[ou]
|
|
5678
|
-
var olNW = ovC.measureText(olNext).width
|
|
5679
|
-
var olUW = olNW - olPW
|
|
5680
|
-
var olUX = olXb + olPW
|
|
5681
|
-
var olUA = 1, olUu = 1
|
|
5682
|
-
if (olFx.k === 'typewriter') {
|
|
5683
|
-
olUA = olT2 >= 0 ? 1 : 0
|
|
5657
|
+
// Per-unit entrance, the plain layer's own arithmetic over the
|
|
5658
|
+
// layout's units: delay = order(index) x st, eased over dur.
|
|
5659
|
+
var rtCount = 0
|
|
5660
|
+
for (var rci = 0; rci < rtN; rci++) rtCount += rtL.lines[rci].units.length
|
|
5661
|
+
var rtIdx = 0
|
|
5662
|
+
for (var rui = 0; rui < rtN; rui++) {
|
|
5663
|
+
var rULine = rtL.lines[rui]
|
|
5664
|
+
var rUBX = rtLeft(rULine.w)
|
|
5665
|
+
var rUY = rtY0 + rui * rtLH
|
|
5666
|
+
for (var ruj = 0; ruj < rULine.units.length; ruj++, rtIdx++) {
|
|
5667
|
+
var rU = rULine.units[ruj]
|
|
5668
|
+
var rOrd = rtFx.d === 1 ? (rtCount - 1 - rtIdx) : rtFx.d === 2 ? Math.abs(rtIdx - (rtCount - 1) / 2) : rtIdx
|
|
5669
|
+
var rT2 = olT - rOrd * rtFx.st
|
|
5670
|
+
var rUA = 1, rUu = 1
|
|
5671
|
+
if (rtFx.k === 'typewriter') {
|
|
5672
|
+
rUA = rT2 >= 0 ? 1 : 0
|
|
5684
5673
|
} else {
|
|
5685
|
-
|
|
5686
|
-
var
|
|
5687
|
-
|
|
5674
|
+
rUu = Math.max(0, Math.min(1, rT2 / rtFx.dur))
|
|
5675
|
+
var rUE = 1 - Math.pow(1 - rUu, 3)
|
|
5676
|
+
rUA = rtFx.k === 'pop' ? Math.min(1, rUu * 2) : rUE
|
|
5688
5677
|
}
|
|
5689
|
-
|
|
5690
|
-
olPref = olNext
|
|
5691
|
-
olPW = olNW
|
|
5692
|
-
if (olUA <= 0.004) continue
|
|
5678
|
+
if (rUA <= 0.004) continue
|
|
5693
5679
|
ovC.save()
|
|
5694
|
-
ovC.globalAlpha = olA *
|
|
5695
|
-
if (
|
|
5696
|
-
if (
|
|
5697
|
-
ovC.translate(0, (1 - (1 - Math.pow(1 -
|
|
5698
|
-
} else if (
|
|
5699
|
-
var
|
|
5700
|
-
|
|
5701
|
-
ovC.
|
|
5702
|
-
ovC.
|
|
5703
|
-
|
|
5704
|
-
|
|
5680
|
+
ovC.globalAlpha = olA * rUA
|
|
5681
|
+
if (rUu < 1) {
|
|
5682
|
+
if (rtFx.k === 'rise') {
|
|
5683
|
+
ovC.translate(0, (1 - (1 - Math.pow(1 - rUu, 3))) * 24 * s)
|
|
5684
|
+
} else if (rtFx.k === 'pop') {
|
|
5685
|
+
var rPS = 1 + 2.70158 * Math.pow(rUu - 1, 3) + 1.70158 * Math.pow(rUu - 1, 2)
|
|
5686
|
+
var rCX = rUBX + rU.x + rU.w / 2
|
|
5687
|
+
ovC.translate(rCX, rUY)
|
|
5688
|
+
ovC.scale(rPS, rPS)
|
|
5689
|
+
ovC.translate(-rCX, -rUY)
|
|
5690
|
+
} else if (rtFx.k === 'blur') {
|
|
5691
|
+
ovC.filter = 'blur(' + ((1 - rUu) * olPx * 0.12).toFixed(2) + 'px)'
|
|
5705
5692
|
}
|
|
5706
5693
|
}
|
|
5707
|
-
|
|
5708
|
-
ovC.strokeStyle = ol.stroke.c
|
|
5709
|
-
ovC.lineWidth = ol.stroke.w * olMS * s
|
|
5710
|
-
ovC.lineJoin = 'round'
|
|
5711
|
-
ovC.strokeText(olUnit, olUX, olLY2)
|
|
5712
|
-
}
|
|
5713
|
-
ovC.fillText(olUnit, olUX, olLY2)
|
|
5694
|
+
for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY, true)
|
|
5714
5695
|
ovC.restore()
|
|
5715
5696
|
}
|
|
5716
5697
|
}
|
|
@@ -8495,7 +8476,8 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
8495
8476
|
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
8477
|
}
|
|
8497
8478
|
}`;
|
|
8498
|
-
function studioLayerData(
|
|
8479
|
+
function studioLayerData(input, duration, pins) {
|
|
8480
|
+
const layers = migrateText(input);
|
|
8499
8481
|
const rec = "source" in layers ? layers : null;
|
|
8500
8482
|
return {
|
|
8501
8483
|
// Music/SFX clips with their gain envelopes baked (shared truth for the
|
|
@@ -8674,12 +8656,13 @@ function studioLayerData(layers, duration, pins) {
|
|
|
8674
8656
|
const rt = styledTextOf(o);
|
|
8675
8657
|
return {
|
|
8676
8658
|
...base,
|
|
8677
|
-
text: o
|
|
8678
|
-
// The lines a person reads
|
|
8659
|
+
text: overlayPlainText(o),
|
|
8660
|
+
// The lines a person reads: what a plain layer is laid out
|
|
8661
|
+
// from, one run a line. A styled layer (rt) is laid out from
|
|
8679
8662
|
// its runs; these are then its words, for the signature.
|
|
8680
|
-
lines: overlayLines(
|
|
8663
|
+
lines: overlayLines(overlayPlainText(o)),
|
|
8681
8664
|
// The styled payload, only when the layer is set in more than
|
|
8682
|
-
// one style
|
|
8665
|
+
// one style.
|
|
8683
8666
|
...rt ? { rt } : {},
|
|
8684
8667
|
fs: st.size,
|
|
8685
8668
|
weight: st.weight,
|
|
@@ -8712,9 +8695,10 @@ function studioLayerData(layers, duration, pins) {
|
|
|
8712
8695
|
r: round8(bx.radius)
|
|
8713
8696
|
}
|
|
8714
8697
|
} : {},
|
|
8715
|
-
// Entrance animation:
|
|
8716
|
-
//
|
|
8717
|
-
// per-unit progress as pure f(t). Absent = data
|
|
8698
|
+
// Entrance animation: timing normalized HERE (deterministic
|
|
8699
|
+
// doc-derived data) — ON_FRAME cuts the units from the layout
|
|
8700
|
+
// and interprets per-unit progress as pure f(t). Absent = data
|
|
8701
|
+
// byte parity.
|
|
8718
8702
|
...(() => {
|
|
8719
8703
|
const olFx = resolveOverlayFx(
|
|
8720
8704
|
o,
|
|
@@ -8728,7 +8712,7 @@ function studioLayerData(layers, duration, pins) {
|
|
|
8728
8712
|
};
|
|
8729
8713
|
}
|
|
8730
8714
|
function lowerToComposition(input) {
|
|
8731
|
-
const doc = migrateMotion(input);
|
|
8715
|
+
const doc = migrateText(migrateMotion(input));
|
|
8732
8716
|
const rated = ratedSegments(doc);
|
|
8733
8717
|
const transitions = docTransitions(doc);
|
|
8734
8718
|
const footageEnd = durationSec(doc, rated);
|
|
@@ -8937,7 +8921,7 @@ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
|
|
|
8937
8921
|
});
|
|
8938
8922
|
}
|
|
8939
8923
|
function docZoomTrack(input) {
|
|
8940
|
-
const doc = migrateMotion(input);
|
|
8924
|
+
const doc = migrateText(migrateMotion(input));
|
|
8941
8925
|
const rated = ratedSegments(doc);
|
|
8942
8926
|
const transitions = docTransitions(doc);
|
|
8943
8927
|
const layout = docCardLayout(doc);
|
|
@@ -11863,7 +11847,7 @@ var overlaysLane = {
|
|
|
11863
11847
|
kind: "clip",
|
|
11864
11848
|
t: round13(o.start),
|
|
11865
11849
|
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"
|
|
11850
|
+
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
11851
|
},
|
|
11868
11852
|
// Pose diamonds render after the clips, so they sit on top.
|
|
11869
11853
|
...poseItems(o.id, o.start, o.duration, o.motion)
|
|
@@ -12276,8 +12260,6 @@ export {
|
|
|
12276
12260
|
DRAG_FIT_LEVEL,
|
|
12277
12261
|
DRAG_MIN_S,
|
|
12278
12262
|
DRAG_MIN_TRAVEL_FRAC,
|
|
12279
|
-
EM_CLOSE,
|
|
12280
|
-
EM_OPEN,
|
|
12281
12263
|
END_CARD_FROM,
|
|
12282
12264
|
END_CARD_RECEDE,
|
|
12283
12265
|
END_CARD_SECONDS,
|
|
@@ -12311,6 +12293,7 @@ export {
|
|
|
12311
12293
|
MEDIA_FRAME_KEYS,
|
|
12312
12294
|
MEDIA_KEY_PREFIX,
|
|
12313
12295
|
MINIMAL_BAR_THEMES,
|
|
12296
|
+
MIXED,
|
|
12314
12297
|
MOTION_DELTA,
|
|
12315
12298
|
MOTION_EASE,
|
|
12316
12299
|
OBJECT_DEFAULT_SCALE,
|
|
@@ -12334,6 +12317,7 @@ export {
|
|
|
12334
12317
|
REJECT_OVERLAP,
|
|
12335
12318
|
REST_TILT_ID,
|
|
12336
12319
|
RETIRED_ZOOM_STYLES,
|
|
12320
|
+
RUN_STYLE_KEYS,
|
|
12337
12321
|
SCENE_MOTION,
|
|
12338
12322
|
SCENE_QUIET,
|
|
12339
12323
|
SEGMENT_ENTER_KINDS,
|
|
@@ -12391,6 +12375,7 @@ export {
|
|
|
12391
12375
|
aspectRatioValue,
|
|
12392
12376
|
audioLane,
|
|
12393
12377
|
backdropMedia,
|
|
12378
|
+
boldWeightFor,
|
|
12394
12379
|
buildDigest,
|
|
12395
12380
|
calloutBox,
|
|
12396
12381
|
calloutClip,
|
|
@@ -12421,9 +12406,11 @@ export {
|
|
|
12421
12406
|
clampSpeedRate,
|
|
12422
12407
|
clampTiltDeg,
|
|
12423
12408
|
clampZoomLevel,
|
|
12409
|
+
clearStyle,
|
|
12424
12410
|
clipEnvelope,
|
|
12425
12411
|
clipLength,
|
|
12426
12412
|
clipsFrom,
|
|
12413
|
+
commonStyle,
|
|
12427
12414
|
computeCardLayout,
|
|
12428
12415
|
computeMicRms,
|
|
12429
12416
|
computePeaks,
|
|
@@ -12442,6 +12429,7 @@ export {
|
|
|
12442
12429
|
deriveViewportCrop,
|
|
12443
12430
|
destinationById,
|
|
12444
12431
|
destinationsForChannel,
|
|
12432
|
+
diffInput,
|
|
12445
12433
|
docCardLayout,
|
|
12446
12434
|
docFreezes,
|
|
12447
12435
|
docMediaList,
|
|
@@ -12517,6 +12505,7 @@ export {
|
|
|
12517
12505
|
lowerProgramDoc,
|
|
12518
12506
|
lowerStudioDoc,
|
|
12519
12507
|
lowerToComposition,
|
|
12508
|
+
marksEmphasis,
|
|
12520
12509
|
mediaAtOutput,
|
|
12521
12510
|
mediaDuration,
|
|
12522
12511
|
mediaFrame,
|
|
@@ -12527,12 +12516,15 @@ export {
|
|
|
12527
12516
|
micLane,
|
|
12528
12517
|
migrateHostedDoc,
|
|
12529
12518
|
migrateMotion,
|
|
12519
|
+
migrateText,
|
|
12520
|
+
migrateTextClip,
|
|
12530
12521
|
migrateZoomStyle,
|
|
12531
12522
|
momentsFromDoc,
|
|
12532
12523
|
motionTrack,
|
|
12533
12524
|
musicBedClip,
|
|
12534
12525
|
nextMediaId,
|
|
12535
12526
|
normalizeCaptureSpace,
|
|
12527
|
+
normalizeRuns,
|
|
12536
12528
|
objectMotionBase,
|
|
12537
12529
|
objectMotionPoseAt,
|
|
12538
12530
|
objectsLane,
|
|
@@ -12541,7 +12533,6 @@ export {
|
|
|
12541
12533
|
outputEnd,
|
|
12542
12534
|
outputRangeToSource,
|
|
12543
12535
|
overlapFraction,
|
|
12544
|
-
overlayDisplayText,
|
|
12545
12536
|
overlayFaceFor,
|
|
12546
12537
|
overlayFontFaces,
|
|
12547
12538
|
overlayFontString,
|
|
@@ -12551,12 +12542,12 @@ export {
|
|
|
12551
12542
|
overlayMotionBase,
|
|
12552
12543
|
overlayMotionPoseAt,
|
|
12553
12544
|
overlayPinPoseAt,
|
|
12545
|
+
overlayPlainText,
|
|
12554
12546
|
overlayRect,
|
|
12555
12547
|
overlaysLane,
|
|
12556
12548
|
ownFreezes,
|
|
12557
12549
|
pageDisplayUrl,
|
|
12558
12550
|
parseCssColour,
|
|
12559
|
-
parseEmphasis,
|
|
12560
12551
|
parsePoseId,
|
|
12561
12552
|
pickStyle,
|
|
12562
12553
|
pinBox,
|
|
@@ -12566,6 +12557,7 @@ export {
|
|
|
12566
12557
|
pinReferent,
|
|
12567
12558
|
pinReferentAt,
|
|
12568
12559
|
placeFreezes,
|
|
12560
|
+
plainText,
|
|
12569
12561
|
planAutoSpeed,
|
|
12570
12562
|
planAutoTilt,
|
|
12571
12563
|
planAutoZoom,
|
|
@@ -12585,7 +12577,7 @@ export {
|
|
|
12585
12577
|
relativeLuminance,
|
|
12586
12578
|
removeSourceRange,
|
|
12587
12579
|
removeSpeedInRange,
|
|
12588
|
-
|
|
12580
|
+
replaceRange,
|
|
12589
12581
|
resolveExportSize,
|
|
12590
12582
|
resolveOverlayBox,
|
|
12591
12583
|
resolveOverlayStyle,
|
|
@@ -12595,6 +12587,7 @@ export {
|
|
|
12595
12587
|
restLeanOf,
|
|
12596
12588
|
restSpansThroughTransitions,
|
|
12597
12589
|
rgbToHex,
|
|
12590
|
+
runsOf,
|
|
12598
12591
|
sameMedia,
|
|
12599
12592
|
sceneChanges,
|
|
12600
12593
|
scrollRuns,
|
|
@@ -12602,17 +12595,24 @@ export {
|
|
|
12602
12595
|
outputLen as segmentOutputLen,
|
|
12603
12596
|
segmentStarts,
|
|
12604
12597
|
setSpeedInRange,
|
|
12598
|
+
setStyle,
|
|
12599
|
+
sliceRuns,
|
|
12605
12600
|
slideSide,
|
|
12606
12601
|
smoothCursor,
|
|
12602
|
+
snapOffset,
|
|
12603
|
+
snapRunWeight,
|
|
12607
12604
|
spanOutputExtent,
|
|
12608
12605
|
speedLane,
|
|
12609
|
-
stripEmphasis,
|
|
12610
12606
|
studioAudioPlan,
|
|
12611
12607
|
studioEntry,
|
|
12612
12608
|
studioLayerData,
|
|
12609
|
+
styleAt,
|
|
12610
|
+
styleOf,
|
|
12613
12611
|
styledTextOf,
|
|
12612
|
+
textRunsOf,
|
|
12614
12613
|
tiltLane,
|
|
12615
12614
|
tiltTrackFromDoc,
|
|
12615
|
+
toggleStyle,
|
|
12616
12616
|
tokenRanges,
|
|
12617
12617
|
traceKindOf,
|
|
12618
12618
|
transitionMult,
|