diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
commit | 40a355a42d4a9444dc753c04c6608dade2f06a23 (patch) | |
tree | 871fc667d2de662f171103ce5ec067014ef85e61 /dom/canvas/test/reftest/colors/color_canvas.html | |
parent | Adding upstream version 124.0.1. (diff) | |
download | firefox-upstream/125.0.1.tar.xz firefox-upstream/125.0.1.zip |
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/canvas/test/reftest/colors/color_canvas.html')
-rw-r--r-- | dom/canvas/test/reftest/colors/color_canvas.html | 461 |
1 files changed, 461 insertions, 0 deletions
diff --git a/dom/canvas/test/reftest/colors/color_canvas.html b/dom/canvas/test/reftest/colors/color_canvas.html new file mode 100644 index 0000000000..7abbc86255 --- /dev/null +++ b/dom/canvas/test/reftest/colors/color_canvas.html @@ -0,0 +1,461 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset=utf-8> + <title>color_canvas.html</title> + </head> + <!-- +# color_canvas.html + +* Default is a 100x100 'css' canvas-equivalent div, filled with 50% srgb red. + +* 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. + --> + <body> + <form id=e_settings><fieldset><legend>Settings</legend> + Width: <input id=e_width type=text value=100> + <br> + Height: <input id=e_height type=text value=100> + <br> + <fieldset><legend>Canvas Context</legend> + Type: <select id=e_context> + <option value=css selected>css</option> + <option value=2d>2d</option> + <option value=webgl>webgl</option> + </select> + <br> + Options: <input id=e_options type=text value={}> + <br> + Colorspace: <input id=e_cspace type=text placeholder=srgb> + <br> + WebGL Format: <input id=e_webgl_format type=text placeholder=RGBA8> + </fieldset> + <br> + Color: <input id=e_color type=text value='color(srgb 0 0.5 0)'> + <br> + <input id=e_publish type=button value=Publish> + <input type=checkbox id=e_publish_omit_defaults checked><label for=e_publish_omit_defaults>Omit defaults</label> + <hr> + </fieldset></form> + <div id=e_canvas_list><canvas></canvas></div> + <script> +'use strict'; + +// - + +function walk_nodes_depth_first(e, fn) { + if (fn(e) === false) return; // Don't stop on `true`, or `undefined`! + for (const c of e.childNodes) { + walk_nodes_depth_first(c, fn); + } +} + +// - + +// Click the canvas to toggle the settings pane. +e_canvas_list.addEventListener('click', () => { + // Toggle display:none to hide/unhide. + e_settings.hidden = !e_settings.hidden; +}); + +// Hide settings initially if there's a query string in the url. +if (window.location.search.startsWith('?')) { + e_settings.hidden = true; +} + +// - + +// Imply .name from .id, because `new FormData` collects based on names. +walk_nodes_depth_first(e_settings, e => { + if (e.id) { + e.name = e.id; + e._default_value = e.value; + } +}); + +// - + +const URL_PARAMS = new URLSearchParams(window.location.search); +URL_PARAMS.forEach((v,k) => { + const e = window[k]; + if (!e) { + if (k) { + console.warn(`Unrecognized setting: ${k} = ${v}`); + } + return; + } + v = decode_url_v(k,v); + e.value = v; +}); + +// - + +globalThis.ASSERT = (() => { + function toPrettyString(arg) { + if (!arg) return ''+arg; + + if (arg.call) { + let s = arg.toString(); + const RE_TRIVIAL_LAMBDA = /\( *\) *=> *(.*)/; + const m = RE_TRIVIAL_LAMBDA.exec(s); + if (m) { + s = '`' + m[1] + '`'; + } + return s; + } + if (arg.constructor == Array) { + return `[${[].join.call(arg, ', ')}]`; + } + return JSON.stringify(arg); + } + + /// new AssertArg(): Construct a wrapper for args to assert functions. + function AssertArg(dict) { + this.label = Object.keys(dict)[0]; + + this.set = function(arg) { + this.arg = arg; + this.value = (arg && arg.call) ? arg.call() : arg; + this.value = toPrettyString(this.value); + }; + this.set(dict[this.label]); + + this.toString = function() { + let ret = `${this.label} ${toPrettyString(this.arg)}`; + if (this.arg.call) { + ret += ` (${this.value})`; + } + return ret; + } + } + + const eq = (a,b) => a == b; + const neq = (a,b) => a != b; + + const CMP_BY_NAME = { + '==': eq, + '!=': neq, + }; + + function IS(cmp, was, expected, _console) { + _console = _console || console; + + _console.assert(was.call, '`was.call` not defined.'); + was = new AssertArg({was}); + expected = new AssertArg({expected}); + + const fn_cmp = CMP_BY_NAME[cmp] || cmp; + + _console.assert(fn_cmp(was.value, expected.value), `${toPrettyString(was.arg)} => ${was.value} not ${cmp} ${expected}`); + if (was.value != expected.value) { + } else if (globalThis.ASSERT && globalThis.ASSERT.verbose) { + const maybe_cmp_str = (cmp == '==') ? '' : ` ${was.value} ${cmp}`; + _console.log(`${toPrettyString(was.arg)} => ${maybe_cmp_str}${expected}`); + } + } + + // - + + const MOCK_CONSOLE = { + _asserts: [], + assert: function(expr, ...args) { + if (!expr) { + this._asserts.push(args); + } + }, + log: function(...args) { + // Don't record. + }, + }; + + // - + // Test `==` + + IS('==', () => 1, 1, MOCK_CONSOLE); + console.assert(MOCK_CONSOLE._asserts.length == 0, MOCK_CONSOLE._asserts); + MOCK_CONSOLE._asserts = []; + + IS('==', () => 2, 2, MOCK_CONSOLE); + console.assert(MOCK_CONSOLE._asserts.length == 0, MOCK_CONSOLE._asserts); + MOCK_CONSOLE._asserts = []; + + IS('==', () => 5, () => 3, MOCK_CONSOLE); + console.assert(MOCK_CONSOLE._asserts.length == 1, MOCK_CONSOLE._asserts); + MOCK_CONSOLE._asserts = []; + + IS('==', () => [1,2], () => [1,2], MOCK_CONSOLE); + console.assert(MOCK_CONSOLE._asserts.length == 0, MOCK_CONSOLE._asserts); + MOCK_CONSOLE._asserts = []; + + // - + // Test `!=` + + IS('!=', () => [1,2,5], () => [1,2,3], MOCK_CONSOLE); + console.assert(MOCK_CONSOLE._asserts.length == 0, MOCK_CONSOLE._asserts); + MOCK_CONSOLE._asserts = []; + + // - + + const ret = { + verbose: false, + IS, + }; + ret.EQ = (was,expected) => ret.IS('==', was, expected); + ret.NEQ = (was,expected) => ret.IS('!=', was, expected); + ret.EEQ = (was,expected) => ret.IS('===', was, expected); + ret.NEEQ = (was,expected) => ret.IS('!==', was, expected); + ret.TRUE = was => ret.EQ(was, true); + ret.FALSE = was => ret.EQ(was, false); + ret.NULL = was => ret.EEQ(was, null); + return ret; +})(); + +// - + +function parse_css_rgb(str) { + // rgb (R ,G ,B /A ) + const m = /rgba?\(([^,]+),([^,]+),([^/)]+)(?: *\/([^)]+))?\)/.exec(str); + if (!m) throw str; + const rgba = m.slice(1,1+4).map((s,i) => { + if (s === undefined && i == 3) { + s = '1'; // Alpha defaults to 1. + } + s = s.trim(); + let v = parseFloat(s); + if (s.endsWith('%')) { + v /= 100; + } else { + if (i < 3) { // r,g,b but not a! + v /= 255; + } + } + return v; + }); + return rgba; +} +ASSERT.EQ(() => parse_css_rgb('rgb(255,255,255)'), [1,1,1,1]); +ASSERT.EQ(() => parse_css_rgb('rgba(255,255,255)'), [1,1,1,1]); +ASSERT.EQ(() => parse_css_rgb('rgb(255,255,255)'), [1,1,1,1]); +ASSERT.EQ(() => parse_css_rgb('rgba(255,255,255)'), [1,1,1,1]); +ASSERT.EQ(() => parse_css_rgb('rgb(20,40,60)'), () => [20/255, 40/255, 60/255, 1]); +ASSERT.EQ(() => parse_css_rgb('rgb(20,40,60 / 0.5)'), () => [20/255, 40/255, 60/255, 0.5]); +ASSERT.EQ(() => parse_css_rgb('rgb(20,40,60 / 0)'), () => [20/255, 40/255, 60/255, 0]); + +// - + +function parse_css_color(str) { + // color ( srgb R G B /A ) + const m = /color\( *([^ ]+) +([^ ]+) +([^ ]+) +([^/)]+)(?:\/([^)]+))?\)/.exec(str); + if (!m) { + return ['srgb', ...parse_css_rgb(str)]; + } + + const cspace = m[1].trim(); + let has_extreme_colors = false; + const rgba = m.slice(2, 2+4).map((s,i) => { + if (s === undefined && i == 3) { + s = '1'; // Alpha defaults to 1. + } + s = s.trim(); + let v = parseFloat(s); + if (s.endsWith('%')) { + v /= 100; + } + if (v < 0 || v > 1) { + has_extreme_colors = true; + } + return v; + }); + if (has_extreme_colors) { + console.warn(`parse_css_color('${str}') has colors outside [0.0,1.0]: ${JSON.stringify(rgba)}`); + } + return [cspace, ...rgba]; +} +ASSERT.EQ(() => parse_css_color('rgb(255,255,255)'), ['srgb',1,1,1,1]); +ASSERT.EQ(() => parse_css_color('rgb(20,40,60 / 0.5)'), () => ['srgb', 20/255, 40/255, 60/255, 0.5]); +ASSERT.EQ(() => parse_css_color('color(srgb 1 0 1 /0.3)'), ['srgb',1,0,1,0.3]); +ASSERT.EQ(() => parse_css_color('color(display-p3 1 0% 100%/ 30%)'), ['display-p3',1,0,1,0.3]); + +// - + +class CssColor { + constructor(cspace, r,g,b,a=1) { + this.cspace = cspace; + this.rgba = [this.r, this.g, this.b, this.a] = [r,g,b,a]; + this.rgb = this.rgba.slice(0,3); + this.tuple = [this.cspace, ...this.rgba]; + } + + toString() { + return `color(${this.cspace} ${this.rgb.join(' ')} / ${this.a})`; + } +}; +CssColor.parse = function(str) { + return new CssColor(...parse_css_color(str)); +} + +{ + let STR; + // Test round-trip. + STR = 'color(display-p3 1 0 1 / 0.3)'; + ASSERT.EQ(() => CssColor.parse(STR).toString(), STR); + + // Test round-trip normalization + ASSERT.EQ(() => CssColor.parse('color( display-p3 1 0 1/30% )').toString(), 'color(display-p3 1 0 1 / 0.3)'); +} + +// - + +function redraw() { + while (e_canvas_list.firstChild) { + e_canvas_list.removeChild(e_canvas_list.firstChild); + } + + const c = make_canvas(e_color.value.trim()); + c.style.border = '4px solid black'; + e_canvas_list.appendChild(c); +} + +function fill_canvas_rect(context /*: CanvasRenderingContext | WebGLRenderingContext*/, css_color, rect=null) { + rect = rect || {left: 0, top: 0, w: context.canvas.width, h: context.canvas.height}; + + const is_c2d = ('fillRect' in context); + if (is_c2d) { + const c2d = context; + c2d.fillStyle = css_color; + c2d.fillRect(rect.left, rect.top, rect.w, rect.h); + return; + } + + const is_webgl = ('drawArrays' in context); + if (is_webgl) { + const gl = context; + console.assert(context.canvas.width == gl.drawingBufferWidth, context.canvas.width, '!=', gl.drawingBufferWidth); + console.assert(context.canvas.height == gl.drawingBufferHeight, context.canvas.height, '!=', gl.drawingBufferHeight); + + gl.enable(gl.SCISSOR_TEST); + gl.disable(gl.DEPTH_TEST); + const bottom = rect.top + rect.h; // in y-down c2d coords + gl.scissor(rect.left, context.canvas.height - bottom, rect.w, rect.h); + + const canvas_cspace = context.drawingBufferColorSpace || 'srgb'; + if (css_color.cspace != canvas_cspace) { + console.warn(`Ignoring mismatched color vs webgl canvas cspace: ${css_color.cspace} vs ${canvas_cspace}`); + } + gl.clearColor(...css_color.rgba); + gl.clear(gl.COLOR_BUFFER_BIT); + return; + } + + console.error('Unhandled context kind:', context); +} + +window.e_canvas = null; + +function make_canvas(css_color) { + css_color = CssColor.parse(css_color); + + // `e_width` and e_friends are elements (by id) that we added to the raw HTML above. + // `e_width` is an old shorthand for `window.e_width || document.getElementById('e_width')`. + const W = parseInt(e_width.value); + const H = parseInt(e_height.value); + if (e_context.value == 'css') { + e_canvas = document.createElement('div'); + e_canvas.style.width = `${W}px`; + e_canvas.style.height = `${H}px`; + e_canvas.style.backgroundColor = css_color; + return e_canvas; + } + e_canvas = document.createElement('canvas'); + e_canvas.width = W; + e_canvas.height = H; + + let requested_options = JSON.parse(e_options.value); + requested_options.colorSpace = e_cspace.value || undefined; + + const context = e_canvas.getContext(e_context.value, requested_options); + if (requested_options.colorSpace) { + if (!context.drawingBufferColorSpace) { + console.warn(`${context.constructor.name}.drawingBufferColorSpace not supported by browser.`); + } else { + context.drawingBufferColorSpace = requested_options.colorSpace; + } + } + + if (e_webgl_format.value) { + if (!context.drawingBufferStorage) { + console.warn(`${context.constructor.name}.drawingBufferStorage not supported by browser.`); + } else { + context.drawingBufferStorage(W, H, context[e_webgl_format.value]); + } + } + + let actual_options; + if (!context.getContextAttributes) { + console.warn(`${canvas.constructor.name}.getContextAttributes not supported by browser.`); + actual_options = requested_options; + } else { + actual_options = context.getContextAttributes(); + } + + // - + + fill_canvas_rect(context, css_color); + + return e_canvas; +} + +e_settings.addEventListener('change', async () => { + redraw(); + const e_updated = document.createElement('i'); + e_updated.textContent = '(Updated!)'; + document.body.appendChild(e_updated); + await new Promise(go => setTimeout(go, 1000)); + document.body.removeChild(e_updated); +}); +redraw(); + +// - + +function encode_url_v(k,v) { + if (k == 'e_color') { + v = v.replaceAll(' ', ','); + } + console.assert(!v.includes(' '), v); + return v +} +function decode_url_v(k,v) { + console.assert(!v.includes(' '), v); + if (k == 'e_color') { + v = v.replaceAll(',', ' '); + } + return v +} +ASSERT.EQ(() => decode_url_v('e_color', encode_url_v('e_color', 'color(srgb 1 0 0)')), 'color(srgb 1 0 0)') + +e_publish.addEventListener('click', () => { + const fd = new FormData(e_settings); + let settings = []; + for (let [k,v] of fd) { + const e = window[k]; + if (e_publish_omit_defaults.checked && v == e._default_value) continue; + + v = encode_url_v(k,v); + settings.push(`${k}=${v}`); + } + settings = settings.join('&'); + if (!settings) { + settings = '='; // Empty key-value pair is 'publish with default settings' + } + window.location.search = '?' + settings; +}); + </script> + </body> +</html> |