text-to-canvas 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/SECURITY.md +37 -0
- package/dist/text-to-canvas.cjs +569 -0
- package/dist/text-to-canvas.esm.min.js +335 -0
- package/dist/text-to-canvas.esm.min.js.map +1 -0
- package/dist/text-to-canvas.min.js +2 -0
- package/dist/text-to-canvas.min.js.map +1 -0
- package/dist/text-to-canvas.mjs +569 -0
- package/dist/text-to-canvas.umd.min.js +2 -0
- package/dist/text-to-canvas.umd.min.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/model.d.ts +217 -0
- package/dist/types/util/height.d.ts +25 -0
- package/dist/types/util/justify.d.ts +22 -0
- package/dist/types/util/split.d.ts +43 -0
- package/dist/types/util/style.d.ts +18 -0
- package/dist/types/util/trim.d.ts +22 -0
- package/dist/types/util/whitespace.d.ts +6 -0
- package/package.json +99 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const DEFAULT_FONT_FAMILY = "Arial";
|
|
4
|
+
const DEFAULT_FONT_SIZE = 14;
|
|
5
|
+
const DEFAULT_FONT_COLOR = "black";
|
|
6
|
+
const getTextFormat = (format, baseFormat) => {
|
|
7
|
+
return Object.assign(
|
|
8
|
+
{},
|
|
9
|
+
{
|
|
10
|
+
fontFamily: DEFAULT_FONT_FAMILY,
|
|
11
|
+
fontSize: DEFAULT_FONT_SIZE,
|
|
12
|
+
fontWeight: "400",
|
|
13
|
+
fontStyle: "",
|
|
14
|
+
fontVariant: "",
|
|
15
|
+
fontColor: DEFAULT_FONT_COLOR
|
|
16
|
+
},
|
|
17
|
+
baseFormat,
|
|
18
|
+
format
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
const getTextStyle = ({
|
|
22
|
+
fontFamily,
|
|
23
|
+
fontSize,
|
|
24
|
+
fontStyle,
|
|
25
|
+
fontVariant,
|
|
26
|
+
fontWeight
|
|
27
|
+
}) => {
|
|
28
|
+
return `${fontStyle || ""} ${fontVariant || ""} ${fontWeight || ""} ${fontSize ?? DEFAULT_FONT_SIZE}px ${fontFamily || DEFAULT_FONT_FAMILY}`.trim();
|
|
29
|
+
};
|
|
30
|
+
const isWhitespace = (text) => {
|
|
31
|
+
return !!text.match(/^\s+$/);
|
|
32
|
+
};
|
|
33
|
+
const _extractWords = (line) => {
|
|
34
|
+
return line.filter((word) => !isWhitespace(word.text));
|
|
35
|
+
};
|
|
36
|
+
const _cloneWord = (word) => {
|
|
37
|
+
const clone = { ...word };
|
|
38
|
+
if (word.format) {
|
|
39
|
+
clone.format = { ...word.format };
|
|
40
|
+
}
|
|
41
|
+
return clone;
|
|
42
|
+
};
|
|
43
|
+
const _joinWords = (words, joiner) => {
|
|
44
|
+
if (words.length <= 1 || joiner.length < 1) {
|
|
45
|
+
return [...words];
|
|
46
|
+
}
|
|
47
|
+
const phrase = [];
|
|
48
|
+
words.forEach((word, wordIdx) => {
|
|
49
|
+
phrase.push(word);
|
|
50
|
+
if (wordIdx < words.length - 1) {
|
|
51
|
+
joiner.forEach((jw) => phrase.push(_cloneWord(jw)));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return phrase;
|
|
55
|
+
};
|
|
56
|
+
const justifyLine = ({
|
|
57
|
+
line,
|
|
58
|
+
spaceWidth,
|
|
59
|
+
spaceChar,
|
|
60
|
+
boxWidth
|
|
61
|
+
}) => {
|
|
62
|
+
const words = _extractWords(line);
|
|
63
|
+
if (words.length <= 1) {
|
|
64
|
+
return line.concat();
|
|
65
|
+
}
|
|
66
|
+
const wordsWidth = words.reduce(
|
|
67
|
+
(width, word) => width + (word.metrics?.width ?? 0),
|
|
68
|
+
0
|
|
69
|
+
);
|
|
70
|
+
const noOfSpacesToInsert = (boxWidth - wordsWidth) / spaceWidth;
|
|
71
|
+
if (words.length > 2) {
|
|
72
|
+
const spacesPerWord = Math.ceil(noOfSpacesToInsert / (words.length - 1));
|
|
73
|
+
const spaces2 = Array.from({ length: spacesPerWord }, () => ({
|
|
74
|
+
text: spaceChar
|
|
75
|
+
}));
|
|
76
|
+
const firstWords = words.slice(0, words.length - 1);
|
|
77
|
+
const firstPart = _joinWords(firstWords, spaces2);
|
|
78
|
+
const remainingSpaces = spaces2.slice(
|
|
79
|
+
0,
|
|
80
|
+
Math.floor(noOfSpacesToInsert) - (firstWords.length - 1) * spaces2.length
|
|
81
|
+
);
|
|
82
|
+
const lastWord = words[words.length - 1];
|
|
83
|
+
return [...firstPart, ...remainingSpaces, lastWord];
|
|
84
|
+
}
|
|
85
|
+
const spaces = Array.from(
|
|
86
|
+
{ length: Math.floor(noOfSpacesToInsert) },
|
|
87
|
+
() => ({ text: spaceChar })
|
|
88
|
+
);
|
|
89
|
+
return _joinWords(words, spaces);
|
|
90
|
+
};
|
|
91
|
+
const trimLine = (line, side = "both") => {
|
|
92
|
+
let leftTrim = 0;
|
|
93
|
+
if (side === "left" || side === "both") {
|
|
94
|
+
for (; leftTrim < line.length; leftTrim++) {
|
|
95
|
+
if (!isWhitespace(line[leftTrim].text)) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (leftTrim >= line.length) {
|
|
100
|
+
return {
|
|
101
|
+
trimmedLeft: line.concat(),
|
|
102
|
+
trimmedRight: [],
|
|
103
|
+
trimmedLine: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let rightTrim = line.length;
|
|
108
|
+
if (side === "right" || side === "both") {
|
|
109
|
+
rightTrim--;
|
|
110
|
+
for (; rightTrim >= 0; rightTrim--) {
|
|
111
|
+
if (!isWhitespace(line[rightTrim].text)) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
rightTrim++;
|
|
116
|
+
if (rightTrim <= 0) {
|
|
117
|
+
return {
|
|
118
|
+
trimmedLeft: [],
|
|
119
|
+
trimmedRight: line.concat(),
|
|
120
|
+
trimmedLine: []
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
trimmedLeft: line.slice(0, leftTrim),
|
|
126
|
+
trimmedRight: line.slice(rightTrim),
|
|
127
|
+
trimmedLine: line.slice(leftTrim, rightTrim)
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const HAIR = " ";
|
|
131
|
+
const SPACE = " ";
|
|
132
|
+
let fontBoundingBoxSupported;
|
|
133
|
+
const _getWordHash = (word) => {
|
|
134
|
+
return `${word.text}${word.format ? JSON.stringify(word.format) : ""}`;
|
|
135
|
+
};
|
|
136
|
+
const _splitIntoLines = (words, inferWhitespace = true) => {
|
|
137
|
+
const lines = [[]];
|
|
138
|
+
let wasWhitespace = false;
|
|
139
|
+
words.forEach((word, wordIdx) => {
|
|
140
|
+
if (word.text.match(/^\n+$/)) {
|
|
141
|
+
for (let i = 0; i < word.text.length; i++) {
|
|
142
|
+
lines.push([]);
|
|
143
|
+
}
|
|
144
|
+
wasWhitespace = true;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (isWhitespace(word.text)) {
|
|
148
|
+
lines.at(-1)?.push(word);
|
|
149
|
+
wasWhitespace = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (word.text === "") {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (inferWhitespace && !wasWhitespace && wordIdx > 0) {
|
|
156
|
+
lines.at(-1)?.push({ text: SPACE });
|
|
157
|
+
}
|
|
158
|
+
lines.at(-1)?.push(word);
|
|
159
|
+
wasWhitespace = false;
|
|
160
|
+
});
|
|
161
|
+
return lines;
|
|
162
|
+
};
|
|
163
|
+
const _generateSpec = ({
|
|
164
|
+
wrappedLines,
|
|
165
|
+
wordMap,
|
|
166
|
+
positioning: {
|
|
167
|
+
width: boxWidth,
|
|
168
|
+
height: boxHeight,
|
|
169
|
+
x: boxX = 0,
|
|
170
|
+
y: boxY = 0,
|
|
171
|
+
align,
|
|
172
|
+
vAlign
|
|
173
|
+
}
|
|
174
|
+
}) => {
|
|
175
|
+
const xEnd = boxX + boxWidth;
|
|
176
|
+
const yEnd = boxY + boxHeight;
|
|
177
|
+
const getHeight = (word) => (
|
|
178
|
+
// NOTE: `metrics` must exist as every `word` MUST have been measured at this point
|
|
179
|
+
word.metrics.fontBoundingBoxAscent + word.metrics.fontBoundingBoxDescent
|
|
180
|
+
);
|
|
181
|
+
const lineHeights = wrappedLines.map(
|
|
182
|
+
(line) => line.reduce((acc, word) => {
|
|
183
|
+
return Math.max(acc, getHeight(word));
|
|
184
|
+
}, 0)
|
|
185
|
+
);
|
|
186
|
+
const totalHeight = lineHeights.reduce((acc, h) => acc + h, 0);
|
|
187
|
+
let lineY;
|
|
188
|
+
let textBaseline;
|
|
189
|
+
if (vAlign === "top") {
|
|
190
|
+
textBaseline = "top";
|
|
191
|
+
lineY = boxY;
|
|
192
|
+
} else if (vAlign === "bottom") {
|
|
193
|
+
textBaseline = "bottom";
|
|
194
|
+
lineY = yEnd - totalHeight;
|
|
195
|
+
} else {
|
|
196
|
+
textBaseline = "top";
|
|
197
|
+
lineY = boxY + boxHeight / 2 - totalHeight / 2;
|
|
198
|
+
}
|
|
199
|
+
const lines = wrappedLines.map((line, lineIdx) => {
|
|
200
|
+
const lineWidth = line.reduce(
|
|
201
|
+
// NOTE: `metrics` must exist as every `word` MUST have been measured at this point
|
|
202
|
+
(acc, word) => acc + word.metrics.width,
|
|
203
|
+
0
|
|
204
|
+
);
|
|
205
|
+
const lineHeight = lineHeights[lineIdx];
|
|
206
|
+
let lineX;
|
|
207
|
+
if (align === "right") {
|
|
208
|
+
lineX = xEnd - lineWidth;
|
|
209
|
+
} else if (align === "left") {
|
|
210
|
+
lineX = boxX;
|
|
211
|
+
} else {
|
|
212
|
+
lineX = boxX + boxWidth / 2 - lineWidth / 2;
|
|
213
|
+
}
|
|
214
|
+
let wordX = lineX;
|
|
215
|
+
const posWords = line.map((word) => {
|
|
216
|
+
const hash = _getWordHash(word);
|
|
217
|
+
const { format } = wordMap.get(hash);
|
|
218
|
+
const x = wordX;
|
|
219
|
+
const height = getHeight(word);
|
|
220
|
+
let y;
|
|
221
|
+
if (vAlign === "top") {
|
|
222
|
+
y = lineY;
|
|
223
|
+
} else if (vAlign === "bottom") {
|
|
224
|
+
y = lineY + lineHeight;
|
|
225
|
+
} else {
|
|
226
|
+
y = lineY + (lineHeight - height) / 2;
|
|
227
|
+
}
|
|
228
|
+
wordX += word.metrics.width;
|
|
229
|
+
return {
|
|
230
|
+
word,
|
|
231
|
+
format,
|
|
232
|
+
// undefined IF base formatting should be used when rendering (i.e. `word.format` is undefined)
|
|
233
|
+
x,
|
|
234
|
+
y,
|
|
235
|
+
width: word.metrics.width,
|
|
236
|
+
height,
|
|
237
|
+
isWhitespace: isWhitespace(word.text)
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
lineY += lineHeight;
|
|
241
|
+
return posWords;
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
lines,
|
|
245
|
+
textBaseline,
|
|
246
|
+
textAlign: "left",
|
|
247
|
+
// always per current algorithm
|
|
248
|
+
width: boxWidth,
|
|
249
|
+
height: totalHeight
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
const _jsonReplacer = function(key, value) {
|
|
253
|
+
if (key === "metrics" && value && typeof value === "object") {
|
|
254
|
+
const metrics = value;
|
|
255
|
+
return {
|
|
256
|
+
width: metrics.width,
|
|
257
|
+
fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,
|
|
258
|
+
fontBoundingBoxDescent: metrics.fontBoundingBoxDescent
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
};
|
|
263
|
+
const specToJson = (specs) => {
|
|
264
|
+
return JSON.stringify(specs, _jsonReplacer);
|
|
265
|
+
};
|
|
266
|
+
const wordsToJson = (words) => {
|
|
267
|
+
return JSON.stringify(words, _jsonReplacer);
|
|
268
|
+
};
|
|
269
|
+
const _measureWord = ({
|
|
270
|
+
ctx,
|
|
271
|
+
word,
|
|
272
|
+
wordMap,
|
|
273
|
+
baseTextFormat
|
|
274
|
+
}) => {
|
|
275
|
+
const hash = _getWordHash(word);
|
|
276
|
+
if (word.metrics) {
|
|
277
|
+
if (!wordMap.has(hash)) {
|
|
278
|
+
let format2 = void 0;
|
|
279
|
+
if (word.format) {
|
|
280
|
+
format2 = getTextFormat(word.format, baseTextFormat);
|
|
281
|
+
}
|
|
282
|
+
wordMap.set(hash, { metrics: word.metrics, format: format2 });
|
|
283
|
+
}
|
|
284
|
+
return word.metrics.width;
|
|
285
|
+
}
|
|
286
|
+
if (wordMap.has(hash)) {
|
|
287
|
+
const { metrics: metrics2 } = wordMap.get(hash);
|
|
288
|
+
word.metrics = metrics2;
|
|
289
|
+
return metrics2.width;
|
|
290
|
+
}
|
|
291
|
+
let ctxSaved = false;
|
|
292
|
+
let format = void 0;
|
|
293
|
+
if (word.format) {
|
|
294
|
+
ctx.save();
|
|
295
|
+
ctxSaved = true;
|
|
296
|
+
format = getTextFormat(word.format, baseTextFormat);
|
|
297
|
+
ctx.font = getTextStyle(format);
|
|
298
|
+
}
|
|
299
|
+
if (!fontBoundingBoxSupported) {
|
|
300
|
+
if (!ctxSaved) {
|
|
301
|
+
ctx.save();
|
|
302
|
+
ctxSaved = true;
|
|
303
|
+
}
|
|
304
|
+
ctx.textBaseline = "bottom";
|
|
305
|
+
}
|
|
306
|
+
const metrics = ctx.measureText(word.text);
|
|
307
|
+
if (typeof metrics.fontBoundingBoxAscent === "number") {
|
|
308
|
+
fontBoundingBoxSupported = true;
|
|
309
|
+
} else {
|
|
310
|
+
fontBoundingBoxSupported = false;
|
|
311
|
+
metrics.fontBoundingBoxAscent = metrics.actualBoundingBoxAscent;
|
|
312
|
+
metrics.fontBoundingBoxDescent = 0;
|
|
313
|
+
}
|
|
314
|
+
word.metrics = metrics;
|
|
315
|
+
wordMap.set(hash, { metrics, format });
|
|
316
|
+
if (ctxSaved) {
|
|
317
|
+
ctx.restore();
|
|
318
|
+
}
|
|
319
|
+
return metrics.width;
|
|
320
|
+
};
|
|
321
|
+
const splitWords = ({
|
|
322
|
+
ctx,
|
|
323
|
+
words,
|
|
324
|
+
justify,
|
|
325
|
+
format: baseFormat,
|
|
326
|
+
inferWhitespace = true,
|
|
327
|
+
...positioning
|
|
328
|
+
// rest of params are related to positioning
|
|
329
|
+
}) => {
|
|
330
|
+
const wordMap = /* @__PURE__ */ new Map();
|
|
331
|
+
const baseTextFormat = getTextFormat(baseFormat);
|
|
332
|
+
const { width: boxWidth } = positioning;
|
|
333
|
+
const measureLine = (lineWords, force = false) => {
|
|
334
|
+
let lineWidth = 0;
|
|
335
|
+
let splitPoint = 0;
|
|
336
|
+
lineWords.every((word, idx) => {
|
|
337
|
+
const wordWidth = _measureWord({ ctx, word, wordMap, baseTextFormat });
|
|
338
|
+
if (!force && lineWidth + wordWidth > boxWidth) {
|
|
339
|
+
if (idx === 0) {
|
|
340
|
+
splitPoint = 1;
|
|
341
|
+
lineWidth = wordWidth;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
splitPoint++;
|
|
346
|
+
lineWidth += wordWidth;
|
|
347
|
+
return true;
|
|
348
|
+
});
|
|
349
|
+
return { lineWidth, splitPoint };
|
|
350
|
+
};
|
|
351
|
+
ctx.save();
|
|
352
|
+
const hardLines = _splitIntoLines(
|
|
353
|
+
trimLine(words).trimmedLine,
|
|
354
|
+
inferWhitespace
|
|
355
|
+
);
|
|
356
|
+
if (hardLines.length <= 0 || boxWidth <= 0 || positioning.height <= 0 || baseFormat && typeof baseFormat.fontSize === "number" && baseFormat.fontSize <= 0) {
|
|
357
|
+
return {
|
|
358
|
+
lines: [],
|
|
359
|
+
textAlign: "center",
|
|
360
|
+
textBaseline: "middle",
|
|
361
|
+
width: positioning.width,
|
|
362
|
+
height: 0
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
ctx.font = getTextStyle(baseTextFormat);
|
|
366
|
+
const hairWidth = justify ? _measureWord({ ctx, word: { text: HAIR }, wordMap, baseTextFormat }) : 0;
|
|
367
|
+
const wrappedLines = [];
|
|
368
|
+
for (const hardLine of hardLines) {
|
|
369
|
+
let { splitPoint } = measureLine(hardLine);
|
|
370
|
+
if (splitPoint >= hardLine.length) {
|
|
371
|
+
wrappedLines.push(hardLine);
|
|
372
|
+
} else {
|
|
373
|
+
let softLine = hardLine.concat();
|
|
374
|
+
while (splitPoint < softLine.length) {
|
|
375
|
+
const splitLine = trimLine(
|
|
376
|
+
softLine.slice(0, splitPoint),
|
|
377
|
+
"right"
|
|
378
|
+
).trimmedLine;
|
|
379
|
+
wrappedLines.push(splitLine);
|
|
380
|
+
softLine = trimLine(softLine.slice(splitPoint), "left").trimmedLine;
|
|
381
|
+
({ splitPoint } = measureLine(softLine));
|
|
382
|
+
}
|
|
383
|
+
wrappedLines.push(softLine);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (justify && wrappedLines.length > 1) {
|
|
387
|
+
wrappedLines.forEach((wrappedLine, idx) => {
|
|
388
|
+
if (idx < wrappedLines.length - 1) {
|
|
389
|
+
const justifiedLine = justifyLine({
|
|
390
|
+
line: wrappedLine,
|
|
391
|
+
spaceWidth: hairWidth,
|
|
392
|
+
spaceChar: HAIR,
|
|
393
|
+
boxWidth
|
|
394
|
+
});
|
|
395
|
+
measureLine(justifiedLine, true);
|
|
396
|
+
wrappedLines[idx] = justifiedLine;
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
const spec = _generateSpec({
|
|
401
|
+
wrappedLines,
|
|
402
|
+
wordMap,
|
|
403
|
+
positioning
|
|
404
|
+
});
|
|
405
|
+
ctx.restore();
|
|
406
|
+
return spec;
|
|
407
|
+
};
|
|
408
|
+
const textToWords = (text) => {
|
|
409
|
+
const words = [];
|
|
410
|
+
let word = void 0;
|
|
411
|
+
let wasWhitespace = false;
|
|
412
|
+
Array.from(text.trim()).forEach((c) => {
|
|
413
|
+
const charIsWhitespace = isWhitespace(c);
|
|
414
|
+
if (charIsWhitespace && !wasWhitespace || !charIsWhitespace && wasWhitespace) {
|
|
415
|
+
wasWhitespace = charIsWhitespace;
|
|
416
|
+
if (word) {
|
|
417
|
+
words.push(word);
|
|
418
|
+
}
|
|
419
|
+
word = { text: c };
|
|
420
|
+
} else {
|
|
421
|
+
if (!word) {
|
|
422
|
+
word = { text: "" };
|
|
423
|
+
}
|
|
424
|
+
word.text += c;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
if (word) {
|
|
428
|
+
words.push(word);
|
|
429
|
+
}
|
|
430
|
+
return words;
|
|
431
|
+
};
|
|
432
|
+
const splitText = ({ text, ...params }) => {
|
|
433
|
+
const words = textToWords(text);
|
|
434
|
+
const results = splitWords({
|
|
435
|
+
...params,
|
|
436
|
+
words,
|
|
437
|
+
inferWhitespace: false
|
|
438
|
+
});
|
|
439
|
+
return results.lines.map(
|
|
440
|
+
(line) => line.map(({ word: { text: t } }) => t).join("")
|
|
441
|
+
);
|
|
442
|
+
};
|
|
443
|
+
const _getHeight = (ctx, text, style) => {
|
|
444
|
+
const previousTextBaseline = ctx.textBaseline;
|
|
445
|
+
const previousFont = ctx.font;
|
|
446
|
+
ctx.textBaseline = "bottom";
|
|
447
|
+
if (style) {
|
|
448
|
+
ctx.font = style;
|
|
449
|
+
}
|
|
450
|
+
const { actualBoundingBoxAscent: height } = ctx.measureText(text);
|
|
451
|
+
ctx.textBaseline = previousTextBaseline;
|
|
452
|
+
if (style) {
|
|
453
|
+
ctx.font = previousFont;
|
|
454
|
+
}
|
|
455
|
+
return height;
|
|
456
|
+
};
|
|
457
|
+
const getWordHeight = ({
|
|
458
|
+
ctx,
|
|
459
|
+
word
|
|
460
|
+
}) => {
|
|
461
|
+
return _getHeight(ctx, word.text, word.format && getTextStyle(word.format));
|
|
462
|
+
};
|
|
463
|
+
const getTextHeight = ({
|
|
464
|
+
ctx,
|
|
465
|
+
text,
|
|
466
|
+
style
|
|
467
|
+
}) => {
|
|
468
|
+
return _getHeight(ctx, text, style);
|
|
469
|
+
};
|
|
470
|
+
const drawText = (ctx, text, config) => {
|
|
471
|
+
const baseFormat = getTextFormat({
|
|
472
|
+
fontFamily: config.fontFamily,
|
|
473
|
+
fontSize: config.fontSize,
|
|
474
|
+
fontStyle: config.fontStyle,
|
|
475
|
+
fontVariant: config.fontVariant,
|
|
476
|
+
fontWeight: config.fontWeight
|
|
477
|
+
});
|
|
478
|
+
const {
|
|
479
|
+
lines: richLines,
|
|
480
|
+
height: totalHeight,
|
|
481
|
+
textBaseline,
|
|
482
|
+
textAlign
|
|
483
|
+
} = splitWords({
|
|
484
|
+
ctx,
|
|
485
|
+
words: Array.isArray(text) ? text : textToWords(text),
|
|
486
|
+
inferWhitespace: Array.isArray(text) ? config.inferWhitespace === void 0 || config.inferWhitespace : void 0,
|
|
487
|
+
// ignore since `text` is a string; we assume it already has all the whitespace it needs
|
|
488
|
+
x: config.x || 0,
|
|
489
|
+
y: config.y || 0,
|
|
490
|
+
width: config.width,
|
|
491
|
+
height: config.height,
|
|
492
|
+
align: config.align,
|
|
493
|
+
vAlign: config.vAlign,
|
|
494
|
+
justify: config.justify,
|
|
495
|
+
format: baseFormat
|
|
496
|
+
});
|
|
497
|
+
ctx.save();
|
|
498
|
+
ctx.textAlign = textAlign;
|
|
499
|
+
ctx.textBaseline = textBaseline;
|
|
500
|
+
ctx.font = getTextStyle(baseFormat);
|
|
501
|
+
ctx.fillStyle = baseFormat.fontColor || DEFAULT_FONT_COLOR;
|
|
502
|
+
richLines.forEach((line) => {
|
|
503
|
+
line.forEach((pw) => {
|
|
504
|
+
if (!pw.isWhitespace) {
|
|
505
|
+
if (pw.format) {
|
|
506
|
+
ctx.save();
|
|
507
|
+
ctx.font = getTextStyle(pw.format);
|
|
508
|
+
if (pw.format.fontColor) {
|
|
509
|
+
ctx.fillStyle = pw.format.fontColor;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
ctx.fillText(pw.word.text, pw.x, pw.y);
|
|
513
|
+
if (pw.format) {
|
|
514
|
+
ctx.restore();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
if (config.debug) {
|
|
520
|
+
const { width, height, x = 0, y = 0 } = config;
|
|
521
|
+
const xEnd = x + width;
|
|
522
|
+
const yEnd = y + height;
|
|
523
|
+
let textAnchor;
|
|
524
|
+
if (config.align === "right") {
|
|
525
|
+
textAnchor = xEnd;
|
|
526
|
+
} else if (config.align === "left") {
|
|
527
|
+
textAnchor = x;
|
|
528
|
+
} else {
|
|
529
|
+
textAnchor = x + width / 2;
|
|
530
|
+
}
|
|
531
|
+
let debugY = y;
|
|
532
|
+
if (config.vAlign === "bottom") {
|
|
533
|
+
debugY = yEnd;
|
|
534
|
+
} else if (config.vAlign === "middle") {
|
|
535
|
+
debugY = y + height / 2;
|
|
536
|
+
}
|
|
537
|
+
const debugColor = "#0C8CE9";
|
|
538
|
+
ctx.lineWidth = 1;
|
|
539
|
+
ctx.strokeStyle = debugColor;
|
|
540
|
+
ctx.strokeRect(x, y, width, height);
|
|
541
|
+
ctx.lineWidth = 1;
|
|
542
|
+
if (!config.align || config.align === "center") {
|
|
543
|
+
ctx.strokeStyle = debugColor;
|
|
544
|
+
ctx.beginPath();
|
|
545
|
+
ctx.moveTo(textAnchor, y);
|
|
546
|
+
ctx.lineTo(textAnchor, yEnd);
|
|
547
|
+
ctx.stroke();
|
|
548
|
+
}
|
|
549
|
+
if (!config.vAlign || config.vAlign === "middle") {
|
|
550
|
+
ctx.strokeStyle = debugColor;
|
|
551
|
+
ctx.beginPath();
|
|
552
|
+
ctx.moveTo(x, debugY);
|
|
553
|
+
ctx.lineTo(xEnd, debugY);
|
|
554
|
+
ctx.stroke();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
ctx.restore();
|
|
558
|
+
return { height: totalHeight };
|
|
559
|
+
};
|
|
560
|
+
exports.drawText = drawText;
|
|
561
|
+
exports.getTextFormat = getTextFormat;
|
|
562
|
+
exports.getTextHeight = getTextHeight;
|
|
563
|
+
exports.getTextStyle = getTextStyle;
|
|
564
|
+
exports.getWordHeight = getWordHeight;
|
|
565
|
+
exports.specToJson = specToJson;
|
|
566
|
+
exports.splitText = splitText;
|
|
567
|
+
exports.splitWords = splitWords;
|
|
568
|
+
exports.textToWords = textToWords;
|
|
569
|
+
exports.wordsToJson = wordsToJson;
|