Source code

Revision control

Copy as Markdown

Other Tools

- name: 2d.layer.global-states
desc: Checks that layers correctly use global render states.
size: [90, 90]
code: |
{{ transform_statement }}
ctx.fillStyle = 'rgba(128, 128, 128, 1)';
ctx.fillRect(20, 15, 50, 50);
{{ alpha_statement }}
{{ composite_op_statement }}
{{ shadow_statement }}
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(255, 0, 0, 1)';
ctx.fillRect(10, 25, 40, 50);
ctx.fillStyle = 'rgba(0, 255, 0, 1)';
ctx.fillRect(30, 5, 50, 40);
ctx.endLayer();
reference: |
{{ transform_statement }}
ctx.fillStyle = 'rgba(128, 128, 128, 1)';
ctx.fillRect(20, 15, 50, 50);
{{ alpha_statement }}
{{ composite_op_statement }}
{{ shadow_statement }}
const canvas2 = document.createElement("canvas");
const ctx2 = canvas2.getContext("2d");
ctx2.globalCompositeOperation = 'screen';
ctx2.fillStyle = 'rgba(255, 0, 0, 1)';
ctx2.fillRect(10, 25, 40, 50);
ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
ctx2.fillRect(30, 5, 50, 40);
ctx.drawImage(canvas2, 0, 0);
variants_layout: [single_file, multi_files, multi_files, multi_files]
variants: &global-state-variants
- no-globalAlpha:
alpha_statement: // No globalAlpha.
globalAlpha:
alpha_statement: ctx.globalAlpha = 0.75;
- no-composite-op:
composite_op_statement: // No globalCompositeOperation.
blending:
composite_op_statement: ctx.globalCompositeOperation = 'multiply';
composite:
composite_op_statement: ctx.globalCompositeOperation = 'source-in';
copy:
composite_op_statement: ctx.globalCompositeOperation = 'copy';
- no-shadow:
shadow_statement: // No shadow.
shadow:
shadow_statement: |-
ctx.shadowOffsetX = -7;
ctx.shadowOffsetY = 7;
ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
ctx.shadowBlur = 3;
- no-transform:
transform_statement: // No transform.
rotation:
transform_statement: |-
ctx.translate(50, 40);
ctx.rotate(Math.PI);
ctx.translate(-45, -45);
- name: 2d.layer.global-states.filter
desc: Checks that layers with filters correctly use global render states.
size: [90, 90]
code: |
{{ transform_statement }}
ctx.fillStyle = 'rgba(128, 128, 128, 1)';
ctx.fillRect(20, 15, 50, 50);
{{ alpha_statement }}
{{ composite_op_statement }}
{{ shadow_statement }}
ctx.beginLayer({filter: [
{name: 'dropShadow',
dx: 5, dy: 5, stdDeviation: 0, floodColor: '#00f'},
{name: 'componentTransfer',
funcA: {type: "table", tableValues: [0, 0.8]}}]});
ctx.fillStyle = 'rgba(255, 0, 0, 1)';
ctx.fillRect(10, 25, 40, 50);
ctx.fillStyle = 'rgba(0, 255, 0, 1)';
ctx.fillRect(30, 5, 50, 40);
ctx.endLayer();
reference: |
const 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="#00f" />
<feComponentTransfer>
<feFuncA type="table" tableValues="0 0.8"></feFuncA>
</feComponentTransfer>
</filter>
<g filter="url(#filter)">
<rect x="10" y="25" width="40" height="50" fill="rgba(255, 0, 0, 1)"/>
<rect x="30" y="5" width="50" height="40" fill="rgba(0, 255, 0, 1)"/>
</g>
</svg>`;
const img = new Image();
img.width = {{ size[0] }};
img.height = {{ size[1] }};
img.onload = () => {
{{ transform_statement | indent(2) }}
ctx.fillStyle = 'rgba(128, 128, 128, 1)';
ctx.fillRect(20, 15, 50, 50);
{{ alpha_statement | indent(2) }}
{{ composite_op_statement | indent(2) }}
{{ shadow_statement | indent(2) }}
ctx.drawImage(img, 0, 0);
};
img.src = 'data:image/svg+xml;base64,' + btoa(svg);
variants_layout: [single_file, multi_files, multi_files, multi_files]
variants: *global-state-variants
- name: 2d.layer.globalCompositeOperation
desc: Checks that layers work with all globalCompositeOperation values.
size: [90, 90]
code: |
ctx.translate(50, 50);
ctx.scale(2, 2);
ctx.rotate(Math.PI);
ctx.translate(-25, -25);
ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
ctx.fillRect(15, 15, 25, 25);
ctx.globalAlpha = 0.75;
ctx.globalCompositeOperation = '{{ variant_names[0] }}';
ctx.shadowOffsetX = 7;
ctx.shadowOffsetY = 7;
ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
ctx.beginLayer();
ctx.fillStyle = 'rgba(204, 0, 0, 1)';
ctx.fillRect(10, 25, 25, 20);
ctx.fillStyle = 'rgba(0, 204, 0, 1)';
ctx.fillRect(25, 10, 20, 25);
ctx.endLayer();
reference: |
ctx.translate(50, 50);
ctx.scale(2, 2);
ctx.rotate(Math.PI);
ctx.translate(-25, -25);
ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
ctx.fillRect(15, 15, 25, 25);
ctx.globalAlpha = 0.75;
ctx.globalCompositeOperation = '{{ variant_names[0] }}';
ctx.shadowOffsetX = 7;
ctx.shadowOffsetY = 7;
ctx.shadowColor = 'rgba(255, 165, 0, 0.5)';
const canvas2 = document.createElement("canvas");
const ctx2 = canvas2.getContext("2d");
ctx2.fillStyle = 'rgba(204, 0, 0, 1)';
ctx2.fillRect(10, 25, 25, 20);
ctx2.fillStyle = 'rgba(0, 204, 0, 1)';
ctx2.fillRect(25, 10, 20, 25);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(canvas2, 0, 0);
variants_layout: [single_file]
grid_width: 7
variants:
- source-over:
source-in:
source-atop:
destination-over:
destination-in:
destination-out:
destination-atop:
lighter:
copy:
xor:
multiply:
screen:
overlay:
darken:
lighten:
color-dodge:
color-burn:
hard-light:
soft-light:
difference:
exclusion:
hue:
saturation:
color:
luminosity:
- 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.
test_type: sync
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: |
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.
test_type: sync
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]
canvas_types: ['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: Throws if {{ variant_names[0] }} is called while layers are open.
size: [200, 200]
test_type: sync
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_layout: [single_file]
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:
canvas_types: ['HtmlCanvas']
operation: canvas.toDataURL()
transferToImageBitmap:
canvas_types: ['OffscreenCanvas', 'Worker']
operation: canvas.transferToImageBitmap()
- name: 2d.layer.malformed-operations-with-promises
desc: Throws if {{ variant_names[0] }} is called 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_layout: [single_file]
variants:
- convertToBlob:
canvas_types: ['OffscreenCanvas', 'Worker']
operation: |-
canvas.convertToBlob()
createImageBitmap:
operation: createImageBitmap(canvas)
toBlob:
canvas_types: ['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 }}.
test_type: sync
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.
test_type: sync
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
test_type: sync
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 = `
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 |
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);