layers.yaml (44498B)
1 - name: 2d.layer.global-states 2 desc: Checks that layers correctly use global render states. 3 size: [90, 90] 4 code: | 5 {{ transform_statement }} 6 7 ctx.fillStyle = 'rgba(128, 128, 128, 1)'; 8 ctx.fillRect(20, 15, 50, 50); 9 10 {{ alpha_statement }} 11 {{ composite_op_statement }} 12 {{ shadow_statement }} 13 {{ filter_statement }} 14 15 ctx.beginLayer(); 16 17 // Enable compositing in the layer to validate that draw calls in the layer 18 // won't individually composite with the background. 19 ctx.globalCompositeOperation = 'screen'; 20 21 ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 22 ctx.fillRect(10, 25, 40, 45); 23 ctx.fillStyle = 'rgba(0, 255, 0, 1)'; 24 ctx.fillRect(30, 5, 45, 40); 25 26 ctx.endLayer(); 27 reference: | 28 {{ transform_statement }} 29 30 ctx.fillStyle = 'rgba(128, 128, 128, 1)'; 31 ctx.fillRect(20, 15, 50, 50); 32 33 {{ alpha_statement }} 34 {{ composite_op_statement }} 35 {{ shadow_statement }} 36 {{ filter_statement }} 37 38 const canvas2 = document.createElement("canvas"); 39 const ctx2 = canvas2.getContext("2d"); 40 41 ctx2.globalCompositeOperation = 'screen'; 42 ctx2.fillStyle = 'rgba(255, 0, 0, 1)'; 43 ctx2.fillRect(10, 25, 40, 45); 44 ctx2.fillStyle = 'rgba(0, 255, 0, 1)'; 45 ctx2.fillRect(30, 5, 45, 40); 46 47 ctx.drawImage(canvas2, 0, 0); 48 variants_layout: 49 [single_file, single_file, single_file, multi_files, multi_files] 50 grid_width: 8 51 variants: &global-state-variants 52 - no-globalAlpha: 53 alpha_statement: // No globalAlpha. 54 globalAlpha: 55 alpha_statement: ctx.globalAlpha = 0.75; 56 - no-composite-op: 57 composite_op_statement: // No globalCompositeOperation. 58 blending: 59 composite_op_statement: ctx.globalCompositeOperation = 'multiply'; 60 composite: 61 composite_op_statement: ctx.globalCompositeOperation = 'source-in'; 62 copy: 63 composite_op_statement: ctx.globalCompositeOperation = 'copy'; 64 - no-shadow: 65 shadow_statement: // No shadow. 66 shadow: 67 shadow_statement: |- 68 ctx.shadowOffsetX = -7; 69 ctx.shadowOffsetY = 7; 70 ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; 71 ctx.shadowBlur = 3; 72 - no-cxt-filter: 73 filter_statement: // No filter. 74 ctx-filter: 75 filter_statement: ctx.filter = 'drop-shadow(-4px -4px 0 fuchsia)'; 76 - no-transform: 77 transform_statement: // No transform. 78 rotation: 79 transform_statement: |- 80 ctx.translate(50, 40); 81 ctx.rotate(Math.PI); 82 ctx.translate(-45, -45); 83 84 85 - name: >- 86 2d.layer.global-states.filter.{{ file_variant_name }}.tentative 87 desc: Checks that layers with filters correctly use global render states. 88 size: [90, 90] 89 code: | 90 {{ transform_statement }} 91 92 ctx.fillStyle = 'rgba(128, 128, 128, 1)'; 93 ctx.fillRect(20, 15, 50, 50); 94 95 {{ alpha_statement }} 96 {{ composite_op_statement }} 97 {{ shadow_statement }} 98 {{ filter_statement }} 99 100 ctx.beginLayer({filter: [ 101 {name: 'dropShadow', 102 dx: 8, dy: 8, stdDeviation: 0, floodColor: '#00f'}, 103 {name: 'componentTransfer', 104 funcA: {type: "table", tableValues: [0, 0.8]}}]}); 105 106 ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 107 ctx.fillRect(10, 25, 40, 45); 108 ctx.fillStyle = 'rgba(0, 255, 0, 1)'; 109 ctx.fillRect(30, 5, 45, 40); 110 111 ctx.endLayer(); 112 reference: | 113 const svg = ` 114 <svg xmlns="http://www.w3.org/2000/svg" 115 width="{{ size[0] }}" height="{{ size[1] }}" 116 color-interpolation-filters="sRGB"> 117 <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> 118 <feDropShadow dx="8" dy="8" stdDeviation="0" flood-color="#00f" /> 119 <feComponentTransfer> 120 <feFuncA type="table" tableValues="0 0.8"></feFuncA> 121 </feComponentTransfer> 122 </filter> 123 <g filter="url(#filter)"> 124 <rect x="10" y="25" width="40" height="45" fill="rgba(255, 0, 0, 1)"/> 125 <rect x="30" y="5" width="45" height="40" fill="rgba(0, 255, 0, 1)"/> 126 </g> 127 </svg>`; 128 129 const img = new Image(); 130 img.width = {{ size[0] }}; 131 img.height = {{ size[1] }}; 132 img.onload = () => { 133 {{ transform_statement | indent(2) }} 134 135 ctx.fillStyle = 'rgba(128, 128, 128, 1)'; 136 ctx.fillRect(20, 15, 50, 50); 137 138 {{ alpha_statement | indent(2) }} 139 {{ composite_op_statement | indent(2) }} 140 {{ shadow_statement | indent(2) }} 141 {{ filter_statement | indent(2) }} 142 143 ctx.drawImage(img, 0, 0); 144 }; 145 img.src = 'data:image/svg+xml;base64,' + btoa(svg); 146 append_variants_to_name: false 147 variants_layout: 148 [single_file, single_file, single_file, multi_files, multi_files] 149 grid_width: 8 150 variants: *global-state-variants 151 152 - name: 2d.layer.globalCompositeOperation 153 desc: Checks that layers work with all globalCompositeOperation values. 154 size: [90, 90] 155 code: | 156 ctx.translate(50, 50); 157 ctx.scale(2, 2); 158 ctx.rotate(Math.PI); 159 ctx.translate(-25, -25); 160 161 ctx.fillStyle = 'rgba(0, 0, 255, 0.8)'; 162 ctx.fillRect(15, 15, 25, 25); 163 164 ctx.globalAlpha = 0.75; 165 ctx.globalCompositeOperation = '{{ variant_name }}'; 166 ctx.shadowOffsetX = 7; 167 ctx.shadowOffsetY = 7; 168 ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; 169 170 ctx.beginLayer(); 171 172 ctx.fillStyle = 'rgba(204, 0, 0, 1)'; 173 ctx.fillRect(10, 25, 25, 20); 174 ctx.fillStyle = 'rgba(0, 204, 0, 1)'; 175 ctx.fillRect(25, 10, 20, 25); 176 177 ctx.endLayer(); 178 reference: | 179 ctx.translate(50, 50); 180 ctx.scale(2, 2); 181 ctx.rotate(Math.PI); 182 ctx.translate(-25, -25); 183 184 ctx.fillStyle = 'rgba(0, 0, 255, 0.8)'; 185 ctx.fillRect(15, 15, 25, 25); 186 187 ctx.globalAlpha = 0.75; 188 ctx.globalCompositeOperation = '{{ variant_name }}'; 189 ctx.shadowOffsetX = 7; 190 ctx.shadowOffsetY = 7; 191 ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; 192 193 const canvas2 = document.createElement("canvas"); 194 const ctx2 = canvas2.getContext("2d"); 195 196 ctx2.fillStyle = 'rgba(204, 0, 0, 1)'; 197 ctx2.fillRect(10, 25, 25, 20); 198 ctx2.fillStyle = 'rgba(0, 204, 0, 1)'; 199 ctx2.fillRect(25, 10, 20, 25); 200 201 ctx.imageSmoothingEnabled = false; 202 ctx.drawImage(canvas2, 0, 0); 203 variants_layout: [single_file] 204 grid_width: 7 205 variants: 206 - source-over: 207 source-in: 208 source-out: 209 source-atop: 210 destination-over: 211 destination-in: 212 destination-out: 213 destination-atop: 214 lighter: 215 copy: 216 xor: 217 multiply: 218 screen: 219 overlay: 220 darken: 221 lighten: 222 color-dodge: 223 color-burn: 224 hard-light: 225 soft-light: 226 difference: 227 exclusion: 228 hue: 229 saturation: 230 color: 231 luminosity: 232 233 - name: 2d.layer.non-invertible-matrix 234 desc: Test drawing layers when the transform is not invertible. 235 size: [200, 200] 236 code: | 237 ctx.fillStyle = 'blue'; 238 ctx.fillRect(30, 30, 50, 50); 239 240 // Anything below will be non-rasterizable. 241 ctx.scale(1, 0); 242 243 // Open the layer with a non-invertible matrix. The whole layer will be 244 // non-rasterizable. 245 ctx.beginLayer(); 246 247 // Because the transform is global, the matrix is still non-invertible. 248 ctx.fillStyle = 'rgba(225, 0, 0, 1)'; 249 ctx.fillRect(40, 70, 50, 50); 250 251 // Set a valid matrix in the middle of the layer. This makes the global 252 // matrix invertible again, but because because the layer was opened with a 253 // non-invertible matrix, the whole layer remains non-rasterizable. 254 ctx.setTransform(1, 0, 0, 1, 0, 0); 255 256 ctx.fillStyle = 'green'; 257 ctx.fillRect(70, 40, 50, 50); 258 259 ctx.endLayer(); 260 reference: | 261 ctx.fillStyle = 'blue'; 262 ctx.fillRect(30, 30, 50, 50); 263 264 - name: 2d.layer.non-invertible-matrix.shadow 265 desc: Test drawing layers when the transform is not invertible. 266 size: [200, 200] 267 code: | 268 ctx.fillStyle = 'blue'; 269 ctx.fillRect(30, 30, 50, 50); 270 271 // Anything below will be non-rasterizable. 272 ctx.scale(1, 0); 273 274 ctx.shadowOffsetX = 0; 275 ctx.shadowOffsetY = 20; 276 ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; 277 278 // Open the layer with a non-invertible matrix. The whole layer will be 279 // non-rasterizable. 280 ctx.beginLayer(); 281 282 // Because the transform is global, the matrix is still non-invertible. 283 ctx.fillStyle = 'rgba(225, 0, 0, 1)'; 284 ctx.fillRect(40, 70, 50, 50); 285 286 // Set a valid matrix in the middle of the layer. This makes the global 287 // matrix invertible again, but because because the layer was opened with a 288 // non-invertible matrix, the whole layer remains non-rasterizable. 289 ctx.setTransform(1, 0, 0, 1, 0, 0); 290 291 ctx.fillStyle = 'green'; 292 ctx.fillRect(70, 40, 50, 50); 293 294 ctx.endLayer(); 295 reference: | 296 ctx.fillStyle = 'blue'; 297 ctx.fillRect(30, 30, 50, 50); 298 299 - name: >- 300 2d.layer.non-invertible-matrix.with-render-states.{{ file_variant_name }}{{ 301 tentative }} 302 desc: Test drawing layers when the transform is not invertible. 303 size: [200, 200] 304 code: | 305 ctx.fillStyle = 'blue'; 306 ctx.fillRect(30, 30, 50, 50); 307 308 // Anything below will be non-rasterizable. 309 ctx.scale(1, 0); 310 311 ctx.globalAlpha = 0.8; 312 ctx.globalCompositeOperation = 'multiply'; 313 ctx.shadowOffsetX = 0; 314 ctx.shadowOffsetY = 20; 315 ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; 316 ctx.filter = 'blur(10px)'; 317 318 // Open the layer with a non-invertible matrix. The whole layer will be 319 // non-rasterizable. 320 {{ begin_layer }} 321 322 // Because the transform is global, the matrix is still non-invertible. 323 ctx.fillStyle = 'rgba(225, 0, 0, 1)'; 324 ctx.fillRect(40, 70, 50, 50); 325 326 // Set a valid matrix in the middle of the layer. This makes the global 327 // matrix invertible again, but because because the layer was opened with a 328 // non-invertible matrix, the whole layer remains non-rasterizable. 329 ctx.setTransform(1, 0, 0, 1, 0, 0); 330 331 ctx.fillStyle = 'green'; 332 ctx.fillRect(70, 40, 50, 50); 333 334 ctx.endLayer(); 335 reference: | 336 ctx.fillStyle = 'blue'; 337 ctx.fillRect(30, 30, 50, 50); 338 append_variants_to_name: false 339 variants: 340 - plain-layer: 341 begin_layer: ctx.beginLayer(); 342 filtered-layer: 343 begin_layer: |- 344 ctx.beginLayer({filter: 345 {name: 'dropShadow', dx: 10, dy: 0, stdDeviation: 0, 346 floodColor: 'rgba(128, 128, 255, 0.7)'}}); 347 tentative: .tentative 348 349 - name: 2d.layer.nested 350 desc: Tests nested canvas layers. 351 size: [200, 200] 352 code: | 353 var circle = new Path2D(); 354 circle.arc(90, 90, 40, 0, 2 * Math.PI); 355 ctx.fill(circle); 356 357 ctx.globalCompositeOperation = 'source-in'; 358 359 ctx.beginLayer(); 360 361 ctx.fillStyle = 'rgba(0, 0, 255, 1)'; 362 ctx.fillRect(60, 60, 75, 50); 363 364 ctx.globalAlpha = 0.5; 365 366 ctx.beginLayer(); 367 368 ctx.fillStyle = 'rgba(225, 0, 0, 1)'; 369 ctx.fillRect(50, 50, 75, 50); 370 ctx.fillStyle = 'rgba(0, 255, 0, 1)'; 371 ctx.fillRect(70, 70, 75, 50); 372 373 ctx.endLayer(); 374 ctx.endLayer(); 375 reference: | 376 var circle = new Path2D(); 377 circle.arc(90, 90, 40, 0, 2 * Math.PI); 378 ctx.fill(circle); 379 380 ctx.globalCompositeOperation = 'source-in'; 381 382 canvas2 = document.createElement("canvas"); 383 ctx2 = canvas2.getContext("2d"); 384 385 ctx2.fillStyle = 'rgba(0, 0, 255, 1)'; 386 ctx2.fillRect(60, 60, 75, 50); 387 388 ctx2.globalAlpha = 0.5; 389 390 canvas3 = document.createElement("canvas"); 391 ctx3 = canvas3.getContext("2d"); 392 393 ctx3.fillStyle = 'rgba(225, 0, 0, 1)'; 394 ctx3.fillRect(50, 50, 75, 50); 395 ctx3.fillStyle = 'rgba(0, 255, 0, 1)'; 396 ctx3.fillRect(70, 70, 75, 50); 397 398 ctx2.drawImage(canvas3, 0, 0); 399 ctx.drawImage(canvas2, 0, 0); 400 401 402 - name: 2d.layer.restore-style 403 desc: Test that ensure layers restores style values upon endLayer. 404 size: [200, 200] 405 fuzzy: maxDifference=0-1; totalPixels=0-950 406 code: | 407 ctx.fillStyle = 'rgba(0,0,255,1)'; 408 ctx.fillRect(50, 50, 75, 50); 409 ctx.globalAlpha = 0.5; 410 411 ctx.beginLayer(); 412 ctx.fillStyle = 'rgba(225, 0, 0, 1)'; 413 ctx.fillRect(60, 60, 75, 50); 414 ctx.endLayer(); 415 416 ctx.fillRect(70, 70, 75, 50); 417 reference: | 418 ctx.fillStyle = 'rgba(0, 0, 255, 1)'; 419 ctx.fillRect(50, 50, 75, 50); 420 ctx.globalAlpha = 0.5; 421 422 canvas2 = document.createElement("canvas"); 423 ctx2 = canvas2.getContext("2d"); 424 ctx2.fillStyle = 'rgba(225, 0, 0, 1)'; 425 ctx2.fillRect(60, 60, 75, 50); 426 ctx.drawImage(canvas2, 0, 0); 427 428 ctx.fillRect(70, 70, 75, 50); 429 430 - name: 2d.layer.layer-rendering-state-reset-in-layer 431 desc: Tests that rendering states are reset in layers and restored after. 432 test_type: sync 433 code: | 434 ctx.globalAlpha = 0.5; 435 ctx.globalCompositeOperation = 'xor'; 436 ctx.shadowColor = '#0000ff'; 437 ctx.shadowOffsetX = 10; 438 ctx.shadowOffsetY = 20; 439 ctx.shadowBlur = 30; 440 ctx.filter = 'blur(5px)'; 441 442 @assert ctx.globalAlpha === 0.5; 443 @assert ctx.globalCompositeOperation === 'xor'; 444 @assert ctx.shadowColor === '#0000ff'; 445 @assert ctx.shadowOffsetX === 10; 446 @assert ctx.shadowOffsetY === 20; 447 @assert ctx.shadowBlur === 30; 448 @assert ctx.filter === 'blur(5px)'; 449 450 ctx.beginLayer(); 451 452 @assert ctx.globalAlpha === 1.0; 453 @assert ctx.globalCompositeOperation === 'source-over'; 454 @assert ctx.shadowColor === 'rgba(0, 0, 0, 0)'; 455 @assert ctx.shadowOffsetX === 0; 456 @assert ctx.shadowOffsetY === 0; 457 @assert ctx.shadowBlur === 0; 458 @assert ctx.filter === 'none'; 459 460 ctx.endLayer(); 461 462 @assert ctx.globalAlpha === 0.5; 463 @assert ctx.globalCompositeOperation === 'xor'; 464 @assert ctx.shadowColor === '#0000ff'; 465 @assert ctx.shadowOffsetX === 10; 466 @assert ctx.shadowOffsetY === 20; 467 @assert ctx.shadowBlur === 30; 468 @assert ctx.filter === 'blur(5px)'; 469 470 - name: 2d.layer.clip-outside.{{ file_variant_name }}{{ tentative }} 471 desc: Check clipping set outside the layer 472 size: [100, 100] 473 code: | 474 ctx.beginPath(); 475 ctx.rect(15, 15, 70, 70); 476 ctx.clip(); 477 478 {{ filtered_layer }} 479 ctx.fillStyle = 'blue'; 480 ctx.fillRect(10, 10, 80, 80); 481 ctx.endLayer(); 482 reference: | 483 const canvas2 = new OffscreenCanvas(200, 200); 484 const ctx2 = canvas2.getContext('2d'); 485 486 ctx2.filter = 'blur(12px)'; 487 ctx2.beginLayer(); 488 ctx2.fillStyle = 'blue'; 489 ctx2.fillRect(10, 10, 80, 80); 490 ctx2.endLayer(); 491 492 ctx.beginPath(); 493 ctx.rect(15, 15, 70, 70); 494 ctx.clip(); 495 496 ctx.drawImage(canvas2, 0, 0); 497 append_variants_to_name: false 498 variants: 499 - ctx-filter: 500 filtered_layer: |- 501 ctx.filter = 'blur(12px)'; 502 ctx.beginLayer(); 503 layer-filter: 504 filtered_layer: |- 505 ctx.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); 506 tentative: .tentative 507 508 - name: 2d.layer.clip-inside.{{ file_variant_name }}{{ tentative }} 509 desc: Check clipping set inside the layer 510 size: [100, 100] 511 code: | 512 {{ filtered_layer }} 513 514 ctx.beginPath(); 515 ctx.rect(15, 15, 70, 70); 516 ctx.clip(); 517 518 ctx.fillStyle = 'blue'; 519 ctx.fillRect(10, 10, 80, 80); 520 ctx.endLayer(); 521 reference: | 522 const canvas2 = new OffscreenCanvas(200, 200); 523 const ctx2 = canvas2.getContext('2d'); 524 525 ctx2.beginPath(); 526 ctx2.rect(15, 15, 70, 70); 527 ctx2.clip(); 528 529 ctx2.fillStyle = 'blue'; 530 ctx2.fillRect(10, 10, 80, 80); 531 532 ctx.filter = 'blur(12px)'; 533 ctx.beginLayer(); 534 ctx.drawImage(canvas2, 0, 0); 535 ctx.endLayer(); 536 append_variants_to_name: false 537 variants: 538 - ctx-filter: 539 filtered_layer: |- 540 ctx.filter = 'blur(12px)'; 541 ctx.beginLayer(); 542 layer-filter: 543 filtered_layer: |- 544 ctx.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); 545 tentative: .tentative 546 547 - name: 2d.layer.clip-inside-and-outside.{{ file_variant_name }}{{ tentative }} 548 desc: Check clipping set inside and outside the layer 549 size: [100, 100] 550 code: | 551 ctx.beginPath(); 552 ctx.rect(15, 15, 70, 70); 553 ctx.clip(); 554 555 {{ filtered_layer }} 556 557 ctx.beginPath(); 558 ctx.rect(15, 15, 70, 70); 559 ctx.clip(); 560 561 ctx.fillStyle = 'blue'; 562 ctx.fillRect(10, 10, 80, 80); 563 ctx.endLayer(); 564 reference: | 565 const canvas2 = new OffscreenCanvas(200, 200); 566 const ctx2 = canvas2.getContext('2d'); 567 568 ctx2.beginPath(); 569 ctx2.rect(15, 15, 70, 70); 570 ctx2.clip(); 571 572 ctx2.fillStyle = 'blue'; 573 ctx2.fillRect(10, 10, 80, 80); 574 575 const canvas3 = new OffscreenCanvas(200, 200); 576 const ctx3 = canvas3.getContext('2d'); 577 578 ctx3.filter = 'blur(12px)'; 579 ctx3.beginLayer(); 580 ctx3.drawImage(canvas2, 0, 0); 581 ctx3.endLayer(); 582 583 ctx.beginPath(); 584 ctx.rect(15, 15, 70, 70); 585 ctx.clip(); 586 ctx.drawImage(canvas3, 0, 0); 587 append_variants_to_name: false 588 variants: 589 - ctx-filter: 590 filtered_layer: |- 591 ctx.filter = 'blur(12px)'; 592 ctx.beginLayer(); 593 layer-filter: 594 filtered_layer: |- 595 ctx.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); 596 tentative: .tentative 597 598 - name: 2d.layer.ctm.layer-filter.tentative 599 desc: Checks that parent transforms affect layer filters. 600 size: [200, 200] 601 code: | 602 // Transforms inside the layer should not apply to the layer's filter. 603 ctx.beginLayer({filter: 'drop-shadow(5px 5px 0px grey)'}); 604 ctx.translate(30, 90); 605 ctx.scale(2, 2); 606 ctx.rotate(Math.PI / 2); 607 ctx.fillRect(-30, -5, 60, 10); 608 ctx.endLayer(); 609 610 // Transforms in the layer's parent should apply to the layer's filter. 611 ctx.translate(80, 90); 612 ctx.scale(2, 2); 613 ctx.rotate(Math.PI / 2); 614 ctx.beginLayer({filter: 'drop-shadow(5px 5px 0px grey)'}); 615 ctx.fillRect(-30, -5, 60, 10); 616 ctx.endLayer(); 617 html_reference: | 618 <svg xmlns="http://www.w3.org/2000/svg" 619 width="{{ size[0] }}" height="{{ size[1] }}" 620 color-interpolation-filters="sRGB"> 621 <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> 622 <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="grey" /> 623 </filter> 624 625 <g filter="url(#filter)"> 626 <g transform="translate(30, 90) scale(2) rotate(90)"> 627 <rect x="-30" y="-5" width=60 height=10></rect> 628 </g> 629 </g> 630 631 <g transform="translate(80, 90) scale(2) rotate(90)"> 632 <g filter="url(#filter)"> 633 <rect x="-30" y="-5" width=60 height=10></rect> 634 </g> 635 </g> 636 </svg> 637 638 - name: 2d.layer.ctm.ctx-filter 639 desc: Checks that parent transforms don't affect context filters. 640 size: [200, 200] 641 code: | 642 ctx.fillStyle = 'grey'; 643 ctx.translate(30, 90); 644 ctx.scale(2, 2); 645 ctx.rotate(Math.PI / 2); 646 647 // The transform doesn't apply to context filter on normal draw calls. 648 ctx.save(); 649 ctx.filter = 'drop-shadow(10px 10px 0px red)'; 650 ctx.fillRect(-30, -5, 60, 10); 651 ctx.restore(); 652 653 // Likewise, the transform doesn't apply to context filter applied on layer. 654 ctx.save(); 655 ctx.filter = 'drop-shadow(10px 10px 0px green)'; 656 ctx.beginLayer(); 657 ctx.fillRect(-30, -25, 60, 10); 658 ctx.endLayer(); 659 ctx.restore(); 660 661 // Filters inside layers aren't affected by transform either. 662 ctx.beginLayer(); 663 ctx.filter = 'drop-shadow(5px 5px 0px blue)'; 664 ctx.fillRect(-30, -45, 60, 10); 665 ctx.endLayer(); 666 reference: | 667 ctx.fillStyle = 'red'; 668 ctx.fillRect(30, 40, 20, 120); 669 ctx.fillStyle = 'grey'; 670 ctx.fillRect(20, 30, 20, 120); 671 672 ctx.fillStyle = 'green'; 673 ctx.fillRect(70, 40, 20, 120); 674 ctx.fillStyle = 'grey'; 675 ctx.fillRect(60, 30, 20, 120); 676 677 ctx.fillStyle = 'blue'; 678 ctx.fillRect(105, 35, 20, 120); 679 ctx.fillStyle = 'grey'; 680 ctx.fillRect(100, 30, 20, 120); 681 682 - name: 2d.layer.ctm.shadow-in-transformed-layer 683 desc: Check shadows inside of a transformed layer. 684 size: [200, 200] 685 code: | 686 ctx.translate(80, 90); 687 ctx.scale(2, 2); 688 ctx.rotate(Math.PI / 2); 689 690 ctx.beginLayer(); 691 ctx.shadowOffsetX = 10; 692 ctx.shadowOffsetY = 10; 693 ctx.shadowColor = 'grey'; 694 ctx.fillRect(-30, -5, 60, 10); 695 696 const canvas2 = new OffscreenCanvas(100, 100); 697 const ctx2 = canvas2.getContext('2d'); 698 ctx2.fillStyle = 'blue'; 699 ctx2.fillRect(0, 0, 40, 10); 700 ctx.drawImage(canvas2, -30, -30); 701 702 ctx.endLayer(); 703 reference: | 704 ctx.translate(80, 90); 705 ctx.scale(2, 2); 706 ctx.rotate(Math.PI / 2); 707 708 ctx.shadowOffsetX = 10; 709 ctx.shadowOffsetY = 10; 710 ctx.shadowColor = 'grey'; 711 ctx.fillRect(-30, -5, 60, 10); 712 713 const canvas2 = new OffscreenCanvas(100, 100); 714 const ctx2 = canvas2.getContext('2d'); 715 ctx2.fillStyle = 'blue'; 716 ctx2.fillRect(0, 0, 40, 10); 717 ctx.drawImage(canvas2, -30, -30); 718 719 - name: 2d.layer.ctm.getTransform 720 desc: Tests getTransform inside layers. 721 test_type: sync 722 code: | 723 ctx.translate(10, 20); 724 ctx.beginLayer(); 725 ctx.scale(2, 3); 726 const m = ctx.getTransform(); 727 assert_array_equals([m.a, m.b, m.c, m.d, m.e, m.f], [2, 0, 0, 3, 10, 20]); 728 ctx.endLayer(); 729 730 - name: 2d.layer.ctm.setTransform 731 desc: Tests setTransform inside layers. 732 code: | 733 ctx.translate(80, 0); 734 735 ctx.beginLayer(); 736 ctx.rotate(2); 737 ctx.beginLayer(); 738 ctx.scale(5, 6); 739 ctx.setTransform(4, 0, 0, 2, 20, 10); 740 ctx.fillStyle = 'blue'; 741 ctx.fillRect(0, 0, 10, 10); 742 ctx.endLayer(); 743 ctx.endLayer(); 744 745 ctx.fillStyle = 'green'; 746 ctx.fillRect(0, 0, 20, 20); 747 reference: | 748 ctx.translate(80, 0); 749 ctx.fillStyle = 'green'; 750 ctx.fillRect(0, 0, 20, 20); 751 752 ctx.setTransform(4, 0, 0, 2, 20, 10); 753 ctx.fillStyle = 'blue'; 754 ctx.fillRect(0, 0, 10, 10); 755 756 - name: 2d.layer.ctm.resetTransform 757 desc: Tests resetTransform inside layers. 758 code: | 759 ctx.translate(40, 0); 760 761 ctx.beginLayer(); 762 ctx.rotate(2); 763 ctx.beginLayer(); 764 ctx.scale(5, 6); 765 ctx.resetTransform(); 766 ctx.fillStyle = 'blue'; 767 ctx.fillRect(0, 0, 20, 20); 768 ctx.endLayer(); 769 ctx.endLayer(); 770 771 ctx.fillStyle = 'green'; 772 ctx.fillRect(0, 0, 20, 20); 773 reference: | 774 ctx.fillStyle = 'blue'; 775 ctx.fillRect(0, 0, 20, 20); 776 777 ctx.translate(40, 0); 778 ctx.fillStyle = 'green'; 779 ctx.fillRect(0, 0, 20, 20); 780 781 - name: 2d.layer.flush-on-frame-presentation.{{ file_variant_name }}{{ tentative }} 782 desc: Check that layers state stack is flushed and rebuilt on frame renders. 783 size: [200, 200] 784 canvas_types: ['HtmlCanvas'] 785 test_type: promise 786 code: | 787 ctx.fillStyle = 'purple'; 788 ctx.fillRect(60, 60, 75, 50); 789 ctx.globalAlpha = 0.5; 790 ctx.filter = 'blur(1px)'; 791 792 {{ begin_layer }} 793 ctx.fillRect(40, 40, 75, 50); 794 ctx.fillStyle = 'grey'; 795 ctx.fillRect(50, 50, 75, 50); 796 797 // Force a flush and restoration of the state stack: 798 await new Promise(resolve => requestAnimationFrame(resolve)); 799 800 ctx.fillRect(70, 70, 75, 50); 801 ctx.fillStyle = 'orange'; 802 ctx.fillRect(80, 80, 75, 50); 803 ctx.endLayer(); 804 805 ctx.fillRect(80, 40, 75, 50); 806 reference: | 807 ctx.fillStyle = 'purple'; 808 ctx.fillRect(60, 60, 75, 50); 809 ctx.globalAlpha = 0.5; 810 ctx.filter = 'blur(1px)'; 811 812 {{ begin_layer }} 813 ctx.fillStyle = 'purple'; 814 ctx.fillRect(40, 40, 75, 50); 815 ctx.fillStyle = 'grey'; 816 ctx.fillRect(50, 50, 75, 50); 817 818 ctx.fillStyle = 'grey'; 819 ctx.fillRect(70, 70, 75, 50); 820 ctx.fillStyle = 'orange'; 821 ctx.fillRect(80, 80, 75, 50); 822 ctx.endLayer(); 823 824 ctx.fillRect(80, 40, 75, 50); 825 append_variants_to_name: false 826 variants: 827 - plain_layer: 828 begin_layer: ctx.beginLayer(); 829 filtered_layer: 830 begin_layer: |- 831 ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); 832 tentative: .tentative 833 834 - name: 2d.layer.malformed-operations 835 desc: Throws if {{ variant_name }} is called while layers are open. 836 size: [200, 200] 837 test_type: sync 838 code: | 839 {{ setup }} 840 // Shouldn't throw on its own. 841 {{ operation }}; 842 // Make sure the exception isn't caused by calling the function twice. 843 {{ operation }}; 844 // Calling again inside a layer should throw. 845 ctx.beginLayer(); 846 assert_throws_dom("InvalidStateError", 847 () => {{ operation }}); 848 variants_layout: [single_file] 849 variants: 850 - createPattern: 851 operation: ctx.createPattern(canvas, 'repeat') 852 drawImage: 853 setup: |- 854 const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); 855 const ctx2 = canvas2.getContext('2d'); 856 operation: |- 857 ctx2.drawImage(canvas, 0, 0) 858 getImageData: 859 operation: ctx.getImageData(0, 0, {{ size[0] }}, {{ size[1] }}) 860 putImageData: 861 setup: |- 862 const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); 863 const ctx2 = canvas2.getContext('2d') 864 const data = ctx2.getImageData(0, 0, 1, 1); 865 operation: |- 866 ctx.putImageData(data, 0, 0) 867 toDataURL: 868 canvas_types: ['HtmlCanvas'] 869 operation: canvas.toDataURL() 870 transferToImageBitmap: 871 canvas_types: ['OffscreenCanvas', 'Worker'] 872 operation: canvas.transferToImageBitmap() 873 874 - name: 2d.layer.malformed-operations-with-promises 875 desc: Throws if {{ variant_name }} is called while layers are open. 876 size: [200, 200] 877 test_type: promise 878 code: | 879 // Shouldn't throw on its own. 880 await {{ operation }}; 881 // Make sure the exception isn't caused by calling the function twice. 882 await {{ operation }}; 883 // Calling again inside a layer should throw. 884 ctx.beginLayer(); 885 await promise_rejects_dom(t, 'InvalidStateError', 886 {{ operation }}); 887 variants_layout: [single_file] 888 variants: 889 - convertToBlob: 890 canvas_types: ['OffscreenCanvas', 'Worker'] 891 operation: |- 892 canvas.convertToBlob() 893 createImageBitmap: 894 operation: createImageBitmap(canvas) 895 toBlob: 896 canvas_types: ['HtmlCanvas'] 897 operation: |- 898 new Promise(resolve => canvas.toBlob(resolve)) 899 900 - name: 2d.layer.several-complex 901 desc: >- 902 Test to ensure beginlayer works for filter, alpha and shadow, even with 903 consecutive layers. 904 size: [500, 500] 905 fuzzy: maxDifference=0-3; totalPixels=0-6318 906 code: | 907 ctx.fillStyle = 'rgba(0, 0, 255, 1)'; 908 ctx.fillRect(50, 50, 95, 70); 909 910 ctx.globalAlpha = 0.5; 911 ctx.shadowOffsetX = -10; 912 ctx.shadowOffsetY = 10; 913 ctx.shadowColor = 'orange'; 914 ctx.shadowBlur = 3 915 916 for (let i = 0; i < 5; i++) { 917 ctx.beginLayer(); 918 919 ctx.fillStyle = 'rgba(225, 0, 0, 1)'; 920 ctx.fillRect(60 + i, 40 + i, 75, 50); 921 ctx.fillStyle = 'rgba(0, 255, 0, 1)'; 922 ctx.fillRect(80 + i, 60 + i, 75, 50); 923 924 ctx.endLayer(); 925 } 926 reference: | 927 ctx.fillStyle = 'rgba(0, 0, 255, 1)'; 928 ctx.fillRect(50, 50, 95, 70); 929 930 ctx.globalAlpha = 0.5; 931 ctx.shadowOffsetX = -10; 932 ctx.shadowOffsetY = 10; 933 ctx.shadowColor = 'orange'; 934 ctx.shadowBlur = 3; 935 936 var canvas2 = [5]; 937 var ctx2 = [5]; 938 939 for (let i = 0; i < 5; i++) { 940 canvas2[i] = document.createElement("canvas"); 941 ctx2[i] = canvas2[i].getContext("2d"); 942 ctx2[i].fillStyle = 'rgba(225, 0, 0, 1)'; 943 ctx2[i].fillRect(60, 40, 75, 50); 944 ctx2[i].fillStyle = 'rgba(0, 255, 0, 1)'; 945 ctx2[i].fillRect(80, 60, 75, 50); 946 947 ctx.drawImage(canvas2[i], i, i); 948 } 949 950 - name: 2d.layer.reset.{{ file_variant_name }}{{ tentative }} 951 desc: Checks that reset discards any pending layers. 952 code: | 953 // Global states: 954 ctx.globalAlpha = 0.3; 955 ctx.globalCompositeOperation = 'source-in'; 956 ctx.shadowOffsetX = -3; 957 ctx.shadowOffsetY = 3; 958 ctx.shadowColor = 'rgba(0, 30, 0, 0.3)'; 959 ctx.shadowBlur = 3; 960 ctx.filter = 'blur(5px)'; 961 962 {{ begin_layer }} 963 964 // Layer states: 965 ctx.globalAlpha = 0.6; 966 ctx.globalCompositeOperation = 'source-in'; 967 ctx.shadowOffsetX = -6; 968 ctx.shadowOffsetY = 6; 969 ctx.shadowColor = 'rgba(0, 60, 0, 0.6)'; 970 ctx.shadowBlur = 3; 971 ctx.filter = 'blur(15px)'; 972 973 ctx.reset(); 974 975 ctx.fillRect(10, 10, 75, 50); 976 reference: 977 ctx.fillRect(10, 10, 75, 50); 978 append_variants_to_name: false 979 variants: 980 - plain_layer: 981 begin_layer: ctx.beginLayer(); 982 filtered_layer: 983 begin_layer: |- 984 ctx.beginLayer({filter: {name: 'dropShadow', dx: -3, dy: 3}}); 985 tentative: .tentative 986 987 - name: 2d.layer.clearRect.partial 988 desc: clearRect inside a layer can clear a portion of the layer content. 989 size: [100, 100] 990 code: | 991 ctx.fillStyle = 'blue'; 992 ctx.fillRect(10, 10, 80, 50); 993 994 ctx.beginLayer(); 995 ctx.fillStyle = 'red'; 996 ctx.fillRect(20, 20, 80, 50); 997 ctx.clearRect(30, 30, 60, 30); 998 ctx.endLayer(); 999 reference: | 1000 ctx.fillStyle = 'blue'; 1001 ctx.fillRect(10, 10, 80, 50); 1002 1003 ctx.fillStyle = 'red'; 1004 ctx.fillRect(20, 20, 80, 10); 1005 ctx.fillRect(20, 60, 80, 10); 1006 ctx.fillRect(20, 20, 10, 50); 1007 ctx.fillRect(90, 20, 10, 50); 1008 1009 - name: 2d.layer.clearRect.full 1010 desc: clearRect inside a layer can clear all of the layer content. 1011 size: [100, 100] 1012 code: | 1013 ctx.fillStyle = 'blue'; 1014 ctx.fillRect(10, 10, 80, 50); 1015 1016 ctx.beginLayer(); 1017 ctx.fillStyle = 'red'; 1018 ctx.fillRect(20, 20, 80, 50); 1019 ctx.fillStyle = 'green'; 1020 ctx.clearRect(0, 0, {{ size[0] }}, {{ size[1] }}); 1021 ctx.endLayer(); 1022 reference: | 1023 ctx.fillStyle = 'blue'; 1024 ctx.fillRect(10, 10, 80, 50); 1025 1026 - name: 2d.layer.drawImage.{{ file_variant_name }}{{ tentative }} 1027 size: [200, 200] 1028 desc: >- 1029 Checks that drawImage writes the image to the layer and not the parent 1030 directly. 1031 code: | 1032 ctx.fillStyle = 'skyblue'; 1033 ctx.fillRect(0, 0, 100, 100); 1034 1035 {{ filtered_layer }} 1036 1037 ctx.fillStyle = 'maroon'; 1038 ctx.fillRect(20, 20, 50, 50); 1039 1040 ctx.globalCompositeOperation = 'xor'; 1041 1042 // The image should xor only with the layer content, not the parents'. 1043 const canvas_image = new OffscreenCanvas(200,200); 1044 const ctx_image = canvas_image.getContext("2d"); 1045 ctx_image.fillStyle = 'pink'; 1046 ctx_image.fillRect(40, 40, 50, 50); 1047 ctx.drawImage(canvas_image, 0, 0); 1048 1049 ctx.endLayer(); 1050 reference: | 1051 ctx.fillStyle = 'skyblue'; 1052 ctx.fillRect(0, 0, 100, 100); 1053 1054 ctx.filter = 'drop-shadow(-10px -10px 0px navy)'; 1055 ctx.beginLayer(); 1056 1057 ctx.fillStyle = 'maroon'; 1058 ctx.fillRect(20, 20, 50, 50); 1059 1060 ctx.globalCompositeOperation = 'xor'; 1061 1062 // Should xor only with the layer content, not the parents'. 1063 ctx.fillStyle = 'pink'; 1064 ctx.fillRect(40, 40, 50, 50); 1065 1066 ctx.endLayer(); 1067 append_variants_to_name: false 1068 variants: 1069 - ctx-filter: 1070 filtered_layer: |- 1071 ctx.filter = 'drop-shadow(-10px -10px 0px navy)'; 1072 ctx.beginLayer(); 1073 layer-filter: 1074 filtered_layer: |- 1075 ctx.beginLayer({filter: {name: 'dropShadow', dx: -10, dy: -10, 1076 stdDeviation: 0, floodColor: 'navy'}}); 1077 tentative: .tentative 1078 1079 - name: 2d.layer.valid-calls 1080 desc: No exception raised on {{ variant_desc }}. 1081 variants: 1082 - save: 1083 variant_desc: lone save() calls 1084 code: ctx.save(); 1085 beginLayer: 1086 variant_desc: lone beginLayer() calls 1087 code: ctx.beginLayer(); 1088 restore: 1089 variant_desc: lone restore() calls 1090 code: ctx.restore(); 1091 save_restore: 1092 variant_desc: save() + restore() 1093 code: |- 1094 ctx.save(); 1095 ctx.restore(); 1096 save_reset_restore: 1097 variant_desc: save() + reset() + restore() 1098 code: |- 1099 ctx.save(); 1100 ctx.reset(); 1101 ctx.restore(); 1102 beginLayer-endLayer: 1103 variant_desc: beginLayer() + endLayer() 1104 code: |- 1105 ctx.beginLayer(); 1106 ctx.save(); 1107 save-beginLayer: 1108 variant_desc: save() + beginLayer() 1109 code: |- 1110 ctx.save(); 1111 ctx.beginLayer(); 1112 beginLayer-save: 1113 variant_desc: beginLayer() + save() 1114 code: |- 1115 ctx.beginLayer(); 1116 ctx.save(); 1117 1118 - name: 2d.layer.invalid-calls 1119 desc: Raises exception on {{ variant_desc }}. 1120 test_type: sync 1121 code: | 1122 assert_throws_dom("INVALID_STATE_ERR", function() { 1123 {{ call_sequence | indent(2) }} 1124 }); 1125 variants: 1126 - endLayer: 1127 variant_desc: lone endLayer calls 1128 call_sequence: ctx.endLayer(); 1129 save-endLayer: 1130 variant_desc: save() + endLayer() 1131 call_sequence: |- 1132 ctx.save(); 1133 ctx.endLayer(); 1134 beginLayer-restore: 1135 variant_desc: beginLayer() + restore() 1136 call_sequence: |- 1137 ctx.beginLayer(); 1138 ctx.restore(); 1139 save-beginLayer-restore: 1140 variant_desc: save() + beginLayer() + restore() 1141 call_sequence: |- 1142 ctx.save(); 1143 ctx.beginLayer(); 1144 ctx.restore(); 1145 beginLayer-save-endLayer: 1146 variant_desc: beginLayer() + save() + endLayer() 1147 call_sequence: |- 1148 ctx.beginLayer(); 1149 ctx.save(); 1150 ctx.endLayer(); 1151 beginLayer-reset-endLayer: 1152 variant_desc: beginLayer() + reset() + endLayer() 1153 call_sequence: |- 1154 ctx.beginLayer(); 1155 ctx.reset(); 1156 ctx.endLayer(); 1157 1158 - name: 2d.layer.exceptions-are-no-op.tentative 1159 desc: Checks that the context state is left unchanged if beginLayer throws. 1160 test_type: sync 1161 code: | 1162 // Get `beginLayer` to throw while parsing the filter. 1163 assert_throws_js(TypeError, 1164 () => ctx.beginLayer({filter: {name: 'colorMatrix', 1165 values: 'foo'}})); 1166 // `beginLayer` shouldn't have opened the layer, so `endLayer` should throw. 1167 assert_throws_dom("InvalidStateError", () => ctx.endLayer()); 1168 1169 - name: 2d.layer.cross-layer-paths 1170 desc: Checks that path defined in a layer is usable outside. 1171 code: | 1172 ctx.beginLayer(); 1173 ctx.translate(50, 0); 1174 ctx.moveTo(0, 0); 1175 ctx.endLayer(); 1176 ctx.lineTo(50, 100); 1177 ctx.stroke(); 1178 reference: 1179 ctx.moveTo(50, 0); 1180 ctx.lineTo(50, 100); 1181 ctx.stroke(); 1182 1183 - name: 2d.layer.beginLayer-options.tentative 1184 desc: Checks beginLayer works for different option parameter values 1185 test_type: sync 1186 code: | 1187 ctx.beginLayer(); ctx.endLayer(); 1188 ctx.beginLayer(null); ctx.endLayer(); 1189 ctx.beginLayer(undefined); ctx.endLayer(); 1190 ctx.beginLayer([]); ctx.endLayer(); 1191 ctx.beginLayer({}); ctx.endLayer(); 1192 1193 @assert throws TypeError ctx.beginLayer(''); 1194 @assert throws TypeError ctx.beginLayer(0); 1195 @assert throws TypeError ctx.beginLayer(1); 1196 @assert throws TypeError ctx.beginLayer(true); 1197 @assert throws TypeError ctx.beginLayer(false); 1198 1199 ctx.beginLayer({filter: null}); ctx.endLayer(); 1200 ctx.beginLayer({filter: undefined}); ctx.endLayer(); 1201 ctx.beginLayer({filter: []}); ctx.endLayer(); 1202 ctx.beginLayer({filter: {}}); ctx.endLayer(); 1203 ctx.beginLayer({filter: {name: "unknown"}}); ctx.endLayer(); 1204 ctx.beginLayer({filter: ''}); ctx.endLayer(); 1205 1206 // These cases don't throw TypeError since they can be casted to a 1207 // DOMString. 1208 ctx.beginLayer({filter: 0}); ctx.endLayer(); 1209 ctx.beginLayer({filter: 1}); ctx.endLayer(); 1210 ctx.beginLayer({filter: true}); ctx.endLayer(); 1211 ctx.beginLayer({filter: false}); ctx.endLayer(); 1212 1213 - name: 2d.layer.blur-from-outside-canvas.{{ file_variant_name }}{{ tentative }} 1214 desc: Checks blur leaking inside from drawing outside the canvas 1215 size: [200, 200] 1216 code: | 1217 {{ clipping }} 1218 1219 {{ filtered_layer }} 1220 1221 ctx.fillStyle = 'turquoise'; 1222 ctx.fillRect(201, 50, 100, 100); 1223 ctx.fillStyle = 'indigo'; 1224 ctx.fillRect(50, 201, 100, 100); 1225 ctx.fillStyle = 'orange'; 1226 ctx.fillRect(-1, 50, -100, 100); 1227 ctx.fillStyle = 'brown'; 1228 ctx.fillRect(50, -1, 100, -100); 1229 1230 ctx.endLayer(); 1231 reference: | 1232 const svg = ` 1233 <svg xmlns="http://www.w3.org/2000/svg" 1234 width="{{ size[0] }}" height="{{ size[1] }}" 1235 color-interpolation-filters="sRGB"> 1236 <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> 1237 <feGaussianBlur in="SourceGraphic" stdDeviation="30" /> 1238 </filter> 1239 <g filter="url(#filter)"> 1240 <rect x="201" y="50" width="100" height="100" fill="turquoise"/> 1241 <rect x="50" y="201" width="100" height="100" fill="indigo"/> 1242 <rect x="-101" y="50" width="100" height="100" fill="orange"/> 1243 <rect x="50" y="-101" width="100" height="100" fill="brown"/> 1244 </g> 1245 </svg>`; 1246 const img = new Image(); 1247 img.width = {{ size[0] }}; 1248 img.height = {{ size[1] }}; 1249 img.onload = () => { 1250 {{ clipping | indent(4) }} 1251 1252 ctx.drawImage(img, 0, 0); 1253 }; 1254 img.src = 'data:image/svg+xml;base64,' + btoa(svg); 1255 append_variants_to_name: false 1256 variants: 1257 - no-clipping: 1258 clipping: // No clipping. 1259 with-clipping: 1260 clipping: |- 1261 const clipRegion = new Path2D(); 1262 clipRegion.rect(20, 20, 160, 160); 1263 ctx.clip(clipRegion); 1264 - ctx-filter: 1265 filtered_layer: |- 1266 ctx.filter = 'blur(30px)'; 1267 ctx.beginLayer(); 1268 layer-filter: 1269 filtered_layer: |- 1270 ctx.beginLayer({filter: [ {name: 'gaussianBlur', stdDeviation: 30} ]}); 1271 tentative: .tentative 1272 1273 1274 - name: >- 1275 2d.layer.shadow-from-outside-canvas.{{ file_variant_name }}{{ tentative }} 1276 desc: Checks shadow produced by object drawn outside the canvas 1277 size: [200, 200] 1278 code: | 1279 {{ distance }} 1280 1281 {{ clipping }} 1282 1283 {{ filtered_layer }} 1284 1285 ctx.fillStyle = 'red'; 1286 ctx.fillRect({{ size[0] }} + delta, {{ size[1] }} + delta, 100, 100); 1287 1288 ctx.endLayer(); 1289 reference: | 1290 {{ distance }} 1291 1292 {{ clipping }} 1293 1294 ctx.fillStyle = 'green'; 1295 ctx.fillRect(0, 0, 100, 100); 1296 append_variants_to_name: false 1297 variants: 1298 - short-distance: 1299 distance: |- 1300 const delta = 1; 1301 clipping: // No clipping. 1302 short-distance-with-clipping: 1303 distance: |- 1304 const delta = 1; 1305 clipping: |- 1306 const clipRegion = new Path2D(); 1307 clipRegion.rect(20, 20, 160, 160); 1308 ctx.clip(clipRegion); 1309 long-distance: 1310 distance: |- 1311 const delta = 10000; 1312 clipping: // No clipping. 1313 long-distance-with-clipping: 1314 distance: |- 1315 const delta = 10000; 1316 clipping: |- 1317 const clipRegion = new Path2D(); 1318 clipRegion.rect(20, 20, 160, 160); 1319 ctx.clip(clipRegion); 1320 - ctx-filter: 1321 filtered_layer: |- 1322 const dx = -({{ size[0] }} + delta); 1323 const dy = -({{ size[1] }} + delta); 1324 ctx.filter = 'drop-shadow(' + dx + 'px ' + dy + 'px 0px green)'; 1325 ctx.beginLayer(); 1326 layer-filter: 1327 filtered_layer: |- 1328 ctx.beginLayer({filter: [ 1329 {name: 'dropShadow', dx: -({{ size[0] }} + delta), 1330 dy: -({{ size[1] }} + delta), stdDeviation: 0, 1331 floodColor: 'green'}, 1332 ]}); 1333 tentative: .tentative 1334 1335 - name: 2d.layer.opaque-canvas.{{ file_variant_name }}{{ tentative }} 1336 desc: Checks that layer blending works inside opaque canvas 1337 size: [300, 300] 1338 code: | 1339 {% if canvas_type == 'HtmlCanvas' %} 1340 const canvas2 = document.createElement('canvas'); 1341 canvas2.width = 200; 1342 canvas2.height = 200; 1343 {% else %} 1344 const canvas2 = new OffscreenCanvas(200, 200); 1345 {% endif %} 1346 const ctx2 = canvas2.getContext('2d', {alpha: false}); 1347 1348 ctx2.fillStyle = 'purple'; 1349 ctx2.fillRect(10, 10, 100, 100); 1350 1351 {{ filtered_layer }} 1352 ctx2.fillStyle = 'green'; 1353 ctx2.fillRect(50, 50, 100, 100); 1354 ctx2.globalAlpha = 0.8; 1355 ctx2.fillStyle = 'yellow'; 1356 ctx2.fillRect(75, 25, 100, 100); 1357 ctx2.endLayer(); 1358 1359 ctx.fillStyle = 'blue'; 1360 ctx.fillRect(0, 0, 300, 300); 1361 ctx.drawImage(canvas2, 0, 0); 1362 reference: | 1363 ctx.fillStyle = 'blue'; 1364 ctx.fillRect(0, 0, 300, 300); 1365 1366 ctx.fillStyle = 'black'; 1367 ctx.fillRect(0, 0, 200, 200); 1368 1369 ctx.fillStyle = 'purple'; 1370 ctx.fillRect(10, 10, 100, 100); 1371 1372 const canvas2 = new OffscreenCanvas(200, 200); 1373 const ctx2 = canvas2.getContext('2d'); 1374 ctx2.fillStyle = 'green'; 1375 ctx2.fillRect(50, 50, 100, 100); 1376 ctx2.globalAlpha = 0.8; 1377 ctx2.fillStyle = 'yellow'; 1378 ctx2.fillRect(75, 25, 100, 100); 1379 1380 ctx.shadowColor = 'rgba(200, 100, 50, 0.5)'; 1381 ctx.shadowOffsetX = -10; 1382 ctx.shadowOffsetY = 10; 1383 ctx.drawImage(canvas2, 0, 0); 1384 append_variants_to_name: false 1385 variants: 1386 - ctx-filter: 1387 filtered_layer: |- 1388 ctx2.filter = 'drop-shadow(-10px 10px 0px rgba(200, 100, 50, 0.5))'; 1389 ctx2.beginLayer(); 1390 layer-filter: 1391 filtered_layer: |- 1392 ctx2.beginLayer({filter: {name: 'dropShadow', dx: -10, dy: 10, 1393 stdDeviation: 0, 1394 floodColor: 'rgba(200, 100, 50, 0.5)'}}); 1395 tentative: .tentative 1396 1397 - name: 2d.layer.css-filters.{{ file_variant_name }}.tentative 1398 desc: Checks that beginLayer works with a CSS filter string as input. 1399 size: [200, 200] 1400 code: &filter-test-code | 1401 ctx.beginLayer({filter: {{ ctx_filter }}}); 1402 1403 ctx.fillStyle = 'teal'; 1404 ctx.fillRect(50, 50, 100, 100); 1405 1406 ctx.endLayer(); 1407 html_reference: &filter-test-reference | 1408 <svg xmlns="http://www.w3.org/2000/svg" 1409 width="{{ size[0] }}" height="{{ size[1] }}" 1410 color-interpolation-filters="sRGB"> 1411 <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> 1412 {{ svg_filter | indent(4) }} 1413 </filter> 1414 <g filter="url(#filter)"> 1415 <rect x="50" y="50" width="100" height="100" fill="teal"/> 1416 </g> 1417 </svg> 1418 append_variants_to_name: false 1419 variants: 1420 - blur: 1421 ctx_filter: |- 1422 'blur(10px)' 1423 svg_filter: |- 1424 <feGaussianBlur stdDeviation="10" /> 1425 shadow: 1426 ctx_filter: |- 1427 'drop-shadow(-10px -10px 5px purple)' 1428 svg_filter: |- 1429 <feDropShadow dx="-10" dy="-10" stdDeviation="5" flood-color="purple" /> 1430 blur-and-shadow: 1431 ctx_filter: |- 1432 'blur(5px) drop-shadow(10px 10px 5px orange)' 1433 svg_filter: |- 1434 <feGaussianBlur stdDeviation="5" /> 1435 <feDropShadow dx="10" dy="10" stdDeviation="5" flood-color="orange" /> 1436 1437 - name: 2d.layer.anisotropic-blur.{{ file_variant_name }}.tentative 1438 desc: Checks that layers allow gaussian blur with separate X and Y components. 1439 size: [200, 200] 1440 code: *filter-test-code 1441 html_reference: *filter-test-reference 1442 append_variants_to_name: false 1443 variants: 1444 - x-only: 1445 ctx_filter: |- 1446 { name: 'gaussianBlur', stdDeviation: [4, 0] } 1447 svg_filter: |- 1448 <feGaussianBlur stdDeviation="4 0" /> 1449 mostly-x: 1450 ctx_filter: |- 1451 { name: 'gaussianBlur', stdDeviation: [4, 1] } 1452 svg_filter: |- 1453 <feGaussianBlur stdDeviation="4 1" /> 1454 isotropic: 1455 ctx_filter: |- 1456 { name: 'gaussianBlur', stdDeviation: [4, 4] } 1457 svg_filter: |- 1458 <feGaussianBlur stdDeviation="4 4" /> 1459 mostly-y: 1460 ctx_filter: |- 1461 { name: 'gaussianBlur', stdDeviation: [1, 4] } 1462 svg_filter: |- 1463 <feGaussianBlur stdDeviation="1 4" /> 1464 y-only: 1465 ctx_filter: |- 1466 { name: 'gaussianBlur', stdDeviation: [0, 4] } 1467 svg_filter: |- 1468 <feGaussianBlur stdDeviation="0 4" /> 1469 1470 - name: 2d.layer.nested-filters.tentative 1471 desc: Checks that nested layers work properly when both apply filters. 1472 size: [400, 200] 1473 code: | 1474 ctx.beginLayer({filter: {name: 'dropShadow', dx: -20, dy: -20, 1475 stdDeviation: 0, floodColor: 'yellow'}}); 1476 ctx.beginLayer({filter: 'drop-shadow(-10px -10px 0 blue)'}); 1477 1478 ctx.fillStyle = 'red'; 1479 ctx.fillRect(50, 50, 100, 100); 1480 1481 ctx.endLayer(); 1482 ctx.endLayer(); 1483 1484 ctx.beginLayer({filter: 'drop-shadow(20px 20px 0 blue)'}); 1485 ctx.beginLayer({filter: {name: 'dropShadow', dx: 10, dy: 10, 1486 stdDeviation: 0, floodColor: 'yellow'}}); 1487 1488 ctx.fillStyle = 'red'; 1489 ctx.fillRect(250, 50, 100, 100); 1490 1491 ctx.endLayer(); 1492 ctx.endLayer(); 1493 reference: | 1494 ctx.fillStyle = 'yellow'; 1495 ctx.fillRect(20, 20, 100, 100); 1496 ctx.fillRect(30, 30, 100, 100); 1497 ctx.fillStyle = 'blue'; 1498 ctx.fillRect(40, 40, 100, 100); 1499 ctx.fillStyle = 'red'; 1500 ctx.fillRect(50, 50, 100, 100); 1501 1502 ctx.fillStyle = 'blue'; 1503 ctx.fillRect(280, 80, 100, 100); 1504 ctx.fillRect(270, 70, 100, 100); 1505 ctx.fillStyle = 'yellow'; 1506 ctx.fillRect(260, 60, 100, 100); 1507 ctx.fillStyle = 'red'; 1508 ctx.fillRect(250, 50, 100, 100); 1509 1510 - name: 2d.layer.nested-ctx-filter 1511 desc: Tests nested canvas layers with context filters. 1512 size: [200, 200] 1513 code: | 1514 ctx.filter = 'drop-shadow(20px 20px 0px red)'; 1515 ctx.beginLayer(); 1516 ctx.filter = 'drop-shadow(10px 10px 0px green)'; 1517 ctx.beginLayer(); 1518 ctx.filter = 'drop-shadow(5px 5px 0px blue)'; 1519 ctx.fillStyle = 'grey'; 1520 ctx.fillRect(10, 10, 100, 100); 1521 ctx.endLayer(); 1522 ctx.endLayer(); 1523 reference: | 1524 ctx.fillStyle = 'red'; 1525 ctx.fillRect(45, 45, 100, 100); 1526 ctx.fillRect(40, 40, 100, 100); 1527 ctx.fillRect(35, 35, 100, 100); 1528 ctx.fillRect(30, 30, 100, 100); 1529 ctx.fillStyle = 'green'; 1530 ctx.fillRect(25, 25, 100, 100); 1531 ctx.fillRect(20, 20, 100, 100); 1532 ctx.fillStyle = 'blue'; 1533 ctx.fillRect(15, 15, 100, 100); 1534 ctx.fillStyle = 'grey'; 1535 ctx.fillRect(10, 10, 100, 100);