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 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
@@ -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: s,
19
- fontWeight: i
20
- }) => `${e || ""} ${s || ""} ${i || ""} ${n ?? 14}px ${t || D}`.trim(), T = (t) => !!t.match(/^\s+$/), M = (t) => t.filter((n) => !T(n.text)), V = (t) => {
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
- }, H = (t, n) => {
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((s, i) => {
28
- e.push(s), i < t.length - 1 && n.forEach((r) => e.push(V(r)));
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: s
34
+ boxWidth: i
35
35
  }) => {
36
- const i = M(t);
37
- if (i.length <= 1)
36
+ const s = M(t);
37
+ if (s.length <= 1)
38
38
  return t.concat();
39
- const r = i.reduce(
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 = (s - r) / n;
46
- if (i.length > 2) {
47
- const a = Math.ceil(l / (i.length - 1)), u = Array.from({ length: a }, () => ({
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 = i.slice(0, i.length - 1), y = H(d, u), c = u.slice(
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 = i[i.length - 1];
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 H(i, o);
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 s = t.length;
72
+ let i = t.length;
73
73
  if (n === "right" || n === "both") {
74
- for (s--; s >= 0 && T(t[s].text); s--)
74
+ for (i--; i >= 0 && T(t[i].text); i--)
75
75
  ;
76
- if (s++, s <= 0)
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(s),
86
- trimmedLine: t.slice(e, s)
85
+ trimmedRight: t.slice(i),
86
+ trimmedLine: t.slice(e, i)
87
87
  };
88
- }, k = " ", Y = " ";
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 s = !1;
93
- return t.forEach((i, r) => {
92
+ let i = !1;
93
+ return t.forEach((s, r) => {
94
94
  var l, o, a;
95
- if (i.text.match(/^\n+$/)) {
96
- for (let u = 0; u < i.text.length; u++)
95
+ if (s.text.match(/^\n+$/)) {
96
+ for (let u = 0; u < s.text.length; u++)
97
97
  e.push([]);
98
- s = !0;
98
+ i = !0;
99
99
  return;
100
100
  }
101
- if (T(i.text)) {
102
- (l = e.at(-1)) == null || l.push(i), s = !0;
101
+ if (T(s.text)) {
102
+ (l = e.at(-1)) == null || l.push(s), i = !0;
103
103
  return;
104
104
  }
105
- i.text !== "" && (n && !s && r > 0 && ((o = e.at(-1)) == null || o.push({ text: Y })), (a = e.at(-1)) == null || a.push(i), s = !1);
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: s,
113
- x: i = 0,
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 = i + e, u = r + s, d = (f) => (
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 + s / 2 - c / 2), {
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 = i : B = i + e / 2 - A / 2;
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), C = ({
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: s
172
+ baseTextFormat: i
173
173
  }) => {
174
- const i = I(n);
174
+ const s = I(n);
175
175
  if (n.metrics) {
176
- if (!e.has(i)) {
176
+ if (!e.has(s)) {
177
177
  let a;
178
- n.format && (a = _(n.format, s)), e.set(i, { metrics: n.metrics, format: a });
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(i)) {
183
- const { metrics: a } = e.get(i);
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, s), t.font = S(l)), v || (r || (t.save(), r = !0), t.textBaseline = "bottom");
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(i, { metrics: o, format: l }), r && t.restore(), o.width;
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: s,
195
- inferWhitespace: i = !0,
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 = _(s), { width: a } = r, u = (m, g = !1) => {
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 = C({ ctx: t, word: A, wordMap: l, baseTextFormat: o });
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
- i
209
+ s
210
210
  );
211
- if (d.length <= 0 || a <= 0 || r.height <= 0 || s && typeof s.fontSize == "number" && s.fontSize <= 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 ? C({ ctx: t, word: { text: k }, wordMap: l, baseTextFormat: o }) : 0, c = [];
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: k,
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, s = !1;
257
- return Array.from(t.trim()).forEach((i) => {
258
- const r = T(i);
259
- r && !s || !r && s ? (s = r, e && n.push(e), e = { text: i }) : (e || (e = { text: "" }), e.text += i);
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
- (i) => i.map(({ word: { text: r } }) => r).join("")
268
+ (s) => s.map(({ word: { text: r } }) => r).join("")
269
269
  );
270
270
  }, R = (t, n, e) => {
271
- const s = t.textBaseline, i = t.font;
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 = s, e && (t.font = i), r;
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 s = _({
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: i,
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: s
312
+ format: i
312
313
  });
313
- if (t.save(), t.textAlign = y, t.textBaseline = d, t.font = S(s), t.fillStyle = s.fontColor || $, e.overflow === !1 && (t.beginPath(), t.rect(l, o, i, r), t.clip()), a.forEach((c) => {
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 + i, h = o + r;
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 + i / 2;
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, i, 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
+ 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":"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"}
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},C=(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=C(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 C(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 O=W;const M=l.map(T=>{const v=R(T),{format:U}=n.get(v),V=O,k=d(T);let b;return o==="top"?b=h:o==="bottom"?b=h+x:b=h+(x-k)/2,O+=T.metrics.width,{word:T,format:U,x:V,y:b,width:T.metrics.width,height:k,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},F=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=F(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}),{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:F(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=F;exports.wordsToJson=Q;
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":"gFAEO,MAAMA,EAAsB,QACtBC,EAAoB,GACpBC,EAAqB,QAQrBC,EAAgB,CAC3BC,EACAC,IAEO,OAAO,OACZ,CAAC,EACD,CACE,WAAYL,EACZ,SAAUC,EACV,WAAY,MACZ,UAAW,GACX,YAAa,GACb,UAAWC,CACb,EACAG,EACAD,CAAA,EAUSE,EAAe,CAAC,CAC3B,WAAAC,EACA,SAAAC,EACA,UAAAC,EACA,YAAAC,EACA,WAAAC,CACF,IAKS,GAAGF,GAAa,EAAE,IAAIC,GAAe,EAAE,IAC5CC,GAAc,EAChB,IAAIH,GAAA,KAAAA,EAAYP,CAAiB,MAAMM,GAAcP,CAAmB,GAAG,OC7ChEY,EAAgBC,GACpB,CAAC,CAACA,EAAK,MAAM,OAAO,ECGvBC,EAAiBC,GACdA,EAAK,OAAQC,GAAS,CAACJ,EAAaI,EAAK,IAAI,CAAC,EASjDC,EAAcD,GAAe,CAC3B,MAAAE,EAAQ,CAAE,GAAGF,GACnB,OAAIA,EAAK,SACPE,EAAM,OAAS,CAAE,GAAGF,EAAK,MAAO,GAE3BE,CACT,EAYMC,EAAa,CAACC,EAAeC,IAAmB,CACpD,GAAID,EAAM,QAAU,GAAKC,EAAO,OAAS,EAChC,MAAA,CAAC,GAAGD,CAAK,EAGlB,MAAME,EAAiB,CAAA,EACjB,OAAAF,EAAA,QAAQ,CAACJ,EAAMO,IAAY,CAC/BD,EAAO,KAAKN,CAAI,EACZO,EAAUH,EAAM,OAAS,GAEpBC,EAAA,QAASG,GAAOF,EAAO,KAAKL,EAAWO,CAAE,CAAC,CAAC,CACpD,CACD,EAEMF,CACT,EAUaG,EAAc,CAAC,CAC1B,KAAAV,EACA,WAAAW,EACA,UAAAC,EACA,SAAAC,CACF,IAYM,CACE,MAAAR,EAAQN,EAAcC,CAAI,EAC5B,GAAAK,EAAM,QAAU,EAClB,OAAOL,EAAK,SAGd,MAAMc,EAAaT,EAAM,OACvB,CAACU,EAAOd,aAAS,OAAAc,IAASC,GAAAC,EAAAhB,EAAK,UAAL,YAAAgB,EAAc,QAAd,KAAAD,EAAuB,IACjD,CAAA,EAEIE,GAAsBL,EAAWC,GAAcH,EAEjD,GAAAN,EAAM,OAAS,EAAG,CAIpB,MAAMc,EAAgB,KAAK,KAAKD,GAAsBb,EAAM,OAAS,EAAE,EACjEe,EAAiB,MAAM,KAAK,CAAE,OAAQD,CAAA,EAAiB,KAAO,CAClE,KAAMP,CACN,EAAA,EACIS,EAAahB,EAAM,MAAM,EAAGA,EAAM,OAAS,CAAC,EAC5CiB,EAAYlB,EAAWiB,EAAYD,CAAM,EACzCG,EAAkBH,EAAO,MAC7B,EACA,KAAK,MAAMF,CAAkB,GAAKG,EAAW,OAAS,GAAKD,EAAO,MAAA,EAE9DI,EAAWnB,EAAMA,EAAM,OAAS,CAAC,EACvC,MAAO,CAAC,GAAGiB,EAAW,GAAGC,EAAiBC,CAAQ,CACpD,CAGA,MAAMJ,EAAiB,MAAM,KAC3B,CAAE,OAAQ,KAAK,MAAMF,CAAkB,CAAE,EACzC,KAAO,CAAE,KAAMN,GAAU,EAEpB,OAAAR,EAAWC,EAAOe,CAAM,CACjC,EC1GaK,EAAW,CACtBzB,EACA0B,EAAkC,SAe/B,CACH,IAAIC,EAAW,EACX,GAAAD,IAAS,QAAUA,IAAS,OAAQ,CAC/B,KAAAC,EAAW3B,EAAK,QAChBH,EAAaG,EAAK2B,CAAQ,EAAE,IAAI,EADRA,IAC7B,CAKE,GAAAA,GAAY3B,EAAK,OAEZ,MAAA,CACL,YAAaA,EAAK,OAAO,EACzB,aAAc,CAAC,EACf,YAAa,CAAC,CAAA,CAGpB,CAEA,IAAI4B,EAAY5B,EAAK,OACjB,GAAA0B,IAAS,SAAWA,IAAS,OAAQ,CAEhC,IADPE,IACOA,GAAa,GACb/B,EAAaG,EAAK4B,CAAS,EAAE,IAAI,EADjBA,IACrB,CAMF,GAFAA,IAEIA,GAAa,EAER,MAAA,CACL,YAAa,CAAC,EACd,aAAc5B,EAAK,OAAO,EAC1B,YAAa,CAAC,CAAA,CAGpB,CAEO,MAAA,CACL,YAAaA,EAAK,MAAM,EAAG2B,CAAQ,EACnC,aAAc3B,EAAK,MAAM4B,CAAS,EAClC,YAAa5B,EAAK,MAAM2B,EAAUC,CAAS,CAAA,CAE/C,ECrDMC,EAAO,IAGPC,EAAQ,IAcd,IAAIC,EAQJ,MAAMC,EAAgB/B,GACb,GAAGA,EAAK,IAAI,GAAGA,EAAK,OAAS,KAAK,UAAUA,EAAK,MAAM,EAAI,EAAE,GAWhEgC,EAAkB,CACtB5B,EACA6B,EAA2B,KACd,CACP,MAAAC,EAAkB,CAAC,CAAA,CAAE,EAE3B,IAAIC,EAAgB,GACd,OAAA/B,EAAA,QAAQ,CAACJ,EAAMO,IAAY,WAG/B,GAAIP,EAAK,KAAK,MAAM,OAAO,EAAG,CAC5B,QAASoC,EAAI,EAAGA,EAAIpC,EAAK,KAAK,OAAQoC,IAC9BF,EAAA,KAAK,CAAA,CAAE,EAECC,EAAA,GAChB,MACF,CAEI,GAAAvC,EAAaI,EAAK,IAAI,EAAG,EAE3BgB,EAAAkB,EAAM,GAAG,EAAE,IAAX,MAAAlB,EAAc,KAAKhB,GACHmC,EAAA,GAChB,MACF,CAEInC,EAAK,OAAS,KAMdiC,GAAmB,CAACE,GAAiB5B,EAAU,KACjDQ,EAAAmB,EAAM,GAAG,EAAE,IAAX,MAAAnB,EAAc,KAAK,CAAE,KAAMc,MAG7BQ,EAAAH,EAAM,GAAG,EAAE,IAAX,MAAAG,EAAc,KAAKrC,GACHmC,EAAA,GAAA,CACjB,EAEMD,CACT,EASMI,EAAgB,CAAC,CACrB,aAAAC,EACA,QAAAC,EACA,YAAa,CACX,MAAO5B,EACP,OAAQ6B,EACR,EAAGC,EAAO,EACV,EAAGC,EAAO,EACV,MAAAC,EACA,OAAAC,CACF,CACF,IAoBkB,CAChB,MAAMC,EAAOJ,EAAO9B,EACdmC,EAAOJ,EAAOF,EASdO,EAAahD,GAEjBA,EAAK,QAAS,sBAAwBA,EAAK,QAAS,uBAGhDiD,EAAcV,EAAa,IAAKxC,GACpCA,EAAK,OAAO,CAACmD,EAAKlD,IACT,KAAK,IAAIkD,EAAKF,EAAUhD,CAAI,CAAC,EACnC,CAAC,CAAA,EAEAmD,EAAcF,EAAY,OAAO,CAACC,EAAKE,IAAMF,EAAME,EAAG,CAAC,EAGzD,IAAAC,EACAC,EACJ,OAAIT,IAAW,OACES,EAAA,MACPD,EAAAV,GACCE,IAAW,UACLS,EAAA,SACfD,EAAQN,EAAOI,IAGAG,EAAA,MACPD,EAAAV,EAAOF,EAAY,EAAIU,EAAc,GA2DxC,CACL,MAzDYZ,EAAa,IAAI,CAACxC,EAAMwD,IAA8B,CAClE,MAAMC,EAAYzD,EAAK,OAErB,CAACmD,EAAKlD,IAASkD,EAAMlD,EAAK,QAAS,MACnC,CAAA,EAEIyD,EAAaR,EAAYM,CAAO,EAGlC,IAAAG,EACAd,IAAU,QACZc,EAAQZ,EAAOU,EACNZ,IAAU,OACXc,EAAAhB,EAGAgB,EAAAhB,EAAO9B,EAAW,EAAI4C,EAAY,EAG5C,IAAIG,EAAQD,EACZ,MAAME,EAAW7D,EAAK,IAAKC,GAAyB,CAI5C,MAAA6D,EAAO9B,EAAa/B,CAAI,EACxB,CAAE,OAAAZ,CAAW,EAAAoD,EAAQ,IAAIqB,CAAI,EAC7BC,EAAIH,EACJI,EAASf,EAAUhD,CAAI,EAGzB,IAAAgE,EACJ,OAAInB,IAAW,MACTmB,EAAAX,EACKR,IAAW,SACpBmB,EAAIX,EAAQI,EAGRO,EAAAX,GAASI,EAAaM,GAAU,EAGtCJ,GAAS3D,EAAK,QAAS,MAChB,CACL,KAAAA,EACA,OAAAZ,EACA,EAAA0E,EACA,EAAAE,EACA,MAAOhE,EAAK,QAAS,MACrB,OAAA+D,EACA,aAAcnE,EAAaI,EAAK,IAAI,CAAA,CACtC,CACD,EAEQ,OAAAqD,GAAAI,EACFG,CAAA,CACR,EAIC,aAAAN,EACA,UAAW,OACX,MAAO1C,EACP,OAAQuC,CAAA,CAEZ,EAaMc,EAAgB,SAAUC,EAAaC,EAAgB,CAC3D,GAAID,IAAQ,WAAaC,GAAS,OAAOA,GAAU,SAAU,CAO3D,MAAMC,EAA6BD,EAC5B,MAAA,CACL,MAAOC,EAAQ,MACf,sBAAuBA,EAAQ,sBAC/B,uBAAwBA,EAAQ,sBAAA,CAEpC,CAEO,OAAAD,CACT,EAYaE,EAAcC,GAClB,KAAK,UAAUA,EAAOL,CAAa,EAa/BM,EAAenE,GACnB,KAAK,UAAUA,EAAO6D,CAAa,EAQtCO,EAAe,CAAC,CACpB,IAAAC,EACA,KAAAzE,EACA,QAAAwC,EACA,eAAAkC,CACF,IAKc,CACN,MAAAb,EAAO9B,EAAa/B,CAAI,EAE9B,GAAIA,EAAK,QAAS,CAIhB,GAAI,CAACwC,EAAQ,IAAIqB,CAAI,EAAG,CACtB,IAAIzE,EACAY,EAAK,SACPZ,EAASD,EAAca,EAAK,OAAQ0E,CAAc,GAE5ClC,EAAA,IAAIqB,EAAM,CAAE,QAAS7D,EAAK,QAAS,OAAAZ,EAAQ,CACrD,CAEA,OAAOY,EAAK,QAAQ,KACtB,CAGI,GAAAwC,EAAQ,IAAIqB,CAAI,EAAG,CACrB,KAAM,CAAE,QAAAO,CAAAA,EAAY5B,EAAQ,IAAIqB,CAAI,EACpC,OAAA7D,EAAK,QAAUoE,EACRA,EAAQ,KACjB,CAEA,IAAIO,EAAW,GAEXvF,EACAY,EAAK,SACPyE,EAAI,KAAK,EACEE,EAAA,GACFvF,EAAAD,EAAca,EAAK,OAAQ0E,CAAc,EAC9CD,EAAA,KAAOnF,EAAaF,CAAM,GAG3B0C,IAGE6C,IACHF,EAAI,KAAK,EACEE,EAAA,IAEbF,EAAI,aAAe,UAGrB,MAAML,EAAUK,EAAI,YAAYzE,EAAK,IAAI,EACrC,OAAA,OAAOoE,EAAQ,uBAA0B,SAChBtC,EAAA,IAEAA,EAAA,GAE3BsC,EAAQ,sBAAwBA,EAAQ,wBAExCA,EAAQ,uBAAyB,GAGnCpE,EAAK,QAAUoE,EACf5B,EAAQ,IAAIqB,EAAM,CAAE,QAAAO,EAAS,OAAAhF,CAAQ,CAAA,EAEjCuF,GACFF,EAAI,QAAQ,EAGPL,EAAQ,KACjB,EASaQ,EAAa,CAAC,CACzB,IAAAH,EACA,MAAArE,EACA,QAAAyE,EACA,OAAQxF,EACR,gBAAA4C,EAAkB,GAClB,GAAG6C,CACL,IAAmC,CAC3B,MAAAtC,MAAuB,IACvBkC,EAAiBvF,EAAcE,CAAU,EACzC,CAAE,MAAOuB,CAAa,EAAAkE,EAetBC,EAAc,CAClBC,EACAC,EAAiB,KAId,CACH,IAAIzB,EAAY,EACZ0B,EAAa,EACP,OAAAF,EAAA,MAAM,CAAChF,EAAMmF,IAAQ,CAC7B,MAAMC,EAAYZ,EAAa,CAAE,IAAAC,EAAK,KAAAzE,EAAM,QAAAwC,EAAS,eAAAkC,EAAgB,EACrE,MAAI,CAACO,GAASzB,EAAY4B,EAAYxE,GAEhCuE,IAAQ,IACGD,EAAA,EACD1B,EAAA4B,GAKP,KAGTF,IACa1B,GAAA4B,EACN,GAAA,CACR,EAEM,CAAE,UAAA5B,EAAW,WAAA0B,EAAW,EAKjCT,EAAI,KAAK,EAKT,MAAMY,EAAYrD,EAChBR,EAASpB,CAAK,EAAE,YAChB6B,CAAA,EAGF,GACEoD,EAAU,QAAU,GACpBzE,GAAY,GACZkE,EAAY,QAAU,GACrBzF,GACC,OAAOA,EAAW,UAAa,UAC/BA,EAAW,UAAY,EAGlB,MAAA,CACL,MAAO,CAAC,EACR,UAAW,SACX,aAAc,SACd,MAAOyF,EAAY,MACnB,OAAQ,CAAA,EAIRL,EAAA,KAAOnF,EAAaoF,CAAc,EAEtC,MAAMY,EAAYT,EACdL,EAAa,CAAE,IAAAC,EAAK,KAAM,CAAE,KAAM7C,CAAK,EAAG,QAAAY,EAAS,eAAAkC,CAAgB,CAAA,EACnE,EACEnC,EAAyB,CAAA,EAI/B,UAAWgD,KAAYF,EAAW,CAChC,GAAI,CAAE,WAAAH,CAAA,EAAeH,EAAYQ,CAAQ,EAKrC,GAAAL,GAAcK,EAAS,OACzBhD,EAAa,KAAKgD,CAAQ,MACrB,CAED,IAAAC,EAAWD,EAAS,SACjB,KAAAL,EAAaM,EAAS,QAAQ,CAEnC,MAAMC,EAAYjE,EAChBgE,EAAS,MAAM,EAAGN,CAAU,EAC5B,OACA,EAAA,YACF3C,EAAa,KAAKkD,CAAS,EAG3BD,EAAWhE,EAASgE,EAAS,MAAMN,CAAU,EAAG,MAAM,EAAE,YACvD,CAAE,WAAAA,CAAA,EAAeH,EAAYS,CAAQ,CACxC,CAKAjD,EAAa,KAAKiD,CAAQ,CAC5B,CACF,CAGIX,GAAWtC,EAAa,OAAS,GACtBA,EAAA,QAAQ,CAACmD,EAAaP,IAAQ,CAErC,GAAAA,EAAM5C,EAAa,OAAS,EAAG,CACjC,MAAMoD,EAAgBlF,EAAY,CAChC,KAAMiF,EACN,WAAYJ,EACZ,UAAW1D,EACX,SAAAhB,CAAA,CACD,EAIDmE,EAAYY,EAAe,EAAI,EAC/BpD,EAAa4C,CAAG,EAAIQ,CACtB,CAAA,CACD,EAGH,MAAMC,EAAOtD,EAAc,CACzB,aAAAC,EACA,QAAAC,EACA,YAAAsC,CAAA,CACD,EAED,OAAAL,EAAI,QAAQ,EACLmB,CACT,EAQaC,EAAehG,GAAiB,CAC3C,MAAMO,EAAgB,CAAA,EAGtB,IAAIJ,EACAmC,EAAgB,GACpB,aAAM,KAAKtC,EAAK,KAAM,CAAA,EAAE,QAASiG,GAAM,CAC/B,MAAAC,EAAmBnG,EAAakG,CAAC,EAEpCC,GAAoB,CAAC5D,GACrB,CAAC4D,GAAoB5D,GAGNA,EAAA4D,EACZ/F,GACFI,EAAM,KAAKJ,CAAI,EAEVA,EAAA,CAAE,KAAM8F,KAGV9F,IACIA,EAAA,CAAE,KAAM,KAEjBA,EAAK,MAAQ8F,EACf,CACD,EAGG9F,GACFI,EAAM,KAAKJ,CAAI,EAGVI,CACT,EAMa4F,EAAY,CAAC,CAAE,KAAAnG,EAAM,GAAGoG,KAAuC,CACpE,MAAA7F,EAAQyF,EAAYhG,CAAI,EAQ9B,OANgB+E,EAAW,CACzB,GAAGqB,EACH,MAAA7F,EACA,gBAAiB,EAAA,CAClB,EAEc,MAAM,IAAKL,GACxBA,EAAK,IAAI,CAAC,CAAE,KAAM,CAAE,KAAMmG,CAAI,CAAA,IAAMA,CAAC,EAAE,KAAK,EAAE,CAAA,CAElD,EChlBMC,EAAa,CAAC1B,EAA0B5E,EAAcuG,IAAmB,CAC7E,MAAMC,EAAuB5B,EAAI,aAC3B6B,EAAe7B,EAAI,KAEzBA,EAAI,aAAe,SACf2B,IACF3B,EAAI,KAAO2B,GAEb,KAAM,CAAE,wBAAyBrC,CAAA,EAAWU,EAAI,YAAY5E,CAAI,EAGhE,OAAA4E,EAAI,aAAe4B,EACfD,IACF3B,EAAI,KAAO6B,GAGNvC,CACT,EAMawC,GAAgB,CAAC,CAC5B,IAAA9B,EACA,KAAAzE,CACF,IAOSmG,EAAW1B,EAAKzE,EAAK,KAAMA,EAAK,QAAUV,EAAaU,EAAK,MAAM,CAAC,EAO/DwG,GAAgB,CAAC,CAC5B,IAAA/B,EACA,KAAA5E,EACA,MAAAuG,CACF,IASSD,EAAW1B,EAAK5E,EAAMuG,CAAK,EC9C9BK,GAAW,CACfhC,EACA5E,EACA6G,IACG,CACH,MAAMrH,EAAaF,EAAc,CAC/B,WAAYuH,EAAO,WACnB,SAAUA,EAAO,SACjB,UAAWA,EAAO,UAClB,YAAaA,EAAO,YACpB,WAAYA,EAAO,WACnB,UAAWA,EAAO,SAAA,CACnB,EAEK,CACJ,MAAO9F,EACP,OAAQ6B,EACR,EAAGC,EAAO,EACV,EAAGC,EAAO,CACR,EAAA+D,EAEE,CACJ,MAAOC,EACP,OAAQxD,EACR,aAAAG,EACA,UAAAsD,GACEhC,EAAW,CACb,IAAAH,EACA,MAAO,MAAM,QAAQ5E,CAAI,EAAIA,EAAOgG,EAAYhG,CAAI,EACpD,gBAAiB,MAAM,QAAQA,CAAI,EAC/B6G,EAAO,kBAAoB,QAAaA,EAAO,gBAC/C,OACJ,EAAGhE,EACH,EAAGC,EACH,MAAO+D,EAAO,MACd,OAAQA,EAAO,OACf,MAAOA,EAAO,MACd,OAAQA,EAAO,OACf,QAASA,EAAO,QAChB,OAAQrH,CAAA,CACT,EAmCD,GAjCAoF,EAAI,KAAK,EACTA,EAAI,UAAYmC,EAChBnC,EAAI,aAAenB,EACfmB,EAAA,KAAOnF,EAAaD,CAAU,EAC9BoF,EAAA,UAAYpF,EAAW,WAAaH,EAEpCwH,EAAO,WAAa,KACtBjC,EAAI,UAAU,EACdA,EAAI,KAAK/B,EAAMC,EAAM/B,EAAU6B,CAAS,EACxCgC,EAAI,KAAK,GAGDkC,EAAA,QAAS5G,GAAS,CACrBA,EAAA,QAAS8G,GAAO,CACdA,EAAG,eAIFA,EAAG,SACLpC,EAAI,KAAK,EACLA,EAAA,KAAOnF,EAAauH,EAAG,MAAM,EAC7BA,EAAG,OAAO,YACRpC,EAAA,UAAYoC,EAAG,OAAO,YAG9BpC,EAAI,SAASoC,EAAG,KAAK,KAAMA,EAAG,EAAGA,EAAG,CAAC,EACjCA,EAAG,QACLpC,EAAI,QAAQ,EAEhB,CACD,CAAA,CACF,EAEGiC,EAAO,MAAO,CAChB,MAAM5D,EAAOJ,EAAO9B,EACdmC,EAAOJ,EAAOF,EAEhB,IAAAqE,EACAJ,EAAO,QAAU,QACNI,EAAAhE,EACJ4D,EAAO,QAAU,OACbI,EAAApE,EAEboE,EAAapE,EAAO9B,EAAW,EAGjC,IAAImG,EAASpE,EACT+D,EAAO,SAAW,SACXK,EAAAhE,EACA2D,EAAO,SAAW,WAC3BK,EAASpE,EAAOF,EAAY,GAG9B,MAAMuE,EAAa,UAGnBvC,EAAI,UAAY,EAChBA,EAAI,YAAcuC,EAClBvC,EAAI,WAAW/B,EAAMC,EAAM/B,EAAU6B,CAAS,EAE9CgC,EAAI,UAAY,GAEZ,CAACiC,EAAO,OAASA,EAAO,QAAU,YAEpCjC,EAAI,YAAcuC,EAClBvC,EAAI,UAAU,EACVA,EAAA,OAAOqC,EAAYnE,CAAI,EACvB8B,EAAA,OAAOqC,EAAY/D,CAAI,EAC3B0B,EAAI,OAAO,IAGT,CAACiC,EAAO,QAAUA,EAAO,SAAW,YAEtCjC,EAAI,YAAcuC,EAClBvC,EAAI,UAAU,EACVA,EAAA,OAAO/B,EAAMqE,CAAM,EACnBtC,EAAA,OAAO3B,EAAMiE,CAAM,EACvBtC,EAAI,OAAO,EAEf,CAEA,OAAAA,EAAI,QAAQ,EAEL,CAAE,OAAQtB,EACnB"}
@@ -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,C)=>B+C.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 C=P(B),{format:et}=n.get(C),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},H=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=H(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}),{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:H(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=H,g.wordsToJson=q,Object.defineProperty(g,Symbol.toStringTag,{value:"Module"})});
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.0",
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 config/vite.config.esm.mts && vite build --config config/vite.config.cjs.mts",
58
- "build:node": "vite build --config config/vite.config.node.mts",
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 config/vite.config.docs.mts && prettier --write \"src/docs/*.d.ts\"",
65
- "fmt": "prettier --write \"{*,config/**/*,src/**/*}.+(js|cjs|mjs|ts|cts|mts|css|yml|json|vue)\"",
66
- "fmt:check": "prettier --check \"{*,config/**/*,src/**/*}.+(js|cjs|mjs|ts|cts|mts|css|yml|json|vue)\"",
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 \"{*,config/**/*,src/**/*}.+(js|mjs|ts|mts|vue)\"",
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 config/vite.config.docs.mts",
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.0",
78
- "@types/node": "^20.11.27",
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.2.0",
81
- "@typescript-eslint/parser": "^7.2.0",
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.6.1",
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.23.0",
86
+ "eslint-plugin-vue": "^9.26.0",
87
87
  "lodash": "^4.17.21",
88
88
  "prettier": "^3.2.5",
89
- "typescript": "^5.4.2",
90
- "unplugin-auto-import": "^0.17.5",
91
- "unplugin-vue-components": "^0.26.0",
92
- "vite": "^5.1.6",
93
- "vite-node": "^1.3.1",
94
- "vue": "^3.4.21"
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"