@gjsify/canvas2d-core 0.4.0 → 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.
@@ -1,614 +0,0 @@
1
- // Browser unit tests for @gjsify/canvas2d-core Canvas 2D behavior.
2
- // Tests native browser Canvas 2D to validate that the browser behaves the way
3
- // our Cairo-backed GJS implementation assumes. Do NOT import @gjsify/* — that
4
- // would drag in @girs/* / gi:// GObject bindings that have no browser equivalent.
5
-
6
- import { run, describe, it, expect } from '@gjsify/unit';
7
-
8
- function makeCtx(width = 50, height = 50): CanvasRenderingContext2D {
9
- const canvas = document.createElement('canvas');
10
- canvas.width = width;
11
- canvas.height = height;
12
- return canvas.getContext('2d')!;
13
- }
14
-
15
- function nearlyEqual(a: number, b: number, eps = 1e-6): boolean {
16
- return Math.abs(a - b) < eps;
17
- }
18
-
19
- function assertPixel(
20
- ctx: CanvasRenderingContext2D,
21
- x: number, y: number,
22
- r: number, g: number, b: number, a: number,
23
- ): void {
24
- const data = ctx.getImageData(x, y, 1, 1).data;
25
- expect(data[0]).toBe(r);
26
- expect(data[1]).toBe(g);
27
- expect(data[2]).toBe(b);
28
- expect(data[3]).toBe(a);
29
- }
30
-
31
- run({
32
- async Canvas2dCoreTest() {
33
-
34
- // -- clearRect --
35
-
36
- await describe('clearRect', async () => {
37
- await it('clears red canvas to transparent black', async () => {
38
- const ctx = makeCtx(20, 20);
39
- ctx.fillStyle = 'rgb(255, 0, 0)';
40
- ctx.fillRect(0, 0, 20, 20);
41
- assertPixel(ctx, 10, 10, 255, 0, 0, 255);
42
- ctx.clearRect(0, 0, 20, 20);
43
- assertPixel(ctx, 10, 10, 0, 0, 0, 0);
44
- });
45
-
46
- await it('clears only the specified sub-rectangle', async () => {
47
- const ctx = makeCtx(20, 20);
48
- ctx.fillStyle = 'rgb(0, 0, 255)';
49
- ctx.fillRect(0, 0, 20, 20);
50
- ctx.clearRect(5, 5, 10, 10);
51
- assertPixel(ctx, 10, 10, 0, 0, 0, 0);
52
- assertPixel(ctx, 2, 2, 0, 0, 255, 255);
53
- });
54
-
55
- await it('clearRect is affected by current transform', async () => {
56
- const ctx = makeCtx(30, 30);
57
- ctx.fillStyle = 'rgb(0, 255, 0)';
58
- ctx.fillRect(0, 0, 30, 30);
59
- ctx.translate(10, 10);
60
- ctx.clearRect(0, 0, 5, 5);
61
- assertPixel(ctx, 12, 12, 0, 0, 0, 0);
62
- assertPixel(ctx, 16, 16, 0, 255, 0, 255);
63
- });
64
-
65
- await it('clearRect ignores globalAlpha', async () => {
66
- const ctx = makeCtx(20, 20);
67
- ctx.fillStyle = 'red';
68
- ctx.fillRect(0, 0, 20, 20);
69
- ctx.globalAlpha = 0.5;
70
- ctx.clearRect(0, 0, 20, 20);
71
- assertPixel(ctx, 10, 10, 0, 0, 0, 0);
72
- });
73
-
74
- await it('clearRect with negative width clears normalized region', async () => {
75
- const ctx = makeCtx(20, 20);
76
- ctx.fillStyle = 'rgb(255, 255, 0)';
77
- ctx.fillRect(0, 0, 20, 20);
78
- ctx.clearRect(15, 5, -10, 10);
79
- assertPixel(ctx, 10, 10, 0, 0, 0, 0);
80
- assertPixel(ctx, 2, 2, 255, 255, 0, 255);
81
- });
82
-
83
- await it('clearRect with zero width is a no-op', async () => {
84
- const ctx = makeCtx(20, 20);
85
- ctx.fillStyle = 'rgb(128, 128, 128)';
86
- ctx.fillRect(0, 0, 20, 20);
87
- ctx.clearRect(5, 5, 0, 10);
88
- assertPixel(ctx, 10, 10, 128, 128, 128, 255);
89
- });
90
-
91
- await it('clearRect restricted by clip region', async () => {
92
- const ctx = makeCtx(30, 30);
93
- ctx.fillStyle = 'rgb(255, 0, 255)';
94
- ctx.fillRect(0, 0, 30, 30);
95
- ctx.beginPath();
96
- ctx.rect(10, 10, 10, 10);
97
- ctx.clip();
98
- ctx.clearRect(0, 0, 30, 30);
99
- assertPixel(ctx, 15, 15, 0, 0, 0, 0);
100
- assertPixel(ctx, 5, 5, 255, 0, 255, 255);
101
- });
102
- });
103
-
104
- // -- State save/restore --
105
-
106
- await describe('save / restore', async () => {
107
- await it('round-trips fillStyle', async () => {
108
- const ctx = makeCtx();
109
- ctx.fillStyle = '#ff0000';
110
- ctx.save();
111
- ctx.fillStyle = '#00ff00';
112
- expect(ctx.fillStyle).toBe('#00ff00');
113
- ctx.restore();
114
- expect(ctx.fillStyle).toBe('#ff0000');
115
- });
116
-
117
- await it('round-trips strokeStyle', async () => {
118
- const ctx = makeCtx();
119
- ctx.strokeStyle = '#112233';
120
- ctx.save();
121
- ctx.strokeStyle = '#445566';
122
- ctx.restore();
123
- expect(ctx.strokeStyle).toBe('#112233');
124
- });
125
-
126
- await it('round-trips globalAlpha', async () => {
127
- const ctx = makeCtx();
128
- ctx.globalAlpha = 0.3;
129
- ctx.save();
130
- ctx.globalAlpha = 0.9;
131
- ctx.restore();
132
- expect(nearlyEqual(ctx.globalAlpha, 0.3)).toBe(true);
133
- });
134
-
135
- await it('round-trips globalCompositeOperation', async () => {
136
- const ctx = makeCtx();
137
- ctx.globalCompositeOperation = 'source-over';
138
- ctx.save();
139
- ctx.globalCompositeOperation = 'destination-over';
140
- ctx.restore();
141
- expect(ctx.globalCompositeOperation).toBe('source-over');
142
- });
143
-
144
- await it('round-trips lineWidth, lineCap, lineJoin', async () => {
145
- const ctx = makeCtx();
146
- ctx.lineWidth = 3;
147
- ctx.lineCap = 'round';
148
- ctx.lineJoin = 'bevel';
149
- ctx.save();
150
- ctx.lineWidth = 10;
151
- ctx.lineCap = 'square';
152
- ctx.lineJoin = 'miter';
153
- ctx.restore();
154
- expect(ctx.lineWidth).toBe(3);
155
- expect(ctx.lineCap).toBe('round');
156
- expect(ctx.lineJoin).toBe('bevel');
157
- });
158
-
159
- await it('round-trips miterLimit', async () => {
160
- const ctx = makeCtx();
161
- ctx.miterLimit = 5;
162
- ctx.save();
163
- ctx.miterLimit = 20;
164
- ctx.restore();
165
- expect(ctx.miterLimit).toBe(5);
166
- });
167
-
168
- await it('round-trips lineDash and lineDashOffset', async () => {
169
- const ctx = makeCtx();
170
- ctx.setLineDash([5, 2]);
171
- ctx.lineDashOffset = 3;
172
- ctx.save();
173
- ctx.setLineDash([10, 10]);
174
- ctx.lineDashOffset = 7;
175
- ctx.restore();
176
- const restored = ctx.getLineDash();
177
- expect(restored.length).toBe(2);
178
- expect(restored[0]).toBe(5);
179
- expect(restored[1]).toBe(2);
180
- expect(ctx.lineDashOffset).toBe(3);
181
- });
182
-
183
- await it('round-trips font, textAlign, textBaseline', async () => {
184
- const ctx = makeCtx();
185
- ctx.font = 'bold 20px sans-serif';
186
- ctx.textAlign = 'center';
187
- ctx.textBaseline = 'middle';
188
- ctx.save();
189
- ctx.font = '10px serif';
190
- ctx.textAlign = 'left';
191
- ctx.textBaseline = 'top';
192
- ctx.restore();
193
- expect(ctx.font).toBe('bold 20px sans-serif');
194
- expect(ctx.textAlign).toBe('center');
195
- expect(ctx.textBaseline).toBe('middle');
196
- });
197
-
198
- await it('round-trips imageSmoothingEnabled', async () => {
199
- const ctx = makeCtx();
200
- ctx.imageSmoothingEnabled = false;
201
- ctx.save();
202
- ctx.imageSmoothingEnabled = true;
203
- ctx.restore();
204
- expect(ctx.imageSmoothingEnabled).toBe(false);
205
- });
206
-
207
- await it('triple save/restore returns to default fillStyle', async () => {
208
- const ctx = makeCtx();
209
- ctx.save();
210
- ctx.fillStyle = '#111111';
211
- ctx.save();
212
- ctx.fillStyle = '#222222';
213
- ctx.save();
214
- ctx.fillStyle = '#333333';
215
- ctx.restore();
216
- expect(ctx.fillStyle).toBe('#222222');
217
- ctx.restore();
218
- expect(ctx.fillStyle).toBe('#111111');
219
- ctx.restore();
220
- expect(ctx.fillStyle).toBe('#000000');
221
- });
222
-
223
- await it('restore on empty stack is a no-op (no throw)', async () => {
224
- const ctx = makeCtx();
225
- expect(() => {
226
- ctx.restore();
227
- ctx.restore();
228
- }).not.toThrow();
229
- });
230
- });
231
-
232
- // -- Transforms --
233
-
234
- await describe('transforms', async () => {
235
- await it('getTransform returns identity for fresh context', async () => {
236
- const ctx = makeCtx();
237
- const m = ctx.getTransform();
238
- expect(nearlyEqual(m.a, 1)).toBe(true);
239
- expect(nearlyEqual(m.b, 0)).toBe(true);
240
- expect(nearlyEqual(m.c, 0)).toBe(true);
241
- expect(nearlyEqual(m.d, 1)).toBe(true);
242
- expect(nearlyEqual(m.e, 0)).toBe(true);
243
- expect(nearlyEqual(m.f, 0)).toBe(true);
244
- });
245
-
246
- await it('translate updates e and f', async () => {
247
- const ctx = makeCtx();
248
- ctx.translate(10, 20);
249
- const m = ctx.getTransform();
250
- expect(nearlyEqual(m.e, 10)).toBe(true);
251
- expect(nearlyEqual(m.f, 20)).toBe(true);
252
- });
253
-
254
- await it('translate composes cumulatively', async () => {
255
- const ctx = makeCtx();
256
- ctx.translate(10, 20);
257
- ctx.translate(5, 7);
258
- const m = ctx.getTransform();
259
- expect(nearlyEqual(m.e, 15)).toBe(true);
260
- expect(nearlyEqual(m.f, 27)).toBe(true);
261
- });
262
-
263
- await it('scale updates a and d', async () => {
264
- const ctx = makeCtx();
265
- ctx.scale(2, 3);
266
- const m = ctx.getTransform();
267
- expect(nearlyEqual(m.a, 2)).toBe(true);
268
- expect(nearlyEqual(m.d, 3)).toBe(true);
269
- });
270
-
271
- await it('transform(1,0,0,1,tx,ty) is a pure translate', async () => {
272
- const ctx = makeCtx();
273
- ctx.transform(1, 0, 0, 1, 15, 25);
274
- const m = ctx.getTransform();
275
- expect(nearlyEqual(m.e, 15)).toBe(true);
276
- expect(nearlyEqual(m.f, 25)).toBe(true);
277
- });
278
-
279
- await it('setTransform resets to the supplied matrix', async () => {
280
- const ctx = makeCtx();
281
- ctx.translate(100, 100);
282
- ctx.setTransform(2, 0, 0, 2, 5, 10);
283
- const m = ctx.getTransform();
284
- expect(nearlyEqual(m.a, 2)).toBe(true);
285
- expect(nearlyEqual(m.d, 2)).toBe(true);
286
- expect(nearlyEqual(m.e, 5)).toBe(true);
287
- expect(nearlyEqual(m.f, 10)).toBe(true);
288
- });
289
-
290
- await it('setTransform() with no args resets to identity', async () => {
291
- const ctx = makeCtx();
292
- ctx.translate(10, 10);
293
- ctx.scale(5, 5);
294
- ctx.setTransform();
295
- const m = ctx.getTransform();
296
- expect(nearlyEqual(m.a, 1)).toBe(true);
297
- expect(nearlyEqual(m.d, 1)).toBe(true);
298
- expect(nearlyEqual(m.e, 0)).toBe(true);
299
- expect(nearlyEqual(m.f, 0)).toBe(true);
300
- });
301
-
302
- await it('setTransform accepts DOMMatrix2DInit object', async () => {
303
- const ctx = makeCtx();
304
- ctx.setTransform({ a: 2, b: 0, c: 0, d: 2, e: 5, f: 10 });
305
- const m = ctx.getTransform();
306
- expect(nearlyEqual(m.a, 2)).toBe(true);
307
- expect(nearlyEqual(m.e, 5)).toBe(true);
308
- expect(nearlyEqual(m.f, 10)).toBe(true);
309
- });
310
-
311
- await it('save/restore reverts transform', async () => {
312
- const ctx = makeCtx();
313
- ctx.translate(10, 20);
314
- ctx.save();
315
- ctx.scale(5, 5);
316
- ctx.translate(100, 200);
317
- ctx.restore();
318
- const m = ctx.getTransform();
319
- expect(nearlyEqual(m.a, 1)).toBe(true);
320
- expect(nearlyEqual(m.e, 10)).toBe(true);
321
- expect(nearlyEqual(m.f, 20)).toBe(true);
322
- });
323
-
324
- await it('getTransform().multiply round-trips via setTransform', async () => {
325
- const ctx = makeCtx();
326
- ctx.translate(50, 50);
327
- const current = ctx.getTransform();
328
- expect(typeof current.multiply).toBe('function');
329
- const other = new DOMMatrix([2, 0, 0, 2, 0, 0]);
330
- const composed = current.multiply(other);
331
- ctx.setTransform(composed);
332
- const final = ctx.getTransform();
333
- expect(nearlyEqual(final.a, 2)).toBe(true);
334
- expect(nearlyEqual(final.d, 2)).toBe(true);
335
- expect(nearlyEqual(final.e, 50)).toBe(true);
336
- expect(nearlyEqual(final.f, 50)).toBe(true);
337
- });
338
- });
339
-
340
- // -- ImageData --
341
-
342
- await describe('ImageData', async () => {
343
- await it('createImageData(w, h) returns correct dimensions', async () => {
344
- const ctx = makeCtx();
345
- const img = ctx.createImageData(10, 5);
346
- expect(img.width).toBe(10);
347
- expect(img.height).toBe(5);
348
- expect(img.data.length).toBe(10 * 5 * 4);
349
- });
350
-
351
- await it('createImageData data is transparent black', async () => {
352
- const ctx = makeCtx();
353
- const img = ctx.createImageData(4, 4);
354
- for (let i = 0; i < img.data.length; i++) {
355
- expect(img.data[i]).toBe(0);
356
- }
357
- });
358
-
359
- await it('createImageData(imageData) clones dimensions', async () => {
360
- const ctx = makeCtx();
361
- const src = ctx.createImageData(8, 3);
362
- const clone = ctx.createImageData(src);
363
- expect(clone.width).toBe(8);
364
- expect(clone.height).toBe(3);
365
- });
366
-
367
- await it('getImageData returns correct RGBA for filled region', async () => {
368
- const ctx = makeCtx(10, 10);
369
- ctx.fillStyle = 'rgb(200, 100, 50)';
370
- ctx.fillRect(0, 0, 10, 10);
371
- const data = ctx.getImageData(5, 5, 1, 1).data;
372
- expect(data[0]).toBe(200);
373
- expect(data[1]).toBe(100);
374
- expect(data[2]).toBe(50);
375
- expect(data[3]).toBe(255);
376
- });
377
-
378
- await it('getImageData preserves RGBA byte order for RGB channels', async () => {
379
- const ctx = makeCtx(3, 1);
380
- ctx.fillStyle = 'rgb(255, 0, 0)';
381
- ctx.fillRect(0, 0, 1, 1);
382
- ctx.fillStyle = 'rgb(0, 255, 0)';
383
- ctx.fillRect(1, 0, 1, 1);
384
- ctx.fillStyle = 'rgb(0, 0, 255)';
385
- ctx.fillRect(2, 0, 1, 1);
386
- const data = ctx.getImageData(0, 0, 3, 1).data;
387
- expect(data[0]).toBe(255); expect(data[1]).toBe(0); expect(data[2]).toBe(0);
388
- expect(data[4]).toBe(0); expect(data[5]).toBe(255); expect(data[6]).toBe(0);
389
- expect(data[8]).toBe(0); expect(data[9]).toBe(0); expect(data[10]).toBe(255);
390
- });
391
-
392
- await it('putImageData round-trips get → clear → put → get', async () => {
393
- const ctx = makeCtx(10, 10);
394
- ctx.fillStyle = 'rgb(77, 88, 99)';
395
- ctx.fillRect(0, 0, 10, 10);
396
- const first = ctx.getImageData(0, 0, 10, 10);
397
- ctx.clearRect(0, 0, 10, 10);
398
- ctx.putImageData(first, 0, 0);
399
- const data = ctx.getImageData(5, 5, 1, 1).data;
400
- expect(data[0]).toBe(77);
401
- expect(data[1]).toBe(88);
402
- expect(data[2]).toBe(99);
403
- expect(data[3]).toBe(255);
404
- });
405
-
406
- await it('putImageData ignores globalAlpha', async () => {
407
- const ctx = makeCtx(10, 10);
408
- ctx.fillStyle = 'rgb(100, 100, 100)';
409
- ctx.fillRect(0, 0, 10, 10);
410
- const src = ctx.getImageData(0, 0, 10, 10);
411
- ctx.clearRect(0, 0, 10, 10);
412
- ctx.globalAlpha = 0.1;
413
- ctx.putImageData(src, 0, 0);
414
- const after = ctx.getImageData(5, 5, 1, 1).data;
415
- expect(after[0]).toBe(100);
416
- expect(after[3]).toBe(255);
417
- });
418
-
419
- await it('putImageData ignores globalCompositeOperation', async () => {
420
- const ctx = makeCtx(10, 10);
421
- ctx.fillStyle = 'rgb(0, 255, 0)';
422
- ctx.fillRect(0, 0, 10, 10);
423
- const src = ctx.getImageData(0, 0, 10, 10);
424
- ctx.fillStyle = 'rgb(255, 0, 0)';
425
- ctx.fillRect(0, 0, 10, 10);
426
- ctx.globalCompositeOperation = 'destination-over';
427
- ctx.putImageData(src, 0, 0);
428
- const data = ctx.getImageData(5, 5, 1, 1).data;
429
- expect(data[0]).toBe(0);
430
- expect(data[1]).toBe(255);
431
- expect(data[2]).toBe(0);
432
- });
433
- });
434
-
435
- // -- Text --
436
-
437
- await describe('text', async () => {
438
- await it('measureText returns a width > 0 for non-empty string', async () => {
439
- const ctx = makeCtx(200, 50);
440
- ctx.font = '16px sans-serif';
441
- const metrics = ctx.measureText('Hello');
442
- expect(metrics.width).toBeGreaterThan(0);
443
- });
444
-
445
- await it('measureText returns 0 width for empty string', async () => {
446
- const ctx = makeCtx(200, 50);
447
- ctx.font = '16px sans-serif';
448
- const metrics = ctx.measureText('');
449
- expect(metrics.width).toBe(0);
450
- });
451
-
452
- await it('textAlign property round-trips', async () => {
453
- const ctx = makeCtx();
454
- ctx.textAlign = 'center';
455
- expect(ctx.textAlign).toBe('center');
456
- ctx.textAlign = 'right';
457
- expect(ctx.textAlign).toBe('right');
458
- });
459
-
460
- await it('textBaseline property round-trips', async () => {
461
- const ctx = makeCtx();
462
- ctx.textBaseline = 'middle';
463
- expect(ctx.textBaseline).toBe('middle');
464
- ctx.textBaseline = 'top';
465
- expect(ctx.textBaseline).toBe('top');
466
- });
467
-
468
- await it('fillText does not throw', async () => {
469
- const ctx = makeCtx(200, 50);
470
- ctx.font = '16px sans-serif';
471
- ctx.fillStyle = 'black';
472
- expect(() => { ctx.fillText('Hello', 10, 30); }).not.toThrow();
473
- });
474
-
475
- await it('strokeText does not throw', async () => {
476
- const ctx = makeCtx(200, 50);
477
- ctx.font = '16px sans-serif';
478
- ctx.strokeStyle = 'black';
479
- expect(() => { ctx.strokeText('Hi', 10, 30); }).not.toThrow();
480
- });
481
- });
482
-
483
- // -- Compositing --
484
-
485
- await describe('globalCompositeOperation', async () => {
486
- await it('source-over draws on top (default)', async () => {
487
- const ctx = makeCtx(10, 10);
488
- ctx.fillStyle = 'rgb(0, 0, 255)';
489
- ctx.fillRect(0, 0, 10, 10);
490
- ctx.globalCompositeOperation = 'source-over';
491
- ctx.fillStyle = 'rgb(255, 0, 0)';
492
- ctx.fillRect(0, 0, 10, 10);
493
- const data = ctx.getImageData(5, 5, 1, 1).data;
494
- expect(data[0]).toBe(255);
495
- expect(data[2]).toBe(0);
496
- });
497
-
498
- await it('destination-over keeps existing content on top', async () => {
499
- const ctx = makeCtx(10, 10);
500
- ctx.fillStyle = 'rgb(255, 0, 0)';
501
- ctx.fillRect(0, 0, 10, 10);
502
- ctx.globalCompositeOperation = 'destination-over';
503
- ctx.fillStyle = 'rgb(0, 0, 255)';
504
- ctx.fillRect(0, 0, 10, 10);
505
- const data = ctx.getImageData(5, 5, 1, 1).data;
506
- // destination-over: existing (red) stays, new (blue) goes below
507
- expect(data[0]).toBe(255);
508
- expect(data[2]).toBe(0);
509
- });
510
-
511
- await it('all standard composite ops can be set without error', async () => {
512
- const ctx = makeCtx();
513
- const ops: GlobalCompositeOperation[] = [
514
- 'source-over', 'source-in', 'source-out', 'source-atop',
515
- 'destination-over', 'destination-in', 'destination-out', 'destination-atop',
516
- 'lighter', 'copy', 'xor', 'multiply', 'screen', 'overlay',
517
- 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light',
518
- 'soft-light', 'difference', 'exclusion', 'hue', 'saturation',
519
- 'color', 'luminosity',
520
- ];
521
- for (const op of ops) {
522
- expect(() => { ctx.globalCompositeOperation = op; }).not.toThrow();
523
- expect(ctx.globalCompositeOperation).toBe(op);
524
- }
525
- });
526
- });
527
-
528
- // -- drawImage (canvas to canvas) --
529
-
530
- await describe('drawImage (canvas source)', async () => {
531
- await it('3-arg: copies source canvas pixels to destination', async () => {
532
- const src = document.createElement('canvas');
533
- src.width = 10; src.height = 10;
534
- const srcCtx = src.getContext('2d')!;
535
- srcCtx.fillStyle = 'rgb(200, 50, 100)';
536
- srcCtx.fillRect(0, 0, 10, 10);
537
-
538
- const dst = makeCtx(20, 20);
539
- dst.drawImage(src, 5, 5);
540
- assertPixel(dst, 7, 7, 200, 50, 100, 255);
541
- assertPixel(dst, 2, 2, 0, 0, 0, 0);
542
- });
543
-
544
- await it('5-arg: draws with destination width/height scaling', async () => {
545
- const src = document.createElement('canvas');
546
- src.width = 10; src.height = 10;
547
- const srcCtx = src.getContext('2d')!;
548
- srcCtx.fillStyle = 'rgb(0, 200, 0)';
549
- srcCtx.fillRect(0, 0, 10, 10);
550
-
551
- const dst = makeCtx(30, 30);
552
- dst.drawImage(src, 0, 0, 20, 20);
553
- assertPixel(dst, 10, 10, 0, 200, 0, 255);
554
- assertPixel(dst, 25, 25, 0, 0, 0, 0);
555
- });
556
-
557
- await it('9-arg: source crop and destination placement', async () => {
558
- const src = document.createElement('canvas');
559
- src.width = 20; src.height = 10;
560
- const srcCtx = src.getContext('2d')!;
561
- srcCtx.fillStyle = 'rgb(255, 0, 0)';
562
- srcCtx.fillRect(0, 0, 10, 10);
563
- srcCtx.fillStyle = 'rgb(0, 255, 0)';
564
- srcCtx.fillRect(10, 0, 10, 10);
565
-
566
- const dst = makeCtx(30, 30);
567
- // Draw only the right half of src (green) at (0,0) 10×10
568
- dst.drawImage(src, 10, 0, 10, 10, 0, 0, 10, 10);
569
- assertPixel(dst, 5, 5, 0, 255, 0, 255);
570
- });
571
- });
572
-
573
- // -- path operations --
574
-
575
- await describe('path operations', async () => {
576
- await it('fillRect then strokeRect produce pixels', async () => {
577
- const ctx = makeCtx(20, 20);
578
- ctx.fillStyle = 'rgb(100, 150, 200)';
579
- ctx.fillRect(2, 2, 10, 10);
580
- const data = ctx.getImageData(5, 5, 1, 1).data;
581
- expect(data[0]).toBe(100);
582
- expect(data[1]).toBe(150);
583
- expect(data[2]).toBe(200);
584
- });
585
-
586
- await it('beginPath + arc + fill produces a colored circle', async () => {
587
- const ctx = makeCtx(50, 50);
588
- ctx.fillStyle = 'rgb(255, 0, 0)';
589
- ctx.beginPath();
590
- ctx.arc(25, 25, 20, 0, Math.PI * 2);
591
- ctx.fill();
592
- // Center pixel should be red
593
- const data = ctx.getImageData(25, 25, 1, 1).data;
594
- expect(data[0]).toBe(255);
595
- expect(data[1]).toBe(0);
596
- expect(data[2]).toBe(0);
597
- expect(data[3]).toBe(255);
598
- });
599
-
600
- await it('clip constrains drawing', async () => {
601
- const ctx = makeCtx(30, 30);
602
- ctx.beginPath();
603
- ctx.rect(10, 10, 10, 10);
604
- ctx.clip();
605
- ctx.fillStyle = 'rgb(0, 0, 255)';
606
- ctx.fillRect(0, 0, 30, 30);
607
- // Inside clip
608
- assertPixel(ctx, 15, 15, 0, 0, 255, 255);
609
- // Outside clip — should be transparent
610
- assertPixel(ctx, 5, 5, 0, 0, 0, 0);
611
- });
612
- });
613
- },
614
- });
package/src/test.mts DELETED
@@ -1,22 +0,0 @@
1
-
2
- import { run } from '@gjsify/unit';
3
-
4
- import canvasTextSuite from './canvas-text.spec.js';
5
- import canvasTransformSuite from './canvas-transform.spec.js';
6
- import canvasDrawimageSuite from './canvas-drawimage.spec.js';
7
- import canvasStateSuite from './canvas-state.spec.js';
8
- import canvasClearingSuite from './canvas-clearing.spec.js';
9
- import canvasImagedataSuite from './canvas-imagedata.spec.js';
10
- import canvasCompositeSuite from './canvas-composite.spec.js';
11
- import canvasColorSuite from './canvas-color.spec.js';
12
-
13
- run({
14
- canvasTextSuite,
15
- canvasTransformSuite,
16
- canvasDrawimageSuite,
17
- canvasStateSuite,
18
- canvasClearingSuite,
19
- canvasImagedataSuite,
20
- canvasCompositeSuite,
21
- canvasColorSuite,
22
- });