ntk 2.1.1 → 3.0.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.
- package/README.md +115 -110
- package/lib/app.js +48 -15
- package/lib/cleanup.js +14 -0
- package/lib/drawable.js +10 -16
- package/lib/events_map.js +81 -87
- package/lib/fontconfig.js +122 -14
- package/lib/glyphset.js +36 -18
- package/lib/image.js +138 -0
- package/lib/index.js +110 -52
- package/lib/path.js +734 -0
- package/lib/picture.js +76 -38
- package/lib/pixmap.js +34 -25
- package/lib/rasterize.js +178 -0
- package/lib/renderingcontext_2d.js +994 -517
- package/lib/renderingcontext_opengl.js +38 -31
- package/lib/renderingcontext_x11.js +56 -64
- package/lib/text/font.js +196 -0
- package/lib/text/fontmanager.js +233 -0
- package/lib/text/fontsource.js +179 -0
- package/lib/text/glyphs.js +456 -0
- package/lib/text/layout.js +356 -0
- package/lib/text/shape.js +141 -0
- package/lib/trapezoid.js +131 -0
- package/lib/widgets/css.js +676 -0
- package/lib/widgets/highlight.js +97 -0
- package/lib/widgets/htmlview.js +879 -0
- package/lib/widgets/markdown.js +148 -0
- package/lib/widgets/markdownview.js +487 -0
- package/lib/widgets/mermaid.js +720 -0
- package/lib/widgets/svgview.js +528 -0
- package/lib/widgets/tex.js +681 -0
- package/lib/window.js +568 -246
- package/package.json +35 -32
- package/.npmignore +0 -15
- package/examples/example-await.js +0 -23
- package/examples/example-co.js +0 -13
- package/examples/example.js +0 -14
- package/examples/fill-styles.js +0 -39
- package/examples/glclock.js +0 -100
- package/examples/glxpixmap.js +0 -12
- package/examples/gradient.js +0 -41
- package/examples/package.json +0 -17
- package/examples/simpletex.js +0 -72
- package/examples/teapot.js +0 -104
- package/examples/teapot.json +0 -6
- package/examples/text-rendering.js +0 -44
- package/lib/fontface.js +0 -109
- package/lib/widgets/webview.js +0 -33
|
@@ -1,601 +1,1078 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import parseFontStyle from 'canvas-fontstyle';
|
|
2
|
+
import extrudePolyline from 'extrude-polyline';
|
|
3
|
+
import parseColorRaw from 'parse-color';
|
|
4
|
+
|
|
5
|
+
import Drawable from './drawable.js';
|
|
6
|
+
import { Image } from './image.js';
|
|
7
|
+
import {
|
|
8
|
+
Path2D,
|
|
9
|
+
flattenPath,
|
|
10
|
+
transformCommands,
|
|
11
|
+
ellipseSegments,
|
|
12
|
+
polysContain,
|
|
13
|
+
matApply,
|
|
14
|
+
matInvert,
|
|
15
|
+
matIsIdentity,
|
|
16
|
+
matMultiply
|
|
17
|
+
} from './path.js';
|
|
18
|
+
import Picture from './picture.js';
|
|
19
|
+
import Pixmap from './pixmap.js';
|
|
20
|
+
import { drawGlyphRuns } from './text/glyphs.js';
|
|
21
|
+
import { TextLayout } from './text/layout.js';
|
|
22
|
+
import { reorderRuns } from './text/shape.js';
|
|
23
|
+
import { trapezoidize } from './trapezoid.js';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_FONT = '20px sans-serif';
|
|
26
|
+
|
|
27
|
+
// canvas globalCompositeOperation -> XRender PictOp name. Porter-Duff ops
|
|
28
|
+
// map directly; with a clip/shape mask active the op only applies inside
|
|
29
|
+
// the mask coverage (outside pixels are left untouched).
|
|
30
|
+
const GCO_TO_PICTOP = {
|
|
31
|
+
'source-over': 'Over',
|
|
32
|
+
copy: 'Src',
|
|
33
|
+
'destination-over': 'OverReverse',
|
|
34
|
+
'source-in': 'In',
|
|
35
|
+
'destination-in': 'InReverse',
|
|
36
|
+
'source-out': 'Out',
|
|
37
|
+
'destination-out': 'OutReverse',
|
|
38
|
+
'source-atop': 'Atop',
|
|
39
|
+
'destination-atop': 'AtopReverse',
|
|
40
|
+
xor: 'Xor',
|
|
41
|
+
lighter: 'Add'
|
|
42
|
+
};
|
|
5
43
|
|
|
6
|
-
|
|
7
|
-
|
|
44
|
+
// extrude-polyline has no round caps/joins; degrade to the closest shape
|
|
45
|
+
const LINE_CAP = { butt: 'butt', square: 'square', round: 'square' };
|
|
46
|
+
const LINE_JOIN = { miter: 'miter', bevel: 'bevel', round: 'bevel' };
|
|
8
47
|
|
|
9
|
-
|
|
10
|
-
|
|
48
|
+
function parseColor(value) {
|
|
49
|
+
if (Array.isArray(value)) return value;
|
|
50
|
+
const c = parseColorRaw(value);
|
|
51
|
+
return [c.rgba[0] / 255, c.rgba[1] / 255, c.rgba[2] / 255, c.rgba[3]];
|
|
11
52
|
}
|
|
12
53
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Canvas-like 2d rendering context backed by the XRender extension: most
|
|
56
|
+
* operations (composition, gradients, text composition) happen server-side.
|
|
57
|
+
*
|
|
58
|
+
* Paths follow the HTML canvas model: the default path records points with
|
|
59
|
+
* the current transform applied at command time; `Path2D` objects are
|
|
60
|
+
* transformed when filled/stroked/clipped. See docs/context-2d.md for the
|
|
61
|
+
* supported surface and the differences from the browser canvas.
|
|
62
|
+
*/
|
|
63
|
+
class RenderingContext2d {
|
|
64
|
+
constructor(window) {
|
|
65
|
+
const X = window.X;
|
|
66
|
+
this.X = X;
|
|
67
|
+
|
|
68
|
+
this.window = window;
|
|
69
|
+
this.Render = window.app.display.Render;
|
|
70
|
+
|
|
71
|
+
// draw into the window's backing pixmap when it has one (double
|
|
72
|
+
// buffering); re-bind when the backing pixmap is reallocated on resize
|
|
73
|
+
this._target = null;
|
|
74
|
+
this._gc = null;
|
|
75
|
+
this.picture = null;
|
|
76
|
+
this._bindTarget();
|
|
77
|
+
if (typeof window.on === 'function') {
|
|
78
|
+
window.on('_backing', () => this._bindTarget());
|
|
79
|
+
// masks are sized to the drawable — recreate them after a resize
|
|
80
|
+
window.on('resize', () => this._dropMasks());
|
|
81
|
+
// window-backed pictures are freed server-side with the window
|
|
82
|
+
window.on('_destroyed', () => {
|
|
83
|
+
if (this.picture && this._target === this.window) {
|
|
84
|
+
this.picture.forget();
|
|
85
|
+
this.picture = null;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
16
89
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
90
|
+
this._solidPictures = new Map();
|
|
91
|
+
this.fillMask = null;
|
|
92
|
+
this.fillMaskDrawable = null;
|
|
93
|
+
this.clipMask = null;
|
|
94
|
+
this.clipMaskDrawable = null;
|
|
95
|
+
this._textStyle = null;
|
|
96
|
+
this._lastFontString = null;
|
|
97
|
+
this.textAlign = 'start';
|
|
98
|
+
this.textBaseline = 'alphabetic';
|
|
99
|
+
|
|
100
|
+
this._path = new Path2D();
|
|
101
|
+
this._m = [1, 0, 0, 1, 0, 0];
|
|
102
|
+
this._stack = [];
|
|
103
|
+
this._clips = []; // [{ polys, rule }] in device space, already stacked
|
|
104
|
+
this._gco = 'source-over';
|
|
105
|
+
this.globalAlpha = 1;
|
|
106
|
+
this.lineCap = 'butt';
|
|
107
|
+
this.lineJoin = 'miter';
|
|
108
|
+
this.miterLimit = 10;
|
|
109
|
+
|
|
110
|
+
this.fillStyle = 'white';
|
|
111
|
+
this.strokeStyle = 'black';
|
|
112
|
+
this.lineWidth = 1;
|
|
21
113
|
}
|
|
22
114
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (window.depth === 32)
|
|
28
|
-
format = this.Render.rgba32;
|
|
29
|
-
else
|
|
30
|
-
format = this.Render.rgb24;
|
|
115
|
+
_bindTarget() {
|
|
116
|
+
const target = this.window._backing || this.window;
|
|
117
|
+
if (this._target === target) return;
|
|
118
|
+
this._target = target;
|
|
31
119
|
|
|
32
|
-
|
|
120
|
+
if (!this._gc) {
|
|
121
|
+
// one GC is enough: it stays valid for any drawable of the same
|
|
122
|
+
// screen and depth (the backing pixmap matches the window depth)
|
|
123
|
+
this._gc = this.X.AllocID();
|
|
124
|
+
this.X.CreateGC(this._gc, target.id);
|
|
125
|
+
}
|
|
126
|
+
if (this.picture) this.picture.destroy();
|
|
127
|
+
// TODO: support a8
|
|
128
|
+
const depth = target.depth ?? this.window.depth;
|
|
129
|
+
const format = depth === 32 ? this.Render.rgba32 : this.Render.rgb24;
|
|
130
|
+
this.picture = new Picture(this.window.app, {
|
|
131
|
+
drawable: target,
|
|
132
|
+
format,
|
|
133
|
+
polyEdge: 1,
|
|
134
|
+
polyMode: 1
|
|
135
|
+
});
|
|
136
|
+
this._dropMasks();
|
|
137
|
+
}
|
|
33
138
|
|
|
139
|
+
_dropMasks() {
|
|
140
|
+
if (this.fillMask) {
|
|
141
|
+
this.fillMask.destroy();
|
|
142
|
+
this.fillMaskDrawable.destroy();
|
|
143
|
+
this.fillMask = this.fillMaskDrawable = null;
|
|
144
|
+
}
|
|
145
|
+
if (this.clipMask) {
|
|
146
|
+
this.clipMask.destroy();
|
|
147
|
+
this.clipMaskDrawable.destroy();
|
|
148
|
+
this.clipMask = this.clipMaskDrawable = null;
|
|
149
|
+
}
|
|
150
|
+
// clip paths are device-space and survive the resize; rebuild the mask
|
|
151
|
+
if (this._clips && this._clips.length) this._rebuildClipMask();
|
|
152
|
+
}
|
|
34
153
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
154
|
+
// notify a double-buffered window that its backing content changed
|
|
155
|
+
_markDirty() {
|
|
156
|
+
if (typeof this.window._markDirty === 'function') this.window._markDirty();
|
|
157
|
+
}
|
|
39
158
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
159
|
+
// html context2d compatibility: canvas.getContext('2d').canvas == canvas
|
|
160
|
+
get canvas() {
|
|
161
|
+
return this.window;
|
|
162
|
+
}
|
|
44
163
|
|
|
45
|
-
|
|
164
|
+
get width() {
|
|
165
|
+
return this.window.width;
|
|
166
|
+
}
|
|
46
167
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.X.PutImage(2, this.window.id, this.window._gc, data.width, data.height, x, y, 0, 24, data.data);
|
|
51
|
-
};
|
|
168
|
+
get height() {
|
|
169
|
+
return this.window.height;
|
|
170
|
+
}
|
|
52
171
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
var key = [r, g, b, a].join('|');
|
|
57
|
-
if (_solidPix[key]) {
|
|
58
|
-
return _solidPix[key];
|
|
59
|
-
}
|
|
60
|
-
// TODO: use window.createPixmap
|
|
61
|
-
var pixmap = new Pixmap(this.window.app, {depth: 32, width: 1, height: 1});
|
|
62
|
-
var pictSolidPix = this.window.X.AllocID();
|
|
63
|
-
this.Render.CreatePicture(pictSolidPix, pixmap.id, this.Render.rgba32, {repeat: 1});
|
|
64
|
-
this.Render.FillRectangles(1, pictSolidPix, [r, g, b, a], [0, 0, 100, 100]);
|
|
65
|
-
var p = new Picture(this.window.app, { id: pictSolidPix });
|
|
66
|
-
_solidPix[key] = p;
|
|
67
|
-
return p;
|
|
68
|
-
};
|
|
172
|
+
_op() {
|
|
173
|
+
return this.Render.PictOp[GCO_TO_PICTOP[this._gco] || 'Over'];
|
|
174
|
+
}
|
|
69
175
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
176
|
+
set globalCompositeOperation(value) {
|
|
177
|
+
if (value in GCO_TO_PICTOP) this._gco = value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get globalCompositeOperation() {
|
|
181
|
+
return this._gco;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
createSolidPicture(r, g, b, a) {
|
|
185
|
+
const key = [r, g, b, a].join('|');
|
|
186
|
+
let p = this._solidPictures.get(key);
|
|
187
|
+
if (p) return p;
|
|
188
|
+
|
|
189
|
+
// TODO: use window.createPixmap
|
|
190
|
+
const pixmap = new Pixmap(this.window.app, { depth: 32, width: 1, height: 1 });
|
|
191
|
+
const pictSolidPix = this.window.X.AllocID();
|
|
192
|
+
this.Render.CreatePicture(pictSolidPix, pixmap.id, this.Render.rgba32, { repeat: 1 });
|
|
193
|
+
this.Render.FillRectangles(1, pictSolidPix, [r, g, b, a], [0, 0, 100, 100]);
|
|
194
|
+
p = new Picture(this.window.app, { id: pictSolidPix });
|
|
195
|
+
p._sourcePixmap = pixmap; // keep the 1x1 pixmap alive alongside the picture
|
|
196
|
+
this._solidPictures.set(key, p);
|
|
197
|
+
return p;
|
|
198
|
+
}
|
|
73
199
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
RC.__defineSetter__('fillStyle', function(value) {
|
|
84
|
-
if (typeof value === 'string' || Array.isArray(value) ) {
|
|
85
|
-
var c = parseColor(value);
|
|
86
|
-
this._backgroundPicture = this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
87
|
-
return this._backgroundPicture;
|
|
88
|
-
} else if (value.constructor === Picture || value.constructor === CanvasGradient) {
|
|
89
|
-
this._backgroundPicture = value;
|
|
90
|
-
return this._backgroundPicture;
|
|
91
|
-
} else {
|
|
200
|
+
_stylePicture(value) {
|
|
201
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
202
|
+
const c = parseColor(value);
|
|
203
|
+
return this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
204
|
+
}
|
|
205
|
+
if (value instanceof Picture || value instanceof CanvasGradient) {
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
92
208
|
throw new Error('Unknown fill style');
|
|
93
209
|
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
RC.__defineSetter__('strokeStyle', function(value) {
|
|
97
|
-
if (typeof value === 'string' || Array.isArray(value) ) {
|
|
98
|
-
var c;
|
|
99
|
-
if (Array.isArray(value))
|
|
100
|
-
c = value;
|
|
101
|
-
else
|
|
102
|
-
c = parseColor(value);
|
|
103
|
-
this._strokePicture = this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
104
|
-
return this._strokePicture;
|
|
105
|
-
} else if (value.constructor === Picture || value.constructor === CanvasGradient) {
|
|
106
|
-
this._strokePicture = value;
|
|
107
|
-
return this._strokePicture;
|
|
108
|
-
} else {
|
|
109
|
-
throw new Error('Unknown stroke style');
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// html context2d compatibility: canvas.getContext('2d').canvas == canvas
|
|
115
|
-
RC.__defineGetter__('canvas', function(){
|
|
116
|
-
return this.window;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
RC.__defineGetter__('width', function(){
|
|
120
|
-
return this.window.width;
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
RC.__defineGetter__('height', function(){
|
|
124
|
-
return this.window.height;
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
RC.clearRect = function(x, y, w, h) {
|
|
128
|
-
this.Render.FillRectangles(this.Render.PictOp.Src, this.picture.id, [1, 1, 1, 1], [x, y, w, h])
|
|
129
|
-
};
|
|
130
210
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// ext.Composite = function(op, src, mask, dst, srcX, srcY, maskX, maskY, dstX, dstY, width, height)
|
|
136
|
-
this.Render.Composite(this.Render.PictOp.Over, this._backgroundPicture.id, mask, this.picture.id, x, y, x, y, x, y, w, h);
|
|
137
|
-
}
|
|
211
|
+
set fillStyle(value) {
|
|
212
|
+
this._fillStyle = value;
|
|
213
|
+
this._backgroundPicture = this._stylePicture(value);
|
|
214
|
+
}
|
|
138
215
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
216
|
+
get fillStyle() {
|
|
217
|
+
return this._fillStyle;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
set strokeStyle(value) {
|
|
221
|
+
this._strokeStyle = value;
|
|
222
|
+
this._strokePicture = this._stylePicture(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get strokeStyle() {
|
|
226
|
+
return this._strokeStyle;
|
|
227
|
+
}
|
|
148
228
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
229
|
+
// ------------------------------------------------------------------
|
|
230
|
+
// state
|
|
231
|
+
|
|
232
|
+
save() {
|
|
233
|
+
this._stack.push({
|
|
234
|
+
fillStyle: this._fillStyle,
|
|
235
|
+
strokeStyle: this._strokeStyle,
|
|
236
|
+
lineWidth: this.lineWidth,
|
|
237
|
+
lineCap: this.lineCap,
|
|
238
|
+
lineJoin: this.lineJoin,
|
|
239
|
+
miterLimit: this.miterLimit,
|
|
240
|
+
globalAlpha: this.globalAlpha,
|
|
241
|
+
gco: this._gco,
|
|
242
|
+
textStyle: this._textStyle,
|
|
243
|
+
fontString: this._lastFontString,
|
|
244
|
+
textAlign: this.textAlign,
|
|
245
|
+
textBaseline: this.textBaseline,
|
|
246
|
+
m: this._m.slice(),
|
|
247
|
+
clips: this._clips
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
restore() {
|
|
252
|
+
const s = this._stack.pop();
|
|
253
|
+
if (!s) return;
|
|
254
|
+
this.fillStyle = s.fillStyle;
|
|
255
|
+
this.strokeStyle = s.strokeStyle;
|
|
256
|
+
this.lineWidth = s.lineWidth;
|
|
257
|
+
this.lineCap = s.lineCap;
|
|
258
|
+
this.lineJoin = s.lineJoin;
|
|
259
|
+
this.miterLimit = s.miterLimit;
|
|
260
|
+
this.globalAlpha = s.globalAlpha;
|
|
261
|
+
this._gco = s.gco;
|
|
262
|
+
this._textStyle = s.textStyle;
|
|
263
|
+
this._lastFontString = s.fontString;
|
|
264
|
+
this.textAlign = s.textAlign;
|
|
265
|
+
this.textBaseline = s.textBaseline;
|
|
266
|
+
this._m = s.m;
|
|
267
|
+
if (s.clips !== this._clips) {
|
|
268
|
+
this._clips = s.clips;
|
|
269
|
+
this._rebuildClipMask();
|
|
164
270
|
}
|
|
165
|
-
|
|
166
|
-
if (g.height > h)
|
|
167
|
-
h = g.height;
|
|
168
|
-
}
|
|
169
|
-
//console.log('measureText', text, w, h)
|
|
170
|
-
return {
|
|
171
|
-
width: w,
|
|
172
|
-
height: h
|
|
173
|
-
};
|
|
174
|
-
}
|
|
271
|
+
}
|
|
175
272
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this._currentPath = [];
|
|
179
|
-
}
|
|
273
|
+
// ------------------------------------------------------------------
|
|
274
|
+
// transform
|
|
180
275
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this._currentPath.push(this._currentPoly)
|
|
185
|
-
this._currentPoly = [{x: x, y: y}];
|
|
186
|
-
}
|
|
276
|
+
translate(x, y) {
|
|
277
|
+
this._m = matMultiply(this._m, [1, 0, 0, 1, x, y]);
|
|
278
|
+
}
|
|
187
279
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (this._currentPoly) {
|
|
193
|
-
var pt = this._currentPoly[this._currentPoly.length - 1];
|
|
194
|
-
var d = Math.abs(pt.x - x) + Math.abs(pt.y - y);
|
|
195
|
-
if (d > 0.5)
|
|
196
|
-
this._currentPoly.push({x: x, y: y});
|
|
197
|
-
} else {
|
|
198
|
-
throw new Error('lineTo called before moveTo()');
|
|
280
|
+
rotate(angle) {
|
|
281
|
+
const c = Math.cos(angle);
|
|
282
|
+
const s = Math.sin(angle);
|
|
283
|
+
this._m = matMultiply(this._m, [c, s, -s, c, 0, 0]);
|
|
199
284
|
}
|
|
200
|
-
}
|
|
201
285
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
var maxX = Math.max(x, x0, x1, x2);
|
|
206
|
-
var minY = Math.min(y, y0, y1, y2);
|
|
207
|
-
var maxY = Math.max(y, y0, y1, y2);
|
|
208
|
-
var dt = 1/Math.max(maxX - minX, maxY - minY);
|
|
209
|
-
if (dt > 0.2)
|
|
210
|
-
dt = 0.2;
|
|
211
|
-
return dt;
|
|
212
|
-
}
|
|
286
|
+
scale(x, y = x) {
|
|
287
|
+
this._m = matMultiply(this._m, [x, 0, 0, y, 0, 0]);
|
|
288
|
+
}
|
|
213
289
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
var cubic = require('bezier').prepare(4);
|
|
217
|
-
var x = [lastPt.x, x1, x2, x];
|
|
218
|
-
var y = [lastPt.y, y1, y2, y];
|
|
219
|
-
var step = bcurveStep(x[0], y[0], x[1], y[1], x[2], y[2], x[3], y[3]);
|
|
220
|
-
for (var t = 0; t < 1; t += step) {
|
|
221
|
-
this.lineTo(cubic(x, t), cubic(y, t))
|
|
290
|
+
transform(a, b, c, d, e, f) {
|
|
291
|
+
this._m = matMultiply(this._m, [a, b, c, d, e, f]);
|
|
222
292
|
}
|
|
223
|
-
}
|
|
224
293
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
294
|
+
setTransform(a, b, c, d, e, f) {
|
|
295
|
+
if (typeof a === 'object' && a !== null) {
|
|
296
|
+
const m = a;
|
|
297
|
+
this._m = Array.isArray(m) ? m.slice(0, 6) : [m.a, m.b, m.c, m.d, m.e, m.f];
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (a === undefined) return this.resetTransform();
|
|
301
|
+
this._m = [a, b, c, d, e, f];
|
|
302
|
+
}
|
|
230
303
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
0, s, 0,
|
|
235
|
-
0, 0, 1
|
|
236
|
-
];
|
|
237
|
-
this.Render.SetPictureTransform(this.picture.id, m)
|
|
238
|
-
}
|
|
304
|
+
resetTransform() {
|
|
305
|
+
this._m = [1, 0, 0, 1, 0, 0];
|
|
306
|
+
}
|
|
239
307
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
308
|
+
getTransform() {
|
|
309
|
+
const [a, b, c, d, e, f] = this._m;
|
|
310
|
+
return { a, b, c, d, e, f };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ------------------------------------------------------------------
|
|
314
|
+
// path building (default path is recorded in device space, per spec)
|
|
315
|
+
|
|
316
|
+
beginPath() {
|
|
317
|
+
this._path = new Path2D();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
closePath() {
|
|
321
|
+
this._path.closePath();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
moveTo(x, y) {
|
|
325
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
326
|
+
this._path.moveTo(dx, dy);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
lineTo(x, y) {
|
|
330
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
331
|
+
this._path.lineTo(dx, dy);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
bezierCurveTo(x1, y1, x2, y2, x, y) {
|
|
335
|
+
const [dx1, dy1] = matApply(this._m, x1, y1);
|
|
336
|
+
const [dx2, dy2] = matApply(this._m, x2, y2);
|
|
337
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
338
|
+
this._path.bezierCurveTo(dx1, dy1, dx2, dy2, dx, dy);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
quadraticCurveTo(x1, y1, x, y) {
|
|
342
|
+
const [dx1, dy1] = matApply(this._m, x1, y1);
|
|
343
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
344
|
+
this._path.quadraticCurveTo(dx1, dy1, dx, dy);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// append user-space segments ({start, cmds}) through the current transform
|
|
348
|
+
_appendUserSegments({ start, cmds }) {
|
|
349
|
+
const [dsx, dsy] = matApply(this._m, start.x, start.y);
|
|
350
|
+
if (this._path._x === null) this._path.moveTo(dsx, dsy);
|
|
351
|
+
else this._path.lineTo(dsx, dsy);
|
|
352
|
+
this._path._append(transformCommands(cmds, this._m));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
arc(x, y, r, startAngle, endAngle, counterclockwise = false) {
|
|
356
|
+
if (r < 0) throw new RangeError('arc: negative radius');
|
|
357
|
+
this._appendUserSegments(ellipseSegments(x, y, r, r, 0, startAngle, endAngle, counterclockwise));
|
|
358
|
+
}
|
|
246
359
|
|
|
247
|
-
|
|
360
|
+
ellipse(x, y, rx, ry, rotation, startAngle, endAngle, counterclockwise = false) {
|
|
361
|
+
if (rx < 0 || ry < 0) throw new RangeError('ellipse: negative radius');
|
|
362
|
+
this._appendUserSegments(ellipseSegments(x, y, rx, ry, rotation, startAngle, endAngle, counterclockwise));
|
|
363
|
+
}
|
|
248
364
|
|
|
249
|
-
|
|
250
|
-
|
|
365
|
+
arcTo(x1, y1, x2, y2, r) {
|
|
366
|
+
if (this._path._x === null) return this.moveTo(x1, y1);
|
|
367
|
+
const inv = matInvert(this._m);
|
|
368
|
+
if (!inv) return;
|
|
369
|
+
const [ux, uy] = matApply(inv, this._path._x, this._path._y);
|
|
370
|
+
const tmp = new Path2D();
|
|
371
|
+
tmp.moveTo(ux, uy);
|
|
372
|
+
tmp.arcTo(x1, y1, x2, y2, r);
|
|
373
|
+
// drop the seed moveTo: the current point is already there
|
|
374
|
+
this._path._append(transformCommands(tmp._cmds.slice(1), this._m));
|
|
375
|
+
}
|
|
251
376
|
|
|
252
|
-
|
|
253
|
-
|
|
377
|
+
rect(x, y, w, h) {
|
|
378
|
+
const tmp = new Path2D();
|
|
379
|
+
tmp.rect(x, y, w, h);
|
|
380
|
+
this._path.addPath(tmp, this._m);
|
|
381
|
+
}
|
|
254
382
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
383
|
+
roundRect(x, y, w, h, radii) {
|
|
384
|
+
const tmp = new Path2D();
|
|
385
|
+
tmp.roundRect(x, y, w, h, radii);
|
|
386
|
+
this._path.addPath(tmp, this._m);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// args: ([path], [fillRule]) — returns device-space polys + rule
|
|
390
|
+
_polysFor(args) {
|
|
391
|
+
if (args[0] instanceof Path2D) {
|
|
392
|
+
return {
|
|
393
|
+
polys: flattenPath(args[0]._cmds, this._m),
|
|
394
|
+
rule: args[1] === 'evenodd' ? 'evenodd' : 'nonzero'
|
|
395
|
+
};
|
|
265
396
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
397
|
+
return {
|
|
398
|
+
polys: flattenPath(this._path._cmds, null),
|
|
399
|
+
rule: args[0] === 'evenodd' ? 'evenodd' : 'nonzero'
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ------------------------------------------------------------------
|
|
404
|
+
// rasterization plumbing
|
|
405
|
+
|
|
406
|
+
_ensureFillMask() {
|
|
407
|
+
if (this.fillMask) return;
|
|
408
|
+
this.fillMaskDrawable = new Pixmap(this.window.app, {
|
|
409
|
+
depth: 8,
|
|
410
|
+
width: this.width,
|
|
411
|
+
height: this.height
|
|
412
|
+
});
|
|
413
|
+
this.fillMask = new Picture(this.window.app, {
|
|
414
|
+
drawable: this.fillMaskDrawable,
|
|
415
|
+
format: this.Render.a8
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_ensureClipMask() {
|
|
420
|
+
if (this.clipMask) return;
|
|
421
|
+
this.clipMaskDrawable = new Pixmap(this.window.app, {
|
|
422
|
+
depth: 8,
|
|
423
|
+
width: this.width,
|
|
424
|
+
height: this.height
|
|
425
|
+
});
|
|
426
|
+
this.clipMask = new Picture(this.window.app, {
|
|
427
|
+
drawable: this.clipMaskDrawable,
|
|
428
|
+
format: this.Render.a8
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_rasterizePolys(picture, polys, rule) {
|
|
433
|
+
const flat = [];
|
|
434
|
+
for (const p of polys) if (p.pts.length >= 6) flat.push(p.pts);
|
|
435
|
+
if (!flat.length) return;
|
|
436
|
+
const traps = trapezoidize(flat, 0, 0, [], rule);
|
|
437
|
+
// stay under the server's maximum request size
|
|
438
|
+
const chunk = 4000 * 6;
|
|
439
|
+
for (let i = 0; i < traps.length; i += chunk) {
|
|
440
|
+
this.Render.AddTraps(picture.id, 0, 0, traps.slice(i, i + chunk));
|
|
269
441
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Core fill: rasterize device-space polys into the scratch a8 mask,
|
|
446
|
+
* scale by globalAlpha, intersect with the clip, composite the source.
|
|
447
|
+
*/
|
|
448
|
+
_fillPolys(polys, rule, { src = null, op = null, alpha = null } = {}) {
|
|
449
|
+
if (!polys.length) return;
|
|
450
|
+
src = src ?? this._backgroundPicture;
|
|
451
|
+
op = op ?? this._op();
|
|
452
|
+
alpha = alpha ?? this.globalAlpha;
|
|
453
|
+
if (alpha <= 0) return;
|
|
454
|
+
|
|
455
|
+
this._ensureFillMask();
|
|
456
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
457
|
+
this._rasterizePolys(this.fillMask, polys, rule);
|
|
458
|
+
|
|
459
|
+
if (alpha < 1) {
|
|
460
|
+
// In with a constant color scales the a8 coverage by that alpha
|
|
461
|
+
this.Render.FillRectangles(this.Render.PictOp.In, this.fillMask.id, [0, 0, 0, alpha], [0, 0, this.width, this.height]);
|
|
273
462
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
var myTriangulator = new PNLTRI.Triangulator();
|
|
285
|
-
var triangList = myTriangulator.triangulate_polygon( path );
|
|
286
|
-
var t = [];
|
|
287
|
-
triangList.forEach(function(tri) {
|
|
288
|
-
for(var i=0; i < 3; ++i) {
|
|
289
|
-
t.push(pts[tri[i]].x);
|
|
290
|
-
t.push(pts[tri[i]].y);
|
|
463
|
+
if (this.clipMask) {
|
|
464
|
+
this.Render.Composite(
|
|
465
|
+
this.Render.PictOp.In,
|
|
466
|
+
this.clipMask.id,
|
|
467
|
+
0,
|
|
468
|
+
this.fillMask.id,
|
|
469
|
+
0, 0, 0, 0, 0, 0,
|
|
470
|
+
this.width,
|
|
471
|
+
this.height
|
|
472
|
+
);
|
|
291
473
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
474
|
+
this.Render.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
475
|
+
this._markDirty();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
_strokePolys(polys, { src = null } = {}) {
|
|
479
|
+
src = src ?? this._strokePicture;
|
|
480
|
+
if (this.globalAlpha <= 0) return;
|
|
481
|
+
// approximate transform-aware line width by the average scale factor
|
|
482
|
+
const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
|
|
483
|
+
const thickness = this.lineWidth * (Math.sqrt(Math.abs(det)) || 1);
|
|
484
|
+
const stroke = extrudePolyline({
|
|
485
|
+
thickness,
|
|
486
|
+
cap: LINE_CAP[this.lineCap] || 'butt',
|
|
487
|
+
join: LINE_JOIN[this.lineJoin] || 'miter',
|
|
488
|
+
miterLimit: this.miterLimit
|
|
489
|
+
});
|
|
296
490
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
var mesh = stroke.build(polyline);
|
|
312
|
-
mesh.cells.forEach(function(tri) {
|
|
313
|
-
for(var i=0; i < 3; ++i) {
|
|
314
|
-
t.push(mesh.positions[tri[i]][0]);
|
|
315
|
-
t.push(mesh.positions[tri[i]][1]);
|
|
491
|
+
const tris = [];
|
|
492
|
+
for (const poly of polys) {
|
|
493
|
+
// drop consecutive (near-)duplicate points — zero-length segments make
|
|
494
|
+
// extrude-polyline emit NaN joins that turn into spikes at the origin
|
|
495
|
+
const pts = [];
|
|
496
|
+
let lx = Infinity;
|
|
497
|
+
let ly = Infinity;
|
|
498
|
+
for (let i = 0; i < poly.pts.length; i += 2) {
|
|
499
|
+
const x = poly.pts[i];
|
|
500
|
+
const y = poly.pts[i + 1];
|
|
501
|
+
if (!(Math.abs(x - lx) > 1e-6 || Math.abs(y - ly) > 1e-6)) continue;
|
|
502
|
+
pts.push([x, y]);
|
|
503
|
+
lx = x;
|
|
504
|
+
ly = y;
|
|
316
505
|
}
|
|
506
|
+
if (pts.length >= 2 && poly.closed) {
|
|
507
|
+
const [fx, fy] = pts[0];
|
|
508
|
+
if (Math.abs(fx - lx) > 1e-6 || Math.abs(fy - ly) > 1e-6) pts.push([fx, fy]);
|
|
509
|
+
}
|
|
510
|
+
if (pts.length < 2) continue;
|
|
511
|
+
const mesh = stroke.build(pts);
|
|
512
|
+
for (const tri of mesh.cells) {
|
|
513
|
+
for (let i = 0; i < 3; ++i) {
|
|
514
|
+
tris.push(mesh.positions[tri[i]][0], mesh.positions[tri[i]][1]);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (!tris.length) return;
|
|
519
|
+
|
|
520
|
+
const op = this._op();
|
|
521
|
+
const direct = this.globalAlpha >= 1 && !this.clipMask && op === this.Render.PictOp.Over;
|
|
522
|
+
const chunk = 4000 * 6;
|
|
523
|
+
if (direct) {
|
|
524
|
+
for (let i = 0; i < tris.length; i += chunk) {
|
|
525
|
+
this.Render.Triangles(op, src.id, 0, 0, this.picture.id, this.Render.a8, tris.slice(i, i + chunk));
|
|
526
|
+
}
|
|
527
|
+
this._markDirty();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// render coverage into the scratch mask, then composite through it so
|
|
532
|
+
// the stroke honors clip / globalAlpha / composite op
|
|
533
|
+
this._ensureFillMask();
|
|
534
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
535
|
+
const opaque = this.createSolidPicture(0, 0, 0, 1);
|
|
536
|
+
for (let i = 0; i < tris.length; i += chunk) {
|
|
537
|
+
this.Render.Triangles(this.Render.PictOp.Add, opaque.id, 0, 0, this.fillMask.id, this.Render.a8, tris.slice(i, i + chunk));
|
|
538
|
+
}
|
|
539
|
+
if (this.globalAlpha < 1) {
|
|
540
|
+
this.Render.FillRectangles(this.Render.PictOp.In, this.fillMask.id, [0, 0, 0, this.globalAlpha], [0, 0, this.width, this.height]);
|
|
541
|
+
}
|
|
542
|
+
if (this.clipMask) {
|
|
543
|
+
this.Render.Composite(this.Render.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
544
|
+
}
|
|
545
|
+
this.Render.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
546
|
+
this._markDirty();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// combined clip ∩ globalAlpha mask for direct composites (rect/image
|
|
550
|
+
// fast paths); returns a picture id or 0. Reuses the fill scratch mask.
|
|
551
|
+
_compositeMask() {
|
|
552
|
+
if (this.globalAlpha >= 1) return this.clipMask ? this.clipMask.id : 0;
|
|
553
|
+
this._ensureFillMask();
|
|
554
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, this.globalAlpha], [0, 0, this.width, this.height]);
|
|
555
|
+
if (this.clipMask) {
|
|
556
|
+
this.Render.Composite(this.Render.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
557
|
+
}
|
|
558
|
+
return this.fillMask.id;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ------------------------------------------------------------------
|
|
562
|
+
// drawing
|
|
563
|
+
|
|
564
|
+
clearRect(x, y, w, h) {
|
|
565
|
+
if (matIsIdentity(this._m) && !this.clipMask) {
|
|
566
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.picture.id, [1, 1, 1, 1], [x, y, w, h]);
|
|
567
|
+
this._markDirty();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const tmp = new Path2D();
|
|
571
|
+
tmp.rect(x, y, w, h);
|
|
572
|
+
// clear = opaque white, ignoring alpha and the composite op (but
|
|
573
|
+
// honoring the clip), matching the identity fast path above
|
|
574
|
+
this._fillPolys(flattenPath(tmp._cmds, this._m), 'nonzero', {
|
|
575
|
+
src: this.createSolidPicture(1, 1, 1, 1),
|
|
576
|
+
op: this.Render.PictOp.Over,
|
|
577
|
+
alpha: 1
|
|
317
578
|
});
|
|
318
579
|
}
|
|
319
|
-
// TODO: mask
|
|
320
|
-
//this.Render.Triangles(this.Render.PictOp.Over, this._strokePicture.id, 0, 0, this.picture.id, 0, t);
|
|
321
|
-
this.Render.Triangles(this.Render.PictOp.Over, this._strokePicture.id, 0, 0, this.picture.id, this.Render.a8, t);
|
|
322
|
-
}
|
|
323
580
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
581
|
+
fillRect(x, y, w, h) {
|
|
582
|
+
if (matIsIdentity(this._m)) {
|
|
583
|
+
const mask = this._compositeMask();
|
|
584
|
+
this.Render.Composite(
|
|
585
|
+
this._op(),
|
|
586
|
+
this._backgroundPicture.id,
|
|
587
|
+
mask,
|
|
588
|
+
this.picture.id,
|
|
589
|
+
x, y,
|
|
590
|
+
x, y,
|
|
591
|
+
x, y,
|
|
592
|
+
w, h
|
|
593
|
+
);
|
|
594
|
+
this._markDirty();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const tmp = new Path2D();
|
|
598
|
+
tmp.rect(x, y, w, h);
|
|
599
|
+
this._fillPolys(flattenPath(tmp._cmds, this._m), 'nonzero');
|
|
328
600
|
}
|
|
329
|
-
this.Render.FillRectangles(1, this.clipMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
330
|
-
this._rasterizePath(this.clipMask);
|
|
331
|
-
}
|
|
332
601
|
|
|
333
|
-
|
|
602
|
+
strokeRect(x, y, w, h) {
|
|
603
|
+
const tmp = new Path2D();
|
|
604
|
+
tmp.rect(x, y, w, h);
|
|
605
|
+
this._strokePolys(flattenPath(tmp._cmds, this._m));
|
|
606
|
+
}
|
|
334
607
|
|
|
335
|
-
|
|
608
|
+
fill(...args) {
|
|
609
|
+
const { polys, rule } = this._polysFor(args);
|
|
610
|
+
this._fillPolys(polys, rule);
|
|
611
|
+
}
|
|
336
612
|
|
|
337
|
-
|
|
613
|
+
stroke(...args) {
|
|
614
|
+
const polys =
|
|
615
|
+
args[0] instanceof Path2D
|
|
616
|
+
? flattenPath(args[0]._cmds, this._m)
|
|
617
|
+
: flattenPath(this._path._cmds, null);
|
|
618
|
+
this._strokePolys(polys);
|
|
619
|
+
}
|
|
338
620
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Intersect the clip region with the given path (or the current path).
|
|
623
|
+
* Cleared by restore() to the state at the matching save() — like the
|
|
624
|
+
* browser canvas, there is no other way to widen the clip again.
|
|
625
|
+
*/
|
|
626
|
+
clip(...args) {
|
|
627
|
+
const { polys, rule } = this._polysFor(args);
|
|
628
|
+
const entry = { polys, rule };
|
|
629
|
+
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
630
|
+
this._clips = this._clips.concat([entry]);
|
|
631
|
+
|
|
632
|
+
if (this._clips.length === 1) {
|
|
633
|
+
this._ensureClipMask();
|
|
634
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
635
|
+
this._rasterizePolys(this.clipMask, polys, rule);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this._intersectClip(entry);
|
|
342
639
|
}
|
|
343
|
-
// TODO: apply global alpha here?
|
|
344
|
-
this.Render.FillRectangles(1, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
345
|
-
this._rasterizePath(this.fillMask);
|
|
346
640
|
|
|
347
|
-
//
|
|
641
|
+
// rasterize one clip entry into a temp a8 and In-composite it onto the mask
|
|
642
|
+
_intersectClip(entry) {
|
|
643
|
+
const tmpPixmap = new Pixmap(this.window.app, { depth: 8, width: this.width, height: this.height });
|
|
644
|
+
const tmpMask = new Picture(this.window.app, { drawable: tmpPixmap, format: this.Render.a8 });
|
|
645
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, tmpMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
646
|
+
this._rasterizePolys(tmpMask, entry.polys, entry.rule);
|
|
647
|
+
this.Render.Composite(this.Render.PictOp.In, tmpMask.id, 0, this.clipMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
648
|
+
tmpMask.destroy();
|
|
649
|
+
tmpPixmap.destroy();
|
|
650
|
+
}
|
|
348
651
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
652
|
+
_rebuildClipMask() {
|
|
653
|
+
if (!this._clips.length) {
|
|
654
|
+
if (this.clipMask) {
|
|
655
|
+
this.clipMask.destroy();
|
|
656
|
+
this.clipMaskDrawable.destroy();
|
|
657
|
+
this.clipMask = this.clipMaskDrawable = null;
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this._ensureClipMask();
|
|
662
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
663
|
+
this._rasterizePolys(this.clipMask, this._clips[0].polys, this._clips[0].rule);
|
|
664
|
+
for (let i = 1; i < this._clips.length; i++) this._intersectClip(this._clips[i]);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
isPointInPath(...args) {
|
|
668
|
+
let path = null;
|
|
669
|
+
let rest = args;
|
|
670
|
+
if (args[0] instanceof Path2D) {
|
|
671
|
+
path = args[0];
|
|
672
|
+
rest = args.slice(1);
|
|
673
|
+
}
|
|
674
|
+
const [x, y, rule] = rest;
|
|
675
|
+
const polys = path ? flattenPath(path._cmds, this._m) : flattenPath(this._path._cmds, null);
|
|
676
|
+
return polysContain(polys, x, y, rule === 'evenodd' ? 'evenodd' : 'nonzero');
|
|
677
|
+
}
|
|
355
678
|
|
|
356
|
-
|
|
357
|
-
|
|
679
|
+
// ------------------------------------------------------------------
|
|
680
|
+
// text
|
|
358
681
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
};
|
|
682
|
+
_resolvedTextStyle() {
|
|
683
|
+
if (!this._textStyle) this.font = DEFAULT_FONT;
|
|
684
|
+
return this._textStyle;
|
|
685
|
+
}
|
|
364
686
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
687
|
+
// horizontal offset of the alignment point, given the shaped width
|
|
688
|
+
_alignOffset(shaped) {
|
|
689
|
+
let align = this.textAlign;
|
|
690
|
+
if (align === 'start') align = shaped.baseLevel & 1 ? 'right' : 'left';
|
|
691
|
+
if (align === 'end') align = shaped.baseLevel & 1 ? 'left' : 'right';
|
|
692
|
+
if (align === 'center') return -shaped.width / 2;
|
|
693
|
+
if (align === 'right') return -shaped.width;
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
369
696
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
697
|
+
// vertical distance from the requested y to the baseline
|
|
698
|
+
_baselineOffset(metrics) {
|
|
699
|
+
switch (this.textBaseline) {
|
|
700
|
+
case 'top':
|
|
701
|
+
return metrics.ascent;
|
|
702
|
+
case 'hanging':
|
|
703
|
+
return metrics.ascent * 0.8;
|
|
704
|
+
case 'middle':
|
|
705
|
+
return (metrics.ascent - metrics.descent) / 2;
|
|
706
|
+
case 'bottom':
|
|
707
|
+
case 'ideographic':
|
|
708
|
+
return -metrics.descent;
|
|
709
|
+
default:
|
|
710
|
+
// 'alphabetic'
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
386
714
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
715
|
+
/**
|
|
716
|
+
* Draw text with full shaping: OpenType kerning/ligatures, complex-script
|
|
717
|
+
* contextual forms, bidi reordering and automatic font fallback.
|
|
718
|
+
* Glyphs are uploaded to the server once per (face, size) and referenced
|
|
719
|
+
* by 1–2 byte ids afterwards — see docs/text.md.
|
|
720
|
+
*
|
|
721
|
+
* The current transform's translation applies to the anchor point;
|
|
722
|
+
* glyphs themselves are not rotated/scaled by the transform (size the
|
|
723
|
+
* font via `ctx.font` instead).
|
|
724
|
+
*/
|
|
725
|
+
fillText(text, x, y) {
|
|
726
|
+
text = String(text ?? '');
|
|
727
|
+
if (!text) return;
|
|
728
|
+
const style = this._resolvedTextStyle();
|
|
729
|
+
const app = this.window.app;
|
|
730
|
+
const shaped = app.fonts.shape(text, style);
|
|
731
|
+
const [tx, ty] = matApply(this._m, x, y);
|
|
732
|
+
const ox = tx + this._alignOffset(shaped);
|
|
733
|
+
const oy = ty + this._baselineOffset(style.font.metrics(style.size));
|
|
734
|
+
|
|
735
|
+
const positioned = [];
|
|
736
|
+
let cursor = ox;
|
|
737
|
+
for (const run of reorderRuns(shaped.runs)) {
|
|
738
|
+
positioned.push({ run, x: cursor, y: oy });
|
|
739
|
+
cursor += run.width;
|
|
740
|
+
}
|
|
741
|
+
drawGlyphRuns(app, this.Render.PictOp.Over, this._backgroundPicture.id, this.picture.id, positioned);
|
|
742
|
+
this._markDirty();
|
|
743
|
+
}
|
|
392
744
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
745
|
+
/**
|
|
746
|
+
* Measure shaped text. Returns a canvas-style TextMetrics object:
|
|
747
|
+
* `width` (advance), actual bounding box (ink extents relative to the
|
|
748
|
+
* origin), and font bounding box (from font metrics).
|
|
749
|
+
*/
|
|
750
|
+
measureText(text) {
|
|
751
|
+
const style = this._resolvedTextStyle();
|
|
752
|
+
const shaped = this.window.app.fonts.shape(String(text ?? ''), style);
|
|
753
|
+
let minX = 0;
|
|
754
|
+
let maxX = 0;
|
|
755
|
+
let minY = 0;
|
|
756
|
+
let maxY = 0;
|
|
757
|
+
let cursor = 0;
|
|
758
|
+
for (const run of reorderRuns(shaped.runs)) {
|
|
759
|
+
for (const g of run.glyphs) {
|
|
760
|
+
const e = run.font.glyphExtents(g.id, run.size);
|
|
761
|
+
const gx = cursor + g.dx;
|
|
762
|
+
const gy = -g.dy;
|
|
763
|
+
if (gx + e.minX < minX) minX = gx + e.minX;
|
|
764
|
+
if (gx + e.maxX > maxX) maxX = gx + e.maxX;
|
|
765
|
+
if (gy + e.minY < minY) minY = gy + e.minY;
|
|
766
|
+
if (gy + e.maxY > maxY) maxY = gy + e.maxY;
|
|
767
|
+
cursor += g.ax;
|
|
405
768
|
}
|
|
406
|
-
this._setFont(
|
|
407
|
-
font.weight
|
|
408
|
-
, font.style
|
|
409
|
-
, font.size
|
|
410
|
-
, font.unit
|
|
411
|
-
, font.family);
|
|
412
769
|
}
|
|
770
|
+
const m = style.font.metrics(style.size);
|
|
771
|
+
return {
|
|
772
|
+
width: shaped.width,
|
|
773
|
+
actualBoundingBoxLeft: -minX,
|
|
774
|
+
actualBoundingBoxRight: maxX,
|
|
775
|
+
actualBoundingBoxAscent: -minY,
|
|
776
|
+
actualBoundingBoxDescent: maxY,
|
|
777
|
+
fontBoundingBoxAscent: m.ascent,
|
|
778
|
+
fontBoundingBoxDescent: m.descent,
|
|
779
|
+
emHeightAscent: m.ascent,
|
|
780
|
+
emHeightDescent: m.descent,
|
|
781
|
+
// legacy ntk field: ink height
|
|
782
|
+
height: maxY - minY
|
|
783
|
+
};
|
|
413
784
|
}
|
|
414
|
-
});
|
|
415
785
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
786
|
+
/**
|
|
787
|
+
* Lay out (possibly styled) text for a target width without drawing it —
|
|
788
|
+
* returns a TextLayout (lines, metrics) that can be inspected and then
|
|
789
|
+
* drawn with `layout.draw(ctx, x, y)`. The current `ctx.font` is the base
|
|
790
|
+
* style; see docs/text.md for spans and options.
|
|
791
|
+
*
|
|
792
|
+
* @param {string|Array} content plain text or spans [{ text, ...style }]
|
|
793
|
+
* @param {object} [options] { maxWidth, align, lineHeight, direction }
|
|
794
|
+
*/
|
|
795
|
+
layoutText(content, options = {}) {
|
|
796
|
+
const style = this._resolvedTextStyle();
|
|
797
|
+
return new TextLayout(this.window.app.fonts, content, style, options);
|
|
798
|
+
}
|
|
423
799
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
800
|
+
/**
|
|
801
|
+
* CSS-style font shorthand, e.g. `'bold italic 40px "DejaVu Sans", serif'`.
|
|
802
|
+
* Resolution goes through `app.fonts`: fonts registered with
|
|
803
|
+
* `app.fonts.load()` win over system (fontconfig) lookup.
|
|
804
|
+
*/
|
|
805
|
+
set font(val) {
|
|
806
|
+
if (!val || typeof val !== 'string') return;
|
|
807
|
+
const parsed = parseFontStyle(val);
|
|
808
|
+
if (!parsed) return;
|
|
809
|
+
const style = {
|
|
810
|
+
family: parsed.family,
|
|
811
|
+
weight: parsed.weight,
|
|
812
|
+
style: parsed.style,
|
|
813
|
+
size: parsed.size
|
|
814
|
+
};
|
|
815
|
+
style.font = this.window.app.fonts.match(style.family, style);
|
|
816
|
+
this._lastFontString = val;
|
|
817
|
+
this._textStyle = style;
|
|
818
|
+
}
|
|
427
819
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function CanvasGradient(type, ctx, p0, p1, p2, p3, p4, p5) {
|
|
433
|
-
this.type = type;
|
|
434
|
-
this.stops = [];
|
|
435
|
-
this.ctx = ctx;
|
|
436
|
-
this._id = null;
|
|
437
|
-
|
|
438
|
-
this.x0 = p0;
|
|
439
|
-
this.y0 = p1;
|
|
440
|
-
if (type == 'linear' || type == 'radial') {
|
|
441
|
-
this.x1 = p2;
|
|
442
|
-
this.y1 = p3;
|
|
443
|
-
this.r0 = p4;
|
|
444
|
-
this.r1 = p5;
|
|
445
|
-
} else {
|
|
446
|
-
this.angle = p2;
|
|
447
|
-
}
|
|
448
|
-
// TODO: check if ChangePictureAttrubute works on gradients ( set repeat edge pixels flag )
|
|
449
|
-
};
|
|
820
|
+
get font() {
|
|
821
|
+
return this._lastFontString || DEFAULT_FONT;
|
|
822
|
+
}
|
|
450
823
|
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
if (this._id !== null)
|
|
454
|
-
return this._id;
|
|
824
|
+
// ------------------------------------------------------------------
|
|
825
|
+
// gradients / images
|
|
455
826
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
//var key = [this.x0, this.y0, this.x1, this.y1, this.r0, this.r1, this.angle, this.stops].join(',');
|
|
460
|
-
//if (gradientsCache[key])
|
|
461
|
-
// return gradientsCache[key].id;
|
|
462
|
-
|
|
463
|
-
this._id = X.AllocID();
|
|
464
|
-
switch (this.type) {
|
|
465
|
-
case 'linear':
|
|
466
|
-
Render.LinearGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.stops);
|
|
467
|
-
break;
|
|
468
|
-
case 'radial':
|
|
469
|
-
Render.RadialGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.r0, this.r1, this.stops);
|
|
470
|
-
break;
|
|
471
|
-
case 'conical':
|
|
472
|
-
Render.ConicalGradient(this._id, [this.x0, this.y0], this.angle, this.stops);
|
|
473
|
-
break;
|
|
474
|
-
default:
|
|
475
|
-
throw new Error('unknown gradient type');
|
|
476
|
-
}
|
|
477
|
-
// wrap with picture so FreePicture is invoked on gc via weakref
|
|
478
|
-
this._picture = new Picture(this.ctx.window.app, { id: this._id });
|
|
479
|
-
//gradientsCache[key] = this;
|
|
480
|
-
return this._id;
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
function parseColor(str) {
|
|
484
|
-
if (Array.isArray(str))
|
|
485
|
-
return str;
|
|
486
|
-
var parseColor_ = require('parse-color');
|
|
487
|
-
var c = parseColor_(str);
|
|
488
|
-
return [c.rgba[0]/255, c.rgba[1]/255, c.rgba[2]/255, c.rgba[3]];
|
|
489
|
-
}
|
|
827
|
+
createLinearGradient(x0, y0, x1, y1) {
|
|
828
|
+
return new CanvasGradient('linear', this, x0, y0, x1, y1);
|
|
829
|
+
}
|
|
490
830
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return this;
|
|
495
|
-
}
|
|
831
|
+
createRadialGradient(x0, y0, r0, x1, y1, r1) {
|
|
832
|
+
return new CanvasGradient('radial', this, x0, y0, x1, y1, r0, r1);
|
|
833
|
+
}
|
|
496
834
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
835
|
+
createConicalGradient(x0, y0, angle) {
|
|
836
|
+
return new CanvasGradient('conical', this, x0, y0, angle);
|
|
837
|
+
}
|
|
500
838
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
839
|
+
createImageData(w, h) {
|
|
840
|
+
return {
|
|
841
|
+
width: w,
|
|
842
|
+
height: h,
|
|
843
|
+
data: Buffer.alloc(w * h * 4)
|
|
844
|
+
};
|
|
845
|
+
}
|
|
504
846
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
847
|
+
// TODO: remove, add drawImage
|
|
848
|
+
putImageData(data, x, y) {
|
|
849
|
+
// todo: use constants
|
|
850
|
+
this.X.PutImage(2, this._target.id, this._gc, data.width, data.height, x, y, 0, 24, data.data);
|
|
851
|
+
this._markDirty();
|
|
852
|
+
}
|
|
508
853
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
};
|
|
515
|
-
};
|
|
854
|
+
getImageData(x, y, w, h, cb) {
|
|
855
|
+
// reads the backing pixmap on double-buffered windows: always valid,
|
|
856
|
+
// even where the window is occluded on screen
|
|
857
|
+
this.X.GetImage(2, this._target.id, x, y, w, h, 0xffffffff, cb);
|
|
858
|
+
}
|
|
516
859
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
860
|
+
/**
|
|
861
|
+
* Draw an image, with the three canvas signatures:
|
|
862
|
+
*
|
|
863
|
+
* ctx.drawImage(image, dx, dy)
|
|
864
|
+
* ctx.drawImage(image, dx, dy, dWidth, dHeight)
|
|
865
|
+
* ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
|
|
866
|
+
*
|
|
867
|
+
* `image` is an ntk `Image` (see `loadImage()` — PNG/JPEG decoded
|
|
868
|
+
* client-side, uploaded to the server once and composited from there;
|
|
869
|
+
* scaling is server-side with bilinear filtering), another
|
|
870
|
+
* RenderingContext2d, or a node-canvas-like object. Images honor the
|
|
871
|
+
* current transform (server-side, via the picture transform).
|
|
872
|
+
*/
|
|
873
|
+
drawImage(image, ...args) {
|
|
874
|
+
if (image instanceof Image) {
|
|
875
|
+
let sx = 0;
|
|
876
|
+
let sy = 0;
|
|
877
|
+
let sw = image.width;
|
|
878
|
+
let sh = image.height;
|
|
879
|
+
let dx = 0;
|
|
880
|
+
let dy = 0;
|
|
881
|
+
let dw;
|
|
882
|
+
let dh;
|
|
883
|
+
if (args.length >= 8) {
|
|
884
|
+
[sx, sy, sw, sh, dx, dy, dw, dh] = args;
|
|
885
|
+
} else if (args.length >= 4) {
|
|
886
|
+
[dx, dy, dw, dh] = args;
|
|
887
|
+
} else {
|
|
888
|
+
[dx = 0, dy = 0] = args;
|
|
889
|
+
dw = sw;
|
|
890
|
+
dh = sh;
|
|
891
|
+
}
|
|
892
|
+
if (!(dw > 0) || !(dh > 0) || !(sw > 0) || !(sh > 0)) return;
|
|
893
|
+
|
|
894
|
+
const picture = image.picture(this.window.app);
|
|
895
|
+
const op = this._op();
|
|
526
896
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
897
|
+
if (!matIsIdentity(this._m)) {
|
|
898
|
+
this._drawImageTransformed(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const mask = this._compositeMask();
|
|
903
|
+
const scaled = dw !== sw || dh !== sh;
|
|
904
|
+
if (scaled) {
|
|
905
|
+
// the picture transform maps composite coordinates into source
|
|
906
|
+
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh)
|
|
907
|
+
this.Render.SetPictureTransform(picture.id, [sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1]);
|
|
908
|
+
picture.setFilter('bilinear');
|
|
909
|
+
this.Render.Composite(
|
|
910
|
+
op,
|
|
911
|
+
picture.id,
|
|
912
|
+
mask,
|
|
913
|
+
this.picture.id,
|
|
914
|
+
0, 0,
|
|
915
|
+
dx, dy,
|
|
916
|
+
dx, dy,
|
|
917
|
+
dw, dh
|
|
918
|
+
);
|
|
919
|
+
// restore defaults so the cached upload can be reused as-is
|
|
920
|
+
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
921
|
+
picture.setFilter('nearest');
|
|
922
|
+
} else {
|
|
923
|
+
this.Render.Composite(
|
|
924
|
+
op,
|
|
925
|
+
picture.id,
|
|
926
|
+
mask,
|
|
927
|
+
this.picture.id,
|
|
928
|
+
sx, sy,
|
|
929
|
+
dx, dy,
|
|
930
|
+
dx, dy,
|
|
931
|
+
dw, dh
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
this._markDirty();
|
|
935
|
+
return;
|
|
556
936
|
}
|
|
557
937
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
938
|
+
const sWidth = image.width;
|
|
939
|
+
const sHeight = image.height;
|
|
940
|
+
const sx = 0;
|
|
941
|
+
const sy = 0;
|
|
942
|
+
|
|
943
|
+
// TODO: allow to draw Window, Pixmap, Picture
|
|
944
|
+
if (image instanceof RenderingContext2d) {
|
|
945
|
+
// TODO: if need to scale, set transform
|
|
946
|
+
// TODO: respect global compositing blend mode
|
|
947
|
+
this.Render.Composite(
|
|
948
|
+
this.Render.PictOp.Over,
|
|
949
|
+
image.picture.id,
|
|
950
|
+
this._compositeMask(),
|
|
951
|
+
this.picture.id,
|
|
952
|
+
0, 0, 0, 0, 0, 0,
|
|
953
|
+
this.width,
|
|
954
|
+
this.height
|
|
955
|
+
);
|
|
956
|
+
this._markDirty();
|
|
957
|
+
} else if (image && image.context && typeof image.context.getImageData === 'function') {
|
|
958
|
+
// node-canvas Canvas ( or any ctx with ctx.canvas.getImageData returning pixels)
|
|
959
|
+
const imageData = image.context.getImageData(sx, sy, sWidth, sHeight);
|
|
960
|
+
// rgba -> bgra
|
|
961
|
+
const data = Buffer.alloc(imageData.data.length);
|
|
962
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
963
|
+
data[i + 2] = imageData.data[i];
|
|
964
|
+
data[i + 1] = imageData.data[i + 1];
|
|
965
|
+
data[i] = imageData.data[i + 2];
|
|
966
|
+
data[i + 3] = imageData.data[i + 3]; // multiply source alpha by context globalAlpha?
|
|
967
|
+
}
|
|
573
968
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
969
|
+
const pixmap = new Pixmap(this.window.app, { depth: 32, width: sWidth, height: sHeight });
|
|
970
|
+
const picture = new Picture(this.window.app, { drawable: pixmap, format: this.Render.rgba32 });
|
|
971
|
+
pixmap._gc = this.X.AllocID();
|
|
972
|
+
this.X.CreateGC(pixmap._gc, pixmap.id);
|
|
973
|
+
this.X.PutImage(2, pixmap.id, pixmap._gc, sWidth, sHeight, sx, sy, 0, 32, data);
|
|
974
|
+
|
|
975
|
+
this.Render.Composite(
|
|
976
|
+
this.Render.PictOp.Over,
|
|
977
|
+
picture.id,
|
|
978
|
+
this._compositeMask(),
|
|
979
|
+
this.picture.id,
|
|
980
|
+
0, 0, 0, 0, 0, 0,
|
|
981
|
+
this.width,
|
|
982
|
+
this.height
|
|
983
|
+
);
|
|
984
|
+
this._markDirty();
|
|
985
|
+
}
|
|
580
986
|
}
|
|
581
|
-
};
|
|
582
987
|
|
|
583
|
-
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
988
|
+
// general-affine drawImage: fold CTM + dest rect + source offset into the
|
|
989
|
+
// source picture transform and composite over the transformed bbox
|
|
990
|
+
_drawImageTransformed(picture, sx, sy, sw, sh, dx, dy, dw, dh, op) {
|
|
991
|
+
// maps source-local (u, v) in [0..sw)x[0..sh) to device coordinates
|
|
992
|
+
const M = matMultiply(this._m, [dw / sw, 0, 0, dh / sh, dx, dy]);
|
|
993
|
+
const inv = matInvert(M);
|
|
994
|
+
if (!inv) return;
|
|
995
|
+
|
|
996
|
+
// device bbox of the transformed dest rect, clamped to the canvas
|
|
997
|
+
const corners = [
|
|
998
|
+
matApply(this._m, dx, dy),
|
|
999
|
+
matApply(this._m, dx + dw, dy),
|
|
1000
|
+
matApply(this._m, dx, dy + dh),
|
|
1001
|
+
matApply(this._m, dx + dw, dy + dh)
|
|
1002
|
+
];
|
|
1003
|
+
const x0 = Math.max(0, Math.floor(Math.min(...corners.map((c) => c[0]))));
|
|
1004
|
+
const y0 = Math.max(0, Math.floor(Math.min(...corners.map((c) => c[1]))));
|
|
1005
|
+
const x1 = Math.min(this.width, Math.ceil(Math.max(...corners.map((c) => c[0]))));
|
|
1006
|
+
const y1 = Math.min(this.height, Math.ceil(Math.max(...corners.map((c) => c[1]))));
|
|
1007
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
1008
|
+
|
|
1009
|
+
// device -> source sample (adding the source-rect origin)
|
|
1010
|
+
const srcM = [inv[0], inv[1], inv[2], inv[3], inv[4] + sx, inv[5] + sy];
|
|
1011
|
+
this.Render.SetPictureTransform(picture.id, [srcM[0], srcM[2], srcM[4], srcM[1], srcM[3], srcM[5], 0, 0, 1]);
|
|
1012
|
+
picture.setFilter('bilinear');
|
|
1013
|
+
const mask = this._compositeMask();
|
|
1014
|
+
this.Render.Composite(op, picture.id, mask, this.picture.id, x0, y0, x0, y0, x0, y0, x1 - x0, y1 - y0);
|
|
1015
|
+
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
1016
|
+
picture.setFilter('nearest');
|
|
1017
|
+
this._markDirty();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
587
1020
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1021
|
+
class CanvasGradient {
|
|
1022
|
+
constructor(type, ctx, p0, p1, p2, p3, p4, p5) {
|
|
1023
|
+
this.type = type;
|
|
1024
|
+
this.stops = [];
|
|
1025
|
+
this.ctx = ctx;
|
|
1026
|
+
this._id = null;
|
|
1027
|
+
this._picture = null;
|
|
1028
|
+
|
|
1029
|
+
this.x0 = p0;
|
|
1030
|
+
this.y0 = p1;
|
|
1031
|
+
if (type === 'linear' || type === 'radial') {
|
|
1032
|
+
this.x1 = p2;
|
|
1033
|
+
this.y1 = p3;
|
|
1034
|
+
this.r0 = p4;
|
|
1035
|
+
this.r1 = p5;
|
|
1036
|
+
} else {
|
|
1037
|
+
this.angle = p2;
|
|
1038
|
+
}
|
|
1039
|
+
// TODO: check if ChangePictureAttribute works on gradients ( set repeat edge pixels flag )
|
|
1040
|
+
}
|
|
591
1041
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1042
|
+
addColorStop(offset, color) {
|
|
1043
|
+
this.stops.push([offset, parseColor(color)]);
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
595
1046
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1047
|
+
// gradient pictures are created lazily, on first use as a fill/stroke style
|
|
1048
|
+
get id() {
|
|
1049
|
+
if (this._id !== null) return this._id;
|
|
1050
|
+
|
|
1051
|
+
const Render = this.ctx.Render;
|
|
1052
|
+
const X = this.ctx.X;
|
|
1053
|
+
|
|
1054
|
+
this._id = X.AllocID();
|
|
1055
|
+
switch (this.type) {
|
|
1056
|
+
case 'linear':
|
|
1057
|
+
Render.LinearGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.stops);
|
|
1058
|
+
break;
|
|
1059
|
+
case 'radial':
|
|
1060
|
+
Render.RadialGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.r0, this.r1, this.stops);
|
|
1061
|
+
break;
|
|
1062
|
+
case 'conical':
|
|
1063
|
+
Render.ConicalGradient(this._id, [this.x0, this.y0], this.angle, this.stops);
|
|
1064
|
+
break;
|
|
1065
|
+
default:
|
|
1066
|
+
throw new Error('unknown gradient type');
|
|
1067
|
+
}
|
|
1068
|
+
// wrap with picture so FreePicture is invoked on gc via FinalizationRegistry
|
|
1069
|
+
this._picture = new Picture(this.ctx.window.app, { id: this._id });
|
|
1070
|
+
return this._id;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
599
1073
|
|
|
600
1074
|
// register context
|
|
601
|
-
|
|
1075
|
+
Drawable.renderingContextFactory['2d'] = (window) => new RenderingContext2d(window);
|
|
1076
|
+
|
|
1077
|
+
export default RenderingContext2d;
|
|
1078
|
+
export { CanvasGradient };
|