UNPKG

12.2 kBJavaScriptView Raw
1import { __decorate, __metadata, __read, __spreadArray } from "tslib";
2import { singleton, inject } from 'mana-syringe';
3import { toFontString } from '../utils';
4import { Rectangle } from '../shapes';
5import { OffscreenCanvasCreator } from './OffscreenCanvasCreator';
6var TEXT_METRICS = {
7 MetricsString: '|ÉqÅ',
8 BaselineSymbol: 'M',
9 BaselineMultiplier: 1.4,
10 HeightMultiplier: 2,
11 Newlines: [0x000a, 0x000d // carriage return
12 ],
13 BreakingSpaces: [0x0009, 0x0020, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2008, 0x2009, 0x200a, 0x205f, 0x3000 // ideographic space
14 ]
15};
16var LATIN_REGEX = /[a-zA-Z0-9\u00C0-\u00D6\u00D8-\u00f6\u00f8-\u00ff!"#$%&'()*+,-./:;]/; // Line breaking rules in CJK (Kinsoku Shori)
17// Refer from https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages
18
19var regexCannotStartZhCn = /[!%),.:;?\]}¢°·'""†‡›℃∶、。〃〆〕〗〞﹚﹜!"%'),.:;?!]}~]/;
20var regexCannotEndZhCn = /[$(£¥·'"〈《「『【〔〖〝﹙﹛$(.[{£¥]/;
21var regexCannotStartZhTw = /[!),.:;?\]}¢·–—'"•"、。〆〞〕〉》」︰︱︲︳﹐﹑﹒﹓﹔﹕﹖﹘﹚﹜!),.:;?︶︸︺︼︾﹀﹂﹗]|}、]/;
22var regexCannotEndZhTw = /[([{£¥'"‵〈《「『〔〝︴﹙﹛({︵︷︹︻︽︿﹁﹃﹏]/;
23var regexCannotStartJaJp = /[)\]}〕〉》」』】〙〗〟'"⦆»ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻‐゠–〜?!‼⁇⁈⁉・、:;,。.]/;
24var regexCannotEndJaJp = /[([{〔〈《「『【〘〖〝'"⦅«—...‥〳〴〵]/;
25var regexCannotStartKoKr = /[!%),.:;?\]}¢°'"†‡℃〆〈《「『〕!%),.:;?]}]/;
26var regexCannotEndKoKr = /[$([{£¥'"々〇〉》」〔$([{⦆¥₩#]/;
27var regexCannotStart = new RegExp("".concat(regexCannotStartZhCn.source, "|").concat(regexCannotStartZhTw.source, "|").concat(regexCannotStartJaJp.source, "|").concat(regexCannotStartKoKr.source));
28var regexCannotEnd = new RegExp("".concat(regexCannotEndZhCn.source, "|").concat(regexCannotEndZhTw.source, "|").concat(regexCannotEndJaJp.source, "|").concat(regexCannotEndKoKr.source));
29
30var TextService =
31/** @class */
32function () {
33 function TextService() {
34 var _this = this;
35
36 this.cache = {};
37
38 this.shouldBreakByKinsokuShorui = function (char, nextChar) {
39 if (_this.isBreakingSpace(nextChar)) return false;
40
41 if (char) {
42 // Line breaking rules in CJK (Kinsoku Shori)
43 if (regexCannotEnd.exec(nextChar) || regexCannotStart.exec(char)) {
44 return true;
45 }
46 }
47
48 return false;
49 };
50
51 this.trimByKinsokuShorui = function (prev) {
52 var next = __spreadArray([], __read(prev), false);
53
54 var prevLine = next[next.length - 2];
55
56 if (!prevLine) {
57 return prev;
58 }
59
60 var lastChar = prevLine[prevLine.length - 1];
61 next[next.length - 2] = prevLine.slice(0, -1);
62 next[next.length - 1] = lastChar + next[next.length - 1];
63 return next;
64 };
65 }
66
67 TextService.prototype.measureFont = function (font, offscreenCanvas) {
68 // as this method is used for preparing assets, don't recalculate things if we don't need to
69 if (this.cache[font]) {
70 return this.cache[font];
71 }
72
73 var properties = {
74 ascent: 0,
75 descent: 0,
76 fontSize: 0
77 };
78 var canvas = this.offscreenCanvas.getOrCreateCanvas(offscreenCanvas);
79 var context = this.offscreenCanvas.getOrCreateContext(offscreenCanvas);
80 context.font = font;
81 var metricsString = TEXT_METRICS.MetricsString + TEXT_METRICS.BaselineSymbol;
82 var width = Math.ceil(context.measureText(metricsString).width);
83 var baseline = Math.ceil(context.measureText(TEXT_METRICS.BaselineSymbol).width);
84 var height = TEXT_METRICS.HeightMultiplier * baseline;
85 baseline = baseline * TEXT_METRICS.BaselineMultiplier | 0; // @ts-ignore
86
87 canvas.width = width; // @ts-ignore
88
89 canvas.height = height;
90 context.fillStyle = '#f00';
91 context.fillRect(0, 0, width, height);
92 context.font = font;
93 context.textBaseline = 'alphabetic';
94 context.fillStyle = '#000';
95 context.fillText(metricsString, 0, baseline);
96 var imagedata = context.getImageData(0, 0, width || 1, height || 1).data;
97 var pixels = imagedata.length;
98 var line = width * 4;
99 var i = 0;
100 var idx = 0;
101 var stop = false; // ascent. scan from top to bottom until we find a non red pixel
102
103 for (i = 0; i < baseline; ++i) {
104 for (var j = 0; j < line; j += 4) {
105 if (imagedata[idx + j] !== 255) {
106 stop = true;
107 break;
108 }
109 }
110
111 if (!stop) {
112 idx += line;
113 } else {
114 break;
115 }
116 }
117
118 properties.ascent = baseline - i;
119 idx = pixels - line;
120 stop = false; // descent. scan from bottom to top until we find a non red pixel
121
122 for (i = height; i > baseline; --i) {
123 for (var j = 0; j < line; j += 4) {
124 if (imagedata[idx + j] !== 255) {
125 stop = true;
126 break;
127 }
128 }
129
130 if (!stop) {
131 idx -= line;
132 } else {
133 break;
134 }
135 }
136
137 properties.descent = i - baseline;
138 properties.fontSize = properties.ascent + properties.descent;
139 this.cache[font] = properties;
140 return properties;
141 };
142
143 TextService.prototype.measureText = function (text, parsedStyle, offscreenCanvas) {
144 var fontSize = parsedStyle.fontSize,
145 wordWrap = parsedStyle.wordWrap,
146 _a = parsedStyle.lineHeight,
147 strokeHeight = _a === void 0 ? 0 : _a,
148 lineWidth = parsedStyle.lineWidth,
149 textBaseline = parsedStyle.textBaseline,
150 textAlign = parsedStyle.textAlign,
151 _b = parsedStyle.letterSpacing,
152 letterSpacing = _b === void 0 ? 0 : _b,
153 // dropShadow = 0,
154 // dropShadowDistance = 0,
155 _c = parsedStyle.leading,
156 // dropShadow = 0,
157 // dropShadowDistance = 0,
158 leading = _c === void 0 ? 0 : _c;
159 var font = toFontString(parsedStyle);
160 var fontProperties = this.measureFont(font, offscreenCanvas); // fallback in case UA disallow canvas data extraction
161 // (toDataURI, getImageData functions)
162
163 if (fontProperties.fontSize === 0) {
164 fontProperties.fontSize = fontSize.value;
165 fontProperties.ascent = fontSize.value;
166 }
167
168 var context = this.offscreenCanvas.getOrCreateContext(offscreenCanvas);
169 context.font = font;
170 var outputText = wordWrap ? this.wordWrap(text, parsedStyle, offscreenCanvas) : text;
171 var lines = outputText.split(/(?:\r\n|\r|\n)/);
172 var lineWidths = new Array(lines.length);
173 var maxLineWidth = 0;
174
175 for (var i = 0; i < lines.length; i++) {
176 var lineWidth_1 = context.measureText(lines[i]).width + (lines[i].length - 1) * letterSpacing;
177 lineWidths[i] = lineWidth_1;
178 maxLineWidth = Math.max(maxLineWidth, lineWidth_1);
179 }
180
181 var width = maxLineWidth + lineWidth.value; // if (dropShadow) {
182 // width += dropShadowDistance;
183 // }
184
185 var lineHeight = strokeHeight || fontProperties.fontSize + lineWidth.value;
186 var height = Math.max(lineHeight, fontProperties.fontSize + lineWidth.value) + (lines.length - 1) * (lineHeight + leading); // if (dropShadow) {
187 // height += dropShadowDistance;
188 // }
189
190 lineHeight += leading; // handle vertical text baseline
191
192 var offsetY = 0;
193
194 if (textBaseline.value === 'middle') {
195 offsetY = -height / 2;
196 } else if (textBaseline.value === 'bottom' || textBaseline.value === 'alphabetic' || textBaseline.value === 'ideographic') {
197 offsetY = -height;
198 } else if (textBaseline.value === 'top' || textBaseline.value === 'hanging') {
199 offsetY = 0;
200 }
201
202 return {
203 font: font,
204 width: width,
205 height: height,
206 lines: lines,
207 lineWidths: lineWidths,
208 lineHeight: lineHeight,
209 maxLineWidth: maxLineWidth,
210 fontProperties: fontProperties,
211 lineMetrics: lineWidths.map(function (width, i) {
212 var offsetX = 0; // handle horizontal text align
213
214 if (textAlign.value === 'center') {
215 offsetX -= width / 2;
216 } else if (textAlign.value === 'right' || textAlign.value === 'end') {
217 offsetX -= width;
218 }
219
220 return new Rectangle(offsetX - lineWidth.value / 2, offsetY + i * lineHeight, width + lineWidth.value, lineHeight);
221 })
222 };
223 };
224
225 TextService.prototype.wordWrap = function (text, _a, offscreenCanvas) {
226 var _this = this;
227
228 var _b = _a.wordWrapWidth,
229 wordWrapWidth = _b === void 0 ? 0 : _b,
230 _c = _a.letterSpacing,
231 letterSpacing = _c === void 0 ? 0 : _c;
232 var context = this.offscreenCanvas.getOrCreateContext(offscreenCanvas);
233 var maxWidth = wordWrapWidth + letterSpacing;
234 var lines = [];
235 var currentIndex = 0;
236 var currentWidth = 0;
237 var cache = {};
238
239 var calcWidth = function calcWidth(char) {
240 return _this.getFromCache(char, letterSpacing, cache, context);
241 };
242
243 Array.from(text).forEach(function (char, i) {
244 var prevChar = text[i - 1];
245 var nextChar = text[i + 1];
246 var width = calcWidth(char);
247
248 if (_this.isNewline(char)) {
249 currentIndex++;
250 currentWidth = 0;
251 lines[currentIndex] = '';
252 return;
253 }
254
255 if (currentWidth > 0 && currentWidth + width > maxWidth) {
256 currentIndex++;
257 currentWidth = 0;
258 lines[currentIndex] = '';
259
260 if (_this.isBreakingSpace(char)) {
261 return;
262 }
263
264 if (!_this.canBreakInLastChar(char)) {
265 lines = _this.trimToBreakable(lines);
266 currentWidth = _this.sumTextWidthByCache(lines[currentIndex] || '', cache);
267 }
268
269 if (_this.shouldBreakByKinsokuShorui(char, nextChar)) {
270 lines = _this.trimByKinsokuShorui(lines);
271 currentWidth += calcWidth(prevChar || '');
272 }
273 }
274
275 currentWidth += width;
276 lines[currentIndex] = (lines[currentIndex] || '') + char;
277 });
278 return lines.join('\n');
279 };
280
281 TextService.prototype.isBreakingSpace = function (char) {
282 if (typeof char !== 'string') {
283 return false;
284 }
285
286 return TEXT_METRICS.BreakingSpaces.indexOf(char.charCodeAt(0)) >= 0;
287 };
288
289 TextService.prototype.isNewline = function (char) {
290 if (typeof char !== 'string') {
291 return false;
292 }
293
294 return TEXT_METRICS.Newlines.indexOf(char.charCodeAt(0)) >= 0;
295 };
296
297 TextService.prototype.trimToBreakable = function (prev) {
298 var next = __spreadArray([], __read(prev), false);
299
300 var prevLine = next[next.length - 2];
301 var index = this.findBreakableIndex(prevLine);
302 if (index === -1 || !prevLine) return next;
303 var trimmedChar = prevLine.slice(index, index + 1);
304 var isTrimmedWithSpace = this.isBreakingSpace(trimmedChar);
305 var trimFrom = index + 1;
306 var trimTo = index + (isTrimmedWithSpace ? 0 : 1);
307 next[next.length - 1] += prevLine.slice(trimFrom, prevLine.length);
308 next[next.length - 2] = prevLine.slice(0, trimTo);
309 return next;
310 };
311
312 TextService.prototype.canBreakInLastChar = function (char) {
313 if (char && LATIN_REGEX.test(char)) return false;
314 return true;
315 };
316
317 TextService.prototype.sumTextWidthByCache = function (text, cache) {
318 return text.split('').reduce(function (sum, c) {
319 if (!cache[c]) throw Error('cannot count the word without cache');
320 return sum + cache[c];
321 }, 0);
322 };
323
324 TextService.prototype.findBreakableIndex = function (line) {
325 for (var i = line.length - 1; i >= 0; i--) {
326 if (!LATIN_REGEX.test(line[i])) return i;
327 }
328
329 return -1;
330 };
331
332 TextService.prototype.getFromCache = function (key, letterSpacing, cache, context) {
333 var width = cache[key];
334
335 if (typeof width !== 'number') {
336 var spacing = key.length * letterSpacing;
337 width = context.measureText(key).width + spacing;
338 cache[key] = width;
339 }
340
341 return width;
342 };
343
344 __decorate([inject(OffscreenCanvasCreator), __metadata("design:type", OffscreenCanvasCreator)], TextService.prototype, "offscreenCanvas", void 0);
345
346 TextService = __decorate([singleton()], TextService);
347 return TextService;
348}();
349
350export { TextService };
\No newline at end of file