1 describe('Canvas 2D emulation', () => {
4 beforeEach(async () => {
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);
14 document.body.removeChild(container);
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);
25 describe('color strings', () => {
27 return parseInt(s, 16);
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));
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));
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));
70 it('properly produces color strings', () => {
71 const colorToString = CanvasKit._testing.colorToString;
73 expect(colorToString(CanvasKit.Color(102, 51, 153, 1.0))).toEqual('#663399');
75 expect(colorToString(CanvasKit.Color(255, 235, 205, 0.5))).toEqual(
76 'rgba(255, 235, 205, 0.50000000)');
79 it('can multiply colors by alpha', () => {
80 const multiplyByAlpha = CanvasKit.multiplyByAlpha;
84 inColor: CanvasKit.Color(102, 51, 153, 1.0),
86 outColor: CanvasKit.Color(102, 51, 153, 1.0),
89 inColor: CanvasKit.Color(102, 51, 153, 1.0),
91 outColor: CanvasKit.Color(102, 51, 153, 0.8),
94 inColor: CanvasKit.Color(102, 51, 153, 0.8),
96 outColor: CanvasKit.Color(102, 51, 153, 0.56),
99 inColor: CanvasKit.Color(102, 51, 153, 0.8),
101 outColor: CanvasKit.Color(102, 51, 153, 1.0),
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));
111 }); // end describe('color string parsing')
113 describe('fonts', () => {
114 it('can parse font sizes', () => {
115 const parseFontString = CanvasKit._testing.parseFontString;
118 'input': '10px monospace',
124 'family': 'monospace',
128 'input': '15pt Arial',
138 'input': '1.5in Arial, san-serif ',
144 'family': 'Arial, san-serif',
148 'input': '1.5em SuperFont',
154 'family': 'SuperFont',
159 for (let i = 0; i < tests.length; i++) {
160 expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
164 it('can parse font attributes', () => {
165 const parseFontString = CanvasKit._testing.parseFontString;
168 'input': 'bold 10px monospace',
174 'family': 'monospace',
178 'input': 'italic bold 10px monospace',
184 'family': 'monospace',
188 'input': 'italic small-caps bold 10px monospace',
191 'variant': 'small-caps',
194 'family': 'monospace',
198 'input': 'small-caps bold 10px monospace',
201 'variant': 'small-caps',
204 'family': 'monospace',
208 'input': 'italic 10px monospace',
214 'family': 'monospace',
218 'input': 'small-caps 10px monospace',
221 'variant': 'small-caps',
224 'family': 'monospace',
228 'input': 'normal bold 10px monospace',
234 'family': 'monospace',
239 for (let i = 0; i < tests.length; i++) {
240 expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
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;
254 console.log('debugging canvaskit');
257 const png = skcanvas.toDataURL();
258 const img = document.createElement('img');
259 document.body.appendChild(img);
267 for (let canvas of [skcanvas, realCanvas]) {
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));
273 Promise.all(promises).then(() => {
276 }).catch(reportError(done));
279 describe('CanvasContext2D API', () => {
280 multipleCanvasGM('all_line_drawing_operations', (canvas) => {
281 const ctx = canvas.getContext('2d');
291 ctx.bezierCurveTo(90, 10, 160, 150, 190, 10);
294 ctx.quadraticCurveTo(66, 188, 120, 136);
297 ctx.rect(5, 170, 20, 25);
299 ctx.moveTo(150, 180);
300 ctx.arcTo(150, 100, 50, 200, 20);
301 ctx.lineTo(160, 160);
304 ctx.arc(20, 120, 18, 0, 1.75 * Math.PI);
308 ctx.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI)
313 // Test edgecases and draw direction
315 ctx.arc(50, 100, 10, Math.PI, -Math.PI/2);
318 ctx.arc(75, 100, 10, Math.PI, -Math.PI/2, true);
321 ctx.arc(100, 100, 10, Math.PI, 100.1 * Math.PI, true);
324 ctx.arc(125, 100, 10, Math.PI, 100.1 * Math.PI, false);
327 ctx.ellipse(155, 100, 10, 15, Math.PI/8, 100.1 * Math.PI, Math.PI, true);
330 ctx.ellipse(180, 100, 10, 15, Math.PI/8, Math.PI, 100.1 * Math.PI, true);
334 multipleCanvasGM('all_matrix_operations', (canvas) => {
335 const ctx = canvas.getContext('2d');
336 ctx.rect(10, 10, 20, 20);
339 ctx.rect(30, 10, 20, 20);
340 ctx.resetTransform();
342 ctx.rotate(Math.PI / 3);
343 ctx.rect(50, 10, 20, 20);
344 ctx.resetTransform();
346 ctx.translate(30, -2);
347 ctx.rect(70, 10, 20, 20);
348 ctx.resetTransform();
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();
357 ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
358 ctx.rect(110, 10, 20, 20);
361 ctx.lineTo(220, 120);
364 ctx.font = '6pt Noto Mono';
365 ctx.fillText('This text should be huge', 10, 80);
366 ctx.resetTransform();
368 ctx.strokeStyle = 'black';
376 ctx.lineTo(280/3, 90/3);
378 ctx.strokeStyle = 'black';
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';
389 ctx.shadowOffsetX = 3;
390 ctx.shadowOffsetY = -8;
391 ctx.rect(10, 10, 30, 30);
394 ctx.strokeStyle = '#C00';
395 ctx.fillStyle = '#00C';
397 ctx.shadowColor = 'transparent';
406 ctx.quadraticCurveTo(66, 188, 120, 136);
411 ctx.shadowColor = '#993366AA';
412 ctx.shadowOffsetX = 8;
414 ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
415 ctx.rect(110, 10, 20, 20);
417 ctx.resetTransform();
418 ctx.lineTo(220, 120);
421 ctx.fillStyle = 'green';
422 ctx.font = '16pt Noto Mono';
423 ctx.fillText('This should be shadowed', 20, 80);
427 ctx.ellipse(10, 290, 30, 30, 0, 0, Math.PI * 2);
430 ctx.ellipse(10, 290, 30, 60, 0, 0, Math.PI * 2);
431 ctx.resetTransform();
432 ctx.shadowColor = '#993366AA';
435 ctx.ellipse(10, 290, 30, 90, 0, 0, Math.PI * 2);
439 multipleCanvasGM('global_dashed_rects', (canvas) => {
440 const ctx = canvas.getContext('2d');
442 ctx.translate(10, 10);
443 // Shouldn't impact the fillRect calls
444 ctx.setLineDash([5, 3]);
446 ctx.fillStyle = 'rgba(200, 0, 100, 0.81)';
447 ctx.fillRect(20, 30, 100, 100);
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;
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);
461 ctx.fillRect(320, 30, 100, 100);
462 ctx.clearRect(330, 40, 80, 80);
464 ctx.strokeStyle = 'blue';
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);
475 ctx.setLineDash([5, 2]);
476 ctx.moveTo(336, 400);
477 ctx.quadraticCurveTo(366, 488, 120, 450);
478 ctx.lineTo(300, 400);
481 ctx.font = '36pt Noto Mono';
482 ctx.strokeText('Dashed', 20, 350);
483 ctx.fillText('Not Dashed', 20, 400);
486 multipleCanvasGM('gradients_clip', (canvas) => {
487 const ctx = canvas.getContext('2d');
489 const rgradient = ctx.createRadialGradient(200, 300, 10, 100, 100, 300);
491 rgradient.addColorStop(0, 'red');
492 rgradient.addColorStop(.7, 'white');
493 rgradient.addColorStop(1, 'blue');
495 ctx.fillStyle = rgradient;
496 ctx.globalAlpha = 0.7;
497 ctx.fillRect(0,0,600,600);
498 ctx.globalAlpha = 0.95;
501 ctx.arc(300, 100, 90, 0, Math.PI*1.66);
503 ctx.strokeStyle = 'yellow';
509 const lgradient = ctx.createLinearGradient(200, 20, 420, 40);
511 lgradient.addColorStop(0, 'green');
512 lgradient.addColorStop(.5, 'cyan');
513 lgradient.addColorStop(1, 'orange');
515 ctx.fillStyle = lgradient;
517 ctx.fillRect(200, 30, 200, 300);
520 ctx.fillRect(550, 550, 40, 40);
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);
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);
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);
548 multipleCanvasGM('shadows_with_rotate_skbug_9947', (canvas) => {
549 const ctx = canvas.getContext('2d');
551 ctx.fillStyle = 'white';
552 ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
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)';
560 ctx.fillStyle = 'black';
561 ctx.strokeStyle = 'red';
563 ctx.rect(-20, -20, 40, 40);
565 ctx.fillRect(30, 30, 40, 40);
566 ctx.strokeRect(30, -20, 40, 40);
567 ctx.fillText('text', -20, -30);
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())
577 skImageData = buffer;
580 const realPromise = fetch('/assets/mandrill_512.png')
581 .then((response) => response.blob())
582 .then((blob) => createImageBitmap(blob))
587 beforeEach(async () => {
592 multipleCanvasGM('draw_patterns', (canvas) => {
593 const ctx = canvas.getContext('2d');
595 if (canvas._config === 'software_canvas') {
596 img = canvas.decodeImage(skImageData);
598 ctx.fillStyle = '#EEE';
599 ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
603 let pattern = ctx.createPattern(img, 'repeat');
604 ctx.fillStyle = pattern;
605 ctx.fillRect(0, 0, 1500, 750);
607 pattern = ctx.createPattern(img, 'repeat-x');
608 ctx.fillStyle = pattern;
609 ctx.fillRect(1500, 0, 3000, 750);
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);
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);
623 multipleCanvasGM('draw_image', (canvas) => {
624 let ctx = canvas.getContext('2d');
626 if (canvas._config === 'software_canvas') {
627 img = canvas.decodeImage(skImageData);
629 ctx.drawImage(img, 30, -200);
631 ctx.globalAlpha = 0.7
633 ctx.imageSmoothingQuality = 'medium';
634 ctx.drawImage(img, 200, 350, 150, 100);
636 ctx.imageSmoothingEnabled = false;
637 ctx.drawImage(img, 100, 150, 400, 350, 10, 400, 150, 100);
639 }); // end describe('using images')
642 const drawPoint = (ctx, x, y, color) => {
643 ctx.fillStyle = color;
644 ctx.fillRect(x, y, 1, 1);
647 const OUT = 'orange';
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]];
661 testFn: (ctx, x, y) => ctx.isPointInPath(x * SCALE, y * SCALE, 'nonzero'),
668 testFn: (ctx, x, y) => ctx.isPointInPath(x * SCALE, y * SCALE, 'evenodd'),
675 testFn: (ctx, x, y) => ctx.isPointInStroke(x * SCALE, y * SCALE),
682 testFn: (ctx, x, y) => ctx.isPointInStroke(x * SCALE, y * SCALE),
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);
695 // Scale up to make single pixels easier to see
696 ctx.scale(SCALE, SCALE);
697 for (const test of tests) {
699 const xOffset = test.xOffset;
700 const yOffset = test.yOffset;
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);
707 ctx.fill(test.fillType);
712 for (const pt of pts) {
716 // naively apply transform when querying because the points queried
718 if (test.testFn(ctx, x, y)) {
719 drawPoint(ctx, x, y, IN);
721 drawPoint(ctx, x, y, OUT);
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
733 }).load().then((font) => {
734 document.fonts.add(font);
737 let fontBuffer = null;
738 const skFontLoaded = fetch('/assets/Bungee-Regular.ttf').then(
739 (response) => response.arrayBuffer()).then(
744 beforeEach(async () => {
745 await realFontLoaded;
749 multipleCanvasGM('custom_font', (canvas) => {
750 if (canvas.loadFont) {
751 canvas.loadFont(fontBuffer, {
752 'family': 'BungeeNonSystem',
757 const ctx = canvas.getContext('2d');
759 ctx.font = '20px monospace';
760 ctx.fillText('20 px monospace', 10, 30);
762 ctx.font = '2.0em BungeeNonSystem';
763 ctx.fillText('2.0em Bungee filled', 10, 80);
764 ctx.strokeText('2.0em Bungee stroked', 10, 130);
766 const m = ctx.measureText('A phrase in English');
767 expect(m).toBeTruthy();
768 expect(m['width']).toBeTruthy();
770 ctx.font = '40pt monospace';
771 ctx.strokeText('40pt monospace', 10, 200);
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);
777 }); // describe('loading custom fonts')
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;
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';
791 const toTest = ['font', 'lineWidth', 'strokeStyle', 'lineCap',
792 'lineJoin', 'miterLimit', 'shadowOffsetY',
793 'shadowBlur', 'shadowColor', 'shadowOffsetX',
794 'globalAlpha', 'globalCompositeOperation',
795 'lineDashOffset', 'imageSmoothingEnabled',
796 'imageFilterQuality'];
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);
806 }); // end describe('CanvasContext2D API')
808 describe('Path2D API', () => {
809 multipleCanvasGM('path2d_line_drawing_operations', (canvas) => {
810 const ctx = canvas.getContext('2d');
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();
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')
828 path.bezierCurveTo(90, 10, 160, 150, 190, 10);
830 path.moveTo(36, 148);
831 path.quadraticCurveTo(66, 188, 120, 136);
832 path.lineTo(36, 148);
834 path.rect(5, 170, 20, 25);
836 path.moveTo(150, 180);
837 path.arcTo(150, 100, 50, 200, 20);
838 path.lineTo(160, 160);
840 path.moveTo(20, 120);
841 path.arc(20, 120, 18, 0, 1.75 * Math.PI);
842 path.lineTo(20, 120);
845 path.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI)
852 }); // end describe('Path2D API')