@vosjs/studio-core 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2740,63 +2740,30 @@ function styledTextOf(clip) {
2740
2740
  h: hashOf(JSON.stringify([l, fs]))
2741
2741
  };
2742
2742
  }
2743
- function overlayTokens(line) {
2744
- return line.match(/\S+\s*/g) ?? [line];
2745
- }
2746
- function wrapOverlayLines(lines, measure, maxPx) {
2747
- if (!(maxPx > 0)) return lines;
2748
- const out = [];
2749
- for (const line of lines) {
2750
- if (!line || measure(line) <= maxPx) {
2751
- out.push(line);
2752
- continue;
2753
- }
2754
- let current = "";
2755
- for (const token of overlayTokens(line)) {
2756
- if (!current) {
2757
- current = token;
2758
- continue;
2759
- }
2760
- if (measure(current + token) <= maxPx) {
2761
- current += token;
2762
- } else {
2763
- out.push(current);
2764
- current = token;
2743
+ function textRunsOf(clip) {
2744
+ const styled = styledTextOf(clip);
2745
+ if (styled) return styled;
2746
+ const style = resolveOverlayStyle(clip);
2747
+ return {
2748
+ l: overlayLines(overlayPlainText(clip)).map((t) => t ? [{ t }] : []),
2749
+ fs: [
2750
+ {
2751
+ w: style.weight,
2752
+ ...style.fontStyle === "italic" ? { i: 1 } : {}
2765
2753
  }
2766
- }
2767
- if (current) out.push(current);
2768
- }
2769
- return out.length ? out : [""];
2770
- }
2771
- function graphemesOf2(line) {
2772
- if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
2773
- return [
2774
- ...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
2775
- line
2776
- )
2777
- ].map((s) => s.segment);
2778
- }
2779
- return [...line];
2780
- }
2781
- function overlaySegments(text, unit) {
2782
- const lines = overlayLines(text);
2783
- if (unit === "block") return [];
2784
- if (unit === "line") return lines.map((l) => [l]);
2785
- if (unit === "word") return lines.map(overlayTokens);
2786
- return lines.map(graphemesOf2);
2754
+ ]
2755
+ };
2787
2756
  }
2788
2757
  var FX_DIR = { forward: 0, reverse: 1, center: 2 };
2789
2758
  function resolveOverlayFx(clip, clipDuration) {
2790
2759
  const spec = overlayFxSpec(clip) ?? clip.fx ?? null;
2791
2760
  if (!spec) return null;
2792
2761
  const unit = spec.unit ?? "block";
2793
- const text = overlayPlainText(clip);
2794
- const units = overlaySegments(text, unit);
2795
- const n = unit === "block" ? 1 : units.reduce((sum, line) => sum + line.length, 0);
2796
- const upperN = unit === "line" && clip.maxWidth ? overlayLines(text).reduce(
2797
- (sum, line) => sum + overlayTokens(line).length,
2798
- 0
2799
- ) : n;
2762
+ const lines = overlayLines(overlayPlainText(clip));
2763
+ const count = (per) => lines.reduce((sum, line) => sum + per(line), 0);
2764
+ const words = () => count((line) => tokenRanges(line).length);
2765
+ const n = unit === "block" ? 1 : unit === "line" ? lines.length : unit === "word" ? words() : count((line) => graphemesOf(line).length);
2766
+ const upperN = unit === "line" && clip.maxWidth ? words() : n;
2800
2767
  const typewriter = spec.fx === "typewriter";
2801
2768
  const dur = typewriter ? 1e-3 : Math.min(2, Math.max(0.05, spec.duration ?? OVERLAY_TRANSITION_DUR));
2802
2769
  const defaultStagger = typewriter ? 0.05 : unit === "block" ? 0 : 0.06;
@@ -2812,9 +2779,7 @@ function resolveOverlayFx(clip, clipDuration) {
2812
2779
  d: FX_DIR[spec.direction ?? "forward"] ?? 0,
2813
2780
  st: round14(st),
2814
2781
  dur: round14(dur),
2815
- tt: round14(st * (upperN - 1) + dur),
2816
- units,
2817
- n
2782
+ tt: round14(st * (upperN - 1) + dur)
2818
2783
  };
2819
2784
  }
2820
2785
  function overlayFontString(style, scale, s) {
@@ -2838,35 +2803,21 @@ function overlayRect(clip, measure, frameW, frameH = 1080, mediaAspect) {
2838
2803
  return { ...base, w: w2, h: w2 / (mediaAspect || 16 / 9) };
2839
2804
  }
2840
2805
  const style = resolveOverlayStyle(clip);
2841
- const font = overlayFontString(style, scale, 1);
2842
2806
  const ls = style.letterSpacing * scale;
2843
- const styled = styledTextOf(clip);
2844
- let w = 0;
2845
- let lineCount = 0;
2846
- if (styled) {
2847
- const fonts = styled.fs.map(
2848
- (f) => overlayFontString(
2849
- { ...style, weight: f.w, fontStyle: f.i ? "italic" : "normal" },
2850
- scale,
2851
- 1
2852
- )
2853
- );
2854
- const laid = layoutText(
2855
- { lines: styled.l, maxWidth: clip.maxWidth ? clip.maxWidth * frameW : 0 },
2856
- (t, f) => measure(t, fonts[f], ls)
2857
- );
2858
- w = laid.width;
2859
- lineCount = laid.lines.length;
2860
- } else {
2861
- const width = (t) => measure(t, font, ls);
2862
- const lines = clip.maxWidth ? wrapOverlayLines(
2863
- overlayLines(overlayPlainText(clip)),
2864
- width,
2865
- clip.maxWidth * frameW
2866
- ) : overlayLines(overlayPlainText(clip));
2867
- for (const line of lines) w = Math.max(w, width(line));
2868
- lineCount = lines.length;
2869
- }
2807
+ const runs = textRunsOf(clip);
2808
+ const fonts = runs.fs.map(
2809
+ (f) => overlayFontString(
2810
+ { ...style, weight: f.w, fontStyle: f.i ? "italic" : "normal" },
2811
+ scale,
2812
+ 1
2813
+ )
2814
+ );
2815
+ const laid = layoutText(
2816
+ { lines: runs.l, maxWidth: clip.maxWidth ? clip.maxWidth * frameW : 0 },
2817
+ (t, f) => measure(t, fonts[f], ls)
2818
+ );
2819
+ const w = laid.width;
2820
+ const lineCount = laid.lines.length;
2870
2821
  const lineH = style.size * scale * style.lineHeight;
2871
2822
  const box = resolveOverlayBox(clip);
2872
2823
  const padX = box ? box.padX * scale : 0;
@@ -5592,334 +5543,155 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5592
5543
  ovC.textAlign = 'center'
5593
5544
  ovC.textBaseline = 'middle'
5594
5545
  ovC.letterSpacing = ((ol.ls || 0) * olMS * s) + 'px'
5595
- // A STYLED layer (ol.rt: lines of runs, each set its own way) is laid
5596
- // out by the layout module, ONCE: the layout is kept per clip under a
5597
- // key of everything it was measured with, so a frame that only moves,
5598
- // fades or reveals the layer measures nothing. Fragments draw left to
5599
- // right from each line's left edge; a per-unit entrance animates the
5600
- // layout's units, each a few parts of fragments.
5546
+ // Every text layer is laid out by the layout module, ONCE: a styled one
5547
+ // from its runs (ol.rt: lines of runs, each set its own way), a plain
5548
+ // one from its lines, each a single run in the layer's own style. The
5549
+ // layout is kept per clip under a key of everything it was measured
5550
+ // with, so a frame that only moves, fades or reveals the layer measures
5551
+ // nothing. Fragments draw left to right from each line's left edge; a
5552
+ // per-unit entrance animates the layout's units, each a few parts of
5553
+ // fragments.
5554
+ var rtFonts = [ovC.font]
5601
5555
  if (ol.rt) {
5602
- var rtFonts = [ovC.font]
5603
5556
  for (var rfi = 1; rfi < ol.rt.fs.length; rfi++) {
5604
5557
  rtFonts.push((ol.rt.fs[rfi].i ? 'italic ' : '') + ol.rt.fs[rfi].w + ' ' + olPx + 'px ' + ol.stack)
5605
5558
  }
5606
- var rtFx = ol.fx && ol.fx.units.length ? ol.fx : null
5607
- var rtMax = ol.mw ? ol.mw * W : 0
5608
- var rtKey = ol.rt.h + '|' + rtFonts[0] + '|' + ((ol.ls || 0) * olMS * s) + '|' + rtMax + '|' + (rtFx ? rtFx.u : '') + '|' + (ns.fontEpoch || 0)
5609
- var rtAll = ns.textLayouts || (ns.textLayouts = {})
5610
- var rtHit = rtAll[ol.id]
5611
- if (!rtHit || rtHit.k !== rtKey) {
5612
- rtHit = rtAll[ol.id] = {
5613
- k: rtKey,
5614
- L: TXL.layoutText(
5615
- { lines: ol.rt.l, maxWidth: rtMax, unit: rtFx ? rtFx.u : undefined },
5616
- function (mt, mf) { ovC.font = rtFonts[mf]; return ovC.measureText(mt).width }
5617
- )
5618
- }
5619
- ovC.font = rtFonts[0]
5620
- }
5621
- var rtL = rtHit.L
5622
- var rtN = rtL.lines.length
5623
- var rtLH = olPx * (ol.lh || ${OVERLAY_LINE_HEIGHT})
5624
- var rtY0 = -((rtN - 1) * rtLH) / 2
5625
- // A line's left edge: lines sit centred on the anchor and align
5626
- // against the widest, as the plain layer's do.
5627
- var rtLeft = function (lw) {
5628
- var xo = ol.align === 'left' ? (lw - rtL.width) / 2 : ol.align === 'right' ? (rtL.width - lw) / 2 : 0
5629
- return xo - lw / 2
5630
- }
5631
- if (ol.box) {
5632
- var rbPX = ol.box.px * olMS * s
5633
- var rbPY = ol.box.py * olMS * s
5634
- var rbW = rtL.width + rbPX * 2
5635
- var rbH = rtN * rtLH + rbPY * 2
5636
- ovC.save()
5637
- ovC.globalAlpha = olA * ol.box.o
5638
- ovC.fillStyle = ol.box.c
5639
- rr(-rbW / 2, -rbH / 2, rbW, rbH, Math.min(ol.box.r * olMS * s, rbH / 2), ovC)
5640
- ovC.fill()
5641
- ovC.restore()
5642
- }
5643
- // Highlights sit behind the words and take no legibility shadow. Under
5644
- // a per-unit entrance each is drawn with its unit instead (below), so
5645
- // it arrives with its word.
5646
- for (var rhi = 0; !rtFx && rhi < rtN; rhi++) {
5647
- var rhLine = rtL.lines[rhi]
5648
- for (var rhj = 0; rhj < rhLine.frags.length; rhj++) {
5649
- var rhF = rhLine.frags[rhj]
5650
- if (!rhF.h) continue
5651
- var rhR = TXL.decorationRect('h', rtLeft(rhLine.w) + rhF.x, rhF.w, rtY0 + rhi * rtLH, olPx)
5652
- ovC.fillStyle = rhF.h
5653
- ovC.fillRect(rhR.x, rhR.y, rhR.w, rhR.h)
5654
- }
5655
- }
5656
- if (ol.shadow > 0) {
5657
- ovC.shadowColor = 'rgba(0,0,0,' + ol.shadow + ')'
5658
- ovC.shadowBlur = olPx * 0.25
5659
- ovC.shadowOffsetY = olPx * 0.04
5660
- }
5661
- ovC.textAlign = 'left'
5662
- // One fragment (or the part of one a unit holds): the underline under
5663
- // the glyphs, stroke under fill, the strikethrough over them.
5664
- var rtPart = function (p, bx, y, withH) {
5665
- if (withH && p.h) {
5666
- var rhU = TXL.decorationRect('h', bx + p.x, p.w, y, olPx)
5667
- var rhS = ovC.shadowColor
5668
- ovC.shadowColor = 'rgba(0,0,0,0)'
5669
- ovC.fillStyle = p.h
5670
- ovC.fillRect(rhU.x, rhU.y, rhU.w, rhU.h)
5671
- ovC.shadowColor = rhS
5672
- }
5673
- ovC.font = rtFonts[p.f || 0]
5674
- ovC.fillStyle = p.c || ol.color
5675
- if (p.u) {
5676
- var ru = TXL.decorationRect('u', bx + p.x, p.w, y, olPx)
5677
- ovC.fillRect(ru.x, ru.y, ru.w, ru.h)
5678
- }
5679
- if (ol.stroke) {
5680
- ovC.strokeStyle = ol.stroke.c
5681
- ovC.lineWidth = ol.stroke.w * olMS * s
5682
- ovC.lineJoin = 'round'
5683
- ovC.strokeText(p.t, bx + p.x, y)
5684
- }
5685
- ovC.fillText(p.t, bx + p.x, y)
5686
- if (p.s) {
5687
- var rs = TXL.decorationRect('s', bx + p.x, p.w, y, olPx)
5688
- ovC.fillRect(rs.x, rs.y, rs.w, rs.h)
5689
- }
5690
- }
5691
- if (!rtFx) {
5692
- for (var rli = 0; rli < rtN; rli++) {
5693
- var rLine = rtL.lines[rli]
5694
- var rBX = rtLeft(rLine.w)
5695
- for (var rfk = 0; rfk < rLine.frags.length; rfk++) rtPart(rLine.frags[rfk], rBX, rtY0 + rli * rtLH)
5696
- }
5697
- } else {
5698
- // Per-unit entrance, the plain layer's own arithmetic over the
5699
- // layout's units: delay = order(index) x st, eased over dur.
5700
- var rtCount = 0
5701
- for (var rci = 0; rci < rtN; rci++) rtCount += rtL.lines[rci].units.length
5702
- var rtIdx = 0
5703
- for (var rui = 0; rui < rtN; rui++) {
5704
- var rULine = rtL.lines[rui]
5705
- var rUBX = rtLeft(rULine.w)
5706
- var rUY = rtY0 + rui * rtLH
5707
- for (var ruj = 0; ruj < rULine.units.length; ruj++, rtIdx++) {
5708
- var rU = rULine.units[ruj]
5709
- var rOrd = rtFx.d === 1 ? (rtCount - 1 - rtIdx) : rtFx.d === 2 ? Math.abs(rtIdx - (rtCount - 1) / 2) : rtIdx
5710
- var rT2 = olT - rOrd * rtFx.st
5711
- var rUA = 1, rUu = 1
5712
- if (rtFx.k === 'typewriter') {
5713
- rUA = rT2 >= 0 ? 1 : 0
5714
- } else {
5715
- rUu = Math.max(0, Math.min(1, rT2 / rtFx.dur))
5716
- var rUE = 1 - Math.pow(1 - rUu, 3)
5717
- rUA = rtFx.k === 'pop' ? Math.min(1, rUu * 2) : rUE
5718
- }
5719
- if (rUA <= 0.004) continue
5720
- ovC.save()
5721
- ovC.globalAlpha = olA * rUA
5722
- if (rUu < 1) {
5723
- if (rtFx.k === 'rise') {
5724
- ovC.translate(0, (1 - (1 - Math.pow(1 - rUu, 3))) * 24 * s)
5725
- } else if (rtFx.k === 'pop') {
5726
- var rPS = 1 + 2.70158 * Math.pow(rUu - 1, 3) + 1.70158 * Math.pow(rUu - 1, 2)
5727
- var rCX = rUBX + rU.x + rU.w / 2
5728
- ovC.translate(rCX, rUY)
5729
- ovC.scale(rPS, rPS)
5730
- ovC.translate(-rCX, -rUY)
5731
- } else if (rtFx.k === 'blur') {
5732
- ovC.filter = 'blur(' + ((1 - rUu) * olPx * 0.12).toFixed(2) + 'px)'
5733
- }
5734
- }
5735
- for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY, true)
5736
- ovC.restore()
5737
- }
5738
- }
5739
- }
5740
- ovC.restore()
5741
- continue
5742
5559
  }
5743
- var olLines = ol.lines || ['']
5744
- // maxWidth wrap (ol.mw = frame-width fraction): greedy over word tokens
5745
- // (/\\S+\\s*/ \u2014 the SAME tokenization fx uses, trailing spaces kept, so
5746
- // unit sequences stay byte-identical) at measured widths. MIRRORS
5747
- // wrapOverlayLines in overlayText.ts \u2014 change together. A token wider
5748
- // than the budget gets its own line; explicit \\n lines wrap independently.
5749
- if (ol.mw) {
5750
- var olWMax = ol.mw * W
5751
- var olWrapped = []
5752
- for (var olwl = 0; olwl < olLines.length; olwl++) {
5753
- var olWLine = olLines[olwl]
5754
- if (!olWLine || ovC.measureText(olWLine).width <= olWMax) {
5755
- olWrapped.push(olWLine)
5756
- continue
5757
- }
5758
- var olToks = olWLine.match(/\\S+\\s*/g) || [olWLine]
5759
- var olCur = ''
5760
- for (var olti = 0; olti < olToks.length; olti++) {
5761
- if (!olCur) { olCur = olToks[olti]; continue }
5762
- if (ovC.measureText(olCur + olToks[olti]).width <= olWMax) {
5763
- olCur += olToks[olti]
5764
- } else {
5765
- olWrapped.push(olCur)
5766
- olCur = olToks[olti]
5767
- }
5768
- }
5769
- if (olCur) olWrapped.push(olCur)
5770
- }
5771
- olLines = olWrapped.length ? olWrapped : ['']
5772
- }
5773
- var olLH = olPx * (ol.lh || ${OVERLAY_LINE_HEIGHT})
5774
- var olY0 = -((olLines.length - 1) * olLH) / 2
5775
- // Per-line widths: needed by the pill, by left/right alignment (lines
5776
- // draw centered; alignment is an x offset against the widest line), and
5777
- // by per-unit fx (units place by prefix advance from the line's left edge).
5778
- var olFx = ol.fx && ol.fx.units.length ? ol.fx : null
5779
- // With wrap active, baked per-line unit arrays regroup onto the WRAPPED
5780
- // lines. Wrapping never reorders: word/char units consume in flat order
5781
- // by string length (wrapped lines are token concatenations); 'line'
5782
- // units become one per wrapped line. Flat delay order is unchanged.
5783
- if (olFx && ol.mw) {
5784
- var olFlat = []
5785
- for (var olfi = 0; olfi < olFx.units.length; olfi++) {
5786
- for (var olfj = 0; olfj < olFx.units[olfi].length; olfj++) {
5787
- olFlat.push(olFx.units[olfi][olfj])
5788
- }
5789
- }
5790
- var olRe = []
5791
- if (olFx.u === 'line') {
5792
- for (var olri = 0; olri < olLines.length; olri++) olRe.push([olLines[olri]])
5793
- } else {
5794
- var olFk = 0
5795
- for (var olri2 = 0; olri2 < olLines.length; olri2++) {
5796
- var olNeed = olLines[olri2].length
5797
- var olArr = []
5798
- var olGot = 0
5799
- while (olFk < olFlat.length && olGot < olNeed) {
5800
- olArr.push(olFlat[olFk])
5801
- olGot += olFlat[olFk].length
5802
- olFk++
5803
- }
5804
- olRe.push(olArr)
5805
- }
5806
- }
5807
- var olReN = 0
5808
- for (var olrn = 0; olrn < olRe.length; olrn++) olReN += olRe[olrn].length
5809
- olFx = { k: olFx.k, u: olFx.u, d: olFx.d, st: olFx.st, dur: olFx.dur, tt: olFx.tt, units: olRe, n: olReN }
5810
- }
5811
- var olLWs = null, olMaxW = 0
5812
- if (ol.box || ol.align || olFx) {
5813
- olLWs = []
5814
- for (var olwi = 0; olwi < olLines.length; olwi++) {
5815
- var olw = ovC.measureText(olLines[olwi]).width
5816
- olLWs.push(olw)
5817
- if (olw > olMaxW) olMaxW = olw
5560
+ var rtFx = ol.fx && ol.fx.u !== 'block' ? ol.fx : null
5561
+ var rtMax = ol.mw ? ol.mw * W : 0
5562
+ var rtKey = (ol.rt ? 'r' + ol.rt.h : 'p' + (ol.lines || ['']).join('\\n')) + '|' + rtFonts[0] + '|' + ((ol.ls || 0) * olMS * s) + '|' + rtMax + '|' + (rtFx ? rtFx.u : '') + '|' + (ns.fontEpoch || 0)
5563
+ var rtAll = ns.textLayouts || (ns.textLayouts = {})
5564
+ var rtHit = rtAll[ol.id]
5565
+ if (!rtHit || rtHit.k !== rtKey) {
5566
+ var rtIn = ol.rt ? ol.rt.l : []
5567
+ if (!ol.rt) {
5568
+ var rtPl = ol.lines || ['']
5569
+ for (var rpi = 0; rpi < rtPl.length; rpi++) rtIn.push(rtPl[rpi] ? [{ t: rtPl[rpi] }] : [])
5570
+ }
5571
+ rtHit = rtAll[ol.id] = {
5572
+ k: rtKey,
5573
+ L: TXL.layoutText(
5574
+ { lines: rtIn, maxWidth: rtMax, unit: rtFx ? rtFx.u : undefined },
5575
+ function (mt, mf) { ovC.font = rtFonts[mf]; return ovC.measureText(mt).width }
5576
+ )
5818
5577
  }
5578
+ ovC.font = rtFonts[0]
5579
+ }
5580
+ var rtL = rtHit.L
5581
+ var rtN = rtL.lines.length
5582
+ var rtLH = olPx * (ol.lh || ${OVERLAY_LINE_HEIGHT})
5583
+ var rtY0 = -((rtN - 1) * rtLH) / 2
5584
+ // A line's left edge: lines sit centred on the anchor and align
5585
+ // against the widest.
5586
+ var rtLeft = function (lw) {
5587
+ var xo = ol.align === 'left' ? (lw - rtL.width) / 2 : ol.align === 'right' ? (rtL.width - lw) / 2 : 0
5588
+ return xo - lw / 2
5819
5589
  }
5820
- // Background pill (ol.box, baked design px at fs): drawn BEFORE the text
5821
- // and before the legibility shadow config, so the pill never inherits the
5822
- // text shadow. Geometry mirrors overlayRect's inflation \u2014 change together.
5823
5590
  if (ol.box) {
5824
- var obPX = ol.box.px * olMS * s
5825
- var obPY = ol.box.py * olMS * s
5826
- var obFullW = olMaxW + obPX * 2
5827
- var obFullH = olLines.length * olLH + obPY * 2
5828
- var obR = Math.min(ol.box.r * olMS * s, obFullH / 2)
5591
+ var rbPX = ol.box.px * olMS * s
5592
+ var rbPY = ol.box.py * olMS * s
5593
+ var rbW = rtL.width + rbPX * 2
5594
+ var rbH = rtN * rtLH + rbPY * 2
5829
5595
  ovC.save()
5830
5596
  ovC.globalAlpha = olA * ol.box.o
5831
5597
  ovC.fillStyle = ol.box.c
5832
- rr(-obFullW / 2, -obFullH / 2, obFullW, obFullH, obR, ovC)
5598
+ rr(-rbW / 2, -rbH / 2, rbW, rbH, Math.min(ol.box.r * olMS * s, rbH / 2), ovC)
5833
5599
  ovC.fill()
5834
5600
  ovC.restore()
5835
5601
  }
5602
+ // Highlights sit behind the words and take no legibility shadow. Under
5603
+ // a per-unit entrance each is drawn with its unit instead (below), so
5604
+ // it arrives with its word.
5605
+ for (var rhi = 0; !rtFx && rhi < rtN; rhi++) {
5606
+ var rhLine = rtL.lines[rhi]
5607
+ for (var rhj = 0; rhj < rhLine.frags.length; rhj++) {
5608
+ var rhF = rhLine.frags[rhj]
5609
+ if (!rhF.h) continue
5610
+ var rhR = TXL.decorationRect('h', rtLeft(rhLine.w) + rhF.x, rhF.w, rtY0 + rhi * rtLH, olPx)
5611
+ ovC.fillStyle = rhF.h
5612
+ ovC.fillRect(rhR.x, rhR.y, rhR.w, rhR.h)
5613
+ }
5614
+ }
5836
5615
  if (ol.shadow > 0) {
5837
5616
  ovC.shadowColor = 'rgba(0,0,0,' + ol.shadow + ')'
5838
5617
  ovC.shadowBlur = olPx * 0.25
5839
5618
  ovC.shadowOffsetY = olPx * 0.04
5840
5619
  }
5841
- ovC.fillStyle = ol.color
5842
- if (!olFx) {
5843
- for (var ok = 0; ok < olLines.length; ok++) {
5844
- var olXof = 0
5845
- if (ol.align && olLWs) {
5846
- olXof = ol.align === 'left'
5847
- ? (olLWs[ok] - olMaxW) / 2
5848
- : (olMaxW - olLWs[ok]) / 2
5849
- }
5850
- var olLY = olY0 + ok * olLH
5851
- if (ol.stroke) {
5852
- ovC.strokeStyle = ol.stroke.c
5853
- ovC.lineWidth = ol.stroke.w * olMS * s
5854
- ovC.lineJoin = 'round'
5855
- ovC.strokeText(olLines[ok], olXof, olLY)
5856
- }
5857
- ovC.fillText(olLines[ok], olXof, olLY)
5620
+ ovC.textAlign = 'left'
5621
+ // One fragment (or the part of one a unit holds): the underline under
5622
+ // the glyphs, stroke under fill, the strikethrough over them.
5623
+ var rtPart = function (p, bx, y, withH) {
5624
+ if (withH && p.h) {
5625
+ var rhU = TXL.decorationRect('h', bx + p.x, p.w, y, olPx)
5626
+ var rhS = ovC.shadowColor
5627
+ ovC.shadowColor = 'rgba(0,0,0,0)'
5628
+ ovC.fillStyle = p.h
5629
+ ovC.fillRect(rhU.x, rhU.y, rhU.w, rhU.h)
5630
+ ovC.shadowColor = rhS
5631
+ }
5632
+ ovC.font = rtFonts[p.f || 0]
5633
+ ovC.fillStyle = p.c || ol.color
5634
+ if (p.u) {
5635
+ var ru = TXL.decorationRect('u', bx + p.x, p.w, y, olPx)
5636
+ ovC.fillRect(ru.x, ru.y, ru.w, ru.h)
5637
+ }
5638
+ if (ol.stroke) {
5639
+ ovC.strokeStyle = ol.stroke.c
5640
+ ovC.lineWidth = ol.stroke.w * olMS * s
5641
+ ovC.lineJoin = 'round'
5642
+ ovC.strokeText(p.t, bx + p.x, y)
5643
+ }
5644
+ ovC.fillText(p.t, bx + p.x, y)
5645
+ if (p.s) {
5646
+ var rs = TXL.decorationRect('s', bx + p.x, p.w, y, olPx)
5647
+ ovC.fillRect(rs.x, rs.y, rs.w, rs.h)
5648
+ }
5649
+ }
5650
+ if (!rtFx) {
5651
+ for (var rli = 0; rli < rtN; rli++) {
5652
+ var rLine = rtL.lines[rli]
5653
+ var rBX = rtLeft(rLine.w)
5654
+ for (var rfk = 0; rfk < rLine.frags.length; rfk++) rtPart(rLine.frags[rfk], rBX, rtY0 + rli * rtLH)
5858
5655
  }
5859
5656
  } else {
5860
- // Per-unit entrance: units draw LEFT-aligned at prefix advances
5861
- // measured from the full line (exact bar cross-unit kerning), so the
5862
- // settled frame matches the non-fx layout. Per-unit progress is pure
5863
- // f(t): delay = order(index)\xB7st, eased over dur; typewriter is a step
5864
- // reveal. Stroke-under-fill per unit; pill/shadow config above apply.
5865
- ovC.textAlign = 'left'
5866
- var olIdx = 0
5867
- for (var ok2 = 0; ok2 < olFx.units.length; ok2++) {
5868
- var olUs = olFx.units[ok2]
5869
- var olLW2 = olLWs ? olLWs[ok2] : 0
5870
- var olLY2 = olY0 + ok2 * olLH
5871
- var olXof2 = 0
5872
- if (ol.align) {
5873
- olXof2 = ol.align === 'left'
5874
- ? (olLW2 - olMaxW) / 2
5875
- : (olMaxW - olLW2) / 2
5876
- }
5877
- var olXb = olXof2 - olLW2 / 2
5878
- var olPref = '', olPW = 0
5879
- for (var ou = 0; ou < olUs.length; ou++, olIdx++) {
5880
- var olOrd = olFx.d === 1
5881
- ? (olFx.n - 1 - olIdx)
5882
- : olFx.d === 2
5883
- ? Math.abs(olIdx - (olFx.n - 1) / 2)
5884
- : olIdx
5885
- var olT2 = olT - olOrd * olFx.st
5886
- var olNext = olPref + olUs[ou]
5887
- var olNW = ovC.measureText(olNext).width
5888
- var olUW = olNW - olPW
5889
- var olUX = olXb + olPW
5890
- var olUA = 1, olUu = 1
5891
- if (olFx.k === 'typewriter') {
5892
- olUA = olT2 >= 0 ? 1 : 0
5657
+ // Per-unit entrance, the plain layer's own arithmetic over the
5658
+ // layout's units: delay = order(index) x st, eased over dur.
5659
+ var rtCount = 0
5660
+ for (var rci = 0; rci < rtN; rci++) rtCount += rtL.lines[rci].units.length
5661
+ var rtIdx = 0
5662
+ for (var rui = 0; rui < rtN; rui++) {
5663
+ var rULine = rtL.lines[rui]
5664
+ var rUBX = rtLeft(rULine.w)
5665
+ var rUY = rtY0 + rui * rtLH
5666
+ for (var ruj = 0; ruj < rULine.units.length; ruj++, rtIdx++) {
5667
+ var rU = rULine.units[ruj]
5668
+ var rOrd = rtFx.d === 1 ? (rtCount - 1 - rtIdx) : rtFx.d === 2 ? Math.abs(rtIdx - (rtCount - 1) / 2) : rtIdx
5669
+ var rT2 = olT - rOrd * rtFx.st
5670
+ var rUA = 1, rUu = 1
5671
+ if (rtFx.k === 'typewriter') {
5672
+ rUA = rT2 >= 0 ? 1 : 0
5893
5673
  } else {
5894
- olUu = Math.max(0, Math.min(1, olT2 / olFx.dur))
5895
- var olUE = 1 - Math.pow(1 - olUu, 3)
5896
- olUA = olFx.k === 'pop' ? Math.min(1, olUu * 2) : olUE
5674
+ rUu = Math.max(0, Math.min(1, rT2 / rtFx.dur))
5675
+ var rUE = 1 - Math.pow(1 - rUu, 3)
5676
+ rUA = rtFx.k === 'pop' ? Math.min(1, rUu * 2) : rUE
5897
5677
  }
5898
- var olUnit = olUs[ou]
5899
- olPref = olNext
5900
- olPW = olNW
5901
- if (olUA <= 0.004) continue
5678
+ if (rUA <= 0.004) continue
5902
5679
  ovC.save()
5903
- ovC.globalAlpha = olA * olUA
5904
- if (olUu < 1) {
5905
- if (olFx.k === 'rise') {
5906
- ovC.translate(0, (1 - (1 - Math.pow(1 - olUu, 3))) * 24 * s)
5907
- } else if (olFx.k === 'pop') {
5908
- var olPS = 1 + 2.70158 * Math.pow(olUu - 1, 3) + 1.70158 * Math.pow(olUu - 1, 2)
5909
- ovC.translate(olUX + olUW / 2, olLY2)
5910
- ovC.scale(olPS, olPS)
5911
- ovC.translate(-(olUX + olUW / 2), -olLY2)
5912
- } else if (olFx.k === 'blur') {
5913
- ovC.filter = 'blur(' + ((1 - olUu) * olPx * 0.12).toFixed(2) + 'px)'
5680
+ ovC.globalAlpha = olA * rUA
5681
+ if (rUu < 1) {
5682
+ if (rtFx.k === 'rise') {
5683
+ ovC.translate(0, (1 - (1 - Math.pow(1 - rUu, 3))) * 24 * s)
5684
+ } else if (rtFx.k === 'pop') {
5685
+ var rPS = 1 + 2.70158 * Math.pow(rUu - 1, 3) + 1.70158 * Math.pow(rUu - 1, 2)
5686
+ var rCX = rUBX + rU.x + rU.w / 2
5687
+ ovC.translate(rCX, rUY)
5688
+ ovC.scale(rPS, rPS)
5689
+ ovC.translate(-rCX, -rUY)
5690
+ } else if (rtFx.k === 'blur') {
5691
+ ovC.filter = 'blur(' + ((1 - rUu) * olPx * 0.12).toFixed(2) + 'px)'
5914
5692
  }
5915
5693
  }
5916
- if (ol.stroke) {
5917
- ovC.strokeStyle = ol.stroke.c
5918
- ovC.lineWidth = ol.stroke.w * olMS * s
5919
- ovC.lineJoin = 'round'
5920
- ovC.strokeText(olUnit, olUX, olLY2)
5921
- }
5922
- ovC.fillText(olUnit, olUX, olLY2)
5694
+ for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY, true)
5923
5695
  ovC.restore()
5924
5696
  }
5925
5697
  }
@@ -8885,11 +8657,12 @@ function studioLayerData(input, duration, pins) {
8885
8657
  return {
8886
8658
  ...base,
8887
8659
  text: overlayPlainText(o),
8888
- // The lines a person reads. A styled layer (rt) is painted from
8660
+ // The lines a person reads: what a plain layer is laid out
8661
+ // from, one run a line. A styled layer (rt) is laid out from
8889
8662
  // its runs; these are then its words, for the signature.
8890
8663
  lines: overlayLines(overlayPlainText(o)),
8891
8664
  // The styled payload, only when the layer is set in more than
8892
- // one style: a plain clip's data is unchanged.
8665
+ // one style.
8893
8666
  ...rt ? { rt } : {},
8894
8667
  fs: st.size,
8895
8668
  weight: st.weight,
@@ -8922,9 +8695,10 @@ function studioLayerData(input, duration, pins) {
8922
8695
  r: round8(bx.radius)
8923
8696
  }
8924
8697
  } : {},
8925
- // Entrance animation: segmentation + timing normalized
8926
- // HERE (deterministic doc-derived data) — ON_FRAME interprets
8927
- // per-unit progress as pure f(t). Absent = data byte parity.
8698
+ // Entrance animation: timing normalized HERE (deterministic
8699
+ // doc-derived data) — ON_FRAME cuts the units from the layout
8700
+ // and interprets per-unit progress as pure f(t). Absent = data
8701
+ // byte parity.
8928
8702
  ...(() => {
8929
8703
  const olFx = resolveOverlayFx(
8930
8704
  o,
@@ -12835,6 +12609,7 @@ export {
12835
12609
  styleAt,
12836
12610
  styleOf,
12837
12611
  styledTextOf,
12612
+ textRunsOf,
12838
12613
  tiltLane,
12839
12614
  tiltTrackFromDoc,
12840
12615
  toggleStyle,