@gjsify/canvas2d-core 0.3.21 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/cairo-types.js +1 -0
- package/lib/esm/cairo-utils.js +1 -1
- package/lib/esm/canvas-gradient.js +1 -1
- package/lib/esm/canvas-path.js +1 -1
- package/lib/esm/canvas-pattern.js +1 -1
- package/lib/esm/canvas-rendering-context-2d.js +1 -1
- package/lib/esm/canvas-state.js +1 -1
- package/lib/esm/color.js +1 -1
- package/lib/esm/dom-types.js +1 -0
- package/lib/esm/image-data.js +1 -1
- package/lib/types/cairo-types.d.ts +20 -0
- package/lib/types/canvas-pattern.d.ts +1 -1
- package/lib/types/canvas-rendering-context-2d.d.ts +18 -6
- package/lib/types/dom-types.d.ts +88 -0
- package/package.json +50 -46
- package/src/cairo-utils.ts +0 -254
- package/src/canvas-clearing.spec.ts +0 -126
- package/src/canvas-color.spec.ts +0 -113
- package/src/canvas-composite.spec.ts +0 -114
- package/src/canvas-drawimage.spec.ts +0 -334
- package/src/canvas-gradient.ts +0 -36
- package/src/canvas-imagedata.spec.ts +0 -150
- package/src/canvas-path.ts +0 -131
- package/src/canvas-pattern.ts +0 -75
- package/src/canvas-rendering-context-2d.ts +0 -1187
- package/src/canvas-state.spec.ts +0 -245
- package/src/canvas-state.ts +0 -77
- package/src/canvas-text.spec.ts +0 -241
- package/src/canvas-transform.spec.ts +0 -211
- package/src/color.ts +0 -177
- package/src/image-data.ts +0 -34
- package/src/index.ts +0 -14
- package/src/test.browser.mts +0 -614
- package/src/test.mts +0 -22
- package/tmp/.tsbuildinfo +0 -1
- package/tsconfig.json +0 -47
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
// Canvas 2D drawImage tests — verifies the full 3/5/9-argument forms,
|
|
2
|
-
// transform composition, cross-canvas sourcing, and the critical
|
|
3
|
-
// imageSmoothingEnabled pixel-art path (Cairo NEAREST filter lock).
|
|
4
|
-
//
|
|
5
|
-
// Ported from refs/wpt/html/canvas/element/drawing-images-to-the-canvas/
|
|
6
|
-
// 2d.drawImage.canvas.html, .5arg.html, .9arg.basic.html,
|
|
7
|
-
// .negativedest.html, .negativedir.html, .transform.html,
|
|
8
|
-
// .alpha.html, .self.1.html, .zerosource.html
|
|
9
|
-
// Plus refs/wpt/html/canvas/element/manual/image-smoothing/imagesmoothing.html
|
|
10
|
-
// Original: Copyright (c) Web Platform Tests contributors. 3-Clause BSD.
|
|
11
|
-
// Reimplemented for GJS using @gjsify/canvas2d-core + @gjsify/unit.
|
|
12
|
-
|
|
13
|
-
import { describe, it, expect } from '@gjsify/unit';
|
|
14
|
-
import { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
|
|
15
|
-
|
|
16
|
-
function makeCtx(width = 50, height = 50): CanvasRenderingContext2D {
|
|
17
|
-
const canvas = { width, height };
|
|
18
|
-
return new CanvasRenderingContext2D(canvas);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Create a test image as an HTMLCanvasElement-like object with known pixel
|
|
23
|
-
* content. Works through the getContext('2d') branch of _getDrawImageSource
|
|
24
|
-
* (canvas-rendering-context-2d.ts:822-834), avoiding the need for PNG
|
|
25
|
-
* fixtures on disk.
|
|
26
|
-
*/
|
|
27
|
-
function createTestImage(
|
|
28
|
-
width: number,
|
|
29
|
-
height: number,
|
|
30
|
-
draw: (ctx: CanvasRenderingContext2D) => void,
|
|
31
|
-
): any {
|
|
32
|
-
const ctx = makeCtx(width, height);
|
|
33
|
-
draw(ctx);
|
|
34
|
-
return {
|
|
35
|
-
width,
|
|
36
|
-
height,
|
|
37
|
-
getContext: (id: string) => (id === '2d' ? ctx : null),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Assert pixel RGBA within tolerance (default ±2 per channel). */
|
|
42
|
-
function assertPixel(
|
|
43
|
-
ctx: CanvasRenderingContext2D,
|
|
44
|
-
x: number,
|
|
45
|
-
y: number,
|
|
46
|
-
r: number,
|
|
47
|
-
g: number,
|
|
48
|
-
b: number,
|
|
49
|
-
a: number,
|
|
50
|
-
tolerance = 2,
|
|
51
|
-
): void {
|
|
52
|
-
const data = ctx.getImageData(x, y, 1, 1).data;
|
|
53
|
-
expect(Math.abs(data[0] - r) <= tolerance).toBe(true);
|
|
54
|
-
expect(Math.abs(data[1] - g) <= tolerance).toBe(true);
|
|
55
|
-
expect(Math.abs(data[2] - b) <= tolerance).toBe(true);
|
|
56
|
-
expect(Math.abs(data[3] - a) <= tolerance).toBe(true);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function assertPixelExact(
|
|
60
|
-
ctx: CanvasRenderingContext2D,
|
|
61
|
-
x: number,
|
|
62
|
-
y: number,
|
|
63
|
-
r: number,
|
|
64
|
-
g: number,
|
|
65
|
-
b: number,
|
|
66
|
-
a: number,
|
|
67
|
-
): void {
|
|
68
|
-
const data = ctx.getImageData(x, y, 1, 1).data;
|
|
69
|
-
expect(data[0]).toBe(r);
|
|
70
|
-
expect(data[1]).toBe(g);
|
|
71
|
-
expect(data[2]).toBe(b);
|
|
72
|
-
expect(data[3]).toBe(a);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export default async () => {
|
|
76
|
-
await describe('CanvasRenderingContext2D — drawImage', async () => {
|
|
77
|
-
|
|
78
|
-
await describe('3-argument form: drawImage(image, dx, dy)', async () => {
|
|
79
|
-
await it('draws a full canvas source at the given destination', async () => {
|
|
80
|
-
const src = createTestImage(10, 10, (c) => {
|
|
81
|
-
c.fillStyle = 'rgb(255, 0, 0)';
|
|
82
|
-
c.fillRect(0, 0, 10, 10);
|
|
83
|
-
});
|
|
84
|
-
const dst = makeCtx(30, 30);
|
|
85
|
-
dst.drawImage(src, 5, 5);
|
|
86
|
-
// Outside the drawn region → transparent
|
|
87
|
-
assertPixel(dst, 2, 2, 0, 0, 0, 0);
|
|
88
|
-
// Inside the drawn region → red
|
|
89
|
-
assertPixel(dst, 8, 8, 255, 0, 0, 255);
|
|
90
|
-
assertPixel(dst, 14, 14, 255, 0, 0, 255);
|
|
91
|
-
// Right at the edge of the drawn region
|
|
92
|
-
assertPixel(dst, 20, 20, 0, 0, 0, 0);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
await describe('5-argument form: drawImage(image, dx, dy, dw, dh)', async () => {
|
|
97
|
-
await it('scales the source to the destination rectangle', async () => {
|
|
98
|
-
const src = createTestImage(4, 4, (c) => {
|
|
99
|
-
c.fillStyle = 'rgb(0, 255, 0)';
|
|
100
|
-
c.fillRect(0, 0, 4, 4);
|
|
101
|
-
});
|
|
102
|
-
const dst = makeCtx(40, 40);
|
|
103
|
-
dst.drawImage(src, 10, 10, 20, 20);
|
|
104
|
-
// Pixel well inside the 20×20 destination region → green
|
|
105
|
-
assertPixel(dst, 20, 20, 0, 255, 0, 255);
|
|
106
|
-
// Pixel outside the destination region → transparent
|
|
107
|
-
assertPixel(dst, 5, 5, 0, 0, 0, 0);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
await describe('9-argument form: drawImage(image, sx,sy,sw,sh, dx,dy,dw,dh)', async () => {
|
|
112
|
-
await it('crops the source region and scales to destination', async () => {
|
|
113
|
-
// Source: 20x20 with left half red, right half blue
|
|
114
|
-
const src = createTestImage(20, 20, (c) => {
|
|
115
|
-
c.fillStyle = 'rgb(255, 0, 0)';
|
|
116
|
-
c.fillRect(0, 0, 10, 20);
|
|
117
|
-
c.fillStyle = 'rgb(0, 0, 255)';
|
|
118
|
-
c.fillRect(10, 0, 10, 20);
|
|
119
|
-
});
|
|
120
|
-
const dst = makeCtx(40, 40);
|
|
121
|
-
// Take only the right (blue) half of the source and scale to 30x30
|
|
122
|
-
dst.drawImage(src, 10, 0, 10, 20, 5, 5, 30, 30);
|
|
123
|
-
assertPixel(dst, 15, 20, 0, 0, 255, 255);
|
|
124
|
-
assertPixel(dst, 30, 20, 0, 0, 255, 255);
|
|
125
|
-
// Outside destination → still transparent
|
|
126
|
-
assertPixel(dst, 2, 2, 0, 0, 0, 0);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
await describe('drawImage respects current transform', async () => {
|
|
131
|
-
await it('translate shifts the destination', async () => {
|
|
132
|
-
const src = createTestImage(10, 10, (c) => {
|
|
133
|
-
c.fillStyle = 'rgb(255, 255, 0)';
|
|
134
|
-
c.fillRect(0, 0, 10, 10);
|
|
135
|
-
});
|
|
136
|
-
const dst = makeCtx(30, 30);
|
|
137
|
-
dst.translate(10, 10);
|
|
138
|
-
dst.drawImage(src, 0, 0);
|
|
139
|
-
// Yellow should now appear at the translated origin
|
|
140
|
-
assertPixel(dst, 15, 15, 255, 255, 0, 255);
|
|
141
|
-
assertPixel(dst, 5, 5, 0, 0, 0, 0);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
await it('save/restore isolates drawImage transforms', async () => {
|
|
145
|
-
const src = createTestImage(5, 5, (c) => {
|
|
146
|
-
c.fillStyle = 'rgb(100, 100, 100)';
|
|
147
|
-
c.fillRect(0, 0, 5, 5);
|
|
148
|
-
});
|
|
149
|
-
const dst = makeCtx(30, 30);
|
|
150
|
-
dst.save();
|
|
151
|
-
dst.translate(20, 20);
|
|
152
|
-
dst.drawImage(src, 0, 0);
|
|
153
|
-
dst.restore();
|
|
154
|
-
// After restore, drawImage at 0,0 goes to the origin
|
|
155
|
-
dst.drawImage(src, 0, 0);
|
|
156
|
-
// Both regions should contain grey pixels
|
|
157
|
-
assertPixel(dst, 2, 2, 100, 100, 100, 255);
|
|
158
|
-
assertPixel(dst, 22, 22, 100, 100, 100, 255);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
await describe('drawImage with globalAlpha', async () => {
|
|
163
|
-
await it('premultiplies globalAlpha into the source color', async () => {
|
|
164
|
-
const src = createTestImage(10, 10, (c) => {
|
|
165
|
-
c.fillStyle = 'rgb(255, 0, 0)';
|
|
166
|
-
c.fillRect(0, 0, 10, 10);
|
|
167
|
-
});
|
|
168
|
-
const dst = makeCtx(30, 30);
|
|
169
|
-
dst.globalAlpha = 0.5;
|
|
170
|
-
dst.drawImage(src, 10, 10);
|
|
171
|
-
// Sample well inside the destination rect to avoid bilinear
|
|
172
|
-
// edge bleeding. Canvas 2D spec: getImageData returns
|
|
173
|
-
// non-premultiplied RGBA, so red stays 255 while alpha
|
|
174
|
-
// becomes ~128. Tolerance ±5 for Cairo's rounding.
|
|
175
|
-
const data = dst.getImageData(15, 15, 1, 1).data;
|
|
176
|
-
expect(data[0]).toBe(255);
|
|
177
|
-
expect(data[1]).toBe(0);
|
|
178
|
-
expect(data[2]).toBe(0);
|
|
179
|
-
expect(data[3] > 120 && data[3] < 135).toBe(true);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
await describe('drawImage — zero-size and edge cases', async () => {
|
|
184
|
-
await it('drawImage with 0-width source is a no-op', async () => {
|
|
185
|
-
const src = createTestImage(10, 10, (c) => {
|
|
186
|
-
c.fillStyle = 'red';
|
|
187
|
-
c.fillRect(0, 0, 10, 10);
|
|
188
|
-
});
|
|
189
|
-
const dst = makeCtx(20, 20);
|
|
190
|
-
// Pre-fill destination to check nothing was overwritten
|
|
191
|
-
dst.fillStyle = 'blue';
|
|
192
|
-
dst.fillRect(0, 0, 20, 20);
|
|
193
|
-
dst.drawImage(src, 0, 0, 0, 10, 5, 5, 10, 10);
|
|
194
|
-
// Pixel should still be the pre-fill blue
|
|
195
|
-
assertPixel(dst, 10, 10, 0, 0, 255, 255);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
await it('drawImage with 0-height destination is a no-op', async () => {
|
|
199
|
-
const src = createTestImage(10, 10, (c) => {
|
|
200
|
-
c.fillStyle = 'red';
|
|
201
|
-
c.fillRect(0, 0, 10, 10);
|
|
202
|
-
});
|
|
203
|
-
const dst = makeCtx(20, 20);
|
|
204
|
-
dst.fillStyle = 'blue';
|
|
205
|
-
dst.fillRect(0, 0, 20, 20);
|
|
206
|
-
dst.drawImage(src, 5, 5, 10, 0);
|
|
207
|
-
assertPixel(dst, 10, 10, 0, 0, 255, 255);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Regression: Excalibur's Loader.onDraw computes its overlay
|
|
211
|
-
// dimensions from `engine.canvasWidth / engine.pixelRatio`. When
|
|
212
|
-
// the engine fixture isn't fully wired (jelly-jumper Canvas 2D
|
|
213
|
-
// fallback path before the camera target binds), pixelRatio
|
|
214
|
-
// can be 0, propagating Infinity into the dy / dh slots. The
|
|
215
|
-
// older guard only matched literal 0; the new guard rejects
|
|
216
|
-
// any non-finite coordinate so Cairo never sees a non-invertible
|
|
217
|
-
// matrix. Without this fix the showcase fataled mid-frame and
|
|
218
|
-
// Excalibur's clock stopped, leaving the background unrendered.
|
|
219
|
-
await it('drawImage with Infinity destination y is a no-op', async () => {
|
|
220
|
-
const src = createTestImage(10, 10, (c) => {
|
|
221
|
-
c.fillStyle = 'red';
|
|
222
|
-
c.fillRect(0, 0, 10, 10);
|
|
223
|
-
});
|
|
224
|
-
const dst = makeCtx(20, 20);
|
|
225
|
-
dst.fillStyle = 'blue';
|
|
226
|
-
dst.fillRect(0, 0, 20, 20);
|
|
227
|
-
// 9-arg form with Infinity in dy — must not throw and must
|
|
228
|
-
// leave the destination untouched.
|
|
229
|
-
dst.drawImage(src, 0, 0, 10, 10, 0, Infinity, 10, 10);
|
|
230
|
-
assertPixel(dst, 10, 10, 0, 0, 255, 255);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
await it('drawImage with NaN destination height is a no-op', async () => {
|
|
234
|
-
const src = createTestImage(10, 10, (c) => {
|
|
235
|
-
c.fillStyle = 'red';
|
|
236
|
-
c.fillRect(0, 0, 10, 10);
|
|
237
|
-
});
|
|
238
|
-
const dst = makeCtx(20, 20);
|
|
239
|
-
dst.fillStyle = 'blue';
|
|
240
|
-
dst.fillRect(0, 0, 20, 20);
|
|
241
|
-
dst.drawImage(src, 0, 0, 10, 10, 5, 5, 10, NaN);
|
|
242
|
-
assertPixel(dst, 10, 10, 0, 0, 255, 255);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
await it('drawImage with -Infinity source width is a no-op', async () => {
|
|
246
|
-
const src = createTestImage(10, 10, (c) => {
|
|
247
|
-
c.fillStyle = 'red';
|
|
248
|
-
c.fillRect(0, 0, 10, 10);
|
|
249
|
-
});
|
|
250
|
-
const dst = makeCtx(20, 20);
|
|
251
|
-
dst.fillStyle = 'blue';
|
|
252
|
-
dst.fillRect(0, 0, 20, 20);
|
|
253
|
-
dst.drawImage(src, 0, 0, -Infinity, 10, 5, 5, 10, 10);
|
|
254
|
-
assertPixel(dst, 10, 10, 0, 0, 255, 255);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
await describe('drawImage — imageSmoothingEnabled pixel-art lock', async () => {
|
|
259
|
-
// Regression for the Cairo Filter.NEAREST fix in drawImage.
|
|
260
|
-
// When imageSmoothingEnabled=false, scaling a 2×2 image by 10 must
|
|
261
|
-
// produce a sharp checker pattern. Otherwise Cairo's default
|
|
262
|
-
// BILINEAR filter blends the pixels and pixel-art games render
|
|
263
|
-
// blurry.
|
|
264
|
-
|
|
265
|
-
await it('imageSmoothingEnabled=false: exact nearest-neighbor scale', async () => {
|
|
266
|
-
// 2×2 image: red (top-left), green (top-right),
|
|
267
|
-
// blue (bottom-left), white (bottom-right).
|
|
268
|
-
const src = createTestImage(2, 2, (c) => {
|
|
269
|
-
c.fillStyle = 'rgb(255, 0, 0)'; c.fillRect(0, 0, 1, 1);
|
|
270
|
-
c.fillStyle = 'rgb(0, 255, 0)'; c.fillRect(1, 0, 1, 1);
|
|
271
|
-
c.fillStyle = 'rgb(0, 0, 255)'; c.fillRect(0, 1, 1, 1);
|
|
272
|
-
c.fillStyle = 'rgb(255, 255, 255)'; c.fillRect(1, 1, 1, 1);
|
|
273
|
-
});
|
|
274
|
-
const dst = makeCtx(20, 20);
|
|
275
|
-
dst.imageSmoothingEnabled = false;
|
|
276
|
-
dst.drawImage(src, 0, 0, 20, 20);
|
|
277
|
-
|
|
278
|
-
// With NEAREST, each source pixel fills a 10×10 quadrant in
|
|
279
|
-
// the destination without color bleeding. Pixel (5,5) should
|
|
280
|
-
// be exactly red, (15,5) exactly green, etc.
|
|
281
|
-
assertPixelExact(dst, 5, 5, 255, 0, 0, 255);
|
|
282
|
-
assertPixelExact(dst, 15, 5, 0, 255, 0, 255);
|
|
283
|
-
assertPixelExact(dst, 5, 15, 0, 0, 255, 255);
|
|
284
|
-
assertPixelExact(dst, 15, 15, 255, 255, 255, 255);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
await it('imageSmoothingEnabled=true: bilinear bleeding at boundaries', async () => {
|
|
288
|
-
const src = createTestImage(2, 2, (c) => {
|
|
289
|
-
c.fillStyle = 'rgb(255, 0, 0)'; c.fillRect(0, 0, 1, 1);
|
|
290
|
-
c.fillStyle = 'rgb(0, 255, 0)'; c.fillRect(1, 0, 1, 1);
|
|
291
|
-
c.fillStyle = 'rgb(0, 0, 255)'; c.fillRect(0, 1, 1, 1);
|
|
292
|
-
c.fillStyle = 'rgb(255, 255, 255)'; c.fillRect(1, 1, 1, 1);
|
|
293
|
-
});
|
|
294
|
-
const dst = makeCtx(20, 20);
|
|
295
|
-
dst.imageSmoothingEnabled = true;
|
|
296
|
-
dst.drawImage(src, 0, 0, 20, 20);
|
|
297
|
-
|
|
298
|
-
// With bilinear, pixel at the boundary between red and green
|
|
299
|
-
// (around x=10, y=5) is a mix — NOT exactly red and NOT
|
|
300
|
-
// exactly green. This test proves the filter is applied.
|
|
301
|
-
const boundary = dst.getImageData(9, 5, 1, 1).data;
|
|
302
|
-
const isExactRed = boundary[0] === 255 && boundary[1] === 0;
|
|
303
|
-
const isExactGreen = boundary[0] === 0 && boundary[1] === 255;
|
|
304
|
-
// Neither pure red nor pure green → bilinear blending is active
|
|
305
|
-
expect(isExactRed).toBe(false);
|
|
306
|
-
expect(isExactGreen).toBe(false);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
await describe('drawImage with composite operation', async () => {
|
|
311
|
-
// Note: the 'copy' operator in Canvas spec wipes the ENTIRE
|
|
312
|
-
// destination, not just the drawImage rect. Our implementation
|
|
313
|
-
// clips to the destination rect for paint(), so the "wipe
|
|
314
|
-
// outside" semantic is NOT implemented here — that would require
|
|
315
|
-
// a separate clearRect path. We only verify that the inside
|
|
316
|
-
// pixel is correctly replaced.
|
|
317
|
-
await it('drawImage honors globalCompositeOperation=source-over', async () => {
|
|
318
|
-
const src = createTestImage(10, 10, (c) => {
|
|
319
|
-
c.fillStyle = 'rgb(0, 255, 0)';
|
|
320
|
-
c.fillRect(0, 0, 10, 10);
|
|
321
|
-
});
|
|
322
|
-
const dst = makeCtx(20, 20);
|
|
323
|
-
// Pre-fill with red
|
|
324
|
-
dst.fillStyle = 'rgb(255, 0, 0)';
|
|
325
|
-
dst.fillRect(0, 0, 20, 20);
|
|
326
|
-
// source-over: green opaque pixel replaces red in the drawn region
|
|
327
|
-
dst.drawImage(src, 5, 5);
|
|
328
|
-
assertPixel(dst, 10, 10, 0, 255, 0, 255);
|
|
329
|
-
// Outside drawn region → still red (unaffected)
|
|
330
|
-
assertPixel(dst, 1, 1, 255, 0, 0, 255);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
};
|
package/src/canvas-gradient.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// CanvasGradient implementation backed by Cairo gradient patterns
|
|
2
|
-
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient
|
|
3
|
-
|
|
4
|
-
import Cairo from 'cairo';
|
|
5
|
-
import { parseColor } from './color.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* CanvasGradient wrapping a Cairo LinearGradient or RadialGradient.
|
|
9
|
-
*/
|
|
10
|
-
export class CanvasGradient {
|
|
11
|
-
private _pattern: Cairo.LinearGradient | Cairo.RadialGradient;
|
|
12
|
-
|
|
13
|
-
constructor(
|
|
14
|
-
type: 'linear' | 'radial',
|
|
15
|
-
x0: number, y0: number,
|
|
16
|
-
x1: number, y1: number,
|
|
17
|
-
r0?: number, r1?: number,
|
|
18
|
-
) {
|
|
19
|
-
if (type === 'radial') {
|
|
20
|
-
this._pattern = new Cairo.RadialGradient(x0, y0, r0!, x1, y1, r1!);
|
|
21
|
-
} else {
|
|
22
|
-
this._pattern = new Cairo.LinearGradient(x0, y0, x1, y1);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
addColorStop(offset: number, color: string): void {
|
|
27
|
-
const parsed = parseColor(color);
|
|
28
|
-
if (!parsed) return;
|
|
29
|
-
this._pattern.addColorStopRGBA(offset, parsed.r, parsed.g, parsed.b, parsed.a);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** @internal Get the underlying Cairo pattern for rendering. */
|
|
33
|
-
_getCairoPattern(): Cairo.LinearGradient | Cairo.RadialGradient {
|
|
34
|
-
return this._pattern;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
// Canvas 2D ImageData tests — createImageData, getImageData, putImageData
|
|
2
|
-
// round-trip. Verifies pixel extraction from Cairo surfaces and the
|
|
3
|
-
// RGBA byte order expected by consumers.
|
|
4
|
-
//
|
|
5
|
-
// Ported from refs/wpt/html/canvas/element/pixel-manipulation/
|
|
6
|
-
// 2d.imageData.{create1.basic,create2.{basic,initial},get.{order.rgb,
|
|
7
|
-
// source.negative,source.outside},put.basic.rgba,put.alpha,put.dirty*}.html
|
|
8
|
-
// Original: Copyright (c) Web Platform Tests contributors. 3-Clause BSD.
|
|
9
|
-
// Reimplemented for GJS using @gjsify/canvas2d-core + @gjsify/unit.
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from '@gjsify/unit';
|
|
12
|
-
import { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
|
|
13
|
-
|
|
14
|
-
function makeCtx(width = 20, height = 20): CanvasRenderingContext2D {
|
|
15
|
-
const canvas = { width, height };
|
|
16
|
-
return new CanvasRenderingContext2D(canvas);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default async () => {
|
|
20
|
-
await describe('CanvasRenderingContext2D — ImageData', async () => {
|
|
21
|
-
|
|
22
|
-
await describe('createImageData', async () => {
|
|
23
|
-
await it('createImageData(w, h) returns an RGBA Uint8ClampedArray of w*h*4 bytes', async () => {
|
|
24
|
-
const ctx = makeCtx();
|
|
25
|
-
const img = ctx.createImageData(10, 5);
|
|
26
|
-
expect(img.width).toBe(10);
|
|
27
|
-
expect(img.height).toBe(5);
|
|
28
|
-
expect(img.data.length).toBe(10 * 5 * 4);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
await it('createImageData data is initialized to transparent black', async () => {
|
|
32
|
-
const ctx = makeCtx();
|
|
33
|
-
const img = ctx.createImageData(4, 4);
|
|
34
|
-
for (let i = 0; i < img.data.length; i++) {
|
|
35
|
-
expect(img.data[i]).toBe(0);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
await it('createImageData(imageData) returns a clone with matching dimensions', async () => {
|
|
40
|
-
const ctx = makeCtx();
|
|
41
|
-
const src = ctx.createImageData(8, 3);
|
|
42
|
-
const clone = ctx.createImageData(src);
|
|
43
|
-
expect(clone.width).toBe(8);
|
|
44
|
-
expect(clone.height).toBe(3);
|
|
45
|
-
expect(clone.data.length).toBe(8 * 3 * 4);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await describe('getImageData', async () => {
|
|
50
|
-
await it('returns correct RGBA for a filled region', async () => {
|
|
51
|
-
const ctx = makeCtx(10, 10);
|
|
52
|
-
ctx.fillStyle = 'rgb(200, 100, 50)';
|
|
53
|
-
ctx.fillRect(0, 0, 10, 10);
|
|
54
|
-
const data = ctx.getImageData(5, 5, 1, 1).data;
|
|
55
|
-
expect(data[0]).toBe(200);
|
|
56
|
-
expect(data[1]).toBe(100);
|
|
57
|
-
expect(data[2]).toBe(50);
|
|
58
|
-
expect(data[3]).toBe(255);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await it('returns a full RGBA grid for a multi-pixel region', async () => {
|
|
62
|
-
const ctx = makeCtx(4, 4);
|
|
63
|
-
ctx.fillStyle = 'rgb(10, 20, 30)';
|
|
64
|
-
ctx.fillRect(0, 0, 4, 4);
|
|
65
|
-
const img = ctx.getImageData(0, 0, 4, 4);
|
|
66
|
-
expect(img.width).toBe(4);
|
|
67
|
-
expect(img.height).toBe(4);
|
|
68
|
-
expect(img.data.length).toBe(4 * 4 * 4);
|
|
69
|
-
// All 16 pixels should be (10, 20, 30, 255)
|
|
70
|
-
for (let i = 0; i < 16; i++) {
|
|
71
|
-
expect(img.data[i * 4 + 0]).toBe(10);
|
|
72
|
-
expect(img.data[i * 4 + 1]).toBe(20);
|
|
73
|
-
expect(img.data[i * 4 + 2]).toBe(30);
|
|
74
|
-
expect(img.data[i * 4 + 3]).toBe(255);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
await it('preserves byte order across fillStyle color channels', async () => {
|
|
79
|
-
const ctx = makeCtx(3, 1);
|
|
80
|
-
ctx.fillStyle = 'rgb(255, 0, 0)';
|
|
81
|
-
ctx.fillRect(0, 0, 1, 1);
|
|
82
|
-
ctx.fillStyle = 'rgb(0, 255, 0)';
|
|
83
|
-
ctx.fillRect(1, 0, 1, 1);
|
|
84
|
-
ctx.fillStyle = 'rgb(0, 0, 255)';
|
|
85
|
-
ctx.fillRect(2, 0, 1, 1);
|
|
86
|
-
const data = ctx.getImageData(0, 0, 3, 1).data;
|
|
87
|
-
// Pixel 0: red
|
|
88
|
-
expect(data[0]).toBe(255);
|
|
89
|
-
expect(data[1]).toBe(0);
|
|
90
|
-
expect(data[2]).toBe(0);
|
|
91
|
-
// Pixel 1: green
|
|
92
|
-
expect(data[4]).toBe(0);
|
|
93
|
-
expect(data[5]).toBe(255);
|
|
94
|
-
expect(data[6]).toBe(0);
|
|
95
|
-
// Pixel 2: blue
|
|
96
|
-
expect(data[8]).toBe(0);
|
|
97
|
-
expect(data[9]).toBe(0);
|
|
98
|
-
expect(data[10]).toBe(255);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
await describe('putImageData', async () => {
|
|
103
|
-
await it('roundtrips get → put → get unchanged', async () => {
|
|
104
|
-
const ctx = makeCtx(10, 10);
|
|
105
|
-
ctx.fillStyle = 'rgb(77, 88, 99)';
|
|
106
|
-
ctx.fillRect(0, 0, 10, 10);
|
|
107
|
-
const first = ctx.getImageData(0, 0, 10, 10);
|
|
108
|
-
ctx.clearRect(0, 0, 10, 10);
|
|
109
|
-
ctx.putImageData(first, 0, 0);
|
|
110
|
-
const second = ctx.getImageData(5, 5, 1, 1).data;
|
|
111
|
-
expect(second[0]).toBe(77);
|
|
112
|
-
expect(second[1]).toBe(88);
|
|
113
|
-
expect(second[2]).toBe(99);
|
|
114
|
-
expect(second[3]).toBe(255);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
await it('putImageData ignores globalAlpha (spec)', async () => {
|
|
118
|
-
const ctx = makeCtx(10, 10);
|
|
119
|
-
ctx.fillStyle = 'rgb(100, 100, 100)';
|
|
120
|
-
ctx.fillRect(0, 0, 10, 10);
|
|
121
|
-
const src = ctx.getImageData(0, 0, 10, 10);
|
|
122
|
-
ctx.clearRect(0, 0, 10, 10);
|
|
123
|
-
ctx.globalAlpha = 0.1;
|
|
124
|
-
ctx.putImageData(src, 0, 0);
|
|
125
|
-
// putImageData writes raw pixels — globalAlpha has no effect.
|
|
126
|
-
const after = ctx.getImageData(5, 5, 1, 1).data;
|
|
127
|
-
expect(after[0]).toBe(100);
|
|
128
|
-
expect(after[3]).toBe(255);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
await it('putImageData ignores globalCompositeOperation (spec: always SOURCE)', async () => {
|
|
132
|
-
const ctx = makeCtx(10, 10);
|
|
133
|
-
ctx.fillStyle = 'rgb(0, 255, 0)';
|
|
134
|
-
ctx.fillRect(0, 0, 10, 10);
|
|
135
|
-
const src = ctx.getImageData(0, 0, 10, 10);
|
|
136
|
-
// Re-fill the canvas with red, then putImageData the green data
|
|
137
|
-
// under a non-default composite.
|
|
138
|
-
ctx.fillStyle = 'rgb(255, 0, 0)';
|
|
139
|
-
ctx.fillRect(0, 0, 10, 10);
|
|
140
|
-
ctx.globalCompositeOperation = 'destination-over';
|
|
141
|
-
ctx.putImageData(src, 0, 0);
|
|
142
|
-
// Spec: putImageData uses SOURCE → green replaces red.
|
|
143
|
-
const data = ctx.getImageData(5, 5, 1, 1).data;
|
|
144
|
-
expect(data[0]).toBe(0);
|
|
145
|
-
expect(data[1]).toBe(255);
|
|
146
|
-
expect(data[2]).toBe(0);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
};
|
package/src/canvas-path.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// Path2D implementation for Canvas 2D context
|
|
2
|
-
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/Path2D
|
|
3
|
-
// Records path operations and replays them on a Cairo context.
|
|
4
|
-
|
|
5
|
-
import { quadraticToCubic, cairoRoundRect } from './cairo-utils.js';
|
|
6
|
-
|
|
7
|
-
/** A recorded path operation. */
|
|
8
|
-
type PathOp =
|
|
9
|
-
| { type: 'moveTo'; x: number; y: number }
|
|
10
|
-
| { type: 'lineTo'; x: number; y: number }
|
|
11
|
-
| { type: 'closePath' }
|
|
12
|
-
| { type: 'bezierCurveTo'; cp1x: number; cp1y: number; cp2x: number; cp2y: number; x: number; y: number }
|
|
13
|
-
| { type: 'quadraticCurveTo'; cpx: number; cpy: number; x: number; y: number }
|
|
14
|
-
| { type: 'arc'; x: number; y: number; radius: number; startAngle: number; endAngle: number; ccw: boolean }
|
|
15
|
-
| { type: 'ellipse'; x: number; y: number; rx: number; ry: number; rotation: number; startAngle: number; endAngle: number; ccw: boolean }
|
|
16
|
-
| { type: 'rect'; x: number; y: number; w: number; h: number }
|
|
17
|
-
| { type: 'roundRect'; x: number; y: number; w: number; h: number; radii: number | number[] };
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Path2D records path operations for later replay on a CanvasRenderingContext2D.
|
|
21
|
-
*/
|
|
22
|
-
export class Path2D {
|
|
23
|
-
/** @internal Recorded operations */
|
|
24
|
-
_ops: PathOp[] = [];
|
|
25
|
-
|
|
26
|
-
constructor(pathOrSvg?: Path2D | string) {
|
|
27
|
-
if (pathOrSvg instanceof Path2D) {
|
|
28
|
-
this._ops = [...pathOrSvg._ops];
|
|
29
|
-
}
|
|
30
|
-
// SVG path string parsing is not implemented (complex, rarely needed)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
addPath(path: Path2D): void {
|
|
34
|
-
this._ops.push(...path._ops);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
moveTo(x: number, y: number): void {
|
|
38
|
-
this._ops.push({ type: 'moveTo', x, y });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
lineTo(x: number, y: number): void {
|
|
42
|
-
this._ops.push({ type: 'lineTo', x, y });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
closePath(): void {
|
|
46
|
-
this._ops.push({ type: 'closePath' });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void {
|
|
50
|
-
this._ops.push({ type: 'bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void {
|
|
54
|
-
this._ops.push({ type: 'quadraticCurveTo', cpx, cpy, x, y });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise = false): void {
|
|
58
|
-
this._ops.push({ type: 'arc', x, y, radius, startAngle, endAngle, ccw: counterclockwise });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise = false): void {
|
|
62
|
-
if (radiusX < 0 || radiusY < 0) throw new RangeError('The radii provided are negative');
|
|
63
|
-
this._ops.push({ type: 'ellipse', x, y, rx: radiusX, ry: radiusY, rotation, startAngle, endAngle, ccw: counterclockwise });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
rect(x: number, y: number, w: number, h: number): void {
|
|
67
|
-
this._ops.push({ type: 'rect', x, y, w, h });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
roundRect(x: number, y: number, w: number, h: number, radii: number | number[] = 0): void {
|
|
71
|
-
this._ops.push({ type: 'roundRect', x, y, w, h, radii });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @internal Replay all recorded path operations onto a Cairo context.
|
|
76
|
-
*/
|
|
77
|
-
_replayOnCairo(ctx: import('cairo').default.Context): void {
|
|
78
|
-
let lastX = 0, lastY = 0;
|
|
79
|
-
|
|
80
|
-
for (const op of this._ops) {
|
|
81
|
-
switch (op.type) {
|
|
82
|
-
case 'moveTo':
|
|
83
|
-
ctx.moveTo(op.x, op.y);
|
|
84
|
-
lastX = op.x; lastY = op.y;
|
|
85
|
-
break;
|
|
86
|
-
case 'lineTo':
|
|
87
|
-
ctx.lineTo(op.x, op.y);
|
|
88
|
-
lastX = op.x; lastY = op.y;
|
|
89
|
-
break;
|
|
90
|
-
case 'closePath':
|
|
91
|
-
ctx.closePath();
|
|
92
|
-
break;
|
|
93
|
-
case 'bezierCurveTo':
|
|
94
|
-
ctx.curveTo(op.cp1x, op.cp1y, op.cp2x, op.cp2y, op.x, op.y);
|
|
95
|
-
lastX = op.x; lastY = op.y;
|
|
96
|
-
break;
|
|
97
|
-
case 'quadraticCurveTo': {
|
|
98
|
-
const { cp1x, cp1y, cp2x, cp2y } = quadraticToCubic(lastX, lastY, op.cpx, op.cpy, op.x, op.y);
|
|
99
|
-
ctx.curveTo(cp1x, cp1y, cp2x, cp2y, op.x, op.y);
|
|
100
|
-
lastX = op.x; lastY = op.y;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case 'arc':
|
|
104
|
-
if (op.ccw) {
|
|
105
|
-
ctx.arcNegative(op.x, op.y, op.radius, op.startAngle, op.endAngle);
|
|
106
|
-
} else {
|
|
107
|
-
ctx.arc(op.x, op.y, op.radius, op.startAngle, op.endAngle);
|
|
108
|
-
}
|
|
109
|
-
break;
|
|
110
|
-
case 'ellipse':
|
|
111
|
-
ctx.save();
|
|
112
|
-
ctx.translate(op.x, op.y);
|
|
113
|
-
ctx.rotate(op.rotation);
|
|
114
|
-
ctx.scale(op.rx, op.ry);
|
|
115
|
-
if (op.ccw) {
|
|
116
|
-
ctx.arcNegative(0, 0, 1, op.startAngle, op.endAngle);
|
|
117
|
-
} else {
|
|
118
|
-
ctx.arc(0, 0, 1, op.startAngle, op.endAngle);
|
|
119
|
-
}
|
|
120
|
-
ctx.restore();
|
|
121
|
-
break;
|
|
122
|
-
case 'rect':
|
|
123
|
-
ctx.rectangle(op.x, op.y, op.w, op.h);
|
|
124
|
-
break;
|
|
125
|
-
case 'roundRect':
|
|
126
|
-
cairoRoundRect(ctx, op.x, op.y, op.w, op.h, op.radii);
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|