2 <html class=
"reftest-wait">
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
16 * Clicking on the canvas toggles the settings pane for further editing.
19 <meta charset=
"utf-8">
20 <title>color_quads.html (
2022-
07-
15)
</title>
24 Image override:
<input id=
"e_img" type=
"text">
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>
34 <br>Canvas Context Options:
<input id=
"e_options" type=
"text" value=
"{}">
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)">
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">
49 <div id=
"e_canvas_holder">
55 // document.body.style.backgroundColor = '#fdf';
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";
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";
72 function map(obj
, fn
) {
75 for (const [k
,v
] of Object
.entries(obj
)) {
81 function map_keys_required(obj
, keys
, fn
) {
85 for (const k
of keys
) {
87 if (v
=== undefined) throw {k
, obj
};
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';
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.
117 // * 3.00000001 -> 3.0
118 if (device_padding
< 0.01) {
121 const css_padding
= device_padding
/ DPR
;
122 // console.log({css, k, device, device_snapped, device_padding, 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
);
133 const SETTING_NODES
= {};
134 e_settings
.childNodes
.forEach(n
=> {
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
];
144 if (k
&& !k
.startsWith('__')) {
145 console
.warn(`Unrecognized setting: ${k} = ${v}`);
154 function UNITTEST_STR_EQ(was
, expected
) {
155 function to_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
);
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
, `)`);
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
);
187 vals
= vals
.split(',').map(s
=> parseFloat(s
));
188 if (vals
.length
== 3) {
191 for (let i
= 0; i
< 3; i
++) {
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]');
206 async
function draw() {
207 while (e_canvas_holder
.firstChild
) {
208 e_canvas_holder
.removeChild(e_canvas_holder
.firstChild
);
212 const img
= document
.createElement("img");
213 img
.src
= e_img
.value
;
214 console
.log('img.src =', img
.src
);
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
);
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
});
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
);
252 if (context
.fillRect
) {
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
) {
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
++) {
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
);
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
);
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
);
304 document
.documentElement
.removeAttribute("class");
309 Object
.values(SETTING_NODES
).forEach(x
=> {
310 x
.addEventListener("change", draw
);
313 e_publish
.addEventListener("click", () => {
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("&");
321 settings
= "="; // Empty key-value pair is "publish with default settings"
323 window
.location
.search
= "?" + settings
;