Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent e5c99a5eee
commit 6ee6502010
2050 changed files with 538359 additions and 57745 deletions
+133 -47
View File
@@ -14,13 +14,13 @@
* Touch:
* touchstart 1 finger → beginDraw
* touchmove 1 finger → continueDraw
* touchend / touchcancel → endDraw
* touchend → endDraw; touchcancel → cancelDraw
* touchstart 2 fingers → pinch-zoom + 2-finger pan
*
* Pan (any free space around the canvas):
* pointerdown / pointermove / pointerup on canvas-area, skipping
* the canvas + transform overlay + UI elements above them. Sets
* canvasArea.dataset.panX/Y + CSS transform on both canvases.
* Pan:
* Hand tool / held Space / middle mouse can drag directly over the
* image. Empty workspace remains draggable with any tool. The same
* path drives one-finger Hand-tool panning on touch screens.
*
* Exposes `canvasArea._resetPan()` so the zoom/fit reset can clear
* the pan offset.
@@ -30,19 +30,65 @@
* beginDraw: (e: Event) => void,
* continueDraw: (e: Event) => void,
* endDraw: (e?: Event) => void,
* cancelDraw?: () => void,
* updateBrushCursor: (e: Event) => void,
* syncZoomControls?: () => void,
* }} ctx
*/
import { state } from './state.js';
import {
applyCanvasPan,
isDirectPanIntent,
nextPanOffset,
syncPanCursor,
} from './canvas-navigation.js';
export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw, updateBrushCursor, syncZoomControls }) {
export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw, cancelDraw, updateBrushCursor, updateEyedropperPreview, syncZoomControls, onViewportChange }) {
let suppressMouseUntil = 0;
// Mouse — mousedown stays on the canvas; mousemove/up are bound to
// the WINDOW so a drag can continue (and end) past the canvas edge.
// Critical for the Resize tool where users overshoot.
state.mainCanvas.addEventListener('mousedown', beginDraw);
window.addEventListener('mousemove', continueDraw);
window.addEventListener('mouseup', endDraw);
state.mainCanvas.addEventListener('mousedown', (e) => {
if (Date.now() < suppressMouseUntil) return;
if (isDirectPanIntent(state.tool, state.spacePanActive, e.button)) return;
beginDraw(e);
});
window.addEventListener('mousemove', (e) => {
if (Date.now() < suppressMouseUntil) return;
continueDraw(e);
});
window.addEventListener('mouseup', (e) => {
if (Date.now() < suppressMouseUntil) return;
endDraw(e);
});
// Preserve pressure and browser-coalesced samples for pen input.
// Compatibility mouse events are briefly suppressed to avoid a
// duplicate stroke after pointerup.
let activePenId = null;
state.mainCanvas.addEventListener('pointerdown', (e) => {
if (e.pointerType !== 'pen') return;
suppressMouseUntil = Date.now() + 500;
activePenId = e.pointerId;
try { state.mainCanvas.setPointerCapture(activePenId); } catch {}
beginDraw(e);
e.preventDefault();
});
state.mainCanvas.addEventListener('pointermove', (e) => {
if (e.pointerType !== 'pen' || e.pointerId !== activePenId) return;
continueDraw(e);
e.preventDefault();
});
const endPen = (e) => {
if (e.pointerType !== 'pen' || e.pointerId !== activePenId) return;
if (e.type === 'pointercancel') cancelDraw?.();
else endDraw(e);
try { state.mainCanvas.releasePointerCapture(activePenId); } catch {}
activePenId = null;
suppressMouseUntil = Date.now() + 500;
e.preventDefault();
};
state.mainCanvas.addEventListener('pointerup', endPen);
state.mainCanvas.addEventListener('pointercancel', endPen);
// Lasso can start OUTSIDE the canvas — fallback mousedown on the
// surrounding canvas-area so the user can begin a lasso path in
// the empty space around the image. Other tools stay canvas-only.
@@ -52,12 +98,21 @@ export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw,
beginDraw(e);
});
state.mainCanvas.addEventListener('mouseenter', (e) => {
if (['brush', 'eraser', 'inpaint', 'lasso', 'clone'].includes(state.tool)) updateBrushCursor(e);
if (state.tool === 'eyedropper') updateEyedropperPreview?.(e);
if (['brush', 'eraser', 'inpaint', 'lasso', 'clone', 'heal', 'smudge', 'dodge', 'burn'].includes(state.tool)) updateBrushCursor(e);
});
state.mainCanvas.addEventListener('mouseleave', () => {
// Only hide the brush-cursor overlay on leave — DO NOT end the
// drag, so the user can drag a resize handle past the canvas edge.
if (state.cursorEl) state.cursorEl.style.display = 'none';
if (state.tool === 'eyedropper') updateEyedropperPreview?.(null, true);
if (state.tool === 'transform' && state.hoveredHandle) {
state.hoveredHandle = null;
state.mainCanvas.style.cursor = 'default';
// The frame is drawn on a separate overlay, so clear its hover state
// explicitly when the pointer leaves the source canvas.
continueDraw?.({ clientX: -1, clientY: -1, target: null });
}
});
// Touch — single finger draws; two fingers pan + pinch-zoom.
@@ -74,13 +129,7 @@ export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw,
const dy = t2.clientY - t1.clientY;
return { cx, cy, dist: Math.hypot(dx, dy) };
};
const applyCanvasOffset = (x, y) => {
canvasArea.dataset.panX = String(x);
canvasArea.dataset.panY = String(y);
const t = `translate3d(${x}px, ${y}px, 0)`;
state.mainCanvas.style.transform = t;
if (state.transformOverlay) state.transformOverlay.style.transform = t;
};
const applyCanvasOffset = (x, y) => applyCanvasPan(state, canvasArea, x, y);
state.mainCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (e.touches.length >= 2) {
@@ -91,13 +140,11 @@ export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw,
multiStartDist = info.dist;
multiStartZoom = state.zoom;
multiStartCenter = { x: info.cx, y: info.cy };
multiStartPan = {
x: parseFloat(canvasArea.dataset.panX || '0') || 0,
y: parseFloat(canvasArea.dataset.panY || '0') || 0,
};
multiStartPan = { x: state.panX || 0, y: state.panY || 0 };
return;
}
if (multiActive) return;
if (state.tool === 'hand') return;
beginDraw(e);
}, { passive: false });
state.mainCanvas.addEventListener('touchmove', (e) => {
@@ -113,10 +160,12 @@ export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw,
const label = state.container.querySelector('.ge-zoom-label');
if (label) label.textContent = Math.round(state.zoom * 100) + '%';
syncZoomControls?.();
onViewportChange?.();
}
const dx = info.cx - multiStartCenter.x;
const dy = info.cy - multiStartCenter.y;
applyCanvasOffset(multiStartPan.x + dx, multiStartPan.y + dy);
onViewportChange?.();
return;
}
if (multiActive) return;
@@ -131,67 +180,104 @@ export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw,
});
state.mainCanvas.addEventListener('touchcancel', () => {
multiActive = false;
endDraw();
cancelDraw?.();
});
// Press-and-drag in the empty space AROUND the canvas pans the
// canvas + overlay via CSS transform. Works even when the image
// fits the viewport (no scroll needed). Skips presses on the canvas
// itself (the canvas owns its own drawing input) or on UI elements
// above it.
// Direct pan gestures own the image as well as the surrounding area.
// With other tools only empty workspace pans, preserving normal edit input.
let panning = false;
let pid = null;
let transformPointerId = null;
let startX = 0, startY = 0;
const getOffset = () => {
const v = canvasArea.dataset.panX || '0';
const u = canvasArea.dataset.panY || '0';
return { x: parseFloat(v) || 0, y: parseFloat(u) || 0 };
};
let startPanX = 0, startPanY = 0;
const getOffset = () => ({ x: state.panX || 0, y: state.panY || 0 });
const applyOffset = (x, y) => {
canvasArea.dataset.panX = String(x);
canvasArea.dataset.panY = String(y);
const t = `translate3d(${x}px, ${y}px, 0)`;
state.mainCanvas.style.transform = t;
if (state.transformOverlay) state.transformOverlay.style.transform = t;
const result = applyCanvasPan(state, canvasArea, x, y);
onViewportChange?.();
return result;
};
canvasArea.addEventListener('pointerdown', (e) => {
if (state.tool === 'lasso') return;
if (e.target === state.mainCanvas || e.target === state.transformOverlay) return;
const directPan = isDirectPanIntent(state.tool, state.spacePanActive, e.button);
if (state.tool === 'lasso' && !directPan) return;
if (e.target.closest('button, input, .ge-adj-popup, .ge-transform-popup, .ge-fx-popup, .ge-inpaint-popup, .ge-controls, .ge-right-panel, .ge-fx-menu')) return;
const onImage = e.target === state.mainCanvas || e.target === state.transformOverlay;
if (onImage && !directPan) return;
// During an active transform the corner/rotation handles render
// OUTSIDE the canvas (over the surrounding area), and the overlay is
// pointer-events:none — so a grab on an outside handle lands here.
// Route it to the transform tool (getHandleAt works in image space,
// even for points beyond the canvas) instead of panning the canvas.
if (state.transformActive) {
if (state.transformActive && !directPan) {
beginDraw(e);
// Only swallow the event (skip pan) if a handle was grabbed OR the
// layer-move fallback engaged; otherwise let the pan logic below
// run so empty space still pans while the transform tool is open.
if (state.transformHandle || state.moving) return;
if (state.transformHandle || state.moving) {
// Mouse drags already continue on window mousemove. Pen/touch drags
// that start on an outside-canvas handle need pointer capture so the
// session survives leaving the editor surface.
if (e.pointerType && e.pointerType !== 'mouse') {
transformPointerId = e.pointerId;
try { canvasArea.setPointerCapture(transformPointerId); } catch {}
e.preventDefault();
}
return;
}
}
const off = getOffset();
panning = true;
pid = e.pointerId;
startX = e.clientX - off.x;
startY = e.clientY - off.y;
startX = e.clientX;
startY = e.clientY;
startPanX = off.x;
startPanY = off.y;
try { canvasArea.setPointerCapture(pid); } catch {}
canvasArea.style.cursor = 'grabbing';
state.navigationPanning = true;
syncPanCursor(state, canvasArea, true);
e.preventDefault();
});
canvasArea.addEventListener('pointermove', (e) => {
if (transformPointerId !== null && e.pointerId === transformPointerId) {
continueDraw(e);
return;
}
if (!panning || e.pointerId !== pid) return;
applyOffset(e.clientX - startX, e.clientY - startY);
const next = nextPanOffset(
{ x: startX, y: startY },
{ x: startPanX, y: startPanY },
{ x: e.clientX, y: e.clientY },
);
applyOffset(next.x, next.y);
});
const endPan = () => {
const endPan = (e) => {
if (transformPointerId !== null && (!e || e.pointerId === transformPointerId)) {
const capturedId = transformPointerId;
transformPointerId = null;
if (e?.type === 'pointercancel') cancelDraw?.();
else endDraw(e);
try { canvasArea.releasePointerCapture(capturedId); } catch {}
}
if (!panning) return;
panning = false;
try { canvasArea.releasePointerCapture(pid); } catch {}
pid = null;
canvasArea.style.cursor = '';
state.navigationPanning = false;
syncPanCursor(state, canvasArea, false);
};
canvasArea.addEventListener('pointerup', endPan);
canvasArea.addEventListener('pointercancel', endPan);
// Reset offset whenever zoom/fit changes the canvas size.
canvasArea._resetPan = () => applyOffset(0, 0);
const navigation = {
resetPan: canvasArea._resetPan,
setTemporaryPan(active) {
state.spacePanActive = !!active;
syncPanCursor(state, canvasArea, panning);
},
updateCursor() {
syncPanCursor(state, canvasArea, panning);
},
};
navigation.updateCursor();
return navigation;
}