@gjsify/canvas2d-core 0.4.0 → 0.4.4

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.
@@ -1,114 +0,0 @@
1
- // Canvas 2D composite operation tests — verifies the Cairo operator
2
- // mapping for the globalCompositeOperation modes that 2D games commonly
3
- // use (source-over, destination-over, source-in, destination-in,
4
- // lighter, xor, copy).
5
- //
6
- // Ported from refs/wpt/html/canvas/element/compositing/
7
- // 2d.composite.canvas.{source-over,destination-over,source-in,
8
- // destination-in,lighter,xor}.html
9
- // Original: Copyright (c) Web Platform Tests contributors. 3-Clause BSD.
10
- // Reimplemented for GJS using @gjsify/canvas2d-core + @gjsify/unit.
11
-
12
- import { describe, it, expect } from '@gjsify/unit';
13
- import { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
14
-
15
- function makeCtx(width = 30, height = 30): CanvasRenderingContext2D {
16
- const canvas = { width, height };
17
- return new CanvasRenderingContext2D(canvas);
18
- }
19
-
20
- function assertPixel(
21
- ctx: CanvasRenderingContext2D,
22
- x: number, y: number,
23
- r: number, g: number, b: number, a: number,
24
- tol = 2,
25
- ): void {
26
- const data = ctx.getImageData(x, y, 1, 1).data;
27
- expect(Math.abs(data[0] - r) <= tol).toBe(true);
28
- expect(Math.abs(data[1] - g) <= tol).toBe(true);
29
- expect(Math.abs(data[2] - b) <= tol).toBe(true);
30
- expect(Math.abs(data[3] - a) <= tol).toBe(true);
31
- }
32
-
33
- /**
34
- * Common setup: draws a red rect on the left half, then configures the
35
- * composite operator, then draws a green rect that overlaps on the right half.
36
- * The test asserts the result in four zones: pure red, overlap, pure green,
37
- * empty.
38
- */
39
- function setupAndDraw(ctx: CanvasRenderingContext2D, mode: GlobalCompositeOperation): void {
40
- ctx.fillStyle = 'rgb(255, 0, 0)';
41
- ctx.fillRect(5, 5, 15, 15);
42
- ctx.globalCompositeOperation = mode;
43
- ctx.fillStyle = 'rgb(0, 255, 0)';
44
- ctx.fillRect(10, 10, 15, 15);
45
- }
46
-
47
- export default async () => {
48
- await describe('CanvasRenderingContext2D — globalCompositeOperation', async () => {
49
-
50
- await describe('source-over (default)', async () => {
51
- await it('green overlay replaces red in the overlap region', async () => {
52
- const ctx = makeCtx();
53
- setupAndDraw(ctx, 'source-over');
54
- // Red-only zone (7,7)
55
- assertPixel(ctx, 7, 7, 255, 0, 0, 255);
56
- // Overlap zone (15,15) — green on top
57
- assertPixel(ctx, 15, 15, 0, 255, 0, 255);
58
- // Green-only zone (22,22)
59
- assertPixel(ctx, 22, 22, 0, 255, 0, 255);
60
- // Empty zone (2,2)
61
- assertPixel(ctx, 2, 2, 0, 0, 0, 0);
62
- });
63
- });
64
-
65
- await describe('destination-over', async () => {
66
- await it('green is drawn BEHIND the existing red', async () => {
67
- const ctx = makeCtx();
68
- setupAndDraw(ctx, 'destination-over');
69
- // Overlap region — red in front
70
- assertPixel(ctx, 15, 15, 255, 0, 0, 255);
71
- // Red-only zone unchanged
72
- assertPixel(ctx, 7, 7, 255, 0, 0, 255);
73
- // Green-only zone
74
- assertPixel(ctx, 22, 22, 0, 255, 0, 255);
75
- });
76
- });
77
-
78
- await describe('destination-in', async () => {
79
- await it('keeps only red pixels where green overlaps', async () => {
80
- const ctx = makeCtx();
81
- setupAndDraw(ctx, 'destination-in');
82
- // Overlap region — red stays (destination-in keeps destination
83
- // where source exists)
84
- assertPixel(ctx, 15, 15, 255, 0, 0, 255);
85
- // Red-only zone — cleared (no source → destination discarded)
86
- assertPixel(ctx, 7, 7, 0, 0, 0, 0);
87
- // Green-only zone — transparent (no destination)
88
- assertPixel(ctx, 22, 22, 0, 0, 0, 0);
89
- });
90
- });
91
-
92
- await describe('xor', async () => {
93
- await it('overlap region becomes transparent', async () => {
94
- const ctx = makeCtx();
95
- setupAndDraw(ctx, 'xor');
96
- // Overlap → transparent
97
- assertPixel(ctx, 15, 15, 0, 0, 0, 0);
98
- // Red-only zone
99
- assertPixel(ctx, 7, 7, 255, 0, 0, 255);
100
- // Green-only zone
101
- assertPixel(ctx, 22, 22, 0, 255, 0, 255);
102
- });
103
- });
104
-
105
- await describe('lighter (additive)', async () => {
106
- await it('overlap region sums red + green → yellow', async () => {
107
- const ctx = makeCtx();
108
- setupAndDraw(ctx, 'lighter');
109
- // Overlap: red 255 + green 255 = (255, 255, 0, 255) yellow
110
- assertPixel(ctx, 15, 15, 255, 255, 0, 255);
111
- });
112
- });
113
- });
114
- };
@@ -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
- };
@@ -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
- };