Bug 1915045 Ensure decode tasks are scheduled on BufferingState::Enter() r=media...
[gecko.git] / dom / canvas / test / reftest / color_quads.html
blob944d1f1370d6543942ac49118de9230f76ba75b6
1 <!DOCTYPE html>
2 <html class="reftest-wait">
3 <!--
4 # color_quads.html
6 * The default is a 400x400 2d canvas, with 0, 16, 235, and 255 "gray" outer
7 quads, and 50%-red, -green, -blue, and -gray inner quads.
9 * We default to showing the settings pane when loaded without a query string.
10 This way, someone naively opens this in a browser, they can immediately see
11 all available options.
13 * The "Publish" button updates the url, and so causes the settings pane to
14 hide.
16 * Clicking on the canvas toggles the settings pane for further editing.
17 -->
18 <head>
19 <meta charset="utf-8">
20 <title>color_quads.html (2022-07-15)</title>
21 </head>
22 <body>
23 <div id="e_settings">
24 Image override: <input id="e_img" type="text">
26 <br>
27 <br>Canvas Width: <input id="e_width" type="text" value="400">
28 <br>Canvas Height: <input id="e_height" type="text" value="400">
29 <br>Canvas Colorspace: <input id="e_cspace" type="text">
30 <br>Canvas Context Type: <select id="e_context">
31 <option value="2d" selected="selected">Canvas2D</option>
32 <option value="webgl">WebGL</option>
33 </select>
34 <br>Canvas Context Options: <input id="e_options" type="text" value="{}">
36 <br>
37 <br>OuterTopLeft: <input id="e_color_o1" type="text" value="rgb(0,0,0)">
38 <br>OuterTopRight: <input id="e_color_o2" type="text" value="rgb(16,16,16)">
39 <br>OuterBottomLeft: <input id="e_color_o3" type="text" value="rgb(235,235,235)">
40 <br>OuterBottomRight: <input id="e_color_o4" type="text" value="rgb(255,255,255)">
41 <br>
42 <br>InnerTopLeft: <input id="e_color_i1" type="text" value="rgb(127,0,0)">
43 <br>InnerTopRight: <input id="e_color_i2" type="text" value="rgb(0,127,0)">
44 <br>InnerBottomLeft: <input id="e_color_i3" type="text" value="rgb(0,0,127)">
45 <br>InnerBottomRight: <input id="e_color_i4" type="text" value="rgb(127,127,127)">
46 <br><input id="e_publish" type="button" value="Publish">
47 <hr>
48 </div>
49 <div id="e_canvas_holder">
50 <canvas></canvas>
51 </div>
52 <script>
53 "use strict";
55 // document.body.style.backgroundColor = '#fdf';
57 // -
59 // Click the canvas to toggle the settings pane.
60 e_canvas_holder.addEventListener("click", () => {
61 // Toggle display:none to hide/unhide.
62 e_settings.style.display = e_settings.style.display ? "" : "none";
63 });
65 // Hide settings initially if there's a query string in the url.
66 if (window.location.search.startsWith("?")) {
67 e_settings.style.display = "none";
70 // -
72 function map(obj, fn) {
73 fn = fn || (x => x);
74 const ret = {};
75 for (const [k,v] of Object.entries(obj)) {
76 ret[k] = fn(v, k);
78 return ret;
81 function map_keys_required(obj, keys, fn) {
82 fn = fn || (x => x);
84 const ret = {};
85 for (const k of keys) {
86 const v = obj[k];
87 if (v === undefined) throw {k, obj};
88 ret[k] = fn(v, k);
90 return ret;
93 function set_device_pixel_size(e, device_size) {
94 const DPR = window.devicePixelRatio;
95 map_keys_required(device_size, ['width', 'height'], (device, k) => {
96 const css = device / DPR;
97 e.style[k] = css + 'px';
98 });
101 function pad_top_left_to_device_pixels(e) {
102 const DPR = window.devicePixelRatio;
104 e.style.padding = '';
105 let css_rect = e.getBoundingClientRect();
106 css_rect = map_keys_required(css_rect, ['left', 'top']);
108 const orig_device_rect = {};
109 const snapped_padding = map(css_rect, (css, k) => {
110 const device = orig_device_rect[k] = css * DPR;
111 const device_snapped = Math.round(device);
112 let device_padding = device_snapped - device;
113 // Negative padding is treated as 0.
114 // We want to pad:
115 // * 3.9 -> 4.0
116 // * 3.1 -> 4.0
117 // * 3.00000001 -> 3.0
118 if (device_padding < 0.01) {
119 device_padding += 1;
121 const css_padding = device_padding / DPR;
122 // console.log({css, k, device, device_snapped, device_padding, css_padding});
123 return css_padding;
126 e.style.paddingLeft = snapped_padding.left + 'px';
127 e.style.paddingTop = snapped_padding.top + 'px';
128 console.log(`[info] At dpr=${DPR}, padding`, css_rect, '(', orig_device_rect, 'device) by', snapped_padding);
131 // -
133 const SETTING_NODES = {};
134 e_settings.childNodes.forEach(n => {
135 if (!n.id) return;
136 SETTING_NODES[n.id] = n;
137 n._default = n.value;
140 const URL_PARAMS = new URLSearchParams(window.location.search);
141 URL_PARAMS.forEach((v,k) => {
142 const n = SETTING_NODES[k];
143 if (!n) {
144 if (k && !k.startsWith('__')) {
145 console.warn(`Unrecognized setting: ${k} = ${v}`);
147 return;
149 n.value = v;
152 // -
154 function UNITTEST_STR_EQ(was, expected) {
155 function to_result(src) {
156 let result = src;
157 if (typeof(result) == 'string') {
158 result = eval(result);
160 let result_str = result.toString();
161 if (result instanceof Array) {
162 result_str = '[' + result_str + ']';
164 return {src, result, result_str};
166 was = to_result(was);
167 expected = to_result(expected);
169 if (false) {
170 if (was.result_str != expected.result_str) {
171 throw {was, expected};
173 console.log(`[unittest] OK `, was.src, ` -> ${was.result_str} (`, expected.src, `)`);
175 console.assert(was.result_str == expected.result_str,
176 was.src, ` -> ${was.result_str} (`, expected.src, `)`);
179 // -
181 /// Non-Premult-Alpha, e.g. [1.0, 1.0, 1.0, 0.5]
182 function parse_css_color_npa(str) {
183 const m = /(rgba?)\((.*)\)/.exec(str);
184 if (!m) throw str;
186 let vals = m[2];
187 vals = vals.split(',').map(s => parseFloat(s));
188 if (vals.length == 3) {
189 vals.push(1.0);
191 for (let i = 0; i < 3; i++) {
192 vals[i] /= 255;
194 return vals;
196 UNITTEST_STR_EQ(`parse_css_color_npa('rgb(255,255,255)');`, [1,1,1,1]);
197 UNITTEST_STR_EQ(`parse_css_color_npa('rgba(255,255,255)');`, [1,1,1,1]);
198 UNITTEST_STR_EQ(`parse_css_color_npa('rgb(20,40,60)');`, '[20/255, 40/255, 60/255, 1]');
199 UNITTEST_STR_EQ(`parse_css_color_npa('rgb(20,40,60,0.5)');`, '[20/255, 40/255, 60/255, 0.5]');
200 UNITTEST_STR_EQ(`parse_css_color_npa('rgb(20,40,60,0)');`, '[20/255, 40/255, 60/255, 0]');
202 // -
204 let e_canvas;
206 async function draw() {
207 while (e_canvas_holder.firstChild) {
208 e_canvas_holder.removeChild(e_canvas_holder.firstChild);
211 if (e_img.value) {
212 const img = document.createElement("img");
213 img.src = e_img.value;
214 console.log('img.src =', img.src);
215 await img.decode();
216 e_canvas_holder.appendChild(img);
217 set_device_pixel_size(img, {width: img.naturalWidth, height: img.naturalHeight});
218 pad_top_left_to_device_pixels(img);
219 return;
222 e_canvas = document.createElement("canvas");
224 let options = eval(`Object.assign(${e_options.value})`);
225 options.colorSpace = e_cspace.value || undefined;
227 const context = e_canvas.getContext(e_context.value, options);
228 if (context.drawingBufferColorSpace && options.colorSpace) {
229 context.drawingBufferColorSpace = options.colorSpace;
231 if (context.getContextAttributes) {
232 options = context.getContextAttributes();
234 console.log({options});
236 // -
238 const W = parseInt(e_width.value);
239 const H = parseInt(e_height.value);
240 context.canvas.width = W;
241 context.canvas.height = H;
242 e_canvas_holder.appendChild(e_canvas);
244 // If we don't snap to the device pixel grid, borders between color blocks
245 // will be filtered, and this causes a lot of fuzzy() annotations.
246 set_device_pixel_size(e_canvas, e_canvas);
247 pad_top_left_to_device_pixels(e_canvas);
249 // -
251 let fillFromElem;
252 if (context.fillRect) {
253 const c2d = context;
254 fillFromElem = (e, left, top, w, h) => {
255 if (!e.value) return;
256 c2d.fillStyle = e.value;
257 c2d.fillRect(left, top, w, h);
260 } else if (context.drawArrays) {
261 const gl = context;
262 gl.enable(gl.SCISSOR_TEST);
263 gl.disable(gl.DEPTH_TEST);
264 fillFromElem = (e, left, top, w, h) => {
265 if (!e.value) return;
266 const rgba = parse_css_color_npa(e.value.trim());
267 if (false && options.premultipliedAlpha) {
268 for (let i = 0; i < 3; i++) {
269 rgba[i] *= rgba[3];
273 const bottom = top+h; // in y-down c2d coords
274 gl.scissor(left, gl.drawingBufferHeight - bottom, w, h);
275 gl.clearColor(...rgba);
276 gl.clear(gl.COLOR_BUFFER_BIT);
280 // -
282 const LEFT_HALF = W/2 | 0; // Round
283 const TOP_HALF = H/2 | 0;
285 fillFromElem(e_color_o1, 0 , 0 , LEFT_HALF, TOP_HALF);
286 fillFromElem(e_color_o2, LEFT_HALF, 0 , W-LEFT_HALF, TOP_HALF);
287 fillFromElem(e_color_o3, 0 , TOP_HALF, LEFT_HALF, H-TOP_HALF);
288 fillFromElem(e_color_o4, LEFT_HALF, TOP_HALF, W-LEFT_HALF, H-TOP_HALF);
290 // -
292 const INNER_SCALE = 1/4;
293 const W_INNER = W*INNER_SCALE | 0;
294 const H_INNER = H*INNER_SCALE | 0;
296 fillFromElem(e_color_i1, LEFT_HALF-W_INNER, TOP_HALF-H_INNER, W_INNER, H_INNER);
297 fillFromElem(e_color_i2, LEFT_HALF , TOP_HALF-H_INNER, W_INNER, H_INNER);
298 fillFromElem(e_color_i3, LEFT_HALF-W_INNER, TOP_HALF , W_INNER, H_INNER);
299 fillFromElem(e_color_i4, LEFT_HALF , TOP_HALF , W_INNER, H_INNER);
302 (async () => {
303 await draw();
304 document.documentElement.removeAttribute("class");
305 })();
307 // -
309 Object.values(SETTING_NODES).forEach(x => {
310 x.addEventListener("change", draw);
313 e_publish.addEventListener("click", () => {
314 let settings = [];
315 for (const n of Object.values(SETTING_NODES)) {
316 if (n.value == n._default) continue;
317 settings.push(`${n.id}=${n.value}`);
319 settings = settings.join("&");
320 if (!settings) {
321 settings = "="; // Empty key-value pair is "publish with default settings"
323 window.location.search = "?" + settings;
325 </script>
326 </body>
327 </html>