Source code
Revision control
Copy as Markdown
Other Tools
<!DOCTYPE html>
<html class="reftest-wait">
<!--
# color_quads.html
* The default is a 400x400 2d canvas, with 0, 16, 235, and 255 "gray" outer
quads, and 50%-red, -green, -blue, and -gray inner quads.
* We default to showing the settings pane when loaded without a query string.
This way, someone naively opens this in a browser, they can immediately see
all available options.
* The "Publish" button updates the url, and so causes the settings pane to
hide.
* Clicking on the canvas toggles the settings pane for further editing.
-->
<head>
<meta charset="utf-8">
<title>color_quads.html (2022-07-15)</title>
</head>
<body>
<div id="e_settings">
Image override: <input id="e_img" type="text">
<br>
<br>Canvas Width: <input id="e_width" type="text" value="400">
<br>Canvas Height: <input id="e_height" type="text" value="400">
<br>Canvas Colorspace: <input id="e_cspace" type="text">
<br>Canvas Context Type: <select id="e_context">
<option value="2d" selected="selected">Canvas2D</option>
<option value="webgl">WebGL</option>
</select>
<br>Canvas Context Options: <input id="e_options" type="text" value="{}">
<br>
<br>OuterTopLeft: <input id="e_color_o1" type="text" value="rgb(0,0,0)">
<br>OuterTopRight: <input id="e_color_o2" type="text" value="rgb(16,16,16)">
<br>OuterBottomLeft: <input id="e_color_o3" type="text" value="rgb(235,235,235)">
<br>OuterBottomRight: <input id="e_color_o4" type="text" value="rgb(255,255,255)">
<br>
<br>InnerTopLeft: <input id="e_color_i1" type="text" value="rgb(127,0,0)">
<br>InnerTopRight: <input id="e_color_i2" type="text" value="rgb(0,127,0)">
<br>InnerBottomLeft: <input id="e_color_i3" type="text" value="rgb(0,0,127)">
<br>InnerBottomRight: <input id="e_color_i4" type="text" value="rgb(127,127,127)">
<br><input id="e_publish" type="button" value="Publish">
<hr>
</div>
<div id="e_canvas_holder">
<canvas></canvas>
</div>
<script>
"use strict";
// document.body.style.backgroundColor = '#fdf';
// -
// Click the canvas to toggle the settings pane.
e_canvas_holder.addEventListener("click", () => {
// Toggle display:none to hide/unhide.
e_settings.style.display = e_settings.style.display ? "" : "none";
});
// Hide settings initially if there's a query string in the url.
if (window.location.search.startsWith("?")) {
e_settings.style.display = "none";
}
// -
function map(obj, fn) {
fn = fn || (x => x);
const ret = {};
for (const [k,v] of Object.entries(obj)) {
ret[k] = fn(v, k);
}
return ret;
}
function map_keys_required(obj, keys, fn) {
fn = fn || (x => x);
const ret = {};
for (const k of keys) {
const v = obj[k];
if (v === undefined) throw {k, obj};
ret[k] = fn(v, k);
}
return ret;
}
function set_device_pixel_size(e, device_size) {
const DPR = window.devicePixelRatio;
map_keys_required(device_size, ['width', 'height'], (device, k) => {
const css = device / DPR;
e.style[k] = css + 'px';
});
}
function pad_top_left_to_device_pixels(e) {
const DPR = window.devicePixelRatio;
e.style.padding = '';
let css_rect = e.getBoundingClientRect();
css_rect = map_keys_required(css_rect, ['left', 'top']);
const orig_device_rect = {};
const snapped_padding = map(css_rect, (css, k) => {
const device = orig_device_rect[k] = css * DPR;
const device_snapped = Math.round(device);
let device_padding = device_snapped - device;
// Negative padding is treated as 0.
// We want to pad:
// * 3.9 -> 4.0
// * 3.1 -> 4.0
// * 3.00000001 -> 3.0
if (device_padding < 0.01) {
device_padding += 1;
}
const css_padding = device_padding / DPR;
// console.log({css, k, device, device_snapped, device_padding, css_padding});
return css_padding;
});
e.style.paddingLeft = snapped_padding.left + 'px';
e.style.paddingTop = snapped_padding.top + 'px';
console.log(`[info] At dpr=${DPR}, padding`, css_rect, '(', orig_device_rect, 'device) by', snapped_padding);
}
// -
const SETTING_NODES = {};
e_settings.childNodes.forEach(n => {
if (!n.id) return;
SETTING_NODES[n.id] = n;
n._default = n.value;
});
const URL_PARAMS = new URLSearchParams(window.location.search);
URL_PARAMS.forEach((v,k) => {
const n = SETTING_NODES[k];
if (!n) {
if (k && !k.startsWith('__')) {
console.warn(`Unrecognized setting: ${k} = ${v}`);
}
return;
}
n.value = v;
});
// -
function UNITTEST_STR_EQ(was, expected) {
function to_result(src) {
let result = src;
if (typeof(result) == 'string') {
result = eval(result);
}
let result_str = result.toString();
if (result instanceof Array) {
result_str = '[' + result_str + ']';
}
return {src, result, result_str};
}
was = to_result(was);
expected = to_result(expected);
if (false) {
if (was.result_str != expected.result_str) {
throw {was, expected};
}
console.log(`[unittest] OK `, was.src, ` -> ${was.result_str} (`, expected.src, `)`);
}
console.assert(was.result_str == expected.result_str,
was.src, ` -> ${was.result_str} (`, expected.src, `)`);
}
// -
/// Non-Premult-Alpha, e.g. [1.0, 1.0, 1.0, 0.5]
function parse_css_color_npa(str) {
const m = /(rgba?)\((.*)\)/.exec(str);
if (!m) throw str;
let vals = m[2];
vals = vals.split(',').map(s => parseFloat(s));
if (vals.length == 3) {
vals.push(1.0);
}
for (let i = 0; i < 3; i++) {
vals[i] /= 255;
}
return vals;
}
UNITTEST_STR_EQ(`parse_css_color_npa('rgb(255,255,255)');`, [1,1,1,1]);
UNITTEST_STR_EQ(`parse_css_color_npa('rgba(255,255,255)');`, [1,1,1,1]);
UNITTEST_STR_EQ(`parse_css_color_npa('rgb(20,40,60)');`, '[20/255, 40/255, 60/255, 1]');
UNITTEST_STR_EQ(`parse_css_color_npa('rgb(20,40,60,0.5)');`, '[20/255, 40/255, 60/255, 0.5]');
UNITTEST_STR_EQ(`parse_css_color_npa('rgb(20,40,60,0)');`, '[20/255, 40/255, 60/255, 0]');
// -
let e_canvas;
async function draw() {
while (e_canvas_holder.firstChild) {
e_canvas_holder.removeChild(e_canvas_holder.firstChild);
}
if (e_img.value) {
const img = document.createElement("img");
img.src = e_img.value;
console.log('img.src =', img.src);
await img.decode();
e_canvas_holder.appendChild(img);
set_device_pixel_size(img, {width: img.naturalWidth, height: img.naturalHeight});
pad_top_left_to_device_pixels(img);
return;
}
e_canvas = document.createElement("canvas");
let options = eval(`Object.assign(${e_options.value})`);
options.colorSpace = e_cspace.value || undefined;
const context = e_canvas.getContext(e_context.value, options);
if (context.drawingBufferColorSpace && options.colorSpace) {
context.drawingBufferColorSpace = options.colorSpace;
}
if (context.getContextAttributes) {
options = context.getContextAttributes();
}
console.log({options});
// -
const W = parseInt(e_width.value);
const H = parseInt(e_height.value);
context.canvas.width = W;
context.canvas.height = H;
e_canvas_holder.appendChild(e_canvas);
// If we don't snap to the device pixel grid, borders between color blocks
// will be filtered, and this causes a lot of fuzzy() annotations.
set_device_pixel_size(e_canvas, e_canvas);
pad_top_left_to_device_pixels(e_canvas);
// -
let fillFromElem;
if (context.fillRect) {
const c2d = context;
fillFromElem = (e, left, top, w, h) => {
if (!e.value) return;
c2d.fillStyle = e.value;
c2d.fillRect(left, top, w, h);
};
} else if (context.drawArrays) {
const gl = context;
gl.enable(gl.SCISSOR_TEST);
gl.disable(gl.DEPTH_TEST);
fillFromElem = (e, left, top, w, h) => {
if (!e.value) return;
const rgba = parse_css_color_npa(e.value.trim());
if (false && options.premultipliedAlpha) {
for (let i = 0; i < 3; i++) {
rgba[i] *= rgba[3];
}
}
const bottom = top+h; // in y-down c2d coords
gl.scissor(left, gl.drawingBufferHeight - bottom, w, h);
gl.clearColor(...rgba);
gl.clear(gl.COLOR_BUFFER_BIT);
};
}
// -
const LEFT_HALF = W/2 | 0; // Round
const TOP_HALF = H/2 | 0;
fillFromElem(e_color_o1, 0 , 0 , LEFT_HALF, TOP_HALF);
fillFromElem(e_color_o2, LEFT_HALF, 0 , W-LEFT_HALF, TOP_HALF);
fillFromElem(e_color_o3, 0 , TOP_HALF, LEFT_HALF, H-TOP_HALF);
fillFromElem(e_color_o4, LEFT_HALF, TOP_HALF, W-LEFT_HALF, H-TOP_HALF);
// -
const INNER_SCALE = 1/4;
const W_INNER = W*INNER_SCALE | 0;
const H_INNER = H*INNER_SCALE | 0;
fillFromElem(e_color_i1, LEFT_HALF-W_INNER, TOP_HALF-H_INNER, W_INNER, H_INNER);
fillFromElem(e_color_i2, LEFT_HALF , TOP_HALF-H_INNER, W_INNER, H_INNER);
fillFromElem(e_color_i3, LEFT_HALF-W_INNER, TOP_HALF , W_INNER, H_INNER);
fillFromElem(e_color_i4, LEFT_HALF , TOP_HALF , W_INNER, H_INNER);
}
(async () => {
await draw();
document.documentElement.removeAttribute("class");
})();
// -
Object.values(SETTING_NODES).forEach(x => {
x.addEventListener("change", draw);
});
e_publish.addEventListener("click", () => {
let settings = [];
for (const n of Object.values(SETTING_NODES)) {
if (n.value == n._default) continue;
settings.push(`${n.id}=${n.value}`);
}
settings = settings.join("&");
if (!settings) {
settings = "="; // Empty key-value pair is "publish with default settings"
}
window.location.search = "?" + settings;
});
</script>
</body>
</html>