ntk 8.8.2 → 8.9.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/lib/glswapchain.js +51 -5
- package/lib/text/fontmanager.js +29 -7
- package/lib/text/layout.js +6 -2
- package/lib/text/shape.js +64 -2
- package/package.json +1 -1
package/lib/glswapchain.js
CHANGED
|
@@ -108,23 +108,69 @@ export class GLSwapchain {
|
|
|
108
108
|
const gen = this.generation;
|
|
109
109
|
if (gen && gen.width === w && gen.height === h) return gen.surface;
|
|
110
110
|
this._retire(gen);
|
|
111
|
-
// a generation that had to go linear keeps that layout: whatever
|
|
112
|
-
//
|
|
111
|
+
// a generation that had to go linear keeps that layout: whatever refused
|
|
112
|
+
// the tiled buffer — the server, or EGL itself — has not changed since
|
|
113
113
|
return this._newGeneration(w, h, gen ? gen.linear : false);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** A GBM surface at this size, in the layout asked for. */
|
|
117
|
+
_createSurface(w, h, linear) {
|
|
118
|
+
return linear
|
|
119
|
+
? this.gpu.createSurface(w, h, this.dri.GBM_USE.RENDERING | this.dri.GBM_USE.LINEAR)
|
|
120
|
+
: this.gpu.createSurface(w, h);
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
_newGeneration(w, h, linear) {
|
|
117
|
-
const use = linear ? this.dri.GBM_USE.RENDERING | this.dri.GBM_USE.LINEAR : undefined;
|
|
118
124
|
let surface;
|
|
119
125
|
try {
|
|
120
|
-
surface =
|
|
126
|
+
surface = this._createSurface(w, h, linear);
|
|
121
127
|
} catch (err) {
|
|
122
|
-
|
|
128
|
+
// Some drivers will not give EGL a window surface over a render-only
|
|
129
|
+
// GBM buffer at all: NVIDIA's answers eglCreateWindowSurface with
|
|
130
|
+
// EGL_BAD_MATCH, and takes the very same size and format once the
|
|
131
|
+
// buffer is linear. That is the retry an import refusal gets below,
|
|
132
|
+
// one step earlier — there the server would not read the buffer, here
|
|
133
|
+
// EGL would not draw into it, and a linear layout is what both of them
|
|
134
|
+
// accept. Cheap to try: the alternative is no direct rendering at all.
|
|
135
|
+
if (linear || !this.policy.linearFallback) {
|
|
136
|
+
throw this._surfaceError(w, h, linear ? 'linear' : 'tiled', err.message, err);
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
surface = this._createSurface(w, h, true);
|
|
140
|
+
} catch (retryErr) {
|
|
141
|
+
throw this._surfaceError(
|
|
142
|
+
w,
|
|
143
|
+
h,
|
|
144
|
+
'tiled or linear',
|
|
145
|
+
`${err.message}; linear: ${retryErr.message}`,
|
|
146
|
+
err
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
linear = true;
|
|
123
150
|
}
|
|
124
151
|
this.generation = new Generation(surface, w, h, linear);
|
|
125
152
|
return surface;
|
|
126
153
|
}
|
|
127
154
|
|
|
155
|
+
/** No surface, naming the layouts that were refused and what to change. */
|
|
156
|
+
_surfaceError(w, h, tried, detail, cause) {
|
|
157
|
+
const hint =
|
|
158
|
+
tried === 'tiled'
|
|
159
|
+
? 'A driver that will not draw into a tiled buffer (NVIDIA) takes a linear one,\n' +
|
|
160
|
+
'which is the retry glPolicy: { linearFallback: true } — the default — makes.\n' +
|
|
161
|
+
'See docs/context-gles.md#buffer-layout.'
|
|
162
|
+
: 'No layout this device offers can back a window surface for this driver.\n' +
|
|
163
|
+
"Another render node may: glPolicy: { devicePath: '/dev/dri/renderD###' }.\n" +
|
|
164
|
+
"Failing that, glPolicy: 'indirect' draws with no GPU buffer at all.\n" +
|
|
165
|
+
'See docs/context-gles.md#buffer-layout.';
|
|
166
|
+
return glError(
|
|
167
|
+
GLError.CONTEXT_FAILED,
|
|
168
|
+
`could not create a ${w}x${h} GPU surface (${tried}): ${detail}`,
|
|
169
|
+
hint,
|
|
170
|
+
cause
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
128
174
|
/** Can a frame be drawn and presented right now? */
|
|
129
175
|
canRender() {
|
|
130
176
|
if (this.destroyed || this.error) return false;
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -43,6 +43,26 @@ function opticalSizeFor(style) {
|
|
|
43
43
|
return typeof size === 'number' && Number.isFinite(size) ? size : undefined;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Cache-key fragment for what shapes a run beyond its face and size: the
|
|
48
|
+
* OpenType features (either form fontkit takes — an array of tags, or an
|
|
49
|
+
* object of tag → on/off), the language and the letter spacing. `''` for a
|
|
50
|
+
* style with none of them, which is nearly every style.
|
|
51
|
+
*/
|
|
52
|
+
function shapingKeyOf(style) {
|
|
53
|
+
const { features, language, letterSpacing } = style;
|
|
54
|
+
if (!features && !language && !letterSpacing) return '';
|
|
55
|
+
let feats = '';
|
|
56
|
+
if (Array.isArray(features)) feats = [...features].sort().join(',');
|
|
57
|
+
else if (features) {
|
|
58
|
+
feats = Object.keys(features)
|
|
59
|
+
.sort()
|
|
60
|
+
.map((tag) => `${tag}=${features[tag] ? 1 : 0}`)
|
|
61
|
+
.join(',');
|
|
62
|
+
}
|
|
63
|
+
return `${feats}|${language ?? ''}|${letterSpacing || 0}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
/** Cache-key fragment for the optical size, `''` when the axis is left alone. */
|
|
47
67
|
function opticalKeyOf(style) {
|
|
48
68
|
return opticalSizeFor(style) ?? '';
|
|
@@ -346,11 +366,16 @@ export default class FontManager {
|
|
|
346
366
|
// variations and optical-size fragments only earn their keep on the
|
|
347
367
|
// family path — where two points of one axis would otherwise share a
|
|
348
368
|
// shaped run, and the second would be drawn with the first's advances.
|
|
369
|
+
// Features, language and spacing change the glyphs a word shapes to, so
|
|
370
|
+
// they are part of the key on both paths: without them a word shaped once
|
|
371
|
+
// plain answered every later request for it with the plain glyphs, and a
|
|
372
|
+
// `tnum` asked for after that point was silently ignored.
|
|
373
|
+
const shaping = shapingKeyOf(style);
|
|
349
374
|
const key = font
|
|
350
|
-
? `${font.key}|${style.size}|${style.weight}|${style.style}|${levelsKey}|${text}`
|
|
375
|
+
? `${font.key}|${style.size}|${style.weight}|${style.style}|${shaping}|${levelsKey}|${text}`
|
|
351
376
|
: `${style.family}|${style.size}|${style.weight}|${style.style}|${variationsKeyOf(
|
|
352
377
|
style.variations
|
|
353
|
-
)}|${opticalKeyOf(style)}|${levelsKey}|${text}`;
|
|
378
|
+
)}|${opticalKeyOf(style)}|${shaping}|${levelsKey}|${text}`;
|
|
354
379
|
let shaped = this._shapeCache.get(key);
|
|
355
380
|
if (shaped) {
|
|
356
381
|
// Map iterates in insertion order: re-inserting a hit moves it to the
|
|
@@ -390,13 +415,10 @@ export default class FontManager {
|
|
|
390
415
|
* levels cannot recover it (RTL text in an RTL paragraph carries level 1,
|
|
391
416
|
* which reads as an even base) and start/end alignment depends on it.
|
|
392
417
|
*
|
|
393
|
-
* `features` and `
|
|
394
|
-
*
|
|
395
|
-
* key either), so a style with either shapes uncached rather than
|
|
396
|
-
* poisoning entries other callers share.
|
|
418
|
+
* `features`, `language` and `letterSpacing` are part of the memo key, so
|
|
419
|
+
* a style with any of them has entries of its own.
|
|
397
420
|
*/
|
|
398
421
|
_shapeCachedWhole(text, style) {
|
|
399
|
-
if (style.features || style.language) return shapeText(this, text, style);
|
|
400
422
|
const emb = embeddingLevels(text, style.direction);
|
|
401
423
|
const baseLevel = emb.paragraphs.length ? emb.paragraphs[0].level : 0;
|
|
402
424
|
const shaped = this._shapeCached(text, style, normalizedLevels(emb.levels, 0, text.length));
|
package/lib/text/layout.js
CHANGED
|
@@ -21,7 +21,8 @@ function isWsGlyph(g) {
|
|
|
21
21
|
* across relayouts), and bidi reordering is applied per line (UAX#9 L2).
|
|
22
22
|
*
|
|
23
23
|
* Content is a plain string or an array of spans
|
|
24
|
-
* `{ text, family?, size?, weight?, style?, features?, language?,
|
|
24
|
+
* `{ text, family?, size?, weight?, style?, features?, language?,
|
|
25
|
+
* letterSpacing?, color? }`;
|
|
25
26
|
* span fields override the base style. Options:
|
|
26
27
|
*
|
|
27
28
|
* - `maxWidth` — target container width (default: unlimited)
|
|
@@ -72,6 +73,7 @@ export class TextLayout {
|
|
|
72
73
|
textRendering: s.textRendering ?? style.textRendering,
|
|
73
74
|
features: s.features ?? style.features,
|
|
74
75
|
language: s.language ?? style.language,
|
|
76
|
+
letterSpacing: s.letterSpacing ?? style.letterSpacing,
|
|
75
77
|
color: s.color ?? style.color ?? null
|
|
76
78
|
};
|
|
77
79
|
merged.font = s.font ?? style.font ?? fonts.match(merged.family, merged);
|
|
@@ -322,7 +324,9 @@ export class TextLayout {
|
|
|
322
324
|
const m = TRAILING_WS.exec(last.text);
|
|
323
325
|
if (m) {
|
|
324
326
|
const spaceGlyph = last.span.font.fk.glyphForCodePoint(0x20);
|
|
325
|
-
|
|
327
|
+
const spacing = Number.isFinite(last.span.letterSpacing) ? last.span.letterSpacing : 0;
|
|
328
|
+
wsWidth =
|
|
329
|
+
m[0].length * (spaceGlyph.advanceWidth * last.span.font.scale(last.span.size) + spacing);
|
|
326
330
|
}
|
|
327
331
|
}
|
|
328
332
|
return { fragments, width, wsWidth, required, start, end };
|
package/lib/text/shape.js
CHANGED
|
@@ -45,6 +45,62 @@ export function normalizedLevels(levels, start, end) {
|
|
|
45
45
|
return Array.prototype.slice.call(levels, start, end).join(',');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The ligatures a font applies by default and a reader can do without. CSS
|
|
50
|
+
* turns them off wherever letters are spaced apart, since an `fi` drawn as
|
|
51
|
+
* one glyph cannot be spaced in its middle: the gap would open on one side of
|
|
52
|
+
* the pair and not the other. Required ones — Arabic joining forms, `rlig` —
|
|
53
|
+
* stay on.
|
|
54
|
+
*/
|
|
55
|
+
const OPTIONAL_LIGATURES = ['liga', 'clig', 'dlig', 'hlig'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The features a letter-spaced run shapes with: the optional ligatures off,
|
|
59
|
+
* underneath whatever the caller asked for — a style that names `liga`
|
|
60
|
+
* itself still gets it. `features` is either form fontkit takes, an array of
|
|
61
|
+
* tags to turn on or an object of tag → on/off.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* A copy of `features` for fontkit, which **adds to the object it is handed**
|
|
65
|
+
* — `rvrn` and the like, as it plans the shaping. Handed a caller's own
|
|
66
|
+
* object that is a style quietly growing keys, which changes the memo key it
|
|
67
|
+
* is filed under next time, and a frozen one throws.
|
|
68
|
+
*/
|
|
69
|
+
function ownFeatures(features) {
|
|
70
|
+
if (!features) return features;
|
|
71
|
+
return Array.isArray(features) ? [...features] : { ...features };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spacedFeatures(features) {
|
|
75
|
+
const off = Object.fromEntries(OPTIONAL_LIGATURES.map((tag) => [tag, false]));
|
|
76
|
+
if (!features) return off;
|
|
77
|
+
if (Array.isArray(features)) {
|
|
78
|
+
for (const tag of features) off[tag] = true;
|
|
79
|
+
return off;
|
|
80
|
+
}
|
|
81
|
+
return { ...off, ...features };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Space a shaped run's glyphs apart by `spacing` px: added to the advance of
|
|
86
|
+
* every glyph that has one, so a combining mark keeps sitting on its base.
|
|
87
|
+
* An rtl run stores its glyphs in visual order, and its spacing belongs on
|
|
88
|
+
* the *reading* side of each glyph — the left — so there the glyph is drawn
|
|
89
|
+
* that much further along its advance as well.
|
|
90
|
+
*/
|
|
91
|
+
function spaceGlyphs(shaped, spacing) {
|
|
92
|
+
const rtl = shaped.direction === 'rtl';
|
|
93
|
+
let width = 0;
|
|
94
|
+
for (const g of shaped.glyphs) {
|
|
95
|
+
if (g.ax > 0) {
|
|
96
|
+
g.ax += spacing;
|
|
97
|
+
if (rtl) g.dx += spacing;
|
|
98
|
+
}
|
|
99
|
+
width += g.ax;
|
|
100
|
+
}
|
|
101
|
+
shaped.width = width;
|
|
102
|
+
}
|
|
103
|
+
|
|
48
104
|
/**
|
|
49
105
|
* Shape a string through the full pipeline:
|
|
50
106
|
*
|
|
@@ -61,7 +117,7 @@ export function normalizedLevels(levels, start, end) {
|
|
|
61
117
|
* @param {FontManager} fonts
|
|
62
118
|
* @param {string} text
|
|
63
119
|
* @param {object} style { font | family/weight/style, size, features,
|
|
64
|
-
* language, direction: 'ltr'|'rtl'|'auto' }
|
|
120
|
+
* language, letterSpacing, direction: 'ltr'|'rtl'|'auto' }
|
|
65
121
|
* @param {ArrayLike<number>} [levels] precomputed embedding levels for `text`
|
|
66
122
|
* @returns {{text, width, baseLevel, runs: Array<{font, size, direction,
|
|
67
123
|
* level, width, glyphs, text, start, end}>}}
|
|
@@ -108,6 +164,11 @@ export function shapeText(fonts, text, style, levels) {
|
|
|
108
164
|
i += ch.length;
|
|
109
165
|
}
|
|
110
166
|
|
|
167
|
+
// px added after every glyph, CSS's `letter-spacing`; 0 leaves the run
|
|
168
|
+
// exactly as the font shapes it, ligatures and all
|
|
169
|
+
const spacing = Number.isFinite(style.letterSpacing) ? style.letterSpacing : 0;
|
|
170
|
+
const features = spacing ? spacedFeatures(style.features) : ownFeatures(style.features);
|
|
171
|
+
|
|
111
172
|
const runs = [];
|
|
112
173
|
let width = 0;
|
|
113
174
|
for (const item of items) {
|
|
@@ -115,9 +176,10 @@ export function shapeText(fonts, text, style, levels) {
|
|
|
115
176
|
const sub = text.slice(item.start, item.end);
|
|
116
177
|
const shaped = item.font.shape(sub, size, {
|
|
117
178
|
direction,
|
|
118
|
-
features
|
|
179
|
+
features,
|
|
119
180
|
language: style.language
|
|
120
181
|
});
|
|
182
|
+
if (spacing) spaceGlyphs(shaped, spacing);
|
|
121
183
|
shaped.level = item.level;
|
|
122
184
|
shaped.text = sub;
|
|
123
185
|
shaped.start = item.start;
|