@solidtv/renderer 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/core/CoreNode.js +4 -2
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.js +9 -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/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/Border.js +6 -4
- package/dist/src/core/shaders/webgl/Border.js.map +1 -1
- 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/shaders/webgl/RoundedWithBorder.js +6 -4
- package/dist/src/core/shaders/webgl/RoundedWithBorder.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js +6 -4
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasTextRenderer.js +5 -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 +21 -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 +61 -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/core/CoreNode.ts +6 -2
- package/src/core/CoreTextNode.ts +9 -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/RadialProgressTemplate.test.ts +125 -0
- package/src/core/shaders/templates/RadialProgressTemplate.ts +112 -0
- package/src/core/shaders/webgl/Border.ts +6 -4
- package/src/core/shaders/webgl/RadialProgress.ts +163 -0
- package/src/core/shaders/webgl/RoundedWithBorder.ts +6 -4
- package/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +6 -4
- package/src/core/text-rendering/CanvasTextRenderer.ts +5 -2
- package/src/core/text-rendering/SdfFontHandler.ts +37 -0
- package/src/core/text-rendering/SdfTextRenderer.ts +28 -22
- package/src/core/text-rendering/TextLayoutEngine.ts +99 -12
- package/src/core/text-rendering/TextRenderer.ts +62 -10
- package/src/main-api/Renderer.ts +33 -2
|
@@ -65,10 +65,12 @@ export const RoundedWithBorder: WebGlShaderType<RoundedWithBorderProps> = {
|
|
|
65
65
|
v_outerSize = vec2(0.0);
|
|
66
66
|
|
|
67
67
|
if(borderZero == 0.0) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
float
|
|
71
|
-
float
|
|
68
|
+
vec4 adjustedBorderWidth = u_borderWidth - 1.0 + clamp(u_borderWidth, -1.0, 1.0);
|
|
69
|
+
|
|
70
|
+
float borderTop = adjustedBorderWidth.x;
|
|
71
|
+
float borderRight = adjustedBorderWidth.y;
|
|
72
|
+
float borderBottom = adjustedBorderWidth.z;
|
|
73
|
+
float borderLeft = adjustedBorderWidth.w;
|
|
72
74
|
|
|
73
75
|
v_outerBorderUv = vec2(0.0);
|
|
74
76
|
v_innerBorderUv = vec2(0.0);
|
|
@@ -72,10 +72,12 @@ export const RoundedWithBorderAndShadow: WebGlShaderType<RoundedWithBorderAndSha
|
|
|
72
72
|
v_outerSize = vec2(0.0);
|
|
73
73
|
|
|
74
74
|
if(borderZero == 0.0) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
float
|
|
78
|
-
float
|
|
75
|
+
vec4 adjustedBorderWidth = u_borderWidth - 1.0 + clamp(u_borderWidth, -1.0, 1.0);
|
|
76
|
+
|
|
77
|
+
float borderTop = adjustedBorderWidth.x;
|
|
78
|
+
float borderRight = adjustedBorderWidth.y;
|
|
79
|
+
float borderBottom = adjustedBorderWidth.z;
|
|
80
|
+
float borderLeft = adjustedBorderWidth.w;
|
|
79
81
|
|
|
80
82
|
v_outerBorderUv = vec2(0.0);
|
|
81
83
|
v_innerBorderUv = vec2(0.0);
|
|
@@ -109,7 +109,10 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
109
109
|
const font = `${fontStyle} ${fontSize}px Unknown, ${fontFamily}`;
|
|
110
110
|
// Get font metrics and calculate line height
|
|
111
111
|
measureContext.font = font;
|
|
112
|
-
|
|
112
|
+
// The layout engine emits line[4] as the alphabetic baseline Y, matching
|
|
113
|
+
// CSS line box layout. Both contexts must use 'alphabetic' so fillText draws
|
|
114
|
+
// the baseline exactly at line[4].
|
|
115
|
+
measureContext.textBaseline = 'alphabetic';
|
|
113
116
|
|
|
114
117
|
const metrics = CanvasFontHandler.getFontMetrics(fontFamily, fontSize);
|
|
115
118
|
|
|
@@ -150,7 +153,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
150
153
|
const a = color & 0xff;
|
|
151
154
|
context.fillStyle = `rgba(${r},${g},${b},${a / 255})`;
|
|
152
155
|
context.font = font;
|
|
153
|
-
context.textBaseline = '
|
|
156
|
+
context.textBaseline = 'alphabetic';
|
|
154
157
|
|
|
155
158
|
// Performance optimization for large fonts
|
|
156
159
|
if (fontSize >= 128) {
|
|
@@ -229,6 +229,43 @@ const processFontData = (
|
|
|
229
229
|
unitsPerEm: 1000,
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
+
// Derive cap-height from the atlas when the metrics block doesn't already
|
|
233
|
+
// supply it. The layout engine uses this value to vertically center
|
|
234
|
+
// capital letters on each line. BMFont stores per-glyph `yoffset` as the
|
|
235
|
+
// distance from the line-box top to the glyph's top, and `common.base` as
|
|
236
|
+
// the distance from the line-box top to the alphabetic baseline — so the
|
|
237
|
+
// distance from the baseline up to the top of 'H' (atlas design px) is
|
|
238
|
+
// `common.base - H.yoffset`. Converted into font units it slots into
|
|
239
|
+
// `FontMetrics.capHeight` alongside the existing ascender / descender
|
|
240
|
+
// values and flows through `normalizeFontMetrics`.
|
|
241
|
+
if (metrics.capHeight === undefined) {
|
|
242
|
+
const capGlyph = glyphMap.get(72); // 'H'
|
|
243
|
+
if (capGlyph !== undefined) {
|
|
244
|
+
const capHeightAtlasPx = fontData.common.base - capGlyph.yoffset;
|
|
245
|
+
metrics = {
|
|
246
|
+
...metrics,
|
|
247
|
+
capHeight: (capHeightAtlasPx / fontData.info.size) * metrics.unitsPerEm,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// If 'H' isn't in the atlas (icon-only fonts, etc.) we leave capHeight
|
|
251
|
+
// undefined and rely on the 0.7 × ascender fallback inside
|
|
252
|
+
// normalizeFontMetrics.
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Same derivation for x-height using glyph 'x' (id 120). Only consumed
|
|
256
|
+
// when `RendererMainSettings.textBaselineMode === 'x'`; otherwise the
|
|
257
|
+
// 0.5 × ascender fallback inside `normalizeFontMetrics` is sufficient.
|
|
258
|
+
if (metrics.xHeight === undefined) {
|
|
259
|
+
const xGlyph = glyphMap.get(120); // 'x'
|
|
260
|
+
if (xGlyph !== undefined) {
|
|
261
|
+
const xHeightAtlasPx = fontData.common.base - xGlyph.yoffset;
|
|
262
|
+
metrics = {
|
|
263
|
+
...metrics,
|
|
264
|
+
xHeight: (xHeightAtlasPx / fontData.info.size) * metrics.unitsPerEm,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
232
269
|
// Cache processed data
|
|
233
270
|
fontCache.set(fontFamily, {
|
|
234
271
|
data: fontData,
|
|
@@ -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
|
|
@@ -144,14 +146,12 @@ const renderQuads = (
|
|
|
144
146
|
cache.vertices,
|
|
145
147
|
cache.glyphCount,
|
|
146
148
|
ctxTexture,
|
|
147
|
-
|
|
148
|
-
renderProps.clippingRect as any,
|
|
149
|
+
renderProps.clippingRect,
|
|
149
150
|
renderProps.worldAlpha,
|
|
150
151
|
layout.width,
|
|
151
152
|
layout.height,
|
|
152
153
|
renderProps.parentHasRenderTexture,
|
|
153
|
-
|
|
154
|
-
renderProps.framebufferDimensions as any,
|
|
154
|
+
renderProps.framebufferDimensions,
|
|
155
155
|
sdfShader!,
|
|
156
156
|
);
|
|
157
157
|
return null;
|
|
@@ -160,7 +160,6 @@ const renderQuads = (
|
|
|
160
160
|
|
|
161
161
|
// --- Cache-miss slow path -----------------------------------------------
|
|
162
162
|
const startIdx = webGlRenderer.sdfBufferIdx;
|
|
163
|
-
|
|
164
163
|
webGlRenderer.addSdfQuads(
|
|
165
164
|
layout.glyphs,
|
|
166
165
|
layout.fontScale,
|
|
@@ -169,13 +168,12 @@ const renderQuads = (
|
|
|
169
168
|
renderProps.worldAlpha,
|
|
170
169
|
layout.distanceRange,
|
|
171
170
|
ctxTexture,
|
|
172
|
-
|
|
173
|
-
renderProps.clippingRect
|
|
171
|
+
|
|
172
|
+
renderProps.clippingRect,
|
|
174
173
|
layout.width,
|
|
175
174
|
layout.height,
|
|
176
175
|
renderProps.parentHasRenderTexture,
|
|
177
|
-
|
|
178
|
-
renderProps.framebufferDimensions as any,
|
|
176
|
+
renderProps.framebufferDimensions,
|
|
179
177
|
sdfShader!,
|
|
180
178
|
);
|
|
181
179
|
|
|
@@ -221,9 +219,14 @@ const generateTextLayout = (
|
|
|
221
219
|
const fontData = fontCache.data;
|
|
222
220
|
const commonFontData = fontData.common;
|
|
223
221
|
const designFontSize = fontData.info.size;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
// common.base = distance from BMFont line-box top to the alphabetic baseline,
|
|
223
|
+
// in atlas design units. Used to convert per-glyph yoffset (BMFont top -> glyph top)
|
|
224
|
+
// into baseline-relative placement.
|
|
225
|
+
const atlasBase = commonFontData.base;
|
|
226
|
+
// When the user does not specify lineHeight, fall back to the engine's
|
|
227
|
+
// 'normal' line height (ascender + lineGap - descender) computed inside
|
|
228
|
+
// mapTextLayout via the supplied metrics. Passing 0 below triggers that path.
|
|
229
|
+
const lineHeight = props.lineHeight;
|
|
227
230
|
|
|
228
231
|
const atlasWidth = commonFontData.scaleW;
|
|
229
232
|
const atlasHeight = commonFontData.scaleH;
|
|
@@ -242,9 +245,9 @@ const generateTextLayout = (
|
|
|
242
245
|
const maxHeight = props.maxHeight;
|
|
243
246
|
const [
|
|
244
247
|
lines,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
_remainingLines,
|
|
249
|
+
_hasRemainingText,
|
|
250
|
+
_bareLineHeight,
|
|
248
251
|
lineHeightPx,
|
|
249
252
|
effectiveWidth,
|
|
250
253
|
effectiveHeight,
|
|
@@ -267,15 +270,16 @@ const generateTextLayout = (
|
|
|
267
270
|
|
|
268
271
|
const glyphs: GlyphLayout[] = [];
|
|
269
272
|
let currentX = 0;
|
|
270
|
-
let
|
|
273
|
+
let baselineY = 0;
|
|
271
274
|
for (let i = 0; i < lineAmount; i++) {
|
|
272
275
|
const line = lines[i] as TextLineStruct;
|
|
273
276
|
const textLine = line[0];
|
|
274
277
|
const textLineLength = textLine.length;
|
|
275
278
|
let prevGlyphId = 0;
|
|
276
279
|
currentX = line[3];
|
|
277
|
-
//
|
|
278
|
-
|
|
280
|
+
// line[4] is the alphabetic baseline Y in screen px. Convert to atlas
|
|
281
|
+
// design units (where glyph.yoffset and atlasBase live).
|
|
282
|
+
baselineY = line[4] / fontScale;
|
|
279
283
|
|
|
280
284
|
for (let j = 0; j < textLineLength; j++) {
|
|
281
285
|
const codepoint = textLine.codePointAt(j) as number;
|
|
@@ -318,10 +322,13 @@ const generateTextLayout = (
|
|
|
318
322
|
// Apply pair kerning before placing this glyph.
|
|
319
323
|
currentX += kerning;
|
|
320
324
|
|
|
321
|
-
//
|
|
325
|
+
// Glyph position in atlas design units. yoffset is measured from the
|
|
326
|
+
// BMFont line-box top; subtracting atlasBase re-anchors it relative to
|
|
327
|
+
// the alphabetic baseline so fonts with different BMFont 'base' values
|
|
328
|
+
// share the same on-screen baseline.
|
|
322
329
|
const glyphLayout: GlyphLayout = {
|
|
323
330
|
x: currentX + glyph.xoffset,
|
|
324
|
-
y:
|
|
331
|
+
y: baselineY + glyph.yoffset - atlasBase,
|
|
325
332
|
width: glyph.width,
|
|
326
333
|
height: glyph.height,
|
|
327
334
|
atlasX: glyph.x * invAtlasWidth,
|
|
@@ -336,7 +343,6 @@ const generateTextLayout = (
|
|
|
336
343
|
currentX += glyph.xadvance + letterSpacing;
|
|
337
344
|
prevGlyphId = glyph.id;
|
|
338
345
|
}
|
|
339
|
-
currentY += lineHeightPx;
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
// Convert final dimensions to pixel space for the layout
|
|
@@ -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,15 @@ export interface TrProps extends TrFontProps {
|
|
|
181
231
|
*/
|
|
182
232
|
maxLines: number;
|
|
183
233
|
/**
|
|
184
|
-
* Vertical
|
|
234
|
+
* Vertical alignment of the text block within `maxHeight`.
|
|
185
235
|
*
|
|
186
236
|
* @remarks
|
|
187
|
-
*
|
|
188
|
-
*
|
|
237
|
+
* Activates when `maxHeight > 0`. Composes with `textBaselineMode`
|
|
238
|
+
* (per-line anchor). CSS line-box semantics — `'top'` leaves
|
|
239
|
+
* half-leading above the first line's cap-top; `'bottom'` leaves
|
|
240
|
+
* half-leading below the last line's descender.
|
|
189
241
|
*
|
|
190
|
-
* @default
|
|
242
|
+
* @default top
|
|
191
243
|
*/
|
|
192
244
|
verticalAlign: TextVerticalAlign;
|
|
193
245
|
/**
|
|
@@ -345,11 +397,11 @@ export interface TextRenderProps {
|
|
|
345
397
|
offsetY: number;
|
|
346
398
|
worldAlpha: number;
|
|
347
399
|
globalTransform: Float32Array;
|
|
348
|
-
clippingRect:
|
|
400
|
+
clippingRect: RectWithValid;
|
|
349
401
|
width: number;
|
|
350
402
|
height: number;
|
|
351
403
|
parentHasRenderTexture: boolean;
|
|
352
|
-
framebufferDimensions:
|
|
404
|
+
framebufferDimensions: Dimensions | null;
|
|
353
405
|
stage: Stage;
|
|
354
406
|
/** Optional SDF vertex cache — passed by CoreTextNode for cache-hit fast path. */
|
|
355
407
|
sdfCache?: SdfVertexCache;
|
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
|
}
|