modern-text 1.11.1 → 2.0.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.
Files changed (30) hide show
  1. package/README.md +283 -22
  2. package/dist/deformations/index.cjs +566 -0
  3. package/dist/deformations/index.d.cts +11 -0
  4. package/dist/deformations/index.d.mts +11 -0
  5. package/dist/deformations/index.d.ts +11 -0
  6. package/dist/deformations/index.mjs +563 -0
  7. package/dist/index.cjs +15 -3
  8. package/dist/index.d.cts +186 -6
  9. package/dist/index.d.mts +186 -6
  10. package/dist/index.d.ts +186 -6
  11. package/dist/index.js +6 -5
  12. package/dist/index.mjs +4 -2
  13. package/dist/shared/modern-text.B2xfrqDc.cjs +556 -0
  14. package/dist/shared/modern-text.BD7PBYt7.d.cts +100 -0
  15. package/dist/shared/modern-text.BxijkspX.d.ts +100 -0
  16. package/dist/shared/{modern-text.BKZQdmgG.cjs → modern-text.CBgc-cQ1.cjs} +288 -18
  17. package/dist/shared/modern-text.CYa4lfoG.d.mts +100 -0
  18. package/dist/shared/{modern-text.Dqw5Z6MV.mjs → modern-text.ChzjFjsk.mjs} +283 -13
  19. package/dist/shared/{modern-text.Db7Uoht6.d.cts → modern-text.D4WopQCu.d.cts} +56 -22
  20. package/dist/shared/{modern-text.Db7Uoht6.d.mts → modern-text.D4WopQCu.d.mts} +56 -22
  21. package/dist/shared/{modern-text.Db7Uoht6.d.ts → modern-text.D4WopQCu.d.ts} +56 -22
  22. package/dist/shared/modern-text.JF1ny7A-.mjs +550 -0
  23. package/dist/shared/modern-text.MC5bIC9E.cjs +316 -0
  24. package/dist/shared/modern-text.fT17R5HY.mjs +310 -0
  25. package/dist/web-components/index.cjs +2 -1
  26. package/dist/web-components/index.d.cts +1 -1
  27. package/dist/web-components/index.d.mts +1 -1
  28. package/dist/web-components/index.d.ts +1 -1
  29. package/dist/web-components/index.mjs +2 -1
  30. package/package.json +12 -7
@@ -57,6 +57,16 @@ declare class Character {
57
57
  constructor(content: string, index: number, parent: Fragment);
58
58
  protected _getFontSFNT(fonts?: Fonts): SFNT | undefined;
59
59
  updateGlyph(sfnt?: SFNT | undefined): this;
60
+ /**
61
+ * Populate glyph metrics only (advance width/height, ascender/descender,
62
+ * baseline, …) without building the glyph `path` or touching boxes.
63
+ *
64
+ * The DOM {@link DomMeasurer} never needs this — it reads positions back from the
65
+ * browser. A pure-JS measurer (e.g. `FontMeasurer`) must know advances *before*
66
+ * it can place characters, so it calls this ahead of layout. `update()` later
67
+ * recomputes the same metrics while building the path, so this is idempotent.
68
+ */
69
+ measureGlyph(fonts?: Fonts): this;
60
70
  update(fonts?: Fonts): this;
61
71
  protected _italic(path: Path2D, startPoint?: Vector2Like): void;
62
72
  getGlyphMinMax(min?: Vector2, max?: Vector2, withStyle?: boolean): {
@@ -66,24 +76,6 @@ declare class Character {
66
76
  getGlyphBoundingBox(withStyle?: boolean): BoundingBox | undefined;
67
77
  }
68
78
 
69
- interface Plugin {
70
- name: string;
71
- pathSet?: Path2DSet;
72
- getBoundingBox?: (text: Text$1) => BoundingBox | undefined;
73
- update?: (text: Text$1) => void;
74
- updateOrder?: number;
75
- render?: (renderer: Canvas2DRenderer) => void;
76
- renderOrder?: number;
77
- load?: (text: Text$1) => Promise<void>;
78
- context?: Record<string, any>;
79
- }
80
- interface Options extends TextObject {
81
- debug?: boolean;
82
- measureDom?: HTMLElement;
83
- fonts?: Fonts;
84
- plugins?: Plugin[];
85
- }
86
-
87
79
  interface MeasuredParagraph {
88
80
  paragraphIndex: number;
89
81
  left: number;
@@ -127,7 +119,7 @@ interface RootDomStyles {
127
119
  section: Record<string, any>;
128
120
  ul: Record<string, any>;
129
121
  }
130
- declare class Measurer {
122
+ declare class DomMeasurer {
131
123
  static notZeroStyles: Set<string>;
132
124
  static pxStyles: Set<string>;
133
125
  protected _styleCache: WeakMap<object, Record<string, any>>;
@@ -166,6 +158,47 @@ declare class Measurer {
166
158
  dispose(): void;
167
159
  }
168
160
 
161
+ interface Plugin {
162
+ name: string;
163
+ pathSet?: Path2DSet;
164
+ getBoundingBox?: (text: Text$1) => BoundingBox | undefined;
165
+ update?: (text: Text$1) => void;
166
+ updateOrder?: number;
167
+ render?: (renderer: Canvas2DRenderer) => void;
168
+ renderOrder?: number;
169
+ load?: (text: Text$1) => Promise<void>;
170
+ context?: Record<string, any>;
171
+ }
172
+ /**
173
+ * Pluggable layout backend. Implementations fill the four-level boxes
174
+ * (`character.inlineBox`/`lineBox`, `fragment.inlineBox`, `paragraph.lineBox`)
175
+ * in place and return the overall bounding box.
176
+ *
177
+ * - `DomMeasurer` — DOM-based, uses the browser as ground truth (default).
178
+ * - `FontMeasurer` — pure-JS, DOM-free; needs `fonts` to resolve glyph advances.
179
+ *
180
+ * `fonts` is passed positionally by `Text.measure()`; DOM-based measurers ignore
181
+ * it (a method may safely declare fewer parameters than the interface).
182
+ */
183
+ interface TextMeasurer {
184
+ measure: (paragraphs: Paragraph[], rootStyle: FullStyle, dom?: HTMLElement, fonts?: Fonts) => MeasureDomResult;
185
+ createDom?: (paragraphs: Paragraph[], rootStyle: FullStyle) => HTMLElement;
186
+ dispose?: () => void;
187
+ }
188
+ /** Built-in layout backends. `'font'` → `FontMeasurer`, `'dom'` → `DomMeasurer`. */
189
+ type MeasurerKind = 'dom' | 'font';
190
+ interface Options extends TextObject {
191
+ debug?: boolean;
192
+ measureDom?: HTMLElement;
193
+ fonts?: Fonts;
194
+ plugins?: Plugin[];
195
+ /**
196
+ * Layout backend: `'font'` (pure-JS) or `'dom'` (browser), or a custom
197
+ * `TextMeasurer`. Defaults to `'font'` when `fonts` are provided, else `'dom'`.
198
+ */
199
+ measurer?: MeasurerKind | TextMeasurer;
200
+ }
201
+
169
202
  interface RenderOptions {
170
203
  view: HTMLCanvasElement;
171
204
  pixelRatio?: number;
@@ -207,6 +240,7 @@ declare class Text$1 extends Reactivable {
207
240
  effects?: NormalizedText['effects'];
208
241
  fill?: NormalizedText['fill'];
209
242
  outline?: NormalizedText['outline'];
243
+ deformation?: NormalizedText['deformation'];
210
244
  measureDom?: HTMLElement;
211
245
  fonts?: Fonts;
212
246
  needsUpdate: boolean;
@@ -220,7 +254,7 @@ declare class Text$1 extends Reactivable {
220
254
  glyphBox: BoundingBox;
221
255
  pathBox: BoundingBox;
222
256
  boundingBox: BoundingBox;
223
- measurer: Measurer;
257
+ measurer: TextMeasurer;
224
258
  plugins: Map<string, Plugin>;
225
259
  pathSets: Path2DSet[];
226
260
  protected _paragraphs: Paragraph[];
@@ -303,5 +337,5 @@ declare class Canvas2DRenderer {
303
337
  drawCharacter: (character: Character, effect?: NormalizedEffect) => void;
304
338
  }
305
339
 
306
- export { Canvas2DRenderer as C, Fragment as F, Paragraph as P, Text$1 as T, Character as a, Measurer as g, textDefaultStyle as t };
307
- export type { DrawShapePathsOptions as D, MeasureDomResult as M, Options as O, RenderOptions as R, MeasureResult as b, MeasuredCharacter as c, MeasuredCharacterRect as d, MeasuredFragment as e, MeasuredParagraph as f, Plugin as h, TextEvents as i };
340
+ export { Canvas2DRenderer as C, DomMeasurer as D, Fragment as F, Paragraph as P, Text$1 as T, Character as a, textDefaultStyle as t };
341
+ export type { MeasureDomResult as M, Options as O, RenderOptions as R, DrawShapePathsOptions as b, MeasureResult as c, MeasuredCharacter as d, MeasuredCharacterRect as e, MeasuredFragment as f, MeasuredParagraph as g, MeasurerKind as h, Plugin as i, TextEvents as j, TextMeasurer as k };
@@ -0,0 +1,550 @@
1
+ import { isNone } from 'modern-idoc';
2
+ import { QuadraticBezierCurve, Vector2, CubicBezierCurve, LineCurve } from 'modern-path2d';
3
+
4
+ function definePlugin(options) {
5
+ return options;
6
+ }
7
+
8
+ function splitCurve(curve, point) {
9
+ let { x, y } = point;
10
+ if (curve instanceof QuadraticBezierCurve) {
11
+ const { p1: v0, cp: v1, p2: v2 } = curve;
12
+ const s = x !== void 0 && Math.abs(Math.sign(v0.x - x) + Math.sign(v1.x - x) + Math.sign(v2.x - x)) === 1;
13
+ const n = y !== void 0 && Math.abs(Math.sign(v0.y - y) + Math.sign(v1.y - y) + Math.sign(v2.y - y)) === 1;
14
+ let o, e, dist, c;
15
+ if (s) {
16
+ o = v0.x - 2 * v1.x + v2.x;
17
+ e = 2 * (-v0.x + v1.x);
18
+ dist = v0.x - x;
19
+ }
20
+ if (n) {
21
+ o = v0.y - 2 * v1.y + v2.y;
22
+ e = 2 * (-v0.y + v1.y);
23
+ dist = v0.y - y;
24
+ }
25
+ if (s || n) {
26
+ c = e * e - 4 * o * dist;
27
+ if (c > 0) {
28
+ const h = Math.sqrt(c);
29
+ const a = (-e + h) / 2 / o;
30
+ const b = (-e - h) / 2 / o;
31
+ const u = [a, b].find((v) => v > 0 && v < 1);
32
+ if (u) {
33
+ const r = Vector2.lerp(v0, v1, u);
34
+ const d = Vector2.lerp(v1, v2, u);
35
+ const B = Vector2.lerp(r, d, u);
36
+ return [new QuadraticBezierCurve(v0, r, B), new QuadraticBezierCurve(B.clone(), d, v2)];
37
+ }
38
+ }
39
+ }
40
+ } else if (curve instanceof CubicBezierCurve) {
41
+ const { p1: v0, cp1: v1, cp2: v2, p2: v3 } = curve;
42
+ const { min, max } = curve.getMinMax();
43
+ const e = x !== void 0 && (min.x - x) * (max.x - x) < 0;
44
+ const l = y !== void 0 && (min.y - y) * (max.y - y) < 0;
45
+ if (e || l) {
46
+ x = 0.5 * v0.x + 0.5 * v1.x;
47
+ y = 0.5 * v0.y + 0.5 * v1.y;
48
+ const c = new Vector2(x, y);
49
+ x = 0.25 * v0.x + 0.5 * v1.x + 0.25 * v2.x;
50
+ y = 0.25 * v0.y + 0.5 * v1.y + 0.25 * v2.y;
51
+ const h = new Vector2(x, y);
52
+ x = 0.125 * v0.x + 0.375 * v1.x + 0.375 * v2.x + 0.125 * v3.x;
53
+ y = 0.125 * v0.y + 0.375 * v1.y + 0.375 * v2.y + 0.125 * v3.y;
54
+ const a = new Vector2(x, y);
55
+ x = 0.25 * v1.x + 0.5 * v2.x + 0.25 * v3.x;
56
+ y = 0.25 * v1.y + 0.5 * v2.y + 0.25 * v3.y;
57
+ const b = new Vector2(x, y);
58
+ x = 0.5 * v2.x + 0.5 * v3.x;
59
+ y = 0.5 * v2.y + 0.5 * v3.y;
60
+ const u = new Vector2(x, y);
61
+ return [new CubicBezierCurve(v0, c, h, a), new CubicBezierCurve(a.clone(), b, u, v3)];
62
+ }
63
+ } else if (curve instanceof LineCurve) {
64
+ const { p1: v1, p2: v2 } = curve;
65
+ const changedX = x !== void 0 && (v1.x - x) * (v2.x - x) < 0;
66
+ const changedY = y !== void 0 && (v1.y - y) * (v2.y - y) < 0;
67
+ if (changedX) {
68
+ y = v1.y + (v2.y - v1.y) / (v2.x - v1.x) * (x - v1.x);
69
+ }
70
+ if (changedY) {
71
+ x = v1.x + (v2.x - v1.x) / (v2.y - v1.y) * (y - v1.y);
72
+ }
73
+ if (changedX || changedY) {
74
+ return [new LineCurve(v1, new Vector2(x, y)), new LineCurve(new Vector2(x, y), v2)];
75
+ }
76
+ }
77
+ return [curve];
78
+ }
79
+
80
+ class Deformer {
81
+ get boundingBox() {
82
+ return this.text.lineBox;
83
+ }
84
+ get paragraphs() {
85
+ return this.text.paragraphs;
86
+ }
87
+ get isHorizontal() {
88
+ return this.text.computedStyle.writingMode.startsWith("horizontal");
89
+ }
90
+ get baseWidth() {
91
+ return this.isHorizontal ? this.boundingBox.width : this.boundingBox.height;
92
+ }
93
+ get baseHeight() {
94
+ return this.isHorizontal ? this.boundingBox.height : this.boundingBox.width;
95
+ }
96
+ get characters() {
97
+ return this.paragraphs.flatMap((p) => p.fragments.flatMap((f) => f.characters));
98
+ }
99
+ constructor({ text, intensities = [], maxFontSize = 100 }) {
100
+ this.text = text;
101
+ this.intensities = intensities.map((val) => val / 100);
102
+ this.lineHeight = maxFontSize;
103
+ }
104
+ _breakLine() {
105
+ const isVertical = !this.isHorizontal;
106
+ const { left, top, bottom, right } = this.boundingBox;
107
+ const x = 0.5 * (left + right);
108
+ const y = 0.5 * (top + bottom);
109
+ this.characters.forEach((character) => {
110
+ if (!character.glyphBox) {
111
+ return;
112
+ }
113
+ character.path.curves.forEach((subPath) => {
114
+ subPath.curves = subPath.curves.flatMap((curve) => {
115
+ return splitCurve(curve, isVertical ? { y } : { x });
116
+ });
117
+ });
118
+ });
119
+ }
120
+ _makeTheJointSmooth() {
121
+ this.characters.forEach((character) => {
122
+ if (!character.glyphBox) {
123
+ return;
124
+ }
125
+ character.path.getFlatCurves().forEach((curve) => {
126
+ if (curve instanceof QuadraticBezierCurve && curve.isFromLine) {
127
+ const { p1, cp, p2 } = curve;
128
+ cp.x = 2 * cp.x - 0.5 * (p1.x + p2.x);
129
+ cp.y = 2 * cp.y - 0.5 * (p1.y + p2.y);
130
+ }
131
+ });
132
+ });
133
+ }
134
+ _lineToQuadraticBezier() {
135
+ this.characters.forEach((character) => {
136
+ if (!character.glyphBox) {
137
+ return;
138
+ }
139
+ character.path.curves.forEach((subPath) => {
140
+ subPath.curves = subPath.curves.map((curve) => {
141
+ if (curve instanceof LineCurve && curve.getLength() > 5) {
142
+ const { p1, p2 } = curve;
143
+ const res = new QuadraticBezierCurve(
144
+ p1.clone(),
145
+ new Vector2(0.5 * (p1.x + p2.x), 0.5 * (p1.y + p2.y)),
146
+ p2.clone()
147
+ );
148
+ res.isFromLine = true;
149
+ return res;
150
+ }
151
+ return curve;
152
+ });
153
+ });
154
+ });
155
+ }
156
+ _transform(transform, getArg1, getArg) {
157
+ const highlight = this.text.plugins.get("highlight");
158
+ const arg = getArg?.();
159
+ let i = 0;
160
+ const charactersLength = this.characters.filter((c) => c.glyphBox).length;
161
+ this.paragraphs.forEach((paragraph, paragraphIndex) => {
162
+ paragraph.fragments.forEach((fragment, fragmentIndex) => {
163
+ fragment.characters.forEach((character, characterIndex) => {
164
+ if (!character.glyphBox) {
165
+ return;
166
+ }
167
+ const arg1 = getArg1?.(
168
+ {
169
+ paragraphIndex,
170
+ fragmentIndex,
171
+ characterIndex,
172
+ character
173
+ },
174
+ arg
175
+ );
176
+ character.path.getControlPointRefs().forEach((point) => {
177
+ const [x, y] = transform(point, arg1);
178
+ point.set(x, y);
179
+ });
180
+ if (getArg1 && highlight?.pathSet?.paths) {
181
+ const step = highlight.pathSet.paths.length / charactersLength;
182
+ const start = i * step;
183
+ for (let _i = 0; _i < step; _i++) {
184
+ highlight?.pathSet?.paths[start + _i]?.getControlPointRefs().forEach((point) => {
185
+ const [x, y] = transform(point, arg1);
186
+ point.set(x, y);
187
+ });
188
+ }
189
+ i++;
190
+ }
191
+ });
192
+ });
193
+ });
194
+ this.paragraphs.forEach((paragraph) => {
195
+ paragraph.fragments.forEach((fragment) => {
196
+ fragment.characters.forEach((character) => {
197
+ character.glyphBox = character.getGlyphBoundingBox();
198
+ if (character.glyphBox) {
199
+ character.inlineBox = character.glyphBox;
200
+ }
201
+ });
202
+ });
203
+ });
204
+ if (!getArg1) {
205
+ highlight?.pathSet?.paths?.forEach((v) => {
206
+ v.getControlPointRefs().forEach((point) => {
207
+ const [x, y] = transform(point);
208
+ point.set(x, y);
209
+ });
210
+ });
211
+ }
212
+ }
213
+ }
214
+
215
+ class BendDeformer extends Deformer {
216
+ constructor(options, preset) {
217
+ super(options);
218
+ this.preset = preset;
219
+ }
220
+ deform() {
221
+ if (!Math.hypot(...this.intensities)) {
222
+ return;
223
+ }
224
+ this._lineToQuadraticBezier();
225
+ this._bend();
226
+ this._makeTheJointSmooth();
227
+ }
228
+ _bend() {
229
+ const { boundingBox, baseWidth, baseHeight, preset } = this;
230
+ const lineHeight = this.lineHeight * 2 / Math.sin(this.intensities[0] * Math.PI * 0.5);
231
+ const isVertical = preset.vertical === void 0 || preset.vertical === "auto" ? !this.isHorizontal : preset.vertical;
232
+ const { left, top, width, height } = boundingBox;
233
+ const size = (isVertical ? height : width) / 2 / lineHeight;
234
+ const center = {
235
+ x: left + width / 2,
236
+ y: top + height / 2
237
+ };
238
+ let centerDistAngle;
239
+ const centerDist = { x: center.x, y: center.y };
240
+ if (isVertical) {
241
+ centerDistAngle = 0;
242
+ centerDist.x -= baseHeight / 2 / Math.tan(size) + Math.sign(size) * width / 2;
243
+ } else {
244
+ centerDistAngle = 1.5 * Math.PI;
245
+ centerDist.y += baseWidth / 2 / Math.tan(size) + Math.sign(size) * height / 2;
246
+ }
247
+ const method = preset.transform({
248
+ lineHeight,
249
+ size,
250
+ center,
251
+ centerDist,
252
+ centerDistAngle,
253
+ width,
254
+ height,
255
+ isHorizontal: this.isHorizontal
256
+ });
257
+ this._transform(method);
258
+ }
259
+ }
260
+
261
+ class FfdDeformer extends Deformer {
262
+ constructor(options, preset) {
263
+ super(options);
264
+ this.preset = preset;
265
+ }
266
+ deform() {
267
+ if (!Math.hypot(...this.intensities)) {
268
+ return;
269
+ }
270
+ const { preset } = this;
271
+ if (preset.breakLine) {
272
+ this._breakLine();
273
+ }
274
+ if (preset.lineToQuad) {
275
+ this._lineToQuadraticBezier();
276
+ }
277
+ const [a, b] = this.intensities;
278
+ const points = this._createFFDControlPoints(preset.hBlocks, preset.vBlocks);
279
+ preset.build(points, {
280
+ a,
281
+ b,
282
+ baseWidth: this.baseWidth,
283
+ baseHeight: this.baseHeight,
284
+ lineHeight: this.lineHeight,
285
+ adjust: (point, dx, dy) => this._adjustControlPoints(point, { x: dx, y: dy })
286
+ });
287
+ if (preset.bezier) {
288
+ this._calculateForBezierFFD(points, preset.hBlocks, preset.vBlocks);
289
+ this._makeTheJointSmooth();
290
+ } else {
291
+ this._calculateForLinearFFD(points, preset.hBlocks, preset.vBlocks);
292
+ }
293
+ }
294
+ _calculateForFFD(cb, points, hBlocks, vBlocks) {
295
+ const { left, top, right, width, height } = this.boundingBox;
296
+ this._transform(
297
+ this.isHorizontal ? (point) => {
298
+ const xProgress = (point.x - left) / width;
299
+ const yProgress = (point.y - top) / height;
300
+ let [x, y] = [0, 0];
301
+ for (let h = 0; h < hBlocks + 2; h++) {
302
+ const _h = cb(hBlocks, h, xProgress);
303
+ for (let v = 0; v < vBlocks + 2; v++) {
304
+ const _v = cb(vBlocks, v, yProgress);
305
+ const p = points[h * (vBlocks + 2) + v];
306
+ x += _h * _v * p.x;
307
+ y += _h * _v * p.y;
308
+ }
309
+ }
310
+ return [x, y];
311
+ } : (point) => {
312
+ const xProgress = (right - point.x) / width;
313
+ const yProgress = (point.y - top) / height;
314
+ let [x, y] = [0, 0];
315
+ for (let h = 0; h < hBlocks + 2; h++) {
316
+ const _h = cb(hBlocks, h, yProgress);
317
+ for (let v = 0; v < vBlocks + 2; v++) {
318
+ const _v = cb(vBlocks, v, xProgress);
319
+ const p = points[h * (vBlocks + 2) + v];
320
+ x += _h * _v * p.x;
321
+ y += _h * _v * p.y;
322
+ }
323
+ }
324
+ return [x, y];
325
+ }
326
+ );
327
+ }
328
+ _createFFDControlPoints(hBlocks, vBlocks) {
329
+ const { left, top, right, width, height } = this.boundingBox;
330
+ const points = [];
331
+ if (this.isHorizontal) {
332
+ const avgWidth = width / (hBlocks + 1);
333
+ const avgHeight = height / (vBlocks + 1);
334
+ for (let h = 0; h < hBlocks + 2; h++) {
335
+ for (let v = 0; v < vBlocks + 2; v++) {
336
+ points.push(new Vector2(left + h * avgWidth, top + v * avgHeight));
337
+ }
338
+ }
339
+ } else {
340
+ const avgWidth = width / (vBlocks + 1);
341
+ const avgHeight = height / (hBlocks + 1);
342
+ for (let h = 0; h < hBlocks + 2; h++) {
343
+ for (let v = 0; v < vBlocks + 2; v++) {
344
+ points.push(new Vector2(right - v * avgWidth, top + h * avgHeight));
345
+ }
346
+ }
347
+ }
348
+ return points;
349
+ }
350
+ _adjustControlPoints(point1, point2) {
351
+ if (this.isHorizontal) {
352
+ point1.x += point2.x;
353
+ point1.y += point2.y;
354
+ } else {
355
+ point1.x -= point2.y;
356
+ point1.y += point2.x;
357
+ }
358
+ }
359
+ _factorialForFFD(val) {
360
+ let result = 1;
361
+ for (let i = 2; i <= val; i++) {
362
+ result *= i;
363
+ }
364
+ return result;
365
+ }
366
+ _combineForFFD(total, current) {
367
+ return this._factorialForFFD(total) / this._factorialForFFD(total - current) / this._factorialForFFD(current);
368
+ }
369
+ _calculateForBezierFFD(points, hBlocks, vBlocks) {
370
+ this._calculateForFFD(
371
+ (count, current, progress) => {
372
+ return this._combineForFFD(count + 1, current) * (1 - progress) ** (count + 1 - current) * progress ** current;
373
+ },
374
+ points,
375
+ hBlocks,
376
+ vBlocks
377
+ );
378
+ }
379
+ _linearBasis(count, current, progress) {
380
+ const t = current < progress * count ? current + 1 - progress * count : progress * count - current + 1;
381
+ return Math.max(0, t);
382
+ }
383
+ _calculateForLinearFFD(points, hBlocks, vBlocks) {
384
+ this._calculateForFFD(
385
+ (count, current, progress) => {
386
+ return this._linearBasis(count + 1, current, progress);
387
+ },
388
+ points,
389
+ hBlocks,
390
+ vBlocks
391
+ );
392
+ }
393
+ }
394
+
395
+ class VerbatimDeformer extends Deformer {
396
+ constructor(options, preset) {
397
+ super(options);
398
+ this.preset = preset;
399
+ }
400
+ _context() {
401
+ return {
402
+ intensities: this.intensities,
403
+ baseWidth: this.baseWidth,
404
+ lineHeight: this.lineHeight,
405
+ isHorizontal: this.isHorizontal,
406
+ boundingBox: this.boundingBox
407
+ };
408
+ }
409
+ deform() {
410
+ if (!Math.hypot(...this.intensities)) {
411
+ return;
412
+ }
413
+ const { preset } = this;
414
+ const ctx = this._context();
415
+ if (preset.engine === "offset") {
416
+ this._transform(
417
+ preset.point,
418
+ preset.perChar ? (info) => preset.perChar(ctx, info) : void 0
419
+ );
420
+ } else {
421
+ const followTangent = preset.followTangent ?? true;
422
+ const { funcPerChar, funcPerPoint } = this._getPerCharAndPointFunc(followTangent);
423
+ this._transform(funcPerPoint, funcPerChar, () => {
424
+ const { width, height, left, top } = this.boundingBox;
425
+ return {
426
+ width,
427
+ height,
428
+ left,
429
+ top,
430
+ curve: preset.makeCurve(ctx),
431
+ isHorizontal: this.isHorizontal,
432
+ needExpandAlongNormal: preset.expandAlongNormal ?? false
433
+ };
434
+ });
435
+ }
436
+ }
437
+ _getPerCharAndPointFunc(rotate = false) {
438
+ let funcPerChar;
439
+ let funcPerPoint;
440
+ if (rotate) {
441
+ funcPerPoint = this._resetPointPos;
442
+ funcPerChar = this._calculateNewCenter;
443
+ } else {
444
+ funcPerPoint = this._resetPointPosWithoutRotate;
445
+ funcPerChar = this._calculateNewCenterWithoutRotate;
446
+ }
447
+ return {
448
+ funcPerPoint,
449
+ funcPerChar
450
+ };
451
+ }
452
+ _calculateNewCenter({ character }, arg) {
453
+ const { width, height, left, top, curve, isHorizontal, needExpandAlongNormal = false } = arg;
454
+ const { center, centerDiviation } = character;
455
+ const pos = isHorizontal ? (center.x - left) / width : (center.y - top) / height;
456
+ const a = isHorizontal ? new Vector2(0, center.y - top) : new Vector2(center.x - left, 0);
457
+ if (needExpandAlongNormal) {
458
+ const p = curve.getNormal(pos);
459
+ a.x += p.x * -centerDiviation;
460
+ a.y += p.y * -centerDiviation;
461
+ }
462
+ const newCenter = curve.getPointAt(pos).add(a);
463
+ const tangent = curve.getTangent(pos);
464
+ const cos = tangent.x;
465
+ const sin = tangent.y;
466
+ return {
467
+ cos,
468
+ sin,
469
+ newCenter,
470
+ center
471
+ };
472
+ }
473
+ _calculateNewCenterWithoutRotate({ character }, arg) {
474
+ const { width, height, left, top, curve, isHorizontal } = arg;
475
+ const { center } = character;
476
+ const pos = isHorizontal ? (center.x - left) / width : (center.y - top) / height;
477
+ const offsetPoint = isHorizontal ? new Vector2(0, center.y - top) : new Vector2(center.x - left, 0);
478
+ return {
479
+ newCenter: curve.getPointAt(pos).add(offsetPoint),
480
+ center
481
+ };
482
+ }
483
+ _resetPointPos(point, arg) {
484
+ const { cos = 1, sin = 0, center, newCenter } = arg;
485
+ const dx = point.x - center.x;
486
+ const dy = point.y - center.y;
487
+ const x = newCenter.x + dx * cos - dy * sin;
488
+ const y = newCenter.y + dx * sin + dy * cos;
489
+ return [x, y];
490
+ }
491
+ _resetPointPosWithoutRotate(point, arg) {
492
+ const { center, newCenter } = arg;
493
+ const x = point.x - center.x + newCenter.x;
494
+ const y = point.y - center.y + newCenter.y;
495
+ return [x, y];
496
+ }
497
+ }
498
+
499
+ const deformationPresets = /* @__PURE__ */ new Map();
500
+ function defineDeformation(name, preset) {
501
+ deformationPresets.set(name, preset);
502
+ }
503
+ function removeDeformation(name) {
504
+ deformationPresets.delete(name);
505
+ }
506
+ function getDeformationNames() {
507
+ return [...deformationPresets.keys()];
508
+ }
509
+ function deformationPlugin() {
510
+ return definePlugin({
511
+ name: "deformation",
512
+ updateOrder: 2,
513
+ update: (text) => {
514
+ const config = text.deformation;
515
+ const type = config?.type;
516
+ if (isNone(type) || !type) {
517
+ return;
518
+ }
519
+ const preset = deformationPresets.get(type);
520
+ if (!preset) {
521
+ return;
522
+ }
523
+ const options = {
524
+ text,
525
+ intensities: config.intensities ?? preset.defaultIntensities,
526
+ maxFontSize: config.maxFontSize
527
+ };
528
+ let deformer;
529
+ switch (preset.engine) {
530
+ case "ffd":
531
+ deformer = new FfdDeformer(options, preset);
532
+ break;
533
+ case "bend":
534
+ deformer = new BendDeformer(options, preset);
535
+ break;
536
+ case "curve":
537
+ case "offset":
538
+ deformer = new VerbatimDeformer(options, preset);
539
+ break;
540
+ }
541
+ deformer.deform();
542
+ const box = text.getGlyphBox();
543
+ text.rawGlyphBox = box;
544
+ text.glyphBox = box;
545
+ text.lineBox = box;
546
+ }
547
+ });
548
+ }
549
+
550
+ export { definePlugin as a, deformationPlugin as b, defineDeformation as d, getDeformationNames as g, removeDeformation as r };