feat: Implement edge panning and middle mouse button panning for zoomed views.
This commit is contained in:
@@ -1772,7 +1772,7 @@ import {
|
||||
}
|
||||
|
||||
/* --- MOBILE STYLES --- */
|
||||
@media (max-width: 1600px) {
|
||||
@media (max-width: 1200px) {
|
||||
.control-panel {
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
|
||||
@@ -353,7 +353,7 @@ import ControlPanel from "../components/ControlPanel.astro";
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
@media (max-width: 1200px) {
|
||||
.ascii-layout {
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
|
||||
@@ -65,6 +65,24 @@ export class AsciiController {
|
||||
private lastTouchPos = { x: 0, y: 0 };
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// Edge panning state
|
||||
private edgePanState = {
|
||||
active: false,
|
||||
directionX: 0, // -1 = left, 1 = right, 0 = none
|
||||
directionY: 0, // -1 = up, 1 = down, 0 = none
|
||||
animationId: null as number | null
|
||||
};
|
||||
private readonly EDGE_THRESHOLD = 0.08; // 8% of canvas edge triggers pan
|
||||
private readonly PAN_SPEED = 0.012; // Speed of panning per frame
|
||||
|
||||
// Middle mouse panning state
|
||||
private middleMousePanState = {
|
||||
isDragging: false,
|
||||
lastMousePos: { x: 0, y: 0 }
|
||||
};
|
||||
private mouseDragHandler: ((e: MouseEvent) => void) | null = null;
|
||||
private mouseUpHandler: ((e: MouseEvent) => void) | null = null;
|
||||
|
||||
// Callbacks
|
||||
private onSettingsChange?: () => void;
|
||||
|
||||
@@ -103,6 +121,16 @@ export class AsciiController {
|
||||
this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
|
||||
this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
|
||||
this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
||||
|
||||
// Middle mouse button panning
|
||||
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
this.mouseDragHandler = this.handleMouseDrag.bind(this);
|
||||
this.mouseUpHandler = this.handleMouseUp.bind(this);
|
||||
document.addEventListener('mousemove', this.mouseDragHandler);
|
||||
document.addEventListener('mouseup', this.mouseUpHandler);
|
||||
|
||||
// Prevent context menu on right-click to allow all mouse buttons
|
||||
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
private handleResize(): void {
|
||||
@@ -490,6 +518,83 @@ export class AsciiController {
|
||||
if (this.zoomState.showMagnifier || wasShowing) {
|
||||
this.requestRender('uniforms');
|
||||
}
|
||||
|
||||
// Handle edge panning when zoomed in
|
||||
this.handleEdgePanning(mx, my);
|
||||
}
|
||||
|
||||
private handleEdgePanning(mx: number, my: number): void {
|
||||
// Only pan when zoomed in
|
||||
if (this.zoomState.zoom <= 1.0) {
|
||||
this.stopEdgePanning();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if mouse is near edges
|
||||
const nearLeft = mx < this.EDGE_THRESHOLD && mx >= 0;
|
||||
const nearRight = mx > (1 - this.EDGE_THRESHOLD) && mx <= 1;
|
||||
const nearTop = my < this.EDGE_THRESHOLD && my >= 0;
|
||||
const nearBottom = my > (1 - this.EDGE_THRESHOLD) && my <= 1;
|
||||
|
||||
// Determine pan direction
|
||||
const dirX = nearLeft ? -1 : nearRight ? 1 : 0;
|
||||
const dirY = nearTop ? -1 : nearBottom ? 1 : 0;
|
||||
|
||||
// Start or update panning
|
||||
if (dirX !== 0 || dirY !== 0) {
|
||||
this.edgePanState.directionX = dirX;
|
||||
this.edgePanState.directionY = dirY;
|
||||
if (!this.edgePanState.active) {
|
||||
this.startEdgePanning();
|
||||
}
|
||||
} else {
|
||||
this.stopEdgePanning();
|
||||
}
|
||||
}
|
||||
|
||||
private startEdgePanning(): void {
|
||||
if (this.edgePanState.active) return;
|
||||
|
||||
this.edgePanState.active = true;
|
||||
this.runEdgePanLoop();
|
||||
}
|
||||
|
||||
private runEdgePanLoop(): void {
|
||||
if (!this.edgePanState.active) return;
|
||||
|
||||
// Calculate pan amount based on zoom level (slower when zoomed in more)
|
||||
const panAmount = this.PAN_SPEED / this.zoomState.zoom;
|
||||
|
||||
// Update zoom center
|
||||
let newX = this.zoomState.zoomCenter.x + this.edgePanState.directionX * panAmount;
|
||||
let newY = this.zoomState.zoomCenter.y + this.edgePanState.directionY * panAmount;
|
||||
|
||||
// Clamp zoom center to keep image visible
|
||||
// When zoomed, the visible area is 1/zoom of the total image
|
||||
const visibleRange = 1.0 / this.zoomState.zoom;
|
||||
const minCenter = visibleRange / 2;
|
||||
const maxCenter = 1.0 - visibleRange / 2;
|
||||
|
||||
this.zoomState.zoomCenter.x = Math.max(minCenter, Math.min(maxCenter, newX));
|
||||
this.zoomState.zoomCenter.y = Math.max(minCenter, Math.min(maxCenter, newY));
|
||||
|
||||
this.requestRender('uniforms');
|
||||
|
||||
// Continue panning
|
||||
this.edgePanState.animationId = requestAnimationFrame(() => this.runEdgePanLoop());
|
||||
}
|
||||
|
||||
private stopEdgePanning(): void {
|
||||
if (!this.edgePanState.active) return;
|
||||
|
||||
this.edgePanState.active = false;
|
||||
this.edgePanState.directionX = 0;
|
||||
this.edgePanState.directionY = 0;
|
||||
|
||||
if (this.edgePanState.animationId !== null) {
|
||||
cancelAnimationFrame(this.edgePanState.animationId);
|
||||
this.edgePanState.animationId = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave(): void {
|
||||
@@ -497,6 +602,59 @@ export class AsciiController {
|
||||
this.zoomState.showMagnifier = false;
|
||||
this.requestRender('uniforms');
|
||||
}
|
||||
this.stopEdgePanning();
|
||||
}
|
||||
|
||||
// Middle mouse button panning handlers
|
||||
handleMouseDown(e: MouseEvent): void {
|
||||
// Middle mouse button is button 1
|
||||
if (e.button === 1 && this.zoomState.zoom > 1.0) {
|
||||
e.preventDefault();
|
||||
this.middleMousePanState.isDragging = true;
|
||||
this.middleMousePanState.lastMousePos = { x: e.clientX, y: e.clientY };
|
||||
// Hide magnifier while dragging
|
||||
this.zoomState.showMagnifier = false;
|
||||
// Stop edge panning while manually panning
|
||||
this.stopEdgePanning();
|
||||
// Change cursor to closed hand (grabbing)
|
||||
this.canvas.style.cursor = 'grabbing';
|
||||
this.requestRender('uniforms');
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDrag(e: MouseEvent): void {
|
||||
if (!this.middleMousePanState.isDragging || this.zoomState.zoom <= 1.0) return;
|
||||
|
||||
const curX = e.clientX;
|
||||
const curY = e.clientY;
|
||||
|
||||
// Calculate movement delta in normalized coordinates
|
||||
const dx = (curX - this.middleMousePanState.lastMousePos.x) / this.canvas.width;
|
||||
const dy = (curY - this.middleMousePanState.lastMousePos.y) / this.canvas.height;
|
||||
|
||||
// Move zoom center opposite to drag direction
|
||||
// Speed is inversely proportional to zoom (more zoom = slower pan for same mouse movement)
|
||||
this.zoomState.zoomCenter.x -= dx / this.zoomState.zoom;
|
||||
this.zoomState.zoomCenter.y -= dy / this.zoomState.zoom;
|
||||
|
||||
// Clamp zoom center to keep image visible
|
||||
const visibleRange = 1.0 / this.zoomState.zoom;
|
||||
const minCenter = visibleRange / 2;
|
||||
const maxCenter = 1.0 - visibleRange / 2;
|
||||
|
||||
this.zoomState.zoomCenter.x = Math.max(minCenter, Math.min(maxCenter, this.zoomState.zoomCenter.x));
|
||||
this.zoomState.zoomCenter.y = Math.max(minCenter, Math.min(maxCenter, this.zoomState.zoomCenter.y));
|
||||
|
||||
this.middleMousePanState.lastMousePos = { x: curX, y: curY };
|
||||
this.requestRender('uniforms');
|
||||
}
|
||||
|
||||
handleMouseUp(e: MouseEvent): void {
|
||||
if (e.button === 1) {
|
||||
this.middleMousePanState.isDragging = false;
|
||||
// Reset cursor
|
||||
this.canvas.style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Touch Support
|
||||
@@ -648,6 +806,14 @@ export class AsciiController {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
this.stopEdgePanning();
|
||||
// Clean up middle mouse panning handlers
|
||||
if (this.mouseDragHandler) {
|
||||
document.removeEventListener('mousemove', this.mouseDragHandler);
|
||||
}
|
||||
if (this.mouseUpHandler) {
|
||||
document.removeEventListener('mouseup', this.mouseUpHandler);
|
||||
}
|
||||
this.renderer?.dispose();
|
||||
this.renderer = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user