ntk 8.0.0 → 8.1.1
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/app.js +4 -0
- package/lib/fontconfig.js +185 -61
- package/lib/renderingcontext_2d.js +616 -22
- package/lib/shadow.js +199 -0
- package/lib/text/fontsource.js +47 -3
- package/lib/text/layout.js +16 -3
- package/lib/widgets/svgview.js +9 -12
- package/package.json +1 -1
|
@@ -27,6 +27,13 @@ import {
|
|
|
27
27
|
import Picture from "./picture.js";
|
|
28
28
|
import Pixmap from "./pixmap.js";
|
|
29
29
|
import { routeRaster } from "./rasterize.js";
|
|
30
|
+
import {
|
|
31
|
+
blurCoverage,
|
|
32
|
+
cachedShadow,
|
|
33
|
+
shadowPolicyOf,
|
|
34
|
+
shadowReach,
|
|
35
|
+
shadowSigma,
|
|
36
|
+
} from "./shadow.js";
|
|
30
37
|
import { Surface } from "./surface.js";
|
|
31
38
|
import {
|
|
32
39
|
BL,
|
|
@@ -76,6 +83,37 @@ function intersectBox(a, b) {
|
|
|
76
83
|
return { x, y, w: right - x, h: bottom - y };
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
/** The device-space ink bounds of a flattened path, unclamped. */
|
|
87
|
+
function polysInk(polys) {
|
|
88
|
+
let minX = Infinity;
|
|
89
|
+
let minY = Infinity;
|
|
90
|
+
let maxX = -Infinity;
|
|
91
|
+
let maxY = -Infinity;
|
|
92
|
+
for (const poly of polys) {
|
|
93
|
+
const pts = poly.pts;
|
|
94
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
95
|
+
if (pts[i] < minX) minX = pts[i];
|
|
96
|
+
if (pts[i] > maxX) maxX = pts[i];
|
|
97
|
+
if (pts[i + 1] < minY) minY = pts[i + 1];
|
|
98
|
+
if (pts[i + 1] > maxY) maxY = pts[i + 1];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return maxX === -Infinity ? null : { minX, minY, maxX, maxY };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** the same list, moved — the shadow's copy of a drawing is the same
|
|
105
|
+
* geometry in the coverage surface's coordinates */
|
|
106
|
+
function shiftPolys(polys, dx, dy) {
|
|
107
|
+
return polys.map((poly) => {
|
|
108
|
+
const pts = new Array(poly.pts.length);
|
|
109
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
110
|
+
pts[i] = poly.pts[i] + dx;
|
|
111
|
+
pts[i + 1] = poly.pts[i + 1] + dy;
|
|
112
|
+
}
|
|
113
|
+
return { ...poly, pts };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
79
117
|
const DEFAULT_FONT = "20px sans-serif";
|
|
80
118
|
|
|
81
119
|
// `"wght" 460, "wdth" 87.5` — the CSS grammar, quotes optional. An object is
|
|
@@ -95,6 +133,17 @@ function parseVariationSettings(val) {
|
|
|
95
133
|
return any ? out : null;
|
|
96
134
|
}
|
|
97
135
|
|
|
136
|
+
/** a stable string for a variation setting, for cache keys — two Fonts of
|
|
137
|
+
* one file at different axis positions share a `key`, so the coordinates
|
|
138
|
+
* have to be part of anything keyed on the rendered result */
|
|
139
|
+
function variationsKey(variations) {
|
|
140
|
+
if (!variations) return "";
|
|
141
|
+
return Object.keys(variations)
|
|
142
|
+
.sort()
|
|
143
|
+
.map((tag) => `${tag}=${variations[tag]}`)
|
|
144
|
+
.join(",");
|
|
145
|
+
}
|
|
146
|
+
|
|
98
147
|
/**
|
|
99
148
|
* Fallback for a context dropped without `destroy()`, matching Pixmap,
|
|
100
149
|
* Picture and GlyphSet. Only the GCs are freed here: everything else a
|
|
@@ -198,13 +247,21 @@ function patternSourceOf(app, source) {
|
|
|
198
247
|
}
|
|
199
248
|
|
|
200
249
|
/**
|
|
201
|
-
* Point a
|
|
202
|
-
*
|
|
203
|
-
* mapping
|
|
204
|
-
*
|
|
250
|
+
* Point a style's picture transform at the transform in force for this
|
|
251
|
+
* paint. Gradients and patterns are both defined in *user* space, so the CTM
|
|
252
|
+
* is part of the mapping — and per the canvas spec it is the CTM at paint
|
|
253
|
+
* time, not the one that happened to be current when the style was made
|
|
254
|
+
* (verified against browsers: a gradient created untransformed and filled
|
|
255
|
+
* after a `translate` moves with the fill, and one created under a translate
|
|
256
|
+
* and filled without it does not).
|
|
257
|
+
*
|
|
258
|
+
* Returns false when nothing would be painted (a singular matrix), true for
|
|
259
|
+
* anything else, including every plain-colour style.
|
|
205
260
|
*/
|
|
206
|
-
function
|
|
207
|
-
return src instanceof CanvasPattern
|
|
261
|
+
function prepareStyle(src, m) {
|
|
262
|
+
return src instanceof CanvasPattern || src instanceof CanvasGradient
|
|
263
|
+
? src._sync(m)
|
|
264
|
+
: true;
|
|
208
265
|
}
|
|
209
266
|
|
|
210
267
|
function isPictureSource(image) {
|
|
@@ -422,6 +479,15 @@ class RenderingContext2d {
|
|
|
422
479
|
this._lineDash = [];
|
|
423
480
|
this._lineDashOffset = 0;
|
|
424
481
|
|
|
482
|
+
this._shadowBlur = 0;
|
|
483
|
+
this._shadowOffsetX = 0;
|
|
484
|
+
this._shadowOffsetY = 0;
|
|
485
|
+
// "transparent black" — the spec's default, and the one value that
|
|
486
|
+
// skips the whole shadow path, so an app that never asks for a shadow
|
|
487
|
+
// never pays for one
|
|
488
|
+
this._shadowColor = "rgba(0, 0, 0, 0)";
|
|
489
|
+
this._shadowRgba = [0, 0, 0, 0];
|
|
490
|
+
|
|
425
491
|
this.fillStyle = "white";
|
|
426
492
|
this.strokeStyle = "black";
|
|
427
493
|
this.lineWidth = 1;
|
|
@@ -660,6 +726,11 @@ class RenderingContext2d {
|
|
|
660
726
|
textRendering: this._textRendering,
|
|
661
727
|
textAlign: this.textAlign,
|
|
662
728
|
textBaseline: this.textBaseline,
|
|
729
|
+
shadowBlur: this._shadowBlur,
|
|
730
|
+
shadowColor: this._shadowColor,
|
|
731
|
+
shadowRgba: this._shadowRgba,
|
|
732
|
+
shadowOffsetX: this._shadowOffsetX,
|
|
733
|
+
shadowOffsetY: this._shadowOffsetY,
|
|
663
734
|
m: this._m.slice(),
|
|
664
735
|
clips: this._clips,
|
|
665
736
|
});
|
|
@@ -684,6 +755,11 @@ class RenderingContext2d {
|
|
|
684
755
|
this._textRendering = s.textRendering;
|
|
685
756
|
this.textAlign = s.textAlign;
|
|
686
757
|
this.textBaseline = s.textBaseline;
|
|
758
|
+
this._shadowBlur = s.shadowBlur;
|
|
759
|
+
this._shadowColor = s.shadowColor;
|
|
760
|
+
this._shadowRgba = s.shadowRgba;
|
|
761
|
+
this._shadowOffsetX = s.shadowOffsetX;
|
|
762
|
+
this._shadowOffsetY = s.shadowOffsetY;
|
|
687
763
|
this._m = s.m;
|
|
688
764
|
if (s.clips !== this._clips) {
|
|
689
765
|
this._clips = s.clips;
|
|
@@ -691,6 +767,398 @@ class RenderingContext2d {
|
|
|
691
767
|
}
|
|
692
768
|
}
|
|
693
769
|
|
|
770
|
+
// ------------------------------------------------------------------
|
|
771
|
+
// shadows (issue #272)
|
|
772
|
+
//
|
|
773
|
+
// The canvas properties, and the three server-side steps behind them: the
|
|
774
|
+
// drawing's coverage into a padded `a8` surface, a separable gaussian over
|
|
775
|
+
// it, and one masked composite in `shadowColor`. See lib/shadow.js for the
|
|
776
|
+
// blur and the cache, and docs/context-2d.md for what an app sees.
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Gaussian blur applied to the shadow, in pixels. Canvas-spec units: this
|
|
780
|
+
* is a **diameter**, and the gaussian it names has σ = `shadowBlur / 2`,
|
|
781
|
+
* so a value here looks the same as the same value in a browser.
|
|
782
|
+
*
|
|
783
|
+
* Negative and non-finite values are ignored, as the spec requires.
|
|
784
|
+
*/
|
|
785
|
+
set shadowBlur(value) {
|
|
786
|
+
const v = Number(value);
|
|
787
|
+
if (Number.isFinite(v) && v >= 0) this._shadowBlur = v;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
get shadowBlur() {
|
|
791
|
+
return this._shadowBlur;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* The shadow's colour. Defaults to fully transparent black, which is the
|
|
796
|
+
* spec's default and the switch that keeps every drawing operation on
|
|
797
|
+
* exactly the path it was on before shadows existed.
|
|
798
|
+
*
|
|
799
|
+
* A value that is not a colour is ignored (the spec's rule), so a
|
|
800
|
+
* mistyped colour leaves the previous shadow rather than throwing from
|
|
801
|
+
* inside a paint.
|
|
802
|
+
*/
|
|
803
|
+
set shadowColor(value) {
|
|
804
|
+
let rgba;
|
|
805
|
+
try {
|
|
806
|
+
rgba = parseColor(value);
|
|
807
|
+
} catch {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
this._shadowColor = value;
|
|
811
|
+
this._shadowRgba = rgba;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
get shadowColor() {
|
|
815
|
+
return this._shadowColor;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* How far the shadow is offset. In **device** pixels: the spec puts
|
|
820
|
+
* shadow offsets outside the current transform, so a rotated drawing has
|
|
821
|
+
* an upright shadow, the same way a rotated element's box-shadow is
|
|
822
|
+
* upright in CSS.
|
|
823
|
+
*
|
|
824
|
+
* Offsets are rounded to whole pixels when the shadow is composited. The
|
|
825
|
+
* drawing's own sub-pixel position is preserved either way; what rounds
|
|
826
|
+
* is where its blurred copy lands.
|
|
827
|
+
*/
|
|
828
|
+
set shadowOffsetX(value) {
|
|
829
|
+
const v = Number(value);
|
|
830
|
+
if (Number.isFinite(v)) this._shadowOffsetX = v;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
get shadowOffsetX() {
|
|
834
|
+
return this._shadowOffsetX;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
set shadowOffsetY(value) {
|
|
838
|
+
const v = Number(value);
|
|
839
|
+
if (Number.isFinite(v)) this._shadowOffsetY = v;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
get shadowOffsetY() {
|
|
843
|
+
return this._shadowOffsetY;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Is there a shadow to paint at all?
|
|
848
|
+
*
|
|
849
|
+
* A shadow with no offset and no blur still paints — it lands exactly
|
|
850
|
+
* under the drawing, where it shows through anything translucent, which
|
|
851
|
+
* is what the spec says and what browsers do. Only a transparent
|
|
852
|
+
* `shadowColor` (the default) skips the work, so the answer is one array
|
|
853
|
+
* read on every fill of every app that never mentioned shadows.
|
|
854
|
+
*/
|
|
855
|
+
_shadowed() {
|
|
856
|
+
return this._shadowRgba[3] > 0 && this.globalAlpha > 0;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* The coverage surface's box in device space: the drawing's ink, padded by
|
|
861
|
+
* `reach` on every side so the blur has room to spread into, and clipped
|
|
862
|
+
* to the part of the drawing whose shadow could land on the target at all.
|
|
863
|
+
*
|
|
864
|
+
* That clip is exact rather than a heuristic. A source pixel at `s`
|
|
865
|
+
* spreads to shadow pixels `s + offset ± reach`, so ink further than
|
|
866
|
+
* `reach` outside the target (once the offset is undone) cannot contribute
|
|
867
|
+
* a single pixel of visible shadow — dropping it costs nothing and keeps a
|
|
868
|
+
* shape far off-screen from allocating a surface the size of its own
|
|
869
|
+
* bounding box.
|
|
870
|
+
*/
|
|
871
|
+
_shadowBox(ink, reach) {
|
|
872
|
+
const ox = Math.round(this._shadowOffsetX);
|
|
873
|
+
const oy = Math.round(this._shadowOffsetY);
|
|
874
|
+
// a pixel of slack for the antialiased edge, as _clampBBox takes
|
|
875
|
+
const x0 = Math.max(Math.floor(ink.minX) - 1, -ox - reach);
|
|
876
|
+
const y0 = Math.max(Math.floor(ink.minY) - 1, -oy - reach);
|
|
877
|
+
const x1 = Math.min(Math.ceil(ink.maxX) + 1, this.width - ox + reach);
|
|
878
|
+
const y1 = Math.min(Math.ceil(ink.maxY) + 1, this.height - oy + reach);
|
|
879
|
+
if (x1 <= x0 || y1 <= y0) return null;
|
|
880
|
+
return {
|
|
881
|
+
x: x0 - reach,
|
|
882
|
+
y: y0 - reach,
|
|
883
|
+
w: x1 - x0 + reach * 2,
|
|
884
|
+
h: y1 - y0 + reach * 2,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Put this context's drawing state onto the coverage surface's context.
|
|
890
|
+
*
|
|
891
|
+
* `fillStyle` is opaque white and nothing else: an `a8` surface stores
|
|
892
|
+
* coverage, so what is drawn into it has to be at full alpha and takes its
|
|
893
|
+
* colour later, at the composite. `globalAlpha`, the composite op and the
|
|
894
|
+
* clip are deliberately *not* copied — they belong to that composite, not
|
|
895
|
+
* to the shape, and applying them twice would square the alpha.
|
|
896
|
+
*/
|
|
897
|
+
_loadShadowState(sctx, dx, dy) {
|
|
898
|
+
// a device-space translation in front of the transform, so a user-space
|
|
899
|
+
// call replayed here lands where the surface expects it. Translation
|
|
900
|
+
// does not change the determinant, so the transform-aware line width
|
|
901
|
+
// in _strokePolys is the same one the real stroke will use.
|
|
902
|
+
sctx._m = matMultiply([1, 0, 0, 1, dx, dy], this._m);
|
|
903
|
+
sctx.fillStyle = "#fff";
|
|
904
|
+
sctx.strokeStyle = "#fff";
|
|
905
|
+
sctx.lineWidth = this.lineWidth;
|
|
906
|
+
sctx.lineCap = this.lineCap;
|
|
907
|
+
sctx.lineJoin = this.lineJoin;
|
|
908
|
+
sctx.miterLimit = this.miterLimit;
|
|
909
|
+
sctx._lineDash = this._lineDash;
|
|
910
|
+
sctx._lineDashOffset = this._lineDashOffset;
|
|
911
|
+
sctx._textStyle = this._textStyle;
|
|
912
|
+
sctx._lastFontString = this._lastFontString;
|
|
913
|
+
sctx._fontVariations = this._fontVariations;
|
|
914
|
+
sctx._textRendering = this._textRendering;
|
|
915
|
+
sctx.textAlign = this.textAlign;
|
|
916
|
+
sctx.textBaseline = this.textBaseline;
|
|
917
|
+
return sctx;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Composite finished shadow coverage: the surface is the mask,
|
|
922
|
+
* `shadowColor` is the source.
|
|
923
|
+
*
|
|
924
|
+
* Borrowing `fillStyle` for the length of the call is what puts the
|
|
925
|
+
* shadow through the same route a coverage `drawImage` takes — clip,
|
|
926
|
+
* `globalAlpha`, composite op and damage reporting all apply to it exactly
|
|
927
|
+
* as they do to the drawing it belongs to.
|
|
928
|
+
*/
|
|
929
|
+
_paintShadow(surface, dx, dy) {
|
|
930
|
+
const style = this._fillStyle;
|
|
931
|
+
const picture = this._backgroundPicture;
|
|
932
|
+
this.fillStyle = this._shadowColor;
|
|
933
|
+
try {
|
|
934
|
+
this._drawCoverage(
|
|
935
|
+
surface.picture(this.window.app),
|
|
936
|
+
0,
|
|
937
|
+
0,
|
|
938
|
+
surface.width,
|
|
939
|
+
surface.height,
|
|
940
|
+
dx,
|
|
941
|
+
dy,
|
|
942
|
+
surface.width,
|
|
943
|
+
surface.height,
|
|
944
|
+
this._op(),
|
|
945
|
+
);
|
|
946
|
+
} finally {
|
|
947
|
+
this._fillStyle = style;
|
|
948
|
+
this._backgroundPicture = picture;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Paint the shadow of one drawing, given its device-space ink bounds and
|
|
954
|
+
* a way to draw it again into the coverage surface.
|
|
955
|
+
*
|
|
956
|
+
* `replay(sctx, dx, dy)` gets a context on that surface with this one's
|
|
957
|
+
* state already on it, and the device offset that maps this context's
|
|
958
|
+
* coordinates into it. Nothing is cached: the geometry of a path has no
|
|
959
|
+
* short name to key it by. Text does, and takes `_shadowOfText` instead.
|
|
960
|
+
*/
|
|
961
|
+
_shadowOfDrawing(ink, replay) {
|
|
962
|
+
if (!ink) return;
|
|
963
|
+
const app = this.window.app;
|
|
964
|
+
const policy = shadowPolicyOf(app);
|
|
965
|
+
const sigma = shadowSigma(this._shadowBlur, policy);
|
|
966
|
+
const reach = shadowReach(sigma);
|
|
967
|
+
const box = this._shadowBox(ink, reach);
|
|
968
|
+
if (!box || box.w * box.h > policy.maxPixels) return;
|
|
969
|
+
|
|
970
|
+
let surface = new Surface(app, {
|
|
971
|
+
width: box.w,
|
|
972
|
+
height: box.h,
|
|
973
|
+
format: "a8",
|
|
974
|
+
});
|
|
975
|
+
surface.render((sctx) => {
|
|
976
|
+
this._loadShadowState(sctx, -box.x, -box.y);
|
|
977
|
+
replay(sctx, -box.x, -box.y);
|
|
978
|
+
});
|
|
979
|
+
if (sigma > 0) surface = blurCoverage(surface, sigma);
|
|
980
|
+
this._paintShadow(
|
|
981
|
+
surface,
|
|
982
|
+
box.x + Math.round(this._shadowOffsetX),
|
|
983
|
+
box.y + Math.round(this._shadowOffsetY),
|
|
984
|
+
);
|
|
985
|
+
surface.destroy();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** the shadow of a path fill or stroke, from its device-space polys */
|
|
989
|
+
_shadowOfPolys(polys, { rule = "nonzero", stroke = false } = {}) {
|
|
990
|
+
const ink = polysInk(polys);
|
|
991
|
+
if (!ink) return;
|
|
992
|
+
if (stroke) {
|
|
993
|
+
// the stroke's ink is the extruded outline, which is not built yet —
|
|
994
|
+
// over-estimate it. Half the width covers the band, the miter limit
|
|
995
|
+
// covers the spike a sharp corner can throw, and the cost of guessing
|
|
996
|
+
// high is blur over a few empty pixels.
|
|
997
|
+
const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
|
|
998
|
+
const scale = Math.sqrt(Math.abs(det)) || 1;
|
|
999
|
+
const half = (this.lineWidth * scale) / 2;
|
|
1000
|
+
const spike =
|
|
1001
|
+
this.lineJoin === "miter" ? Math.min(Math.max(this.miterLimit, 1), 10) : 1.5;
|
|
1002
|
+
const slack = half * spike + 1;
|
|
1003
|
+
ink.minX -= slack;
|
|
1004
|
+
ink.minY -= slack;
|
|
1005
|
+
ink.maxX += slack;
|
|
1006
|
+
ink.maxY += slack;
|
|
1007
|
+
}
|
|
1008
|
+
this._shadowOfDrawing(ink, (sctx, dx, dy) => {
|
|
1009
|
+
const moved = shiftPolys(polys, dx, dy);
|
|
1010
|
+
if (stroke) sctx._strokePolys(moved);
|
|
1011
|
+
else sctx._fillPolys(moved, rule);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* The shadow of a run of text, cached.
|
|
1017
|
+
*
|
|
1018
|
+
* Text is the one drawing with a short, stable name — the string, the
|
|
1019
|
+
* font and the blur — so its coverage is built once and composited on
|
|
1020
|
+
* every frame afterwards, which is the difference between a specimen that
|
|
1021
|
+
* rebuilds two surfaces and a blur per slider tick and one that does not.
|
|
1022
|
+
*
|
|
1023
|
+
* The cached copy is position-independent: the run's origin sits at a
|
|
1024
|
+
* whole pixel inside the surface, and the composite carries it to wherever
|
|
1025
|
+
* the text is. Glyph origins are rounded to whole pixels on the way to the
|
|
1026
|
+
* server anyway, so nothing is lost by it.
|
|
1027
|
+
*/
|
|
1028
|
+
_shadowOfText(text, x, y) {
|
|
1029
|
+
const app = this.window.app;
|
|
1030
|
+
const policy = shadowPolicyOf(app);
|
|
1031
|
+
const sigma = shadowSigma(this._shadowBlur, policy);
|
|
1032
|
+
const reach = shadowReach(sigma);
|
|
1033
|
+
const style = this._resolvedTextStyle();
|
|
1034
|
+
// the same shaping memo fillText draws from, so a shadowed label shapes
|
|
1035
|
+
// once per frame rather than twice
|
|
1036
|
+
const shaped = app.fonts._shapeCachedWhole(text, style);
|
|
1037
|
+
const ink = this._shapedInk(shaped);
|
|
1038
|
+
// rounded outwards, with a pixel of antialiasing slack
|
|
1039
|
+
const left = Math.ceil(-ink.minX) + 1;
|
|
1040
|
+
const right = Math.ceil(ink.maxX) + 1;
|
|
1041
|
+
const ascent = Math.ceil(-ink.minY) + 1;
|
|
1042
|
+
const descent = Math.ceil(ink.maxY) + 1;
|
|
1043
|
+
const width = left + right + reach * 2;
|
|
1044
|
+
const height = ascent + descent + reach * 2;
|
|
1045
|
+
if (width <= 0 || height <= 0) return;
|
|
1046
|
+
// the run's origin inside the surface — whole pixels, so the coverage
|
|
1047
|
+
// is the same wherever on the target the text is drawn
|
|
1048
|
+
const originX = reach + left;
|
|
1049
|
+
const originY = reach + ascent;
|
|
1050
|
+
|
|
1051
|
+
// where the run's origin lands on the target, exactly as fillText puts it
|
|
1052
|
+
const [tx, ty] = matApply(this._m, x, y);
|
|
1053
|
+
const runX = tx + this._alignOffset(shaped);
|
|
1054
|
+
const runY = ty + this._baselineOffset(style.font.metrics(style.size));
|
|
1055
|
+
|
|
1056
|
+
// A run whose padded ink is larger than a shadow surface may be does not
|
|
1057
|
+
// get one: fall back to the clipped, uncached path, which sizes itself
|
|
1058
|
+
// to the part of the shadow that can actually be seen.
|
|
1059
|
+
if (width * height > policy.maxPixels) {
|
|
1060
|
+
this._shadowOfDrawing(
|
|
1061
|
+
{
|
|
1062
|
+
minX: runX + ink.minX,
|
|
1063
|
+
maxX: runX + ink.maxX,
|
|
1064
|
+
minY: runY + ink.minY,
|
|
1065
|
+
maxY: runY + ink.maxY,
|
|
1066
|
+
},
|
|
1067
|
+
(sctx) => sctx.fillText(text, x, y),
|
|
1068
|
+
);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const key = [
|
|
1073
|
+
text,
|
|
1074
|
+
this._lastFontString,
|
|
1075
|
+
style.font.key,
|
|
1076
|
+
variationsKey(this._fontVariations),
|
|
1077
|
+
this._textRendering ?? "",
|
|
1078
|
+
sigma,
|
|
1079
|
+
].join("\u0000");
|
|
1080
|
+
const surface = cachedShadow(app, key, () => {
|
|
1081
|
+
let coverage = new Surface(app, { width, height, format: "a8" });
|
|
1082
|
+
coverage.render((sctx) => {
|
|
1083
|
+
this._loadShadowState(sctx, 0, 0);
|
|
1084
|
+
// the origin is placed by hand, so neither alignment nor the
|
|
1085
|
+
// baseline may move it again
|
|
1086
|
+
sctx._m = [1, 0, 0, 1, 0, 0];
|
|
1087
|
+
sctx.textAlign = "left";
|
|
1088
|
+
sctx.textBaseline = "alphabetic";
|
|
1089
|
+
sctx.fillText(text, originX, originY);
|
|
1090
|
+
});
|
|
1091
|
+
if (sigma > 0) coverage = blurCoverage(coverage, sigma);
|
|
1092
|
+
return coverage;
|
|
1093
|
+
});
|
|
1094
|
+
if (!surface) return;
|
|
1095
|
+
this._paintShadow(
|
|
1096
|
+
surface,
|
|
1097
|
+
Math.round(runX + this._shadowOffsetX) - originX,
|
|
1098
|
+
Math.round(runY + this._shadowOffsetY) - originY,
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** the shadow of a `fill()`/`stroke()`-shaped call, from its arguments */
|
|
1103
|
+
_shadowOfPath(args, stroke) {
|
|
1104
|
+
if (stroke) {
|
|
1105
|
+
const polys =
|
|
1106
|
+
args[0] instanceof Path2D
|
|
1107
|
+
? flattenPath(args[0]._cmds, this._m)
|
|
1108
|
+
: flattenPath(this._path._cmds, null);
|
|
1109
|
+
this._shadowOfPolys(polys, { stroke: true });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const { polys, rule } = this._polysFor(args);
|
|
1113
|
+
this._shadowOfPolys(polys, { rule });
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* The shadow of a `drawImage`, from the destination rectangle its
|
|
1118
|
+
* arguments describe.
|
|
1119
|
+
*
|
|
1120
|
+
* The image is drawn again into the coverage surface rather than its alpha
|
|
1121
|
+
* being read out: an `a8` destination *is* the alpha channel, so an
|
|
1122
|
+
* ordinary composite of the image onto one leaves exactly the coverage the
|
|
1123
|
+
* shadow needs — including a translucent image's soft edges, and whatever
|
|
1124
|
+
* scaling or transform the call asked for.
|
|
1125
|
+
*/
|
|
1126
|
+
_shadowOfImage(image, args) {
|
|
1127
|
+
const iw = image?.width;
|
|
1128
|
+
const ih = image?.height;
|
|
1129
|
+
if (!Number.isFinite(iw) || !Number.isFinite(ih)) return;
|
|
1130
|
+
let rect;
|
|
1131
|
+
if (args.length >= 8) rect = args.slice(4, 8);
|
|
1132
|
+
else if (args.length >= 4) rect = args.slice(0, 4);
|
|
1133
|
+
else rect = [args[0] ?? 0, args[1] ?? 0, iw, ih];
|
|
1134
|
+
const [dx, dy, dw, dh] = rect;
|
|
1135
|
+
if (!(dw > 0) || !(dh > 0)) return;
|
|
1136
|
+
const corners = [
|
|
1137
|
+
matApply(this._m, dx, dy),
|
|
1138
|
+
matApply(this._m, dx + dw, dy),
|
|
1139
|
+
matApply(this._m, dx, dy + dh),
|
|
1140
|
+
matApply(this._m, dx + dw, dy + dh),
|
|
1141
|
+
];
|
|
1142
|
+
const xs = corners.map((c) => c[0]);
|
|
1143
|
+
const ys = corners.map((c) => c[1]);
|
|
1144
|
+
this._shadowOfDrawing(
|
|
1145
|
+
{
|
|
1146
|
+
minX: Math.min(...xs),
|
|
1147
|
+
maxX: Math.max(...xs),
|
|
1148
|
+
minY: Math.min(...ys),
|
|
1149
|
+
maxY: Math.max(...ys),
|
|
1150
|
+
},
|
|
1151
|
+
(sctx) => sctx.drawImage(image, ...args),
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** the shadow of an axis-aligned rectangle in user space */
|
|
1156
|
+
_shadowOfRect(x, y, w, h, stroke) {
|
|
1157
|
+
const tmp = new Path2D();
|
|
1158
|
+
tmp.rect(x, y, w, h);
|
|
1159
|
+
this._shadowOfPolys(flattenPath(tmp._cmds, this._m), { stroke });
|
|
1160
|
+
}
|
|
1161
|
+
|
|
694
1162
|
// ------------------------------------------------------------------
|
|
695
1163
|
// transform
|
|
696
1164
|
|
|
@@ -1135,7 +1603,7 @@ class RenderingContext2d {
|
|
|
1135
1603
|
op = op ?? this._op();
|
|
1136
1604
|
alpha = alpha ?? this.globalAlpha;
|
|
1137
1605
|
if (alpha <= 0) return;
|
|
1138
|
-
if (!
|
|
1606
|
+
if (!prepareStyle(src, this._m)) return;
|
|
1139
1607
|
|
|
1140
1608
|
// one box per subpath, so a path holding disjoint ones can be masked as
|
|
1141
1609
|
// the pieces it is rather than as the box around all of them
|
|
@@ -1178,6 +1646,7 @@ class RenderingContext2d {
|
|
|
1178
1646
|
_strokePolys(polys, { src = null } = {}) {
|
|
1179
1647
|
src = src ?? this._strokePicture;
|
|
1180
1648
|
if (this.globalAlpha <= 0) return;
|
|
1649
|
+
if (!prepareStyle(src, this._m)) return;
|
|
1181
1650
|
// approximate transform-aware line width by the average scale factor
|
|
1182
1651
|
const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
|
|
1183
1652
|
const scale = Math.sqrt(Math.abs(det)) || 1;
|
|
@@ -1662,7 +2131,9 @@ class RenderingContext2d {
|
|
|
1662
2131
|
* @param {Picture} src source picture the glyphs paint with — a solid
|
|
1663
2132
|
* (`ctx.createSolidPicture(r, g, b, a)`, premultiplied 0..1) or a gradient
|
|
1664
2133
|
* @param {Array<{run, x, y, textRendering?}>} positioned runs in visual
|
|
1665
|
-
* order; `x`/`y` is the run's baseline origin in
|
|
2134
|
+
* order; `x`/`y` is the run's baseline origin in **user space** — the
|
|
2135
|
+
* current transform applies to it, as it does to every other drawing
|
|
2136
|
+
* call. `run` is
|
|
1666
2137
|
* `{ font, size, glyphs }` — a `Font`, a pixel size, and glyphs
|
|
1667
2138
|
* `{ id, ax, dx, dy }` in drawing order: `id` a font glyph id
|
|
1668
2139
|
* (`Font.shape()`'s `glyphs[].id`, or `Font.glyphIdFor(cp)`), `ax` the
|
|
@@ -1673,6 +2144,32 @@ class RenderingContext2d {
|
|
|
1673
2144
|
* (`codePoints`, `width`, …) are ignored. `textRendering` optionally
|
|
1674
2145
|
* overrides the bitmap/vector routing per run (docs/text.md).
|
|
1675
2146
|
*
|
|
2147
|
+
* The transform moves each run's origin, exactly as `fillText` moves its
|
|
2148
|
+
* anchor; the glyphs themselves are not rotated or scaled by it (size the
|
|
2149
|
+
* font via `ctx.font`, or `run.size`, instead). Advances and `dx`/`dy` are
|
|
2150
|
+
* therefore device pixels on both calls. Without this, a `TextLayout`
|
|
2151
|
+
* drawn into a translated context — which is every react-x11 canvas that
|
|
2152
|
+
* is not at the window's origin — landed at the untransformed coordinates
|
|
2153
|
+
* and was then cut by the clip, while the neighbouring `fillRect` and
|
|
2154
|
+
* `drawImage` moved (issue #280).
|
|
2155
|
+
*/
|
|
2156
|
+
drawGlyphs(op, src, positioned) {
|
|
2157
|
+
const m = this._m;
|
|
2158
|
+
if (!matIsIdentity(m)) {
|
|
2159
|
+
positioned = positioned.map((p) => {
|
|
2160
|
+
const [x, y] = matApply(m, p.x, p.y);
|
|
2161
|
+
return { ...p, x, y };
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
this._drawGlyphsDevice(op, src, positioned);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* `drawGlyphs` with the origins already in device space — the primitive
|
|
2169
|
+
* under it, for callers that place glyphs themselves (`fillText`, which
|
|
2170
|
+
* has to add the alignment and baseline offsets *after* the transform,
|
|
2171
|
+
* because glyph advances are device pixels).
|
|
2172
|
+
*
|
|
1676
2173
|
* CompositeGlyphs writes straight to the destination picture, so it has
|
|
1677
2174
|
* no way to consult our clip mask — text drawn through it used to spill
|
|
1678
2175
|
* out of clipped boxes while every fill and stroke stayed inside. With a
|
|
@@ -1681,10 +2178,10 @@ class RenderingContext2d {
|
|
|
1681
2178
|
* through the result — the same shape as _fillPolys. A rectangular clip
|
|
1682
2179
|
* takes the server-side fast path below instead of the mask.
|
|
1683
2180
|
*/
|
|
1684
|
-
|
|
2181
|
+
_drawGlyphsDevice(op, src, positioned) {
|
|
1685
2182
|
const app = this.window.app;
|
|
1686
2183
|
const R = this.Render;
|
|
1687
|
-
if (!
|
|
2184
|
+
if (!prepareStyle(src, this._m)) return;
|
|
1688
2185
|
if (!this._clips.length) {
|
|
1689
2186
|
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
1690
2187
|
this._markDirty();
|
|
@@ -1905,8 +2402,15 @@ class RenderingContext2d {
|
|
|
1905
2402
|
}
|
|
1906
2403
|
|
|
1907
2404
|
fillRect(x, y, w, h) {
|
|
2405
|
+
if (this._shadowed()) this._shadowOfRect(x, y, w, h, false);
|
|
2406
|
+
this._fillRect(x, y, w, h);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
/** fillRect minus the shadow — the batch in `fillRects` paints one shadow
|
|
2410
|
+
* for the whole list and then draws the rectangles through here */
|
|
2411
|
+
_fillRect(x, y, w, h) {
|
|
1908
2412
|
if (matIsIdentity(this._m)) {
|
|
1909
|
-
if (!
|
|
2413
|
+
if (!prepareStyle(this._backgroundPicture, this._m)) return;
|
|
1910
2414
|
const mask = this._compositeMask();
|
|
1911
2415
|
this.Render.Composite(
|
|
1912
2416
|
this._op(),
|
|
@@ -1965,6 +2469,17 @@ class RenderingContext2d {
|
|
|
1965
2469
|
}
|
|
1966
2470
|
if (!flat.length) return;
|
|
1967
2471
|
|
|
2472
|
+
// one shadow for the batch, not one per rectangle: the shadow of a
|
|
2473
|
+
// group of rectangles is their combined coverage blurred once, and N
|
|
2474
|
+
// separate shadows would darken every overlap
|
|
2475
|
+
if (this._shadowed()) {
|
|
2476
|
+
const rectPath = new Path2D();
|
|
2477
|
+
for (let i = 0; i < flat.length; i += 4) {
|
|
2478
|
+
rectPath.rect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
2479
|
+
}
|
|
2480
|
+
this._shadowOfPolys(flattenPath(rectPath._cmds, this._m), {});
|
|
2481
|
+
}
|
|
2482
|
+
|
|
1968
2483
|
const clip = this._clips.length ? this._clipRect() : null;
|
|
1969
2484
|
if (
|
|
1970
2485
|
!matIsIdentity(this._m) ||
|
|
@@ -1972,7 +2487,7 @@ class RenderingContext2d {
|
|
|
1972
2487
|
(this._clips.length && !clip)
|
|
1973
2488
|
) {
|
|
1974
2489
|
for (let i = 0; i < flat.length; i += 4)
|
|
1975
|
-
this.
|
|
2490
|
+
this._fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
1976
2491
|
return;
|
|
1977
2492
|
}
|
|
1978
2493
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
@@ -2005,6 +2520,7 @@ class RenderingContext2d {
|
|
|
2005
2520
|
}
|
|
2006
2521
|
|
|
2007
2522
|
strokeRect(x, y, w, h) {
|
|
2523
|
+
if (this._shadowed()) this._shadowOfRect(x, y, w, h, true);
|
|
2008
2524
|
const m = this._m;
|
|
2009
2525
|
if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1) {
|
|
2010
2526
|
const zero = { x: 0, y: 0 };
|
|
@@ -2025,12 +2541,14 @@ class RenderingContext2d {
|
|
|
2025
2541
|
}
|
|
2026
2542
|
|
|
2027
2543
|
fill(...args) {
|
|
2544
|
+
if (this._shadowed()) this._shadowOfPath(args, false);
|
|
2028
2545
|
if (this._tryRoundRectFill(args)) return;
|
|
2029
2546
|
const { polys, rule } = this._polysFor(args);
|
|
2030
2547
|
this._fillPolys(polys, rule);
|
|
2031
2548
|
}
|
|
2032
2549
|
|
|
2033
2550
|
stroke(...args) {
|
|
2551
|
+
if (this._shadowed()) this._shadowOfPath(args, true);
|
|
2034
2552
|
const box = this._shapeBoxFor(args);
|
|
2035
2553
|
if (box && this._tryStrokeBox(box)) return;
|
|
2036
2554
|
const polys =
|
|
@@ -2803,6 +3321,7 @@ class RenderingContext2d {
|
|
|
2803
3321
|
fillText(text, x, y) {
|
|
2804
3322
|
text = String(text ?? "");
|
|
2805
3323
|
if (!text) return;
|
|
3324
|
+
if (this._shadowed()) this._shadowOfText(text, x, y);
|
|
2806
3325
|
const style = this._resolvedTextStyle();
|
|
2807
3326
|
const app = this.window.app;
|
|
2808
3327
|
// through the shaping memo TextLayout uses: a label repainted every
|
|
@@ -2823,7 +3342,9 @@ class RenderingContext2d {
|
|
|
2823
3342
|
});
|
|
2824
3343
|
cursor += run.width;
|
|
2825
3344
|
}
|
|
2826
|
-
|
|
3345
|
+
// already device space: the anchor went through the matrix above, and
|
|
3346
|
+
// the offsets and advances added to it are device pixels
|
|
3347
|
+
this._drawGlyphsDevice(
|
|
2827
3348
|
this.Render.PictOp.Over,
|
|
2828
3349
|
this._backgroundPicture,
|
|
2829
3350
|
positioned,
|
|
@@ -2835,9 +3356,13 @@ class RenderingContext2d {
|
|
|
2835
3356
|
* `width` (advance), actual bounding box (ink extents relative to the
|
|
2836
3357
|
* origin), and font bounding box (from font metrics).
|
|
2837
3358
|
*/
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
3359
|
+
/**
|
|
3360
|
+
* Ink extents of a shaped run, relative to its origin: the loop behind
|
|
3361
|
+
* `measureText`'s actual bounding box, shared with the shadow path so a
|
|
3362
|
+
* shadowed `fillText` does not shape its text a second time to find out
|
|
3363
|
+
* how big its coverage surface has to be.
|
|
3364
|
+
*/
|
|
3365
|
+
_shapedInk(shaped) {
|
|
2841
3366
|
let minX = 0;
|
|
2842
3367
|
let maxX = 0;
|
|
2843
3368
|
let minY = 0;
|
|
@@ -2855,6 +3380,13 @@ class RenderingContext2d {
|
|
|
2855
3380
|
cursor += g.ax;
|
|
2856
3381
|
}
|
|
2857
3382
|
}
|
|
3383
|
+
return { minX, maxX, minY, maxY };
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
measureText(text) {
|
|
3387
|
+
const style = this._resolvedTextStyle();
|
|
3388
|
+
const shaped = this.window.app.fonts.shape(String(text ?? ""), style);
|
|
3389
|
+
const { minX, maxX, minY, maxY } = this._shapedInk(shaped);
|
|
2858
3390
|
const m = style.font.metrics(style.size);
|
|
2859
3391
|
return {
|
|
2860
3392
|
width: shaped.width,
|
|
@@ -3239,11 +3771,7 @@ class RenderingContext2d {
|
|
|
3239
3771
|
_coverageSource() {
|
|
3240
3772
|
const style = this._fillStyle;
|
|
3241
3773
|
if (this.globalAlpha >= 1 || !isPlainColor(style)) {
|
|
3242
|
-
|
|
3243
|
-
// a source is transparent black — no call site here has to know
|
|
3244
|
-
return preparePattern(this._backgroundPicture, this._m)
|
|
3245
|
-
? this._backgroundPicture
|
|
3246
|
-
: this.createSolidPicture(0, 0, 0, 0);
|
|
3774
|
+
return this._backgroundPicture;
|
|
3247
3775
|
}
|
|
3248
3776
|
const c = parseColor(style);
|
|
3249
3777
|
return this.createSolidPicture(c[0], c[1], c[2], c[3] * this.globalAlpha);
|
|
@@ -3260,6 +3788,9 @@ class RenderingContext2d {
|
|
|
3260
3788
|
*/
|
|
3261
3789
|
_drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op) {
|
|
3262
3790
|
const R = this.Render;
|
|
3791
|
+
// the coverage is painted in the current fillStyle, on both branches
|
|
3792
|
+
// below — a gradient/pattern whose transform collapsed paints nothing
|
|
3793
|
+
if (!prepareStyle(this._backgroundPicture, this._m)) return;
|
|
3263
3794
|
const scaled = dw !== sw || dh !== sh;
|
|
3264
3795
|
if (scaled) {
|
|
3265
3796
|
R.SetPictureTransform(picture.id, [
|
|
@@ -3419,6 +3950,7 @@ class RenderingContext2d {
|
|
|
3419
3950
|
}
|
|
3420
3951
|
|
|
3421
3952
|
drawImage(image, ...args) {
|
|
3953
|
+
if (this._shadowed()) this._shadowOfImage(image, args);
|
|
3422
3954
|
if (isPictureSource(image)) {
|
|
3423
3955
|
let sx = 0;
|
|
3424
3956
|
let sy = 0;
|
|
@@ -3665,6 +4197,21 @@ class RenderingContext2d {
|
|
|
3665
4197
|
}
|
|
3666
4198
|
}
|
|
3667
4199
|
|
|
4200
|
+
/**
|
|
4201
|
+
* The fill/stroke style the `create*Gradient` methods return: colour stops
|
|
4202
|
+
* along a line, between two circles, or around a point, backed by one
|
|
4203
|
+
* XRender gradient picture.
|
|
4204
|
+
*
|
|
4205
|
+
* Its coordinates are **user space**, like every other coordinate a caller
|
|
4206
|
+
* gives the context, and are resolved against the transform in force when
|
|
4207
|
+
* the gradient is *painted* — the picture transform is that CTM's inverse,
|
|
4208
|
+
* installed by `_sync` before each use, exactly as a pattern's is. A
|
|
4209
|
+
* gradient made for a node's own coordinates therefore keeps painting in
|
|
4210
|
+
* them after the context is translated to that node's origin (issue #271).
|
|
4211
|
+
*
|
|
4212
|
+
* The picture is created on first use and freed by the GC, through
|
|
4213
|
+
* `Picture`'s finalizer.
|
|
4214
|
+
*/
|
|
3668
4215
|
class CanvasGradient {
|
|
3669
4216
|
constructor(type, ctx, p0, p1, p2, p3, p4, p5) {
|
|
3670
4217
|
this.type = type;
|
|
@@ -3672,6 +4219,9 @@ class CanvasGradient {
|
|
|
3672
4219
|
this.ctx = ctx;
|
|
3673
4220
|
this._id = null;
|
|
3674
4221
|
this._picture = null;
|
|
4222
|
+
// what the server currently holds: a fresh gradient picture is
|
|
4223
|
+
// untransformed, so an untransformed fill costs no extra request
|
|
4224
|
+
this._applied = [1, 0, 0, 1, 0, 0];
|
|
3675
4225
|
|
|
3676
4226
|
this.x0 = p0;
|
|
3677
4227
|
this.y0 = p1;
|
|
@@ -3683,7 +4233,6 @@ class CanvasGradient {
|
|
|
3683
4233
|
} else {
|
|
3684
4234
|
this.angle = p2;
|
|
3685
4235
|
}
|
|
3686
|
-
// TODO: check if ChangePictureAttribute works on gradients ( set repeat edge pixels flag )
|
|
3687
4236
|
}
|
|
3688
4237
|
|
|
3689
4238
|
addColorStop(offset, color) {
|
|
@@ -3729,10 +4278,55 @@ class CanvasGradient {
|
|
|
3729
4278
|
default:
|
|
3730
4279
|
throw new Error("unknown gradient type");
|
|
3731
4280
|
}
|
|
4281
|
+
// Past the outermost stop a gradient clamps to that stop's colour, as
|
|
4282
|
+
// the canvas and CSS specs say — which is XRender's RepeatPad, not the
|
|
4283
|
+
// RepeatNone (transparent) a gradient picture is born with. Without it
|
|
4284
|
+
// an app has to place its gradient exactly on the fill or lose the
|
|
4285
|
+
// corners. The in-process JS server pads unconditionally, so only a real
|
|
4286
|
+
// server can tell the difference.
|
|
4287
|
+
Render.ChangePicture(this._id, { repeat: 2 }); // Repeat.Pad
|
|
3732
4288
|
// wrap with picture so FreePicture is invoked on gc via FinalizationRegistry
|
|
3733
4289
|
this._picture = new Picture(this.ctx.window.app, { id: this._id });
|
|
3734
4290
|
return this._id;
|
|
3735
4291
|
}
|
|
4292
|
+
|
|
4293
|
+
/**
|
|
4294
|
+
* Make the server-side mapping match the CTM this paint runs under. The
|
|
4295
|
+
* gradient's own coordinates are user space and every fill samples the
|
|
4296
|
+
* source at device coordinates, so the picture transform — which takes a
|
|
4297
|
+
* source coordinate to a gradient one — is the CTM's inverse.
|
|
4298
|
+
*
|
|
4299
|
+
* Returns false when the CTM collapses (a zero scale), which paints
|
|
4300
|
+
* nothing, exactly as the canvas spec says.
|
|
4301
|
+
*/
|
|
4302
|
+
_sync(ctm) {
|
|
4303
|
+
const inv = matInvert(ctm);
|
|
4304
|
+
if (!inv) return false;
|
|
4305
|
+
const id = this.id; // lazily creates the picture
|
|
4306
|
+
const a = this._applied;
|
|
4307
|
+
if (
|
|
4308
|
+
inv[0] !== a[0] ||
|
|
4309
|
+
inv[1] !== a[1] ||
|
|
4310
|
+
inv[2] !== a[2] ||
|
|
4311
|
+
inv[3] !== a[3] ||
|
|
4312
|
+
inv[4] !== a[4] ||
|
|
4313
|
+
inv[5] !== a[5]
|
|
4314
|
+
) {
|
|
4315
|
+
this.ctx.Render.SetPictureTransform(id, [
|
|
4316
|
+
inv[0],
|
|
4317
|
+
inv[2],
|
|
4318
|
+
inv[4],
|
|
4319
|
+
inv[1],
|
|
4320
|
+
inv[3],
|
|
4321
|
+
inv[5],
|
|
4322
|
+
0,
|
|
4323
|
+
0,
|
|
4324
|
+
1,
|
|
4325
|
+
]);
|
|
4326
|
+
this._applied = inv;
|
|
4327
|
+
}
|
|
4328
|
+
return true;
|
|
4329
|
+
}
|
|
3736
4330
|
}
|
|
3737
4331
|
|
|
3738
4332
|
/**
|