- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
- 539
- 540
- 541
- 542
- 543
- 544
- 545
- 546
- 547
- 548
- 549
- 550
- 551
- 552
- 553
- 554
- 555
- 556
- 557
- 558
- 559
- 560
- 561
- 562
- 563
- 564
- 565
- 566
- 567
- 568
- 569
- 570
- 571
- 572
- 573
- 574
- 575
- 576
- 577
- 578
- 579
- 580
- 581
- 582
- 583
- 584
- 585
- 586
- 587
- 588
- 589
- 590
- 591
- 592
- 593
- 594
- 595
- 596
- 597
- 598
- 599
- 600
- 601
- 602
- 603
- 604
- 605
- 606
- 607
- 608
- 609
- 610
- 611
- 612
- 613
- 614
- 615
- 616
- 617
- 618
- 619
- 620
- 621
- 622
- 623
- 624
- 625
- 626
- 627
- 628
- 629
- 630
- 631
- 632
- 633
- 634
- 635
- 636
- 637
- 638
- 639
- 640
- 641
- 642
- 643
- 644
- 645
- 646
- 647
- 648
- 649
- 650
- 651
- 652
- 653
- 654
- 655
- 656
- 657
- 658
- 659
- 660
- 661
- 662
- 663
- 664
- 665
- 666
- 667
- 668
- 669
- 670
- 671
- 672
- 673
- 674
- 675
- 676
- 677
- 678
- 679
- 680
- 681
- 682
- 683
- 684
- 685
- 686
- 687
- 688
- 689
- 690
- 691
- 692
- 693
- 694
- 695
- 696
- 697
- 698
- 699
- 700
- 701
- 702
- 703
- 704
- 705
- 706
- 707
- 708
- 709
- 710
- 711
- 712
- 713
- 714
- 715
- 716
- 717
- 718
- 719
- 720
- 721
- 722
- 723
- 724
- 725
- 726
- 727
- 728
- 729
- 730
- 731
- 732
- 733
- 734
- 735
- 736
- 737
- 738
- 739
- 740
- 741
- 742
- 743
- 744
- 745
- 746
- 747
- 748
- 749
- 750
- 751
- 752
- 753
- 754
- 755
- 756
- 757
- 758
- 759
- 760
- 761
- 762
- 763
- 764
- 765
- 766
- 767
- 768
- 769
- 770
- 771
- 772
- 773
- 774
- 775
- 776
- 777
- 778
- 779
- 780
- 781
- 782
- 783
- 784
- 785
- 786
- 787
- 788
- 789
- 790
- 791
- 792
- 793
- 794
- 795
- 796
- 797
- 798
- 799
- 800
- 801
- 802
- 803
- 804
- 805
- 806
- 807
- 808
- 809
- 810
- 811
- 812
- 813
- 814
- 815
- 816
- 817
- 818
- 819
- 820
- 821
- 822
- 823
- 824
- 825
- 826
- 827
- 828
- 829
- 830
- 831
- 832
- 833
- 834
- 835
- 836
- 837
- 838
- 839
- 840
- 841
- 842
- 843
- 844
- 845
- 846
- 847
- 848
- 849
- 850
- 851
- 852
- 853
- 854
- 855
- 856
import { settings } from '@pixi/settings';
import type { TextStyle, TextStyleWhiteSpace } from './TextStyle';
interface IFontMetrics
{
ascent: number;
descent: number;
fontSize: number;
}
type CharacterWidthCache = { [key: string]: number };
/**
* The TextMetrics object represents the measurement of a block of text with a specified style.
*
* ```js
* let style = new PIXI.TextStyle({fontFamily : 'Arial', fontSize: 24, fill : 0xff1010, align : 'center'})
* let textMetrics = PIXI.TextMetrics.measureText('Your text', style)
* ```
* @memberof PIXI
*/
export class TextMetrics
{
/** The text that was measured. */
public text: string;
/** The style that was measured. */
public style: TextStyle;
/** The measured width of the text. */
public width: number;
/** The measured height of the text. */
public height: number;
/** An array of lines of the text broken by new lines and wrapping is specified in style. */
public lines: string[];
/** An array of the line widths for each line matched to `lines`. */
public lineWidths: number[];
/** The measured line height for this style. */
public lineHeight: number;
/** The maximum line width for all measured lines. */
public maxLineWidth: number;
/**
* The font properties object from TextMetrics.measureFont.
* @type {PIXI.IFontMetrics}
*/
public fontProperties: IFontMetrics;
public static METRICS_STRING: string;
public static BASELINE_SYMBOL: string;
public static BASELINE_MULTIPLIER: number;
public static HEIGHT_MULTIPLIER: number;
private static __canvas: HTMLCanvasElement | OffscreenCanvas;
private static __context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
// TODO: These should be protected but they're initialized outside of the class.
public static _fonts: { [font: string]: IFontMetrics };
public static _newlines: number[];
public static _breakingSpaces: number[];
/**
* @param text - the text that was measured
* @param style - the style that was measured
* @param width - the measured width of the text
* @param height - the measured height of the text
* @param lines - an array of the lines of text broken by new lines and wrapping if specified in style
* @param lineWidths - an array of the line widths for each line matched to `lines`
* @param lineHeight - the measured line height for this style
* @param maxLineWidth - the maximum line width for all measured lines
* @param {PIXI.IFontMetrics} fontProperties - the font properties object from TextMetrics.measureFont
*/
constructor(text: string, style: TextStyle, width: number, height: number, lines: string[], lineWidths: number[],
lineHeight: number, maxLineWidth: number, fontProperties: IFontMetrics)
{
this.text = text;
this.style = style;
this.width = width;
this.height = height;
this.lines = lines;
this.lineWidths = lineWidths;
this.lineHeight = lineHeight;
this.maxLineWidth = maxLineWidth;
this.fontProperties = fontProperties;
}
/**
* Measures the supplied string of text and returns a Rectangle.
* @param text - The text to measure.
* @param style - The text style to use for measuring
* @param wordWrap - Override for if word-wrap should be applied to the text.
* @param canvas - optional specification of the canvas to use for measuring.
* @returns Measured width and height of the text.
*/
public static measureText(
text: string,
style: TextStyle,
wordWrap?: boolean,
canvas: HTMLCanvasElement | OffscreenCanvas = TextMetrics._canvas
): TextMetrics
{
wordWrap = (wordWrap === undefined || wordWrap === null) ? style.wordWrap : wordWrap;
const font = style.toFontString();
const fontProperties = TextMetrics.measureFont(font);
// fallback in case UA disallow canvas data extraction
// (toDataURI, getImageData functions)
if (fontProperties.fontSize === 0)
{
fontProperties.fontSize = style.fontSize as number;
fontProperties.ascent = style.fontSize as number;
}
const context = canvas.getContext('2d');
context.font = font;
const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text;
const lines = outputText.split(/(?:\r\n|\r|\n)/);
const lineWidths = new Array<number>(lines.length);
let maxLineWidth = 0;
for (let i = 0; i < lines.length; i++)
{
const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing);
lineWidths[i] = lineWidth;
maxLineWidth = Math.max(maxLineWidth, lineWidth);
}
let width = maxLineWidth + style.strokeThickness;
if (style.dropShadow)
{
width += style.dropShadowDistance;
}
const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness;
let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness)
+ ((lines.length - 1) * (lineHeight + style.leading));
if (style.dropShadow)
{
height += style.dropShadowDistance;
}
return new TextMetrics(
text,
style,
width,
height,
lines,
lineWidths,
lineHeight + style.leading,
maxLineWidth,
fontProperties
);
}
/**
* Applies newlines to a string to have it optimally fit into the horizontal
* bounds set by the Text object's wordWrapWidth property.
* @param text - String to apply word wrapping to
* @param style - the style to use when wrapping
* @param canvas - optional specification of the canvas to use for measuring.
* @returns New string with new lines applied where required
*/
private static wordWrap(
text: string,
style: TextStyle,
canvas: HTMLCanvasElement | OffscreenCanvas = TextMetrics._canvas
): string
{
const context = canvas.getContext('2d');
let width = 0;
let line = '';
let lines = '';
const cache: CharacterWidthCache = Object.create(null);
const { letterSpacing, whiteSpace } = style;
// How to handle whitespaces
const collapseSpaces = TextMetrics.collapseSpaces(whiteSpace);
const collapseNewlines = TextMetrics.collapseNewlines(whiteSpace);
// whether or not spaces may be added to the beginning of lines
let canPrependSpaces = !collapseSpaces;
// There is letterSpacing after every char except the last one
// t_h_i_s_' '_i_s_' '_a_n_' '_e_x_a_m_p_l_e_' '_!
// so for convenience the above needs to be compared to width + 1 extra letterSpace
// t_h_i_s_' '_i_s_' '_a_n_' '_e_x_a_m_p_l_e_' '_!_
// ________________________________________________
// And then the final space is simply no appended to each line
const wordWrapWidth = style.wordWrapWidth + letterSpacing;
// break text into words, spaces and newline chars
const tokens = TextMetrics.tokenize(text);
for (let i = 0; i < tokens.length; i++)
{
// get the word, space or newlineChar
let token = tokens[i];
// if word is a new line
if (TextMetrics.isNewline(token))
{
// keep the new line
if (!collapseNewlines)
{
lines += TextMetrics.addLine(line);
canPrependSpaces = !collapseSpaces;
line = '';
width = 0;
continue;
}
// if we should collapse new lines
// we simply convert it into a space
token = ' ';
}
// if we should collapse repeated whitespaces
if (collapseSpaces)
{
// check both this and the last tokens for spaces
const currIsBreakingSpace = TextMetrics.isBreakingSpace(token);
const lastIsBreakingSpace = TextMetrics.isBreakingSpace(line[line.length - 1]);
if (currIsBreakingSpace && lastIsBreakingSpace)
{
continue;
}
}
// get word width from cache if possible
const tokenWidth = TextMetrics.getFromCache(token, letterSpacing, cache, context);
// word is longer than desired bounds
if (tokenWidth > wordWrapWidth)
{
// if we are not already at the beginning of a line
if (line !== '')
{
// start newlines for overflow words
lines += TextMetrics.addLine(line);
line = '';
width = 0;
}
// break large word over multiple lines
if (TextMetrics.canBreakWords(token, style.breakWords))
{
// break word into characters
const characters = TextMetrics.wordWrapSplit(token);
// loop the characters
for (let j = 0; j < characters.length; j++)
{
let char = characters[j];
let k = 1;
// we are not at the end of the token
while (characters[j + k])
{
const nextChar = characters[j + k];
const lastChar = char[char.length - 1];
// should not split chars
if (!TextMetrics.canBreakChars(lastChar, nextChar, token, j, style.breakWords))
{
// combine chars & move forward one
char += nextChar;
}
else
{
break;
}
k++;
}
j += char.length - 1;
const characterWidth = TextMetrics.getFromCache(char, letterSpacing, cache, context);
if (characterWidth + width > wordWrapWidth)
{
lines += TextMetrics.addLine(line);
canPrependSpaces = false;
line = '';
width = 0;
}
line += char;
width += characterWidth;
}
}
// run word out of the bounds
else
{
// if there are words in this line already
// finish that line and start a new one
if (line.length > 0)
{
lines += TextMetrics.addLine(line);
line = '';
width = 0;
}
const isLastToken = i === tokens.length - 1;
// give it its own line if it's not the end
lines += TextMetrics.addLine(token, !isLastToken);
canPrependSpaces = false;
line = '';
width = 0;
}
}
// word could fit
else
{
// word won't fit because of existing words
// start a new line
if (tokenWidth + width > wordWrapWidth)
{
// if its a space we don't want it
canPrependSpaces = false;
// add a new line
lines += TextMetrics.addLine(line);
// start a new line
line = '';
width = 0;
}
// don't add spaces to the beginning of lines
if (line.length > 0 || !TextMetrics.isBreakingSpace(token) || canPrependSpaces)
{
// add the word to the current line
line += token;
// update width counter
width += tokenWidth;
}
}
}
lines += TextMetrics.addLine(line, false);
return lines;
}
/**
* Convienience function for logging each line added during the wordWrap method.
* @param line - The line of text to add
* @param newLine - Add new line character to end
* @returns A formatted line
*/
private static addLine(line: string, newLine = true): string
{
line = TextMetrics.trimRight(line);
line = (newLine) ? `${line}\n` : line;
return line;
}
/**
* Gets & sets the widths of calculated characters in a cache object
* @param key - The key
* @param letterSpacing - The letter spacing
* @param cache - The cache
* @param context - The canvas context
* @returns The from cache.
*/
private static getFromCache(key: string, letterSpacing: number, cache: CharacterWidthCache,
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D): number
{
let width = cache[key];
if (typeof width !== 'number')
{
const spacing = ((key.length) * letterSpacing);
width = context.measureText(key).width + spacing;
cache[key] = width;
}
return width;
}
/**
* Determines whether we should collapse breaking spaces.
* @param whiteSpace - The TextStyle property whiteSpace
* @returns Should collapse
*/
private static collapseSpaces(whiteSpace: TextStyleWhiteSpace): boolean
{
return (whiteSpace === 'normal' || whiteSpace === 'pre-line');
}
/**
* Determines whether we should collapse newLine chars.
* @param whiteSpace - The white space
* @returns should collapse
*/
private static collapseNewlines(whiteSpace: TextStyleWhiteSpace): boolean
{
return (whiteSpace === 'normal');
}
/**
* Trims breaking whitespaces from string.
* @param text - The text
* @returns Trimmed string
*/
private static trimRight(text: string): string
{
if (typeof text !== 'string')
{
return '';
}
for (let i = text.length - 1; i >= 0; i--)
{
const char = text[i];
if (!TextMetrics.isBreakingSpace(char))
{
break;
}
text = text.slice(0, -1);
}
return text;
}
/**
* Determines if char is a newline.
* @param char - The character
* @returns True if newline, False otherwise.
*/
private static isNewline(char: string): boolean
{
if (typeof char !== 'string')
{
return false;
}
return (TextMetrics._newlines.indexOf(char.charCodeAt(0)) >= 0);
}
/**
* Determines if char is a breaking whitespace.
*
* It allows one to determine whether char should be a breaking whitespace
* For example certain characters in CJK langs or numbers.
* It must return a boolean.
* @param char - The character
* @param [_nextChar] - The next character
* @returns True if whitespace, False otherwise.
*/
static isBreakingSpace(char: string, _nextChar?: string): boolean
{
if (typeof char !== 'string')
{
return false;
}
return (TextMetrics._breakingSpaces.indexOf(char.charCodeAt(0)) >= 0);
}
/**
* Splits a string into words, breaking-spaces and newLine characters
* @param text - The text
* @returns A tokenized array
*/
private static tokenize(text: string): string[]
{
const tokens: string[] = [];
let token = '';
if (typeof text !== 'string')
{
return tokens;
}
for (let i = 0; i < text.length; i++)
{
const char = text[i];
const nextChar = text[i + 1];
if (TextMetrics.isBreakingSpace(char, nextChar) || TextMetrics.isNewline(char))
{
if (token !== '')
{
tokens.push(token);
token = '';
}
tokens.push(char);
continue;
}
token += char;
}
if (token !== '')
{
tokens.push(token);
}
return tokens;
}
/**
* Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior.
*
* It allows one to customise which words should break
* Examples are if the token is CJK or numbers.
* It must return a boolean.
* @param _token - The token
* @param breakWords - The style attr break words
* @returns Whether to break word or not
*/
static canBreakWords(_token: string, breakWords: boolean): boolean
{
return breakWords;
}
/**
* Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior.
*
* It allows one to determine whether a pair of characters
* should be broken by newlines
* For example certain characters in CJK langs or numbers.
* It must return a boolean.
* @param _char - The character
* @param _nextChar - The next character
* @param _token - The token/word the characters are from
* @param _index - The index in the token of the char
* @param _breakWords - The style attr break words
* @returns whether to break word or not
*/
static canBreakChars(_char: string, _nextChar: string, _token: string, _index: number,
_breakWords: boolean): boolean
{
return true;
}
/**
* Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior.
*
* It is called when a token (usually a word) has to be split into separate pieces
* in order to determine the point to break a word.
* It must return an array of characters.
* @example
* // Correctly splits emojis, eg "🤪🤪" will result in two element array, each with one emoji.
* TextMetrics.wordWrapSplit = (token) => [...token];
* @param token - The token to split
* @returns The characters of the token
*/
static wordWrapSplit(token: string): string[]
{
return token.split('');
}
/**
* Calculates the ascent, descent and fontSize of a given font-style
* @param font - String representing the style of the font
* @returns Font properties object
*/
public static measureFont(font: string): IFontMetrics
{
// as this method is used for preparing assets, don't recalculate things if we don't need to
if (TextMetrics._fonts[font])
{
return TextMetrics._fonts[font];
}
const properties: IFontMetrics = {
ascent: 0,
descent: 0,
fontSize: 0,
};
const canvas = TextMetrics._canvas;
const context = TextMetrics._context;
context.font = font;
const metricsString = TextMetrics.METRICS_STRING + TextMetrics.BASELINE_SYMBOL;
const width = Math.ceil(context.measureText(metricsString).width);
let baseline = Math.ceil(context.measureText(TextMetrics.BASELINE_SYMBOL).width);
const height = Math.ceil(TextMetrics.HEIGHT_MULTIPLIER * baseline);
baseline = baseline * TextMetrics.BASELINE_MULTIPLIER | 0;
canvas.width = width;
canvas.height = height;
context.fillStyle = '#f00';
context.fillRect(0, 0, width, height);
context.font = font;
context.textBaseline = 'alphabetic';
context.fillStyle = '#000';
context.fillText(metricsString, 0, baseline);
const imagedata = context.getImageData(0, 0, width, height).data;
const pixels = imagedata.length;
const line = width * 4;
let i = 0;
let idx = 0;
let stop = false;
// ascent. scan from top to bottom until we find a non red pixel
for (i = 0; i < baseline; ++i)
{
for (let j = 0; j < line; j += 4)
{
if (imagedata[idx + j] !== 255)
{
stop = true;
break;
}
}
if (!stop)
{
idx += line;
}
else
{
break;
}
}
properties.ascent = baseline - i;
idx = pixels - line;
stop = false;
// descent. scan from bottom to top until we find a non red pixel
for (i = height; i > baseline; --i)
{
for (let j = 0; j < line; j += 4)
{
if (imagedata[idx + j] !== 255)
{
stop = true;
break;
}
}
if (!stop)
{
idx -= line;
}
else
{
break;
}
}
properties.descent = i - baseline;
properties.fontSize = properties.ascent + properties.descent;
TextMetrics._fonts[font] = properties;
return properties;
}
/**
* Clear font metrics in metrics cache.
* @param {string} [font] - font name. If font name not set then clear cache for all fonts.
*/
public static clearMetrics(font = ''): void
{
if (font)
{
delete TextMetrics._fonts[font];
}
else
{
TextMetrics._fonts = {};
}
}
/**
* Cached canvas element for measuring text
* TODO: this should be private, but isn't because of backward compat, will fix later.
* @ignore
*/
public static get _canvas(): HTMLCanvasElement | OffscreenCanvas
{
if (!TextMetrics.__canvas)
{
let canvas: HTMLCanvasElement | OffscreenCanvas;
try
{
// OffscreenCanvas2D measureText can be up to 40% faster.
const c = new OffscreenCanvas(0, 0);
const context = c.getContext('2d');
if (context && context.measureText)
{
TextMetrics.__canvas = c;
return c;
}
canvas = settings.ADAPTER.createCanvas();
}
catch (ex)
{
canvas = settings.ADAPTER.createCanvas();
}
canvas.width = canvas.height = 10;
TextMetrics.__canvas = canvas;
}
return TextMetrics.__canvas;
}
/**
* TODO: this should be private, but isn't because of backward compat, will fix later.
* @ignore
*/
public static get _context(): CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
{
if (!TextMetrics.__context)
{
TextMetrics.__context = TextMetrics._canvas.getContext('2d');
}
return TextMetrics.__context;
}
}
/**
* Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}.
* @typedef {object} FontMetrics
* @property {number} ascent - The ascent distance
* @property {number} descent - The descent distance
* @property {number} fontSize - Font size from ascent to descent
* @memberof PIXI.TextMetrics
* @private
*/
/**
* Cache of {@see PIXI.TextMetrics.FontMetrics} objects.
* @memberof PIXI.TextMetrics
* @type {object}
* @private
*/
TextMetrics._fonts = {};
/**
* String used for calculate font metrics.
* These characters are all tall to help calculate the height required for text.
* @static
* @memberof PIXI.TextMetrics
* @name METRICS_STRING
* @type {string}
* @default |ÉqÅ
*/
TextMetrics.METRICS_STRING = '|ÉqÅ';
/**
* Baseline symbol for calculate font metrics.
* @static
* @memberof PIXI.TextMetrics
* @name BASELINE_SYMBOL
* @type {string}
* @default M
*/
TextMetrics.BASELINE_SYMBOL = 'M';
/**
* Baseline multiplier for calculate font metrics.
* @static
* @memberof PIXI.TextMetrics
* @name BASELINE_MULTIPLIER
* @type {number}
* @default 1.4
*/
TextMetrics.BASELINE_MULTIPLIER = 1.4;
/**
* Height multiplier for setting height of canvas to calculate font metrics.
* @static
* @memberof PIXI.TextMetrics
* @name HEIGHT_MULTIPLIER
* @type {number}
* @default 2.00
*/
TextMetrics.HEIGHT_MULTIPLIER = 2.0;
/**
* Cache of new line chars.
* @memberof PIXI.TextMetrics
* @type {number[]}
* @private
*/
TextMetrics._newlines = [
0x000A, // line feed
0x000D, // carriage return
];
/**
* Cache of breaking spaces.
* @memberof PIXI.TextMetrics
* @type {number[]}
* @private
*/
TextMetrics._breakingSpaces = [
0x0009, // character tabulation
0x0020, // space
0x2000, // en quad
0x2001, // em quad
0x2002, // en space
0x2003, // em space
0x2004, // three-per-em space
0x2005, // four-per-em space
0x2006, // six-per-em space
0x2008, // punctuation space
0x2009, // thin space
0x200A, // hair space
0x205F, // medium mathematical space
0x3000, // ideographic space
];
/**
* A number, or a string containing a number.
* @memberof PIXI
* @typedef {object} IFontMetrics
* @property {number} ascent - Font ascent
* @property {number} descent - Font descent
* @property {number} fontSize - Font size
*/