- 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 = ` `; 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 = ` `; 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_filter | indent(4) }} variants: blur: ctx_filter: |- 'blur(10px)' svg_filter: |- shadow: ctx_filter: |- 'drop-shadow(-10px -10px 5px purple)' svg_filter: |- blur-and-shadow: ctx_filter: |- 'blur(5px) drop-shadow(10px 10px 5px orange)' svg_filter: |- - 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: |- mostly-x: ctx_filter: |- { name: 'gaussianBlur', stdDeviation: [4, 1] } svg_filter: |- isotropic: ctx_filter: |- { name: 'gaussianBlur', stdDeviation: [4, 4] } svg_filter: |- mostly-y: ctx_filter: |- { name: 'gaussianBlur', stdDeviation: [1, 4] } svg_filter: |- y-only: ctx_filter: |- { name: 'gaussianBlur', stdDeviation: [0, 4] } svg_filter: |- - 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);