More features added to our draw widget.
This commit is contained in:
parent
55f18b1cbe
commit
d207f1da2c
1 changed files with 285 additions and 223 deletions
|
|
@ -11,7 +11,7 @@
|
||||||
linear-gradient(to right, #ccc 1px, transparent 1px),
|
linear-gradient(to right, #ccc 1px, transparent 1px),
|
||||||
linear-gradient(to bottom, #ccc 1px, transparent 1px);
|
linear-gradient(to bottom, #ccc 1px, transparent 1px);
|
||||||
background-size: var(--grid) var(--grid);
|
background-size: var(--grid) var(--grid);
|
||||||
cursor: none;
|
cursor: crosshair;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(round(nearest, 80vh, var(--grid)) + 1px);
|
height: calc(round(nearest, 80vh, var(--grid)) + 1px);
|
||||||
|
|
@ -65,47 +65,73 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" id="line" name="tool">
|
<input type="radio" class="btn-check" id="line" name="tool">
|
||||||
<label for="line" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
<label for="line"
|
||||||
|
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
⎯
|
⎯
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="vr mx-1"></div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
|
||||||
|
id="export">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-floppy" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 2H9v3h2z" />
|
||||||
|
<path
|
||||||
|
d="M1.5 0h11.586a1.5 1.5 0 0 1 1.06.44l1.415 1.414A1.5 1.5 0 0 1 16 2.914V14.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5v-13A1.5 1.5 0 0 1 1.5 0M1 1.5v13a.5.5 0 0 0 .5.5H2v-4.5A1.5 1.5 0 0 1 3.5 9h9a1.5 1.5 0 0 1 1.5 1.5V15h.5a.5.5 0 0 0 .5-.5V2.914a.5.5 0 0 0-.146-.353l-1.415-1.415A.5.5 0 0 0 13.086 1H13v4.5A1.5 1.5 0 0 1 11.5 7h-7A1.5 1.5 0 0 1 3 5.5V1H1.5a.5.5 0 0 0-.5.5m3 4a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5V1H4zM3 15h10v-4.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center"
|
||||||
|
id="clear">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-x-lg" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="dot" class="position-absolute p-0 m-0 d-none">
|
<span id="dot" class="position-absolute p-0 m-0 d-none">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="{{ dot_size }}"
|
<svg xmlns="http://www.w3.org/2000/svg" width="{{ dot_size }}" height="{{ dot_size }}" viewBox="0 0 32 32"
|
||||||
height="{{ dot_size }}" viewBox="0 0 32 32" id="dotSVG">
|
id="dotSVG">
|
||||||
<circle cx="16" cy="16" r="4" fill="black" />
|
<circle cx="16" cy="16" r="4" fill="black" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div id="coords" class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none"></div>
|
<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>
|
<canvas id="overlay" class="w-100 h-100"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
const canvasEl = document.getElementById('overlay');
|
const canvasEl = document.getElementById('overlay');
|
||||||
const colorEl = document.getElementById('color');
|
const clearEl = document.getElementById('clear');
|
||||||
const coordsEl = document.getElementById('coords');
|
const colorEl = document.getElementById('color');
|
||||||
const dotEl = document.getElementById('dot');
|
const coordsEl = document.getElementById('coords');
|
||||||
const dotSVGEl = document.getElementById('dotSVG');
|
const dotEl = document.getElementById('dot');
|
||||||
const gridEl = document.getElementById('grid');
|
const dotSVGEl = document.getElementById('dotSVG');
|
||||||
|
const exportEl = document.getElementById('export');
|
||||||
|
const gridEl = document.getElementById('grid');
|
||||||
|
|
||||||
let ctx;
|
let ctx;
|
||||||
let dpr = 1;
|
let dpr = 1;
|
||||||
let selectedColor = '#000000';
|
let selectedColor;
|
||||||
|
|
||||||
let currentShape = null;
|
let currentShape = null;
|
||||||
let shapes = [];
|
let shapes = JSON.parse(localStorage.getItem('gridShapes')) || [];
|
||||||
|
|
||||||
resizeAndSetupCanvas();
|
resizeAndSetupCanvas();
|
||||||
window.addEventListener('resize', resizeAndSetupCanvas);
|
window.addEventListener('resize', resizeAndSetupCanvas);
|
||||||
|
|
||||||
function getActiveTool() {
|
function getActiveTool() {
|
||||||
const checked = document.querySelector('input[name="tool"]:checked');
|
const checked = document.querySelector('input[name="tool"]:checked');
|
||||||
return checked ? checked.id : 'outline';
|
return checked ? checked.id : 'outline';
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapToGrid(x, y) {
|
function snapToGrid(x, y) {
|
||||||
const rect = gridEl.getBoundingClientRect();
|
const rect = gridEl.getBoundingClientRect();
|
||||||
const clampedX = Math.min(Math.max(x, rect.left), rect.right);
|
const clampedX = Math.min(Math.max(x, rect.left), rect.right);
|
||||||
const clampedY = Math.min(Math.max(y, rect.top), rect.bottom);
|
const clampedY = Math.min(Math.max(y, rect.top), rect.bottom);
|
||||||
|
|
@ -113,17 +139,21 @@
|
||||||
const localX = clampedX - rect.left;
|
const localX = clampedX - rect.left;
|
||||||
const localY = clampedY - rect.top;
|
const localY = clampedY - rect.top;
|
||||||
|
|
||||||
const ix = Math.round(localX / {{ grid_size }});
|
const maxIx = Math.floor(rect.width / {{ grid_size }});
|
||||||
const iy = Math.round(localY / {{ grid_size }});
|
const maxIy = Math.floor(rect.height / {{ grid_size }});
|
||||||
|
|
||||||
|
const ix = Math.min(Math.max(Math.round(localX / {{ grid_size }}), 0), maxIx);
|
||||||
|
const iy = Math.min(Math.max(Math.round(localY / {{ grid_size }}), 0), maxIy);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ix,
|
ix,
|
||||||
iy,
|
iy,
|
||||||
x: ix * {{ grid_size }},
|
x: ix * {{ grid_size }},
|
||||||
y: iy * {{ grid_size }}
|
y: iy * {{ grid_size }}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRect(shape) {
|
function normalizeRect(shape) {
|
||||||
const x = Math.min(shape.x1, shape.x2);
|
const x = Math.min(shape.x1, shape.x2);
|
||||||
const y = Math.min(shape.y1, shape.y2);
|
const y = Math.min(shape.y1, shape.y2);
|
||||||
const w = Math.abs(shape.x2 - shape.x1);
|
const w = Math.abs(shape.x2 - shape.x1);
|
||||||
|
|
@ -138,9 +168,20 @@
|
||||||
color: shape.color,
|
color: shape.color,
|
||||||
fill: shape.fill
|
fill: shape.fill
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeAndSetupCanvas() {
|
function normalizeLine(shape) {
|
||||||
|
return {
|
||||||
|
type: 'line',
|
||||||
|
x1: shape.x1,
|
||||||
|
y1: shape.y1,
|
||||||
|
x2: shape.x2,
|
||||||
|
y2: shape.y2,
|
||||||
|
color: shape.color
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeAndSetupCanvas() {
|
||||||
dpr = window.devicePixelRatio || 1;
|
dpr = window.devicePixelRatio || 1;
|
||||||
const rect = canvasEl.getBoundingClientRect();
|
const rect = canvasEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
|
@ -150,17 +191,23 @@
|
||||||
ctx = canvasEl.getContext('2d');
|
ctx = canvasEl.getContext('2d');
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
redrawAll();
|
selectedColor = colorEl.value || '#000000';
|
||||||
|
const circle = dotSVGEl.querySelector('circle');
|
||||||
|
if (circle) {
|
||||||
|
circle.setAttribute('fill', selectedColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
function redrawAll() {
|
redrawAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redrawAll() {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
clearCanvas();
|
clearCanvas();
|
||||||
shapes.forEach(drawShape);
|
shapes.forEach(drawShape);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawShape(shape) {
|
function drawShape(shape) {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
@ -175,7 +222,7 @@
|
||||||
ctx.fillRect(shape.x, shape.y, shape.w, shape.h);
|
ctx.fillRect(shape.x, shape.y, shape.w, shape.h);
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
} else if ( shape.type === 'line' ) {
|
} else if (shape.type === 'line') {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(shape.x1, shape.y1);
|
ctx.moveTo(shape.x1, shape.y1);
|
||||||
ctx.lineTo(shape.x2, shape.y2);
|
ctx.lineTo(shape.x2, shape.y2);
|
||||||
|
|
@ -183,36 +230,57 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCanvas() {
|
function clearCanvas() {
|
||||||
ctx.clearRect(0, 0, canvasEl.width / dpr, canvasEl.height / dpr);
|
if (!ctx) return;
|
||||||
}
|
const rect = canvasEl.getBoundingClientRect();
|
||||||
|
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
colorEl.addEventListener('input', () => {
|
exportEl.addEventListener('click', () => {
|
||||||
|
const blob = new Blob([JSON.stringify(shapes, null,2)], {type: 'application/json'});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'grid-shapes.json';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
clearEl.addEventListener('click', () => {
|
||||||
|
shapes = [];
|
||||||
|
localStorage.removeItem('gridShapes');
|
||||||
|
redrawAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
colorEl.addEventListener('input', () => {
|
||||||
selectedColor = document.getElementById('color').value;
|
selectedColor = document.getElementById('color').value;
|
||||||
const circle = dotSVGEl.querySelector('circle');
|
const circle = dotSVGEl.querySelector('circle');
|
||||||
if (circle) {
|
if (circle) {
|
||||||
circle.setAttribute('fill', selectedColor);
|
circle.setAttribute('fill', selectedColor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
const key = e.key.toLowerCase();
|
const key = e.key.toLowerCase();
|
||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && key === 'z') {
|
if ((e.ctrlKey || e.metaKey) && key === 'z') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (shapes.length > 0) {
|
||||||
shapes.pop();
|
shapes.pop();
|
||||||
|
localStorage.setItem('gridShapes', JSON.stringify(shapes));
|
||||||
redrawAll();
|
redrawAll();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (key === 'escape' && currentShape) {
|
if (key === 'escape' && currentShape) {
|
||||||
currentShape = null;
|
currentShape = null;
|
||||||
redrawAll();
|
redrawAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
gridEl.addEventListener('mousemove', (e) => {
|
gridEl.addEventListener('mousemove', (e) => {
|
||||||
const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
||||||
|
|
||||||
coordsEl.innerText = `(${ix}, ${iy})`;
|
coordsEl.innerText = `(${ix}, ${iy})`;
|
||||||
|
|
@ -257,21 +325,21 @@
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
gridEl.addEventListener('mouseleave', (e) => {
|
gridEl.addEventListener('mouseleave', (e) => {
|
||||||
coordsEl.classList.add('d-none');
|
coordsEl.classList.add('d-none');
|
||||||
dotEl.classList.add('d-none');
|
dotEl.classList.add('d-none');
|
||||||
});
|
});
|
||||||
|
|
||||||
gridEl.addEventListener('mousedown', (e) => {
|
gridEl.addEventListener('mousedown', (e) => {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (e.target.closest('#toolBar')) return;
|
if (e.target.closest('#toolBar')) return;
|
||||||
|
|
||||||
const {x: snapX, y: snapY} = snapToGrid(e.clientX, e.clientY);
|
const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
||||||
const tool = getActiveTool();
|
const tool = getActiveTool();
|
||||||
|
|
||||||
if (tool === 'line') {
|
if (tool === 'line') {
|
||||||
|
|
@ -295,9 +363,9 @@
|
||||||
fill: document.getElementById('filled').checked
|
fill: document.getElementById('filled').checked
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('mouseup', (e) => {
|
window.addEventListener('mouseup', (e) => {
|
||||||
if (!currentShape) return;
|
if (!currentShape) return;
|
||||||
|
|
||||||
const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
||||||
|
|
@ -308,14 +376,7 @@
|
||||||
let finalShape = null;
|
let finalShape = null;
|
||||||
|
|
||||||
if (currentShape.tool === 'line') {
|
if (currentShape.tool === 'line') {
|
||||||
finalShape = {
|
finalShape = normalizeLine(currentShape);
|
||||||
type: 'line',
|
|
||||||
x1: currentShape.x1,
|
|
||||||
y1: currentShape.y1,
|
|
||||||
x2: currentShape.x2,
|
|
||||||
y2: currentShape.y2,
|
|
||||||
color: currentShape.color
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
const rect = normalizeRect(currentShape);
|
const rect = normalizeRect(currentShape);
|
||||||
|
|
||||||
|
|
@ -326,11 +387,12 @@
|
||||||
|
|
||||||
if (finalShape) {
|
if (finalShape) {
|
||||||
shapes.push(finalShape);
|
shapes.push(finalShape);
|
||||||
|
localStorage.setItem('gridShapes', JSON.stringify(shapes));
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCanvas();
|
clearCanvas();
|
||||||
shapes.forEach(drawShape);
|
shapes.forEach(drawShape);
|
||||||
|
|
||||||
currentShape = null;
|
currentShape = null;
|
||||||
});
|
});
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue