ntk 3.5.1 → 3.5.2
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 +71 -1
- package/lib/text/layout.js +19 -15
- package/package.json +1 -1
|
@@ -754,6 +754,19 @@ class RenderingContext2d {
|
|
|
754
754
|
this._markDirty();
|
|
755
755
|
return;
|
|
756
756
|
}
|
|
757
|
+
// Fast path: a rectangular clip is something the server can do itself.
|
|
758
|
+
// Two small requests around the ordinary glyph composite, instead of
|
|
759
|
+
// clearing and compositing a full-surface a8 mask three times — which
|
|
760
|
+
// costs the same on the wire but many times the pixel work.
|
|
761
|
+
const rect = this._clipRect();
|
|
762
|
+
if (rect) {
|
|
763
|
+
if (rect.w === 0 || rect.h === 0) return;
|
|
764
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [rect.x, rect.y, rect.w, rect.h]);
|
|
765
|
+
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
766
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, this.width, this.height]);
|
|
767
|
+
this._markDirty();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
757
770
|
this._ensureFillMask();
|
|
758
771
|
this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
|
|
759
772
|
R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
@@ -841,9 +854,66 @@ class RenderingContext2d {
|
|
|
841
854
|
* Cleared by restore() to the state at the matching save() — like the
|
|
842
855
|
* browser canvas, there is no other way to widen the clip again.
|
|
843
856
|
*/
|
|
857
|
+
/**
|
|
858
|
+
* The axis-aligned rectangle a flattened path describes, or null. Clips
|
|
859
|
+
* in practice are almost always rectangles — a content box, a scroll
|
|
860
|
+
* viewport, `overflow: hidden` — and a rectangular clip can be pushed
|
|
861
|
+
* to the server instead of rasterized into a mask.
|
|
862
|
+
*/
|
|
863
|
+
static _rectOfPolys(polys) {
|
|
864
|
+
if (polys.length !== 1) return null;
|
|
865
|
+
const { pts } = polys[0];
|
|
866
|
+
if (pts.length !== 8) return null;
|
|
867
|
+
const [x0, y0, x1, y1, x2, y2, x3, y3] = pts;
|
|
868
|
+
const axis =
|
|
869
|
+
(y0 === y1 && x1 === x2 && y2 === y3 && x3 === x0) ||
|
|
870
|
+
(x0 === x1 && y1 === y2 && x2 === x3 && y3 === y0);
|
|
871
|
+
if (!axis) return null;
|
|
872
|
+
const xs = [x0, x1, x2, x3];
|
|
873
|
+
const ys = [y0, y1, y2, y3];
|
|
874
|
+
const x = Math.min(...xs);
|
|
875
|
+
const y = Math.min(...ys);
|
|
876
|
+
const w = Math.max(...xs) - x;
|
|
877
|
+
const h = Math.max(...ys) - y;
|
|
878
|
+
if (!(w > 0 && h > 0)) return null;
|
|
879
|
+
// Server clip rectangles are integers, so a fractional edge could only
|
|
880
|
+
// be honoured by rounding — which would differ from the mask path's
|
|
881
|
+
// antialiased edge by up to a pixel. Rare, so leave those to the mask
|
|
882
|
+
// rather than quietly changing what they look like.
|
|
883
|
+
const integral = (v) => Math.abs(v - Math.round(v)) < 1e-6;
|
|
884
|
+
if (![x, y, w, h].every(integral)) return null;
|
|
885
|
+
return { x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h) };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** Intersection of the whole clip stack, or null if any part is not a
|
|
889
|
+
* rectangle (or the stack is empty). */
|
|
890
|
+
_clipRect() {
|
|
891
|
+
if (!this._clips.length) return null;
|
|
892
|
+
let out = null;
|
|
893
|
+
for (const entry of this._clips) {
|
|
894
|
+
const r = entry.rect;
|
|
895
|
+
if (!r) return null;
|
|
896
|
+
if (!out) {
|
|
897
|
+
out = { ...r };
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const x = Math.max(out.x, r.x);
|
|
901
|
+
const y = Math.max(out.y, r.y);
|
|
902
|
+
const right = Math.min(out.x + out.w, r.x + r.w);
|
|
903
|
+
const bottom = Math.min(out.y + out.h, r.y + r.h);
|
|
904
|
+
out = { x, y, w: Math.max(0, right - x), h: Math.max(0, bottom - y) };
|
|
905
|
+
}
|
|
906
|
+
// clamp into the surface: the server rejects out-of-range rectangles
|
|
907
|
+
const x = Math.max(0, Math.min(out.x, this.width));
|
|
908
|
+
const y = Math.max(0, Math.min(out.y, this.height));
|
|
909
|
+
const w = Math.max(0, Math.min(out.x + out.w, this.width) - x);
|
|
910
|
+
const h = Math.max(0, Math.min(out.y + out.h, this.height) - y);
|
|
911
|
+
return { x, y, w, h };
|
|
912
|
+
}
|
|
913
|
+
|
|
844
914
|
clip(...args) {
|
|
845
915
|
const { polys, rule } = this._polysFor(args);
|
|
846
|
-
const entry = { polys, rule };
|
|
916
|
+
const entry = { polys, rule, rect: RenderingContext2d._rectOfPolys(polys) };
|
|
847
917
|
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
848
918
|
this._clips = this._clips.concat([entry]);
|
|
849
919
|
|
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
|
}
|