tegaki 0.3.0 → 0.4.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.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`2236325`](https://github.com/KurtGokhan/tegaki/commit/2236325c7119b6de47be3f479b3e01b2cae4b907) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Rework font loading and improve defaults
8
+
9
+ - **Breaking**: Remove `registerFontFace()` from `TegakiBundle`. Font registration is now handled internally by `TegakiRenderer` via the FontFace API.
10
+ - Add `fontFaceCSS` property to `TegakiBundle` for SSR/stylesheet-based font loading.
11
+ - Export `ensureFontFace()` utility for manually preloading a bundle's font.
12
+ - Fix font layout being calculated with wrong font metrics when switching fonts or when the font isn't loaded yet.
13
+ - Enable `pressureWidth` effect by default.
14
+ - Handle non-JS environments (SSR) more gracefully.
15
+
16
+ ## 0.3.1
17
+
18
+ ### Patch Changes
19
+
20
+ - [`706375b`](https://github.com/KurtGokhan/tegaki/commit/706375bf056caefb8fd4c4279da9e0124535b706) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Accessibility, SSR and RSC fixes
21
+
3
22
  ## 0.3.0
4
23
 
5
24
  ### Minor 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
@@ -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,25 +517,61 @@ 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;
519
553
  const CSS_TIME = "--tegaki-time";
520
554
  const CSS_PROGRESS = "--tegaki-progress";
521
555
  const CSS_DURATION = "--tegaki-duration";
522
- if (typeof CSS !== "undefined" && "registerProperty" in CSS) for (const prop of [
523
- CSS_TIME,
524
- CSS_PROGRESS,
525
- CSS_DURATION
526
- ]) try {
527
- CSS.registerProperty({
528
- name: prop,
529
- syntax: "<number>",
530
- inherits: true,
531
- initialValue: "0"
532
- });
533
- } catch {}
556
+ let cssPropertiesRegistered = false;
557
+ function registerCssProperties() {
558
+ if (cssPropertiesRegistered) return;
559
+ cssPropertiesRegistered = true;
560
+ if (typeof CSS !== "undefined" && "registerProperty" in CSS) for (const prop of [
561
+ CSS_TIME,
562
+ CSS_PROGRESS,
563
+ CSS_DURATION
564
+ ]) try {
565
+ CSS.registerProperty({
566
+ name: prop,
567
+ syntax: "<number>",
568
+ inherits: true,
569
+ initialValue: "0"
570
+ });
571
+ } catch {}
572
+ }
534
573
  function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete, effects, segmentSize, timing, showOverlay, ...props }) {
574
+ registerCssProperties();
535
575
  const resolvedText = text ?? coerceToString(children);
536
576
  const resolvedEffects = useMemo(() => resolveEffects(effects), [effects]);
537
577
  const [seed] = useState(() => Math.random() * 1e3);
@@ -568,20 +608,21 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
568
608
  onTimeChangeRef.current = onTimeChange;
569
609
  const onCompleteRef = useRef(onComplete);
570
610
  onCompleteRef.current = onComplete;
571
- const [fontReady, setFontReady] = useState(() => !!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;
572
613
  useEffect(() => {
573
614
  if (!font) {
574
- setFontReady(false);
615
+ setLoadedFont(null);
575
616
  return;
576
617
  }
577
- if (document.fonts.check(`16px "${font.family}"`)) {
578
- setFontReady(true);
618
+ const pending = ensureFont(font.family, font.fontUrl);
619
+ if (pending === null) {
620
+ setLoadedFont(font);
579
621
  return;
580
622
  }
581
- setFontReady(false);
582
623
  let cancelled = false;
583
- font.registerFontFace().then(() => {
584
- if (!cancelled) setFontReady(true);
624
+ pending.then(() => {
625
+ if (!cancelled) setLoadedFont(font);
585
626
  });
586
627
  return () => {
587
628
  cancelled = true;
@@ -638,8 +679,23 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
638
679
  useEffect(() => {
639
680
  if (!isControlled) onTimeChangeRef.current?.(internalTime);
640
681
  }, [internalTime, isControlled]);
682
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
683
+ useEffect(() => {
684
+ const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
685
+ setPrefersReducedMotion(mql.matches);
686
+ const onChange = (e) => setPrefersReducedMotion(e.matches);
687
+ mql.addEventListener("change", onChange);
688
+ return () => mql.removeEventListener("change", onChange);
689
+ }, []);
690
+ useEffect(() => {
691
+ if (prefersReducedMotion && !isControlled && timeline.totalDuration > 0) setInternalTime(timeline.totalDuration);
692
+ }, [
693
+ prefersReducedMotion,
694
+ isControlled,
695
+ timeline.totalDuration
696
+ ]);
641
697
  useEffect(() => {
642
- if (isControlled || !playing || !font || !fontReady) return;
698
+ if (isControlled || !playing || !font || !fontReady || prefersReducedMotion) return;
643
699
  smoothedBoostRef.current = 0;
644
700
  let lastTs = null;
645
701
  let raf;
@@ -678,7 +734,8 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
678
734
  loop,
679
735
  catchUp,
680
736
  font,
681
- fontReady
737
+ fontReady,
738
+ prefersReducedMotion
682
739
  ]);
683
740
  useEffect(() => {
684
741
  const el = rootRef.current;
@@ -724,6 +781,7 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
724
781
  ]);
725
782
  const padH = PADDING_H_EM * fontSize;
726
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)`;
727
785
  const canvasRef = useRef(null);
728
786
  useLayoutEffect(() => {
729
787
  const canvas = canvasRef.current;
@@ -792,10 +850,6 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
792
850
  seed,
793
851
  segmentSize
794
852
  ]);
795
- if (!font || !resolvedText || !fontReady) return /* @__PURE__ */ jsx("div", {
796
- ref: rootRef,
797
- ...props
798
- });
799
853
  return /* @__PURE__ */ jsx("div", {
800
854
  ref: rootRef,
801
855
  ...props,
@@ -805,6 +859,7 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
805
859
  maxWidth: "100%",
806
860
  width: "auto",
807
861
  height: "auto",
862
+ fontFamily,
808
863
  [CSS_DURATION]: timeline.totalDuration,
809
864
  [CSS_TIME]: currentTime,
810
865
  [CSS_PROGRESS]: timeline.totalDuration > 0 ? currentTime / timeline.totalDuration : 0
@@ -814,7 +869,7 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
814
869
  children: [
815
870
  /* @__PURE__ */ jsx("span", {
816
871
  ref: sentinelRef,
817
- "aria-hidden": true,
872
+ "aria-hidden": "true",
818
873
  style: {
819
874
  position: "absolute",
820
875
  width: 0,
@@ -829,14 +884,22 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
829
884
  }),
830
885
  /* @__PURE__ */ jsx("canvas", {
831
886
  ref: canvasRef,
832
- "aria-hidden": true,
887
+ "aria-hidden": "true",
833
888
  style: {
834
889
  position: "absolute",
835
- inset: `${-padV}px ${-padH}px`,
836
- width: `calc(100% + ${padH * 2}px)`,
837
- height: `calc(100% + ${padV * 2}px)`,
838
- pointerEvents: "none"
839
- }
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
+ })
840
903
  }),
841
904
  /* @__PURE__ */ jsx("div", {
842
905
  style: {
@@ -845,7 +908,6 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
845
908
  overflowWrap: "break-word",
846
909
  paddingRight: 1,
847
910
  WebkitTextFillColor: showOverlay ? void 0 : "transparent",
848
- fontFamily,
849
911
  color: showOverlay ? "rgba(255, 0, 0, 0.4)" : void 0
850
912
  },
851
913
  children: resolvedText
@@ -855,6 +917,6 @@ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete,
855
917
  });
856
918
  }
857
919
  //#endregion
858
- export { TegakiRenderer, computeTimeline, drawGlyph };
920
+ export { TegakiRenderer, computeTimeline, drawGlyph, ensureFontFace };
859
921
 
860
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","import { 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// Calling registerProperty twice with the same name throws, so guard with try/catch.\nif (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\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 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(() => !!font && document.fonts.check(`16px \"${font?.family}\"`));\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 // --- Uncontrolled: rAF playback loop ---\n useEffect(() => {\n if (isControlled || !playing || !font || !fontReady) 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]);\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 if (!font || !resolvedText || !fontReady) {\n return <div ref={rootRef} {...props} />;\n }\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\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\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.0",
3
+ "version": "0.4.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,
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import { type ComponentProps, type Ref, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
4
  import type { TegakiBundle, TegakiEffects } from '../types.ts';
3
5
  import { drawFallbackGlyph } from './drawFallbackGlyph.ts';
@@ -9,6 +11,41 @@ import { computeTimeline } from './timeline.ts';
9
11
  import type { Coercible } from './utils.ts';
10
12
  import { coerceToString, graphemes } from './utils.ts';
11
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
+
12
49
  const PADDING_H_EM = 0.2;
13
50
  const MIN_LINE_HEIGHT_EM = 1.8;
14
51
  const MIN_PADDING_V_EM = 0.2;
@@ -20,13 +57,18 @@ const CSS_PROGRESS = '--tegaki-progress';
20
57
  const CSS_DURATION = '--tegaki-duration';
21
58
 
22
59
  // Register custom properties so they are animatable (typed as <number>).
23
- // Calling registerProperty twice with the same name throws, so guard with try/catch.
24
- if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
25
- for (const prop of [CSS_TIME, CSS_PROGRESS, CSS_DURATION]) {
26
- try {
27
- CSS.registerProperty({ name: prop, syntax: '<number>', inherits: true, initialValue: '0' });
28
- } catch {
29
- // Already registered ignore.
60
+ // Deferred to first mount to avoid running at import time during SSR.
61
+ let cssPropertiesRegistered = false;
62
+ function registerCssProperties() {
63
+ if (cssPropertiesRegistered) return;
64
+ cssPropertiesRegistered = true;
65
+ if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
66
+ for (const prop of [CSS_TIME, CSS_PROGRESS, CSS_DURATION]) {
67
+ try {
68
+ CSS.registerProperty({ name: prop, syntax: '<number>', inherits: true, initialValue: '0' });
69
+ } catch {
70
+ // Already registered — ignore.
71
+ }
30
72
  }
31
73
  }
32
74
  }
@@ -144,6 +186,8 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
144
186
  showOverlay,
145
187
  ...props
146
188
  }: TegakiRendererProps<E>) {
189
+ registerCssProperties();
190
+
147
191
  const resolvedText = text ?? coerceToString(children);
148
192
 
149
193
  // --- Resolve effects ---
@@ -200,22 +244,26 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
200
244
  onCompleteRef.current = onComplete;
201
245
 
202
246
  // --- Font loading ---
203
- const [fontReady, setFontReady] = useState(() => !!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,
251
+ );
252
+ const fontReady = !!font && loadedFont === font;
253
+
204
254
  useEffect(() => {
205
255
  if (!font) {
206
- setFontReady(false);
256
+ setLoadedFont(null);
207
257
  return;
208
258
  }
209
- // Check if the font is already loaded
210
- if (document.fonts.check(`16px "${font.family}"`)) {
211
- setFontReady(true);
259
+ const pending = ensureFont(font.family, font.fontUrl);
260
+ if (pending === null) {
261
+ setLoadedFont(font);
212
262
  return;
213
263
  }
214
- // New font — mark not ready and start loading
215
- setFontReady(false);
216
264
  let cancelled = false;
217
- font.registerFontFace().then(() => {
218
- if (!cancelled) setFontReady(true);
265
+ pending.then(() => {
266
+ if (!cancelled) setLoadedFont(font);
219
267
  });
220
268
  return () => {
221
269
  cancelled = true;
@@ -294,9 +342,28 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
294
342
  }
295
343
  }, [internalTime, isControlled]);
296
344
 
345
+ // --- Reduced motion preference ---
346
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(
347
+ () => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches,
348
+ );
349
+ useEffect(() => {
350
+ const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
351
+ setPrefersReducedMotion(mql.matches);
352
+ const onChange = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);
353
+ mql.addEventListener('change', onChange);
354
+ return () => mql.removeEventListener('change', onChange);
355
+ }, []);
356
+
357
+ // When reduced motion is preferred, skip to end of timeline
358
+ useEffect(() => {
359
+ if (prefersReducedMotion && !isControlled && timeline.totalDuration > 0) {
360
+ setInternalTime(timeline.totalDuration);
361
+ }
362
+ }, [prefersReducedMotion, isControlled, timeline.totalDuration]);
363
+
297
364
  // --- Uncontrolled: rAF playback loop ---
298
365
  useEffect(() => {
299
- if (isControlled || !playing || !font || !fontReady) return;
366
+ if (isControlled || !playing || !font || !fontReady || prefersReducedMotion) return;
300
367
 
301
368
  // Reset smoothed boost when the loop restarts
302
369
  smoothedBoostRef.current = 0;
@@ -341,7 +408,7 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
341
408
 
342
409
  raf = requestAnimationFrame(tick);
343
410
  return () => cancelAnimationFrame(raf);
344
- }, [isControlled, playing, speed, loop, catchUp, font, fontReady]);
411
+ }, [isControlled, playing, speed, loop, catchUp, font, fontReady, prefersReducedMotion]);
345
412
 
346
413
  // --- Container size observation ---
347
414
  useEffect(() => {
@@ -394,6 +461,7 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
394
461
  // --- Canvas padding ---
395
462
  const padH = PADDING_H_EM * fontSize;
396
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)`;
397
465
 
398
466
  // --- Canvas rendering ---
399
467
  const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -490,10 +558,6 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
490
558
 
491
559
  // --- Rendering ---
492
560
 
493
- if (!font || !resolvedText || !fontReady) {
494
- return <div ref={rootRef} {...props} />;
495
- }
496
-
497
561
  return (
498
562
  <div
499
563
  ref={rootRef}
@@ -504,6 +568,7 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
504
568
  maxWidth: '100%',
505
569
  width: 'auto',
506
570
  height: 'auto',
571
+ fontFamily,
507
572
  ...{
508
573
  [CSS_DURATION]: timeline.totalDuration,
509
574
  [CSS_TIME]: currentTime,
@@ -515,7 +580,7 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
515
580
  {/* Sentinel: inherits font-size & line-height; its height changes when either changes */}
516
581
  <span
517
582
  ref={sentinelRef}
518
- aria-hidden
583
+ aria-hidden="true"
519
584
  style={{
520
585
  position: 'absolute',
521
586
  width: 0,
@@ -533,15 +598,18 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
533
598
  </span>
534
599
  <canvas
535
600
  ref={canvasRef}
536
- aria-hidden
601
+ aria-hidden="true"
537
602
  style={{
538
603
  position: 'absolute',
539
- inset: `${-padV}px ${-padH}px`,
540
- width: `calc(100% + ${padH * 2}px)`,
541
- 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})`,
542
607
  pointerEvents: 'none',
608
+ overflow: 'visible',
543
609
  }}
544
- />
610
+ >
611
+ <span style={{ display: 'inline-block', padding: `${padVCss} 0.2em` }}>{resolvedText}</span>
612
+ </canvas>
545
613
 
546
614
  <div
547
615
  style={{
@@ -550,7 +618,6 @@ export function TegakiRenderer<const E extends TegakiEffects<E> = Record<string,
550
618
  overflowWrap: 'break-word',
551
619
  paddingRight: 1,
552
620
  WebkitTextFillColor: showOverlay ? undefined : 'transparent',
553
- fontFamily,
554
621
  color: showOverlay ? 'rgba(255, 0, 0, 0.4)' : undefined,
555
622
  }}
556
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.ts CHANGED
@@ -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
  }