tegaki 0.13.0 → 0.15.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 +50 -0
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{core-Ds9ohwR7.mjs → core-B7NiOWkP.mjs} +230 -55
- package/dist/core-B7NiOWkP.mjs.map +1 -0
- package/dist/{index-e-RN9Gi3.d.mts → index-B_c_g7Y-.d.mts} +122 -26
- package/dist/{index-BwhATGJw.d.mts → index-DMa72T_I.d.mts} +2 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/react/index.d.mts +3 -3
- package/dist/react/index.mjs +3 -3
- package/dist/{react-D3lP9bU2.mjs → react-ZkNwynZ5.mjs} +2 -2
- package/dist/{react-D3lP9bU2.mjs.map → react-ZkNwynZ5.mjs.map} +1 -1
- package/dist/solid/index.d.mts +2 -2
- package/dist/solid/index.mjs +2 -2
- package/dist/wc/index.d.mts +6 -5
- package/dist/wc/index.mjs +11 -6
- package/dist/wc/index.mjs.map +1 -1
- package/package.json +17 -4
- package/src/core/engine.ts +57 -8
- package/src/core/index.ts +11 -2
- package/src/core/types.ts +10 -1
- package/src/lib/catmullRom.ts +106 -0
- package/src/lib/drawFallbackGlyph.ts +6 -6
- package/src/lib/drawGlyph.ts +19 -10
- package/src/lib/effects.test.ts +188 -0
- package/src/lib/effects.ts +106 -3
- package/src/lib/strokeCache.ts +43 -17
- package/src/lib/textLayout.ts +47 -5
- package/src/nuxt/index.ts +2 -0
- package/src/nuxt/module.ts +41 -0
- package/src/types.test.ts +17 -7
- package/src/types.ts +13 -2
- package/src/wc/TegakiElement.ts +8 -3
- package/dist/core-Ds9ohwR7.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# tegaki
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ecba479: Split `gradient` into `strokeGradient` + `globalGradient`, and add render-stage hooks for layout-spanning effects. Closes #26.
|
|
8
|
+
|
|
9
|
+
**Breaking**
|
|
10
|
+
|
|
11
|
+
The `gradient` effect is renamed to `strokeGradient` with unchanged behavior (each stroke independently maps its progress to the color stops; `colors: 'rainbow'` still works). Rename the key in your `effects` prop:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// before
|
|
15
|
+
effects={{ gradient: { colors: ['#f00', '#00f'] } }}
|
|
16
|
+
// after
|
|
17
|
+
effects={{ strokeGradient: { colors: ['#f00', '#00f'] } }}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**New — `globalGradient`**
|
|
21
|
+
|
|
22
|
+
A canvas-space linear gradient that spans the full text bounding box — the leftmost pixel of the first glyph is `colors[0]` and the rightmost pixel of the last glyph is `colors[N]`, regardless of stroke boundaries. Matches CSS `background-clip: text` semantics.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
effects={{
|
|
26
|
+
globalGradient: {
|
|
27
|
+
colors: ['#f00', '#00f'],
|
|
28
|
+
angle: 0, // 0 = left→right (default); 90 = top→bottom; positive = clockwise
|
|
29
|
+
},
|
|
30
|
+
}}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`strokeGradient` and `globalGradient` can be enabled independently. If both are on, `strokeGradient` wins per segment (its per-stroke color overrides `globalGradient`'s canvas-wide paint); this combination is unusual but predictable.
|
|
34
|
+
|
|
35
|
+
**New — effect render-stage hooks**
|
|
36
|
+
|
|
37
|
+
Effects can now declare optional `beforeRender(stage, config)` / `afterRender(stage, config)` hooks on their `EffectDefinition` metadata. The stage context exposes the 2D context, the `TextLayout`, a pre-computed `LayoutBBox`, base color, and seed. Hooks run once around the glyph loop (before in forward order, after in reverse), so effects spanning the whole layout — like `globalGradient` — have a natural place to set up canvas state. Built-in per-stroke effects (`glow`, `wobble`, `pressureWidth`, `taper`, `strokeGradient`) declare no hooks and are unaffected.
|
|
38
|
+
|
|
39
|
+
**New public exports from `tegaki/core`**: `EffectDefinition`, `RenderStageContext`, `LayoutBBox`, `getEffectDefinition`, `hasRenderHooks`, `computeLayoutBbox`, plus the previously-private `findEffect` / `findEffects`.
|
|
40
|
+
|
|
41
|
+
## 0.14.0
|
|
42
|
+
|
|
43
|
+
### Minor Changes
|
|
44
|
+
|
|
45
|
+
- 79a0e6a: Add Nuxt module and usage example. Fixes [#35](https://github.com/KurtGokhan/tegaki/issues/35).
|
|
46
|
+
- 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.
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- 84ad2b2: text layout was broken when element had transform applied
|
|
51
|
+
- b6967aa: canvas was not cleared when all text removed
|
|
52
|
+
|
|
3
53
|
## 0.13.0
|
|
4
54
|
|
|
5
55
|
### Minor Changes
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as
|
|
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 };
|
|
1
|
+
import { A as computeLayoutBbox, B as Point, C as findEffect, D as resolveEffects, E as hasRenderHooks, F as CSSLength, G as TegakiEffects, H as TegakiBundle, I as FontOutput, J as TegakiSingletonEffectName, K as TegakiGlyphData, L as GlyphData, M as BBox, N as BUNDLE_VERSION, O as LayoutBBox, P as COMPATIBLE_BUNDLE_VERSIONS, R as LineCap, S as ResolvedEffect, T as getEffectDefinition, U as TegakiEffectConfigs, V as Stroke, W as TegakiEffectName, Y as TimedPoint, _ as computeTimeline, a as CreateElementFn, b as EffectDefinition, c as TimeControlMode, d as getBundle, f as registerBundle, g as TimelineEntry, h as TimelineConfig, i as TegakiEngine, j as computeTextLayout, k as TextLayout, l as TimeControlProp, m as Timeline, n as buildRootProps, o as TegakiEngineOptions, p as resolveBundle, q as TegakiMultiEffectName, r as domCreateElement, s as TegakiQuality, t as buildChildren, u as createBundle, v as ensureFontFace, w as findEffects, x as RenderStageContext, y as drawGlyph, z as PathCommand } from "../index-B_c_g7Y-.mjs";
|
|
2
|
+
export { BBox, BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, CSSLength, CreateElementFn, EffectDefinition, FontOutput, GlyphData, LayoutBBox, LineCap, PathCommand, Point, RenderStageContext, ResolvedEffect, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiQuality, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createBundle, c as resolveBundle, d as computeTimeline, f as
|
|
2
|
-
export { BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, TegakiEngine, buildChildren, buildRootProps, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, getBundle, registerBundle, resolveBundle, resolveEffects };
|
|
1
|
+
import { _ as findEffect, a as createBundle, b as hasRenderHooks, c as resolveBundle, d as computeTimeline, f as computeLayoutBbox, h as drawGlyph, i as domCreateElement, l as BUNDLE_VERSION, m as ensureFontFace, n as buildChildren, o as getBundle, p as computeTextLayout, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as COMPATIBLE_BUNDLE_VERSIONS, v as findEffects, x as resolveEffects, y as getEffectDefinition } from "../core-B7NiOWkP.mjs";
|
|
2
|
+
export { BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, TegakiEngine, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects };
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
//#region src/lib/effects.ts
|
|
2
2
|
const defaultEffects = { pressureWidth: true };
|
|
3
|
-
const knownEffects =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
const knownEffects = {
|
|
4
|
+
glow: {},
|
|
5
|
+
wobble: {},
|
|
6
|
+
pressureWidth: {},
|
|
7
|
+
taper: {},
|
|
8
|
+
strokeGradient: {},
|
|
9
|
+
globalGradient: { beforeRender(stage, config) {
|
|
10
|
+
const colors = config.colors;
|
|
11
|
+
if (!Array.isArray(colors) || colors.length === 0) return;
|
|
12
|
+
const { ctx, bbox } = stage;
|
|
13
|
+
const rad = (config.angle ?? 0) * Math.PI / 180;
|
|
14
|
+
const dx = Math.cos(rad);
|
|
15
|
+
const dy = Math.sin(rad);
|
|
16
|
+
const cx = bbox.x + bbox.width / 2;
|
|
17
|
+
const cy = bbox.y + bbox.height / 2;
|
|
18
|
+
const halfW = bbox.width / 2;
|
|
19
|
+
const halfH = bbox.height / 2;
|
|
20
|
+
const proj = Math.abs(dx * halfW) + Math.abs(dy * halfH);
|
|
21
|
+
const grad = ctx.createLinearGradient(cx - dx * proj, cy - dy * proj, cx + dx * proj, cy + dy * proj);
|
|
22
|
+
if (colors.length === 1) {
|
|
23
|
+
grad.addColorStop(0, colors[0]);
|
|
24
|
+
grad.addColorStop(1, colors[0]);
|
|
25
|
+
} else for (let i = 0; i < colors.length; i++) grad.addColorStop(i / (colors.length - 1), colors[i]);
|
|
26
|
+
stage.strokeStyle = grad;
|
|
27
|
+
} }
|
|
28
|
+
};
|
|
10
29
|
/**
|
|
11
30
|
* Normalizes an effects record into a sorted array of resolved effects.
|
|
12
31
|
* Known keys infer the effect name; custom keys read it from the `effect` field.
|
|
@@ -24,13 +43,13 @@ function resolveEffects(effects) {
|
|
|
24
43
|
let config;
|
|
25
44
|
let order;
|
|
26
45
|
if (value === true) {
|
|
27
|
-
effectName =
|
|
46
|
+
effectName = Object.hasOwn(knownEffects, key) ? key : void 0;
|
|
28
47
|
if (!effectName) continue;
|
|
29
48
|
config = {};
|
|
30
49
|
order = 0;
|
|
31
50
|
} else {
|
|
32
51
|
if (value.enabled === false) continue;
|
|
33
|
-
effectName = value.effect ?? (
|
|
52
|
+
effectName = value.effect ?? (Object.hasOwn(knownEffects, key) ? key : void 0);
|
|
34
53
|
if (!effectName) continue;
|
|
35
54
|
const { effect: _, order: o, enabled: __, ...rest } = value;
|
|
36
55
|
config = rest;
|
|
@@ -53,6 +72,93 @@ function findEffect(effects, name) {
|
|
|
53
72
|
function findEffects(effects, name) {
|
|
54
73
|
return effects.filter((e) => e.effect === name);
|
|
55
74
|
}
|
|
75
|
+
/** Look up the render-hook metadata for an effect name. Unknown names return undefined. */
|
|
76
|
+
function getEffectDefinition(name) {
|
|
77
|
+
return Object.hasOwn(knownEffects, name) ? knownEffects[name] : void 0;
|
|
78
|
+
}
|
|
79
|
+
/** True when any resolved effect defines a before/after render hook. */
|
|
80
|
+
function hasRenderHooks(effects) {
|
|
81
|
+
for (const e of effects) {
|
|
82
|
+
const def = getEffectDefinition(e.effect);
|
|
83
|
+
if (def?.beforeRender || def?.afterRender) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/lib/catmullRom.ts
|
|
89
|
+
/**
|
|
90
|
+
* Sample `count` points along a centripetal Catmull-Rom segment from `p1` to
|
|
91
|
+
* `p2`, with neighbor control points `p0` and `p3`. Samples are emitted at
|
|
92
|
+
* `u = k/count` for `k = 1..count`, so the last sample equals `p2`.
|
|
93
|
+
*
|
|
94
|
+
* Centripetal parameterization (α = 0.5) avoids the cusps and self-loops that
|
|
95
|
+
* uniform/chordal Catmull-Rom can produce on sharp corners — relevant for the
|
|
96
|
+
* baked RDP-simplified polylines the renderer consumes.
|
|
97
|
+
*
|
|
98
|
+
* For endpoint segments where a neighbor is missing, pass a phantom point
|
|
99
|
+
* built with {@link reflect}. Zero-length chords are clamped to a tiny epsilon
|
|
100
|
+
* so the knot parameterization stays non-degenerate.
|
|
101
|
+
*/
|
|
102
|
+
function sampleCatmullRom(p0, p1, p2, p3, count) {
|
|
103
|
+
const d01 = Math.max(dist(p0, p1), 1e-6);
|
|
104
|
+
const d12 = Math.max(dist(p1, p2), 1e-6);
|
|
105
|
+
const d23 = Math.max(dist(p2, p3), 1e-6);
|
|
106
|
+
const t0 = 0;
|
|
107
|
+
const t1 = t0 + Math.sqrt(d01);
|
|
108
|
+
const t2 = t1 + Math.sqrt(d12);
|
|
109
|
+
const t3 = t2 + Math.sqrt(d23);
|
|
110
|
+
const out = new Array(count);
|
|
111
|
+
for (let k = 1; k <= count; k++) {
|
|
112
|
+
const t = t1 + k / count * (t2 - t1);
|
|
113
|
+
out[k - 1] = evalBarryGoldman(p0, p1, p2, p3, t0, t1, t2, t3, t);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Reflect `p` across `anchor` to produce a phantom neighbor for endpoint
|
|
119
|
+
* segments. The result lies on the extension of (p, anchor) past `anchor` at
|
|
120
|
+
* the same distance — equivalent to a zero-curvature extrapolation, which
|
|
121
|
+
* gives a natural straight start/end tangent.
|
|
122
|
+
*/
|
|
123
|
+
function reflect(anchor, p) {
|
|
124
|
+
return [
|
|
125
|
+
2 * anchor[0] - p[0],
|
|
126
|
+
2 * anchor[1] - p[1],
|
|
127
|
+
anchor[2]
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
function dist(a, b) {
|
|
131
|
+
const dx = b[0] - a[0];
|
|
132
|
+
const dy = b[1] - a[1];
|
|
133
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
134
|
+
}
|
|
135
|
+
function evalBarryGoldman(p0, p1, p2, p3, t0, t1, t2, t3, t) {
|
|
136
|
+
const a1x = lerp(p0[0], p1[0], t0, t1, t);
|
|
137
|
+
const a1y = lerp(p0[1], p1[1], t0, t1, t);
|
|
138
|
+
const a1w = lerp(p0[2], p1[2], t0, t1, t);
|
|
139
|
+
const a2x = lerp(p1[0], p2[0], t1, t2, t);
|
|
140
|
+
const a2y = lerp(p1[1], p2[1], t1, t2, t);
|
|
141
|
+
const a2w = lerp(p1[2], p2[2], t1, t2, t);
|
|
142
|
+
const a3x = lerp(p2[0], p3[0], t2, t3, t);
|
|
143
|
+
const a3y = lerp(p2[1], p3[1], t2, t3, t);
|
|
144
|
+
const a3w = lerp(p2[2], p3[2], t2, t3, t);
|
|
145
|
+
const b1x = lerp(a1x, a2x, t0, t2, t);
|
|
146
|
+
const b1y = lerp(a1y, a2y, t0, t2, t);
|
|
147
|
+
const b1w = lerp(a1w, a2w, t0, t2, t);
|
|
148
|
+
const b2x = lerp(a2x, a3x, t1, t3, t);
|
|
149
|
+
const b2y = lerp(a2y, a3y, t1, t3, t);
|
|
150
|
+
const b2w = lerp(a2w, a3w, t1, t3, t);
|
|
151
|
+
return {
|
|
152
|
+
x: lerp(b1x, b2x, t1, t2, t),
|
|
153
|
+
y: lerp(b1y, b2y, t1, t2, t),
|
|
154
|
+
width: lerp(b1w, b2w, t1, t2, t)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function lerp(a, b, ta, tb, t) {
|
|
158
|
+
const span = tb - ta;
|
|
159
|
+
if (span === 0) return a;
|
|
160
|
+
return a + (b - a) * ((t - ta) / span);
|
|
161
|
+
}
|
|
56
162
|
//#endregion
|
|
57
163
|
//#region src/lib/strokeCache.ts
|
|
58
164
|
/**
|
|
@@ -60,11 +166,17 @@ function findEffects(effects, name) {
|
|
|
60
166
|
* Pass `Infinity` (or any non-finite value) to skip subdivision and return
|
|
61
167
|
* the raw polyline.
|
|
62
168
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
169
|
+
* When `smoothing` is true, intermediate vertices are placed on a centripetal
|
|
170
|
+
* Catmull-Rom spline through the original points (see `catmullRom.ts`) —
|
|
171
|
+
* hiding the polyline facets that show up at large render sizes. The original
|
|
172
|
+
* points remain on the curve, so endpoints and `cumLen`/`idx` semantics are
|
|
173
|
+
* preserved. Has no effect when `maxSegLen` is non-finite (no subdivision).
|
|
174
|
+
*
|
|
175
|
+
* Output depends only on `(stroke.p, maxSegLen, smoothing)` — not on position,
|
|
176
|
+
* seed, progress, or effect config — so it can be cached and shared across
|
|
177
|
+
* every instance of the same glyph at the same font size.
|
|
66
178
|
*/
|
|
67
|
-
function subdivideStroke(stroke, maxSegLen) {
|
|
179
|
+
function subdivideStroke(stroke, maxSegLen, smoothing = false) {
|
|
68
180
|
const pts = stroke.p;
|
|
69
181
|
const n = pts.length;
|
|
70
182
|
if (n === 0) return {
|
|
@@ -86,20 +198,43 @@ function subdivideStroke(stroke, maxSegLen) {
|
|
|
86
198
|
const cur = pts[j];
|
|
87
199
|
const dx = cur[0] - prev[0];
|
|
88
200
|
const dy = cur[1] - prev[1];
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
201
|
+
const chordLen = Math.sqrt(dx * dx + dy * dy);
|
|
202
|
+
const count = chordLen > 0 && Number.isFinite(maxSegLen) && maxSegLen > 0 ? Math.max(1, Math.ceil(chordLen / maxSegLen)) : 1;
|
|
203
|
+
if (smoothing && count > 1) {
|
|
204
|
+
const samples = sampleCatmullRom(j >= 2 ? pts[j - 2] : reflect(prev, cur), prev, cur, j + 1 < n ? pts[j + 1] : reflect(cur, prev), count);
|
|
205
|
+
let px = prev[0];
|
|
206
|
+
let py = prev[1];
|
|
207
|
+
let segAccum = 0;
|
|
208
|
+
for (let k = 0; k < count; k++) {
|
|
209
|
+
const s = samples[k];
|
|
210
|
+
const ex = s.x - px;
|
|
211
|
+
const ey = s.y - py;
|
|
212
|
+
segAccum += Math.sqrt(ex * ex + ey * ey);
|
|
213
|
+
vertices.push({
|
|
214
|
+
x: s.x,
|
|
215
|
+
y: s.y,
|
|
216
|
+
width: s.width,
|
|
217
|
+
cumLen: cumLen + segAccum,
|
|
218
|
+
idx: j - 1 + (k + 1) / count
|
|
219
|
+
});
|
|
220
|
+
px = s.x;
|
|
221
|
+
py = s.y;
|
|
222
|
+
}
|
|
223
|
+
cumLen += segAccum;
|
|
224
|
+
} else {
|
|
225
|
+
const dw = cur[2] - prev[2];
|
|
226
|
+
for (let k = 1; k <= count; k++) {
|
|
227
|
+
const t = k / count;
|
|
228
|
+
vertices.push({
|
|
229
|
+
x: prev[0] + dx * t,
|
|
230
|
+
y: prev[1] + dy * t,
|
|
231
|
+
width: prev[2] + dw * t,
|
|
232
|
+
cumLen: cumLen + chordLen * t,
|
|
233
|
+
idx: j - 1 + t
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
cumLen += chordLen;
|
|
101
237
|
}
|
|
102
|
-
cumLen += segLen;
|
|
103
238
|
}
|
|
104
239
|
let widthSum = 0;
|
|
105
240
|
for (const p of pts) widthSum += p[2];
|
|
@@ -210,7 +345,8 @@ function defaultStrokeEasing(t) {
|
|
|
210
345
|
* font, fontSize, or segment size changes; if omitted here, strokes are
|
|
211
346
|
* subdivided inline each call (useful for testing).
|
|
212
347
|
*/
|
|
213
|
-
function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], seed = 0, getSubdivided, strokeEasing = defaultStrokeEasing, strokeScale = 1) {
|
|
348
|
+
function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], seed = 0, getSubdivided, strokeEasing = defaultStrokeEasing, strokeScale = 1, strokeStyleOverride) {
|
|
349
|
+
const defaultStrokePaint = strokeStyleOverride ?? color;
|
|
214
350
|
const scale = pos.fontSize / pos.unitsPerEm;
|
|
215
351
|
const ox = pos.x;
|
|
216
352
|
const oy = pos.y;
|
|
@@ -218,7 +354,7 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
218
354
|
const wobbleEffect = findEffect(effects, "wobble");
|
|
219
355
|
const pressureEffect = findEffect(effects, "pressureWidth");
|
|
220
356
|
const taperEffect = findEffect(effects, "taper");
|
|
221
|
-
const
|
|
357
|
+
const strokeGradientEffect = findEffect(effects, "strokeGradient");
|
|
222
358
|
const pressureAmount = pressureEffect ? Math.max(0, Math.min(pressureEffect.config.strength ?? 1, 1)) : 0;
|
|
223
359
|
const wobbleAmplitude = wobbleEffect ? wobbleEffect.config.amplitude ?? 1.5 : 0;
|
|
224
360
|
const wobbleFrequency = wobbleEffect ? wobbleEffect.config.frequency ?? 8 : 0;
|
|
@@ -226,12 +362,12 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
226
362
|
const hasWobble = !!wobbleEffect;
|
|
227
363
|
const taperStart = taperEffect ? Math.max(0, Math.min(taperEffect.config.startLength ?? .15, 1)) : 0;
|
|
228
364
|
const taperEnd = taperEffect ? Math.max(0, Math.min(taperEffect.config.endLength ?? .15, 1)) : 0;
|
|
229
|
-
const gradientColors =
|
|
365
|
+
const gradientColors = strokeGradientEffect?.config.colors;
|
|
230
366
|
const isRainbow = gradientColors === "rainbow";
|
|
231
367
|
const gradientColorStops = Array.isArray(gradientColors) ? gradientColors : void 0;
|
|
232
|
-
const gradientSaturation =
|
|
233
|
-
const gradientLightness =
|
|
234
|
-
const
|
|
368
|
+
const gradientSaturation = strokeGradientEffect?.config.saturation ?? 80;
|
|
369
|
+
const gradientLightness = strokeGradientEffect?.config.lightness ?? 55;
|
|
370
|
+
const hasStrokeGradient = !!strokeGradientEffect;
|
|
235
371
|
const needsPerSegment = pressureAmount > 0 || !!taperEffect;
|
|
236
372
|
const subdivide = getSubdivided ?? ((s) => subdivideStroke(s, Infinity));
|
|
237
373
|
const wobbleDx = (_x, y, idx) => {
|
|
@@ -285,7 +421,7 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
285
421
|
ctx.fill();
|
|
286
422
|
ctx.restore();
|
|
287
423
|
}
|
|
288
|
-
ctx.fillStyle = colorAt(0);
|
|
424
|
+
ctx.fillStyle = hasStrokeGradient ? colorAt(0) : defaultStrokePaint;
|
|
289
425
|
ctx.beginPath();
|
|
290
426
|
if (lineCap === "round") {
|
|
291
427
|
ctx.arc(dotX, dotY, dotWidth / 2, 0, Math.PI * 2);
|
|
@@ -355,8 +491,8 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
355
491
|
ctx.stroke();
|
|
356
492
|
ctx.restore();
|
|
357
493
|
}
|
|
358
|
-
if (!needsPerSegment && !
|
|
359
|
-
ctx.strokeStyle =
|
|
494
|
+
if (!needsPerSegment && !hasStrokeGradient) {
|
|
495
|
+
ctx.strokeStyle = defaultStrokePaint;
|
|
360
496
|
ctx.lineWidth = baseLineWidth;
|
|
361
497
|
tracePolyline();
|
|
362
498
|
ctx.stroke();
|
|
@@ -374,7 +510,7 @@ function drawGlyph(ctx, glyph, pos, localTime, lineCap, color, effects = [], see
|
|
|
374
510
|
lw = Math.max(baseLineWidth + (perPoint - baseLineWidth) * pressureAmount, .5 * scale * strokeScale) * taperMultiplier(midProgress);
|
|
375
511
|
}
|
|
376
512
|
ctx.lineWidth = lw;
|
|
377
|
-
ctx.strokeStyle =
|
|
513
|
+
ctx.strokeStyle = hasStrokeGradient ? colorAt(midProgress) : defaultStrokePaint;
|
|
378
514
|
ctx.beginPath();
|
|
379
515
|
ctx.moveTo(txs[i - 1], tys[i - 1]);
|
|
380
516
|
ctx.lineTo(txs[i], tys[i]);
|
|
@@ -410,6 +546,24 @@ function ensureFont(family, url) {
|
|
|
410
546
|
}
|
|
411
547
|
//#endregion
|
|
412
548
|
//#region src/lib/textLayout.ts
|
|
549
|
+
/**
|
|
550
|
+
* Compute the text bounding box from a measured layout. Inputs are in CSS
|
|
551
|
+
* pixels. Assumes the layout's char offsets are em-relative to the left edge
|
|
552
|
+
* of each line (as produced by `computeTextLayout`).
|
|
553
|
+
*/
|
|
554
|
+
function computeLayoutBbox(layout, fontSize, lineHeight) {
|
|
555
|
+
let maxRight = 0;
|
|
556
|
+
for (const lineIndices of layout.lines) for (const charIdx of lineIndices) {
|
|
557
|
+
const right = ((layout.charOffsets[charIdx] ?? 0) + (layout.charWidths[charIdx] ?? 0)) * fontSize;
|
|
558
|
+
if (right > maxRight) maxRight = right;
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
x: 0,
|
|
562
|
+
y: 0,
|
|
563
|
+
width: maxRight,
|
|
564
|
+
height: layout.lines.length * lineHeight
|
|
565
|
+
};
|
|
566
|
+
}
|
|
413
567
|
function computeTextLayout(elOrText, fontSize, fontFamily, lineHeight, maxWidth) {
|
|
414
568
|
if (typeof elOrText === "string") return measureWithTempElement(elOrText, fontFamily, fontSize, lineHeight, maxWidth);
|
|
415
569
|
return measureElement(elOrText, fontSize);
|
|
@@ -427,7 +581,9 @@ function measureElement(el, fontSize) {
|
|
|
427
581
|
charOffsets: [],
|
|
428
582
|
charWidths: []
|
|
429
583
|
};
|
|
430
|
-
const
|
|
584
|
+
const elRect = el.getBoundingClientRect();
|
|
585
|
+
const elLeft = elRect.left;
|
|
586
|
+
const scale = el.offsetWidth > 0 ? elRect.width / el.offsetWidth : 1;
|
|
431
587
|
const range = document.createRange();
|
|
432
588
|
const charOffsets = [];
|
|
433
589
|
const charWidths = [];
|
|
@@ -458,13 +614,13 @@ function measureElement(el, fontSize) {
|
|
|
458
614
|
continue;
|
|
459
615
|
}
|
|
460
616
|
const rect = rects[rects.length - 1];
|
|
461
|
-
if (currentLine.length > 0 && rect.top - prevTop > fontSize * .25) {
|
|
617
|
+
if (currentLine.length > 0 && rect.top - prevTop > fontSize * .25 * scale) {
|
|
462
618
|
lines.push(currentLine);
|
|
463
619
|
currentLine = [];
|
|
464
620
|
}
|
|
465
621
|
if (currentLine.length === 0) prevTop = rect.top;
|
|
466
|
-
charOffsets.push((rect.left - elLeft) / fontSize);
|
|
467
|
-
charWidths.push(rect.width / fontSize);
|
|
622
|
+
charOffsets.push((rect.left - elLeft) / scale / fontSize);
|
|
623
|
+
charWidths.push(rect.width / scale / fontSize);
|
|
468
624
|
currentLine.push(i);
|
|
469
625
|
}
|
|
470
626
|
if (currentLine.length > 0) lines.push(currentLine);
|
|
@@ -639,12 +795,12 @@ function registerCssProperties() {
|
|
|
639
795
|
//#endregion
|
|
640
796
|
//#region src/lib/drawFallbackGlyph.ts
|
|
641
797
|
/**
|
|
642
|
-
* Draw a fallback glyph (plain text) with applicable effects (glow,
|
|
798
|
+
* Draw a fallback glyph (plain text) with applicable effects (glow, strokeGradient, wobble).
|
|
643
799
|
*/
|
|
644
800
|
function drawFallbackGlyph(ctx, char, x, baseline, fontSize, fontFamily, color, effects = [], seed = 0) {
|
|
645
801
|
const glowEffects = findEffects(effects, "glow");
|
|
646
802
|
const wobbleEffect = findEffect(effects, "wobble");
|
|
647
|
-
const
|
|
803
|
+
const strokeGradientEffect = findEffect(effects, "strokeGradient");
|
|
648
804
|
let dx = 0;
|
|
649
805
|
let dy = 0;
|
|
650
806
|
if (wobbleEffect) {
|
|
@@ -656,11 +812,11 @@ function drawFallbackGlyph(ctx, char, x, baseline, fontSize, fontFamily, color,
|
|
|
656
812
|
const drawX = x + dx;
|
|
657
813
|
const drawY = baseline + dy;
|
|
658
814
|
let fillColor = color;
|
|
659
|
-
if (
|
|
660
|
-
const colors =
|
|
815
|
+
if (strokeGradientEffect) {
|
|
816
|
+
const colors = strokeGradientEffect.config.colors;
|
|
661
817
|
if (colors === "rainbow") {
|
|
662
|
-
const saturation =
|
|
663
|
-
const lightness =
|
|
818
|
+
const saturation = strokeGradientEffect.config.saturation ?? 80;
|
|
819
|
+
const lightness = strokeGradientEffect.config.lightness ?? 55;
|
|
664
820
|
fillColor = `hsl(${seed * 137.5 % 360}, ${saturation}%, ${lightness}%)`;
|
|
665
821
|
} else if (Array.isArray(colors) && colors.length > 0) fillColor = colors[Math.floor(seed) % colors.length];
|
|
666
822
|
}
|
|
@@ -1114,7 +1270,8 @@ var TegakiEngine = class {
|
|
|
1114
1270
|
}
|
|
1115
1271
|
}
|
|
1116
1272
|
if (e.propertyName === "--tegaki-progress") {
|
|
1117
|
-
|
|
1273
|
+
const rawProgress = Number(styles.getPropertyValue(CSS_PROGRESS));
|
|
1274
|
+
this._cssTime = rawProgress * this._timeline.totalDuration;
|
|
1118
1275
|
changed = true;
|
|
1119
1276
|
}
|
|
1120
1277
|
if (changed) this._render();
|
|
@@ -1265,7 +1422,6 @@ var TegakiEngine = class {
|
|
|
1265
1422
|
const font = this._font;
|
|
1266
1423
|
const layout = this._layout;
|
|
1267
1424
|
const fontSize = this._fontSize;
|
|
1268
|
-
if (!font?.glyphData || !layout || !fontSize) return;
|
|
1269
1425
|
const effectiveDpr = (window.devicePixelRatio || 1) * Math.max(this._quality?.pixelRatio ?? 1, 0);
|
|
1270
1426
|
const w = canvas.offsetWidth;
|
|
1271
1427
|
const h = canvas.offsetHeight;
|
|
@@ -1277,6 +1433,7 @@ var TegakiEngine = class {
|
|
|
1277
1433
|
if (!ctx) return;
|
|
1278
1434
|
ctx.setTransform(effectiveDpr, 0, 0, effectiveDpr, 0, 0);
|
|
1279
1435
|
ctx.clearRect(0, 0, w, h);
|
|
1436
|
+
if (!font?.glyphData || !layout || !fontSize) return;
|
|
1280
1437
|
const padH = PADDING_H_EM * fontSize;
|
|
1281
1438
|
const lineHeight = this._lineHeight;
|
|
1282
1439
|
const padV = Math.max(MIN_PADDING_V_EM * fontSize, (MIN_LINE_HEIGHT_EM * fontSize - lineHeight) / 2);
|
|
@@ -1285,14 +1442,15 @@ var TegakiEngine = class {
|
|
|
1285
1442
|
const halfLeading = (lineHeight - (font.ascender - font.descender) / font.unitsPerEm * fontSize) / 2;
|
|
1286
1443
|
const characters = graphemes(this._text);
|
|
1287
1444
|
const currentTime = this.currentTime;
|
|
1288
|
-
const effectsNeedSubdivision = !!findEffect(this._resolvedEffects, "wobble") || !!findEffect(this._resolvedEffects, "
|
|
1445
|
+
const effectsNeedSubdivision = !!findEffect(this._resolvedEffects, "wobble") || !!findEffect(this._resolvedEffects, "strokeGradient") || !!findEffect(this._resolvedEffects, "taper") || (() => {
|
|
1289
1446
|
const p = findEffect(this._resolvedEffects, "pressureWidth");
|
|
1290
1447
|
return !!p && Math.max(0, Math.min(p.config.strength ?? 1, 1)) > 0;
|
|
1291
1448
|
})();
|
|
1292
|
-
const
|
|
1449
|
+
const smoothing = this._quality?.smoothing === true;
|
|
1450
|
+
const resolvedSegmentSize = this._quality?.segmentSize ?? (effectsNeedSubdivision || smoothing ? 2 : void 0);
|
|
1293
1451
|
const scale = fontSize / font.unitsPerEm;
|
|
1294
1452
|
const maxSegLenFU = resolvedSegmentSize != null ? resolvedSegmentSize / scale : Infinity;
|
|
1295
|
-
const cacheKey = `${font.family}|${maxSegLenFU}`;
|
|
1453
|
+
const cacheKey = `${font.family}|${maxSegLenFU}|${smoothing ? "s" : "l"}`;
|
|
1296
1454
|
if (cacheKey !== this._strokeCacheKey) {
|
|
1297
1455
|
this._strokeCache = /* @__PURE__ */ new WeakMap();
|
|
1298
1456
|
this._strokeCacheKey = cacheKey;
|
|
@@ -1301,13 +1459,26 @@ var TegakiEngine = class {
|
|
|
1301
1459
|
const getSubdivided = (stroke) => {
|
|
1302
1460
|
let sub = strokeCache.get(stroke);
|
|
1303
1461
|
if (!sub) {
|
|
1304
|
-
sub = subdivideStroke(stroke, maxSegLenFU);
|
|
1462
|
+
sub = subdivideStroke(stroke, maxSegLenFU, smoothing);
|
|
1305
1463
|
strokeCache.set(stroke, sub);
|
|
1306
1464
|
}
|
|
1307
1465
|
return sub;
|
|
1308
1466
|
};
|
|
1309
1467
|
const clipText = this._quality?.clipText;
|
|
1310
1468
|
const strokeScale = typeof clipText === "number" ? clipText : 1;
|
|
1469
|
+
const stage = hasRenderHooks(this._resolvedEffects) ? {
|
|
1470
|
+
ctx,
|
|
1471
|
+
layout,
|
|
1472
|
+
fontSize,
|
|
1473
|
+
lineHeight,
|
|
1474
|
+
unitsPerEm: font.unitsPerEm,
|
|
1475
|
+
ascender: font.ascender,
|
|
1476
|
+
descender: font.descender,
|
|
1477
|
+
bbox: computeLayoutBbox(layout, fontSize, lineHeight),
|
|
1478
|
+
baseColor: color,
|
|
1479
|
+
seed: this._seed
|
|
1480
|
+
} : null;
|
|
1481
|
+
if (stage) for (const effect of this._resolvedEffects) getEffectDefinition(effect.effect)?.beforeRender?.(stage, effect.config);
|
|
1311
1482
|
let y = 0;
|
|
1312
1483
|
for (const lineIndices of layout.lines) {
|
|
1313
1484
|
for (const charIdx of lineIndices) {
|
|
@@ -1327,11 +1498,15 @@ var TegakiEngine = class {
|
|
|
1327
1498
|
unitsPerEm: font.unitsPerEm,
|
|
1328
1499
|
ascender: font.ascender,
|
|
1329
1500
|
descender: font.descender
|
|
1330
|
-
}, localTime, font.lineCap, color, this._resolvedEffects, this._seed + charIdx, getSubdivided, this._timing?.strokeEasing, strokeScale);
|
|
1501
|
+
}, localTime, font.lineCap, color, this._resolvedEffects, this._seed + charIdx, getSubdivided, this._timing?.strokeEasing, strokeScale, stage?.strokeStyle);
|
|
1331
1502
|
} 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);
|
|
1332
1503
|
}
|
|
1333
1504
|
y += lineHeight;
|
|
1334
1505
|
}
|
|
1506
|
+
if (stage) for (let i = this._resolvedEffects.length - 1; i >= 0; i--) {
|
|
1507
|
+
const effect = this._resolvedEffects[i];
|
|
1508
|
+
getEffectDefinition(effect.effect)?.afterRender?.(stage, effect.config);
|
|
1509
|
+
}
|
|
1335
1510
|
if (clipText) {
|
|
1336
1511
|
if (!this._maskCanvas) this._maskCanvas = document.createElement("canvas");
|
|
1337
1512
|
const maskCanvas = this._maskCanvas;
|
|
@@ -1365,6 +1540,6 @@ var TegakiEngine = class {
|
|
|
1365
1540
|
}
|
|
1366
1541
|
};
|
|
1367
1542
|
//#endregion
|
|
1368
|
-
export { createBundle as a, resolveBundle as c, computeTimeline as d,
|
|
1543
|
+
export { findEffect as _, createBundle as a, hasRenderHooks as b, resolveBundle as c, computeTimeline as d, computeLayoutBbox as f, coerceToString as g, drawGlyph as h, domCreateElement as i, BUNDLE_VERSION as l, ensureFontFace as m, buildChildren as n, getBundle as o, computeTextLayout as p, buildRootProps as r, registerBundle as s, TegakiEngine as t, COMPATIBLE_BUNDLE_VERSIONS as u, findEffects as v, resolveEffects as x, getEffectDefinition as y };
|
|
1369
1544
|
|
|
1370
|
-
//# sourceMappingURL=core-
|
|
1545
|
+
//# sourceMappingURL=core-B7NiOWkP.mjs.map
|