Add undo/redo.
This commit is contained in:
parent
641ae1470d
commit
e8d9d1a330
1 changed files with 59 additions and 31 deletions
|
|
@ -205,6 +205,9 @@ const toolBarEl = document.getElementById('toolBar');
|
||||||
const fillOpacityEl = document.getElementById('fillOpacity');
|
const fillOpacityEl = document.getElementById('fillOpacity');
|
||||||
|
|
||||||
let doc = loadDoc();
|
let doc = loadDoc();
|
||||||
|
let shapes = sanitizeShapes(Array.isArray(doc.shapes) ? doc.shapes : []);
|
||||||
|
saveDoc({ ...doc, shapes });
|
||||||
|
|
||||||
let cellSize = Number(doc.cellSize) || 25;
|
let cellSize = Number(doc.cellSize) || 25;
|
||||||
cellSizeEl.value = cellSize;
|
cellSizeEl.value = cellSize;
|
||||||
let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
|
let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
|
||||||
|
|
@ -212,10 +215,11 @@ let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
|
||||||
let ctx;
|
let ctx;
|
||||||
let dpr = 1;
|
let dpr = 1;
|
||||||
let selectedColor;
|
let selectedColor;
|
||||||
let currentOpacity = clamp01(fillOpacity?.value ?? 0.15, 0.15);
|
let currentOpacity = clamp01(fillOpacityEl?.value ?? 0.15, 0.15);
|
||||||
|
|
||||||
let currentShape = null;
|
let currentShape = null;
|
||||||
let shapes = Array.isArray(doc.shapes) ? doc.shapes : [];
|
const history = [structuredClone(shapes)];
|
||||||
|
let historyIndex = 0
|
||||||
|
|
||||||
let sizingRAF = 0;
|
let sizingRAF = 0;
|
||||||
let lastApplied = { w: 0, h: 0 };
|
let lastApplied = { w: 0, h: 0 };
|
||||||
|
|
@ -234,11 +238,36 @@ if (savedType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeAndSetupCanvas();
|
resizeAndSetupCanvas();
|
||||||
window.addEventListener('resize', resizeAndSetupCanvas);
|
|
||||||
|
|
||||||
setGrid();
|
setGrid();
|
||||||
scheduleSnappedcellSize();
|
scheduleSnappedcellSize();
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
if (historyIndex <= 0) return;
|
||||||
|
historyIndex--;
|
||||||
|
shapes = structuredClone(history[historyIndex]);
|
||||||
|
saveDoc({ ...doc, shapes, cellSize });
|
||||||
|
redrawAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
if (historyIndex >= history.length - 1) return;
|
||||||
|
historyIndex++;
|
||||||
|
shapes = structuredClone(history[historyIndex]);
|
||||||
|
saveDoc({ ...doc, shapes, cellSize });
|
||||||
|
redrawAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit(nextShapes) {
|
||||||
|
history.splice(historyIndex + 1);
|
||||||
|
history.push(structuredClone(nextShapes));
|
||||||
|
historyIndex++;
|
||||||
|
shapes = nextShapes;
|
||||||
|
|
||||||
|
saveDoc({ ...doc, shapes, cellSize });
|
||||||
|
redrawAll();
|
||||||
|
}
|
||||||
|
|
||||||
function clamp01(n, fallback = 0.15) {
|
function clamp01(n, fallback = 0.15) {
|
||||||
const x = Number(n);
|
const x = Number(n);
|
||||||
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
|
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
|
||||||
|
|
@ -247,23 +276,27 @@ function clamp01(n, fallback = 0.15) {
|
||||||
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
|
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
|
||||||
|
|
||||||
function sanitizeShapes(list) {
|
function sanitizeShapes(list) {
|
||||||
const allowed = ['rect', 'ellipse', 'line'];
|
const allowed = new Set(['rect','ellipse','line']);
|
||||||
|
|
||||||
return list.filter(s => {
|
return list.flatMap((s) => {
|
||||||
if (!s || typeof s !== 'object') return false;
|
if (!s || typeof s !== 'object' || !allowed.has(s.type)) return [];
|
||||||
if (!allowed.includes(s.type)) return false;
|
const color = typeof s.color === 'string' ? s.color : '#000000';
|
||||||
|
const opacity = clamp01(s.opacity, 0.15);
|
||||||
|
|
||||||
if (!s.color) s.color = '#000000';
|
if (s.type === 'line') {
|
||||||
|
if (!['x1','y1','x2','y2'].every(k => isFiniteNum(s[k]))) return [];
|
||||||
|
return [{ type:'line', x1:+s.x1, y1:+s.y1, x2:+s.x2, y2:+s.y2, color }];
|
||||||
|
}
|
||||||
|
|
||||||
if (s.opacity == null) s.opacity = 0.15;
|
if (!['x','y','w','h'].every(k => isFiniteNum(s[k]))) return [];
|
||||||
s.opacity = clamp01(s.opacity, 0.15);
|
return [{
|
||||||
|
type: s.type,
|
||||||
if (s.type === 'line') {
|
x:+s.x, y:+s.y, w:+s.w, h:+s.h,
|
||||||
return ['x1','y1','x2','y2'].every(k => isFiniteNum(s[k]));
|
color,
|
||||||
} else {
|
fill: !!s.fill,
|
||||||
return ['x','y','w','h'].every(k => isFiniteNum(s[k]));
|
opacity
|
||||||
}
|
}];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDoc() {
|
function loadDoc() {
|
||||||
|
|
@ -382,11 +415,11 @@ function snapToGrid(x, y) {
|
||||||
let snapY = localY;
|
let snapY = localY;
|
||||||
|
|
||||||
if (type === 'fullGrid' || type === 'verticalGrid') {
|
if (type === 'fullGrid' || type === 'verticalGrid') {
|
||||||
snapX = ix * grid;
|
snapX = Math.min(ix * grid, rect.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'fullGrid' || type === 'horizontalGrid') {
|
if (type === 'fullGrid' || type === 'horizontalGrid') {
|
||||||
snapY = iy * grid;
|
snapY = Math.min(iy * grid, rect.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -637,11 +670,9 @@ exportEl.addEventListener('click', () => {
|
||||||
|
|
||||||
clearEl.addEventListener('click', () => {
|
clearEl.addEventListener('click', () => {
|
||||||
cellSize = 25;
|
cellSize = 25;
|
||||||
shapes = [];
|
|
||||||
saveDoc({...doc, shapes, cellSize});
|
|
||||||
cellSizeEl.value = 25;
|
cellSizeEl.value = 25;
|
||||||
applycellSize(25);
|
applycellSize(25);
|
||||||
redrawAll();
|
commit([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
colorEl.addEventListener('input', () => {
|
colorEl.addEventListener('input', () => {
|
||||||
|
|
@ -657,11 +688,11 @@ document.addEventListener('keydown', (e) => {
|
||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && key === 'z') {
|
if ((e.ctrlKey || e.metaKey) && key === 'z') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (shapes.length > 0) {
|
undo();
|
||||||
shapes.pop();
|
}
|
||||||
saveDoc({ ...doc, shapes, cellSize });
|
|
||||||
redrawAll();
|
if ((e.ctrlKey || e.metaKey) && (key === 'y' || (e.shiftKey && key === 'z'))) {
|
||||||
}
|
e.preventDefault(); redo();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'escape' && currentShape) {
|
if (key === 'escape' && currentShape) {
|
||||||
|
|
@ -833,10 +864,7 @@ window.addEventListener('pointerup', (e) => {
|
||||||
if (ellipse.w > 0 && ellipse.h > 0) finalShape = ellipse;
|
if (ellipse.w > 0 && ellipse.h > 0) finalShape = ellipse;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalShape) {
|
if (finalShape) commit([...shapes, finalShape]);
|
||||||
shapes.push(finalShape);
|
|
||||||
saveDoc({...doc, shapes, cellSize});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCanvas();
|
clearCanvas();
|
||||||
shapes.forEach(drawShape);
|
shapes.forEach(drawShape);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue