tegaki 0.5.0 → 0.6.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 +6 -0
- package/README.md +33 -160
- package/dist/core/index.d.mts +2 -0
- package/dist/core/index.mjs +2 -0
- package/dist/core-aZknK_0L.mjs +1121 -0
- package/dist/core-aZknK_0L.mjs.map +1 -0
- package/dist/index-BUNd9bnS.d.mts +39 -0
- package/dist/index-Bzxy01Zq.d.mts +329 -0
- package/dist/index.d.mts +3 -288
- package/dist/index.mjs +3 -922
- package/dist/react/index.d.mts +3 -0
- package/dist/react/index.mjs +3 -0
- package/dist/react-Ddx6VfJc.mjs +53 -0
- package/dist/react-Ddx6VfJc.mjs.map +1 -0
- package/dist/solid/index.d.mts +19 -0
- package/dist/solid/index.mjs +77 -0
- package/dist/solid/index.mjs.map +1 -0
- package/package.json +55 -4
- package/src/astro/TegakiRenderer.astro +126 -0
- package/src/core/engine.ts +878 -0
- package/src/core/index.ts +14 -0
- package/src/env.d.ts +12 -0
- package/src/index.ts +1 -12
- package/src/lib/css-properties.ts +24 -0
- package/src/lib/font.ts +29 -0
- package/src/react/TegakiRenderer.tsx +98 -0
- package/src/react/index.ts +2 -0
- package/src/solid/TegakiRenderer.tsx +99 -0
- package/src/solid/index.ts +2 -0
- package/src/svelte/TegakiRenderer.svelte +90 -0
- package/src/svelte/index.ts +2 -0
- package/src/vue/TegakiRenderer.vue +95 -0
- package/src/vue/index.ts +2 -0
- package/dist/index.mjs.map +0 -1
- package/src/lib/TegakiRenderer.tsx +0 -629
package/dist/index.mjs
CHANGED
|
@@ -1,922 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
]);
|
|
13
|
-
/**
|
|
14
|
-
* Normalizes an effects record into a sorted array of resolved effects.
|
|
15
|
-
* Known keys infer the effect name; custom keys read it from the `effect` field.
|
|
16
|
-
* Boolean `true` becomes an empty config. `false`/absent entries are skipped.
|
|
17
|
-
*/
|
|
18
|
-
function resolveEffects(effects) {
|
|
19
|
-
const merged = {
|
|
20
|
-
...defaultEffects,
|
|
21
|
-
...effects
|
|
22
|
-
};
|
|
23
|
-
const result = [];
|
|
24
|
-
for (const [key, value] of Object.entries(merged)) {
|
|
25
|
-
if (value === false || value == null) continue;
|
|
26
|
-
let effectName;
|
|
27
|
-
let config;
|
|
28
|
-
let order;
|
|
29
|
-
if (value === true) {
|
|
30
|
-
effectName = knownEffects.has(key) ? key : void 0;
|
|
31
|
-
if (!effectName) continue;
|
|
32
|
-
config = {};
|
|
33
|
-
order = 0;
|
|
34
|
-
} else {
|
|
35
|
-
if (value.enabled === false) continue;
|
|
36
|
-
effectName = value.effect ?? (knownEffects.has(key) ? key : void 0);
|
|
37
|
-
if (!effectName) continue;
|
|
38
|
-
const { effect: _, order: o, enabled: __, ...rest } = value;
|
|
39
|
-
config = rest;
|
|
40
|
-
order = o ?? 0;
|
|
41
|
-
}
|
|
42
|
-
result.push({
|
|
43
|
-
effect: effectName,
|
|
44
|
-
order,
|
|
45
|
-
config
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
result.sort((a, b) => a.order - b.order);
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
/** Check if a specific effect is active. */
|
|
52
|
-
function findEffect(effects, name) {
|
|
53
|
-
return effects.find((e) => e.effect === name);
|
|
54
|
-
}
|
|
55
|
-
/** Get all instances of a specific effect (for duplicates). */
|
|
56
|
-
function findEffects(effects, name) {
|
|
57
|
-
return effects.filter((e) => e.effect === name);
|
|
58
|
-
}
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region src/lib/utils.ts
|
|
61
|
-
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
62
|
-
/** Resolve a CSSLength to pixels. Plain numbers are px, `"Nem"` is N * fontSize. */
|
|
63
|
-
function resolveCSSLength(value, fontSize) {
|
|
64
|
-
if (typeof value === "number") return value;
|
|
65
|
-
return parseFloat(value) * fontSize;
|
|
66
|
-
}
|
|
67
|
-
function graphemes(text) {
|
|
68
|
-
return Array.from(segmenter.segment(text), (s) => s.segment);
|
|
69
|
-
}
|
|
70
|
-
function coerceToString(value) {
|
|
71
|
-
if (value == null || typeof value === "boolean") return "";
|
|
72
|
-
if (typeof value === "string") return value;
|
|
73
|
-
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
74
|
-
if (Array.isArray(value)) return value.map(coerceToString).join("");
|
|
75
|
-
return "";
|
|
76
|
-
}
|
|
77
|
-
//#endregion
|
|
78
|
-
//#region src/lib/drawGlyph.ts
|
|
79
|
-
function parseColor(color) {
|
|
80
|
-
const h = color.replace("#", "");
|
|
81
|
-
if (h.length === 3) return [
|
|
82
|
-
parseInt(h[0] + h[0], 16),
|
|
83
|
-
parseInt(h[1] + h[1], 16),
|
|
84
|
-
parseInt(h[2] + h[2], 16),
|
|
85
|
-
1
|
|
86
|
-
];
|
|
87
|
-
if (h.length === 4) return [
|
|
88
|
-
parseInt(h[0] + h[0], 16),
|
|
89
|
-
parseInt(h[1] + h[1], 16),
|
|
90
|
-
parseInt(h[2] + h[2], 16),
|
|
91
|
-
parseInt(h[3] + h[3], 16) / 255
|
|
92
|
-
];
|
|
93
|
-
if (h.length === 8) return [
|
|
94
|
-
parseInt(h.slice(0, 2), 16),
|
|
95
|
-
parseInt(h.slice(2, 4), 16),
|
|
96
|
-
parseInt(h.slice(4, 6), 16),
|
|
97
|
-
parseInt(h.slice(6, 8), 16) / 255
|
|
98
|
-
];
|
|
99
|
-
return [
|
|
100
|
-
parseInt(h.slice(0, 2), 16),
|
|
101
|
-
parseInt(h.slice(2, 4), 16),
|
|
102
|
-
parseInt(h.slice(4, 6), 16),
|
|
103
|
-
1
|
|
104
|
-
];
|
|
105
|
-
}
|
|
106
|
-
function lerpColor(a, b, t) {
|
|
107
|
-
const r = Math.round(a[0] + (b[0] - a[0]) * t);
|
|
108
|
-
const g = Math.round(a[1] + (b[1] - a[1]) * t);
|
|
109
|
-
const bl = Math.round(a[2] + (b[2] - a[2]) * t);
|
|
110
|
-
const al = a[3] + (b[3] - a[3]) * t;
|
|
111
|
-
if (al >= 1) return `rgb(${r},${g},${bl})`;
|
|
112
|
-
return `rgba(${r},${g},${bl},${al.toFixed(3)})`;
|
|
113
|
-
}
|
|
114
|
-
function gradientColor(progress, colors, seed) {
|
|
115
|
-
if (colors.length === 0) return "#000";
|
|
116
|
-
if (colors.length === 1) return colors[0];
|
|
117
|
-
const scaledT = ((progress + seed * .1) % 1 + 1) % 1 * (colors.length - 1);
|
|
118
|
-
const i = Math.min(Math.floor(scaledT), colors.length - 2);
|
|
119
|
-
const frac = scaledT - i;
|
|
120
|
-
return lerpColor(parseColor(colors[i]), parseColor(colors[i + 1]), frac);
|
|
121
|
-
}
|
|
122
|
-
function rainbowColor(progress, saturation, lightness, seed) {
|
|
123
|
-
return `hsl(${(progress * 360 + seed * 137.5) % 360}, ${saturation}%, ${lightness}%)`;
|
|
124
|
-
}
|
|
125
|
-
function hash(x) {
|
|
126
|
-
let h = x * 2654435761 | 0;
|
|
127
|
-
h = (h >>> 16 ^ h) * 73244475;
|
|
128
|
-
h = (h >>> 16 ^ h) * 73244475;
|
|
129
|
-
h = h >>> 16 ^ h;
|
|
130
|
-
return (h & 2147483647) / 2147483647;
|
|
131
|
-
}
|
|
132
|
-
function noise1d(x, seed) {
|
|
133
|
-
const i = Math.floor(x);
|
|
134
|
-
const f = x - i;
|
|
135
|
-
const t = f * f * (3 - 2 * f);
|
|
136
|
-
return hash(i + seed * 7919) * (1 - t) + hash(i + 1 + seed * 7919) * t;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Draw a single glyph's strokes onto a canvas context, animated up to `localTime`.
|
|
140
|
-
* `localTime` is seconds relative to this glyph's start (0 = glyph begins).
|
|
141
|
-
*/
|
|
142
|
-
function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], seed = 0, segmentSize) {
|
|
143
|
-
const scale = pos.fontSize / pos.unitsPerEm;
|
|
144
|
-
const ox = pos.x;
|
|
145
|
-
const oy = pos.y;
|
|
146
|
-
const glowEffects = findEffects(effects, "glow");
|
|
147
|
-
const wobbleEffect = findEffect(effects, "wobble");
|
|
148
|
-
const pressureEffect = findEffect(effects, "pressureWidth");
|
|
149
|
-
const taperEffect = findEffect(effects, "taper");
|
|
150
|
-
const gradientEffect = findEffect(effects, "gradient");
|
|
151
|
-
const pressureAmount = pressureEffect ? Math.max(0, Math.min(pressureEffect.config.strength ?? 1, 1)) : 0;
|
|
152
|
-
const wobbleAmplitude = wobbleEffect ? wobbleEffect.config.amplitude ?? 1.5 : 0;
|
|
153
|
-
const wobbleFrequency = wobbleEffect ? wobbleEffect.config.frequency ?? 8 : 0;
|
|
154
|
-
const wobbleMode = wobbleEffect?.config.mode ?? "sine";
|
|
155
|
-
const taperStart = taperEffect ? Math.max(0, Math.min(taperEffect.config.startLength ?? .15, 1)) : 0;
|
|
156
|
-
const taperEnd = taperEffect ? Math.max(0, Math.min(taperEffect.config.endLength ?? .15, 1)) : 0;
|
|
157
|
-
const gradientColors = gradientEffect?.config.colors;
|
|
158
|
-
const isRainbow = gradientColors === "rainbow";
|
|
159
|
-
const gradientColorStops = Array.isArray(gradientColors) ? gradientColors : void 0;
|
|
160
|
-
const gradientSaturation = gradientEffect?.config.saturation ?? 80;
|
|
161
|
-
const gradientLightness = gradientEffect?.config.lightness ?? 55;
|
|
162
|
-
const wobbleX = (x, y, idx) => {
|
|
163
|
-
if (!wobbleEffect) return x;
|
|
164
|
-
if (wobbleMode === "noise") return x + wobbleAmplitude * (noise1d(y * .1 + idx * .7, seed) * 2 - 1);
|
|
165
|
-
return x + wobbleAmplitude * Math.sin(wobbleFrequency * (y * .01 + idx * .7) + seed);
|
|
166
|
-
};
|
|
167
|
-
const wobbleY = (x, y, idx) => {
|
|
168
|
-
if (!wobbleEffect) return y;
|
|
169
|
-
if (wobbleMode === "noise") return y + wobbleAmplitude * (noise1d(x * .1 + idx * .5, seed * 1.3 + 1e3) * 2 - 1);
|
|
170
|
-
return y + wobbleAmplitude * Math.cos(wobbleFrequency * (x * .01 + idx * .5) + seed * 1.3);
|
|
171
|
-
};
|
|
172
|
-
const px = (x) => ox + x * scale;
|
|
173
|
-
const py = (y) => oy + (y + pos.ascender) * scale;
|
|
174
|
-
const colorAt = (progress) => {
|
|
175
|
-
if (isRainbow) return rainbowColor(progress, gradientSaturation, gradientLightness, seed);
|
|
176
|
-
if (gradientColorStops) return gradientColor(progress, gradientColorStops, seed);
|
|
177
|
-
return color;
|
|
178
|
-
};
|
|
179
|
-
const hasGradient = !!gradientEffect;
|
|
180
|
-
const taperMultiplier = (progress) => {
|
|
181
|
-
let m = 1;
|
|
182
|
-
if (taperStart > 0 && progress < taperStart) m = Math.min(m, progress / taperStart);
|
|
183
|
-
if (taperEnd > 0 && progress > 1 - taperEnd) m = Math.min(m, (1 - progress) / taperEnd);
|
|
184
|
-
return m;
|
|
185
|
-
};
|
|
186
|
-
for (const stroke of glyph.s) {
|
|
187
|
-
if (localTime < stroke.d) continue;
|
|
188
|
-
const elapsed = localTime - stroke.d;
|
|
189
|
-
const progress = Math.min(elapsed / stroke.a, 1);
|
|
190
|
-
const pts = stroke.p;
|
|
191
|
-
if (pts.length === 0) continue;
|
|
192
|
-
const avgWidth = pts.reduce((s, p) => s + p[2], 0) / pts.length;
|
|
193
|
-
const baseLineWidth = Math.max(avgWidth, .5) * scale;
|
|
194
|
-
if (pts.length === 1) {
|
|
195
|
-
if (progress <= 0) continue;
|
|
196
|
-
const p = pts[0];
|
|
197
|
-
const dotX = px(wobbleX(p[0], p[1], 0));
|
|
198
|
-
const dotY = py(wobbleY(p[0], p[1], 0));
|
|
199
|
-
let dotWidth = baseLineWidth + (Math.max(p[2], .5) * scale - baseLineWidth) * pressureAmount;
|
|
200
|
-
dotWidth *= taperMultiplier(.5);
|
|
201
|
-
for (const glow of glowEffects) {
|
|
202
|
-
ctx.save();
|
|
203
|
-
ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, pos.fontSize);
|
|
204
|
-
ctx.shadowColor = glow.config.color ?? color;
|
|
205
|
-
ctx.shadowOffsetX = (glow.config.offsetX ?? 0) * scale;
|
|
206
|
-
ctx.shadowOffsetY = (glow.config.offsetY ?? 0) * scale;
|
|
207
|
-
ctx.fillStyle = glow.config.color ?? color;
|
|
208
|
-
ctx.beginPath();
|
|
209
|
-
if (lineCap === "round") ctx.arc(dotX, dotY, dotWidth / 2, 0, Math.PI * 2);
|
|
210
|
-
else ctx.rect(dotX - dotWidth / 2, dotY - dotWidth / 2, dotWidth, dotWidth);
|
|
211
|
-
ctx.fill();
|
|
212
|
-
ctx.restore();
|
|
213
|
-
}
|
|
214
|
-
ctx.fillStyle = colorAt(0);
|
|
215
|
-
ctx.beginPath();
|
|
216
|
-
if (lineCap === "round") {
|
|
217
|
-
ctx.arc(dotX, dotY, dotWidth / 2, 0, Math.PI * 2);
|
|
218
|
-
ctx.fill();
|
|
219
|
-
} else ctx.fillRect(dotX - dotWidth / 2, dotY - dotWidth / 2, dotWidth, dotWidth);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
let totalLen = 0;
|
|
223
|
-
for (let j = 1; j < pts.length; j++) {
|
|
224
|
-
const dx = pts[j][0] - pts[j - 1][0];
|
|
225
|
-
const dy = pts[j][1] - pts[j - 1][1];
|
|
226
|
-
totalLen += Math.sqrt(dx * dx + dy * dy);
|
|
227
|
-
}
|
|
228
|
-
const drawLen = totalLen * progress;
|
|
229
|
-
if (drawLen <= 0) continue;
|
|
230
|
-
const segments = [];
|
|
231
|
-
let accumulated = 0;
|
|
232
|
-
for (let j = 1; j < pts.length; j++) {
|
|
233
|
-
const prev = pts[j - 1];
|
|
234
|
-
const cur = pts[j];
|
|
235
|
-
const dx = cur[0] - prev[0];
|
|
236
|
-
const dy = cur[1] - prev[1];
|
|
237
|
-
const segLen = Math.sqrt(dx * dx + dy * dy);
|
|
238
|
-
if (accumulated + segLen <= drawLen) {
|
|
239
|
-
segments.push({
|
|
240
|
-
x0: px(wobbleX(prev[0], prev[1], j - 1)),
|
|
241
|
-
y0: py(wobbleY(prev[0], prev[1], j - 1)),
|
|
242
|
-
x1: px(wobbleX(cur[0], cur[1], j)),
|
|
243
|
-
y1: py(wobbleY(cur[0], cur[1], j)),
|
|
244
|
-
width0: prev[2],
|
|
245
|
-
width1: cur[2],
|
|
246
|
-
segProgress: (accumulated + segLen / 2) / totalLen
|
|
247
|
-
});
|
|
248
|
-
accumulated += segLen;
|
|
249
|
-
} else {
|
|
250
|
-
const remaining = drawLen - accumulated;
|
|
251
|
-
const frac = segLen > 0 ? remaining / segLen : 0;
|
|
252
|
-
const ix = prev[0] + dx * frac;
|
|
253
|
-
const iy = prev[1] + dy * frac;
|
|
254
|
-
const iw = prev[2] + (cur[2] - prev[2]) * frac;
|
|
255
|
-
segments.push({
|
|
256
|
-
x0: px(wobbleX(prev[0], prev[1], j - 1)),
|
|
257
|
-
y0: py(wobbleY(prev[0], prev[1], j - 1)),
|
|
258
|
-
x1: px(wobbleX(ix, iy, j)),
|
|
259
|
-
y1: py(wobbleY(ix, iy, j)),
|
|
260
|
-
width0: prev[2],
|
|
261
|
-
width1: iw,
|
|
262
|
-
segProgress: (accumulated + remaining / 2) / totalLen
|
|
263
|
-
});
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (segments.length === 0) continue;
|
|
268
|
-
const coarseSegments = segments.slice();
|
|
269
|
-
const resolvedSegmentSize = segmentSize ?? (pressureAmount > 0 || hasGradient || !!wobbleEffect || !!taperEffect ? 2 : void 0);
|
|
270
|
-
if (resolvedSegmentSize != null) {
|
|
271
|
-
const maxSegLen = resolvedSegmentSize * scale;
|
|
272
|
-
const subdivided = [];
|
|
273
|
-
for (const seg of segments) {
|
|
274
|
-
const dx = seg.x1 - seg.x0;
|
|
275
|
-
const dy = seg.y1 - seg.y0;
|
|
276
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
277
|
-
const count = Math.max(1, Math.ceil(len / maxSegLen));
|
|
278
|
-
for (let k = 0; k < count; k++) {
|
|
279
|
-
const t0 = k / count;
|
|
280
|
-
const t1 = (k + 1) / count;
|
|
281
|
-
subdivided.push({
|
|
282
|
-
x0: seg.x0 + dx * t0,
|
|
283
|
-
y0: seg.y0 + dy * t0,
|
|
284
|
-
x1: seg.x0 + dx * t1,
|
|
285
|
-
y1: seg.y0 + dy * t1,
|
|
286
|
-
width0: seg.width0 + (seg.width1 - seg.width0) * t0,
|
|
287
|
-
width1: seg.width0 + (seg.width1 - seg.width0) * t1,
|
|
288
|
-
segProgress: seg.segProgress
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
for (let k = 0; k < subdivided.length; k++) subdivided[k].segProgress = subdivided.length > 1 ? k / (subdivided.length - 1) : 0;
|
|
293
|
-
segments.length = 0;
|
|
294
|
-
segments.push(...subdivided);
|
|
295
|
-
}
|
|
296
|
-
const segWidth = (seg) => {
|
|
297
|
-
const perPoint = (seg.width0 + seg.width1) / 2 * scale;
|
|
298
|
-
return Math.max(baseLineWidth + (perPoint - baseLineWidth) * pressureAmount, .5 * scale) * taperMultiplier(seg.segProgress);
|
|
299
|
-
};
|
|
300
|
-
const needsPerSegment = pressureAmount > 0 || taperEffect;
|
|
301
|
-
const drawStrokePath = () => {
|
|
302
|
-
if (needsPerSegment) for (const seg of segments) {
|
|
303
|
-
ctx.lineWidth = segWidth(seg);
|
|
304
|
-
ctx.beginPath();
|
|
305
|
-
ctx.moveTo(seg.x0, seg.y0);
|
|
306
|
-
ctx.lineTo(seg.x1, seg.y1);
|
|
307
|
-
ctx.stroke();
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
ctx.lineWidth = baseLineWidth;
|
|
311
|
-
ctx.beginPath();
|
|
312
|
-
ctx.moveTo(segments[0].x0, segments[0].y0);
|
|
313
|
-
for (const seg of segments) ctx.lineTo(seg.x1, seg.y1);
|
|
314
|
-
ctx.stroke();
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
const drawGradientPath = () => {
|
|
318
|
-
for (const seg of segments) {
|
|
319
|
-
ctx.strokeStyle = colorAt(seg.segProgress);
|
|
320
|
-
if (needsPerSegment) ctx.lineWidth = segWidth(seg);
|
|
321
|
-
ctx.beginPath();
|
|
322
|
-
ctx.moveTo(seg.x0, seg.y0);
|
|
323
|
-
ctx.lineTo(seg.x1, seg.y1);
|
|
324
|
-
ctx.stroke();
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
ctx.lineCap = lineCap;
|
|
328
|
-
ctx.lineJoin = "round";
|
|
329
|
-
for (const glow of glowEffects) {
|
|
330
|
-
ctx.save();
|
|
331
|
-
ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, pos.fontSize);
|
|
332
|
-
ctx.shadowColor = glow.config.color ?? color;
|
|
333
|
-
ctx.shadowOffsetX = (glow.config.offsetX ?? 0) * scale;
|
|
334
|
-
ctx.shadowOffsetY = (glow.config.offsetY ?? 0) * scale;
|
|
335
|
-
ctx.strokeStyle = glow.config.color ?? color;
|
|
336
|
-
ctx.lineWidth = baseLineWidth;
|
|
337
|
-
ctx.beginPath();
|
|
338
|
-
ctx.moveTo(coarseSegments[0].x0, coarseSegments[0].y0);
|
|
339
|
-
for (const seg of coarseSegments) ctx.lineTo(seg.x1, seg.y1);
|
|
340
|
-
ctx.stroke();
|
|
341
|
-
ctx.restore();
|
|
342
|
-
}
|
|
343
|
-
if (hasGradient) drawGradientPath();
|
|
344
|
-
else {
|
|
345
|
-
ctx.strokeStyle = color;
|
|
346
|
-
drawStrokePath();
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/lib/drawFallbackGlyph.ts
|
|
352
|
-
/**
|
|
353
|
-
* Draw a fallback glyph (plain text) with applicable effects (glow, gradient, wobble).
|
|
354
|
-
*/
|
|
355
|
-
function drawFallbackGlyph(ctx, char, x, baseline, fontSize, fontFamily, color, effects = [], seed = 0) {
|
|
356
|
-
const glowEffects = findEffects(effects, "glow");
|
|
357
|
-
const wobbleEffect = findEffect(effects, "wobble");
|
|
358
|
-
const gradientEffect = findEffect(effects, "gradient");
|
|
359
|
-
let dx = 0;
|
|
360
|
-
let dy = 0;
|
|
361
|
-
if (wobbleEffect) {
|
|
362
|
-
const amplitude = (wobbleEffect.config.amplitude ?? 1.5) * (fontSize / 100);
|
|
363
|
-
const frequency = wobbleEffect.config.frequency ?? 8;
|
|
364
|
-
dx = amplitude * Math.sin(frequency * (baseline * .01) + seed);
|
|
365
|
-
dy = amplitude * Math.cos(frequency * (x * .01) + seed * 1.3);
|
|
366
|
-
}
|
|
367
|
-
const drawX = x + dx;
|
|
368
|
-
const drawY = baseline + dy;
|
|
369
|
-
let fillColor = color;
|
|
370
|
-
if (gradientEffect) {
|
|
371
|
-
const colors = gradientEffect.config.colors;
|
|
372
|
-
if (colors === "rainbow") {
|
|
373
|
-
const saturation = gradientEffect.config.saturation ?? 80;
|
|
374
|
-
const lightness = gradientEffect.config.lightness ?? 55;
|
|
375
|
-
fillColor = `hsl(${seed * 137.5 % 360}, ${saturation}%, ${lightness}%)`;
|
|
376
|
-
} else if (Array.isArray(colors) && colors.length > 0) fillColor = colors[Math.floor(seed) % colors.length];
|
|
377
|
-
}
|
|
378
|
-
ctx.save();
|
|
379
|
-
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
380
|
-
ctx.textBaseline = "alphabetic";
|
|
381
|
-
for (const glow of glowEffects) {
|
|
382
|
-
ctx.save();
|
|
383
|
-
ctx.shadowBlur = resolveCSSLength(glow.config.radius ?? 8, fontSize);
|
|
384
|
-
ctx.shadowColor = glow.config.color ?? color;
|
|
385
|
-
ctx.shadowOffsetX = glow.config.offsetX ?? 0;
|
|
386
|
-
ctx.shadowOffsetY = glow.config.offsetY ?? 0;
|
|
387
|
-
ctx.fillStyle = glow.config.color ?? color;
|
|
388
|
-
ctx.fillText(char, drawX, drawY);
|
|
389
|
-
ctx.restore();
|
|
390
|
-
}
|
|
391
|
-
ctx.fillStyle = fillColor;
|
|
392
|
-
ctx.fillText(char, drawX, drawY);
|
|
393
|
-
ctx.restore();
|
|
394
|
-
}
|
|
395
|
-
//#endregion
|
|
396
|
-
//#region src/lib/textLayout.ts
|
|
397
|
-
function computeTextLayout(text, fontFamily, fontSize, lineHeight, maxWidth) {
|
|
398
|
-
const fontStr = `${fontSize}px ${fontFamily}`;
|
|
399
|
-
const chars = graphemes(text);
|
|
400
|
-
const widthCache = /* @__PURE__ */ new Map();
|
|
401
|
-
const charWidths = [];
|
|
402
|
-
for (const char of chars) {
|
|
403
|
-
let w = widthCache.get(char);
|
|
404
|
-
if (w === void 0) {
|
|
405
|
-
if (char === "\n") w = 0;
|
|
406
|
-
else {
|
|
407
|
-
const r = layoutWithLines(prepareWithSegments(char, fontStr, { whiteSpace: "pre-wrap" }), Infinity, lineHeight);
|
|
408
|
-
w = r.lines.length > 0 ? r.lines[0].width / fontSize : 0;
|
|
409
|
-
}
|
|
410
|
-
widthCache.set(char, w);
|
|
411
|
-
}
|
|
412
|
-
charWidths.push(w);
|
|
413
|
-
}
|
|
414
|
-
const prepared = prepareWithSegments(text, fontStr, { whiteSpace: "pre-wrap" });
|
|
415
|
-
const singleLineResult = layoutWithLines(prepared, Infinity, lineHeight);
|
|
416
|
-
const intrinsicWidth = Math.max(0, ...singleLineResult.lines.map((l) => l.width)) / fontSize;
|
|
417
|
-
const result = layoutWithLines(prepared, maxWidth, lineHeight);
|
|
418
|
-
const utf16ToCodePoint = [];
|
|
419
|
-
for (let ci = 0; ci < chars.length; ci++) for (let j = 0; j < chars[ci].length; j++) utf16ToCodePoint.push(ci);
|
|
420
|
-
const lines = [];
|
|
421
|
-
let utf16Offset = 0;
|
|
422
|
-
for (const line of result.lines) {
|
|
423
|
-
const indices = [];
|
|
424
|
-
const seen = /* @__PURE__ */ new Set();
|
|
425
|
-
for (let i = 0; i < line.text.length; i++) {
|
|
426
|
-
const cpIdx = utf16ToCodePoint[utf16Offset + i];
|
|
427
|
-
if (!seen.has(cpIdx)) {
|
|
428
|
-
seen.add(cpIdx);
|
|
429
|
-
indices.push(cpIdx);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
utf16Offset += line.text.length;
|
|
433
|
-
if (utf16Offset < text.length && text[utf16Offset] === "\n") {
|
|
434
|
-
const cpIdx = utf16ToCodePoint[utf16Offset];
|
|
435
|
-
indices.push(cpIdx);
|
|
436
|
-
utf16Offset++;
|
|
437
|
-
}
|
|
438
|
-
lines.push(indices);
|
|
439
|
-
}
|
|
440
|
-
if (utf16Offset < text.length) {
|
|
441
|
-
const indices = [];
|
|
442
|
-
const seen = /* @__PURE__ */ new Set();
|
|
443
|
-
for (let i = utf16Offset; i < text.length; i++) {
|
|
444
|
-
const cpIdx = utf16ToCodePoint[i];
|
|
445
|
-
if (!seen.has(cpIdx)) {
|
|
446
|
-
seen.add(cpIdx);
|
|
447
|
-
indices.push(cpIdx);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
lines.push(indices);
|
|
451
|
-
}
|
|
452
|
-
const kernings = [];
|
|
453
|
-
const pairCache = /* @__PURE__ */ new Map();
|
|
454
|
-
for (let i = 0; i < chars.length - 1; i++) {
|
|
455
|
-
const a = chars[i];
|
|
456
|
-
const b = chars[i + 1];
|
|
457
|
-
if (a === "\n" || b === "\n") {
|
|
458
|
-
kernings.push(0);
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
const pair = `${a}${b}`;
|
|
462
|
-
let k = pairCache.get(pair);
|
|
463
|
-
if (k === void 0) {
|
|
464
|
-
const r = layoutWithLines(prepareWithSegments(pair, fontStr, { whiteSpace: "pre-wrap" }), Infinity, lineHeight);
|
|
465
|
-
k = (r.lines.length > 0 ? r.lines[0].width / fontSize : 0) - (widthCache.get(a) ?? 0) - (widthCache.get(b) ?? 0);
|
|
466
|
-
if (Math.abs(k) < .001) k = 0;
|
|
467
|
-
pairCache.set(pair, k);
|
|
468
|
-
}
|
|
469
|
-
kernings.push(k);
|
|
470
|
-
}
|
|
471
|
-
return {
|
|
472
|
-
lines,
|
|
473
|
-
charWidths,
|
|
474
|
-
kernings,
|
|
475
|
-
intrinsicWidth
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
//#endregion
|
|
479
|
-
//#region src/lib/timeline.ts
|
|
480
|
-
const DEFAULTS = {
|
|
481
|
-
glyphGap: .1,
|
|
482
|
-
wordGap: .15,
|
|
483
|
-
lineGap: .3,
|
|
484
|
-
unknownDuration: .2
|
|
485
|
-
};
|
|
486
|
-
function computeTimeline(text, font, config) {
|
|
487
|
-
const glyphGap = config?.glyphGap ?? DEFAULTS.glyphGap;
|
|
488
|
-
const wordGap = config?.wordGap ?? DEFAULTS.wordGap;
|
|
489
|
-
const lineGap = config?.lineGap ?? DEFAULTS.lineGap;
|
|
490
|
-
const unknownDuration = config?.unknownDuration ?? DEFAULTS.unknownDuration;
|
|
491
|
-
const chars = graphemes(text);
|
|
492
|
-
const entries = [];
|
|
493
|
-
let offset = 0;
|
|
494
|
-
for (const char of chars) {
|
|
495
|
-
const glyph = font.glyphData[char];
|
|
496
|
-
const hasGlyph = !!glyph;
|
|
497
|
-
const duration = hasGlyph ? glyph.t ?? 1 : unknownDuration;
|
|
498
|
-
entries.push({
|
|
499
|
-
char,
|
|
500
|
-
offset,
|
|
501
|
-
duration,
|
|
502
|
-
hasGlyph
|
|
503
|
-
});
|
|
504
|
-
offset += duration;
|
|
505
|
-
if (char === "\n") offset += lineGap;
|
|
506
|
-
else if (char === " ") offset += wordGap;
|
|
507
|
-
else offset += glyphGap;
|
|
508
|
-
}
|
|
509
|
-
if (entries.length > 0) {
|
|
510
|
-
const lastChar = chars[chars.length - 1];
|
|
511
|
-
offset -= lastChar === "\n" ? lineGap : lastChar === " " ? wordGap : glyphGap;
|
|
512
|
-
}
|
|
513
|
-
return {
|
|
514
|
-
entries,
|
|
515
|
-
totalDuration: Math.max(0, offset)
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
//#endregion
|
|
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
|
-
}
|
|
550
|
-
const PADDING_H_EM = .2;
|
|
551
|
-
const MIN_LINE_HEIGHT_EM = 1.8;
|
|
552
|
-
const MIN_PADDING_V_EM = .2;
|
|
553
|
-
const CSS_TIME = "--tegaki-time";
|
|
554
|
-
const CSS_PROGRESS = "--tegaki-progress";
|
|
555
|
-
const CSS_DURATION = "--tegaki-duration";
|
|
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
|
-
}
|
|
573
|
-
function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete, effects, segmentSize, timing, showOverlay, ...props }) {
|
|
574
|
-
registerCssProperties();
|
|
575
|
-
const resolvedText = text ?? coerceToString(children);
|
|
576
|
-
const resolvedEffects = useMemo(() => resolveEffects(effects), [effects]);
|
|
577
|
-
const [seed] = useState(() => Math.random() * 1e3);
|
|
578
|
-
const timeControl = timeProp == null ? { mode: "uncontrolled" } : typeof timeProp === "number" ? {
|
|
579
|
-
mode: "controlled",
|
|
580
|
-
value: timeProp
|
|
581
|
-
} : timeProp === "css" ? { mode: "css" } : timeProp;
|
|
582
|
-
const isCss = timeControl.mode === "css";
|
|
583
|
-
const isControlled = timeControl.mode === "controlled" || isCss;
|
|
584
|
-
const controlledTime = timeControl.mode === "controlled" ? timeControl.value : void 0;
|
|
585
|
-
const defaultTime = timeControl.mode === "uncontrolled" ? timeControl.initialTime ?? 0 : 0;
|
|
586
|
-
const speed = timeControl.mode === "uncontrolled" ? timeControl.speed ?? 1 : 1;
|
|
587
|
-
const propPlaying = timeControl.mode === "uncontrolled" ? timeControl.playing ?? true : false;
|
|
588
|
-
const loop = timeControl.mode === "uncontrolled" ? timeControl.loop ?? false : false;
|
|
589
|
-
const catchUp = timeControl.mode === "uncontrolled" ? timeControl.catchUp ?? 0 : 0;
|
|
590
|
-
const onTimeChange = timeControl.mode === "uncontrolled" ? timeControl.onTimeChange : void 0;
|
|
591
|
-
const [playingOverride, setPlayingOverride] = useState(void 0);
|
|
592
|
-
const playing = playingOverride ?? propPlaying;
|
|
593
|
-
const prevPropPlaying = useRef(propPlaying);
|
|
594
|
-
if (prevPropPlaying.current !== propPlaying) {
|
|
595
|
-
prevPropPlaying.current = propPlaying;
|
|
596
|
-
setPlayingOverride(void 0);
|
|
597
|
-
}
|
|
598
|
-
const [internalTime, setInternalTime] = useState(defaultTime);
|
|
599
|
-
const [cssTime, setCssTime] = useState(0);
|
|
600
|
-
const currentTime = isCss ? cssTime : isControlled ? controlledTime : internalTime;
|
|
601
|
-
const currentTimeRef = useRef(currentTime);
|
|
602
|
-
currentTimeRef.current = currentTime;
|
|
603
|
-
const playingRef = useRef(playing);
|
|
604
|
-
playingRef.current = playing;
|
|
605
|
-
const isControlledRef = useRef(isControlled);
|
|
606
|
-
isControlledRef.current = isControlled;
|
|
607
|
-
const onTimeChangeRef = useRef(onTimeChange);
|
|
608
|
-
onTimeChangeRef.current = onTimeChange;
|
|
609
|
-
const onCompleteRef = useRef(onComplete);
|
|
610
|
-
onCompleteRef.current = onComplete;
|
|
611
|
-
const [loadedFont, setLoadedFont] = useState(() => font && ensureFont(font.family, font.fontUrl) === null ? font : null);
|
|
612
|
-
const fontReady = !!font && loadedFont === font;
|
|
613
|
-
useEffect(() => {
|
|
614
|
-
if (!font) {
|
|
615
|
-
setLoadedFont(null);
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
const pending = ensureFont(font.family, font.fontUrl);
|
|
619
|
-
if (pending === null) {
|
|
620
|
-
setLoadedFont(font);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
let cancelled = false;
|
|
624
|
-
pending.then(() => {
|
|
625
|
-
if (!cancelled) setLoadedFont(font);
|
|
626
|
-
});
|
|
627
|
-
return () => {
|
|
628
|
-
cancelled = true;
|
|
629
|
-
};
|
|
630
|
-
}, [font]);
|
|
631
|
-
const fontFamily = font?.family;
|
|
632
|
-
const emHeight = font ? (font.ascender - font.descender) / font.unitsPerEm : 0;
|
|
633
|
-
const rootRef = useRef(null);
|
|
634
|
-
const [containerWidth, setContainerWidth] = useState(0);
|
|
635
|
-
const [fontSize, setFontSize] = useState(0);
|
|
636
|
-
const [lineHeight, setLineHeight] = useState(0);
|
|
637
|
-
const [currentColor, setCurrentColor] = useState("");
|
|
638
|
-
const timeline = useMemo(() => font && resolvedText ? computeTimeline(resolvedText, font, timing) : {
|
|
639
|
-
entries: [],
|
|
640
|
-
totalDuration: 0
|
|
641
|
-
}, [
|
|
642
|
-
resolvedText,
|
|
643
|
-
font,
|
|
644
|
-
timing
|
|
645
|
-
]);
|
|
646
|
-
const totalDurationRef = useRef(timeline.totalDuration);
|
|
647
|
-
totalDurationRef.current = timeline.totalDuration;
|
|
648
|
-
const smoothedBoostRef = useRef(0);
|
|
649
|
-
const prevCompletedRef = useRef(false);
|
|
650
|
-
const isComplete = timeline.totalDuration > 0 && currentTime >= timeline.totalDuration;
|
|
651
|
-
useEffect(() => {
|
|
652
|
-
if (isComplete && !prevCompletedRef.current) {
|
|
653
|
-
prevCompletedRef.current = true;
|
|
654
|
-
onCompleteRef.current?.();
|
|
655
|
-
} else if (!isComplete) prevCompletedRef.current = false;
|
|
656
|
-
});
|
|
657
|
-
useImperativeHandle(ref, () => ({
|
|
658
|
-
getElement: () => rootRef.current,
|
|
659
|
-
getCurrentTime: () => currentTimeRef.current,
|
|
660
|
-
getDuration: () => totalDurationRef.current,
|
|
661
|
-
getIsPlaying: () => playingRef.current,
|
|
662
|
-
getIsComplete: () => totalDurationRef.current > 0 && currentTimeRef.current >= totalDurationRef.current,
|
|
663
|
-
play: () => {
|
|
664
|
-
if (!isControlledRef.current) setPlayingOverride(true);
|
|
665
|
-
},
|
|
666
|
-
pause: () => {
|
|
667
|
-
if (!isControlledRef.current) setPlayingOverride(false);
|
|
668
|
-
},
|
|
669
|
-
seek: (time) => {
|
|
670
|
-
if (!isControlledRef.current) setInternalTime(Math.max(0, Math.min(time, totalDurationRef.current)));
|
|
671
|
-
},
|
|
672
|
-
restart: () => {
|
|
673
|
-
if (!isControlledRef.current) {
|
|
674
|
-
setInternalTime(0);
|
|
675
|
-
setPlayingOverride(true);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}), []);
|
|
679
|
-
useEffect(() => {
|
|
680
|
-
if (!isControlled) onTimeChangeRef.current?.(internalTime);
|
|
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
|
-
]);
|
|
697
|
-
useEffect(() => {
|
|
698
|
-
if (isControlled || !playing || !font || !fontReady || prefersReducedMotion) return;
|
|
699
|
-
smoothedBoostRef.current = 0;
|
|
700
|
-
let lastTs = null;
|
|
701
|
-
let raf;
|
|
702
|
-
const attackRate = 4;
|
|
703
|
-
const releaseRate = loop ? 30 : 2;
|
|
704
|
-
const tick = (ts) => {
|
|
705
|
-
if (lastTs === null) lastTs = ts;
|
|
706
|
-
const dtSec = (ts - lastTs) / 1e3;
|
|
707
|
-
lastTs = ts;
|
|
708
|
-
setInternalTime((prev) => {
|
|
709
|
-
const totalDur = totalDurationRef.current;
|
|
710
|
-
if (totalDur === 0 || !loop && prev >= totalDur) return totalDur;
|
|
711
|
-
let effectiveSpeed = speed;
|
|
712
|
-
if (catchUp > 0) {
|
|
713
|
-
const remaining = Math.max(0, totalDur - prev);
|
|
714
|
-
const targetBoost = catchUp * Math.max(0, remaining - 2);
|
|
715
|
-
const rate = targetBoost > smoothedBoostRef.current ? attackRate : releaseRate;
|
|
716
|
-
smoothedBoostRef.current += (targetBoost - smoothedBoostRef.current) * (1 - Math.exp(-rate * dtSec));
|
|
717
|
-
effectiveSpeed = speed + smoothedBoostRef.current;
|
|
718
|
-
}
|
|
719
|
-
let next = prev + dtSec * effectiveSpeed;
|
|
720
|
-
if (next >= totalDur) {
|
|
721
|
-
next = loop ? next % totalDur : totalDur;
|
|
722
|
-
smoothedBoostRef.current = 0;
|
|
723
|
-
}
|
|
724
|
-
return next;
|
|
725
|
-
});
|
|
726
|
-
raf = requestAnimationFrame(tick);
|
|
727
|
-
};
|
|
728
|
-
raf = requestAnimationFrame(tick);
|
|
729
|
-
return () => cancelAnimationFrame(raf);
|
|
730
|
-
}, [
|
|
731
|
-
isControlled,
|
|
732
|
-
playing,
|
|
733
|
-
speed,
|
|
734
|
-
loop,
|
|
735
|
-
catchUp,
|
|
736
|
-
font,
|
|
737
|
-
fontReady,
|
|
738
|
-
prefersReducedMotion
|
|
739
|
-
]);
|
|
740
|
-
useEffect(() => {
|
|
741
|
-
const el = rootRef.current;
|
|
742
|
-
if (!el) return;
|
|
743
|
-
const ro = new ResizeObserver(([entry]) => {
|
|
744
|
-
if (entry) {
|
|
745
|
-
setContainerWidth(entry.contentRect.width);
|
|
746
|
-
const styles = getComputedStyle(el);
|
|
747
|
-
setFontSize(Number.parseFloat(styles.fontSize));
|
|
748
|
-
setLineHeight(Number.parseFloat(styles.lineHeight));
|
|
749
|
-
setCurrentColor(styles.color);
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
ro.observe(el);
|
|
753
|
-
return () => ro.disconnect();
|
|
754
|
-
}, []);
|
|
755
|
-
const sentinelRef = useRef(null);
|
|
756
|
-
useEffect(() => {
|
|
757
|
-
const el = sentinelRef.current;
|
|
758
|
-
if (!el) return;
|
|
759
|
-
const onTransition = (e) => {
|
|
760
|
-
const styles = getComputedStyle(el);
|
|
761
|
-
if (e.propertyName === "font-size" || e.propertyName === "line-height") {
|
|
762
|
-
setFontSize(Number.parseFloat(styles.fontSize));
|
|
763
|
-
setLineHeight(Number.parseFloat(styles.lineHeight));
|
|
764
|
-
}
|
|
765
|
-
if (e.propertyName === "color") setCurrentColor(styles.color);
|
|
766
|
-
if (e.propertyName === CSS_PROGRESS) setCssTime(Number(styles.getPropertyValue(CSS_PROGRESS)) * totalDurationRef.current);
|
|
767
|
-
};
|
|
768
|
-
el.addEventListener("transitionend", onTransition);
|
|
769
|
-
return () => el.removeEventListener("transitionend", onTransition);
|
|
770
|
-
}, []);
|
|
771
|
-
const layout = useMemo(() => {
|
|
772
|
-
if (!fontReady || !fontFamily || !fontSize || !containerWidth || !resolvedText) return null;
|
|
773
|
-
return computeTextLayout(resolvedText, fontFamily, fontSize, lineHeight, containerWidth);
|
|
774
|
-
}, [
|
|
775
|
-
fontReady,
|
|
776
|
-
resolvedText,
|
|
777
|
-
fontFamily,
|
|
778
|
-
fontSize,
|
|
779
|
-
lineHeight,
|
|
780
|
-
containerWidth
|
|
781
|
-
]);
|
|
782
|
-
const padH = PADDING_H_EM * fontSize;
|
|
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)`;
|
|
785
|
-
const canvasRef = useRef(null);
|
|
786
|
-
useLayoutEffect(() => {
|
|
787
|
-
const canvas = canvasRef.current;
|
|
788
|
-
if (!canvas || !font?.glyphData || !layout || !fontSize) return;
|
|
789
|
-
const dpr = window.devicePixelRatio || 1;
|
|
790
|
-
const el = rootRef.current;
|
|
791
|
-
if (!el) return;
|
|
792
|
-
const canvasRect = canvas.getBoundingClientRect();
|
|
793
|
-
const w = canvasRect.width;
|
|
794
|
-
const h = canvasRect.height;
|
|
795
|
-
if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
|
|
796
|
-
canvas.width = Math.round(w * dpr);
|
|
797
|
-
canvas.height = Math.round(h * dpr);
|
|
798
|
-
}
|
|
799
|
-
const ctx = canvas.getContext("2d");
|
|
800
|
-
if (!ctx) return;
|
|
801
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
802
|
-
ctx.clearRect(0, 0, w, h);
|
|
803
|
-
ctx.translate(padH, padV);
|
|
804
|
-
const color = currentColor || getComputedStyle(el).color;
|
|
805
|
-
const halfLeading = (lineHeight - emHeight * fontSize) / 2;
|
|
806
|
-
const characters = graphemes(resolvedText);
|
|
807
|
-
let y = 0;
|
|
808
|
-
for (const lineIndices of layout.lines) {
|
|
809
|
-
let x = 0;
|
|
810
|
-
for (const charIdx of lineIndices) {
|
|
811
|
-
const char = characters[charIdx];
|
|
812
|
-
if (char === "\n") continue;
|
|
813
|
-
const entry = timeline.entries[charIdx];
|
|
814
|
-
const charWidth = layout.charWidths[charIdx] ?? 0;
|
|
815
|
-
const kerning = layout.kernings[charIdx] ?? 0;
|
|
816
|
-
const glyph = font.glyphData[char];
|
|
817
|
-
if (glyph && entry.hasGlyph) {
|
|
818
|
-
const localTime = Math.max(0, Math.min(currentTime - entry.offset, entry.duration));
|
|
819
|
-
const glyphY = y + halfLeading;
|
|
820
|
-
drawGlyph(ctx, glyph, {
|
|
821
|
-
x,
|
|
822
|
-
y: glyphY,
|
|
823
|
-
fontSize,
|
|
824
|
-
unitsPerEm: font.unitsPerEm,
|
|
825
|
-
ascender: font.ascender,
|
|
826
|
-
descender: font.descender
|
|
827
|
-
}, localTime, font.lineCap, color, resolvedEffects, seed + charIdx, segmentSize);
|
|
828
|
-
} else if (!entry.hasGlyph && currentTime >= entry.offset + entry.duration) {
|
|
829
|
-
const baseline = y + halfLeading + font.ascender / font.unitsPerEm * fontSize;
|
|
830
|
-
drawFallbackGlyph(ctx, char, x, baseline, fontSize, fontFamily, color, resolvedEffects, seed + charIdx);
|
|
831
|
-
}
|
|
832
|
-
x += (charWidth + kerning) * fontSize;
|
|
833
|
-
}
|
|
834
|
-
y += lineHeight;
|
|
835
|
-
}
|
|
836
|
-
}, [
|
|
837
|
-
currentTime,
|
|
838
|
-
timeline,
|
|
839
|
-
layout,
|
|
840
|
-
font,
|
|
841
|
-
fontFamily,
|
|
842
|
-
fontSize,
|
|
843
|
-
lineHeight,
|
|
844
|
-
resolvedText,
|
|
845
|
-
emHeight,
|
|
846
|
-
padH,
|
|
847
|
-
padV,
|
|
848
|
-
currentColor,
|
|
849
|
-
resolvedEffects,
|
|
850
|
-
seed,
|
|
851
|
-
segmentSize
|
|
852
|
-
]);
|
|
853
|
-
return /* @__PURE__ */ jsx("div", {
|
|
854
|
-
ref: rootRef,
|
|
855
|
-
...props,
|
|
856
|
-
style: {
|
|
857
|
-
...props.style,
|
|
858
|
-
position: "relative",
|
|
859
|
-
maxWidth: "100%",
|
|
860
|
-
width: "auto",
|
|
861
|
-
height: "auto",
|
|
862
|
-
fontFamily,
|
|
863
|
-
[CSS_DURATION]: timeline.totalDuration,
|
|
864
|
-
[CSS_TIME]: currentTime,
|
|
865
|
-
[CSS_PROGRESS]: timeline.totalDuration > 0 ? currentTime / timeline.totalDuration : 0
|
|
866
|
-
},
|
|
867
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
868
|
-
style: { position: "relative" },
|
|
869
|
-
children: [
|
|
870
|
-
/* @__PURE__ */ jsx("span", {
|
|
871
|
-
ref: sentinelRef,
|
|
872
|
-
"aria-hidden": "true",
|
|
873
|
-
style: {
|
|
874
|
-
position: "absolute",
|
|
875
|
-
width: 0,
|
|
876
|
-
overflow: "hidden",
|
|
877
|
-
pointerEvents: "none",
|
|
878
|
-
fontSize: "inherit",
|
|
879
|
-
lineHeight: "inherit",
|
|
880
|
-
visibility: "hidden",
|
|
881
|
-
transition: isCss ? `font-size 0.001s, line-height 0.001s, color 0.001s, ${CSS_PROGRESS} 0.001s` : "font-size 0.001s, line-height 0.001s, color 0.001s"
|
|
882
|
-
},
|
|
883
|
-
children: "\xA0"
|
|
884
|
-
}),
|
|
885
|
-
/* @__PURE__ */ jsx("canvas", {
|
|
886
|
-
ref: canvasRef,
|
|
887
|
-
"aria-hidden": "true",
|
|
888
|
-
style: {
|
|
889
|
-
position: "absolute",
|
|
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
|
-
})
|
|
903
|
-
}),
|
|
904
|
-
/* @__PURE__ */ jsx("div", {
|
|
905
|
-
style: {
|
|
906
|
-
userSelect: "auto",
|
|
907
|
-
whiteSpace: "pre-wrap",
|
|
908
|
-
overflowWrap: "break-word",
|
|
909
|
-
paddingRight: 1,
|
|
910
|
-
WebkitTextFillColor: showOverlay ? void 0 : "transparent",
|
|
911
|
-
color: showOverlay ? "rgba(255, 0, 0, 0.4)" : void 0
|
|
912
|
-
},
|
|
913
|
-
children: resolvedText
|
|
914
|
-
})
|
|
915
|
-
]
|
|
916
|
-
})
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
//#endregion
|
|
920
|
-
export { TegakiRenderer, computeTimeline, drawGlyph, ensureFontFace };
|
|
921
|
-
|
|
922
|
-
//# sourceMappingURL=index.mjs.map
|
|
1
|
+
import { a as drawGlyph, i as ensureFontFace, n as computeTimeline, r as computeTextLayout, s as resolveEffects, t as TegakiEngine } from "./core-aZknK_0L.mjs";
|
|
2
|
+
import { t as TegakiRenderer } from "./react-Ddx6VfJc.mjs";
|
|
3
|
+
export { TegakiEngine, TegakiRenderer, computeTextLayout, computeTimeline, drawGlyph, ensureFontFace, resolveEffects };
|