Experimental drawing widget?
This commit is contained in:
parent
7d4b76d19f
commit
5b8f14c99b
2 changed files with 229 additions and 9 deletions
|
|
@ -7,6 +7,6 @@ bp_testing = Blueprint("testing", __name__)
|
||||||
def init_testing_routes(app):
|
def init_testing_routes(app):
|
||||||
@bp_testing.get('/testing')
|
@bp_testing.get('/testing')
|
||||||
def test_page():
|
def test_page():
|
||||||
return render_template('testing.html')
|
return render_template('testing.html', grid_size=25)
|
||||||
|
|
||||||
app.register_blueprint(bp_testing)
|
app.register_blueprint(bp_testing)
|
||||||
|
|
@ -1,21 +1,241 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% set dot_size = [grid_size * 1.25, 32]|max|int %}
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
#outer {
|
:root {
|
||||||
border-style: dashed !important;
|
--grid: {{ grid_size }}px;
|
||||||
display: grid;
|
|
||||||
height: 85vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
#grid {
|
||||||
border-style: dashed !important;
|
background-image:
|
||||||
|
linear-gradient(to right, #ccc 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, #ccc 1px, transparent 1px);
|
||||||
|
background-size: var(--grid) var(--grid);
|
||||||
|
cursor: none;
|
||||||
|
height: calc(round(nearest, 80vh, var(--grid)) + 1px);
|
||||||
|
width: calc(round(nearest, 100%, var(--grid)) + 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolBar {
|
||||||
|
top: 10px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#coords {
|
||||||
|
bottom: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#overlay {
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
inset: 0;
|
||||||
}
|
}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="border border-primary border-5 border-opacity-50 rounded-4 bg-primary-subtle mt-3" id="outer">
|
<div class="container">
|
||||||
|
<div id="grid" class="position-relative overflow-hidden">
|
||||||
|
<div id="toolBar"
|
||||||
|
class="btn-toolbar bg-light position-absolute border border-secondary-subtle rounded start-50 p-1">
|
||||||
|
<input type="color" class="form-control form-control-sm form-control-color" id="color" oninput="setColor()">
|
||||||
|
<div class="vr mx-1"></div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<input type="radio" class="btn-check" id="outline" name="tool" checked>
|
||||||
|
<label for="outline"
|
||||||
|
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-square" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" id="filled" name="tool">
|
||||||
|
<label for="filled"
|
||||||
|
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-square-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2z" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span id="dot" class="position-absolute p-0 m-0 d-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="{{ dot_size }}"
|
||||||
|
height="{{ dot_size }}" viewBox="0 0 32 32" id="dotSVG">
|
||||||
|
<circle cx="16" cy="16" r="4" fill="black" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div id="coords" class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none"></div>
|
||||||
|
<canvas id="overlay" class="w-100 h-100"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
const canvasEl = document.getElementById('overlay');
|
||||||
|
const coordsEl = document.getElementById('coords');
|
||||||
|
const dotEl = document.getElementById('dot');
|
||||||
|
const dotSVGEl = document.getElementById('dotSVG');
|
||||||
|
const gridEl = document.getElementById('grid');
|
||||||
|
|
||||||
|
let ctx;
|
||||||
|
let selectedColor = '#000000';
|
||||||
|
|
||||||
|
let currentRect = null;
|
||||||
|
let rects = [];
|
||||||
|
|
||||||
|
resizeAndSetupCanvas();
|
||||||
|
window.addEventListener('resize', resizeAndSetupCanvas);
|
||||||
|
|
||||||
|
function snapToGrid(x, y) {
|
||||||
|
const rect = canvasEl.getBoundingClientRect();
|
||||||
|
const clampedX = Math.min(Math.max(x, rect.left), rect.right);
|
||||||
|
const clampedY = Math.min(Math.max(y, rect.top), rect.bottom);
|
||||||
|
|
||||||
|
const localX = clampedX - rect.left;
|
||||||
|
const localY = clampedY - rect.top;
|
||||||
|
|
||||||
|
const ix = Math.round(localX / {{ grid_size }});
|
||||||
|
const iy = Math.round(localY / {{ grid_size }});
|
||||||
|
return {
|
||||||
|
ix,
|
||||||
|
iy,
|
||||||
|
x: ix * {{ grid_size }},
|
||||||
|
y: iy * {{ grid_size }}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRect(rect) {
|
||||||
|
return {
|
||||||
|
x: Math.min(rect.x1, rect.x2),
|
||||||
|
y: Math.min(rect.y1, rect.y2),
|
||||||
|
w: Math.abs(rect.x2 - rect.x1),
|
||||||
|
h: Math.abs(rect.y2 - rect.y1),
|
||||||
|
color: rect.color
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeAndSetupCanvas() {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const rect = canvasEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
canvasEl.width = rect.width * dpr;
|
||||||
|
canvasEl.height = rect.height * dpr;
|
||||||
|
|
||||||
|
ctx = canvasEl.getContext('2d');
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
|
redrawAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redrawAll() {
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
clearCanvas();
|
||||||
|
rects.forEach(drawRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawRect(rect) {
|
||||||
|
ctx.strokeStyle = rect.color;
|
||||||
|
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
||||||
|
if (rect.fill) {
|
||||||
|
ctx.globalAlpha = 0.15;
|
||||||
|
ctx.fillStyle = rect.color;
|
||||||
|
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCanvas() {
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setColor = function setColor() {
|
||||||
|
selectedColor = document.getElementById('color').value;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
|
||||||
|
rects.pop();
|
||||||
|
redrawAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gridEl.addEventListener('mousemove', (e) => {
|
||||||
|
const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
||||||
|
|
||||||
|
coordsEl.innerText = `(${ix}, ${iy})`;
|
||||||
|
|
||||||
|
const renderX = snapX - {{ dot_size }} / 2;
|
||||||
|
const renderY = snapY - {{ dot_size }} / 2;
|
||||||
|
|
||||||
|
coordsEl.classList.remove('d-none');
|
||||||
|
|
||||||
|
dotEl.classList.remove('d-none');
|
||||||
|
dotEl.style.top = `${renderY}px`;
|
||||||
|
dotEl.style.left = `${renderX}px`;
|
||||||
|
|
||||||
|
if (currentRect) {
|
||||||
|
const previewRect = normalizeRect({
|
||||||
|
...currentRect,
|
||||||
|
x2: snapX,
|
||||||
|
y2: snapY
|
||||||
|
});
|
||||||
|
|
||||||
|
clearCanvas();
|
||||||
|
rects.forEach(drawRect);
|
||||||
|
ctx.setLineDash([5, 3]);
|
||||||
|
drawRect(previewRect);
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gridEl.addEventListener('mouseleave', (e) => {
|
||||||
|
coordsEl.classList.add('d-none');
|
||||||
|
dotEl.classList.add('d-none');
|
||||||
|
});
|
||||||
|
|
||||||
|
gridEl.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.target.closest('#toolBar')) return;
|
||||||
|
|
||||||
|
const {ix, iy, x: snapX, y: snapY} = snapToGrid(e.clientX, e.clientY);
|
||||||
|
|
||||||
|
currentRect = {
|
||||||
|
x1: snapX,
|
||||||
|
y1: snapY,
|
||||||
|
x2: snapX,
|
||||||
|
y2: snapY,
|
||||||
|
color: selectedColor
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
gridEl.addEventListener('mouseup', (e) => {
|
||||||
|
if (!currentRect) return;
|
||||||
|
|
||||||
|
const {ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
||||||
|
|
||||||
|
currentRect.x2 = snapX;
|
||||||
|
currentRect.y2 = snapY;
|
||||||
|
|
||||||
|
const finalRect = normalizeRect(currentRect);
|
||||||
|
|
||||||
|
if (finalRect.w > 0 && finalRect.h > 0) {
|
||||||
|
rects.push(finalRect);
|
||||||
|
}
|
||||||
|
clearCanvas();
|
||||||
|
rects.forEach(drawRect);
|
||||||
|
|
||||||
|
currentRect = null;
|
||||||
|
});
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue