tegaki 0.10.0 → 0.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # tegaki
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`4b7db41`](https://github.com/KurtGokhan/tegaki/commit/4b7db41fb1c247ed766ff10284e9cdabd4ab0a25) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Implement new text layout based on DOM and text ranges
8
+
9
+ ### Patch Changes
10
+
11
+ - [`f3602b0`](https://github.com/KurtGokhan/tegaki/commit/f3602b04970c8cb88ea41e87e63ee4709b086d61) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - improve line cap detection for CJK fonts
12
+
13
+ - [`28f58c6`](https://github.com/KurtGokhan/tegaki/commit/28f58c67f9eae8e0123a915d0efea03eaccd5e27) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - fixed a bug with generator that caused it to not load all characters in a font, especially CJK
14
+
15
+ - [`047e5e3`](https://github.com/KurtGokhan/tegaki/commit/047e5e31d3ffabbecf25dd36b5f56d298731c630) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Add `duration` and `easing` options to uncontrolled time mode.
16
+
17
+ - `duration` stretches or compresses one iteration to take exactly N seconds, derived from the natural timeline inside the engine. Mutually exclusive with `speed` / `catchUp` at the type level (discriminated union); when both are set at runtime, `duration` takes precedence.
18
+ - `easing: (t: number) => number` maps linear progress (0–1) to displayed progress (0–1). Applied at read-time, so `currentTime`, `onTimeChange`, and the `--tegaki-time` / `--tegaki-progress` CSS custom properties all reflect the eased value. Completion is evaluated against linear progress so overshoot/undershoot curves (e.g. `easeOutBack`) don't trip completion early or late.
19
+ - The web component adapter accepts a `duration` attribute; `easing` is available via the `time` JS property only (it's function-valued).
20
+
3
21
  ## 0.10.0
4
22
 
5
23
  ### Minor Changes
@@ -1,2 +1,2 @@
1
- import { A as TegakiMultiEffectName, C as Point, D as TegakiEffectName, E as TegakiEffectConfigs, M as TimedPoint, O as TegakiEffects, S as PathCommand, T as TegakiBundle, _ as BBox, a as TimeControlProp, b as GlyphData, c as TimelineConfig, d as TextLayout, f as computeTextLayout, g as resolveEffects, h as ResolvedEffect, i as TimeControlMode, j as TegakiSingletonEffectName, k as TegakiGlyphData, l as TimelineEntry, m as drawGlyph, n as TegakiEngine, o as createBundle, p as ensureFontFace, r as TegakiEngineOptions, s as Timeline, t as CreateElementFn, u as computeTimeline, v as CSSLength, w as Stroke, x as LineCap, y as FontOutput } from "../index-BdBLaKjh.mjs";
2
- export { BBox, CSSLength, CreateElementFn, FontOutput, GlyphData, LineCap, PathCommand, Point, ResolvedEffect, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, computeTextLayout, computeTimeline, createBundle, drawGlyph, ensureFontFace, resolveEffects };
1
+ import { A as Stroke, C as BBox, D as LineCap, E as GlyphData, F as TegakiGlyphData, I as TegakiMultiEffectName, L as TegakiSingletonEffectName, M as TegakiEffectConfigs, N as TegakiEffectName, O as PathCommand, P as TegakiEffects, R as TimedPoint, S as resolveEffects, T as FontOutput, _ as TextLayout, a as CreateElementFn, b as drawGlyph, c as TimeControlProp, d as registerBundle, f as resolveBundle, g as computeTimeline, h as TimelineEntry, i as TegakiEngine, j as TegakiBundle, k as Point, l as createBundle, m as TimelineConfig, n as buildRootProps, o as TegakiEngineOptions, p as Timeline, r as domCreateElement, s as TimeControlMode, t as buildChildren, u as getBundle, v as computeTextLayout, w as CSSLength, x as ResolvedEffect, y as ensureFontFace } from "../index-CZFVynOK.mjs";
2
+ export { BBox, CSSLength, CreateElementFn, FontOutput, GlyphData, LineCap, PathCommand, Point, ResolvedEffect, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, buildChildren, buildRootProps, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, getBundle, registerBundle, resolveBundle, resolveEffects };
@@ -1,2 +1,2 @@
1
- import { a as ensureFontFace, c as resolveEffects, i as computeTextLayout, n as createBundle, o as drawGlyph, r as computeTimeline, t as TegakiEngine } from "../core-D68zOEne.mjs";
2
- export { TegakiEngine, computeTextLayout, computeTimeline, createBundle, drawGlyph, ensureFontFace, resolveEffects };
1
+ import { a as createBundle, c as resolveBundle, d as ensureFontFace, f as drawGlyph, i as domCreateElement, l as computeTimeline, m as resolveEffects, n as buildChildren, o as getBundle, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as computeTextLayout } from "../core-I9K3LqxK.mjs";
2
+ export { TegakiEngine, buildChildren, buildRootProps, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, getBundle, registerBundle, resolveBundle, resolveEffects };
@@ -1,4 +1,3 @@
1
- import { layoutWithLines, prepareWithSegments } from "@chenglou/pretext";
2
1
  //#region src/lib/effects.ts
3
2
  const defaultEffects = { pressureWidth: true };
4
3
  const knownEffects = new Set([
@@ -372,87 +371,91 @@ function ensureFont(family, url) {
372
371
  }
373
372
  //#endregion
374
373
  //#region src/lib/textLayout.ts
375
- function computeTextLayout(text, fontFamily, fontSize, lineHeight, maxWidth) {
376
- const fontStr = `${fontSize}px ${fontFamily}`;
377
- const chars = graphemes(text);
378
- const widthCache = /* @__PURE__ */ new Map();
374
+ function computeTextLayout(elOrText, fontSize, fontFamily, lineHeight, maxWidth) {
375
+ if (typeof elOrText === "string") return measureWithTempElement(elOrText, fontFamily, fontSize, lineHeight, maxWidth);
376
+ return measureElement(elOrText, fontSize);
377
+ }
378
+ function measureElement(el, fontSize) {
379
+ const textNode = el.firstChild;
380
+ if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return {
381
+ lines: [],
382
+ charOffsets: [],
383
+ charWidths: []
384
+ };
385
+ const chars = graphemes(textNode.textContent ?? "");
386
+ if (!chars.length) return {
387
+ lines: [],
388
+ charOffsets: [],
389
+ charWidths: []
390
+ };
391
+ const range = document.createRange();
392
+ const charOffsets = [];
379
393
  const charWidths = [];
380
- for (const char of chars) {
381
- let w = widthCache.get(char);
382
- if (w === void 0) {
383
- if (char === "\n") w = 0;
384
- else {
385
- const r = layoutWithLines(prepareWithSegments(char, fontStr, { whiteSpace: "pre-wrap" }), Infinity, lineHeight);
386
- w = r.lines.length > 0 ? r.lines[0].width / fontSize : 0;
387
- }
388
- widthCache.set(char, w);
389
- }
390
- charWidths.push(w);
391
- }
392
- const prepared = prepareWithSegments(text, fontStr, { whiteSpace: "pre-wrap" });
393
- const singleLineResult = layoutWithLines(prepared, Infinity, lineHeight);
394
- const intrinsicWidth = Math.max(0, ...singleLineResult.lines.map((l) => l.width)) / fontSize;
395
- const result = layoutWithLines(prepared, maxWidth, lineHeight);
396
- const utf16ToCodePoint = [];
397
- for (let ci = 0; ci < chars.length; ci++) for (let j = 0; j < chars[ci].length; j++) utf16ToCodePoint.push(ci);
398
394
  const lines = [];
395
+ let currentLine = [];
396
+ let prevTop = -Infinity;
397
+ let lineStartX = 0;
399
398
  let utf16Offset = 0;
400
- for (const line of result.lines) {
401
- const indices = [];
402
- const seen = /* @__PURE__ */ new Set();
403
- for (let i = 0; i < line.text.length; i++) {
404
- const cpIdx = utf16ToCodePoint[utf16Offset + i];
405
- if (!seen.has(cpIdx)) {
406
- seen.add(cpIdx);
407
- indices.push(cpIdx);
408
- }
409
- }
410
- utf16Offset += line.text.length;
411
- if (utf16Offset < text.length && text[utf16Offset] === "\n") {
412
- const cpIdx = utf16ToCodePoint[utf16Offset];
413
- indices.push(cpIdx);
414
- utf16Offset++;
415
- }
416
- lines.push(indices);
417
- }
418
- if (utf16Offset < text.length) {
419
- const indices = [];
420
- const seen = /* @__PURE__ */ new Set();
421
- for (let i = utf16Offset; i < text.length; i++) {
422
- const cpIdx = utf16ToCodePoint[i];
423
- if (!seen.has(cpIdx)) {
424
- seen.add(cpIdx);
425
- indices.push(cpIdx);
426
- }
399
+ for (let i = 0; i < chars.length; i++) {
400
+ const char = chars[i];
401
+ if (char === "\n") {
402
+ charOffsets.push(0);
403
+ charWidths.push(0);
404
+ currentLine.push(i);
405
+ lines.push(currentLine);
406
+ currentLine = [];
407
+ prevTop = -Infinity;
408
+ utf16Offset += char.length;
409
+ continue;
427
410
  }
428
- lines.push(indices);
429
- }
430
- const kernings = [];
431
- const pairCache = /* @__PURE__ */ new Map();
432
- for (let i = 0; i < chars.length - 1; i++) {
433
- const a = chars[i];
434
- const b = chars[i + 1];
435
- if (a === "\n" || b === "\n") {
436
- kernings.push(0);
411
+ range.setStart(textNode, utf16Offset);
412
+ range.setEnd(textNode, utf16Offset + char.length);
413
+ const rects = range.getClientRects();
414
+ utf16Offset += char.length;
415
+ if (rects.length === 0) {
416
+ charOffsets.push(0);
417
+ charWidths.push(0);
418
+ currentLine.push(i);
437
419
  continue;
438
420
  }
439
- const pair = `${a}${b}`;
440
- let k = pairCache.get(pair);
441
- if (k === void 0) {
442
- const r = layoutWithLines(prepareWithSegments(pair, fontStr, { whiteSpace: "pre-wrap" }), Infinity, lineHeight);
443
- k = (r.lines.length > 0 ? r.lines[0].width / fontSize : 0) - (widthCache.get(a) ?? 0) - (widthCache.get(b) ?? 0);
444
- if (Math.abs(k) < .001) k = 0;
445
- pairCache.set(pair, k);
421
+ const rect = rects[0];
422
+ if (currentLine.length > 0 && rect.top - prevTop > fontSize * .25) {
423
+ lines.push(currentLine);
424
+ currentLine = [];
446
425
  }
447
- kernings.push(k);
426
+ if (currentLine.length === 0) {
427
+ prevTop = rect.top;
428
+ lineStartX = rect.left;
429
+ }
430
+ charOffsets.push((rect.left - lineStartX) / fontSize);
431
+ charWidths.push(rect.width / fontSize);
432
+ currentLine.push(i);
448
433
  }
434
+ if (currentLine.length > 0) lines.push(currentLine);
449
435
  return {
450
436
  lines,
451
- charWidths,
452
- kernings,
453
- intrinsicWidth
437
+ charOffsets,
438
+ charWidths
454
439
  };
455
440
  }
441
+ function measureWithTempElement(text, fontFamily, fontSize, lineHeight, maxWidth) {
442
+ const el = document.createElement("div");
443
+ el.style.position = "absolute";
444
+ el.style.left = "-9999px";
445
+ el.style.top = "-9999px";
446
+ el.style.visibility = "hidden";
447
+ el.style.fontFamily = fontFamily;
448
+ el.style.fontSize = `${fontSize}px`;
449
+ el.style.lineHeight = `${lineHeight}px`;
450
+ el.style.whiteSpace = "pre-wrap";
451
+ el.style.overflowWrap = "break-word";
452
+ el.style.width = `${maxWidth}px`;
453
+ el.textContent = text;
454
+ document.body.appendChild(el);
455
+ const result = measureElement(el, fontSize);
456
+ document.body.removeChild(el);
457
+ return result;
458
+ }
456
459
  //#endregion
457
460
  //#region src/lib/timeline.ts
458
461
  const DEFAULTS = {
@@ -494,6 +497,25 @@ function computeTimeline(text, font, config) {
494
497
  };
495
498
  }
496
499
  //#endregion
500
+ //#region src/core/bundle-registry.ts
501
+ const bundles = /* @__PURE__ */ new Map();
502
+ /** Register a font bundle so it can be referenced by family name. */
503
+ function registerBundle(bundle) {
504
+ bundles.set(bundle.family, bundle);
505
+ }
506
+ /** Look up a registered bundle by family name. */
507
+ function getBundle(family) {
508
+ return bundles.get(family);
509
+ }
510
+ function resolveBundle(font) {
511
+ if (typeof font === "string") {
512
+ const bundle = getBundle(font);
513
+ if (!bundle) throw new Error(`TegakiEngine: no bundle registered for "${font}". Call TegakiEngine.registerBundle() first.`);
514
+ return bundle;
515
+ }
516
+ return font;
517
+ }
518
+ //#endregion
497
519
  //#region src/core/createBundle.ts
498
520
  /**
499
521
  * Creates a {@link TegakiBundle} from its constituent parts.
@@ -593,14 +615,17 @@ function drawFallbackGlyph(ctx, char, x, baseline, fontSize, fontFamily, color,
593
615
  ctx.restore();
594
616
  }
595
617
  //#endregion
596
- //#region src/core/engine.ts
618
+ //#region src/core/render-elements.ts
597
619
  const PAD_V_CSS = "max(0.2em, 0.9em - 0.5lh)";
598
620
  function buildRootProps(options) {
599
621
  const text = options.text ?? "";
600
622
  const font = resolveBundle(options.font);
601
623
  const fontFamily = font?.family;
602
624
  const duration = text && font ? computeTimeline(text, font, options.timing).totalDuration : 0;
603
- const time = typeof options.time === "number" ? options.time : typeof options.time === "object" && options.time?.mode === "controlled" ? options.time.unit === "progress" ? options.time.value * duration : options.time.value : typeof options.time === "object" && options.time?.mode === "uncontrolled" ? options.time.initialTime ?? 0 : 0;
625
+ const timeObj = typeof options.time === "object" ? options.time : null;
626
+ const rawTime = typeof options.time === "number" ? options.time : timeObj?.mode === "controlled" ? timeObj.unit === "progress" ? timeObj.value * duration : timeObj.value : timeObj?.mode === "uncontrolled" ? timeObj.initialTime ?? 0 : 0;
627
+ const easing = timeObj?.mode === "uncontrolled" ? timeObj.easing : void 0;
628
+ const time = easing && duration > 0 ? easing(rawTime / duration) * duration : rawTime;
604
629
  const progress = duration > 0 ? time / duration : 0;
605
630
  return {
606
631
  "data-tegaki": "root",
@@ -677,6 +702,8 @@ function domCreateElement(tag, props, ...children) {
677
702
  else el.appendChild(child);
678
703
  return el;
679
704
  }
705
+ //#endregion
706
+ //#region src/core/engine.ts
680
707
  function resolveTimeControl(prop) {
681
708
  if (prop == null) return { mode: "uncontrolled" };
682
709
  if (typeof prop === "number") return {
@@ -686,23 +713,14 @@ function resolveTimeControl(prop) {
686
713
  if (prop === "css") return { mode: "css" };
687
714
  return prop;
688
715
  }
689
- function resolveBundle(font) {
690
- if (typeof font === "string") {
691
- const bundle = TegakiEngine.getBundle(font);
692
- if (!bundle) throw new Error(`TegakiEngine: no bundle registered for "${font}". Call TegakiEngine.registerBundle() first.`);
693
- return bundle;
694
- }
695
- return font;
696
- }
697
- var TegakiEngine = class TegakiEngine {
698
- static _bundles = /* @__PURE__ */ new Map();
716
+ var TegakiEngine = class {
699
717
  /** Register a font bundle so it can be referenced by family name. */
700
718
  static registerBundle(bundle) {
701
- TegakiEngine._bundles.set(bundle.family, bundle);
719
+ registerBundle(bundle);
702
720
  }
703
721
  /** Look up a registered bundle by family name. */
704
722
  static getBundle(family) {
705
- return TegakiEngine._bundles.get(family);
723
+ return getBundle(family);
706
724
  }
707
725
  _rootEl;
708
726
  _contentEl = null;
@@ -725,6 +743,7 @@ var TegakiEngine = class TegakiEngine {
725
743
  totalDuration: 0
726
744
  };
727
745
  _layout = null;
746
+ _layoutKey = "";
728
747
  _fontReady = false;
729
748
  _containerWidth = 0;
730
749
  _fontSize = 0;
@@ -788,6 +807,8 @@ var TegakiEngine = class TegakiEngine {
788
807
  const tc = this._timeControl;
789
808
  if (tc.mode === "css") return this._cssTime;
790
809
  if (tc.mode === "controlled") return tc.unit === "progress" ? tc.value * this._timeline.totalDuration : tc.value;
810
+ const totalDur = this._timeline.totalDuration;
811
+ if (tc.easing && totalDur > 0) return tc.easing(this._internalTime / totalDur) * totalDur;
791
812
  return this._internalTime;
792
813
  }
793
814
  get duration() {
@@ -797,7 +818,10 @@ var TegakiEngine = class TegakiEngine {
797
818
  return this._playing;
798
819
  }
799
820
  get isComplete() {
800
- return this._timeline.totalDuration > 0 && this.currentTime >= this._timeline.totalDuration;
821
+ const totalDur = this._timeline.totalDuration;
822
+ if (totalDur === 0) return false;
823
+ if (this._timeControl.mode === "uncontrolled") return this._internalTime >= totalDur;
824
+ return this.currentTime >= totalDur;
801
825
  }
802
826
  get element() {
803
827
  return this._rootEl;
@@ -857,7 +881,7 @@ var TegakiEngine = class TegakiEngine {
857
881
  const oldTc = this._timeControl;
858
882
  const modeChanged = newTc.mode !== oldTc.mode;
859
883
  const controlledValueChanged = newTc.mode === "controlled" && oldTc.mode === "controlled" && (newTc.value !== oldTc.value || newTc.unit !== oldTc.unit);
860
- const uncontrolledChanged = newTc.mode === "uncontrolled" && oldTc.mode === "uncontrolled" && (newTc.speed !== oldTc.speed || newTc.playing !== oldTc.playing || newTc.loop !== oldTc.loop || newTc.delay !== oldTc.delay || newTc.loopGap !== oldTc.loopGap || newTc.catchUp !== oldTc.catchUp);
884
+ const uncontrolledChanged = newTc.mode === "uncontrolled" && oldTc.mode === "uncontrolled" && (newTc.speed !== oldTc.speed || newTc.duration !== oldTc.duration || newTc.playing !== oldTc.playing || newTc.loop !== oldTc.loop || newTc.delay !== oldTc.delay || newTc.loopGap !== oldTc.loopGap || newTc.catchUp !== oldTc.catchUp || newTc.easing !== oldTc.easing);
861
885
  if (modeChanged || controlledValueChanged || uncontrolledChanged) {
862
886
  this._timeControl = newTc;
863
887
  if (newTc.mode === "uncontrolled") {
@@ -894,12 +918,10 @@ var TegakiEngine = class TegakiEngine {
894
918
  }
895
919
  if ("onComplete" in options) this._onComplete = options.onComplete;
896
920
  if (dirtyTimeline) this._recomputeTimeline();
921
+ if (dirtyRender || dirtyTimeline || dirtyLayout) this._updateDom();
897
922
  if (dirtyLayout) this._recomputeLayout();
898
923
  if (dirtyPlayback) this._evaluatePlayback();
899
- if (dirtyRender || dirtyTimeline || dirtyLayout) {
900
- this._updateDom();
901
- this._render();
902
- }
924
+ if (dirtyRender || dirtyTimeline || dirtyLayout) this._render();
903
925
  }
904
926
  destroy() {
905
927
  this._destroyed = true;
@@ -1028,9 +1050,9 @@ var TegakiEngine = class TegakiEngine {
1028
1050
  if (this._font === currentFont && !this._destroyed) {
1029
1051
  this._fontReady = true;
1030
1052
  this._recomputeTimeline();
1053
+ this._updateDom();
1031
1054
  this._recomputeLayout();
1032
1055
  this._evaluatePlayback();
1033
- this._updateDom();
1034
1056
  this._render();
1035
1057
  }
1036
1058
  });
@@ -1043,9 +1065,15 @@ var TegakiEngine = class TegakiEngine {
1043
1065
  };
1044
1066
  }
1045
1067
  _recomputeLayout() {
1046
- const fontFamily = this._font?.family;
1047
- if (this._fontReady && fontFamily && this._fontSize && this._containerWidth && this._text) this._layout = computeTextLayout(this._text, fontFamily, this._fontSize, this._lineHeight, this._containerWidth);
1048
- else this._layout = null;
1068
+ if (this._fontReady && this._font?.family && this._fontSize && this._containerWidth && this._text) {
1069
+ const key = `${this._text}\0${this._font.family}\0${this._fontSize}\0${this._lineHeight}\0${this._containerWidth}`;
1070
+ if (key === this._layoutKey) return;
1071
+ this._layoutKey = key;
1072
+ this._layout = computeTextLayout(this._overlayEl, this._fontSize);
1073
+ } else {
1074
+ this._layoutKey = "";
1075
+ this._layout = null;
1076
+ }
1049
1077
  }
1050
1078
  _evaluatePlayback() {
1051
1079
  if (this._timeControl.mode === "uncontrolled" && this._playing && !!this._font && this._fontReady && !this._prefersReducedMotion) this._startLoop();
@@ -1070,10 +1098,10 @@ var TegakiEngine = class TegakiEngine {
1070
1098
  this._lastTs = ts;
1071
1099
  const tc = this._timeControl;
1072
1100
  if (tc.mode !== "uncontrolled") return;
1073
- const speed = tc.speed ?? 1;
1074
1101
  const loop = tc.loop ?? false;
1075
- const catchUp = tc.catchUp ?? 0;
1076
1102
  const totalDur = this._timeline.totalDuration;
1103
+ const durationOverride = tc.duration;
1104
+ const useDuration = durationOverride !== void 0 && durationOverride > 0;
1077
1105
  if (totalDur === 0 || !loop && this._internalTime >= totalDur) {
1078
1106
  this._internalTime = totalDur;
1079
1107
  this._rafId = requestAnimationFrame(this._tick);
@@ -1097,15 +1125,21 @@ var TegakiEngine = class TegakiEngine {
1097
1125
  this._rafId = requestAnimationFrame(this._tick);
1098
1126
  return;
1099
1127
  }
1100
- let effectiveSpeed = speed;
1101
- if (catchUp > 0) {
1102
- const remaining = Math.max(0, totalDur - this._internalTime);
1103
- const targetBoost = catchUp * Math.max(0, remaining - 2);
1104
- const attackRate = 4;
1105
- const releaseRate = loop ? 30 : 2;
1106
- const rate = targetBoost > this._smoothedBoost ? attackRate : releaseRate;
1107
- this._smoothedBoost += (targetBoost - this._smoothedBoost) * (1 - Math.exp(-rate * dtSec));
1108
- effectiveSpeed = speed + this._smoothedBoost;
1128
+ let effectiveSpeed;
1129
+ if (useDuration) effectiveSpeed = totalDur / durationOverride;
1130
+ else {
1131
+ const speed = tc.speed ?? 1;
1132
+ const catchUp = tc.catchUp ?? 0;
1133
+ effectiveSpeed = speed;
1134
+ if (catchUp > 0) {
1135
+ const remaining = Math.max(0, totalDur - this._internalTime);
1136
+ const targetBoost = catchUp * Math.max(0, remaining - 2);
1137
+ const attackRate = 4;
1138
+ const releaseRate = loop ? 30 : 2;
1139
+ const rate = targetBoost > this._smoothedBoost ? attackRate : releaseRate;
1140
+ this._smoothedBoost += (targetBoost - this._smoothedBoost) * (1 - Math.exp(-rate * dtSec));
1141
+ effectiveSpeed = speed + this._smoothedBoost;
1142
+ }
1109
1143
  }
1110
1144
  let next = this._internalTime + dtSec * effectiveSpeed;
1111
1145
  if (next >= totalDur) {
@@ -1114,7 +1148,8 @@ var TegakiEngine = class TegakiEngine {
1114
1148
  if (loopGap > 0) {
1115
1149
  next = totalDur;
1116
1150
  this._loopGapRemaining = loopGap;
1117
- } else next = next % totalDur;
1151
+ } else if (this._internalTime < totalDur) next = totalDur;
1152
+ else next %= totalDur;
1118
1153
  } else next = totalDur;
1119
1154
  this._smoothedBoost = 0;
1120
1155
  }
@@ -1127,10 +1162,10 @@ var TegakiEngine = class TegakiEngine {
1127
1162
  };
1128
1163
  _notifyTimeChange() {
1129
1164
  const tc = this._timeControl;
1130
- if (tc.mode === "uncontrolled" && tc.onTimeChange) tc.onTimeChange(this._internalTime);
1165
+ if (tc.mode === "uncontrolled" && tc.onTimeChange) tc.onTimeChange(this.currentTime);
1131
1166
  }
1132
1167
  _checkCompletion() {
1133
- const complete = this._timeline.totalDuration > 0 && this.currentTime >= this._timeline.totalDuration;
1168
+ const complete = this.isComplete;
1134
1169
  if (complete && !this._prevCompleted) {
1135
1170
  this._prevCompleted = true;
1136
1171
  this._onComplete?.();
@@ -1163,36 +1198,29 @@ var TegakiEngine = class TegakiEngine {
1163
1198
  const currentTime = this.currentTime;
1164
1199
  let y = 0;
1165
1200
  for (const lineIndices of layout.lines) {
1166
- let x = 0;
1167
1201
  for (const charIdx of lineIndices) {
1168
1202
  const char = characters[charIdx];
1169
1203
  if (char === "\n") continue;
1170
1204
  const entry = this._timeline.entries[charIdx];
1171
- const charWidth = layout.charWidths[charIdx] ?? 0;
1172
- const kerning = layout.kernings[charIdx] ?? 0;
1205
+ const x = (layout.charOffsets[charIdx] ?? 0) * fontSize;
1173
1206
  const glyph = font.glyphData[char];
1174
1207
  if (glyph && entry.hasGlyph) {
1175
1208
  const localTime = Math.max(0, Math.min(currentTime - entry.offset, entry.duration));
1176
- const glyphY = y + halfLeading;
1177
1209
  drawGlyph(ctx, glyph, {
1178
1210
  x,
1179
- y: glyphY,
1211
+ y: y + halfLeading,
1180
1212
  fontSize,
1181
1213
  unitsPerEm: font.unitsPerEm,
1182
1214
  ascender: font.ascender,
1183
1215
  descender: font.descender
1184
1216
  }, localTime, font.lineCap, color, this._resolvedEffects, this._seed + charIdx, this._segmentSize);
1185
- } else if (!entry.hasGlyph && currentTime >= entry.offset + entry.duration) {
1186
- const baseline = y + halfLeading + font.ascender / font.unitsPerEm * fontSize;
1187
- drawFallbackGlyph(ctx, char, x, baseline, fontSize, font.family, color, this._resolvedEffects, this._seed + charIdx);
1188
- }
1189
- x += (charWidth + kerning) * fontSize;
1217
+ } else if (!entry.hasGlyph && currentTime >= entry.offset + entry.duration) drawFallbackGlyph(ctx, char, x, y + halfLeading + font.ascender / font.unitsPerEm * fontSize, fontSize, font.family, color, this._resolvedEffects, this._seed + charIdx);
1190
1218
  }
1191
1219
  y += lineHeight;
1192
1220
  }
1193
1221
  }
1194
1222
  };
1195
1223
  //#endregion
1196
- export { ensureFontFace as a, resolveEffects as c, computeTextLayout as i, createBundle as n, drawGlyph as o, computeTimeline as r, coerceToString as s, TegakiEngine as t };
1224
+ export { createBundle as a, resolveBundle as c, ensureFontFace as d, drawGlyph as f, domCreateElement as i, computeTimeline as l, resolveEffects as m, buildChildren as n, getBundle as o, coerceToString as p, buildRootProps as r, registerBundle as s, TegakiEngine as t, computeTextLayout as u };
1197
1225
 
1198
- //# sourceMappingURL=core-D68zOEne.mjs.map
1226
+ //# sourceMappingURL=core-I9K3LqxK.mjs.map