summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fractiform.js500
-rw-r--r--index.html50
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);
diff --git a/index.html b/index.html
index d184202..70c0554 100644
--- a/index.html
+++ b/index.html
@@ -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&reg;.
</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">