tegaki 0.17.1 → 0.18.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 (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/index.d.mts +3 -3
  3. package/dist/core/index.mjs +1 -1
  4. package/dist/{core-Cbi21OMg.mjs → core-MwnsJAbN.mjs} +145 -10
  5. package/dist/core-MwnsJAbN.mjs.map +1 -0
  6. package/dist/fonts/amiri/bundle.mjs.map +1 -1
  7. package/dist/fonts/caveat/bundle.mjs.map +1 -1
  8. package/dist/fonts/italianno/bundle.mjs.map +1 -1
  9. package/dist/fonts/klee-one/bundle.mjs.map +1 -1
  10. package/dist/fonts/parisienne/bundle.mjs.map +1 -1
  11. package/dist/fonts/suez-one/bundle.mjs.map +1 -1
  12. package/dist/fonts/tangerine/bundle.mjs.map +1 -1
  13. package/dist/fonts/tillana/bundle.mjs.map +1 -1
  14. package/dist/{index-BcyoxWKO.d.mts → index-B2rqZbJt.d.mts} +3 -3
  15. package/dist/{index-BRsSO7q7.d.mts → index-BVYJFRMq.d.mts} +49 -4
  16. package/dist/index.d.mts +4 -4
  17. package/dist/index.mjs +2 -2
  18. package/dist/react/index.d.mts +4 -4
  19. package/dist/react/index.mjs +2 -2
  20. package/dist/{react-Aw2TTpfu.mjs → react-174En_ya.mjs} +2 -2
  21. package/dist/{react-Aw2TTpfu.mjs.map → react-174En_ya.mjs.map} +1 -1
  22. package/dist/shaper-harfbuzz/index.d.mts +1 -1
  23. package/dist/shaper-harfbuzz/index.mjs +25 -34
  24. package/dist/shaper-harfbuzz/index.mjs.map +1 -1
  25. package/dist/{shaper-registry-DVS5R37Q.d.mts → shaper-registry-CrfNiB1j.d.mts} +2 -2
  26. package/dist/solid/index.d.mts +3 -3
  27. package/dist/solid/index.mjs +1 -1
  28. package/dist/solid/index.mjs.map +1 -1
  29. package/dist/wc/index.d.mts +3 -3
  30. package/dist/wc/index.mjs +1 -1
  31. package/dist/wc/index.mjs.map +1 -1
  32. package/package.json +7 -7
  33. package/src/astro/TegakiRenderer.astro +31 -5
  34. package/src/core/bundle-registry.ts +3 -0
  35. package/src/core/engine.ts +1 -0
  36. package/src/core/index.ts +7 -1
  37. package/src/env.d.ts +2 -0
  38. package/src/lib/drawGlyph.ts +12 -2
  39. package/src/lib/font.ts +1 -1
  40. package/src/lib/timeline.test.ts +173 -0
  41. package/src/lib/timeline.ts +179 -10
  42. package/src/nuxt/module.ts +0 -3
  43. package/src/shaper-harfbuzz/index.ts +24 -42
  44. package/src/types.ts +1 -1
  45. package/src/vue/TegakiRenderer.vue +2 -2
  46. package/src/vue/index.ts +1 -0
  47. package/dist/core-Cbi21OMg.mjs.map +0 -1
  48. package/src/lib/harfbuzzjs.d.ts +0 -71
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # tegaki
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b6e883b: Update Harfbuzz to v1.1.0
8
+ - d358cbd: Add `stagger` timing mode where each glyph starts a fixed advance (seconds or `"N%"` of the previous glyph's effective duration) after the previous one, with an optional static per-glyph duration that scales strokes to fit. Exposed in the website previewer via the new `st` / `sa` / `sd` URL keys.
9
+
10
+ ### Patch Changes
11
+
12
+ - 150296e: Fix the Astro adapter so passing `font={bundle}` hydrates without an explicit `bundle` prop and lookups by human-friendly font name resolve correctly. Animations now also re-hydrate after Astro View Transitions navigations.
13
+ - 150296e: Make the Vue adapter's `effects` prop generic so custom effect configs are correctly type-inferred. Also drop a redundant Nuxt config type augmentation.
14
+
3
15
  ## 0.17.1
4
16
 
5
17
  ### Patch Changes
@@ -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-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
- 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
+ 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-CrfNiB1j.mjs";
2
+ import { A as TimelineEntry, C as resolveEffects, D as computeTextLayout, E as computeLayoutBbox, M as computeTimeline, 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 TimelineStaggerConfig, 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-BVYJFRMq.mjs";
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, TimelineStaggerConfig, 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-Cbi21OMg.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-MwnsJAbN.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 };
@@ -368,8 +368,13 @@ function defaultStrokeEasing(t) {
368
368
  * delay relative to `localTime = 0`. Used by the timeline scheduler to defer
369
369
  * priority-tagged strokes (disconnected marks / i-dots / Arabic nuqṭa) to
370
370
  * after every body stroke in the word has drawn.
371
+ *
372
+ * `strokeTimeScale` multiplies both bundled `d` and `a` so a glyph's strokes
373
+ * fit a stretched/compressed time slot (used by stagger mode with a static
374
+ * `duration`). Defaults to `1` (no scaling). `strokeDelays` are already
375
+ * scheduler-relative seconds and are not affected by this scale.
371
376
  */
372
- function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], seed = 0, getSubdivided, strokeEasing = defaultStrokeEasing, strokeScale = 1, strokeStyleOverride, strokeDelays) {
377
+ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], seed = 0, getSubdivided, strokeEasing = defaultStrokeEasing, strokeScale = 1, strokeStyleOverride, strokeDelays, strokeTimeScale = 1) {
373
378
  const defaultStrokePaint = strokeStyleOverride ?? color;
374
379
  const scale = pos.fontSize / pos.unitsPerEm;
375
380
  const ox = pos.x;
@@ -419,10 +424,11 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
419
424
  };
420
425
  for (let si = 0; si < glyph.s.length; si++) {
421
426
  const stroke = glyph.s[si];
422
- const delay = strokeDelays?.[si] ?? stroke.d;
427
+ const delay = strokeDelays?.[si] ?? stroke.d * strokeTimeScale;
423
428
  if (localTime < delay) continue;
424
429
  const elapsed = localTime - delay;
425
- const linearProgress = Math.min(elapsed / stroke.a, 1);
430
+ const animDuration = stroke.a * strokeTimeScale;
431
+ const linearProgress = animDuration > 0 ? Math.min(elapsed / animDuration, 1) : 1;
426
432
  const progress = strokeEasing ? strokeEasing(linearProgress) : linearProgress;
427
433
  const rawPts = stroke.p;
428
434
  if (rawPts.length === 0) continue;
@@ -875,6 +881,79 @@ function computeTimeline(text, font, config, shaper) {
875
881
  if (shaper && font.glyphDataById) return computeShapedTimeline(text, font, config, shaper);
876
882
  return computeGraphemeTimeline(text, font, config);
877
883
  }
884
+ /** Parse a stagger advance value into seconds, given the previous glyph's bundled duration. */
885
+ function resolveAdvance(advance, prevBundled) {
886
+ if (typeof advance === "number") return Math.max(0, advance);
887
+ const m = /^(-?\d+(?:\.\d+)?)\s*%$/.exec(advance);
888
+ if (m) {
889
+ const pct = Number(m[1]) / 100;
890
+ return Math.max(0, pct * prevBundled);
891
+ }
892
+ const n = Number(advance);
893
+ return Number.isFinite(n) ? Math.max(0, n) : 0;
894
+ }
895
+ var StaggerScheduler = class {
896
+ wordGap;
897
+ lineGap;
898
+ advance;
899
+ staticDuration;
900
+ entries = [];
901
+ offset = 0;
902
+ /**
903
+ * Effective duration of the most recent glyph (= `staticDuration` when set,
904
+ * else its bundled `glyph.t`). Basis for percent advances — so
905
+ * `advance: '100%'` always means "start once the previous glyph finishes",
906
+ * independent of whether the duration was overridden.
907
+ */
908
+ prevEffective = 0;
909
+ hasPrev = false;
910
+ /** Accumulated word/line gap pending until the next glyph (or finalize). */
911
+ pendingGap = 0;
912
+ constructor(wordGap, lineGap, advance, staticDuration) {
913
+ this.wordGap = wordGap;
914
+ this.lineGap = lineGap;
915
+ this.advance = advance;
916
+ this.staticDuration = staticDuration;
917
+ }
918
+ addGlyph(fields, bundledDuration) {
919
+ if (this.hasPrev) {
920
+ this.offset += resolveAdvance(this.advance, this.prevEffective) + this.pendingGap;
921
+ this.pendingGap = 0;
922
+ }
923
+ const duration = this.staticDuration ?? bundledDuration;
924
+ const strokeTimeScale = bundledDuration > 0 ? duration / bundledDuration : 1;
925
+ this.entries.push({
926
+ ...fields,
927
+ offset: this.offset,
928
+ duration,
929
+ ...strokeTimeScale !== 1 ? { strokeTimeScale } : {}
930
+ });
931
+ this.prevEffective = duration;
932
+ this.hasPrev = true;
933
+ }
934
+ /** Emit a zero-duration marker (e.g. whitespace) at the current offset without advancing the cursor. */
935
+ addMarker(fields) {
936
+ this.entries.push({
937
+ ...fields,
938
+ offset: this.offset,
939
+ duration: 0
940
+ });
941
+ }
942
+ separator(sep) {
943
+ this.pendingGap += sep === "line" ? this.lineGap : this.wordGap;
944
+ }
945
+ finalize() {
946
+ let total = 0;
947
+ for (const e of this.entries) {
948
+ const end = e.offset + e.duration;
949
+ if (end > total) total = end;
950
+ }
951
+ return {
952
+ entries: this.entries,
953
+ totalDuration: total
954
+ };
955
+ }
956
+ };
878
957
  /** Decompose a glyph into a body phase and an optional dot phase. */
879
958
  function partitionGlyph(glyph, fallbackTotal, deferDots) {
880
959
  const strokes = glyph.s;
@@ -916,6 +995,9 @@ function partitionGlyph(glyph, fallbackTotal, deferDots) {
916
995
  };
917
996
  }
918
997
  var Scheduler = class {
998
+ glyphGap;
999
+ wordGap;
1000
+ lineGap;
919
1001
  entries = [];
920
1002
  group = [];
921
1003
  offset = 0;
@@ -1013,6 +1095,40 @@ function computeGraphemeTimeline(text, font, config) {
1013
1095
  const unknownDuration = config?.unknownDuration ?? DEFAULTS.unknownDuration;
1014
1096
  const deferDots = config?.deferDots ?? DEFAULTS.deferDots;
1015
1097
  const chars = graphemes(text);
1098
+ if (config?.stagger) {
1099
+ const staticDur = config.stagger.duration === "auto" || config.stagger.duration === void 0 ? void 0 : config.stagger.duration;
1100
+ const sched = new StaggerScheduler(wordGap, lineGap, config.stagger.advance, staticDur);
1101
+ for (let i = 0; i < chars.length; i++) {
1102
+ const char = chars[i];
1103
+ const isLineBreak = char === "\n";
1104
+ const isWhitespace = !isLineBreak && /^\s+$/.test(char);
1105
+ if (isLineBreak) {
1106
+ sched.separator("line");
1107
+ continue;
1108
+ }
1109
+ if (isWhitespace) {
1110
+ sched.addMarker({
1111
+ char,
1112
+ graphemeIndex: i,
1113
+ hasGlyph: false
1114
+ });
1115
+ sched.separator("word");
1116
+ continue;
1117
+ }
1118
+ const glyph = lookupGlyphData(font, char);
1119
+ if (glyph) sched.addGlyph({
1120
+ char,
1121
+ graphemeIndex: i,
1122
+ hasGlyph: true
1123
+ }, glyph.t ?? unknownDuration);
1124
+ else sched.addGlyph({
1125
+ char,
1126
+ graphemeIndex: i,
1127
+ hasGlyph: false
1128
+ }, unknownDuration);
1129
+ }
1130
+ return sched.finalize();
1131
+ }
1016
1132
  const sched = new Scheduler(glyphGap, wordGap, lineGap);
1017
1133
  for (let i = 0; i < chars.length; i++) {
1018
1134
  const char = chars[i];
@@ -1079,7 +1195,8 @@ function computeShapedTimeline(text, font, config, shaper) {
1079
1195
  }
1080
1196
  utf16ToGrapheme[text.length] = chars.length;
1081
1197
  }
1082
- const sched = new Scheduler(glyphGap, wordGap, lineGap);
1198
+ const staggerSched = config?.stagger ? new StaggerScheduler(wordGap, lineGap, config.stagger.advance, config.stagger.duration === "auto" || config.stagger.duration === void 0 ? void 0 : config.stagger.duration) : null;
1199
+ const sched = staggerSched ? null : new Scheduler(glyphGap, wordGap, lineGap);
1083
1200
  let lineStart = 0;
1084
1201
  for (let i = 0; i <= text.length; i++) {
1085
1202
  const atEnd = i === text.length;
@@ -1102,7 +1219,15 @@ function computeShapedTimeline(text, font, config, shaper) {
1102
1219
  const data = font.glyphDataById?.[glyph.g] ?? lookupGlyphData(font, firstChar);
1103
1220
  const hasGlyph = !!data;
1104
1221
  if (isWhitespace) {
1105
- sched.separate("word", {
1222
+ if (staggerSched) {
1223
+ staggerSched.addMarker({
1224
+ char: firstChar,
1225
+ graphemeIndex: graphemeIdx,
1226
+ glyphId: glyph.g,
1227
+ hasGlyph
1228
+ });
1229
+ staggerSched.separator("word");
1230
+ } else sched.separate("word", {
1106
1231
  fields: {
1107
1232
  char: firstChar,
1108
1233
  graphemeIndex: graphemeIdx,
@@ -1113,7 +1238,15 @@ function computeShapedTimeline(text, font, config, shaper) {
1113
1238
  });
1114
1239
  continue;
1115
1240
  }
1116
- if (hasGlyph && data) {
1241
+ if (staggerSched) {
1242
+ const bundled = hasGlyph && data ? data.t ?? unknownDuration : unknownDuration;
1243
+ staggerSched.addGlyph({
1244
+ char: firstChar,
1245
+ graphemeIndex: graphemeIdx,
1246
+ glyphId: glyph.g,
1247
+ hasGlyph: !!(hasGlyph && data)
1248
+ }, bundled);
1249
+ } else if (hasGlyph && data) {
1117
1250
  const part = partitionGlyph(data, unknownDuration, deferDots);
1118
1251
  sched.add({
1119
1252
  fields: {
@@ -1144,11 +1277,12 @@ function computeShapedTimeline(text, font, config, shaper) {
1144
1277
  }
1145
1278
  }
1146
1279
  if (!atEnd) {
1147
- sched.separate("line");
1280
+ if (staggerSched) staggerSched.separator("line");
1281
+ else sched.separate("line");
1148
1282
  lineStart = i + 1;
1149
1283
  }
1150
1284
  }
1151
- return sched.finalize();
1285
+ return staggerSched ? staggerSched.finalize() : sched.finalize();
1152
1286
  }
1153
1287
  //#endregion
1154
1288
  //#region src/types.ts
@@ -1178,6 +1312,7 @@ function checkBundleVersion(bundle) {
1178
1312
  function registerBundle(bundle) {
1179
1313
  checkBundleVersion(bundle);
1180
1314
  bundles.set(bundle.family, bundle);
1315
+ if (bundle.fullFamily && bundle.fullFamily !== bundle.family) bundles.set(bundle.fullFamily, bundle);
1181
1316
  }
1182
1317
  /** Look up a registered bundle by family name. */
1183
1318
  function getBundle(family) {
@@ -2114,7 +2249,7 @@ var TegakiEngine = class {
2114
2249
  unitsPerEm: font.unitsPerEm,
2115
2250
  ascender: font.ascender,
2116
2251
  descender: font.descender
2117
- }, localTime, font.lineCap, color, this._resolvedEffects, this._seed + charIdx, getSubdivided, this._timing?.strokeEasing, strokeScale, stage?.strokeStyle, entry.strokeDelays);
2252
+ }, localTime, font.lineCap, color, this._resolvedEffects, this._seed + charIdx, getSubdivided, this._timing?.strokeEasing, strokeScale, stage?.strokeStyle, entry.strokeDelays, entry.strokeTimeScale);
2118
2253
  } else if (!entry.hasGlyph && currentTime >= entry.offset + entry.duration) {
2119
2254
  const baseline = y + halfLeading + font.ascender / font.unitsPerEm * fontSize;
2120
2255
  drawFallbackGlyph(ctx, entry.char, x, baseline, fontSize, cssFontFamily(font), color, this._resolvedEffects, this._seed + charIdx);
@@ -2162,4 +2297,4 @@ var TegakiEngine = class {
2162
2297
  //#endregion
2163
2298
  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 };
2164
2299
 
2165
- //# sourceMappingURL=core-Cbi21OMg.mjs.map
2300
+ //# sourceMappingURL=core-MwnsJAbN.mjs.map