tegaki 0.3.1 → 0.5.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,24 @@
1
1
  # tegaki
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`dc581bf`](https://github.com/KurtGokhan/tegaki/commit/dc581bf2e68324ba810c01aea3b7d5c646462a42) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Fix font bundle types and make sure they are assignable to the expected type.
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`2236325`](https://github.com/KurtGokhan/tegaki/commit/2236325c7119b6de47be3f479b3e01b2cae4b907) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Rework font loading and improve defaults
14
+
15
+ - **Breaking**: Remove `registerFontFace()` from `TegakiBundle`. Font registration is now handled internally by `TegakiRenderer` via the FontFace API.
16
+ - Add `fontFaceCSS` property to `TegakiBundle` for SSR/stylesheet-based font loading.
17
+ - Export `ensureFontFace()` utility for manually preloading a bundle's font.
18
+ - Fix font layout being calculated with wrong font metrics when switching fonts or when the font isn't loaded yet.
19
+ - Enable `pressureWidth` effect by default.
20
+ - Handle non-JS environments (SSR) more gracefully.
21
+
3
22
  ## 0.3.1
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -28,8 +28,6 @@ Each glyph is run through a processing pipeline — flatten bezier curves, raste
28
28
  import { TegakiRenderer } from 'tegaki';
29
29
  import font from './output/caveat/bundle.ts';
30
30
 
31
- await font.registerFontFace();
32
-
33
31
  function App() {
34
32
  return (
35
33
  <TegakiRenderer font={font} style={{ fontSize: '48px' }}>
@@ -60,8 +58,6 @@ Tegaki ships with pre-generated bundles for four Google Fonts, ready to use with
60
58
  import { TegakiRenderer } from 'tegaki';
61
59
  import caveat from 'tegaki/fonts/caveat';
62
60
 
63
- await caveat.registerFontFace();
64
-
65
61
  function App() {
66
62
  return (
67
63
  <TegakiRenderer font={caveat} style={{ fontSize: '48px' }}>
package/dist/index.d.mts CHANGED
@@ -65,7 +65,7 @@ interface TegakiGlyphData {
65
65
  w: number;
66
66
  t: number;
67
67
  s: {
68
- p: [x: number, y: number, width: number][];
68
+ p: ([x: number, y: number, width: number] | number[])[];
69
69
  d: number;
70
70
  a: number;
71
71
  }[];
@@ -118,11 +118,11 @@ interface TegakiBundle {
118
118
  family: string;
119
119
  lineCap: LineCap;
120
120
  fontUrl: string;
121
+ fontFaceCSS: string;
121
122
  unitsPerEm: number;
122
123
  ascender: number;
123
124
  descender: number;
124
125
  glyphData: Record<string, TegakiGlyphData>;
125
- registerFontFace: () => Promise<void>;
126
126
  }
127
127
  //#endregion
128
128
  //#region src/lib/effects.d.ts
@@ -180,6 +180,18 @@ declare function computeTimeline(text: string, font: TegakiBundle, config?: Time
180
180
  type Coercible = string | number | boolean | null | undefined | readonly Coercible[];
181
181
  //#endregion
182
182
  //#region src/lib/TegakiRenderer.d.ts
183
+ /**
184
+ * Returns a promise that resolves when the font is ready for text measurement.
185
+ * - Already loaded (by us or externally): resolves immediately.
186
+ * - Currently loading externally: waits for `document.fonts.ready`.
187
+ * - Not registered at all: loads it via the FontFace API.
188
+ * Returns `null` if the font is already loaded synchronously.
189
+ */
190
+ /**
191
+ * Ensures the bundle's font face is loaded and available for rendering.
192
+ * Resolves immediately if the font is already loaded.
193
+ */
194
+ declare function ensureFontFace(bundle: TegakiBundle): Promise<void>;
183
195
  type TimeControlMode = {
184
196
  controlled: {
185
197
  mode: 'controlled'; /** Current time in seconds. */
@@ -272,5 +284,5 @@ declare function TegakiRenderer<const E extends TegakiEffects<E> = Record<string
272
284
  ...props
273
285
  }: TegakiRendererProps<E>): _$react_jsx_runtime0.JSX.Element;
274
286
  //#endregion
275
- export { BBox, CSSLength, FontOutput, GlyphData, LineCap, PathCommand, Point, Stroke, TegakiBundle, type TegakiEffectConfigs, TegakiEffectName, type TegakiEffects, TegakiGlyphData, TegakiMultiEffectName, TegakiRenderer, type TegakiRendererHandle, type TegakiRendererProps, TegakiSingletonEffectName, type TimeControlMode, type TimeControlProp, TimedPoint, type Timeline, type TimelineConfig, computeTimeline, drawGlyph };
287
+ export { BBox, CSSLength, FontOutput, GlyphData, LineCap, PathCommand, Point, Stroke, TegakiBundle, type TegakiEffectConfigs, TegakiEffectName, type TegakiEffects, TegakiGlyphData, TegakiMultiEffectName, TegakiRenderer, type TegakiRendererHandle, type TegakiRendererProps, TegakiSingletonEffectName, type TimeControlMode, type TimeControlProp, TimedPoint, type Timeline, type TimelineConfig, computeTimeline, drawGlyph, ensureFontFace };
276
288
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -2,22 +2,26 @@ import { useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useSt
2
2
  import { layoutWithLines, prepareWithSegments } from "@chenglou/pretext";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/lib/effects.ts
5
+ const defaultEffects = { pressureWidth: true };
6
+ const knownEffects = new Set([
7
+ "glow",
8
+ "wobble",
9
+ "pressureWidth",
10
+ "taper",
11
+ "gradient"
12
+ ]);
5
13
  /**
6
14
  * Normalizes an effects record into a sorted array of resolved effects.
7
15
  * Known keys infer the effect name; custom keys read it from the `effect` field.
8
16
  * Boolean `true` becomes an empty config. `false`/absent entries are skipped.
9
17
  */
10
18
  function resolveEffects(effects) {
11
- if (!effects) return [];
12
- const knownEffects = new Set([
13
- "glow",
14
- "wobble",
15
- "pressureWidth",
16
- "taper",
17
- "gradient"
18
- ]);
19
+ const merged = {
20
+ ...defaultEffects,
21
+ ...effects
22
+ };
19
23
  const result = [];
20
- for (const [key, value] of Object.entries(effects)) {
24
+ for (const [key, value] of Object.entries(merged)) {
21
25
  if (value === false || value == null) continue;
22
26
  let effectName;
23
27
  let config;
@@ -513,6 +517,36 @@ function computeTimeline(text, font, config) {
513
517
  }
514
518
  //#endregion
515
519
  //#region src/lib/TegakiRenderer.tsx
520
+ const fontFaceCache = /* @__PURE__ */ new Map();
521
+ /**
522
+ * Returns a promise that resolves when the font is ready for text measurement.
523
+ * - Already loaded (by us or externally): resolves immediately.
524
+ * - Currently loading externally: waits for `document.fonts.ready`.
525
+ * - Not registered at all: loads it via the FontFace API.
526
+ * Returns `null` if the font is already loaded synchronously.
527
+ */
528
+ /**
529
+ * Ensures the bundle's font face is loaded and available for rendering.
530
+ * Resolves immediately if the font is already loaded.
531
+ */
532
+ async function ensureFontFace(bundle) {
533
+ await ensureFont(bundle.family, bundle.fontUrl);
534
+ }
535
+ function ensureFont(family, url) {
536
+ if (typeof document === "undefined") return Promise.resolve();
537
+ for (const face of document.fonts) if (face.family === family) {
538
+ if (face.status === "loaded") return null;
539
+ if (face.status === "loading") return face.loaded.then(() => {});
540
+ }
541
+ let cached = fontFaceCache.get(url);
542
+ if (!cached) {
543
+ cached = new FontFace(family, `url(${url})`, { featureSettings: "'calt' 0, 'liga' 0" }).load().then((loaded) => {
544
+ document.fonts.add(loaded);
545
+ });
546
+ fontFaceCache.set(url, cached);
547
+ }
548
+ return cached;
549
+ }
516
550
  const PADDING_H_EM = .2;
517
551
  const MIN_LINE_HEIGHT_EM = 1.8;
518
552
  const MIN_PADDING_V_EM = .2;
@@ -574,20 +608,21 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
574
608
  onTimeChangeRef.current = onTimeChange;
575
609
  const onCompleteRef = useRef(onComplete);
576
610
  onCompleteRef.current = onComplete;
577
- const [fontReady, setFontReady] = useState(() => typeof document !== "undefined" && !!font && document.fonts.check(`16px "${font.family}"`));
611
+ const [loadedFont, setLoadedFont] = useState(() => font && ensureFont(font.family, font.fontUrl) === null ? font : null);
612
+ const fontReady = !!font && loadedFont === font;
578
613
  useEffect(() => {
579
614
  if (!font) {
580
- setFontReady(false);
615
+ setLoadedFont(null);
581
616
  return;
582
617
  }
583
- if (document.fonts.check(`16px "${font.family}"`)) {
584
- setFontReady(true);
618
+ const pending = ensureFont(font.family, font.fontUrl);
619
+ if (pending === null) {
620
+ setLoadedFont(font);
585
621
  return;
586
622
  }
587
- setFontReady(false);
588
623
  let cancelled = false;
589
- font.registerFontFace().then(() => {
590
- if (!cancelled) setFontReady(true);
624
+ pending.then(() => {
625
+ if (!cancelled) setLoadedFont(font);
591
626
  });
592
627
  return () => {
593
628
  cancelled = true;
@@ -746,6 +781,7 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
746
781
  ]);
747
782
  const padH = PADDING_H_EM * fontSize;
748
783
  const padV = fontSize ? Math.max(MIN_PADDING_V_EM * fontSize, (MIN_LINE_HEIGHT_EM * fontSize - lineHeight) / 2) : 0;
784
+ const padVCss = `max(0.2em, 0.9em - 0.5lh)`;
749
785
  const canvasRef = useRef(null);
750
786
  useLayoutEffect(() => {
751
787
  const canvas = canvasRef.current;
@@ -823,6 +859,7 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
823
859
  maxWidth: "100%",
824
860
  width: "auto",
825
861
  height: "auto",
862
+ fontFamily,
826
863
  [CSS_DURATION]: timeline.totalDuration,
827
864
  [CSS_TIME]: currentTime,
828
865
  [CSS_PROGRESS]: timeline.totalDuration > 0 ? currentTime / timeline.totalDuration : 0
@@ -850,11 +887,19 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
850
887
  "aria-hidden": "true",
851
888
  style: {
852
889
  position: "absolute",
853
- inset: `${-padV}px ${-padH}px`,
854
- width: `calc(100% + ${padH * 2}px)`,
855
- height: `calc(100% + ${padV * 2}px)`,
856
- pointerEvents: "none"
857
- }
890
+ inset: `calc(-1 * ${padVCss}) -0.2em`,
891
+ width: `calc(100% + 0.4em)`,
892
+ height: `calc(100% + 2 * ${padVCss})`,
893
+ pointerEvents: "none",
894
+ overflow: "visible"
895
+ },
896
+ children: /* @__PURE__ */ jsx("span", {
897
+ style: {
898
+ display: "inline-block",
899
+ padding: `${padVCss} 0.2em`
900
+ },
901
+ children: resolvedText
902
+ })
858
903
  }),
859
904
  /* @__PURE__ */ jsx("div", {
860
905
  style: {
@@ -863,7 +908,6 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
863
908
  overflowWrap: "break-word",
864
909
  paddingRight: 1,
865
910
  WebkitTextFillColor: showOverlay ? void 0 : "transparent",
866
- fontFamily,
867
911
  color: showOverlay ? "rgba(255, 0, 0, 0.4)" : void 0
868
912
  },
869
913
  children: resolvedText
@@ -873,6 +917,6 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
873
917
  });
874
918
  }
875
919
  //#endregion
876
- export { TegakiRenderer, computeTimeline, drawGlyph };
920
+ export { TegakiRenderer, computeTimeline, drawGlyph, ensureFontFace };
877
921
 
878
922
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/effects.ts","../src/lib/utils.ts","../src/lib/drawGlyph.ts","../src/lib/drawFallbackGlyph.ts","../src/lib/textLayout.ts","../src/lib/timeline.ts","../src/lib/TegakiRenderer.tsx"],"sourcesContent":["import type { TegakiEffectConfigs, TegakiEffectName } from '../types.ts';\n\nexport interface ResolvedEffect<K extends TegakiEffectName = TegakiEffectName> {\n effect: K;\n order: number;\n config: TegakiEffectConfigs[K];\n}\n\n/**\n * Normalizes an effects record into a sorted array of resolved effects.\n * Known keys infer the effect name; custom keys read it from the `effect` field.\n * Boolean `true` becomes an empty config. `false`/absent entries are skipped.\n */\nexport function resolveEffects(effects: Record<string, any> | undefined): ResolvedEffect[] {\n if (!effects) return [];\n\n const knownEffects: Set<string> = new Set(['glow', 'wobble', 'pressureWidth', 'taper', 'gradient']);\n const result: ResolvedEffect[] = [];\n\n for (const [key, value] of Object.entries(effects)) {\n if (value === false || value == null) continue;\n\n let effectName: TegakiEffectName;\n let config: Record<string, any>;\n let order: number;\n\n if (value === true) {\n effectName = (knownEffects.has(key) ? key : undefined) as TegakiEffectName;\n if (!effectName) continue;\n config = {};\n order = 0;\n } else {\n if (value.enabled === false) continue;\n effectName = value.effect ?? (knownEffects.has(key) ? key : undefined);\n if (!effectName) continue;\n const { effect: _, order: o, enabled: __, ...rest } = value;\n config = rest;\n order = o ?? 0;\n }\n\n result.push({ effect: effectName, order, config });\n }\n\n result.sort((a, b) => a.order - b.order);\n return result;\n}\n\n/** Check if a specific effect is active. */\nexport function findEffect<K extends TegakiEffectName>(effects: ResolvedEffect[], name: K): ResolvedEffect<K> | undefined {\n return effects.find((e) => e.effect === name) as ResolvedEffect<K> | undefined;\n}\n\n/** Get all instances of a specific effect (for duplicates). */\nexport function findEffects<K extends TegakiEffectName>(effects: ResolvedEffect[], name: K): ResolvedEffect<K>[] {\n return effects.filter((e) => e.effect === name) as ResolvedEffect<K>[];\n}\n","import type { CSSLength } from '../types.ts';\n\nconst segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n\n/** Resolve a CSSLength to pixels. Plain numbers are px, `\"Nem\"` is N * fontSize. */\nexport function resolveCSSLength(value: CSSLength, fontSize: number): number {\n if (typeof value === 'number') return value;\n return parseFloat(value) * fontSize;\n}\n\nexport function graphemes(text: string): string[] {\n return Array.from(segmenter.segment(text), (s) => s.segment);\n}\nexport type Coercible = string | number | boolean | null | undefined | readonly Coercible[];\n\nexport function coerceToString(value: unknown): string {\n if (value == null || typeof value === 'boolean') return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'bigint') return String(value);\n if (Array.isArray(value)) return value.map(coerceToString).join('');\n return '';\n}\n","import type { LineCap, TegakiGlyphData } from '../types.ts';\nimport { findEffect, findEffects, type ResolvedEffect } from './effects.ts';\nimport { resolveCSSLength } from './utils.ts';\n\ninterface GlyphPosition {\n /** X offset in CSS pixels */\n x: number;\n /** Y offset in CSS pixels (top of em square) */\n y: number;\n /** Font size in CSS pixels */\n fontSize: number;\n /** Units per em from the font */\n unitsPerEm: number;\n /** Font ascender in font units */\n ascender: number;\n /** Font descender in font units (negative) */\n descender: number;\n}\n\n// --- Color helpers ---\n\nfunction parseColor(color: string): [number, number, number, number] {\n const h = color.replace('#', '');\n if (h.length === 3) {\n return [parseInt(h[0]! + h[0]!, 16), parseInt(h[1]! + h[1]!, 16), parseInt(h[2]! + h[2]!, 16), 1];\n }\n if (h.length === 4) {\n return [parseInt(h[0]! + h[0]!, 16), parseInt(h[1]! + h[1]!, 16), parseInt(h[2]! + h[2]!, 16), parseInt(h[3]! + h[3]!, 16) / 255];\n }\n if (h.length === 8) {\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), parseInt(h.slice(6, 8), 16) / 255];\n }\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), 1];\n}\n\nfunction lerpColor(a: [number, number, number, number], b: [number, number, number, number], t: number): string {\n const r = Math.round(a[0] + (b[0] - a[0]) * t);\n const g = Math.round(a[1] + (b[1] - a[1]) * t);\n const bl = Math.round(a[2] + (b[2] - a[2]) * t);\n const al = a[3] + (b[3] - a[3]) * t;\n if (al >= 1) return `rgb(${r},${g},${bl})`;\n return `rgba(${r},${g},${bl},${al.toFixed(3)})`;\n}\n\nfunction gradientColor(progress: number, colors: string[], seed: number): string {\n if (colors.length === 0) return '#000';\n if (colors.length === 1) return colors[0]!;\n const t = (((progress + seed * 0.1) % 1) + 1) % 1;\n const scaledT = t * (colors.length - 1);\n const i = Math.min(Math.floor(scaledT), colors.length - 2);\n const frac = scaledT - i;\n return lerpColor(parseColor(colors[i]!), parseColor(colors[i + 1]!), frac);\n}\n\nfunction rainbowColor(progress: number, saturation: number, lightness: number, seed: number): string {\n const hue = (progress * 360 + seed * 137.5) % 360;\n return `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n}\n\n// --- Noise helper for wobble ---\n\nfunction hash(x: number): number {\n let h = (x * 2654435761) | 0;\n h = ((h >>> 16) ^ h) * 0x45d9f3b;\n h = ((h >>> 16) ^ h) * 0x45d9f3b;\n h = (h >>> 16) ^ h;\n return (h & 0x7fffffff) / 0x7fffffff; // 0-1\n}\n\nfunction noise1d(x: number, seed: number): number {\n const i = Math.floor(x);\n const f = x - i;\n const t = f * f * (3 - 2 * f); // smoothstep\n return hash(i + seed * 7919) * (1 - t) + hash(i + 1 + seed * 7919) * t;\n}\n\n/**\n * Draw a single glyph's strokes onto a canvas context, animated up to `localTime`.\n * `localTime` is seconds relative to this glyph's start (0 = glyph begins).\n */\nexport function drawGlyph(\n ctx: CanvasRenderingContext2D,\n glyph: TegakiGlyphData,\n pos: GlyphPosition,\n localTime: number,\n lineCap: LineCap,\n color: string,\n effects: ResolvedEffect[] = [],\n seed = 0,\n segmentSize?: number,\n) {\n const scale = pos.fontSize / pos.unitsPerEm;\n const ox = pos.x;\n const oy = pos.y;\n\n const glowEffects = findEffects(effects, 'glow');\n const wobbleEffect = findEffect(effects, 'wobble');\n const pressureEffect = findEffect(effects, 'pressureWidth');\n const taperEffect = findEffect(effects, 'taper');\n const gradientEffect = findEffect(effects, 'gradient');\n\n // Pressure params (0 = uniform avg width, 1 = fully per-point width)\n const pressureAmount = pressureEffect ? Math.max(0, Math.min(pressureEffect.config.strength ?? 1, 1)) : 0;\n\n // Wobble params\n const wobbleAmplitude = wobbleEffect ? (wobbleEffect.config.amplitude ?? 1.5) : 0;\n const wobbleFrequency = wobbleEffect ? (wobbleEffect.config.frequency ?? 8) : 0;\n const wobbleMode = wobbleEffect?.config.mode ?? 'sine';\n\n // Taper params\n const taperStart = taperEffect ? Math.max(0, Math.min(taperEffect.config.startLength ?? 0.15, 1)) : 0;\n const taperEnd = taperEffect ? Math.max(0, Math.min(taperEffect.config.endLength ?? 0.15, 1)) : 0;\n\n // Gradient params\n const gradientColors = gradientEffect?.config.colors;\n const isRainbow = gradientColors === 'rainbow';\n const gradientColorStops = Array.isArray(gradientColors) ? gradientColors : undefined;\n const gradientSaturation = gradientEffect?.config.saturation ?? 80;\n const gradientLightness = gradientEffect?.config.lightness ?? 55;\n\n // Helper: apply wobble offset to a point in font units\n const wobbleX = (x: number, y: number, idx: number) => {\n if (!wobbleEffect) return x;\n if (wobbleMode === 'noise') {\n return x + wobbleAmplitude * (noise1d(y * 0.1 + idx * 0.7, seed) * 2 - 1);\n }\n return x + wobbleAmplitude * Math.sin(wobbleFrequency * (y * 0.01 + idx * 0.7) + seed);\n };\n const wobbleY = (x: number, y: number, idx: number) => {\n if (!wobbleEffect) return y;\n if (wobbleMode === 'noise') {\n return y + wobbleAmplitude * (noise1d(x * 0.1 + idx * 0.5, seed * 1.3 + 1000) * 2 - 1);\n }\n return y + wobbleAmplitude * Math.cos(wobbleFrequency * (x * 0.01 + idx * 0.5) + seed * 1.3);\n };\n\n // Helper: convert font-unit point to pixel\n const px = (x: number) => ox + x * scale;\n const py = (y: number) => oy + (y + pos.ascender) * scale;\n\n // Helper: get color for a given stroke progress\n const colorAt = (progress: number): string => {\n if (isRainbow) return rainbowColor(progress, gradientSaturation, gradientLightness, seed);\n if (gradientColorStops) return gradientColor(progress, gradientColorStops, seed);\n return color;\n };\n const hasGradient = !!gradientEffect;\n\n // Helper: taper multiplier (0-1) for a given stroke progress\n const taperMultiplier = (progress: number): number => {\n let m = 1;\n if (taperStart > 0 && progress < taperStart) m = Math.min(m, progress / taperStart);\n if (taperEnd > 0 && progress > 1 - taperEnd) m = Math.min(m, (1 - progress) / taperEnd);\n return m;\n };\n\n for (const stroke of glyph.s) {\n if (localTime < stroke.d) continue;\n const elapsed = localTime - stroke.d;\n const progress = Math.min(elapsed / stroke.a, 1);\n\n const pts = stroke.p;\n if (pts.length === 0) continue;\n\n const avgWidth = pts.reduce((s, p) => s + p[2], 0) / pts.length;\n const baseLineWidth = Math.max(avgWidth, 0.5) * scale;\n\n // --- Single-point dot ---\n if (pts.length === 1) {\n if (progress <= 0) continue;\n const p = pts[0]!;\n const dotX = px(wobbleX(p[0], p[1], 0));\n const dotY = py(wobbleY(p[0], p[1], 0));\n const perPointDot = Math.max(p[2], 0.5) * scale;\n let dotWidth = baseLineWidth + (perPointDot - baseLineWidth) * pressureAmount;\n dotWidth *= taperMultiplier(0.5);\n\n // Glow passes for dots\n for (const glow of glowEffects) {\n ctx.save();\n ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, pos.fontSize);\n ctx.shadowColor = glow.config.color ?? color;\n ctx.shadowOffsetX = (glow.config.offsetX ?? 0) * scale;\n ctx.shadowOffsetY = (glow.config.offsetY ?? 0) * scale;\n ctx.fillStyle = glow.config.color ?? color;\n ctx.beginPath();\n if (lineCap === 'round') {\n ctx.arc(dotX, dotY, dotWidth / 2, 0, Math.PI * 2);\n } else {\n ctx.rect(dotX - dotWidth / 2, dotY - dotWidth / 2, dotWidth, dotWidth);\n }\n ctx.fill();\n ctx.restore();\n }\n\n // Main dot\n ctx.fillStyle = colorAt(0);\n ctx.beginPath();\n if (lineCap === 'round') {\n ctx.arc(dotX, dotY, dotWidth / 2, 0, Math.PI * 2);\n ctx.fill();\n } else {\n ctx.fillRect(dotX - dotWidth / 2, dotY - dotWidth / 2, dotWidth, dotWidth);\n }\n continue;\n }\n\n // --- Compute total path length ---\n let totalLen = 0;\n for (let j = 1; j < pts.length; j++) {\n const dx = pts[j]![0] - pts[j - 1]![0];\n const dy = pts[j]![1] - pts[j - 1]![1];\n totalLen += Math.sqrt(dx * dx + dy * dy);\n }\n\n const drawLen = totalLen * progress;\n if (drawLen <= 0) continue;\n\n // --- Collect drawable segments ---\n const segments: {\n x0: number;\n y0: number;\n x1: number;\n y1: number;\n width0: number;\n width1: number;\n segProgress: number;\n }[] = [];\n\n let accumulated = 0;\n for (let j = 1; j < pts.length; j++) {\n const prev = pts[j - 1]!;\n const cur = pts[j]!;\n const dx = cur[0] - prev[0];\n const dy = cur[1] - prev[1];\n const segLen = Math.sqrt(dx * dx + dy * dy);\n\n if (accumulated + segLen <= drawLen) {\n segments.push({\n x0: px(wobbleX(prev[0], prev[1], j - 1)),\n y0: py(wobbleY(prev[0], prev[1], j - 1)),\n x1: px(wobbleX(cur[0], cur[1], j)),\n y1: py(wobbleY(cur[0], cur[1], j)),\n width0: prev[2],\n width1: cur[2],\n segProgress: (accumulated + segLen / 2) / totalLen,\n });\n accumulated += segLen;\n } else {\n const remaining = drawLen - accumulated;\n const frac = segLen > 0 ? remaining / segLen : 0;\n const ix = prev[0] + dx * frac;\n const iy = prev[1] + dy * frac;\n const iw = prev[2] + (cur[2] - prev[2]) * frac;\n segments.push({\n x0: px(wobbleX(prev[0], prev[1], j - 1)),\n y0: py(wobbleY(prev[0], prev[1], j - 1)),\n x1: px(wobbleX(ix, iy, j)),\n y1: py(wobbleY(ix, iy, j)),\n width0: prev[2],\n width1: iw,\n segProgress: (accumulated + remaining / 2) / totalLen,\n });\n break;\n }\n }\n\n if (segments.length === 0) continue;\n\n // Keep coarse segments for glow (shadowBlur is expensive per draw call)\n const coarseSegments = segments.slice();\n\n // --- Subdivide long segments for smooth effect transitions ---\n const effectsNeedSubdivision = pressureAmount > 0 || hasGradient || !!wobbleEffect || !!taperEffect;\n const resolvedSegmentSize = segmentSize ?? (effectsNeedSubdivision ? 2 : undefined);\n if (resolvedSegmentSize != null) {\n const maxSegLen = resolvedSegmentSize * scale;\n const subdivided: typeof segments = [];\n for (const seg of segments) {\n const dx = seg.x1 - seg.x0;\n const dy = seg.y1 - seg.y0;\n const len = Math.sqrt(dx * dx + dy * dy);\n const count = Math.max(1, Math.ceil(len / maxSegLen));\n for (let k = 0; k < count; k++) {\n const t0 = k / count;\n const t1 = (k + 1) / count;\n subdivided.push({\n x0: seg.x0 + dx * t0,\n y0: seg.y0 + dy * t0,\n x1: seg.x0 + dx * t1,\n y1: seg.y0 + dy * t1,\n width0: seg.width0 + (seg.width1 - seg.width0) * t0,\n width1: seg.width0 + (seg.width1 - seg.width0) * t1,\n segProgress: seg.segProgress,\n });\n }\n }\n for (let k = 0; k < subdivided.length; k++) {\n subdivided[k]!.segProgress = subdivided.length > 1 ? k / (subdivided.length - 1) : 0;\n }\n segments.length = 0;\n segments.push(...subdivided);\n }\n\n // Helper: compute segment line width with pressure and taper\n const segWidth = (seg: (typeof segments)[0]) => {\n const perPoint = ((seg.width0 + seg.width1) / 2) * scale;\n const w = Math.max(baseLineWidth + (perPoint - baseLineWidth) * pressureAmount, 0.5 * scale);\n return w * taperMultiplier(seg.segProgress);\n };\n\n const needsPerSegment = pressureAmount > 0 || taperEffect;\n\n const drawStrokePath = () => {\n if (needsPerSegment) {\n for (const seg of segments) {\n ctx.lineWidth = segWidth(seg);\n ctx.beginPath();\n ctx.moveTo(seg.x0, seg.y0);\n ctx.lineTo(seg.x1, seg.y1);\n ctx.stroke();\n }\n } else {\n ctx.lineWidth = baseLineWidth;\n ctx.beginPath();\n ctx.moveTo(segments[0]!.x0, segments[0]!.y0);\n for (const seg of segments) {\n ctx.lineTo(seg.x1, seg.y1);\n }\n ctx.stroke();\n }\n };\n\n const drawGradientPath = () => {\n for (const seg of segments) {\n ctx.strokeStyle = colorAt(seg.segProgress);\n if (needsPerSegment) ctx.lineWidth = segWidth(seg);\n ctx.beginPath();\n ctx.moveTo(seg.x0, seg.y0);\n ctx.lineTo(seg.x1, seg.y1);\n ctx.stroke();\n }\n };\n\n ctx.lineCap = lineCap;\n ctx.lineJoin = 'round';\n\n // --- Glow passes (use coarse segments to avoid expensive per-subsegment shadowBlur) ---\n for (const glow of glowEffects) {\n ctx.save();\n ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, pos.fontSize);\n ctx.shadowColor = glow.config.color ?? color;\n ctx.shadowOffsetX = (glow.config.offsetX ?? 0) * scale;\n ctx.shadowOffsetY = (glow.config.offsetY ?? 0) * scale;\n ctx.strokeStyle = glow.config.color ?? color;\n ctx.lineWidth = baseLineWidth;\n ctx.beginPath();\n ctx.moveTo(coarseSegments[0]!.x0, coarseSegments[0]!.y0);\n for (const seg of coarseSegments) {\n ctx.lineTo(seg.x1, seg.y1);\n }\n ctx.stroke();\n ctx.restore();\n }\n\n // --- Main stroke ---\n if (hasGradient) {\n drawGradientPath();\n } else {\n ctx.strokeStyle = color;\n drawStrokePath();\n }\n }\n}\n","import { findEffect, findEffects, type ResolvedEffect } from './effects.ts';\nimport { resolveCSSLength } from './utils.ts';\n\n/**\n * Draw a fallback glyph (plain text) with applicable effects (glow, gradient, wobble).\n */\nexport function drawFallbackGlyph(\n ctx: CanvasRenderingContext2D,\n char: string,\n x: number,\n baseline: number,\n fontSize: number,\n fontFamily: string,\n color: string,\n effects: ResolvedEffect[] = [],\n seed = 0,\n) {\n const glowEffects = findEffects(effects, 'glow');\n const wobbleEffect = findEffect(effects, 'wobble');\n const gradientEffect = findEffect(effects, 'gradient');\n\n // Wobble offsets\n let dx = 0;\n let dy = 0;\n if (wobbleEffect) {\n const amplitude = (wobbleEffect.config.amplitude ?? 1.5) * (fontSize / 100);\n const frequency = wobbleEffect.config.frequency ?? 8;\n dx = amplitude * Math.sin(frequency * (baseline * 0.01) + seed);\n dy = amplitude * Math.cos(frequency * (x * 0.01) + seed * 1.3);\n }\n\n const drawX = x + dx;\n const drawY = baseline + dy;\n\n // Gradient / rainbow color\n let fillColor = color;\n if (gradientEffect) {\n const colors = gradientEffect.config.colors;\n if (colors === 'rainbow') {\n const saturation = gradientEffect.config.saturation ?? 80;\n const lightness = gradientEffect.config.lightness ?? 55;\n const hue = (seed * 137.5) % 360;\n fillColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n } else if (Array.isArray(colors) && colors.length > 0) {\n fillColor = colors[Math.floor(seed) % colors.length]!;\n }\n }\n\n ctx.save();\n ctx.font = `${fontSize}px ${fontFamily}`;\n ctx.textBaseline = 'alphabetic';\n\n // Glow passes\n for (const glow of glowEffects) {\n ctx.save();\n ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, fontSize);\n ctx.shadowColor = glow.config.color ?? color;\n ctx.shadowOffsetX = glow.config.offsetX ?? 0;\n ctx.shadowOffsetY = glow.config.offsetY ?? 0;\n ctx.fillStyle = glow.config.color ?? color;\n ctx.fillText(char, drawX, drawY);\n ctx.restore();\n }\n\n // Main text\n ctx.fillStyle = fillColor;\n ctx.fillText(char, drawX, drawY);\n\n ctx.restore();\n}\n","import { layoutWithLines, prepareWithSegments } from '@chenglou/pretext';\nimport { graphemes } from './utils.ts';\n\nexport interface TextLayout {\n /** Character indices per line */\n lines: number[][];\n /** Width in em per character index */\n charWidths: number[];\n /** Kerning adjustment in em between character at index i and i+1 */\n kernings: number[];\n /** Intrinsic (single-line) width in em */\n intrinsicWidth: number;\n}\n\nexport function computeTextLayout(text: string, fontFamily: string, fontSize: number, lineHeight: number, maxWidth: number): TextLayout {\n const fontStr = `${fontSize}px ${fontFamily}`;\n const chars = graphemes(text);\n\n // Measure unique character widths\n const widthCache = new Map<string, number>();\n const charWidths: number[] = [];\n for (const char of chars) {\n let w = widthCache.get(char);\n if (w === undefined) {\n if (char === '\\n') {\n w = 0;\n } else {\n const p = prepareWithSegments(char, fontStr, { whiteSpace: 'pre-wrap' });\n const r = layoutWithLines(p, Infinity, lineHeight);\n w = r.lines.length > 0 ? r.lines[0]!.width / fontSize : 0;\n }\n widthCache.set(char, w);\n }\n charWidths.push(w);\n }\n\n // Compute intrinsic width (single-line, no wrapping)\n const prepared = prepareWithSegments(text, fontStr, { whiteSpace: 'pre-wrap' });\n const singleLineResult = layoutWithLines(prepared, Infinity, lineHeight);\n const intrinsicWidth = Math.max(0, ...singleLineResult.lines.map((l) => l.width)) / fontSize;\n\n // Line breaking at actual available width\n const result = layoutWithLines(prepared, maxWidth, lineHeight);\n\n // Map line texts back to character indices (grapheme-based)\n // Build a mapping from UTF-16 offset to grapheme index\n const utf16ToCodePoint: number[] = [];\n for (let ci = 0; ci < chars.length; ci++) {\n for (let j = 0; j < chars[ci]!.length; j++) {\n utf16ToCodePoint.push(ci);\n }\n }\n\n const lines: number[][] = [];\n let utf16Offset = 0;\n for (const line of result.lines) {\n const indices: number[] = [];\n const seen = new Set<number>();\n for (let i = 0; i < line.text.length; i++) {\n const cpIdx = utf16ToCodePoint[utf16Offset + i]!;\n if (!seen.has(cpIdx)) {\n seen.add(cpIdx);\n indices.push(cpIdx);\n }\n }\n utf16Offset += line.text.length;\n // Consume the newline that caused this line break\n if (utf16Offset < text.length && text[utf16Offset] === '\\n') {\n const cpIdx = utf16ToCodePoint[utf16Offset]!;\n indices.push(cpIdx);\n utf16Offset++;\n }\n lines.push(indices);\n }\n\n // Any remaining characters (shouldn't happen, but safety)\n if (utf16Offset < text.length) {\n const indices: number[] = [];\n const seen = new Set<number>();\n for (let i = utf16Offset; i < text.length; i++) {\n const cpIdx = utf16ToCodePoint[i]!;\n if (!seen.has(cpIdx)) {\n seen.add(cpIdx);\n indices.push(cpIdx);\n }\n }\n lines.push(indices);\n }\n\n // Measure kerning between adjacent character pairs\n const kernings: number[] = [];\n const pairCache = new Map<string, number>();\n for (let i = 0; i < chars.length - 1; i++) {\n const a = chars[i]!;\n const b = chars[i + 1]!;\n if (a === '\\n' || b === '\\n') {\n kernings.push(0);\n continue;\n }\n const pair = `${a}${b}`;\n let k = pairCache.get(pair);\n if (k === undefined) {\n const p = prepareWithSegments(pair, fontStr, { whiteSpace: 'pre-wrap' });\n const r = layoutWithLines(p, Infinity, lineHeight);\n const pairWidth = r.lines.length > 0 ? r.lines[0]!.width / fontSize : 0;\n k = pairWidth - (widthCache.get(a) ?? 0) - (widthCache.get(b) ?? 0);\n if (Math.abs(k) < 0.001) k = 0;\n pairCache.set(pair, k);\n }\n kernings.push(k);\n }\n\n return { lines, charWidths, kernings, intrinsicWidth };\n}\n","import type { TegakiBundle } from '../types.ts';\nimport { graphemes } from './utils.ts';\n\nexport interface TimelineConfig {\n /** Pause between glyphs (seconds). Default: `0.1` */\n glyphGap?: number;\n /** Pause after a space character (seconds). Default: `0.15` */\n wordGap?: number;\n /** Pause after a newline / line break (seconds). Default: `0.3` */\n lineGap?: number;\n /** Duration for characters without glyph data (seconds). Default: `0.2` */\n unknownDuration?: number;\n}\n\nconst DEFAULTS: Required<TimelineConfig> = {\n glyphGap: 0.1,\n wordGap: 0.15,\n lineGap: 0.3,\n unknownDuration: 0.2,\n};\n\nexport interface TimelineEntry {\n char: string;\n offset: number;\n duration: number;\n hasGlyph: boolean;\n}\n\nexport interface Timeline {\n entries: TimelineEntry[];\n totalDuration: number;\n}\n\nexport function computeTimeline(text: string, font: TegakiBundle, config?: TimelineConfig): Timeline {\n const glyphGap = config?.glyphGap ?? DEFAULTS.glyphGap;\n const wordGap = config?.wordGap ?? DEFAULTS.wordGap;\n const lineGap = config?.lineGap ?? DEFAULTS.lineGap;\n const unknownDuration = config?.unknownDuration ?? DEFAULTS.unknownDuration;\n\n const chars = graphemes(text);\n const entries: TimelineEntry[] = [];\n let offset = 0;\n for (const char of chars) {\n const glyph = font.glyphData[char];\n const hasGlyph = !!glyph;\n const duration = hasGlyph ? (glyph.t ?? 1) : unknownDuration;\n entries.push({ char, offset, duration, hasGlyph });\n offset += duration;\n\n // Gap after this character\n if (char === '\\n') {\n offset += lineGap;\n } else if (char === ' ') {\n offset += wordGap;\n } else {\n offset += glyphGap;\n }\n }\n // Remove trailing gap\n if (entries.length > 0) {\n const lastChar = chars[chars.length - 1]!;\n const trailingGap = lastChar === '\\n' ? lineGap : lastChar === ' ' ? wordGap : glyphGap;\n offset -= trailingGap;\n }\n return { entries, totalDuration: Math.max(0, offset) };\n}\n","'use client';\n\nimport { type ComponentProps, type Ref, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { TegakiBundle, TegakiEffects } from '../types.ts';\nimport { drawFallbackGlyph } from './drawFallbackGlyph.ts';\nimport { drawGlyph } from './drawGlyph.ts';\nimport { resolveEffects } from './effects.ts';\nimport { computeTextLayout } from './textLayout.ts';\nimport type { TimelineConfig, TimelineEntry } from './timeline.ts';\nimport { computeTimeline } from './timeline.ts';\nimport type { Coercible } from './utils.ts';\nimport { coerceToString, graphemes } from './utils.ts';\n\nconst PADDING_H_EM = 0.2;\nconst MIN_LINE_HEIGHT_EM = 1.8;\nconst MIN_PADDING_V_EM = 0.2;\n\n// --- CSS custom property names ---\n\nconst CSS_TIME = '--tegaki-time';\nconst CSS_PROGRESS = '--tegaki-progress';\nconst CSS_DURATION = '--tegaki-duration';\n\n// Register custom properties so they are animatable (typed as <number>).\n// Deferred to first mount to avoid running at import time during SSR.\nlet cssPropertiesRegistered = false;\nfunction registerCssProperties() {\n if (cssPropertiesRegistered) return;\n cssPropertiesRegistered = true;\n if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {\n for (const prop of [CSS_TIME, CSS_PROGRESS, CSS_DURATION]) {\n try {\n CSS.registerProperty({ name: prop, syntax: '<number>', inherits: true, initialValue: '0' });\n } catch {\n // Already registered — ignore.\n }\n }\n }\n}\n\nexport type TimeControlMode = {\n controlled: {\n mode: 'controlled';\n /** Current time in seconds. */\n value: number;\n };\n uncontrolled: {\n mode: 'uncontrolled';\n /** Initial time in seconds. Default: `0` */\n initialTime?: number;\n /** Playback speed multiplier. Default: `1` */\n speed?: number;\n /** Whether animation is playing. Default: `true` */\n playing?: boolean;\n /** Loop animation when it reaches the end. Default: `false` */\n loop?: boolean;\n /**\n * Catch-up strength. When positive, playback speeds up when there is a\n * large amount of remaining animation and decays back to normal gradually.\n * `0` disables catch-up (default). Higher values ramp up more aggressively.\n * Typical range: `0.2` – `2`.\n */\n catchUp?: number;\n /** Called on every frame with the current time. */\n onTimeChange?: (time: number) => void;\n };\n css: {\n mode: 'css';\n };\n};\n\n/**\n * A plain number is shorthand for `{ mode: 'controlled', value: number }`.\n * `'css'` is shorthand for `{ mode: 'css' }`.\n * Omit for uncontrolled mode with default settings.\n */\nexport type TimeControlProp = null | undefined | number | 'css' | TimeControlMode[keyof TimeControlMode];\n\n/** Imperative handle exposed via the `ref` prop. */\nexport interface TegakiRendererHandle {\n /** The root DOM element. */\n getElement(): HTMLDivElement | null;\n /** Current animation time in seconds. */\n getCurrentTime(): number;\n /** Total timeline duration in seconds. */\n getDuration(): number;\n /** Whether the animation is currently playing (uncontrolled mode only). */\n getIsPlaying(): boolean;\n /** Whether the animation has reached the end. */\n getIsComplete(): boolean;\n /** Resume playback (uncontrolled mode only). No-op in controlled/css mode. */\n play(): void;\n /** Pause playback (uncontrolled mode only). No-op in controlled/css mode. */\n pause(): void;\n /** Jump to a specific time in seconds (uncontrolled mode only). No-op in controlled/css mode. */\n seek(time: number): void;\n /** Seek to 0 and play (uncontrolled mode only). No-op in controlled/css mode. */\n restart(): void;\n}\n\nexport interface TegakiRendererProps<E extends TegakiEffects<E> = Record<string, never>>\n extends Omit<ComponentProps<'div'>, 'children' | 'ref'> {\n /** Imperative handle ref for playback controls and DOM access. */\n ref?: Ref<TegakiRendererHandle>;\n\n /** TegakiBundle with font data and animated glyph SVGs. */\n font?: TegakiBundle;\n\n /** Text to animate. Takes precedence over children. */\n text?: string;\n\n /** Children coerced to string. Strings and numbers are kept; everything else is ignored. */\n children?: Coercible;\n\n /**\n * Time control. Accepts a number (controlled shorthand), or an object\n * specifying the mode (`'controlled'`, `'uncontrolled'`, or `'css'`).\n * Omit for uncontrolled playback with default settings.\n */\n time?: TimeControlProp;\n\n /** Called once when the animation reaches the end of the timeline. */\n onComplete?: () => void;\n\n /** Visual effects applied during canvas rendering. */\n effects?: E;\n\n /** Maximum segment size in pixels for effect subdivision. Lower values produce\n * smoother effects but cost more to render. Default: `2` */\n segmentSize?: number;\n\n /** Timeline timing configuration (gap between glyphs, words, lines, etc.). */\n timing?: TimelineConfig;\n\n /** Show debug text overlay. */\n showOverlay?: boolean;\n}\n\n// --- Component ---\n\nexport function TegakiRenderer<const E extends TegakiEffects<E> = Record<string, never>>({\n ref,\n font,\n text,\n children,\n time: timeProp,\n onComplete,\n effects,\n segmentSize,\n timing,\n showOverlay,\n ...props\n}: TegakiRendererProps<E>) {\n registerCssProperties();\n\n const resolvedText = text ?? coerceToString(children);\n\n // --- Resolve effects ---\n const resolvedEffects = useMemo(() => resolveEffects(effects as Record<string, any>), [effects]);\n const [seed] = useState(() => Math.random() * 1000);\n\n // --- Resolve time control ---\n const timeControl: TimeControlMode[keyof TimeControlMode] =\n timeProp == null\n ? { mode: 'uncontrolled' }\n : typeof timeProp === 'number'\n ? { mode: 'controlled', value: timeProp }\n : timeProp === 'css'\n ? { mode: 'css' }\n : timeProp;\n\n const isCss = timeControl.mode === 'css';\n const isControlled = timeControl.mode === 'controlled' || isCss;\n const controlledTime = timeControl.mode === 'controlled' ? timeControl.value : undefined;\n const defaultTime = timeControl.mode === 'uncontrolled' ? (timeControl.initialTime ?? 0) : 0;\n const speed = timeControl.mode === 'uncontrolled' ? (timeControl.speed ?? 1) : 1;\n const propPlaying = timeControl.mode === 'uncontrolled' ? (timeControl.playing ?? true) : false;\n const loop = timeControl.mode === 'uncontrolled' ? (timeControl.loop ?? false) : false;\n const catchUp = timeControl.mode === 'uncontrolled' ? (timeControl.catchUp ?? 0) : 0;\n const onTimeChange = timeControl.mode === 'uncontrolled' ? timeControl.onTimeChange : undefined;\n\n // Imperative playing override (undefined = follow prop)\n const [playingOverride, setPlayingOverride] = useState<boolean | undefined>(undefined);\n const playing = playingOverride ?? propPlaying;\n\n // Reset override when the prop changes so the prop regains control\n const prevPropPlaying = useRef(propPlaying);\n if (prevPropPlaying.current !== propPlaying) {\n prevPropPlaying.current = propPlaying;\n setPlayingOverride(undefined);\n }\n\n // --- Internal time (uncontrolled mode) ---\n const [internalTime, setInternalTime] = useState(defaultTime);\n // --- CSS-driven time ---\n const [cssTime, setCssTime] = useState(0);\n const currentTime = isCss ? cssTime : isControlled ? controlledTime! : internalTime;\n\n // Stable refs so the imperative handle and rAF loop always see latest values\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n const playingRef = useRef(playing);\n playingRef.current = playing;\n const isControlledRef = useRef(isControlled);\n isControlledRef.current = isControlled;\n const onTimeChangeRef = useRef(onTimeChange);\n onTimeChangeRef.current = onTimeChange;\n const onCompleteRef = useRef(onComplete);\n onCompleteRef.current = onComplete;\n\n // --- Font loading ---\n const [fontReady, setFontReady] = useState(\n () => typeof document !== 'undefined' && !!font && document.fonts.check(`16px \"${font.family}\"`),\n );\n useEffect(() => {\n if (!font) {\n setFontReady(false);\n return;\n }\n // Check if the font is already loaded\n if (document.fonts.check(`16px \"${font.family}\"`)) {\n setFontReady(true);\n return;\n }\n // New font — mark not ready and start loading\n setFontReady(false);\n let cancelled = false;\n font.registerFontFace().then(() => {\n if (!cancelled) setFontReady(true);\n });\n return () => {\n cancelled = true;\n };\n }, [font]);\n\n // --- Font-derived constants ---\n const fontFamily = font?.family;\n const emHeight = font ? (font.ascender - font.descender) / font.unitsPerEm : 0;\n\n // --- Container measurement ---\n const rootRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState(0);\n const [fontSize, setFontSize] = useState(0);\n const [lineHeight, setLineHeight] = useState(0);\n const [currentColor, setCurrentColor] = useState('');\n\n // --- Timeline ---\n const timeline = useMemo(\n () => (font && resolvedText ? computeTimeline(resolvedText, font, timing) : { entries: [] as TimelineEntry[], totalDuration: 0 }),\n [resolvedText, font, timing],\n );\n\n // Duration ref so the rAF loop always sees the latest value without restarting\n const totalDurationRef = useRef(timeline.totalDuration);\n totalDurationRef.current = timeline.totalDuration;\n\n // Smoothed catch-up boost (raw bonus on top of base speed; attack/release smoothed)\n const smoothedBoostRef = useRef(0);\n\n // --- Completion tracking ---\n const prevCompletedRef = useRef(false);\n const isComplete = timeline.totalDuration > 0 && currentTime >= timeline.totalDuration;\n\n useEffect(() => {\n if (isComplete && !prevCompletedRef.current) {\n prevCompletedRef.current = true;\n onCompleteRef.current?.();\n } else if (!isComplete) {\n prevCompletedRef.current = false;\n }\n });\n\n // --- Imperative handle (stable — reads from refs) ---\n useImperativeHandle(\n ref,\n () => ({\n getElement: () => rootRef.current,\n getCurrentTime: () => currentTimeRef.current,\n getDuration: () => totalDurationRef.current,\n getIsPlaying: () => playingRef.current,\n getIsComplete: () => totalDurationRef.current > 0 && currentTimeRef.current >= totalDurationRef.current,\n play: () => {\n if (!isControlledRef.current) setPlayingOverride(true);\n },\n pause: () => {\n if (!isControlledRef.current) setPlayingOverride(false);\n },\n seek: (time: number) => {\n if (!isControlledRef.current) setInternalTime(Math.max(0, Math.min(time, totalDurationRef.current)));\n },\n restart: () => {\n if (!isControlledRef.current) {\n setInternalTime(0);\n setPlayingOverride(true);\n }\n },\n }),\n [],\n );\n\n // --- Uncontrolled: time change notification ---\n useEffect(() => {\n if (!isControlled) {\n onTimeChangeRef.current?.(internalTime);\n }\n }, [internalTime, isControlled]);\n\n // --- Reduced motion preference ---\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(\n () => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches,\n );\n useEffect(() => {\n const mql = window.matchMedia('(prefers-reduced-motion: reduce)');\n setPrefersReducedMotion(mql.matches);\n const onChange = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);\n mql.addEventListener('change', onChange);\n return () => mql.removeEventListener('change', onChange);\n }, []);\n\n // When reduced motion is preferred, skip to end of timeline\n useEffect(() => {\n if (prefersReducedMotion && !isControlled && timeline.totalDuration > 0) {\n setInternalTime(timeline.totalDuration);\n }\n }, [prefersReducedMotion, isControlled, timeline.totalDuration]);\n\n // --- Uncontrolled: rAF playback loop ---\n useEffect(() => {\n if (isControlled || !playing || !font || !fontReady || prefersReducedMotion) return;\n\n // Reset smoothed boost when the loop restarts\n smoothedBoostRef.current = 0;\n\n let lastTs: number | null = null;\n let raf: number;\n\n // Catch-up smoothing rates (per-second exponential factors)\n const attackRate = 4; // fast ramp-up\n const releaseRate = loop ? 30 : 2; // slow decay back to base\n\n const tick = (ts: number) => {\n if (lastTs === null) lastTs = ts;\n const dtSec = (ts - lastTs) / 1000;\n lastTs = ts;\n\n setInternalTime((prev: number) => {\n const totalDur = totalDurationRef.current;\n if (totalDur === 0 || (!loop && prev >= totalDur)) return totalDur;\n\n // Compute effective speed with catch-up\n let effectiveSpeed = speed;\n if (catchUp > 0) {\n const remaining = Math.max(0, totalDur - prev);\n const excess = Math.max(0, remaining - 2);\n const targetBoost = catchUp * excess;\n const rate = targetBoost > smoothedBoostRef.current ? attackRate : releaseRate;\n smoothedBoostRef.current += (targetBoost - smoothedBoostRef.current) * (1 - Math.exp(-rate * dtSec));\n effectiveSpeed = speed + smoothedBoostRef.current;\n }\n\n let next = prev + dtSec * effectiveSpeed;\n if (next >= totalDur) {\n next = loop ? next % totalDur : totalDur;\n smoothedBoostRef.current = 0; // reset boost on loop\n }\n return next;\n });\n\n raf = requestAnimationFrame(tick);\n };\n\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [isControlled, playing, speed, loop, catchUp, font, fontReady, prefersReducedMotion]);\n\n // --- Container size observation ---\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const ro = new ResizeObserver(([entry]) => {\n if (entry) {\n setContainerWidth(entry.contentRect.width);\n const styles = getComputedStyle(el);\n setFontSize(Number.parseFloat(styles.fontSize));\n setLineHeight(Number.parseFloat(styles.lineHeight));\n setCurrentColor(styles.color);\n }\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n // Sentinel element ref — a hidden child with `font-size: inherit` and a near-zero\n // CSS transition. When any ancestor changes font-size, the transition fires an event\n // so we can read the new value without polling getComputedStyle every render.\n const sentinelRef = useRef<HTMLSpanElement>(null);\n useEffect(() => {\n const el = sentinelRef.current;\n if (!el) return;\n const onTransition = (e: TransitionEvent) => {\n const styles = getComputedStyle(el);\n if (e.propertyName === 'font-size' || e.propertyName === 'line-height') {\n setFontSize(Number.parseFloat(styles.fontSize));\n setLineHeight(Number.parseFloat(styles.lineHeight));\n }\n if (e.propertyName === 'color') {\n setCurrentColor(styles.color);\n }\n if (e.propertyName === CSS_PROGRESS) {\n const rawProgress = Number(styles.getPropertyValue(CSS_PROGRESS));\n setCssTime(rawProgress * totalDurationRef.current);\n }\n };\n el.addEventListener('transitionend', onTransition);\n return () => el.removeEventListener('transitionend', onTransition);\n }, []);\n\n // --- Text layout ---\n const layout = useMemo(() => {\n if (!fontReady || !fontFamily || !fontSize || !containerWidth || !resolvedText) return null;\n return computeTextLayout(resolvedText, fontFamily, fontSize, lineHeight, containerWidth);\n }, [fontReady, resolvedText, fontFamily, fontSize, lineHeight, containerWidth]);\n\n // --- Canvas padding ---\n const padH = PADDING_H_EM * fontSize;\n const padV = fontSize ? Math.max(MIN_PADDING_V_EM * fontSize, (MIN_LINE_HEIGHT_EM * fontSize - lineHeight) / 2) : 0;\n\n // --- Canvas rendering ---\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n useLayoutEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas || !font?.glyphData || !layout || !fontSize) return;\n\n const dpr = window.devicePixelRatio || 1;\n const el = rootRef.current;\n if (!el) return;\n const canvasRect = canvas.getBoundingClientRect();\n const w = canvasRect.width;\n const h = canvasRect.height;\n\n // Resize canvas backing store if needed\n const needsResize = canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr);\n if (needsResize) {\n canvas.width = Math.round(w * dpr);\n canvas.height = Math.round(h * dpr);\n }\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, w, h);\n ctx.translate(padH, padV);\n\n const color = currentColor || getComputedStyle(el).color;\n\n const emHeightPx = emHeight * fontSize;\n const halfLeading = (lineHeight - emHeightPx) / 2;\n const characters = graphemes(resolvedText);\n\n let y = 0;\n for (const lineIndices of layout.lines) {\n let x = 0;\n for (const charIdx of lineIndices) {\n const char = characters[charIdx]!;\n if (char === '\\n') continue;\n const entry = timeline.entries[charIdx]!;\n const charWidth = layout.charWidths[charIdx] ?? 0;\n const kerning = layout.kernings[charIdx] ?? 0;\n const glyph = font.glyphData[char];\n\n if (glyph && entry.hasGlyph) {\n const localTime = Math.max(0, Math.min(currentTime - entry.offset, entry.duration));\n const glyphY = y + halfLeading;\n drawGlyph(\n ctx,\n glyph,\n {\n x,\n y: glyphY,\n fontSize,\n unitsPerEm: font.unitsPerEm,\n ascender: font.ascender,\n descender: font.descender,\n },\n localTime,\n font.lineCap,\n color,\n resolvedEffects,\n seed + charIdx,\n segmentSize,\n );\n } else if (!entry.hasGlyph && currentTime >= entry.offset + entry.duration) {\n const baseline = y + halfLeading + (font.ascender / font.unitsPerEm) * fontSize;\n drawFallbackGlyph(ctx, char, x, baseline, fontSize, fontFamily!, color, resolvedEffects, seed + charIdx);\n }\n\n x += (charWidth + kerning) * fontSize;\n }\n y += lineHeight;\n }\n }, [\n currentTime,\n timeline,\n layout,\n font,\n fontFamily,\n fontSize,\n lineHeight,\n resolvedText,\n emHeight,\n padH,\n padV,\n currentColor,\n resolvedEffects,\n seed,\n segmentSize,\n ]);\n\n // --- Rendering ---\n\n return (\n <div\n ref={rootRef}\n {...props}\n style={{\n ...props.style,\n position: 'relative',\n maxWidth: '100%',\n width: 'auto',\n height: 'auto',\n ...{\n [CSS_DURATION]: timeline.totalDuration,\n [CSS_TIME]: currentTime,\n [CSS_PROGRESS]: timeline.totalDuration > 0 ? currentTime / timeline.totalDuration : 0,\n },\n }}\n >\n <div style={{ position: 'relative' }}>\n {/* Sentinel: inherits font-size & line-height; its height changes when either changes */}\n <span\n ref={sentinelRef}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n width: 0,\n overflow: 'hidden',\n pointerEvents: 'none',\n fontSize: 'inherit',\n lineHeight: 'inherit',\n visibility: 'hidden',\n transition: isCss\n ? `font-size 0.001s, line-height 0.001s, color 0.001s, ${CSS_PROGRESS} 0.001s`\n : 'font-size 0.001s, line-height 0.001s, color 0.001s',\n }}\n >\n {'\\u00A0'}\n </span>\n <canvas\n ref={canvasRef}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: `${-padV}px ${-padH}px`,\n width: `calc(100% + ${padH * 2}px)`,\n height: `calc(100% + ${padV * 2}px)`,\n pointerEvents: 'none',\n }}\n />\n\n <div\n style={{\n userSelect: 'auto',\n whiteSpace: 'pre-wrap',\n overflowWrap: 'break-word',\n paddingRight: 1,\n WebkitTextFillColor: showOverlay ? undefined : 'transparent',\n fontFamily,\n color: showOverlay ? 'rgba(255, 0, 0, 0.4)' : undefined,\n }}\n >\n {resolvedText}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/effects.ts","../src/lib/utils.ts","../src/lib/drawGlyph.ts","../src/lib/drawFallbackGlyph.ts","../src/lib/textLayout.ts","../src/lib/timeline.ts","../src/lib/TegakiRenderer.tsx"],"sourcesContent":["import type { TegakiEffectConfigs, TegakiEffectName } from '../types.ts';\n\nexport interface ResolvedEffect<K extends TegakiEffectName = TegakiEffectName> {\n effect: K;\n order: number;\n config: TegakiEffectConfigs[K];\n}\n\nconst defaultEffects: Record<string, any> = { pressureWidth: true };\nconst knownEffects: Set<string> = new Set(['glow', 'wobble', 'pressureWidth', 'taper', 'gradient']);\n\n/**\n * Normalizes an effects record into a sorted array of resolved effects.\n * Known keys infer the effect name; custom keys read it from the `effect` field.\n * Boolean `true` becomes an empty config. `false`/absent entries are skipped.\n */\nexport function resolveEffects(effects: Record<string, any> | undefined): ResolvedEffect[] {\n const merged = { ...defaultEffects, ...effects };\n\n const result: ResolvedEffect[] = [];\n\n for (const [key, value] of Object.entries(merged)) {\n if (value === false || value == null) continue;\n\n let effectName: TegakiEffectName;\n let config: Record<string, any>;\n let order: number;\n\n if (value === true) {\n effectName = (knownEffects.has(key) ? key : undefined) as TegakiEffectName;\n if (!effectName) continue;\n config = {};\n order = 0;\n } else {\n if (value.enabled === false) continue;\n effectName = value.effect ?? (knownEffects.has(key) ? key : undefined);\n if (!effectName) continue;\n const { effect: _, order: o, enabled: __, ...rest } = value;\n config = rest;\n order = o ?? 0;\n }\n\n result.push({ effect: effectName, order, config });\n }\n\n result.sort((a, b) => a.order - b.order);\n return result;\n}\n\n/** Check if a specific effect is active. */\nexport function findEffect<K extends TegakiEffectName>(effects: ResolvedEffect[], name: K): ResolvedEffect<K> | undefined {\n return effects.find((e) => e.effect === name) as ResolvedEffect<K> | undefined;\n}\n\n/** Get all instances of a specific effect (for duplicates). */\nexport function findEffects<K extends TegakiEffectName>(effects: ResolvedEffect[], name: K): ResolvedEffect<K>[] {\n return effects.filter((e) => e.effect === name) as ResolvedEffect<K>[];\n}\n","import type { CSSLength } from '../types.ts';\n\nconst segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n\n/** Resolve a CSSLength to pixels. Plain numbers are px, `\"Nem\"` is N * fontSize. */\nexport function resolveCSSLength(value: CSSLength, fontSize: number): number {\n if (typeof value === 'number') return value;\n return parseFloat(value) * fontSize;\n}\n\nexport function graphemes(text: string): string[] {\n return Array.from(segmenter.segment(text), (s) => s.segment);\n}\nexport type Coercible = string | number | boolean | null | undefined | readonly Coercible[];\n\nexport function coerceToString(value: unknown): string {\n if (value == null || typeof value === 'boolean') return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'bigint') return String(value);\n if (Array.isArray(value)) return value.map(coerceToString).join('');\n return '';\n}\n","import type { LineCap, TegakiGlyphData } from '../types.ts';\nimport { findEffect, findEffects, type ResolvedEffect } from './effects.ts';\nimport { resolveCSSLength } from './utils.ts';\n\ninterface GlyphPosition {\n /** X offset in CSS pixels */\n x: number;\n /** Y offset in CSS pixels (top of em square) */\n y: number;\n /** Font size in CSS pixels */\n fontSize: number;\n /** Units per em from the font */\n unitsPerEm: number;\n /** Font ascender in font units */\n ascender: number;\n /** Font descender in font units (negative) */\n descender: number;\n}\n\n// --- Color helpers ---\n\nfunction parseColor(color: string): [number, number, number, number] {\n const h = color.replace('#', '');\n if (h.length === 3) {\n return [parseInt(h[0]! + h[0]!, 16), parseInt(h[1]! + h[1]!, 16), parseInt(h[2]! + h[2]!, 16), 1];\n }\n if (h.length === 4) {\n return [parseInt(h[0]! + h[0]!, 16), parseInt(h[1]! + h[1]!, 16), parseInt(h[2]! + h[2]!, 16), parseInt(h[3]! + h[3]!, 16) / 255];\n }\n if (h.length === 8) {\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), parseInt(h.slice(6, 8), 16) / 255];\n }\n return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), 1];\n}\n\nfunction lerpColor(a: [number, number, number, number], b: [number, number, number, number], t: number): string {\n const r = Math.round(a[0] + (b[0] - a[0]) * t);\n const g = Math.round(a[1] + (b[1] - a[1]) * t);\n const bl = Math.round(a[2] + (b[2] - a[2]) * t);\n const al = a[3] + (b[3] - a[3]) * t;\n if (al >= 1) return `rgb(${r},${g},${bl})`;\n return `rgba(${r},${g},${bl},${al.toFixed(3)})`;\n}\n\nfunction gradientColor(progress: number, colors: string[], seed: number): string {\n if (colors.length === 0) return '#000';\n if (colors.length === 1) return colors[0]!;\n const t = (((progress + seed * 0.1) % 1) + 1) % 1;\n const scaledT = t * (colors.length - 1);\n const i = Math.min(Math.floor(scaledT), colors.length - 2);\n const frac = scaledT - i;\n return lerpColor(parseColor(colors[i]!), parseColor(colors[i + 1]!), frac);\n}\n\nfunction rainbowColor(progress: number, saturation: number, lightness: number, seed: number): string {\n const hue = (progress * 360 + seed * 137.5) % 360;\n return `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n}\n\n// --- Noise helper for wobble ---\n\nfunction hash(x: number): number {\n let h = (x * 2654435761) | 0;\n h = ((h >>> 16) ^ h) * 0x45d9f3b;\n h = ((h >>> 16) ^ h) * 0x45d9f3b;\n h = (h >>> 16) ^ h;\n return (h & 0x7fffffff) / 0x7fffffff; // 0-1\n}\n\nfunction noise1d(x: number, seed: number): number {\n const i = Math.floor(x);\n const f = x - i;\n const t = f * f * (3 - 2 * f); // smoothstep\n return hash(i + seed * 7919) * (1 - t) + hash(i + 1 + seed * 7919) * t;\n}\n\n/**\n * Draw a single glyph's strokes onto a canvas context, animated up to `localTime`.\n * `localTime` is seconds relative to this glyph's start (0 = glyph begins).\n */\nexport function drawGlyph(\n ctx: CanvasRenderingContext2D,\n glyph: TegakiGlyphData,\n pos: GlyphPosition,\n localTime: number,\n lineCap: LineCap,\n color: string,\n effects: ResolvedEffect[] = [],\n seed = 0,\n segmentSize?: number,\n) {\n const scale = pos.fontSize / pos.unitsPerEm;\n const ox = pos.x;\n const oy = pos.y;\n\n const glowEffects = findEffects(effects, 'glow');\n const wobbleEffect = findEffect(effects, 'wobble');\n const pressureEffect = findEffect(effects, 'pressureWidth');\n const taperEffect = findEffect(effects, 'taper');\n const gradientEffect = findEffect(effects, 'gradient');\n\n // Pressure params (0 = uniform avg width, 1 = fully per-point width)\n const pressureAmount = pressureEffect ? Math.max(0, Math.min(pressureEffect.config.strength ?? 1, 1)) : 0;\n\n // Wobble params\n const wobbleAmplitude = wobbleEffect ? (wobbleEffect.config.amplitude ?? 1.5) : 0;\n const wobbleFrequency = wobbleEffect ? (wobbleEffect.config.frequency ?? 8) : 0;\n const wobbleMode = wobbleEffect?.config.mode ?? 'sine';\n\n // Taper params\n const taperStart = taperEffect ? Math.max(0, Math.min(taperEffect.config.startLength ?? 0.15, 1)) : 0;\n const taperEnd = taperEffect ? Math.max(0, Math.min(taperEffect.config.endLength ?? 0.15, 1)) : 0;\n\n // Gradient params\n const gradientColors = gradientEffect?.config.colors;\n const isRainbow = gradientColors === 'rainbow';\n const gradientColorStops = Array.isArray(gradientColors) ? gradientColors : undefined;\n const gradientSaturation = gradientEffect?.config.saturation ?? 80;\n const gradientLightness = gradientEffect?.config.lightness ?? 55;\n\n // Helper: apply wobble offset to a point in font units\n const wobbleX = (x: number, y: number, idx: number) => {\n if (!wobbleEffect) return x;\n if (wobbleMode === 'noise') {\n return x + wobbleAmplitude * (noise1d(y * 0.1 + idx * 0.7, seed) * 2 - 1);\n }\n return x + wobbleAmplitude * Math.sin(wobbleFrequency * (y * 0.01 + idx * 0.7) + seed);\n };\n const wobbleY = (x: number, y: number, idx: number) => {\n if (!wobbleEffect) return y;\n if (wobbleMode === 'noise') {\n return y + wobbleAmplitude * (noise1d(x * 0.1 + idx * 0.5, seed * 1.3 + 1000) * 2 - 1);\n }\n return y + wobbleAmplitude * Math.cos(wobbleFrequency * (x * 0.01 + idx * 0.5) + seed * 1.3);\n };\n\n // Helper: convert font-unit point to pixel\n const px = (x: number) => ox + x * scale;\n const py = (y: number) => oy + (y + pos.ascender) * scale;\n\n // Helper: get color for a given stroke progress\n const colorAt = (progress: number): string => {\n if (isRainbow) return rainbowColor(progress, gradientSaturation, gradientLightness, seed);\n if (gradientColorStops) return gradientColor(progress, gradientColorStops, seed);\n return color;\n };\n const hasGradient = !!gradientEffect;\n\n // Helper: taper multiplier (0-1) for a given stroke progress\n const taperMultiplier = (progress: number): number => {\n let m = 1;\n if (taperStart > 0 && progress < taperStart) m = Math.min(m, progress / taperStart);\n if (taperEnd > 0 && progress > 1 - taperEnd) m = Math.min(m, (1 - progress) / taperEnd);\n return m;\n };\n\n for (const stroke of glyph.s) {\n if (localTime < stroke.d) continue;\n const elapsed = localTime - stroke.d;\n const progress = Math.min(elapsed / stroke.a, 1);\n\n const pts = stroke.p;\n if (pts.length === 0) continue;\n\n const avgWidth = pts.reduce((s, p) => s + p[2], 0) / pts.length;\n const baseLineWidth = Math.max(avgWidth, 0.5) * scale;\n\n // --- Single-point dot ---\n if (pts.length === 1) {\n if (progress <= 0) continue;\n const p = pts[0]!;\n const dotX = px(wobbleX(p[0], p[1], 0));\n const dotY = py(wobbleY(p[0], p[1], 0));\n const perPointDot = Math.max(p[2], 0.5) * scale;\n let dotWidth = baseLineWidth + (perPointDot - baseLineWidth) * pressureAmount;\n dotWidth *= taperMultiplier(0.5);\n\n // Glow passes for dots\n for (const glow of glowEffects) {\n ctx.save();\n ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, pos.fontSize);\n ctx.shadowColor = glow.config.color ?? color;\n ctx.shadowOffsetX = (glow.config.offsetX ?? 0) * scale;\n ctx.shadowOffsetY = (glow.config.offsetY ?? 0) * scale;\n ctx.fillStyle = glow.config.color ?? color;\n ctx.beginPath();\n if (lineCap === 'round') {\n ctx.arc(dotX, dotY, dotWidth / 2, 0, Math.PI * 2);\n } else {\n ctx.rect(dotX - dotWidth / 2, dotY - dotWidth / 2, dotWidth, dotWidth);\n }\n ctx.fill();\n ctx.restore();\n }\n\n // Main dot\n ctx.fillStyle = colorAt(0);\n ctx.beginPath();\n if (lineCap === 'round') {\n ctx.arc(dotX, dotY, dotWidth / 2, 0, Math.PI * 2);\n ctx.fill();\n } else {\n ctx.fillRect(dotX - dotWidth / 2, dotY - dotWidth / 2, dotWidth, dotWidth);\n }\n continue;\n }\n\n // --- Compute total path length ---\n let totalLen = 0;\n for (let j = 1; j < pts.length; j++) {\n const dx = pts[j]![0] - pts[j - 1]![0];\n const dy = pts[j]![1] - pts[j - 1]![1];\n totalLen += Math.sqrt(dx * dx + dy * dy);\n }\n\n const drawLen = totalLen * progress;\n if (drawLen <= 0) continue;\n\n // --- Collect drawable segments ---\n const segments: {\n x0: number;\n y0: number;\n x1: number;\n y1: number;\n width0: number;\n width1: number;\n segProgress: number;\n }[] = [];\n\n let accumulated = 0;\n for (let j = 1; j < pts.length; j++) {\n const prev = pts[j - 1]!;\n const cur = pts[j]!;\n const dx = cur[0] - prev[0];\n const dy = cur[1] - prev[1];\n const segLen = Math.sqrt(dx * dx + dy * dy);\n\n if (accumulated + segLen <= drawLen) {\n segments.push({\n x0: px(wobbleX(prev[0], prev[1], j - 1)),\n y0: py(wobbleY(prev[0], prev[1], j - 1)),\n x1: px(wobbleX(cur[0], cur[1], j)),\n y1: py(wobbleY(cur[0], cur[1], j)),\n width0: prev[2],\n width1: cur[2],\n segProgress: (accumulated + segLen / 2) / totalLen,\n });\n accumulated += segLen;\n } else {\n const remaining = drawLen - accumulated;\n const frac = segLen > 0 ? remaining / segLen : 0;\n const ix = prev[0] + dx * frac;\n const iy = prev[1] + dy * frac;\n const iw = prev[2] + (cur[2] - prev[2]) * frac;\n segments.push({\n x0: px(wobbleX(prev[0], prev[1], j - 1)),\n y0: py(wobbleY(prev[0], prev[1], j - 1)),\n x1: px(wobbleX(ix, iy, j)),\n y1: py(wobbleY(ix, iy, j)),\n width0: prev[2],\n width1: iw,\n segProgress: (accumulated + remaining / 2) / totalLen,\n });\n break;\n }\n }\n\n if (segments.length === 0) continue;\n\n // Keep coarse segments for glow (shadowBlur is expensive per draw call)\n const coarseSegments = segments.slice();\n\n // --- Subdivide long segments for smooth effect transitions ---\n const effectsNeedSubdivision = pressureAmount > 0 || hasGradient || !!wobbleEffect || !!taperEffect;\n const resolvedSegmentSize = segmentSize ?? (effectsNeedSubdivision ? 2 : undefined);\n if (resolvedSegmentSize != null) {\n const maxSegLen = resolvedSegmentSize * scale;\n const subdivided: typeof segments = [];\n for (const seg of segments) {\n const dx = seg.x1 - seg.x0;\n const dy = seg.y1 - seg.y0;\n const len = Math.sqrt(dx * dx + dy * dy);\n const count = Math.max(1, Math.ceil(len / maxSegLen));\n for (let k = 0; k < count; k++) {\n const t0 = k / count;\n const t1 = (k + 1) / count;\n subdivided.push({\n x0: seg.x0 + dx * t0,\n y0: seg.y0 + dy * t0,\n x1: seg.x0 + dx * t1,\n y1: seg.y0 + dy * t1,\n width0: seg.width0 + (seg.width1 - seg.width0) * t0,\n width1: seg.width0 + (seg.width1 - seg.width0) * t1,\n segProgress: seg.segProgress,\n });\n }\n }\n for (let k = 0; k < subdivided.length; k++) {\n subdivided[k]!.segProgress = subdivided.length > 1 ? k / (subdivided.length - 1) : 0;\n }\n segments.length = 0;\n segments.push(...subdivided);\n }\n\n // Helper: compute segment line width with pressure and taper\n const segWidth = (seg: (typeof segments)[0]) => {\n const perPoint = ((seg.width0 + seg.width1) / 2) * scale;\n const w = Math.max(baseLineWidth + (perPoint - baseLineWidth) * pressureAmount, 0.5 * scale);\n return w * taperMultiplier(seg.segProgress);\n };\n\n const needsPerSegment = pressureAmount > 0 || taperEffect;\n\n const drawStrokePath = () => {\n if (needsPerSegment) {\n for (const seg of segments) {\n ctx.lineWidth = segWidth(seg);\n ctx.beginPath();\n ctx.moveTo(seg.x0, seg.y0);\n ctx.lineTo(seg.x1, seg.y1);\n ctx.stroke();\n }\n } else {\n ctx.lineWidth = baseLineWidth;\n ctx.beginPath();\n ctx.moveTo(segments[0]!.x0, segments[0]!.y0);\n for (const seg of segments) {\n ctx.lineTo(seg.x1, seg.y1);\n }\n ctx.stroke();\n }\n };\n\n const drawGradientPath = () => {\n for (const seg of segments) {\n ctx.strokeStyle = colorAt(seg.segProgress);\n if (needsPerSegment) ctx.lineWidth = segWidth(seg);\n ctx.beginPath();\n ctx.moveTo(seg.x0, seg.y0);\n ctx.lineTo(seg.x1, seg.y1);\n ctx.stroke();\n }\n };\n\n ctx.lineCap = lineCap;\n ctx.lineJoin = 'round';\n\n // --- Glow passes (use coarse segments to avoid expensive per-subsegment shadowBlur) ---\n for (const glow of glowEffects) {\n ctx.save();\n ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, pos.fontSize);\n ctx.shadowColor = glow.config.color ?? color;\n ctx.shadowOffsetX = (glow.config.offsetX ?? 0) * scale;\n ctx.shadowOffsetY = (glow.config.offsetY ?? 0) * scale;\n ctx.strokeStyle = glow.config.color ?? color;\n ctx.lineWidth = baseLineWidth;\n ctx.beginPath();\n ctx.moveTo(coarseSegments[0]!.x0, coarseSegments[0]!.y0);\n for (const seg of coarseSegments) {\n ctx.lineTo(seg.x1, seg.y1);\n }\n ctx.stroke();\n ctx.restore();\n }\n\n // --- Main stroke ---\n if (hasGradient) {\n drawGradientPath();\n } else {\n ctx.strokeStyle = color;\n drawStrokePath();\n }\n }\n}\n","import { findEffect, findEffects, type ResolvedEffect } from './effects.ts';\nimport { resolveCSSLength } from './utils.ts';\n\n/**\n * Draw a fallback glyph (plain text) with applicable effects (glow, gradient, wobble).\n */\nexport function drawFallbackGlyph(\n ctx: CanvasRenderingContext2D,\n char: string,\n x: number,\n baseline: number,\n fontSize: number,\n fontFamily: string,\n color: string,\n effects: ResolvedEffect[] = [],\n seed = 0,\n) {\n const glowEffects = findEffects(effects, 'glow');\n const wobbleEffect = findEffect(effects, 'wobble');\n const gradientEffect = findEffect(effects, 'gradient');\n\n // Wobble offsets\n let dx = 0;\n let dy = 0;\n if (wobbleEffect) {\n const amplitude = (wobbleEffect.config.amplitude ?? 1.5) * (fontSize / 100);\n const frequency = wobbleEffect.config.frequency ?? 8;\n dx = amplitude * Math.sin(frequency * (baseline * 0.01) + seed);\n dy = amplitude * Math.cos(frequency * (x * 0.01) + seed * 1.3);\n }\n\n const drawX = x + dx;\n const drawY = baseline + dy;\n\n // Gradient / rainbow color\n let fillColor = color;\n if (gradientEffect) {\n const colors = gradientEffect.config.colors;\n if (colors === 'rainbow') {\n const saturation = gradientEffect.config.saturation ?? 80;\n const lightness = gradientEffect.config.lightness ?? 55;\n const hue = (seed * 137.5) % 360;\n fillColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n } else if (Array.isArray(colors) && colors.length > 0) {\n fillColor = colors[Math.floor(seed) % colors.length]!;\n }\n }\n\n ctx.save();\n ctx.font = `${fontSize}px ${fontFamily}`;\n ctx.textBaseline = 'alphabetic';\n\n // Glow passes\n for (const glow of glowEffects) {\n ctx.save();\n ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, fontSize);\n ctx.shadowColor = glow.config.color ?? color;\n ctx.shadowOffsetX = glow.config.offsetX ?? 0;\n ctx.shadowOffsetY = glow.config.offsetY ?? 0;\n ctx.fillStyle = glow.config.color ?? color;\n ctx.fillText(char, drawX, drawY);\n ctx.restore();\n }\n\n // Main text\n ctx.fillStyle = fillColor;\n ctx.fillText(char, drawX, drawY);\n\n ctx.restore();\n}\n","import { layoutWithLines, prepareWithSegments } from '@chenglou/pretext';\nimport { graphemes } from './utils.ts';\n\nexport interface TextLayout {\n /** Character indices per line */\n lines: number[][];\n /** Width in em per character index */\n charWidths: number[];\n /** Kerning adjustment in em between character at index i and i+1 */\n kernings: number[];\n /** Intrinsic (single-line) width in em */\n intrinsicWidth: number;\n}\n\nexport function computeTextLayout(text: string, fontFamily: string, fontSize: number, lineHeight: number, maxWidth: number): TextLayout {\n const fontStr = `${fontSize}px ${fontFamily}`;\n const chars = graphemes(text);\n\n // Measure unique character widths\n const widthCache = new Map<string, number>();\n const charWidths: number[] = [];\n for (const char of chars) {\n let w = widthCache.get(char);\n if (w === undefined) {\n if (char === '\\n') {\n w = 0;\n } else {\n const p = prepareWithSegments(char, fontStr, { whiteSpace: 'pre-wrap' });\n const r = layoutWithLines(p, Infinity, lineHeight);\n w = r.lines.length > 0 ? r.lines[0]!.width / fontSize : 0;\n }\n widthCache.set(char, w);\n }\n charWidths.push(w);\n }\n\n // Compute intrinsic width (single-line, no wrapping)\n const prepared = prepareWithSegments(text, fontStr, { whiteSpace: 'pre-wrap' });\n const singleLineResult = layoutWithLines(prepared, Infinity, lineHeight);\n const intrinsicWidth = Math.max(0, ...singleLineResult.lines.map((l) => l.width)) / fontSize;\n\n // Line breaking at actual available width\n const result = layoutWithLines(prepared, maxWidth, lineHeight);\n\n // Map line texts back to character indices (grapheme-based)\n // Build a mapping from UTF-16 offset to grapheme index\n const utf16ToCodePoint: number[] = [];\n for (let ci = 0; ci < chars.length; ci++) {\n for (let j = 0; j < chars[ci]!.length; j++) {\n utf16ToCodePoint.push(ci);\n }\n }\n\n const lines: number[][] = [];\n let utf16Offset = 0;\n for (const line of result.lines) {\n const indices: number[] = [];\n const seen = new Set<number>();\n for (let i = 0; i < line.text.length; i++) {\n const cpIdx = utf16ToCodePoint[utf16Offset + i]!;\n if (!seen.has(cpIdx)) {\n seen.add(cpIdx);\n indices.push(cpIdx);\n }\n }\n utf16Offset += line.text.length;\n // Consume the newline that caused this line break\n if (utf16Offset < text.length && text[utf16Offset] === '\\n') {\n const cpIdx = utf16ToCodePoint[utf16Offset]!;\n indices.push(cpIdx);\n utf16Offset++;\n }\n lines.push(indices);\n }\n\n // Any remaining characters (shouldn't happen, but safety)\n if (utf16Offset < text.length) {\n const indices: number[] = [];\n const seen = new Set<number>();\n for (let i = utf16Offset; i < text.length; i++) {\n const cpIdx = utf16ToCodePoint[i]!;\n if (!seen.has(cpIdx)) {\n seen.add(cpIdx);\n indices.push(cpIdx);\n }\n }\n lines.push(indices);\n }\n\n // Measure kerning between adjacent character pairs\n const kernings: number[] = [];\n const pairCache = new Map<string, number>();\n for (let i = 0; i < chars.length - 1; i++) {\n const a = chars[i]!;\n const b = chars[i + 1]!;\n if (a === '\\n' || b === '\\n') {\n kernings.push(0);\n continue;\n }\n const pair = `${a}${b}`;\n let k = pairCache.get(pair);\n if (k === undefined) {\n const p = prepareWithSegments(pair, fontStr, { whiteSpace: 'pre-wrap' });\n const r = layoutWithLines(p, Infinity, lineHeight);\n const pairWidth = r.lines.length > 0 ? r.lines[0]!.width / fontSize : 0;\n k = pairWidth - (widthCache.get(a) ?? 0) - (widthCache.get(b) ?? 0);\n if (Math.abs(k) < 0.001) k = 0;\n pairCache.set(pair, k);\n }\n kernings.push(k);\n }\n\n return { lines, charWidths, kernings, intrinsicWidth };\n}\n","import type { TegakiBundle } from '../types.ts';\nimport { graphemes } from './utils.ts';\n\nexport interface TimelineConfig {\n /** Pause between glyphs (seconds). Default: `0.1` */\n glyphGap?: number;\n /** Pause after a space character (seconds). Default: `0.15` */\n wordGap?: number;\n /** Pause after a newline / line break (seconds). Default: `0.3` */\n lineGap?: number;\n /** Duration for characters without glyph data (seconds). Default: `0.2` */\n unknownDuration?: number;\n}\n\nconst DEFAULTS: Required<TimelineConfig> = {\n glyphGap: 0.1,\n wordGap: 0.15,\n lineGap: 0.3,\n unknownDuration: 0.2,\n};\n\nexport interface TimelineEntry {\n char: string;\n offset: number;\n duration: number;\n hasGlyph: boolean;\n}\n\nexport interface Timeline {\n entries: TimelineEntry[];\n totalDuration: number;\n}\n\nexport function computeTimeline(text: string, font: TegakiBundle, config?: TimelineConfig): Timeline {\n const glyphGap = config?.glyphGap ?? DEFAULTS.glyphGap;\n const wordGap = config?.wordGap ?? DEFAULTS.wordGap;\n const lineGap = config?.lineGap ?? DEFAULTS.lineGap;\n const unknownDuration = config?.unknownDuration ?? DEFAULTS.unknownDuration;\n\n const chars = graphemes(text);\n const entries: TimelineEntry[] = [];\n let offset = 0;\n for (const char of chars) {\n const glyph = font.glyphData[char];\n const hasGlyph = !!glyph;\n const duration = hasGlyph ? (glyph.t ?? 1) : unknownDuration;\n entries.push({ char, offset, duration, hasGlyph });\n offset += duration;\n\n // Gap after this character\n if (char === '\\n') {\n offset += lineGap;\n } else if (char === ' ') {\n offset += wordGap;\n } else {\n offset += glyphGap;\n }\n }\n // Remove trailing gap\n if (entries.length > 0) {\n const lastChar = chars[chars.length - 1]!;\n const trailingGap = lastChar === '\\n' ? lineGap : lastChar === ' ' ? wordGap : glyphGap;\n offset -= trailingGap;\n }\n return { entries, totalDuration: Math.max(0, offset) };\n}\n","'use client';\n\nimport { type ComponentProps, type Ref, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { TegakiBundle, TegakiEffects } from '../types.ts';\nimport { drawFallbackGlyph } from './drawFallbackGlyph.ts';\nimport { drawGlyph } from './drawGlyph.ts';\nimport { resolveEffects } from './effects.ts';\nimport { computeTextLayout } from './textLayout.ts';\nimport type { TimelineConfig, TimelineEntry } from './timeline.ts';\nimport { computeTimeline } from './timeline.ts';\nimport type { Coercible } from './utils.ts';\nimport { coerceToString, graphemes } from './utils.ts';\n\nconst fontFaceCache = new Map<string, Promise<void>>();\n\n/**\n * Returns a promise that resolves when the font is ready for text measurement.\n * - Already loaded (by us or externally): resolves immediately.\n * - Currently loading externally: waits for `document.fonts.ready`.\n * - Not registered at all: loads it via the FontFace API.\n * Returns `null` if the font is already loaded synchronously.\n */\n/**\n * Ensures the bundle's font face is loaded and available for rendering.\n * Resolves immediately if the font is already loaded.\n */\nexport async function ensureFontFace(bundle: TegakiBundle): Promise<void> {\n await ensureFont(bundle.family, bundle.fontUrl);\n}\n\nfunction ensureFont(family: string, url: string): Promise<void> | null {\n if (typeof document === 'undefined') return Promise.resolve();\n for (const face of document.fonts) {\n if (face.family === family) {\n if (face.status === 'loaded') return null;\n if (face.status === 'loading') return face.loaded.then(() => {});\n }\n }\n let cached = fontFaceCache.get(url);\n if (!cached) {\n cached = new FontFace(family, `url(${url})`, { featureSettings: \"'calt' 0, 'liga' 0\" }).load().then((loaded) => {\n document.fonts.add(loaded);\n });\n fontFaceCache.set(url, cached);\n }\n return cached;\n}\n\nconst PADDING_H_EM = 0.2;\nconst MIN_LINE_HEIGHT_EM = 1.8;\nconst MIN_PADDING_V_EM = 0.2;\n\n// --- CSS custom property names ---\n\nconst CSS_TIME = '--tegaki-time';\nconst CSS_PROGRESS = '--tegaki-progress';\nconst CSS_DURATION = '--tegaki-duration';\n\n// Register custom properties so they are animatable (typed as <number>).\n// Deferred to first mount to avoid running at import time during SSR.\nlet cssPropertiesRegistered = false;\nfunction registerCssProperties() {\n if (cssPropertiesRegistered) return;\n cssPropertiesRegistered = true;\n if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {\n for (const prop of [CSS_TIME, CSS_PROGRESS, CSS_DURATION]) {\n try {\n CSS.registerProperty({ name: prop, syntax: '<number>', inherits: true, initialValue: '0' });\n } catch {\n // Already registered — ignore.\n }\n }\n }\n}\n\nexport type TimeControlMode = {\n controlled: {\n mode: 'controlled';\n /** Current time in seconds. */\n value: number;\n };\n uncontrolled: {\n mode: 'uncontrolled';\n /** Initial time in seconds. Default: `0` */\n initialTime?: number;\n /** Playback speed multiplier. Default: `1` */\n speed?: number;\n /** Whether animation is playing. Default: `true` */\n playing?: boolean;\n /** Loop animation when it reaches the end. Default: `false` */\n loop?: boolean;\n /**\n * Catch-up strength. When positive, playback speeds up when there is a\n * large amount of remaining animation and decays back to normal gradually.\n * `0` disables catch-up (default). Higher values ramp up more aggressively.\n * Typical range: `0.2` – `2`.\n */\n catchUp?: number;\n /** Called on every frame with the current time. */\n onTimeChange?: (time: number) => void;\n };\n css: {\n mode: 'css';\n };\n};\n\n/**\n * A plain number is shorthand for `{ mode: 'controlled', value: number }`.\n * `'css'` is shorthand for `{ mode: 'css' }`.\n * Omit for uncontrolled mode with default settings.\n */\nexport type TimeControlProp = null | undefined | number | 'css' | TimeControlMode[keyof TimeControlMode];\n\n/** Imperative handle exposed via the `ref` prop. */\nexport interface TegakiRendererHandle {\n /** The root DOM element. */\n getElement(): HTMLDivElement | null;\n /** Current animation time in seconds. */\n getCurrentTime(): number;\n /** Total timeline duration in seconds. */\n getDuration(): number;\n /** Whether the animation is currently playing (uncontrolled mode only). */\n getIsPlaying(): boolean;\n /** Whether the animation has reached the end. */\n getIsComplete(): boolean;\n /** Resume playback (uncontrolled mode only). No-op in controlled/css mode. */\n play(): void;\n /** Pause playback (uncontrolled mode only). No-op in controlled/css mode. */\n pause(): void;\n /** Jump to a specific time in seconds (uncontrolled mode only). No-op in controlled/css mode. */\n seek(time: number): void;\n /** Seek to 0 and play (uncontrolled mode only). No-op in controlled/css mode. */\n restart(): void;\n}\n\nexport interface TegakiRendererProps<E extends TegakiEffects<E> = Record<string, never>>\n extends Omit<ComponentProps<'div'>, 'children' | 'ref'> {\n /** Imperative handle ref for playback controls and DOM access. */\n ref?: Ref<TegakiRendererHandle>;\n\n /** TegakiBundle with font data and animated glyph SVGs. */\n font?: TegakiBundle;\n\n /** Text to animate. Takes precedence over children. */\n text?: string;\n\n /** Children coerced to string. Strings and numbers are kept; everything else is ignored. */\n children?: Coercible;\n\n /**\n * Time control. Accepts a number (controlled shorthand), or an object\n * specifying the mode (`'controlled'`, `'uncontrolled'`, or `'css'`).\n * Omit for uncontrolled playback with default settings.\n */\n time?: TimeControlProp;\n\n /** Called once when the animation reaches the end of the timeline. */\n onComplete?: () => void;\n\n /** Visual effects applied during canvas rendering. */\n effects?: E;\n\n /** Maximum segment size in pixels for effect subdivision. Lower values produce\n * smoother effects but cost more to render. Default: `2` */\n segmentSize?: number;\n\n /** Timeline timing configuration (gap between glyphs, words, lines, etc.). */\n timing?: TimelineConfig;\n\n /** Show debug text overlay. */\n showOverlay?: boolean;\n}\n\n// --- Component ---\n\nexport function TegakiRenderer<const E extends TegakiEffects<E> = Record<string, never>>({\n ref,\n font,\n text,\n children,\n time: timeProp,\n onComplete,\n effects,\n segmentSize,\n timing,\n showOverlay,\n ...props\n}: TegakiRendererProps<E>) {\n registerCssProperties();\n\n const resolvedText = text ?? coerceToString(children);\n\n // --- Resolve effects ---\n const resolvedEffects = useMemo(() => resolveEffects(effects as Record<string, any>), [effects]);\n const [seed] = useState(() => Math.random() * 1000);\n\n // --- Resolve time control ---\n const timeControl: TimeControlMode[keyof TimeControlMode] =\n timeProp == null\n ? { mode: 'uncontrolled' }\n : typeof timeProp === 'number'\n ? { mode: 'controlled', value: timeProp }\n : timeProp === 'css'\n ? { mode: 'css' }\n : timeProp;\n\n const isCss = timeControl.mode === 'css';\n const isControlled = timeControl.mode === 'controlled' || isCss;\n const controlledTime = timeControl.mode === 'controlled' ? timeControl.value : undefined;\n const defaultTime = timeControl.mode === 'uncontrolled' ? (timeControl.initialTime ?? 0) : 0;\n const speed = timeControl.mode === 'uncontrolled' ? (timeControl.speed ?? 1) : 1;\n const propPlaying = timeControl.mode === 'uncontrolled' ? (timeControl.playing ?? true) : false;\n const loop = timeControl.mode === 'uncontrolled' ? (timeControl.loop ?? false) : false;\n const catchUp = timeControl.mode === 'uncontrolled' ? (timeControl.catchUp ?? 0) : 0;\n const onTimeChange = timeControl.mode === 'uncontrolled' ? timeControl.onTimeChange : undefined;\n\n // Imperative playing override (undefined = follow prop)\n const [playingOverride, setPlayingOverride] = useState<boolean | undefined>(undefined);\n const playing = playingOverride ?? propPlaying;\n\n // Reset override when the prop changes so the prop regains control\n const prevPropPlaying = useRef(propPlaying);\n if (prevPropPlaying.current !== propPlaying) {\n prevPropPlaying.current = propPlaying;\n setPlayingOverride(undefined);\n }\n\n // --- Internal time (uncontrolled mode) ---\n const [internalTime, setInternalTime] = useState(defaultTime);\n // --- CSS-driven time ---\n const [cssTime, setCssTime] = useState(0);\n const currentTime = isCss ? cssTime : isControlled ? controlledTime! : internalTime;\n\n // Stable refs so the imperative handle and rAF loop always see latest values\n const currentTimeRef = useRef(currentTime);\n currentTimeRef.current = currentTime;\n const playingRef = useRef(playing);\n playingRef.current = playing;\n const isControlledRef = useRef(isControlled);\n isControlledRef.current = isControlled;\n const onTimeChangeRef = useRef(onTimeChange);\n onTimeChangeRef.current = onTimeChange;\n const onCompleteRef = useRef(onComplete);\n onCompleteRef.current = onComplete;\n\n // --- Font loading ---\n // Track which font object has been loaded, so fontReady resets synchronously\n // when the font prop changes (no stale `true` from the previous font).\n const [loadedFont, setLoadedFont] = useState<TegakiBundle | null>(() =>\n font && ensureFont(font.family, font.fontUrl) === null ? font : null,\n );\n const fontReady = !!font && loadedFont === font;\n\n useEffect(() => {\n if (!font) {\n setLoadedFont(null);\n return;\n }\n const pending = ensureFont(font.family, font.fontUrl);\n if (pending === null) {\n setLoadedFont(font);\n return;\n }\n let cancelled = false;\n pending.then(() => {\n if (!cancelled) setLoadedFont(font);\n });\n return () => {\n cancelled = true;\n };\n }, [font]);\n\n // --- Font-derived constants ---\n const fontFamily = font?.family;\n const emHeight = font ? (font.ascender - font.descender) / font.unitsPerEm : 0;\n\n // --- Container measurement ---\n const rootRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState(0);\n const [fontSize, setFontSize] = useState(0);\n const [lineHeight, setLineHeight] = useState(0);\n const [currentColor, setCurrentColor] = useState('');\n\n // --- Timeline ---\n const timeline = useMemo(\n () => (font && resolvedText ? computeTimeline(resolvedText, font, timing) : { entries: [] as TimelineEntry[], totalDuration: 0 }),\n [resolvedText, font, timing],\n );\n\n // Duration ref so the rAF loop always sees the latest value without restarting\n const totalDurationRef = useRef(timeline.totalDuration);\n totalDurationRef.current = timeline.totalDuration;\n\n // Smoothed catch-up boost (raw bonus on top of base speed; attack/release smoothed)\n const smoothedBoostRef = useRef(0);\n\n // --- Completion tracking ---\n const prevCompletedRef = useRef(false);\n const isComplete = timeline.totalDuration > 0 && currentTime >= timeline.totalDuration;\n\n useEffect(() => {\n if (isComplete && !prevCompletedRef.current) {\n prevCompletedRef.current = true;\n onCompleteRef.current?.();\n } else if (!isComplete) {\n prevCompletedRef.current = false;\n }\n });\n\n // --- Imperative handle (stable — reads from refs) ---\n useImperativeHandle(\n ref,\n () => ({\n getElement: () => rootRef.current,\n getCurrentTime: () => currentTimeRef.current,\n getDuration: () => totalDurationRef.current,\n getIsPlaying: () => playingRef.current,\n getIsComplete: () => totalDurationRef.current > 0 && currentTimeRef.current >= totalDurationRef.current,\n play: () => {\n if (!isControlledRef.current) setPlayingOverride(true);\n },\n pause: () => {\n if (!isControlledRef.current) setPlayingOverride(false);\n },\n seek: (time: number) => {\n if (!isControlledRef.current) setInternalTime(Math.max(0, Math.min(time, totalDurationRef.current)));\n },\n restart: () => {\n if (!isControlledRef.current) {\n setInternalTime(0);\n setPlayingOverride(true);\n }\n },\n }),\n [],\n );\n\n // --- Uncontrolled: time change notification ---\n useEffect(() => {\n if (!isControlled) {\n onTimeChangeRef.current?.(internalTime);\n }\n }, [internalTime, isControlled]);\n\n // --- Reduced motion preference ---\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(\n () => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches,\n );\n useEffect(() => {\n const mql = window.matchMedia('(prefers-reduced-motion: reduce)');\n setPrefersReducedMotion(mql.matches);\n const onChange = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);\n mql.addEventListener('change', onChange);\n return () => mql.removeEventListener('change', onChange);\n }, []);\n\n // When reduced motion is preferred, skip to end of timeline\n useEffect(() => {\n if (prefersReducedMotion && !isControlled && timeline.totalDuration > 0) {\n setInternalTime(timeline.totalDuration);\n }\n }, [prefersReducedMotion, isControlled, timeline.totalDuration]);\n\n // --- Uncontrolled: rAF playback loop ---\n useEffect(() => {\n if (isControlled || !playing || !font || !fontReady || prefersReducedMotion) return;\n\n // Reset smoothed boost when the loop restarts\n smoothedBoostRef.current = 0;\n\n let lastTs: number | null = null;\n let raf: number;\n\n // Catch-up smoothing rates (per-second exponential factors)\n const attackRate = 4; // fast ramp-up\n const releaseRate = loop ? 30 : 2; // slow decay back to base\n\n const tick = (ts: number) => {\n if (lastTs === null) lastTs = ts;\n const dtSec = (ts - lastTs) / 1000;\n lastTs = ts;\n\n setInternalTime((prev: number) => {\n const totalDur = totalDurationRef.current;\n if (totalDur === 0 || (!loop && prev >= totalDur)) return totalDur;\n\n // Compute effective speed with catch-up\n let effectiveSpeed = speed;\n if (catchUp > 0) {\n const remaining = Math.max(0, totalDur - prev);\n const excess = Math.max(0, remaining - 2);\n const targetBoost = catchUp * excess;\n const rate = targetBoost > smoothedBoostRef.current ? attackRate : releaseRate;\n smoothedBoostRef.current += (targetBoost - smoothedBoostRef.current) * (1 - Math.exp(-rate * dtSec));\n effectiveSpeed = speed + smoothedBoostRef.current;\n }\n\n let next = prev + dtSec * effectiveSpeed;\n if (next >= totalDur) {\n next = loop ? next % totalDur : totalDur;\n smoothedBoostRef.current = 0; // reset boost on loop\n }\n return next;\n });\n\n raf = requestAnimationFrame(tick);\n };\n\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [isControlled, playing, speed, loop, catchUp, font, fontReady, prefersReducedMotion]);\n\n // --- Container size observation ---\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const ro = new ResizeObserver(([entry]) => {\n if (entry) {\n setContainerWidth(entry.contentRect.width);\n const styles = getComputedStyle(el);\n setFontSize(Number.parseFloat(styles.fontSize));\n setLineHeight(Number.parseFloat(styles.lineHeight));\n setCurrentColor(styles.color);\n }\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n // Sentinel element ref — a hidden child with `font-size: inherit` and a near-zero\n // CSS transition. When any ancestor changes font-size, the transition fires an event\n // so we can read the new value without polling getComputedStyle every render.\n const sentinelRef = useRef<HTMLSpanElement>(null);\n useEffect(() => {\n const el = sentinelRef.current;\n if (!el) return;\n const onTransition = (e: TransitionEvent) => {\n const styles = getComputedStyle(el);\n if (e.propertyName === 'font-size' || e.propertyName === 'line-height') {\n setFontSize(Number.parseFloat(styles.fontSize));\n setLineHeight(Number.parseFloat(styles.lineHeight));\n }\n if (e.propertyName === 'color') {\n setCurrentColor(styles.color);\n }\n if (e.propertyName === CSS_PROGRESS) {\n const rawProgress = Number(styles.getPropertyValue(CSS_PROGRESS));\n setCssTime(rawProgress * totalDurationRef.current);\n }\n };\n el.addEventListener('transitionend', onTransition);\n return () => el.removeEventListener('transitionend', onTransition);\n }, []);\n\n // --- Text layout ---\n const layout = useMemo(() => {\n if (!fontReady || !fontFamily || !fontSize || !containerWidth || !resolvedText) return null;\n return computeTextLayout(resolvedText, fontFamily, fontSize, lineHeight, containerWidth);\n }, [fontReady, resolvedText, fontFamily, fontSize, lineHeight, containerWidth]);\n\n // --- Canvas padding ---\n const padH = PADDING_H_EM * fontSize;\n const padV = fontSize ? Math.max(MIN_PADDING_V_EM * fontSize, (MIN_LINE_HEIGHT_EM * fontSize - lineHeight) / 2) : 0;\n const padVCss = `max(0.2em, 0.9em - 0.5lh)`;\n\n // --- Canvas rendering ---\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n useLayoutEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas || !font?.glyphData || !layout || !fontSize) return;\n\n const dpr = window.devicePixelRatio || 1;\n const el = rootRef.current;\n if (!el) return;\n const canvasRect = canvas.getBoundingClientRect();\n const w = canvasRect.width;\n const h = canvasRect.height;\n\n // Resize canvas backing store if needed\n const needsResize = canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr);\n if (needsResize) {\n canvas.width = Math.round(w * dpr);\n canvas.height = Math.round(h * dpr);\n }\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, w, h);\n ctx.translate(padH, padV);\n\n const color = currentColor || getComputedStyle(el).color;\n\n const emHeightPx = emHeight * fontSize;\n const halfLeading = (lineHeight - emHeightPx) / 2;\n const characters = graphemes(resolvedText);\n\n let y = 0;\n for (const lineIndices of layout.lines) {\n let x = 0;\n for (const charIdx of lineIndices) {\n const char = characters[charIdx]!;\n if (char === '\\n') continue;\n const entry = timeline.entries[charIdx]!;\n const charWidth = layout.charWidths[charIdx] ?? 0;\n const kerning = layout.kernings[charIdx] ?? 0;\n const glyph = font.glyphData[char];\n\n if (glyph && entry.hasGlyph) {\n const localTime = Math.max(0, Math.min(currentTime - entry.offset, entry.duration));\n const glyphY = y + halfLeading;\n drawGlyph(\n ctx,\n glyph,\n {\n x,\n y: glyphY,\n fontSize,\n unitsPerEm: font.unitsPerEm,\n ascender: font.ascender,\n descender: font.descender,\n },\n localTime,\n font.lineCap,\n color,\n resolvedEffects,\n seed + charIdx,\n segmentSize,\n );\n } else if (!entry.hasGlyph && currentTime >= entry.offset + entry.duration) {\n const baseline = y + halfLeading + (font.ascender / font.unitsPerEm) * fontSize;\n drawFallbackGlyph(ctx, char, x, baseline, fontSize, fontFamily!, color, resolvedEffects, seed + charIdx);\n }\n\n x += (charWidth + kerning) * fontSize;\n }\n y += lineHeight;\n }\n }, [\n currentTime,\n timeline,\n layout,\n font,\n fontFamily,\n fontSize,\n lineHeight,\n resolvedText,\n emHeight,\n padH,\n padV,\n currentColor,\n resolvedEffects,\n seed,\n segmentSize,\n ]);\n\n // --- Rendering ---\n\n return (\n <div\n ref={rootRef}\n {...props}\n style={{\n ...props.style,\n position: 'relative',\n maxWidth: '100%',\n width: 'auto',\n height: 'auto',\n fontFamily,\n ...{\n [CSS_DURATION]: timeline.totalDuration,\n [CSS_TIME]: currentTime,\n [CSS_PROGRESS]: timeline.totalDuration > 0 ? currentTime / timeline.totalDuration : 0,\n },\n }}\n >\n <div style={{ position: 'relative' }}>\n {/* Sentinel: inherits font-size & line-height; its height changes when either changes */}\n <span\n ref={sentinelRef}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n width: 0,\n overflow: 'hidden',\n pointerEvents: 'none',\n fontSize: 'inherit',\n lineHeight: 'inherit',\n visibility: 'hidden',\n transition: isCss\n ? `font-size 0.001s, line-height 0.001s, color 0.001s, ${CSS_PROGRESS} 0.001s`\n : 'font-size 0.001s, line-height 0.001s, color 0.001s',\n }}\n >\n {'\\u00A0'}\n </span>\n <canvas\n ref={canvasRef}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: `calc(-1 * ${padVCss}) -0.2em`,\n width: `calc(100% + 0.4em)`,\n height: `calc(100% + 2 * ${padVCss})`,\n pointerEvents: 'none',\n overflow: 'visible',\n }}\n >\n <span style={{ display: 'inline-block', padding: `${padVCss} 0.2em` }}>{resolvedText}</span>\n </canvas>\n\n <div\n style={{\n userSelect: 'auto',\n whiteSpace: 'pre-wrap',\n overflowWrap: 'break-word',\n paddingRight: 1,\n WebkitTextFillColor: showOverlay ? undefined : 'transparent',\n color: showOverlay ? 'rgba(255, 0, 0, 0.4)' : undefined,\n }}\n >\n {resolvedText}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -2,24 +2,15 @@
2
2
  import fontUrl from './caveat.ttf' with { type: 'url' };
3
3
  import glyphData from './glyphData.json' with { type: 'json' };
4
4
 
5
- let registered: Promise<void> | null = null;
6
-
7
5
  const bundle = {
8
6
  family: 'Caveat',
9
7
  lineCap: 'round',
10
8
  fontUrl,
9
+ fontFaceCSS: `@font-face { font-family: 'Caveat'; src: url(${fontUrl}); }`,
11
10
  unitsPerEm: 1000,
12
11
  ascender: 960,
13
12
  descender: -300,
14
13
  glyphData,
15
- registerFontFace() {
16
- if (!registered) {
17
- registered = new FontFace(bundle.family, `url(${fontUrl})`, { featureSettings: "'calt' 0, 'liga' 0" })
18
- .load()
19
- .then((loaded) => { document.fonts.add(loaded); });
20
- }
21
- return registered;
22
- },
23
14
  } as const;
24
15
 
25
16
  export default bundle;
@@ -2,24 +2,15 @@
2
2
  import fontUrl from './italianno.ttf' with { type: 'url' };
3
3
  import glyphData from './glyphData.json' with { type: 'json' };
4
4
 
5
- let registered: Promise<void> | null = null;
6
-
7
5
  const bundle = {
8
6
  family: 'Italianno',
9
7
  lineCap: 'round',
10
8
  fontUrl,
9
+ fontFaceCSS: `@font-face { font-family: 'Italianno'; src: url(${fontUrl}); }`,
11
10
  unitsPerEm: 1000,
12
11
  ascender: 800,
13
12
  descender: -450,
14
13
  glyphData,
15
- registerFontFace() {
16
- if (!registered) {
17
- registered = new FontFace(bundle.family, `url(${fontUrl})`, { featureSettings: "'calt' 0, 'liga' 0" })
18
- .load()
19
- .then((loaded) => { document.fonts.add(loaded); });
20
- }
21
- return registered;
22
- },
23
14
  } as const;
24
15
 
25
16
  export default bundle;
@@ -2,24 +2,15 @@
2
2
  import fontUrl from './parisienne.ttf' with { type: 'url' };
3
3
  import glyphData from './glyphData.json' with { type: 'json' };
4
4
 
5
- let registered: Promise<void> | null = null;
6
-
7
5
  const bundle = {
8
6
  family: 'Parisienne',
9
7
  lineCap: 'round',
10
8
  fontUrl,
9
+ fontFaceCSS: `@font-face { font-family: 'Parisienne'; src: url(${fontUrl}); }`,
11
10
  unitsPerEm: 2048,
12
11
  ascender: 1875,
13
12
  descender: -915,
14
13
  glyphData,
15
- registerFontFace() {
16
- if (!registered) {
17
- registered = new FontFace(bundle.family, `url(${fontUrl})`, { featureSettings: "'calt' 0, 'liga' 0" })
18
- .load()
19
- .then((loaded) => { document.fonts.add(loaded); });
20
- }
21
- return registered;
22
- },
23
14
  } as const;
24
15
 
25
16
  export default bundle;
@@ -2,24 +2,15 @@
2
2
  import fontUrl from './tangerine.ttf' with { type: 'url' };
3
3
  import glyphData from './glyphData.json' with { type: 'json' };
4
4
 
5
- let registered: Promise<void> | null = null;
6
-
7
5
  const bundle = {
8
6
  family: 'Tangerine',
9
7
  lineCap: 'round',
10
8
  fontUrl,
9
+ fontFaceCSS: `@font-face { font-family: 'Tangerine'; src: url(${fontUrl}); }`,
11
10
  unitsPerEm: 1000,
12
11
  ascender: 750,
13
12
  descender: -250,
14
13
  glyphData,
15
- registerFontFace() {
16
- if (!registered) {
17
- registered = new FontFace(bundle.family, `url(${fontUrl})`, { featureSettings: "'calt' 0, 'liga' 0" })
18
- .load()
19
- .then((loaded) => { document.fonts.add(loaded); });
20
- }
21
- return registered;
22
- },
23
14
  } as const;
24
15
 
25
16
  export default bundle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tegaki",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "React component for rendering animated handwriting from any font",
6
6
  "keywords": [
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { drawGlyph } from './lib/drawGlyph.ts';
2
2
  export {
3
+ ensureFontFace,
3
4
  TegakiRenderer,
4
5
  type TegakiRendererHandle,
5
6
  type TegakiRendererProps,
@@ -11,6 +11,41 @@ import { computeTimeline } from './timeline.ts';
11
11
  import type { Coercible } from './utils.ts';
12
12
  import { coerceToString, graphemes } from './utils.ts';
13
13
 
14
+ const fontFaceCache = new Map<string, Promise<void>>();
15
+
16
+ /**
17
+ * Returns a promise that resolves when the font is ready for text measurement.
18
+ * - Already loaded (by us or externally): resolves immediately.
19
+ * - Currently loading externally: waits for `document.fonts.ready`.
20
+ * - Not registered at all: loads it via the FontFace API.
21
+ * Returns `null` if the font is already loaded synchronously.
22
+ */
23
+ /**
24
+ * Ensures the bundle's font face is loaded and available for rendering.
25
+ * Resolves immediately if the font is already loaded.
26
+ */
27
+ export async function ensureFontFace(bundle: TegakiBundle): Promise<void> {
28
+ await ensureFont(bundle.family, bundle.fontUrl);
29
+ }
30
+
31
+ function ensureFont(family: string, url: string): Promise<void> | null {
32
+ if (typeof document === 'undefined') return Promise.resolve();
33
+ for (const face of document.fonts) {
34
+ if (face.family === family) {
35
+ if (face.status === 'loaded') return null;
36
+ if (face.status === 'loading') return face.loaded.then(() => {});
37
+ }
38
+ }
39
+ let cached = fontFaceCache.get(url);
40
+ if (!cached) {
41
+ cached = new FontFace(family, `url(${url})`, { featureSettings: "'calt' 0, 'liga' 0" }).load().then((loaded) => {
42
+ document.fonts.add(loaded);
43
+ });
44
+ fontFaceCache.set(url, cached);
45
+ }
46
+ return cached;
47
+ }
48
+
14
49
  const PADDING_H_EM = 0.2;
15
50
  const MIN_LINE_HEIGHT_EM = 1.8;
16
51
  const MIN_PADDING_V_EM = 0.2;
@@ -209,24 +244,26 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
209
244
  onCompleteRef.current = onComplete;
210
245
 
211
246
  // --- Font loading ---
212
- const [fontReady, setFontReady] = useState(
213
- () => typeof document !== 'undefined' && !!font && document.fonts.check(`16px "${font.family}"`),
247
+ // Track which font object has been loaded, so fontReady resets synchronously
248
+ // when the font prop changes (no stale `true` from the previous font).
249
+ const [loadedFont, setLoadedFont] = useState<TegakiBundle | null>(() =>
250
+ font && ensureFont(font.family, font.fontUrl) === null ? font : null,
214
251
  );
252
+ const fontReady = !!font && loadedFont === font;
253
+
215
254
  useEffect(() => {
216
255
  if (!font) {
217
- setFontReady(false);
256
+ setLoadedFont(null);
218
257
  return;
219
258
  }
220
- // Check if the font is already loaded
221
- if (document.fonts.check(`16px "${font.family}"`)) {
222
- setFontReady(true);
259
+ const pending = ensureFont(font.family, font.fontUrl);
260
+ if (pending === null) {
261
+ setLoadedFont(font);
223
262
  return;
224
263
  }
225
- // New font — mark not ready and start loading
226
- setFontReady(false);
227
264
  let cancelled = false;
228
- font.registerFontFace().then(() => {
229
- if (!cancelled) setFontReady(true);
265
+ pending.then(() => {
266
+ if (!cancelled) setLoadedFont(font);
230
267
  });
231
268
  return () => {
232
269
  cancelled = true;
@@ -424,6 +461,7 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
424
461
  // --- Canvas padding ---
425
462
  const padH = PADDING_H_EM * fontSize;
426
463
  const padV = fontSize ? Math.max(MIN_PADDING_V_EM * fontSize, (MIN_LINE_HEIGHT_EM * fontSize - lineHeight) / 2) : 0;
464
+ const padVCss = `max(0.2em, 0.9em - 0.5lh)`;
427
465
 
428
466
  // --- Canvas rendering ---
429
467
  const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -530,6 +568,7 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
530
568
  maxWidth: '100%',
531
569
  width: 'auto',
532
570
  height: 'auto',
571
+ fontFamily,
533
572
  ...{
534
573
  [CSS_DURATION]: timeline.totalDuration,
535
574
  [CSS_TIME]: currentTime,
@@ -562,12 +601,15 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
562
601
  aria-hidden="true"
563
602
  style={{
564
603
  position: 'absolute',
565
- inset: `${-padV}px ${-padH}px`,
566
- width: `calc(100% + ${padH * 2}px)`,
567
- height: `calc(100% + ${padV * 2}px)`,
604
+ inset: `calc(-1 * ${padVCss}) -0.2em`,
605
+ width: `calc(100% + 0.4em)`,
606
+ height: `calc(100% + 2 * ${padVCss})`,
568
607
  pointerEvents: 'none',
608
+ overflow: 'visible',
569
609
  }}
570
- />
610
+ >
611
+ <span style={{ display: 'inline-block', padding: `${padVCss} 0.2em` }}>{resolvedText}</span>
612
+ </canvas>
571
613
 
572
614
  <div
573
615
  style={{
@@ -576,7 +618,6 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
576
618
  overflowWrap: 'break-word',
577
619
  paddingRight: 1,
578
620
  WebkitTextFillColor: showOverlay ? undefined : 'transparent',
579
- fontFamily,
580
621
  color: showOverlay ? 'rgba(255, 0, 0, 0.4)' : undefined,
581
622
  }}
582
623
  >
@@ -6,18 +6,20 @@ export interface ResolvedEffect<K extends TegakiEffectName = TegakiEffectName> {
6
6
  config: TegakiEffectConfigs[K];
7
7
  }
8
8
 
9
+ const defaultEffects: Record<string, any> = { pressureWidth: true };
10
+ const knownEffects: Set<string> = new Set(['glow', 'wobble', 'pressureWidth', 'taper', 'gradient']);
11
+
9
12
  /**
10
13
  * Normalizes an effects record into a sorted array of resolved effects.
11
14
  * Known keys infer the effect name; custom keys read it from the `effect` field.
12
15
  * Boolean `true` becomes an empty config. `false`/absent entries are skipped.
13
16
  */
14
17
  export function resolveEffects(effects: Record<string, any> | undefined): ResolvedEffect[] {
15
- if (!effects) return [];
18
+ const merged = { ...defaultEffects, ...effects };
16
19
 
17
- const knownEffects: Set<string> = new Set(['glow', 'wobble', 'pressureWidth', 'taper', 'gradient']);
18
20
  const result: ResolvedEffect[] = [];
19
21
 
20
- for (const [key, value] of Object.entries(effects)) {
22
+ for (const [key, value] of Object.entries(merged)) {
21
23
  if (value === false || value == null) continue;
22
24
 
23
25
  let effectName: TegakiEffectName;
package/src/types.test.ts CHANGED
@@ -1,8 +1,15 @@
1
- import { describe, test } from 'bun:test';
2
- import type { TegakiEffects } from './types.ts';
1
+ import { describe, expectTypeOf, test } from 'bun:test';
2
+ import font from 'tegaki/fonts/tangerine';
3
+ import type { TegakiBundle, TegakiEffects } from './types.ts';
3
4
 
4
5
  function assertType<const E extends TegakiEffects<E>>(_value: E) {}
5
6
 
7
+ describe.skip('TegakiEffects', () => {
8
+ test('ensure font assignable', () => {
9
+ expectTypeOf(font).toExtend<TegakiBundle>();
10
+ });
11
+ });
12
+
6
13
  describe.skip('TegakiEffects', () => {
7
14
  test('known key with config', () => {
8
15
  assertType({ glow: { radius: 5 } });
package/src/types.ts CHANGED
@@ -69,7 +69,7 @@ export interface TegakiGlyphData {
69
69
  w: number;
70
70
  t: number;
71
71
  s: {
72
- p: [x: number, y: number, width: number][];
72
+ p: ([x: number, y: number, width: number] | number[])[];
73
73
  d: number;
74
74
  a: number;
75
75
  }[];
@@ -111,9 +111,9 @@ export interface TegakiBundle {
111
111
  family: string;
112
112
  lineCap: LineCap;
113
113
  fontUrl: string;
114
+ fontFaceCSS: string;
114
115
  unitsPerEm: number;
115
116
  ascender: number;
116
117
  descender: number;
117
118
  glyphData: Record<string, TegakiGlyphData>;
118
- registerFontFace: () => Promise<void>;
119
119
  }