ntk 7.1.0 → 7.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.
@@ -525,6 +525,7 @@ class RenderingContext2d {
525
525
  textStyle: this._textStyle,
526
526
  fontString: this._lastFontString,
527
527
  fontVariations: this._fontVariations,
528
+ textRendering: this._textRendering,
528
529
  textAlign: this.textAlign,
529
530
  textBaseline: this.textBaseline,
530
531
  m: this._m.slice(),
@@ -548,6 +549,7 @@ class RenderingContext2d {
548
549
  this._textStyle = s.textStyle;
549
550
  this._lastFontString = s.fontString;
550
551
  this._fontVariations = s.fontVariations;
552
+ this._textRendering = s.textRendering;
551
553
  this.textAlign = s.textAlign;
552
554
  this.textBaseline = s.textBaseline;
553
555
  this._m = s.m;
@@ -2339,7 +2341,12 @@ class RenderingContext2d {
2339
2341
  const positioned = [];
2340
2342
  let cursor = ox;
2341
2343
  for (const run of reorderRuns(shaped.runs)) {
2342
- positioned.push({ run, x: cursor, y: oy });
2344
+ positioned.push({
2345
+ run,
2346
+ x: cursor,
2347
+ y: oy,
2348
+ textRendering: this._textRendering,
2349
+ });
2343
2350
  cursor += run.width;
2344
2351
  }
2345
2352
  this.drawGlyphs(
@@ -2451,6 +2458,30 @@ class RenderingContext2d {
2451
2458
  return this._fontVariations ?? null;
2452
2459
  }
2453
2460
 
2461
+ /**
2462
+ * CSS's `text-rendering`: which glyph path this text takes, overriding the
2463
+ * size thresholds in `app.textPolicy`.
2464
+ *
2465
+ * - `'geometricPrecision'` — outlines every draw, glyph origins **not**
2466
+ * rounded to whole pixels. What display text wants, and what any text
2467
+ * whose shape is being animated wants: a variable font's axis moves
2468
+ * advances by fractions of a pixel, and cached glyphs can only land on
2469
+ * whole ones, so those fractions accumulate until a glyph crosses a
2470
+ * rounding boundary and jumps a pixel on its own.
2471
+ * - `'optimizeSpeed'` — cached server-side glyphs at any size.
2472
+ * - `'auto'` (default) — the thresholds decide.
2473
+ *
2474
+ * `'optimizeLegibility'` is accepted and means `'auto'`; ntk has no
2475
+ * hinting to turn on.
2476
+ */
2477
+ set textRendering(val) {
2478
+ this._textRendering = val || undefined;
2479
+ }
2480
+
2481
+ get textRendering() {
2482
+ return this._textRendering ?? "auto";
2483
+ }
2484
+
2454
2485
  // ------------------------------------------------------------------
2455
2486
  // gradients / images
2456
2487
 
@@ -20,11 +20,17 @@ import { trapezoidize } from '../trapezoid.js';
20
20
  * - `cacheBytes` — LRU budget for uploaded glyph bitmaps per connection;
21
21
  * least-recently-drawn (face, size) pages are freed server-side
22
22
  * (FreeGlyphSet) so transient sizes don't accumulate.
23
+ * - `textRendering` — an app-wide default for the per-run property of the
24
+ * same name (see `routeForRendering`). Left undefined the thresholds
25
+ * decide, which is what almost every app wants; `'geometricPrecision'`
26
+ * here is the blunt instrument for a window that is all display text.
27
+ * A run that names its own always wins.
23
28
  */
24
29
  export const DEFAULT_TEXT_POLICY = {
25
30
  bitmapMax: 128,
26
31
  vectorFrom: 256,
27
- cacheBytes: 8 << 20
32
+ cacheBytes: 8 << 20,
33
+ textRendering: undefined
28
34
  };
29
35
 
30
36
  function policyOf(app) {
@@ -228,21 +234,75 @@ export function positionGlyphs(positioned) {
228
234
  *
229
235
  * @returns {'bitmap'|'vector'}
230
236
  */
231
- export function routeGlyphSize(app, font, size, policy = policyOf(app)) {
237
+ /**
238
+ * CSS's `text-rendering`, as far as it means anything here: a run's own
239
+ * answer to which glyph path it takes, overriding the size thresholds.
240
+ *
241
+ * - `geometricPrecision` — the vector path, always. Outlines are flattened
242
+ * at the exact size and glyph origins are not rounded, so advances land
243
+ * where shaping put them. This is what display text wants, and what any
244
+ * text whose shape is being animated wants: a variable font's axis moves
245
+ * advances by fractions of a pixel, and on the bitmap path those fractions
246
+ * accumulate silently until a glyph crosses a rounding boundary and jumps
247
+ * a whole pixel on its own. It is also the path that caches nothing, so
248
+ * an axis under a slider stops minting a glyph page per step.
249
+ * - `optimizeSpeed` — the bitmap path, always. Cached server-side glyphs at
250
+ * any size, for text that is large but static.
251
+ * - `auto` (the default, and anything unrecognized) — the size thresholds
252
+ * and the churn ring decide, as before.
253
+ *
254
+ * `optimizeLegibility` is accepted and means `auto`: ntk has no hinting to
255
+ * turn on, so promising anything by it would be a lie.
256
+ */
257
+ export function routeForRendering(textRendering) {
258
+ if (textRendering === 'geometricPrecision') return 'vector';
259
+ if (textRendering === 'optimizeSpeed') return 'bitmap';
260
+ return null; // auto: ask the thresholds
261
+ }
262
+
263
+ export function routeGlyphSize(app, font, size, policy = policyOf(app), textRendering) {
264
+ // Inlined rather than a call to `routeForRendering`: this is the first
265
+ // thing every run of every draw does, and the overwhelmingly common answer
266
+ // is "nobody asked". One `??` and one comparison get us past it.
267
+ const asked = textRendering ?? policy.textRendering;
268
+ if (asked !== undefined && asked !== 'auto') {
269
+ const route = routeForRendering(asked);
270
+ if (route) return route;
271
+ }
232
272
  if (size <= policy.bitmapMax) return 'bitmap';
233
273
  if (size > policy.vectorFrom) return 'vector';
234
274
 
275
+ // The ring answers "is this face being drawn at something it has drawn
276
+ // recently, or is it churning?" — so it has to be keyed by the thing that
277
+ // stays put while the churn happens.
278
+ //
279
+ // For a variable font that is the *base* face, not the instance: every
280
+ // point on an axis is a Font of its own, with its own key, so an animated
281
+ // axis handed each step a fresh empty ring and the churn was invisible —
282
+ // it read as eight unrelated faces each drawn once. Keyed by the base and
283
+ // recording the instance alongside the size, an axis sweep and a size
284
+ // sweep look like what they both are, and a page of static text at one
285
+ // weight still reuses its entry on every frame.
235
286
  if (!app._sizeRings) app._sizeRings = new Map();
236
- let ring = app._sizeRings.get(font.key);
287
+ // A face with no instances behind it keeps the old ring exactly: its key
288
+ // is already constant across the ring, so the size alone identifies a
289
+ // glyph page and the entries stay numbers. Only a variable instance pays
290
+ // for the composite key, and only in this band — text at or below
291
+ // `bitmapMax` returned above without touching any of it.
292
+ const base = font.variationOf;
293
+ let ring = app._sizeRings.get(base ? base.key : font.key);
237
294
  if (!ring) {
238
295
  ring = [];
239
- app._sizeRings.set(font.key, ring);
296
+ app._sizeRings.set(base ? base.key : font.key, ring);
240
297
  }
241
- const reused = ring.includes(size);
298
+ // what a glyph page is keyed by, which is exactly what has to repeat for
299
+ // caching to pay for itself
300
+ const entry = base ? `${font.key}@${size}` : size;
301
+ const reused = ring.includes(entry);
242
302
  // dedupe consecutive entries so one frame drawing many runs at one size
243
303
  // occupies a single slot — the ring then spans ~8 distinct frames
244
- if (ring[ring.length - 1] !== size) {
245
- ring.push(size);
304
+ if (ring[ring.length - 1] !== entry) {
305
+ ring.push(entry);
246
306
  if (ring.length > 8) ring.shift();
247
307
  }
248
308
 
@@ -269,7 +329,9 @@ export function drawGlyphRuns(app, op, srcId, dstId, positioned) {
269
329
  let vector = null;
270
330
  for (let i = 0; i < positioned.length; i++) {
271
331
  const { run } = positioned[i];
272
- if (routeGlyphSize(app, run.font, run.size, policy) === 'vector') {
332
+ if (
333
+ routeGlyphSize(app, run.font, run.size, policy, positioned[i].textRendering) === 'vector'
334
+ ) {
273
335
  if (!vector) {
274
336
  vector = [];
275
337
  bitmap = positioned.slice(0, i);
@@ -67,6 +67,7 @@ export class TextLayout {
67
67
  weight: s.weight ?? style.weight,
68
68
  style: s.style ?? style.style,
69
69
  variations: s.variations ?? style.variations,
70
+ textRendering: s.textRendering ?? style.textRendering,
70
71
  features: s.features ?? style.features,
71
72
  language: s.language ?? style.language,
72
73
  color: s.color ?? style.color ?? null
@@ -497,7 +498,15 @@ export class TextLayout {
497
498
  const color = r.span.color;
498
499
  if (batch.length && color !== batchColor) flush();
499
500
  batchColor = color;
500
- batch.push({ run: r.run, x: x + line.x + r.x, y: y + line.baseline });
501
+ batch.push({
502
+ run: r.run,
503
+ x: x + line.x + r.x,
504
+ y: y + line.baseline,
505
+ // per run, because it is a span property: one paragraph may hold
506
+ // a display word that wants exact positions and body text that
507
+ // wants its glyph cache. `drawGlyphRuns` already partitions.
508
+ textRendering: r.span.textRendering
509
+ });
501
510
  }
502
511
  }
503
512
  flush();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",