@vosjs/studio-core 0.25.0 → 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 +98 -42
- package/dist/index.js +289 -309
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1260,362 +1260,226 @@ function outputEnd(doc, footageEnd) {
|
|
|
1260
1260
|
return Math.max(footageEnd, lastLayerEnd(doc));
|
|
1261
1261
|
}
|
|
1262
1262
|
|
|
1263
|
-
// src/docVersion.ts
|
|
1264
|
-
var DOC_SCHEMA_VERSION = 4;
|
|
1265
|
-
function migrateHostedDoc(raw) {
|
|
1266
|
-
const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
|
|
1267
|
-
if (version >= DOC_SCHEMA_VERSION) return raw;
|
|
1268
|
-
let doc = raw;
|
|
1269
|
-
if (version < 4 && doc.source && typeof doc.source === "object") {
|
|
1270
|
-
doc = migrateMotion(doc);
|
|
1271
|
-
}
|
|
1272
|
-
return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// src/doc/studioDoc.ts
|
|
1276
|
-
var anchorKindOf = (doc) => "source" in doc ? "recording" : "program";
|
|
1277
|
-
var isRecordingDoc = (doc) => "source" in doc;
|
|
1278
|
-
var isProgramDoc = (doc) => !("source" in doc);
|
|
1279
|
-
function programDuration(doc) {
|
|
1280
|
-
const own = doc.program.duration;
|
|
1281
|
-
if (typeof own === "number" && own > 0) return own;
|
|
1282
|
-
const cfg = doc.program.config.duration;
|
|
1283
|
-
return typeof cfg === "number" && cfg > 0 ? cfg : 0;
|
|
1284
|
-
}
|
|
1285
|
-
function anchorSourceDuration(doc) {
|
|
1286
|
-
return isRecordingDoc(doc) ? doc.source.meta.durationMs / 1e3 : programDuration(doc);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// src/planner/smoothing.ts
|
|
1290
|
-
var SNAP_BEFORE = 0.12;
|
|
1291
|
-
var SNAP_AFTER = 0.18;
|
|
1292
|
-
var SNAP_GAIN = 0.5;
|
|
1293
|
-
function sampleAt(points, t) {
|
|
1294
|
-
if (points.length === 0) return { x: 0, y: 0 };
|
|
1295
|
-
if (t <= points[0].t) return { x: points[0].x, y: points[0].y };
|
|
1296
|
-
const last = points[points.length - 1];
|
|
1297
|
-
if (t >= last.t) return { x: last.x, y: last.y };
|
|
1298
|
-
for (let i = 1; i < points.length; i++) {
|
|
1299
|
-
if (points[i].t >= t) {
|
|
1300
|
-
const a = points[i - 1];
|
|
1301
|
-
const b = points[i];
|
|
1302
|
-
const f = (t - a.t) / (b.t - a.t || 1);
|
|
1303
|
-
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
return { x: last.x, y: last.y };
|
|
1307
|
-
}
|
|
1308
|
-
function smoothCursor(track, options = {}) {
|
|
1309
|
-
const factor = clamp2(options.factor ?? 0.15, 0.01, 1);
|
|
1310
|
-
const fps = options.fps ?? 60;
|
|
1311
|
-
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 }));
|
|
1312
|
-
if (raw.length === 0) return [];
|
|
1313
|
-
const clicks = options.clickSnap ? track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y })) : [];
|
|
1314
|
-
const start = raw[0].t;
|
|
1315
|
-
const end = raw[raw.length - 1].t;
|
|
1316
|
-
const step = 1 / fps;
|
|
1317
|
-
const out = [];
|
|
1318
|
-
let cx = raw[0].x;
|
|
1319
|
-
let cy = raw[0].y;
|
|
1320
|
-
let ci = 0;
|
|
1321
|
-
for (let t = start; t <= end + 1e-9; t += step) {
|
|
1322
|
-
const target = sampleAt(raw, t);
|
|
1323
|
-
cx += (target.x - cx) * factor;
|
|
1324
|
-
cy += (target.y - cy) * factor;
|
|
1325
|
-
if (clicks.length) {
|
|
1326
|
-
while (ci < clicks.length - 1 && t > clicks[ci].t + SNAP_AFTER) ci++;
|
|
1327
|
-
const ck = clicks[ci];
|
|
1328
|
-
const u = clamp2(
|
|
1329
|
-
(t - (ck.t - SNAP_BEFORE)) / SNAP_BEFORE,
|
|
1330
|
-
0,
|
|
1331
|
-
t <= ck.t + SNAP_AFTER ? 1 : 0
|
|
1332
|
-
);
|
|
1333
|
-
const w = u * u * (3 - 2 * u) * SNAP_GAIN;
|
|
1334
|
-
if (w > 0) {
|
|
1335
|
-
cx += (ck.x - cx) * w;
|
|
1336
|
-
cy += (ck.y - cy) * w;
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
out.push({ t, x: cx, y: cy });
|
|
1340
|
-
}
|
|
1341
|
-
return out;
|
|
1342
|
-
}
|
|
1343
|
-
function clamp2(v, lo, hi) {
|
|
1344
|
-
return Math.max(lo, Math.min(hi, v));
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
1263
|
// src/zoomStyle.ts
|
|
1348
|
-
var
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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",
|
|
1371
1312
|
rampIn: 0.6,
|
|
1372
|
-
rampInOverlap: 0.2,
|
|
1373
1313
|
rampOut: 0.5,
|
|
1374
1314
|
chainGap: 3,
|
|
1375
|
-
pan: 0.55
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
followRecenter: 0.55,
|
|
1388
|
-
followLookahead: 0.4,
|
|
1389
|
-
tilt: { intensity: "off" }
|
|
1390
|
-
},
|
|
1391
|
-
// 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.
|
|
1392
1327
|
focus: {
|
|
1393
1328
|
autoZoom: true,
|
|
1394
|
-
clusterGap: 1.
|
|
1329
|
+
clusterGap: 1.5,
|
|
1395
1330
|
minClusterClicks: 1,
|
|
1396
1331
|
targetFill: 0.5,
|
|
1397
|
-
minLevel: 1.
|
|
1398
|
-
maxLevel: 2
|
|
1399
|
-
dragLevel: 1.
|
|
1332
|
+
minLevel: 1.4,
|
|
1333
|
+
maxLevel: 2,
|
|
1334
|
+
dragLevel: 1.6,
|
|
1400
1335
|
lead: 0.35,
|
|
1401
|
-
hold: 1,
|
|
1336
|
+
hold: 1.5,
|
|
1402
1337
|
followByDefault: false,
|
|
1403
1338
|
typingZoom: true,
|
|
1404
1339
|
typingGap: 2,
|
|
1405
|
-
typingHold: 1.
|
|
1406
|
-
typingMinLevel: 1.
|
|
1407
|
-
rampIn:
|
|
1408
|
-
rampInOverlap: 0.
|
|
1409
|
-
rampOut:
|
|
1410
|
-
chainGap:
|
|
1411
|
-
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,
|
|
1412
1347
|
ease: "css-bezier(0.28, 0.03, 0.09, 1)",
|
|
1413
1348
|
panEase: "css-bezier(0.26, 0.08, 0.2, 1)",
|
|
1414
1349
|
followSafeRatio: 0.5,
|
|
1415
|
-
followRecenter: 0.
|
|
1350
|
+
followRecenter: 0.55,
|
|
1416
1351
|
followLookahead: 0,
|
|
1417
|
-
tilt: {
|
|
1352
|
+
tilt: {
|
|
1353
|
+
intensity: "off",
|
|
1354
|
+
rampIn: 0.8,
|
|
1355
|
+
rampOut: 0.65,
|
|
1356
|
+
chainGap: 2.4,
|
|
1357
|
+
pan: 0.7
|
|
1358
|
+
}
|
|
1418
1359
|
},
|
|
1419
|
-
// 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.
|
|
1420
1365
|
cinema: {
|
|
1421
1366
|
autoZoom: true,
|
|
1422
|
-
clusterGap:
|
|
1367
|
+
clusterGap: 2.4,
|
|
1423
1368
|
minClusterClicks: 1,
|
|
1424
|
-
targetFill: 0.
|
|
1425
|
-
minLevel: 1.
|
|
1426
|
-
maxLevel:
|
|
1369
|
+
targetFill: 0.42,
|
|
1370
|
+
minLevel: 1.3,
|
|
1371
|
+
maxLevel: 1.8,
|
|
1427
1372
|
dragLevel: 1.5,
|
|
1428
1373
|
lead: 0.6,
|
|
1429
|
-
hold:
|
|
1374
|
+
hold: 2,
|
|
1430
1375
|
followByDefault: true,
|
|
1431
1376
|
typingZoom: true,
|
|
1432
1377
|
typingGap: 3,
|
|
1433
|
-
typingHold: 1.
|
|
1434
|
-
typingMinLevel: 1.
|
|
1435
|
-
rampIn: 1.
|
|
1436
|
-
rampInOverlap: 0.
|
|
1437
|
-
rampOut: 1
|
|
1438
|
-
chainGap:
|
|
1439
|
-
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,
|
|
1440
1385
|
ease: "css-bezier(0.33, 0, 0.15, 1)",
|
|
1441
1386
|
panEase: "css-bezier(0.33, 0, 0.22, 1)",
|
|
1442
1387
|
followSafeRatio: 0.6,
|
|
1443
|
-
followRecenter:
|
|
1388
|
+
followRecenter: 0.9,
|
|
1444
1389
|
followLookahead: 0.5,
|
|
1445
|
-
tilt: {
|
|
1390
|
+
tilt: {
|
|
1391
|
+
intensity: "off",
|
|
1392
|
+
rampIn: 1.2,
|
|
1393
|
+
rampOut: 1,
|
|
1394
|
+
chainGap: 3.5,
|
|
1395
|
+
pan: 1.1
|
|
1396
|
+
}
|
|
1446
1397
|
},
|
|
1447
|
-
// 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.
|
|
1448
1401
|
snappy: {
|
|
1449
1402
|
autoZoom: true,
|
|
1450
1403
|
clusterGap: 1.2,
|
|
1451
1404
|
minClusterClicks: 1,
|
|
1452
1405
|
targetFill: 0.5,
|
|
1453
1406
|
minLevel: 1.4,
|
|
1454
|
-
maxLevel: 2.
|
|
1455
|
-
dragLevel: 1.
|
|
1407
|
+
maxLevel: 2.2,
|
|
1408
|
+
dragLevel: 1.6,
|
|
1456
1409
|
lead: 0.25,
|
|
1457
|
-
hold: 1,
|
|
1410
|
+
hold: 1.2,
|
|
1458
1411
|
followByDefault: false,
|
|
1459
1412
|
typingZoom: true,
|
|
1460
1413
|
typingGap: 1.6,
|
|
1461
|
-
typingHold: 0.
|
|
1414
|
+
typingHold: 0.9,
|
|
1462
1415
|
typingMinLevel: 1.5,
|
|
1463
|
-
rampIn: 0.
|
|
1464
|
-
rampInOverlap: 0.
|
|
1465
|
-
rampOut: 0.
|
|
1466
|
-
chainGap:
|
|
1467
|
-
pan: 0.
|
|
1416
|
+
rampIn: 0.4,
|
|
1417
|
+
rampInOverlap: 0.15,
|
|
1418
|
+
rampOut: 0.4,
|
|
1419
|
+
chainGap: 2,
|
|
1420
|
+
pan: 0.5,
|
|
1468
1421
|
ease: "css-bezier(0.3, 0.55, 0.2, 1)",
|
|
1469
1422
|
panEase: "css-bezier(0.25, 0.1, 0.25, 1)",
|
|
1470
1423
|
followSafeRatio: 0.5,
|
|
1471
|
-
followRecenter: 0.
|
|
1424
|
+
followRecenter: 0.45,
|
|
1472
1425
|
followLookahead: 0,
|
|
1473
|
-
tilt: {
|
|
1426
|
+
tilt: {
|
|
1427
|
+
intensity: "off",
|
|
1428
|
+
rampIn: 0.4,
|
|
1429
|
+
rampOut: 0.4,
|
|
1430
|
+
chainGap: 2,
|
|
1431
|
+
pan: 0.5
|
|
1432
|
+
}
|
|
1474
1433
|
},
|
|
1475
|
-
// 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.
|
|
1476
1438
|
cut: {
|
|
1477
1439
|
autoZoom: true,
|
|
1478
1440
|
clusterGap: 1.2,
|
|
1479
1441
|
minClusterClicks: 1,
|
|
1480
1442
|
targetFill: 0.5,
|
|
1481
|
-
minLevel: 1.
|
|
1482
|
-
maxLevel: 2
|
|
1483
|
-
dragLevel: 1.
|
|
1443
|
+
minLevel: 1.4,
|
|
1444
|
+
maxLevel: 2,
|
|
1445
|
+
dragLevel: 1.6,
|
|
1484
1446
|
lead: 0.2,
|
|
1485
|
-
hold: 1,
|
|
1447
|
+
hold: 1.2,
|
|
1486
1448
|
followByDefault: false,
|
|
1487
1449
|
typingZoom: true,
|
|
1488
1450
|
typingGap: 1.2,
|
|
1489
1451
|
typingHold: 0.6,
|
|
1490
|
-
typingMinLevel: 1.
|
|
1452
|
+
typingMinLevel: 1.5,
|
|
1491
1453
|
rampIn: 0.14,
|
|
1492
1454
|
rampInOverlap: 0.07,
|
|
1493
1455
|
rampOut: 0.14,
|
|
1494
|
-
chainGap: 1.
|
|
1456
|
+
chainGap: 1.3,
|
|
1495
1457
|
pan: 0.35,
|
|
1496
1458
|
ease: "css-bezier(0.2, 0, 0.4, 1)",
|
|
1497
1459
|
panEase: "css-bezier(0.2, 0, 0.4, 1)",
|
|
1498
1460
|
followSafeRatio: 0.5,
|
|
1499
1461
|
followRecenter: 0.35,
|
|
1500
1462
|
followLookahead: 0,
|
|
1501
|
-
tilt: { intensity: "off" }
|
|
1502
|
-
},
|
|
1503
|
-
// Auto-zoom off (every competitor ships this switch). Camera params still
|
|
1504
|
-
// apply to MANUAL spans — they get the default (glide) motion.
|
|
1505
|
-
none: {
|
|
1506
|
-
autoZoom: false,
|
|
1507
|
-
clusterGap: 3,
|
|
1508
|
-
minClusterClicks: 2,
|
|
1509
|
-
targetFill: 0.42,
|
|
1510
|
-
minLevel: 1.3,
|
|
1511
|
-
maxLevel: 1.8,
|
|
1512
|
-
dragLevel: 1.5,
|
|
1513
|
-
lead: 0.5,
|
|
1514
|
-
hold: 0.6,
|
|
1515
|
-
followByDefault: false,
|
|
1516
|
-
typingZoom: true,
|
|
1517
|
-
typingGap: 2.5,
|
|
1518
|
-
typingHold: 1.1,
|
|
1519
|
-
typingMinLevel: 1.4,
|
|
1520
|
-
rampIn: 1.1,
|
|
1521
|
-
rampInOverlap: 0.35,
|
|
1522
|
-
rampOut: 1,
|
|
1523
|
-
chainGap: 2.5,
|
|
1524
|
-
pan: 1,
|
|
1525
|
-
ease: "css-bezier(0.26, 0, 0.16, 1)",
|
|
1526
|
-
panEase: "css-bezier(0.3, 0, 0.2, 1)",
|
|
1527
|
-
followSafeRatio: 0.45,
|
|
1528
|
-
followRecenter: 0.8,
|
|
1529
|
-
followLookahead: 0,
|
|
1530
|
-
tilt: { intensity: "off" }
|
|
1531
|
-
},
|
|
1532
|
-
// ── tilt-forward styles — the camera moves in DEPTH too.
|
|
1533
|
-
// Research note (2026-08-03): no competitor ships a document-wide personality
|
|
1534
|
-
// driving BOTH auto-zoom dynamics and focus-following tilt from one name —
|
|
1535
|
-
// FocuSee's Subtle/Default/Strong 3D Motion is a separate layered effect,
|
|
1536
|
-
// TiltIt/ScreenDrift sell per-clip templates. These two own that space.
|
|
1537
|
-
//
|
|
1538
|
-
// "keynote": the launch-film sentence (the Apple register: rehearsed,
|
|
1539
|
-
// restrained, one move per beat) — glide's session-merged, modest zooms plus
|
|
1540
|
-
// a MEDIUM lean toward each zoom's focus, tilt ramps MATCHED to the zoom
|
|
1541
|
-
// ramps so lean and zoom read as one camera move; chained so back-to-back
|
|
1542
|
-
// beats swing pose-to-pose. Never oscillation, ±5..18° band.
|
|
1543
|
-
keynote: {
|
|
1544
|
-
autoZoom: true,
|
|
1545
|
-
clusterGap: 3,
|
|
1546
|
-
minClusterClicks: 1,
|
|
1547
|
-
targetFill: 0.42,
|
|
1548
|
-
minLevel: 1.3,
|
|
1549
|
-
maxLevel: 1.8,
|
|
1550
|
-
dragLevel: 1.5,
|
|
1551
|
-
lead: 0.5,
|
|
1552
|
-
hold: 0.7,
|
|
1553
|
-
followByDefault: true,
|
|
1554
|
-
typingZoom: true,
|
|
1555
|
-
typingGap: 2.5,
|
|
1556
|
-
typingHold: 1.2,
|
|
1557
|
-
typingMinLevel: 1.4,
|
|
1558
|
-
rampIn: 1.2,
|
|
1559
|
-
rampInOverlap: 0.35,
|
|
1560
|
-
rampOut: 1.1,
|
|
1561
|
-
chainGap: 2.5,
|
|
1562
|
-
pan: 1.1,
|
|
1563
|
-
ease: "css-bezier(0.26, 0, 0.16, 1)",
|
|
1564
|
-
panEase: "css-bezier(0.3, 0, 0.2, 1)",
|
|
1565
|
-
followSafeRatio: 0.45,
|
|
1566
|
-
followRecenter: 0.8,
|
|
1567
|
-
followLookahead: 0.4,
|
|
1568
|
-
// Lean lands WITH the zoom (matched ramps read as one camera move).
|
|
1569
1463
|
tilt: {
|
|
1570
|
-
intensity: "
|
|
1571
|
-
rampIn:
|
|
1572
|
-
rampOut:
|
|
1573
|
-
chainGap:
|
|
1574
|
-
pan:
|
|
1464
|
+
intensity: "off",
|
|
1465
|
+
rampIn: 0.3,
|
|
1466
|
+
rampOut: 0.3,
|
|
1467
|
+
chainGap: 1.3,
|
|
1468
|
+
pan: 0.35
|
|
1575
1469
|
}
|
|
1576
1470
|
},
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
|
|
1581
|
-
drift: {
|
|
1582
|
-
autoZoom: true,
|
|
1583
|
-
clusterGap: 1.8,
|
|
1584
|
-
minClusterClicks: 1,
|
|
1585
|
-
targetFill: 0.45,
|
|
1586
|
-
minLevel: 1.35,
|
|
1587
|
-
maxLevel: 2,
|
|
1588
|
-
dragLevel: 1.5,
|
|
1589
|
-
lead: 0.6,
|
|
1590
|
-
hold: 1.4,
|
|
1591
|
-
followByDefault: true,
|
|
1592
|
-
typingZoom: true,
|
|
1593
|
-
typingGap: 3,
|
|
1594
|
-
typingHold: 1.6,
|
|
1595
|
-
typingMinLevel: 1.45,
|
|
1596
|
-
rampIn: 1.8,
|
|
1597
|
-
rampInOverlap: 0.55,
|
|
1598
|
-
rampOut: 1.5,
|
|
1599
|
-
chainGap: 2.2,
|
|
1600
|
-
pan: 1.4,
|
|
1601
|
-
ease: "css-bezier(0.33, 0, 0.15, 1)",
|
|
1602
|
-
panEase: "css-bezier(0.33, 0, 0.22, 1)",
|
|
1603
|
-
followSafeRatio: 0.6,
|
|
1604
|
-
followRecenter: 1,
|
|
1605
|
-
followLookahead: 0.5,
|
|
1606
|
-
tilt: {
|
|
1607
|
-
intensity: "subtle",
|
|
1608
|
-
rampIn: 1.6,
|
|
1609
|
-
rampOut: 1.4,
|
|
1610
|
-
chainGap: 3,
|
|
1611
|
-
pan: 1.4
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
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 }
|
|
1614
1475
|
};
|
|
1615
1476
|
var DEFAULT_ZOOM_STYLE = "glide";
|
|
1616
1477
|
function resolveZoomStyle(name, overrides) {
|
|
1617
|
-
const
|
|
1618
|
-
|
|
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;
|
|
1619
1483
|
}
|
|
1620
1484
|
var ZOOM_STYLE_OPTIONS = [
|
|
1621
1485
|
{
|
|
@@ -1623,22 +1487,16 @@ var ZOOM_STYLE_OPTIONS = [
|
|
|
1623
1487
|
label: "Glide",
|
|
1624
1488
|
hint: "One steady zoom that travels between clicks"
|
|
1625
1489
|
},
|
|
1626
|
-
{
|
|
1627
|
-
name: "keynote",
|
|
1628
|
-
label: "Keynote",
|
|
1629
|
-
hint: "Gliding zooms that lean toward each focus"
|
|
1630
|
-
},
|
|
1631
|
-
{
|
|
1632
|
-
name: "drift",
|
|
1633
|
-
label: "Drift",
|
|
1634
|
-
hint: "Slow, fluid moves with a subtle ambient lean"
|
|
1635
|
-
},
|
|
1636
1490
|
{
|
|
1637
1491
|
name: "focus",
|
|
1638
1492
|
label: "Focus",
|
|
1639
|
-
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"
|
|
1640
1499
|
},
|
|
1641
|
-
{ name: "cinema", label: "Cinema", hint: "Slow, fluid camera moves" },
|
|
1642
1500
|
{ name: "snappy", label: "Snappy", hint: "Quick, energetic zoom cycles" },
|
|
1643
1501
|
{ name: "cut", label: "Cut", hint: "Instant zooms, no glide" },
|
|
1644
1502
|
{
|
|
@@ -1648,6 +1506,102 @@ var ZOOM_STYLE_OPTIONS = [
|
|
|
1648
1506
|
}
|
|
1649
1507
|
];
|
|
1650
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
|
+
|
|
1651
1605
|
// src/planner/autoZoom.ts
|
|
1652
1606
|
var DWELL_MOVE_FRAC = 0.02;
|
|
1653
1607
|
var DWELL_MIN = 0.45;
|
|
@@ -1924,7 +1878,8 @@ function dwellSpans(track, width, height, maxLevel, reserved) {
|
|
|
1924
1878
|
if (!breaks) continue;
|
|
1925
1879
|
const endT = i < moves.length ? moves[i].t : trackEnd;
|
|
1926
1880
|
const dur = endT - moves[start].t;
|
|
1927
|
-
|
|
1881
|
+
const opening = start === 0;
|
|
1882
|
+
if (!opening && dur >= DWELL_MIN && dur <= DWELL_MAX) {
|
|
1928
1883
|
const run = moves.slice(start, i);
|
|
1929
1884
|
candidates.push({
|
|
1930
1885
|
center: (moves[start].t + endT) / 2,
|
|
@@ -5269,6 +5224,13 @@ function spanOutputExtent(segments, sIn, sOut, media) {
|
|
|
5269
5224
|
function spanEase(ease, fallback) {
|
|
5270
5225
|
return ease && ease in EASINGS ? ease : fallback;
|
|
5271
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
|
+
}
|
|
5272
5234
|
function trackEmitter() {
|
|
5273
5235
|
const keyframes = [];
|
|
5274
5236
|
const push2 = (t, value, ease) => {
|
|
@@ -5297,17 +5259,19 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
|
|
|
5297
5259
|
let cur = entry;
|
|
5298
5260
|
const m = transitionMult(z.transition);
|
|
5299
5261
|
if (!chained) {
|
|
5300
|
-
const
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
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)
|
|
5304
5268
|
);
|
|
5305
|
-
push2(start + style.rampIn * m, entry, spanEase(z.ease, style.ease));
|
|
5306
5269
|
}
|
|
5307
5270
|
let inPath = false;
|
|
5308
5271
|
for (const e of z.followEvents ?? []) {
|
|
5309
5272
|
const eOut = sourceToTimeline(segments, e.t);
|
|
5310
5273
|
if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
|
|
5274
|
+
if (eOut - tIn < RAMP_FLOOR || tOut - eOut < RAMP_FLOOR) continue;
|
|
5311
5275
|
const next2 = [cur[0], e.cx, e.cy, 1];
|
|
5312
5276
|
if (e.path) {
|
|
5313
5277
|
if (!inPath) {
|
|
@@ -5330,9 +5294,12 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
|
|
|
5330
5294
|
const mNext = transitionMult(next.z.transition);
|
|
5331
5295
|
const nextValue = [clampZoomLevel(next.z.level), next.z.cx, next.z.cy, 1];
|
|
5332
5296
|
push2(
|
|
5333
|
-
Math.
|
|
5334
|
-
tOut +
|
|
5335
|
-
|
|
5297
|
+
Math.max(
|
|
5298
|
+
tOut + RAMP_FLOOR,
|
|
5299
|
+
Math.min(
|
|
5300
|
+
tOut + style.pan * mNext,
|
|
5301
|
+
next.tIn + style.rampInOverlap * mNext
|
|
5302
|
+
)
|
|
5336
5303
|
),
|
|
5337
5304
|
nextValue,
|
|
5338
5305
|
panEase
|
|
@@ -5370,15 +5337,23 @@ function tiltTrackFromDoc(tilt, segments, motion = {}) {
|
|
|
5370
5337
|
const pose = [clampTiltDeg(z.rx), clampTiltDeg(z.ry)];
|
|
5371
5338
|
const m = transitionMult(z.transition);
|
|
5372
5339
|
if (!chained) {
|
|
5373
|
-
const
|
|
5374
|
-
|
|
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
|
+
);
|
|
5375
5347
|
}
|
|
5376
5348
|
push2(tOut, pose, "none");
|
|
5377
5349
|
const next = mapped.at(i + 1);
|
|
5378
5350
|
if (next && next.tIn - tOut <= chainGap) {
|
|
5379
5351
|
const nextPose = [clampTiltDeg(next.z.rx), clampTiltDeg(next.z.ry)];
|
|
5380
5352
|
push2(
|
|
5381
|
-
Math.
|
|
5353
|
+
Math.max(
|
|
5354
|
+
tOut + RAMP_FLOOR,
|
|
5355
|
+
Math.min(tOut + panDur * transitionMult(next.z.transition), next.tIn)
|
|
5356
|
+
),
|
|
5382
5357
|
nextPose,
|
|
5383
5358
|
panEase
|
|
5384
5359
|
);
|
|
@@ -10982,8 +10957,10 @@ export {
|
|
|
10982
10957
|
PLAYBACK_ACTIVITY,
|
|
10983
10958
|
PRIMARY_MEDIA,
|
|
10984
10959
|
PROGRAM_RETIME,
|
|
10960
|
+
PUMP_REST,
|
|
10985
10961
|
REJECT_OVERLAP,
|
|
10986
10962
|
REST_TILT_ID,
|
|
10963
|
+
RETIRED_ZOOM_STYLES,
|
|
10987
10964
|
SCENE_MOTION,
|
|
10988
10965
|
SCENE_QUIET,
|
|
10989
10966
|
SEGMENT_ENTER_KINDS,
|
|
@@ -11148,6 +11125,7 @@ export {
|
|
|
11148
11125
|
layerMedia,
|
|
11149
11126
|
layoutOf,
|
|
11150
11127
|
levelForFocusFraction,
|
|
11128
|
+
liveZoomStyleName,
|
|
11151
11129
|
lookFromBrand,
|
|
11152
11130
|
lookKindForGround,
|
|
11153
11131
|
lowerProgramDoc,
|
|
@@ -11163,6 +11141,7 @@ export {
|
|
|
11163
11141
|
micLane,
|
|
11164
11142
|
migrateHostedDoc,
|
|
11165
11143
|
migrateMotion,
|
|
11144
|
+
migrateZoomStyle,
|
|
11166
11145
|
momentsFromDoc,
|
|
11167
11146
|
motionTrack,
|
|
11168
11147
|
musicBedClip,
|
|
@@ -11206,6 +11185,7 @@ export {
|
|
|
11206
11185
|
platformBarKind,
|
|
11207
11186
|
programDuration,
|
|
11208
11187
|
projectFromArtifact,
|
|
11188
|
+
pumpFreeChainGap,
|
|
11209
11189
|
ratedSegments,
|
|
11210
11190
|
recommendedExportResolution,
|
|
11211
11191
|
refillAudioBeds,
|