@vosjs/studio-core 0.24.1 → 0.26.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/README.md +2 -2
- package/dist/index.d.ts +242 -57
- package/dist/index.js +555 -326
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -236,6 +236,210 @@ var MEDIA_FRAME_KEYS = [
|
|
|
236
236
|
"focusFollow"
|
|
237
237
|
];
|
|
238
238
|
|
|
239
|
+
// src/callout.ts
|
|
240
|
+
var CALLOUT_SHAPES = ["tag", "note", "code"];
|
|
241
|
+
var CALLOUT_TYPE_RATIO = 1.35;
|
|
242
|
+
var CALLOUT_DEFAULT_BODY_PX = 14;
|
|
243
|
+
var HOUSE_REGISTER = {
|
|
244
|
+
ground: "#ffffff",
|
|
245
|
+
accent: "#7c3aed"
|
|
246
|
+
};
|
|
247
|
+
var DEFAULT_FACE = "'Inter',-apple-system,system-ui,sans-serif";
|
|
248
|
+
var DEFAULT_MONO = "'JetBrains Mono',ui-monospace,monospace";
|
|
249
|
+
function calloutLook(register, scale = 1) {
|
|
250
|
+
const ground = hexToRgb(register.ground) ?? [255, 255, 255];
|
|
251
|
+
const accent = hexToRgb(register.accent) ?? [124, 58, 237];
|
|
252
|
+
const light = relativeLuminance(ground) > 0.4;
|
|
253
|
+
const [h, s] = rgbToHsl(accent);
|
|
254
|
+
const cardGround = light ? hslToHex(h, Math.min(0.35, s * 0.6), 0.07) : hslToHex(h, Math.min(0.25, s * 0.35), 0.97);
|
|
255
|
+
const bodyPx = register.body ?? CALLOUT_DEFAULT_BODY_PX;
|
|
256
|
+
const titlePx = Math.max(20, Math.round(CALLOUT_TYPE_RATIO * bodyPx * scale));
|
|
257
|
+
return {
|
|
258
|
+
light,
|
|
259
|
+
cardGround,
|
|
260
|
+
cardInk: light ? "#fafafa" : "#101012",
|
|
261
|
+
cardMuted: light ? "rgba(255,255,255,0.64)" : "rgba(16,16,18,0.6)",
|
|
262
|
+
hairline: light ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.1)",
|
|
263
|
+
accent: rgbToHex(accent),
|
|
264
|
+
shadow: "0 24px 60px rgba(0,0,0,0.35), 0 2px 6px rgba(0,0,0,0.16)",
|
|
265
|
+
face: register.face ? quoteFamily(register.face) : DEFAULT_FACE,
|
|
266
|
+
mono: register.mono ? quoteFamily(register.mono) : DEFAULT_MONO,
|
|
267
|
+
kickerPx: Math.max(11, Math.round(titlePx * 0.55)),
|
|
268
|
+
titlePx,
|
|
269
|
+
bodyPx: Math.max(15, Math.round(titlePx * 0.75)),
|
|
270
|
+
codePx: Math.max(14, Math.round(titlePx * 0.68)),
|
|
271
|
+
radius: 16,
|
|
272
|
+
padX: 24,
|
|
273
|
+
padY: 20
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function calloutBox(shape, look, words) {
|
|
277
|
+
const em = (px, chars, adv) => Math.ceil(px * adv * chars);
|
|
278
|
+
if (shape === "tag") {
|
|
279
|
+
const text = words.kicker ?? words.title ?? "";
|
|
280
|
+
const w = em(look.kickerPx * 1.1, text.length, 0.62) + 14 + 18 * 2;
|
|
281
|
+
return {
|
|
282
|
+
width: clamp(w, 120, 720),
|
|
283
|
+
height: Math.max(36, Math.round(look.kickerPx * 1.1 * 2.6))
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (shape === "code") {
|
|
287
|
+
const lines = (words.code ?? "").split("\n");
|
|
288
|
+
const longest = Math.max(1, ...lines.map((l) => l.length));
|
|
289
|
+
const lineH = Math.round(look.codePx * 1.7);
|
|
290
|
+
const w = em(look.codePx, longest, 0.62) + look.padX * 2;
|
|
291
|
+
const h = lines.length * lineH + look.padY * 2 + (words.kicker ? look.kickerPx * 2 : 0);
|
|
292
|
+
return { width: clamp(w, 360, 820), height: clamp(h, 72, 640) };
|
|
293
|
+
}
|
|
294
|
+
const title = words.title ?? "";
|
|
295
|
+
const body = words.body ?? "";
|
|
296
|
+
const titleW = em(look.titlePx, title.length, 0.56);
|
|
297
|
+
const width = clamp(Math.max(titleW, 320) + look.padX * 2, 360, 640);
|
|
298
|
+
const inner = width - look.padX * 2;
|
|
299
|
+
const bodyLines = body ? Math.max(1, Math.ceil(em(look.bodyPx, body.length, 0.54) / inner)) : 0;
|
|
300
|
+
const titleLines = Math.max(1, Math.ceil(titleW / inner));
|
|
301
|
+
const height = look.padY * 2 + (words.kicker ? Math.round(look.kickerPx * 1.6) : 0) + titleLines * Math.round(look.titlePx * 1.25) + (bodyLines ? 8 + bodyLines * Math.round(look.bodyPx * 1.5) : 0);
|
|
302
|
+
return { width, height: clamp(height, 64, 480) };
|
|
303
|
+
}
|
|
304
|
+
function calloutSource(shape, look, words, box) {
|
|
305
|
+
const base = `box-sizing:border-box;width:${box.width}px;height:${box.height}px;background:${look.cardGround};color:${look.cardInk};border:1px solid ${look.hairline};border-radius:${look.radius}px;box-shadow:${look.shadow};-webkit-font-smoothing:antialiased;`;
|
|
306
|
+
if (shape === "tag") {
|
|
307
|
+
const text = esc(words.kicker ?? words.title ?? "");
|
|
308
|
+
return {
|
|
309
|
+
html: `<div class="tag"><span class="dot"></span><span class="t">${text}</span></div>`,
|
|
310
|
+
css: `.tag{font-family:${look.face};display:flex;align-items:center;gap:10px;${base}border-radius:999px;padding:0 18px;background:${look.accent};color:#fff;border-color:rgba(255,255,255,0.18);font-size:${Math.round(look.kickerPx * 1.1)}px;font-weight:600;letter-spacing:0.02em;text-transform:uppercase;white-space:nowrap}.dot{width:8px;height:8px;border-radius:99px;background:#fff;opacity:0.9;flex:none}`
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (shape === "code") {
|
|
314
|
+
const lines = (words.code ?? "").split("\n").map(
|
|
315
|
+
(l) => /^\s*(\/\/|#|\$)/.test(l) ? `<span class="mut">${esc(l)}</span>` : esc(l)
|
|
316
|
+
).join("\n");
|
|
317
|
+
const kicker2 = words.kicker ? `<div class="k">${esc(words.kicker)}</div>` : "";
|
|
318
|
+
return {
|
|
319
|
+
html: `<div class="code">${kicker2}<pre class="p">${lines}</pre></div>`,
|
|
320
|
+
css: `.code{font-family:${look.mono};${base}padding:${look.padY}px ${look.padX}px;font-size:${look.codePx}px;line-height:1.7;letter-spacing:-0.01em}.k{font-family:${look.face};font-size:${look.kickerPx}px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:${look.accent};margin:0 0 ${Math.round(look.kickerPx * 0.9)}px}.p{margin:0;white-space:pre;font:inherit}.mut{color:${look.cardMuted}}`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const kicker = words.kicker ? `<div class="k"><span class="dot"></span>${esc(words.kicker)}</div>` : "";
|
|
324
|
+
const body = words.body ? `<div class="b">${esc(words.body)}</div>` : "";
|
|
325
|
+
return {
|
|
326
|
+
html: `<div class="note">${kicker}<div class="t">${esc(words.title ?? "")}</div>${body}</div>`,
|
|
327
|
+
css: `.note{font-family:${look.face};${base}padding:${look.padY}px ${look.padX}px;display:flex;flex-direction:column;justify-content:center}.k{display:flex;align-items:center;gap:8px;font-size:${look.kickerPx}px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:${look.accent};margin-bottom:${Math.round(look.kickerPx * 0.6)}px}.dot{width:7px;height:7px;border-radius:99px;background:${look.accent};box-shadow:0 0 10px ${look.accent}}.t{font-size:${look.titlePx}px;font-weight:600;line-height:1.25;letter-spacing:-0.01em}.b{margin-top:8px;font-size:${look.bodyPx}px;line-height:1.5;color:${look.cardMuted}}`
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function calloutClip(shape, register, words, opts) {
|
|
331
|
+
const look = calloutLook(register, opts.scale ?? 1);
|
|
332
|
+
const box = calloutBox(shape, look, words);
|
|
333
|
+
const { html, css } = calloutSource(shape, look, words, box);
|
|
334
|
+
return {
|
|
335
|
+
id: opts.id,
|
|
336
|
+
kind: "html",
|
|
337
|
+
start: opts.start,
|
|
338
|
+
duration: opts.duration,
|
|
339
|
+
html,
|
|
340
|
+
css,
|
|
341
|
+
box,
|
|
342
|
+
transform: {
|
|
343
|
+
x: opts.at?.x ?? 0.5,
|
|
344
|
+
y: opts.at?.y ?? 0.5,
|
|
345
|
+
scale: 1,
|
|
346
|
+
rotation: 0
|
|
347
|
+
},
|
|
348
|
+
anim: { enter: "rise", exit: "fade" },
|
|
349
|
+
...opts.pin ? { pin: opts.pin } : {}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function calloutGroundOf(css) {
|
|
353
|
+
const m = /background(?:-color)?\s*:\s*(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\))/.exec(css);
|
|
354
|
+
if (!m) return null;
|
|
355
|
+
const rgb = parseCssColour(m[1]);
|
|
356
|
+
return rgb ? rgbToHex(rgb) : null;
|
|
357
|
+
}
|
|
358
|
+
function hexToRgb(hex) {
|
|
359
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim());
|
|
360
|
+
if (!m) return null;
|
|
361
|
+
let h = m[1];
|
|
362
|
+
if (h.length === 3)
|
|
363
|
+
h = h.split("").map((c) => c + c).join("");
|
|
364
|
+
return [
|
|
365
|
+
parseInt(h.slice(0, 2), 16),
|
|
366
|
+
parseInt(h.slice(2, 4), 16),
|
|
367
|
+
parseInt(h.slice(4, 6), 16)
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
function rgbToHex([r, g, b]) {
|
|
371
|
+
const c = (v) => Math.round(clamp(v, 0, 255)).toString(16).padStart(2, "0");
|
|
372
|
+
return `#${c(r)}${c(g)}${c(b)}`;
|
|
373
|
+
}
|
|
374
|
+
function parseCssColour(s) {
|
|
375
|
+
const hex = hexToRgb(s);
|
|
376
|
+
if (hex) return hex;
|
|
377
|
+
const m = /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/.exec(s);
|
|
378
|
+
if (!m) return null;
|
|
379
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
380
|
+
}
|
|
381
|
+
function relativeLuminance([r, g, b]) {
|
|
382
|
+
const lin = (v) => {
|
|
383
|
+
const c = v / 255;
|
|
384
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
385
|
+
};
|
|
386
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
387
|
+
}
|
|
388
|
+
function rgbToHsl([r, g, b]) {
|
|
389
|
+
const rr = r / 255;
|
|
390
|
+
const gg = g / 255;
|
|
391
|
+
const bb = b / 255;
|
|
392
|
+
const max = Math.max(rr, gg, bb);
|
|
393
|
+
const min = Math.min(rr, gg, bb);
|
|
394
|
+
const l = (max + min) / 2;
|
|
395
|
+
if (max === min) return [0, 0, l];
|
|
396
|
+
const d = max - min;
|
|
397
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
398
|
+
let h = 0;
|
|
399
|
+
if (max === rr) h = (gg - bb) / d + (gg < bb ? 6 : 0);
|
|
400
|
+
else if (max === gg) h = (bb - rr) / d + 2;
|
|
401
|
+
else h = (rr - gg) / d + 4;
|
|
402
|
+
return [h / 6, s, l];
|
|
403
|
+
}
|
|
404
|
+
function hslToHex(h, s, l) {
|
|
405
|
+
const f = (n) => {
|
|
406
|
+
const k = (n + h * 12) % 12;
|
|
407
|
+
const a = s * Math.min(l, 1 - l);
|
|
408
|
+
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
409
|
+
};
|
|
410
|
+
return rgbToHex([f(0) * 255, f(8) * 255, f(4) * 255]);
|
|
411
|
+
}
|
|
412
|
+
function deltaE(a, b) {
|
|
413
|
+
const la = rgbToLab(a);
|
|
414
|
+
const lb = rgbToLab(b);
|
|
415
|
+
return Math.hypot(la[0] - lb[0], la[1] - lb[1], la[2] - lb[2]);
|
|
416
|
+
}
|
|
417
|
+
function rgbToLab([r, g, b]) {
|
|
418
|
+
const lin = (v) => {
|
|
419
|
+
const c = v / 255;
|
|
420
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
421
|
+
};
|
|
422
|
+
const R = lin(r);
|
|
423
|
+
const G = lin(g);
|
|
424
|
+
const B = lin(b);
|
|
425
|
+
const x = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
|
|
426
|
+
const y = R * 0.2126 + G * 0.7152 + B * 0.0722;
|
|
427
|
+
const z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 1.08883;
|
|
428
|
+
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
429
|
+
return [116 * f(y) - 16, 500 * (f(x) - f(y)), 200 * (f(y) - f(z))];
|
|
430
|
+
}
|
|
431
|
+
function quoteFamily(family) {
|
|
432
|
+
const first = family.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
|
|
433
|
+
const rest = family.includes(",") ? family.slice(family.indexOf(",") + 1).trim() : /mono/i.test(first) ? "ui-monospace,monospace" : "-apple-system,system-ui,sans-serif";
|
|
434
|
+
return /^[a-z-]+$/i.test(first) && /^(ui-|system-ui|sans-serif|serif|monospace)/.test(first) ? `${first},${rest}` : `'${first}',${rest}`;
|
|
435
|
+
}
|
|
436
|
+
function esc(s) {
|
|
437
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
438
|
+
}
|
|
439
|
+
function clamp(v, lo, hi) {
|
|
440
|
+
return Math.max(lo, Math.min(hi, v));
|
|
441
|
+
}
|
|
442
|
+
|
|
239
443
|
// src/capture.ts
|
|
240
444
|
var CHROME_TOP_MIN = 20;
|
|
241
445
|
var CHROME_TOP_MAX = 220;
|
|
@@ -1056,362 +1260,226 @@ function outputEnd(doc, footageEnd) {
|
|
|
1056
1260
|
return Math.max(footageEnd, lastLayerEnd(doc));
|
|
1057
1261
|
}
|
|
1058
1262
|
|
|
1059
|
-
// src/docVersion.ts
|
|
1060
|
-
var DOC_SCHEMA_VERSION = 4;
|
|
1061
|
-
function migrateHostedDoc(raw) {
|
|
1062
|
-
const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
|
|
1063
|
-
if (version >= DOC_SCHEMA_VERSION) return raw;
|
|
1064
|
-
let doc = raw;
|
|
1065
|
-
if (version < 4 && doc.source && typeof doc.source === "object") {
|
|
1066
|
-
doc = migrateMotion(doc);
|
|
1067
|
-
}
|
|
1068
|
-
return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// src/doc/studioDoc.ts
|
|
1072
|
-
var anchorKindOf = (doc) => "source" in doc ? "recording" : "program";
|
|
1073
|
-
var isRecordingDoc = (doc) => "source" in doc;
|
|
1074
|
-
var isProgramDoc = (doc) => !("source" in doc);
|
|
1075
|
-
function programDuration(doc) {
|
|
1076
|
-
const own = doc.program.duration;
|
|
1077
|
-
if (typeof own === "number" && own > 0) return own;
|
|
1078
|
-
const cfg = doc.program.config.duration;
|
|
1079
|
-
return typeof cfg === "number" && cfg > 0 ? cfg : 0;
|
|
1080
|
-
}
|
|
1081
|
-
function anchorSourceDuration(doc) {
|
|
1082
|
-
return isRecordingDoc(doc) ? doc.source.meta.durationMs / 1e3 : programDuration(doc);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// src/planner/smoothing.ts
|
|
1086
|
-
var SNAP_BEFORE = 0.12;
|
|
1087
|
-
var SNAP_AFTER = 0.18;
|
|
1088
|
-
var SNAP_GAIN = 0.5;
|
|
1089
|
-
function sampleAt(points, t) {
|
|
1090
|
-
if (points.length === 0) return { x: 0, y: 0 };
|
|
1091
|
-
if (t <= points[0].t) return { x: points[0].x, y: points[0].y };
|
|
1092
|
-
const last = points[points.length - 1];
|
|
1093
|
-
if (t >= last.t) return { x: last.x, y: last.y };
|
|
1094
|
-
for (let i = 1; i < points.length; i++) {
|
|
1095
|
-
if (points[i].t >= t) {
|
|
1096
|
-
const a = points[i - 1];
|
|
1097
|
-
const b = points[i];
|
|
1098
|
-
const f = (t - a.t) / (b.t - a.t || 1);
|
|
1099
|
-
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
return { x: last.x, y: last.y };
|
|
1103
|
-
}
|
|
1104
|
-
function smoothCursor(track, options = {}) {
|
|
1105
|
-
const factor = clamp(options.factor ?? 0.15, 0.01, 1);
|
|
1106
|
-
const fps = options.fps ?? 60;
|
|
1107
|
-
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 }));
|
|
1108
|
-
if (raw.length === 0) return [];
|
|
1109
|
-
const clicks = options.clickSnap ? track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y })) : [];
|
|
1110
|
-
const start = raw[0].t;
|
|
1111
|
-
const end = raw[raw.length - 1].t;
|
|
1112
|
-
const step = 1 / fps;
|
|
1113
|
-
const out = [];
|
|
1114
|
-
let cx = raw[0].x;
|
|
1115
|
-
let cy = raw[0].y;
|
|
1116
|
-
let ci = 0;
|
|
1117
|
-
for (let t = start; t <= end + 1e-9; t += step) {
|
|
1118
|
-
const target = sampleAt(raw, t);
|
|
1119
|
-
cx += (target.x - cx) * factor;
|
|
1120
|
-
cy += (target.y - cy) * factor;
|
|
1121
|
-
if (clicks.length) {
|
|
1122
|
-
while (ci < clicks.length - 1 && t > clicks[ci].t + SNAP_AFTER) ci++;
|
|
1123
|
-
const ck = clicks[ci];
|
|
1124
|
-
const u = clamp(
|
|
1125
|
-
(t - (ck.t - SNAP_BEFORE)) / SNAP_BEFORE,
|
|
1126
|
-
0,
|
|
1127
|
-
t <= ck.t + SNAP_AFTER ? 1 : 0
|
|
1128
|
-
);
|
|
1129
|
-
const w = u * u * (3 - 2 * u) * SNAP_GAIN;
|
|
1130
|
-
if (w > 0) {
|
|
1131
|
-
cx += (ck.x - cx) * w;
|
|
1132
|
-
cy += (ck.y - cy) * w;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
out.push({ t, x: cx, y: cy });
|
|
1136
|
-
}
|
|
1137
|
-
return out;
|
|
1138
|
-
}
|
|
1139
|
-
function clamp(v, lo, hi) {
|
|
1140
|
-
return Math.max(lo, Math.min(hi, v));
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
1263
|
// src/zoomStyle.ts
|
|
1144
|
-
var
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1264
|
+
var RETIRED_ZOOM_STYLES = {
|
|
1265
|
+
keynote: { style: "glide", tilt: "medium" },
|
|
1266
|
+
drift: { style: "cinema", tilt: "subtle" }
|
|
1267
|
+
};
|
|
1268
|
+
function liveZoomStyleName(name) {
|
|
1269
|
+
if (!name) return null;
|
|
1270
|
+
if (name in ZOOM_STYLES) return name;
|
|
1271
|
+
return RETIRED_ZOOM_STYLES[name]?.style ?? null;
|
|
1272
|
+
}
|
|
1273
|
+
var PUMP_REST = 1;
|
|
1274
|
+
function pumpFreeChainGap(p) {
|
|
1275
|
+
return p.rampOut + (p.rampIn - p.rampInOverlap) + PUMP_REST;
|
|
1276
|
+
}
|
|
1277
|
+
var glide = {
|
|
1278
|
+
autoZoom: true,
|
|
1279
|
+
clusterGap: 3,
|
|
1280
|
+
minClusterClicks: 1,
|
|
1281
|
+
targetFill: 0.42,
|
|
1282
|
+
minLevel: 1.3,
|
|
1283
|
+
maxLevel: 1.8,
|
|
1284
|
+
dragLevel: 1.5,
|
|
1285
|
+
lead: 0.35,
|
|
1286
|
+
hold: 1.2,
|
|
1287
|
+
followByDefault: true,
|
|
1288
|
+
typingZoom: true,
|
|
1289
|
+
typingGap: 2.5,
|
|
1290
|
+
typingHold: 1.1,
|
|
1291
|
+
typingMinLevel: 1.4,
|
|
1292
|
+
rampIn: 0.6,
|
|
1293
|
+
rampInOverlap: 0.2,
|
|
1294
|
+
rampOut: 0.5,
|
|
1295
|
+
chainGap: 3,
|
|
1296
|
+
pan: 0.55,
|
|
1297
|
+
// The reference's corner trace at 60 fps: a quarter of the move by a
|
|
1298
|
+
// fifth of the time, two thirds by half, nine tenths by three
|
|
1299
|
+
// quarters, over ~0.6 s. Fitted against those three points,
|
|
1300
|
+
// (0.36, 0, 0.4, 1) is the closest cubic (15 / 64 / 93 %; half the
|
|
1301
|
+
// move at ~245 ms and 90 % at ~425 ms at 0.6 s): a gentle start and a
|
|
1302
|
+
// long settle. CSS ease (0.25, 0.1, 0.25, 1) reached the midpoint
|
|
1303
|
+
// 70 ms earlier and read a touch fast; (0.16, 1, 0.3, 1) put half the
|
|
1304
|
+
// move in the first sixty milliseconds and read as a twitch.
|
|
1305
|
+
ease: "css-bezier(0.36, 0, 0.4, 1)",
|
|
1306
|
+
panEase: "css-bezier(0.36, 0, 0.4, 1)",
|
|
1307
|
+
followSafeRatio: 0.45,
|
|
1308
|
+
followRecenter: 0.55,
|
|
1309
|
+
followLookahead: 0.4,
|
|
1310
|
+
tilt: {
|
|
1311
|
+
intensity: "off",
|
|
1167
1312
|
rampIn: 0.6,
|
|
1168
|
-
rampInOverlap: 0.2,
|
|
1169
1313
|
rampOut: 0.5,
|
|
1170
1314
|
chainGap: 3,
|
|
1171
|
-
pan: 0.55
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
followRecenter: 0.55,
|
|
1184
|
-
followLookahead: 0.4,
|
|
1185
|
-
tilt: { intensity: "off" }
|
|
1186
|
-
},
|
|
1187
|
-
// Screen Studio "Focused" / Recordly — block zooms, spring-settled, readable.
|
|
1315
|
+
pan: 0.55
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
var ZOOM_STYLES = {
|
|
1319
|
+
glide,
|
|
1320
|
+
// Screen Studio "Focused" / Recordly — a block per click, framed and
|
|
1321
|
+
// released, no travel. The spring-settled family ease (near-linear onset,
|
|
1322
|
+
// long settle) at the default's tempo: 0.8 s in, 0.65 s out, and a beat
|
|
1323
|
+
// of 1.5 s after the click so the block is read before it goes. At its
|
|
1324
|
+
// 2026-07 timing (1.4 s in, 1.0 s out around a 1.35 s span) a lone click
|
|
1325
|
+
// spent 74% of its beat moving and every dwell or sped-up click was
|
|
1326
|
+
// shorter than the ramp, which the lowering compressed into a cut.
|
|
1188
1327
|
focus: {
|
|
1189
1328
|
autoZoom: true,
|
|
1190
|
-
clusterGap: 1.
|
|
1329
|
+
clusterGap: 1.5,
|
|
1191
1330
|
minClusterClicks: 1,
|
|
1192
1331
|
targetFill: 0.5,
|
|
1193
|
-
minLevel: 1.
|
|
1194
|
-
maxLevel: 2
|
|
1195
|
-
dragLevel: 1.
|
|
1332
|
+
minLevel: 1.4,
|
|
1333
|
+
maxLevel: 2,
|
|
1334
|
+
dragLevel: 1.6,
|
|
1196
1335
|
lead: 0.35,
|
|
1197
|
-
hold: 1,
|
|
1336
|
+
hold: 1.5,
|
|
1198
1337
|
followByDefault: false,
|
|
1199
1338
|
typingZoom: true,
|
|
1200
1339
|
typingGap: 2,
|
|
1201
|
-
typingHold: 1.
|
|
1202
|
-
typingMinLevel: 1.
|
|
1203
|
-
rampIn:
|
|
1204
|
-
rampInOverlap: 0.
|
|
1205
|
-
rampOut:
|
|
1206
|
-
chainGap:
|
|
1207
|
-
pan:
|
|
1340
|
+
typingHold: 1.3,
|
|
1341
|
+
typingMinLevel: 1.5,
|
|
1342
|
+
rampIn: 0.8,
|
|
1343
|
+
rampInOverlap: 0.25,
|
|
1344
|
+
rampOut: 0.65,
|
|
1345
|
+
chainGap: 2.4,
|
|
1346
|
+
pan: 0.7,
|
|
1208
1347
|
ease: "css-bezier(0.28, 0.03, 0.09, 1)",
|
|
1209
1348
|
panEase: "css-bezier(0.26, 0.08, 0.2, 1)",
|
|
1210
1349
|
followSafeRatio: 0.5,
|
|
1211
|
-
followRecenter: 0.
|
|
1350
|
+
followRecenter: 0.55,
|
|
1212
1351
|
followLookahead: 0,
|
|
1213
|
-
tilt: {
|
|
1352
|
+
tilt: {
|
|
1353
|
+
intensity: "off",
|
|
1354
|
+
rampIn: 0.8,
|
|
1355
|
+
rampOut: 0.65,
|
|
1356
|
+
chainGap: 2.4,
|
|
1357
|
+
pan: 0.7
|
|
1358
|
+
}
|
|
1214
1359
|
},
|
|
1215
|
-
// Screen Studio "Smooth" —
|
|
1360
|
+
// Screen Studio "Smooth" — the long-HOLD style: 1.2 s in, 1.0 s out,
|
|
1361
|
+
// two-second holds, chains across 3.5 s so a slow clip travels rather
|
|
1362
|
+
// than pumps, and the default's level range, because a watched clip
|
|
1363
|
+
// wants the frame, not the magnifier. Its ramps still exceed a dwell or
|
|
1364
|
+
// a sped-up click; the lowering fits them to the room.
|
|
1216
1365
|
cinema: {
|
|
1217
1366
|
autoZoom: true,
|
|
1218
|
-
clusterGap:
|
|
1367
|
+
clusterGap: 2.4,
|
|
1219
1368
|
minClusterClicks: 1,
|
|
1220
|
-
targetFill: 0.
|
|
1221
|
-
minLevel: 1.
|
|
1222
|
-
maxLevel:
|
|
1369
|
+
targetFill: 0.42,
|
|
1370
|
+
minLevel: 1.3,
|
|
1371
|
+
maxLevel: 1.8,
|
|
1223
1372
|
dragLevel: 1.5,
|
|
1224
1373
|
lead: 0.6,
|
|
1225
|
-
hold:
|
|
1374
|
+
hold: 2,
|
|
1226
1375
|
followByDefault: true,
|
|
1227
1376
|
typingZoom: true,
|
|
1228
1377
|
typingGap: 3,
|
|
1229
|
-
typingHold: 1.
|
|
1230
|
-
typingMinLevel: 1.
|
|
1231
|
-
rampIn: 1.
|
|
1232
|
-
rampInOverlap: 0.
|
|
1233
|
-
rampOut: 1
|
|
1234
|
-
chainGap:
|
|
1235
|
-
pan: 1.
|
|
1378
|
+
typingHold: 1.8,
|
|
1379
|
+
typingMinLevel: 1.4,
|
|
1380
|
+
rampIn: 1.2,
|
|
1381
|
+
rampInOverlap: 0.4,
|
|
1382
|
+
rampOut: 1,
|
|
1383
|
+
chainGap: 3.5,
|
|
1384
|
+
pan: 1.1,
|
|
1236
1385
|
ease: "css-bezier(0.33, 0, 0.15, 1)",
|
|
1237
1386
|
panEase: "css-bezier(0.33, 0, 0.22, 1)",
|
|
1238
1387
|
followSafeRatio: 0.6,
|
|
1239
|
-
followRecenter:
|
|
1388
|
+
followRecenter: 0.9,
|
|
1240
1389
|
followLookahead: 0.5,
|
|
1241
|
-
tilt: {
|
|
1390
|
+
tilt: {
|
|
1391
|
+
intensity: "off",
|
|
1392
|
+
rampIn: 1.2,
|
|
1393
|
+
rampOut: 1,
|
|
1394
|
+
chainGap: 3.5,
|
|
1395
|
+
pan: 1.1
|
|
1396
|
+
}
|
|
1242
1397
|
},
|
|
1243
|
-
// The studio's original pace with the instant-velocity onset defect fixed
|
|
1398
|
+
// The studio's original pace with the instant-velocity onset defect fixed:
|
|
1399
|
+
// 0.4 s cycles on the onset-capped ease, the 2.5× ceiling of the 2026-07
|
|
1400
|
+
// comparison's defect two brought to 2.2, a chain gap past its pump band.
|
|
1244
1401
|
snappy: {
|
|
1245
1402
|
autoZoom: true,
|
|
1246
1403
|
clusterGap: 1.2,
|
|
1247
1404
|
minClusterClicks: 1,
|
|
1248
1405
|
targetFill: 0.5,
|
|
1249
1406
|
minLevel: 1.4,
|
|
1250
|
-
maxLevel: 2.
|
|
1251
|
-
dragLevel: 1.
|
|
1407
|
+
maxLevel: 2.2,
|
|
1408
|
+
dragLevel: 1.6,
|
|
1252
1409
|
lead: 0.25,
|
|
1253
|
-
hold: 1,
|
|
1410
|
+
hold: 1.2,
|
|
1254
1411
|
followByDefault: false,
|
|
1255
1412
|
typingZoom: true,
|
|
1256
1413
|
typingGap: 1.6,
|
|
1257
|
-
typingHold: 0.
|
|
1414
|
+
typingHold: 0.9,
|
|
1258
1415
|
typingMinLevel: 1.5,
|
|
1259
|
-
rampIn: 0.
|
|
1260
|
-
rampInOverlap: 0.
|
|
1261
|
-
rampOut: 0.
|
|
1262
|
-
chainGap:
|
|
1263
|
-
pan: 0.
|
|
1416
|
+
rampIn: 0.4,
|
|
1417
|
+
rampInOverlap: 0.15,
|
|
1418
|
+
rampOut: 0.4,
|
|
1419
|
+
chainGap: 2,
|
|
1420
|
+
pan: 0.5,
|
|
1264
1421
|
ease: "css-bezier(0.3, 0.55, 0.2, 1)",
|
|
1265
1422
|
panEase: "css-bezier(0.25, 0.1, 0.25, 1)",
|
|
1266
1423
|
followSafeRatio: 0.5,
|
|
1267
|
-
followRecenter: 0.
|
|
1424
|
+
followRecenter: 0.45,
|
|
1268
1425
|
followLookahead: 0,
|
|
1269
|
-
tilt: {
|
|
1426
|
+
tilt: {
|
|
1427
|
+
intensity: "off",
|
|
1428
|
+
rampIn: 0.4,
|
|
1429
|
+
rampOut: 0.4,
|
|
1430
|
+
chainGap: 2,
|
|
1431
|
+
pan: 0.5
|
|
1432
|
+
}
|
|
1270
1433
|
},
|
|
1271
|
-
// Screen Studio's "instant zoom" — a cut-in (
|
|
1434
|
+
// Screen Studio's "instant zoom" — a cut-in (eight frames at 60 fps),
|
|
1435
|
+
// tutorial tempo. Untouched in motion (the one style that never pumped
|
|
1436
|
+
// and never left a ramp unsettled); the ceiling comes down with the
|
|
1437
|
+
// others and the hold gains the beat a cut-in needs to be read.
|
|
1272
1438
|
cut: {
|
|
1273
1439
|
autoZoom: true,
|
|
1274
1440
|
clusterGap: 1.2,
|
|
1275
1441
|
minClusterClicks: 1,
|
|
1276
1442
|
targetFill: 0.5,
|
|
1277
|
-
minLevel: 1.
|
|
1278
|
-
maxLevel: 2
|
|
1279
|
-
dragLevel: 1.
|
|
1443
|
+
minLevel: 1.4,
|
|
1444
|
+
maxLevel: 2,
|
|
1445
|
+
dragLevel: 1.6,
|
|
1280
1446
|
lead: 0.2,
|
|
1281
|
-
hold: 1,
|
|
1447
|
+
hold: 1.2,
|
|
1282
1448
|
followByDefault: false,
|
|
1283
1449
|
typingZoom: true,
|
|
1284
1450
|
typingGap: 1.2,
|
|
1285
1451
|
typingHold: 0.6,
|
|
1286
|
-
typingMinLevel: 1.
|
|
1452
|
+
typingMinLevel: 1.5,
|
|
1287
1453
|
rampIn: 0.14,
|
|
1288
1454
|
rampInOverlap: 0.07,
|
|
1289
1455
|
rampOut: 0.14,
|
|
1290
|
-
chainGap: 1.
|
|
1456
|
+
chainGap: 1.3,
|
|
1291
1457
|
pan: 0.35,
|
|
1292
1458
|
ease: "css-bezier(0.2, 0, 0.4, 1)",
|
|
1293
1459
|
panEase: "css-bezier(0.2, 0, 0.4, 1)",
|
|
1294
1460
|
followSafeRatio: 0.5,
|
|
1295
1461
|
followRecenter: 0.35,
|
|
1296
1462
|
followLookahead: 0,
|
|
1297
|
-
tilt: { intensity: "off" }
|
|
1298
|
-
},
|
|
1299
|
-
// Auto-zoom off (every competitor ships this switch). Camera params still
|
|
1300
|
-
// apply to MANUAL spans — they get the default (glide) motion.
|
|
1301
|
-
none: {
|
|
1302
|
-
autoZoom: false,
|
|
1303
|
-
clusterGap: 3,
|
|
1304
|
-
minClusterClicks: 2,
|
|
1305
|
-
targetFill: 0.42,
|
|
1306
|
-
minLevel: 1.3,
|
|
1307
|
-
maxLevel: 1.8,
|
|
1308
|
-
dragLevel: 1.5,
|
|
1309
|
-
lead: 0.5,
|
|
1310
|
-
hold: 0.6,
|
|
1311
|
-
followByDefault: false,
|
|
1312
|
-
typingZoom: true,
|
|
1313
|
-
typingGap: 2.5,
|
|
1314
|
-
typingHold: 1.1,
|
|
1315
|
-
typingMinLevel: 1.4,
|
|
1316
|
-
rampIn: 1.1,
|
|
1317
|
-
rampInOverlap: 0.35,
|
|
1318
|
-
rampOut: 1,
|
|
1319
|
-
chainGap: 2.5,
|
|
1320
|
-
pan: 1,
|
|
1321
|
-
ease: "css-bezier(0.26, 0, 0.16, 1)",
|
|
1322
|
-
panEase: "css-bezier(0.3, 0, 0.2, 1)",
|
|
1323
|
-
followSafeRatio: 0.45,
|
|
1324
|
-
followRecenter: 0.8,
|
|
1325
|
-
followLookahead: 0,
|
|
1326
|
-
tilt: { intensity: "off" }
|
|
1327
|
-
},
|
|
1328
|
-
// ── tilt-forward styles — the camera moves in DEPTH too.
|
|
1329
|
-
// Research note (2026-08-03): no competitor ships a document-wide personality
|
|
1330
|
-
// driving BOTH auto-zoom dynamics and focus-following tilt from one name —
|
|
1331
|
-
// FocuSee's Subtle/Default/Strong 3D Motion is a separate layered effect,
|
|
1332
|
-
// TiltIt/ScreenDrift sell per-clip templates. These two own that space.
|
|
1333
|
-
//
|
|
1334
|
-
// "keynote": the launch-film sentence (the Apple register: rehearsed,
|
|
1335
|
-
// restrained, one move per beat) — glide's session-merged, modest zooms plus
|
|
1336
|
-
// a MEDIUM lean toward each zoom's focus, tilt ramps MATCHED to the zoom
|
|
1337
|
-
// ramps so lean and zoom read as one camera move; chained so back-to-back
|
|
1338
|
-
// beats swing pose-to-pose. Never oscillation, ±5..18° band.
|
|
1339
|
-
keynote: {
|
|
1340
|
-
autoZoom: true,
|
|
1341
|
-
clusterGap: 3,
|
|
1342
|
-
minClusterClicks: 1,
|
|
1343
|
-
targetFill: 0.42,
|
|
1344
|
-
minLevel: 1.3,
|
|
1345
|
-
maxLevel: 1.8,
|
|
1346
|
-
dragLevel: 1.5,
|
|
1347
|
-
lead: 0.5,
|
|
1348
|
-
hold: 0.7,
|
|
1349
|
-
followByDefault: true,
|
|
1350
|
-
typingZoom: true,
|
|
1351
|
-
typingGap: 2.5,
|
|
1352
|
-
typingHold: 1.2,
|
|
1353
|
-
typingMinLevel: 1.4,
|
|
1354
|
-
rampIn: 1.2,
|
|
1355
|
-
rampInOverlap: 0.35,
|
|
1356
|
-
rampOut: 1.1,
|
|
1357
|
-
chainGap: 2.5,
|
|
1358
|
-
pan: 1.1,
|
|
1359
|
-
ease: "css-bezier(0.26, 0, 0.16, 1)",
|
|
1360
|
-
panEase: "css-bezier(0.3, 0, 0.2, 1)",
|
|
1361
|
-
followSafeRatio: 0.45,
|
|
1362
|
-
followRecenter: 0.8,
|
|
1363
|
-
followLookahead: 0.4,
|
|
1364
|
-
// Lean lands WITH the zoom (matched ramps read as one camera move).
|
|
1365
1463
|
tilt: {
|
|
1366
|
-
intensity: "
|
|
1367
|
-
rampIn:
|
|
1368
|
-
rampOut:
|
|
1369
|
-
chainGap:
|
|
1370
|
-
pan:
|
|
1464
|
+
intensity: "off",
|
|
1465
|
+
rampIn: 0.3,
|
|
1466
|
+
rampOut: 0.3,
|
|
1467
|
+
chainGap: 1.3,
|
|
1468
|
+
pan: 0.35
|
|
1371
1469
|
}
|
|
1372
1470
|
},
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
|
|
1377
|
-
drift: {
|
|
1378
|
-
autoZoom: true,
|
|
1379
|
-
clusterGap: 1.8,
|
|
1380
|
-
minClusterClicks: 1,
|
|
1381
|
-
targetFill: 0.45,
|
|
1382
|
-
minLevel: 1.35,
|
|
1383
|
-
maxLevel: 2,
|
|
1384
|
-
dragLevel: 1.5,
|
|
1385
|
-
lead: 0.6,
|
|
1386
|
-
hold: 1.4,
|
|
1387
|
-
followByDefault: true,
|
|
1388
|
-
typingZoom: true,
|
|
1389
|
-
typingGap: 3,
|
|
1390
|
-
typingHold: 1.6,
|
|
1391
|
-
typingMinLevel: 1.45,
|
|
1392
|
-
rampIn: 1.8,
|
|
1393
|
-
rampInOverlap: 0.55,
|
|
1394
|
-
rampOut: 1.5,
|
|
1395
|
-
chainGap: 2.2,
|
|
1396
|
-
pan: 1.4,
|
|
1397
|
-
ease: "css-bezier(0.33, 0, 0.15, 1)",
|
|
1398
|
-
panEase: "css-bezier(0.33, 0, 0.22, 1)",
|
|
1399
|
-
followSafeRatio: 0.6,
|
|
1400
|
-
followRecenter: 1,
|
|
1401
|
-
followLookahead: 0.5,
|
|
1402
|
-
tilt: {
|
|
1403
|
-
intensity: "subtle",
|
|
1404
|
-
rampIn: 1.6,
|
|
1405
|
-
rampOut: 1.4,
|
|
1406
|
-
chainGap: 3,
|
|
1407
|
-
pan: 1.4
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1471
|
+
// Auto-zoom off (every competitor ships this switch). Camera params still
|
|
1472
|
+
// apply to MANUAL spans — they move the way the default camera moves
|
|
1473
|
+
// TODAY (spelled as a spread, never a copy that fossilises).
|
|
1474
|
+
none: { ...glide, autoZoom: false }
|
|
1410
1475
|
};
|
|
1411
1476
|
var DEFAULT_ZOOM_STYLE = "glide";
|
|
1412
1477
|
function resolveZoomStyle(name, overrides) {
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1478
|
+
const live = liveZoomStyleName(name) ?? DEFAULT_ZOOM_STYLE;
|
|
1479
|
+
const params = { ...ZOOM_STYLES[live], ...overrides };
|
|
1480
|
+
const floor = pumpFreeChainGap(params);
|
|
1481
|
+
if (params.chainGap < floor) params.chainGap = floor;
|
|
1482
|
+
return params;
|
|
1415
1483
|
}
|
|
1416
1484
|
var ZOOM_STYLE_OPTIONS = [
|
|
1417
1485
|
{
|
|
@@ -1419,22 +1487,16 @@ var ZOOM_STYLE_OPTIONS = [
|
|
|
1419
1487
|
label: "Glide",
|
|
1420
1488
|
hint: "One steady zoom that travels between clicks"
|
|
1421
1489
|
},
|
|
1422
|
-
{
|
|
1423
|
-
name: "keynote",
|
|
1424
|
-
label: "Keynote",
|
|
1425
|
-
hint: "Gliding zooms that lean toward each focus"
|
|
1426
|
-
},
|
|
1427
|
-
{
|
|
1428
|
-
name: "drift",
|
|
1429
|
-
label: "Drift",
|
|
1430
|
-
hint: "Slow, fluid moves with a subtle ambient lean"
|
|
1431
|
-
},
|
|
1432
1490
|
{
|
|
1433
1491
|
name: "focus",
|
|
1434
1492
|
label: "Focus",
|
|
1435
|
-
hint: "A zoom per click
|
|
1493
|
+
hint: "A zoom per click, framed and released"
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
name: "cinema",
|
|
1497
|
+
label: "Cinema",
|
|
1498
|
+
hint: "Slow moves and long holds, for content that is watched"
|
|
1436
1499
|
},
|
|
1437
|
-
{ name: "cinema", label: "Cinema", hint: "Slow, fluid camera moves" },
|
|
1438
1500
|
{ name: "snappy", label: "Snappy", hint: "Quick, energetic zoom cycles" },
|
|
1439
1501
|
{ name: "cut", label: "Cut", hint: "Instant zooms, no glide" },
|
|
1440
1502
|
{
|
|
@@ -1444,6 +1506,102 @@ var ZOOM_STYLE_OPTIONS = [
|
|
|
1444
1506
|
}
|
|
1445
1507
|
];
|
|
1446
1508
|
|
|
1509
|
+
// src/docVersion.ts
|
|
1510
|
+
var DOC_SCHEMA_VERSION = 5;
|
|
1511
|
+
function migrateHostedDoc(raw) {
|
|
1512
|
+
const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
|
|
1513
|
+
if (version >= DOC_SCHEMA_VERSION) return raw;
|
|
1514
|
+
let doc = raw;
|
|
1515
|
+
if (version < 4 && doc.source && typeof doc.source === "object") {
|
|
1516
|
+
doc = migrateMotion(doc);
|
|
1517
|
+
}
|
|
1518
|
+
if (version < 5) doc = migrateZoomStyle(doc);
|
|
1519
|
+
return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
|
|
1520
|
+
}
|
|
1521
|
+
function migrateZoomStyle(doc) {
|
|
1522
|
+
const name = doc.zoomStyle;
|
|
1523
|
+
if (typeof name !== "string") return doc;
|
|
1524
|
+
const retired = RETIRED_ZOOM_STYLES[name];
|
|
1525
|
+
if (!retired) return doc;
|
|
1526
|
+
return {
|
|
1527
|
+
...doc,
|
|
1528
|
+
zoomStyle: retired.style,
|
|
1529
|
+
tiltStyle: doc.tiltStyle ?? retired.tilt
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/doc/studioDoc.ts
|
|
1534
|
+
var anchorKindOf = (doc) => "source" in doc ? "recording" : "program";
|
|
1535
|
+
var isRecordingDoc = (doc) => "source" in doc;
|
|
1536
|
+
var isProgramDoc = (doc) => !("source" in doc);
|
|
1537
|
+
function programDuration(doc) {
|
|
1538
|
+
const own = doc.program.duration;
|
|
1539
|
+
if (typeof own === "number" && own > 0) return own;
|
|
1540
|
+
const cfg = doc.program.config.duration;
|
|
1541
|
+
return typeof cfg === "number" && cfg > 0 ? cfg : 0;
|
|
1542
|
+
}
|
|
1543
|
+
function anchorSourceDuration(doc) {
|
|
1544
|
+
return isRecordingDoc(doc) ? doc.source.meta.durationMs / 1e3 : programDuration(doc);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/planner/smoothing.ts
|
|
1548
|
+
var SNAP_BEFORE = 0.12;
|
|
1549
|
+
var SNAP_AFTER = 0.18;
|
|
1550
|
+
var SNAP_GAIN = 0.5;
|
|
1551
|
+
function sampleAt(points, t) {
|
|
1552
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
1553
|
+
if (t <= points[0].t) return { x: points[0].x, y: points[0].y };
|
|
1554
|
+
const last = points[points.length - 1];
|
|
1555
|
+
if (t >= last.t) return { x: last.x, y: last.y };
|
|
1556
|
+
for (let i = 1; i < points.length; i++) {
|
|
1557
|
+
if (points[i].t >= t) {
|
|
1558
|
+
const a = points[i - 1];
|
|
1559
|
+
const b = points[i];
|
|
1560
|
+
const f = (t - a.t) / (b.t - a.t || 1);
|
|
1561
|
+
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return { x: last.x, y: last.y };
|
|
1565
|
+
}
|
|
1566
|
+
function smoothCursor(track, options = {}) {
|
|
1567
|
+
const factor = clamp2(options.factor ?? 0.15, 0.01, 1);
|
|
1568
|
+
const fps = options.fps ?? 60;
|
|
1569
|
+
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 }));
|
|
1570
|
+
if (raw.length === 0) return [];
|
|
1571
|
+
const clicks = options.clickSnap ? track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y })) : [];
|
|
1572
|
+
const start = raw[0].t;
|
|
1573
|
+
const end = raw[raw.length - 1].t;
|
|
1574
|
+
const step = 1 / fps;
|
|
1575
|
+
const out = [];
|
|
1576
|
+
let cx = raw[0].x;
|
|
1577
|
+
let cy = raw[0].y;
|
|
1578
|
+
let ci = 0;
|
|
1579
|
+
for (let t = start; t <= end + 1e-9; t += step) {
|
|
1580
|
+
const target = sampleAt(raw, t);
|
|
1581
|
+
cx += (target.x - cx) * factor;
|
|
1582
|
+
cy += (target.y - cy) * factor;
|
|
1583
|
+
if (clicks.length) {
|
|
1584
|
+
while (ci < clicks.length - 1 && t > clicks[ci].t + SNAP_AFTER) ci++;
|
|
1585
|
+
const ck = clicks[ci];
|
|
1586
|
+
const u = clamp2(
|
|
1587
|
+
(t - (ck.t - SNAP_BEFORE)) / SNAP_BEFORE,
|
|
1588
|
+
0,
|
|
1589
|
+
t <= ck.t + SNAP_AFTER ? 1 : 0
|
|
1590
|
+
);
|
|
1591
|
+
const w = u * u * (3 - 2 * u) * SNAP_GAIN;
|
|
1592
|
+
if (w > 0) {
|
|
1593
|
+
cx += (ck.x - cx) * w;
|
|
1594
|
+
cy += (ck.y - cy) * w;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
out.push({ t, x: cx, y: cy });
|
|
1598
|
+
}
|
|
1599
|
+
return out;
|
|
1600
|
+
}
|
|
1601
|
+
function clamp2(v, lo, hi) {
|
|
1602
|
+
return Math.max(lo, Math.min(hi, v));
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1447
1605
|
// src/planner/autoZoom.ts
|
|
1448
1606
|
var DWELL_MOVE_FRAC = 0.02;
|
|
1449
1607
|
var DWELL_MIN = 0.45;
|
|
@@ -1720,7 +1878,8 @@ function dwellSpans(track, width, height, maxLevel, reserved) {
|
|
|
1720
1878
|
if (!breaks) continue;
|
|
1721
1879
|
const endT = i < moves.length ? moves[i].t : trackEnd;
|
|
1722
1880
|
const dur = endT - moves[start].t;
|
|
1723
|
-
|
|
1881
|
+
const opening = start === 0;
|
|
1882
|
+
if (!opening && dur >= DWELL_MIN && dur <= DWELL_MAX) {
|
|
1724
1883
|
const run = moves.slice(start, i);
|
|
1725
1884
|
candidates.push({
|
|
1726
1885
|
center: (moves[start].t + endT) / 2,
|
|
@@ -1782,7 +1941,7 @@ function focusFor(cluster, width, height, targetFill, minLevel, maxLevel) {
|
|
|
1782
1941
|
const cx = clamp012(sx / n / width);
|
|
1783
1942
|
const cy = clamp012(sy / n / height);
|
|
1784
1943
|
const level = fitLevel({ x: 0, y: 0, w: maxW, h: maxH }, width, height, targetFill) ?? maxLevel;
|
|
1785
|
-
return { cx, cy, level:
|
|
1944
|
+
return { cx, cy, level: clamp3(level, minLevel, maxLevel) };
|
|
1786
1945
|
}
|
|
1787
1946
|
function clusterFocus(events, width, height) {
|
|
1788
1947
|
let sx = 0;
|
|
@@ -1815,11 +1974,11 @@ function clusterFocus(events, width, height) {
|
|
|
1815
1974
|
} : null;
|
|
1816
1975
|
return { cx: clamp012(sx / n / width), cy: clamp012(sy / n / height), rect };
|
|
1817
1976
|
}
|
|
1818
|
-
function
|
|
1977
|
+
function clamp3(v, lo, hi) {
|
|
1819
1978
|
return Math.max(lo, Math.min(hi, v));
|
|
1820
1979
|
}
|
|
1821
1980
|
function clamp012(v) {
|
|
1822
|
-
return
|
|
1981
|
+
return clamp3(v, 0, 1);
|
|
1823
1982
|
}
|
|
1824
1983
|
function round(v) {
|
|
1825
1984
|
return Math.round(v * 1e3) / 1e3;
|
|
@@ -5065,6 +5224,13 @@ function spanOutputExtent(segments, sIn, sOut, media) {
|
|
|
5065
5224
|
function spanEase(ease, fallback) {
|
|
5066
5225
|
return ease && ease in EASINGS ? ease : fallback;
|
|
5067
5226
|
}
|
|
5227
|
+
var RAMP_FLOOR = 0.14;
|
|
5228
|
+
function rampLanding(start, startWanted, duration, spanEnd) {
|
|
5229
|
+
if (Math.abs(start - startWanted) < 1e-9) return start + duration;
|
|
5230
|
+
const landWanted = startWanted + duration;
|
|
5231
|
+
const landFull = Math.min(start + duration, spanEnd);
|
|
5232
|
+
return Math.max(start + RAMP_FLOOR, landWanted, landFull);
|
|
5233
|
+
}
|
|
5068
5234
|
function trackEmitter() {
|
|
5069
5235
|
const keyframes = [];
|
|
5070
5236
|
const push2 = (t, value, ease) => {
|
|
@@ -5093,17 +5259,19 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
|
|
|
5093
5259
|
let cur = entry;
|
|
5094
5260
|
const m = transitionMult(z.transition);
|
|
5095
5261
|
if (!chained) {
|
|
5096
|
-
const
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5262
|
+
const startWanted = tIn - (style.rampIn - style.rampInOverlap) * m;
|
|
5263
|
+
const start = push2(startWanted, [1, z.cx, z.cy, 0], "none");
|
|
5264
|
+
push2(
|
|
5265
|
+
rampLanding(start, startWanted, style.rampIn * m, tOut),
|
|
5266
|
+
entry,
|
|
5267
|
+
spanEase(z.ease, style.ease)
|
|
5100
5268
|
);
|
|
5101
|
-
push2(start + style.rampIn * m, entry, spanEase(z.ease, style.ease));
|
|
5102
5269
|
}
|
|
5103
5270
|
let inPath = false;
|
|
5104
5271
|
for (const e of z.followEvents ?? []) {
|
|
5105
5272
|
const eOut = sourceToTimeline(segments, e.t);
|
|
5106
5273
|
if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
|
|
5274
|
+
if (eOut - tIn < RAMP_FLOOR || tOut - eOut < RAMP_FLOOR) continue;
|
|
5107
5275
|
const next2 = [cur[0], e.cx, e.cy, 1];
|
|
5108
5276
|
if (e.path) {
|
|
5109
5277
|
if (!inPath) {
|
|
@@ -5126,9 +5294,12 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
|
|
|
5126
5294
|
const mNext = transitionMult(next.z.transition);
|
|
5127
5295
|
const nextValue = [clampZoomLevel(next.z.level), next.z.cx, next.z.cy, 1];
|
|
5128
5296
|
push2(
|
|
5129
|
-
Math.
|
|
5130
|
-
tOut +
|
|
5131
|
-
|
|
5297
|
+
Math.max(
|
|
5298
|
+
tOut + RAMP_FLOOR,
|
|
5299
|
+
Math.min(
|
|
5300
|
+
tOut + style.pan * mNext,
|
|
5301
|
+
next.tIn + style.rampInOverlap * mNext
|
|
5302
|
+
)
|
|
5132
5303
|
),
|
|
5133
5304
|
nextValue,
|
|
5134
5305
|
panEase
|
|
@@ -5166,15 +5337,23 @@ function tiltTrackFromDoc(tilt, segments, motion = {}) {
|
|
|
5166
5337
|
const pose = [clampTiltDeg(z.rx), clampTiltDeg(z.ry)];
|
|
5167
5338
|
const m = transitionMult(z.transition);
|
|
5168
5339
|
if (!chained) {
|
|
5169
|
-
const
|
|
5170
|
-
|
|
5340
|
+
const startWanted = tIn - rampInDur * m;
|
|
5341
|
+
const start = push2(startWanted, rest, "none");
|
|
5342
|
+
push2(
|
|
5343
|
+
rampLanding(start, startWanted, rampInDur * m, tOut),
|
|
5344
|
+
pose,
|
|
5345
|
+
spanEase(z.ease, TILT_EASE)
|
|
5346
|
+
);
|
|
5171
5347
|
}
|
|
5172
5348
|
push2(tOut, pose, "none");
|
|
5173
5349
|
const next = mapped.at(i + 1);
|
|
5174
5350
|
if (next && next.tIn - tOut <= chainGap) {
|
|
5175
5351
|
const nextPose = [clampTiltDeg(next.z.rx), clampTiltDeg(next.z.ry)];
|
|
5176
5352
|
push2(
|
|
5177
|
-
Math.
|
|
5353
|
+
Math.max(
|
|
5354
|
+
tOut + RAMP_FLOOR,
|
|
5355
|
+
Math.min(tOut + panDur * transitionMult(next.z.transition), next.tIn)
|
|
5356
|
+
),
|
|
5178
5357
|
nextPose,
|
|
5179
5358
|
panEase
|
|
5180
5359
|
);
|
|
@@ -7532,6 +7711,29 @@ function resolvePins(doc, layout, camera, zoomTrack) {
|
|
|
7532
7711
|
}
|
|
7533
7712
|
return out;
|
|
7534
7713
|
}
|
|
7714
|
+
function pinCandidates(doc, clip) {
|
|
7715
|
+
const rated = ratedSegments(doc);
|
|
7716
|
+
const out = [];
|
|
7717
|
+
const a0 = clip.start;
|
|
7718
|
+
const a1 = clip.start + clip.duration;
|
|
7719
|
+
for (const s of doc.source.meta.steps ?? []) {
|
|
7720
|
+
if (s.skipped) continue;
|
|
7721
|
+
const ref = pinReferent(doc, { step: s.id ?? s.step });
|
|
7722
|
+
if (!ref) continue;
|
|
7723
|
+
const ext = spanOutputExtent(rated, s.tStart, s.tEnd);
|
|
7724
|
+
if (!ext) continue;
|
|
7725
|
+
const overlap = Math.min(a1, ext.end) - Math.max(a0, ext.start);
|
|
7726
|
+
if (overlap <= 0) continue;
|
|
7727
|
+
out.push({
|
|
7728
|
+
step: s.id ?? s.step,
|
|
7729
|
+
do: s.do,
|
|
7730
|
+
...s.selector ? { selector: s.selector } : {},
|
|
7731
|
+
output: { start: round7(ext.start), end: round7(ext.end) },
|
|
7732
|
+
overlap: round7(overlap)
|
|
7733
|
+
});
|
|
7734
|
+
}
|
|
7735
|
+
return out.sort((x, y) => y.overlap - x.overlap);
|
|
7736
|
+
}
|
|
7535
7737
|
function overlayPinPoseAt(doc, o, t) {
|
|
7536
7738
|
if (!o.pin) return null;
|
|
7537
7739
|
const layout = docCardLayout(doc);
|
|
@@ -9079,24 +9281,31 @@ function destinationsForChannel(channel) {
|
|
|
9079
9281
|
}
|
|
9080
9282
|
|
|
9081
9283
|
// src/htmlStarters.ts
|
|
9284
|
+
function grammarStarter(id, name, words) {
|
|
9285
|
+
const clip = calloutClip(id, HOUSE_REGISTER, words, {
|
|
9286
|
+
id,
|
|
9287
|
+
start: 0,
|
|
9288
|
+
duration: 1,
|
|
9289
|
+
scale: 1.4
|
|
9290
|
+
});
|
|
9291
|
+
return { id, name, html: clip.html, css: clip.css ?? "", box: clip.box };
|
|
9292
|
+
}
|
|
9082
9293
|
var WORDS = "font-family:'Inter',-apple-system,system-ui,sans-serif;color:#fafafa;-webkit-font-smoothing:antialiased;";
|
|
9083
9294
|
var MONO = "font-family:'JetBrains Mono',ui-monospace,monospace;";
|
|
9084
9295
|
var CARD = "background:#0b0b0d;border:1px solid rgba(255,255,255,.08);border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.45);";
|
|
9085
9296
|
var HTML_STARTERS = [
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
},
|
|
9093
|
-
{
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
css: `.code{${MONO}${CARD}color:#e8e8ec;box-sizing:border-box;width:460px;height:168px;padding:22px 24px;font-size:14px;line-height:1.8;white-space:pre;letter-spacing:-0.01em}.kw{color:#c4b5fd}.fn{color:#7dd3fc}.tag{color:#f0abfc}.mut{color:#5b5b66}`
|
|
9099
|
-
},
|
|
9297
|
+
// The callout grammar (three shapes a viewer learns once), in the house
|
|
9298
|
+
// register: a dark card in the house hue over a light app.
|
|
9299
|
+
grammarStarter("note", "Note", {
|
|
9300
|
+
kicker: "Presets",
|
|
9301
|
+
title: "One of 43, picked here.",
|
|
9302
|
+
body: "One line that says what this is about."
|
|
9303
|
+
}),
|
|
9304
|
+
grammarStarter("tag", "Tag", { kicker: "New in 2.0" }),
|
|
9305
|
+
grammarStarter("code", "Code", {
|
|
9306
|
+
kicker: "index.css",
|
|
9307
|
+
code: ":root {\n --primary: oklch(0.63 0.06 4.6);\n --radius: 2.25rem;\n}"
|
|
9308
|
+
}),
|
|
9100
9309
|
{
|
|
9101
9310
|
id: "terminal",
|
|
9102
9311
|
name: "Terminal",
|
|
@@ -10637,6 +10846,9 @@ export {
|
|
|
10637
10846
|
AUDIO_PLAN_STEP,
|
|
10638
10847
|
BACKGROUND_Z,
|
|
10639
10848
|
BASE_FRAME_STYLE,
|
|
10849
|
+
CALLOUT_DEFAULT_BODY_PX,
|
|
10850
|
+
CALLOUT_SHAPES,
|
|
10851
|
+
CALLOUT_TYPE_RATIO,
|
|
10640
10852
|
CAMERA_CENTRE_RAMP,
|
|
10641
10853
|
CAMERA_COVER_MIN,
|
|
10642
10854
|
CAMERA_FAR,
|
|
@@ -10711,6 +10923,7 @@ export {
|
|
|
10711
10923
|
FREEZE_SECONDS_MAX,
|
|
10712
10924
|
FREEZE_SECONDS_MIN,
|
|
10713
10925
|
HOUSE_GRADIENT,
|
|
10926
|
+
HOUSE_REGISTER,
|
|
10714
10927
|
HTML_LAYER_FOREIGN_FIELDS,
|
|
10715
10928
|
HTML_LAYER_MAX_BYTES,
|
|
10716
10929
|
HTML_LAYER_SVG_CODE,
|
|
@@ -10744,8 +10957,10 @@ export {
|
|
|
10744
10957
|
PLAYBACK_ACTIVITY,
|
|
10745
10958
|
PRIMARY_MEDIA,
|
|
10746
10959
|
PROGRAM_RETIME,
|
|
10960
|
+
PUMP_REST,
|
|
10747
10961
|
REJECT_OVERLAP,
|
|
10748
10962
|
REST_TILT_ID,
|
|
10963
|
+
RETIRED_ZOOM_STYLES,
|
|
10749
10964
|
SCENE_MOTION,
|
|
10750
10965
|
SCENE_QUIET,
|
|
10751
10966
|
SEGMENT_ENTER_KINDS,
|
|
@@ -10802,6 +11017,11 @@ export {
|
|
|
10802
11017
|
audioLane,
|
|
10803
11018
|
backdropMedia,
|
|
10804
11019
|
buildDigest,
|
|
11020
|
+
calloutBox,
|
|
11021
|
+
calloutClip,
|
|
11022
|
+
calloutGroundOf,
|
|
11023
|
+
calloutLook,
|
|
11024
|
+
calloutSource,
|
|
10805
11025
|
camBubbleRect,
|
|
10806
11026
|
camBubbleRectAt,
|
|
10807
11027
|
camLane,
|
|
@@ -10836,6 +11056,7 @@ export {
|
|
|
10836
11056
|
cssFamilies,
|
|
10837
11057
|
cssWeights,
|
|
10838
11058
|
cursorIdleFade,
|
|
11059
|
+
deltaE,
|
|
10839
11060
|
deriveViewportCrop,
|
|
10840
11061
|
destinationById,
|
|
10841
11062
|
destinationsForChannel,
|
|
@@ -10873,6 +11094,7 @@ export {
|
|
|
10873
11094
|
frameGeometry,
|
|
10874
11095
|
freezeLane,
|
|
10875
11096
|
freezeOutputExtents,
|
|
11097
|
+
hexToRgb,
|
|
10876
11098
|
hexToRgbTriplet,
|
|
10877
11099
|
houseLook,
|
|
10878
11100
|
htmlLayerBleed,
|
|
@@ -10903,6 +11125,7 @@ export {
|
|
|
10903
11125
|
layerMedia,
|
|
10904
11126
|
layoutOf,
|
|
10905
11127
|
levelForFocusFraction,
|
|
11128
|
+
liveZoomStyleName,
|
|
10906
11129
|
lookFromBrand,
|
|
10907
11130
|
lookKindForGround,
|
|
10908
11131
|
lowerProgramDoc,
|
|
@@ -10918,6 +11141,7 @@ export {
|
|
|
10918
11141
|
micLane,
|
|
10919
11142
|
migrateHostedDoc,
|
|
10920
11143
|
migrateMotion,
|
|
11144
|
+
migrateZoomStyle,
|
|
10921
11145
|
momentsFromDoc,
|
|
10922
11146
|
motionTrack,
|
|
10923
11147
|
musicBedClip,
|
|
@@ -10943,9 +11167,11 @@ export {
|
|
|
10943
11167
|
overlaysLane,
|
|
10944
11168
|
ownFreezes,
|
|
10945
11169
|
pageDisplayUrl,
|
|
11170
|
+
parseCssColour,
|
|
10946
11171
|
parsePoseId,
|
|
10947
11172
|
pickStyle,
|
|
10948
11173
|
pinBox,
|
|
11174
|
+
pinCandidates,
|
|
10949
11175
|
pinPlacement,
|
|
10950
11176
|
pinRectOnScreen,
|
|
10951
11177
|
pinReferent,
|
|
@@ -10959,10 +11185,12 @@ export {
|
|
|
10959
11185
|
platformBarKind,
|
|
10960
11186
|
programDuration,
|
|
10961
11187
|
projectFromArtifact,
|
|
11188
|
+
pumpFreeChainGap,
|
|
10962
11189
|
ratedSegments,
|
|
10963
11190
|
recommendedExportResolution,
|
|
10964
11191
|
refillAudioBeds,
|
|
10965
11192
|
rejectSpan,
|
|
11193
|
+
relativeLuminance,
|
|
10966
11194
|
removeSourceRange,
|
|
10967
11195
|
removeSpeedInRange,
|
|
10968
11196
|
resolveExportSize,
|
|
@@ -10973,6 +11201,7 @@ export {
|
|
|
10973
11201
|
resolveZoomStyle,
|
|
10974
11202
|
restLeanOf,
|
|
10975
11203
|
restSpansThroughTransitions,
|
|
11204
|
+
rgbToHex,
|
|
10976
11205
|
sameMedia,
|
|
10977
11206
|
sceneChanges,
|
|
10978
11207
|
scrollRuns,
|