tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>