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">
|
||||
<div class="vr mx-1"></div>
|
||||
<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"
|
||||
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"
|
||||
|
|
@ -246,6 +256,18 @@ resizeAndSetupCanvas();
|
|||
setGrid();
|
||||
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() {
|
||||
if (historyIndex <= 0) return;
|
||||
historyIndex--;
|
||||
|
|
@ -280,7 +302,7 @@ function clamp01(n, fallback = 1) {
|
|||
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
|
||||
|
||||
function sanitizeShapes(list) {
|
||||
const allowed = new Set(['rect','ellipse','line']);
|
||||
const allowed = new Set(['rect','ellipse','line','path']);
|
||||
|
||||
return list.flatMap((s) => {
|
||||
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 }];
|
||||
}
|
||||
|
||||
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 [];
|
||||
return [{
|
||||
type: s.type,
|
||||
|
|
@ -550,6 +592,24 @@ function drawShape(shape) {
|
|||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
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();
|
||||
|
|
@ -727,10 +787,11 @@ gridEl.addEventListener('pointermove', (e) => {
|
|||
if (!ctx) return;
|
||||
|
||||
const { ix, iy, x: snapX, y: snapY, localX, localY } = snapToGrid(e.clientX, e.clientY);
|
||||
const tool = getActiveTool();
|
||||
|
||||
coordsEl.classList.remove('d-none');
|
||||
|
||||
if (getActiveType() !== 'noGrid') {
|
||||
if (getActiveType() !== 'noGrid' && tool !== 'pen') {
|
||||
dotEl.classList.remove('d-none');
|
||||
|
||||
const gridRect = gridEl.getBoundingClientRect();
|
||||
|
|
@ -758,6 +819,28 @@ gridEl.addEventListener('pointermove', (e) => {
|
|||
ctx.save();
|
||||
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') {
|
||||
const previewLine = normalizeLine({
|
||||
type: 'line',
|
||||
|
|
@ -837,6 +920,16 @@ gridEl.addEventListener('pointerdown', (e) => {
|
|||
fill: (tool === 'filledEllipse'),
|
||||
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;
|
||||
|
||||
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') {
|
||||
const line = normalizeLine(currentShape);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue