tegaki 0.12.0 → 0.14.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 +25 -0
- package/dist/core/index.d.mts +1 -1
- package/dist/core/index.mjs +1 -1
- package/dist/{core-CIzLDu_Q.mjs → core-BYU5BEaZ.mjs} +202 -48
- package/dist/core-BYU5BEaZ.mjs.map +1 -0
- package/dist/{index-DtvcCtXG.d.mts → index-BBRejOMe.d.mts} +24 -2
- package/dist/{index-d5-O-2U1.d.mts → index-DlYttK-R.d.mts} +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react/index.d.mts +2 -2
- package/dist/react/index.mjs +2 -2
- package/dist/{react-CVoAINjv.mjs → react-0gtC94df.mjs} +2 -2
- package/dist/{react-CVoAINjv.mjs.map → react-0gtC94df.mjs.map} +1 -1
- package/dist/solid/index.d.mts +1 -1
- package/dist/solid/index.mjs +1 -1
- package/dist/wc/index.d.mts +5 -4
- package/dist/wc/index.mjs +10 -5
- package/dist/wc/index.mjs.map +1 -1
- package/package.json +17 -4
- package/src/core/engine.ts +65 -11
- package/src/core/render-elements.ts +5 -1
- package/src/core/types.ts +21 -0
- package/src/lib/catmullRom.ts +106 -0
- package/src/lib/drawGlyph.ts +6 -5
- package/src/lib/strokeCache.ts +43 -17
- package/src/lib/textLayout.ts +13 -6
- package/src/lib/timeline.ts +6 -4
- package/src/lib/utils.ts +8 -2
- package/src/nuxt/index.ts +2 -0
- package/src/nuxt/module.ts +41 -0
- package/src/wc/TegakiElement.ts +8 -3
- package/dist/core-CIzLDu_Q.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# tegaki
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 79a0e6a: Add Nuxt module and usage example. Fixes [#35](https://github.com/KurtGokhan/tegaki/issues/35).
|
|
8
|
+
- 9a0d74a: Add `quality.smoothing` option that interpolates stroke points with a centripetal Catmull-Rom spline, hiding the faceted corners visible at large render sizes where the baked polyline resolution shows through. Enabling it forces subdivision on (default `segmentSize=2` CSS px) and rebuilds the subdivision cache; the original points stay on the curve, so animation timing and wobble phase are unchanged. Default is `false` (existing bundles render identically). Also exposed on the web component as the `smoothing` attribute.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 84ad2b2: text layout was broken when element had transform applied
|
|
13
|
+
- b6967aa: canvas was not cleared when all text removed
|
|
14
|
+
|
|
15
|
+
## 0.13.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 8fd875a: Add `clipText` quality option that clips handwriting strokes to the filled text shape using canvas composite operations. Accepts `true` for clipping with normal stroke widths, or a number to scale stroke widths (e.g. `2` for 2x wider strokes that fill more of the glyph interior).
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 2a46c09: fix compatibility with old Safari versions, and a bug with text layout when text is wrapped. Fixes [#29](https://github.com/KurtGokhan/tegaki/issues/29)
|
|
24
|
+
- cdb2993: Fix timing around whitespace characters. Spaces and line breaks no longer consume `unknownDuration` on top of `wordGap`/`lineGap` — the gap alone now represents the full pause. `\r\n` and `\r` are normalized to `\n`, and all Unicode whitespace (NBSP, tab, ideographic space, etc.) is treated as a word gap.
|
|
25
|
+
|
|
26
|
+
Fixes [#28](https://github.com/KurtGokhan/tegaki/issues/28)
|
|
27
|
+
|
|
3
28
|
## 0.12.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as LineCap, B as TegakiSingletonEffectName, C as resolveEffects, D as CSSLength, E as COMPATIBLE_BUNDLE_VERSIONS, F as TegakiEffectConfigs, I as TegakiEffectName, L as TegakiEffects, M as Point, N as Stroke, O as FontOutput, P as TegakiBundle, R as TegakiGlyphData, S as ResolvedEffect, T as BUNDLE_VERSION, V as TimedPoint, _ as computeTimeline, a as CreateElementFn, b as ensureFontFace, c as TimeControlMode, d as getBundle, f as registerBundle, g as TimelineEntry, h as TimelineConfig, i as TegakiEngine, j as PathCommand, k as GlyphData, l as TimeControlProp, m as Timeline, n as buildRootProps, o as TegakiEngineOptions, p as resolveBundle, r as domCreateElement, s as TegakiQuality, t as buildChildren, u as createBundle, v as TextLayout, w as BBox, x as drawGlyph, y as computeTextLayout, z as TegakiMultiEffectName } from "../index-
|
|
1
|
+
import { A as LineCap, B as TegakiSingletonEffectName, C as resolveEffects, D as CSSLength, E as COMPATIBLE_BUNDLE_VERSIONS, F as TegakiEffectConfigs, I as TegakiEffectName, L as TegakiEffects, M as Point, N as Stroke, O as FontOutput, P as TegakiBundle, R as TegakiGlyphData, S as ResolvedEffect, T as BUNDLE_VERSION, V as TimedPoint, _ as computeTimeline, a as CreateElementFn, b as ensureFontFace, c as TimeControlMode, d as getBundle, f as registerBundle, g as TimelineEntry, h as TimelineConfig, i as TegakiEngine, j as PathCommand, k as GlyphData, l as TimeControlProp, m as Timeline, n as buildRootProps, o as TegakiEngineOptions, p as resolveBundle, r as domCreateElement, s as TegakiQuality, t as buildChildren, u as createBundle, v as TextLayout, w as BBox, x as drawGlyph, y as computeTextLayout, z as TegakiMultiEffectName } from "../index-BBRejOMe.mjs";
|
|
2
2
|
export { BBox, BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, CSSLength, CreateElementFn, FontOutput, GlyphData, LineCap, PathCommand, Point, ResolvedEffect, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiQuality, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, buildChildren, buildRootProps, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, getBundle, registerBundle, resolveBundle, resolveEffects };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createBundle, c as resolveBundle, d as computeTimeline, f as computeTextLayout, g as resolveEffects, i as domCreateElement, l as BUNDLE_VERSION, m as drawGlyph, n as buildChildren, o as getBundle, p as ensureFontFace, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as COMPATIBLE_BUNDLE_VERSIONS } from "../core-
|
|
1
|
+
import { a as createBundle, c as resolveBundle, d as computeTimeline, f as computeTextLayout, g as resolveEffects, i as domCreateElement, l as BUNDLE_VERSION, m as drawGlyph, n as buildChildren, o as getBundle, p as ensureFontFace, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as COMPATIBLE_BUNDLE_VERSIONS } from "../core-BYU5BEaZ.mjs";
|
|
2
2
|
export { BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, TegakiEngine, buildChildren, buildRootProps, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, getBundle, registerBundle, resolveBundle, resolveEffects };
|
|
@@ -54,17 +54,98 @@ function findEffects(effects, name) {
|
|
|
54
54
|
return effects.filter((e) => e.effect === name);
|
|
55
55
|
}
|
|
56
56
|
//#endregion
|
|
57
|
+
//#region src/lib/catmullRom.ts
|
|
58
|
+
/**
|
|
59
|
+
* Sample `count` points along a centripetal Catmull-Rom segment from `p1` to
|
|
60
|
+
* `p2`, with neighbor control points `p0` and `p3`. Samples are emitted at
|
|
61
|
+
* `u = k/count` for `k = 1..count`, so the last sample equals `p2`.
|
|
62
|
+
*
|
|
63
|
+
* Centripetal parameterization (α = 0.5) avoids the cusps and self-loops that
|
|
64
|
+
* uniform/chordal Catmull-Rom can produce on sharp corners — relevant for the
|
|
65
|
+
* baked RDP-simplified polylines the renderer consumes.
|
|
66
|
+
*
|
|
67
|
+
* For endpoint segments where a neighbor is missing, pass a phantom point
|
|
68
|
+
* built with {@link reflect}. Zero-length chords are clamped to a tiny epsilon
|
|
69
|
+
* so the knot parameterization stays non-degenerate.
|
|
70
|
+
*/
|
|
71
|
+
function sampleCatmullRom(p0, p1, p2, p3, count) {
|
|
72
|
+
const d01 = Math.max(dist(p0, p1), 1e-6);
|
|
73
|
+
const d12 = Math.max(dist(p1, p2), 1e-6);
|
|
74
|
+
const d23 = Math.max(dist(p2, p3), 1e-6);
|
|
75
|
+
const t0 = 0;
|
|
76
|
+
const t1 = t0 + Math.sqrt(d01);
|
|
77
|
+
const t2 = t1 + Math.sqrt(d12);
|
|
78
|
+
const t3 = t2 + Math.sqrt(d23);
|
|
79
|
+
const out = new Array(count);
|
|
80
|
+
for (let k = 1; k <= count; k++) {
|
|
81
|
+
const t = t1 + k / count * (t2 - t1);
|
|
82
|
+
out[k - 1] = evalBarryGoldman(p0, p1, p2, p3, t0, t1, t2, t3, t);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reflect `p` across `anchor` to produce a phantom neighbor for endpoint
|
|
88
|
+
* segments. The result lies on the extension of (p, anchor) past `anchor` at
|
|
89
|
+
* the same distance — equivalent to a zero-curvature extrapolation, which
|
|
90
|
+
* gives a natural straight start/end tangent.
|
|
91
|
+
*/
|
|
92
|
+
function reflect(anchor, p) {
|
|
93
|
+
return [
|
|
94
|
+
2 * anchor[0] - p[0],
|
|
95
|
+
2 * anchor[1] - p[1],
|
|
96
|
+
anchor[2]
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
function dist(a, b) {
|
|
100
|
+
const dx = b[0] - a[0];
|
|
101
|
+
const dy = b[1] - a[1];
|
|
102
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
103
|
+
}
|
|
104
|
+
function evalBarryGoldman(p0, p1, p2, p3, t0, t1, t2, t3, t) {
|
|
105
|
+
const a1x = lerp(p0[0], p1[0], t0, t1, t);
|
|
106
|
+
const a1y = lerp(p0[1], p1[1], t0, t1, t);
|
|
107
|
+
const a1w = lerp(p0[2], p1[2], t0, t1, t);
|
|
108
|
+
const a2x = lerp(p1[0], p2[0], t1, t2, t);
|
|
109
|
+
const a2y = lerp(p1[1], p2[1], t1, t2, t);
|
|
110
|
+
const a2w = lerp(p1[2], p2[2], t1, t2, t);
|
|
111
|
+
const a3x = lerp(p2[0], p3[0], t2, t3, t);
|
|
112
|
+
const a3y = lerp(p2[1], p3[1], t2, t3, t);
|
|
113
|
+
const a3w = lerp(p2[2], p3[2], t2, t3, t);
|
|
114
|
+
const b1x = lerp(a1x, a2x, t0, t2, t);
|
|
115
|
+
const b1y = lerp(a1y, a2y, t0, t2, t);
|
|
116
|
+
const b1w = lerp(a1w, a2w, t0, t2, t);
|
|
117
|
+
const b2x = lerp(a2x, a3x, t1, t3, t);
|
|
118
|
+
const b2y = lerp(a2y, a3y, t1, t3, t);
|
|
119
|
+
const b2w = lerp(a2w, a3w, t1, t3, t);
|
|
120
|
+
return {
|
|
121
|
+
x: lerp(b1x, b2x, t1, t2, t),
|
|
122
|
+
y: lerp(b1y, b2y, t1, t2, t),
|
|
123
|
+
width: lerp(b1w, b2w, t1, t2, t)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function lerp(a, b, ta, tb, t) {
|
|
127
|
+
const span = tb - ta;
|
|
128
|
+
if (span === 0) return a;
|
|
129
|
+
return a + (b - a) * ((t - ta) / span);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
57
132
|
//#region src/lib/strokeCache.ts
|
|
58
133
|
/**
|
|
59
134
|
* Subdivide a stroke so that no sub-segment exceeds `maxSegLen` font units.
|
|
60
135
|
* Pass `Infinity` (or any non-finite value) to skip subdivision and return
|
|
61
136
|
* the raw polyline.
|
|
62
137
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
138
|
+
* When `smoothing` is true, intermediate vertices are placed on a centripetal
|
|
139
|
+
* Catmull-Rom spline through the original points (see `catmullRom.ts`) —
|
|
140
|
+
* hiding the polyline facets that show up at large render sizes. The original
|
|
141
|
+
* points remain on the curve, so endpoints and `cumLen`/`idx` semantics are
|
|
142
|
+
* preserved. Has no effect when `maxSegLen` is non-finite (no subdivision).
|
|
143
|
+
*
|
|
144
|
+
* Output depends only on `(stroke.p, maxSegLen, smoothing)` — not on position,
|
|
145
|
+
* seed, progress, or effect config — so it can be cached and shared across
|
|
146
|
+
* every instance of the same glyph at the same font size.
|
|
66
147
|
*/
|
|
67
|
-
function subdivideStroke(stroke, maxSegLen) {
|
|
148
|
+
function subdivideStroke(stroke, maxSegLen, smoothing = false) {
|
|
68
149
|
const pts = stroke.p;
|
|
69
150
|
const n = pts.length;
|
|
70
151
|
if (n === 0) return {
|
|
@@ -86,20 +167,43 @@ function subdivideStroke(stroke, maxSegLen) {
|
|
|
86
167
|
const cur = pts[j];
|
|
87
168
|
const dx = cur[0] - prev[0];
|
|
88
169
|
const dy = cur[1] - prev[1];
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
170
|
+
const chordLen = Math.sqrt(dx * dx + dy * dy);
|
|
171
|
+
const count = chordLen > 0 && Number.isFinite(maxSegLen) && maxSegLen > 0 ? Math.max(1, Math.ceil(chordLen / maxSegLen)) : 1;
|
|
172
|
+
if (smoothing && count > 1) {
|
|
173
|
+
const samples = sampleCatmullRom(j >= 2 ? pts[j - 2] : reflect(prev, cur), prev, cur, j + 1 < n ? pts[j + 1] : reflect(cur, prev), count);
|
|
174
|
+
let px = prev[0];
|
|
175
|
+
let py = prev[1];
|
|
176
|
+
let segAccum = 0;
|
|
177
|
+
for (let k = 0; k < count; k++) {
|
|
178
|
+
const s = samples[k];
|
|
179
|
+
const ex = s.x - px;
|
|
180
|
+
const ey = s.y - py;
|
|
181
|
+
segAccum += Math.sqrt(ex * ex + ey * ey);
|
|
182
|
+
vertices.push({
|
|
183
|
+
x: s.x,
|
|
184
|
+
y: s.y,
|
|
185
|
+
width: s.width,
|
|
186
|
+
cumLen: cumLen + segAccum,
|
|
187
|
+
idx: j - 1 + (k + 1) / count
|
|
188
|
+
});
|
|
189
|
+
px = s.x;
|
|
190
|
+
py = s.y;
|
|
191
|
+
}
|
|
192
|
+
cumLen += segAccum;
|
|
193
|
+
} else {
|
|
194
|
+
const dw = cur[2] - prev[2];
|
|
195
|
+
for (let k = 1; k <= count; k++) {
|
|
196
|
+
const t = k / count;
|
|
197
|
+
vertices.push({
|
|
198
|
+
x: prev[0] + dx * t,
|
|
199
|
+
y: prev[1] + dy * t,
|
|
200
|
+
width: prev[2] + dw * t,
|
|
201
|
+
cumLen: cumLen + chordLen * t,
|
|
202
|
+
idx: j - 1 + t
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
cumLen += chordLen;
|
|
101
206
|
}
|
|
102
|
-
cumLen += segLen;
|
|
103
207
|
}
|
|
104
208
|
let widthSum = 0;
|
|
105
209
|
for (const p of pts) widthSum += p[2];
|
|
@@ -111,14 +215,15 @@ function subdivideStroke(stroke, maxSegLen) {
|
|
|
111
215
|
}
|
|
112
216
|
//#endregion
|
|
113
217
|
//#region src/lib/utils.ts
|
|
114
|
-
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
218
|
+
const segmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
115
219
|
/** Resolve a CSSLength to pixels. Plain numbers are px, `"Nem"` is N * fontSize. */
|
|
116
220
|
function resolveCSSLength(value, fontSize) {
|
|
117
221
|
if (typeof value === "number") return value;
|
|
118
222
|
return parseFloat(value) * fontSize;
|
|
119
223
|
}
|
|
120
224
|
function graphemes(text) {
|
|
121
|
-
return Array.from(segmenter.segment(text), (s) => s.segment);
|
|
225
|
+
if (segmenter) return Array.from(segmenter.segment(text), (s) => s.segment);
|
|
226
|
+
return Array.from(text);
|
|
122
227
|
}
|
|
123
228
|
/**
|
|
124
229
|
* Build the CSS `font-family` value for a bundle, including the full
|
|
@@ -209,7 +314,7 @@ function defaultStrokeEasing(t) {
|
|
|
209
314
|
* font, fontSize, or segment size changes; if omitted here, strokes are
|
|
210
315
|
* subdivided inline each call (useful for testing).
|
|
211
316
|
*/
|
|
212
|
-
function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], seed = 0, getSubdivided, strokeEasing = defaultStrokeEasing) {
|
|
317
|
+
function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], seed = 0, getSubdivided, strokeEasing = defaultStrokeEasing, strokeScale = 1) {
|
|
213
318
|
const scale = pos.fontSize / pos.unitsPerEm;
|
|
214
319
|
const ox = pos.x;
|
|
215
320
|
const oy = pos.y;
|
|
@@ -268,8 +373,8 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
268
373
|
const p = rawPts[0];
|
|
269
374
|
const dotX = px(p[0] + wobbleDx(p[0], p[1], 0));
|
|
270
375
|
const dotY = py(p[1] + wobbleDy(p[0], p[1], 0));
|
|
271
|
-
const baseLineWidth = Math.max(p[2], .5) * scale;
|
|
272
|
-
let dotWidth = baseLineWidth + (Math.max(p[2], .5) * scale - baseLineWidth) * pressureAmount;
|
|
376
|
+
const baseLineWidth = Math.max(p[2], .5) * scale * strokeScale;
|
|
377
|
+
let dotWidth = baseLineWidth + (Math.max(p[2], .5) * scale * strokeScale - baseLineWidth) * pressureAmount;
|
|
273
378
|
dotWidth *= taperMultiplier(.5);
|
|
274
379
|
for (const glow of glowEffects) {
|
|
275
380
|
ctx.save();
|
|
@@ -296,7 +401,7 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
296
401
|
if (vertices.length < 2 || totalLen <= 0) continue;
|
|
297
402
|
const drawLen = totalLen * progress;
|
|
298
403
|
if (drawLen <= 0) continue;
|
|
299
|
-
const baseLineWidth = Math.max(avgWidth, .5) * scale;
|
|
404
|
+
const baseLineWidth = Math.max(avgWidth, .5) * scale * strokeScale;
|
|
300
405
|
let lo = 0;
|
|
301
406
|
let hi = vertices.length - 1;
|
|
302
407
|
while (lo < hi) {
|
|
@@ -369,8 +474,8 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
369
474
|
const midProgress = (aCum + bCum) * .5 * invTotalLen;
|
|
370
475
|
let lw = baseLineWidth;
|
|
371
476
|
if (needsPerSegment) {
|
|
372
|
-
const perPoint = (aWidth + bWidth) * .5 * scale;
|
|
373
|
-
lw = Math.max(baseLineWidth + (perPoint - baseLineWidth) * pressureAmount, .5 * scale) * taperMultiplier(midProgress);
|
|
477
|
+
const perPoint = (aWidth + bWidth) * .5 * scale * strokeScale;
|
|
478
|
+
lw = Math.max(baseLineWidth + (perPoint - baseLineWidth) * pressureAmount, .5 * scale * strokeScale) * taperMultiplier(midProgress);
|
|
374
479
|
}
|
|
375
480
|
ctx.lineWidth = lw;
|
|
376
481
|
ctx.strokeStyle = hasGradient ? colorAt(midProgress) : color;
|
|
@@ -426,7 +531,9 @@ function measureElement(el, fontSize) {
|
|
|
426
531
|
charOffsets: [],
|
|
427
532
|
charWidths: []
|
|
428
533
|
};
|
|
429
|
-
const
|
|
534
|
+
const elRect = el.getBoundingClientRect();
|
|
535
|
+
const elLeft = elRect.left;
|
|
536
|
+
const scale = el.offsetWidth > 0 ? elRect.width / el.offsetWidth : 1;
|
|
430
537
|
const range = document.createRange();
|
|
431
538
|
const charOffsets = [];
|
|
432
539
|
const charWidths = [];
|
|
@@ -456,14 +563,14 @@ function measureElement(el, fontSize) {
|
|
|
456
563
|
currentLine.push(i);
|
|
457
564
|
continue;
|
|
458
565
|
}
|
|
459
|
-
const rect = rects[
|
|
460
|
-
if (currentLine.length > 0 && rect.top - prevTop > fontSize * .25) {
|
|
566
|
+
const rect = rects[rects.length - 1];
|
|
567
|
+
if (currentLine.length > 0 && rect.top - prevTop > fontSize * .25 * scale) {
|
|
461
568
|
lines.push(currentLine);
|
|
462
569
|
currentLine = [];
|
|
463
570
|
}
|
|
464
571
|
if (currentLine.length === 0) prevTop = rect.top;
|
|
465
|
-
charOffsets.push((rect.left - elLeft) / fontSize);
|
|
466
|
-
charWidths.push(rect.width / fontSize);
|
|
572
|
+
charOffsets.push((rect.left - elLeft) / scale / fontSize);
|
|
573
|
+
charWidths.push(rect.width / scale / fontSize);
|
|
467
574
|
currentLine.push(i);
|
|
468
575
|
}
|
|
469
576
|
if (currentLine.length > 0) lines.push(currentLine);
|
|
@@ -510,7 +617,9 @@ function computeTimeline(text, font, config) {
|
|
|
510
617
|
for (const char of chars) {
|
|
511
618
|
const glyph = font.glyphData[char];
|
|
512
619
|
const hasGlyph = !!glyph;
|
|
513
|
-
const
|
|
620
|
+
const isLineBreak = char === "\n";
|
|
621
|
+
const isWhitespace = isLineBreak || /^\s+$/.test(char);
|
|
622
|
+
const duration = isWhitespace ? 0 : hasGlyph ? glyph.t ?? unknownDuration : unknownDuration;
|
|
514
623
|
entries.push({
|
|
515
624
|
char,
|
|
516
625
|
offset,
|
|
@@ -518,13 +627,14 @@ function computeTimeline(text, font, config) {
|
|
|
518
627
|
hasGlyph
|
|
519
628
|
});
|
|
520
629
|
offset += duration;
|
|
521
|
-
if (
|
|
522
|
-
else if (
|
|
630
|
+
if (isLineBreak) offset += lineGap;
|
|
631
|
+
else if (isWhitespace) offset += wordGap;
|
|
523
632
|
else offset += glyphGap;
|
|
524
633
|
}
|
|
525
634
|
if (entries.length > 0) {
|
|
526
635
|
const lastChar = chars[chars.length - 1];
|
|
527
|
-
|
|
636
|
+
const trailingGap = lastChar === "\n" ? lineGap : /^\s+$/.test(lastChar) ? wordGap : glyphGap;
|
|
637
|
+
offset -= trailingGap;
|
|
528
638
|
}
|
|
529
639
|
return {
|
|
530
640
|
entries,
|
|
@@ -730,7 +840,10 @@ function buildChildren(options, h) {
|
|
|
730
840
|
"aria-hidden": "true",
|
|
731
841
|
style: {
|
|
732
842
|
position: "absolute",
|
|
733
|
-
|
|
843
|
+
top: `calc(-1 * ${PAD_V_CSS})`,
|
|
844
|
+
right: "-0.2em",
|
|
845
|
+
bottom: `calc(-1 * ${PAD_V_CSS})`,
|
|
846
|
+
left: "-0.2em",
|
|
734
847
|
width: "calc(100% + 0.4em)",
|
|
735
848
|
height: `calc(100% + 2 * ${PAD_V_CSS})`,
|
|
736
849
|
pointerEvents: "none",
|
|
@@ -792,6 +905,7 @@ var TegakiEngine = class {
|
|
|
792
905
|
_canvasEl;
|
|
793
906
|
_overlayEl;
|
|
794
907
|
_canvasFallbackEl;
|
|
908
|
+
_maskCanvas = null;
|
|
795
909
|
_text = "";
|
|
796
910
|
_font = null;
|
|
797
911
|
_timeControl = { mode: "uncontrolled" };
|
|
@@ -865,7 +979,8 @@ var TegakiEngine = class {
|
|
|
865
979
|
if (typeof window !== "undefined") {
|
|
866
980
|
this._mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
867
981
|
this._prefersReducedMotion = this._mql.matches;
|
|
868
|
-
this._mql.addEventListener("change", this._onReducedMotionChange);
|
|
982
|
+
if (this._mql.addEventListener) this._mql.addEventListener("change", this._onReducedMotionChange);
|
|
983
|
+
else this._mql.addListener(this._onReducedMotionChange);
|
|
869
984
|
}
|
|
870
985
|
this._measure();
|
|
871
986
|
if (options) this.update(options);
|
|
@@ -929,10 +1044,13 @@ var TegakiEngine = class {
|
|
|
929
1044
|
let dirtyLayout = false;
|
|
930
1045
|
let dirtyRender = false;
|
|
931
1046
|
let dirtyPlayback = false;
|
|
932
|
-
if ("text" in options
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1047
|
+
if ("text" in options) {
|
|
1048
|
+
const nextText = (options.text ?? "").replace(/\r\n?/g, "\n");
|
|
1049
|
+
if (nextText !== this._text) {
|
|
1050
|
+
this._text = nextText;
|
|
1051
|
+
dirtyTimeline = true;
|
|
1052
|
+
dirtyLayout = true;
|
|
1053
|
+
}
|
|
936
1054
|
}
|
|
937
1055
|
if ("font" in options) {
|
|
938
1056
|
const resolved = resolveBundle(options.font) ?? null;
|
|
@@ -1000,10 +1118,12 @@ var TegakiEngine = class {
|
|
|
1000
1118
|
this._stopLoop();
|
|
1001
1119
|
this._resizeObserver.disconnect();
|
|
1002
1120
|
this._sentinelEl.removeEventListener("transitionend", this._onSentinelTransition);
|
|
1003
|
-
this._mql
|
|
1121
|
+
if (this._mql) if (this._mql.removeEventListener) this._mql.removeEventListener("change", this._onReducedMotionChange);
|
|
1122
|
+
else this._mql.removeListener(this._onReducedMotionChange);
|
|
1004
1123
|
this._contentEl?.remove();
|
|
1005
1124
|
this._strokeCache = /* @__PURE__ */ new WeakMap();
|
|
1006
1125
|
this._strokeCacheKey = "";
|
|
1126
|
+
this._maskCanvas = null;
|
|
1007
1127
|
}
|
|
1008
1128
|
/** Estimate line-height from font metrics when CSS returns "normal". */
|
|
1009
1129
|
_fallbackLineHeight(fontSize) {
|
|
@@ -1100,7 +1220,8 @@ var TegakiEngine = class {
|
|
|
1100
1220
|
}
|
|
1101
1221
|
}
|
|
1102
1222
|
if (e.propertyName === "--tegaki-progress") {
|
|
1103
|
-
|
|
1223
|
+
const rawProgress = Number(styles.getPropertyValue(CSS_PROGRESS));
|
|
1224
|
+
this._cssTime = rawProgress * this._timeline.totalDuration;
|
|
1104
1225
|
changed = true;
|
|
1105
1226
|
}
|
|
1106
1227
|
if (changed) this._render();
|
|
@@ -1251,7 +1372,6 @@ var TegakiEngine = class {
|
|
|
1251
1372
|
const font = this._font;
|
|
1252
1373
|
const layout = this._layout;
|
|
1253
1374
|
const fontSize = this._fontSize;
|
|
1254
|
-
if (!font?.glyphData || !layout || !fontSize) return;
|
|
1255
1375
|
const effectiveDpr = (window.devicePixelRatio || 1) * Math.max(this._quality?.pixelRatio ?? 1, 0);
|
|
1256
1376
|
const w = canvas.offsetWidth;
|
|
1257
1377
|
const h = canvas.offsetHeight;
|
|
@@ -1263,6 +1383,7 @@ var TegakiEngine = class {
|
|
|
1263
1383
|
if (!ctx) return;
|
|
1264
1384
|
ctx.setTransform(effectiveDpr, 0, 0, effectiveDpr, 0, 0);
|
|
1265
1385
|
ctx.clearRect(0, 0, w, h);
|
|
1386
|
+
if (!font?.glyphData || !layout || !fontSize) return;
|
|
1266
1387
|
const padH = PADDING_H_EM * fontSize;
|
|
1267
1388
|
const lineHeight = this._lineHeight;
|
|
1268
1389
|
const padV = Math.max(MIN_PADDING_V_EM * fontSize, (MIN_LINE_HEIGHT_EM * fontSize - lineHeight) / 2);
|
|
@@ -1275,10 +1396,11 @@ var TegakiEngine = class {
|
|
|
1275
1396
|
const p = findEffect(this._resolvedEffects, "pressureWidth");
|
|
1276
1397
|
return !!p && Math.max(0, Math.min(p.config.strength ?? 1, 1)) > 0;
|
|
1277
1398
|
})();
|
|
1278
|
-
const
|
|
1399
|
+
const smoothing = this._quality?.smoothing === true;
|
|
1400
|
+
const resolvedSegmentSize = this._quality?.segmentSize ?? (effectsNeedSubdivision || smoothing ? 2 : void 0);
|
|
1279
1401
|
const scale = fontSize / font.unitsPerEm;
|
|
1280
1402
|
const maxSegLenFU = resolvedSegmentSize != null ? resolvedSegmentSize / scale : Infinity;
|
|
1281
|
-
const cacheKey = `${font.family}|${maxSegLenFU}`;
|
|
1403
|
+
const cacheKey = `${font.family}|${maxSegLenFU}|${smoothing ? "s" : "l"}`;
|
|
1282
1404
|
if (cacheKey !== this._strokeCacheKey) {
|
|
1283
1405
|
this._strokeCache = /* @__PURE__ */ new WeakMap();
|
|
1284
1406
|
this._strokeCacheKey = cacheKey;
|
|
@@ -1287,11 +1409,13 @@ var TegakiEngine = class {
|
|
|
1287
1409
|
const getSubdivided = (stroke) => {
|
|
1288
1410
|
let sub = strokeCache.get(stroke);
|
|
1289
1411
|
if (!sub) {
|
|
1290
|
-
sub = subdivideStroke(stroke, maxSegLenFU);
|
|
1412
|
+
sub = subdivideStroke(stroke, maxSegLenFU, smoothing);
|
|
1291
1413
|
strokeCache.set(stroke, sub);
|
|
1292
1414
|
}
|
|
1293
1415
|
return sub;
|
|
1294
1416
|
};
|
|
1417
|
+
const clipText = this._quality?.clipText;
|
|
1418
|
+
const strokeScale = typeof clipText === "number" ? clipText : 1;
|
|
1295
1419
|
let y = 0;
|
|
1296
1420
|
for (const lineIndices of layout.lines) {
|
|
1297
1421
|
for (const charIdx of lineIndices) {
|
|
@@ -1311,14 +1435,44 @@ var TegakiEngine = class {
|
|
|
1311
1435
|
unitsPerEm: font.unitsPerEm,
|
|
1312
1436
|
ascender: font.ascender,
|
|
1313
1437
|
descender: font.descender
|
|
1314
|
-
}, localTime, font.lineCap, color, this._resolvedEffects, this._seed + charIdx, getSubdivided, this._timing?.strokeEasing);
|
|
1438
|
+
}, localTime, font.lineCap, color, this._resolvedEffects, this._seed + charIdx, getSubdivided, this._timing?.strokeEasing, strokeScale);
|
|
1315
1439
|
} else if (!entry.hasGlyph && currentTime >= entry.offset + entry.duration) drawFallbackGlyph(ctx, char, x, y + halfLeading + font.ascender / font.unitsPerEm * fontSize, fontSize, cssFontFamily(font), color, this._resolvedEffects, this._seed + charIdx);
|
|
1316
1440
|
}
|
|
1317
1441
|
y += lineHeight;
|
|
1318
1442
|
}
|
|
1443
|
+
if (clipText) {
|
|
1444
|
+
if (!this._maskCanvas) this._maskCanvas = document.createElement("canvas");
|
|
1445
|
+
const maskCanvas = this._maskCanvas;
|
|
1446
|
+
if (maskCanvas.width !== canvas.width || maskCanvas.height !== canvas.height) {
|
|
1447
|
+
maskCanvas.width = canvas.width;
|
|
1448
|
+
maskCanvas.height = canvas.height;
|
|
1449
|
+
}
|
|
1450
|
+
const maskCtx = maskCanvas.getContext("2d");
|
|
1451
|
+
maskCtx.setTransform(effectiveDpr, 0, 0, effectiveDpr, 0, 0);
|
|
1452
|
+
maskCtx.clearRect(0, 0, w, h);
|
|
1453
|
+
maskCtx.translate(padH, padV);
|
|
1454
|
+
maskCtx.font = `${fontSize}px ${cssFontFamily(font)}`;
|
|
1455
|
+
maskCtx.textBaseline = "alphabetic";
|
|
1456
|
+
let clipY = 0;
|
|
1457
|
+
for (const lineIndices of layout.lines) {
|
|
1458
|
+
for (const charIdx of lineIndices) {
|
|
1459
|
+
const char = characters[charIdx];
|
|
1460
|
+
if (char === "\n") continue;
|
|
1461
|
+
const x = (layout.charOffsets[charIdx] ?? 0) * fontSize;
|
|
1462
|
+
const baseline = clipY + halfLeading + font.ascender / font.unitsPerEm * fontSize;
|
|
1463
|
+
maskCtx.fillText(char, x, baseline);
|
|
1464
|
+
}
|
|
1465
|
+
clipY += lineHeight;
|
|
1466
|
+
}
|
|
1467
|
+
ctx.save();
|
|
1468
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1469
|
+
ctx.globalCompositeOperation = "destination-in";
|
|
1470
|
+
ctx.drawImage(maskCanvas, 0, 0);
|
|
1471
|
+
ctx.restore();
|
|
1472
|
+
}
|
|
1319
1473
|
}
|
|
1320
1474
|
};
|
|
1321
1475
|
//#endregion
|
|
1322
1476
|
export { createBundle as a, resolveBundle as c, computeTimeline as d, computeTextLayout as f, resolveEffects as g, coerceToString as h, domCreateElement as i, BUNDLE_VERSION as l, drawGlyph as m, buildChildren as n, getBundle as o, ensureFontFace as p, buildRootProps as r, registerBundle as s, TegakiEngine as t, COMPATIBLE_BUNDLE_VERSIONS as u };
|
|
1323
1477
|
|
|
1324
|
-
//# sourceMappingURL=core-
|
|
1478
|
+
//# sourceMappingURL=core-BYU5BEaZ.mjs.map
|