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