tegaki 0.16.0 → 0.17.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/FONTS-LICENSE.md +6 -0
  3. package/README.md +1 -0
  4. package/dist/core/index.d.mts +2 -2
  5. package/dist/core/index.mjs +1 -1
  6. package/dist/{core-BRYlZ8i2.mjs → core-BrU5Htjs.mjs} +96 -14
  7. package/dist/core-BrU5Htjs.mjs.map +1 -0
  8. package/dist/fonts/amiri/amiri-7df37680.ttf +0 -0
  9. package/dist/fonts/amiri/amiri.ttf +0 -0
  10. package/dist/fonts/amiri/bundle.d.mts +10631 -0
  11. package/dist/fonts/amiri/bundle.mjs +130866 -0
  12. package/dist/fonts/amiri/bundle.mjs.map +1 -0
  13. package/dist/fonts/caveat/bundle.d.mts +1992 -10
  14. package/dist/fonts/caveat/bundle.mjs +31054 -105
  15. package/dist/fonts/caveat/bundle.mjs.map +1 -1
  16. package/dist/fonts/italianno/bundle.d.mts +1548 -10
  17. package/dist/fonts/italianno/bundle.mjs +32832 -198
  18. package/dist/fonts/italianno/bundle.mjs.map +1 -1
  19. package/dist/fonts/klee-one/bundle.d.mts +11442 -0
  20. package/dist/fonts/klee-one/bundle.mjs +234910 -0
  21. package/dist/fonts/klee-one/bundle.mjs.map +1 -0
  22. package/dist/fonts/klee-one/klee-one-d192e144.ttf +0 -0
  23. package/dist/fonts/klee-one/klee-one.ttf +0 -0
  24. package/dist/fonts/parisienne/bundle.d.mts +939 -10
  25. package/dist/fonts/parisienne/bundle.mjs +19540 -192
  26. package/dist/fonts/parisienne/bundle.mjs.map +1 -1
  27. package/dist/fonts/suez-one/bundle.d.mts +2181 -0
  28. package/dist/fonts/suez-one/bundle.mjs +30466 -0
  29. package/dist/fonts/suez-one/bundle.mjs.map +1 -0
  30. package/dist/fonts/suez-one/suez-one-ac7f1d1c.ttf +0 -0
  31. package/dist/fonts/suez-one/suez-one.ttf +0 -0
  32. package/dist/fonts/tangerine/bundle.d.mts +40 -10
  33. package/dist/fonts/tangerine/bundle.mjs +52 -119
  34. package/dist/fonts/tangerine/bundle.mjs.map +1 -1
  35. package/dist/fonts/tillana/bundle.d.mts +8557 -0
  36. package/dist/fonts/tillana/bundle.mjs +224308 -0
  37. package/dist/fonts/tillana/bundle.mjs.map +1 -0
  38. package/dist/fonts/tillana/tillana-12e48378.ttf +0 -0
  39. package/dist/fonts/tillana/tillana.ttf +0 -0
  40. package/dist/{index-Duog5eW6.d.mts → index-JT-cQ_gO.d.mts} +3 -3
  41. package/dist/{index-Qr39WZaW.d.mts → index-vDGB0qjx.d.mts} +121 -63
  42. package/dist/index.d.mts +3 -3
  43. package/dist/index.mjs +2 -2
  44. package/dist/react/index.d.mts +3 -3
  45. package/dist/react/index.mjs +2 -2
  46. package/dist/{react-Eq0zlDsb.mjs → react-DBKcqp07.mjs} +5 -4
  47. package/dist/react-DBKcqp07.mjs.map +1 -0
  48. package/dist/shaper-harfbuzz/index.d.mts +1 -1
  49. package/dist/shaper-harfbuzz/index.mjs +35 -1
  50. package/dist/shaper-harfbuzz/index.mjs.map +1 -1
  51. package/dist/{shaper-registry-HD6_qkK4.d.mts → shaper-registry-DVS5R37Q.d.mts} +10 -5
  52. package/dist/solid/index.d.mts +2 -2
  53. package/dist/solid/index.mjs +4 -2
  54. package/dist/solid/index.mjs.map +1 -1
  55. package/dist/wc/index.d.mts +6 -2
  56. package/dist/wc/index.mjs +12 -2
  57. package/dist/wc/index.mjs.map +1 -1
  58. package/fonts/amiri/glyphDataById.json +1 -1
  59. package/fonts/caveat/bundle.ts +3 -0
  60. package/fonts/caveat/glyphData.json +1 -1
  61. package/fonts/caveat/glyphDataById.json +1 -0
  62. package/fonts/italianno/bundle.ts +3 -0
  63. package/fonts/italianno/glyphData.json +1 -1
  64. package/fonts/italianno/glyphDataById.json +1 -0
  65. package/fonts/klee-one/glyphDataById.json +1 -1
  66. package/fonts/parisienne/bundle.ts +3 -0
  67. package/fonts/parisienne/glyphData.json +1 -1
  68. package/fonts/parisienne/glyphDataById.json +1 -0
  69. package/fonts/suez-one/glyphDataById.json +1 -1
  70. package/fonts/tangerine/glyphData.json +1 -1
  71. package/fonts/tillana/bundle.ts +23 -0
  72. package/fonts/tillana/glyphData.json +1 -0
  73. package/fonts/tillana/glyphDataById.json +1 -0
  74. package/fonts/tillana/tillana-12e48378.ttf +0 -0
  75. package/fonts/tillana/tillana.ttf +0 -0
  76. package/package.json +8 -1
  77. package/src/astro/TegakiRenderer.astro +2 -2
  78. package/src/core/engine.ts +60 -6
  79. package/src/core/types.ts +10 -1
  80. package/src/lib/textLayout.ts +65 -10
  81. package/src/lib/timeline.test.ts +203 -0
  82. package/src/lib/timeline.ts +25 -3
  83. package/src/lib/utils.test.ts +73 -0
  84. package/src/lib/utils.ts +20 -1
  85. package/src/react/TegakiRenderer.tsx +2 -0
  86. package/src/shaper-harfbuzz/index.ts +60 -2
  87. package/src/solid/TegakiRenderer.tsx +2 -0
  88. package/src/svelte/TegakiRenderer.svelte +3 -2
  89. package/src/types.ts +9 -4
  90. package/src/vue/TegakiRenderer.vue +3 -1
  91. package/src/wc/TegakiElement.ts +12 -0
  92. package/dist/core-BRYlZ8i2.mjs.map +0 -1
  93. package/dist/react-Eq0zlDsb.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # tegaki
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2b4b435: Support Devanagari writing system and add Tillana as built-in font. Also fixed a bug with generating n-grams, which affected Arabic fonts.
8
+
9
+ ### Patch Changes
10
+
11
+ - ee2db76: Fix GPOS and advance width features for some Arabic fonts like "Aref Ruqaa"
12
+
3
13
  ## 0.16.0
4
14
 
5
15
  ### Minor Changes
package/FONTS-LICENSE.md CHANGED
@@ -45,6 +45,12 @@ All fonts are licensed under the [SIL Open Font License, Version 1.1](https://op
45
45
  - **Copyright**: Copyright 2010-2022 The Amiri Project Authors (https://github.com/aliftype/amiri)
46
46
  - **License**: SIL Open Font License, Version 1.1
47
47
 
48
+ ## Tillana
49
+
50
+ - **Designer**: Indian Type Foundry, Shiva Nallaperumal
51
+ - **Copyright**: Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com)
52
+ - **License**: SIL Open Font License, Version 1.1
53
+
48
54
  ---
49
55
 
50
56
  ## SIL Open Font License, Version 1.1
package/README.md CHANGED
@@ -73,6 +73,7 @@ Several handwriting fonts are bundled and ready to use:
73
73
  - **Parisienne** — `tegaki/fonts/parisienne` _(Latin)_
74
74
  - **Suez One** — `tegaki/fonts/suez-one` _(Hebrew + Latin)_
75
75
  - **Amiri** — `tegaki/fonts/amiri` _(Arabic + Latin)_
76
+ - **Tillana** — `tegaki/fonts/tillana` _(Devanagari + Latin)_
76
77
  - **Klee One** — `tegaki/fonts/klee-one` _(Japanese: kana + Kyōiku grade 1–2 kanji + Latin)_
77
78
 
78
79
  For other fonts, use the [interactive generator](https://gkurt.com/tegaki/generator/) to create a custom bundle.
@@ -1,3 +1,3 @@
1
- import { S as TimedPoint, _ as TegakiEffectName, a as BBox, b as TegakiMultiEffectName, c as CSSLength, d as LineCap, f as PathCommand, g as TegakiEffectConfigs, h as TegakiBundle, i as ShapedGlyph, l as FontOutput, m as Stroke, o as BUNDLE_VERSION, p as Point, r as BundleShaper, s as COMPATIBLE_BUNDLE_VERSIONS, t as ShaperFactory, u as GlyphData, v as TegakiEffects, x as TegakiSingletonEffectName, y as TegakiGlyphData } from "../shaper-registry-HD6_qkK4.mjs";
2
- import { A as computeLayoutBbox, C as findEffect, D as resolveEffects, E as hasRenderHooks, O as LayoutBBox, S as ResolvedEffect, T as getEffectDefinition, _ as computeTimeline, a as CreateElementFn, b as EffectDefinition, c as TimeControlMode, d as getBundle, f as registerBundle, g as TimelineEntry, h as TimelineConfig, i as TegakiEngine, j as computeTextLayout, k as TextLayout, l as TimeControlProp, m as Timeline, n as buildRootProps, o as TegakiEngineOptions, p as resolveBundle, r as domCreateElement, s as TegakiQuality, t as buildChildren, u as createBundle, v as ensureFontFace, w as findEffects, x as RenderStageContext, y as drawGlyph } from "../index-Qr39WZaW.mjs";
1
+ import { S as TimedPoint, _ as TegakiEffectName, a as BBox, b as TegakiMultiEffectName, c as CSSLength, d as LineCap, f as PathCommand, g as TegakiEffectConfigs, h as TegakiBundle, i as ShapedGlyph, l as FontOutput, m as Stroke, o as BUNDLE_VERSION, p as Point, r as BundleShaper, s as COMPATIBLE_BUNDLE_VERSIONS, t as ShaperFactory, u as GlyphData, v as TegakiEffects, x as TegakiSingletonEffectName, y as TegakiGlyphData } from "../shaper-registry-DVS5R37Q.mjs";
2
+ import { A as TimelineEntry, C as resolveEffects, D as computeTextLayout, E as computeLayoutBbox, O as Timeline, S as hasRenderHooks, T as TextLayout, _ as RenderStageContext, a as CreateElementFn, b as findEffects, c as TimeControlMode, d as getBundle, f as registerBundle, g as EffectDefinition, h as drawGlyph, i as TegakiEngine, j as computeTimeline, k as TimelineConfig, l as TimeControlProp, m as ensureFontFace, n as buildRootProps, o as TegakiEngineOptions, p as resolveBundle, r as domCreateElement, s as TegakiQuality, t as buildChildren, u as createBundle, v as ResolvedEffect, w as LayoutBBox, x as getEffectDefinition, y as findEffect } from "../index-vDGB0qjx.mjs";
3
3
  export { BBox, BUNDLE_VERSION, BundleShaper, COMPATIBLE_BUNDLE_VERSIONS, CSSLength, CreateElementFn, EffectDefinition, FontOutput, GlyphData, LayoutBBox, LineCap, PathCommand, Point, RenderStageContext, ResolvedEffect, ShapedGlyph, ShaperFactory, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiQuality, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects };
@@ -1,2 +1,2 @@
1
- import { _ as findEffect, a as createBundle, b as hasRenderHooks, c as resolveBundle, d as computeTimeline, f as computeLayoutBbox, h as drawGlyph, i as domCreateElement, l as BUNDLE_VERSION, m as ensureFontFace, n as buildChildren, o as getBundle, p as computeTextLayout, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as COMPATIBLE_BUNDLE_VERSIONS, v as findEffects, x as resolveEffects, y as getEffectDefinition } from "../core-BRYlZ8i2.mjs";
1
+ import { _ as findEffect, a as createBundle, b as hasRenderHooks, c as resolveBundle, d as computeTimeline, f as computeLayoutBbox, h as drawGlyph, i as domCreateElement, l as BUNDLE_VERSION, m as ensureFontFace, n as buildChildren, o as getBundle, p as computeTextLayout, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as COMPATIBLE_BUNDLE_VERSIONS, v as findEffects, x as resolveEffects, y as getEffectDefinition } from "../core-BrU5Htjs.mjs";
2
2
  export { BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, TegakiEngine, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects };
@@ -264,6 +264,24 @@ function cssFontFamily(bundle) {
264
264
  if (bundle.fullFamily) return `'${bundle.family}', '${bundle.fullFamily}'`;
265
265
  return `'${bundle.family}'`;
266
266
  }
267
+ /**
268
+ * Look up `glyphData` for a grapheme cluster, with a fallback to its leading
269
+ * codepoint. Devanagari clusters like `"हि"` or `"न्दी"` have no entry in the
270
+ * single-codepoint-keyed `glyphData`; without the fallback every shaped glyph
271
+ * inside such a cluster that the variant map skipped (i.e. nominal forms like
272
+ * the bare `ह`) would resolve to `undefined`, get tagged `hasGlyph: false`,
273
+ * and collapse onto the 0.2s `unknownDuration` slot — drawn at end-of-slot
274
+ * via DOM `fillText`. The fallback resolves the cluster's first codepoint
275
+ * (e.g. `ह` from `"हि"`), so the nominal glyph picks up its real stroke data
276
+ * and animates instead of popping in.
277
+ */
278
+ function lookupGlyphData(font, char) {
279
+ const direct = font.glyphData[char];
280
+ if (direct || char.length <= 1) return direct;
281
+ const cp = char.codePointAt(0);
282
+ if (cp === void 0) return void 0;
283
+ return font.glyphData[String.fromCodePoint(cp)];
284
+ }
267
285
  function coerceToString(value) {
268
286
  if (value == null || typeof value === "boolean") return "";
269
287
  if (typeof value === "string") return value;
@@ -681,7 +699,12 @@ function measureElement(el, fontSize) {
681
699
  }
682
700
  /**
683
701
  * Replace `layout.charOffsets` and `charWidths` with values computed from the
684
- * shaper's advances, while preserving the DOM's line-break decisions.
702
+ * shaper's advances, while preserving the DOM's line-break decisions. When a
703
+ * `timeline` is provided, each entry's `xOffsetEm` and `yOffsetEm` are also
704
+ * filled in from the shaper's per-glyph pen-walk (mutated in place) and
705
+ * `layout.lineLefts` is populated — together they let the engine draw each
706
+ * glyph at its GPOS-positioned origin (cursive-attachment lift, mark
707
+ * attachment) instead of sharing one position per cluster.
685
708
  *
686
709
  * The DOM's Range API returns imprecise per-grapheme rects inside a complex-
687
710
  * shaped cluster (Arabic joining, Indic conjuncts, ligatures with kern/mark
@@ -693,7 +716,7 @@ function measureElement(el, fontSize) {
693
716
  * DOM using a full-line Range — per-grapheme rects inside shaped clusters are
694
717
  * not reliable enough to anchor against.
695
718
  */
696
- function applyShaperPositions(layout, el, text, fontSize, font, shaper) {
719
+ function applyShaperPositions(layout, el, text, fontSize, font, shaper, timeline) {
697
720
  const chars = graphemes(text);
698
721
  if (!chars.length) return layout;
699
722
  const textNode = el.firstChild;
@@ -715,9 +738,19 @@ function applyShaperPositions(layout, el, text, fontSize, font, shaper) {
715
738
  utf16ToGrapheme[text.length] = chars.length;
716
739
  const charOffsets = layout.charOffsets.slice();
717
740
  const charWidths = layout.charWidths.slice();
741
+ const lineLefts = new Array(layout.lines.length).fill(0);
718
742
  const emPerUnit = 1 / font.unitsPerEm;
719
- for (const lineIndices of layout.lines) {
720
- const realIndices = lineIndices.filter((idx) => chars[idx] !== "\n");
743
+ const entryQueue = /* @__PURE__ */ new Map();
744
+ if (timeline) for (let ei = 0; ei < timeline.entries.length; ei++) {
745
+ const e = timeline.entries[ei];
746
+ if (e.glyphId === void 0) continue;
747
+ const key = `${e.graphemeIndex}:${e.glyphId}`;
748
+ const list = entryQueue.get(key);
749
+ if (list) list.push(ei);
750
+ else entryQueue.set(key, [ei]);
751
+ }
752
+ for (let li = 0; li < layout.lines.length; li++) {
753
+ const realIndices = layout.lines[li].filter((idx) => chars[idx] !== "\n");
721
754
  if (realIndices.length === 0) continue;
722
755
  const lineStartU = graphemeStartU[realIndices[0]];
723
756
  const lastReal = realIndices[realIndices.length - 1];
@@ -729,6 +762,7 @@ function applyShaperPositions(layout, el, text, fontSize, font, shaper) {
729
762
  let lineLeftPx = Infinity;
730
763
  for (const r of lineRects) if (r.left < lineLeftPx) lineLeftPx = r.left;
731
764
  const lineLeftEm = (lineLeftPx - elLeft) / scale / fontSize;
765
+ lineLefts[li] = lineLeftEm;
732
766
  const lineText = text.slice(lineStartU, lineEndU);
733
767
  const lineRTL = RTL_CHAR_RE.test(lineText);
734
768
  const shaped = shaper.shape(lineText);
@@ -737,12 +771,29 @@ function applyShaperPositions(layout, el, text, fontSize, font, shaper) {
737
771
  const clusterLeft = /* @__PURE__ */ new Map();
738
772
  const clusterAdvance = /* @__PURE__ */ new Map();
739
773
  let penEm = 0;
774
+ let penYEm = 0;
740
775
  for (const g of visualGlyphs) {
741
776
  const axEm = g.ax * emPerUnit;
777
+ const ayEm = g.ay * emPerUnit;
742
778
  const dxEm = g.dx * emPerUnit;
743
- if (!clusterLeft.has(g.cl)) clusterLeft.set(g.cl, penEm + dxEm);
779
+ const dyEm = g.dy * emPerUnit;
780
+ const glyphXEm = penEm + dxEm;
781
+ const glyphYEm = penYEm - dyEm;
782
+ if (!clusterLeft.has(g.cl)) clusterLeft.set(g.cl, glyphXEm);
744
783
  clusterAdvance.set(g.cl, (clusterAdvance.get(g.cl) ?? 0) + axEm);
784
+ if (timeline) {
785
+ const gIdx = utf16ToGrapheme[lineStartU + g.cl];
786
+ if (gIdx !== void 0 && gIdx >= 0) {
787
+ const ei = entryQueue.get(`${gIdx}:${g.g}`)?.shift();
788
+ if (ei !== void 0) {
789
+ const entry = timeline.entries[ei];
790
+ entry.xOffsetEm = glyphXEm;
791
+ entry.yOffsetEm = glyphYEm;
792
+ }
793
+ }
794
+ }
745
795
  penEm += axEm;
796
+ penYEm -= ayEm;
746
797
  }
747
798
  const assigned = /* @__PURE__ */ new Set();
748
799
  for (const [cl, leftEm] of clusterLeft) {
@@ -767,7 +818,8 @@ function applyShaperPositions(layout, el, text, fontSize, font, shaper) {
767
818
  return {
768
819
  lines: layout.lines,
769
820
  charOffsets,
770
- charWidths
821
+ charWidths,
822
+ lineLefts
771
823
  };
772
824
  }
773
825
  /**
@@ -981,7 +1033,7 @@ function computeGraphemeTimeline(text, font, config) {
981
1033
  });
982
1034
  continue;
983
1035
  }
984
- const glyph = font.glyphData[char];
1036
+ const glyph = lookupGlyphData(font, char);
985
1037
  if (glyph) {
986
1038
  const part = partitionGlyph(glyph, unknownDuration, deferDots);
987
1039
  sched.add({
@@ -1047,7 +1099,7 @@ function computeShapedTimeline(text, font, config, shaper) {
1047
1099
  const clusterText = text.slice(clusterStart, clusterEnd);
1048
1100
  const firstChar = chars[graphemeIdx];
1049
1101
  const isWhitespace = /^\s+$/.test(clusterText);
1050
- const data = font.glyphDataById?.[glyph.g] ?? font.glyphData[firstChar];
1102
+ const data = font.glyphDataById?.[glyph.g] ?? lookupGlyphData(font, firstChar);
1051
1103
  const hasGlyph = !!data;
1052
1104
  if (isWhitespace) {
1053
1105
  sched.separate("word", {
@@ -1412,6 +1464,7 @@ var TegakiEngine = class {
1412
1464
  _quality;
1413
1465
  _showOverlay = false;
1414
1466
  _onComplete;
1467
+ _onChangeTimeline;
1415
1468
  _direction;
1416
1469
  _resolvedEffects = resolveEffects(void 0);
1417
1470
  _seed;
@@ -1498,6 +1551,32 @@ var TegakiEngine = class {
1498
1551
  get duration() {
1499
1552
  return this._timeline.totalDuration;
1500
1553
  }
1554
+ /**
1555
+ * The engine's current timeline — the same object that drives rendering.
1556
+ * Reflects the resolved shaper once the (async) shaper promise has
1557
+ * settled; use the `onChangeTimeline` option to be notified of recomputations.
1558
+ * Treat the returned object as read-only.
1559
+ */
1560
+ get timeline() {
1561
+ return this._timeline;
1562
+ }
1563
+ /**
1564
+ * Compute a timeline for arbitrary text against this engine's currently-
1565
+ * loaded font, timing config, and resolved shaper. Useful for measuring
1566
+ * the duration of hypothetical text without changing what's rendered
1567
+ * (e.g. layout planning, fade-in scheduling).
1568
+ *
1569
+ * Returns an empty timeline when no font is loaded. The result reflects
1570
+ * shaper state at call time — call after `onChangeTimeline` has fired
1571
+ * once to be sure the shaper has resolved.
1572
+ */
1573
+ computeTimeline(text) {
1574
+ if (!this._font) return {
1575
+ entries: [],
1576
+ totalDuration: 0
1577
+ };
1578
+ return computeTimeline(text, this._font, this._timing, this._shaper);
1579
+ }
1501
1580
  get isPlaying() {
1502
1581
  return this._playing;
1503
1582
  }
@@ -1547,7 +1626,7 @@ var TegakiEngine = class {
1547
1626
  let dirtyRender = false;
1548
1627
  let dirtyPlayback = false;
1549
1628
  if ("text" in options) {
1550
- const nextText = (options.text ?? "").replace(/\r\n?/g, "\n");
1629
+ const nextText = (options.text ?? "").replace(/\r\n?/g, "\n").normalize("NFC");
1551
1630
  if (nextText !== this._text) {
1552
1631
  this._text = nextText;
1553
1632
  dirtyTimeline = true;
@@ -1621,6 +1700,7 @@ var TegakiEngine = class {
1621
1700
  dirtyRender = true;
1622
1701
  }
1623
1702
  if ("onComplete" in options) this._onComplete = options.onComplete;
1703
+ if ("onChangeTimeline" in options) this._onChangeTimeline = options.onChangeTimeline;
1624
1704
  if (dirtyTimeline) this._recomputeTimeline();
1625
1705
  if (dirtyRender || dirtyTimeline || dirtyLayout) this._updateDom();
1626
1706
  if (dirtyLayout) this._recomputeLayout();
@@ -1802,6 +1882,7 @@ var TegakiEngine = class {
1802
1882
  entries: [],
1803
1883
  totalDuration: 0
1804
1884
  };
1885
+ this._onChangeTimeline?.(this._timeline);
1805
1886
  }
1806
1887
  _recomputeLayout() {
1807
1888
  if (this._fontReady && this._font?.family && this._fontSize && this._containerWidth && this._text) {
@@ -1810,7 +1891,7 @@ var TegakiEngine = class {
1810
1891
  if (key === this._layoutKey) return;
1811
1892
  this._layoutKey = key;
1812
1893
  let layout = computeTextLayout(this._overlayEl, this._fontSize);
1813
- if (this._shaper && this._font) layout = applyShaperPositions(layout, this._overlayEl, this._text, this._fontSize, this._font, this._shaper);
1894
+ if (this._shaper && this._font) layout = applyShaperPositions(layout, this._overlayEl, this._text, this._fontSize, this._font, this._shaper, this._timeline);
1814
1895
  this._layout = layout;
1815
1896
  } else {
1816
1897
  this._layoutKey = "";
@@ -1987,15 +2068,16 @@ var TegakiEngine = class {
1987
2068
  const lineIdx = graphemeToLine[charIdx] ?? -1;
1988
2069
  if (lineIdx < 0) continue;
1989
2070
  const y = lineIdx * lineHeight;
1990
- const x = (layout.charOffsets[charIdx] ?? 0) * fontSize;
1991
- const glyph = (entry.glyphId !== void 0 ? font.glyphDataById?.[entry.glyphId] : void 0) ?? font.glyphData[entry.char];
2071
+ const lineLeftEm = layout.lineLefts?.[lineIdx];
2072
+ const x = entry.xOffsetEm !== void 0 && lineLeftEm !== void 0 ? (lineLeftEm + entry.xOffsetEm) * fontSize : (layout.charOffsets[charIdx] ?? 0) * fontSize;
2073
+ const glyph = (entry.glyphId !== void 0 ? font.glyphDataById?.[entry.glyphId] : void 0) ?? lookupGlyphData(font, entry.char);
1992
2074
  if (glyph && entry.hasGlyph) {
1993
2075
  let localTime = Math.max(0, Math.min(currentTime - entry.offset, entry.duration));
1994
2076
  const glyphEasing = this._timing?.glyphEasing;
1995
2077
  if (glyphEasing && entry.duration > 0) localTime = glyphEasing(localTime / entry.duration) * entry.duration;
1996
2078
  drawGlyph(ctx, glyph, {
1997
2079
  x,
1998
- y: y + halfLeading,
2080
+ y: y + halfLeading + (entry.yOffsetEm ?? 0) * fontSize,
1999
2081
  fontSize,
2000
2082
  unitsPerEm: font.unitsPerEm,
2001
2083
  ascender: font.ascender,
@@ -2048,4 +2130,4 @@ var TegakiEngine = class {
2048
2130
  //#endregion
2049
2131
  export { findEffect as _, createBundle as a, hasRenderHooks as b, resolveBundle as c, computeTimeline as d, computeLayoutBbox as f, coerceToString as g, drawGlyph as h, domCreateElement as i, BUNDLE_VERSION as l, ensureFontFace as m, buildChildren as n, getBundle as o, computeTextLayout as p, buildRootProps as r, registerBundle as s, TegakiEngine as t, COMPATIBLE_BUNDLE_VERSIONS as u, findEffects as v, resolveEffects as x, getEffectDefinition as y };
2050
2132
 
2051
- //# sourceMappingURL=core-BRYlZ8i2.mjs.map
2133
+ //# sourceMappingURL=core-BrU5Htjs.mjs.map