@vosjs/studio-core 0.7.0 → 0.8.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 CHANGED
@@ -36,7 +36,7 @@ The shared layers lower as one engine `stack` entry, `STUDIO_ENTRY_ID` (`'vosso.
36
36
  - **Seconds everywhere.** Zoom, speed, tilt and cam-motion spans and the cursor samples are anchored in source time, so they follow footage through trims. Overlays, audio clips and click effects are anchored in output time, so a title keeps its perceived length through a speed change. `ratedSegments`, `effectiveSegments`, `docOutputDuration` and `outputRangeToSource` are the remaps.
37
37
  - **Normalized coordinates.** A zoom's `cx`/`cy` and an overlay's `transform.x`/`y` are fractions of the frame in `[0, 1]`, never pixels, so a document survives an aspect-ratio switch. `focusBounds` and `clampFocus` keep a focus inside the card at a given `level`.
38
38
  - **Camera styles.** `ZOOM_STYLES` names eight camera personalities (`glide`, the default, then `focus`, `cinema`, `snappy`, `cut`, `keynote`, `drift`, `none`); a style parametrizes both the planner and the camera motion, and `doc.zoomParams` layers per-document overrides.
39
- - **The stage.** `CARD_FOV`, `CARD_Z`, `OVERLAY_Z`, `BACKGROUND_Z` and `planeSizeAtDepth` define the frustum-filling planes the program draws (background, the tiltable card, the overlay); `computeCardLayout` and `docCardLayout` are the host-side mirror of the program's card math, pinned to it by tests.
39
+ - **The stage.** `CARD_FOV`, `CARD_Z`, `OVERLAY_Z`, `BACKGROUND_Z` and `planeSizeAtDepth` define the frustum-filling planes the program draws (background, the tiltable card, the overlay); `cardVisibleExtent` and `cardOverscanFor` are how far a posed card is seen past the frame, which is what the card layer's canvas and plane grow by (so a lean over zoomed footage never shows the texture's edge); `computeCardLayout` and `docCardLayout` are the host-side mirror of the program's card math, pinned to it by tests.
40
40
  - **Backdrops.** `withBackdrop(frame, backdrop)` and `backdropMedia` put a looping video or a still behind the card, output-anchored modulo its duration. The package carries the mechanism and `BASE_FRAME_STYLE`; which loop a host opens on is the host's choice.
41
41
 
42
42
  ## Seeing a recording: the digest
package/dist/index.js CHANGED
@@ -1266,7 +1266,8 @@ var CARD_FOV = 30;
1266
1266
  var OVERLAY_Z = -2;
1267
1267
  var CARD_Z = -4;
1268
1268
  var BACKGROUND_Z = -6;
1269
- var CARD_OVERSCAN = 1.25;
1269
+ var CARD_OVERSCAN_STEP = 0.05;
1270
+ var CARD_OVERSCAN_MAX = 4;
1270
1271
  var CAMERA_NEAR = 0.1;
1271
1272
  var CAMERA_FAR = 100;
1272
1273
  function planeSizeAtDepth(distance, fovDeg, aspect) {
@@ -1813,7 +1814,11 @@ var STUDIO_SETUP = `async (ctx) => {
1813
1814
  if (olv && olv.length && typeof FontFace !== 'undefined') {
1814
1815
  try {
1815
1816
  const faces = ctx.data.overlayFonts || ${JSON.stringify(OVERLAY_FONT_FACES)}
1817
+ // Mark what SETUP registers, so ON_FRAME's live path (a text overlay
1818
+ // arriving through SET_DATA) never adds the same face twice.
1819
+ const ofSet = window.__voilaFontSet || (window.__voilaFontSet = {})
1816
1820
  const loads = faces.map((f) => {
1821
+ ofSet[f.family + '|' + f.weight] = 1
1817
1822
  const ff = new FontFace(f.family, 'url(' + f.url + ')', { weight: String(f.weight) })
1818
1823
  document.fonts.add(ff)
1819
1824
  return ff.load()
@@ -2273,20 +2278,37 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
2273
2278
  continue
2274
2279
  }
2275
2280
  var olPx = ol.fs * olMS * s
2276
- // Live style edits: SET_DATA never re-runs SETUP, so an override face
2277
- // arriving mid-session lazy-loads here (fail-open; frames repaint as it
2278
- // lands). Cold loads (export) awaited the full list in SETUP already.
2279
- if (ol.face && typeof FontFace !== 'undefined') {
2281
+ // Faces arriving LIVE: SET_DATA never re-runs SETUP, so the first text
2282
+ // overlay a session gains registers the house faces here (a title added
2283
+ // to a fresh document used to paint in the stack's system fallback until
2284
+ // the next cold load), and an override face lazy-loads the same way.
2285
+ // Keyed per window, so SETUP's cold-load faces and repeat frames never
2286
+ // re-register; fail-open, frames repaint as each face lands.
2287
+ if (typeof FontFace !== 'undefined') {
2280
2288
  var ofSet = window.__voilaFontSet || (window.__voilaFontSet = {})
2281
- var ofKey = ol.face.f + '|' + ol.face.w
2282
- if (!ofSet[ofKey]) {
2283
- ofSet[ofKey] = 1
2289
+ var ofBase = d.overlayFonts || ${JSON.stringify(OVERLAY_FONT_FACES)}
2290
+ for (var ofi = 0; ofi < ofBase.length; ofi++) {
2291
+ var ofB = ofBase[ofi]
2292
+ var ofBK = ofB.family + '|' + ofB.weight
2293
+ if (ofSet[ofBK]) continue
2294
+ ofSet[ofBK] = 1
2284
2295
  try {
2285
- var ofFace = new FontFace(ol.face.f, 'url(' + ol.face.u + ')', { weight: String(ol.face.w) })
2286
- document.fonts.add(ofFace)
2287
- ofFace.load().catch(function () {})
2296
+ var ofBF = new FontFace(ofB.family, 'url(' + ofB.url + ')', { weight: String(ofB.weight) })
2297
+ document.fonts.add(ofBF)
2298
+ ofBF.load().catch(function () {})
2288
2299
  } catch (e) {}
2289
2300
  }
2301
+ if (ol.face) {
2302
+ var ofKey = ol.face.f + '|' + ol.face.w
2303
+ if (!ofSet[ofKey]) {
2304
+ ofSet[ofKey] = 1
2305
+ try {
2306
+ var ofFace = new FontFace(ol.face.f, 'url(' + ol.face.u + ')', { weight: String(ol.face.w) })
2307
+ document.fonts.add(ofFace)
2308
+ ofFace.load().catch(function () {})
2309
+ } catch (e) {}
2310
+ }
2311
+ }
2290
2312
  }
2291
2313
  ovC.save()
2292
2314
  ovC.globalAlpha = olA
@@ -3561,30 +3583,81 @@ var ON_FRAME = `(ctx, content, dt) => {
3561
3583
  // the GPU texture allocated at the original dims and re-uploads the new canvas
3562
3584
  // against stale dims \u2192 stretch/duplicate; dispose() forces a full realloc) and
3563
3585
  // rebuilding each frustum-filling plane at the new aspect + syncing the camera.
3564
- // Card OVERSCAN (stage.ts CARD_OVERSCAN): a frame whose inset bleeds the
3565
- // card past an edge paints it on a canvas and a plane larger than the
3566
- // frame by ck about the centre, so a tilt that turns the bled edge back
3567
- // toward the viewer shows card there, not the texture's edge. Every doc
3568
- // that keeps its card inside the frame (and the flat stub, which has no
3569
- // card layer) stays at 1: byte- and pixel-identical.
3586
+ // Card OVERSCAN: the card's pose (the tilt track, the card-pose track)
3587
+ // lets the frame see MORE plane than the frame is wide on a receding
3588
+ // side, and the card's content (zoomed footage, a bled card, the shadow)
3589
+ // runs past the frame, so a plane that ends at the frame edge shows its
3590
+ // texture's edge, and the backdrop through it, as soon as it leans. The
3591
+ // card layer's canvas and plane therefore grow about the frame's centre
3592
+ // by the extent the two tracks can reach at the LIVE aspect: each frame
3593
+ // corner's viewing ray meets the posed plane and reads back in plane
3594
+ // units; the extremes of both tracks (largest lean per axis, smallest
3595
+ // scale, largest rise, every sign) bound every interpolated frame. This
3596
+ // is the twin of stage.ts cardVisibleExtent/cardOverscanFor (pinned by
3597
+ // stage.test.ts + cardTilt.test.ts; change them together). A card that
3598
+ // never moves, and the flat stub (no card layer), stay at 1: byte- and
3599
+ // pixel-identical. The factor is quantised up in steps so a scrubbed
3600
+ // pose grows the canvas a few times, not per tick.
3570
3601
  var d = ctx.data || {}
3571
- var ckIns = (d.frame && d.frame.inset) || {}
3572
- var ck = r.card && (ckIns.left < 0 || ckIns.right < 0 || ckIns.top < 0 || ckIns.bottom < 0) ? ${CARD_OVERSCAN} : 1
3573
- var cW = Math.round(W * ck), cH = Math.round(H * ck)
3602
+ var okx = 1, oky = 1
3603
+ if (r.card) {
3604
+ var okT = d.tiltTrack, okP = d.cardPoseTrack, okKf, oki, okv
3605
+ var okRx = 0, okRy = 0, okSc = 1, okDy = 0
3606
+ okKf = okT && okT.keyframes || []
3607
+ for (oki = 0; oki < okKf.length; oki++) { okv = okKf[oki].value || []; okRx = Math.max(okRx, Math.abs(okv[0] || 0)); okRy = Math.max(okRy, Math.abs(okv[1] || 0)) }
3608
+ okKf = okP && okP.keyframes || []
3609
+ for (oki = 0; oki < okKf.length; oki++) { okv = okKf[oki].value || []; okSc = Math.min(okSc, okv[0] == null ? 1 : okv[0]); okDy = Math.max(okDy, Math.abs(okv[1] || 0)) }
3610
+ if (okRx || okRy || okSc < 1 || okDy) {
3611
+ var okD = ${Math.abs(CARD_Z)}, okMax = ${CARD_OVERSCAN_MAX}, okStep = ${CARD_OVERSCAN_STEP}
3612
+ var okHh = okD * Math.tan(${CARD_FOV} * Math.PI / 180 / 2), okHw = okHh * W / H
3613
+ // [max |x|, max |y|] of the frame corners on the posed plane, in
3614
+ // half-plane units (R = Rx \xB7 Ry, three.js Euler XYZ with rz = 0).
3615
+ var okE = function (rxd, ryd, sc, dy) {
3616
+ var rx = rxd * Math.PI / 180, ry = ryd * Math.PI / 180
3617
+ var cx = Math.cos(rx), sx = Math.sin(rx), cy = Math.cos(ry), sy = Math.sin(ry)
3618
+ var scl = Math.max(0.05, sc), py = dy * 2 * okHh
3619
+ var n0 = sy, n1 = -sx * cy, n2 = cx * cy
3620
+ var nPos = n1 * py - n2 * okD
3621
+ var ex = 0, ey = 0
3622
+ for (var kx = -1; kx <= 1; kx += 2) for (var ky = -1; ky <= 1; ky += 2) {
3623
+ var d0 = kx * okHw, d1 = ky * okHh, d2 = -okD
3624
+ var nd = n0 * d0 + n1 * d1 + n2 * d2
3625
+ var lam = nd === 0 ? -1 : nPos / nd
3626
+ if (!(lam > 0)) { ex = okMax; ey = okMax; continue }
3627
+ var q0 = lam * d0, q1 = lam * d1 - py, q2 = lam * d2 + okD
3628
+ ex = Math.max(ex, Math.abs((cy * q0 + sx * sy * q1 - cx * sy * q2) / scl / okHw))
3629
+ ey = Math.max(ey, Math.abs((cx * q1 + sx * q2) / scl / okHh))
3630
+ }
3631
+ return [Math.min(okMax, ex), Math.min(okMax, ey)]
3632
+ }
3633
+ for (var okA = -1; okA <= 1; okA += 2) for (var okB = -1; okB <= 1; okB += 2) for (var okC = -1; okC <= 1; okC += 2) {
3634
+ var okR = okE(okA * okRx, okB * okRy, okSc, okC * okDy)
3635
+ okx = Math.max(okx, okR[0]); oky = Math.max(oky, okR[1])
3636
+ }
3637
+ var okQ = function (k) { if (!(k > 1)) return 1; var q = Math.ceil(Math.round((k + okStep / 10) * 1000) / (okStep * 1000)) * okStep; return Math.min(okMax, Math.round(q * 1000) / 1000) }
3638
+ okx = okQ(okx); oky = okQ(oky)
3639
+ }
3640
+ }
3641
+ // The canvas grows by an EVEN number of pixels on each axis so the frame
3642
+ // region sits on whole pixels inside it (a half-pixel base translation
3643
+ // would resample every draw).
3644
+ var cW = W + 2 * Math.round(W * (okx - 1) / 2), cH = H + 2 * Math.round(H * (oky - 1) / 2)
3574
3645
  if (cv.width !== cW || cv.height !== cH) {
3575
3646
  var THREE = ctx.THREE
3576
3647
  var aspect = W / H
3577
3648
  var cm = ctx.camera
3578
3649
  if (cm && cm.isPerspectiveCamera) { cm.aspect = aspect; cm.updateProjectionMatrix() }
3579
- var lyr = [[bg, ${BACKGROUND_Z}, 1], [card, ${CARD_Z}, ck], [ov, ${OVERLAY_Z}, 1]]
3650
+ // Each layer's canvas size and its plane's per-axis growth; the card's
3651
+ // plane grows by exactly its canvas's ratio, so a texel stays a pixel.
3652
+ var lyr = [[bg, ${BACKGROUND_Z}, W, H], [card, ${CARD_Z}, cW, cH], [ov, ${OVERLAY_Z}, W, H]]
3580
3653
  for (var Li = 0; Li < lyr.length; Li++) {
3581
- var Ly = lyr[Li][0], Lz = lyr[Li][1], Lk = lyr[Li][2]
3582
- if (Ly.canvas) { Ly.canvas.width = Math.round(W * Lk); Ly.canvas.height = Math.round(H * Lk) }
3654
+ var Ly = lyr[Li][0], Lz = lyr[Li][1], Lw = lyr[Li][2], Lhp = lyr[Li][3]
3655
+ if (Ly.canvas) { Ly.canvas.width = Lw; Ly.canvas.height = Lhp }
3583
3656
  if (Ly.texture && Ly.texture.dispose) Ly.texture.dispose()
3584
3657
  if (Ly.mesh && THREE) {
3585
3658
  if (Ly.mesh.geometry && Ly.mesh.geometry.dispose) Ly.mesh.geometry.dispose()
3586
- var Lh = 2 * Math.abs(Lz) * Math.tan(${CARD_FOV} * Math.PI / 180 / 2) * Lk
3587
- Ly.mesh.geometry = new THREE.PlaneGeometry(Lh * aspect, Lh)
3659
+ var Lh = 2 * Math.abs(Lz) * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
3660
+ Ly.mesh.geometry = new THREE.PlaneGeometry(Lh * aspect * (Lw / W), Lh * (Lhp / H))
3588
3661
  }
3589
3662
  }
3590
3663
  }
@@ -3908,9 +3981,10 @@ var ON_FRAME = `(ctx, content, dt) => {
3908
3981
  // the contain-fit card shows the background layer through the plane's alpha.
3909
3982
  // Under overscan the canvas is larger than the frame and every frame-space
3910
3983
  // draw below rides a base translation to its centre.
3911
- if (ck !== 1) c.setTransform(1, 0, 0, 1, 0, 0)
3984
+ var okOn = cW !== W || cH !== H
3985
+ if (okOn) c.setTransform(1, 0, 0, 1, 0, 0)
3912
3986
  c.clearRect(0, 0, cv.width, cv.height)
3913
- if (ck !== 1) c.setTransform(1, 0, 0, 1, (cW - W) / 2, (cH - H) / 2)
3987
+ if (okOn) c.setTransform(1, 0, 0, 1, (cW - W) / 2, (cH - H) / 2)
3914
3988
  // Footage is nearly always drawn SMALLER than it was recorded (a 1920
3915
3989
  // recording in a padded 1080 card, or a 440-wide tile): the default
3916
3990
  // bilinear tap aliases and blurs a large downscale; 'high' resamples.
@@ -4421,8 +4495,10 @@ var ON_FRAME = `(ctx, content, dt) => {
4421
4495
  card.mesh.rotation.y = ry
4422
4496
  // The card's pose through an entrance or an end card: [scale, dy,
4423
4497
  // opacity]; absent = the rest pose, exactly as before the track existed.
4498
+ // dy is a fraction of the FRAME plane's height (not the geometry's,
4499
+ // which the overscan grows).
4424
4500
  var cpk = d.cardPoseTrack
4425
- var cpH = card.mesh.geometry && card.mesh.geometry.parameters ? card.mesh.geometry.parameters.height : 0
4501
+ var cpH = 2 * ${Math.abs(CARD_Z)} * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
4426
4502
  if (cpk && cpk.keyframes && cpk.keyframes.length && card.mesh.scale && card.mesh.position) {
4427
4503
  var cpv = TL.sample(cpk, t, TL.lerpArray)
4428
4504
  card.mesh.scale.x = cpv[0]; card.mesh.scale.y = cpv[0]