diff options
Diffstat (limited to 'testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml')
-rw-r--r-- | testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml | 1022 |
1 files changed, 1022 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml b/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml new file mode 100644 index 0000000000..a44cb2ea2c --- /dev/null +++ b/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml @@ -0,0 +1,1022 @@ +- 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: + 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: + render_states: |- + ctx.globalAlpha = 0.6; + ctx.globalCompositeOperation = 'multiply'; + alpha.composite: + render_states: |- + ctx.globalAlpha = 0.6; + ctx.globalCompositeOperation = 'source-in'; + 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 + +- 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.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.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); |