ntk 3.5.1 → 3.5.3
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/renderingcontext_2d.js +116 -13
- package/lib/text/layout.js +19 -15
- package/package.json +1 -1
|
@@ -552,6 +552,34 @@ class RenderingContext2d {
|
|
|
552
552
|
* Core fill: rasterize device-space polys into the scratch a8 mask,
|
|
553
553
|
* scale by globalAlpha, intersect with the clip, composite the source.
|
|
554
554
|
*/
|
|
555
|
+
/**
|
|
556
|
+
* Device-space bounding box of flattened polys, with a pixel of slack for
|
|
557
|
+
* the antialiased edge, clamped to the surface. Null when nothing lands
|
|
558
|
+
* on it.
|
|
559
|
+
*/
|
|
560
|
+
_polysBBox(polys) {
|
|
561
|
+
let minX = Infinity;
|
|
562
|
+
let minY = Infinity;
|
|
563
|
+
let maxX = -Infinity;
|
|
564
|
+
let maxY = -Infinity;
|
|
565
|
+
for (const poly of polys) {
|
|
566
|
+
const pts = poly.pts;
|
|
567
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
568
|
+
if (pts[i] < minX) minX = pts[i];
|
|
569
|
+
if (pts[i] > maxX) maxX = pts[i];
|
|
570
|
+
if (pts[i + 1] < minY) minY = pts[i + 1];
|
|
571
|
+
if (pts[i + 1] > maxY) maxY = pts[i + 1];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (maxX === -Infinity) return null;
|
|
575
|
+
const x = Math.max(0, Math.floor(minX) - 1);
|
|
576
|
+
const y = Math.max(0, Math.floor(minY) - 1);
|
|
577
|
+
const w = Math.min(this.width, Math.ceil(maxX) + 1) - x;
|
|
578
|
+
const h = Math.min(this.height, Math.ceil(maxY) + 1) - y;
|
|
579
|
+
if (w <= 0 || h <= 0) return null;
|
|
580
|
+
return { x, y, w, h };
|
|
581
|
+
}
|
|
582
|
+
|
|
555
583
|
_fillPolys(polys, rule, { src = null, op = null, alpha = null } = {}) {
|
|
556
584
|
if (!polys.length) return;
|
|
557
585
|
src = src ?? this._backgroundPicture;
|
|
@@ -559,26 +587,31 @@ class RenderingContext2d {
|
|
|
559
587
|
alpha = alpha ?? this.globalAlpha;
|
|
560
588
|
if (alpha <= 0) return;
|
|
561
589
|
|
|
590
|
+
// Everything below is bounded to the shape's bounding box rather than
|
|
591
|
+
// the whole surface. On the wire it makes no difference — a Composite
|
|
592
|
+
// request is the same size either way — but it is the difference
|
|
593
|
+
// between the server touching a 34x34 box and a 400x400 one per fill.
|
|
594
|
+
// Stale mask content outside the box is never composited, so clearing
|
|
595
|
+
// only the box is safe.
|
|
596
|
+
const b = this._polysBBox(polys);
|
|
597
|
+
if (!b) return;
|
|
598
|
+
const R = this.Render;
|
|
599
|
+
|
|
562
600
|
this._ensureFillMask();
|
|
563
|
-
|
|
601
|
+
R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [b.x, b.y, b.w, b.h]);
|
|
564
602
|
this._rasterizePolys(this.fillMask, polys, rule);
|
|
565
603
|
|
|
566
604
|
if (alpha < 1) {
|
|
567
605
|
// In with a constant color scales the a8 coverage by that alpha
|
|
568
|
-
|
|
606
|
+
R.FillRectangles(R.PictOp.In, this.fillMask.id, [0, 0, 0, alpha], [b.x, b.y, b.w, b.h]);
|
|
569
607
|
}
|
|
570
608
|
if (this.clipMask) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.clipMask.id,
|
|
574
|
-
0,
|
|
575
|
-
this.fillMask.id,
|
|
576
|
-
0, 0, 0, 0, 0, 0,
|
|
577
|
-
this.width,
|
|
578
|
-
this.height
|
|
579
|
-
);
|
|
609
|
+
// clipMask is surface-aligned, so it is sampled at the same offset
|
|
610
|
+
R.Composite(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, b.x, b.y, 0, 0, b.x, b.y, b.w, b.h);
|
|
580
611
|
}
|
|
581
|
-
|
|
612
|
+
// src is either a 1x1 repeating solid (offset irrelevant) or a
|
|
613
|
+
// surface-aligned gradient, so it is sampled at the same offset too
|
|
614
|
+
R.Composite(op, src.id, this.fillMask.id, this.picture.id, b.x, b.y, b.x, b.y, b.x, b.y, b.w, b.h);
|
|
582
615
|
this._markDirty();
|
|
583
616
|
}
|
|
584
617
|
|
|
@@ -754,6 +787,19 @@ class RenderingContext2d {
|
|
|
754
787
|
this._markDirty();
|
|
755
788
|
return;
|
|
756
789
|
}
|
|
790
|
+
// Fast path: a rectangular clip is something the server can do itself.
|
|
791
|
+
// Two small requests around the ordinary glyph composite, instead of
|
|
792
|
+
// clearing and compositing a full-surface a8 mask three times — which
|
|
793
|
+
// costs the same on the wire but many times the pixel work.
|
|
794
|
+
const rect = this._clipRect();
|
|
795
|
+
if (rect) {
|
|
796
|
+
if (rect.w === 0 || rect.h === 0) return;
|
|
797
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [rect.x, rect.y, rect.w, rect.h]);
|
|
798
|
+
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
799
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, this.width, this.height]);
|
|
800
|
+
this._markDirty();
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
757
803
|
this._ensureFillMask();
|
|
758
804
|
this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
|
|
759
805
|
R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
@@ -841,9 +887,66 @@ class RenderingContext2d {
|
|
|
841
887
|
* Cleared by restore() to the state at the matching save() — like the
|
|
842
888
|
* browser canvas, there is no other way to widen the clip again.
|
|
843
889
|
*/
|
|
890
|
+
/**
|
|
891
|
+
* The axis-aligned rectangle a flattened path describes, or null. Clips
|
|
892
|
+
* in practice are almost always rectangles — a content box, a scroll
|
|
893
|
+
* viewport, `overflow: hidden` — and a rectangular clip can be pushed
|
|
894
|
+
* to the server instead of rasterized into a mask.
|
|
895
|
+
*/
|
|
896
|
+
static _rectOfPolys(polys) {
|
|
897
|
+
if (polys.length !== 1) return null;
|
|
898
|
+
const { pts } = polys[0];
|
|
899
|
+
if (pts.length !== 8) return null;
|
|
900
|
+
const [x0, y0, x1, y1, x2, y2, x3, y3] = pts;
|
|
901
|
+
const axis =
|
|
902
|
+
(y0 === y1 && x1 === x2 && y2 === y3 && x3 === x0) ||
|
|
903
|
+
(x0 === x1 && y1 === y2 && x2 === x3 && y3 === y0);
|
|
904
|
+
if (!axis) return null;
|
|
905
|
+
const xs = [x0, x1, x2, x3];
|
|
906
|
+
const ys = [y0, y1, y2, y3];
|
|
907
|
+
const x = Math.min(...xs);
|
|
908
|
+
const y = Math.min(...ys);
|
|
909
|
+
const w = Math.max(...xs) - x;
|
|
910
|
+
const h = Math.max(...ys) - y;
|
|
911
|
+
if (!(w > 0 && h > 0)) return null;
|
|
912
|
+
// Server clip rectangles are integers, so a fractional edge could only
|
|
913
|
+
// be honoured by rounding — which would differ from the mask path's
|
|
914
|
+
// antialiased edge by up to a pixel. Rare, so leave those to the mask
|
|
915
|
+
// rather than quietly changing what they look like.
|
|
916
|
+
const integral = (v) => Math.abs(v - Math.round(v)) < 1e-6;
|
|
917
|
+
if (![x, y, w, h].every(integral)) return null;
|
|
918
|
+
return { x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h) };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** Intersection of the whole clip stack, or null if any part is not a
|
|
922
|
+
* rectangle (or the stack is empty). */
|
|
923
|
+
_clipRect() {
|
|
924
|
+
if (!this._clips.length) return null;
|
|
925
|
+
let out = null;
|
|
926
|
+
for (const entry of this._clips) {
|
|
927
|
+
const r = entry.rect;
|
|
928
|
+
if (!r) return null;
|
|
929
|
+
if (!out) {
|
|
930
|
+
out = { ...r };
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
const x = Math.max(out.x, r.x);
|
|
934
|
+
const y = Math.max(out.y, r.y);
|
|
935
|
+
const right = Math.min(out.x + out.w, r.x + r.w);
|
|
936
|
+
const bottom = Math.min(out.y + out.h, r.y + r.h);
|
|
937
|
+
out = { x, y, w: Math.max(0, right - x), h: Math.max(0, bottom - y) };
|
|
938
|
+
}
|
|
939
|
+
// clamp into the surface: the server rejects out-of-range rectangles
|
|
940
|
+
const x = Math.max(0, Math.min(out.x, this.width));
|
|
941
|
+
const y = Math.max(0, Math.min(out.y, this.height));
|
|
942
|
+
const w = Math.max(0, Math.min(out.x + out.w, this.width) - x);
|
|
943
|
+
const h = Math.max(0, Math.min(out.y + out.h, this.height) - y);
|
|
944
|
+
return { x, y, w, h };
|
|
945
|
+
}
|
|
946
|
+
|
|
844
947
|
clip(...args) {
|
|
845
948
|
const { polys, rule } = this._polysFor(args);
|
|
846
|
-
const entry = { polys, rule };
|
|
949
|
+
const entry = { polys, rule, rect: RenderingContext2d._rectOfPolys(polys) };
|
|
847
950
|
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
848
951
|
this._clips = this._clips.concat([entry]);
|
|
849
952
|
|
package/lib/text/layout.js
CHANGED
|
@@ -328,29 +328,33 @@ export class TextLayout {
|
|
|
328
328
|
draw(ctx, x = 0, y = 0) {
|
|
329
329
|
const app = ctx.window.app;
|
|
330
330
|
const Render = app.display.Render;
|
|
331
|
+
// One batch for the whole layout, not one per line: every positioned
|
|
332
|
+
// run carries its own absolute baseline, so a uniformly coloured
|
|
333
|
+
// paragraph becomes a single glyph composite instead of one per line.
|
|
334
|
+
// Only a colour change forces a flush.
|
|
335
|
+
let batch = [];
|
|
336
|
+
let batchColor;
|
|
337
|
+
const flush = () => {
|
|
338
|
+
if (batch.length === 0) return;
|
|
339
|
+
const src = batchColor ? ctx._stylePicture(batchColor) : ctx._backgroundPicture;
|
|
340
|
+
// via the context so the clip is applied (drawGlyphRuns composites
|
|
341
|
+
// straight onto the picture and cannot see it)
|
|
342
|
+
if (typeof ctx.drawGlyphs === 'function') {
|
|
343
|
+
ctx.drawGlyphs(Render.PictOp.Over, src, batch);
|
|
344
|
+
} else {
|
|
345
|
+
drawGlyphRuns(app, Render.PictOp.Over, src.id, ctx.picture.id, batch);
|
|
346
|
+
}
|
|
347
|
+
batch = [];
|
|
348
|
+
};
|
|
331
349
|
for (const line of this.lines) {
|
|
332
|
-
let batch = [];
|
|
333
|
-
let batchColor;
|
|
334
|
-
const flush = () => {
|
|
335
|
-
if (batch.length === 0) return;
|
|
336
|
-
const src = batchColor ? ctx._stylePicture(batchColor) : ctx._backgroundPicture;
|
|
337
|
-
// via the context so the clip mask is applied (drawGlyphRuns
|
|
338
|
-
// composites straight onto the picture and cannot see it)
|
|
339
|
-
if (typeof ctx.drawGlyphs === 'function') {
|
|
340
|
-
ctx.drawGlyphs(Render.PictOp.Over, src, batch);
|
|
341
|
-
} else {
|
|
342
|
-
drawGlyphRuns(app, Render.PictOp.Over, src.id, ctx.picture.id, batch);
|
|
343
|
-
}
|
|
344
|
-
batch = [];
|
|
345
|
-
};
|
|
346
350
|
for (const r of line.runs) {
|
|
347
351
|
const color = r.span.color;
|
|
348
352
|
if (batch.length && color !== batchColor) flush();
|
|
349
353
|
batchColor = color;
|
|
350
354
|
batch.push({ run: r.run, x: x + line.x + r.x, y: y + line.baseline });
|
|
351
355
|
}
|
|
352
|
-
flush();
|
|
353
356
|
}
|
|
357
|
+
flush();
|
|
354
358
|
ctx._markDirty();
|
|
355
359
|
return this;
|
|
356
360
|
}
|