@vosjs/studio-core 0.38.0 → 0.40.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
@@ -2213,7 +2213,7 @@ function htmlLayerPayload(clip) {
2213
2213
  };
2214
2214
  }
2215
2215
 
2216
- // src/richText/layout.ts
2216
+ // ../elements/dist/chunk-GYJBPCR2.js
2217
2217
  var sameStyle = (a, b) => (a.f || 0) === (b.f || 0) && a.c === b.c && a.u === b.u && a.s === b.s && a.h === b.h;
2218
2218
  function graphemesOf(text) {
2219
2219
  if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
@@ -2375,8 +2375,6 @@ function decorationRect(kind, x, w, y, px) {
2375
2375
  );
2376
2376
  return { x: x0, y: top, w: x1 - x0, h: t };
2377
2377
  }
2378
-
2379
- // src/richText/runs.ts
2380
2378
  var RUN_STYLE_KEYS = [
2381
2379
  "weight",
2382
2380
  "italic",
@@ -2421,6 +2419,24 @@ function normalizeRuns(text) {
2421
2419
  if (out.length === 1 && !isStyled(out[0])) return out[0].text;
2422
2420
  return out;
2423
2421
  }
2422
+ function layoutLines(text, fontOf) {
2423
+ const lines = [[]];
2424
+ for (const run of runsOf(text)) {
2425
+ const f = fontOf(run);
2426
+ const set = {
2427
+ ...f ? { f } : {},
2428
+ ...run.color ? { c: run.color } : {},
2429
+ ...run.underline ? { u: 1 } : {},
2430
+ ...run.strike ? { s: 1 } : {},
2431
+ ...run.highlight ? { h: run.highlight } : {}
2432
+ };
2433
+ run.text.split("\n").forEach((part, i) => {
2434
+ if (i > 0) lines.push([]);
2435
+ if (part) lines[lines.length - 1].push({ t: part, ...set });
2436
+ });
2437
+ }
2438
+ return lines;
2439
+ }
2424
2440
  function snapOffset(plain, offset, dir) {
2425
2441
  const o = Math.max(0, Math.min(plain.length, offset));
2426
2442
  let at = 0;
@@ -2530,6 +2546,68 @@ function diffInput(prev, next, caretAfter) {
2530
2546
  };
2531
2547
  }
2532
2548
 
2549
+ // ../elements/dist/text/index.js
2550
+ function caretStops(line, measure) {
2551
+ const stops = [{ o: line.a, x: 0 }];
2552
+ for (const fr of line.frags) {
2553
+ let at = 0;
2554
+ for (const g of graphemesOf(fr.t)) {
2555
+ at += g.length;
2556
+ stops.push({
2557
+ o: line.a + fr.o + at,
2558
+ x: fr.x + measure(fr.t.slice(0, at), fr.f || 0)
2559
+ });
2560
+ }
2561
+ }
2562
+ return stops;
2563
+ }
2564
+ function lineOfOffset(layout, offset) {
2565
+ const lines = layout.lines;
2566
+ for (let i = lines.length - 1; i >= 0; i--) {
2567
+ if (offset >= lines[i].a) return i;
2568
+ }
2569
+ return 0;
2570
+ }
2571
+ function caretAt(layout, offset, measure) {
2572
+ const li = lineOfOffset(layout, offset);
2573
+ const stops = caretStops(layout.lines[li], measure);
2574
+ let x = stops[stops.length - 1].x;
2575
+ for (const s of stops) {
2576
+ if (s.o >= offset) {
2577
+ x = s.x;
2578
+ break;
2579
+ }
2580
+ }
2581
+ return { line: li, x };
2582
+ }
2583
+ function offsetAtX(line, x, measure) {
2584
+ const stops = caretStops(line, measure);
2585
+ let best = stops[0];
2586
+ for (const s of stops) {
2587
+ if (Math.abs(s.x - x) < Math.abs(best.x - x)) best = s;
2588
+ }
2589
+ return best.o;
2590
+ }
2591
+ function rangeRects(layout, start, end, measure, breakTail = 0) {
2592
+ const out = [];
2593
+ if (end <= start) return out;
2594
+ layout.lines.forEach((line, i) => {
2595
+ const lineEnd = line.a + line.n;
2596
+ if (end <= line.a || start > lineEnd) return;
2597
+ const stops = caretStops(line, measure);
2598
+ const xOf = (o) => {
2599
+ for (const s of stops) if (s.o >= o) return s.x;
2600
+ return stops[stops.length - 1].x;
2601
+ };
2602
+ const x0 = xOf(Math.max(start, line.a));
2603
+ const x1 = xOf(Math.min(end, lineEnd));
2604
+ const next = layout.lines[i + 1];
2605
+ const tail = next && next.a > lineEnd && end > lineEnd ? breakTail : 0;
2606
+ if (x1 + tail > x0) out.push({ line: i, x: x0, w: x1 - x0 + tail });
2607
+ });
2608
+ return out;
2609
+ }
2610
+
2533
2611
  // src/overlayText.ts
2534
2612
  var OVERLAY_BOX_PAD_X = 0.6;
2535
2613
  var OVERLAY_BOX_PAD_Y = 0.35;
@@ -2699,24 +2777,13 @@ function styledTextOf(clip) {
2699
2777
  }
2700
2778
  return at;
2701
2779
  };
2702
- const l = [[]];
2703
- for (const run of runs) {
2704
- const f = fontOf(
2780
+ const l = layoutLines(
2781
+ runs,
2782
+ (run) => fontOf(
2705
2783
  run.weight !== void 0 ? snapRunWeight(clip, run.weight) : style.weight,
2706
2784
  run.italic ?? italic
2707
- );
2708
- const set = {
2709
- ...f ? { f } : {},
2710
- ...run.color ? { c: run.color } : {},
2711
- ...run.underline ? { u: 1 } : {},
2712
- ...run.strike ? { s: 1 } : {},
2713
- ...run.highlight ? { h: run.highlight } : {}
2714
- };
2715
- run.text.split("\n").forEach((part, i) => {
2716
- if (i > 0) l.push([]);
2717
- if (part) l[l.length - 1].push({ t: part, ...set });
2718
- });
2719
- }
2785
+ )
2786
+ );
2720
2787
  const entry = familyEntryOf(clip);
2721
2788
  const faces = [];
2722
2789
  for (const font of fs.slice(1)) {
@@ -2740,63 +2807,30 @@ function styledTextOf(clip) {
2740
2807
  h: hashOf(JSON.stringify([l, fs]))
2741
2808
  };
2742
2809
  }
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;
2810
+ function textRunsOf(clip) {
2811
+ const styled = styledTextOf(clip);
2812
+ if (styled) return styled;
2813
+ const style = resolveOverlayStyle(clip);
2814
+ return {
2815
+ l: overlayLines(overlayPlainText(clip)).map((t) => t ? [{ t }] : []),
2816
+ fs: [
2817
+ {
2818
+ w: style.weight,
2819
+ ...style.fontStyle === "italic" ? { i: 1 } : {}
2765
2820
  }
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);
2821
+ ]
2822
+ };
2787
2823
  }
2788
2824
  var FX_DIR = { forward: 0, reverse: 1, center: 2 };
2789
2825
  function resolveOverlayFx(clip, clipDuration) {
2790
2826
  const spec = overlayFxSpec(clip) ?? clip.fx ?? null;
2791
2827
  if (!spec) return null;
2792
2828
  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;
2829
+ const lines = overlayLines(overlayPlainText(clip));
2830
+ const count = (per) => lines.reduce((sum, line) => sum + per(line), 0);
2831
+ const words = () => count((line) => tokenRanges(line).length);
2832
+ const n = unit === "block" ? 1 : unit === "line" ? lines.length : unit === "word" ? words() : count((line) => graphemesOf(line).length);
2833
+ const upperN = unit === "line" && clip.maxWidth ? words() : n;
2800
2834
  const typewriter = spec.fx === "typewriter";
2801
2835
  const dur = typewriter ? 1e-3 : Math.min(2, Math.max(0.05, spec.duration ?? OVERLAY_TRANSITION_DUR));
2802
2836
  const defaultStagger = typewriter ? 0.05 : unit === "block" ? 0 : 0.06;
@@ -2812,9 +2846,7 @@ function resolveOverlayFx(clip, clipDuration) {
2812
2846
  d: FX_DIR[spec.direction ?? "forward"] ?? 0,
2813
2847
  st: round14(st),
2814
2848
  dur: round14(dur),
2815
- tt: round14(st * (upperN - 1) + dur),
2816
- units,
2817
- n
2849
+ tt: round14(st * (upperN - 1) + dur)
2818
2850
  };
2819
2851
  }
2820
2852
  function overlayFontString(style, scale, s) {
@@ -2838,35 +2870,21 @@ function overlayRect(clip, measure, frameW, frameH = 1080, mediaAspect) {
2838
2870
  return { ...base, w: w2, h: w2 / (mediaAspect || 16 / 9) };
2839
2871
  }
2840
2872
  const style = resolveOverlayStyle(clip);
2841
- const font = overlayFontString(style, scale, 1);
2842
2873
  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
- }
2874
+ const runs = textRunsOf(clip);
2875
+ const fonts = runs.fs.map(
2876
+ (f) => overlayFontString(
2877
+ { ...style, weight: f.w, fontStyle: f.i ? "italic" : "normal" },
2878
+ scale,
2879
+ 1
2880
+ )
2881
+ );
2882
+ const laid = layoutText(
2883
+ { lines: runs.l, maxWidth: clip.maxWidth ? clip.maxWidth * frameW : 0 },
2884
+ (t, f) => measure(t, fonts[f], ls)
2885
+ );
2886
+ const w = laid.width;
2887
+ const lineCount = laid.lines.length;
2870
2888
  const lineH = style.size * scale * style.lineHeight;
2871
2889
  const box = resolveOverlayBox(clip);
2872
2890
  const padX = box ? box.padX * scale : 0;
@@ -5117,7 +5135,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5117
5135
  var s = H / 1080 // scale design-px controls to comp px
5118
5136
  var ns = window.__vos__ || {}
5119
5137
  var playing = ns.isPaused === false
5120
- // The styled-text layout (richText/layout.ts, generated), installed once:
5138
+ // The styled-text layout (the engine's text/layout.ts, generated), installed once:
5121
5139
  // the page lays a text layer out with the code the host picks with.
5122
5140
  if (!ns.textLayout) { ${textLayoutCode}; ns.textLayout = __vosTextLayout }
5123
5141
  var TXL = ns.textLayout
@@ -5592,334 +5610,155 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5592
5610
  ovC.textAlign = 'center'
5593
5611
  ovC.textBaseline = 'middle'
5594
5612
  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.
5613
+ // Every text layer is laid out by the layout module, ONCE: a styled one
5614
+ // from its runs (ol.rt: lines of runs, each set its own way), a plain
5615
+ // one from its lines, each a single run in the layer's own style. The
5616
+ // layout is kept per clip under a key of everything it was measured
5617
+ // with, so a frame that only moves, fades or reveals the layer measures
5618
+ // nothing. Fragments draw left to right from each line's left edge; a
5619
+ // per-unit entrance animates the layout's units, each a few parts of
5620
+ // fragments.
5621
+ var rtFonts = [ovC.font]
5601
5622
  if (ol.rt) {
5602
- var rtFonts = [ovC.font]
5603
5623
  for (var rfi = 1; rfi < ol.rt.fs.length; rfi++) {
5604
5624
  rtFonts.push((ol.rt.fs[rfi].i ? 'italic ' : '') + ol.rt.fs[rfi].w + ' ' + olPx + 'px ' + ol.stack)
5605
5625
  }
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
5626
  }
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
5627
+ var rtFx = ol.fx && ol.fx.u !== 'block' ? ol.fx : null
5628
+ var rtMax = ol.mw ? ol.mw * W : 0
5629
+ 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)
5630
+ var rtAll = ns.textLayouts || (ns.textLayouts = {})
5631
+ var rtHit = rtAll[ol.id]
5632
+ if (!rtHit || rtHit.k !== rtKey) {
5633
+ var rtIn = ol.rt ? ol.rt.l : []
5634
+ if (!ol.rt) {
5635
+ var rtPl = ol.lines || ['']
5636
+ for (var rpi = 0; rpi < rtPl.length; rpi++) rtIn.push(rtPl[rpi] ? [{ t: rtPl[rpi] }] : [])
5637
+ }
5638
+ rtHit = rtAll[ol.id] = {
5639
+ k: rtKey,
5640
+ L: TXL.layoutText(
5641
+ { lines: rtIn, maxWidth: rtMax, unit: rtFx ? rtFx.u : undefined },
5642
+ function (mt, mf) { ovC.font = rtFonts[mf]; return ovC.measureText(mt).width }
5643
+ )
5818
5644
  }
5645
+ ovC.font = rtFonts[0]
5646
+ }
5647
+ var rtL = rtHit.L
5648
+ var rtN = rtL.lines.length
5649
+ var rtLH = olPx * (ol.lh || ${OVERLAY_LINE_HEIGHT})
5650
+ var rtY0 = -((rtN - 1) * rtLH) / 2
5651
+ // A line's left edge: lines sit centred on the anchor and align
5652
+ // against the widest.
5653
+ var rtLeft = function (lw) {
5654
+ var xo = ol.align === 'left' ? (lw - rtL.width) / 2 : ol.align === 'right' ? (rtL.width - lw) / 2 : 0
5655
+ return xo - lw / 2
5819
5656
  }
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
5657
  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)
5658
+ var rbPX = ol.box.px * olMS * s
5659
+ var rbPY = ol.box.py * olMS * s
5660
+ var rbW = rtL.width + rbPX * 2
5661
+ var rbH = rtN * rtLH + rbPY * 2
5829
5662
  ovC.save()
5830
5663
  ovC.globalAlpha = olA * ol.box.o
5831
5664
  ovC.fillStyle = ol.box.c
5832
- rr(-obFullW / 2, -obFullH / 2, obFullW, obFullH, obR, ovC)
5665
+ rr(-rbW / 2, -rbH / 2, rbW, rbH, Math.min(ol.box.r * olMS * s, rbH / 2), ovC)
5833
5666
  ovC.fill()
5834
5667
  ovC.restore()
5835
5668
  }
5669
+ // Highlights sit behind the words and take no legibility shadow. Under
5670
+ // a per-unit entrance each is drawn with its unit instead (below), so
5671
+ // it arrives with its word.
5672
+ for (var rhi = 0; !rtFx && rhi < rtN; rhi++) {
5673
+ var rhLine = rtL.lines[rhi]
5674
+ for (var rhj = 0; rhj < rhLine.frags.length; rhj++) {
5675
+ var rhF = rhLine.frags[rhj]
5676
+ if (!rhF.h) continue
5677
+ var rhR = TXL.decorationRect('h', rtLeft(rhLine.w) + rhF.x, rhF.w, rtY0 + rhi * rtLH, olPx)
5678
+ ovC.fillStyle = rhF.h
5679
+ ovC.fillRect(rhR.x, rhR.y, rhR.w, rhR.h)
5680
+ }
5681
+ }
5836
5682
  if (ol.shadow > 0) {
5837
5683
  ovC.shadowColor = 'rgba(0,0,0,' + ol.shadow + ')'
5838
5684
  ovC.shadowBlur = olPx * 0.25
5839
5685
  ovC.shadowOffsetY = olPx * 0.04
5840
5686
  }
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)
5687
+ ovC.textAlign = 'left'
5688
+ // One fragment (or the part of one a unit holds): the underline under
5689
+ // the glyphs, stroke under fill, the strikethrough over them.
5690
+ var rtPart = function (p, bx, y, withH) {
5691
+ if (withH && p.h) {
5692
+ var rhU = TXL.decorationRect('h', bx + p.x, p.w, y, olPx)
5693
+ var rhS = ovC.shadowColor
5694
+ ovC.shadowColor = 'rgba(0,0,0,0)'
5695
+ ovC.fillStyle = p.h
5696
+ ovC.fillRect(rhU.x, rhU.y, rhU.w, rhU.h)
5697
+ ovC.shadowColor = rhS
5698
+ }
5699
+ ovC.font = rtFonts[p.f || 0]
5700
+ ovC.fillStyle = p.c || ol.color
5701
+ if (p.u) {
5702
+ var ru = TXL.decorationRect('u', bx + p.x, p.w, y, olPx)
5703
+ ovC.fillRect(ru.x, ru.y, ru.w, ru.h)
5704
+ }
5705
+ if (ol.stroke) {
5706
+ ovC.strokeStyle = ol.stroke.c
5707
+ ovC.lineWidth = ol.stroke.w * olMS * s
5708
+ ovC.lineJoin = 'round'
5709
+ ovC.strokeText(p.t, bx + p.x, y)
5710
+ }
5711
+ ovC.fillText(p.t, bx + p.x, y)
5712
+ if (p.s) {
5713
+ var rs = TXL.decorationRect('s', bx + p.x, p.w, y, olPx)
5714
+ ovC.fillRect(rs.x, rs.y, rs.w, rs.h)
5715
+ }
5716
+ }
5717
+ if (!rtFx) {
5718
+ for (var rli = 0; rli < rtN; rli++) {
5719
+ var rLine = rtL.lines[rli]
5720
+ var rBX = rtLeft(rLine.w)
5721
+ for (var rfk = 0; rfk < rLine.frags.length; rfk++) rtPart(rLine.frags[rfk], rBX, rtY0 + rli * rtLH)
5858
5722
  }
5859
5723
  } 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
5724
+ // Per-unit entrance, the plain layer's own arithmetic over the
5725
+ // layout's units: delay = order(index) x st, eased over dur.
5726
+ var rtCount = 0
5727
+ for (var rci = 0; rci < rtN; rci++) rtCount += rtL.lines[rci].units.length
5728
+ var rtIdx = 0
5729
+ for (var rui = 0; rui < rtN; rui++) {
5730
+ var rULine = rtL.lines[rui]
5731
+ var rUBX = rtLeft(rULine.w)
5732
+ var rUY = rtY0 + rui * rtLH
5733
+ for (var ruj = 0; ruj < rULine.units.length; ruj++, rtIdx++) {
5734
+ var rU = rULine.units[ruj]
5735
+ var rOrd = rtFx.d === 1 ? (rtCount - 1 - rtIdx) : rtFx.d === 2 ? Math.abs(rtIdx - (rtCount - 1) / 2) : rtIdx
5736
+ var rT2 = olT - rOrd * rtFx.st
5737
+ var rUA = 1, rUu = 1
5738
+ if (rtFx.k === 'typewriter') {
5739
+ rUA = rT2 >= 0 ? 1 : 0
5893
5740
  } 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
5741
+ rUu = Math.max(0, Math.min(1, rT2 / rtFx.dur))
5742
+ var rUE = 1 - Math.pow(1 - rUu, 3)
5743
+ rUA = rtFx.k === 'pop' ? Math.min(1, rUu * 2) : rUE
5897
5744
  }
5898
- var olUnit = olUs[ou]
5899
- olPref = olNext
5900
- olPW = olNW
5901
- if (olUA <= 0.004) continue
5745
+ if (rUA <= 0.004) continue
5902
5746
  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)'
5747
+ ovC.globalAlpha = olA * rUA
5748
+ if (rUu < 1) {
5749
+ if (rtFx.k === 'rise') {
5750
+ ovC.translate(0, (1 - (1 - Math.pow(1 - rUu, 3))) * 24 * s)
5751
+ } else if (rtFx.k === 'pop') {
5752
+ var rPS = 1 + 2.70158 * Math.pow(rUu - 1, 3) + 1.70158 * Math.pow(rUu - 1, 2)
5753
+ var rCX = rUBX + rU.x + rU.w / 2
5754
+ ovC.translate(rCX, rUY)
5755
+ ovC.scale(rPS, rPS)
5756
+ ovC.translate(-rCX, -rUY)
5757
+ } else if (rtFx.k === 'blur') {
5758
+ ovC.filter = 'blur(' + ((1 - rUu) * olPx * 0.12).toFixed(2) + 'px)'
5914
5759
  }
5915
5760
  }
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)
5761
+ for (var rpk = 0; rpk < rU.parts.length; rpk++) rtPart(rU.parts[rpk], rUBX, rUY, true)
5923
5762
  ovC.restore()
5924
5763
  }
5925
5764
  }
@@ -8885,11 +8724,12 @@ function studioLayerData(input, duration, pins) {
8885
8724
  return {
8886
8725
  ...base,
8887
8726
  text: overlayPlainText(o),
8888
- // The lines a person reads. A styled layer (rt) is painted from
8727
+ // The lines a person reads: what a plain layer is laid out
8728
+ // from, one run a line. A styled layer (rt) is laid out from
8889
8729
  // its runs; these are then its words, for the signature.
8890
8730
  lines: overlayLines(overlayPlainText(o)),
8891
8731
  // The styled payload, only when the layer is set in more than
8892
- // one style: a plain clip's data is unchanged.
8732
+ // one style.
8893
8733
  ...rt ? { rt } : {},
8894
8734
  fs: st.size,
8895
8735
  weight: st.weight,
@@ -8922,9 +8762,10 @@ function studioLayerData(input, duration, pins) {
8922
8762
  r: round8(bx.radius)
8923
8763
  }
8924
8764
  } : {},
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.
8765
+ // Entrance animation: timing normalized HERE (deterministic
8766
+ // doc-derived data) — ON_FRAME cuts the units from the layout
8767
+ // and interprets per-unit progress as pure f(t). Absent = data
8768
+ // byte parity.
8928
8769
  ...(() => {
8929
8770
  const olFx = resolveOverlayFx(
8930
8771
  o,
@@ -11023,68 +10864,6 @@ function applyLook(frame, look, size, video, placement) {
11023
10864
  return out;
11024
10865
  }
11025
10866
 
11026
- // src/richText/caret.ts
11027
- function caretStops(line, measure) {
11028
- const stops = [{ o: line.a, x: 0 }];
11029
- for (const fr of line.frags) {
11030
- let at = 0;
11031
- for (const g of graphemesOf(fr.t)) {
11032
- at += g.length;
11033
- stops.push({
11034
- o: line.a + fr.o + at,
11035
- x: fr.x + measure(fr.t.slice(0, at), fr.f || 0)
11036
- });
11037
- }
11038
- }
11039
- return stops;
11040
- }
11041
- function lineOfOffset(layout, offset) {
11042
- const lines = layout.lines;
11043
- for (let i = lines.length - 1; i >= 0; i--) {
11044
- if (offset >= lines[i].a) return i;
11045
- }
11046
- return 0;
11047
- }
11048
- function caretAt(layout, offset, measure) {
11049
- const li = lineOfOffset(layout, offset);
11050
- const stops = caretStops(layout.lines[li], measure);
11051
- let x = stops[stops.length - 1].x;
11052
- for (const s of stops) {
11053
- if (s.o >= offset) {
11054
- x = s.x;
11055
- break;
11056
- }
11057
- }
11058
- return { line: li, x };
11059
- }
11060
- function offsetAtX(line, x, measure) {
11061
- const stops = caretStops(line, measure);
11062
- let best = stops[0];
11063
- for (const s of stops) {
11064
- if (Math.abs(s.x - x) < Math.abs(best.x - x)) best = s;
11065
- }
11066
- return best.o;
11067
- }
11068
- function rangeRects(layout, start, end, measure, breakTail = 0) {
11069
- const out = [];
11070
- if (end <= start) return out;
11071
- layout.lines.forEach((line, i) => {
11072
- const lineEnd = line.a + line.n;
11073
- if (end <= line.a || start > lineEnd) return;
11074
- const stops = caretStops(line, measure);
11075
- const xOf = (o) => {
11076
- for (const s of stops) if (s.o >= o) return s.x;
11077
- return stops[stops.length - 1].x;
11078
- };
11079
- const x0 = xOf(Math.max(start, line.a));
11080
- const x1 = xOf(Math.min(end, lineEnd));
11081
- const next = layout.lines[i + 1];
11082
- const tail = next && next.a > lineEnd && end > lineEnd ? breakTail : 0;
11083
- if (x1 + tail > x0) out.push({ line: i, x: x0, w: x1 - x0 + tail });
11084
- });
11085
- return out;
11086
- }
11087
-
11088
10867
  // src/timeline/lanes.ts
11089
10868
  import {
11090
10869
  mapTime as mapTime3,
@@ -12835,6 +12614,7 @@ export {
12835
12614
  styleAt,
12836
12615
  styleOf,
12837
12616
  styledTextOf,
12617
+ textRunsOf,
12838
12618
  tiltLane,
12839
12619
  tiltTrackFromDoc,
12840
12620
  toggleStyle,