ntk 8.8.2 → 8.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.
- 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/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;
|