diff options
-rw-r--r-- | fractiform.js | 500 | ||||
-rw-r--r-- | index.html | 50 |
2 files changed, 29 insertions, 521 deletions
diff --git a/fractiform.js b/fractiform.js index 7a96018..9fe9681 100644 --- a/fractiform.js +++ b/fractiform.js @@ -21,33 +21,10 @@ let framebuffer; let framebuffer_color_texture; let sampler_texture; let current_time; -let vertices_main = []; -let indices_main = []; -let vertices_changed = false; let ui_shown = true; -let mouse_pos = {x: -1e10, y: -1e10}; -Object.preventExtensions(mouse_pos); -let viewport_width, viewport_height; -let ui_shape = []; -let ui_vertices = []; -let ui_vertex_properties_div; -let ui_color_input; -let ui_color_mix_input; -let ui_grid_divisions_x_input, ui_grid_divisions_y_input; let ui_div; -let shift_key = false; -let ctrl_key = false; -let ui_specify_uv = false; - -const TOOL_TRIANGLE = 1; -const TOOL_SELECT = 3; -const TOOL_PARALLELOGRAM = 4; - -let ui_tool; - -const vertex_radius = 10; -// radius for snapping to vertices, selecting vertices -const vertex_mouse_radius = 20; +let viewport_width, viewport_height; +let shift_key = false, ctrl_key = false; let width = 1920, height = 1920; @@ -59,23 +36,6 @@ function set_ui_shown(to) { page.dataset.uiShown = to ? '1' : '0'; } -function ui_get_color() { - return ui_color_input.value; -} - -function ui_get_color_mix() { - let v = parseFloat(ui_color_mix_input.value); - return !isNaN(v) && v >= 0.0 && v <= 1.0 ? v : 0.0; -} - -function ui_get_color_rgba() { - let alpha = Math.floor(ui_get_color_mix() * 255).toString(16); - while (alpha.length < 2) { - alpha = '0' + alpha; - } - return ui_get_color() + alpha; -} - function rgba_hex_to_float(hex) { let color = { r: parseInt(hex.substr(1, 2), 16) / 255, @@ -101,26 +61,6 @@ function rgba_float_to_hex(flt) { return '#' + comp(flt.r) + comp(flt.g) + comp(flt.b) + comp(flt.a); } -function ui_escape_tool() { - ui_vertices = []; - ui_shape = []; - ui_tool = TOOL_SELECT; - ui_specify_uv = false; -} - -function ui_set_tool(tool) { - if (ui_tool === tool) { - return; - } - ui_escape_tool(); - ui_tool = tool; - let tool_buttons = document.getElementsByClassName('tool-button'); - for (let i = 0; i < tool_buttons.length; i++) { - let button = tool_buttons[i]; - button.dataset.selected = parseInt(button.dataset.tool) === tool; - } -} - function update_key_modifiers_from_event(e) { shift_key = e.shiftKey; ctrl_key = e.ctrlKey; @@ -142,28 +82,6 @@ function on_key_press(e) { set_ui_shown(!ui_shown); e.preventDefault(); break; - case 27: // escape - ui_escape_tool(); - break; - case 49: // 1 - case 50: // 2 - case 51: // 3 - case 52: // 4 - case 53: // 5 - case 54: // 6 - case 55: // 7 - case 56: // 8 - case 57: // 9 - { - let tools = document.getElementsByClassName('tool-button'); - let i = code - 49; - if (i < tools.length) { - let tool = parseInt(tools[i].dataset.tool); - console.assert(!isNaN(tool), 'bad data-tool value'); - ui_set_tool(tool); - } - } - break; } } @@ -171,41 +89,9 @@ function on_key_release(e) { update_key_modifiers_from_event(e); } -function ndc_to_px(pos) { - let point = { - x: (pos.x * 0.5 + 0.5) * viewport_width, - y: (-pos.y * 0.5 + 0.5) * viewport_height, - }; - Object.preventExtensions(point); - return point; -} - - -function px_to_ndc(pos) { - let point = { - x: 2 * pos.x / viewport_width - 1, - y: 1 - 2 * pos.y / viewport_height, - }; - Object.preventExtensions(point); - return point; -} - - -function get_mouse_pos_from_event(e) { - if (e.target !== canvas && e.target !== ui_canvas) { - mouse_pos = {x: -1e10, y: -1e10}; - } else { - mouse_pos = px_to_ndc({x: e.offsetX, y: e.offsetY}); - } -} function on_mouse_move(e) { update_key_modifiers_from_event(e); - get_mouse_pos_from_event(e); -} - -function is_mouse_in_canvas() { - return Math.abs(mouse_pos.x) <= 1 && Math.abs(mouse_pos.y) <= 1; } function distance(p0, p1) { @@ -214,168 +100,17 @@ function distance(p0, p1) { return Math.sqrt(dx * dx + dy * dy); } -function snapped_pos(p) { - let px = ndc_to_px(p); - let closest_pos = null; - let closest_dist = Infinity; - function consider_pos(v) { - let dist = distance(ndc_to_px(v), px); - if (dist < closest_dist) { - closest_dist = dist; - closest_pos = v; - } - } - vertices_main.forEach(consider_pos); - ui_vertices.forEach(consider_pos); - const g = ui_grid_divisions(); - for (let y = 0; y <= g.y; ++y) { - for (let x = 0; x <= g.x; ++x) { - consider_pos({ - x: x / g.x * 2 - 1, - y: y / g.y * 2 - 1, - }); - } - } - if (closest_dist < vertex_mouse_radius) { - return Object.preventExtensions({ - x: closest_pos.x, - y: closest_pos.y, - }); - } - return Object.preventExtensions({x: p.x, y: p.y}); -} - -function snapped_mouse_pos() { - return ctrl_key ? mouse_pos : snapped_pos(mouse_pos); -} - -function lerp(a, b, x) { - return a + (b - a) * x; -} - -const VERTEX_POS = 0; -const VERTEX_UV = 8; -const VERTEX_COLOR = 16; -const VERTEX_SIZE = 32; - -function get_vertex_data() { - let array = new Uint8Array(indices_main.length * VERTEX_SIZE); - indices_main.forEach(function (index, i) { - let vertex = vertices_main[index]; - array.set(new Uint8Array((new Float32Array([vertex.x, vertex.y])).buffer), - VERTEX_SIZE * i + VERTEX_POS); - array.set(new Uint8Array((new Float32Array([vertex.uv.x, vertex.uv.y])).buffer), - VERTEX_SIZE * i + VERTEX_UV); - array.set(new Uint8Array((new Float32Array([vertex.color.r, vertex.color.g, vertex.color.b, vertex.color.a])).buffer), - VERTEX_SIZE * i + VERTEX_COLOR); - }); - return array; -} - -function ui_commit_vertices() { - let vertices = ui_vertices; - let i0 = vertices_main.length; - vertices.forEach(function (v) { - vertices_main.push(v); - }); - switch (ui_tool) { - case TOOL_TRIANGLE: - indices_main.push(i0, i0 + 1, i0 + 2); - break; - case TOOL_PARALLELOGRAM: - indices_main.push(i0, i0 + 1, i0 + 2, i0, i0 + 2, i0 + 3); - break; - } - vertices_changed = true; - - ui_shape = []; - ui_vertices = []; -} - function on_click(e) { - get_mouse_pos_from_event(e); update_key_modifiers_from_event(e); - if (!is_mouse_in_canvas()) { - return; - } if (!ui_shown) { return; } - - if (ui_is_editing_shape()) { - let pos = snapped_mouse_pos(); - let vertex = { - x: pos.x, - y: pos.y, - color: rgba_hex_to_float(ui_get_color_rgba()), - }; - ui_shape.push(vertex); - switch (ui_tool) { - case TOOL_TRIANGLE: - case TOOL_PARALLELOGRAM: - if (ui_specify_uv && ui_shape.length === 3) { - let uv = ui_shape; - let vertices = ui_vertices; - for (let i = 0; i < uv.length; i++) { - vertices[i].uv = {x: uv[i].x * 0.5 + 0.5, y: uv[i].y * 0.5 + 0.5}; - } - if (ui_tool === TOOL_PARALLELOGRAM) { - let v0 = vertices[0]; - let v1 = vertices[1]; - let v2 = vertices[2]; - let v3 = vertices[3]; - v0.first_in_quad = true; - v3.uv = Object.preventExtensions({ - x: v0.uv.x + v2.uv.x - v1.uv.x, - y: v0.uv.y + v2.uv.y - v1.uv.y, - }); - } - ui_commit_vertices(); - ui_set_tool(TOOL_SELECT); - } else if (ui_shape.length === 3) { - ui_specify_uv = true; - ui_vertices = ui_shape; - if (ui_tool === TOOL_PARALLELOGRAM) { - let v0 = ui_vertices[0]; - let v1 = ui_vertices[1]; - let v2 = ui_vertices[2]; - let v3 = { - color: v1.color, - x: v0.x + v2.x - v1.x, - y: v0.y + v2.y - v1.y - }; - ui_vertices.push(v3); - } - ui_shape = []; - let all_full_alpha = true; - ui_vertices.forEach(function (v) { - if (v.color.a < 1) { - all_full_alpha = false; - } - }); - if (all_full_alpha) { - // skip UV specification; it doesn't matter - ui_vertices.forEach(function (v) { v.uv = {x: 0, y: 0}; } ); - ui_commit_vertices(); - ui_set_tool(TOOL_SELECT); - } - } - break; - } - } } function startup() { page = document.getElementById('page'); canvas = document.getElementById('canvas'); ui_div = document.getElementById('ui'); - ui_canvas = document.getElementById('ui-canvas'); - ui_color_input = document.getElementById('color-input'); - ui_color_mix_input = document.getElementById('color-mix-input'); - ui_vertex_properties_div = document.getElementById('vertex-properties'); - ui_grid_divisions_x_input = document.getElementById('grid-divisions-x-input'); - ui_grid_divisions_y_input = document.getElementById('grid-divisions-y-input'); - ui_ctx = ui_canvas.getContext('2d'); gl = canvas.getContext('webgl'); if (gl === null) { @@ -405,59 +140,29 @@ function startup() { ]), gl.STATIC_DRAW); vertex_buffer_main = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer_main); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, + -1.0, -1.0, 1.0, 1.0, -1.0, 1.0 + ]), gl.STATIC_DRAW); framebuffer_color_texture = gl.createTexture(); sampler_texture = gl.createTexture(); set_up_framebuffer(); - ui_set_tool(TOOL_TRIANGLE); - frame(0.0); window.addEventListener('keydown', on_key_press); window.addEventListener('keyup', on_key_release); window.addEventListener('mousemove', on_mouse_move); window.addEventListener('click', on_click); - // set up tool buttons - Array.prototype.forEach.call( - document.getElementsByClassName('tool-button'), - function (tool_button) { - tool_button.addEventListener('click', function(e) { - let button = e.target; - while (button !== null && button.tagName !== 'BUTTON') { - button = button.parentElement; - } - console.assert(button !== null, 'what how did the event listener fire then'); - let n = parseInt(button.dataset.tool); - console.assert(!isNaN(n), 'bad data-tool value: ' + button.dataset.tool); - ui_set_tool(n); - }); - } - ); -} - -function ui_is_editing_shape() { - return ui_tool === TOOL_TRIANGLE || ui_tool === TOOL_PARALLELOGRAM; -} - -function ui_is_editing_vertex() { - return ui_tool === TOOL_TRIANGLE || ui_tool === TOOL_PARALLELOGRAM; -} - -function draw_vertex(vertex) { - ui_circle(vertex, vertex_radius, { - strokeStyle: '#ffffff', - fillStyle: rgba_float_to_hex(vertex.color), - }); } function frame(time) { - ui_vertex_properties_div.style.display = ui_is_editing_vertex() ? 'inline-block' : 'none'; current_time = time * 1e-3; let page_width = page.offsetWidth; - let page_height = page.offsetHeight; - + let page_height = page.offsetHeight; let aspect_ratio = width / height; let canvas_x = 0, canvas_y = 0; @@ -477,25 +182,12 @@ function frame(time) { canvas.height = viewport_height; canvas.style.left = canvas_x + 'px'; canvas.style.top = canvas_y + 'px'; - ui_canvas.width = viewport_width; - ui_canvas.height = viewport_height; - ui_canvas.style.left = canvas_x + 'px'; - ui_canvas.style.top = canvas_y + 'px'; - - if (vertices_changed) { - let vertex_data = get_vertex_data(); - gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer_main); - gl.bufferData(gl.ARRAY_BUFFER, vertex_data, gl.DYNAMIC_DRAW); - vertices_changed = false; - } - let step = true; if (step) { perform_step(); } - gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, viewport_width, viewport_height); gl.clearColor(0.0, 0.0, 0.0, 1.0); @@ -516,164 +208,8 @@ function frame(time) { return; } requestAnimationFrame(frame); - - ui_ctx.clearRect(0, 0, width, height); - - if (ui_shown) { - vertices_main.forEach(draw_vertex); - - for (let i = 0; i < indices_main.length / 3; i++) { - const line_options = { - strokeStyle: '#ffffff' - }; - let i0 = indices_main[3*i]; - let i1 = indices_main[3*i+1]; - let i2 = indices_main[3*i+2]; - let v0 = vertices_main[i0]; - let v1 = vertices_main[i1]; - let v2 = vertices_main[i2]; - if (!('first_in_quad' in v0 && i1 == i0 + 2)) - ui_line(v0, v1, line_options); - if (!('first_in_quad' in v0 && i2 == i0 + 2)) - ui_line(v0, v2, line_options); - ui_line(v1, v2, line_options); - } - - if (ui_specify_uv) { - ui_polygon(ui_vertices, { - strokeStyle: '#ffffff', - fillStyle: '#ffffff44', - }); - ui_vertices.forEach(draw_vertex); - } - - if (ui_is_editing_shape()) { - let color; - if (ui_specify_uv) { - color = '#3333ff'; - } else { - color = '#ffffff'; - } - let options_shape = { - strokeStyle: color, - fillStyle: color + '44', - }; - - if (ui_shape.length < 3 && is_mouse_in_canvas()) { - let mpos = snapped_mouse_pos(); - - // vertex where the mouse is - ui_circle(mpos, vertex_radius, { - strokeStyle: options_shape.strokeStyle, - fillStyle: ui_specify_uv ? color + '44' : ui_get_color_rgba(), - }); - - if (ui_shape.length === 1) { - ui_line(ui_shape[0], mpos, options_shape); - } else if (ui_shape.length === 2) { - if (ui_tool === TOOL_TRIANGLE) { - // triangle preview - ui_polygon([ui_shape[0], ui_shape[1], mpos], options_shape); - } else if (ui_tool === TOOL_PARALLELOGRAM) { - // parallelogram preview - let v0 = ui_shape[0]; - let v1 = ui_shape[1]; - let v2 = mpos; - let v3 = { - x: v0.x + v2.x - v1.x, - y: v0.y + v2.y - v1.y, - }; - ui_polygon([v0, v1, v2, v3], options_shape); - } else { - console.assert(false, 'bad tool'); - } - } - } - - for (let i = 0; i < ui_shape.length; i++) { - let vertex = ui_shape[i]; - - if (i > 0 && ui_shape.length < 3) { - let prev = ui_shape[i - 1]; - ui_line(prev, vertex, options_shape); - } - - ui_circle(vertex, vertex_radius, { - strokeStyle: options_shape.strokeStyle, - fillStyle: ui_specify_uv ? color + '44' : rgba_float_to_hex(vertex.color), - }); - } - - } - - { // draw grid - const g = ui_grid_divisions(); - const options = { - strokeStyle: '#ffffff66', - }; - for (let y = 1; y < g.y; ++y) { - let v = y / g.y * 2 - 1; - ui_line({x: -1, y: v}, {x: 1, y: v}, options); - } - - for (let x = 1; x < g.x; ++x) { - let v = x / g.x * 2 - 1; - ui_line({x: v, y: -1}, {x: v, y: 1}, options); - } - } - - } } -function ui_grid_divisions() { - let x = parseInt(ui_grid_divisions_x_input.value); - let y = parseInt(ui_grid_divisions_y_input.value); - if (isNaN(x) || isNaN(y) || x <= 0 || y <= 0 || x >= 100 || y >= 100) { - return null; - } - return Object.preventExtensions({x: x, y: y}); -} - -function ui_circle(pos, r, options) { - pos = ndc_to_px(pos); - ui_ctx.beginPath(); - ui_ctx.strokeStyle = 'strokeStyle' in options ? options.strokeStyle : '#000'; - ui_ctx.fillStyle = 'fillStyle' in options ? options.fillStyle : 'transparent'; - ui_ctx.lineWidth = 'lineWidth' in options ? options.lineWidth : 2; - ui_ctx.ellipse(pos.x, pos.y, r, r, 0, 0, 2 * Math.PI); - ui_ctx.stroke(); - ui_ctx.fill(); -} - -function ui_line(p0, p1, options) { - p0 = ndc_to_px(p0); - p1 = ndc_to_px(p1); - ui_ctx.beginPath(); - ui_ctx.strokeStyle = 'strokeStyle' in options ? options.strokeStyle : '#000'; - ui_ctx.lineWidth = 'lineWidth' in options ? options.lineWidth : 2; - ui_ctx.moveTo(p0.x, p0.y); - ui_ctx.lineTo(p1.x, p1.y); - ui_ctx.stroke(); -} - -function ui_polygon(vertices, options) { - console.assert(vertices.length >= 3, 'polygon must have at least 3 vertices'); - ui_ctx.beginPath(); - ui_ctx.strokeStyle = 'strokeStyle' in options ? options.strokeStyle : '#000'; - ui_ctx.fillStyle = 'fillStyle' in options ? options.fillStyle : 'transparent'; - ui_ctx.lineWidth = 'lineWidth' in options ? options.lineWidth : 2; - const v0 = ndc_to_px(vertices[0]); - ui_ctx.moveTo(v0.x, v0.y); - for (let i = 1; i < vertices.length; i++) { - const v = ndc_to_px(vertices[i]); - ui_ctx.lineTo(v.x, v.y); - } - ui_ctx.lineTo(v0.x, v0.y); - ui_ctx.stroke(); - ui_ctx.fill(); -} - - function perform_step() { if (width === -1) { // not properly loaded yet @@ -688,22 +224,14 @@ function perform_step() { gl.useProgram(program_main); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, sampler_texture); - gl.uniform4fv(gl.getUniformLocation(program_main, 'u_color'), [1.0, 1.0, 1.0, 1.0]); - gl.uniform1i(gl.getUniformLocation(program_main, 'u_sampler_texture'), 0); + gl.uniform1i(gl.getUniformLocation(program_main, 'u_texture'), 0); + gl.uniform1f(gl.getUniformLocation(program_main, 'u_time'), current_time % 3600); gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer_main); - if (indices_main.length >= 0) { - let v_pos = gl.getAttribLocation(program_main, 'v_pos'); - let v_uv = gl.getAttribLocation(program_main, 'v_uv'); - let v_color = gl.getAttribLocation(program_main, 'v_color'); - gl.enableVertexAttribArray(v_pos); - gl.enableVertexAttribArray(v_uv); - gl.enableVertexAttribArray(v_color); - gl.vertexAttribPointer(v_pos, 2, gl.FLOAT, false, VERTEX_SIZE, VERTEX_POS); - gl.vertexAttribPointer(v_uv, 2, gl.FLOAT, false, VERTEX_SIZE, VERTEX_UV); - gl.vertexAttribPointer(v_color, 4, gl.FLOAT, false, VERTEX_SIZE, VERTEX_COLOR); - gl.drawArrays(gl.TRIANGLES, 0, indices_main.length); - } + let v_pos = gl.getAttribLocation(program_main, 'v_pos'); + gl.enableVertexAttribArray(v_pos); + gl.vertexAttribPointer(v_pos, 2, gl.FLOAT, false, 8, 0); + gl.drawArrays(gl.TRIANGLES, 0, 6); gl.bindTexture(gl.TEXTURE_2D, sampler_texture); gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, width, height, 0); @@ -117,13 +117,9 @@ <meta content="width=device-width,initial-scale=1" name="viewport"> <script id="main-vertex-shader" type="x-shader/x-vertex"> attribute vec2 v_pos; -attribute vec2 v_uv; -attribute vec4 v_color; varying vec2 uv; -varying vec4 color; void main() { - uv = v_uv; - color = v_color; + uv = v_pos * 0.5 + 0.5; gl_Position = vec4(v_pos, 0.0, 1.0); } </script> @@ -133,11 +129,22 @@ precision highp float; #endif uniform sampler2D u_texture; -varying vec4 color; +uniform float u_time; varying vec2 uv; void main() { - gl_FragColor = mix(texture2D(u_texture, uv), vec4(color.xyz, 1.0), color.w); + vec2 u = pow(uv,vec2(1.2 + 0.4 * sin(u_time))); + vec2 k =floor(3.0 * u); + int i = int(k.y * 3.0 + k.x); + if (i == 4) discard; + vec3 sample = texture2D(u_texture, mod(3.0*u, 1.0)).xyz; + float h = mod(float(i) * 5.0, 8.0) / 8.0; + sample = vec3( + mix(sample.x, sample.z, h), + mix(sample.y, sample.x, h), + mix(sample.z, sample.y, h) + ); + gl_FragColor = vec4(mix(sample, vec3(1.0,0.0,0.0), 0.2),1.0); } </script> @@ -176,36 +183,9 @@ void main() { Try upgrading to the latest version of Microsoft Internet Explorer®. </p> </canvas> - <canvas id="ui-canvas"></canvas> </div> <div id="ui"> - <div id="vertex-properties"> - <!-- - <input type="text" value="1.0" name="red-input" id="red-input"> - <label for="red-input">R</label> - <input type="text" value="1.0" name="green-input" id="green-input"> - <label for="green-input">G</label> - <input type="text" value="1.0" name="blue-input" id="blue-input"> - <label for="blue-input">B</label> - --> - <input type="color" value="#ff00aa" name="color-input" id="color-input" aria-label="Color"> - <input type="text" value="0.2" name="color-mix-input" id="color-mix-input"> - <label for="color-mix-input">opacity</label> - </div> - <div style="float:right;margin-right:0.2em;"> - <input type="number" value="8" id="grid-divisions-x-input"> - <input type="number" value="8" id="grid-divisions-y-input"> - <label for="grid-divisions-y-input">grid</label> - <button class="tool-button" data-tool="3" title="select tool (1)"> - <img alt="select" class="icon" src="data:image/png;base64,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"> - </button> - <button class="tool-button" data-tool="1" title="triangle tool (2)"> - <img alt="triangle" class="icon" src="data:image/png;base64,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"> - </button> - <button class="tool-button" data-tool="4" title="parallelogram tool (3)"> - <img alt="parallelogram" class="icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAQAAAAkGDomAAAE60lEQVRo3u2ay28bVRSHv7Gd5kHTlqZB1EUKooi3UCsERChqQEgsWNAqLFiyy97wB0RZ0GXCthVrpC5aVIkFIJBaNQIlKapYNIJFKopIQKR5Omrc2p4fi0zmYc/YM54Zu5VyZpe5c+bLnXt+95x7DPv2WJpGNaU5bWrXNjWnKY1GdeJ7JQA3osvyt8saiQWXBKTGtaRgW9J4bLw4iBrXPTW2e00RFepKCa85okJfLay92o97TQW9qYKu1X3okdh40RFrQuOGxlz3xnTDGy6NAX1FIB6gRj0AF5SvuZ/XBc+I0SC8hiLQOqKmPLOX9xmR98zilD9gUxFoGXDO5WcsYMyYa8xcyyLQIuCmExoNRjnhshlDBMIDKqse9WtQx10eCg3GF5xhu3/JOSLABAOe0de5ygwjnMW9YAeYYIGZICC66KKHHDl6OcIxhniOEx7PPzf4j+r82oAUyHsGThtXAJhnWmMUcJQpT8FxpCw99JEjSy8HeZohTnKCAY4yQD8HyJEjg+Hy/S6zgYB1+mfsiQDulXGRSWPZG2FM4Nb3c8xZczTEq5xkkCc5Qj+9HLCAvFBu+8V4J3gN2l9ryzjcQATUTAT+0Pea1x2tqKiSyqqoKlNhLXoUe0XAPwD0sWuMqUpoIFNVVVSNpYNuEQiKUWV0MwJQWSUVtaq/9btm9Z1+aG0n2QuSQ/bIq27h9iyjLHO8EbBDmgiTCg/ZocgG66ywyG3+YosyJapU+IqP7CfGeUV7YQjUhCFcMa57g8SZrreY94kiQBnO4WziJiZVypQossoaqyyxyF3+ZZsdqlS4T8mourMZLpEPIWSwzCfGTK3MOIE+750cF9ILrhs/8h+L3OYuG+xQoUKJMmU3Uo1gzGiS8x5FHMUvJVhl0phpsBE1SLCcjaio4xrUE8q2KaP2i+K6q9lWnmZNUq+DdVfzZCjNqq4+nazDa55ORq+Li7a/ki7p/YgJuefjhkvIo2K+pn8sj6t6T5n0S5qogKe0Zvnc0HB7isKogOuW13WdbldZ3SrgqfYdTIQHPB0ZMKGjnbDvGtaG5XstNKAtAjdVsh5+qN80HVdYfOuVD7VlvWNFr0d9/Fn9qqokU3d0Rl0pnBF261NtW4DLetl/VLD2HCRPBjA4TJZqCqeYGQ7Z7y8HlYjBgDm67TxwxzBTADTo8hxqRAR0UkHxIKWTYCPMNIc7cO2YZeL8d7Hnr9d6izCjA3a7PnE6luWY/f5SUBhmAo8wjtr3qpipAHbzlPUOUaQUbQZzDLokoJzSDPbbX2k7aBJycTSq9XSVs4zwEv32ahzibb6J4qJPn+m+pfJ/6sUE4ZLZ69Wnz9MATCxbSgewlXwzE0ejouLVlO1+NsD5cM2wQ/pSD6yjoAU9n8jaS7Lm0aC+VlmSVNWsnkkAMIk2jitdvWXh7TZyLiZQB8dv46SZ8CfSxkmzZGpjG6dFwHa2cSKVRTHbOPZe7NvG+ZZbDPMBZzw6NaEFo21tHCdZCGrj/MQXnWzjWHnzY9fGaSoCnW3jBDz+qLRxAqe/I22cvSDxbePUJegdaOPsBYmrjWPMB81gJ9o49TVJTRvHFe6mHo02TqiNqLNtnBa38jRrkoSSofSqusTSySgnuNF+3phgQp6OdaKNk3zNmnifhDaJQFsR29jG6eTRTsqQSfzGed/2bd/aZf8DSKJozbS2TtQAAAAASUVORK5CYII="> - </button> - </div> + a </div> <dialog id="error-dialog"> |