Pen tool added.
This commit is contained in:
parent
c5cc368ef9
commit
24b74f78c0
1 changed files with 113 additions and 3 deletions
|
|
@ -60,7 +60,17 @@
|
||||||
<input type="color" class="form-control form-control-sm form-control-color" id="color">
|
<input type="color" class="form-control form-control-sm form-control-color" id="color">
|
||||||
<div class="vr mx-1"></div>
|
<div class="vr mx-1"></div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<input type="radio" class="btn-check" id="outline" name="tool" checked>
|
<input type="radio" class="btn-check" id="pen" name="tool" checked>
|
||||||
|
<label for="pen"
|
||||||
|
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-pen"
|
||||||
|
viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="m13.498.795.149-.149a1.207 1.207 0 1 1 1.707 1.708l-.149.148a1.5 1.5 0 0 1-.059 2.059L4.854 14.854a.5.5 0 0 1-.233.131l-4 1a.5.5 0 0 1-.606-.606l1-4a.5.5 0 0 1 .131-.232l9.642-9.642a.5.5 0 0 0-.642.056L6.854 4.854a.5.5 0 1 1-.708-.708L9.44.854A1.5 1.5 0 0 1 11.5.796a1.5 1.5 0 0 1 1.998-.001m-.644.766a.5.5 0 0 0-.707 0L1.95 11.756l-.764 3.057 3.057-.764L14.44 3.854a.5.5 0 0 0 0-.708z" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" id="outline" name="tool">
|
||||||
<label for="outline"
|
<label for="outline"
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
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"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-square"
|
||||||
|
|
@ -246,6 +256,18 @@ resizeAndSetupCanvas();
|
||||||
setGrid();
|
setGrid();
|
||||||
scheduleSnappedcellSize();
|
scheduleSnappedcellSize();
|
||||||
|
|
||||||
|
function pxToDocPoint(clientX, clientY) {
|
||||||
|
const rect = gridEl.getBoundingClientRect();
|
||||||
|
const x = Math.min(Math.max(clientX, rect.left), rect.right) - rect.left;
|
||||||
|
const y = Math.min(Math.max(clientY, rect.top), rect.bottom) - rect.top;
|
||||||
|
return { x: pxToGrid(x), y: pxToGrid(y) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function dist2(a, b) {
|
||||||
|
const dx = a.x - b.x, dy = a.y - b.y;
|
||||||
|
return dx*dx + dy*dy
|
||||||
|
}
|
||||||
|
|
||||||
function undo() {
|
function undo() {
|
||||||
if (historyIndex <= 0) return;
|
if (historyIndex <= 0) return;
|
||||||
historyIndex--;
|
historyIndex--;
|
||||||
|
|
@ -280,7 +302,7 @@ function clamp01(n, fallback = 1) {
|
||||||
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
|
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
|
||||||
|
|
||||||
function sanitizeShapes(list) {
|
function sanitizeShapes(list) {
|
||||||
const allowed = new Set(['rect','ellipse','line']);
|
const allowed = new Set(['rect','ellipse','line','path']);
|
||||||
|
|
||||||
return list.flatMap((s) => {
|
return list.flatMap((s) => {
|
||||||
if (!s || typeof s !== 'object' || !allowed.has(s.type)) return [];
|
if (!s || typeof s !== 'object' || !allowed.has(s.type)) return [];
|
||||||
|
|
@ -292,6 +314,26 @@ function sanitizeShapes(list) {
|
||||||
return [{ type:'line', x1:+s.x1, y1:+s.y1, x2:+s.x2, y2:+s.y2, color }];
|
return [{ type:'line', x1:+s.x1, y1:+s.y1, x2:+s.x2, y2:+s.y2, color }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s.type === 'path') {
|
||||||
|
if (!Array.isArray(s.points) || s.points.length < 2) return [];
|
||||||
|
const points = s.points.flatMap(p => {
|
||||||
|
if (!p || !isFiniteNum(p.x) || !isFiniteNum(p.y)) return [];
|
||||||
|
return [{ x: +p.x, y: +p.y }];
|
||||||
|
});
|
||||||
|
if (points.length < 2) return [];
|
||||||
|
|
||||||
|
const width = isFiniteNum(s.width) ? Math.max(0.05, +s.width) : 0.12;
|
||||||
|
const opacity = clamp01(s.opacity, 1);
|
||||||
|
|
||||||
|
return [{
|
||||||
|
type: 'path',
|
||||||
|
points,
|
||||||
|
color,
|
||||||
|
width,
|
||||||
|
opacity
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
if (!['x','y','w','h'].every(k => isFiniteNum(s[k]))) return [];
|
if (!['x','y','w','h'].every(k => isFiniteNum(s[k]))) return [];
|
||||||
return [{
|
return [{
|
||||||
type: s.type,
|
type: s.type,
|
||||||
|
|
@ -550,6 +592,24 @@ function drawShape(shape) {
|
||||||
ctx.moveTo(x1, y1);
|
ctx.moveTo(x1, y1);
|
||||||
ctx.lineTo(x2, y2);
|
ctx.lineTo(x2, y2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
} else if (shape.type === 'path') {
|
||||||
|
const toPx = (v) => v * cellSize;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = shape.color || '#000000';
|
||||||
|
ctx.globalAlpha = clamp01(shape.opacity, 1);
|
||||||
|
|
||||||
|
ctx.lineWidth = Math.max(1, toPx(shape.width ?? 0.12));
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
const pts = shape.points;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(toPx(pts[0].x), toPx(pts[0].y));
|
||||||
|
for (let i = 1; i < pts.length; i++) {
|
||||||
|
ctx.lineTo(toPx(pts[i].x), toPx(pts[i].y));
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
@ -727,10 +787,11 @@ gridEl.addEventListener('pointermove', (e) => {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const { ix, iy, x: snapX, y: snapY, localX, localY } = snapToGrid(e.clientX, e.clientY);
|
const { ix, iy, x: snapX, y: snapY, localX, localY } = snapToGrid(e.clientX, e.clientY);
|
||||||
|
const tool = getActiveTool();
|
||||||
|
|
||||||
coordsEl.classList.remove('d-none');
|
coordsEl.classList.remove('d-none');
|
||||||
|
|
||||||
if (getActiveType() !== 'noGrid') {
|
if (getActiveType() !== 'noGrid' && tool !== 'pen') {
|
||||||
dotEl.classList.remove('d-none');
|
dotEl.classList.remove('d-none');
|
||||||
|
|
||||||
const gridRect = gridEl.getBoundingClientRect();
|
const gridRect = gridEl.getBoundingClientRect();
|
||||||
|
|
@ -758,6 +819,28 @@ gridEl.addEventListener('pointermove', (e) => {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.setLineDash([5, 3]);
|
ctx.setLineDash([5, 3]);
|
||||||
|
|
||||||
|
if (currentShape.tool === 'pen') {
|
||||||
|
const p = pxToDocPoint(e.clientX, e.clientY);
|
||||||
|
|
||||||
|
const pts = currentShape.points;
|
||||||
|
const last = pts[pts.length - 1];
|
||||||
|
const minStep = 0.02;
|
||||||
|
if (!last || dist2(p, last) >= minStep * minStep) {
|
||||||
|
pts.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCanvas();
|
||||||
|
shapes.forEach(drawShape);
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.setLineDash([5, 3]);
|
||||||
|
drawShape(currentShape);
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tool === 'line') {
|
if (tool === 'line') {
|
||||||
const previewLine = normalizeLine({
|
const previewLine = normalizeLine({
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
|
@ -837,6 +920,16 @@ gridEl.addEventListener('pointerdown', (e) => {
|
||||||
fill: (tool === 'filledEllipse'),
|
fill: (tool === 'filledEllipse'),
|
||||||
opacity: currentOpacity
|
opacity: currentOpacity
|
||||||
};
|
};
|
||||||
|
} else if (tool === 'pen') {
|
||||||
|
const p = pxToDocPoint(e.clientX, e.clientY);
|
||||||
|
currentShape = {
|
||||||
|
tool,
|
||||||
|
type: 'path',
|
||||||
|
points: [p],
|
||||||
|
color: selectedColor,
|
||||||
|
width: 0.12,
|
||||||
|
opacity: 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -854,6 +947,23 @@ window.addEventListener('pointerup', (e) => {
|
||||||
|
|
||||||
let finalShape = null;
|
let finalShape = null;
|
||||||
|
|
||||||
|
if (currentShape.tool === 'pen') {
|
||||||
|
const pts = currentShape.points;
|
||||||
|
|
||||||
|
if (pts.length >= 2) {
|
||||||
|
const simplified = [pts[0]];
|
||||||
|
const minStep = 0.03;
|
||||||
|
for (let i = 1; i < pts.length; i++) {
|
||||||
|
if (dist2(pts[i], simplified[simplified.length - 1]) >= minStep * minStep) {
|
||||||
|
simplified.push(pts[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (simplified.length >= 2) {
|
||||||
|
finalShape = { ...currentShape, points: simplified };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentShape.tool === 'line') {
|
if (currentShape.tool === 'line') {
|
||||||
const line = normalizeLine(currentShape);
|
const line = normalizeLine(currentShape);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue