tegaki 0.16.0 → 0.17.1
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/CHANGELOG.md +16 -0
- package/FONTS-LICENSE.md +6 -0
- package/README.md +1 -0
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/{core-BRYlZ8i2.mjs → core-Cbi21OMg.mjs} +130 -16
- package/dist/core-Cbi21OMg.mjs.map +1 -0
- package/dist/fonts/amiri/amiri-7df37680.ttf +0 -0
- package/dist/fonts/amiri/amiri.ttf +0 -0
- package/dist/fonts/amiri/bundle.d.mts +10631 -0
- package/dist/fonts/amiri/bundle.mjs +130866 -0
- package/dist/fonts/amiri/bundle.mjs.map +1 -0
- package/dist/fonts/caveat/bundle.d.mts +1992 -10
- package/dist/fonts/caveat/bundle.mjs +31054 -105
- package/dist/fonts/caveat/bundle.mjs.map +1 -1
- package/dist/fonts/italianno/bundle.d.mts +1548 -10
- package/dist/fonts/italianno/bundle.mjs +32832 -198
- package/dist/fonts/italianno/bundle.mjs.map +1 -1
- package/dist/fonts/klee-one/bundle.d.mts +11442 -0
- package/dist/fonts/klee-one/bundle.mjs +234910 -0
- package/dist/fonts/klee-one/bundle.mjs.map +1 -0
- package/dist/fonts/klee-one/klee-one-d192e144.ttf +0 -0
- package/dist/fonts/klee-one/klee-one.ttf +0 -0
- package/dist/fonts/parisienne/bundle.d.mts +939 -10
- package/dist/fonts/parisienne/bundle.mjs +19540 -192
- package/dist/fonts/parisienne/bundle.mjs.map +1 -1
- package/dist/fonts/suez-one/bundle.d.mts +2181 -0
- package/dist/fonts/suez-one/bundle.mjs +30466 -0
- package/dist/fonts/suez-one/bundle.mjs.map +1 -0
- package/dist/fonts/suez-one/suez-one-ac7f1d1c.ttf +0 -0
- package/dist/fonts/suez-one/suez-one.ttf +0 -0
- package/dist/fonts/tangerine/bundle.d.mts +40 -10
- package/dist/fonts/tangerine/bundle.mjs +52 -119
- package/dist/fonts/tangerine/bundle.mjs.map +1 -1
- package/dist/fonts/tillana/bundle.d.mts +8557 -0
- package/dist/fonts/tillana/bundle.mjs +224308 -0
- package/dist/fonts/tillana/bundle.mjs.map +1 -0
- package/dist/fonts/tillana/tillana-12e48378.ttf +0 -0
- package/dist/fonts/tillana/tillana.ttf +0 -0
- package/dist/{index-Qr39WZaW.d.mts → index-BRsSO7q7.d.mts} +130 -65
- package/dist/{index-Duog5eW6.d.mts → index-BcyoxWKO.d.mts} +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/react/index.d.mts +3 -3
- package/dist/react/index.mjs +2 -2
- package/dist/{react-Eq0zlDsb.mjs → react-Aw2TTpfu.mjs} +5 -4
- package/dist/react-Aw2TTpfu.mjs.map +1 -0
- package/dist/shaper-harfbuzz/index.d.mts +1 -1
- package/dist/shaper-harfbuzz/index.mjs +35 -1
- package/dist/shaper-harfbuzz/index.mjs.map +1 -1
- package/dist/{shaper-registry-HD6_qkK4.d.mts → shaper-registry-DVS5R37Q.d.mts} +10 -5
- package/dist/solid/index.d.mts +2 -2
- package/dist/solid/index.mjs +4 -2
- package/dist/solid/index.mjs.map +1 -1
- package/dist/wc/index.d.mts +7 -3
- package/dist/wc/index.mjs +14 -3
- package/dist/wc/index.mjs.map +1 -1
- package/fonts/amiri/glyphDataById.json +1 -1
- package/fonts/caveat/bundle.ts +3 -0
- package/fonts/caveat/glyphData.json +1 -1
- package/fonts/caveat/glyphDataById.json +1 -0
- package/fonts/italianno/bundle.ts +3 -0
- package/fonts/italianno/glyphData.json +1 -1
- package/fonts/italianno/glyphDataById.json +1 -0
- package/fonts/klee-one/glyphDataById.json +1 -1
- package/fonts/parisienne/bundle.ts +3 -0
- package/fonts/parisienne/glyphData.json +1 -1
- package/fonts/parisienne/glyphDataById.json +1 -0
- package/fonts/suez-one/glyphDataById.json +1 -1
- package/fonts/tangerine/glyphData.json +1 -1
- package/fonts/tillana/bundle.ts +23 -0
- package/fonts/tillana/glyphData.json +1 -0
- package/fonts/tillana/glyphDataById.json +1 -0
- package/fonts/tillana/tillana-12e48378.ttf +0 -0
- package/fonts/tillana/tillana.ttf +0 -0
- package/package.json +8 -1
- package/src/astro/TegakiRenderer.astro +2 -2
- package/src/core/engine.ts +94 -9
- package/src/core/types.ts +13 -2
- package/src/lib/textLayout.ts +65 -10
- package/src/lib/timeline.test.ts +203 -0
- package/src/lib/timeline.ts +25 -3
- package/src/lib/utils.test.ts +73 -0
- package/src/lib/utils.ts +20 -1
- package/src/react/TegakiRenderer.tsx +2 -0
- package/src/shaper-harfbuzz/index.ts +60 -2
- package/src/solid/TegakiRenderer.tsx +2 -0
- package/src/svelte/TegakiRenderer.svelte +3 -2
- package/src/types.ts +9 -4
- package/src/vue/TegakiRenderer.vue +3 -1
- package/src/wc/TegakiElement.ts +17 -2
- package/dist/core-BRYlZ8i2.mjs.map +0 -1
- package/dist/react-Eq0zlDsb.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# tegaki
|
|
2
2
|
|
|
3
|
+
## 0.17.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- affe5a5: Add shorthand for setting time property to a percentage for controlled progress mode.
|
|
8
|
+
|
|
9
|
+
## 0.17.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 2b4b435: Support Devanagari writing system and add Tillana as built-in font. Also fixed a bug with generating n-grams, which affected Arabic fonts.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- ee2db76: Fix GPOS and advance width features for some Arabic fonts like "Aref Ruqaa"
|
|
18
|
+
|
|
3
19
|
## 0.16.0
|
|
4
20
|
|
|
5
21
|
### 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.
|
package/dist/core/index.d.mts
CHANGED
|
@@ -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-
|
|
2
|
-
import { A as
|
|
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-BRsSO7q7.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 };
|
package/dist/core/index.mjs
CHANGED
|
@@ -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-
|
|
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-Cbi21OMg.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
|
-
|
|
720
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1102
|
+
const data = font.glyphDataById?.[glyph.g] ?? lookupGlyphData(font, firstChar);
|
|
1051
1103
|
const hasGlyph = !!data;
|
|
1052
1104
|
if (isWhitespace) {
|
|
1053
1105
|
sched.separate("word", {
|
|
@@ -1373,13 +1425,34 @@ function getShaperForBundle(bundle) {
|
|
|
1373
1425
|
}
|
|
1374
1426
|
//#endregion
|
|
1375
1427
|
//#region src/core/engine.ts
|
|
1428
|
+
/**
|
|
1429
|
+
* Parse a percentage string like `"50%"` into a 0–1 fraction. Returns `null`
|
|
1430
|
+
* for non-percentage strings or unparseable input. Whitespace around the
|
|
1431
|
+
* value is tolerated; the numeric part is parsed with `Number(...)`, so any
|
|
1432
|
+
* finite numeric form (including negatives and decimals) is accepted.
|
|
1433
|
+
*/
|
|
1434
|
+
function parsePercentage(s) {
|
|
1435
|
+
const trimmed = s.trim();
|
|
1436
|
+
if (!trimmed.endsWith("%")) return null;
|
|
1437
|
+
const num = Number(trimmed.slice(0, -1));
|
|
1438
|
+
return Number.isFinite(num) ? num / 100 : null;
|
|
1439
|
+
}
|
|
1376
1440
|
function resolveTimeControl(prop) {
|
|
1377
1441
|
if (prop == null) return { mode: "uncontrolled" };
|
|
1378
1442
|
if (typeof prop === "number") return {
|
|
1379
1443
|
mode: "controlled",
|
|
1380
1444
|
value: prop
|
|
1381
1445
|
};
|
|
1382
|
-
if (prop === "
|
|
1446
|
+
if (typeof prop === "string") {
|
|
1447
|
+
if (prop === "css") return { mode: "css" };
|
|
1448
|
+
const pct = parsePercentage(prop);
|
|
1449
|
+
if (pct != null) return {
|
|
1450
|
+
mode: "controlled",
|
|
1451
|
+
value: pct,
|
|
1452
|
+
unit: "progress"
|
|
1453
|
+
};
|
|
1454
|
+
return { mode: "uncontrolled" };
|
|
1455
|
+
}
|
|
1383
1456
|
return prop;
|
|
1384
1457
|
}
|
|
1385
1458
|
var TegakiEngine = class {
|
|
@@ -1412,6 +1485,7 @@ var TegakiEngine = class {
|
|
|
1412
1485
|
_quality;
|
|
1413
1486
|
_showOverlay = false;
|
|
1414
1487
|
_onComplete;
|
|
1488
|
+
_onChangeTimeline;
|
|
1415
1489
|
_direction;
|
|
1416
1490
|
_resolvedEffects = resolveEffects(void 0);
|
|
1417
1491
|
_seed;
|
|
@@ -1498,6 +1572,32 @@ var TegakiEngine = class {
|
|
|
1498
1572
|
get duration() {
|
|
1499
1573
|
return this._timeline.totalDuration;
|
|
1500
1574
|
}
|
|
1575
|
+
/**
|
|
1576
|
+
* The engine's current timeline — the same object that drives rendering.
|
|
1577
|
+
* Reflects the resolved shaper once the (async) shaper promise has
|
|
1578
|
+
* settled; use the `onChangeTimeline` option to be notified of recomputations.
|
|
1579
|
+
* Treat the returned object as read-only.
|
|
1580
|
+
*/
|
|
1581
|
+
get timeline() {
|
|
1582
|
+
return this._timeline;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Compute a timeline for arbitrary text against this engine's currently-
|
|
1586
|
+
* loaded font, timing config, and resolved shaper. Useful for measuring
|
|
1587
|
+
* the duration of hypothetical text without changing what's rendered
|
|
1588
|
+
* (e.g. layout planning, fade-in scheduling).
|
|
1589
|
+
*
|
|
1590
|
+
* Returns an empty timeline when no font is loaded. The result reflects
|
|
1591
|
+
* shaper state at call time — call after `onChangeTimeline` has fired
|
|
1592
|
+
* once to be sure the shaper has resolved.
|
|
1593
|
+
*/
|
|
1594
|
+
computeTimeline(text) {
|
|
1595
|
+
if (!this._font) return {
|
|
1596
|
+
entries: [],
|
|
1597
|
+
totalDuration: 0
|
|
1598
|
+
};
|
|
1599
|
+
return computeTimeline(text, this._font, this._timing, this._shaper);
|
|
1600
|
+
}
|
|
1501
1601
|
get isPlaying() {
|
|
1502
1602
|
return this._playing;
|
|
1503
1603
|
}
|
|
@@ -1520,9 +1620,20 @@ var TegakiEngine = class {
|
|
|
1520
1620
|
this._playing = false;
|
|
1521
1621
|
this._evaluatePlayback();
|
|
1522
1622
|
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Seek the (uncontrolled) timeline to an absolute time. Accepts seconds
|
|
1625
|
+
* (number) or a percentage string like `"50%"`, which is interpreted as
|
|
1626
|
+
* a fraction of the timeline's total duration.
|
|
1627
|
+
*/
|
|
1523
1628
|
seek(time) {
|
|
1524
1629
|
if (this._timeControl.mode !== "uncontrolled") return;
|
|
1525
|
-
|
|
1630
|
+
let resolved;
|
|
1631
|
+
if (typeof time === "string") {
|
|
1632
|
+
const pct = parsePercentage(time);
|
|
1633
|
+
if (pct == null) return;
|
|
1634
|
+
resolved = pct * this._timeline.totalDuration;
|
|
1635
|
+
} else resolved = time;
|
|
1636
|
+
this._internalTime = Math.max(0, Math.min(resolved, this._timeline.totalDuration));
|
|
1526
1637
|
this._delayRemaining = 0;
|
|
1527
1638
|
this._loopGapRemaining = 0;
|
|
1528
1639
|
this._checkCompletion();
|
|
@@ -1547,7 +1658,7 @@ var TegakiEngine = class {
|
|
|
1547
1658
|
let dirtyRender = false;
|
|
1548
1659
|
let dirtyPlayback = false;
|
|
1549
1660
|
if ("text" in options) {
|
|
1550
|
-
const nextText = (options.text ?? "").replace(/\r\n?/g, "\n");
|
|
1661
|
+
const nextText = (options.text ?? "").replace(/\r\n?/g, "\n").normalize("NFC");
|
|
1551
1662
|
if (nextText !== this._text) {
|
|
1552
1663
|
this._text = nextText;
|
|
1553
1664
|
dirtyTimeline = true;
|
|
@@ -1621,6 +1732,7 @@ var TegakiEngine = class {
|
|
|
1621
1732
|
dirtyRender = true;
|
|
1622
1733
|
}
|
|
1623
1734
|
if ("onComplete" in options) this._onComplete = options.onComplete;
|
|
1735
|
+
if ("onChangeTimeline" in options) this._onChangeTimeline = options.onChangeTimeline;
|
|
1624
1736
|
if (dirtyTimeline) this._recomputeTimeline();
|
|
1625
1737
|
if (dirtyRender || dirtyTimeline || dirtyLayout) this._updateDom();
|
|
1626
1738
|
if (dirtyLayout) this._recomputeLayout();
|
|
@@ -1802,6 +1914,7 @@ var TegakiEngine = class {
|
|
|
1802
1914
|
entries: [],
|
|
1803
1915
|
totalDuration: 0
|
|
1804
1916
|
};
|
|
1917
|
+
this._onChangeTimeline?.(this._timeline);
|
|
1805
1918
|
}
|
|
1806
1919
|
_recomputeLayout() {
|
|
1807
1920
|
if (this._fontReady && this._font?.family && this._fontSize && this._containerWidth && this._text) {
|
|
@@ -1810,7 +1923,7 @@ var TegakiEngine = class {
|
|
|
1810
1923
|
if (key === this._layoutKey) return;
|
|
1811
1924
|
this._layoutKey = key;
|
|
1812
1925
|
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);
|
|
1926
|
+
if (this._shaper && this._font) layout = applyShaperPositions(layout, this._overlayEl, this._text, this._fontSize, this._font, this._shaper, this._timeline);
|
|
1814
1927
|
this._layout = layout;
|
|
1815
1928
|
} else {
|
|
1816
1929
|
this._layoutKey = "";
|
|
@@ -1987,15 +2100,16 @@ var TegakiEngine = class {
|
|
|
1987
2100
|
const lineIdx = graphemeToLine[charIdx] ?? -1;
|
|
1988
2101
|
if (lineIdx < 0) continue;
|
|
1989
2102
|
const y = lineIdx * lineHeight;
|
|
1990
|
-
const
|
|
1991
|
-
const
|
|
2103
|
+
const lineLeftEm = layout.lineLefts?.[lineIdx];
|
|
2104
|
+
const x = entry.xOffsetEm !== void 0 && lineLeftEm !== void 0 ? (lineLeftEm + entry.xOffsetEm) * fontSize : (layout.charOffsets[charIdx] ?? 0) * fontSize;
|
|
2105
|
+
const glyph = (entry.glyphId !== void 0 ? font.glyphDataById?.[entry.glyphId] : void 0) ?? lookupGlyphData(font, entry.char);
|
|
1992
2106
|
if (glyph && entry.hasGlyph) {
|
|
1993
2107
|
let localTime = Math.max(0, Math.min(currentTime - entry.offset, entry.duration));
|
|
1994
2108
|
const glyphEasing = this._timing?.glyphEasing;
|
|
1995
2109
|
if (glyphEasing && entry.duration > 0) localTime = glyphEasing(localTime / entry.duration) * entry.duration;
|
|
1996
2110
|
drawGlyph(ctx, glyph, {
|
|
1997
2111
|
x,
|
|
1998
|
-
y: y + halfLeading,
|
|
2112
|
+
y: y + halfLeading + (entry.yOffsetEm ?? 0) * fontSize,
|
|
1999
2113
|
fontSize,
|
|
2000
2114
|
unitsPerEm: font.unitsPerEm,
|
|
2001
2115
|
ascender: font.ascender,
|
|
@@ -2048,4 +2162,4 @@ var TegakiEngine = class {
|
|
|
2048
2162
|
//#endregion
|
|
2049
2163
|
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
2164
|
|
|
2051
|
-
//# sourceMappingURL=core-
|
|
2165
|
+
//# sourceMappingURL=core-Cbi21OMg.mjs.map
|