canvas-composite-modes.html (6074B)
1 <!DOCTYPE HTML> 2 <meta charset="utf-8"> 3 <title>Test of <composite-mode> values in canvas globalCompositeOperation</title> 4 <link rel="help" href="https://html.spec.whatwg.org/multipage/C#compositing"> 5 <link rel="help" href="https://drafts.fxtf.org/compositing/#canvascompositingandblending"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 9 <canvas id="canvas" width="2" height="2"></canvas> 10 11 <script> 12 13 // Test a small set of sRGB color and alpha values in the 0-255 range. 14 const VALUES = [ 0, 47, 193, 255 ]; 15 16 const COMPOSITE_OPERATORS = { 17 // Define a "color" function accepting source and destination colors 18 // and alphas, and an "alpha" function accepting source and 19 // destination alphas. 20 "clear": { 21 "color": (sa, sc, da, dc) => 0, 22 "alpha": (sa, da) => 0 23 }, 24 "copy": { 25 "color": (sa, sc, da, dc) => sa * sc, 26 "alpha": (sa, da) => sa 27 }, 28 "destination": { 29 // TODO(dbaron): The spec says this should work, but none of 30 // Chromium, Gecko, or WebKit appear to implement it. 31 "color": (sa, sc, da, dc) => da * dc, 32 "alpha": (sa, da) => da 33 }, 34 "source-over": { 35 "color": (sa, sc, da, dc) => sa * sc + da * dc * (1 - sa), 36 "alpha": (sa, da) => sa + da * (1 - sa) 37 }, 38 "destination-over": { 39 "color": (sa, sc, da, dc) => sa * sc * (1 - da) + da * dc, 40 "alpha": (sa, da) => sa * (1 - da) + da 41 }, 42 "source-in": { 43 "color": (sa, sc, da, dc) => sa * sc * da, 44 "alpha": (sa, da) => sa * da 45 }, 46 "destination-in": { 47 "color": (sa, sc, da, dc) => da * dc * sa, 48 "alpha": (sa, da) => da * sa 49 }, 50 "source-out": { 51 "color": (sa, sc, da, dc) => sa * sc * (1 - da), 52 "alpha": (sa, da) => sa * (1 - da) 53 }, 54 "destination-out": { 55 "color": (sa, sc, da, dc) => da * dc * (1 - sa), 56 "alpha": (sa, da) => da * (1 - sa) 57 }, 58 "source-atop": { 59 "color": (sa, sc, da, dc) => sa * sc * da + da * dc * (1 - sa), 60 "alpha": (sa, da) => sa * da + da * (1 - sa) 61 }, 62 "destination-atop": { 63 "color": (sa, sc, da, dc) => sa * sc * (1 - da) + da * dc * sa, 64 "alpha": (sa, da) => sa * (1 - da) + da * sa 65 }, 66 "xor": { 67 "color": (sa, sc, da, dc) => sa * sc * (1 - da) + da * dc * (1 - sa), 68 "alpha": (sa, da) => sa * (1 - da) + da * (1 - sa) 69 }, 70 "lighter": { 71 // TODO(https://github.com/w3c/fxtf-drafts/issues/446): All engines 72 // actually implement 'lighter' using the formula for 'plus-lighter' 73 // given below; we should update the spec to match! 74 "color": (sa, sc, da, dc) => sa * sc + da * dc, 75 "alpha": (sa, da) => sa + da 76 }, 77 "plus-darker": { 78 // TODO(https://github.com/w3c/fxtf-drafts/issues/447): This formula 79 // is almost certainly wrong. It doesn't make sense, and the one 80 // engine that implements this value (WebKit) does something very 81 // different. 82 "color": (sa, sc, da, dc) => Math.max(0, 1 - sa * sc + 1 - da * dc), 83 "alpha": (sa, da) => Math.max(0, 1 - sa + 1 - da) 84 }, 85 "plus-lighter": { 86 "color": (sa, sc, da, dc) => Math.min(1, sa * sc + da * dc), 87 "alpha": (sa, da) => Math.min(1, sa + da) 88 } 89 }; 90 91 let canvas = document.getElementById("canvas"); 92 let cx = canvas.getContext("2d", { willReadFrequently: true }); 93 94 function roundup_255th(n) { 95 return Math.ceil(n * 255) / 255; 96 } 97 98 function rounddown_255th(n) { 99 return Math.floor(n * 255) / 255; 100 } 101 102 for (let op in COMPOSITE_OPERATORS) { 103 test(function() { 104 cx.save(); 105 this.add_cleanup(() => { cx.restore(); }); 106 for (let sc of VALUES) { 107 for (let sa of VALUES) { 108 for (let dc of VALUES) { 109 for (let da of VALUES) { 110 let desc = `g=${sc} a=${sa} ${op} g=${dc} a=${da}`; 111 cx.restore(); 112 cx.save(); 113 cx.clearRect(0, 0, 2, 2); 114 cx.fillStyle = `rgb(0, ${dc}, 0)`; 115 cx.globalAlpha = da / 255; 116 cx.fillRect(0, 0, 2, 2); 117 cx.globalCompositeOperation = op; 118 assert_equals(cx.globalCompositeOperation, op, "composite operation"); 119 cx.fillStyle = `rgb(0, ${sc}, 0)`; 120 cx.globalAlpha = sa / 255; 121 cx.fillRect(0, 0, 2, 2); 122 let imageData = cx.getImageData(0, 0, 1, 1); 123 assert_equals(imageData.data.length, 4, "length of ImageData"); 124 assert_equals(imageData.data[0], 0, `red: ${desc}`); 125 assert_equals(imageData.data[2], 0, `blue: ${desc}`); 126 let expected_color = COMPOSITE_OPERATORS[op].color(sa/255, sc/255, da/255, dc/255); 127 let expected_alpha = COMPOSITE_OPERATORS[op].alpha(sa/255, da/255); 128 let allowed_color_error; 129 // undo the premultiplication: 130 if (expected_alpha == 0) { 131 assert_equals(expected_color, 0, `premultiplication zero check: ${desc}`); 132 allowed_color_error = 0; 133 } else { 134 // We want to allow for the error in the color expectation 135 // to increase when the expected alpha is small, because 136 // we want to allow for roughly the same amount of error 137 // in the (smaller) *premultiplied* value. 138 let expected_min_color = rounddown_255th(expected_color) / roundup_255th(expected_alpha); 139 let expected_max_color = roundup_255th(expected_color) / rounddown_255th(expected_alpha); 140 // Set the expectation to the midpoint of the error range 141 // rather than the actual accurate expectation. 142 expected_color = (expected_max_color + expected_min_color) / 2; 143 allowed_color_error = (expected_max_color - expected_min_color) / 2; 144 } 145 expected_color *= 255; 146 expected_alpha *= 255; 147 allowed_color_error *= 255; 148 allowed_color_error += 3.5; 149 assert_approx_equals(imageData.data[1], expected_color, allowed_color_error, `green: ${desc}`); 150 assert_approx_equals(imageData.data[3], expected_alpha, 1.01, `alpha: ${desc}`); 151 } 152 } 153 } 154 } 155 }, `globalCompositeOperation ${op}`); 156 } 157 </script>