@solidtv/renderer 1.1.5 → 1.2.1
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/dist/exports/canvas-shaders.d.ts +1 -0
- package/dist/exports/canvas-shaders.js +1 -0
- package/dist/exports/canvas-shaders.js.map +1 -1
- package/dist/exports/webgl-shaders.d.ts +1 -0
- package/dist/exports/webgl-shaders.js +1 -0
- package/dist/exports/webgl-shaders.js.map +1 -1
- package/dist/src/common/CommonTypes.d.ts +5 -0
- package/dist/src/core/CoreNode.js +4 -2
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.d.ts +17 -0
- package/dist/src/core/CoreTextNode.js +33 -7
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/Stage.js +6 -0
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +11 -8
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +3 -2
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/shaders/canvas/RadialProgress.d.ts +10 -0
- package/dist/src/core/shaders/canvas/RadialProgress.js +87 -0
- package/dist/src/core/shaders/canvas/RadialProgress.js.map +1 -0
- package/dist/src/core/shaders/templates/BorderTemplate.d.ts +1 -1
- package/dist/src/core/shaders/templates/BorderTemplate.js +1 -1
- package/dist/src/core/shaders/templates/BorderTemplate.js.map +1 -1
- package/dist/src/core/shaders/templates/RadialProgressTemplate.d.ts +61 -0
- package/dist/src/core/shaders/templates/RadialProgressTemplate.js +54 -0
- package/dist/src/core/shaders/templates/RadialProgressTemplate.js.map +1 -0
- package/dist/src/core/shaders/webgl/RadialProgress.d.ts +3 -0
- package/dist/src/core/shaders/webgl/RadialProgress.js +145 -0
- package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -0
- package/dist/src/core/text-rendering/CanvasTextRenderer.js +12 -2
- package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/SdfFontHandler.js +35 -0
- package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +32 -20
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.d.ts +7 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.js +94 -10
- package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
- package/dist/src/core/text-rendering/TextRenderer.d.ts +84 -10
- package/dist/src/main-api/Renderer.d.ts +23 -1
- package/dist/src/main-api/Renderer.js +6 -1
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/exports/canvas-shaders.ts +1 -0
- package/exports/webgl-shaders.ts +1 -0
- package/package.json +1 -1
- package/src/common/CommonTypes.ts +5 -0
- package/src/core/CoreNode.ts +6 -2
- package/src/core/CoreTextNode.ts +34 -6
- package/src/core/Stage.ts +7 -0
- package/src/core/renderers/webgl/WebGlCtxTexture.test.ts +94 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +14 -10
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +3 -2
- package/src/core/shaders/canvas/RadialProgress.ts +113 -0
- package/src/core/shaders/templates/BorderTemplate.ts +2 -2
- package/src/core/shaders/templates/RadialProgressTemplate.test.ts +125 -0
- package/src/core/shaders/templates/RadialProgressTemplate.ts +112 -0
- package/src/core/shaders/webgl/RadialProgress.ts +163 -0
- package/src/core/text-rendering/CanvasTextRenderer.ts +14 -2
- package/src/core/text-rendering/SdfFontHandler.ts +37 -0
- package/src/core/text-rendering/SdfTextRenderer.ts +41 -22
- package/src/core/text-rendering/TextLayoutEngine.ts +99 -12
- package/src/core/text-rendering/TextRenderer.ts +85 -10
- package/src/main-api/Renderer.ts +33 -2
|
@@ -16,6 +16,8 @@ import type { WebGlShaderNode } from '../renderers/webgl/WebGlShaderNode.js';
|
|
|
16
16
|
import { isProductionEnvironment } from '../../utils.js';
|
|
17
17
|
import type { TextLayout, GlyphLayout } from './TextRenderer.js';
|
|
18
18
|
import { mapTextLayout } from './TextLayoutEngine.js';
|
|
19
|
+
import type { RectWithValid } from '../lib/utils.js';
|
|
20
|
+
import type { Dimensions } from '../../common/CommonTypes.js';
|
|
19
21
|
|
|
20
22
|
// Type definition to match interface
|
|
21
23
|
const type = 'sdf' as const;
|
|
@@ -35,7 +37,7 @@ const font: FontHandler = SdfFontHandler;
|
|
|
35
37
|
const layoutCache = new Map<string, TextLayout>();
|
|
36
38
|
|
|
37
39
|
const getLayoutCacheKey = (props: CoreTextNodeProps): string =>
|
|
38
|
-
`${props.fontFamily}-${props.fontSize}-${props.letterSpacing}-${props.lineHeight}-${props.maxHeight}-${props.maxWidth}-${props.maxLines}-${props.textAlign}-${props.wordBreak}-${props.overflowSuffix}-${props.text}`;
|
|
40
|
+
`${props.fontFamily}-${props.fontStyle}-${props.fontSize}-${props.letterSpacing}-${props.lineHeight}-${props.maxHeight}-${props.maxWidth}-${props.maxLines}-${props.textAlign}-${props.wordBreak}-${props.overflowSuffix}-${props.text}`;
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* SDF text renderer using MSDF/SDF fonts with WebGL
|
|
@@ -50,6 +52,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
50
52
|
return {
|
|
51
53
|
width: 0,
|
|
52
54
|
height: 0,
|
|
55
|
+
trimmedHeight: 0,
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -61,6 +64,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
61
64
|
hasRemainingText: false,
|
|
62
65
|
width: layout.width,
|
|
63
66
|
height: layout.height,
|
|
67
|
+
trimmedHeight: layout.trimmedHeight,
|
|
64
68
|
layout,
|
|
65
69
|
};
|
|
66
70
|
}
|
|
@@ -72,6 +76,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
72
76
|
return {
|
|
73
77
|
width: 0,
|
|
74
78
|
height: 0,
|
|
79
|
+
trimmedHeight: 0,
|
|
75
80
|
};
|
|
76
81
|
}
|
|
77
82
|
|
|
@@ -85,6 +90,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
85
90
|
hasRemainingText: false,
|
|
86
91
|
width: layout.width,
|
|
87
92
|
height: layout.height,
|
|
93
|
+
trimmedHeight: layout.trimmedHeight,
|
|
88
94
|
layout,
|
|
89
95
|
};
|
|
90
96
|
};
|
|
@@ -144,14 +150,12 @@ const renderQuads = (
|
|
|
144
150
|
cache.vertices,
|
|
145
151
|
cache.glyphCount,
|
|
146
152
|
ctxTexture,
|
|
147
|
-
|
|
148
|
-
renderProps.clippingRect as any,
|
|
153
|
+
renderProps.clippingRect,
|
|
149
154
|
renderProps.worldAlpha,
|
|
150
155
|
layout.width,
|
|
151
156
|
layout.height,
|
|
152
157
|
renderProps.parentHasRenderTexture,
|
|
153
|
-
|
|
154
|
-
renderProps.framebufferDimensions as any,
|
|
158
|
+
renderProps.framebufferDimensions,
|
|
155
159
|
sdfShader!,
|
|
156
160
|
);
|
|
157
161
|
return null;
|
|
@@ -160,7 +164,6 @@ const renderQuads = (
|
|
|
160
164
|
|
|
161
165
|
// --- Cache-miss slow path -----------------------------------------------
|
|
162
166
|
const startIdx = webGlRenderer.sdfBufferIdx;
|
|
163
|
-
|
|
164
167
|
webGlRenderer.addSdfQuads(
|
|
165
168
|
layout.glyphs,
|
|
166
169
|
layout.fontScale,
|
|
@@ -169,13 +172,12 @@ const renderQuads = (
|
|
|
169
172
|
renderProps.worldAlpha,
|
|
170
173
|
layout.distanceRange,
|
|
171
174
|
ctxTexture,
|
|
172
|
-
|
|
173
|
-
renderProps.clippingRect
|
|
175
|
+
|
|
176
|
+
renderProps.clippingRect,
|
|
174
177
|
layout.width,
|
|
175
178
|
layout.height,
|
|
176
179
|
renderProps.parentHasRenderTexture,
|
|
177
|
-
|
|
178
|
-
renderProps.framebufferDimensions as any,
|
|
180
|
+
renderProps.framebufferDimensions,
|
|
179
181
|
sdfShader!,
|
|
180
182
|
);
|
|
181
183
|
|
|
@@ -221,9 +223,14 @@ const generateTextLayout = (
|
|
|
221
223
|
const fontData = fontCache.data;
|
|
222
224
|
const commonFontData = fontData.common;
|
|
223
225
|
const designFontSize = fontData.info.size;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
// common.base = distance from BMFont line-box top to the alphabetic baseline,
|
|
227
|
+
// in atlas design units. Used to convert per-glyph yoffset (BMFont top -> glyph top)
|
|
228
|
+
// into baseline-relative placement.
|
|
229
|
+
const atlasBase = commonFontData.base;
|
|
230
|
+
// When the user does not specify lineHeight, fall back to the engine's
|
|
231
|
+
// 'normal' line height (ascender + lineGap - descender) computed inside
|
|
232
|
+
// mapTextLayout via the supplied metrics. Passing 0 below triggers that path.
|
|
233
|
+
const lineHeight = props.lineHeight;
|
|
227
234
|
|
|
228
235
|
const atlasWidth = commonFontData.scaleW;
|
|
229
236
|
const atlasHeight = commonFontData.scaleH;
|
|
@@ -242,9 +249,9 @@ const generateTextLayout = (
|
|
|
242
249
|
const maxHeight = props.maxHeight;
|
|
243
250
|
const [
|
|
244
251
|
lines,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
_remainingLines,
|
|
253
|
+
_hasRemainingText,
|
|
254
|
+
_bareLineHeight,
|
|
248
255
|
lineHeightPx,
|
|
249
256
|
effectiveWidth,
|
|
250
257
|
effectiveHeight,
|
|
@@ -267,15 +274,16 @@ const generateTextLayout = (
|
|
|
267
274
|
|
|
268
275
|
const glyphs: GlyphLayout[] = [];
|
|
269
276
|
let currentX = 0;
|
|
270
|
-
let
|
|
277
|
+
let baselineY = 0;
|
|
271
278
|
for (let i = 0; i < lineAmount; i++) {
|
|
272
279
|
const line = lines[i] as TextLineStruct;
|
|
273
280
|
const textLine = line[0];
|
|
274
281
|
const textLineLength = textLine.length;
|
|
275
282
|
let prevGlyphId = 0;
|
|
276
283
|
currentX = line[3];
|
|
277
|
-
//
|
|
278
|
-
|
|
284
|
+
// line[4] is the alphabetic baseline Y in screen px. Convert to atlas
|
|
285
|
+
// design units (where glyph.yoffset and atlasBase live).
|
|
286
|
+
baselineY = line[4] / fontScale;
|
|
279
287
|
|
|
280
288
|
for (let j = 0; j < textLineLength; j++) {
|
|
281
289
|
const codepoint = textLine.codePointAt(j) as number;
|
|
@@ -318,10 +326,13 @@ const generateTextLayout = (
|
|
|
318
326
|
// Apply pair kerning before placing this glyph.
|
|
319
327
|
currentX += kerning;
|
|
320
328
|
|
|
321
|
-
//
|
|
329
|
+
// Glyph position in atlas design units. yoffset is measured from the
|
|
330
|
+
// BMFont line-box top; subtracting atlasBase re-anchors it relative to
|
|
331
|
+
// the alphabetic baseline so fonts with different BMFont 'base' values
|
|
332
|
+
// share the same on-screen baseline.
|
|
322
333
|
const glyphLayout: GlyphLayout = {
|
|
323
334
|
x: currentX + glyph.xoffset,
|
|
324
|
-
y:
|
|
335
|
+
y: baselineY + glyph.yoffset - atlasBase,
|
|
325
336
|
width: glyph.width,
|
|
326
337
|
height: glyph.height,
|
|
327
338
|
atlasX: glyph.x * invAtlasWidth,
|
|
@@ -336,15 +347,23 @@ const generateTextLayout = (
|
|
|
336
347
|
currentX += glyph.xadvance + letterSpacing;
|
|
337
348
|
prevGlyphId = glyph.id;
|
|
338
349
|
}
|
|
339
|
-
currentY += lineHeightPx;
|
|
340
350
|
}
|
|
341
351
|
|
|
352
|
+
// Cap-top of first line to descender bottom of last line.
|
|
353
|
+
// descender is negative in NormalizedFontMetrics, so subtracting it
|
|
354
|
+
// adds the descender depth. Zero when there are no rendered lines.
|
|
355
|
+
const trimmedHeight =
|
|
356
|
+
lineAmount > 0
|
|
357
|
+
? metrics.capHeight - metrics.descender + (lineAmount - 1) * lineHeightPx
|
|
358
|
+
: 0;
|
|
359
|
+
|
|
342
360
|
// Convert final dimensions to pixel space for the layout
|
|
343
361
|
return {
|
|
344
362
|
glyphs,
|
|
345
363
|
distanceRange: fontScale * fontData.distanceField.distanceRange,
|
|
346
364
|
width: effectiveWidth * fontScale,
|
|
347
365
|
height: effectiveHeight,
|
|
366
|
+
trimmedHeight,
|
|
348
367
|
fontScale: fontScale,
|
|
349
368
|
lineHeight: lineHeightPx,
|
|
350
369
|
fontFamily,
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
FontMetrics,
|
|
3
3
|
MeasureTextFn,
|
|
4
4
|
NormalizedFontMetrics,
|
|
5
|
+
TextBaselineMode,
|
|
5
6
|
TextLayoutStruct,
|
|
6
7
|
TextLineStruct,
|
|
7
8
|
WrappedLinesStruct,
|
|
@@ -35,18 +36,53 @@ type WrapStrategyFn = (
|
|
|
35
36
|
overflowWidth: number,
|
|
36
37
|
) => [string, number, string];
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Generic Latin-font ratios used when exact metric values aren't available.
|
|
41
|
+
* Within a few percent of OS/2 sCapHeight / sxHeight across the common Latin
|
|
42
|
+
* families (Ubuntu, Noto, Roboto, Arial, etc.).
|
|
43
|
+
*/
|
|
44
|
+
const CAP_HEIGHT_FALLBACK_RATIO = 0.7;
|
|
45
|
+
const X_HEIGHT_FALLBACK_RATIO = 0.5;
|
|
46
|
+
|
|
38
47
|
export const normalizeFontMetrics = (
|
|
39
48
|
metrics: FontMetrics,
|
|
40
49
|
fontSize: number,
|
|
41
50
|
): NormalizedFontMetrics => {
|
|
42
51
|
const scale = fontSize / metrics.unitsPerEm;
|
|
52
|
+
const capHeightUnits =
|
|
53
|
+
metrics.capHeight !== undefined
|
|
54
|
+
? metrics.capHeight
|
|
55
|
+
: metrics.ascender * CAP_HEIGHT_FALLBACK_RATIO;
|
|
56
|
+
const xHeightUnits =
|
|
57
|
+
metrics.xHeight !== undefined
|
|
58
|
+
? metrics.xHeight
|
|
59
|
+
: metrics.ascender * X_HEIGHT_FALLBACK_RATIO;
|
|
43
60
|
return {
|
|
44
61
|
ascender: metrics.ascender * scale,
|
|
45
62
|
descender: metrics.descender * scale,
|
|
46
63
|
lineGap: metrics.lineGap * scale,
|
|
64
|
+
capHeight: capHeightUnits * scale,
|
|
65
|
+
xHeight: xHeightUnits * scale,
|
|
47
66
|
};
|
|
48
67
|
};
|
|
49
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Engine-wide per-line baseline anchor. Configured once at renderer creation
|
|
71
|
+
* via {@link RendererMainSettings.textBaselineMode}, not exposed per node so
|
|
72
|
+
* a single app can't mix anchor models across its text. Defaults to `'cap'`
|
|
73
|
+
* — see {@link TextBaselineMode} for the rationale.
|
|
74
|
+
*/
|
|
75
|
+
let baselineMode: TextBaselineMode = 'cap';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sets the engine-wide baseline anchor. Called by `Stage` during construction;
|
|
79
|
+
* not intended to be called from user code (changing this mid-session would
|
|
80
|
+
* silently reflow every cached text layout).
|
|
81
|
+
*/
|
|
82
|
+
export const setBaselineMode = (mode: TextBaselineMode): void => {
|
|
83
|
+
baselineMode = mode;
|
|
84
|
+
};
|
|
85
|
+
|
|
50
86
|
export const mapTextLayout = (
|
|
51
87
|
measureText: MeasureTextFn,
|
|
52
88
|
metrics: NormalizedFontMetrics,
|
|
@@ -61,14 +97,12 @@ export const mapTextLayout = (
|
|
|
61
97
|
maxWidth: number,
|
|
62
98
|
maxHeight: number,
|
|
63
99
|
): TextLayoutStruct => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const bareLineHeight =
|
|
100
|
+
// Default line height matches CSS 'normal': ascender + lineGap - descender.
|
|
101
|
+
// metrics.descender is negative for descents below the baseline, so
|
|
102
|
+
// subtracting it adds the descent depth.
|
|
103
|
+
const bareLineHeight = metrics.ascender - metrics.descender + metrics.lineGap;
|
|
68
104
|
const lineHeightPx =
|
|
69
105
|
lineHeight <= 3 ? lineHeight * bareLineHeight : lineHeight;
|
|
70
|
-
const lineHeightDelta = lineHeightPx - bareLineHeight;
|
|
71
|
-
const halfDelta = lineHeightDelta * 0.5;
|
|
72
106
|
|
|
73
107
|
let effectiveMaxLines = maxLines;
|
|
74
108
|
|
|
@@ -107,12 +141,27 @@ export const mapTextLayout = (
|
|
|
107
141
|
effectiveMaxLines,
|
|
108
142
|
);
|
|
109
143
|
|
|
110
|
-
|
|
144
|
+
const effectiveLineAmount = lines.length;
|
|
111
145
|
let effectiveMaxWidth = 0;
|
|
112
146
|
|
|
147
|
+
// CSS letter-spacing applies between characters, not after the trailing one.
|
|
148
|
+
// measureText accumulates one advance + letterSpacing per glyph, so each
|
|
149
|
+
// line carries one extra trailing letterSpacing. Trim it once per line for
|
|
150
|
+
// alignment / reported width purposes. Wrap decisions inside the loops above
|
|
151
|
+
// intentionally still use the un-trimmed width (CSS engines also consider
|
|
152
|
+
// trailing letter-spacing during break decisions; only the rendered line
|
|
153
|
+
// width is trimmed).
|
|
154
|
+
if (letterSpacing !== 0) {
|
|
155
|
+
for (let i = 0; i < effectiveLineAmount; i++) {
|
|
156
|
+
const line = lines[i]!;
|
|
157
|
+
if (line[0].length > 0) {
|
|
158
|
+
line[1] -= letterSpacing;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
113
163
|
if (effectiveLineAmount > 0) {
|
|
114
164
|
effectiveMaxWidth = lines[0]![1];
|
|
115
|
-
//check for longest line
|
|
116
165
|
if (effectiveLineAmount > 1) {
|
|
117
166
|
for (let i = 1; i < effectiveLineAmount; i++) {
|
|
118
167
|
effectiveMaxWidth = Math.max(effectiveMaxWidth, lines[i]![1]);
|
|
@@ -134,12 +183,50 @@ export const mapTextLayout = (
|
|
|
134
183
|
|
|
135
184
|
const effectiveMaxHeight = effectiveLineAmount * lineHeightPx;
|
|
136
185
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
186
|
+
// line[4] stores the alphabetic baseline Y of each line in screen px.
|
|
187
|
+
//
|
|
188
|
+
// ── Per-line anchor: cap-height centering ─────────────────────────────
|
|
189
|
+
//
|
|
190
|
+
// baselineY(i) = lineHeight/2 + capHeight/2 + i × lineHeight
|
|
191
|
+
//
|
|
192
|
+
// The baseline sits below the line's geometric mid-line by exactly
|
|
193
|
+
// `capHeight / 2`, so the top of an uppercase letter lands the same
|
|
194
|
+
// distance *above* the mid-line — i.e. capital letters bracket the
|
|
195
|
+
// center symmetrically. Cap-height centering matches what designers
|
|
196
|
+
// expect for UI text (button labels, headings, badges): TXYZ and 1234
|
|
197
|
+
// sit centered; descenders like 'gjpq' hang slightly below, mirroring
|
|
198
|
+
// CSS button behavior in browsers.
|
|
199
|
+
//
|
|
200
|
+
// Alternative anchors considered (kept here for the record):
|
|
201
|
+
//
|
|
202
|
+
// ── x-height centering ──────────────────────────────────────────────
|
|
203
|
+
// baselineY(i) = lineHeight/2 + xHeight/2 + i × lineHeight
|
|
204
|
+
// Centers lowercase letters on the mid-line. Matches CSS inline
|
|
205
|
+
// `vertical-align: middle`. Reads well for running body text but
|
|
206
|
+
// capitals appear high in headings/labels — wrong default for TV UI.
|
|
207
|
+
//
|
|
208
|
+
// ── line-box centering (pre-cap-height behavior) ───────────────────
|
|
209
|
+
// const halfLeading = (lineHeightPx − bareLineHeight) / 2
|
|
210
|
+
// baselineY(i) = halfLeading + ascender + i × lineHeight
|
|
211
|
+
// Centers the abstract asc-to-desc-plus-leading rectangle. The visible
|
|
212
|
+
// ink lands noticeably high because asc/(asc−desc) is asymmetric for
|
|
213
|
+
// most Latin fonts (~4.2:1 for Ubuntu). Mathematically tidy, visually
|
|
214
|
+
// wrong.
|
|
215
|
+
//
|
|
216
|
+
// The active anchor is configured at renderer creation via
|
|
217
|
+
// `RendererMainSettings.textBaselineMode`. Defaults to `'cap'`.
|
|
218
|
+
let firstBaselineY: number;
|
|
219
|
+
if (baselineMode === 'x') {
|
|
220
|
+
firstBaselineY = (lineHeightPx + metrics.xHeight) * 0.5;
|
|
221
|
+
} else if (baselineMode === 'linebox') {
|
|
222
|
+
const halfLeading = (lineHeightPx - bareLineHeight) * 0.5;
|
|
223
|
+
firstBaselineY = halfLeading + metrics.ascender;
|
|
224
|
+
} else {
|
|
225
|
+
firstBaselineY = (lineHeightPx + metrics.capHeight) * 0.5;
|
|
226
|
+
}
|
|
140
227
|
for (let i = 0; i < effectiveLineAmount; i++) {
|
|
141
228
|
const line = lines[i] as TextLineStruct;
|
|
142
|
-
line[4] =
|
|
229
|
+
line[4] = firstBaselineY + lineHeightPx * i;
|
|
143
230
|
}
|
|
144
231
|
|
|
145
232
|
return [
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { Dimensions } from '../../common/CommonTypes.js';
|
|
1
2
|
import type { CoreTextNodeProps } from '../CoreTextNode.js';
|
|
3
|
+
import type { RectWithValid } from '../lib/utils.js';
|
|
2
4
|
import type { CoreRenderer } from '../renderers/CoreRenderer.js';
|
|
3
5
|
import type { SdfRenderOp } from '../renderers/webgl/SdfRenderOp.js';
|
|
4
6
|
import type { Stage } from '../Stage.js';
|
|
@@ -12,6 +14,24 @@ export type TextBaseline =
|
|
|
12
14
|
| 'bottom';
|
|
13
15
|
export type TextVerticalAlign = 'top' | 'middle' | 'bottom';
|
|
14
16
|
export type TextRenderers = 'canvas' | 'sdf';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Selects which font-derived height the text layout engine centers on each
|
|
20
|
+
* line's geometric mid-line. Configured via {@link RendererMainSettings} and
|
|
21
|
+
* cannot be overridden per node — see the engine-wide reasoning in
|
|
22
|
+
* `TextLayoutEngine.mapTextLayout`.
|
|
23
|
+
*
|
|
24
|
+
* - `'cap'` (default): capital letters centered. Best for UI text — button
|
|
25
|
+
* labels, headings, badges. Capitals and digits bracket the center
|
|
26
|
+
* symmetrically; descenders hang slightly below, matching CSS button
|
|
27
|
+
* behavior in browsers.
|
|
28
|
+
* - `'x'`: lowercase x-height centered. Better for running body text;
|
|
29
|
+
* capitals appear slightly high in headings.
|
|
30
|
+
* - `'linebox'`: legacy. Centers the abstract asc-to-desc-plus-leading
|
|
31
|
+
* rectangle. Mathematically tidy but visually unbalanced because most
|
|
32
|
+
* Latin fonts have asymmetric asc/desc ratios.
|
|
33
|
+
*/
|
|
34
|
+
export type TextBaselineMode = 'cap' | 'x' | 'linebox';
|
|
15
35
|
/**
|
|
16
36
|
* Structure mapping font family names to a set of font faces.
|
|
17
37
|
*/
|
|
@@ -39,24 +59,54 @@ export interface FontMetrics {
|
|
|
39
59
|
* The number of font units per 1 EM.
|
|
40
60
|
*/
|
|
41
61
|
unitsPerEm: number;
|
|
62
|
+
/**
|
|
63
|
+
* The distance, in font units, from the baseline to the top of an uppercase
|
|
64
|
+
* letter (OS/2 sCapHeight).
|
|
65
|
+
*
|
|
66
|
+
* Used by the layout engine to vertically center capital letters on each
|
|
67
|
+
* line's geometric mid-line. When absent, the SDF backend derives this
|
|
68
|
+
* value from glyph `H` (id 72) in the BMFont atlas; the Canvas backend
|
|
69
|
+
* falls back to `0.7 × ascender` (a generic Latin-font approximation).
|
|
70
|
+
*/
|
|
71
|
+
capHeight?: number;
|
|
72
|
+
/**
|
|
73
|
+
* The distance, in font units, from the baseline to the top of a lowercase
|
|
74
|
+
* letter (OS/2 sxHeight). Optional; used only when the baseline-anchor
|
|
75
|
+
* mode is set to x-height centering (experimental).
|
|
76
|
+
*/
|
|
77
|
+
xHeight?: number;
|
|
42
78
|
}
|
|
43
79
|
|
|
44
80
|
/**
|
|
45
|
-
* Normalized font metrics where values are expressed
|
|
81
|
+
* Normalized font metrics where values are expressed in pixels at the
|
|
82
|
+
* configured font size (em-px).
|
|
46
83
|
*/
|
|
47
84
|
export interface NormalizedFontMetrics {
|
|
48
85
|
/**
|
|
49
|
-
* The distance,
|
|
86
|
+
* The distance, in em-px, from the baseline to the highest point of the font.
|
|
50
87
|
*/
|
|
51
88
|
ascender: number;
|
|
52
89
|
/**
|
|
53
|
-
* The distance,
|
|
90
|
+
* The distance, in em-px, from the baseline to the lowest point of the font.
|
|
54
91
|
*/
|
|
55
92
|
descender: number;
|
|
56
93
|
/**
|
|
57
|
-
* The additional space used in the calculation of the default line height
|
|
94
|
+
* The additional space used in the calculation of the default line height, in em-px.
|
|
58
95
|
*/
|
|
59
96
|
lineGap: number;
|
|
97
|
+
/**
|
|
98
|
+
* The distance, in em-px, from the baseline to the top of an uppercase letter.
|
|
99
|
+
* Always populated; derived or approximated when {@link FontMetrics.capHeight}
|
|
100
|
+
* is not provided by the caller.
|
|
101
|
+
*/
|
|
102
|
+
capHeight: number;
|
|
103
|
+
/**
|
|
104
|
+
* The distance, in em-px, from the baseline to the top of a lowercase letter.
|
|
105
|
+
* Always populated; derived from glyph `x` for SDF, falls back to
|
|
106
|
+
* `0.5 × ascender` otherwise. Only used by the experimental x-height
|
|
107
|
+
* baseline-anchor mode.
|
|
108
|
+
*/
|
|
109
|
+
xHeight: number;
|
|
60
110
|
}
|
|
61
111
|
|
|
62
112
|
/**
|
|
@@ -181,13 +231,18 @@ export interface TrProps extends TrFontProps {
|
|
|
181
231
|
*/
|
|
182
232
|
maxLines: number;
|
|
183
233
|
/**
|
|
184
|
-
* Vertical
|
|
234
|
+
* Vertical alignment of the text block within its containing box.
|
|
185
235
|
*
|
|
186
236
|
* @remarks
|
|
187
|
-
*
|
|
188
|
-
*
|
|
237
|
+
* The containing box is `maxHeight` if set, otherwise the node's
|
|
238
|
+
* own `h` (which a flex parent or the user may have grown beyond
|
|
239
|
+
* the intrinsic text height). Activates whenever the box is taller
|
|
240
|
+
* than the intrinsic text height. Composes with `textBaselineMode`
|
|
241
|
+
* (per-line anchor). CSS line-box semantics — `'top'` leaves
|
|
242
|
+
* half-leading above the first line's cap-top; `'bottom'` leaves
|
|
243
|
+
* half-leading below the last line's descender.
|
|
189
244
|
*
|
|
190
|
-
* @default
|
|
245
|
+
* @default top
|
|
191
246
|
*/
|
|
192
247
|
verticalAlign: TextVerticalAlign;
|
|
193
248
|
/**
|
|
@@ -262,6 +317,11 @@ export interface TextLayout {
|
|
|
262
317
|
* Total text height
|
|
263
318
|
*/
|
|
264
319
|
height: number;
|
|
320
|
+
/**
|
|
321
|
+
* Trimmed text height — cap-top of the first line to descender bottom
|
|
322
|
+
* of the last line. See `TextRenderInfo.trimmedHeight`.
|
|
323
|
+
*/
|
|
324
|
+
trimmedHeight: number;
|
|
265
325
|
/**
|
|
266
326
|
* Font scale factor
|
|
267
327
|
*/
|
|
@@ -345,11 +405,11 @@ export interface TextRenderProps {
|
|
|
345
405
|
offsetY: number;
|
|
346
406
|
worldAlpha: number;
|
|
347
407
|
globalTransform: Float32Array;
|
|
348
|
-
clippingRect:
|
|
408
|
+
clippingRect: RectWithValid;
|
|
349
409
|
width: number;
|
|
350
410
|
height: number;
|
|
351
411
|
parentHasRenderTexture: boolean;
|
|
352
|
-
framebufferDimensions:
|
|
412
|
+
framebufferDimensions: Dimensions | null;
|
|
353
413
|
stage: Stage;
|
|
354
414
|
/** Optional SDF vertex cache — passed by CoreTextNode for cache-hit fast path. */
|
|
355
415
|
sdfCache?: SdfVertexCache;
|
|
@@ -358,6 +418,21 @@ export interface TextRenderProps {
|
|
|
358
418
|
export interface TextRenderInfo {
|
|
359
419
|
width: number;
|
|
360
420
|
height: number;
|
|
421
|
+
/**
|
|
422
|
+
* Height of the visible glyph extent — from the first line's cap-top to
|
|
423
|
+
* the last line's descender bottom. Excludes half-leading and the slack
|
|
424
|
+
* between the font's ascender and cap-top.
|
|
425
|
+
*
|
|
426
|
+
* @remarks
|
|
427
|
+
* Formula: `capHeight − descender + (lines − 1) × lineHeightPx`
|
|
428
|
+
* (descender is negative in font metrics, so subtracting it adds the
|
|
429
|
+
* descender depth). For empty text, this is 0.
|
|
430
|
+
*
|
|
431
|
+
* Use this when you want flex `alignItems: 'center'` (or any layout
|
|
432
|
+
* that aligns by node `h`) to optically center the visible glyphs.
|
|
433
|
+
* Set `node.h = node.trimmedHeight` after the `loaded` event.
|
|
434
|
+
*/
|
|
435
|
+
trimmedHeight: number;
|
|
361
436
|
hasRemainingText?: boolean;
|
|
362
437
|
remainingLines?: number;
|
|
363
438
|
imageData?: ImageData | null; // Image data for Canvas Text Renderer
|
package/src/main-api/Renderer.ts
CHANGED
|
@@ -6,7 +6,10 @@ import { CoreNode, type CoreNodeProps } from '../core/CoreNode.js';
|
|
|
6
6
|
import { type CoreTextNodeProps } from '../core/CoreTextNode.js';
|
|
7
7
|
import type { INode, INodeProps, ITextNode, ITextNodeProps } from './INode.js';
|
|
8
8
|
import type { TextureMemoryManagerSettings } from '../core/TextureMemoryManager.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
TextBaselineMode,
|
|
11
|
+
TextRenderer,
|
|
12
|
+
} from '../core/text-rendering/TextRenderer.js';
|
|
10
13
|
import type { CanvasRenderer } from '../core/renderers/canvas/CanvasRenderer.js';
|
|
11
14
|
import type { WebGlRenderer } from '../core/renderers/webgl/WebGlRenderer.js';
|
|
12
15
|
import type { Inspector, InspectorOptions } from './Inspector.js';
|
|
@@ -363,6 +366,28 @@ export type RendererMainSettings = RendererRuntimeSettings & {
|
|
|
363
366
|
*/
|
|
364
367
|
fontEngines: TextRenderer[];
|
|
365
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Per-line vertical baseline anchor used by the text layout engine.
|
|
371
|
+
*
|
|
372
|
+
* @remarks
|
|
373
|
+
* Picks which font-derived height is centered on each line's geometric
|
|
374
|
+
* mid-line. This is engine-wide and intentionally not exposed per node —
|
|
375
|
+
* mixing anchor models within one app produces visually inconsistent text.
|
|
376
|
+
*
|
|
377
|
+
* - `'cap'` (default): capital letters and digits sit centered on the line.
|
|
378
|
+
* Best fit for UI text (button labels, headings, badges); descenders on
|
|
379
|
+
* words like 'gjpq' hang slightly below center, matching CSS button
|
|
380
|
+
* behavior in browsers.
|
|
381
|
+
* - `'x'`: lowercase x-height is centered. Better for running body text;
|
|
382
|
+
* capitals appear slightly high in headings.
|
|
383
|
+
* - `'linebox'`: legacy mode. Centers the asc/lineGap/desc rectangle.
|
|
384
|
+
* Mathematically tidy but visually unbalanced because most Latin fonts
|
|
385
|
+
* have asymmetric asc/desc ratios.
|
|
386
|
+
*
|
|
387
|
+
* @defaultValue `'cap'`
|
|
388
|
+
*/
|
|
389
|
+
textBaselineMode: TextBaselineMode;
|
|
390
|
+
|
|
366
391
|
/**
|
|
367
392
|
* Force WebGL2
|
|
368
393
|
*
|
|
@@ -516,7 +541,7 @@ export class RendererMain extends EventEmitter {
|
|
|
516
541
|
boundsMargin: settings.boundsMargin || 0,
|
|
517
542
|
deviceLogicalPixelRatio: settings.deviceLogicalPixelRatio || 1,
|
|
518
543
|
devicePhysicalPixelRatio:
|
|
519
|
-
settings.devicePhysicalPixelRatio ||
|
|
544
|
+
settings.devicePhysicalPixelRatio || this.windowDevicePixelRatio() || 1,
|
|
520
545
|
clearColor: settings.clearColor ?? 0x00000000,
|
|
521
546
|
fpsUpdateInterval: settings.fpsUpdateInterval || 0,
|
|
522
547
|
enableClear: settings.enableClear ?? true,
|
|
@@ -530,6 +555,7 @@ export class RendererMain extends EventEmitter {
|
|
|
530
555
|
renderEngine: settings.renderEngine,
|
|
531
556
|
quadBufferSize: settings.quadBufferSize ?? 4 * 1024 * 1024,
|
|
532
557
|
fontEngines: settings.fontEngines ?? [],
|
|
558
|
+
textBaselineMode: settings.textBaselineMode ?? 'cap',
|
|
533
559
|
textureProcessingTimeLimit: settings.textureProcessingTimeLimit || 10,
|
|
534
560
|
canvas: settings.canvas,
|
|
535
561
|
createImageBitmapSupport: settings.createImageBitmapSupport || 'full',
|
|
@@ -588,6 +614,7 @@ export class RendererMain extends EventEmitter {
|
|
|
588
614
|
eventBus: this,
|
|
589
615
|
quadBufferSize: settings.quadBufferSize!,
|
|
590
616
|
fontEngines: settings.fontEngines!,
|
|
617
|
+
textBaselineMode: settings.textBaselineMode!,
|
|
591
618
|
inspector: settings.inspector !== null,
|
|
592
619
|
targetFPS: settings.targetFPS!,
|
|
593
620
|
textureProcessingTimeLimit: settings.textureProcessingTimeLimit!,
|
|
@@ -1039,4 +1066,8 @@ export class RendererMain extends EventEmitter {
|
|
|
1039
1066
|
this.stage.options.targetFPS = fps > 0 ? fps : 0;
|
|
1040
1067
|
this.stage.updateTargetFrameTime();
|
|
1041
1068
|
}
|
|
1069
|
+
|
|
1070
|
+
private windowDevicePixelRatio() {
|
|
1071
|
+
return typeof window !== 'undefined' ? window.devicePixelRatio : undefined;
|
|
1072
|
+
}
|
|
1042
1073
|
}
|