- name: 2d.composite.globalAlpha.range code: | ctx.globalAlpha = 0.5; // This may not set it to exactly 0.5 if it is rounded/quantised, so // remember for future comparisons. var a = ctx.globalAlpha; @assert ctx.globalAlpha === a; ctx.globalAlpha = 1.1; @assert ctx.globalAlpha === a; ctx.globalAlpha = -0.1; @assert ctx.globalAlpha === a; ctx.globalAlpha = 0; @assert ctx.globalAlpha === 0; ctx.globalAlpha = 1; @assert ctx.globalAlpha === 1; - name: 2d.composite.globalAlpha.invalid code: | ctx.globalAlpha = 0.5; // This may not set it to exactly 0.5 if it is rounded/quantised, so // remember for future comparisons. var a = ctx.globalAlpha; ctx.globalAlpha = Infinity; @assert ctx.globalAlpha === a; ctx.globalAlpha = -Infinity; @assert ctx.globalAlpha === a; ctx.globalAlpha = NaN; @assert ctx.globalAlpha === a; - name: 2d.composite.globalAlpha.default code: | @assert ctx.globalAlpha === 1.0; - name: 2d.composite.globalAlpha.fill code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); // Avoiding any potential alpha = 0 optimisations. ctx.globalAlpha = 0.01; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); @assert pixel 50,25 ==~ 2,253,0,255; expected: green - name: 2d.composite.globalAlpha.canvas canvas_types: ['HtmlCanvas'] code: | var canvas2 = document.createElement('canvas'); canvas2.width = 100; canvas2.height = 50; var ctx2 = canvas2.getContext('2d'); ctx2.fillStyle = '#f00'; ctx2.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); // Avoiding any potential alpha = 0 optimisations. ctx.globalAlpha = 0.01; ctx.drawImage(canvas2, 0, 0); @assert pixel 50,25 ==~ 2,253,0,255; expected: green - name: 2d.composite.globalAlpha.canvaspattern canvas_types: ['HtmlCanvas'] code: | var canvas2 = document.createElement('canvas'); canvas2.width = 100; canvas2.height = 50; var ctx2 = canvas2.getContext('2d'); ctx2.fillStyle = '#f00'; ctx2.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = ctx.createPattern(canvas2, 'no-repeat'); // Avoiding any potential alpha = 0 optimisations. ctx.globalAlpha = 0.01; ctx.fillRect(0, 0, 100, 50); @assert pixel 50,25 ==~ 2,253,0,255; expected: green - name: 2d.composite.globalAlpha.canvascopy canvas_types: ['HtmlCanvas'] code: | var canvas2 = document.createElement('canvas'); canvas2.width = 100; canvas2.height = 50; var ctx2 = canvas2.getContext('2d'); ctx2.fillStyle = '#0f0'; ctx2.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.globalCompositeOperation = 'copy' ctx.globalAlpha = 0.51; ctx.drawImage(canvas2, 0, 0); @assert pixel 50,25 ==~ 0,255,0,130; expected: green - name: 2d.composite.operation.get code: | var modes = ['source-atop', 'source-in', 'source-out', 'source-over', 'destination-atop', 'destination-in', 'destination-out', 'destination-over', 'lighter', 'copy', 'xor']; for (var i = 0; i < modes.length; ++i) { ctx.globalCompositeOperation = modes[i]; @assert ctx.globalCompositeOperation === modes[i]; } - name: 2d.composite.operation.unrecognised code: | ctx.globalCompositeOperation = 'xor'; ctx.globalCompositeOperation = 'nonexistent'; @assert ctx.globalCompositeOperation === 'xor'; - name: 2d.composite.operation.darker code: | ctx.globalCompositeOperation = 'xor'; ctx.globalCompositeOperation = 'darker'; @assert ctx.globalCompositeOperation === 'xor'; - name: 2d.composite.operation.over code: | ctx.globalCompositeOperation = 'xor'; ctx.globalCompositeOperation = 'over'; @assert ctx.globalCompositeOperation === 'xor'; - name: 2d.composite.operation.clear code: | ctx.globalCompositeOperation = 'xor'; ctx.globalCompositeOperation = 'clear'; @assert ctx.globalCompositeOperation === 'clear'; - name: 2d.composite.operation.highlight code: | ctx.globalCompositeOperation = 'xor'; ctx.globalCompositeOperation = 'highlight'; @assert ctx.globalCompositeOperation === 'xor'; - name: 2d.composite.operation.nullsuffix code: | ctx.globalCompositeOperation = 'xor'; ctx.globalCompositeOperation = 'source-over\0'; @assert ctx.globalCompositeOperation === 'xor'; - name: 2d.composite.operation.casesensitive code: | ctx.globalCompositeOperation = 'xor'; ctx.globalCompositeOperation = 'Source-over'; @assert ctx.globalCompositeOperation === 'xor'; - name: 2d.composite.operation.default code: | @assert ctx.globalCompositeOperation === 'source-over'; - name: 2d.composite.globalAlpha.image test_type: promise code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); // Avoiding any potential alpha = 0 optimisations. ctx.globalAlpha = 0.01; const response = await fetch('/images/red.png'); const blob = await response.blob(); const bitmap = await createImageBitmap(blob); ctx.drawImage(bitmap, 0, 0); @assert pixel 50,25 ==~ 2,253,0,255; expected: green - name: 2d.composite.globalAlpha.canvas canvas_types: ['OffscreenCanvas', 'Worker'] code: | var offscreenCanvas2 = new OffscreenCanvas(100, 50); var ctx2 = offscreenCanvas2.getContext('2d'); ctx2.fillStyle = '#f00'; ctx2.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); // Avoiding any potential alpha = 0 optimisations. ctx.globalAlpha = 0.01; ctx.drawImage(offscreenCanvas2, 0, 0); @assert pixel 50,25 ==~ 2,253,0,255; - name: 2d.composite.globalAlpha.imagepattern test_type: promise code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); const response = await fetch('/images/red.png'); const blob = await response.blob(); const bitmap = await createImageBitmap(blob); ctx.fillStyle = ctx.createPattern(bitmap, 'no-repeat'); // Avoiding any potential alpha = 0 optimisations. ctx.globalAlpha = 0.01; ctx.fillRect(0, 0, 100, 50); @assert pixel 50,25 ==~ 2,253,0,255; expected: green - name: 2d.composite.globalAlpha.canvaspattern canvas_types: ['OffscreenCanvas', 'Worker'] code: | var offscreenCanvas2 = new OffscreenCanvas(100, 50); var ctx2 = offscreenCanvas2.getContext('2d'); ctx2.fillStyle = '#f00'; ctx2.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = ctx.createPattern(offscreenCanvas2, 'no-repeat'); // Avoiding any potential alpha = 0 optimisations. ctx.globalAlpha = 0.01; ctx.fillRect(0, 0, 100, 50); @assert pixel 50,25 ==~ 2,253,0,255; - name: 2d.composite.globalAlpha.canvascopy canvas_types: ['OffscreenCanvas', 'Worker'] code: | var offscreenCanvas2 = new OffscreenCanvas(100, 50); var ctx2 = offscreenCanvas2.getContext('2d'); ctx2.fillStyle = '#0f0'; ctx2.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.globalCompositeOperation = 'copy' ctx.globalAlpha = 0.51; ctx.drawImage(offscreenCanvas2, 0, 0); @assert pixel 50,25 ==~ 0,255,0,130; - name: 2d.composite.grid size: [80, 60] code: | ctx.fillStyle = 'rgba(0, 102, 240, 0.8)'; ctx.fillRect(15, 15, 50, 30); ctx.translate(25, 20); ctx.rotate(Math.PI / 2); ctx.scale(0.6, 1.2); ctx.translate(-25, -20); ctx.globalAlpha = 0.5; {{ js_filter_code }} {{ js_shadow_code }} ctx.globalCompositeOperation = '{{ variant_names[0] }}'; {{ js_draw_code }} cairo_reference: | # Background. cr.push_group() cr.set_source_rgba(0, 102/255, 240/255, 0.8) cr.rectangle(15, 15, 50, 30) cr.fill() background = cr.pop_group() # Foreground. cr.push_group() cr.translate(25, 20) cr.rotate(math.pi / 2) cr.scale(0.6, 1.2) cr.translate(-25, -20) cr.set_source_rgba(52/255, 1, 52/255, 0.5) cr.rectangle(5, 5, 50, 30) cr.fill() foreground = cr.pop_group() # Filtered foreground. cr.push_group() {{ cairo_filter_code }} cr.set_source(foreground) cr.paint() filtered_foreground = cr.pop_group() {% if cairo_operator != 'SOURCE' %} cr.set_source(background) cr.paint() {% endif %} cr.set_operator(cairo.OPERATOR_{{ cairo_operator }}) {% if cairo_operator != 'SOURCE' %} {{ cairo_shadow_code }} {% endif %} cr.set_source(filtered_foreground) cr.paint() fuzzy: maxDifference=0-4; totalPixels=0-33000 variants_layout: - single_file - multi_files - multi_files - multi_files grid_width: 6 variants: - source-over: cairo_operator: OVER source-in: cairo_operator: IN source-out: cairo_operator: OUT source-atop: cairo_operator: ATOP destination-over: cairo_operator: DEST_OVER destination-in: cairo_operator: DEST_IN destination-out: cairo_operator: DEST_OUT destination-atop: cairo_operator: DEST_ATOP lighter: cairo_operator: ADD copy: cairo_operator: SOURCE xor: cairo_operator: XOR multiply: cairo_operator: MULTIPLY screen: cairo_operator: SCREEN overlay: cairo_operator: OVERLAY darken: cairo_operator: DARKEN lighten: cairo_operator: LIGHTEN color-dodge: cairo_operator: COLOR_DODGE color-burn: cairo_operator: COLOR_BURN hard-light: cairo_operator: HARD_LIGHT soft-light: cairo_operator: SOFT_LIGHT difference: cairo_operator: DIFFERENCE exclusion: cairo_operator: EXCLUSION hue: cairo_operator: HSL_HUE saturation: cairo_operator: HSL_SATURATION color: cairo_operator: HSL_COLOR luminosity: cairo_operator: HSL_LUMINOSITY - no_filter: js_filter_code: // No filter. cairo_filter_code: "# No filter." filter: js_filter_code: |- ctx.filter = 'drop-shadow(5px -5px 0px rgb(255, 154, 100))'; cairo_filter_code: |- cr.push_group() cr.set_operator(cairo.OPERATOR_OVER) cr.translate(5, -5) # Filter offset. cr.set_source(foreground) cr.paint() cr.set_operator(cairo.OPERATOR_IN) cr.set_source_rgb(1, 154/255, 100/255) cr.paint() cr.pop_group_to_source() cr.paint() - no_shadow: js_shadow_code: // No shadow. cairo_shadow_code: "# No shadow." shadow: js_shadow_code: |- ctx.shadowOffsetX = 20; ctx.shadowOffsetY = 20; ctx.shadowColor = 'rgba(154, 0, 154, 0.8)'; cairo_shadow_code: |- cr.push_group() cr.set_operator(cairo.OPERATOR_OVER) cr.translate(20, 20) # Shadow offset. cr.set_source(filtered_foreground) cr.paint() cr.set_operator(cairo.OPERATOR_IN) cr.set_source_rgba(154/255, 0, 154/255, 0.8) cr.paint() cr.pop_group_to_source() cr.paint() - fillRect: js_draw_code: |- ctx.fillStyle = 'rgb(52, 255, 52)'; ctx.fillRect(5, 5, 50, 30); drawImage: js_draw_code: |- const img_canvas = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); const img_ctx = img_canvas.getContext('2d'); img_ctx.fillStyle = 'rgb(52, 255, 52)'; img_ctx.fillRect(0, 0, {{ size[0] }}, {{ size[1] }}); ctx.drawImage(img_canvas, 5, 5, 50, 30); pattern: js_draw_code: |- const img_canvas = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); const img_ctx = img_canvas.getContext('2d'); img_ctx.fillStyle = 'rgb(52, 255, 52)'; img_ctx.fillRect(0, 0, {{ size[0] }}, {{ size[1] }}); ctx.fillStyle = ctx.createPattern(img_canvas, 'repeat'); ctx.fillRect(5, 5, 50, 30); # Composite operation tests # - name: 2d.composite macros: | {% macro calc_output(A, B, FA, FB) %} {% set RA, GA, BA, aA = A -%} {% set RB, GB, BB, aB = B -%} {% set rA, gA, bA = RA * aA, GA * aA, BA * aA -%} {% set rB, gB, bB = RB * aB, GB * aB, BB * aB -%} {% set FA = FA[0] + FA[1] * aA + FA[2] * aB -%} {% set FB = FB[0] + FB[1] * aA + FB[2] * aB -%} {% set rO = rA * FA + rB * FB -%} {% set gO = gA * FA + gB * FB -%} {% set bO = bA * FA + bB * FB -%} {% set aO = aA * FA + aB * FB -%} {% set rO = (255, rO) | min -%} {% set gO = (255, gO) | min -%} {% set bO = (255, bO) | min -%} {% set aO = (1, aO) | min -%} {% set RO = rO / aO if aO else 0 -%} {% set GO = gO / aO if aO else 0 -%} {% set BO = bO / aO if aO else 0 -%} {{- '%f,%f,%f,%f' | format(RO, GO, BO, aO) -}} {% endmacro %} {% macro rgba_format(color) %} {% set r, g, b, a = color -%} rgba{{ (r, g, b, a) -}} {% endmacro %} {% macro js_format(color) %} {% set r, g, b, a = color.split(',') | map('float') %} {{- '%d,%d,%d,%d' | format(r | round, g | round, b | round, (a * 255) | round) -}} {% endmacro %} {% macro cairo_format(color) %} {% set r, g, b, a = color.split(',') | map('float') %} {{- '%f,%f,%f,%f' | format(r / 255.0, g / 255.0, b / 255.0, a) -}} {% endmacro %} code: | {% import 'macros' as m -%} ctx.fillStyle = '{{ m.rgba_format(dest_color) }}'; ctx.fillRect(0, 0, 100, 50); ctx.globalCompositeOperation = '{{ variant_names[1] }}'; {{ draw_code }} {{ assertion }} assertion: |- {% import 'macros' as m -%} @assert pixel 50,25 ==~ {{ m.js_format(expected_color) }} +/- 5; expected: | {% import 'macros' as m %} size 100 50 cr.set_source_rgba({{ m.cairo_format(expected_color) }}) cr.rectangle(0, 0, 100, 50) cr.fill() new_auxiliary_canvas: |- {%- if canvas_type == 'HtmlCanvas' -%} document.createElement('canvas'); canvas2.width = canvas.width; canvas2.height = canvas.height; {%- else -%} new OffscreenCanvas(canvas.width, canvas.height); {%- endif -%} variants: - solid: src_color: [255, 255, 0, 1.0] dest_color: [0, 255, 255, 1.0] draw_code: |- {% import 'macros' as m %} ctx.fillStyle = '{{ m.rgba_format(src_color) }}'; ctx.fillRect(0, 0, 100, 50); expected_color: | {% import 'macros' as m %} {{ m.calc_output(src_color, dest_color, fa, fb) }} transparent: src_color: [0, 0, 255, 0.75] dest_color: [0, 255, 0, 0.5] draw_code: |- {% import 'macros' as m %} ctx.fillStyle = '{{ m.rgba_format(src_color) }}'; ctx.fillRect(0, 0, 100, 50); expected_color: | {% import 'macros' as m %} {{ m.calc_output(src_color, dest_color, fa, fb) }} image: src_color: [255, 255, 0, 0.75] dest_color: [0, 255, 255, 0.5] test_type: 'promise' draw_code: |- const response = await fetch('/images/yellow75.png') const blob = await response.blob(); const bitmap = await createImageBitmap(blob); ctx.drawImage(bitmap, 0, 0); expected_color: | {% import 'macros' as m %} {{ m.calc_output(src_color, dest_color, fa, fb) }} canvas: src_color: [255, 255, 0, 0.75] dest_color: [0, 255, 255, 0.5] test_type: 'promise' draw_code: |- const canvas2 = {{ new_auxiliary_canvas }} const ctx2 = canvas2.getContext('2d'); const response = await fetch('/images/yellow75.png') const blob = await response.blob(); const bitmap = await createImageBitmap(blob); ctx2.drawImage(bitmap, 0, 0); ctx.drawImage(canvas2, 0, 0); expected_color: | {% import 'macros' as m %} {{ m.calc_output(src_color, dest_color, fa, fb) }} uncovered.fill: desc: >- fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged. src_color: [0, 0, 255, 0.75] dest_color: [0, 255, 0, 0.5] draw_code: |- {% import 'macros' as m %} ctx.fillStyle = '{{ m.rgba_format(src_color) }}'; ctx.translate(0, 25); ctx.fillRect(0, 50, 100, 50); expected_color: | {% import 'macros' as m %} {{ m.calc_output([0, 0, 0, 0], dest_color, fa, fb) }} enabled: |- {{ variant_names[1] in ['source-in', 'destination-in', 'source-out', 'destination-atop', 'copy'] }} timeout: |- {%- if variant_names[1] == 'destination-in' and canvas_type != 'HtmlCanvas' -%} long {%- endif -%} uncovered.image: desc: >- drawImage() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged. src_color: [255, 255, 0, 1.0] dest_color: [0, 255, 255, 0.5] test_type: 'promise' draw_code: |- const response = await fetch('/images/yellow.png') const blob = await response.blob(); const bitmap = await createImageBitmap(blob); ctx.drawImage(bitmap, 40, 40, 10, 10, 40, 50, 10, 10); expected_color: | {% import 'macros' as m %} {{ m.calc_output([0, 0, 0, 0], dest_color, fa, fb) }} enabled: |- {{ variant_names[1] in ['source-in', 'destination-in', 'source-out', 'destination-atop', 'copy'] }} uncovered.nocontext: desc: >- drawImage() of a canvas with no context draws pixels as (0,0,0,0), and does not leave the pixels unchanged. src_color: [255, 255, 0, 1.0] dest_color: [0, 255, 255, 0.5] draw_code: |- const canvas2 = {{ new_auxiliary_canvas }} ctx.drawImage(canvas2, 0, 0); expected_color: | {% import 'macros' as m %} {{ m.calc_output([0, 0, 0, 0], dest_color, fa, fb) }} enabled: |- {{ variant_names[1] in ['source-in', 'destination-in', 'source-out', 'destination-atop', 'copy'] }} uncovered.pattern: desc: >- Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged. src_color: [255, 255, 0, 1.0] dest_color: [0, 255, 255, 0.5] test_type: 'promise' draw_code: |- const response = await fetch('/images/yellow.png') const blob = await response.blob(); const bitmap = await createImageBitmap(blob); ctx.fillStyle = ctx.createPattern(bitmap, 'no-repeat'); ctx.fillRect(0, 50, 100, 50); expected_color: | {% import 'macros' as m %} {{ m.calc_output([0, 0, 0, 0], dest_color, fa, fb) }} enabled: |- {{ variant_names[1] in ['source-in', 'destination-in', 'source-out', 'destination-atop', 'copy'] }} clip: desc: fill() does not affect pixels outside the clip region. src_color: [255, 0, 0, 1] dest_color: [0, 255, 0, 1] draw_code: |- {% import 'macros' as m %} ctx.rect(-20, -20, 10, 10); ctx.clip(); ctx.fillStyle = '{{ m.rgba_format(src_color) }}'; ctx.fillRect(0, 0, 50, 50); assertion: |- @assert pixel 50,25 == 0,255,0,255; expected: green - source-over: fa: [1, 0, 0] # 1 fb: [1, -1, 0] # 1-aA destination-over: fa: [1, 0, -1] # 1-aB fb: [1, 0, 0] # 1 source-in: fa: [0, 0, 1] # aB fb: [0, 0, 0] # 0 destination-in: fa: [0, 0, 0] # 0 fb: [0, 1, 0] # aA source-out: fa: [1, 0, -1] # 1-aB fb: [0, 0, 0] # 0 destination-out: fa: [0, 0, 0] # 0 fb: [1, -1, 0] # 1-aA source-atop: fa: [0, 0, 1] # aB fb: [1, -1, 0] # 1-aA destination-atop: fa: [1, 0, -1] # 1-aB fb: [0, 1, 0] # aA xor: fa: [1, 0, -1] # 1-aB fb: [1, -1, 0] # 1-aA copy: fa: [1, 0, 0] # 1 fb: [0, 0, 0] # 0 lighter: fa: [1, 0, 0] # 1 fb: [1, 0, 0] # 1 clear: fa: [0, 0, 0] # 0 fb: [0, 0, 0] # 0