@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/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 ZOOM_STYLES = {
1349
- // Cursorful strategy — one steady zoom per activity session, travel by pans.
1350
- // The camera's timing was measured against a Cursorful take of the same
1351
- // steps: a zoom-in of ~0.55 s on a heavy ease-out (most of the move in
1352
- // the first quarter second, a long settle), a zoom-out of ~0.45 s, and
1353
- // holds of four to five seconds per beat. The old 1.1 s ramp started
1354
- // 0.75 s BEFORE the click and landed 0.35 s after it, so the frame was
1355
- // moving more of the time than it was resting.
1356
- glide: {
1357
- autoZoom: true,
1358
- clusterGap: 3,
1359
- minClusterClicks: 1,
1360
- targetFill: 0.42,
1361
- minLevel: 1.3,
1362
- maxLevel: 1.8,
1363
- dragLevel: 1.5,
1364
- lead: 0.35,
1365
- hold: 1.2,
1366
- followByDefault: true,
1367
- typingZoom: true,
1368
- typingGap: 2.5,
1369
- typingHold: 1.1,
1370
- typingMinLevel: 1.4,
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
- // The reference's corner trace at 60 fps: a quarter of the move by a
1377
- // fifth of the time, two thirds by half, nine tenths by three
1378
- // quarters, over ~0.6 s. Fitted against those three points,
1379
- // (0.36, 0, 0.4, 1) is the closest cubic (15 / 64 / 93 %; half the
1380
- // move at ~245 ms and 90 % at ~425 ms at 0.6 s): a gentle start and a
1381
- // long settle. CSS ease (0.25, 0.1, 0.25, 1) reached the midpoint
1382
- // 70 ms earlier and read a touch fast; (0.16, 1, 0.3, 1) put half the
1383
- // move in the first sixty milliseconds and read as a twitch.
1384
- ease: "css-bezier(0.36, 0, 0.4, 1)",
1385
- panEase: "css-bezier(0.36, 0, 0.4, 1)",
1386
- followSafeRatio: 0.45,
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.2,
1329
+ clusterGap: 1.5,
1395
1330
  minClusterClicks: 1,
1396
1331
  targetFill: 0.5,
1397
- minLevel: 1.5,
1398
- maxLevel: 2.2,
1399
- dragLevel: 1.7,
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.2,
1406
- typingMinLevel: 1.6,
1407
- rampIn: 1.4,
1408
- rampInOverlap: 0.5,
1409
- rampOut: 1,
1410
- chainGap: 1.35,
1411
- pan: 1,
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.6,
1350
+ followRecenter: 0.55,
1416
1351
  followLookahead: 0,
1417
- tilt: { intensity: "off" }
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" — slower, more fluid; creative content over reading.
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: 1.8,
1367
+ clusterGap: 2.4,
1423
1368
  minClusterClicks: 1,
1424
- targetFill: 0.45,
1425
- minLevel: 1.35,
1426
- maxLevel: 2,
1369
+ targetFill: 0.42,
1370
+ minLevel: 1.3,
1371
+ maxLevel: 1.8,
1427
1372
  dragLevel: 1.5,
1428
1373
  lead: 0.6,
1429
- hold: 1.4,
1374
+ hold: 2,
1430
1375
  followByDefault: true,
1431
1376
  typingZoom: true,
1432
1377
  typingGap: 3,
1433
- typingHold: 1.6,
1434
- typingMinLevel: 1.45,
1435
- rampIn: 1.8,
1436
- rampInOverlap: 0.55,
1437
- rampOut: 1.5,
1438
- chainGap: 2.2,
1439
- pan: 1.4,
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: 1,
1388
+ followRecenter: 0.9,
1444
1389
  followLookahead: 0.5,
1445
- tilt: { intensity: "off" }
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.5,
1455
- dragLevel: 1.7,
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.8,
1414
+ typingHold: 0.9,
1462
1415
  typingMinLevel: 1.5,
1463
- rampIn: 0.7,
1464
- rampInOverlap: 0.3,
1465
- rampOut: 0.75,
1466
- chainGap: 1.5,
1467
- pan: 0.8,
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.5,
1424
+ followRecenter: 0.45,
1472
1425
  followLookahead: 0,
1473
- tilt: { intensity: "off" }
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 (2–4 frames), tutorial tempo.
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.5,
1482
- maxLevel: 2.2,
1483
- dragLevel: 1.7,
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.6,
1452
+ typingMinLevel: 1.5,
1491
1453
  rampIn: 0.14,
1492
1454
  rampInOverlap: 0.07,
1493
1455
  rampOut: 0.14,
1494
- chainGap: 1.2,
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: "medium",
1571
- rampIn: 1.2,
1572
- rampOut: 1.1,
1573
- chainGap: 2.5,
1574
- pan: 1.1
1464
+ intensity: "off",
1465
+ rampIn: 0.3,
1466
+ rampOut: 0.3,
1467
+ chainGap: 1.3,
1468
+ pan: 0.35
1575
1469
  }
1576
1470
  },
1577
- // "drift": calm ambient depth — cinema's slow, fluid blocks with a SUBTLE
1578
- // lean that eases in over ~1.6s and lingers (long chain gap keeps the card
1579
- // from flattening between nearby beats). For watched-not-read content:
1580
- // launch films, portfolio clips, hero loops.
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 params = name ? ZOOM_STYLES[name] : void 0;
1618
- return { ...params ?? ZOOM_STYLES[DEFAULT_ZOOM_STYLE], ...overrides };
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 cluster, settles fast"
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
- if (dur >= DWELL_MIN && dur <= DWELL_MAX) {
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 start = push2(
5301
- tIn - (style.rampIn - style.rampInOverlap) * m,
5302
- [1, z.cx, z.cy, 0],
5303
- "none"
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.min(
5334
- tOut + style.pan * mNext,
5335
- next.tIn + style.rampInOverlap * mNext
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 start = push2(tIn - rampInDur * m, rest, "none");
5374
- push2(start + rampInDur * m, pose, spanEase(z.ease, TILT_EASE));
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.min(tOut + panDur * transitionMult(next.z.transition), next.tIn),
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,