| - name: 2d.layer.global-states |
| desc: Checks that layers correctly use global render states. |
| size: [200, 200] |
| code: | |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| |
| var circle = new Path2D(); |
| circle.arc(90, 90, 45, 0, 2 * Math.PI); |
| ctx.fill(circle); |
| |
| {{ render_states }} |
| |
| ctx.beginLayer(); |
| |
| // Enable compositing in the layer to validate that draw calls in the layer |
| // won't individually composite with the background. |
| ctx.globalCompositeOperation = 'screen'; |
| |
| ctx.fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx.fillRect(50, 50, 75, 50); |
| ctx.fillStyle = 'rgba(0, 255, 0, 1)'; |
| ctx.fillRect(70, 70, 75, 50); |
| |
| ctx.endLayer(); |
| reference: | |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| |
| var circle = new Path2D(); |
| circle.arc(90, 90, 45, 0, 2 * Math.PI); |
| ctx.fill(circle); |
| |
| {{ render_states }} |
| |
| canvas2 = document.createElement("canvas"); |
| ctx2 = canvas2.getContext("2d"); |
| |
| ctx2.globalCompositeOperation = 'screen'; |
| ctx2.fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx2.fillRect(50, 50, 75, 50); |
| ctx2.fillStyle = 'rgba(0, 255, 0, 1)'; |
| ctx2.fillRect(70, 70, 75, 50); |
| |
| ctx.drawImage(canvas2, 0, 0); |
| variants: &global-state-variants |
| no-global-states: |
| render_states: // No global states. |
| alpha: &global-state-alpha |
| render_states: ctx.globalAlpha = 0.6; |
| blending: |
| render_states: ctx.globalCompositeOperation = 'multiply'; |
| composite: |
| render_states: ctx.globalCompositeOperation = 'source-in'; |
| shadow: |
| render_states: |- |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; |
| ctx.shadowBlur = 3; |
| alpha.blending: &global-state-alpha-blending |
| render_states: |- |
| ctx.globalAlpha = 0.6; |
| ctx.globalCompositeOperation = 'multiply'; |
| alpha.composite: &global-state-alpha-composite |
| render_states: |- |
| ctx.globalAlpha = 0.6; |
| ctx.globalCompositeOperation = 'source-in'; |
| alpha.shadow: &global-state-alpha-shadow |
| render_states: |- |
| ctx.globalAlpha = 0.5; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; |
| ctx.shadowBlur = 3; |
| alpha.blending.shadow: |
| render_states: |- |
| ctx.globalAlpha = 0.6; |
| ctx.globalCompositeOperation = 'multiply'; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; |
| ctx.shadowBlur = 3; |
| alpha.composite.shadow: |
| render_states: |- |
| ctx.globalAlpha = 0.6; |
| ctx.globalCompositeOperation = 'source-in'; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; |
| ctx.shadowBlur = 3; |
| blending.shadow: |
| render_states: |- |
| ctx.globalCompositeOperation = 'multiply'; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; |
| ctx.shadowBlur = 3; |
| composite.shadow: |
| render_states: |- |
| ctx.globalCompositeOperation = 'source-in'; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'rgba(255, 165, 0, 0.5)'; |
| ctx.shadowBlur = 3; |
| |
| - name: 2d.layer.global-states.filter |
| desc: Checks that layers with filters correctly use global render states. |
| size: [200, 200] |
| code: | |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| |
| var circle = new Path2D(); |
| circle.arc(90, 90, 45, 0, 2 * Math.PI); |
| ctx.fill(circle); |
| |
| {{ render_states }} |
| |
| ctx.beginLayer({filter: [ |
| {name: 'colorMatrix', values: [0.393, 0.769, 0.189, 0, 0, |
| 0.349, 0.686, 0.168, 0, 0, |
| 0.272, 0.534, 0.131, 0, 0, |
| 0, 0, 0, 1, 0]}, |
| {name: 'componentTransfer', |
| funcA: {type: "table", tableValues: [0, 0.7]}}, |
| {name: 'dropShadow', dx: 5, dy: 5, floodColor: '#81e'}]}); |
| |
| ctx.fillStyle = 'rgba(200, 0, 0, 1)'; |
| ctx.fillRect(50, 50, 75, 50); |
| ctx.fillStyle = 'rgba(0, 200, 0, 1)'; |
| ctx.fillRect(70, 70, 75, 50); |
| |
| ctx.endLayer(); |
| reference: | |
| const svg = ` |
| <svg xmlns="http://www.w3.org/2000/svg" |
| width="{{ size[0] }}" height="{{ size[1] }}" |
| color-interpolation-filters="sRGB"> |
| <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> |
| <feColorMatrix |
| type="matrix" |
| values="0.393 0.769 0.189 0 0 |
| 0.349 0.686 0.168 0 0 |
| 0.272 0.534 0.131 0 0 |
| 0 0 0 1 0" /> |
| <feComponentTransfer> |
| <feFuncA type="table" tableValues="0 0.7"></feFuncA> |
| </feComponentTransfer> |
| <feDropShadow dx="5" dy="5" flood-color="#81e" /> |
| </filter> |
| <g filter="url(#filter)"> |
| <rect x="50" y="50" width="75" height="50" fill="rgba(200, 0, 0, 1)"/> |
| <rect x="70" y="70" width="75" height="50" fill="rgba(0, 200, 0, 1)"/> |
| </g> |
| </svg>`; |
| |
| const img = new Image(); |
| img.width = {{ size[0] }}; |
| img.height = {{ size[1] }}; |
| img.onload = () => { |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| |
| var circle = new Path2D(); |
| circle.arc(90, 90, 45, 0, 2 * Math.PI); |
| ctx.fill(circle); |
| |
| {{ render_states }} |
| |
| ctx.drawImage(img, 0, 0); |
| }; |
| img.src = 'data:image/svg+xml;base64,' + btoa(svg); |
| variants: |
| <<: *global-state-variants |
| alpha: |
| <<: *global-state-alpha |
| fuzzy: maxDifference=0-2; totalPixels=0-6766 |
| alpha.blending: |
| <<: *global-state-alpha-blending |
| fuzzy: maxDifference=0-1; totalPixels=0-2440 |
| alpha.composite: |
| <<: *global-state-alpha-composite |
| fuzzy: maxDifference=0-1; totalPixels=0-5204 |
| alpha.shadow: |
| <<: *global-state-alpha-shadow |
| fuzzy: maxDifference=0-2; totalPixels=0-6311 |
| |
| - name: 2d.layer.global-filter |
| desc: Tests that layers ignore the global context filter. |
| size: [150, 100] |
| code: | |
| ctx.filter = 'blur(5px)' |
| |
| ctx.beginLayer(); |
| ctx.fillRect(10, 10, 30, 30); // `ctx.filter` applied to draw call. |
| ctx.endLayer(); |
| |
| ctx.beginLayer(); |
| ctx.filter = 'none'; |
| ctx.fillRect(60, 10, 30, 30); // Should not be filted by the layer. |
| ctx.endLayer(); |
| |
| ctx.fillRect(110, 10, 30, 30); // `ctx.filter` is still set. |
| reference: | |
| ctx.fillRect(60, 10, 30, 30); |
| ctx.filter = 'blur(5px)' |
| ctx.fillRect(10, 10, 30, 30); |
| ctx.fillRect(110, 10, 30, 30); |
| |
| - name: 2d.layer.nested |
| desc: Tests nested canvas layers. |
| size: [200, 200] |
| code: | |
| var circle = new Path2D(); |
| circle.arc(90, 90, 40, 0, 2 * Math.PI); |
| ctx.fill(circle); |
| |
| ctx.globalCompositeOperation = 'source-in'; |
| |
| ctx.beginLayer(); |
| |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| ctx.fillRect(60, 60, 75, 50); |
| |
| ctx.globalAlpha = 0.5; |
| |
| ctx.beginLayer(); |
| |
| ctx.fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx.fillRect(50, 50, 75, 50); |
| ctx.fillStyle = 'rgba(0, 255, 0, 1)'; |
| ctx.fillRect(70, 70, 75, 50); |
| |
| ctx.endLayer(); |
| ctx.endLayer(); |
| reference: | |
| var circle = new Path2D(); |
| circle.arc(90, 90, 40, 0, 2 * Math.PI); |
| ctx.fill(circle); |
| |
| ctx.globalCompositeOperation = 'source-in'; |
| |
| canvas2 = document.createElement("canvas"); |
| ctx2 = canvas2.getContext("2d"); |
| |
| ctx2.fillStyle = 'rgba(0, 0, 255, 1)'; |
| ctx2.fillRect(60, 60, 75, 50); |
| |
| ctx2.globalAlpha = 0.5; |
| |
| canvas3 = document.createElement("canvas"); |
| ctx3 = canvas3.getContext("2d"); |
| |
| ctx3.fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx3.fillRect(50, 50, 75, 50); |
| ctx3.fillStyle = 'rgba(0, 255, 0, 1)'; |
| ctx3.fillRect(70, 70, 75, 50); |
| |
| ctx2.drawImage(canvas3, 0, 0); |
| ctx.drawImage(canvas2, 0, 0); |
| |
| |
| - name: 2d.layer.restore-style |
| desc: Test that ensure layers restores style values upon endLayer. |
| size: [200, 200] |
| fuzzy: maxDifference=0-1; totalPixels=0-950 |
| code: | |
| ctx.fillStyle = 'rgba(0,0,255,1)'; |
| ctx.fillRect(50, 50, 75, 50); |
| ctx.globalAlpha = 0.5; |
| |
| ctx.beginLayer(); |
| ctx.fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx.fillRect(60, 60, 75, 50); |
| ctx.endLayer(); |
| |
| ctx.fillRect(70, 70, 75, 50); |
| reference: | |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| ctx.fillRect(50, 50, 75, 50); |
| ctx.globalAlpha = 0.5; |
| |
| canvas2 = document.createElement("canvas"); |
| ctx2 = canvas2.getContext("2d"); |
| ctx2.fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx2.fillRect(60, 60, 75, 50); |
| ctx.drawImage(canvas2, 0, 0); |
| |
| ctx.fillRect(70, 70, 75, 50); |
| |
| - name: 2d.layer.layer-rendering-state-reset-in-layer |
| desc: Tests that layers ignore the global context filter. |
| code: | |
| ctx.globalAlpha = 0.5; |
| ctx.globalCompositeOperation = 'xor'; |
| ctx.shadowColor = '#0000ff'; |
| ctx.shadowOffsetX = 10; |
| ctx.shadowOffsetY = 20; |
| ctx.shadowBlur = 30; |
| |
| @assert ctx.globalAlpha === 0.5; |
| @assert ctx.globalCompositeOperation === 'xor'; |
| @assert ctx.shadowColor === '#0000ff'; |
| @assert ctx.shadowOffsetX === 10; |
| @assert ctx.shadowOffsetY === 20; |
| @assert ctx.shadowBlur === 30; |
| |
| ctx.beginLayer(); |
| |
| @assert ctx.globalAlpha === 1.0; |
| @assert ctx.globalCompositeOperation === 'source-over'; |
| @assert ctx.shadowColor === 'rgba(0, 0, 0, 0)'; |
| @assert ctx.shadowOffsetX === 0; |
| @assert ctx.shadowOffsetY === 0; |
| @assert ctx.shadowBlur === 0; |
| |
| ctx.endLayer(); |
| |
| @assert ctx.globalAlpha === 0.5; |
| @assert ctx.globalCompositeOperation === 'xor'; |
| @assert ctx.shadowColor === '#0000ff'; |
| @assert ctx.shadowOffsetX === 10; |
| @assert ctx.shadowOffsetY === 20; |
| @assert ctx.shadowBlur === 30; |
| |
| - name: 2d.layer.clip-outside |
| desc: Check clipping set outside the layer |
| size: [100, 100] |
| code: | |
| ctx.beginPath(); |
| ctx.rect(15, 15, 70, 70); |
| ctx.clip(); |
| |
| ctx.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(10, 10, 80, 80); |
| ctx.endLayer(); |
| reference: | |
| const canvas2 = new OffscreenCanvas(200, 200); |
| const ctx2 = canvas2.getContext('2d'); |
| |
| ctx2.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); |
| ctx2.fillStyle = 'blue'; |
| ctx2.fillRect(10, 10, 80, 80); |
| ctx2.endLayer(); |
| |
| ctx.beginPath(); |
| ctx.rect(15, 15, 70, 70); |
| ctx.clip(); |
| |
| ctx.drawImage(canvas2, 0, 0); |
| |
| - name: 2d.layer.ctm.filter |
| desc: Checks that parent transforms affect layer filters. |
| size: [200, 200] |
| code: | |
| // Transforms inside the layer should not apply to the layer's filter. |
| ctx.beginLayer({filter: 'drop-shadow(5px 5px 0px grey)'}); |
| ctx.translate(30, 90); |
| ctx.scale(2, 2); |
| ctx.rotate(Math.PI / 2); |
| ctx.fillRect(-30, -5, 60, 10); |
| ctx.endLayer(); |
| |
| // Transforms in the layer's parent should apply to the layer's filter. |
| ctx.translate(80, 90); |
| ctx.scale(2, 2); |
| ctx.rotate(Math.PI / 2); |
| ctx.beginLayer({filter: 'drop-shadow(5px 5px 0px grey)'}); |
| ctx.fillRect(-30, -5, 60, 10); |
| ctx.endLayer(); |
| html_reference: | |
| <svg xmlns="http://www.w3.org/2000/svg" |
| width="{{ size[0] }}" height="{{ size[1] }}" |
| color-interpolation-filters="sRGB"> |
| <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> |
| <feDropShadow dx="5" dy="5" stdDeviation="0" flood-color="grey" /> |
| </filter> |
| |
| <g filter="url(#filter)"> |
| <g transform="translate(30, 90) scale(2) rotate(90)"> |
| <rect x="-30" y="-5" width=60 height=10></rect> |
| </g> |
| </g> |
| |
| <g transform="translate(80, 90) scale(2) rotate(90)"> |
| <g filter="url(#filter)"> |
| <rect x="-30" y="-5" width=60 height=10></rect> |
| </g> |
| </g> |
| </svg> |
| |
| - name: 2d.layer.ctm.shadow-in-transformed-layer |
| desc: Check shadows inside of a transformed layer. |
| size: [200, 200] |
| code: | |
| ctx.translate(80, 90); |
| ctx.scale(2, 2); |
| ctx.rotate(Math.PI / 2); |
| |
| ctx.beginLayer(); |
| ctx.shadowOffsetX = 10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'grey'; |
| ctx.fillRect(-30, -5, 60, 10); |
| |
| const canvas2 = new OffscreenCanvas(100, 100); |
| const ctx2 = canvas2.getContext('2d'); |
| ctx2.fillStyle = 'blue'; |
| ctx2.fillRect(0, 0, 40, 10); |
| ctx.drawImage(canvas2, -30, -30); |
| |
| ctx.endLayer(); |
| reference: | |
| ctx.translate(80, 90); |
| ctx.scale(2, 2); |
| ctx.rotate(Math.PI / 2); |
| |
| ctx.shadowOffsetX = 10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'grey'; |
| ctx.fillRect(-30, -5, 60, 10); |
| |
| const canvas2 = new OffscreenCanvas(100, 100); |
| const ctx2 = canvas2.getContext('2d'); |
| ctx2.fillStyle = 'blue'; |
| ctx2.fillRect(0, 0, 40, 10); |
| ctx.drawImage(canvas2, -30, -30); |
| |
| - name: 2d.layer.ctm.getTransform |
| desc: Tests getTransform inside layers. |
| code: | |
| ctx.translate(10, 20); |
| ctx.beginLayer(); |
| ctx.scale(2, 3); |
| const m = ctx.getTransform(); |
| assert_array_equals([m.a, m.b, m.c, m.d, m.e, m.f], [2, 0, 0, 3, 10, 20]); |
| ctx.endLayer(); |
| |
| - name: 2d.layer.ctm.setTransform |
| desc: Tests setTransform inside layers. |
| code: | |
| ctx.translate(80, 0); |
| |
| ctx.beginLayer(); |
| ctx.rotate(2); |
| ctx.beginLayer(); |
| ctx.scale(5, 6); |
| ctx.setTransform(4, 0, 0, 2, 20, 10); |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(0, 0, 10, 10); |
| ctx.endLayer(); |
| ctx.endLayer(); |
| |
| ctx.fillStyle = 'green'; |
| ctx.fillRect(0, 0, 20, 20); |
| reference: | |
| ctx.translate(80, 0); |
| ctx.fillStyle = 'green'; |
| ctx.fillRect(0, 0, 20, 20); |
| |
| ctx.setTransform(4, 0, 0, 2, 20, 10); |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(0, 0, 10, 10); |
| |
| - name: 2d.layer.ctm.resetTransform |
| desc: Tests resetTransform inside layers. |
| code: | |
| ctx.translate(40, 0); |
| |
| ctx.beginLayer(); |
| ctx.rotate(2); |
| ctx.beginLayer(); |
| ctx.scale(5, 6); |
| ctx.resetTransform(); |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(0, 0, 20, 20); |
| ctx.endLayer(); |
| ctx.endLayer(); |
| |
| ctx.fillStyle = 'green'; |
| ctx.fillRect(0, 0, 20, 20); |
| reference: | |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(0, 0, 20, 20); |
| |
| ctx.translate(40, 0); |
| ctx.fillStyle = 'green'; |
| ctx.fillRect(0, 0, 20, 20); |
| |
| - name: 2d.layer.clip-inside |
| desc: Check clipping set inside the layer |
| size: [100, 100] |
| code: | |
| ctx.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); |
| |
| ctx.beginPath(); |
| ctx.rect(15, 15, 70, 70); |
| ctx.clip(); |
| |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(10, 10, 80, 80); |
| ctx.endLayer(); |
| reference: | |
| const canvas2 = new OffscreenCanvas(200, 200); |
| const ctx2 = canvas2.getContext('2d'); |
| |
| ctx2.beginPath(); |
| ctx2.rect(15, 15, 70, 70); |
| ctx2.clip(); |
| |
| ctx2.fillStyle = 'blue'; |
| ctx2.fillRect(10, 10, 80, 80); |
| |
| ctx.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); |
| ctx.drawImage(canvas2, 0, 0); |
| ctx.endLayer(); |
| |
| - name: 2d.layer.clip-inside-and-outside |
| desc: Check clipping set inside and outside the layer |
| size: [100, 100] |
| code: | |
| ctx.beginPath(); |
| ctx.rect(15, 15, 70, 70); |
| ctx.clip(); |
| |
| ctx.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); |
| |
| ctx.beginPath(); |
| ctx.rect(15, 15, 70, 70); |
| ctx.clip(); |
| |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(10, 10, 80, 80); |
| ctx.endLayer(); |
| reference: | |
| const canvas2 = new OffscreenCanvas(200, 200); |
| const ctx2 = canvas2.getContext('2d'); |
| |
| ctx2.beginPath(); |
| ctx2.rect(15, 15, 70, 70); |
| ctx2.clip(); |
| |
| ctx2.fillStyle = 'blue'; |
| ctx2.fillRect(10, 10, 80, 80); |
| |
| const canvas3 = new OffscreenCanvas(200, 200); |
| const ctx3 = canvas3.getContext('2d'); |
| |
| ctx3.beginLayer({filter: {name: "gaussianBlur", stdDeviation: 12}}); |
| ctx3.drawImage(canvas2, 0, 0); |
| ctx3.endLayer(); |
| |
| ctx.beginPath(); |
| ctx.rect(15, 15, 70, 70); |
| ctx.clip(); |
| ctx.drawImage(canvas3, 0, 0); |
| |
| - name: 2d.layer.flush-on-frame-presentation |
| desc: Check that layers state stack is flushed and rebuilt on frame renders. |
| size: [200, 200] |
| canvasType: ['HTMLCanvas'] |
| test_type: "promise" |
| code: | |
| ctx.fillStyle = 'purple'; |
| ctx.fillRect(60, 60, 75, 50); |
| ctx.globalAlpha = 0.5; |
| |
| ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); |
| ctx.fillRect(40, 40, 75, 50); |
| ctx.fillStyle = 'grey'; |
| ctx.fillRect(50, 50, 75, 50); |
| |
| // Force a flush and restoration of the state stack: |
| await new Promise(resolve => requestAnimationFrame(resolve)); |
| |
| ctx.fillRect(70, 70, 75, 50); |
| ctx.fillStyle = 'orange'; |
| ctx.fillRect(80, 80, 75, 50); |
| ctx.endLayer(); |
| |
| ctx.fillRect(80, 40, 75, 50); |
| reference: | |
| ctx.fillStyle = 'purple'; |
| ctx.fillRect(60, 60, 75, 50); |
| ctx.globalAlpha = 0.5; |
| |
| ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); |
| ctx.fillStyle = 'purple'; |
| ctx.fillRect(40, 40, 75, 50); |
| ctx.fillStyle = 'grey'; |
| ctx.fillRect(50, 50, 75, 50); |
| |
| ctx.fillStyle = 'grey'; |
| ctx.fillRect(70, 70, 75, 50); |
| ctx.fillStyle = 'orange'; |
| ctx.fillRect(80, 80, 75, 50); |
| ctx.endLayer(); |
| |
| ctx.fillRect(80, 40, 75, 50); |
| |
| - name: 2d.layer.malformed-operations |
| desc: >- |
| Check that exceptions are thrown for operations that are malformed while |
| layers are open. |
| size: [200, 200] |
| code: | |
| {{ setup }} |
| // Shouldn't throw on its own. |
| {{ operation }}; |
| // Make sure the exception isn't caused by calling the function twice. |
| {{ operation }}; |
| // Calling again inside a layer should throw. |
| ctx.beginLayer(); |
| assert_throws_dom("InvalidStateError", |
| () => {{ operation }}); |
| variants: |
| createPattern: |
| operation: ctx.createPattern(canvas, 'repeat') |
| drawImage: |
| setup: |- |
| const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); |
| const ctx2 = canvas2.getContext('2d'); |
| operation: |- |
| ctx2.drawImage(canvas, 0, 0) |
| getImageData: |
| operation: ctx.getImageData(0, 0, {{ size[0] }}, {{ size[1] }}) |
| putImageData: |
| setup: |- |
| const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); |
| const ctx2 = canvas2.getContext('2d') |
| const data = ctx2.getImageData(0, 0, 1, 1); |
| operation: |- |
| ctx.putImageData(data, 0, 0) |
| toDataURL: |
| canvasType: ['HTMLCanvas'] |
| operation: canvas.toDataURL() |
| transferToImageBitmap: |
| canvasType: ['OffscreenCanvas', 'Worker'] |
| operation: canvas.transferToImageBitmap() |
| |
| - name: 2d.layer.malformed-operations-with-promises |
| desc: >- |
| Check that exceptions are thrown for operations that are malformed while |
| layers are open. |
| size: [200, 200] |
| test_type: "promise" |
| code: | |
| // Shouldn't throw on its own. |
| await {{ operation }}; |
| // Make sure the exception isn't caused by calling the function twice. |
| await {{ operation }}; |
| // Calling again inside a layer should throw. |
| ctx.beginLayer(); |
| await promise_rejects_dom(t, 'InvalidStateError', {{ operation }}); |
| variants: |
| convertToBlob: |
| canvasType: ['OffscreenCanvas', 'Worker'] |
| operation: |- |
| canvas.convertToBlob() |
| createImageBitmap: |
| operation: createImageBitmap(canvas) |
| toBlob: |
| canvasType: ['HTMLCanvas'] |
| operation: |- |
| new Promise(resolve => canvas.toBlob(resolve)) |
| |
| - name: 2d.layer.several-complex |
| desc: >- |
| Test to ensure beginlayer works for filter, alpha and shadow, even with |
| consecutive layers. |
| size: [500, 500] |
| fuzzy: maxDifference=0-3; totalPixels=0-6318 |
| code: | |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| ctx.fillRect(50, 50, 95, 70); |
| |
| ctx.globalAlpha = 0.5; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'orange'; |
| ctx.shadowBlur = 3 |
| |
| for (let i = 0; i < 5; i++) { |
| ctx.beginLayer(); |
| |
| ctx.fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx.fillRect(60 + i, 40 + i, 75, 50); |
| ctx.fillStyle = 'rgba(0, 255, 0, 1)'; |
| ctx.fillRect(80 + i, 60 + i, 75, 50); |
| |
| ctx.endLayer(); |
| } |
| reference: | |
| ctx.fillStyle = 'rgba(0, 0, 255, 1)'; |
| ctx.fillRect(50, 50, 95, 70); |
| |
| ctx.globalAlpha = 0.5; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.shadowColor = 'orange'; |
| ctx.shadowBlur = 3; |
| |
| var canvas2 = [5]; |
| var ctx2 = [5]; |
| |
| for (let i = 0; i < 5; i++) { |
| canvas2[i] = document.createElement("canvas"); |
| ctx2[i] = canvas2[i].getContext("2d"); |
| ctx2[i].fillStyle = 'rgba(225, 0, 0, 1)'; |
| ctx2[i].fillRect(60, 40, 75, 50); |
| ctx2[i].fillStyle = 'rgba(0, 255, 0, 1)'; |
| ctx2[i].fillRect(80, 60, 75, 50); |
| |
| ctx.drawImage(canvas2[i], i, i); |
| } |
| |
| - name: 2d.layer.reset |
| desc: Checks that reset discards any pending layers. |
| code: | |
| // Global states: |
| ctx.globalAlpha = 0.3; |
| ctx.globalCompositeOperation = 'source-in'; |
| ctx.shadowOffsetX = -3; |
| ctx.shadowOffsetY = 3; |
| ctx.shadowColor = 'rgba(0, 30, 0, 0.3)'; |
| ctx.shadowBlur = 3; |
| |
| ctx.beginLayer({filter: {name: 'dropShadow', dx: -3, dy: 3}}); |
| |
| // Layer states: |
| ctx.globalAlpha = 0.6; |
| ctx.globalCompositeOperation = 'source-in'; |
| ctx.shadowOffsetX = -6; |
| ctx.shadowOffsetY = 6; |
| ctx.shadowColor = 'rgba(0, 60, 0, 0.6)'; |
| ctx.shadowBlur = 3; |
| |
| ctx.reset(); |
| |
| ctx.fillRect(10, 10, 75, 50); |
| reference: |
| ctx.fillRect(10, 10, 75, 50); |
| |
| - name: 2d.layer.clearRect.partial |
| desc: clearRect inside a layer can clear a portion of the layer content. |
| size: [100, 100] |
| code: | |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(10, 10, 80, 50); |
| |
| ctx.beginLayer(); |
| ctx.fillStyle = 'red'; |
| ctx.fillRect(20, 20, 80, 50); |
| ctx.clearRect(30, 30, 60, 30); |
| ctx.endLayer(); |
| reference: | |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(10, 10, 80, 50); |
| |
| ctx.fillStyle = 'red'; |
| ctx.fillRect(20, 20, 80, 10); |
| ctx.fillRect(20, 60, 80, 10); |
| ctx.fillRect(20, 20, 10, 50); |
| ctx.fillRect(90, 20, 10, 50); |
| |
| - name: 2d.layer.clearRect.full |
| desc: clearRect inside a layer can clear all of the layer content. |
| size: [100, 100] |
| code: | |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(10, 10, 80, 50); |
| |
| ctx.beginLayer(); |
| ctx.fillStyle = 'red'; |
| ctx.fillRect(20, 20, 80, 50); |
| ctx.fillStyle = 'green'; |
| ctx.clearRect(0, 0, {{ size[0] }}, {{ size[1] }}); |
| ctx.endLayer(); |
| reference: | |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(10, 10, 80, 50); |
| |
| - name: 2d.layer.drawImage |
| size: [200, 200] |
| desc: >- |
| Checks that drawImage writes the image to the layer and not the parent |
| directly. |
| code: | |
| ctx.fillStyle = 'skyblue'; |
| ctx.fillRect(0, 0, 100, 100); |
| |
| ctx.beginLayer({filter: {name: 'dropShadow', dx: -10, dy: -10, |
| stdDeviation: 0, floodColor: 'navy'}}); |
| |
| ctx.fillStyle = 'maroon'; |
| ctx.fillRect(20, 20, 50, 50); |
| |
| ctx.globalCompositeOperation = 'xor'; |
| |
| // The image should xor only with the layer content, not the parents'. |
| const canvas_image = new OffscreenCanvas(200,200); |
| const ctx_image = canvas_image.getContext("2d"); |
| ctx_image.fillStyle = 'pink'; |
| ctx_image.fillRect(40, 40, 50, 50); |
| ctx.drawImage(canvas_image, 0, 0); |
| |
| ctx.endLayer(); |
| reference: | |
| ctx.fillStyle = 'skyblue'; |
| ctx.fillRect(0, 0, 100, 100); |
| |
| ctx.beginLayer({filter: {name: 'dropShadow', dx: -10, dy: -10, |
| stdDeviation: 0, floodColor: 'navy'}}); |
| |
| ctx.fillStyle = 'maroon'; |
| ctx.fillRect(20, 20, 50, 50); |
| |
| ctx.globalCompositeOperation = 'xor'; |
| |
| // Should xor only with the layer content, not the parents'. |
| ctx.fillStyle = 'pink'; |
| ctx.fillRect(40, 40, 50, 50); |
| |
| ctx.endLayer(); |
| |
| - name: 2d.layer.valid-calls |
| desc: No exception raised on {{ variant_desc }}. |
| variants: |
| save: |
| variant_desc: lone save() calls |
| code: ctx.save(); |
| beginLayer: |
| variant_desc: lone beginLayer() calls |
| code: ctx.beginLayer(); |
| restore: |
| variant_desc: lone restore() calls |
| code: ctx.restore(); |
| save_restore: |
| variant_desc: save() + restore() |
| code: |- |
| ctx.save(); |
| ctx.restore(); |
| save_reset_restore: |
| variant_desc: save() + reset() + restore() |
| code: |- |
| ctx.save(); |
| ctx.reset(); |
| ctx.restore(); |
| beginLayer-endLayer: |
| variant_desc: beginLayer() + endLayer() |
| code: |- |
| ctx.beginLayer(); |
| ctx.save(); |
| save-beginLayer: |
| variant_desc: save() + beginLayer() |
| code: |- |
| ctx.save(); |
| ctx.beginLayer(); |
| beginLayer-save: |
| variant_desc: beginLayer() + save() |
| code: |- |
| ctx.beginLayer(); |
| ctx.save(); |
| |
| - name: 2d.layer.invalid-calls |
| desc: Raises exception on {{ variant_desc }}. |
| code: | |
| assert_throws_dom("INVALID_STATE_ERR", function() { |
| {{ call_sequence | indent(2) }} |
| }); |
| variants: |
| endLayer: |
| variant_desc: lone endLayer calls |
| call_sequence: ctx.endLayer(); |
| save-endLayer: |
| variant_desc: save() + endLayer() |
| call_sequence: |- |
| ctx.save(); |
| ctx.endLayer(); |
| beginLayer-restore: |
| variant_desc: beginLayer() + restore() |
| call_sequence: |- |
| ctx.beginLayer(); |
| ctx.restore(); |
| save-beginLayer-restore: |
| variant_desc: save() + beginLayer() + restore() |
| call_sequence: |- |
| ctx.save(); |
| ctx.beginLayer(); |
| ctx.restore(); |
| beginLayer-save-endLayer: |
| variant_desc: beginLayer() + save() + endLayer() |
| call_sequence: |- |
| ctx.beginLayer(); |
| ctx.save(); |
| ctx.endLayer(); |
| beginLayer-reset-endLayer: |
| variant_desc: beginLayer() + reset() + endLayer() |
| call_sequence: |- |
| ctx.beginLayer(); |
| ctx.reset(); |
| ctx.endLayer(); |
| |
| - name: 2d.layer.exceptions-are-no-op |
| desc: Checks that the context state is left unchanged if beginLayer throws. |
| code: | |
| // Get `beginLayer` to throw while parsing the filter. |
| assert_throws_js(TypeError, |
| () => ctx.beginLayer({filter: {name: 'colorMatrix', |
| values: 'foo'}})); |
| // `beginLayer` shouldn't have opened the layer, so `endLayer` should throw. |
| assert_throws_dom("InvalidStateError", () => ctx.endLayer()); |
| |
| - name: 2d.layer.cross-layer-paths |
| desc: Checks that path defined in a layer is usable outside. |
| code: | |
| ctx.beginLayer(); |
| ctx.translate(50, 0); |
| ctx.moveTo(0, 0); |
| ctx.endLayer(); |
| ctx.lineTo(50, 100); |
| ctx.stroke(); |
| reference: |
| ctx.moveTo(50, 0); |
| ctx.lineTo(50, 100); |
| ctx.stroke(); |
| |
| - name: 2d.layer.beginLayer-options |
| desc: Checks beginLayer works for different option parameter values |
| code: | |
| ctx.beginLayer(); ctx.endLayer(); |
| ctx.beginLayer(null); ctx.endLayer(); |
| ctx.beginLayer(undefined); ctx.endLayer(); |
| ctx.beginLayer([]); ctx.endLayer(); |
| ctx.beginLayer({}); ctx.endLayer(); |
| |
| @assert throws TypeError ctx.beginLayer(''); |
| @assert throws TypeError ctx.beginLayer(0); |
| @assert throws TypeError ctx.beginLayer(1); |
| @assert throws TypeError ctx.beginLayer(true); |
| @assert throws TypeError ctx.beginLayer(false); |
| |
| ctx.beginLayer({filter: null}); ctx.endLayer(); |
| ctx.beginLayer({filter: undefined}); ctx.endLayer(); |
| ctx.beginLayer({filter: []}); ctx.endLayer(); |
| ctx.beginLayer({filter: {}}); ctx.endLayer(); |
| ctx.beginLayer({filter: {name: "unknown"}}); ctx.endLayer(); |
| ctx.beginLayer({filter: ''}); ctx.endLayer(); |
| |
| // These cases don't throw TypeError since they can be casted to a |
| // DOMString. |
| ctx.beginLayer({filter: 0}); ctx.endLayer(); |
| ctx.beginLayer({filter: 1}); ctx.endLayer(); |
| ctx.beginLayer({filter: true}); ctx.endLayer(); |
| ctx.beginLayer({filter: false}); ctx.endLayer(); |
| |
| - name: 2d.layer.blur-from-outside-canvas |
| desc: Checks blur leaking inside from drawing outside the canvas |
| size: [200, 200] |
| code: | |
| {{ clipping }} |
| |
| ctx.beginLayer({filter: [ {name: 'gaussianBlur', stdDeviation: 30} ]}); |
| |
| ctx.fillStyle = 'turquoise'; |
| ctx.fillRect(201, 50, 100, 100); |
| ctx.fillStyle = 'indigo'; |
| ctx.fillRect(50, 201, 100, 100); |
| ctx.fillStyle = 'orange'; |
| ctx.fillRect(-1, 50, -100, 100); |
| ctx.fillStyle = 'brown'; |
| ctx.fillRect(50, -1, 100, -100); |
| |
| ctx.endLayer(); |
| reference: | |
| const svg = ` |
| <svg xmlns="http://www.w3.org/2000/svg" |
| width="{{ size[0] }}" height="{{ size[1] }}" |
| color-interpolation-filters="sRGB"> |
| <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> |
| <feGaussianBlur in="SourceGraphic" stdDeviation="30" /> |
| </filter> |
| <g filter="url(#filter)"> |
| <rect x="201" y="50" width="100" height="100" fill="turquoise"/> |
| <rect x="50" y="201" width="100" height="100" fill="indigo"/> |
| <rect x="-101" y="50" width="100" height="100" fill="orange"/> |
| <rect x="50" y="-101" width="100" height="100" fill="brown"/> |
| </g> |
| </svg>`; |
| const img = new Image(); |
| img.width = {{ size[0] }}; |
| img.height = {{ size[1] }}; |
| img.onload = () => { |
| {{ clipping | indent(4) }} |
| |
| ctx.drawImage(img, 0, 0); |
| }; |
| img.src = 'data:image/svg+xml;base64,' + btoa(svg); |
| variants: |
| no-clipping: |
| clipping: // No clipping. |
| with-clipping: |
| clipping: |- |
| const clipRegion = new Path2D(); |
| clipRegion.rect(20, 20, 160, 160); |
| ctx.clip(clipRegion); |
| |
| - name: 2d.layer.shadow-from-outside-canvas |
| desc: Checks shadow produced by object drawn outside the canvas |
| size: [200, 200] |
| code: | |
| {{ distance }} |
| |
| {{ clipping }} |
| |
| ctx.beginLayer({filter: [ |
| {name: 'dropShadow', dx: -({{ size[0] }} + delta), |
| dy: -({{ size[1] }} + delta), stdDeviation: 0, |
| floodColor: 'green'}, |
| ]}); |
| |
| ctx.fillStyle = 'red'; |
| ctx.fillRect({{ size[0] }} + delta, {{ size[1] }} + delta, 100, 100); |
| |
| ctx.endLayer(); |
| reference: | |
| {{ distance }} |
| |
| {{ clipping }} |
| |
| ctx.fillStyle = 'green'; |
| ctx.fillRect(0, 0, 100, 100); |
| variants: |
| short-distance: |
| distance: |- |
| const delta = 1; |
| clipping: // No clipping. |
| short-distance-with-clipping: |
| distance: |- |
| const delta = 1; |
| clipping: |- |
| const clipRegion = new Path2D(); |
| clipRegion.rect(20, 20, 160, 160); |
| ctx.clip(clipRegion); |
| long-distance: |
| distance: |- |
| const delta = 10000; |
| clipping: // No clipping. |
| long-distance-with-clipping: |
| distance: |- |
| const delta = 10000; |
| clipping: |- |
| const clipRegion = new Path2D(); |
| clipRegion.rect(20, 20, 160, 160); |
| ctx.clip(clipRegion); |
| |
| - name: 2d.layer.opaque-canvas |
| desc: Checks that layer blending works inside opaque canvas |
| size: [300, 300] |
| code: | |
| {% if canvas_type == 'htmlcanvas' %} |
| const canvas2 = document.createElement('canvas'); |
| canvas2.width = 200; |
| canvas2.height = 200; |
| {% else %} |
| const canvas2 = new OffscreenCanvas(200, 200); |
| {% endif %} |
| const ctx2 = canvas2.getContext('2d', {alpha: false}); |
| |
| ctx2.fillStyle = 'purple'; |
| ctx2.fillRect(10, 10, 100, 100); |
| |
| ctx2.beginLayer({filter: {name: 'dropShadow', dx: -10, dy: 10, |
| stdDeviation: 0, |
| floodColor: 'rgba(200, 100, 50, 0.5)'}}); |
| ctx2.fillStyle = 'green'; |
| ctx2.fillRect(50, 50, 100, 100); |
| ctx2.globalAlpha = 0.8; |
| ctx2.fillStyle = 'yellow'; |
| ctx2.fillRect(75, 25, 100, 100); |
| ctx2.endLayer(); |
| |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(0, 0, 300, 300); |
| ctx.drawImage(canvas2, 0, 0); |
| reference: | |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(0, 0, 300, 300); |
| |
| ctx.fillStyle = 'black'; |
| ctx.fillRect(0, 0, 200, 200); |
| |
| ctx.fillStyle = 'purple'; |
| ctx.fillRect(10, 10, 100, 100); |
| |
| const canvas2 = new OffscreenCanvas(200, 200); |
| const ctx2 = canvas2.getContext('2d'); |
| ctx2.fillStyle = 'green'; |
| ctx2.fillRect(50, 50, 100, 100); |
| ctx2.globalAlpha = 0.8; |
| ctx2.fillStyle = 'yellow'; |
| ctx2.fillRect(75, 25, 100, 100); |
| |
| ctx.shadowColor = 'rgba(200, 100, 50, 0.5)'; |
| ctx.shadowOffsetX = -10; |
| ctx.shadowOffsetY = 10; |
| ctx.drawImage(canvas2, 0, 0); |
| |
| - name: 2d.layer.css-filters |
| desc: Checks that beginLayer works with a CSS filter string as input. |
| size: [200, 200] |
| code: &filter-test-code | |
| ctx.beginLayer({filter: {{ ctx_filter }}}); |
| |
| ctx.fillStyle = 'teal'; |
| ctx.fillRect(50, 50, 100, 100); |
| |
| ctx.endLayer(); |
| html_reference: &filter-test-reference | |
| <svg xmlns="http://www.w3.org/2000/svg" |
| width="{{ size[0] }}" height="{{ size[1] }}" |
| color-interpolation-filters="sRGB"> |
| <filter id="filter" x="-100%" y="-100%" width="300%" height="300%"> |
| {{ svg_filter | indent(4) }} |
| </filter> |
| <g filter="url(#filter)"> |
| <rect x="50" y="50" width="100" height="100" fill="teal"/> |
| </g> |
| </svg> |
| variants: |
| blur: |
| ctx_filter: |- |
| 'blur(10px)' |
| svg_filter: |- |
| <feGaussianBlur stdDeviation="10" /> |
| shadow: |
| ctx_filter: |- |
| 'drop-shadow(-10px -10px 5px purple)' |
| svg_filter: |- |
| <feDropShadow dx="-10" dy="-10" stdDeviation="5" flood-color="purple" /> |
| blur-and-shadow: |
| ctx_filter: |- |
| 'blur(5px) drop-shadow(10px 10px 5px orange)' |
| svg_filter: |- |
| <feGaussianBlur stdDeviation="5" /> |
| <feDropShadow dx="10" dy="10" stdDeviation="5" flood-color="orange" /> |
| |
| - name: 2d.layer.anisotropic-blur |
| desc: Checks that layers allow gaussian blur with separate X and Y components. |
| size: [200, 200] |
| code: *filter-test-code |
| html_reference: *filter-test-reference |
| variants: |
| x-only: |
| ctx_filter: |- |
| { name: 'gaussianBlur', stdDeviation: [4, 0] } |
| svg_filter: |- |
| <feGaussianBlur stdDeviation="4 0" /> |
| mostly-x: |
| ctx_filter: |- |
| { name: 'gaussianBlur', stdDeviation: [4, 1] } |
| svg_filter: |- |
| <feGaussianBlur stdDeviation="4 1" /> |
| isotropic: |
| ctx_filter: |- |
| { name: 'gaussianBlur', stdDeviation: [4, 4] } |
| svg_filter: |- |
| <feGaussianBlur stdDeviation="4 4" /> |
| mostly-y: |
| ctx_filter: |- |
| { name: 'gaussianBlur', stdDeviation: [1, 4] } |
| svg_filter: |- |
| <feGaussianBlur stdDeviation="1 4" /> |
| y-only: |
| ctx_filter: |- |
| { name: 'gaussianBlur', stdDeviation: [0, 4] } |
| svg_filter: |- |
| <feGaussianBlur stdDeviation="0 4" /> |
| |
| - name: 2d.layer.nested-filters |
| desc: Checks that nested layers work properly when both apply filters. |
| size: [400, 200] |
| code: | |
| ctx.beginLayer({filter: {name: 'dropShadow', dx: -20, dy: -20, |
| stdDeviation: 0, floodColor: 'yellow'}}); |
| ctx.beginLayer({filter: 'drop-shadow(-10px -10px 0 blue)'}); |
| |
| ctx.fillStyle = 'red'; |
| ctx.fillRect(50, 50, 100, 100); |
| |
| ctx.endLayer(); |
| ctx.endLayer(); |
| |
| ctx.beginLayer({filter: 'drop-shadow(20px 20px 0 blue)'}); |
| ctx.beginLayer({filter: {name: 'dropShadow', dx: 10, dy: 10, |
| stdDeviation: 0, floodColor: 'yellow'}}); |
| |
| ctx.fillStyle = 'red'; |
| ctx.fillRect(250, 50, 100, 100); |
| |
| ctx.endLayer(); |
| ctx.endLayer(); |
| reference: | |
| ctx.fillStyle = 'yellow'; |
| ctx.fillRect(20, 20, 100, 100); |
| ctx.fillRect(30, 30, 100, 100); |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(40, 40, 100, 100); |
| ctx.fillStyle = 'red'; |
| ctx.fillRect(50, 50, 100, 100); |
| |
| ctx.fillStyle = 'blue'; |
| ctx.fillRect(280, 80, 100, 100); |
| ctx.fillRect(270, 70, 100, 100); |
| ctx.fillStyle = 'yellow'; |
| ctx.fillRect(260, 60, 100, 100); |
| ctx.fillStyle = 'red'; |
| ctx.fillRect(250, 50, 100, 100); |