text-to-canvas 1.1.0 → 1.1.2
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 +10 -0
- package/README.md +1 -0
- package/dist/text-to-canvas.cjs +2 -1
- package/dist/text-to-canvas.esm.min.js +68 -67
- package/dist/text-to-canvas.esm.min.js.map +1 -1
- package/dist/text-to-canvas.min.js +1 -1
- package/dist/text-to-canvas.min.js.map +1 -1
- package/dist/text-to-canvas.mjs +2 -1
- package/dist/text-to-canvas.umd.min.js +1 -1
- package/dist/text-to-canvas.umd.min.js.map +1 -1
- package/package.json +20 -20
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v1.1.2
|
|
9
|
+
|
|
10
|
+
- Fixed bug where `drawText()` config `fontColor` option was not being included in the base font format used to render the text ([#64](https://github.com/stefcameron/text-to-canvas/issues/64)).
|
|
11
|
+
|
|
12
|
+
## v1.1.1
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- README updated to include the new `overflow` option in the API docs.
|
|
17
|
+
|
|
8
18
|
## v1.1.0
|
|
9
19
|
|
|
10
20
|
### Added
|
package/README.md
CHANGED
|
@@ -185,6 +185,7 @@ You can run this demo locally with `npm run node:demo`
|
|
|
185
185
|
| `fontColor` | `'black'` | Base font color, same as css color. Examples: `blue`, `#00ff00`. |
|
|
186
186
|
| `justify` | `false` | Justify text if `true`, it will insert spaces between words when necessary. |
|
|
187
187
|
| `inferWhitespace` | `true` | If whitespace in the text should be inferred. Only applies if the text given to `drawText()` is a `Word[]`. If the text is a `string`, this config setting is ignored. |
|
|
188
|
+
| `overflow` | `true` | Allows the text to overflow out of the box if the box is too narrow/short to fit it all. `false` will clip the text to the box's boundaries. |
|
|
188
189
|
| `debug` | `false` | Draws the border and alignment lines of the text box for debugging purposes. |
|
|
189
190
|
|
|
190
191
|
## Functions
|
package/dist/text-to-canvas.cjs
CHANGED
|
@@ -473,7 +473,8 @@ const drawText = (ctx, text, config) => {
|
|
|
473
473
|
fontSize: config.fontSize,
|
|
474
474
|
fontStyle: config.fontStyle,
|
|
475
475
|
fontVariant: config.fontVariant,
|
|
476
|
-
fontWeight: config.fontWeight
|
|
476
|
+
fontWeight: config.fontWeight,
|
|
477
|
+
fontColor: config.fontColor
|
|
477
478
|
});
|
|
478
479
|
const {
|
|
479
480
|
width: boxWidth,
|
|
@@ -15,48 +15,48 @@ const $ = "black", _ = (t, n) => Object.assign(
|
|
|
15
15
|
fontFamily: t,
|
|
16
16
|
fontSize: n,
|
|
17
17
|
fontStyle: e,
|
|
18
|
-
fontVariant:
|
|
19
|
-
fontWeight:
|
|
20
|
-
}) => `${e || ""} ${
|
|
18
|
+
fontVariant: i,
|
|
19
|
+
fontWeight: s
|
|
20
|
+
}) => `${e || ""} ${i || ""} ${s || ""} ${n ?? 14}px ${t || D}`.trim(), T = (t) => !!t.match(/^\s+$/), M = (t) => t.filter((n) => !T(n.text)), V = (t) => {
|
|
21
21
|
const n = { ...t };
|
|
22
22
|
return t.format && (n.format = { ...t.format }), n;
|
|
23
|
-
},
|
|
23
|
+
}, C = (t, n) => {
|
|
24
24
|
if (t.length <= 1 || n.length < 1)
|
|
25
25
|
return [...t];
|
|
26
26
|
const e = [];
|
|
27
|
-
return t.forEach((
|
|
28
|
-
e.push(
|
|
27
|
+
return t.forEach((i, s) => {
|
|
28
|
+
e.push(i), s < t.length - 1 && n.forEach((r) => e.push(V(r)));
|
|
29
29
|
}), e;
|
|
30
30
|
}, Z = ({
|
|
31
31
|
line: t,
|
|
32
32
|
spaceWidth: n,
|
|
33
33
|
spaceChar: e,
|
|
34
|
-
boxWidth:
|
|
34
|
+
boxWidth: i
|
|
35
35
|
}) => {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
36
|
+
const s = M(t);
|
|
37
|
+
if (s.length <= 1)
|
|
38
38
|
return t.concat();
|
|
39
|
-
const r =
|
|
39
|
+
const r = s.reduce(
|
|
40
40
|
(a, u) => {
|
|
41
41
|
var d;
|
|
42
42
|
return a + (((d = u.metrics) == null ? void 0 : d.width) ?? 0);
|
|
43
43
|
},
|
|
44
44
|
0
|
|
45
|
-
), l = (
|
|
46
|
-
if (
|
|
47
|
-
const a = Math.ceil(l / (
|
|
45
|
+
), l = (i - r) / n;
|
|
46
|
+
if (s.length > 2) {
|
|
47
|
+
const a = Math.ceil(l / (s.length - 1)), u = Array.from({ length: a }, () => ({
|
|
48
48
|
text: e
|
|
49
|
-
})), d =
|
|
49
|
+
})), d = s.slice(0, s.length - 1), y = C(d, u), c = u.slice(
|
|
50
50
|
0,
|
|
51
51
|
Math.floor(l) - (d.length - 1) * u.length
|
|
52
|
-
), h =
|
|
52
|
+
), h = s[s.length - 1];
|
|
53
53
|
return [...y, ...c, h];
|
|
54
54
|
}
|
|
55
55
|
const o = Array.from(
|
|
56
56
|
{ length: Math.floor(l) },
|
|
57
57
|
() => ({ text: e })
|
|
58
58
|
);
|
|
59
|
-
return
|
|
59
|
+
return C(s, o);
|
|
60
60
|
}, x = (t, n = "both") => {
|
|
61
61
|
let e = 0;
|
|
62
62
|
if (n === "left" || n === "both") {
|
|
@@ -69,11 +69,11 @@ const $ = "black", _ = (t, n) => Object.assign(
|
|
|
69
69
|
trimmedLine: []
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
let
|
|
72
|
+
let i = t.length;
|
|
73
73
|
if (n === "right" || n === "both") {
|
|
74
|
-
for (
|
|
74
|
+
for (i--; i >= 0 && T(t[i].text); i--)
|
|
75
75
|
;
|
|
76
|
-
if (
|
|
76
|
+
if (i++, i <= 0)
|
|
77
77
|
return {
|
|
78
78
|
trimmedLeft: [],
|
|
79
79
|
trimmedRight: t.concat(),
|
|
@@ -82,48 +82,48 @@ const $ = "black", _ = (t, n) => Object.assign(
|
|
|
82
82
|
}
|
|
83
83
|
return {
|
|
84
84
|
trimmedLeft: t.slice(0, e),
|
|
85
|
-
trimmedRight: t.slice(
|
|
86
|
-
trimmedLine: t.slice(e,
|
|
85
|
+
trimmedRight: t.slice(i),
|
|
86
|
+
trimmedLine: t.slice(e, i)
|
|
87
87
|
};
|
|
88
|
-
},
|
|
88
|
+
}, H = " ", Y = " ";
|
|
89
89
|
let v;
|
|
90
90
|
const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t, n = !0) => {
|
|
91
91
|
const e = [[]];
|
|
92
|
-
let
|
|
93
|
-
return t.forEach((
|
|
92
|
+
let i = !1;
|
|
93
|
+
return t.forEach((s, r) => {
|
|
94
94
|
var l, o, a;
|
|
95
|
-
if (
|
|
96
|
-
for (let u = 0; u <
|
|
95
|
+
if (s.text.match(/^\n+$/)) {
|
|
96
|
+
for (let u = 0; u < s.text.length; u++)
|
|
97
97
|
e.push([]);
|
|
98
|
-
|
|
98
|
+
i = !0;
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
if (T(
|
|
102
|
-
(l = e.at(-1)) == null || l.push(
|
|
101
|
+
if (T(s.text)) {
|
|
102
|
+
(l = e.at(-1)) == null || l.push(s), i = !0;
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
s.text !== "" && (n && !i && r > 0 && ((o = e.at(-1)) == null || o.push({ text: Y })), (a = e.at(-1)) == null || a.push(s), i = !1);
|
|
106
106
|
}), e;
|
|
107
107
|
}, q = ({
|
|
108
108
|
wrappedLines: t,
|
|
109
109
|
wordMap: n,
|
|
110
110
|
positioning: {
|
|
111
111
|
width: e,
|
|
112
|
-
height:
|
|
113
|
-
x:
|
|
112
|
+
height: i,
|
|
113
|
+
x: s = 0,
|
|
114
114
|
y: r = 0,
|
|
115
115
|
align: l,
|
|
116
116
|
vAlign: o
|
|
117
117
|
}
|
|
118
118
|
}) => {
|
|
119
|
-
const a =
|
|
119
|
+
const a = s + e, u = r + i, d = (f) => (
|
|
120
120
|
// NOTE: `metrics` must exist as every `word` MUST have been measured at this point
|
|
121
121
|
f.metrics.fontBoundingBoxAscent + f.metrics.fontBoundingBoxDescent
|
|
122
122
|
), y = t.map(
|
|
123
123
|
(f) => f.reduce((p, A) => Math.max(p, d(A)), 0)
|
|
124
124
|
), c = y.reduce((f, p) => f + p, 0);
|
|
125
125
|
let h, m;
|
|
126
|
-
return o === "top" ? (m = "top", h = r) : o === "bottom" ? (m = "bottom", h = u - c) : (m = "top", h = r +
|
|
126
|
+
return o === "top" ? (m = "top", h = r) : o === "bottom" ? (m = "bottom", h = u - c) : (m = "top", h = r + i / 2 - c / 2), {
|
|
127
127
|
lines: t.map((f, p) => {
|
|
128
128
|
const A = f.reduce(
|
|
129
129
|
// NOTE: `metrics` must exist as every `word` MUST have been measured at this point
|
|
@@ -131,7 +131,7 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
131
131
|
0
|
|
132
132
|
), L = y[p];
|
|
133
133
|
let B;
|
|
134
|
-
l === "right" ? B = a - A : l === "left" ? B =
|
|
134
|
+
l === "right" ? B = a - A : l === "left" ? B = s : B = s + e / 2 - A / 2;
|
|
135
135
|
let F = B;
|
|
136
136
|
const z = f.map((W) => {
|
|
137
137
|
const b = I(W), { format: J } = n.get(b), U = F, O = d(W);
|
|
@@ -165,50 +165,50 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
return n;
|
|
168
|
-
}, G = (t) => JSON.stringify(t, N), K = (t) => JSON.stringify(t, N),
|
|
168
|
+
}, G = (t) => JSON.stringify(t, N), K = (t) => JSON.stringify(t, N), k = ({
|
|
169
169
|
ctx: t,
|
|
170
170
|
word: n,
|
|
171
171
|
wordMap: e,
|
|
172
|
-
baseTextFormat:
|
|
172
|
+
baseTextFormat: i
|
|
173
173
|
}) => {
|
|
174
|
-
const
|
|
174
|
+
const s = I(n);
|
|
175
175
|
if (n.metrics) {
|
|
176
|
-
if (!e.has(
|
|
176
|
+
if (!e.has(s)) {
|
|
177
177
|
let a;
|
|
178
|
-
n.format && (a = _(n.format,
|
|
178
|
+
n.format && (a = _(n.format, i)), e.set(s, { metrics: n.metrics, format: a });
|
|
179
179
|
}
|
|
180
180
|
return n.metrics.width;
|
|
181
181
|
}
|
|
182
|
-
if (e.has(
|
|
183
|
-
const { metrics: a } = e.get(
|
|
182
|
+
if (e.has(s)) {
|
|
183
|
+
const { metrics: a } = e.get(s);
|
|
184
184
|
return n.metrics = a, a.width;
|
|
185
185
|
}
|
|
186
186
|
let r = !1, l;
|
|
187
|
-
n.format && (t.save(), r = !0, l = _(n.format,
|
|
187
|
+
n.format && (t.save(), r = !0, l = _(n.format, i), t.font = S(l)), v || (r || (t.save(), r = !0), t.textBaseline = "bottom");
|
|
188
188
|
const o = t.measureText(n.text);
|
|
189
|
-
return typeof o.fontBoundingBoxAscent == "number" ? v = !0 : (v = !1, o.fontBoundingBoxAscent = o.actualBoundingBoxAscent, o.fontBoundingBoxDescent = 0), n.metrics = o, e.set(
|
|
189
|
+
return typeof o.fontBoundingBoxAscent == "number" ? v = !0 : (v = !1, o.fontBoundingBoxAscent = o.actualBoundingBoxAscent, o.fontBoundingBoxDescent = 0), n.metrics = o, e.set(s, { metrics: o, format: l }), r && t.restore(), o.width;
|
|
190
190
|
}, P = ({
|
|
191
191
|
ctx: t,
|
|
192
192
|
words: n,
|
|
193
193
|
justify: e,
|
|
194
|
-
format:
|
|
195
|
-
inferWhitespace:
|
|
194
|
+
format: i,
|
|
195
|
+
inferWhitespace: s = !0,
|
|
196
196
|
...r
|
|
197
197
|
// rest of params are related to positioning
|
|
198
198
|
}) => {
|
|
199
|
-
const l = /* @__PURE__ */ new Map(), o = _(
|
|
199
|
+
const l = /* @__PURE__ */ new Map(), o = _(i), { width: a } = r, u = (m, g = !1) => {
|
|
200
200
|
let f = 0, p = 0;
|
|
201
201
|
return m.every((A, L) => {
|
|
202
|
-
const B =
|
|
202
|
+
const B = k({ ctx: t, word: A, wordMap: l, baseTextFormat: o });
|
|
203
203
|
return !g && f + B > a ? (L === 0 && (p = 1, f = B), !1) : (p++, f += B, !0);
|
|
204
204
|
}), { lineWidth: f, splitPoint: p };
|
|
205
205
|
};
|
|
206
206
|
t.save();
|
|
207
207
|
const d = X(
|
|
208
208
|
x(n).trimmedLine,
|
|
209
|
-
|
|
209
|
+
s
|
|
210
210
|
);
|
|
211
|
-
if (d.length <= 0 || a <= 0 || r.height <= 0 ||
|
|
211
|
+
if (d.length <= 0 || a <= 0 || r.height <= 0 || i && typeof i.fontSize == "number" && i.fontSize <= 0)
|
|
212
212
|
return {
|
|
213
213
|
lines: [],
|
|
214
214
|
textAlign: "center",
|
|
@@ -217,7 +217,7 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
217
217
|
height: 0
|
|
218
218
|
};
|
|
219
219
|
t.font = S(o);
|
|
220
|
-
const y = e ?
|
|
220
|
+
const y = e ? k({ ctx: t, word: { text: H }, wordMap: l, baseTextFormat: o }) : 0, c = [];
|
|
221
221
|
for (const m of d) {
|
|
222
222
|
let { splitPoint: g } = u(m);
|
|
223
223
|
if (g >= m.length)
|
|
@@ -239,7 +239,7 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
239
239
|
const f = Z({
|
|
240
240
|
line: m,
|
|
241
241
|
spaceWidth: y,
|
|
242
|
-
spaceChar:
|
|
242
|
+
spaceChar: H,
|
|
243
243
|
boxWidth: a
|
|
244
244
|
});
|
|
245
245
|
u(f, !0), c[g] = f;
|
|
@@ -253,10 +253,10 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
253
253
|
return t.restore(), h;
|
|
254
254
|
}, j = (t) => {
|
|
255
255
|
const n = [];
|
|
256
|
-
let e,
|
|
257
|
-
return Array.from(t.trim()).forEach((
|
|
258
|
-
const r = T(
|
|
259
|
-
r && !
|
|
256
|
+
let e, i = !1;
|
|
257
|
+
return Array.from(t.trim()).forEach((s) => {
|
|
258
|
+
const r = T(s);
|
|
259
|
+
r && !i || !r && i ? (i = r, e && n.push(e), e = { text: s }) : (e || (e = { text: "" }), e.text += s);
|
|
260
260
|
}), e && n.push(e), n;
|
|
261
261
|
}, Q = ({ text: t, ...n }) => {
|
|
262
262
|
const e = j(t);
|
|
@@ -265,13 +265,13 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
265
265
|
words: e,
|
|
266
266
|
inferWhitespace: !1
|
|
267
267
|
}).lines.map(
|
|
268
|
-
(
|
|
268
|
+
(s) => s.map(({ word: { text: r } }) => r).join("")
|
|
269
269
|
);
|
|
270
270
|
}, R = (t, n, e) => {
|
|
271
|
-
const
|
|
271
|
+
const i = t.textBaseline, s = t.font;
|
|
272
272
|
t.textBaseline = "bottom", e && (t.font = e);
|
|
273
273
|
const { actualBoundingBoxAscent: r } = t.measureText(n);
|
|
274
|
-
return t.textBaseline =
|
|
274
|
+
return t.textBaseline = i, e && (t.font = s), r;
|
|
275
275
|
}, w = ({
|
|
276
276
|
ctx: t,
|
|
277
277
|
word: n
|
|
@@ -280,14 +280,15 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
280
280
|
text: n,
|
|
281
281
|
style: e
|
|
282
282
|
}) => R(t, n, e), et = (t, n, e) => {
|
|
283
|
-
const
|
|
283
|
+
const i = _({
|
|
284
284
|
fontFamily: e.fontFamily,
|
|
285
285
|
fontSize: e.fontSize,
|
|
286
286
|
fontStyle: e.fontStyle,
|
|
287
287
|
fontVariant: e.fontVariant,
|
|
288
|
-
fontWeight: e.fontWeight
|
|
288
|
+
fontWeight: e.fontWeight,
|
|
289
|
+
fontColor: e.fontColor
|
|
289
290
|
}), {
|
|
290
|
-
width:
|
|
291
|
+
width: s,
|
|
291
292
|
height: r,
|
|
292
293
|
x: l = 0,
|
|
293
294
|
y: o = 0
|
|
@@ -308,20 +309,20 @@ const I = (t) => `${t.text}${t.format ? JSON.stringify(t.format) : ""}`, X = (t,
|
|
|
308
309
|
align: e.align,
|
|
309
310
|
vAlign: e.vAlign,
|
|
310
311
|
justify: e.justify,
|
|
311
|
-
format:
|
|
312
|
+
format: i
|
|
312
313
|
});
|
|
313
|
-
if (t.save(), t.textAlign = y, t.textBaseline = d, t.font = S(
|
|
314
|
+
if (t.save(), t.textAlign = y, t.textBaseline = d, t.font = S(i), t.fillStyle = i.fontColor || $, e.overflow === !1 && (t.beginPath(), t.rect(l, o, s, r), t.clip()), a.forEach((c) => {
|
|
314
315
|
c.forEach((h) => {
|
|
315
316
|
h.isWhitespace || (h.format && (t.save(), t.font = S(h.format), h.format.fontColor && (t.fillStyle = h.format.fontColor)), t.fillText(h.word.text, h.x, h.y), h.format && t.restore());
|
|
316
317
|
});
|
|
317
318
|
}), e.debug) {
|
|
318
|
-
const c = l +
|
|
319
|
+
const c = l + s, h = o + r;
|
|
319
320
|
let m;
|
|
320
|
-
e.align === "right" ? m = c : e.align === "left" ? m = l : m = l +
|
|
321
|
+
e.align === "right" ? m = c : e.align === "left" ? m = l : m = l + s / 2;
|
|
321
322
|
let g = o;
|
|
322
323
|
e.vAlign === "bottom" ? g = h : e.vAlign === "middle" && (g = o + r / 2);
|
|
323
324
|
const f = "#0C8CE9";
|
|
324
|
-
t.lineWidth = 1, t.strokeStyle = f, t.strokeRect(l, o,
|
|
325
|
+
t.lineWidth = 1, t.strokeStyle = f, t.strokeRect(l, o, s, r), t.lineWidth = 1, (!e.align || e.align === "center") && (t.strokeStyle = f, t.beginPath(), t.moveTo(m, o), t.lineTo(m, h), t.stroke()), (!e.vAlign || e.vAlign === "middle") && (t.strokeStyle = f, t.beginPath(), t.moveTo(l, g), t.lineTo(c, g), t.stroke());
|
|
325
326
|
}
|
|
326
327
|
return t.restore(), { height: u };
|
|
327
328
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-to-canvas.esm.min.js","sources":["../src/lib/util/style.ts","../src/lib/util/whitespace.ts","../src/lib/util/justify.ts","../src/lib/util/trim.ts","../src/lib/util/split.ts","../src/lib/util/height.ts","../src/lib/index.ts"],"sourcesContent":["import { TextFormat } from '../model';\n\nexport const DEFAULT_FONT_FAMILY = 'Arial';\nexport const DEFAULT_FONT_SIZE = 14;\nexport const DEFAULT_FONT_COLOR = 'black';\n\n/**\n * Generates a text format based on defaults and any provided overrides.\n * @param format Overrides to `baseFormat` and default format.\n * @param baseFormat Overrides to default format.\n * @returns Full text format (all properties specified).\n */\nexport const getTextFormat = (\n format?: TextFormat,\n baseFormat?: TextFormat\n): Required<TextFormat> => {\n return Object.assign(\n {},\n {\n fontFamily: DEFAULT_FONT_FAMILY,\n fontSize: DEFAULT_FONT_SIZE,\n fontWeight: '400',\n fontStyle: '',\n fontVariant: '',\n fontColor: DEFAULT_FONT_COLOR,\n },\n baseFormat,\n format\n );\n};\n\n/**\n * Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.\n * @param format\n * @returns Style string to set on context's `font` property. Note this __does not include\n * the font color__ as that is not part of the CSS font value. Color must be handled separately.\n */\nexport const getTextStyle = ({\n fontFamily,\n fontSize,\n fontStyle,\n fontVariant,\n fontWeight,\n}: TextFormat) => {\n // per spec:\n // - font-style, font-variant and font-weight must precede font-size\n // - font-family must be the last value specified\n // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font\n return `${fontStyle || ''} ${fontVariant || ''} ${\n fontWeight || ''\n } ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();\n};\n","/**\n * Determines if a string is only whitespace (one or more characters of it).\n * @param text\n * @returns True if `text` is one or more characters of whitespace, only.\n */\nexport const isWhitespace = (text: string) => {\n return !!text.match(/^\\s+$/);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * @private\n * Extracts the __visible__ (i.e. non-whitespace) words from a line.\n * @param line\n * @returns New array with only non-whitespace words.\n */\nconst _extractWords = (line: Word[]) => {\n return line.filter((word) => !isWhitespace(word.text));\n};\n\n/**\n * @private\n * Deep-clones a Word.\n * @param word\n * @returns Deep-cloned Word.\n */\nconst _cloneWord = (word: Word) => {\n const clone = { ...word };\n if (word.format) {\n clone.format = { ...word.format };\n }\n return clone;\n};\n\n/**\n * @private\n * Joins Words together using another set of Words.\n * @param words Words to join.\n * @param joiner Words to use when joining `words`. These will be deep-cloned and inserted\n * in between every word in `words`, similar to `Array.join(string)` where the `string`\n * is inserted in between every element.\n * @returns New array of Words. Empty if `words` is empty. New array of one Word if `words`\n * contains only one Word.\n */\nconst _joinWords = (words: Word[], joiner: Word[]) => {\n if (words.length <= 1 || joiner.length < 1) {\n return [...words];\n }\n\n const phrase: Word[] = [];\n words.forEach((word, wordIdx) => {\n phrase.push(word);\n if (wordIdx < words.length - 1) {\n // don't append after last `word`\n joiner.forEach((jw) => phrase.push(_cloneWord(jw)));\n }\n });\n\n return phrase;\n};\n\n/**\n * Inserts spaces between words in a line in order to raise the line width to the box width.\n * The spaces are evenly spread in the line, and extra spaces (if any) are only inserted\n * between words, not at either end of the `line`.\n *\n * @returns New array containing original words from the `line` with additional whitespace\n * for justification to `boxWidth`.\n */\nexport const justifyLine = ({\n line,\n spaceWidth,\n spaceChar,\n boxWidth,\n}: {\n /** Assumed to have already been trimmed on both ends. */\n line: Word[];\n /** Width (px) of `spaceChar`. */\n spaceWidth: number;\n /**\n * Character used as a whitespace in justification. Will be injected in between Words in\n * `line` in order to justify the text on the line within `lineWidth`.\n */\n spaceChar: string;\n /** Width (px) of the box containing the text (i.e. max `line` width). */\n boxWidth: number;\n}) => {\n const words = _extractWords(line);\n if (words.length <= 1) {\n return line.concat();\n }\n\n const wordsWidth = words.reduce(\n (width, word) => width + (word.metrics?.width ?? 0),\n 0\n );\n const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;\n\n if (words.length > 2) {\n // use CEILING so we spread the partial spaces throughout except between the second-last\n // and last word so that the spacing is more even and as tight as we can get it to\n // the `boxWidth`\n const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));\n const spaces: Word[] = Array.from({ length: spacesPerWord }, () => ({\n text: spaceChar,\n }));\n const firstWords = words.slice(0, words.length - 1); // all but last word\n const firstPart = _joinWords(firstWords, spaces);\n const remainingSpaces = spaces.slice(\n 0,\n Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces.length\n );\n const lastWord = words[words.length - 1];\n return [...firstPart, ...remainingSpaces, lastWord];\n }\n // only 2 words so fill with spaces in between them: use FLOOR to make sure we don't\n // go past `boxWidth`\n const spaces: Word[] = Array.from(\n { length: Math.floor(noOfSpacesToInsert) },\n () => ({ text: spaceChar })\n );\n return _joinWords(words, spaces);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * Trims whitespace from the beginning and end of a `line`.\n * @param line\n * @param side Which side to trim.\n * @returns An object containing trimmed characters, and the new trimmed line.\n */\nexport const trimLine = (\n line: Word[],\n side: 'left' | 'right' | 'both' = 'both'\n): {\n /**\n * New array containing what was trimmed from the left (empty if none).\n */\n trimmedLeft: Word[];\n /**\n * New array containing what was trimmed from the right (empty if none).\n */\n trimmedRight: Word[];\n /**\n * New array representing the trimmed line, even if nothing gets trimmed. Empty array if\n * all whitespace.\n */\n trimmedLine: Word[];\n} => {\n let leftTrim = 0;\n if (side === 'left' || side === 'both') {\n for (; leftTrim < line.length; leftTrim++) {\n if (!isWhitespace(line[leftTrim].text)) {\n break;\n }\n }\n\n if (leftTrim >= line.length) {\n // all whitespace\n return {\n trimmedLeft: line.concat(),\n trimmedRight: [],\n trimmedLine: [],\n };\n }\n }\n\n let rightTrim = line.length;\n if (side === 'right' || side === 'both') {\n rightTrim--;\n for (; rightTrim >= 0; rightTrim--) {\n if (!isWhitespace(line[rightTrim].text)) {\n break;\n }\n }\n rightTrim++; // back up one since we started one down for 0-based indexes\n\n if (rightTrim <= 0) {\n // all whitespace\n return {\n trimmedLeft: [],\n trimmedRight: line.concat(),\n trimmedLine: [],\n };\n }\n }\n\n return {\n trimmedLeft: line.slice(0, leftTrim),\n trimmedRight: line.slice(rightTrim),\n trimmedLine: line.slice(leftTrim, rightTrim),\n };\n};\n","import { getTextFormat, getTextStyle } from './style';\nimport { isWhitespace } from './whitespace';\nimport { justifyLine } from './justify';\nimport {\n PositionedWord,\n SplitTextProps,\n SplitWordsProps,\n RenderSpec,\n Word,\n WordMap,\n CanvasTextMetrics,\n TextFormat,\n CanvasRenderContext,\n} from '../model';\nimport { trimLine } from './trim';\n\n// Hair space character for precise justification\nconst HAIR = '\\u{200a}';\n\n// for when we're inferring whitespace between words\nconst SPACE = ' ';\n\n/**\n * Whether the canvas API being used supports the newer `fontBoundingBox*` properties or not.\n *\n * True if it does, false if not; undefined until we determine either way.\n *\n * Note about `fontBoundingBoxAscent/Descent`: Only later browsers support this and the Node-based\n * `canvas` package does not. Having these properties will have a noticeable increase in performance\n * on large pieces of text to render. Failing these, a fallback is used which involves\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics\n * @see https://www.npmjs.com/package/canvas\n */\nlet fontBoundingBoxSupported: boolean;\n\n/**\n * @private\n * Generates a word hash for use as a key in a `WordMap`.\n * @param word\n * @returns Hash.\n */\nconst _getWordHash = (word: Word) => {\n return `${word.text}${word.format ? JSON.stringify(word.format) : ''}`;\n};\n\n/**\n * @private\n * Splits words into lines based on words that are single newline characters.\n * @param words\n * @param inferWhitespace True (default) if whitespace should be inferred (and injected)\n * based on words; false if we're to assume the words already include all necessary whitespace.\n * @returns Words expressed as lines.\n */\nconst _splitIntoLines = (\n words: Word[],\n inferWhitespace: boolean = true\n): Word[][] => {\n const lines: Word[][] = [[]];\n\n let wasWhitespace = false; // true if previous word was whitespace\n words.forEach((word, wordIdx) => {\n // TODO: this is likely a naive split (at least based on character?); should at least\n // think about this more; text format shouldn't matter on a line break, right (hope not)?\n if (word.text.match(/^\\n+$/)) {\n for (let i = 0; i < word.text.length; i++) {\n lines.push([]);\n }\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (isWhitespace(word.text)) {\n // whitespace OTHER THAN newlines since we checked for newlines above\n lines.at(-1)?.push(word);\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (word.text === '') {\n return; // skip to next `word`\n }\n\n // looks like a non-empty, non-whitespace word at this point, so if it isn't the first\n // word and the one before wasn't whitespace, insert a space\n if (inferWhitespace && !wasWhitespace && wordIdx > 0) {\n lines.at(-1)?.push({ text: SPACE });\n }\n\n lines.at(-1)?.push(word);\n wasWhitespace = false;\n });\n\n return lines;\n};\n\n/**\n * @private\n * Helper for `splitWords()` that takes the words that have been wrapped into lines and\n * determines their positions on canvas for future rendering based on alignment settings.\n * @param params\n * @returns Results to return via `splitWords()`\n */\nconst _generateSpec = ({\n wrappedLines,\n wordMap,\n positioning: {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n align,\n vAlign,\n },\n}: {\n /** Words organized/wrapped into lines to be rendered. */\n wrappedLines: Word[][];\n\n /** Map of Word to measured dimensions (px) as it would be rendered. */\n wordMap: WordMap;\n\n /**\n * Details on where to render the Words onto canvas. These parameters ultimately come\n * from `SplitWordsProps`, and they come from `DrawTextConfig`.\n */\n positioning: {\n width: SplitWordsProps['width'];\n // NOTE: height does NOT constrain the text; used only for vertical alignment\n height: SplitWordsProps['height'];\n x: SplitWordsProps['x'];\n y: SplitWordsProps['y'];\n align?: SplitWordsProps['align'];\n vAlign?: SplitWordsProps['vAlign'];\n };\n}): RenderSpec => {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n // NOTE: using __font__ ascent/descent to account for all possible characters in the font\n // so that lines with ascenders but no descenders, or vice versa, are all properly\n // aligned to the baseline, and so that lines aren't scrunched\n // NOTE: even for middle vertical alignment, we want to use the __font__ ascent/descent\n // so that words, per line, are still aligned to the baseline (as much as possible; if\n // each word has a different font size, then things will still be offset, but for the\n // same font size, the baseline should match from left to right)\n const getHeight = (word: Word): number =>\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n word.metrics!.fontBoundingBoxAscent + word.metrics!.fontBoundingBoxDescent;\n\n // max height per line\n const lineHeights = wrappedLines.map((line) =>\n line.reduce((acc, word) => {\n return Math.max(acc, getHeight(word));\n }, 0)\n );\n const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);\n\n // vertical alignment (defaults to middle)\n let lineY: number;\n let textBaseline: CanvasTextBaseline;\n if (vAlign === 'top') {\n textBaseline = 'top';\n lineY = boxY;\n } else if (vAlign === 'bottom') {\n textBaseline = 'bottom';\n lineY = yEnd - totalHeight;\n } else {\n // middle\n textBaseline = 'top'; // YES, using 'top' baseline for 'middle' v-align\n lineY = boxY + boxHeight / 2 - totalHeight / 2;\n }\n\n const lines = wrappedLines.map((line, lineIdx): PositionedWord[] => {\n const lineWidth = line.reduce(\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n (acc, word) => acc + word.metrics!.width,\n 0\n );\n const lineHeight = lineHeights[lineIdx];\n\n // horizontal alignment (defaults to center)\n let lineX: number;\n if (align === 'right') {\n lineX = xEnd - lineWidth;\n } else if (align === 'left') {\n lineX = boxX;\n } else {\n // center\n lineX = boxX + boxWidth / 2 - lineWidth / 2;\n }\n\n let wordX = lineX;\n const posWords = line.map((word): PositionedWord => {\n // NOTE: `word.metrics` and `wordMap.get(hash)` must exist as every `word` MUST have\n // been measured at this point\n\n const hash = _getWordHash(word);\n const { format } = wordMap.get(hash)!;\n const x = wordX;\n const height = getHeight(word);\n\n // vertical alignment (defaults to middle)\n let y: number;\n if (vAlign === 'top') {\n y = lineY;\n } else if (vAlign === 'bottom') {\n y = lineY + lineHeight;\n } else {\n // middle\n y = lineY + (lineHeight - height) / 2;\n }\n\n wordX += word.metrics!.width;\n return {\n word,\n format, // undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)\n x,\n y,\n width: word.metrics!.width,\n height,\n isWhitespace: isWhitespace(word.text),\n };\n });\n\n lineY += lineHeight;\n return posWords;\n });\n\n return {\n lines,\n textBaseline,\n textAlign: 'left', // always per current algorithm\n width: boxWidth,\n height: totalHeight,\n };\n};\n\n/**\n * @private\n * Replacer for use with `JSON.stringify()` to deal with `TextMetrics` objects which\n * only have getters/setters instead of value-based properties.\n * @param key Key being processed in `this`.\n * @param value Value of `key` in `this`.\n * @returns Processed value to be serialized, or `undefined` to omit the `key` from the\n * serialized object.\n */\n// CAREFUL: use a `function`, not an arrow function, as stringify() sets its context to\n// the object being serialized on each call to the replacer\nconst _jsonReplacer = function (key: string, value: unknown) {\n if (key === 'metrics' && value && typeof value === 'object') {\n // TODO: need better typings here, if possible, so that TSC warns if we aren't\n // including a property we should be if a new one is needed in the future (i.e. if\n // a new property is added to the `TextMetricsLike` type)\n // NOTE: TextMetrics objects don't have own-enumerable properties; they only have getters,\n // so we have to explicitly get the values we care about instead of spreading them into\n // the new object\n const metrics: CanvasTextMetrics = value as CanvasTextMetrics;\n return {\n width: metrics.width,\n fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,\n fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,\n };\n }\n\n return value;\n};\n\n/**\n * Serializes render specs to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param specs\n * @returns Specs serialized as JSON.\n */\nexport const specToJson = (specs: RenderSpec): string => {\n return JSON.stringify(specs, _jsonReplacer);\n};\n\n/**\n * Serializes a list of Words to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param words\n * @returns Words serialized as JSON.\n */\nexport const wordsToJson = (words: Word[]): string => {\n return JSON.stringify(words, _jsonReplacer);\n};\n\n/**\n * @private\n * Measures a Word in a rendering context, assigning its `TextMetrics` to its `metrics` property.\n * @returns The Word's width, in pixels.\n */\nconst _measureWord = ({\n ctx,\n word,\n wordMap,\n baseTextFormat,\n}: {\n ctx: CanvasRenderContext;\n word: Word;\n wordMap: WordMap;\n baseTextFormat: TextFormat;\n}): number => {\n const hash = _getWordHash(word);\n\n if (word.metrics) {\n // assume Word's text and format haven't changed since last measurement and metrics are good\n\n // make sure we have the metrics and full formatting cached for other identical Words\n if (!wordMap.has(hash)) {\n let format = undefined;\n if (word.format) {\n format = getTextFormat(word.format, baseTextFormat);\n }\n wordMap.set(hash, { metrics: word.metrics, format });\n }\n\n return word.metrics.width;\n }\n\n // check to see if we have already measured an identical Word\n if (wordMap.has(hash)) {\n const { metrics } = wordMap.get(hash)!; // will be there because of `if(has())` check\n word.metrics = metrics;\n return metrics.width;\n }\n\n let ctxSaved = false;\n\n let format = undefined;\n if (word.format) {\n ctx.save();\n ctxSaved = true;\n format = getTextFormat(word.format, baseTextFormat);\n ctx.font = getTextStyle(format); // `fontColor` is ignored as it has no effect on metrics\n }\n\n if (!fontBoundingBoxSupported) {\n // use fallback which comes close enough and still gives us properly-aligned text, albeit\n // lines are a couple pixels tighter together\n if (!ctxSaved) {\n ctx.save();\n ctxSaved = true;\n }\n ctx.textBaseline = 'bottom';\n }\n\n const metrics = ctx.measureText(word.text);\n if (typeof metrics.fontBoundingBoxAscent === 'number') {\n fontBoundingBoxSupported = true;\n } else {\n fontBoundingBoxSupported = false;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxDescent = 0;\n }\n\n word.metrics = metrics;\n wordMap.set(hash, { metrics, format });\n\n if (ctxSaved) {\n ctx.restore();\n }\n\n return metrics.width;\n};\n\n/**\n * Splits Words into positioned lines of Words as they need to be rendred in 2D space,\n * but does not render anything.\n * @param config\n * @returns Lines of positioned words to be rendered, and total height required to\n * render all lines.\n */\nexport const splitWords = ({\n ctx,\n words,\n justify,\n format: baseFormat,\n inferWhitespace = true,\n ...positioning // rest of params are related to positioning\n}: SplitWordsProps): RenderSpec => {\n const wordMap: WordMap = new Map();\n const baseTextFormat = getTextFormat(baseFormat);\n const { width: boxWidth } = positioning;\n\n //// text measurement\n\n // measures an entire line's width up to the `boxWidth` as a max, unless `force=true`,\n // in which case the entire line is measured regardless of `boxWidth`.\n //\n // - Returned `lineWidth` is width up to, but not including, the `splitPoint` (always <= `boxWidth`\n // unless the first Word is too wide to fit, in which case `lineWidth` will be that Word's\n // width even though it's > `boxWidth`).\n // - If `force=true`, will be the full width of the line regardless of `boxWidth`.\n // - Returned `splitPoint` is index into `words` of the Word immediately FOLLOWING the last\n // Word included in the `lineWidth` (and is `words.length` if all Words were included);\n // `splitPoint` could also be thought of as the number of `words` included in the `lineWidth`.\n // - If `force=true`, will always be `words.length`.\n const measureLine = (\n lineWords: Word[],\n force: boolean = false\n ): {\n lineWidth: number;\n splitPoint: number;\n } => {\n let lineWidth = 0;\n let splitPoint = 0;\n lineWords.every((word, idx) => {\n const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });\n if (!force && lineWidth + wordWidth > boxWidth) {\n // at minimum, MUST include at least first Word, even if it's wider than box width\n if (idx === 0) {\n splitPoint = 1;\n lineWidth = wordWidth;\n }\n // else, `lineWidth` already includes at least one Word so this current Word will\n // be the `splitPoint` such that `lineWidth` remains < `boxWidth`\n\n return false; // break\n }\n\n splitPoint++;\n lineWidth += wordWidth;\n return true; // next\n });\n\n return { lineWidth, splitPoint };\n };\n\n //// main\n\n ctx.save();\n\n // start by trimming the `words` to remove any whitespace at either end, then split the `words`\n // into an initial set of lines dictated by explicit hard breaks, if any (if none, we'll have\n // one super long line)\n const hardLines = _splitIntoLines(\n trimLine(words).trimmedLine,\n inferWhitespace\n );\n\n if (\n hardLines.length <= 0 ||\n boxWidth <= 0 ||\n positioning.height <= 0 ||\n (baseFormat &&\n typeof baseFormat.fontSize === 'number' &&\n baseFormat.fontSize <= 0)\n ) {\n // width or height or font size cannot be 0, or there are no lines after trimming\n return {\n lines: [],\n textAlign: 'center',\n textBaseline: 'middle',\n width: positioning.width,\n height: 0,\n };\n }\n\n ctx.font = getTextStyle(baseTextFormat);\n\n const hairWidth = justify\n ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat })\n : 0;\n const wrappedLines: Word[][] = [];\n\n // now further wrap every hard line to make sure it fits within the `boxWidth`, down to a\n // MINIMUM of 1 Word per line\n for (const hardLine of hardLines) {\n let { splitPoint } = measureLine(hardLine);\n\n // if the line fits, we're done; else, we have to break it down further to fit\n // as best as we can (i.e. MIN one word per line, no breaks within words, no\n // leading/pending whitespace)\n if (splitPoint >= hardLine.length) {\n wrappedLines.push(hardLine);\n } else {\n // shallow clone because we're going to break this line down further to get the best fit\n let softLine = hardLine.concat();\n while (splitPoint < softLine.length) {\n // right-trim what we split off in case we split just after some whitespace\n const splitLine = trimLine(\n softLine.slice(0, splitPoint),\n 'right'\n ).trimmedLine;\n wrappedLines.push(splitLine);\n\n // left-trim what remains in case we split just before some whitespace\n softLine = trimLine(softLine.slice(splitPoint), 'left').trimmedLine;\n ({ splitPoint } = measureLine(softLine));\n }\n\n // get the last bit of the `softLine`\n // NOTE: since we started by timming the entire line, and we just left-trimmed\n // what remained of `softLine`, there should be no need to trim again\n wrappedLines.push(softLine);\n }\n }\n\n // never justify a single line because there's no other line to visually justify it to\n if (justify && wrappedLines.length > 1) {\n wrappedLines.forEach((wrappedLine, idx) => {\n // never justify the last line (common in text editors)\n if (idx < wrappedLines.length - 1) {\n const justifiedLine = justifyLine({\n line: wrappedLine,\n spaceWidth: hairWidth,\n spaceChar: HAIR,\n boxWidth,\n });\n\n // make sure any new Words used for justification get measured so we're able to\n // position them later when we generate the render spec\n measureLine(justifiedLine, true);\n wrappedLines[idx] = justifiedLine;\n }\n });\n }\n\n const spec = _generateSpec({\n wrappedLines,\n wordMap,\n positioning,\n });\n\n ctx.restore();\n return spec;\n};\n\n/**\n * Converts a string of text containing words and whitespace, as well as line breaks (newlines),\n * into a `Word[]` that can be given to `splitWords()`.\n * @param text String to convert into Words.\n * @returns Converted text.\n */\nexport const textToWords = (text: string) => {\n const words: Word[] = [];\n\n // split the `text` into a series of Words, preserving whitespace\n let word: Word | undefined = undefined;\n let wasWhitespace = false;\n Array.from(text.trim()).forEach((c) => {\n const charIsWhitespace = isWhitespace(c);\n if (\n (charIsWhitespace && !wasWhitespace) ||\n (!charIsWhitespace && wasWhitespace)\n ) {\n // save current `word`, if any, and start new `word`\n wasWhitespace = charIsWhitespace;\n if (word) {\n words.push(word);\n }\n word = { text: c };\n } else {\n // accumulate into current `word`\n if (!word) {\n word = { text: '' };\n }\n word.text += c;\n }\n });\n\n // make sure we have the last word! ;)\n if (word) {\n words.push(word);\n }\n\n return words;\n};\n\n/**\n * Splits plain text into lines in the order in which they should be rendered, top-down,\n * preserving whitespace __only within the text__ (whitespace on either end is trimmed).\n */\nexport const splitText = ({ text, ...params }: SplitTextProps): string[] => {\n const words = textToWords(text);\n\n const results = splitWords({\n ...params,\n words,\n inferWhitespace: false,\n });\n\n return results.lines.map((line) =>\n line.map(({ word: { text: t } }) => t).join('')\n );\n};\n","import { getTextStyle } from './style';\nimport { CanvasRenderContext, Word } from '../model';\n\n/** @private */\nconst _getHeight = (ctx: CanvasRenderContext, text: string, style?: string) => {\n const previousTextBaseline = ctx.textBaseline;\n const previousFont = ctx.font;\n\n ctx.textBaseline = 'bottom';\n if (style) {\n ctx.font = style;\n }\n const { actualBoundingBoxAscent: height } = ctx.measureText(text);\n\n // Reset baseline\n ctx.textBaseline = previousTextBaseline;\n if (style) {\n ctx.font = previousFont;\n }\n\n return height;\n};\n\n/**\n * Gets the measured height of a given `Word` using its text style.\n * @returns {number} Height in pixels.\n */\nexport const getWordHeight = ({\n ctx,\n word,\n}: {\n ctx: CanvasRenderContext;\n /**\n * Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.\n */\n word: Word;\n}) => {\n return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));\n};\n\n/**\n * Gets the measured height of a given `string` using a given text style.\n * @returns {number} Height in pixels.\n */\nexport const getTextHeight = ({\n ctx,\n text,\n style,\n}: {\n ctx: CanvasRenderContext;\n text: string;\n /**\n * CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font\n * settings/styles are used.\n */\n style?: string;\n}) => {\n return _getHeight(ctx, text, style);\n};\n","import {\n specToJson,\n splitWords,\n splitText,\n textToWords,\n wordsToJson,\n} from './util/split';\nimport { getTextHeight, getWordHeight } from './util/height';\nimport { getTextStyle, getTextFormat, DEFAULT_FONT_COLOR } from './util/style';\nimport { CanvasRenderContext, DrawTextConfig, Text } from './model';\n\nconst drawText = (\n ctx: CanvasRenderContext,\n text: Text,\n config: DrawTextConfig\n) => {\n const baseFormat = getTextFormat({\n fontFamily: config.fontFamily,\n fontSize: config.fontSize,\n fontStyle: config.fontStyle,\n fontVariant: config.fontVariant,\n fontWeight: config.fontWeight,\n });\n\n const {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n } = config;\n\n const {\n lines: richLines,\n height: totalHeight,\n textBaseline,\n textAlign,\n } = splitWords({\n ctx,\n words: Array.isArray(text) ? text : textToWords(text),\n inferWhitespace: Array.isArray(text)\n ? config.inferWhitespace === undefined || config.inferWhitespace\n : undefined, // ignore since `text` is a string; we assume it already has all the whitespace it needs\n x: boxX,\n y: boxY,\n width: config.width,\n height: config.height,\n align: config.align,\n vAlign: config.vAlign,\n justify: config.justify,\n format: baseFormat,\n });\n\n ctx.save();\n ctx.textAlign = textAlign;\n ctx.textBaseline = textBaseline;\n ctx.font = getTextStyle(baseFormat);\n ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;\n\n if (config.overflow === false) {\n ctx.beginPath();\n ctx.rect(boxX, boxY, boxWidth, boxHeight);\n ctx.clip(); // part of saved context state\n }\n\n richLines.forEach((line) => {\n line.forEach((pw) => {\n if (!pw.isWhitespace) {\n // NOTE: don't use the `pw.word.format` as this could be incomplete; use `pw.format`\n // if it exists as this will always be the __full__ TextFormat used to measure the\n // Word, and so should be what is used to render it\n if (pw.format) {\n ctx.save();\n ctx.font = getTextStyle(pw.format);\n if (pw.format.fontColor) {\n ctx.fillStyle = pw.format.fontColor;\n }\n }\n ctx.fillText(pw.word.text, pw.x, pw.y);\n if (pw.format) {\n ctx.restore();\n }\n }\n });\n });\n\n if (config.debug) {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n let textAnchor: number;\n if (config.align === 'right') {\n textAnchor = xEnd;\n } else if (config.align === 'left') {\n textAnchor = boxX;\n } else {\n textAnchor = boxX + boxWidth / 2;\n }\n\n let debugY = boxY;\n if (config.vAlign === 'bottom') {\n debugY = yEnd;\n } else if (config.vAlign === 'middle') {\n debugY = boxY + boxHeight / 2;\n }\n\n const debugColor = '#0C8CE9';\n\n // Text box\n ctx.lineWidth = 1;\n ctx.strokeStyle = debugColor;\n ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);\n\n ctx.lineWidth = 1;\n\n if (!config.align || config.align === 'center') {\n // Horizontal Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(textAnchor, boxY);\n ctx.lineTo(textAnchor, yEnd);\n ctx.stroke();\n }\n\n if (!config.vAlign || config.vAlign === 'middle') {\n // Vertical Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(boxX, debugY);\n ctx.lineTo(xEnd, debugY);\n ctx.stroke();\n }\n }\n\n ctx.restore();\n\n return { height: totalHeight };\n};\n\nexport {\n drawText,\n specToJson,\n splitText,\n splitWords,\n textToWords,\n wordsToJson,\n getTextHeight,\n getWordHeight,\n getTextStyle,\n getTextFormat,\n};\nexport * from './model';\n"],"names":["DEFAULT_FONT_FAMILY","DEFAULT_FONT_COLOR","getTextFormat","format","baseFormat","getTextStyle","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","isWhitespace","text","_extractWords","line","word","_cloneWord","clone","_joinWords","words","joiner","phrase","wordIdx","jw","justifyLine","spaceWidth","spaceChar","boxWidth","wordsWidth","width","_a","noOfSpacesToInsert","spacesPerWord","spaces","firstWords","firstPart","remainingSpaces","lastWord","trimLine","side","leftTrim","rightTrim","HAIR","SPACE","fontBoundingBoxSupported","_getWordHash","_splitIntoLines","inferWhitespace","lines","wasWhitespace","_b","_c","i","_generateSpec","wrappedLines","wordMap","boxHeight","boxX","boxY","align","vAlign","xEnd","yEnd","getHeight","lineHeights","acc","totalHeight","h","lineY","textBaseline","lineIdx","lineWidth","lineHeight","lineX","wordX","posWords","hash","x","height","y","_jsonReplacer","key","value","metrics","specToJson","specs","wordsToJson","_measureWord","ctx","baseTextFormat","ctxSaved","splitWords","justify","positioning","measureLine","lineWords","force","splitPoint","idx","wordWidth","hardLines","hairWidth","hardLine","softLine","splitLine","wrappedLine","justifiedLine","spec","textToWords","c","charIsWhitespace","splitText","params","t","_getHeight","style","previousTextBaseline","previousFont","getWordHeight","getTextHeight","drawText","config","richLines","textAlign","pw","textAnchor","debugY","debugColor"],"mappings":"AAEO,MAAMA,IAAsB;AAE5B,MAAMC,IAAqB,SAQrBC,IAAgB,CAC3BC,GACAC,MAEO,OAAO;AAAA,EACZ,CAAC;AAAA,EACD;AAAA,IACE,YAAYJ;AAAA,IACZ,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,aAAa;AAAA,IACb,WAAWC;AAAA,EACb;AAAA,EACAG;AAAA,EACAD;AAAA,GAUSE,IAAe,CAAC;AAAA,EAC3B,YAAAC;AAAA,EACA,UAAAC;AAAA,EACA,WAAAC;AAAA,EACA,aAAAC;AAAA,EACA,YAAAC;AACF,MAKS,GAAGF,KAAa,EAAE,IAAIC,KAAe,EAAE,IAC5CC,KAAc,EAChB,IAAIH,KAAY,EAAiB,MAAMD,KAAcN,CAAmB,GAAG,QC7ChEW,IAAe,CAACC,MACpB,CAAC,CAACA,EAAK,MAAM,OAAO,GCGvBC,IAAgB,CAACC,MACdA,EAAK,OAAO,CAACC,MAAS,CAACJ,EAAaI,EAAK,IAAI,CAAC,GASjDC,IAAa,CAACD,MAAe;AAC3B,QAAAE,IAAQ,EAAE,GAAGF;AACnB,SAAIA,EAAK,WACPE,EAAM,SAAS,EAAE,GAAGF,EAAK,OAAO,IAE3BE;AACT,GAYMC,IAAa,CAACC,GAAeC,MAAmB;AACpD,MAAID,EAAM,UAAU,KAAKC,EAAO,SAAS;AAChC,WAAA,CAAC,GAAGD,CAAK;AAGlB,QAAME,IAAiB,CAAA;AACjB,SAAAF,EAAA,QAAQ,CAACJ,GAAMO,MAAY;AAC/B,IAAAD,EAAO,KAAKN,CAAI,GACZO,IAAUH,EAAM,SAAS,KAEpBC,EAAA,QAAQ,CAACG,MAAOF,EAAO,KAAKL,EAAWO,CAAE,CAAC,CAAC;AAAA,EACpD,CACD,GAEMF;AACT,GAUaG,IAAc,CAAC;AAAA,EAC1B,MAAAV;AAAA,EACA,YAAAW;AAAA,EACA,WAAAC;AAAA,EACA,UAAAC;AACF,MAYM;AACE,QAAAR,IAAQN,EAAcC,CAAI;AAC5B,MAAAK,EAAM,UAAU;AAClB,WAAOL,EAAK;AAGd,QAAMc,IAAaT,EAAM;AAAA,IACvB,CAACU,GAAOd;AFpFL,UAAAe;AEoFc,aAAAD,OAASC,IAAAf,EAAK,YAAL,gBAAAe,EAAc,UAAS;AAAA;AAAA,IACjD;AAAA,EAAA,GAEIC,KAAsBJ,IAAWC,KAAcH;AAEjD,MAAAN,EAAM,SAAS,GAAG;AAIpB,UAAMa,IAAgB,KAAK,KAAKD,KAAsBZ,EAAM,SAAS,EAAE,GACjEc,IAAiB,MAAM,KAAK,EAAE,QAAQD,EAAA,GAAiB,OAAO;AAAA,MAClE,MAAMN;AAAA,IACN,EAAA,GACIQ,IAAaf,EAAM,MAAM,GAAGA,EAAM,SAAS,CAAC,GAC5CgB,IAAYjB,EAAWgB,GAAYD,CAAM,GACzCG,IAAkBH,EAAO;AAAA,MAC7B;AAAA,MACA,KAAK,MAAMF,CAAkB,KAAKG,EAAW,SAAS,KAAKD,EAAO;AAAA,IAAA,GAE9DI,IAAWlB,EAAMA,EAAM,SAAS,CAAC;AACvC,WAAO,CAAC,GAAGgB,GAAW,GAAGC,GAAiBC,CAAQ;AAAA,EACpD;AAGA,QAAMJ,IAAiB,MAAM;AAAA,IAC3B,EAAE,QAAQ,KAAK,MAAMF,CAAkB,EAAE;AAAA,IACzC,OAAO,EAAE,MAAML;EAAU;AAEpB,SAAAR,EAAWC,GAAOc,CAAM;AACjC,GC1GaK,IAAW,CACtBxB,GACAyB,IAAkC,WAe/B;AACH,MAAIC,IAAW;AACX,MAAAD,MAAS,UAAUA,MAAS,QAAQ;AAC/B,WAAAC,IAAW1B,EAAK,UAChBH,EAAaG,EAAK0B,CAAQ,EAAE,IAAI,GADRA;AAC7B;AAKE,QAAAA,KAAY1B,EAAK;AAEZ,aAAA;AAAA,QACL,aAAaA,EAAK,OAAO;AAAA,QACzB,cAAc,CAAC;AAAA,QACf,aAAa,CAAC;AAAA,MAAA;AAAA,EAGpB;AAEA,MAAI2B,IAAY3B,EAAK;AACjB,MAAAyB,MAAS,WAAWA,MAAS,QAAQ;AAEhC,SADPE,KACOA,KAAa,KACb9B,EAAaG,EAAK2B,CAAS,EAAE,IAAI,GADjBA;AACrB;AAMF,QAFAA,KAEIA,KAAa;AAER,aAAA;AAAA,QACL,aAAa,CAAC;AAAA,QACd,cAAc3B,EAAK,OAAO;AAAA,QAC1B,aAAa,CAAC;AAAA,MAAA;AAAA,EAGpB;AAEO,SAAA;AAAA,IACL,aAAaA,EAAK,MAAM,GAAG0B,CAAQ;AAAA,IACnC,cAAc1B,EAAK,MAAM2B,CAAS;AAAA,IAClC,aAAa3B,EAAK,MAAM0B,GAAUC,CAAS;AAAA,EAAA;AAE/C,GCrDMC,IAAO,KAGPC,IAAQ;AAcd,IAAIC;AAQJ,MAAMC,IAAe,CAAC9B,MACb,GAAGA,EAAK,IAAI,GAAGA,EAAK,SAAS,KAAK,UAAUA,EAAK,MAAM,IAAI,EAAE,IAWhE+B,IAAkB,CACtB3B,GACA4B,IAA2B,OACd;AACP,QAAAC,IAAkB,CAAC,CAAA,CAAE;AAE3B,MAAIC,IAAgB;AACd,SAAA9B,EAAA,QAAQ,CAACJ,GAAMO,MAAY;AJ3D5B,QAAAQ,GAAAoB,GAAAC;AI8DH,QAAIpC,EAAK,KAAK,MAAM,OAAO,GAAG;AAC5B,eAASqC,IAAI,GAAGA,IAAIrC,EAAK,KAAK,QAAQqC;AAC9B,QAAAJ,EAAA,KAAK,CAAA,CAAE;AAEC,MAAAC,IAAA;AAChB;AAAA,IACF;AAEI,QAAAtC,EAAaI,EAAK,IAAI,GAAG;AAE3B,OAAAe,IAAAkB,EAAM,GAAG,EAAE,MAAX,QAAAlB,EAAc,KAAKf,IACHkC,IAAA;AAChB;AAAA,IACF;AAEI,IAAAlC,EAAK,SAAS,OAMdgC,KAAmB,CAACE,KAAiB3B,IAAU,OACjD4B,IAAAF,EAAM,GAAG,EAAE,MAAX,QAAAE,EAAc,KAAK,EAAE,MAAMP,QAG7BQ,IAAAH,EAAM,GAAG,EAAE,MAAX,QAAAG,EAAc,KAAKpC,IACHkC,IAAA;AAAA,EAAA,CACjB,GAEMD;AACT,GASMK,IAAgB,CAAC;AAAA,EACrB,cAAAC;AAAA,EACA,SAAAC;AAAA,EACA,aAAa;AAAA,IACX,OAAO5B;AAAA,IACP,QAAQ6B;AAAA,IACR,GAAGC,IAAO;AAAA,IACV,GAAGC,IAAO;AAAA,IACV,OAAAC;AAAA,IACA,QAAAC;AAAA,EACF;AACF,MAoBkB;AAChB,QAAMC,IAAOJ,IAAO9B,GACdmC,IAAOJ,IAAOF,GASdO,IAAY,CAAChD;AAAA;AAAA,IAEjBA,EAAK,QAAS,wBAAwBA,EAAK,QAAS;AAAA,KAGhDiD,IAAcV,EAAa;AAAA,IAAI,CAACxC,MACpCA,EAAK,OAAO,CAACmD,GAAKlD,MACT,KAAK,IAAIkD,GAAKF,EAAUhD,CAAI,CAAC,GACnC,CAAC;AAAA,EAAA,GAEAmD,IAAcF,EAAY,OAAO,CAACC,GAAKE,MAAMF,IAAME,GAAG,CAAC;AAGzD,MAAAC,GACAC;AACJ,SAAIT,MAAW,SACES,IAAA,OACPD,IAAAV,KACCE,MAAW,YACLS,IAAA,UACfD,IAAQN,IAAOI,MAGAG,IAAA,OACPD,IAAAV,IAAOF,IAAY,IAAIU,IAAc,IA2DxC;AAAA,IACL,OAzDYZ,EAAa,IAAI,CAACxC,GAAMwD,MAA8B;AAClE,YAAMC,IAAYzD,EAAK;AAAA;AAAA,QAErB,CAACmD,GAAKlD,MAASkD,IAAMlD,EAAK,QAAS;AAAA,QACnC;AAAA,MAAA,GAEIyD,IAAaR,EAAYM,CAAO;AAGlC,UAAAG;AACJ,MAAId,MAAU,UACZc,IAAQZ,IAAOU,IACNZ,MAAU,SACXc,IAAAhB,IAGAgB,IAAAhB,IAAO9B,IAAW,IAAI4C,IAAY;AAG5C,UAAIG,IAAQD;AACZ,YAAME,IAAW7D,EAAK,IAAI,CAACC,MAAyB;AAI5C,cAAA6D,IAAO/B,EAAa9B,CAAI,GACxB,EAAE,QAAAZ,EAAW,IAAAoD,EAAQ,IAAIqB,CAAI,GAC7BC,IAAIH,GACJI,IAASf,EAAUhD,CAAI;AAGzB,YAAAgE;AACJ,eAAInB,MAAW,QACTmB,IAAAX,IACKR,MAAW,WACpBmB,IAAIX,IAAQI,IAGRO,IAAAX,KAASI,IAAaM,KAAU,GAGtCJ,KAAS3D,EAAK,QAAS,OAChB;AAAA,UACL,MAAAA;AAAA,UACA,QAAAZ;AAAA;AAAA,UACA,GAAA0E;AAAA,UACA,GAAAE;AAAA,UACA,OAAOhE,EAAK,QAAS;AAAA,UACrB,QAAA+D;AAAA,UACA,cAAcnE,EAAaI,EAAK,IAAI;AAAA,QAAA;AAAA,MACtC,CACD;AAEQ,aAAAqD,KAAAI,GACFG;AAAA,IAAA,CACR;AAAA,IAIC,cAAAN;AAAA,IACA,WAAW;AAAA;AAAA,IACX,OAAO1C;AAAA,IACP,QAAQuC;AAAA,EAAA;AAEZ,GAaMc,IAAgB,SAAUC,GAAaC,GAAgB;AAC3D,MAAID,MAAQ,aAAaC,KAAS,OAAOA,KAAU,UAAU;AAO3D,UAAMC,IAA6BD;AAC5B,WAAA;AAAA,MACL,OAAOC,EAAQ;AAAA,MACf,uBAAuBA,EAAQ;AAAA,MAC/B,wBAAwBA,EAAQ;AAAA,IAAA;AAAA,EAEpC;AAEO,SAAAD;AACT,GAYaE,IAAa,CAACC,MAClB,KAAK,UAAUA,GAAOL,CAAa,GAa/BM,IAAc,CAACnE,MACnB,KAAK,UAAUA,GAAO6D,CAAa,GAQtCO,IAAe,CAAC;AAAA,EACpB,KAAAC;AAAA,EACA,MAAAzE;AAAA,EACA,SAAAwC;AAAA,EACA,gBAAAkC;AACF,MAKc;AACN,QAAAb,IAAO/B,EAAa9B,CAAI;AAE9B,MAAIA,EAAK,SAAS;AAIhB,QAAI,CAACwC,EAAQ,IAAIqB,CAAI,GAAG;AACtB,UAAIzE;AACJ,MAAIY,EAAK,WACPZ,IAASD,EAAca,EAAK,QAAQ0E,CAAc,IAE5ClC,EAAA,IAAIqB,GAAM,EAAE,SAAS7D,EAAK,SAAS,QAAAZ,GAAQ;AAAA,IACrD;AAEA,WAAOY,EAAK,QAAQ;AAAA,EACtB;AAGI,MAAAwC,EAAQ,IAAIqB,CAAI,GAAG;AACrB,UAAM,EAAE,SAAAO,EAAAA,IAAY5B,EAAQ,IAAIqB,CAAI;AACpC,WAAA7D,EAAK,UAAUoE,GACRA,EAAQ;AAAA,EACjB;AAEA,MAAIO,IAAW,IAEXvF;AACJ,EAAIY,EAAK,WACPyE,EAAI,KAAK,GACEE,IAAA,IACFvF,IAAAD,EAAca,EAAK,QAAQ0E,CAAc,GAC9CD,EAAA,OAAOnF,EAAaF,CAAM,IAG3ByC,MAGE8C,MACHF,EAAI,KAAK,GACEE,IAAA,KAEbF,EAAI,eAAe;AAGrB,QAAML,IAAUK,EAAI,YAAYzE,EAAK,IAAI;AACrC,SAAA,OAAOoE,EAAQ,yBAA0B,WAChBvC,IAAA,MAEAA,IAAA,IAE3BuC,EAAQ,wBAAwBA,EAAQ,yBAExCA,EAAQ,yBAAyB,IAGnCpE,EAAK,UAAUoE,GACf5B,EAAQ,IAAIqB,GAAM,EAAE,SAAAO,GAAS,QAAAhF,EAAQ,CAAA,GAEjCuF,KACFF,EAAI,QAAQ,GAGPL,EAAQ;AACjB,GASaQ,IAAa,CAAC;AAAA,EACzB,KAAAH;AAAA,EACA,OAAArE;AAAA,EACA,SAAAyE;AAAA,EACA,QAAQxF;AAAA,EACR,iBAAA2C,IAAkB;AAAA,EAClB,GAAG8C;AAAA;AACL,MAAmC;AAC3B,QAAAtC,wBAAuB,OACvBkC,IAAiBvF,EAAcE,CAAU,GACzC,EAAE,OAAOuB,EAAa,IAAAkE,GAetBC,IAAc,CAClBC,GACAC,IAAiB,OAId;AACH,QAAIzB,IAAY,GACZ0B,IAAa;AACP,WAAAF,EAAA,MAAM,CAAChF,GAAMmF,MAAQ;AAC7B,YAAMC,IAAYZ,EAAa,EAAE,KAAAC,GAAK,MAAAzE,GAAM,SAAAwC,GAAS,gBAAAkC,GAAgB;AACrE,aAAI,CAACO,KAASzB,IAAY4B,IAAYxE,KAEhCuE,MAAQ,MACGD,IAAA,GACD1B,IAAA4B,IAKP,OAGTF,KACa1B,KAAA4B,GACN;AAAA,IAAA,CACR,GAEM,EAAE,WAAA5B,GAAW,YAAA0B;EAAW;AAKjC,EAAAT,EAAI,KAAK;AAKT,QAAMY,IAAYtD;AAAA,IAChBR,EAASnB,CAAK,EAAE;AAAA,IAChB4B;AAAA,EAAA;AAGF,MACEqD,EAAU,UAAU,KACpBzE,KAAY,KACZkE,EAAY,UAAU,KACrBzF,KACC,OAAOA,EAAW,YAAa,YAC/BA,EAAW,YAAY;AAGlB,WAAA;AAAA,MACL,OAAO,CAAC;AAAA,MACR,WAAW;AAAA,MACX,cAAc;AAAA,MACd,OAAOyF,EAAY;AAAA,MACnB,QAAQ;AAAA,IAAA;AAIR,EAAAL,EAAA,OAAOnF,EAAaoF,CAAc;AAEtC,QAAMY,IAAYT,IACdL,EAAa,EAAE,KAAAC,GAAK,MAAM,EAAE,MAAM9C,EAAK,GAAG,SAAAa,GAAS,gBAAAkC,EAAgB,CAAA,IACnE,GACEnC,IAAyB,CAAA;AAI/B,aAAWgD,KAAYF,GAAW;AAChC,QAAI,EAAE,YAAAH,EAAA,IAAeH,EAAYQ,CAAQ;AAKrC,QAAAL,KAAcK,EAAS;AACzB,MAAAhD,EAAa,KAAKgD,CAAQ;AAAA,SACrB;AAED,UAAAC,IAAWD,EAAS;AACjB,aAAAL,IAAaM,EAAS,UAAQ;AAEnC,cAAMC,IAAYlE;AAAA,UAChBiE,EAAS,MAAM,GAAGN,CAAU;AAAA,UAC5B;AAAA,QACA,EAAA;AACF,QAAA3C,EAAa,KAAKkD,CAAS,GAG3BD,IAAWjE,EAASiE,EAAS,MAAMN,CAAU,GAAG,MAAM,EAAE,aACvD,EAAE,YAAAA,EAAA,IAAeH,EAAYS,CAAQ;AAAA,MACxC;AAKA,MAAAjD,EAAa,KAAKiD,CAAQ;AAAA,IAC5B;AAAA,EACF;AAGI,EAAAX,KAAWtC,EAAa,SAAS,KACtBA,EAAA,QAAQ,CAACmD,GAAaP,MAAQ;AAErC,QAAAA,IAAM5C,EAAa,SAAS,GAAG;AACjC,YAAMoD,IAAgBlF,EAAY;AAAA,QAChC,MAAMiF;AAAA,QACN,YAAYJ;AAAA,QACZ,WAAW3D;AAAA,QACX,UAAAf;AAAA,MAAA,CACD;AAID,MAAAmE,EAAYY,GAAe,EAAI,GAC/BpD,EAAa4C,CAAG,IAAIQ;AAAA,IACtB;AAAA,EAAA,CACD;AAGH,QAAMC,IAAOtD,EAAc;AAAA,IACzB,cAAAC;AAAA,IACA,SAAAC;AAAA,IACA,aAAAsC;AAAA,EAAA,CACD;AAED,SAAAL,EAAI,QAAQ,GACLmB;AACT,GAQaC,IAAc,CAAChG,MAAiB;AAC3C,QAAMO,IAAgB,CAAA;AAGtB,MAAIJ,GACAkC,IAAgB;AACpB,eAAM,KAAKrC,EAAK,KAAM,CAAA,EAAE,QAAQ,CAACiG,MAAM;AAC/B,UAAAC,IAAmBnG,EAAakG,CAAC;AACvC,IACGC,KAAoB,CAAC7D,KACrB,CAAC6D,KAAoB7D,KAGNA,IAAA6D,GACZ/F,KACFI,EAAM,KAAKJ,CAAI,GAEVA,IAAA,EAAE,MAAM8F,QAGV9F,MACIA,IAAA,EAAE,MAAM,OAEjBA,EAAK,QAAQ8F;AAAA,EACf,CACD,GAGG9F,KACFI,EAAM,KAAKJ,CAAI,GAGVI;AACT,GAMa4F,IAAY,CAAC,EAAE,MAAAnG,GAAM,GAAGoG,QAAuC;AACpE,QAAA7F,IAAQyF,EAAYhG,CAAI;AAQ9B,SANgB+E,EAAW;AAAA,IACzB,GAAGqB;AAAA,IACH,OAAA7F;AAAA,IACA,iBAAiB;AAAA,EAAA,CAClB,EAEc,MAAM;AAAA,IAAI,CAACL,MACxBA,EAAK,IAAI,CAAC,EAAE,MAAM,EAAE,MAAMmG,EAAI,EAAA,MAAMA,CAAC,EAAE,KAAK,EAAE;AAAA,EAAA;AAElD,GChlBMC,IAAa,CAAC1B,GAA0B5E,GAAcuG,MAAmB;AAC7E,QAAMC,IAAuB5B,EAAI,cAC3B6B,IAAe7B,EAAI;AAEzB,EAAAA,EAAI,eAAe,UACf2B,MACF3B,EAAI,OAAO2B;AAEb,QAAM,EAAE,yBAAyBrC,EAAA,IAAWU,EAAI,YAAY5E,CAAI;AAGhE,SAAA4E,EAAI,eAAe4B,GACfD,MACF3B,EAAI,OAAO6B,IAGNvC;AACT,GAMawC,IAAgB,CAAC;AAAA,EAC5B,KAAA9B;AAAA,EACA,MAAAzE;AACF,MAOSmG,EAAW1B,GAAKzE,EAAK,MAAMA,EAAK,UAAUV,EAAaU,EAAK,MAAM,CAAC,GAO/DwG,KAAgB,CAAC;AAAA,EAC5B,KAAA/B;AAAA,EACA,MAAA5E;AAAA,EACA,OAAAuG;AACF,MASSD,EAAW1B,GAAK5E,GAAMuG,CAAK,GC9C9BK,KAAW,CACfhC,GACA5E,GACA6G,MACG;AACH,QAAMrH,IAAaF,EAAc;AAAA,IAC/B,YAAYuH,EAAO;AAAA,IACnB,UAAUA,EAAO;AAAA,IACjB,WAAWA,EAAO;AAAA,IAClB,aAAaA,EAAO;AAAA,IACpB,YAAYA,EAAO;AAAA,EAAA,CACpB,GAEK;AAAA,IACJ,OAAO9F;AAAA,IACP,QAAQ6B;AAAA,IACR,GAAGC,IAAO;AAAA,IACV,GAAGC,IAAO;AAAA,EACR,IAAA+D,GAEE;AAAA,IACJ,OAAOC;AAAA,IACP,QAAQxD;AAAA,IACR,cAAAG;AAAA,IACA,WAAAsD;AAAA,MACEhC,EAAW;AAAA,IACb,KAAAH;AAAA,IACA,OAAO,MAAM,QAAQ5E,CAAI,IAAIA,IAAOgG,EAAYhG,CAAI;AAAA,IACpD,iBAAiB,MAAM,QAAQA,CAAI,IAC/B6G,EAAO,oBAAoB,UAAaA,EAAO,kBAC/C;AAAA;AAAA,IACJ,GAAGhE;AAAA,IACH,GAAGC;AAAA,IACH,OAAO+D,EAAO;AAAA,IACd,QAAQA,EAAO;AAAA,IACf,OAAOA,EAAO;AAAA,IACd,QAAQA,EAAO;AAAA,IACf,SAASA,EAAO;AAAA,IAChB,QAAQrH;AAAA,EAAA,CACT;AAmCD,MAjCAoF,EAAI,KAAK,GACTA,EAAI,YAAYmC,GAChBnC,EAAI,eAAenB,GACfmB,EAAA,OAAOnF,EAAaD,CAAU,GAC9BoF,EAAA,YAAYpF,EAAW,aAAaH,GAEpCwH,EAAO,aAAa,OACtBjC,EAAI,UAAU,GACdA,EAAI,KAAK/B,GAAMC,GAAM/B,GAAU6B,CAAS,GACxCgC,EAAI,KAAK,IAGDkC,EAAA,QAAQ,CAAC5G,MAAS;AACrB,IAAAA,EAAA,QAAQ,CAAC8G,MAAO;AACf,MAACA,EAAG,iBAIFA,EAAG,WACLpC,EAAI,KAAK,GACLA,EAAA,OAAOnF,EAAauH,EAAG,MAAM,GAC7BA,EAAG,OAAO,cACRpC,EAAA,YAAYoC,EAAG,OAAO,aAG9BpC,EAAI,SAASoC,EAAG,KAAK,MAAMA,EAAG,GAAGA,EAAG,CAAC,GACjCA,EAAG,UACLpC,EAAI,QAAQ;AAAA,IAEhB,CACD;AAAA,EAAA,CACF,GAEGiC,EAAO,OAAO;AAChB,UAAM5D,IAAOJ,IAAO9B,GACdmC,IAAOJ,IAAOF;AAEhB,QAAAqE;AACA,IAAAJ,EAAO,UAAU,UACNI,IAAAhE,IACJ4D,EAAO,UAAU,SACbI,IAAApE,IAEboE,IAAapE,IAAO9B,IAAW;AAGjC,QAAImG,IAASpE;AACT,IAAA+D,EAAO,WAAW,WACXK,IAAAhE,IACA2D,EAAO,WAAW,aAC3BK,IAASpE,IAAOF,IAAY;AAG9B,UAAMuE,IAAa;AAGnB,IAAAvC,EAAI,YAAY,GAChBA,EAAI,cAAcuC,GAClBvC,EAAI,WAAW/B,GAAMC,GAAM/B,GAAU6B,CAAS,GAE9CgC,EAAI,YAAY,IAEZ,CAACiC,EAAO,SAASA,EAAO,UAAU,cAEpCjC,EAAI,cAAcuC,GAClBvC,EAAI,UAAU,GACVA,EAAA,OAAOqC,GAAYnE,CAAI,GACvB8B,EAAA,OAAOqC,GAAY/D,CAAI,GAC3B0B,EAAI,OAAO,KAGT,CAACiC,EAAO,UAAUA,EAAO,WAAW,cAEtCjC,EAAI,cAAcuC,GAClBvC,EAAI,UAAU,GACVA,EAAA,OAAO/B,GAAMqE,CAAM,GACnBtC,EAAA,OAAO3B,GAAMiE,CAAM,GACvBtC,EAAI,OAAO;AAAA,EAEf;AAEA,SAAAA,EAAI,QAAQ,GAEL,EAAE,QAAQtB;AACnB;"}
|
|
1
|
+
{"version":3,"file":"text-to-canvas.esm.min.js","sources":["../src/lib/util/style.ts","../src/lib/util/whitespace.ts","../src/lib/util/justify.ts","../src/lib/util/trim.ts","../src/lib/util/split.ts","../src/lib/util/height.ts","../src/lib/index.ts"],"sourcesContent":["import { TextFormat } from '../model';\n\nexport const DEFAULT_FONT_FAMILY = 'Arial';\nexport const DEFAULT_FONT_SIZE = 14;\nexport const DEFAULT_FONT_COLOR = 'black';\n\n/**\n * Generates a text format based on defaults and any provided overrides.\n * @param format Overrides to `baseFormat` and default format.\n * @param baseFormat Overrides to default format.\n * @returns Full text format (all properties specified).\n */\nexport const getTextFormat = (\n format?: TextFormat,\n baseFormat?: TextFormat\n): Required<TextFormat> => {\n return Object.assign(\n {},\n {\n fontFamily: DEFAULT_FONT_FAMILY,\n fontSize: DEFAULT_FONT_SIZE,\n fontWeight: '400',\n fontStyle: '',\n fontVariant: '',\n fontColor: DEFAULT_FONT_COLOR,\n },\n baseFormat,\n format\n );\n};\n\n/**\n * Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.\n * @param format\n * @returns Style string to set on context's `font` property. Note this __does not include\n * the font color__ as that is not part of the CSS font value. Color must be handled separately.\n */\nexport const getTextStyle = ({\n fontFamily,\n fontSize,\n fontStyle,\n fontVariant,\n fontWeight,\n}: TextFormat) => {\n // per spec:\n // - font-style, font-variant and font-weight must precede font-size\n // - font-family must be the last value specified\n // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font\n return `${fontStyle || ''} ${fontVariant || ''} ${\n fontWeight || ''\n } ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();\n};\n","/**\n * Determines if a string is only whitespace (one or more characters of it).\n * @param text\n * @returns True if `text` is one or more characters of whitespace, only.\n */\nexport const isWhitespace = (text: string) => {\n return !!text.match(/^\\s+$/);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * @private\n * Extracts the __visible__ (i.e. non-whitespace) words from a line.\n * @param line\n * @returns New array with only non-whitespace words.\n */\nconst _extractWords = (line: Word[]) => {\n return line.filter((word) => !isWhitespace(word.text));\n};\n\n/**\n * @private\n * Deep-clones a Word.\n * @param word\n * @returns Deep-cloned Word.\n */\nconst _cloneWord = (word: Word) => {\n const clone = { ...word };\n if (word.format) {\n clone.format = { ...word.format };\n }\n return clone;\n};\n\n/**\n * @private\n * Joins Words together using another set of Words.\n * @param words Words to join.\n * @param joiner Words to use when joining `words`. These will be deep-cloned and inserted\n * in between every word in `words`, similar to `Array.join(string)` where the `string`\n * is inserted in between every element.\n * @returns New array of Words. Empty if `words` is empty. New array of one Word if `words`\n * contains only one Word.\n */\nconst _joinWords = (words: Word[], joiner: Word[]) => {\n if (words.length <= 1 || joiner.length < 1) {\n return [...words];\n }\n\n const phrase: Word[] = [];\n words.forEach((word, wordIdx) => {\n phrase.push(word);\n if (wordIdx < words.length - 1) {\n // don't append after last `word`\n joiner.forEach((jw) => phrase.push(_cloneWord(jw)));\n }\n });\n\n return phrase;\n};\n\n/**\n * Inserts spaces between words in a line in order to raise the line width to the box width.\n * The spaces are evenly spread in the line, and extra spaces (if any) are only inserted\n * between words, not at either end of the `line`.\n *\n * @returns New array containing original words from the `line` with additional whitespace\n * for justification to `boxWidth`.\n */\nexport const justifyLine = ({\n line,\n spaceWidth,\n spaceChar,\n boxWidth,\n}: {\n /** Assumed to have already been trimmed on both ends. */\n line: Word[];\n /** Width (px) of `spaceChar`. */\n spaceWidth: number;\n /**\n * Character used as a whitespace in justification. Will be injected in between Words in\n * `line` in order to justify the text on the line within `lineWidth`.\n */\n spaceChar: string;\n /** Width (px) of the box containing the text (i.e. max `line` width). */\n boxWidth: number;\n}) => {\n const words = _extractWords(line);\n if (words.length <= 1) {\n return line.concat();\n }\n\n const wordsWidth = words.reduce(\n (width, word) => width + (word.metrics?.width ?? 0),\n 0\n );\n const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;\n\n if (words.length > 2) {\n // use CEILING so we spread the partial spaces throughout except between the second-last\n // and last word so that the spacing is more even and as tight as we can get it to\n // the `boxWidth`\n const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));\n const spaces: Word[] = Array.from({ length: spacesPerWord }, () => ({\n text: spaceChar,\n }));\n const firstWords = words.slice(0, words.length - 1); // all but last word\n const firstPart = _joinWords(firstWords, spaces);\n const remainingSpaces = spaces.slice(\n 0,\n Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces.length\n );\n const lastWord = words[words.length - 1];\n return [...firstPart, ...remainingSpaces, lastWord];\n }\n // only 2 words so fill with spaces in between them: use FLOOR to make sure we don't\n // go past `boxWidth`\n const spaces: Word[] = Array.from(\n { length: Math.floor(noOfSpacesToInsert) },\n () => ({ text: spaceChar })\n );\n return _joinWords(words, spaces);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * Trims whitespace from the beginning and end of a `line`.\n * @param line\n * @param side Which side to trim.\n * @returns An object containing trimmed characters, and the new trimmed line.\n */\nexport const trimLine = (\n line: Word[],\n side: 'left' | 'right' | 'both' = 'both'\n): {\n /**\n * New array containing what was trimmed from the left (empty if none).\n */\n trimmedLeft: Word[];\n /**\n * New array containing what was trimmed from the right (empty if none).\n */\n trimmedRight: Word[];\n /**\n * New array representing the trimmed line, even if nothing gets trimmed. Empty array if\n * all whitespace.\n */\n trimmedLine: Word[];\n} => {\n let leftTrim = 0;\n if (side === 'left' || side === 'both') {\n for (; leftTrim < line.length; leftTrim++) {\n if (!isWhitespace(line[leftTrim].text)) {\n break;\n }\n }\n\n if (leftTrim >= line.length) {\n // all whitespace\n return {\n trimmedLeft: line.concat(),\n trimmedRight: [],\n trimmedLine: [],\n };\n }\n }\n\n let rightTrim = line.length;\n if (side === 'right' || side === 'both') {\n rightTrim--;\n for (; rightTrim >= 0; rightTrim--) {\n if (!isWhitespace(line[rightTrim].text)) {\n break;\n }\n }\n rightTrim++; // back up one since we started one down for 0-based indexes\n\n if (rightTrim <= 0) {\n // all whitespace\n return {\n trimmedLeft: [],\n trimmedRight: line.concat(),\n trimmedLine: [],\n };\n }\n }\n\n return {\n trimmedLeft: line.slice(0, leftTrim),\n trimmedRight: line.slice(rightTrim),\n trimmedLine: line.slice(leftTrim, rightTrim),\n };\n};\n","import { getTextFormat, getTextStyle } from './style';\nimport { isWhitespace } from './whitespace';\nimport { justifyLine } from './justify';\nimport {\n PositionedWord,\n SplitTextProps,\n SplitWordsProps,\n RenderSpec,\n Word,\n WordMap,\n CanvasTextMetrics,\n TextFormat,\n CanvasRenderContext,\n} from '../model';\nimport { trimLine } from './trim';\n\n// Hair space character for precise justification\nconst HAIR = '\\u{200a}';\n\n// for when we're inferring whitespace between words\nconst SPACE = ' ';\n\n/**\n * Whether the canvas API being used supports the newer `fontBoundingBox*` properties or not.\n *\n * True if it does, false if not; undefined until we determine either way.\n *\n * Note about `fontBoundingBoxAscent/Descent`: Only later browsers support this and the Node-based\n * `canvas` package does not. Having these properties will have a noticeable increase in performance\n * on large pieces of text to render. Failing these, a fallback is used which involves\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics\n * @see https://www.npmjs.com/package/canvas\n */\nlet fontBoundingBoxSupported: boolean;\n\n/**\n * @private\n * Generates a word hash for use as a key in a `WordMap`.\n * @param word\n * @returns Hash.\n */\nconst _getWordHash = (word: Word) => {\n return `${word.text}${word.format ? JSON.stringify(word.format) : ''}`;\n};\n\n/**\n * @private\n * Splits words into lines based on words that are single newline characters.\n * @param words\n * @param inferWhitespace True (default) if whitespace should be inferred (and injected)\n * based on words; false if we're to assume the words already include all necessary whitespace.\n * @returns Words expressed as lines.\n */\nconst _splitIntoLines = (\n words: Word[],\n inferWhitespace: boolean = true\n): Word[][] => {\n const lines: Word[][] = [[]];\n\n let wasWhitespace = false; // true if previous word was whitespace\n words.forEach((word, wordIdx) => {\n // TODO: this is likely a naive split (at least based on character?); should at least\n // think about this more; text format shouldn't matter on a line break, right (hope not)?\n if (word.text.match(/^\\n+$/)) {\n for (let i = 0; i < word.text.length; i++) {\n lines.push([]);\n }\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (isWhitespace(word.text)) {\n // whitespace OTHER THAN newlines since we checked for newlines above\n lines.at(-1)?.push(word);\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (word.text === '') {\n return; // skip to next `word`\n }\n\n // looks like a non-empty, non-whitespace word at this point, so if it isn't the first\n // word and the one before wasn't whitespace, insert a space\n if (inferWhitespace && !wasWhitespace && wordIdx > 0) {\n lines.at(-1)?.push({ text: SPACE });\n }\n\n lines.at(-1)?.push(word);\n wasWhitespace = false;\n });\n\n return lines;\n};\n\n/**\n * @private\n * Helper for `splitWords()` that takes the words that have been wrapped into lines and\n * determines their positions on canvas for future rendering based on alignment settings.\n * @param params\n * @returns Results to return via `splitWords()`\n */\nconst _generateSpec = ({\n wrappedLines,\n wordMap,\n positioning: {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n align,\n vAlign,\n },\n}: {\n /** Words organized/wrapped into lines to be rendered. */\n wrappedLines: Word[][];\n\n /** Map of Word to measured dimensions (px) as it would be rendered. */\n wordMap: WordMap;\n\n /**\n * Details on where to render the Words onto canvas. These parameters ultimately come\n * from `SplitWordsProps`, and they come from `DrawTextConfig`.\n */\n positioning: {\n width: SplitWordsProps['width'];\n // NOTE: height does NOT constrain the text; used only for vertical alignment\n height: SplitWordsProps['height'];\n x: SplitWordsProps['x'];\n y: SplitWordsProps['y'];\n align?: SplitWordsProps['align'];\n vAlign?: SplitWordsProps['vAlign'];\n };\n}): RenderSpec => {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n // NOTE: using __font__ ascent/descent to account for all possible characters in the font\n // so that lines with ascenders but no descenders, or vice versa, are all properly\n // aligned to the baseline, and so that lines aren't scrunched\n // NOTE: even for middle vertical alignment, we want to use the __font__ ascent/descent\n // so that words, per line, are still aligned to the baseline (as much as possible; if\n // each word has a different font size, then things will still be offset, but for the\n // same font size, the baseline should match from left to right)\n const getHeight = (word: Word): number =>\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n word.metrics!.fontBoundingBoxAscent + word.metrics!.fontBoundingBoxDescent;\n\n // max height per line\n const lineHeights = wrappedLines.map((line) =>\n line.reduce((acc, word) => {\n return Math.max(acc, getHeight(word));\n }, 0)\n );\n const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);\n\n // vertical alignment (defaults to middle)\n let lineY: number;\n let textBaseline: CanvasTextBaseline;\n if (vAlign === 'top') {\n textBaseline = 'top';\n lineY = boxY;\n } else if (vAlign === 'bottom') {\n textBaseline = 'bottom';\n lineY = yEnd - totalHeight;\n } else {\n // middle\n textBaseline = 'top'; // YES, using 'top' baseline for 'middle' v-align\n lineY = boxY + boxHeight / 2 - totalHeight / 2;\n }\n\n const lines = wrappedLines.map((line, lineIdx): PositionedWord[] => {\n const lineWidth = line.reduce(\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n (acc, word) => acc + word.metrics!.width,\n 0\n );\n const lineHeight = lineHeights[lineIdx];\n\n // horizontal alignment (defaults to center)\n let lineX: number;\n if (align === 'right') {\n lineX = xEnd - lineWidth;\n } else if (align === 'left') {\n lineX = boxX;\n } else {\n // center\n lineX = boxX + boxWidth / 2 - lineWidth / 2;\n }\n\n let wordX = lineX;\n const posWords = line.map((word): PositionedWord => {\n // NOTE: `word.metrics` and `wordMap.get(hash)` must exist as every `word` MUST have\n // been measured at this point\n\n const hash = _getWordHash(word);\n const { format } = wordMap.get(hash)!;\n const x = wordX;\n const height = getHeight(word);\n\n // vertical alignment (defaults to middle)\n let y: number;\n if (vAlign === 'top') {\n y = lineY;\n } else if (vAlign === 'bottom') {\n y = lineY + lineHeight;\n } else {\n // middle\n y = lineY + (lineHeight - height) / 2;\n }\n\n wordX += word.metrics!.width;\n return {\n word,\n format, // undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)\n x,\n y,\n width: word.metrics!.width,\n height,\n isWhitespace: isWhitespace(word.text),\n };\n });\n\n lineY += lineHeight;\n return posWords;\n });\n\n return {\n lines,\n textBaseline,\n textAlign: 'left', // always per current algorithm\n width: boxWidth,\n height: totalHeight,\n };\n};\n\n/**\n * @private\n * Replacer for use with `JSON.stringify()` to deal with `TextMetrics` objects which\n * only have getters/setters instead of value-based properties.\n * @param key Key being processed in `this`.\n * @param value Value of `key` in `this`.\n * @returns Processed value to be serialized, or `undefined` to omit the `key` from the\n * serialized object.\n */\n// CAREFUL: use a `function`, not an arrow function, as stringify() sets its context to\n// the object being serialized on each call to the replacer\nconst _jsonReplacer = function (key: string, value: unknown) {\n if (key === 'metrics' && value && typeof value === 'object') {\n // TODO: need better typings here, if possible, so that TSC warns if we aren't\n // including a property we should be if a new one is needed in the future (i.e. if\n // a new property is added to the `TextMetricsLike` type)\n // NOTE: TextMetrics objects don't have own-enumerable properties; they only have getters,\n // so we have to explicitly get the values we care about instead of spreading them into\n // the new object\n const metrics: CanvasTextMetrics = value as CanvasTextMetrics;\n return {\n width: metrics.width,\n fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,\n fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,\n };\n }\n\n return value;\n};\n\n/**\n * Serializes render specs to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param specs\n * @returns Specs serialized as JSON.\n */\nexport const specToJson = (specs: RenderSpec): string => {\n return JSON.stringify(specs, _jsonReplacer);\n};\n\n/**\n * Serializes a list of Words to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param words\n * @returns Words serialized as JSON.\n */\nexport const wordsToJson = (words: Word[]): string => {\n return JSON.stringify(words, _jsonReplacer);\n};\n\n/**\n * @private\n * Measures a Word in a rendering context, assigning its `TextMetrics` to its `metrics` property.\n * @returns The Word's width, in pixels.\n */\nconst _measureWord = ({\n ctx,\n word,\n wordMap,\n baseTextFormat,\n}: {\n ctx: CanvasRenderContext;\n word: Word;\n wordMap: WordMap;\n baseTextFormat: TextFormat;\n}): number => {\n const hash = _getWordHash(word);\n\n if (word.metrics) {\n // assume Word's text and format haven't changed since last measurement and metrics are good\n\n // make sure we have the metrics and full formatting cached for other identical Words\n if (!wordMap.has(hash)) {\n let format = undefined;\n if (word.format) {\n format = getTextFormat(word.format, baseTextFormat);\n }\n wordMap.set(hash, { metrics: word.metrics, format });\n }\n\n return word.metrics.width;\n }\n\n // check to see if we have already measured an identical Word\n if (wordMap.has(hash)) {\n const { metrics } = wordMap.get(hash)!; // will be there because of `if(has())` check\n word.metrics = metrics;\n return metrics.width;\n }\n\n let ctxSaved = false;\n\n let format = undefined;\n if (word.format) {\n ctx.save();\n ctxSaved = true;\n format = getTextFormat(word.format, baseTextFormat);\n ctx.font = getTextStyle(format); // `fontColor` is ignored as it has no effect on metrics\n }\n\n if (!fontBoundingBoxSupported) {\n // use fallback which comes close enough and still gives us properly-aligned text, albeit\n // lines are a couple pixels tighter together\n if (!ctxSaved) {\n ctx.save();\n ctxSaved = true;\n }\n ctx.textBaseline = 'bottom';\n }\n\n const metrics = ctx.measureText(word.text);\n if (typeof metrics.fontBoundingBoxAscent === 'number') {\n fontBoundingBoxSupported = true;\n } else {\n fontBoundingBoxSupported = false;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxDescent = 0;\n }\n\n word.metrics = metrics;\n wordMap.set(hash, { metrics, format });\n\n if (ctxSaved) {\n ctx.restore();\n }\n\n return metrics.width;\n};\n\n/**\n * Splits Words into positioned lines of Words as they need to be rendred in 2D space,\n * but does not render anything.\n * @param config\n * @returns Lines of positioned words to be rendered, and total height required to\n * render all lines.\n */\nexport const splitWords = ({\n ctx,\n words,\n justify,\n format: baseFormat,\n inferWhitespace = true,\n ...positioning // rest of params are related to positioning\n}: SplitWordsProps): RenderSpec => {\n const wordMap: WordMap = new Map();\n const baseTextFormat = getTextFormat(baseFormat);\n const { width: boxWidth } = positioning;\n\n //// text measurement\n\n // measures an entire line's width up to the `boxWidth` as a max, unless `force=true`,\n // in which case the entire line is measured regardless of `boxWidth`.\n //\n // - Returned `lineWidth` is width up to, but not including, the `splitPoint` (always <= `boxWidth`\n // unless the first Word is too wide to fit, in which case `lineWidth` will be that Word's\n // width even though it's > `boxWidth`).\n // - If `force=true`, will be the full width of the line regardless of `boxWidth`.\n // - Returned `splitPoint` is index into `words` of the Word immediately FOLLOWING the last\n // Word included in the `lineWidth` (and is `words.length` if all Words were included);\n // `splitPoint` could also be thought of as the number of `words` included in the `lineWidth`.\n // - If `force=true`, will always be `words.length`.\n const measureLine = (\n lineWords: Word[],\n force: boolean = false\n ): {\n lineWidth: number;\n splitPoint: number;\n } => {\n let lineWidth = 0;\n let splitPoint = 0;\n lineWords.every((word, idx) => {\n const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });\n if (!force && lineWidth + wordWidth > boxWidth) {\n // at minimum, MUST include at least first Word, even if it's wider than box width\n if (idx === 0) {\n splitPoint = 1;\n lineWidth = wordWidth;\n }\n // else, `lineWidth` already includes at least one Word so this current Word will\n // be the `splitPoint` such that `lineWidth` remains < `boxWidth`\n\n return false; // break\n }\n\n splitPoint++;\n lineWidth += wordWidth;\n return true; // next\n });\n\n return { lineWidth, splitPoint };\n };\n\n //// main\n\n ctx.save();\n\n // start by trimming the `words` to remove any whitespace at either end, then split the `words`\n // into an initial set of lines dictated by explicit hard breaks, if any (if none, we'll have\n // one super long line)\n const hardLines = _splitIntoLines(\n trimLine(words).trimmedLine,\n inferWhitespace\n );\n\n if (\n hardLines.length <= 0 ||\n boxWidth <= 0 ||\n positioning.height <= 0 ||\n (baseFormat &&\n typeof baseFormat.fontSize === 'number' &&\n baseFormat.fontSize <= 0)\n ) {\n // width or height or font size cannot be 0, or there are no lines after trimming\n return {\n lines: [],\n textAlign: 'center',\n textBaseline: 'middle',\n width: positioning.width,\n height: 0,\n };\n }\n\n ctx.font = getTextStyle(baseTextFormat);\n\n const hairWidth = justify\n ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat })\n : 0;\n const wrappedLines: Word[][] = [];\n\n // now further wrap every hard line to make sure it fits within the `boxWidth`, down to a\n // MINIMUM of 1 Word per line\n for (const hardLine of hardLines) {\n let { splitPoint } = measureLine(hardLine);\n\n // if the line fits, we're done; else, we have to break it down further to fit\n // as best as we can (i.e. MIN one word per line, no breaks within words, no\n // leading/pending whitespace)\n if (splitPoint >= hardLine.length) {\n wrappedLines.push(hardLine);\n } else {\n // shallow clone because we're going to break this line down further to get the best fit\n let softLine = hardLine.concat();\n while (splitPoint < softLine.length) {\n // right-trim what we split off in case we split just after some whitespace\n const splitLine = trimLine(\n softLine.slice(0, splitPoint),\n 'right'\n ).trimmedLine;\n wrappedLines.push(splitLine);\n\n // left-trim what remains in case we split just before some whitespace\n softLine = trimLine(softLine.slice(splitPoint), 'left').trimmedLine;\n ({ splitPoint } = measureLine(softLine));\n }\n\n // get the last bit of the `softLine`\n // NOTE: since we started by timming the entire line, and we just left-trimmed\n // what remained of `softLine`, there should be no need to trim again\n wrappedLines.push(softLine);\n }\n }\n\n // never justify a single line because there's no other line to visually justify it to\n if (justify && wrappedLines.length > 1) {\n wrappedLines.forEach((wrappedLine, idx) => {\n // never justify the last line (common in text editors)\n if (idx < wrappedLines.length - 1) {\n const justifiedLine = justifyLine({\n line: wrappedLine,\n spaceWidth: hairWidth,\n spaceChar: HAIR,\n boxWidth,\n });\n\n // make sure any new Words used for justification get measured so we're able to\n // position them later when we generate the render spec\n measureLine(justifiedLine, true);\n wrappedLines[idx] = justifiedLine;\n }\n });\n }\n\n const spec = _generateSpec({\n wrappedLines,\n wordMap,\n positioning,\n });\n\n ctx.restore();\n return spec;\n};\n\n/**\n * Converts a string of text containing words and whitespace, as well as line breaks (newlines),\n * into a `Word[]` that can be given to `splitWords()`.\n * @param text String to convert into Words.\n * @returns Converted text.\n */\nexport const textToWords = (text: string) => {\n const words: Word[] = [];\n\n // split the `text` into a series of Words, preserving whitespace\n let word: Word | undefined = undefined;\n let wasWhitespace = false;\n Array.from(text.trim()).forEach((c) => {\n const charIsWhitespace = isWhitespace(c);\n if (\n (charIsWhitespace && !wasWhitespace) ||\n (!charIsWhitespace && wasWhitespace)\n ) {\n // save current `word`, if any, and start new `word`\n wasWhitespace = charIsWhitespace;\n if (word) {\n words.push(word);\n }\n word = { text: c };\n } else {\n // accumulate into current `word`\n if (!word) {\n word = { text: '' };\n }\n word.text += c;\n }\n });\n\n // make sure we have the last word! ;)\n if (word) {\n words.push(word);\n }\n\n return words;\n};\n\n/**\n * Splits plain text into lines in the order in which they should be rendered, top-down,\n * preserving whitespace __only within the text__ (whitespace on either end is trimmed).\n */\nexport const splitText = ({ text, ...params }: SplitTextProps): string[] => {\n const words = textToWords(text);\n\n const results = splitWords({\n ...params,\n words,\n inferWhitespace: false,\n });\n\n return results.lines.map((line) =>\n line.map(({ word: { text: t } }) => t).join('')\n );\n};\n","import { getTextStyle } from './style';\nimport { CanvasRenderContext, Word } from '../model';\n\n/** @private */\nconst _getHeight = (ctx: CanvasRenderContext, text: string, style?: string) => {\n const previousTextBaseline = ctx.textBaseline;\n const previousFont = ctx.font;\n\n ctx.textBaseline = 'bottom';\n if (style) {\n ctx.font = style;\n }\n const { actualBoundingBoxAscent: height } = ctx.measureText(text);\n\n // Reset baseline\n ctx.textBaseline = previousTextBaseline;\n if (style) {\n ctx.font = previousFont;\n }\n\n return height;\n};\n\n/**\n * Gets the measured height of a given `Word` using its text style.\n * @returns {number} Height in pixels.\n */\nexport const getWordHeight = ({\n ctx,\n word,\n}: {\n ctx: CanvasRenderContext;\n /**\n * Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.\n */\n word: Word;\n}) => {\n return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));\n};\n\n/**\n * Gets the measured height of a given `string` using a given text style.\n * @returns {number} Height in pixels.\n */\nexport const getTextHeight = ({\n ctx,\n text,\n style,\n}: {\n ctx: CanvasRenderContext;\n text: string;\n /**\n * CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font\n * settings/styles are used.\n */\n style?: string;\n}) => {\n return _getHeight(ctx, text, style);\n};\n","import {\n specToJson,\n splitWords,\n splitText,\n textToWords,\n wordsToJson,\n} from './util/split';\nimport { getTextHeight, getWordHeight } from './util/height';\nimport { getTextStyle, getTextFormat, DEFAULT_FONT_COLOR } from './util/style';\nimport { CanvasRenderContext, DrawTextConfig, Text } from './model';\n\nconst drawText = (\n ctx: CanvasRenderContext,\n text: Text,\n config: DrawTextConfig\n) => {\n const baseFormat = getTextFormat({\n fontFamily: config.fontFamily,\n fontSize: config.fontSize,\n fontStyle: config.fontStyle,\n fontVariant: config.fontVariant,\n fontWeight: config.fontWeight,\n fontColor: config.fontColor,\n });\n\n const {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n } = config;\n\n const {\n lines: richLines,\n height: totalHeight,\n textBaseline,\n textAlign,\n } = splitWords({\n ctx,\n words: Array.isArray(text) ? text : textToWords(text),\n inferWhitespace: Array.isArray(text)\n ? config.inferWhitespace === undefined || config.inferWhitespace\n : undefined, // ignore since `text` is a string; we assume it already has all the whitespace it needs\n x: boxX,\n y: boxY,\n width: config.width,\n height: config.height,\n align: config.align,\n vAlign: config.vAlign,\n justify: config.justify,\n format: baseFormat,\n });\n\n ctx.save();\n ctx.textAlign = textAlign;\n ctx.textBaseline = textBaseline;\n ctx.font = getTextStyle(baseFormat);\n ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;\n\n if (config.overflow === false) {\n ctx.beginPath();\n ctx.rect(boxX, boxY, boxWidth, boxHeight);\n ctx.clip(); // part of saved context state\n }\n\n richLines.forEach((line) => {\n line.forEach((pw) => {\n if (!pw.isWhitespace) {\n // NOTE: don't use the `pw.word.format` as this could be incomplete; use `pw.format`\n // if it exists as this will always be the __full__ TextFormat used to measure the\n // Word, and so should be what is used to render it\n if (pw.format) {\n ctx.save();\n ctx.font = getTextStyle(pw.format);\n if (pw.format.fontColor) {\n ctx.fillStyle = pw.format.fontColor;\n }\n }\n ctx.fillText(pw.word.text, pw.x, pw.y);\n if (pw.format) {\n ctx.restore();\n }\n }\n });\n });\n\n if (config.debug) {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n let textAnchor: number;\n if (config.align === 'right') {\n textAnchor = xEnd;\n } else if (config.align === 'left') {\n textAnchor = boxX;\n } else {\n textAnchor = boxX + boxWidth / 2;\n }\n\n let debugY = boxY;\n if (config.vAlign === 'bottom') {\n debugY = yEnd;\n } else if (config.vAlign === 'middle') {\n debugY = boxY + boxHeight / 2;\n }\n\n const debugColor = '#0C8CE9';\n\n // Text box\n ctx.lineWidth = 1;\n ctx.strokeStyle = debugColor;\n ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);\n\n ctx.lineWidth = 1;\n\n if (!config.align || config.align === 'center') {\n // Horizontal Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(textAnchor, boxY);\n ctx.lineTo(textAnchor, yEnd);\n ctx.stroke();\n }\n\n if (!config.vAlign || config.vAlign === 'middle') {\n // Vertical Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(boxX, debugY);\n ctx.lineTo(xEnd, debugY);\n ctx.stroke();\n }\n }\n\n ctx.restore();\n\n return { height: totalHeight };\n};\n\nexport {\n drawText,\n specToJson,\n splitText,\n splitWords,\n textToWords,\n wordsToJson,\n getTextHeight,\n getWordHeight,\n getTextStyle,\n getTextFormat,\n};\nexport * from './model';\n"],"names":["DEFAULT_FONT_FAMILY","DEFAULT_FONT_COLOR","getTextFormat","format","baseFormat","getTextStyle","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","isWhitespace","text","_extractWords","line","word","_cloneWord","clone","_joinWords","words","joiner","phrase","wordIdx","jw","justifyLine","spaceWidth","spaceChar","boxWidth","wordsWidth","width","_a","noOfSpacesToInsert","spacesPerWord","spaces","firstWords","firstPart","remainingSpaces","lastWord","trimLine","side","leftTrim","rightTrim","HAIR","SPACE","fontBoundingBoxSupported","_getWordHash","_splitIntoLines","inferWhitespace","lines","wasWhitespace","_b","_c","i","_generateSpec","wrappedLines","wordMap","boxHeight","boxX","boxY","align","vAlign","xEnd","yEnd","getHeight","lineHeights","acc","totalHeight","h","lineY","textBaseline","lineIdx","lineWidth","lineHeight","lineX","wordX","posWords","hash","x","height","y","_jsonReplacer","key","value","metrics","specToJson","specs","wordsToJson","_measureWord","ctx","baseTextFormat","ctxSaved","splitWords","justify","positioning","measureLine","lineWords","force","splitPoint","idx","wordWidth","hardLines","hairWidth","hardLine","softLine","splitLine","wrappedLine","justifiedLine","spec","textToWords","c","charIsWhitespace","splitText","params","t","_getHeight","style","previousTextBaseline","previousFont","getWordHeight","getTextHeight","drawText","config","richLines","textAlign","pw","textAnchor","debugY","debugColor"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const j="Arial",D=14,J="black",S=(t,n)=>Object.assign({},{fontFamily:j,fontSize:D,fontWeight:"400",fontStyle:"",fontVariant:"",fontColor:J},n,t),A=({fontFamily:t,fontSize:n,fontStyle:e,fontVariant:i,fontWeight:s})=>`${e||""} ${i||""} ${s||""} ${n!=null?n:D}px ${t||j}`.trim(),L=t=>!!t.match(/^\s+$/),Y=t=>t.filter(n=>!L(n.text)),z=t=>{const n={...t};return t.format&&(n.format={...t.format}),n},
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const j="Arial",D=14,J="black",S=(t,n)=>Object.assign({},{fontFamily:j,fontSize:D,fontWeight:"400",fontStyle:"",fontVariant:"",fontColor:J},n,t),A=({fontFamily:t,fontSize:n,fontStyle:e,fontVariant:i,fontWeight:s})=>`${e||""} ${i||""} ${s||""} ${n!=null?n:D}px ${t||j}`.trim(),L=t=>!!t.match(/^\s+$/),Y=t=>t.filter(n=>!L(n.text)),z=t=>{const n={...t};return t.format&&(n.format={...t.format}),n},k=(t,n)=>{if(t.length<=1||n.length<1)return[...t];const e=[];return t.forEach((i,s)=>{e.push(i),s<t.length-1&&n.forEach(r=>e.push(z(r)))}),e},X=({line:t,spaceWidth:n,spaceChar:e,boxWidth:i})=>{const s=Y(t);if(s.length<=1)return t.concat();const r=s.reduce((a,u)=>{var d,p;return a+((p=(d=u.metrics)==null?void 0:d.width)!=null?p:0)},0),f=(i-r)/n;if(s.length>2){const a=Math.ceil(f/(s.length-1)),u=Array.from({length:a},()=>({text:e})),d=s.slice(0,s.length-1),p=k(d,u),c=u.slice(0,Math.floor(f)-(d.length-1)*u.length),h=s[s.length-1];return[...p,...c,h]}const o=Array.from({length:Math.floor(f)},()=>({text:e}));return k(s,o)},E=(t,n="both")=>{let e=0;if(n==="left"||n==="both"){for(;e<t.length&&L(t[e].text);e++);if(e>=t.length)return{trimmedLeft:t.concat(),trimmedRight:[],trimmedLine:[]}}let i=t.length;if(n==="right"||n==="both"){for(i--;i>=0&&L(t[i].text);i--);if(i++,i<=0)return{trimmedLeft:[],trimmedRight:t.concat(),trimmedLine:[]}}return{trimmedLeft:t.slice(0,e),trimmedRight:t.slice(i),trimmedLine:t.slice(e,i)}},P=" ",Z=" ";let _;const R=t=>`${t.text}${t.format?JSON.stringify(t.format):""}`,q=(t,n=!0)=>{const e=[[]];let i=!1;return t.forEach((s,r)=>{var f,o,a;if(s.text.match(/^\n+$/)){for(let u=0;u<s.text.length;u++)e.push([]);i=!0;return}if(L(s.text)){(f=e.at(-1))==null||f.push(s),i=!0;return}s.text!==""&&(n&&!i&&r>0&&((o=e.at(-1))==null||o.push({text:Z})),(a=e.at(-1))==null||a.push(s),i=!1)}),e},G=({wrappedLines:t,wordMap:n,positioning:{width:e,height:i,x:s=0,y:r=0,align:f,vAlign:o}})=>{const a=s+e,u=r+i,d=l=>l.metrics.fontBoundingBoxAscent+l.metrics.fontBoundingBoxDescent,p=t.map(l=>l.reduce((y,B)=>Math.max(y,d(B)),0)),c=p.reduce((l,y)=>l+y,0);let h,m;return o==="top"?(m="top",h=r):o==="bottom"?(m="bottom",h=u-c):(m="top",h=r+i/2-c/2),{lines:t.map((l,y)=>{const B=l.reduce((T,v)=>T+v.metrics.width,0),x=p[y];let W;f==="right"?W=a-B:f==="left"?W=s:W=s+e/2-B/2;let F=W;const M=l.map(T=>{const v=R(T),{format:U}=n.get(v),V=F,O=d(T);let b;return o==="top"?b=h:o==="bottom"?b=h+x:b=h+(x-O)/2,F+=T.metrics.width,{word:T,format:U,x:V,y:b,width:T.metrics.width,height:O,isWhitespace:L(T.text)}});return h+=x,M}),textBaseline:m,textAlign:"left",width:e,height:c}},I=function(t,n){if(t==="metrics"&&n&&typeof n=="object"){const e=n;return{width:e.width,fontBoundingBoxAscent:e.fontBoundingBoxAscent,fontBoundingBoxDescent:e.fontBoundingBoxDescent}}return n},K=t=>JSON.stringify(t,I),Q=t=>JSON.stringify(t,I),$=({ctx:t,word:n,wordMap:e,baseTextFormat:i})=>{const s=R(n);if(n.metrics){if(!e.has(s)){let a;n.format&&(a=S(n.format,i)),e.set(s,{metrics:n.metrics,format:a})}return n.metrics.width}if(e.has(s)){const{metrics:a}=e.get(s);return n.metrics=a,a.width}let r=!1,f;n.format&&(t.save(),r=!0,f=S(n.format,i),t.font=A(f)),_||(r||(t.save(),r=!0),t.textBaseline="bottom");const o=t.measureText(n.text);return typeof o.fontBoundingBoxAscent=="number"?_=!0:(_=!1,o.fontBoundingBoxAscent=o.actualBoundingBoxAscent,o.fontBoundingBoxDescent=0),n.metrics=o,e.set(s,{metrics:o,format:f}),r&&t.restore(),o.width},H=({ctx:t,words:n,justify:e,format:i,inferWhitespace:s=!0,...r})=>{const f=new Map,o=S(i),{width:a}=r,u=(m,g=!1)=>{let l=0,y=0;return m.every((B,x)=>{const W=$({ctx:t,word:B,wordMap:f,baseTextFormat:o});return!g&&l+W>a?(x===0&&(y=1,l=W),!1):(y++,l+=W,!0)}),{lineWidth:l,splitPoint:y}};t.save();const d=q(E(n).trimmedLine,s);if(d.length<=0||a<=0||r.height<=0||i&&typeof i.fontSize=="number"&&i.fontSize<=0)return{lines:[],textAlign:"center",textBaseline:"middle",width:r.width,height:0};t.font=A(o);const p=e?$({ctx:t,word:{text:P},wordMap:f,baseTextFormat:o}):0,c=[];for(const m of d){let{splitPoint:g}=u(m);if(g>=m.length)c.push(m);else{let l=m.concat();for(;g<l.length;){const y=E(l.slice(0,g),"right").trimmedLine;c.push(y),l=E(l.slice(g),"left").trimmedLine,{splitPoint:g}=u(l)}c.push(l)}}e&&c.length>1&&c.forEach((m,g)=>{if(g<c.length-1){const l=X({line:m,spaceWidth:p,spaceChar:P,boxWidth:a});u(l,!0),c[g]=l}});const h=G({wrappedLines:c,wordMap:f,positioning:r});return t.restore(),h},C=t=>{const n=[];let e,i=!1;return Array.from(t.trim()).forEach(s=>{const r=L(s);r&&!i||!r&&i?(i=r,e&&n.push(e),e={text:s}):(e||(e={text:""}),e.text+=s)}),e&&n.push(e),n},w=({text:t,...n})=>{const e=C(t);return H({...n,words:e,inferWhitespace:!1}).lines.map(s=>s.map(({word:{text:r}})=>r).join(""))},N=(t,n,e)=>{const i=t.textBaseline,s=t.font;t.textBaseline="bottom",e&&(t.font=e);const{actualBoundingBoxAscent:r}=t.measureText(n);return t.textBaseline=i,e&&(t.font=s),r},tt=({ctx:t,word:n})=>N(t,n.text,n.format&&A(n.format)),et=({ctx:t,text:n,style:e})=>N(t,n,e),nt=(t,n,e)=>{const i=S({fontFamily:e.fontFamily,fontSize:e.fontSize,fontStyle:e.fontStyle,fontVariant:e.fontVariant,fontWeight:e.fontWeight,fontColor:e.fontColor}),{width:s,height:r,x:f=0,y:o=0}=e,{lines:a,height:u,textBaseline:d,textAlign:p}=H({ctx:t,words:Array.isArray(n)?n:C(n),inferWhitespace:Array.isArray(n)?e.inferWhitespace===void 0||e.inferWhitespace:void 0,x:f,y:o,width:e.width,height:e.height,align:e.align,vAlign:e.vAlign,justify:e.justify,format:i});if(t.save(),t.textAlign=p,t.textBaseline=d,t.font=A(i),t.fillStyle=i.fontColor||J,e.overflow===!1&&(t.beginPath(),t.rect(f,o,s,r),t.clip()),a.forEach(c=>{c.forEach(h=>{h.isWhitespace||(h.format&&(t.save(),t.font=A(h.format),h.format.fontColor&&(t.fillStyle=h.format.fontColor)),t.fillText(h.word.text,h.x,h.y),h.format&&t.restore())})}),e.debug){const c=f+s,h=o+r;let m;e.align==="right"?m=c:e.align==="left"?m=f:m=f+s/2;let g=o;e.vAlign==="bottom"?g=h:e.vAlign==="middle"&&(g=o+r/2);const l="#0C8CE9";t.lineWidth=1,t.strokeStyle=l,t.strokeRect(f,o,s,r),t.lineWidth=1,(!e.align||e.align==="center")&&(t.strokeStyle=l,t.beginPath(),t.moveTo(m,o),t.lineTo(m,h),t.stroke()),(!e.vAlign||e.vAlign==="middle")&&(t.strokeStyle=l,t.beginPath(),t.moveTo(f,g),t.lineTo(c,g),t.stroke())}return t.restore(),{height:u}};exports.drawText=nt;exports.getTextFormat=S;exports.getTextHeight=et;exports.getTextStyle=A;exports.getWordHeight=tt;exports.specToJson=K;exports.splitText=w;exports.splitWords=H;exports.textToWords=C;exports.wordsToJson=Q;
|
|
2
2
|
//# sourceMappingURL=text-to-canvas.min.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-to-canvas.min.js","sources":["../src/lib/util/style.ts","../src/lib/util/whitespace.ts","../src/lib/util/justify.ts","../src/lib/util/trim.ts","../src/lib/util/split.ts","../src/lib/util/height.ts","../src/lib/index.ts"],"sourcesContent":["import { TextFormat } from '../model';\n\nexport const DEFAULT_FONT_FAMILY = 'Arial';\nexport const DEFAULT_FONT_SIZE = 14;\nexport const DEFAULT_FONT_COLOR = 'black';\n\n/**\n * Generates a text format based on defaults and any provided overrides.\n * @param format Overrides to `baseFormat` and default format.\n * @param baseFormat Overrides to default format.\n * @returns Full text format (all properties specified).\n */\nexport const getTextFormat = (\n format?: TextFormat,\n baseFormat?: TextFormat\n): Required<TextFormat> => {\n return Object.assign(\n {},\n {\n fontFamily: DEFAULT_FONT_FAMILY,\n fontSize: DEFAULT_FONT_SIZE,\n fontWeight: '400',\n fontStyle: '',\n fontVariant: '',\n fontColor: DEFAULT_FONT_COLOR,\n },\n baseFormat,\n format\n );\n};\n\n/**\n * Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.\n * @param format\n * @returns Style string to set on context's `font` property. Note this __does not include\n * the font color__ as that is not part of the CSS font value. Color must be handled separately.\n */\nexport const getTextStyle = ({\n fontFamily,\n fontSize,\n fontStyle,\n fontVariant,\n fontWeight,\n}: TextFormat) => {\n // per spec:\n // - font-style, font-variant and font-weight must precede font-size\n // - font-family must be the last value specified\n // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font\n return `${fontStyle || ''} ${fontVariant || ''} ${\n fontWeight || ''\n } ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();\n};\n","/**\n * Determines if a string is only whitespace (one or more characters of it).\n * @param text\n * @returns True if `text` is one or more characters of whitespace, only.\n */\nexport const isWhitespace = (text: string) => {\n return !!text.match(/^\\s+$/);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * @private\n * Extracts the __visible__ (i.e. non-whitespace) words from a line.\n * @param line\n * @returns New array with only non-whitespace words.\n */\nconst _extractWords = (line: Word[]) => {\n return line.filter((word) => !isWhitespace(word.text));\n};\n\n/**\n * @private\n * Deep-clones a Word.\n * @param word\n * @returns Deep-cloned Word.\n */\nconst _cloneWord = (word: Word) => {\n const clone = { ...word };\n if (word.format) {\n clone.format = { ...word.format };\n }\n return clone;\n};\n\n/**\n * @private\n * Joins Words together using another set of Words.\n * @param words Words to join.\n * @param joiner Words to use when joining `words`. These will be deep-cloned and inserted\n * in between every word in `words`, similar to `Array.join(string)` where the `string`\n * is inserted in between every element.\n * @returns New array of Words. Empty if `words` is empty. New array of one Word if `words`\n * contains only one Word.\n */\nconst _joinWords = (words: Word[], joiner: Word[]) => {\n if (words.length <= 1 || joiner.length < 1) {\n return [...words];\n }\n\n const phrase: Word[] = [];\n words.forEach((word, wordIdx) => {\n phrase.push(word);\n if (wordIdx < words.length - 1) {\n // don't append after last `word`\n joiner.forEach((jw) => phrase.push(_cloneWord(jw)));\n }\n });\n\n return phrase;\n};\n\n/**\n * Inserts spaces between words in a line in order to raise the line width to the box width.\n * The spaces are evenly spread in the line, and extra spaces (if any) are only inserted\n * between words, not at either end of the `line`.\n *\n * @returns New array containing original words from the `line` with additional whitespace\n * for justification to `boxWidth`.\n */\nexport const justifyLine = ({\n line,\n spaceWidth,\n spaceChar,\n boxWidth,\n}: {\n /** Assumed to have already been trimmed on both ends. */\n line: Word[];\n /** Width (px) of `spaceChar`. */\n spaceWidth: number;\n /**\n * Character used as a whitespace in justification. Will be injected in between Words in\n * `line` in order to justify the text on the line within `lineWidth`.\n */\n spaceChar: string;\n /** Width (px) of the box containing the text (i.e. max `line` width). */\n boxWidth: number;\n}) => {\n const words = _extractWords(line);\n if (words.length <= 1) {\n return line.concat();\n }\n\n const wordsWidth = words.reduce(\n (width, word) => width + (word.metrics?.width ?? 0),\n 0\n );\n const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;\n\n if (words.length > 2) {\n // use CEILING so we spread the partial spaces throughout except between the second-last\n // and last word so that the spacing is more even and as tight as we can get it to\n // the `boxWidth`\n const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));\n const spaces: Word[] = Array.from({ length: spacesPerWord }, () => ({\n text: spaceChar,\n }));\n const firstWords = words.slice(0, words.length - 1); // all but last word\n const firstPart = _joinWords(firstWords, spaces);\n const remainingSpaces = spaces.slice(\n 0,\n Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces.length\n );\n const lastWord = words[words.length - 1];\n return [...firstPart, ...remainingSpaces, lastWord];\n }\n // only 2 words so fill with spaces in between them: use FLOOR to make sure we don't\n // go past `boxWidth`\n const spaces: Word[] = Array.from(\n { length: Math.floor(noOfSpacesToInsert) },\n () => ({ text: spaceChar })\n );\n return _joinWords(words, spaces);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * Trims whitespace from the beginning and end of a `line`.\n * @param line\n * @param side Which side to trim.\n * @returns An object containing trimmed characters, and the new trimmed line.\n */\nexport const trimLine = (\n line: Word[],\n side: 'left' | 'right' | 'both' = 'both'\n): {\n /**\n * New array containing what was trimmed from the left (empty if none).\n */\n trimmedLeft: Word[];\n /**\n * New array containing what was trimmed from the right (empty if none).\n */\n trimmedRight: Word[];\n /**\n * New array representing the trimmed line, even if nothing gets trimmed. Empty array if\n * all whitespace.\n */\n trimmedLine: Word[];\n} => {\n let leftTrim = 0;\n if (side === 'left' || side === 'both') {\n for (; leftTrim < line.length; leftTrim++) {\n if (!isWhitespace(line[leftTrim].text)) {\n break;\n }\n }\n\n if (leftTrim >= line.length) {\n // all whitespace\n return {\n trimmedLeft: line.concat(),\n trimmedRight: [],\n trimmedLine: [],\n };\n }\n }\n\n let rightTrim = line.length;\n if (side === 'right' || side === 'both') {\n rightTrim--;\n for (; rightTrim >= 0; rightTrim--) {\n if (!isWhitespace(line[rightTrim].text)) {\n break;\n }\n }\n rightTrim++; // back up one since we started one down for 0-based indexes\n\n if (rightTrim <= 0) {\n // all whitespace\n return {\n trimmedLeft: [],\n trimmedRight: line.concat(),\n trimmedLine: [],\n };\n }\n }\n\n return {\n trimmedLeft: line.slice(0, leftTrim),\n trimmedRight: line.slice(rightTrim),\n trimmedLine: line.slice(leftTrim, rightTrim),\n };\n};\n","import { getTextFormat, getTextStyle } from './style';\nimport { isWhitespace } from './whitespace';\nimport { justifyLine } from './justify';\nimport {\n PositionedWord,\n SplitTextProps,\n SplitWordsProps,\n RenderSpec,\n Word,\n WordMap,\n CanvasTextMetrics,\n TextFormat,\n CanvasRenderContext,\n} from '../model';\nimport { trimLine } from './trim';\n\n// Hair space character for precise justification\nconst HAIR = '\\u{200a}';\n\n// for when we're inferring whitespace between words\nconst SPACE = ' ';\n\n/**\n * Whether the canvas API being used supports the newer `fontBoundingBox*` properties or not.\n *\n * True if it does, false if not; undefined until we determine either way.\n *\n * Note about `fontBoundingBoxAscent/Descent`: Only later browsers support this and the Node-based\n * `canvas` package does not. Having these properties will have a noticeable increase in performance\n * on large pieces of text to render. Failing these, a fallback is used which involves\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics\n * @see https://www.npmjs.com/package/canvas\n */\nlet fontBoundingBoxSupported: boolean;\n\n/**\n * @private\n * Generates a word hash for use as a key in a `WordMap`.\n * @param word\n * @returns Hash.\n */\nconst _getWordHash = (word: Word) => {\n return `${word.text}${word.format ? JSON.stringify(word.format) : ''}`;\n};\n\n/**\n * @private\n * Splits words into lines based on words that are single newline characters.\n * @param words\n * @param inferWhitespace True (default) if whitespace should be inferred (and injected)\n * based on words; false if we're to assume the words already include all necessary whitespace.\n * @returns Words expressed as lines.\n */\nconst _splitIntoLines = (\n words: Word[],\n inferWhitespace: boolean = true\n): Word[][] => {\n const lines: Word[][] = [[]];\n\n let wasWhitespace = false; // true if previous word was whitespace\n words.forEach((word, wordIdx) => {\n // TODO: this is likely a naive split (at least based on character?); should at least\n // think about this more; text format shouldn't matter on a line break, right (hope not)?\n if (word.text.match(/^\\n+$/)) {\n for (let i = 0; i < word.text.length; i++) {\n lines.push([]);\n }\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (isWhitespace(word.text)) {\n // whitespace OTHER THAN newlines since we checked for newlines above\n lines.at(-1)?.push(word);\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (word.text === '') {\n return; // skip to next `word`\n }\n\n // looks like a non-empty, non-whitespace word at this point, so if it isn't the first\n // word and the one before wasn't whitespace, insert a space\n if (inferWhitespace && !wasWhitespace && wordIdx > 0) {\n lines.at(-1)?.push({ text: SPACE });\n }\n\n lines.at(-1)?.push(word);\n wasWhitespace = false;\n });\n\n return lines;\n};\n\n/**\n * @private\n * Helper for `splitWords()` that takes the words that have been wrapped into lines and\n * determines their positions on canvas for future rendering based on alignment settings.\n * @param params\n * @returns Results to return via `splitWords()`\n */\nconst _generateSpec = ({\n wrappedLines,\n wordMap,\n positioning: {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n align,\n vAlign,\n },\n}: {\n /** Words organized/wrapped into lines to be rendered. */\n wrappedLines: Word[][];\n\n /** Map of Word to measured dimensions (px) as it would be rendered. */\n wordMap: WordMap;\n\n /**\n * Details on where to render the Words onto canvas. These parameters ultimately come\n * from `SplitWordsProps`, and they come from `DrawTextConfig`.\n */\n positioning: {\n width: SplitWordsProps['width'];\n // NOTE: height does NOT constrain the text; used only for vertical alignment\n height: SplitWordsProps['height'];\n x: SplitWordsProps['x'];\n y: SplitWordsProps['y'];\n align?: SplitWordsProps['align'];\n vAlign?: SplitWordsProps['vAlign'];\n };\n}): RenderSpec => {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n // NOTE: using __font__ ascent/descent to account for all possible characters in the font\n // so that lines with ascenders but no descenders, or vice versa, are all properly\n // aligned to the baseline, and so that lines aren't scrunched\n // NOTE: even for middle vertical alignment, we want to use the __font__ ascent/descent\n // so that words, per line, are still aligned to the baseline (as much as possible; if\n // each word has a different font size, then things will still be offset, but for the\n // same font size, the baseline should match from left to right)\n const getHeight = (word: Word): number =>\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n word.metrics!.fontBoundingBoxAscent + word.metrics!.fontBoundingBoxDescent;\n\n // max height per line\n const lineHeights = wrappedLines.map((line) =>\n line.reduce((acc, word) => {\n return Math.max(acc, getHeight(word));\n }, 0)\n );\n const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);\n\n // vertical alignment (defaults to middle)\n let lineY: number;\n let textBaseline: CanvasTextBaseline;\n if (vAlign === 'top') {\n textBaseline = 'top';\n lineY = boxY;\n } else if (vAlign === 'bottom') {\n textBaseline = 'bottom';\n lineY = yEnd - totalHeight;\n } else {\n // middle\n textBaseline = 'top'; // YES, using 'top' baseline for 'middle' v-align\n lineY = boxY + boxHeight / 2 - totalHeight / 2;\n }\n\n const lines = wrappedLines.map((line, lineIdx): PositionedWord[] => {\n const lineWidth = line.reduce(\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n (acc, word) => acc + word.metrics!.width,\n 0\n );\n const lineHeight = lineHeights[lineIdx];\n\n // horizontal alignment (defaults to center)\n let lineX: number;\n if (align === 'right') {\n lineX = xEnd - lineWidth;\n } else if (align === 'left') {\n lineX = boxX;\n } else {\n // center\n lineX = boxX + boxWidth / 2 - lineWidth / 2;\n }\n\n let wordX = lineX;\n const posWords = line.map((word): PositionedWord => {\n // NOTE: `word.metrics` and `wordMap.get(hash)` must exist as every `word` MUST have\n // been measured at this point\n\n const hash = _getWordHash(word);\n const { format } = wordMap.get(hash)!;\n const x = wordX;\n const height = getHeight(word);\n\n // vertical alignment (defaults to middle)\n let y: number;\n if (vAlign === 'top') {\n y = lineY;\n } else if (vAlign === 'bottom') {\n y = lineY + lineHeight;\n } else {\n // middle\n y = lineY + (lineHeight - height) / 2;\n }\n\n wordX += word.metrics!.width;\n return {\n word,\n format, // undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)\n x,\n y,\n width: word.metrics!.width,\n height,\n isWhitespace: isWhitespace(word.text),\n };\n });\n\n lineY += lineHeight;\n return posWords;\n });\n\n return {\n lines,\n textBaseline,\n textAlign: 'left', // always per current algorithm\n width: boxWidth,\n height: totalHeight,\n };\n};\n\n/**\n * @private\n * Replacer for use with `JSON.stringify()` to deal with `TextMetrics` objects which\n * only have getters/setters instead of value-based properties.\n * @param key Key being processed in `this`.\n * @param value Value of `key` in `this`.\n * @returns Processed value to be serialized, or `undefined` to omit the `key` from the\n * serialized object.\n */\n// CAREFUL: use a `function`, not an arrow function, as stringify() sets its context to\n// the object being serialized on each call to the replacer\nconst _jsonReplacer = function (key: string, value: unknown) {\n if (key === 'metrics' && value && typeof value === 'object') {\n // TODO: need better typings here, if possible, so that TSC warns if we aren't\n // including a property we should be if a new one is needed in the future (i.e. if\n // a new property is added to the `TextMetricsLike` type)\n // NOTE: TextMetrics objects don't have own-enumerable properties; they only have getters,\n // so we have to explicitly get the values we care about instead of spreading them into\n // the new object\n const metrics: CanvasTextMetrics = value as CanvasTextMetrics;\n return {\n width: metrics.width,\n fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,\n fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,\n };\n }\n\n return value;\n};\n\n/**\n * Serializes render specs to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param specs\n * @returns Specs serialized as JSON.\n */\nexport const specToJson = (specs: RenderSpec): string => {\n return JSON.stringify(specs, _jsonReplacer);\n};\n\n/**\n * Serializes a list of Words to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param words\n * @returns Words serialized as JSON.\n */\nexport const wordsToJson = (words: Word[]): string => {\n return JSON.stringify(words, _jsonReplacer);\n};\n\n/**\n * @private\n * Measures a Word in a rendering context, assigning its `TextMetrics` to its `metrics` property.\n * @returns The Word's width, in pixels.\n */\nconst _measureWord = ({\n ctx,\n word,\n wordMap,\n baseTextFormat,\n}: {\n ctx: CanvasRenderContext;\n word: Word;\n wordMap: WordMap;\n baseTextFormat: TextFormat;\n}): number => {\n const hash = _getWordHash(word);\n\n if (word.metrics) {\n // assume Word's text and format haven't changed since last measurement and metrics are good\n\n // make sure we have the metrics and full formatting cached for other identical Words\n if (!wordMap.has(hash)) {\n let format = undefined;\n if (word.format) {\n format = getTextFormat(word.format, baseTextFormat);\n }\n wordMap.set(hash, { metrics: word.metrics, format });\n }\n\n return word.metrics.width;\n }\n\n // check to see if we have already measured an identical Word\n if (wordMap.has(hash)) {\n const { metrics } = wordMap.get(hash)!; // will be there because of `if(has())` check\n word.metrics = metrics;\n return metrics.width;\n }\n\n let ctxSaved = false;\n\n let format = undefined;\n if (word.format) {\n ctx.save();\n ctxSaved = true;\n format = getTextFormat(word.format, baseTextFormat);\n ctx.font = getTextStyle(format); // `fontColor` is ignored as it has no effect on metrics\n }\n\n if (!fontBoundingBoxSupported) {\n // use fallback which comes close enough and still gives us properly-aligned text, albeit\n // lines are a couple pixels tighter together\n if (!ctxSaved) {\n ctx.save();\n ctxSaved = true;\n }\n ctx.textBaseline = 'bottom';\n }\n\n const metrics = ctx.measureText(word.text);\n if (typeof metrics.fontBoundingBoxAscent === 'number') {\n fontBoundingBoxSupported = true;\n } else {\n fontBoundingBoxSupported = false;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxDescent = 0;\n }\n\n word.metrics = metrics;\n wordMap.set(hash, { metrics, format });\n\n if (ctxSaved) {\n ctx.restore();\n }\n\n return metrics.width;\n};\n\n/**\n * Splits Words into positioned lines of Words as they need to be rendred in 2D space,\n * but does not render anything.\n * @param config\n * @returns Lines of positioned words to be rendered, and total height required to\n * render all lines.\n */\nexport const splitWords = ({\n ctx,\n words,\n justify,\n format: baseFormat,\n inferWhitespace = true,\n ...positioning // rest of params are related to positioning\n}: SplitWordsProps): RenderSpec => {\n const wordMap: WordMap = new Map();\n const baseTextFormat = getTextFormat(baseFormat);\n const { width: boxWidth } = positioning;\n\n //// text measurement\n\n // measures an entire line's width up to the `boxWidth` as a max, unless `force=true`,\n // in which case the entire line is measured regardless of `boxWidth`.\n //\n // - Returned `lineWidth` is width up to, but not including, the `splitPoint` (always <= `boxWidth`\n // unless the first Word is too wide to fit, in which case `lineWidth` will be that Word's\n // width even though it's > `boxWidth`).\n // - If `force=true`, will be the full width of the line regardless of `boxWidth`.\n // - Returned `splitPoint` is index into `words` of the Word immediately FOLLOWING the last\n // Word included in the `lineWidth` (and is `words.length` if all Words were included);\n // `splitPoint` could also be thought of as the number of `words` included in the `lineWidth`.\n // - If `force=true`, will always be `words.length`.\n const measureLine = (\n lineWords: Word[],\n force: boolean = false\n ): {\n lineWidth: number;\n splitPoint: number;\n } => {\n let lineWidth = 0;\n let splitPoint = 0;\n lineWords.every((word, idx) => {\n const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });\n if (!force && lineWidth + wordWidth > boxWidth) {\n // at minimum, MUST include at least first Word, even if it's wider than box width\n if (idx === 0) {\n splitPoint = 1;\n lineWidth = wordWidth;\n }\n // else, `lineWidth` already includes at least one Word so this current Word will\n // be the `splitPoint` such that `lineWidth` remains < `boxWidth`\n\n return false; // break\n }\n\n splitPoint++;\n lineWidth += wordWidth;\n return true; // next\n });\n\n return { lineWidth, splitPoint };\n };\n\n //// main\n\n ctx.save();\n\n // start by trimming the `words` to remove any whitespace at either end, then split the `words`\n // into an initial set of lines dictated by explicit hard breaks, if any (if none, we'll have\n // one super long line)\n const hardLines = _splitIntoLines(\n trimLine(words).trimmedLine,\n inferWhitespace\n );\n\n if (\n hardLines.length <= 0 ||\n boxWidth <= 0 ||\n positioning.height <= 0 ||\n (baseFormat &&\n typeof baseFormat.fontSize === 'number' &&\n baseFormat.fontSize <= 0)\n ) {\n // width or height or font size cannot be 0, or there are no lines after trimming\n return {\n lines: [],\n textAlign: 'center',\n textBaseline: 'middle',\n width: positioning.width,\n height: 0,\n };\n }\n\n ctx.font = getTextStyle(baseTextFormat);\n\n const hairWidth = justify\n ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat })\n : 0;\n const wrappedLines: Word[][] = [];\n\n // now further wrap every hard line to make sure it fits within the `boxWidth`, down to a\n // MINIMUM of 1 Word per line\n for (const hardLine of hardLines) {\n let { splitPoint } = measureLine(hardLine);\n\n // if the line fits, we're done; else, we have to break it down further to fit\n // as best as we can (i.e. MIN one word per line, no breaks within words, no\n // leading/pending whitespace)\n if (splitPoint >= hardLine.length) {\n wrappedLines.push(hardLine);\n } else {\n // shallow clone because we're going to break this line down further to get the best fit\n let softLine = hardLine.concat();\n while (splitPoint < softLine.length) {\n // right-trim what we split off in case we split just after some whitespace\n const splitLine = trimLine(\n softLine.slice(0, splitPoint),\n 'right'\n ).trimmedLine;\n wrappedLines.push(splitLine);\n\n // left-trim what remains in case we split just before some whitespace\n softLine = trimLine(softLine.slice(splitPoint), 'left').trimmedLine;\n ({ splitPoint } = measureLine(softLine));\n }\n\n // get the last bit of the `softLine`\n // NOTE: since we started by timming the entire line, and we just left-trimmed\n // what remained of `softLine`, there should be no need to trim again\n wrappedLines.push(softLine);\n }\n }\n\n // never justify a single line because there's no other line to visually justify it to\n if (justify && wrappedLines.length > 1) {\n wrappedLines.forEach((wrappedLine, idx) => {\n // never justify the last line (common in text editors)\n if (idx < wrappedLines.length - 1) {\n const justifiedLine = justifyLine({\n line: wrappedLine,\n spaceWidth: hairWidth,\n spaceChar: HAIR,\n boxWidth,\n });\n\n // make sure any new Words used for justification get measured so we're able to\n // position them later when we generate the render spec\n measureLine(justifiedLine, true);\n wrappedLines[idx] = justifiedLine;\n }\n });\n }\n\n const spec = _generateSpec({\n wrappedLines,\n wordMap,\n positioning,\n });\n\n ctx.restore();\n return spec;\n};\n\n/**\n * Converts a string of text containing words and whitespace, as well as line breaks (newlines),\n * into a `Word[]` that can be given to `splitWords()`.\n * @param text String to convert into Words.\n * @returns Converted text.\n */\nexport const textToWords = (text: string) => {\n const words: Word[] = [];\n\n // split the `text` into a series of Words, preserving whitespace\n let word: Word | undefined = undefined;\n let wasWhitespace = false;\n Array.from(text.trim()).forEach((c) => {\n const charIsWhitespace = isWhitespace(c);\n if (\n (charIsWhitespace && !wasWhitespace) ||\n (!charIsWhitespace && wasWhitespace)\n ) {\n // save current `word`, if any, and start new `word`\n wasWhitespace = charIsWhitespace;\n if (word) {\n words.push(word);\n }\n word = { text: c };\n } else {\n // accumulate into current `word`\n if (!word) {\n word = { text: '' };\n }\n word.text += c;\n }\n });\n\n // make sure we have the last word! ;)\n if (word) {\n words.push(word);\n }\n\n return words;\n};\n\n/**\n * Splits plain text into lines in the order in which they should be rendered, top-down,\n * preserving whitespace __only within the text__ (whitespace on either end is trimmed).\n */\nexport const splitText = ({ text, ...params }: SplitTextProps): string[] => {\n const words = textToWords(text);\n\n const results = splitWords({\n ...params,\n words,\n inferWhitespace: false,\n });\n\n return results.lines.map((line) =>\n line.map(({ word: { text: t } }) => t).join('')\n );\n};\n","import { getTextStyle } from './style';\nimport { CanvasRenderContext, Word } from '../model';\n\n/** @private */\nconst _getHeight = (ctx: CanvasRenderContext, text: string, style?: string) => {\n const previousTextBaseline = ctx.textBaseline;\n const previousFont = ctx.font;\n\n ctx.textBaseline = 'bottom';\n if (style) {\n ctx.font = style;\n }\n const { actualBoundingBoxAscent: height } = ctx.measureText(text);\n\n // Reset baseline\n ctx.textBaseline = previousTextBaseline;\n if (style) {\n ctx.font = previousFont;\n }\n\n return height;\n};\n\n/**\n * Gets the measured height of a given `Word` using its text style.\n * @returns {number} Height in pixels.\n */\nexport const getWordHeight = ({\n ctx,\n word,\n}: {\n ctx: CanvasRenderContext;\n /**\n * Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.\n */\n word: Word;\n}) => {\n return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));\n};\n\n/**\n * Gets the measured height of a given `string` using a given text style.\n * @returns {number} Height in pixels.\n */\nexport const getTextHeight = ({\n ctx,\n text,\n style,\n}: {\n ctx: CanvasRenderContext;\n text: string;\n /**\n * CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font\n * settings/styles are used.\n */\n style?: string;\n}) => {\n return _getHeight(ctx, text, style);\n};\n","import {\n specToJson,\n splitWords,\n splitText,\n textToWords,\n wordsToJson,\n} from './util/split';\nimport { getTextHeight, getWordHeight } from './util/height';\nimport { getTextStyle, getTextFormat, DEFAULT_FONT_COLOR } from './util/style';\nimport { CanvasRenderContext, DrawTextConfig, Text } from './model';\n\nconst drawText = (\n ctx: CanvasRenderContext,\n text: Text,\n config: DrawTextConfig\n) => {\n const baseFormat = getTextFormat({\n fontFamily: config.fontFamily,\n fontSize: config.fontSize,\n fontStyle: config.fontStyle,\n fontVariant: config.fontVariant,\n fontWeight: config.fontWeight,\n });\n\n const {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n } = config;\n\n const {\n lines: richLines,\n height: totalHeight,\n textBaseline,\n textAlign,\n } = splitWords({\n ctx,\n words: Array.isArray(text) ? text : textToWords(text),\n inferWhitespace: Array.isArray(text)\n ? config.inferWhitespace === undefined || config.inferWhitespace\n : undefined, // ignore since `text` is a string; we assume it already has all the whitespace it needs\n x: boxX,\n y: boxY,\n width: config.width,\n height: config.height,\n align: config.align,\n vAlign: config.vAlign,\n justify: config.justify,\n format: baseFormat,\n });\n\n ctx.save();\n ctx.textAlign = textAlign;\n ctx.textBaseline = textBaseline;\n ctx.font = getTextStyle(baseFormat);\n ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;\n\n if (config.overflow === false) {\n ctx.beginPath();\n ctx.rect(boxX, boxY, boxWidth, boxHeight);\n ctx.clip(); // part of saved context state\n }\n\n richLines.forEach((line) => {\n line.forEach((pw) => {\n if (!pw.isWhitespace) {\n // NOTE: don't use the `pw.word.format` as this could be incomplete; use `pw.format`\n // if it exists as this will always be the __full__ TextFormat used to measure the\n // Word, and so should be what is used to render it\n if (pw.format) {\n ctx.save();\n ctx.font = getTextStyle(pw.format);\n if (pw.format.fontColor) {\n ctx.fillStyle = pw.format.fontColor;\n }\n }\n ctx.fillText(pw.word.text, pw.x, pw.y);\n if (pw.format) {\n ctx.restore();\n }\n }\n });\n });\n\n if (config.debug) {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n let textAnchor: number;\n if (config.align === 'right') {\n textAnchor = xEnd;\n } else if (config.align === 'left') {\n textAnchor = boxX;\n } else {\n textAnchor = boxX + boxWidth / 2;\n }\n\n let debugY = boxY;\n if (config.vAlign === 'bottom') {\n debugY = yEnd;\n } else if (config.vAlign === 'middle') {\n debugY = boxY + boxHeight / 2;\n }\n\n const debugColor = '#0C8CE9';\n\n // Text box\n ctx.lineWidth = 1;\n ctx.strokeStyle = debugColor;\n ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);\n\n ctx.lineWidth = 1;\n\n if (!config.align || config.align === 'center') {\n // Horizontal Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(textAnchor, boxY);\n ctx.lineTo(textAnchor, yEnd);\n ctx.stroke();\n }\n\n if (!config.vAlign || config.vAlign === 'middle') {\n // Vertical Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(boxX, debugY);\n ctx.lineTo(xEnd, debugY);\n ctx.stroke();\n }\n }\n\n ctx.restore();\n\n return { height: totalHeight };\n};\n\nexport {\n drawText,\n specToJson,\n splitText,\n splitWords,\n textToWords,\n wordsToJson,\n getTextHeight,\n getWordHeight,\n getTextStyle,\n getTextFormat,\n};\nexport * from './model';\n"],"names":["DEFAULT_FONT_FAMILY","DEFAULT_FONT_SIZE","DEFAULT_FONT_COLOR","getTextFormat","format","baseFormat","getTextStyle","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","isWhitespace","text","_extractWords","line","word","_cloneWord","clone","_joinWords","words","joiner","phrase","wordIdx","jw","justifyLine","spaceWidth","spaceChar","boxWidth","wordsWidth","width","_b","_a","noOfSpacesToInsert","spacesPerWord","spaces","firstWords","firstPart","remainingSpaces","lastWord","trimLine","side","leftTrim","rightTrim","HAIR","SPACE","fontBoundingBoxSupported","_getWordHash","_splitIntoLines","inferWhitespace","lines","wasWhitespace","i","_c","_generateSpec","wrappedLines","wordMap","boxHeight","boxX","boxY","align","vAlign","xEnd","yEnd","getHeight","lineHeights","acc","totalHeight","h","lineY","textBaseline","lineIdx","lineWidth","lineHeight","lineX","wordX","posWords","hash","x","height","y","_jsonReplacer","key","value","metrics","specToJson","specs","wordsToJson","_measureWord","ctx","baseTextFormat","ctxSaved","splitWords","justify","positioning","measureLine","lineWords","force","splitPoint","idx","wordWidth","hardLines","hairWidth","hardLine","softLine","splitLine","wrappedLine","justifiedLine","spec","textToWords","c","charIsWhitespace","splitText","params","t","_getHeight","style","previousTextBaseline","previousFont","getWordHeight","getTextHeight","drawText","config","richLines","textAlign","pw","textAnchor","debugY","debugColor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"text-to-canvas.min.js","sources":["../src/lib/util/style.ts","../src/lib/util/whitespace.ts","../src/lib/util/justify.ts","../src/lib/util/trim.ts","../src/lib/util/split.ts","../src/lib/util/height.ts","../src/lib/index.ts"],"sourcesContent":["import { TextFormat } from '../model';\n\nexport const DEFAULT_FONT_FAMILY = 'Arial';\nexport const DEFAULT_FONT_SIZE = 14;\nexport const DEFAULT_FONT_COLOR = 'black';\n\n/**\n * Generates a text format based on defaults and any provided overrides.\n * @param format Overrides to `baseFormat` and default format.\n * @param baseFormat Overrides to default format.\n * @returns Full text format (all properties specified).\n */\nexport const getTextFormat = (\n format?: TextFormat,\n baseFormat?: TextFormat\n): Required<TextFormat> => {\n return Object.assign(\n {},\n {\n fontFamily: DEFAULT_FONT_FAMILY,\n fontSize: DEFAULT_FONT_SIZE,\n fontWeight: '400',\n fontStyle: '',\n fontVariant: '',\n fontColor: DEFAULT_FONT_COLOR,\n },\n baseFormat,\n format\n );\n};\n\n/**\n * Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.\n * @param format\n * @returns Style string to set on context's `font` property. Note this __does not include\n * the font color__ as that is not part of the CSS font value. Color must be handled separately.\n */\nexport const getTextStyle = ({\n fontFamily,\n fontSize,\n fontStyle,\n fontVariant,\n fontWeight,\n}: TextFormat) => {\n // per spec:\n // - font-style, font-variant and font-weight must precede font-size\n // - font-family must be the last value specified\n // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font\n return `${fontStyle || ''} ${fontVariant || ''} ${\n fontWeight || ''\n } ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();\n};\n","/**\n * Determines if a string is only whitespace (one or more characters of it).\n * @param text\n * @returns True if `text` is one or more characters of whitespace, only.\n */\nexport const isWhitespace = (text: string) => {\n return !!text.match(/^\\s+$/);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * @private\n * Extracts the __visible__ (i.e. non-whitespace) words from a line.\n * @param line\n * @returns New array with only non-whitespace words.\n */\nconst _extractWords = (line: Word[]) => {\n return line.filter((word) => !isWhitespace(word.text));\n};\n\n/**\n * @private\n * Deep-clones a Word.\n * @param word\n * @returns Deep-cloned Word.\n */\nconst _cloneWord = (word: Word) => {\n const clone = { ...word };\n if (word.format) {\n clone.format = { ...word.format };\n }\n return clone;\n};\n\n/**\n * @private\n * Joins Words together using another set of Words.\n * @param words Words to join.\n * @param joiner Words to use when joining `words`. These will be deep-cloned and inserted\n * in between every word in `words`, similar to `Array.join(string)` where the `string`\n * is inserted in between every element.\n * @returns New array of Words. Empty if `words` is empty. New array of one Word if `words`\n * contains only one Word.\n */\nconst _joinWords = (words: Word[], joiner: Word[]) => {\n if (words.length <= 1 || joiner.length < 1) {\n return [...words];\n }\n\n const phrase: Word[] = [];\n words.forEach((word, wordIdx) => {\n phrase.push(word);\n if (wordIdx < words.length - 1) {\n // don't append after last `word`\n joiner.forEach((jw) => phrase.push(_cloneWord(jw)));\n }\n });\n\n return phrase;\n};\n\n/**\n * Inserts spaces between words in a line in order to raise the line width to the box width.\n * The spaces are evenly spread in the line, and extra spaces (if any) are only inserted\n * between words, not at either end of the `line`.\n *\n * @returns New array containing original words from the `line` with additional whitespace\n * for justification to `boxWidth`.\n */\nexport const justifyLine = ({\n line,\n spaceWidth,\n spaceChar,\n boxWidth,\n}: {\n /** Assumed to have already been trimmed on both ends. */\n line: Word[];\n /** Width (px) of `spaceChar`. */\n spaceWidth: number;\n /**\n * Character used as a whitespace in justification. Will be injected in between Words in\n * `line` in order to justify the text on the line within `lineWidth`.\n */\n spaceChar: string;\n /** Width (px) of the box containing the text (i.e. max `line` width). */\n boxWidth: number;\n}) => {\n const words = _extractWords(line);\n if (words.length <= 1) {\n return line.concat();\n }\n\n const wordsWidth = words.reduce(\n (width, word) => width + (word.metrics?.width ?? 0),\n 0\n );\n const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;\n\n if (words.length > 2) {\n // use CEILING so we spread the partial spaces throughout except between the second-last\n // and last word so that the spacing is more even and as tight as we can get it to\n // the `boxWidth`\n const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));\n const spaces: Word[] = Array.from({ length: spacesPerWord }, () => ({\n text: spaceChar,\n }));\n const firstWords = words.slice(0, words.length - 1); // all but last word\n const firstPart = _joinWords(firstWords, spaces);\n const remainingSpaces = spaces.slice(\n 0,\n Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces.length\n );\n const lastWord = words[words.length - 1];\n return [...firstPart, ...remainingSpaces, lastWord];\n }\n // only 2 words so fill with spaces in between them: use FLOOR to make sure we don't\n // go past `boxWidth`\n const spaces: Word[] = Array.from(\n { length: Math.floor(noOfSpacesToInsert) },\n () => ({ text: spaceChar })\n );\n return _joinWords(words, spaces);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * Trims whitespace from the beginning and end of a `line`.\n * @param line\n * @param side Which side to trim.\n * @returns An object containing trimmed characters, and the new trimmed line.\n */\nexport const trimLine = (\n line: Word[],\n side: 'left' | 'right' | 'both' = 'both'\n): {\n /**\n * New array containing what was trimmed from the left (empty if none).\n */\n trimmedLeft: Word[];\n /**\n * New array containing what was trimmed from the right (empty if none).\n */\n trimmedRight: Word[];\n /**\n * New array representing the trimmed line, even if nothing gets trimmed. Empty array if\n * all whitespace.\n */\n trimmedLine: Word[];\n} => {\n let leftTrim = 0;\n if (side === 'left' || side === 'both') {\n for (; leftTrim < line.length; leftTrim++) {\n if (!isWhitespace(line[leftTrim].text)) {\n break;\n }\n }\n\n if (leftTrim >= line.length) {\n // all whitespace\n return {\n trimmedLeft: line.concat(),\n trimmedRight: [],\n trimmedLine: [],\n };\n }\n }\n\n let rightTrim = line.length;\n if (side === 'right' || side === 'both') {\n rightTrim--;\n for (; rightTrim >= 0; rightTrim--) {\n if (!isWhitespace(line[rightTrim].text)) {\n break;\n }\n }\n rightTrim++; // back up one since we started one down for 0-based indexes\n\n if (rightTrim <= 0) {\n // all whitespace\n return {\n trimmedLeft: [],\n trimmedRight: line.concat(),\n trimmedLine: [],\n };\n }\n }\n\n return {\n trimmedLeft: line.slice(0, leftTrim),\n trimmedRight: line.slice(rightTrim),\n trimmedLine: line.slice(leftTrim, rightTrim),\n };\n};\n","import { getTextFormat, getTextStyle } from './style';\nimport { isWhitespace } from './whitespace';\nimport { justifyLine } from './justify';\nimport {\n PositionedWord,\n SplitTextProps,\n SplitWordsProps,\n RenderSpec,\n Word,\n WordMap,\n CanvasTextMetrics,\n TextFormat,\n CanvasRenderContext,\n} from '../model';\nimport { trimLine } from './trim';\n\n// Hair space character for precise justification\nconst HAIR = '\\u{200a}';\n\n// for when we're inferring whitespace between words\nconst SPACE = ' ';\n\n/**\n * Whether the canvas API being used supports the newer `fontBoundingBox*` properties or not.\n *\n * True if it does, false if not; undefined until we determine either way.\n *\n * Note about `fontBoundingBoxAscent/Descent`: Only later browsers support this and the Node-based\n * `canvas` package does not. Having these properties will have a noticeable increase in performance\n * on large pieces of text to render. Failing these, a fallback is used which involves\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics\n * @see https://www.npmjs.com/package/canvas\n */\nlet fontBoundingBoxSupported: boolean;\n\n/**\n * @private\n * Generates a word hash for use as a key in a `WordMap`.\n * @param word\n * @returns Hash.\n */\nconst _getWordHash = (word: Word) => {\n return `${word.text}${word.format ? JSON.stringify(word.format) : ''}`;\n};\n\n/**\n * @private\n * Splits words into lines based on words that are single newline characters.\n * @param words\n * @param inferWhitespace True (default) if whitespace should be inferred (and injected)\n * based on words; false if we're to assume the words already include all necessary whitespace.\n * @returns Words expressed as lines.\n */\nconst _splitIntoLines = (\n words: Word[],\n inferWhitespace: boolean = true\n): Word[][] => {\n const lines: Word[][] = [[]];\n\n let wasWhitespace = false; // true if previous word was whitespace\n words.forEach((word, wordIdx) => {\n // TODO: this is likely a naive split (at least based on character?); should at least\n // think about this more; text format shouldn't matter on a line break, right (hope not)?\n if (word.text.match(/^\\n+$/)) {\n for (let i = 0; i < word.text.length; i++) {\n lines.push([]);\n }\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (isWhitespace(word.text)) {\n // whitespace OTHER THAN newlines since we checked for newlines above\n lines.at(-1)?.push(word);\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (word.text === '') {\n return; // skip to next `word`\n }\n\n // looks like a non-empty, non-whitespace word at this point, so if it isn't the first\n // word and the one before wasn't whitespace, insert a space\n if (inferWhitespace && !wasWhitespace && wordIdx > 0) {\n lines.at(-1)?.push({ text: SPACE });\n }\n\n lines.at(-1)?.push(word);\n wasWhitespace = false;\n });\n\n return lines;\n};\n\n/**\n * @private\n * Helper for `splitWords()` that takes the words that have been wrapped into lines and\n * determines their positions on canvas for future rendering based on alignment settings.\n * @param params\n * @returns Results to return via `splitWords()`\n */\nconst _generateSpec = ({\n wrappedLines,\n wordMap,\n positioning: {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n align,\n vAlign,\n },\n}: {\n /** Words organized/wrapped into lines to be rendered. */\n wrappedLines: Word[][];\n\n /** Map of Word to measured dimensions (px) as it would be rendered. */\n wordMap: WordMap;\n\n /**\n * Details on where to render the Words onto canvas. These parameters ultimately come\n * from `SplitWordsProps`, and they come from `DrawTextConfig`.\n */\n positioning: {\n width: SplitWordsProps['width'];\n // NOTE: height does NOT constrain the text; used only for vertical alignment\n height: SplitWordsProps['height'];\n x: SplitWordsProps['x'];\n y: SplitWordsProps['y'];\n align?: SplitWordsProps['align'];\n vAlign?: SplitWordsProps['vAlign'];\n };\n}): RenderSpec => {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n // NOTE: using __font__ ascent/descent to account for all possible characters in the font\n // so that lines with ascenders but no descenders, or vice versa, are all properly\n // aligned to the baseline, and so that lines aren't scrunched\n // NOTE: even for middle vertical alignment, we want to use the __font__ ascent/descent\n // so that words, per line, are still aligned to the baseline (as much as possible; if\n // each word has a different font size, then things will still be offset, but for the\n // same font size, the baseline should match from left to right)\n const getHeight = (word: Word): number =>\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n word.metrics!.fontBoundingBoxAscent + word.metrics!.fontBoundingBoxDescent;\n\n // max height per line\n const lineHeights = wrappedLines.map((line) =>\n line.reduce((acc, word) => {\n return Math.max(acc, getHeight(word));\n }, 0)\n );\n const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);\n\n // vertical alignment (defaults to middle)\n let lineY: number;\n let textBaseline: CanvasTextBaseline;\n if (vAlign === 'top') {\n textBaseline = 'top';\n lineY = boxY;\n } else if (vAlign === 'bottom') {\n textBaseline = 'bottom';\n lineY = yEnd - totalHeight;\n } else {\n // middle\n textBaseline = 'top'; // YES, using 'top' baseline for 'middle' v-align\n lineY = boxY + boxHeight / 2 - totalHeight / 2;\n }\n\n const lines = wrappedLines.map((line, lineIdx): PositionedWord[] => {\n const lineWidth = line.reduce(\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n (acc, word) => acc + word.metrics!.width,\n 0\n );\n const lineHeight = lineHeights[lineIdx];\n\n // horizontal alignment (defaults to center)\n let lineX: number;\n if (align === 'right') {\n lineX = xEnd - lineWidth;\n } else if (align === 'left') {\n lineX = boxX;\n } else {\n // center\n lineX = boxX + boxWidth / 2 - lineWidth / 2;\n }\n\n let wordX = lineX;\n const posWords = line.map((word): PositionedWord => {\n // NOTE: `word.metrics` and `wordMap.get(hash)` must exist as every `word` MUST have\n // been measured at this point\n\n const hash = _getWordHash(word);\n const { format } = wordMap.get(hash)!;\n const x = wordX;\n const height = getHeight(word);\n\n // vertical alignment (defaults to middle)\n let y: number;\n if (vAlign === 'top') {\n y = lineY;\n } else if (vAlign === 'bottom') {\n y = lineY + lineHeight;\n } else {\n // middle\n y = lineY + (lineHeight - height) / 2;\n }\n\n wordX += word.metrics!.width;\n return {\n word,\n format, // undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)\n x,\n y,\n width: word.metrics!.width,\n height,\n isWhitespace: isWhitespace(word.text),\n };\n });\n\n lineY += lineHeight;\n return posWords;\n });\n\n return {\n lines,\n textBaseline,\n textAlign: 'left', // always per current algorithm\n width: boxWidth,\n height: totalHeight,\n };\n};\n\n/**\n * @private\n * Replacer for use with `JSON.stringify()` to deal with `TextMetrics` objects which\n * only have getters/setters instead of value-based properties.\n * @param key Key being processed in `this`.\n * @param value Value of `key` in `this`.\n * @returns Processed value to be serialized, or `undefined` to omit the `key` from the\n * serialized object.\n */\n// CAREFUL: use a `function`, not an arrow function, as stringify() sets its context to\n// the object being serialized on each call to the replacer\nconst _jsonReplacer = function (key: string, value: unknown) {\n if (key === 'metrics' && value && typeof value === 'object') {\n // TODO: need better typings here, if possible, so that TSC warns if we aren't\n // including a property we should be if a new one is needed in the future (i.e. if\n // a new property is added to the `TextMetricsLike` type)\n // NOTE: TextMetrics objects don't have own-enumerable properties; they only have getters,\n // so we have to explicitly get the values we care about instead of spreading them into\n // the new object\n const metrics: CanvasTextMetrics = value as CanvasTextMetrics;\n return {\n width: metrics.width,\n fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,\n fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,\n };\n }\n\n return value;\n};\n\n/**\n * Serializes render specs to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param specs\n * @returns Specs serialized as JSON.\n */\nexport const specToJson = (specs: RenderSpec): string => {\n return JSON.stringify(specs, _jsonReplacer);\n};\n\n/**\n * Serializes a list of Words to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param words\n * @returns Words serialized as JSON.\n */\nexport const wordsToJson = (words: Word[]): string => {\n return JSON.stringify(words, _jsonReplacer);\n};\n\n/**\n * @private\n * Measures a Word in a rendering context, assigning its `TextMetrics` to its `metrics` property.\n * @returns The Word's width, in pixels.\n */\nconst _measureWord = ({\n ctx,\n word,\n wordMap,\n baseTextFormat,\n}: {\n ctx: CanvasRenderContext;\n word: Word;\n wordMap: WordMap;\n baseTextFormat: TextFormat;\n}): number => {\n const hash = _getWordHash(word);\n\n if (word.metrics) {\n // assume Word's text and format haven't changed since last measurement and metrics are good\n\n // make sure we have the metrics and full formatting cached for other identical Words\n if (!wordMap.has(hash)) {\n let format = undefined;\n if (word.format) {\n format = getTextFormat(word.format, baseTextFormat);\n }\n wordMap.set(hash, { metrics: word.metrics, format });\n }\n\n return word.metrics.width;\n }\n\n // check to see if we have already measured an identical Word\n if (wordMap.has(hash)) {\n const { metrics } = wordMap.get(hash)!; // will be there because of `if(has())` check\n word.metrics = metrics;\n return metrics.width;\n }\n\n let ctxSaved = false;\n\n let format = undefined;\n if (word.format) {\n ctx.save();\n ctxSaved = true;\n format = getTextFormat(word.format, baseTextFormat);\n ctx.font = getTextStyle(format); // `fontColor` is ignored as it has no effect on metrics\n }\n\n if (!fontBoundingBoxSupported) {\n // use fallback which comes close enough and still gives us properly-aligned text, albeit\n // lines are a couple pixels tighter together\n if (!ctxSaved) {\n ctx.save();\n ctxSaved = true;\n }\n ctx.textBaseline = 'bottom';\n }\n\n const metrics = ctx.measureText(word.text);\n if (typeof metrics.fontBoundingBoxAscent === 'number') {\n fontBoundingBoxSupported = true;\n } else {\n fontBoundingBoxSupported = false;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxDescent = 0;\n }\n\n word.metrics = metrics;\n wordMap.set(hash, { metrics, format });\n\n if (ctxSaved) {\n ctx.restore();\n }\n\n return metrics.width;\n};\n\n/**\n * Splits Words into positioned lines of Words as they need to be rendred in 2D space,\n * but does not render anything.\n * @param config\n * @returns Lines of positioned words to be rendered, and total height required to\n * render all lines.\n */\nexport const splitWords = ({\n ctx,\n words,\n justify,\n format: baseFormat,\n inferWhitespace = true,\n ...positioning // rest of params are related to positioning\n}: SplitWordsProps): RenderSpec => {\n const wordMap: WordMap = new Map();\n const baseTextFormat = getTextFormat(baseFormat);\n const { width: boxWidth } = positioning;\n\n //// text measurement\n\n // measures an entire line's width up to the `boxWidth` as a max, unless `force=true`,\n // in which case the entire line is measured regardless of `boxWidth`.\n //\n // - Returned `lineWidth` is width up to, but not including, the `splitPoint` (always <= `boxWidth`\n // unless the first Word is too wide to fit, in which case `lineWidth` will be that Word's\n // width even though it's > `boxWidth`).\n // - If `force=true`, will be the full width of the line regardless of `boxWidth`.\n // - Returned `splitPoint` is index into `words` of the Word immediately FOLLOWING the last\n // Word included in the `lineWidth` (and is `words.length` if all Words were included);\n // `splitPoint` could also be thought of as the number of `words` included in the `lineWidth`.\n // - If `force=true`, will always be `words.length`.\n const measureLine = (\n lineWords: Word[],\n force: boolean = false\n ): {\n lineWidth: number;\n splitPoint: number;\n } => {\n let lineWidth = 0;\n let splitPoint = 0;\n lineWords.every((word, idx) => {\n const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });\n if (!force && lineWidth + wordWidth > boxWidth) {\n // at minimum, MUST include at least first Word, even if it's wider than box width\n if (idx === 0) {\n splitPoint = 1;\n lineWidth = wordWidth;\n }\n // else, `lineWidth` already includes at least one Word so this current Word will\n // be the `splitPoint` such that `lineWidth` remains < `boxWidth`\n\n return false; // break\n }\n\n splitPoint++;\n lineWidth += wordWidth;\n return true; // next\n });\n\n return { lineWidth, splitPoint };\n };\n\n //// main\n\n ctx.save();\n\n // start by trimming the `words` to remove any whitespace at either end, then split the `words`\n // into an initial set of lines dictated by explicit hard breaks, if any (if none, we'll have\n // one super long line)\n const hardLines = _splitIntoLines(\n trimLine(words).trimmedLine,\n inferWhitespace\n );\n\n if (\n hardLines.length <= 0 ||\n boxWidth <= 0 ||\n positioning.height <= 0 ||\n (baseFormat &&\n typeof baseFormat.fontSize === 'number' &&\n baseFormat.fontSize <= 0)\n ) {\n // width or height or font size cannot be 0, or there are no lines after trimming\n return {\n lines: [],\n textAlign: 'center',\n textBaseline: 'middle',\n width: positioning.width,\n height: 0,\n };\n }\n\n ctx.font = getTextStyle(baseTextFormat);\n\n const hairWidth = justify\n ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat })\n : 0;\n const wrappedLines: Word[][] = [];\n\n // now further wrap every hard line to make sure it fits within the `boxWidth`, down to a\n // MINIMUM of 1 Word per line\n for (const hardLine of hardLines) {\n let { splitPoint } = measureLine(hardLine);\n\n // if the line fits, we're done; else, we have to break it down further to fit\n // as best as we can (i.e. MIN one word per line, no breaks within words, no\n // leading/pending whitespace)\n if (splitPoint >= hardLine.length) {\n wrappedLines.push(hardLine);\n } else {\n // shallow clone because we're going to break this line down further to get the best fit\n let softLine = hardLine.concat();\n while (splitPoint < softLine.length) {\n // right-trim what we split off in case we split just after some whitespace\n const splitLine = trimLine(\n softLine.slice(0, splitPoint),\n 'right'\n ).trimmedLine;\n wrappedLines.push(splitLine);\n\n // left-trim what remains in case we split just before some whitespace\n softLine = trimLine(softLine.slice(splitPoint), 'left').trimmedLine;\n ({ splitPoint } = measureLine(softLine));\n }\n\n // get the last bit of the `softLine`\n // NOTE: since we started by timming the entire line, and we just left-trimmed\n // what remained of `softLine`, there should be no need to trim again\n wrappedLines.push(softLine);\n }\n }\n\n // never justify a single line because there's no other line to visually justify it to\n if (justify && wrappedLines.length > 1) {\n wrappedLines.forEach((wrappedLine, idx) => {\n // never justify the last line (common in text editors)\n if (idx < wrappedLines.length - 1) {\n const justifiedLine = justifyLine({\n line: wrappedLine,\n spaceWidth: hairWidth,\n spaceChar: HAIR,\n boxWidth,\n });\n\n // make sure any new Words used for justification get measured so we're able to\n // position them later when we generate the render spec\n measureLine(justifiedLine, true);\n wrappedLines[idx] = justifiedLine;\n }\n });\n }\n\n const spec = _generateSpec({\n wrappedLines,\n wordMap,\n positioning,\n });\n\n ctx.restore();\n return spec;\n};\n\n/**\n * Converts a string of text containing words and whitespace, as well as line breaks (newlines),\n * into a `Word[]` that can be given to `splitWords()`.\n * @param text String to convert into Words.\n * @returns Converted text.\n */\nexport const textToWords = (text: string) => {\n const words: Word[] = [];\n\n // split the `text` into a series of Words, preserving whitespace\n let word: Word | undefined = undefined;\n let wasWhitespace = false;\n Array.from(text.trim()).forEach((c) => {\n const charIsWhitespace = isWhitespace(c);\n if (\n (charIsWhitespace && !wasWhitespace) ||\n (!charIsWhitespace && wasWhitespace)\n ) {\n // save current `word`, if any, and start new `word`\n wasWhitespace = charIsWhitespace;\n if (word) {\n words.push(word);\n }\n word = { text: c };\n } else {\n // accumulate into current `word`\n if (!word) {\n word = { text: '' };\n }\n word.text += c;\n }\n });\n\n // make sure we have the last word! ;)\n if (word) {\n words.push(word);\n }\n\n return words;\n};\n\n/**\n * Splits plain text into lines in the order in which they should be rendered, top-down,\n * preserving whitespace __only within the text__ (whitespace on either end is trimmed).\n */\nexport const splitText = ({ text, ...params }: SplitTextProps): string[] => {\n const words = textToWords(text);\n\n const results = splitWords({\n ...params,\n words,\n inferWhitespace: false,\n });\n\n return results.lines.map((line) =>\n line.map(({ word: { text: t } }) => t).join('')\n );\n};\n","import { getTextStyle } from './style';\nimport { CanvasRenderContext, Word } from '../model';\n\n/** @private */\nconst _getHeight = (ctx: CanvasRenderContext, text: string, style?: string) => {\n const previousTextBaseline = ctx.textBaseline;\n const previousFont = ctx.font;\n\n ctx.textBaseline = 'bottom';\n if (style) {\n ctx.font = style;\n }\n const { actualBoundingBoxAscent: height } = ctx.measureText(text);\n\n // Reset baseline\n ctx.textBaseline = previousTextBaseline;\n if (style) {\n ctx.font = previousFont;\n }\n\n return height;\n};\n\n/**\n * Gets the measured height of a given `Word` using its text style.\n * @returns {number} Height in pixels.\n */\nexport const getWordHeight = ({\n ctx,\n word,\n}: {\n ctx: CanvasRenderContext;\n /**\n * Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.\n */\n word: Word;\n}) => {\n return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));\n};\n\n/**\n * Gets the measured height of a given `string` using a given text style.\n * @returns {number} Height in pixels.\n */\nexport const getTextHeight = ({\n ctx,\n text,\n style,\n}: {\n ctx: CanvasRenderContext;\n text: string;\n /**\n * CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font\n * settings/styles are used.\n */\n style?: string;\n}) => {\n return _getHeight(ctx, text, style);\n};\n","import {\n specToJson,\n splitWords,\n splitText,\n textToWords,\n wordsToJson,\n} from './util/split';\nimport { getTextHeight, getWordHeight } from './util/height';\nimport { getTextStyle, getTextFormat, DEFAULT_FONT_COLOR } from './util/style';\nimport { CanvasRenderContext, DrawTextConfig, Text } from './model';\n\nconst drawText = (\n ctx: CanvasRenderContext,\n text: Text,\n config: DrawTextConfig\n) => {\n const baseFormat = getTextFormat({\n fontFamily: config.fontFamily,\n fontSize: config.fontSize,\n fontStyle: config.fontStyle,\n fontVariant: config.fontVariant,\n fontWeight: config.fontWeight,\n fontColor: config.fontColor,\n });\n\n const {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n } = config;\n\n const {\n lines: richLines,\n height: totalHeight,\n textBaseline,\n textAlign,\n } = splitWords({\n ctx,\n words: Array.isArray(text) ? text : textToWords(text),\n inferWhitespace: Array.isArray(text)\n ? config.inferWhitespace === undefined || config.inferWhitespace\n : undefined, // ignore since `text` is a string; we assume it already has all the whitespace it needs\n x: boxX,\n y: boxY,\n width: config.width,\n height: config.height,\n align: config.align,\n vAlign: config.vAlign,\n justify: config.justify,\n format: baseFormat,\n });\n\n ctx.save();\n ctx.textAlign = textAlign;\n ctx.textBaseline = textBaseline;\n ctx.font = getTextStyle(baseFormat);\n ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;\n\n if (config.overflow === false) {\n ctx.beginPath();\n ctx.rect(boxX, boxY, boxWidth, boxHeight);\n ctx.clip(); // part of saved context state\n }\n\n richLines.forEach((line) => {\n line.forEach((pw) => {\n if (!pw.isWhitespace) {\n // NOTE: don't use the `pw.word.format` as this could be incomplete; use `pw.format`\n // if it exists as this will always be the __full__ TextFormat used to measure the\n // Word, and so should be what is used to render it\n if (pw.format) {\n ctx.save();\n ctx.font = getTextStyle(pw.format);\n if (pw.format.fontColor) {\n ctx.fillStyle = pw.format.fontColor;\n }\n }\n ctx.fillText(pw.word.text, pw.x, pw.y);\n if (pw.format) {\n ctx.restore();\n }\n }\n });\n });\n\n if (config.debug) {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n let textAnchor: number;\n if (config.align === 'right') {\n textAnchor = xEnd;\n } else if (config.align === 'left') {\n textAnchor = boxX;\n } else {\n textAnchor = boxX + boxWidth / 2;\n }\n\n let debugY = boxY;\n if (config.vAlign === 'bottom') {\n debugY = yEnd;\n } else if (config.vAlign === 'middle') {\n debugY = boxY + boxHeight / 2;\n }\n\n const debugColor = '#0C8CE9';\n\n // Text box\n ctx.lineWidth = 1;\n ctx.strokeStyle = debugColor;\n ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);\n\n ctx.lineWidth = 1;\n\n if (!config.align || config.align === 'center') {\n // Horizontal Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(textAnchor, boxY);\n ctx.lineTo(textAnchor, yEnd);\n ctx.stroke();\n }\n\n if (!config.vAlign || config.vAlign === 'middle') {\n // Vertical Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(boxX, debugY);\n ctx.lineTo(xEnd, debugY);\n ctx.stroke();\n }\n }\n\n ctx.restore();\n\n return { height: totalHeight };\n};\n\nexport {\n drawText,\n specToJson,\n splitText,\n splitWords,\n textToWords,\n wordsToJson,\n getTextHeight,\n getWordHeight,\n getTextStyle,\n getTextFormat,\n};\nexport * from './model';\n"],"names":["DEFAULT_FONT_FAMILY","DEFAULT_FONT_SIZE","DEFAULT_FONT_COLOR","getTextFormat","format","baseFormat","getTextStyle","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","isWhitespace","text","_extractWords","line","word","_cloneWord","clone","_joinWords","words","joiner","phrase","wordIdx","jw","justifyLine","spaceWidth","spaceChar","boxWidth","wordsWidth","width","_b","_a","noOfSpacesToInsert","spacesPerWord","spaces","firstWords","firstPart","remainingSpaces","lastWord","trimLine","side","leftTrim","rightTrim","HAIR","SPACE","fontBoundingBoxSupported","_getWordHash","_splitIntoLines","inferWhitespace","lines","wasWhitespace","i","_c","_generateSpec","wrappedLines","wordMap","boxHeight","boxX","boxY","align","vAlign","xEnd","yEnd","getHeight","lineHeights","acc","totalHeight","h","lineY","textBaseline","lineIdx","lineWidth","lineHeight","lineX","wordX","posWords","hash","x","height","y","_jsonReplacer","key","value","metrics","specToJson","specs","wordsToJson","_measureWord","ctx","baseTextFormat","ctxSaved","splitWords","justify","positioning","measureLine","lineWords","force","splitPoint","idx","wordWidth","hardLines","hairWidth","hardLine","softLine","splitLine","wrappedLine","justifiedLine","spec","textToWords","c","charIsWhitespace","splitText","params","t","_getHeight","style","previousTextBaseline","previousFont","getWordHeight","getTextHeight","drawText","config","richLines","textAlign","pw","textAnchor","debugY","debugColor"],"mappings":"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"}
|
package/dist/text-to-canvas.mjs
CHANGED
|
@@ -471,7 +471,8 @@ const drawText = (ctx, text, config) => {
|
|
|
471
471
|
fontSize: config.fontSize,
|
|
472
472
|
fontStyle: config.fontStyle,
|
|
473
473
|
fontVariant: config.fontVariant,
|
|
474
|
-
fontWeight: config.fontWeight
|
|
474
|
+
fontWeight: config.fontWeight,
|
|
475
|
+
fontColor: config.fontColor
|
|
475
476
|
});
|
|
476
477
|
const {
|
|
477
478
|
width: boxWidth,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(g,A){typeof exports=="object"&&typeof module!="undefined"?A(exports):typeof define=="function"&&define.amd?define(["exports"],A):(g=typeof globalThis!="undefined"?globalThis:g||self,A(g.textToCanvas={}))})(this,function(g){"use strict";const A="Arial",j="black",E=(t,n)=>Object.assign({},{fontFamily:A,fontSize:14,fontWeight:"400",fontStyle:"",fontVariant:"",fontColor:j},n,t),L=({fontFamily:t,fontSize:n,fontStyle:e,fontVariant:s,fontWeight:i})=>`${e||""} ${s||""} ${i||""} ${n!=null?n:14}px ${t||A}`.trim(),S=t=>!!t.match(/^\s+$/),U=t=>t.filter(n=>!S(n.text)),M=t=>{const n={...t};return t.format&&(n.format={...t.format}),n},k=(t,n)=>{if(t.length<=1||n.length<1)return[...t];const e=[];return t.forEach((s,i)=>{e.push(s),i<t.length-1&&n.forEach(o=>e.push(M(o)))}),e},V=({line:t,spaceWidth:n,spaceChar:e,boxWidth:s})=>{const i=U(t);if(i.length<=1)return t.concat();const o=i.reduce((a,u)=>{var p,T;return a+((T=(p=u.metrics)==null?void 0:p.width)!=null?T:0)},0),l=(s-o)/n;if(i.length>2){const a=Math.ceil(l/(i.length-1)),u=Array.from({length:a},()=>({text:e})),p=i.slice(0,i.length-1),T=k(p,u),c=u.slice(0,Math.floor(l)-(p.length-1)*u.length),h=i[i.length-1];return[...T,...c,h]}const r=Array.from({length:Math.floor(l)},()=>({text:e}));return k(i,r)},x=(t,n="both")=>{let e=0;if(n==="left"||n==="both"){for(;e<t.length&&S(t[e].text);e++);if(e>=t.length)return{trimmedLeft:t.concat(),trimmedRight:[],trimmedLine:[]}}let s=t.length;if(n==="right"||n==="both"){for(s--;s>=0&&S(t[s].text);s--);if(s++,s<=0)return{trimmedLeft:[],trimmedRight:t.concat(),trimmedLine:[]}}return{trimmedLeft:t.slice(0,e),trimmedRight:t.slice(s),trimmedLine:t.slice(e,s)}},D=" ",Z=" ";let F;const P=t=>`${t.text}${t.format?JSON.stringify(t.format):""}`,Y=(t,n=!0)=>{const e=[[]];let s=!1;return t.forEach((i,o)=>{var l,r,a;if(i.text.match(/^\n+$/)){for(let u=0;u<i.text.length;u++)e.push([]);s=!0;return}if(S(i.text)){(l=e.at(-1))==null||l.push(i),s=!0;return}i.text!==""&&(n&&!s&&o>0&&((r=e.at(-1))==null||r.push({text:Z})),(a=e.at(-1))==null||a.push(i),s=!1)}),e},z=({wrappedLines:t,wordMap:n,positioning:{width:e,height:s,x:i=0,y:o=0,align:l,vAlign:r}})=>{const a=i+e,u=o+s,p=f=>f.metrics.fontBoundingBoxAscent+f.metrics.fontBoundingBoxDescent,T=t.map(f=>f.reduce((y,b)=>Math.max(y,p(b)),0)),c=T.reduce((f,y)=>f+y,0);let h,m;return r==="top"?(m="top",h=o):r==="bottom"?(m="bottom",h=u-c):(m="top",h=o+s/2-c/2),{lines:t.map((f,y)=>{const b=f.reduce((B,
|
|
1
|
+
(function(g,A){typeof exports=="object"&&typeof module!="undefined"?A(exports):typeof define=="function"&&define.amd?define(["exports"],A):(g=typeof globalThis!="undefined"?globalThis:g||self,A(g.textToCanvas={}))})(this,function(g){"use strict";const A="Arial",j="black",E=(t,n)=>Object.assign({},{fontFamily:A,fontSize:14,fontWeight:"400",fontStyle:"",fontVariant:"",fontColor:j},n,t),L=({fontFamily:t,fontSize:n,fontStyle:e,fontVariant:s,fontWeight:i})=>`${e||""} ${s||""} ${i||""} ${n!=null?n:14}px ${t||A}`.trim(),S=t=>!!t.match(/^\s+$/),U=t=>t.filter(n=>!S(n.text)),M=t=>{const n={...t};return t.format&&(n.format={...t.format}),n},k=(t,n)=>{if(t.length<=1||n.length<1)return[...t];const e=[];return t.forEach((s,i)=>{e.push(s),i<t.length-1&&n.forEach(o=>e.push(M(o)))}),e},V=({line:t,spaceWidth:n,spaceChar:e,boxWidth:s})=>{const i=U(t);if(i.length<=1)return t.concat();const o=i.reduce((a,u)=>{var p,T;return a+((T=(p=u.metrics)==null?void 0:p.width)!=null?T:0)},0),l=(s-o)/n;if(i.length>2){const a=Math.ceil(l/(i.length-1)),u=Array.from({length:a},()=>({text:e})),p=i.slice(0,i.length-1),T=k(p,u),c=u.slice(0,Math.floor(l)-(p.length-1)*u.length),h=i[i.length-1];return[...T,...c,h]}const r=Array.from({length:Math.floor(l)},()=>({text:e}));return k(i,r)},x=(t,n="both")=>{let e=0;if(n==="left"||n==="both"){for(;e<t.length&&S(t[e].text);e++);if(e>=t.length)return{trimmedLeft:t.concat(),trimmedRight:[],trimmedLine:[]}}let s=t.length;if(n==="right"||n==="both"){for(s--;s>=0&&S(t[s].text);s--);if(s++,s<=0)return{trimmedLeft:[],trimmedRight:t.concat(),trimmedLine:[]}}return{trimmedLeft:t.slice(0,e),trimmedRight:t.slice(s),trimmedLine:t.slice(e,s)}},D=" ",Z=" ";let F;const P=t=>`${t.text}${t.format?JSON.stringify(t.format):""}`,Y=(t,n=!0)=>{const e=[[]];let s=!1;return t.forEach((i,o)=>{var l,r,a;if(i.text.match(/^\n+$/)){for(let u=0;u<i.text.length;u++)e.push([]);s=!0;return}if(S(i.text)){(l=e.at(-1))==null||l.push(i),s=!0;return}i.text!==""&&(n&&!s&&o>0&&((r=e.at(-1))==null||r.push({text:Z})),(a=e.at(-1))==null||a.push(i),s=!1)}),e},z=({wrappedLines:t,wordMap:n,positioning:{width:e,height:s,x:i=0,y:o=0,align:l,vAlign:r}})=>{const a=i+e,u=o+s,p=f=>f.metrics.fontBoundingBoxAscent+f.metrics.fontBoundingBoxDescent,T=t.map(f=>f.reduce((y,b)=>Math.max(y,p(b)),0)),c=T.reduce((f,y)=>f+y,0);let h,m;return r==="top"?(m="top",h=o):r==="bottom"?(m="bottom",h=u-c):(m="top",h=o+s/2-c/2),{lines:t.map((f,y)=>{const b=f.reduce((B,H)=>B+H.metrics.width,0),v=T[y];let W;l==="right"?W=a-b:l==="left"?W=i:W=i+e/2-b/2;let J=W;const tt=f.map(B=>{const H=P(B),{format:et}=n.get(H),nt=J,R=p(B);let _;return r==="top"?_=h:r==="bottom"?_=h+v:_=h+(v-R)/2,J+=B.metrics.width,{word:B,format:et,x:nt,y:_,width:B.metrics.width,height:R,isWhitespace:S(B.text)}});return h+=v,tt}),textBaseline:m,textAlign:"left",width:e,height:c}},$=function(t,n){if(t==="metrics"&&n&&typeof n=="object"){const e=n;return{width:e.width,fontBoundingBoxAscent:e.fontBoundingBoxAscent,fontBoundingBoxDescent:e.fontBoundingBoxDescent}}return n},X=t=>JSON.stringify(t,$),q=t=>JSON.stringify(t,$),I=({ctx:t,word:n,wordMap:e,baseTextFormat:s})=>{const i=P(n);if(n.metrics){if(!e.has(i)){let a;n.format&&(a=E(n.format,s)),e.set(i,{metrics:n.metrics,format:a})}return n.metrics.width}if(e.has(i)){const{metrics:a}=e.get(i);return n.metrics=a,a.width}let o=!1,l;n.format&&(t.save(),o=!0,l=E(n.format,s),t.font=L(l)),F||(o||(t.save(),o=!0),t.textBaseline="bottom");const r=t.measureText(n.text);return typeof r.fontBoundingBoxAscent=="number"?F=!0:(F=!1,r.fontBoundingBoxAscent=r.actualBoundingBoxAscent,r.fontBoundingBoxDescent=0),n.metrics=r,e.set(i,{metrics:r,format:l}),o&&t.restore(),r.width},O=({ctx:t,words:n,justify:e,format:s,inferWhitespace:i=!0,...o})=>{const l=new Map,r=E(s),{width:a}=o,u=(m,d=!1)=>{let f=0,y=0;return m.every((b,v)=>{const W=I({ctx:t,word:b,wordMap:l,baseTextFormat:r});return!d&&f+W>a?(v===0&&(y=1,f=W),!1):(y++,f+=W,!0)}),{lineWidth:f,splitPoint:y}};t.save();const p=Y(x(n).trimmedLine,i);if(p.length<=0||a<=0||o.height<=0||s&&typeof s.fontSize=="number"&&s.fontSize<=0)return{lines:[],textAlign:"center",textBaseline:"middle",width:o.width,height:0};t.font=L(r);const T=e?I({ctx:t,word:{text:D},wordMap:l,baseTextFormat:r}):0,c=[];for(const m of p){let{splitPoint:d}=u(m);if(d>=m.length)c.push(m);else{let f=m.concat();for(;d<f.length;){const y=x(f.slice(0,d),"right").trimmedLine;c.push(y),f=x(f.slice(d),"left").trimmedLine,{splitPoint:d}=u(f)}c.push(f)}}e&&c.length>1&&c.forEach((m,d)=>{if(d<c.length-1){const f=V({line:m,spaceWidth:T,spaceChar:D,boxWidth:a});u(f,!0),c[d]=f}});const h=z({wrappedLines:c,wordMap:l,positioning:o});return t.restore(),h},C=t=>{const n=[];let e,s=!1;return Array.from(t.trim()).forEach(i=>{const o=S(i);o&&!s||!o&&s?(s=o,e&&n.push(e),e={text:i}):(e||(e={text:""}),e.text+=i)}),e&&n.push(e),n},G=({text:t,...n})=>{const e=C(t);return O({...n,words:e,inferWhitespace:!1}).lines.map(i=>i.map(({word:{text:o}})=>o).join(""))},N=(t,n,e)=>{const s=t.textBaseline,i=t.font;t.textBaseline="bottom",e&&(t.font=e);const{actualBoundingBoxAscent:o}=t.measureText(n);return t.textBaseline=s,e&&(t.font=i),o},K=({ctx:t,word:n})=>N(t,n.text,n.format&&L(n.format)),Q=({ctx:t,text:n,style:e})=>N(t,n,e),w=(t,n,e)=>{const s=E({fontFamily:e.fontFamily,fontSize:e.fontSize,fontStyle:e.fontStyle,fontVariant:e.fontVariant,fontWeight:e.fontWeight,fontColor:e.fontColor}),{width:i,height:o,x:l=0,y:r=0}=e,{lines:a,height:u,textBaseline:p,textAlign:T}=O({ctx:t,words:Array.isArray(n)?n:C(n),inferWhitespace:Array.isArray(n)?e.inferWhitespace===void 0||e.inferWhitespace:void 0,x:l,y:r,width:e.width,height:e.height,align:e.align,vAlign:e.vAlign,justify:e.justify,format:s});if(t.save(),t.textAlign=T,t.textBaseline=p,t.font=L(s),t.fillStyle=s.fontColor||j,e.overflow===!1&&(t.beginPath(),t.rect(l,r,i,o),t.clip()),a.forEach(c=>{c.forEach(h=>{h.isWhitespace||(h.format&&(t.save(),t.font=L(h.format),h.format.fontColor&&(t.fillStyle=h.format.fontColor)),t.fillText(h.word.text,h.x,h.y),h.format&&t.restore())})}),e.debug){const c=l+i,h=r+o;let m;e.align==="right"?m=c:e.align==="left"?m=l:m=l+i/2;let d=r;e.vAlign==="bottom"?d=h:e.vAlign==="middle"&&(d=r+o/2);const f="#0C8CE9";t.lineWidth=1,t.strokeStyle=f,t.strokeRect(l,r,i,o),t.lineWidth=1,(!e.align||e.align==="center")&&(t.strokeStyle=f,t.beginPath(),t.moveTo(m,r),t.lineTo(m,h),t.stroke()),(!e.vAlign||e.vAlign==="middle")&&(t.strokeStyle=f,t.beginPath(),t.moveTo(l,d),t.lineTo(c,d),t.stroke())}return t.restore(),{height:u}};g.drawText=w,g.getTextFormat=E,g.getTextHeight=Q,g.getTextStyle=L,g.getWordHeight=K,g.specToJson=X,g.splitText=G,g.splitWords=O,g.textToWords=C,g.wordsToJson=q,Object.defineProperty(g,Symbol.toStringTag,{value:"Module"})});
|
|
2
2
|
//# sourceMappingURL=text-to-canvas.umd.min.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-to-canvas.umd.min.js","sources":["../src/lib/util/style.ts","../src/lib/util/whitespace.ts","../src/lib/util/justify.ts","../src/lib/util/trim.ts","../src/lib/util/split.ts","../src/lib/util/height.ts","../src/lib/index.ts"],"sourcesContent":["import { TextFormat } from '../model';\n\nexport const DEFAULT_FONT_FAMILY = 'Arial';\nexport const DEFAULT_FONT_SIZE = 14;\nexport const DEFAULT_FONT_COLOR = 'black';\n\n/**\n * Generates a text format based on defaults and any provided overrides.\n * @param format Overrides to `baseFormat` and default format.\n * @param baseFormat Overrides to default format.\n * @returns Full text format (all properties specified).\n */\nexport const getTextFormat = (\n format?: TextFormat,\n baseFormat?: TextFormat\n): Required<TextFormat> => {\n return Object.assign(\n {},\n {\n fontFamily: DEFAULT_FONT_FAMILY,\n fontSize: DEFAULT_FONT_SIZE,\n fontWeight: '400',\n fontStyle: '',\n fontVariant: '',\n fontColor: DEFAULT_FONT_COLOR,\n },\n baseFormat,\n format\n );\n};\n\n/**\n * Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.\n * @param format\n * @returns Style string to set on context's `font` property. Note this __does not include\n * the font color__ as that is not part of the CSS font value. Color must be handled separately.\n */\nexport const getTextStyle = ({\n fontFamily,\n fontSize,\n fontStyle,\n fontVariant,\n fontWeight,\n}: TextFormat) => {\n // per spec:\n // - font-style, font-variant and font-weight must precede font-size\n // - font-family must be the last value specified\n // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font\n return `${fontStyle || ''} ${fontVariant || ''} ${\n fontWeight || ''\n } ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();\n};\n","/**\n * Determines if a string is only whitespace (one or more characters of it).\n * @param text\n * @returns True if `text` is one or more characters of whitespace, only.\n */\nexport const isWhitespace = (text: string) => {\n return !!text.match(/^\\s+$/);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * @private\n * Extracts the __visible__ (i.e. non-whitespace) words from a line.\n * @param line\n * @returns New array with only non-whitespace words.\n */\nconst _extractWords = (line: Word[]) => {\n return line.filter((word) => !isWhitespace(word.text));\n};\n\n/**\n * @private\n * Deep-clones a Word.\n * @param word\n * @returns Deep-cloned Word.\n */\nconst _cloneWord = (word: Word) => {\n const clone = { ...word };\n if (word.format) {\n clone.format = { ...word.format };\n }\n return clone;\n};\n\n/**\n * @private\n * Joins Words together using another set of Words.\n * @param words Words to join.\n * @param joiner Words to use when joining `words`. These will be deep-cloned and inserted\n * in between every word in `words`, similar to `Array.join(string)` where the `string`\n * is inserted in between every element.\n * @returns New array of Words. Empty if `words` is empty. New array of one Word if `words`\n * contains only one Word.\n */\nconst _joinWords = (words: Word[], joiner: Word[]) => {\n if (words.length <= 1 || joiner.length < 1) {\n return [...words];\n }\n\n const phrase: Word[] = [];\n words.forEach((word, wordIdx) => {\n phrase.push(word);\n if (wordIdx < words.length - 1) {\n // don't append after last `word`\n joiner.forEach((jw) => phrase.push(_cloneWord(jw)));\n }\n });\n\n return phrase;\n};\n\n/**\n * Inserts spaces between words in a line in order to raise the line width to the box width.\n * The spaces are evenly spread in the line, and extra spaces (if any) are only inserted\n * between words, not at either end of the `line`.\n *\n * @returns New array containing original words from the `line` with additional whitespace\n * for justification to `boxWidth`.\n */\nexport const justifyLine = ({\n line,\n spaceWidth,\n spaceChar,\n boxWidth,\n}: {\n /** Assumed to have already been trimmed on both ends. */\n line: Word[];\n /** Width (px) of `spaceChar`. */\n spaceWidth: number;\n /**\n * Character used as a whitespace in justification. Will be injected in between Words in\n * `line` in order to justify the text on the line within `lineWidth`.\n */\n spaceChar: string;\n /** Width (px) of the box containing the text (i.e. max `line` width). */\n boxWidth: number;\n}) => {\n const words = _extractWords(line);\n if (words.length <= 1) {\n return line.concat();\n }\n\n const wordsWidth = words.reduce(\n (width, word) => width + (word.metrics?.width ?? 0),\n 0\n );\n const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;\n\n if (words.length > 2) {\n // use CEILING so we spread the partial spaces throughout except between the second-last\n // and last word so that the spacing is more even and as tight as we can get it to\n // the `boxWidth`\n const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));\n const spaces: Word[] = Array.from({ length: spacesPerWord }, () => ({\n text: spaceChar,\n }));\n const firstWords = words.slice(0, words.length - 1); // all but last word\n const firstPart = _joinWords(firstWords, spaces);\n const remainingSpaces = spaces.slice(\n 0,\n Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces.length\n );\n const lastWord = words[words.length - 1];\n return [...firstPart, ...remainingSpaces, lastWord];\n }\n // only 2 words so fill with spaces in between them: use FLOOR to make sure we don't\n // go past `boxWidth`\n const spaces: Word[] = Array.from(\n { length: Math.floor(noOfSpacesToInsert) },\n () => ({ text: spaceChar })\n );\n return _joinWords(words, spaces);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * Trims whitespace from the beginning and end of a `line`.\n * @param line\n * @param side Which side to trim.\n * @returns An object containing trimmed characters, and the new trimmed line.\n */\nexport const trimLine = (\n line: Word[],\n side: 'left' | 'right' | 'both' = 'both'\n): {\n /**\n * New array containing what was trimmed from the left (empty if none).\n */\n trimmedLeft: Word[];\n /**\n * New array containing what was trimmed from the right (empty if none).\n */\n trimmedRight: Word[];\n /**\n * New array representing the trimmed line, even if nothing gets trimmed. Empty array if\n * all whitespace.\n */\n trimmedLine: Word[];\n} => {\n let leftTrim = 0;\n if (side === 'left' || side === 'both') {\n for (; leftTrim < line.length; leftTrim++) {\n if (!isWhitespace(line[leftTrim].text)) {\n break;\n }\n }\n\n if (leftTrim >= line.length) {\n // all whitespace\n return {\n trimmedLeft: line.concat(),\n trimmedRight: [],\n trimmedLine: [],\n };\n }\n }\n\n let rightTrim = line.length;\n if (side === 'right' || side === 'both') {\n rightTrim--;\n for (; rightTrim >= 0; rightTrim--) {\n if (!isWhitespace(line[rightTrim].text)) {\n break;\n }\n }\n rightTrim++; // back up one since we started one down for 0-based indexes\n\n if (rightTrim <= 0) {\n // all whitespace\n return {\n trimmedLeft: [],\n trimmedRight: line.concat(),\n trimmedLine: [],\n };\n }\n }\n\n return {\n trimmedLeft: line.slice(0, leftTrim),\n trimmedRight: line.slice(rightTrim),\n trimmedLine: line.slice(leftTrim, rightTrim),\n };\n};\n","import { getTextFormat, getTextStyle } from './style';\nimport { isWhitespace } from './whitespace';\nimport { justifyLine } from './justify';\nimport {\n PositionedWord,\n SplitTextProps,\n SplitWordsProps,\n RenderSpec,\n Word,\n WordMap,\n CanvasTextMetrics,\n TextFormat,\n CanvasRenderContext,\n} from '../model';\nimport { trimLine } from './trim';\n\n// Hair space character for precise justification\nconst HAIR = '\\u{200a}';\n\n// for when we're inferring whitespace between words\nconst SPACE = ' ';\n\n/**\n * Whether the canvas API being used supports the newer `fontBoundingBox*` properties or not.\n *\n * True if it does, false if not; undefined until we determine either way.\n *\n * Note about `fontBoundingBoxAscent/Descent`: Only later browsers support this and the Node-based\n * `canvas` package does not. Having these properties will have a noticeable increase in performance\n * on large pieces of text to render. Failing these, a fallback is used which involves\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics\n * @see https://www.npmjs.com/package/canvas\n */\nlet fontBoundingBoxSupported: boolean;\n\n/**\n * @private\n * Generates a word hash for use as a key in a `WordMap`.\n * @param word\n * @returns Hash.\n */\nconst _getWordHash = (word: Word) => {\n return `${word.text}${word.format ? JSON.stringify(word.format) : ''}`;\n};\n\n/**\n * @private\n * Splits words into lines based on words that are single newline characters.\n * @param words\n * @param inferWhitespace True (default) if whitespace should be inferred (and injected)\n * based on words; false if we're to assume the words already include all necessary whitespace.\n * @returns Words expressed as lines.\n */\nconst _splitIntoLines = (\n words: Word[],\n inferWhitespace: boolean = true\n): Word[][] => {\n const lines: Word[][] = [[]];\n\n let wasWhitespace = false; // true if previous word was whitespace\n words.forEach((word, wordIdx) => {\n // TODO: this is likely a naive split (at least based on character?); should at least\n // think about this more; text format shouldn't matter on a line break, right (hope not)?\n if (word.text.match(/^\\n+$/)) {\n for (let i = 0; i < word.text.length; i++) {\n lines.push([]);\n }\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (isWhitespace(word.text)) {\n // whitespace OTHER THAN newlines since we checked for newlines above\n lines.at(-1)?.push(word);\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (word.text === '') {\n return; // skip to next `word`\n }\n\n // looks like a non-empty, non-whitespace word at this point, so if it isn't the first\n // word and the one before wasn't whitespace, insert a space\n if (inferWhitespace && !wasWhitespace && wordIdx > 0) {\n lines.at(-1)?.push({ text: SPACE });\n }\n\n lines.at(-1)?.push(word);\n wasWhitespace = false;\n });\n\n return lines;\n};\n\n/**\n * @private\n * Helper for `splitWords()` that takes the words that have been wrapped into lines and\n * determines their positions on canvas for future rendering based on alignment settings.\n * @param params\n * @returns Results to return via `splitWords()`\n */\nconst _generateSpec = ({\n wrappedLines,\n wordMap,\n positioning: {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n align,\n vAlign,\n },\n}: {\n /** Words organized/wrapped into lines to be rendered. */\n wrappedLines: Word[][];\n\n /** Map of Word to measured dimensions (px) as it would be rendered. */\n wordMap: WordMap;\n\n /**\n * Details on where to render the Words onto canvas. These parameters ultimately come\n * from `SplitWordsProps`, and they come from `DrawTextConfig`.\n */\n positioning: {\n width: SplitWordsProps['width'];\n // NOTE: height does NOT constrain the text; used only for vertical alignment\n height: SplitWordsProps['height'];\n x: SplitWordsProps['x'];\n y: SplitWordsProps['y'];\n align?: SplitWordsProps['align'];\n vAlign?: SplitWordsProps['vAlign'];\n };\n}): RenderSpec => {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n // NOTE: using __font__ ascent/descent to account for all possible characters in the font\n // so that lines with ascenders but no descenders, or vice versa, are all properly\n // aligned to the baseline, and so that lines aren't scrunched\n // NOTE: even for middle vertical alignment, we want to use the __font__ ascent/descent\n // so that words, per line, are still aligned to the baseline (as much as possible; if\n // each word has a different font size, then things will still be offset, but for the\n // same font size, the baseline should match from left to right)\n const getHeight = (word: Word): number =>\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n word.metrics!.fontBoundingBoxAscent + word.metrics!.fontBoundingBoxDescent;\n\n // max height per line\n const lineHeights = wrappedLines.map((line) =>\n line.reduce((acc, word) => {\n return Math.max(acc, getHeight(word));\n }, 0)\n );\n const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);\n\n // vertical alignment (defaults to middle)\n let lineY: number;\n let textBaseline: CanvasTextBaseline;\n if (vAlign === 'top') {\n textBaseline = 'top';\n lineY = boxY;\n } else if (vAlign === 'bottom') {\n textBaseline = 'bottom';\n lineY = yEnd - totalHeight;\n } else {\n // middle\n textBaseline = 'top'; // YES, using 'top' baseline for 'middle' v-align\n lineY = boxY + boxHeight / 2 - totalHeight / 2;\n }\n\n const lines = wrappedLines.map((line, lineIdx): PositionedWord[] => {\n const lineWidth = line.reduce(\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n (acc, word) => acc + word.metrics!.width,\n 0\n );\n const lineHeight = lineHeights[lineIdx];\n\n // horizontal alignment (defaults to center)\n let lineX: number;\n if (align === 'right') {\n lineX = xEnd - lineWidth;\n } else if (align === 'left') {\n lineX = boxX;\n } else {\n // center\n lineX = boxX + boxWidth / 2 - lineWidth / 2;\n }\n\n let wordX = lineX;\n const posWords = line.map((word): PositionedWord => {\n // NOTE: `word.metrics` and `wordMap.get(hash)` must exist as every `word` MUST have\n // been measured at this point\n\n const hash = _getWordHash(word);\n const { format } = wordMap.get(hash)!;\n const x = wordX;\n const height = getHeight(word);\n\n // vertical alignment (defaults to middle)\n let y: number;\n if (vAlign === 'top') {\n y = lineY;\n } else if (vAlign === 'bottom') {\n y = lineY + lineHeight;\n } else {\n // middle\n y = lineY + (lineHeight - height) / 2;\n }\n\n wordX += word.metrics!.width;\n return {\n word,\n format, // undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)\n x,\n y,\n width: word.metrics!.width,\n height,\n isWhitespace: isWhitespace(word.text),\n };\n });\n\n lineY += lineHeight;\n return posWords;\n });\n\n return {\n lines,\n textBaseline,\n textAlign: 'left', // always per current algorithm\n width: boxWidth,\n height: totalHeight,\n };\n};\n\n/**\n * @private\n * Replacer for use with `JSON.stringify()` to deal with `TextMetrics` objects which\n * only have getters/setters instead of value-based properties.\n * @param key Key being processed in `this`.\n * @param value Value of `key` in `this`.\n * @returns Processed value to be serialized, or `undefined` to omit the `key` from the\n * serialized object.\n */\n// CAREFUL: use a `function`, not an arrow function, as stringify() sets its context to\n// the object being serialized on each call to the replacer\nconst _jsonReplacer = function (key: string, value: unknown) {\n if (key === 'metrics' && value && typeof value === 'object') {\n // TODO: need better typings here, if possible, so that TSC warns if we aren't\n // including a property we should be if a new one is needed in the future (i.e. if\n // a new property is added to the `TextMetricsLike` type)\n // NOTE: TextMetrics objects don't have own-enumerable properties; they only have getters,\n // so we have to explicitly get the values we care about instead of spreading them into\n // the new object\n const metrics: CanvasTextMetrics = value as CanvasTextMetrics;\n return {\n width: metrics.width,\n fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,\n fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,\n };\n }\n\n return value;\n};\n\n/**\n * Serializes render specs to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param specs\n * @returns Specs serialized as JSON.\n */\nexport const specToJson = (specs: RenderSpec): string => {\n return JSON.stringify(specs, _jsonReplacer);\n};\n\n/**\n * Serializes a list of Words to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param words\n * @returns Words serialized as JSON.\n */\nexport const wordsToJson = (words: Word[]): string => {\n return JSON.stringify(words, _jsonReplacer);\n};\n\n/**\n * @private\n * Measures a Word in a rendering context, assigning its `TextMetrics` to its `metrics` property.\n * @returns The Word's width, in pixels.\n */\nconst _measureWord = ({\n ctx,\n word,\n wordMap,\n baseTextFormat,\n}: {\n ctx: CanvasRenderContext;\n word: Word;\n wordMap: WordMap;\n baseTextFormat: TextFormat;\n}): number => {\n const hash = _getWordHash(word);\n\n if (word.metrics) {\n // assume Word's text and format haven't changed since last measurement and metrics are good\n\n // make sure we have the metrics and full formatting cached for other identical Words\n if (!wordMap.has(hash)) {\n let format = undefined;\n if (word.format) {\n format = getTextFormat(word.format, baseTextFormat);\n }\n wordMap.set(hash, { metrics: word.metrics, format });\n }\n\n return word.metrics.width;\n }\n\n // check to see if we have already measured an identical Word\n if (wordMap.has(hash)) {\n const { metrics } = wordMap.get(hash)!; // will be there because of `if(has())` check\n word.metrics = metrics;\n return metrics.width;\n }\n\n let ctxSaved = false;\n\n let format = undefined;\n if (word.format) {\n ctx.save();\n ctxSaved = true;\n format = getTextFormat(word.format, baseTextFormat);\n ctx.font = getTextStyle(format); // `fontColor` is ignored as it has no effect on metrics\n }\n\n if (!fontBoundingBoxSupported) {\n // use fallback which comes close enough and still gives us properly-aligned text, albeit\n // lines are a couple pixels tighter together\n if (!ctxSaved) {\n ctx.save();\n ctxSaved = true;\n }\n ctx.textBaseline = 'bottom';\n }\n\n const metrics = ctx.measureText(word.text);\n if (typeof metrics.fontBoundingBoxAscent === 'number') {\n fontBoundingBoxSupported = true;\n } else {\n fontBoundingBoxSupported = false;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxDescent = 0;\n }\n\n word.metrics = metrics;\n wordMap.set(hash, { metrics, format });\n\n if (ctxSaved) {\n ctx.restore();\n }\n\n return metrics.width;\n};\n\n/**\n * Splits Words into positioned lines of Words as they need to be rendred in 2D space,\n * but does not render anything.\n * @param config\n * @returns Lines of positioned words to be rendered, and total height required to\n * render all lines.\n */\nexport const splitWords = ({\n ctx,\n words,\n justify,\n format: baseFormat,\n inferWhitespace = true,\n ...positioning // rest of params are related to positioning\n}: SplitWordsProps): RenderSpec => {\n const wordMap: WordMap = new Map();\n const baseTextFormat = getTextFormat(baseFormat);\n const { width: boxWidth } = positioning;\n\n //// text measurement\n\n // measures an entire line's width up to the `boxWidth` as a max, unless `force=true`,\n // in which case the entire line is measured regardless of `boxWidth`.\n //\n // - Returned `lineWidth` is width up to, but not including, the `splitPoint` (always <= `boxWidth`\n // unless the first Word is too wide to fit, in which case `lineWidth` will be that Word's\n // width even though it's > `boxWidth`).\n // - If `force=true`, will be the full width of the line regardless of `boxWidth`.\n // - Returned `splitPoint` is index into `words` of the Word immediately FOLLOWING the last\n // Word included in the `lineWidth` (and is `words.length` if all Words were included);\n // `splitPoint` could also be thought of as the number of `words` included in the `lineWidth`.\n // - If `force=true`, will always be `words.length`.\n const measureLine = (\n lineWords: Word[],\n force: boolean = false\n ): {\n lineWidth: number;\n splitPoint: number;\n } => {\n let lineWidth = 0;\n let splitPoint = 0;\n lineWords.every((word, idx) => {\n const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });\n if (!force && lineWidth + wordWidth > boxWidth) {\n // at minimum, MUST include at least first Word, even if it's wider than box width\n if (idx === 0) {\n splitPoint = 1;\n lineWidth = wordWidth;\n }\n // else, `lineWidth` already includes at least one Word so this current Word will\n // be the `splitPoint` such that `lineWidth` remains < `boxWidth`\n\n return false; // break\n }\n\n splitPoint++;\n lineWidth += wordWidth;\n return true; // next\n });\n\n return { lineWidth, splitPoint };\n };\n\n //// main\n\n ctx.save();\n\n // start by trimming the `words` to remove any whitespace at either end, then split the `words`\n // into an initial set of lines dictated by explicit hard breaks, if any (if none, we'll have\n // one super long line)\n const hardLines = _splitIntoLines(\n trimLine(words).trimmedLine,\n inferWhitespace\n );\n\n if (\n hardLines.length <= 0 ||\n boxWidth <= 0 ||\n positioning.height <= 0 ||\n (baseFormat &&\n typeof baseFormat.fontSize === 'number' &&\n baseFormat.fontSize <= 0)\n ) {\n // width or height or font size cannot be 0, or there are no lines after trimming\n return {\n lines: [],\n textAlign: 'center',\n textBaseline: 'middle',\n width: positioning.width,\n height: 0,\n };\n }\n\n ctx.font = getTextStyle(baseTextFormat);\n\n const hairWidth = justify\n ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat })\n : 0;\n const wrappedLines: Word[][] = [];\n\n // now further wrap every hard line to make sure it fits within the `boxWidth`, down to a\n // MINIMUM of 1 Word per line\n for (const hardLine of hardLines) {\n let { splitPoint } = measureLine(hardLine);\n\n // if the line fits, we're done; else, we have to break it down further to fit\n // as best as we can (i.e. MIN one word per line, no breaks within words, no\n // leading/pending whitespace)\n if (splitPoint >= hardLine.length) {\n wrappedLines.push(hardLine);\n } else {\n // shallow clone because we're going to break this line down further to get the best fit\n let softLine = hardLine.concat();\n while (splitPoint < softLine.length) {\n // right-trim what we split off in case we split just after some whitespace\n const splitLine = trimLine(\n softLine.slice(0, splitPoint),\n 'right'\n ).trimmedLine;\n wrappedLines.push(splitLine);\n\n // left-trim what remains in case we split just before some whitespace\n softLine = trimLine(softLine.slice(splitPoint), 'left').trimmedLine;\n ({ splitPoint } = measureLine(softLine));\n }\n\n // get the last bit of the `softLine`\n // NOTE: since we started by timming the entire line, and we just left-trimmed\n // what remained of `softLine`, there should be no need to trim again\n wrappedLines.push(softLine);\n }\n }\n\n // never justify a single line because there's no other line to visually justify it to\n if (justify && wrappedLines.length > 1) {\n wrappedLines.forEach((wrappedLine, idx) => {\n // never justify the last line (common in text editors)\n if (idx < wrappedLines.length - 1) {\n const justifiedLine = justifyLine({\n line: wrappedLine,\n spaceWidth: hairWidth,\n spaceChar: HAIR,\n boxWidth,\n });\n\n // make sure any new Words used for justification get measured so we're able to\n // position them later when we generate the render spec\n measureLine(justifiedLine, true);\n wrappedLines[idx] = justifiedLine;\n }\n });\n }\n\n const spec = _generateSpec({\n wrappedLines,\n wordMap,\n positioning,\n });\n\n ctx.restore();\n return spec;\n};\n\n/**\n * Converts a string of text containing words and whitespace, as well as line breaks (newlines),\n * into a `Word[]` that can be given to `splitWords()`.\n * @param text String to convert into Words.\n * @returns Converted text.\n */\nexport const textToWords = (text: string) => {\n const words: Word[] = [];\n\n // split the `text` into a series of Words, preserving whitespace\n let word: Word | undefined = undefined;\n let wasWhitespace = false;\n Array.from(text.trim()).forEach((c) => {\n const charIsWhitespace = isWhitespace(c);\n if (\n (charIsWhitespace && !wasWhitespace) ||\n (!charIsWhitespace && wasWhitespace)\n ) {\n // save current `word`, if any, and start new `word`\n wasWhitespace = charIsWhitespace;\n if (word) {\n words.push(word);\n }\n word = { text: c };\n } else {\n // accumulate into current `word`\n if (!word) {\n word = { text: '' };\n }\n word.text += c;\n }\n });\n\n // make sure we have the last word! ;)\n if (word) {\n words.push(word);\n }\n\n return words;\n};\n\n/**\n * Splits plain text into lines in the order in which they should be rendered, top-down,\n * preserving whitespace __only within the text__ (whitespace on either end is trimmed).\n */\nexport const splitText = ({ text, ...params }: SplitTextProps): string[] => {\n const words = textToWords(text);\n\n const results = splitWords({\n ...params,\n words,\n inferWhitespace: false,\n });\n\n return results.lines.map((line) =>\n line.map(({ word: { text: t } }) => t).join('')\n );\n};\n","import { getTextStyle } from './style';\nimport { CanvasRenderContext, Word } from '../model';\n\n/** @private */\nconst _getHeight = (ctx: CanvasRenderContext, text: string, style?: string) => {\n const previousTextBaseline = ctx.textBaseline;\n const previousFont = ctx.font;\n\n ctx.textBaseline = 'bottom';\n if (style) {\n ctx.font = style;\n }\n const { actualBoundingBoxAscent: height } = ctx.measureText(text);\n\n // Reset baseline\n ctx.textBaseline = previousTextBaseline;\n if (style) {\n ctx.font = previousFont;\n }\n\n return height;\n};\n\n/**\n * Gets the measured height of a given `Word` using its text style.\n * @returns {number} Height in pixels.\n */\nexport const getWordHeight = ({\n ctx,\n word,\n}: {\n ctx: CanvasRenderContext;\n /**\n * Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.\n */\n word: Word;\n}) => {\n return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));\n};\n\n/**\n * Gets the measured height of a given `string` using a given text style.\n * @returns {number} Height in pixels.\n */\nexport const getTextHeight = ({\n ctx,\n text,\n style,\n}: {\n ctx: CanvasRenderContext;\n text: string;\n /**\n * CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font\n * settings/styles are used.\n */\n style?: string;\n}) => {\n return _getHeight(ctx, text, style);\n};\n","import {\n specToJson,\n splitWords,\n splitText,\n textToWords,\n wordsToJson,\n} from './util/split';\nimport { getTextHeight, getWordHeight } from './util/height';\nimport { getTextStyle, getTextFormat, DEFAULT_FONT_COLOR } from './util/style';\nimport { CanvasRenderContext, DrawTextConfig, Text } from './model';\n\nconst drawText = (\n ctx: CanvasRenderContext,\n text: Text,\n config: DrawTextConfig\n) => {\n const baseFormat = getTextFormat({\n fontFamily: config.fontFamily,\n fontSize: config.fontSize,\n fontStyle: config.fontStyle,\n fontVariant: config.fontVariant,\n fontWeight: config.fontWeight,\n });\n\n const {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n } = config;\n\n const {\n lines: richLines,\n height: totalHeight,\n textBaseline,\n textAlign,\n } = splitWords({\n ctx,\n words: Array.isArray(text) ? text : textToWords(text),\n inferWhitespace: Array.isArray(text)\n ? config.inferWhitespace === undefined || config.inferWhitespace\n : undefined, // ignore since `text` is a string; we assume it already has all the whitespace it needs\n x: boxX,\n y: boxY,\n width: config.width,\n height: config.height,\n align: config.align,\n vAlign: config.vAlign,\n justify: config.justify,\n format: baseFormat,\n });\n\n ctx.save();\n ctx.textAlign = textAlign;\n ctx.textBaseline = textBaseline;\n ctx.font = getTextStyle(baseFormat);\n ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;\n\n if (config.overflow === false) {\n ctx.beginPath();\n ctx.rect(boxX, boxY, boxWidth, boxHeight);\n ctx.clip(); // part of saved context state\n }\n\n richLines.forEach((line) => {\n line.forEach((pw) => {\n if (!pw.isWhitespace) {\n // NOTE: don't use the `pw.word.format` as this could be incomplete; use `pw.format`\n // if it exists as this will always be the __full__ TextFormat used to measure the\n // Word, and so should be what is used to render it\n if (pw.format) {\n ctx.save();\n ctx.font = getTextStyle(pw.format);\n if (pw.format.fontColor) {\n ctx.fillStyle = pw.format.fontColor;\n }\n }\n ctx.fillText(pw.word.text, pw.x, pw.y);\n if (pw.format) {\n ctx.restore();\n }\n }\n });\n });\n\n if (config.debug) {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n let textAnchor: number;\n if (config.align === 'right') {\n textAnchor = xEnd;\n } else if (config.align === 'left') {\n textAnchor = boxX;\n } else {\n textAnchor = boxX + boxWidth / 2;\n }\n\n let debugY = boxY;\n if (config.vAlign === 'bottom') {\n debugY = yEnd;\n } else if (config.vAlign === 'middle') {\n debugY = boxY + boxHeight / 2;\n }\n\n const debugColor = '#0C8CE9';\n\n // Text box\n ctx.lineWidth = 1;\n ctx.strokeStyle = debugColor;\n ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);\n\n ctx.lineWidth = 1;\n\n if (!config.align || config.align === 'center') {\n // Horizontal Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(textAnchor, boxY);\n ctx.lineTo(textAnchor, yEnd);\n ctx.stroke();\n }\n\n if (!config.vAlign || config.vAlign === 'middle') {\n // Vertical Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(boxX, debugY);\n ctx.lineTo(xEnd, debugY);\n ctx.stroke();\n }\n }\n\n ctx.restore();\n\n return { height: totalHeight };\n};\n\nexport {\n drawText,\n specToJson,\n splitText,\n splitWords,\n textToWords,\n wordsToJson,\n getTextHeight,\n getWordHeight,\n getTextStyle,\n getTextFormat,\n};\nexport * from './model';\n"],"names":["DEFAULT_FONT_FAMILY","DEFAULT_FONT_COLOR","getTextFormat","format","baseFormat","getTextStyle","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","isWhitespace","text","_extractWords","line","word","_cloneWord","clone","_joinWords","words","joiner","phrase","wordIdx","jw","justifyLine","spaceWidth","spaceChar","boxWidth","wordsWidth","width","_b","_a","noOfSpacesToInsert","spacesPerWord","spaces","firstWords","firstPart","remainingSpaces","lastWord","trimLine","side","leftTrim","rightTrim","HAIR","SPACE","fontBoundingBoxSupported","_getWordHash","_splitIntoLines","inferWhitespace","lines","wasWhitespace","i","_c","_generateSpec","wrappedLines","wordMap","boxHeight","boxX","boxY","align","vAlign","xEnd","yEnd","getHeight","lineHeights","acc","totalHeight","h","lineY","textBaseline","lineIdx","lineWidth","lineHeight","lineX","wordX","posWords","hash","x","height","y","_jsonReplacer","key","value","metrics","specToJson","specs","wordsToJson","_measureWord","ctx","baseTextFormat","ctxSaved","splitWords","justify","positioning","measureLine","lineWords","force","splitPoint","idx","wordWidth","hardLines","hairWidth","hardLine","softLine","splitLine","wrappedLine","justifiedLine","spec","textToWords","c","charIsWhitespace","splitText","params","t","_getHeight","style","previousTextBaseline","previousFont","getWordHeight","getTextHeight","drawText","config","richLines","textAlign","pw","textAnchor","debugY","debugColor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"text-to-canvas.umd.min.js","sources":["../src/lib/util/style.ts","../src/lib/util/whitespace.ts","../src/lib/util/justify.ts","../src/lib/util/trim.ts","../src/lib/util/split.ts","../src/lib/util/height.ts","../src/lib/index.ts"],"sourcesContent":["import { TextFormat } from '../model';\n\nexport const DEFAULT_FONT_FAMILY = 'Arial';\nexport const DEFAULT_FONT_SIZE = 14;\nexport const DEFAULT_FONT_COLOR = 'black';\n\n/**\n * Generates a text format based on defaults and any provided overrides.\n * @param format Overrides to `baseFormat` and default format.\n * @param baseFormat Overrides to default format.\n * @returns Full text format (all properties specified).\n */\nexport const getTextFormat = (\n format?: TextFormat,\n baseFormat?: TextFormat\n): Required<TextFormat> => {\n return Object.assign(\n {},\n {\n fontFamily: DEFAULT_FONT_FAMILY,\n fontSize: DEFAULT_FONT_SIZE,\n fontWeight: '400',\n fontStyle: '',\n fontVariant: '',\n fontColor: DEFAULT_FONT_COLOR,\n },\n baseFormat,\n format\n );\n};\n\n/**\n * Generates a [CSS font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) value.\n * @param format\n * @returns Style string to set on context's `font` property. Note this __does not include\n * the font color__ as that is not part of the CSS font value. Color must be handled separately.\n */\nexport const getTextStyle = ({\n fontFamily,\n fontSize,\n fontStyle,\n fontVariant,\n fontWeight,\n}: TextFormat) => {\n // per spec:\n // - font-style, font-variant and font-weight must precede font-size\n // - font-family must be the last value specified\n // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font\n return `${fontStyle || ''} ${fontVariant || ''} ${\n fontWeight || ''\n } ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();\n};\n","/**\n * Determines if a string is only whitespace (one or more characters of it).\n * @param text\n * @returns True if `text` is one or more characters of whitespace, only.\n */\nexport const isWhitespace = (text: string) => {\n return !!text.match(/^\\s+$/);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * @private\n * Extracts the __visible__ (i.e. non-whitespace) words from a line.\n * @param line\n * @returns New array with only non-whitespace words.\n */\nconst _extractWords = (line: Word[]) => {\n return line.filter((word) => !isWhitespace(word.text));\n};\n\n/**\n * @private\n * Deep-clones a Word.\n * @param word\n * @returns Deep-cloned Word.\n */\nconst _cloneWord = (word: Word) => {\n const clone = { ...word };\n if (word.format) {\n clone.format = { ...word.format };\n }\n return clone;\n};\n\n/**\n * @private\n * Joins Words together using another set of Words.\n * @param words Words to join.\n * @param joiner Words to use when joining `words`. These will be deep-cloned and inserted\n * in between every word in `words`, similar to `Array.join(string)` where the `string`\n * is inserted in between every element.\n * @returns New array of Words. Empty if `words` is empty. New array of one Word if `words`\n * contains only one Word.\n */\nconst _joinWords = (words: Word[], joiner: Word[]) => {\n if (words.length <= 1 || joiner.length < 1) {\n return [...words];\n }\n\n const phrase: Word[] = [];\n words.forEach((word, wordIdx) => {\n phrase.push(word);\n if (wordIdx < words.length - 1) {\n // don't append after last `word`\n joiner.forEach((jw) => phrase.push(_cloneWord(jw)));\n }\n });\n\n return phrase;\n};\n\n/**\n * Inserts spaces between words in a line in order to raise the line width to the box width.\n * The spaces are evenly spread in the line, and extra spaces (if any) are only inserted\n * between words, not at either end of the `line`.\n *\n * @returns New array containing original words from the `line` with additional whitespace\n * for justification to `boxWidth`.\n */\nexport const justifyLine = ({\n line,\n spaceWidth,\n spaceChar,\n boxWidth,\n}: {\n /** Assumed to have already been trimmed on both ends. */\n line: Word[];\n /** Width (px) of `spaceChar`. */\n spaceWidth: number;\n /**\n * Character used as a whitespace in justification. Will be injected in between Words in\n * `line` in order to justify the text on the line within `lineWidth`.\n */\n spaceChar: string;\n /** Width (px) of the box containing the text (i.e. max `line` width). */\n boxWidth: number;\n}) => {\n const words = _extractWords(line);\n if (words.length <= 1) {\n return line.concat();\n }\n\n const wordsWidth = words.reduce(\n (width, word) => width + (word.metrics?.width ?? 0),\n 0\n );\n const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;\n\n if (words.length > 2) {\n // use CEILING so we spread the partial spaces throughout except between the second-last\n // and last word so that the spacing is more even and as tight as we can get it to\n // the `boxWidth`\n const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));\n const spaces: Word[] = Array.from({ length: spacesPerWord }, () => ({\n text: spaceChar,\n }));\n const firstWords = words.slice(0, words.length - 1); // all but last word\n const firstPart = _joinWords(firstWords, spaces);\n const remainingSpaces = spaces.slice(\n 0,\n Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces.length\n );\n const lastWord = words[words.length - 1];\n return [...firstPart, ...remainingSpaces, lastWord];\n }\n // only 2 words so fill with spaces in between them: use FLOOR to make sure we don't\n // go past `boxWidth`\n const spaces: Word[] = Array.from(\n { length: Math.floor(noOfSpacesToInsert) },\n () => ({ text: spaceChar })\n );\n return _joinWords(words, spaces);\n};\n","import { isWhitespace } from './whitespace';\nimport { Word } from '../model';\n\n/**\n * Trims whitespace from the beginning and end of a `line`.\n * @param line\n * @param side Which side to trim.\n * @returns An object containing trimmed characters, and the new trimmed line.\n */\nexport const trimLine = (\n line: Word[],\n side: 'left' | 'right' | 'both' = 'both'\n): {\n /**\n * New array containing what was trimmed from the left (empty if none).\n */\n trimmedLeft: Word[];\n /**\n * New array containing what was trimmed from the right (empty if none).\n */\n trimmedRight: Word[];\n /**\n * New array representing the trimmed line, even if nothing gets trimmed. Empty array if\n * all whitespace.\n */\n trimmedLine: Word[];\n} => {\n let leftTrim = 0;\n if (side === 'left' || side === 'both') {\n for (; leftTrim < line.length; leftTrim++) {\n if (!isWhitespace(line[leftTrim].text)) {\n break;\n }\n }\n\n if (leftTrim >= line.length) {\n // all whitespace\n return {\n trimmedLeft: line.concat(),\n trimmedRight: [],\n trimmedLine: [],\n };\n }\n }\n\n let rightTrim = line.length;\n if (side === 'right' || side === 'both') {\n rightTrim--;\n for (; rightTrim >= 0; rightTrim--) {\n if (!isWhitespace(line[rightTrim].text)) {\n break;\n }\n }\n rightTrim++; // back up one since we started one down for 0-based indexes\n\n if (rightTrim <= 0) {\n // all whitespace\n return {\n trimmedLeft: [],\n trimmedRight: line.concat(),\n trimmedLine: [],\n };\n }\n }\n\n return {\n trimmedLeft: line.slice(0, leftTrim),\n trimmedRight: line.slice(rightTrim),\n trimmedLine: line.slice(leftTrim, rightTrim),\n };\n};\n","import { getTextFormat, getTextStyle } from './style';\nimport { isWhitespace } from './whitespace';\nimport { justifyLine } from './justify';\nimport {\n PositionedWord,\n SplitTextProps,\n SplitWordsProps,\n RenderSpec,\n Word,\n WordMap,\n CanvasTextMetrics,\n TextFormat,\n CanvasRenderContext,\n} from '../model';\nimport { trimLine } from './trim';\n\n// Hair space character for precise justification\nconst HAIR = '\\u{200a}';\n\n// for when we're inferring whitespace between words\nconst SPACE = ' ';\n\n/**\n * Whether the canvas API being used supports the newer `fontBoundingBox*` properties or not.\n *\n * True if it does, false if not; undefined until we determine either way.\n *\n * Note about `fontBoundingBoxAscent/Descent`: Only later browsers support this and the Node-based\n * `canvas` package does not. Having these properties will have a noticeable increase in performance\n * on large pieces of text to render. Failing these, a fallback is used which involves\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics\n * @see https://www.npmjs.com/package/canvas\n */\nlet fontBoundingBoxSupported: boolean;\n\n/**\n * @private\n * Generates a word hash for use as a key in a `WordMap`.\n * @param word\n * @returns Hash.\n */\nconst _getWordHash = (word: Word) => {\n return `${word.text}${word.format ? JSON.stringify(word.format) : ''}`;\n};\n\n/**\n * @private\n * Splits words into lines based on words that are single newline characters.\n * @param words\n * @param inferWhitespace True (default) if whitespace should be inferred (and injected)\n * based on words; false if we're to assume the words already include all necessary whitespace.\n * @returns Words expressed as lines.\n */\nconst _splitIntoLines = (\n words: Word[],\n inferWhitespace: boolean = true\n): Word[][] => {\n const lines: Word[][] = [[]];\n\n let wasWhitespace = false; // true if previous word was whitespace\n words.forEach((word, wordIdx) => {\n // TODO: this is likely a naive split (at least based on character?); should at least\n // think about this more; text format shouldn't matter on a line break, right (hope not)?\n if (word.text.match(/^\\n+$/)) {\n for (let i = 0; i < word.text.length; i++) {\n lines.push([]);\n }\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (isWhitespace(word.text)) {\n // whitespace OTHER THAN newlines since we checked for newlines above\n lines.at(-1)?.push(word);\n wasWhitespace = true;\n return; // next `word`\n }\n\n if (word.text === '') {\n return; // skip to next `word`\n }\n\n // looks like a non-empty, non-whitespace word at this point, so if it isn't the first\n // word and the one before wasn't whitespace, insert a space\n if (inferWhitespace && !wasWhitespace && wordIdx > 0) {\n lines.at(-1)?.push({ text: SPACE });\n }\n\n lines.at(-1)?.push(word);\n wasWhitespace = false;\n });\n\n return lines;\n};\n\n/**\n * @private\n * Helper for `splitWords()` that takes the words that have been wrapped into lines and\n * determines their positions on canvas for future rendering based on alignment settings.\n * @param params\n * @returns Results to return via `splitWords()`\n */\nconst _generateSpec = ({\n wrappedLines,\n wordMap,\n positioning: {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n align,\n vAlign,\n },\n}: {\n /** Words organized/wrapped into lines to be rendered. */\n wrappedLines: Word[][];\n\n /** Map of Word to measured dimensions (px) as it would be rendered. */\n wordMap: WordMap;\n\n /**\n * Details on where to render the Words onto canvas. These parameters ultimately come\n * from `SplitWordsProps`, and they come from `DrawTextConfig`.\n */\n positioning: {\n width: SplitWordsProps['width'];\n // NOTE: height does NOT constrain the text; used only for vertical alignment\n height: SplitWordsProps['height'];\n x: SplitWordsProps['x'];\n y: SplitWordsProps['y'];\n align?: SplitWordsProps['align'];\n vAlign?: SplitWordsProps['vAlign'];\n };\n}): RenderSpec => {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n // NOTE: using __font__ ascent/descent to account for all possible characters in the font\n // so that lines with ascenders but no descenders, or vice versa, are all properly\n // aligned to the baseline, and so that lines aren't scrunched\n // NOTE: even for middle vertical alignment, we want to use the __font__ ascent/descent\n // so that words, per line, are still aligned to the baseline (as much as possible; if\n // each word has a different font size, then things will still be offset, but for the\n // same font size, the baseline should match from left to right)\n const getHeight = (word: Word): number =>\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n word.metrics!.fontBoundingBoxAscent + word.metrics!.fontBoundingBoxDescent;\n\n // max height per line\n const lineHeights = wrappedLines.map((line) =>\n line.reduce((acc, word) => {\n return Math.max(acc, getHeight(word));\n }, 0)\n );\n const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);\n\n // vertical alignment (defaults to middle)\n let lineY: number;\n let textBaseline: CanvasTextBaseline;\n if (vAlign === 'top') {\n textBaseline = 'top';\n lineY = boxY;\n } else if (vAlign === 'bottom') {\n textBaseline = 'bottom';\n lineY = yEnd - totalHeight;\n } else {\n // middle\n textBaseline = 'top'; // YES, using 'top' baseline for 'middle' v-align\n lineY = boxY + boxHeight / 2 - totalHeight / 2;\n }\n\n const lines = wrappedLines.map((line, lineIdx): PositionedWord[] => {\n const lineWidth = line.reduce(\n // NOTE: `metrics` must exist as every `word` MUST have been measured at this point\n (acc, word) => acc + word.metrics!.width,\n 0\n );\n const lineHeight = lineHeights[lineIdx];\n\n // horizontal alignment (defaults to center)\n let lineX: number;\n if (align === 'right') {\n lineX = xEnd - lineWidth;\n } else if (align === 'left') {\n lineX = boxX;\n } else {\n // center\n lineX = boxX + boxWidth / 2 - lineWidth / 2;\n }\n\n let wordX = lineX;\n const posWords = line.map((word): PositionedWord => {\n // NOTE: `word.metrics` and `wordMap.get(hash)` must exist as every `word` MUST have\n // been measured at this point\n\n const hash = _getWordHash(word);\n const { format } = wordMap.get(hash)!;\n const x = wordX;\n const height = getHeight(word);\n\n // vertical alignment (defaults to middle)\n let y: number;\n if (vAlign === 'top') {\n y = lineY;\n } else if (vAlign === 'bottom') {\n y = lineY + lineHeight;\n } else {\n // middle\n y = lineY + (lineHeight - height) / 2;\n }\n\n wordX += word.metrics!.width;\n return {\n word,\n format, // undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)\n x,\n y,\n width: word.metrics!.width,\n height,\n isWhitespace: isWhitespace(word.text),\n };\n });\n\n lineY += lineHeight;\n return posWords;\n });\n\n return {\n lines,\n textBaseline,\n textAlign: 'left', // always per current algorithm\n width: boxWidth,\n height: totalHeight,\n };\n};\n\n/**\n * @private\n * Replacer for use with `JSON.stringify()` to deal with `TextMetrics` objects which\n * only have getters/setters instead of value-based properties.\n * @param key Key being processed in `this`.\n * @param value Value of `key` in `this`.\n * @returns Processed value to be serialized, or `undefined` to omit the `key` from the\n * serialized object.\n */\n// CAREFUL: use a `function`, not an arrow function, as stringify() sets its context to\n// the object being serialized on each call to the replacer\nconst _jsonReplacer = function (key: string, value: unknown) {\n if (key === 'metrics' && value && typeof value === 'object') {\n // TODO: need better typings here, if possible, so that TSC warns if we aren't\n // including a property we should be if a new one is needed in the future (i.e. if\n // a new property is added to the `TextMetricsLike` type)\n // NOTE: TextMetrics objects don't have own-enumerable properties; they only have getters,\n // so we have to explicitly get the values we care about instead of spreading them into\n // the new object\n const metrics: CanvasTextMetrics = value as CanvasTextMetrics;\n return {\n width: metrics.width,\n fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,\n fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,\n };\n }\n\n return value;\n};\n\n/**\n * Serializes render specs to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param specs\n * @returns Specs serialized as JSON.\n */\nexport const specToJson = (specs: RenderSpec): string => {\n return JSON.stringify(specs, _jsonReplacer);\n};\n\n/**\n * Serializes a list of Words to JSON for storage or for sending via `postMessage()`\n * between the main thread and a Web Worker thread.\n *\n * This is primarily to help with the fact that `postMessage()` fails if given a native\n * Canvas `TextMetrics` object to serialize somewhere in its `message` parameter.\n *\n * @param words\n * @returns Words serialized as JSON.\n */\nexport const wordsToJson = (words: Word[]): string => {\n return JSON.stringify(words, _jsonReplacer);\n};\n\n/**\n * @private\n * Measures a Word in a rendering context, assigning its `TextMetrics` to its `metrics` property.\n * @returns The Word's width, in pixels.\n */\nconst _measureWord = ({\n ctx,\n word,\n wordMap,\n baseTextFormat,\n}: {\n ctx: CanvasRenderContext;\n word: Word;\n wordMap: WordMap;\n baseTextFormat: TextFormat;\n}): number => {\n const hash = _getWordHash(word);\n\n if (word.metrics) {\n // assume Word's text and format haven't changed since last measurement and metrics are good\n\n // make sure we have the metrics and full formatting cached for other identical Words\n if (!wordMap.has(hash)) {\n let format = undefined;\n if (word.format) {\n format = getTextFormat(word.format, baseTextFormat);\n }\n wordMap.set(hash, { metrics: word.metrics, format });\n }\n\n return word.metrics.width;\n }\n\n // check to see if we have already measured an identical Word\n if (wordMap.has(hash)) {\n const { metrics } = wordMap.get(hash)!; // will be there because of `if(has())` check\n word.metrics = metrics;\n return metrics.width;\n }\n\n let ctxSaved = false;\n\n let format = undefined;\n if (word.format) {\n ctx.save();\n ctxSaved = true;\n format = getTextFormat(word.format, baseTextFormat);\n ctx.font = getTextStyle(format); // `fontColor` is ignored as it has no effect on metrics\n }\n\n if (!fontBoundingBoxSupported) {\n // use fallback which comes close enough and still gives us properly-aligned text, albeit\n // lines are a couple pixels tighter together\n if (!ctxSaved) {\n ctx.save();\n ctxSaved = true;\n }\n ctx.textBaseline = 'bottom';\n }\n\n const metrics = ctx.measureText(word.text);\n if (typeof metrics.fontBoundingBoxAscent === 'number') {\n fontBoundingBoxSupported = true;\n } else {\n fontBoundingBoxSupported = false;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;\n // @ts-expect-error -- property doesn't exist; we need to polyfill it\n metrics.fontBoundingBoxDescent = 0;\n }\n\n word.metrics = metrics;\n wordMap.set(hash, { metrics, format });\n\n if (ctxSaved) {\n ctx.restore();\n }\n\n return metrics.width;\n};\n\n/**\n * Splits Words into positioned lines of Words as they need to be rendred in 2D space,\n * but does not render anything.\n * @param config\n * @returns Lines of positioned words to be rendered, and total height required to\n * render all lines.\n */\nexport const splitWords = ({\n ctx,\n words,\n justify,\n format: baseFormat,\n inferWhitespace = true,\n ...positioning // rest of params are related to positioning\n}: SplitWordsProps): RenderSpec => {\n const wordMap: WordMap = new Map();\n const baseTextFormat = getTextFormat(baseFormat);\n const { width: boxWidth } = positioning;\n\n //// text measurement\n\n // measures an entire line's width up to the `boxWidth` as a max, unless `force=true`,\n // in which case the entire line is measured regardless of `boxWidth`.\n //\n // - Returned `lineWidth` is width up to, but not including, the `splitPoint` (always <= `boxWidth`\n // unless the first Word is too wide to fit, in which case `lineWidth` will be that Word's\n // width even though it's > `boxWidth`).\n // - If `force=true`, will be the full width of the line regardless of `boxWidth`.\n // - Returned `splitPoint` is index into `words` of the Word immediately FOLLOWING the last\n // Word included in the `lineWidth` (and is `words.length` if all Words were included);\n // `splitPoint` could also be thought of as the number of `words` included in the `lineWidth`.\n // - If `force=true`, will always be `words.length`.\n const measureLine = (\n lineWords: Word[],\n force: boolean = false\n ): {\n lineWidth: number;\n splitPoint: number;\n } => {\n let lineWidth = 0;\n let splitPoint = 0;\n lineWords.every((word, idx) => {\n const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });\n if (!force && lineWidth + wordWidth > boxWidth) {\n // at minimum, MUST include at least first Word, even if it's wider than box width\n if (idx === 0) {\n splitPoint = 1;\n lineWidth = wordWidth;\n }\n // else, `lineWidth` already includes at least one Word so this current Word will\n // be the `splitPoint` such that `lineWidth` remains < `boxWidth`\n\n return false; // break\n }\n\n splitPoint++;\n lineWidth += wordWidth;\n return true; // next\n });\n\n return { lineWidth, splitPoint };\n };\n\n //// main\n\n ctx.save();\n\n // start by trimming the `words` to remove any whitespace at either end, then split the `words`\n // into an initial set of lines dictated by explicit hard breaks, if any (if none, we'll have\n // one super long line)\n const hardLines = _splitIntoLines(\n trimLine(words).trimmedLine,\n inferWhitespace\n );\n\n if (\n hardLines.length <= 0 ||\n boxWidth <= 0 ||\n positioning.height <= 0 ||\n (baseFormat &&\n typeof baseFormat.fontSize === 'number' &&\n baseFormat.fontSize <= 0)\n ) {\n // width or height or font size cannot be 0, or there are no lines after trimming\n return {\n lines: [],\n textAlign: 'center',\n textBaseline: 'middle',\n width: positioning.width,\n height: 0,\n };\n }\n\n ctx.font = getTextStyle(baseTextFormat);\n\n const hairWidth = justify\n ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat })\n : 0;\n const wrappedLines: Word[][] = [];\n\n // now further wrap every hard line to make sure it fits within the `boxWidth`, down to a\n // MINIMUM of 1 Word per line\n for (const hardLine of hardLines) {\n let { splitPoint } = measureLine(hardLine);\n\n // if the line fits, we're done; else, we have to break it down further to fit\n // as best as we can (i.e. MIN one word per line, no breaks within words, no\n // leading/pending whitespace)\n if (splitPoint >= hardLine.length) {\n wrappedLines.push(hardLine);\n } else {\n // shallow clone because we're going to break this line down further to get the best fit\n let softLine = hardLine.concat();\n while (splitPoint < softLine.length) {\n // right-trim what we split off in case we split just after some whitespace\n const splitLine = trimLine(\n softLine.slice(0, splitPoint),\n 'right'\n ).trimmedLine;\n wrappedLines.push(splitLine);\n\n // left-trim what remains in case we split just before some whitespace\n softLine = trimLine(softLine.slice(splitPoint), 'left').trimmedLine;\n ({ splitPoint } = measureLine(softLine));\n }\n\n // get the last bit of the `softLine`\n // NOTE: since we started by timming the entire line, and we just left-trimmed\n // what remained of `softLine`, there should be no need to trim again\n wrappedLines.push(softLine);\n }\n }\n\n // never justify a single line because there's no other line to visually justify it to\n if (justify && wrappedLines.length > 1) {\n wrappedLines.forEach((wrappedLine, idx) => {\n // never justify the last line (common in text editors)\n if (idx < wrappedLines.length - 1) {\n const justifiedLine = justifyLine({\n line: wrappedLine,\n spaceWidth: hairWidth,\n spaceChar: HAIR,\n boxWidth,\n });\n\n // make sure any new Words used for justification get measured so we're able to\n // position them later when we generate the render spec\n measureLine(justifiedLine, true);\n wrappedLines[idx] = justifiedLine;\n }\n });\n }\n\n const spec = _generateSpec({\n wrappedLines,\n wordMap,\n positioning,\n });\n\n ctx.restore();\n return spec;\n};\n\n/**\n * Converts a string of text containing words and whitespace, as well as line breaks (newlines),\n * into a `Word[]` that can be given to `splitWords()`.\n * @param text String to convert into Words.\n * @returns Converted text.\n */\nexport const textToWords = (text: string) => {\n const words: Word[] = [];\n\n // split the `text` into a series of Words, preserving whitespace\n let word: Word | undefined = undefined;\n let wasWhitespace = false;\n Array.from(text.trim()).forEach((c) => {\n const charIsWhitespace = isWhitespace(c);\n if (\n (charIsWhitespace && !wasWhitespace) ||\n (!charIsWhitespace && wasWhitespace)\n ) {\n // save current `word`, if any, and start new `word`\n wasWhitespace = charIsWhitespace;\n if (word) {\n words.push(word);\n }\n word = { text: c };\n } else {\n // accumulate into current `word`\n if (!word) {\n word = { text: '' };\n }\n word.text += c;\n }\n });\n\n // make sure we have the last word! ;)\n if (word) {\n words.push(word);\n }\n\n return words;\n};\n\n/**\n * Splits plain text into lines in the order in which they should be rendered, top-down,\n * preserving whitespace __only within the text__ (whitespace on either end is trimmed).\n */\nexport const splitText = ({ text, ...params }: SplitTextProps): string[] => {\n const words = textToWords(text);\n\n const results = splitWords({\n ...params,\n words,\n inferWhitespace: false,\n });\n\n return results.lines.map((line) =>\n line.map(({ word: { text: t } }) => t).join('')\n );\n};\n","import { getTextStyle } from './style';\nimport { CanvasRenderContext, Word } from '../model';\n\n/** @private */\nconst _getHeight = (ctx: CanvasRenderContext, text: string, style?: string) => {\n const previousTextBaseline = ctx.textBaseline;\n const previousFont = ctx.font;\n\n ctx.textBaseline = 'bottom';\n if (style) {\n ctx.font = style;\n }\n const { actualBoundingBoxAscent: height } = ctx.measureText(text);\n\n // Reset baseline\n ctx.textBaseline = previousTextBaseline;\n if (style) {\n ctx.font = previousFont;\n }\n\n return height;\n};\n\n/**\n * Gets the measured height of a given `Word` using its text style.\n * @returns {number} Height in pixels.\n */\nexport const getWordHeight = ({\n ctx,\n word,\n}: {\n ctx: CanvasRenderContext;\n /**\n * Note: If the word doesn't have a `format`, current `ctx` font settings/styles are used.\n */\n word: Word;\n}) => {\n return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));\n};\n\n/**\n * Gets the measured height of a given `string` using a given text style.\n * @returns {number} Height in pixels.\n */\nexport const getTextHeight = ({\n ctx,\n text,\n style,\n}: {\n ctx: CanvasRenderContext;\n text: string;\n /**\n * CSS font. Same syntax as CSS font specifier. If not specified, current `ctx` font\n * settings/styles are used.\n */\n style?: string;\n}) => {\n return _getHeight(ctx, text, style);\n};\n","import {\n specToJson,\n splitWords,\n splitText,\n textToWords,\n wordsToJson,\n} from './util/split';\nimport { getTextHeight, getWordHeight } from './util/height';\nimport { getTextStyle, getTextFormat, DEFAULT_FONT_COLOR } from './util/style';\nimport { CanvasRenderContext, DrawTextConfig, Text } from './model';\n\nconst drawText = (\n ctx: CanvasRenderContext,\n text: Text,\n config: DrawTextConfig\n) => {\n const baseFormat = getTextFormat({\n fontFamily: config.fontFamily,\n fontSize: config.fontSize,\n fontStyle: config.fontStyle,\n fontVariant: config.fontVariant,\n fontWeight: config.fontWeight,\n fontColor: config.fontColor,\n });\n\n const {\n width: boxWidth,\n height: boxHeight,\n x: boxX = 0,\n y: boxY = 0,\n } = config;\n\n const {\n lines: richLines,\n height: totalHeight,\n textBaseline,\n textAlign,\n } = splitWords({\n ctx,\n words: Array.isArray(text) ? text : textToWords(text),\n inferWhitespace: Array.isArray(text)\n ? config.inferWhitespace === undefined || config.inferWhitespace\n : undefined, // ignore since `text` is a string; we assume it already has all the whitespace it needs\n x: boxX,\n y: boxY,\n width: config.width,\n height: config.height,\n align: config.align,\n vAlign: config.vAlign,\n justify: config.justify,\n format: baseFormat,\n });\n\n ctx.save();\n ctx.textAlign = textAlign;\n ctx.textBaseline = textBaseline;\n ctx.font = getTextStyle(baseFormat);\n ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;\n\n if (config.overflow === false) {\n ctx.beginPath();\n ctx.rect(boxX, boxY, boxWidth, boxHeight);\n ctx.clip(); // part of saved context state\n }\n\n richLines.forEach((line) => {\n line.forEach((pw) => {\n if (!pw.isWhitespace) {\n // NOTE: don't use the `pw.word.format` as this could be incomplete; use `pw.format`\n // if it exists as this will always be the __full__ TextFormat used to measure the\n // Word, and so should be what is used to render it\n if (pw.format) {\n ctx.save();\n ctx.font = getTextStyle(pw.format);\n if (pw.format.fontColor) {\n ctx.fillStyle = pw.format.fontColor;\n }\n }\n ctx.fillText(pw.word.text, pw.x, pw.y);\n if (pw.format) {\n ctx.restore();\n }\n }\n });\n });\n\n if (config.debug) {\n const xEnd = boxX + boxWidth;\n const yEnd = boxY + boxHeight;\n\n let textAnchor: number;\n if (config.align === 'right') {\n textAnchor = xEnd;\n } else if (config.align === 'left') {\n textAnchor = boxX;\n } else {\n textAnchor = boxX + boxWidth / 2;\n }\n\n let debugY = boxY;\n if (config.vAlign === 'bottom') {\n debugY = yEnd;\n } else if (config.vAlign === 'middle') {\n debugY = boxY + boxHeight / 2;\n }\n\n const debugColor = '#0C8CE9';\n\n // Text box\n ctx.lineWidth = 1;\n ctx.strokeStyle = debugColor;\n ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);\n\n ctx.lineWidth = 1;\n\n if (!config.align || config.align === 'center') {\n // Horizontal Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(textAnchor, boxY);\n ctx.lineTo(textAnchor, yEnd);\n ctx.stroke();\n }\n\n if (!config.vAlign || config.vAlign === 'middle') {\n // Vertical Center\n ctx.strokeStyle = debugColor;\n ctx.beginPath();\n ctx.moveTo(boxX, debugY);\n ctx.lineTo(xEnd, debugY);\n ctx.stroke();\n }\n }\n\n ctx.restore();\n\n return { height: totalHeight };\n};\n\nexport {\n drawText,\n specToJson,\n splitText,\n splitWords,\n textToWords,\n wordsToJson,\n getTextHeight,\n getWordHeight,\n getTextStyle,\n getTextFormat,\n};\nexport * from './model';\n"],"names":["DEFAULT_FONT_FAMILY","DEFAULT_FONT_COLOR","getTextFormat","format","baseFormat","getTextStyle","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","isWhitespace","text","_extractWords","line","word","_cloneWord","clone","_joinWords","words","joiner","phrase","wordIdx","jw","justifyLine","spaceWidth","spaceChar","boxWidth","wordsWidth","width","_b","_a","noOfSpacesToInsert","spacesPerWord","spaces","firstWords","firstPart","remainingSpaces","lastWord","trimLine","side","leftTrim","rightTrim","HAIR","SPACE","fontBoundingBoxSupported","_getWordHash","_splitIntoLines","inferWhitespace","lines","wasWhitespace","i","_c","_generateSpec","wrappedLines","wordMap","boxHeight","boxX","boxY","align","vAlign","xEnd","yEnd","getHeight","lineHeights","acc","totalHeight","h","lineY","textBaseline","lineIdx","lineWidth","lineHeight","lineX","wordX","posWords","hash","x","height","y","_jsonReplacer","key","value","metrics","specToJson","specs","wordsToJson","_measureWord","ctx","baseTextFormat","ctxSaved","splitWords","justify","positioning","measureLine","lineWords","force","splitPoint","idx","wordWidth","hardLines","hairWidth","hardLine","softLine","splitLine","wrappedLine","justifiedLine","spec","textToWords","c","charIsWhitespace","splitText","params","t","_getHeight","style","previousTextBaseline","previousFont","getWordHeight","getTextHeight","drawText","config","richLines","textAlign","pw","textAnchor","debugY","debugColor"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "text-to-canvas",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Render multiline plain or rich text into textboxes on HTML Canvas with automatic line wrapping",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -54,44 +54,44 @@
|
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "rm -rf ./dist && npm run build:browser && npm run build:node && npm run build:types",
|
|
57
|
-
"build:browser": "vite build --config
|
|
58
|
-
"build:node": "vite build --config
|
|
57
|
+
"build:browser": "vite build --config build/vite.config.esm.mts && vite build --config build/vite.config.cjs.mts",
|
|
58
|
+
"build:node": "vite build --config build/vite.config.node.mts",
|
|
59
59
|
"build:types": "tsc src/lib/index.ts --declaration --emitDeclarationOnly --outDir dist/types",
|
|
60
60
|
"ci:build": "npm run build && npm run docs",
|
|
61
61
|
"ci:lint": "npm run lint",
|
|
62
62
|
"ci:test": "npm run test:unit && npm run demo:node",
|
|
63
63
|
"demo:node": "vite-node ./src/demos/node-demo.mts",
|
|
64
|
-
"docs": "vite build --config
|
|
65
|
-
"fmt": "prettier --write \"{*,
|
|
66
|
-
"fmt:check": "prettier --check \"{*,
|
|
64
|
+
"docs": "vite build --config build/vite.config.docs.mts && prettier --write \"src/docs/*.d.ts\"",
|
|
65
|
+
"fmt": "prettier --write \"{*,build/**/*,src/**/*}.+(js|cjs|mjs|ts|cts|mts|css|yml|json|vue)\"",
|
|
66
|
+
"fmt:check": "prettier --check \"{*,build/**/*,src/**/*}.+(js|cjs|mjs|ts|cts|mts|css|yml|json|vue)\"",
|
|
67
67
|
"lint": "npm run lint:code && npm run lint:types && npm run fmt:check",
|
|
68
|
-
"lint:code": "eslint \"{*,
|
|
68
|
+
"lint:code": "eslint \"{*,build/**/*,src/**/*}.+(js|mjs|ts|mts|vue)\"",
|
|
69
69
|
"lint:types": "tsc",
|
|
70
70
|
"prepare": "npm run build",
|
|
71
71
|
"prepublishOnly": "npm run lint && npm run test:unit && npm run build",
|
|
72
|
-
"start": "vite serve --config
|
|
72
|
+
"start": "vite serve --config build/vite.config.docs.mts",
|
|
73
73
|
"test": "npm run lint && npm run test:unit && npm run build",
|
|
74
74
|
"test:unit": "echo 'TODO: Add unit tests...'"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@types/lodash": "^4.17.
|
|
78
|
-
"@types/node": "^20.
|
|
77
|
+
"@types/lodash": "^4.17.1",
|
|
78
|
+
"@types/node": "^20.12.12",
|
|
79
79
|
"@types/offscreencanvas": "^2019.7.3",
|
|
80
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
81
|
-
"@typescript-eslint/parser": "^7.
|
|
80
|
+
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
|
81
|
+
"@typescript-eslint/parser": "^7.9.0",
|
|
82
82
|
"@vitejs/plugin-vue": "^5.0.4",
|
|
83
|
-
"element-plus": "^2.
|
|
83
|
+
"element-plus": "^2.7.3",
|
|
84
84
|
"eslint": "^8.57.0",
|
|
85
85
|
"eslint-config-prettier": "^9.1.0",
|
|
86
|
-
"eslint-plugin-vue": "^9.
|
|
86
|
+
"eslint-plugin-vue": "^9.26.0",
|
|
87
87
|
"lodash": "^4.17.21",
|
|
88
88
|
"prettier": "^3.2.5",
|
|
89
|
-
"typescript": "^5.4.
|
|
90
|
-
"unplugin-auto-import": "^0.17.
|
|
91
|
-
"unplugin-vue-components": "^0.
|
|
92
|
-
"vite": "^5.
|
|
93
|
-
"vite-node": "^1.
|
|
94
|
-
"vue": "^3.4.
|
|
89
|
+
"typescript": "^5.4.5",
|
|
90
|
+
"unplugin-auto-import": "^0.17.6",
|
|
91
|
+
"unplugin-vue-components": "^0.27.0",
|
|
92
|
+
"vite": "^5.2.11",
|
|
93
|
+
"vite-node": "^1.6.0",
|
|
94
|
+
"vue": "^3.4.27"
|
|
95
95
|
},
|
|
96
96
|
"optionalDependencies": {
|
|
97
97
|
"canvas": "^2.11.2"
|