feat: proper mobile zoom
This commit is contained in:
@ -8,6 +8,7 @@
|
||||
y?: number;
|
||||
zIndex?: number;
|
||||
canvasOffset?: { x: number; y: number };
|
||||
canvasZoom?: number;
|
||||
onMove?: (id: number, x: number, y: number) => void;
|
||||
onClick?: (id: number) => void;
|
||||
onDrop?: (id: number, x: number, y: number) => void;
|
||||
@ -22,6 +23,7 @@
|
||||
y = $bindable(0),
|
||||
zIndex = $bindable(0),
|
||||
canvasOffset = { x: 0, y: 0 },
|
||||
canvasZoom = 1,
|
||||
onMove,
|
||||
onClick,
|
||||
onDrop
|
||||
@ -38,10 +40,11 @@
|
||||
|
||||
isDragging = true;
|
||||
hasMoved = false;
|
||||
const rect = cardElement.getBoundingClientRect();
|
||||
|
||||
// Calculate offset relative to card position in canvas coordinates
|
||||
dragOffset = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
x: (event.clientX - canvasOffset.x) / canvasZoom - x,
|
||||
y: (event.clientY - canvasOffset.y) / canvasZoom - y
|
||||
};
|
||||
dragStartPos = {
|
||||
x: event.clientX,
|
||||
@ -61,8 +64,8 @@
|
||||
hasMoved = true;
|
||||
}
|
||||
|
||||
const newX = event.clientX - dragOffset.x - canvasOffset.x;
|
||||
const newY = event.clientY - dragOffset.y - canvasOffset.y;
|
||||
const newX = (event.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||
const newY = (event.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||
|
||||
x = newX;
|
||||
y = newY;
|
||||
@ -84,6 +87,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart(event: TouchEvent) {
|
||||
if (event.touches.length !== 1) return;
|
||||
event.stopPropagation(); // Prevent canvas panning
|
||||
|
||||
const touch = event.touches[0];
|
||||
isDragging = true;
|
||||
hasMoved = false;
|
||||
|
||||
// Calculate offset relative to card position in canvas coordinates
|
||||
dragOffset = {
|
||||
x: (touch.clientX - canvasOffset.x) / canvasZoom - x,
|
||||
y: (touch.clientY - canvasOffset.y) / canvasZoom - y
|
||||
};
|
||||
dragStartPos = {
|
||||
x: touch.clientX,
|
||||
y: touch.clientY
|
||||
};
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleTouchMove(event: TouchEvent) {
|
||||
if (!isDragging || event.touches.length !== 1) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - dragStartPos.x);
|
||||
const deltaY = Math.abs(touch.clientY - dragStartPos.y);
|
||||
|
||||
if (deltaX > 10 || deltaY > 10) {
|
||||
hasMoved = true;
|
||||
}
|
||||
|
||||
const newX = (touch.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||
const newY = (touch.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||
|
||||
x = newX;
|
||||
y = newY;
|
||||
|
||||
onMove?.(id, newX, newY);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleTouchEnd(event: TouchEvent) {
|
||||
if (isDragging) {
|
||||
onDrop?.(id, x, y);
|
||||
if (!hasMoved) {
|
||||
flipped = !flipped;
|
||||
onClick?.(id);
|
||||
}
|
||||
}
|
||||
isDragging = false;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
@ -104,6 +161,9 @@
|
||||
style="left: {x}px; top: {y}px; z-index: {zIndex};"
|
||||
onmousedown={handleMouseDown}
|
||||
onclick={handleClick}
|
||||
ontouchstart={handleTouchStart}
|
||||
ontouchmove={handleTouchMove}
|
||||
ontouchend={handleTouchEnd}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
|
@ -7,6 +7,7 @@
|
||||
zIndex: number;
|
||||
color: string;
|
||||
canvasOffset?: { x: number; y: number };
|
||||
canvasZoom?: number;
|
||||
onMove?: (id: number, x: number, y: number) => void;
|
||||
onUpdate?: (id: number, text: string) => void;
|
||||
onDelete?: (id: number) => void;
|
||||
@ -20,6 +21,7 @@
|
||||
zIndex = $bindable(0),
|
||||
color,
|
||||
canvasOffset = { x: 0, y: 0 },
|
||||
canvasZoom = 1,
|
||||
onMove,
|
||||
onUpdate,
|
||||
onDelete
|
||||
@ -38,10 +40,11 @@
|
||||
|
||||
isDragging = true;
|
||||
hasMoved = false;
|
||||
const rect = noteElement.getBoundingClientRect();
|
||||
|
||||
// Calculate offset relative to note position in canvas coordinates
|
||||
dragOffset = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
x: (event.clientX - canvasOffset.x) / canvasZoom - x,
|
||||
y: (event.clientY - canvasOffset.y) / canvasZoom - y
|
||||
};
|
||||
dragStartPos = {
|
||||
x: event.clientX,
|
||||
@ -61,8 +64,8 @@
|
||||
hasMoved = true;
|
||||
}
|
||||
|
||||
const newX = event.clientX - dragOffset.x - canvasOffset.x;
|
||||
const newY = event.clientY - dragOffset.y - canvasOffset.y;
|
||||
const newX = (event.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||
const newY = (event.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||
|
||||
x = newX;
|
||||
y = newY;
|
||||
@ -102,6 +105,59 @@
|
||||
onDelete?.(id);
|
||||
}
|
||||
|
||||
function handleTouchStart(event: TouchEvent) {
|
||||
if (event.touches.length !== 1) return;
|
||||
event.stopPropagation(); // Prevent canvas panning
|
||||
|
||||
const touch = event.touches[0];
|
||||
isDragging = true;
|
||||
hasMoved = false;
|
||||
|
||||
// Calculate offset relative to note position in canvas coordinates
|
||||
dragOffset = {
|
||||
x: (touch.clientX - canvasOffset.x) / canvasZoom - x,
|
||||
y: (touch.clientY - canvasOffset.y) / canvasZoom - y
|
||||
};
|
||||
dragStartPos = {
|
||||
x: touch.clientX,
|
||||
y: touch.clientY
|
||||
};
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleTouchMove(event: TouchEvent) {
|
||||
if (!isDragging || event.touches.length !== 1) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - dragStartPos.x);
|
||||
const deltaY = Math.abs(touch.clientY - dragStartPos.y);
|
||||
|
||||
if (deltaX > 5 || deltaY > 5) {
|
||||
hasMoved = true;
|
||||
}
|
||||
|
||||
const newX = (touch.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||
const newY = (touch.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||
|
||||
x = newX;
|
||||
y = newY;
|
||||
|
||||
onMove?.(id, newX, newY);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleTouchEnd(event: TouchEvent) {
|
||||
if (isDragging) {
|
||||
if (!hasMoved) {
|
||||
isEditing = true;
|
||||
setTimeout(() => textareaElement?.focus(), 0);
|
||||
}
|
||||
}
|
||||
isDragging = false;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
@ -122,6 +178,9 @@
|
||||
style="left: {x}px; top: {y}px; z-index: {zIndex}; background-color: {color};"
|
||||
onmousedown={handleMouseDown}
|
||||
onclick={handleClick}
|
||||
ontouchstart={handleTouchStart}
|
||||
ontouchmove={handleTouchMove}
|
||||
ontouchend={handleTouchEnd}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
|
@ -101,9 +101,13 @@
|
||||
|
||||
let deckPosition = $state({ x: 50, y: 300 });
|
||||
let canvasOffset = $state({ x: 0, y: 0 });
|
||||
let canvasZoom = $state(1);
|
||||
let isDragging = $state(false);
|
||||
let dragStart = $state({ x: 0, y: 0 });
|
||||
let dragStartOffset = $state({ x: 0, y: 0 });
|
||||
let isPinching = $state(false);
|
||||
let lastPinchDistance = $state(0);
|
||||
let pinchCenter = $state({ x: 0, y: 0 });
|
||||
|
||||
let cards = $state<CardData[]>([]);
|
||||
let maxZIndex = $state(0);
|
||||
@ -304,13 +308,37 @@
|
||||
|
||||
function handleCanvasWheel(event: WheelEvent) {
|
||||
event.preventDefault();
|
||||
const scrollSpeed = 1;
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
// Zoom with mouse wheel + ctrl/cmd towards viewport center
|
||||
const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newZoom = Math.max(0.1, Math.min(3, canvasZoom * zoomFactor));
|
||||
|
||||
// Get viewport center
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const centerX = rect.width / 2;
|
||||
const centerY = rect.height / 2;
|
||||
|
||||
// Convert viewport center to canvas coordinates before zoom
|
||||
const canvasCenterX = (centerX - canvasOffset.x) / canvasZoom;
|
||||
const canvasCenterY = (centerY - canvasOffset.y) / canvasZoom;
|
||||
|
||||
// Update zoom
|
||||
canvasZoom = newZoom;
|
||||
|
||||
// Adjust offset to keep the viewport center fixed
|
||||
canvasOffset.x = centerX - canvasCenterX * canvasZoom;
|
||||
canvasOffset.y = centerY - canvasCenterY * canvasZoom;
|
||||
} else {
|
||||
// Pan with mouse wheel
|
||||
const scrollSpeed = 1 / canvasZoom; // Adjust pan speed for zoom level
|
||||
canvasOffset.x -= event.deltaX * scrollSpeed;
|
||||
canvasOffset.y -= event.deltaY * scrollSpeed;
|
||||
|
||||
canvasOffset.x = Math.max(-2000, Math.min(2000, canvasOffset.x));
|
||||
canvasOffset.y = Math.max(-2000, Math.min(2000, canvasOffset.y));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCanvasMouseDown(event: MouseEvent) {
|
||||
if (event.button === 1 || event.button === 2) {
|
||||
@ -344,6 +372,96 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function getTouchDistance(touches: TouchList) {
|
||||
if (touches.length < 2) return 0;
|
||||
const touch1 = touches[0];
|
||||
const touch2 = touches[1];
|
||||
return Math.sqrt(
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function getTouchCenter(touches: TouchList) {
|
||||
if (touches.length < 2) return { x: 0, y: 0 };
|
||||
const touch1 = touches[0];
|
||||
const touch2 = touches[1];
|
||||
return {
|
||||
x: (touch1.clientX + touch2.clientX) / 2,
|
||||
y: (touch1.clientY + touch2.clientY) / 2
|
||||
};
|
||||
}
|
||||
|
||||
function handleCanvasTouchStart(event: TouchEvent) {
|
||||
isDragging = true;
|
||||
if (event.touches.length >= 2) {
|
||||
// Two or more touches - pinch zoom
|
||||
isPinching = true;
|
||||
lastPinchDistance = getTouchDistance(event.touches);
|
||||
pinchCenter = getTouchCenter(event.touches);
|
||||
|
||||
// Convert pinch center to canvas coordinates
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
pinchCenter.x -= rect.left;
|
||||
pinchCenter.y -= rect.top;
|
||||
} else if (event.touches.length === 1) {
|
||||
// Single touch - pan
|
||||
const touch = event.touches[0];
|
||||
dragStart.x = touch.clientX;
|
||||
dragStart.y = touch.clientY;
|
||||
dragStartOffset.x = canvasOffset.x;
|
||||
dragStartOffset.y = canvasOffset.y;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleCanvasTouchMove(event: TouchEvent) {
|
||||
if (event.touches.length === 1 && isDragging && !isPinching) {
|
||||
// Single touch pan
|
||||
const touch = event.touches[0];
|
||||
const deltaX = touch.clientX - dragStart.x;
|
||||
const deltaY = touch.clientY - dragStart.y;
|
||||
|
||||
canvasOffset.x = Math.max(-2000, Math.min(2000, dragStartOffset.x + deltaX));
|
||||
canvasOffset.y = Math.max(-2000, Math.min(2000, dragStartOffset.y + deltaY));
|
||||
} else if (event.touches.length === 2 && isPinching) {
|
||||
// Pinch zoom
|
||||
const currentDistance = getTouchDistance(event.touches);
|
||||
const zoomFactor = currentDistance / lastPinchDistance;
|
||||
const newZoom = Math.max(0.1, Math.min(3, canvasZoom * zoomFactor));
|
||||
|
||||
// Convert pinch center to canvas coordinates before zoom
|
||||
const canvasCenterX = (pinchCenter.x - canvasOffset.x) / canvasZoom;
|
||||
const canvasCenterY = (pinchCenter.y - canvasOffset.y) / canvasZoom;
|
||||
|
||||
// Update zoom
|
||||
canvasZoom = newZoom;
|
||||
|
||||
// Adjust offset to keep the pinch center fixed
|
||||
canvasOffset.x = pinchCenter.x - canvasCenterX * canvasZoom;
|
||||
canvasOffset.y = pinchCenter.y - canvasCenterY * canvasZoom;
|
||||
|
||||
lastPinchDistance = currentDistance;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleCanvasTouchEnd(event: TouchEvent) {
|
||||
if (event.touches.length === 0) {
|
||||
isDragging = false;
|
||||
isPinching = false;
|
||||
} else if (event.touches.length === 1) {
|
||||
isPinching = false;
|
||||
// A pinch ended. Reset pan state for the remaining finger to prevent a jump.
|
||||
const touch = event.touches[0];
|
||||
dragStart.x = touch.clientX;
|
||||
dragStart.y = touch.clientY;
|
||||
dragStartOffset.x = canvasOffset.x;
|
||||
dragStartOffset.y = canvasOffset.y;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function addStickyNote() {
|
||||
const randomColor = noteColors[Math.floor(Math.random() * noteColors.length)];
|
||||
const newNote: StickyNote = {
|
||||
@ -387,6 +505,7 @@
|
||||
cards: cards.map(card => ({...card})),
|
||||
stickyNotes: stickyNotes.map(note => ({...note})),
|
||||
canvasOffset,
|
||||
canvasZoom,
|
||||
deckPosition,
|
||||
maxZIndex,
|
||||
nextNoteId,
|
||||
@ -431,6 +550,7 @@
|
||||
cards = canvasState.cards || [];
|
||||
stickyNotes = canvasState.stickyNotes || [];
|
||||
canvasOffset = canvasState.canvasOffset || { x: 0, y: 0 };
|
||||
canvasZoom = canvasState.canvasZoom || 1;
|
||||
deckPosition = canvasState.deckPosition || { x: 50, y: 300 };
|
||||
maxZIndex = canvasState.maxZIndex || cards.length;
|
||||
nextNoteId = canvasState.nextNoteId || 1;
|
||||
@ -485,16 +605,20 @@
|
||||
onmousemove={handleCanvasMouseMove}
|
||||
onmouseup={handleCanvasMouseUp}
|
||||
oncontextmenu={handleCanvasContextMenu}
|
||||
ontouchstart={handleCanvasTouchStart}
|
||||
ontouchmove={handleCanvasTouchMove}
|
||||
ontouchend={handleCanvasTouchEnd}
|
||||
>
|
||||
<div
|
||||
class="canvas-content"
|
||||
style="transform: translate({canvasOffset.x}px, {canvasOffset.y}px);"
|
||||
style="transform: translate({canvasOffset.x}px, {canvasOffset.y}px) scale({canvasZoom});"
|
||||
>
|
||||
{#each cards as card (card.id)}
|
||||
<Card
|
||||
{...card}
|
||||
backImage={card.isRiskCategory ? 'risks/risikocat_back.png' : backImage}
|
||||
canvasOffset={canvasOffset}
|
||||
canvasZoom={canvasZoom}
|
||||
bind:flipped={card.flipped}
|
||||
bind:x={card.x}
|
||||
bind:y={card.y}
|
||||
@ -509,6 +633,7 @@
|
||||
<StickyNote
|
||||
{...note}
|
||||
canvasOffset={canvasOffset}
|
||||
canvasZoom={canvasZoom}
|
||||
bind:text={note.text}
|
||||
bind:x={note.x}
|
||||
bind:y={note.y}
|
||||
@ -597,18 +722,40 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.game-area:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
gap: 5px;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-content {
|
||||
position: relative;
|
||||
width: 500vw;
|
||||
height: 500vh;
|
||||
min-width: 500vw;
|
||||
min-height: 500vh;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.deck-area {
|
||||
|
Reference in New Issue
Block a user