summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/canvas/tools/yaml-new/layers.yaml
diff options
context:
space:
mode:
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.yaml1022
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);