xref: /aosp_15_r20/external/skia/modules/canvaskit/tests/canvas2d_test.js (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1describe('Canvas 2D emulation', () => {
2    let container;
3
4    beforeEach(async () => {
5        await EverythingLoaded;
6        container = document.createElement('div');
7        container.innerHTML = `
8            <canvas width=600 height=600 id=test></canvas>
9            <canvas width=600 height=600 id=report></canvas>`;
10        document.body.appendChild(container);
11    });
12
13    afterEach(() => {
14        document.body.removeChild(container);
15    });
16
17    const expectColorCloseTo = (a, b) => {
18        expect(a.length).toEqual(4);
19        expect(b.length).toEqual(4);
20        for (let i=0; i<4; i++) {
21            expect(a[i]).toBeCloseTo(b[i], 3);
22        }
23    }
24
25    describe('color strings', () => {
26        const hex = (s) => {
27            return parseInt(s, 16);
28        }
29
30        it('parses hex color strings', () => {
31            const parseColor = CanvasKit.parseColorString;
32            expectColorCloseTo(parseColor('#FED'),
33                CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), 1));
34            expectColorCloseTo(parseColor('#FEDC'),
35                CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), hex('CC')/255));
36            expectColorCloseTo(parseColor('#fed'),
37                CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), 1));
38            expectColorCloseTo(parseColor('#fedc'),
39                CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), hex('CC')/255));
40        });
41        it('parses rgba color strings', () => {
42            const parseColor = CanvasKit.parseColorString;
43            expectColorCloseTo(parseColor('rgba(117, 33, 64, 0.75)'),
44                CanvasKit.Color(117, 33, 64, 0.75));
45            expectColorCloseTo(parseColor('rgb(117, 33, 64, 0.75)'),
46                CanvasKit.Color(117, 33, 64, 0.75));
47            expectColorCloseTo(parseColor('rgba(117,33,64)'),
48                CanvasKit.Color(117, 33, 64, 1.0));
49            expectColorCloseTo(parseColor('rgb(117,33, 64)'),
50                CanvasKit.Color(117, 33, 64, 1.0));
51            expectColorCloseTo(parseColor('rgb(117,33, 64, 32%)'),
52                CanvasKit.Color(117, 33, 64, 0.32));
53            expectColorCloseTo(parseColor('rgb(117,33, 64, 0.001)'),
54                CanvasKit.Color(117, 33, 64, 0.001));
55            expectColorCloseTo(parseColor('rgb(117,33,64,0)'),
56                CanvasKit.Color(117, 33, 64, 0.0));
57        });
58        it('parses named color strings', () => {
59            // Keep this one as the _testing version, because we don't include the large
60            // color map by default.
61            const parseColor = CanvasKit._testing.parseColor;
62            expectColorCloseTo(parseColor('grey'),
63                CanvasKit.Color(128, 128, 128, 1.0));
64            expectColorCloseTo(parseColor('blanchedalmond'),
65                CanvasKit.Color(255, 235, 205, 1.0));
66            expectColorCloseTo(parseColor('transparent'),
67                CanvasKit.Color(0, 0, 0, 0));
68        });
69
70        it('properly produces color strings', () => {
71            const colorToString = CanvasKit._testing.colorToString;
72
73            expect(colorToString(CanvasKit.Color(102, 51, 153, 1.0))).toEqual('#663399');
74
75            expect(colorToString(CanvasKit.Color(255, 235, 205, 0.5))).toEqual(
76                                           'rgba(255, 235, 205, 0.50000000)');
77        });
78
79        it('can multiply colors by alpha', () => {
80            const multiplyByAlpha = CanvasKit.multiplyByAlpha;
81
82            const testCases = [
83                   {
84                    inColor:  CanvasKit.Color(102, 51, 153, 1.0),
85                    inAlpha:  1.0,
86                    outColor: CanvasKit.Color(102, 51, 153, 1.0),
87                },
88                {
89                    inColor:  CanvasKit.Color(102, 51, 153, 1.0),
90                    inAlpha:  0.8,
91                    outColor: CanvasKit.Color(102, 51, 153, 0.8),
92                },
93                {
94                    inColor:  CanvasKit.Color(102, 51, 153, 0.8),
95                    inAlpha:  0.7,
96                    outColor: CanvasKit.Color(102, 51, 153, 0.56),
97                },
98                {
99                    inColor:  CanvasKit.Color(102, 51, 153, 0.8),
100                    inAlpha:  1000,
101                    outColor: CanvasKit.Color(102, 51, 153, 1.0),
102                },
103            ];
104
105            for (const tc of testCases) {
106                // Print out the test case if the two don't match.
107                expect(multiplyByAlpha(tc.inColor, tc.inAlpha))
108                      .toEqual(tc.outColor, JSON.stringify(tc));
109            }
110        });
111    }); // end describe('color string parsing')
112
113    describe('fonts', () => {
114        it('can parse font sizes', () => {
115            const parseFontString = CanvasKit._testing.parseFontString;
116
117            const tests = [{
118                    'input': '10px monospace',
119                    'output': {
120                        'style': '',
121                        'variant': '',
122                        'weight': '',
123                        'sizePx': 10,
124                        'family': 'monospace',
125                    }
126                },
127                {
128                    'input': '15pt Arial',
129                    'output': {
130                        'style': '',
131                        'variant': '',
132                        'weight': '',
133                        'sizePx': 20,
134                        'family': 'Arial',
135                    }
136                },
137                {
138                    'input': '1.5in Arial, san-serif ',
139                    'output': {
140                        'style': '',
141                        'variant': '',
142                        'weight': '',
143                        'sizePx': 144,
144                        'family': 'Arial, san-serif',
145                    }
146                },
147                {
148                    'input': '1.5em SuperFont',
149                    'output': {
150                        'style': '',
151                        'variant': '',
152                        'weight': '',
153                        'sizePx': 24,
154                        'family': 'SuperFont',
155                    }
156                },
157            ];
158
159            for (let i = 0; i < tests.length; i++) {
160                expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
161            }
162        });
163
164        it('can parse font attributes', () => {
165            const parseFontString = CanvasKit._testing.parseFontString;
166
167            const tests = [{
168                    'input': 'bold 10px monospace',
169                    'output': {
170                        'style': '',
171                        'variant': '',
172                        'weight': 'bold',
173                        'sizePx': 10,
174                        'family': 'monospace',
175                    }
176                },
177                {
178                    'input': 'italic bold 10px monospace',
179                    'output': {
180                        'style': 'italic',
181                        'variant': '',
182                        'weight': 'bold',
183                        'sizePx': 10,
184                        'family': 'monospace',
185                    }
186                },
187                {
188                    'input': 'italic small-caps bold 10px monospace',
189                    'output': {
190                        'style': 'italic',
191                        'variant': 'small-caps',
192                        'weight': 'bold',
193                        'sizePx': 10,
194                        'family': 'monospace',
195                    }
196                },
197                {
198                    'input': 'small-caps bold 10px monospace',
199                    'output': {
200                        'style': '',
201                        'variant': 'small-caps',
202                        'weight': 'bold',
203                        'sizePx': 10,
204                        'family': 'monospace',
205                    }
206                },
207                {
208                    'input': 'italic 10px monospace',
209                    'output': {
210                        'style': 'italic',
211                        'variant': '',
212                        'weight': '',
213                        'sizePx': 10,
214                        'family': 'monospace',
215                    }
216                },
217                {
218                    'input': 'small-caps 10px monospace',
219                    'output': {
220                        'style': '',
221                        'variant': 'small-caps',
222                        'weight': '',
223                        'sizePx': 10,
224                        'family': 'monospace',
225                    }
226                },
227                {
228                    'input': 'normal bold 10px monospace',
229                    'output': {
230                        'style': 'normal',
231                        'variant': '',
232                        'weight': 'bold',
233                        'sizePx': 10,
234                        'family': 'monospace',
235                    }
236                },
237            ];
238
239            for (let i = 0; i < tests.length; i++) {
240                expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
241            }
242        });
243    });
244
245    const multipleCanvasTest = (testname, done, test) => {
246        const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
247        skcanvas._config = 'software_canvas';
248        const realCanvas = document.getElementById('test');
249        realCanvas._config = 'html_canvas';
250        realCanvas.width = CANVAS_WIDTH;
251        realCanvas.height = CANVAS_HEIGHT;
252
253        if (!done) {
254            console.log('debugging canvaskit');
255            test(realCanvas);
256            test(skcanvas);
257            const png = skcanvas.toDataURL();
258            const img = document.createElement('img');
259            document.body.appendChild(img);
260            img.src = png;
261            debugger;
262            return;
263        }
264
265        let promises = [];
266
267        for (let canvas of [skcanvas, realCanvas]) {
268            test(canvas);
269            // canvas has .toDataURL (even though skcanvas is not a real Canvas)
270            // so this will work.
271            promises.push(reportCanvas(canvas, testname, canvas._config));
272        }
273        Promise.all(promises).then(() => {
274            skcanvas.dispose();
275            done();
276        }).catch(reportError(done));
277    }
278
279    describe('CanvasContext2D API', () => {
280        multipleCanvasGM('all_line_drawing_operations', (canvas) => {
281            const ctx = canvas.getContext('2d');
282            ctx.scale(3.0, 3.0);
283            ctx.moveTo(20, 5);
284            ctx.lineTo(30, 20);
285            ctx.lineTo(40, 10);
286            ctx.lineTo(50, 20);
287            ctx.lineTo(60, 0);
288            ctx.lineTo(20, 5);
289
290            ctx.moveTo(20, 80);
291            ctx.bezierCurveTo(90, 10, 160, 150, 190, 10);
292
293            ctx.moveTo(36, 148);
294            ctx.quadraticCurveTo(66, 188, 120, 136);
295            ctx.lineTo(36, 148);
296
297            ctx.rect(5, 170, 20, 25);
298
299            ctx.moveTo(150, 180);
300            ctx.arcTo(150, 100, 50, 200, 20);
301            ctx.lineTo(160, 160);
302
303            ctx.moveTo(20, 120);
304            ctx.arc(20, 120, 18, 0, 1.75 * Math.PI);
305            ctx.lineTo(20, 120);
306
307            ctx.moveTo(150, 5);
308            ctx.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI)
309
310            ctx.lineWidth = 2;
311            ctx.stroke();
312
313            // Test edgecases and draw direction
314            ctx.beginPath();
315            ctx.arc(50, 100, 10, Math.PI, -Math.PI/2);
316            ctx.stroke();
317            ctx.beginPath();
318            ctx.arc(75, 100, 10, Math.PI, -Math.PI/2, true);
319            ctx.stroke();
320            ctx.beginPath();
321            ctx.arc(100, 100, 10, Math.PI, 100.1 * Math.PI, true);
322            ctx.stroke();
323            ctx.beginPath();
324            ctx.arc(125, 100, 10, Math.PI, 100.1 * Math.PI, false);
325            ctx.stroke();
326            ctx.beginPath();
327            ctx.ellipse(155, 100, 10, 15, Math.PI/8, 100.1 * Math.PI, Math.PI, true);
328            ctx.stroke();
329            ctx.beginPath();
330            ctx.ellipse(180, 100, 10, 15, Math.PI/8, Math.PI, 100.1 * Math.PI, true);
331            ctx.stroke();
332        });
333
334        multipleCanvasGM('all_matrix_operations', (canvas) => {
335            const ctx = canvas.getContext('2d');
336            ctx.rect(10, 10, 20, 20);
337
338            ctx.scale(2.0, 4.0);
339            ctx.rect(30, 10, 20, 20);
340            ctx.resetTransform();
341
342            ctx.rotate(Math.PI / 3);
343            ctx.rect(50, 10, 20, 20);
344            ctx.resetTransform();
345
346            ctx.translate(30, -2);
347            ctx.rect(70, 10, 20, 20);
348            ctx.resetTransform();
349
350            ctx.translate(60, 0);
351            ctx.rotate(Math.PI / 6);
352            ctx.transform(1.5, 0, 0, 0.5, 0, 0); // effectively scale
353            ctx.rect(90, 10, 20, 20);
354            ctx.resetTransform();
355
356            ctx.save();
357            ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
358            ctx.rect(110, 10, 20, 20);
359            ctx.lineTo(110, 0);
360            ctx.restore();
361            ctx.lineTo(220, 120);
362
363            ctx.scale(3.0, 3.0);
364            ctx.font = '6pt Noto Mono';
365            ctx.fillText('This text should be huge', 10, 80);
366            ctx.resetTransform();
367
368            ctx.strokeStyle = 'black';
369            ctx.lineWidth = 2;
370            ctx.stroke();
371
372            ctx.beginPath();
373            ctx.moveTo(250, 30);
374            ctx.lineTo(250, 80);
375            ctx.scale(3.0, 3.0);
376            ctx.lineTo(280/3, 90/3);
377            ctx.closePath();
378            ctx.strokeStyle = 'black';
379            ctx.lineWidth = 5;
380            ctx.stroke();
381        });
382
383        multipleCanvasGM('shadows_and_save_restore', (canvas) => {
384            const ctx = canvas.getContext('2d');
385            ctx.strokeStyle = '#000';
386            ctx.fillStyle = '#CCC';
387            ctx.shadowColor = 'rebeccapurple';
388            ctx.shadowBlur = 1;
389            ctx.shadowOffsetX = 3;
390            ctx.shadowOffsetY = -8;
391            ctx.rect(10, 10, 30, 30);
392
393            ctx.save();
394            ctx.strokeStyle = '#C00';
395            ctx.fillStyle = '#00C';
396            ctx.shadowBlur = 0;
397            ctx.shadowColor = 'transparent';
398
399            ctx.stroke();
400
401            ctx.restore();
402            ctx.fill();
403
404            ctx.beginPath();
405            ctx.moveTo(36, 148);
406            ctx.quadraticCurveTo(66, 188, 120, 136);
407            ctx.closePath();
408            ctx.stroke();
409
410            ctx.beginPath();
411            ctx.shadowColor = '#993366AA';
412            ctx.shadowOffsetX = 8;
413            ctx.shadowBlur = 5;
414            ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
415            ctx.rect(110, 10, 20, 20);
416            ctx.lineTo(110, 0);
417            ctx.resetTransform();
418            ctx.lineTo(220, 120);
419            ctx.stroke();
420
421            ctx.fillStyle = 'green';
422            ctx.font = '16pt Noto Mono';
423            ctx.fillText('This should be shadowed', 20, 80);
424
425            ctx.beginPath();
426            ctx.lineWidth = 6;
427            ctx.ellipse(10, 290, 30, 30, 0, 0, Math.PI * 2);
428            ctx.scale(2, 1);
429            ctx.moveTo(10, 290)
430            ctx.ellipse(10, 290, 30, 60, 0, 0, Math.PI * 2);
431            ctx.resetTransform();
432            ctx.shadowColor = '#993366AA';
433            ctx.scale(3, 1);
434            ctx.moveTo(10, 290)
435            ctx.ellipse(10, 290, 30, 90, 0, 0, Math.PI * 2);
436            ctx.stroke();
437        });
438
439        multipleCanvasGM('global_dashed_rects', (canvas) => {
440            const ctx = canvas.getContext('2d');
441            ctx.scale(1.1, 1.1);
442            ctx.translate(10, 10);
443            // Shouldn't impact the fillRect calls
444            ctx.setLineDash([5, 3]);
445
446            ctx.fillStyle = 'rgba(200, 0, 100, 0.81)';
447            ctx.fillRect(20, 30, 100, 100);
448
449            ctx.globalAlpha = 0.81;
450            ctx.fillStyle = 'rgba(200, 0, 100, 1.0)';
451            ctx.fillRect(120, 30, 100, 100);
452            // This shouldn't do anything
453            ctx.globalAlpha = 0.1;
454
455            ctx.fillStyle = 'rgba(200, 0, 100, 0.9)';
456            ctx.globalAlpha = 0.9;
457            // Intentional no-op to check ordering
458            ctx.clearRect(220, 30, 100, 100);
459            ctx.fillRect(220, 30, 100, 100);
460
461            ctx.fillRect(320, 30, 100, 100);
462            ctx.clearRect(330, 40, 80, 80);
463
464            ctx.strokeStyle = 'blue';
465            ctx.lineWidth = 3;
466            ctx.setLineDash([5, 3]);
467            ctx.strokeRect(20, 150, 100, 100);
468            ctx.setLineDash([50, 30]);
469            ctx.strokeRect(125, 150, 100, 100);
470            ctx.lineDashOffset = 25;
471            ctx.strokeRect(230, 150, 100, 100);
472            ctx.setLineDash([2, 5, 9]);
473            ctx.strokeRect(335, 150, 100, 100);
474
475            ctx.setLineDash([5, 2]);
476            ctx.moveTo(336, 400);
477            ctx.quadraticCurveTo(366, 488, 120, 450);
478            ctx.lineTo(300, 400);
479            ctx.stroke();
480
481            ctx.font = '36pt Noto Mono';
482            ctx.strokeText('Dashed', 20, 350);
483            ctx.fillText('Not Dashed', 20, 400);
484        });
485
486        multipleCanvasGM('gradients_clip', (canvas) => {
487            const ctx = canvas.getContext('2d');
488
489            const rgradient = ctx.createRadialGradient(200, 300, 10, 100, 100, 300);
490
491            rgradient.addColorStop(0, 'red');
492            rgradient.addColorStop(.7, 'white');
493            rgradient.addColorStop(1, 'blue');
494
495            ctx.fillStyle = rgradient;
496            ctx.globalAlpha = 0.7;
497            ctx.fillRect(0,0,600,600);
498            ctx.globalAlpha = 0.95;
499
500            ctx.beginPath();
501            ctx.arc(300, 100, 90, 0, Math.PI*1.66);
502            ctx.closePath();
503            ctx.strokeStyle = 'yellow';
504            ctx.lineWidth = 5;
505            ctx.stroke();
506            ctx.save();
507            ctx.clip();
508
509            const lgradient = ctx.createLinearGradient(200, 20, 420, 40);
510
511            lgradient.addColorStop(0, 'green');
512            lgradient.addColorStop(.5, 'cyan');
513            lgradient.addColorStop(1, 'orange');
514
515            ctx.fillStyle = lgradient;
516
517            ctx.fillRect(200, 30, 200, 300);
518
519            ctx.restore();
520            ctx.fillRect(550, 550, 40, 40);
521        });
522
523        multipleCanvasGM('get_put_imagedata', (canvas) => {
524            const ctx = canvas.getContext('2d');
525            // Make a gradient so we see if the pixels copying worked
526            const grad = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
527            grad.addColorStop(0, 'yellow');
528            grad.addColorStop(1, 'red');
529            ctx.fillStyle = grad;
530            ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
531
532            const iData = ctx.getImageData(400, 100, 200, 150);
533            expect(iData.width).toBe(200);
534            expect(iData.height).toBe(150);
535            expect(iData.data.byteLength).toBe(200*150*4);
536            ctx.putImageData(iData, 10, 10);
537            ctx.putImageData(iData, 350, 350, 100, 75, 45, 40);
538            ctx.strokeRect(350, 350, 200, 150);
539
540            const box = ctx.createImageData(20, 40);
541            ctx.putImageData(box, 10, 300);
542            const biggerBox = ctx.createImageData(iData);
543            ctx.putImageData(biggerBox, 10, 350);
544            expect(biggerBox.width).toBe(iData.width);
545            expect(biggerBox.height).toBe(iData.height);
546        });
547
548        multipleCanvasGM('shadows_with_rotate_skbug_9947', (canvas) => {
549            const ctx = canvas.getContext('2d');
550            const angle = 240;
551            ctx.fillStyle = 'white';
552            ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
553            ctx.save();
554            ctx.translate(80, 80);
555            ctx.rotate((angle * Math.PI) / 180);
556            ctx.shadowOffsetX = 10;
557            ctx.shadowOffsetY = 10;
558            ctx.shadowColor = 'rgba(100,100,100,0.5)';
559            ctx.shadowBlur = 1;
560            ctx.fillStyle = 'black';
561            ctx.strokeStyle = 'red';
562            ctx.beginPath();
563            ctx.rect(-20, -20, 40, 40);
564            ctx.fill();
565            ctx.fillRect(30, 30, 40, 40);
566            ctx.strokeRect(30, -20, 40, 40);
567            ctx.fillText('text', -20, -30);
568            ctx.restore();
569        });
570
571        describe('using images', () => {
572            let skImageData = null;
573            let htmlImage = null;
574            const skPromise = fetch('/assets/mandrill_512.png')
575                .then((response) => response.arrayBuffer())
576                .then((buffer) => {
577                    skImageData = buffer;
578
579                });
580            const realPromise = fetch('/assets/mandrill_512.png')
581                .then((response) => response.blob())
582                .then((blob) => createImageBitmap(blob))
583                .then((bitmap) => {
584                    htmlImage = bitmap;
585                });
586
587            beforeEach(async () => {
588                await skPromise;
589                await realPromise;
590            });
591
592            multipleCanvasGM('draw_patterns', (canvas) => {
593                const ctx = canvas.getContext('2d');
594                let img = htmlImage;
595                if (canvas._config === 'software_canvas') {
596                    img = canvas.decodeImage(skImageData);
597                }
598                ctx.fillStyle = '#EEE';
599                ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
600                ctx.lineWidth = 20;
601                ctx.scale(0.2, 0.4);
602
603                let pattern = ctx.createPattern(img, 'repeat');
604                ctx.fillStyle = pattern;
605                ctx.fillRect(0, 0, 1500, 750);
606
607                pattern = ctx.createPattern(img, 'repeat-x');
608                ctx.fillStyle = pattern;
609                ctx.fillRect(1500, 0, 3000, 750);
610
611                ctx.globalAlpha = 0.7
612                pattern = ctx.createPattern(img, 'repeat-y');
613                ctx.fillStyle = pattern;
614                ctx.fillRect(0, 750, 1500, 1500);
615                ctx.strokeRect(0, 750, 1500, 1500);
616
617                pattern = ctx.createPattern(img, 'no-repeat');
618                ctx.fillStyle = pattern;
619                pattern.setTransform({a: 1, b: -.1, c:.1, d: 0.5, e: 1800, f:800});
620                ctx.fillRect(0, 0, 3000, 1500);
621            });
622
623            multipleCanvasGM('draw_image', (canvas) => {
624                let ctx = canvas.getContext('2d');
625                let img = htmlImage;
626                if (canvas._config === 'software_canvas') {
627                    img = canvas.decodeImage(skImageData);
628                }
629                ctx.drawImage(img, 30, -200);
630
631                ctx.globalAlpha = 0.7
632                ctx.rotate(.1);
633                ctx.imageSmoothingQuality = 'medium';
634                ctx.drawImage(img, 200, 350, 150, 100);
635                ctx.rotate(-.2);
636                ctx.imageSmoothingEnabled = false;
637                ctx.drawImage(img, 100, 150, 400, 350, 10, 400, 150, 100);
638            });
639        }); // end describe('using images')
640
641        {
642            const drawPoint = (ctx, x, y, color) => {
643                ctx.fillStyle = color;
644                ctx.fillRect(x, y, 1, 1);
645            }
646            const IN = 'purple';
647            const OUT = 'orange';
648            const SCALE = 8;
649
650            // Check to see if these points are in or out on each of the
651            // test configurations.
652            const pts = [[3, 3], [4, 4], [5, 5], [10, 10], [8, 10], [6, 10],
653                         [6.5, 9], [15, 10], [17, 10], [17, 11], [24, 24],
654                         [25, 25], [26, 26], [27, 27]];
655            const tests = [
656                {
657                    xOffset: 0,
658                    yOffset: 0,
659                    fillType: 'nonzero',
660                    strokeWidth: 0,
661                    testFn: (ctx, x, y) => ctx.isPointInPath(x * SCALE, y * SCALE, 'nonzero'),
662                },
663                {
664                    xOffset: 30,
665                    yOffset: 0,
666                    fillType: 'evenodd',
667                    strokeWidth: 0,
668                    testFn: (ctx, x, y) => ctx.isPointInPath(x * SCALE, y * SCALE, 'evenodd'),
669                },
670                {
671                    xOffset: 0,
672                    yOffset: 30,
673                    fillType: null,
674                    strokeWidth: 1,
675                    testFn: (ctx, x, y) => ctx.isPointInStroke(x * SCALE, y * SCALE),
676                },
677                {
678                    xOffset: 30,
679                    yOffset: 30,
680                    fillType: null,
681                    strokeWidth: 2,
682                    testFn: (ctx, x, y) => ctx.isPointInStroke(x * SCALE, y * SCALE),
683                },
684            ];
685            multipleCanvasGM('points_in_path_stroke', (canvas) => {
686                const ctx = canvas.getContext('2d');
687                ctx.font = '20px Noto Mono';
688                // Draw some visual aids
689                ctx.fillText('path-nonzero', 60, 30);
690                ctx.fillText('path-evenodd', 300, 30);
691                ctx.fillText('stroke-1px-wide', 60, 260);
692                ctx.fillText('stroke-2px-wide', 300, 260);
693                ctx.fillText('purple is IN, orange is OUT', 20, 560);
694
695                // Scale up to make single pixels easier to see
696                ctx.scale(SCALE, SCALE);
697                for (const test of tests) {
698                    ctx.beginPath();
699                    const xOffset = test.xOffset;
700                    const yOffset = test.yOffset;
701
702                    ctx.fillStyle = '#AAA';
703                    ctx.lineWidth = test.strokeWidth;
704                    ctx.rect(5+xOffset, 5+yOffset, 20, 20);
705                    ctx.arc(15+xOffset, 15+yOffset, 8, 0, Math.PI*2, false);
706                    if (test.fillType) {
707                        ctx.fill(test.fillType);
708                    } else {
709                        ctx.stroke();
710                    }
711
712                    for (const pt of pts) {
713                        let [x, y] = pt;
714                        x += xOffset;
715                        y += yOffset;
716                        // naively apply transform when querying because the points queried
717                        // ignore the CTM.
718                        if (test.testFn(ctx, x, y)) {
719                          drawPoint(ctx, x, y, IN);
720                        } else {
721                          drawPoint(ctx, x, y, OUT);
722                        }
723                    }
724                }
725            });
726        }
727
728        describe('loading custom fonts', () => {
729            const realFontLoaded = new FontFace('BungeeNonSystem', 'url(/assets/Bungee-Regular.ttf)', {
730                'family': 'BungeeNonSystem', // Make sure the canvas does not use the system font
731                'style': 'normal',
732                'weight': '400',
733            }).load().then((font) => {
734                document.fonts.add(font);
735            });
736
737            let fontBuffer = null;
738            const skFontLoaded = fetch('/assets/Bungee-Regular.ttf').then(
739                (response) => response.arrayBuffer()).then(
740                (buffer) => {
741                    fontBuffer = buffer;
742                });
743
744            beforeEach(async () => {
745                await realFontLoaded;
746                await skFontLoaded;
747            });
748
749            multipleCanvasGM('custom_font', (canvas) => {
750                if (canvas.loadFont) {
751                    canvas.loadFont(fontBuffer, {
752                        'family': 'BungeeNonSystem',
753                        'style': 'normal',
754                        'weight': '400',
755                    });
756                }
757                const ctx = canvas.getContext('2d');
758
759                ctx.font = '20px monospace';
760                ctx.fillText('20 px monospace', 10, 30);
761
762                ctx.font = '2.0em BungeeNonSystem';
763                ctx.fillText('2.0em Bungee filled', 10, 80);
764                ctx.strokeText('2.0em Bungee stroked', 10, 130);
765
766                const m = ctx.measureText('A phrase in English');
767                expect(m).toBeTruthy();
768                expect(m['width']).toBeTruthy();
769
770                ctx.font = '40pt monospace';
771                ctx.strokeText('40pt monospace', 10, 200);
772
773                // bold wasn't defined, so should fallback to just the 400 weight
774                ctx.font = 'bold 45px BungeeNonSystem';
775                ctx.fillText('45px Bungee filled', 10, 260);
776            });
777        }); // describe('loading custom fonts')
778
779        it('can read default properties', () => {
780            const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
781            const realCanvas = document.getElementById('test');
782            realCanvas.width = CANVAS_WIDTH;
783            realCanvas.height = CANVAS_HEIGHT;
784
785            const skcontext = skcanvas.getContext('2d');
786            const realContext = realCanvas.getContext('2d');
787            // The skia canvas only comes with a monospace font by default
788            // Set the html canvas to be monospace too.
789            realContext.font = '10px monospace';
790
791            const toTest = ['font', 'lineWidth', 'strokeStyle', 'lineCap',
792                            'lineJoin', 'miterLimit', 'shadowOffsetY',
793                            'shadowBlur', 'shadowColor', 'shadowOffsetX',
794                            'globalAlpha', 'globalCompositeOperation',
795                            'lineDashOffset', 'imageSmoothingEnabled',
796                            'imageFilterQuality'];
797
798            // Compare all the default values of the properties of skcanvas
799            // to the default values on the properties of a real canvas.
800            for(let attr of toTest) {
801                expect(skcontext[attr]).toBe(realContext[attr], attr);
802            }
803
804            skcanvas.dispose();
805        });
806    }); // end describe('CanvasContext2D API')
807
808    describe('Path2D API', () => {
809        multipleCanvasGM('path2d_line_drawing_operations', (canvas) => {
810            const ctx = canvas.getContext('2d');
811            let clock;
812            let path;
813            if (canvas.makePath2D) {
814                clock = canvas.makePath2D('M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z');
815                path = canvas.makePath2D();
816            } else {
817                clock = new Path2D('M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z')
818                path = new Path2D();
819            }
820            path.moveTo(20, 5);
821            path.lineTo(30, 20);
822            path.lineTo(40, 10);
823            path.lineTo(50, 20);
824            path.lineTo(60, 0);
825            path.lineTo(20, 5);
826
827            path.moveTo(20, 80);
828            path.bezierCurveTo(90, 10, 160, 150, 190, 10);
829
830            path.moveTo(36, 148);
831            path.quadraticCurveTo(66, 188, 120, 136);
832            path.lineTo(36, 148);
833
834            path.rect(5, 170, 20, 25);
835
836            path.moveTo(150, 180);
837            path.arcTo(150, 100, 50, 200, 20);
838            path.lineTo(160, 160);
839
840            path.moveTo(20, 120);
841            path.arc(20, 120, 18, 0, 1.75 * Math.PI);
842            path.lineTo(20, 120);
843
844            path.moveTo(150, 5);
845            path.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI)
846
847            ctx.lineWidth = 2;
848            ctx.scale(3.0, 3.0);
849            ctx.stroke(path);
850            ctx.stroke(clock);
851        });
852    }); // end describe('Path2D API')
853});
854