color_quads.html (9635B)
1 <!DOCTYPE html> 2 <html class="reftest-wait"> 3 <!-- 4 # color_quads.html 5 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. 8 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. 12 13 * The "Publish" button updates the url, and so causes the settings pane to 14 hide. 15 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"> 25 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="{}"> 35 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"; 54 55 // document.body.style.backgroundColor = '#fdf'; 56 57 // - 58 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 }); 64 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"; 68 } 69 70 // - 71 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); 77 } 78 return ret; 79 } 80 81 function map_keys_required(obj, keys, fn) { 82 fn = fn || (x => x); 83 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); 89 } 90 return ret; 91 } 92 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 }); 99 } 100 101 function pad_top_left_to_device_pixels(e) { 102 const DPR = window.devicePixelRatio; 103 104 e.style.padding = ''; 105 let css_rect = e.getBoundingClientRect(); 106 css_rect = map_keys_required(css_rect, ['left', 'top']); 107 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; 120 } 121 const css_padding = device_padding / DPR; 122 // console.log({css, k, device, device_snapped, device_padding, css_padding}); 123 return css_padding; 124 }); 125 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); 129 } 130 131 // - 132 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; 138 }); 139 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}`); 146 } 147 return; 148 } 149 n.value = v; 150 }); 151 152 // - 153 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); 159 } 160 let result_str = result.toString(); 161 if (result instanceof Array) { 162 result_str = '[' + result_str + ']'; 163 } 164 return {src, result, result_str}; 165 } 166 was = to_result(was); 167 expected = to_result(expected); 168 169 if (false) { 170 if (was.result_str != expected.result_str) { 171 throw {was, expected}; 172 } 173 console.log(`[unittest] OK `, was.src, ` -> ${was.result_str} (`, expected.src, `)`); 174 } 175 console.assert(was.result_str == expected.result_str, 176 was.src, ` -> ${was.result_str} (`, expected.src, `)`); 177 } 178 179 // - 180 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; 185 186 let vals = m[2]; 187 vals = vals.split(',').map(s => parseFloat(s)); 188 if (vals.length == 3) { 189 vals.push(1.0); 190 } 191 for (let i = 0; i < 3; i++) { 192 vals[i] /= 255; 193 } 194 return vals; 195 } 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]'); 201 202 // - 203 204 let e_canvas; 205 206 async function draw() { 207 while (e_canvas_holder.firstChild) { 208 e_canvas_holder.removeChild(e_canvas_holder.firstChild); 209 } 210 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; 220 } 221 222 e_canvas = document.createElement("canvas"); 223 224 let options = eval(`Object.assign(${e_options.value})`); 225 options.colorSpace = e_cspace.value || undefined; 226 227 const context = e_canvas.getContext(e_context.value, options); 228 if (context.drawingBufferColorSpace && options.colorSpace) { 229 context.drawingBufferColorSpace = options.colorSpace; 230 } 231 if (context.getContextAttributes) { 232 options = context.getContextAttributes(); 233 } 234 console.log({options}); 235 236 // - 237 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); 243 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); 248 249 // - 250 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); 258 }; 259 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]; 270 } 271 } 272 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); 277 }; 278 } 279 280 // - 281 282 const LEFT_HALF = W/2 | 0; // Round 283 const TOP_HALF = H/2 | 0; 284 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); 289 290 // - 291 292 const INNER_SCALE = 1/4; 293 const W_INNER = W*INNER_SCALE | 0; 294 const H_INNER = H*INNER_SCALE | 0; 295 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); 300 } 301 302 (async () => { 303 await draw(); 304 document.documentElement.removeAttribute("class"); 305 })(); 306 307 // - 308 309 Object.values(SETTING_NODES).forEach(x => { 310 x.addEventListener("change", draw); 311 }); 312 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}`); 318 } 319 settings = settings.join("&"); 320 if (!settings) { 321 settings = "="; // Empty key-value pair is "publish with default settings" 322 } 323 window.location.search = "?" + settings; 324 }); 325 </script> 326 </body> 327 </html>