ntk 3.5.0 → 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 +100 -3
- package/lib/text/layout.js +19 -9
- package/package.json +1 -1
|
@@ -736,6 +736,47 @@ class RenderingContext2d {
|
|
|
736
736
|
this._markDirty();
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Composite shaped glyph runs onto this context, honouring the clip.
|
|
741
|
+
*
|
|
742
|
+
* CompositeGlyphs writes straight to the destination picture, so it has
|
|
743
|
+
* no way to consult our clip mask — text drawn through it used to spill
|
|
744
|
+
* out of clipped boxes while every fill and stroke stayed inside. With a
|
|
745
|
+
* clip active, render the glyph coverage into the scratch a8 mask
|
|
746
|
+
* instead, intersect that with the clip, and paint the real source
|
|
747
|
+
* through the result — the same shape as _fillPolys.
|
|
748
|
+
*/
|
|
749
|
+
drawGlyphs(op, src, positioned) {
|
|
750
|
+
const app = this.window.app;
|
|
751
|
+
const R = this.Render;
|
|
752
|
+
if (!this.clipMask) {
|
|
753
|
+
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
754
|
+
this._markDirty();
|
|
755
|
+
return;
|
|
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
|
+
}
|
|
770
|
+
this._ensureFillMask();
|
|
771
|
+
this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
|
|
772
|
+
R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
773
|
+
// solid white through the glyphs leaves their coverage in the a8 mask
|
|
774
|
+
drawGlyphRuns(app, R.PictOp.Over, this._glyphSource.id, this.fillMask.id, positioned);
|
|
775
|
+
R.Composite(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
776
|
+
R.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
777
|
+
this._markDirty();
|
|
778
|
+
}
|
|
779
|
+
|
|
739
780
|
// combined clip ∩ globalAlpha mask for direct composites (rect/image
|
|
740
781
|
// fast paths); returns a picture id or 0. Reuses the fill scratch mask.
|
|
741
782
|
_compositeMask() {
|
|
@@ -813,9 +854,66 @@ class RenderingContext2d {
|
|
|
813
854
|
* Cleared by restore() to the state at the matching save() — like the
|
|
814
855
|
* browser canvas, there is no other way to widen the clip again.
|
|
815
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
|
+
|
|
816
914
|
clip(...args) {
|
|
817
915
|
const { polys, rule } = this._polysFor(args);
|
|
818
|
-
const entry = { polys, rule };
|
|
916
|
+
const entry = { polys, rule, rect: RenderingContext2d._rectOfPolys(polys) };
|
|
819
917
|
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
820
918
|
this._clips = this._clips.concat([entry]);
|
|
821
919
|
|
|
@@ -928,8 +1026,7 @@ class RenderingContext2d {
|
|
|
928
1026
|
positioned.push({ run, x: cursor, y: oy });
|
|
929
1027
|
cursor += run.width;
|
|
930
1028
|
}
|
|
931
|
-
|
|
932
|
-
this._markDirty();
|
|
1029
|
+
this.drawGlyphs(this.Render.PictOp.Over, this._backgroundPicture, positioned);
|
|
933
1030
|
}
|
|
934
1031
|
|
|
935
1032
|
/**
|
package/lib/text/layout.js
CHANGED
|
@@ -328,23 +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
|
-
for
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 {
|
|
337
345
|
drawGlyphRuns(app, Render.PictOp.Over, src.id, ctx.picture.id, batch);
|
|
338
|
-
|
|
339
|
-
|
|
346
|
+
}
|
|
347
|
+
batch = [];
|
|
348
|
+
};
|
|
349
|
+
for (const line of this.lines) {
|
|
340
350
|
for (const r of line.runs) {
|
|
341
351
|
const color = r.span.color;
|
|
342
352
|
if (batch.length && color !== batchColor) flush();
|
|
343
353
|
batchColor = color;
|
|
344
354
|
batch.push({ run: r.run, x: x + line.x + r.x, y: y + line.baseline });
|
|
345
355
|
}
|
|
346
|
-
flush();
|
|
347
356
|
}
|
|
357
|
+
flush();
|
|
348
358
|
ctx._markDirty();
|
|
349
359
|
return this;
|
|
350
360
|
}
|