@lightningjs/renderer 0.8.4 → 0.9.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.
Files changed (59) hide show
  1. package/dist/src/core/lib/Matrix3d.d.ts +38 -24
  2. package/dist/src/core/lib/Matrix3d.js +143 -166
  3. package/dist/src/core/lib/Matrix3d.js.map +1 -1
  4. package/dist/src/core/text-rendering/TextTextureRendererUtils.d.ts +19 -0
  5. package/dist/src/core/text-rendering/TextTextureRendererUtils.js +61 -0
  6. package/dist/src/core/text-rendering/TextTextureRendererUtils.js.map +1 -1
  7. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.d.ts +8 -2
  8. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.js +24 -3
  9. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.js.map +1 -1
  10. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.d.ts +2 -0
  11. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.js.map +1 -1
  12. package/dist/src/core/text-rendering/font-face-types/TrFontFace.d.ts +72 -1
  13. package/dist/src/core/text-rendering/font-face-types/TrFontFace.js +11 -1
  14. package/dist/src/core/text-rendering/font-face-types/TrFontFace.js.map +1 -1
  15. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.d.ts +5 -2
  16. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.js +4 -3
  17. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.js.map +1 -1
  18. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.d.ts +8 -0
  19. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js +42 -16
  20. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js.map +1 -1
  21. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.d.ts +7 -1
  22. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.js +42 -11
  23. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.js.map +1 -1
  24. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.d.ts +4 -0
  25. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js +26 -9
  26. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js.map +1 -1
  27. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.d.ts +2 -1
  28. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.js +32 -5
  29. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.js.map +1 -1
  30. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.d.ts +2 -1
  31. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.js +2 -1
  32. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.js.map +1 -1
  33. package/dist/src/core/text-rendering/renderers/TextRenderer.d.ts +8 -4
  34. package/dist/src/core/text-rendering/renderers/TextRenderer.js +6 -5
  35. package/dist/src/core/text-rendering/renderers/TextRenderer.js.map +1 -1
  36. package/dist/src/main-api/RendererMain.js +2 -2
  37. package/dist/src/main-api/RendererMain.js.map +1 -1
  38. package/dist/src/render-drivers/main/MainOnlyTextNode.js +1 -3
  39. package/dist/src/render-drivers/main/MainOnlyTextNode.js.map +1 -1
  40. package/dist/src/render-drivers/threadx/TextNodeStruct.js +3 -1
  41. package/dist/src/render-drivers/threadx/TextNodeStruct.js.map +1 -1
  42. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  43. package/package.json +2 -2
  44. package/src/core/lib/Matrix3d.ts +144 -190
  45. package/src/core/text-rendering/TextRenderingUtils.ts +36 -0
  46. package/src/core/text-rendering/TextTextureRendererUtils.ts +74 -0
  47. package/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts +41 -12
  48. package/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.ts +2 -0
  49. package/src/core/text-rendering/font-face-types/TrFontFace.ts +86 -1
  50. package/src/core/text-rendering/font-face-types/WebTrFontFace.ts +13 -7
  51. package/src/core/text-rendering/renderers/CanvasTextRenderer.ts +52 -20
  52. package/src/core/text-rendering/renderers/LightningTextTextureRenderer.ts +59 -13
  53. package/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts +30 -9
  54. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.ts +38 -5
  55. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.ts +5 -2
  56. package/src/core/text-rendering/renderers/TextRenderer.ts +14 -10
  57. package/src/main-api/RendererMain.ts +2 -2
  58. package/src/render-drivers/main/MainOnlyTextNode.ts +1 -3
  59. package/src/render-drivers/threadx/TextNodeStruct.ts +3 -1
@@ -64,13 +64,98 @@ export interface TrFontFaceDescriptors {
64
64
  variant?: string;
65
65
  }
66
66
 
67
+ export interface FontMetrics {
68
+ /**
69
+ * The distance, in font units, from the baseline to the highest point of the font.
70
+ */
71
+ ascender: number;
72
+ /**
73
+ * The distance, in font units, from the baseline to the lowest point of the font.
74
+ */
75
+ descender: number;
76
+ /**
77
+ * The additional space used in the calculation of the default line height in font units.
78
+ */
79
+ lineGap: number;
80
+ /**
81
+ * The number of font units per 1 EM.
82
+ */
83
+ unitsPerEm: number;
84
+ }
85
+
86
+ export interface NormalizedFontMetrics {
87
+ /**
88
+ * The distance, as a fraction of 1 EM, from the baseline to the highest point of the font.
89
+ *
90
+ * @remarks
91
+ * This value should be positive.
92
+ */
93
+ ascender: number;
94
+ /**
95
+ * The distance, as a fraction of 1 EM, from the baseline to the lowest point of the font.
96
+ *
97
+ * @remarks
98
+ * This value should be positive.
99
+ */
100
+ descender: number;
101
+ /**
102
+ * The additional space used in the calculation of the default line height as a fraction of 1 EM
103
+ *
104
+ * @remarks
105
+ * This value should be positive.
106
+ */
107
+ lineGap: number;
108
+ }
109
+
110
+ export interface TrFontFaceOptions {
111
+ fontFamily: string;
112
+ descriptors: Partial<TrFontFaceDescriptors>;
113
+ /**
114
+ * Font metrics used for layout and default line height calculations.
115
+ *
116
+ * @remarks
117
+ * Provides the font metrics in order to ensure consistent text layout across
118
+ * different font types (Canvas/Web, SDF, etc.).
119
+ *
120
+ * **Important**: We HIGHLY recommend providing these metrics for both SDF
121
+ * and Web/Canvas fonts. You can use the Lightning 3 [msdf-generator](https://github.com/lightning-js/msdf-generator/)
122
+ * tool to generate these metrics from a font file. The metrics generated by
123
+ * this tool can be used with both SDF and Web/Canvas fonts.
124
+ *
125
+ * If not provided, a warning will be logged and the text layout may not be
126
+ * consistent across different font types.
127
+ *
128
+ * If not provided, the metrics will be gathered depending on the font type.
129
+ * - For SDF fonts, the metrics will be gathered from the font atlas data.
130
+ * If the font is generated using Lightning 3's msdf-generator tool, the
131
+ * metrics will be the most accurate.
132
+ * - For Web/Canvas fonts, the metrics will be gathered from the
133
+ * CanvasRenderingContext2D.measureText() method. The accuracy/consistency
134
+ * of these metrics depends on the browser implementation. And will not
135
+ * guarantee the line-by-line alignment of text with SDF fonts.
136
+ */
137
+ metrics?: FontMetrics;
138
+ }
139
+
67
140
  export class TrFontFace extends EventEmitter {
68
141
  public readonly fontFamily: string;
69
142
  public readonly descriptors: TrFontFaceDescriptors;
70
143
  public readonly loaded: boolean = false;
144
+ public readonly metrics: NormalizedFontMetrics | null = null;
71
145
 
72
- constructor(fontFamily: string, descriptors: Partial<TrFontFaceDescriptors>) {
146
+ constructor(options: TrFontFaceOptions) {
73
147
  super();
148
+ const { fontFamily, descriptors, metrics } = options;
149
+
150
+ if (metrics) {
151
+ // Normalize metrics to be in the range of 0 to 1
152
+ this.metrics = {
153
+ ascender: metrics.ascender / metrics.unitsPerEm,
154
+ descender: metrics.descender / metrics.unitsPerEm,
155
+ lineGap: metrics.lineGap / metrics.unitsPerEm,
156
+ };
157
+ }
158
+
74
159
  this.fontFamily = fontFamily;
75
160
  this.descriptors = {
76
161
  style: 'normal',
@@ -17,7 +17,11 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
- import { TrFontFace, type TrFontFaceDescriptors } from './TrFontFace.js';
20
+ import {
21
+ TrFontFace,
22
+ type TrFontFaceDescriptors,
23
+ type TrFontFaceOptions,
24
+ } from './TrFontFace.js';
21
25
 
22
26
  declare module './TrFontFace.js' {
23
27
  interface TrFontFaceMap {
@@ -25,16 +29,18 @@ declare module './TrFontFace.js' {
25
29
  }
26
30
  }
27
31
 
32
+ export interface WebTrFontFaceOptions extends TrFontFaceOptions {
33
+ fontUrl: string;
34
+ }
35
+
28
36
  export class WebTrFontFace extends TrFontFace {
29
37
  public readonly fontFace: FontFace;
30
38
  public readonly fontUrl: string;
31
39
 
32
- constructor(
33
- fontFamily: string,
34
- descriptors: Partial<TrFontFaceDescriptors>,
35
- fontUrl: string,
36
- ) {
37
- super(fontFamily, descriptors);
40
+ constructor(options: WebTrFontFaceOptions) {
41
+ super(options);
42
+
43
+ const { fontFamily, fontUrl } = options;
38
44
 
39
45
  // Filter out parentheses from fontUrl
40
46
  const fontUrlWithoutParentheses = fontUrl.replace(/\(|\)/g, '');
@@ -33,6 +33,7 @@ import {
33
33
  type RectWithValid,
34
34
  } from '../../lib/utils.js';
35
35
  import type { ImageTexture } from '../../textures/ImageTexture.js';
36
+ import { TrFontManager, type FontFamilyMap } from '../TrFontManager.js';
36
37
  import type { TrFontFace } from '../font-face-types/TrFontFace.js';
37
38
  import { WebTrFontFace } from '../font-face-types/WebTrFontFace.js';
38
39
  import {
@@ -81,6 +82,7 @@ export interface CanvasTextRendererState extends TextRendererState {
81
82
  fontFaceLoadedHandler: (() => void) | undefined;
82
83
  fontInfo:
83
84
  | {
85
+ fontFace: WebTrFontFace;
84
86
  cssString: string;
85
87
  loaded: boolean;
86
88
  }
@@ -107,6 +109,12 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
107
109
  | OffscreenCanvasRenderingContext2D
108
110
  | CanvasRenderingContext2D;
109
111
  private rendererBounds: Bound;
112
+ /**
113
+ * Font family map used to store web font faces that were added to the
114
+ * canvas text renderer.
115
+ */
116
+ private fontFamilies: FontFamilyMap = {};
117
+ private fontFamilyArray: FontFamilyMap[] = [this.fontFamilies];
110
118
 
111
119
  constructor(stage: Stage) {
112
120
  super(stage);
@@ -135,6 +143,14 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
135
143
  x2: this.stage.options.appWidth,
136
144
  y2: this.stage.options.appHeight,
137
145
  };
146
+ // Install the default 'san-serif' font face
147
+ this.addFontFace(
148
+ new WebTrFontFace({
149
+ fontFamily: 'sans-serif',
150
+ descriptors: {},
151
+ fontUrl: '',
152
+ }),
153
+ );
138
154
  }
139
155
 
140
156
  //#region Overrides
@@ -259,10 +275,25 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
259
275
  // the `isFontFaceSupported` check)
260
276
  assertTruthy(fontFace instanceof WebTrFontFace);
261
277
 
262
- // We simply add the font face to the document
263
- // @ts-expect-error `add()` method should be available from a FontFaceSet
264
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
265
- globalFontSet.add(fontFace.fontFace);
278
+ // Add the font face to the document
279
+ // Except for the 'sans-serif' font family, which the Renderer provides
280
+ // as a special default fallback.
281
+ if (fontFace.fontFamily !== 'sans-serif') {
282
+ // @ts-expect-error `add()` method should be available from a FontFaceSet
283
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
284
+ globalFontSet.add(fontFace.fontFace);
285
+ }
286
+
287
+ const { fontFamilies } = this;
288
+ const familyName = fontFace.fontFace.family;
289
+
290
+ let faceSet = fontFamilies[familyName];
291
+ if (!faceSet) {
292
+ faceSet = new Set();
293
+ fontFamilies[familyName] = faceSet;
294
+ }
295
+
296
+ faceSet.add(fontFace);
266
297
  }
267
298
 
268
299
  override createState(props: TrProps): CanvasTextRendererState {
@@ -312,7 +343,13 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
312
343
  // If fontInfo is invalid, we need to establish it
313
344
  if (!state.fontInfo) {
314
345
  const cssString = getFontCssString(state.props);
346
+ const trFontFace = TrFontManager.resolveFontFace(
347
+ this.fontFamilyArray,
348
+ state.props,
349
+ ) as WebTrFontFace | undefined;
350
+ assertTruthy(trFontFace, `Could not resolve font face for ${cssString}`);
315
351
  state.fontInfo = {
352
+ fontFace: trFontFace,
316
353
  cssString: cssString,
317
354
  // TODO: For efficiency we would use this here but it's not reliable on WPE -> document.fonts.check(cssString),
318
355
  loaded: false,
@@ -333,22 +370,11 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
333
370
  }
334
371
 
335
372
  if (!state.renderInfo) {
336
- const maxLines = state.props.maxLines;
337
- const containedMaxLines =
338
- state.props.contain === 'both'
339
- ? Math.floor(
340
- (state.props.height - state.props.offsetY) /
341
- state.props.lineHeight,
342
- )
343
- : 0;
344
- const calcMaxLines =
345
- containedMaxLines > 0 && maxLines > 0
346
- ? Math.min(containedMaxLines, maxLines)
347
- : Math.max(containedMaxLines, maxLines);
348
373
  state.lightning2TextRenderer.settings = {
349
374
  text: state.props.text,
350
375
  textAlign: state.props.textAlign,
351
- fontFace: state.props.fontFamily,
376
+ fontFamily: state.props.fontFamily,
377
+ trFontFace: state.fontInfo.fontFace,
352
378
  fontSize: state.props.fontSize,
353
379
  fontStyle: [
354
380
  state.props.fontStretch,
@@ -356,16 +382,21 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
356
382
  state.props.fontWeight,
357
383
  ].join(' '),
358
384
  textColor: getNormalizedRgbaComponents(state.props.color),
359
- offsetY: state.props.fontSize + state.props.offsetY,
385
+ offsetY: state.props.offsetY,
360
386
  wordWrap: state.props.contain !== 'none',
361
387
  wordWrapWidth:
362
388
  state.props.contain === 'none' ? undefined : state.props.width,
363
389
  letterSpacing: state.props.letterSpacing,
364
- lineHeight: state.props.lineHeight,
365
- maxLines: calcMaxLines,
390
+ lineHeight: state.props.lineHeight ?? null,
391
+ maxLines: state.props.maxLines,
392
+ maxHeight:
393
+ state.props.contain === 'both'
394
+ ? state.props.height - state.props.offsetY
395
+ : null,
366
396
  textBaseline: state.props.textBaseline,
367
397
  verticalAlign: state.props.verticalAlign,
368
398
  overflowSuffix: state.props.overflowSuffix,
399
+ w: state.props.contain !== 'none' ? state.props.width : undefined,
369
400
  };
370
401
  // const renderInfoCalculateTime = performance.now();
371
402
  state.renderInfo = state.lightning2TextRenderer.calculateRenderInfo();
@@ -712,6 +743,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
712
743
  }
713
744
 
714
745
  override destroyState(state: CanvasTextRendererState): void {
746
+ super.destroyState(state);
715
747
  // Remove state object owner from any canvas page textures
716
748
  state.canvasPages?.forEach((pageInfo) => {
717
749
  pageInfo.texture?.setRenderableOwner(state, false);
@@ -19,7 +19,12 @@
19
19
 
20
20
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
21
21
 
22
+ import { assertTruthy } from '../../../utils.js';
22
23
  import { getRgbaString, type RGBA } from '../../lib/utils.js';
24
+ import { calcDefaultLineHeight } from '../TextRenderingUtils.js';
25
+ import { getWebFontMetrics } from '../TextTextureRendererUtils.js';
26
+ import type { NormalizedFontMetrics } from '../font-face-types/TrFontFace.js';
27
+ import type { WebTrFontFace } from '../font-face-types/WebTrFontFace.js';
23
28
 
24
29
  const MAX_TEXTURE_DIMENSION = 2048;
25
30
 
@@ -62,7 +67,8 @@ export interface Settings {
62
67
  fontStyle: string;
63
68
  fontSize: number;
64
69
  fontBaselineRatio: number;
65
- fontFace: string | null;
70
+ fontFamily: string | null;
71
+ trFontFace: WebTrFontFace | null;
66
72
  wordWrap: boolean;
67
73
  wordWrapWidth: number;
68
74
  wordBreak: boolean;
@@ -73,6 +79,7 @@ export interface Settings {
73
79
  verticalAlign: TextVerticalAlign;
74
80
  offsetY: number | null;
75
81
  maxLines: number;
82
+ maxHeight: number | null;
76
83
  overflowSuffix: string;
77
84
  precision: number;
78
85
  textColor: RGBA;
@@ -117,12 +124,14 @@ export interface RenderInfo {
117
124
  cutEx: number;
118
125
  cutEy: number;
119
126
  lineHeight: number;
127
+ defLineHeight: number;
120
128
  lineWidths: number[];
121
129
  offsetY: number;
122
130
  paddingLeft: number;
123
131
  paddingRight: number;
124
132
  letterSpacing: number;
125
133
  textIndent: number;
134
+ metrics: NormalizedFontMetrics;
126
135
  }
127
136
 
128
137
  /**
@@ -186,7 +195,7 @@ export class LightningTextTextureRenderer {
186
195
  }
187
196
 
188
197
  _getFontSetting() {
189
- const ff = [this._settings.fontFace];
198
+ const ff = [this._settings.fontFamily];
190
199
 
191
200
  const ffs = [];
192
201
  for (let i = 0, n = ff.length; i < n; i++) {
@@ -238,7 +247,6 @@ export class LightningTextTextureRenderer {
238
247
  this._settings.offsetY === null
239
248
  ? null
240
249
  : this._settings.offsetY * precision;
241
- let lineHeight = (this._settings.lineHeight || fontSize) * precision;
242
250
  const w = this._settings.w * precision;
243
251
  const h = this._settings.h * precision;
244
252
  let wordWrapWidth = this._settings.wordWrapWidth * precision;
@@ -248,10 +256,31 @@ export class LightningTextTextureRenderer {
248
256
  const cutEy = this._settings.cutEy * precision;
249
257
  const letterSpacing = (this._settings.letterSpacing || 0) * precision;
250
258
  const textIndent = this._settings.textIndent * precision;
259
+ const trFontFace = this._settings.trFontFace;
251
260
 
252
261
  // Set font properties.
253
262
  this.setFontProperties();
254
263
 
264
+ assertTruthy(trFontFace);
265
+ const metrics = getWebFontMetrics(this._context, trFontFace, fontSize);
266
+ const defLineHeight = calcDefaultLineHeight(metrics, fontSize) * precision;
267
+ const lineHeight =
268
+ this._settings.lineHeight !== null
269
+ ? this._settings.lineHeight * precision
270
+ : defLineHeight;
271
+
272
+ const maxHeight = this._settings.maxHeight;
273
+ const containedMaxLines =
274
+ maxHeight !== null && lineHeight > 0
275
+ ? Math.floor(maxHeight / lineHeight)
276
+ : 0;
277
+
278
+ const setMaxLines = this._settings.maxLines;
279
+ const calcMaxLines =
280
+ containedMaxLines > 0 && setMaxLines > 0
281
+ ? Math.min(containedMaxLines, setMaxLines)
282
+ : Math.max(containedMaxLines, setMaxLines);
283
+
255
284
  // Total width.
256
285
  let width = w || 2048 / this.getPrecision();
257
286
 
@@ -305,8 +334,8 @@ export class LightningTextTextureRenderer {
305
334
  }
306
335
  let lines = linesInfo.l;
307
336
 
308
- if (this._settings.maxLines && lines.length > this._settings.maxLines) {
309
- const usedLines = lines.slice(0, this._settings.maxLines);
337
+ if (calcMaxLines && lines.length > calcMaxLines) {
338
+ const usedLines = lines.slice(0, calcMaxLines);
310
339
 
311
340
  let otherLines = null;
312
341
  if (this._settings.overflowSuffix) {
@@ -333,7 +362,7 @@ export class LightningTextTextureRenderer {
333
362
  const n = lines.length;
334
363
  let j = 0;
335
364
  const m = linesInfo.n.length;
336
- for (i = this._settings.maxLines; i < n; i++) {
365
+ for (i = calcMaxLines; i < n; i++) {
337
366
  otherLines[j] += `${otherLines[j] ? ' ' : ''}${lines[i]!}`;
338
367
  if (i + 1 < m && linesInfo.n[i + 1]) {
339
368
  j++;
@@ -368,9 +397,6 @@ export class LightningTextTextureRenderer {
368
397
  innerWidth = maxLineWidth;
369
398
  }
370
399
 
371
- // calculate text height
372
- lineHeight = lineHeight || fontSize;
373
-
374
400
  let height;
375
401
  if (h) {
376
402
  height = h;
@@ -420,12 +446,14 @@ export class LightningTextTextureRenderer {
420
446
  renderInfo.cutEx = cutEx;
421
447
  renderInfo.cutEy = cutEy;
422
448
  renderInfo.lineHeight = lineHeight;
449
+ renderInfo.defLineHeight = defLineHeight;
423
450
  renderInfo.lineWidths = lineWidths;
424
451
  renderInfo.offsetY = offsetY;
425
452
  renderInfo.paddingLeft = paddingLeft;
426
453
  renderInfo.paddingRight = paddingRight;
427
454
  renderInfo.letterSpacing = letterSpacing;
428
455
  renderInfo.textIndent = textIndent;
456
+ renderInfo.metrics = metrics;
429
457
 
430
458
  return renderInfo as RenderInfo;
431
459
  }
@@ -477,17 +505,33 @@ export class LightningTextTextureRenderer {
477
505
 
478
506
  const drawLines = [];
479
507
 
508
+ const { metrics } = renderInfo;
509
+
510
+ /**
511
+ * Ascender (in pixels)
512
+ */
513
+ const ascenderPx = metrics
514
+ ? metrics.ascender * renderInfo.fontSize
515
+ : renderInfo.fontSize;
516
+
517
+ /**
518
+ * Bare line height is the distance between the ascender and descender of the font.
519
+ * without the line gap metric.
520
+ */
521
+ const bareLineHeightPx =
522
+ (metrics.ascender - metrics.descender) * renderInfo.fontSize;
523
+
480
524
  // Draw lines line by line.
481
525
  for (let i = 0, n = lines.length; i < n; i++) {
482
526
  linePositionX = i === 0 ? renderInfo.textIndent : 0;
483
527
 
484
528
  // By default, text is aligned to top
485
- linePositionY = i * renderInfo.lineHeight + renderInfo.offsetY;
529
+ linePositionY = i * renderInfo.lineHeight + ascenderPx;
486
530
 
487
531
  if (this._settings.verticalAlign == 'middle') {
488
- linePositionY += (renderInfo.lineHeight - renderInfo.fontSize) / 2;
532
+ linePositionY += (renderInfo.lineHeight - bareLineHeightPx) / 2;
489
533
  } else if (this._settings.verticalAlign == 'bottom') {
490
- linePositionY += renderInfo.lineHeight - renderInfo.fontSize;
534
+ linePositionY += renderInfo.lineHeight - bareLineHeightPx;
491
535
  }
492
536
 
493
537
  if (this._settings.textAlign === 'right') {
@@ -699,7 +743,8 @@ export class LightningTextTextureRenderer {
699
743
  h: 0,
700
744
  fontStyle: 'normal',
701
745
  fontSize: 40,
702
- fontFace: null,
746
+ fontFamily: null,
747
+ trFontFace: null,
703
748
  wordWrap: true,
704
749
  wordWrapWidth: 0,
705
750
  wordBreak: false,
@@ -710,6 +755,7 @@ export class LightningTextTextureRenderer {
710
755
  verticalAlign: 'top',
711
756
  offsetY: null,
712
757
  maxLines: 0,
758
+ maxHeight: null,
713
759
  overflowSuffix: '...',
714
760
  textColor: [1.0, 1.0, 1.0, 1.0],
715
761
  paddingLeft: 0,
@@ -58,6 +58,7 @@ import { EventEmitter } from '../../../../common/EventEmitter.js';
58
58
  import type { Matrix3d } from '../../../lib/Matrix3d.js';
59
59
  import type { Dimensions } from '../../../../common/CommonTypes.js';
60
60
  import { WebGlCoreRenderer } from '../../../renderers/webgl/WebGlCoreRenderer.js';
61
+ import { calcDefaultLineHeight } from '../../TextRenderingUtils.js';
61
62
 
62
63
  declare module '../TextRenderer.js' {
63
64
  interface TextRendererMap {
@@ -105,6 +106,11 @@ export interface SdfTextRendererState extends TextRendererState {
105
106
  distanceRange: number;
106
107
 
107
108
  trFontFace: SdfTrFontFace | undefined;
109
+
110
+ /**
111
+ * Resolved line height in logical screen pixel units
112
+ */
113
+ resLineHeight: number | undefined;
108
114
  }
109
115
 
110
116
  /**
@@ -251,6 +257,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
251
257
  },
252
258
  lineHeight: (state, value) => {
253
259
  state.props.lineHeight = value;
260
+ state.resLineHeight = undefined;
254
261
  this.invalidateLayoutCache(state);
255
262
  },
256
263
  maxLines: (state, value) => {
@@ -363,6 +370,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
363
370
  distanceRange: 0,
364
371
  trFontFace: undefined,
365
372
  isRenderable: false,
373
+ resLineHeight: undefined,
366
374
  debugData: {
367
375
  updateCount: 0,
368
376
  layoutCount: 0,
@@ -409,6 +417,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
409
417
 
410
418
  // If the font is loaded then so should the data
411
419
  assertTruthy(trFontFace.data, 'Font face data should be loaded');
420
+ assertTruthy(trFontFace.metrics, 'Font face metrics should be loaded');
412
421
 
413
422
  const {
414
423
  text,
@@ -418,7 +427,6 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
418
427
  contain,
419
428
  width,
420
429
  height,
421
- lineHeight,
422
430
  verticalAlign,
423
431
  scrollable,
424
432
  overflowSuffix,
@@ -441,8 +449,21 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
441
449
  */
442
450
  const fontSizeRatio = fontSize / sdfFontSize;
443
451
 
452
+ // If not already resolved, resolve the line height and store it in the state
453
+ let resLineHeight = state.resLineHeight;
454
+ if (resLineHeight === undefined) {
455
+ const lineHeight = state.props.lineHeight;
456
+ // If lineHeight is undefined, use the maxCharHeight from the font face
457
+ if (lineHeight === undefined) {
458
+ resLineHeight = calcDefaultLineHeight(trFontFace.metrics, fontSize);
459
+ } else {
460
+ resLineHeight = lineHeight;
461
+ }
462
+ state.resLineHeight = resLineHeight;
463
+ }
464
+
444
465
  // Needed in renderWindow calculation
445
- const sdfLineHeight = lineHeight / fontSizeRatio;
466
+ const sdfLineHeight = resLineHeight / fontSizeRatio;
446
467
 
447
468
  state.distanceRange =
448
469
  fontSizeRatio * trFontFace.data.distanceField.distanceRange;
@@ -499,7 +520,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
499
520
  x,
500
521
  y,
501
522
  scrollY,
502
- lineHeight,
523
+ resLineHeight,
503
524
  contain === 'both' ? elementBounds.y2 - elementBounds.y1 : 0,
504
525
  elementBounds,
505
526
  fontSizeRatio,
@@ -510,7 +531,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
510
531
  const start = getStartConditions(
511
532
  sdfFontSize,
512
533
  sdfLineHeight,
513
- lineHeight,
534
+ trFontFace,
514
535
  verticalAlign,
515
536
  offsetY,
516
537
  fontSizeRatio,
@@ -537,7 +558,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
537
558
  width,
538
559
  height,
539
560
  fontSize,
540
- lineHeight,
561
+ resLineHeight,
541
562
  letterSpacing,
542
563
  vertexBuffer,
543
564
  contain,
@@ -561,7 +582,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
561
582
  // If we didn't exit early, we know we have completely computed w/h
562
583
  if (out2.fullyProcessed) {
563
584
  state.textW = out2.maxX * fontSizeRatio;
564
- state.textH = out2.maxY * fontSizeRatio;
585
+ state.textH = out2.numLines * sdfLineHeight * fontSizeRatio;
565
586
  }
566
587
 
567
588
  // if (state.props.debug.printLayoutTime) {
@@ -584,8 +605,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
584
605
  return;
585
606
  }
586
607
 
587
- const renderer: WebGlCoreRenderer = this.stage
588
- .renderer as WebGlCoreRenderer;
608
+ const renderer = this.stage.renderer;
589
609
  assertTruthy(renderer instanceof WebGlCoreRenderer);
590
610
 
591
611
  const { fontSize, color, contain, scrollable, zIndex, debug } = state.props;
@@ -670,7 +690,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
670
690
  webGlBuffers,
671
691
  this.sdfShader,
672
692
  {
673
- transform: transform.data,
693
+ transform: transform.getFloatArr(),
674
694
  // IMPORTANT: The SDF Shader expects the color NOT to be premultiplied
675
695
  // for the best blending results. Which is why we use `mergeColorAlpha`
676
696
  // instead of `mergeColorAlphaPremultiplied` here.
@@ -780,6 +800,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
780
800
  * @param state
781
801
  */
782
802
  protected releaseFontFace(state: SdfTextRendererState) {
803
+ state.resLineHeight = undefined;
783
804
  if (state.trFontFace) {
784
805
  state.trFontFace.texture.setRenderableOwner(state, false);
785
806
  state.trFontFace = undefined;
@@ -17,7 +17,9 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
+ import { assertTruthy } from '../../../../../utils.js';
20
21
  import type { Bound } from '../../../../lib/utils.js';
22
+ import type { SdfTrFontFace } from '../../../font-face-types/SdfTrFontFace/SdfTrFontFace.js';
21
23
  import type { TrProps, TextRendererState } from '../../TextRenderer.js';
22
24
  import type { SdfTextRendererState } from '../SdfTextRenderer.js';
23
25
  import type { SdfRenderWindow } from './setRenderWindow.js';
@@ -39,7 +41,7 @@ import type { SdfRenderWindow } from './setRenderWindow.js';
39
41
  export function getStartConditions(
40
42
  sdfFontSize: number,
41
43
  sdfLineHeight: number,
42
- lineHeight: number,
44
+ fontFace: SdfTrFontFace,
43
45
  verticalAlign: TrProps['verticalAlign'],
44
46
  offsetY: TrProps['offsetY'],
45
47
  fontSizeRatio: number,
@@ -59,17 +61,48 @@ export function getStartConditions(
59
61
  lineCache.length,
60
62
  );
61
63
 
62
- // TODO: (fontSize / 6.4286 / fontSizeRatio) Adding this to the startY helps the text line up better with Canvas rendered text
63
64
  const sdfStartX = 0;
65
+ const { metrics } = fontFace;
66
+ assertTruthy(metrics, 'Font metrics not loaded');
67
+ assertTruthy(fontFace.data, 'Font data not loaded');
68
+
69
+ /**
70
+ * Bare line height is the distance between the ascender and descender of the font.
71
+ * without the line gap metric.
72
+ */
73
+ const sdfBareLineHeight =
74
+ (metrics.ascender - metrics.descender) * sdfFontSize;
64
75
  let sdfVerticalAlignYOffset = 0;
65
76
  if (verticalAlign === 'middle') {
66
- sdfVerticalAlignYOffset = (sdfLineHeight - sdfFontSize) / 2;
77
+ sdfVerticalAlignYOffset = (sdfLineHeight - sdfBareLineHeight) / 2;
67
78
  } else if (verticalAlign === 'bottom') {
68
- sdfVerticalAlignYOffset = sdfLineHeight - sdfFontSize;
79
+ sdfVerticalAlignYOffset = sdfLineHeight - sdfBareLineHeight;
69
80
  }
81
+
70
82
  const sdfOffsetY = offsetY / fontSizeRatio;
83
+
84
+ /**
85
+ * This is the position from the top of the text drawing line to where the
86
+ * baseline of the text will be according to the encoded positioning data for
87
+ * each glyph in the SDF data. This also happens to be the ascender value
88
+ * that is encoded into the font data.
89
+ */
90
+ const sdfEncodedAscender = fontFace.data.common.base;
91
+ /**
92
+ * This is the ascender that is configured and overridable in the font face.
93
+ */
94
+ const sdfConfiguredAscender = metrics.ascender * sdfFontSize;
95
+ /**
96
+ * If the configured ascender is different from the SDF data's encoded
97
+ * ascender, the offset of the text will be adjusted by the difference.
98
+ */
99
+ const sdfAscenderAdjOffset = sdfConfiguredAscender - sdfEncodedAscender;
100
+
71
101
  const sdfStartY =
72
- sdfOffsetY + startLineIndex * sdfLineHeight + sdfVerticalAlignYOffset; // TODO: Figure out what determines the initial y offset of text.
102
+ sdfOffsetY +
103
+ sdfAscenderAdjOffset +
104
+ startLineIndex * sdfLineHeight +
105
+ sdfVerticalAlignYOffset; // TODO: Figure out what determines the initial y offset of text.
73
106
 
74
107
  // Don't attempt to render anything if we know we're starting past the established end of the text
75
108
  if (textH && sdfStartY >= textH / fontSizeRatio) {