feat: proper mobile zoom
This commit is contained in:
		@ -8,6 +8,7 @@
 | 
				
			|||||||
		y?: number;
 | 
							y?: number;
 | 
				
			||||||
		zIndex?: number;
 | 
							zIndex?: number;
 | 
				
			||||||
		canvasOffset?: { x: number; y: number };
 | 
							canvasOffset?: { x: number; y: number };
 | 
				
			||||||
 | 
							canvasZoom?: number;
 | 
				
			||||||
		onMove?: (id: number, x: number, y: number) => void;
 | 
							onMove?: (id: number, x: number, y: number) => void;
 | 
				
			||||||
		onClick?: (id: number) => void;
 | 
							onClick?: (id: number) => void;
 | 
				
			||||||
		onDrop?: (id: number, x: number, y: number) => void;
 | 
							onDrop?: (id: number, x: number, y: number) => void;
 | 
				
			||||||
@ -22,6 +23,7 @@
 | 
				
			|||||||
		y = $bindable(0),
 | 
							y = $bindable(0),
 | 
				
			||||||
		zIndex = $bindable(0),
 | 
							zIndex = $bindable(0),
 | 
				
			||||||
		canvasOffset = { x: 0, y: 0 },
 | 
							canvasOffset = { x: 0, y: 0 },
 | 
				
			||||||
 | 
							canvasZoom = 1,
 | 
				
			||||||
		onMove,
 | 
							onMove,
 | 
				
			||||||
		onClick,
 | 
							onClick,
 | 
				
			||||||
		onDrop
 | 
							onDrop
 | 
				
			||||||
@ -38,10 +40,11 @@
 | 
				
			|||||||
		
 | 
							
 | 
				
			||||||
		isDragging = true;
 | 
							isDragging = true;
 | 
				
			||||||
		hasMoved = false;
 | 
							hasMoved = false;
 | 
				
			||||||
		const rect = cardElement.getBoundingClientRect();
 | 
							
 | 
				
			||||||
 | 
							// Calculate offset relative to card position in canvas coordinates
 | 
				
			||||||
		dragOffset = {
 | 
							dragOffset = {
 | 
				
			||||||
			x: event.clientX - rect.left,
 | 
								x: (event.clientX - canvasOffset.x) / canvasZoom - x,
 | 
				
			||||||
			y: event.clientY - rect.top
 | 
								y: (event.clientY - canvasOffset.y) / canvasZoom - y
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		dragStartPos = {
 | 
							dragStartPos = {
 | 
				
			||||||
			x: event.clientX,
 | 
								x: event.clientX,
 | 
				
			||||||
@ -61,8 +64,8 @@
 | 
				
			|||||||
			hasMoved = true;
 | 
								hasMoved = true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		const newX = event.clientX - dragOffset.x - canvasOffset.x;
 | 
							const newX = (event.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
 | 
				
			||||||
		const newY = event.clientY - dragOffset.y - canvasOffset.y;
 | 
							const newY = (event.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		x = newX;
 | 
							x = newX;
 | 
				
			||||||
		y = newY;
 | 
							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(() => {
 | 
						$effect(() => {
 | 
				
			||||||
		if (isDragging) {
 | 
							if (isDragging) {
 | 
				
			||||||
			document.addEventListener('mousemove', handleMouseMove);
 | 
								document.addEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
@ -104,6 +161,9 @@
 | 
				
			|||||||
	style="left: {x}px; top: {y}px; z-index: {zIndex};"
 | 
						style="left: {x}px; top: {y}px; z-index: {zIndex};"
 | 
				
			||||||
	onmousedown={handleMouseDown}
 | 
						onmousedown={handleMouseDown}
 | 
				
			||||||
	onclick={handleClick}
 | 
						onclick={handleClick}
 | 
				
			||||||
 | 
						ontouchstart={handleTouchStart}
 | 
				
			||||||
 | 
						ontouchmove={handleTouchMove}
 | 
				
			||||||
 | 
						ontouchend={handleTouchEnd}
 | 
				
			||||||
	role="button"
 | 
						role="button"
 | 
				
			||||||
	tabindex="0"
 | 
						tabindex="0"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@
 | 
				
			|||||||
		zIndex: number;
 | 
							zIndex: number;
 | 
				
			||||||
		color: string;
 | 
							color: string;
 | 
				
			||||||
		canvasOffset?: { x: number; y: number };
 | 
							canvasOffset?: { x: number; y: number };
 | 
				
			||||||
 | 
							canvasZoom?: number;
 | 
				
			||||||
		onMove?: (id: number, x: number, y: number) => void;
 | 
							onMove?: (id: number, x: number, y: number) => void;
 | 
				
			||||||
		onUpdate?: (id: number, text: string) => void;
 | 
							onUpdate?: (id: number, text: string) => void;
 | 
				
			||||||
		onDelete?: (id: number) => void;
 | 
							onDelete?: (id: number) => void;
 | 
				
			||||||
@ -20,6 +21,7 @@
 | 
				
			|||||||
		zIndex = $bindable(0),
 | 
							zIndex = $bindable(0),
 | 
				
			||||||
		color,
 | 
							color,
 | 
				
			||||||
		canvasOffset = { x: 0, y: 0 },
 | 
							canvasOffset = { x: 0, y: 0 },
 | 
				
			||||||
 | 
							canvasZoom = 1,
 | 
				
			||||||
		onMove,
 | 
							onMove,
 | 
				
			||||||
		onUpdate,
 | 
							onUpdate,
 | 
				
			||||||
		onDelete
 | 
							onDelete
 | 
				
			||||||
@ -38,10 +40,11 @@
 | 
				
			|||||||
		
 | 
							
 | 
				
			||||||
		isDragging = true;
 | 
							isDragging = true;
 | 
				
			||||||
		hasMoved = false;
 | 
							hasMoved = false;
 | 
				
			||||||
		const rect = noteElement.getBoundingClientRect();
 | 
							
 | 
				
			||||||
 | 
							// Calculate offset relative to note position in canvas coordinates
 | 
				
			||||||
		dragOffset = {
 | 
							dragOffset = {
 | 
				
			||||||
			x: event.clientX - rect.left,
 | 
								x: (event.clientX - canvasOffset.x) / canvasZoom - x,
 | 
				
			||||||
			y: event.clientY - rect.top
 | 
								y: (event.clientY - canvasOffset.y) / canvasZoom - y
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		dragStartPos = {
 | 
							dragStartPos = {
 | 
				
			||||||
			x: event.clientX,
 | 
								x: event.clientX,
 | 
				
			||||||
@ -61,8 +64,8 @@
 | 
				
			|||||||
			hasMoved = true;
 | 
								hasMoved = true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		const newX = event.clientX - dragOffset.x - canvasOffset.x;
 | 
							const newX = (event.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
 | 
				
			||||||
		const newY = event.clientY - dragOffset.y - canvasOffset.y;
 | 
							const newY = (event.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		x = newX;
 | 
							x = newX;
 | 
				
			||||||
		y = newY;
 | 
							y = newY;
 | 
				
			||||||
@ -102,6 +105,59 @@
 | 
				
			|||||||
		onDelete?.(id);
 | 
							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(() => {
 | 
						$effect(() => {
 | 
				
			||||||
		if (isDragging) {
 | 
							if (isDragging) {
 | 
				
			||||||
			document.addEventListener('mousemove', handleMouseMove);
 | 
								document.addEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
@ -122,6 +178,9 @@
 | 
				
			|||||||
	style="left: {x}px; top: {y}px; z-index: {zIndex}; background-color: {color};"
 | 
						style="left: {x}px; top: {y}px; z-index: {zIndex}; background-color: {color};"
 | 
				
			||||||
	onmousedown={handleMouseDown}
 | 
						onmousedown={handleMouseDown}
 | 
				
			||||||
	onclick={handleClick}
 | 
						onclick={handleClick}
 | 
				
			||||||
 | 
						ontouchstart={handleTouchStart}
 | 
				
			||||||
 | 
						ontouchmove={handleTouchMove}
 | 
				
			||||||
 | 
						ontouchend={handleTouchEnd}
 | 
				
			||||||
	role="button"
 | 
						role="button"
 | 
				
			||||||
	tabindex="0"
 | 
						tabindex="0"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
 | 
				
			|||||||
@ -101,9 +101,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	let deckPosition = $state({ x: 50, y: 300 });
 | 
						let deckPosition = $state({ x: 50, y: 300 });
 | 
				
			||||||
	let canvasOffset = $state({ x: 0, y: 0 });
 | 
						let canvasOffset = $state({ x: 0, y: 0 });
 | 
				
			||||||
 | 
						let canvasZoom = $state(1);
 | 
				
			||||||
	let isDragging = $state(false);
 | 
						let isDragging = $state(false);
 | 
				
			||||||
	let dragStart = $state({ x: 0, y: 0 });
 | 
						let dragStart = $state({ x: 0, y: 0 });
 | 
				
			||||||
	let dragStartOffset = $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 cards = $state<CardData[]>([]);
 | 
				
			||||||
	let maxZIndex = $state(0);
 | 
						let maxZIndex = $state(0);
 | 
				
			||||||
@ -304,13 +308,37 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	function handleCanvasWheel(event: WheelEvent) {
 | 
						function handleCanvasWheel(event: WheelEvent) {
 | 
				
			||||||
		event.preventDefault();
 | 
							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.x -= event.deltaX * scrollSpeed;
 | 
				
			||||||
			canvasOffset.y -= event.deltaY * scrollSpeed;
 | 
								canvasOffset.y -= event.deltaY * scrollSpeed;
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			canvasOffset.x = Math.max(-2000, Math.min(2000, canvasOffset.x));
 | 
								canvasOffset.x = Math.max(-2000, Math.min(2000, canvasOffset.x));
 | 
				
			||||||
			canvasOffset.y = Math.max(-2000, Math.min(2000, canvasOffset.y));
 | 
								canvasOffset.y = Math.max(-2000, Math.min(2000, canvasOffset.y));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function handleCanvasMouseDown(event: MouseEvent) {
 | 
						function handleCanvasMouseDown(event: MouseEvent) {
 | 
				
			||||||
		if (event.button === 1 || event.button === 2) {
 | 
							if (event.button === 1 || event.button === 2) {
 | 
				
			||||||
@ -344,6 +372,96 @@
 | 
				
			|||||||
		event.preventDefault();
 | 
							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() {
 | 
						function addStickyNote() {
 | 
				
			||||||
		const randomColor = noteColors[Math.floor(Math.random() * noteColors.length)];
 | 
							const randomColor = noteColors[Math.floor(Math.random() * noteColors.length)];
 | 
				
			||||||
		const newNote: StickyNote = {
 | 
							const newNote: StickyNote = {
 | 
				
			||||||
@ -387,6 +505,7 @@
 | 
				
			|||||||
			cards: cards.map(card => ({...card})),
 | 
								cards: cards.map(card => ({...card})),
 | 
				
			||||||
			stickyNotes: stickyNotes.map(note => ({...note})),
 | 
								stickyNotes: stickyNotes.map(note => ({...note})),
 | 
				
			||||||
			canvasOffset,
 | 
								canvasOffset,
 | 
				
			||||||
 | 
								canvasZoom,
 | 
				
			||||||
			deckPosition,
 | 
								deckPosition,
 | 
				
			||||||
			maxZIndex,
 | 
								maxZIndex,
 | 
				
			||||||
			nextNoteId,
 | 
								nextNoteId,
 | 
				
			||||||
@ -431,6 +550,7 @@
 | 
				
			|||||||
					cards = canvasState.cards || [];
 | 
										cards = canvasState.cards || [];
 | 
				
			||||||
					stickyNotes = canvasState.stickyNotes || [];
 | 
										stickyNotes = canvasState.stickyNotes || [];
 | 
				
			||||||
					canvasOffset = canvasState.canvasOffset || { x: 0, y: 0 };
 | 
										canvasOffset = canvasState.canvasOffset || { x: 0, y: 0 };
 | 
				
			||||||
 | 
										canvasZoom = canvasState.canvasZoom || 1;
 | 
				
			||||||
					deckPosition = canvasState.deckPosition || { x: 50, y: 300 };
 | 
										deckPosition = canvasState.deckPosition || { x: 50, y: 300 };
 | 
				
			||||||
					maxZIndex = canvasState.maxZIndex || cards.length;
 | 
										maxZIndex = canvasState.maxZIndex || cards.length;
 | 
				
			||||||
					nextNoteId = canvasState.nextNoteId || 1;
 | 
										nextNoteId = canvasState.nextNoteId || 1;
 | 
				
			||||||
@ -485,16 +605,20 @@
 | 
				
			|||||||
		onmousemove={handleCanvasMouseMove}
 | 
							onmousemove={handleCanvasMouseMove}
 | 
				
			||||||
		onmouseup={handleCanvasMouseUp}
 | 
							onmouseup={handleCanvasMouseUp}
 | 
				
			||||||
		oncontextmenu={handleCanvasContextMenu}
 | 
							oncontextmenu={handleCanvasContextMenu}
 | 
				
			||||||
 | 
							ontouchstart={handleCanvasTouchStart}
 | 
				
			||||||
 | 
							ontouchmove={handleCanvasTouchMove}
 | 
				
			||||||
 | 
							ontouchend={handleCanvasTouchEnd}
 | 
				
			||||||
	>
 | 
						>
 | 
				
			||||||
		<div 
 | 
							<div 
 | 
				
			||||||
			class="canvas-content" 
 | 
								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)}
 | 
								{#each cards as card (card.id)}
 | 
				
			||||||
				<Card
 | 
									<Card
 | 
				
			||||||
					{...card}
 | 
										{...card}
 | 
				
			||||||
					backImage={card.isRiskCategory ? 'risks/risikocat_back.png' : backImage}
 | 
										backImage={card.isRiskCategory ? 'risks/risikocat_back.png' : backImage}
 | 
				
			||||||
					canvasOffset={canvasOffset}
 | 
										canvasOffset={canvasOffset}
 | 
				
			||||||
 | 
										canvasZoom={canvasZoom}
 | 
				
			||||||
					bind:flipped={card.flipped}
 | 
										bind:flipped={card.flipped}
 | 
				
			||||||
					bind:x={card.x}
 | 
										bind:x={card.x}
 | 
				
			||||||
					bind:y={card.y}
 | 
										bind:y={card.y}
 | 
				
			||||||
@ -509,6 +633,7 @@
 | 
				
			|||||||
				<StickyNote
 | 
									<StickyNote
 | 
				
			||||||
					{...note}
 | 
										{...note}
 | 
				
			||||||
					canvasOffset={canvasOffset}
 | 
										canvasOffset={canvasOffset}
 | 
				
			||||||
 | 
										canvasZoom={canvasZoom}
 | 
				
			||||||
					bind:text={note.text}
 | 
										bind:text={note.text}
 | 
				
			||||||
					bind:x={note.x}
 | 
										bind:x={note.x}
 | 
				
			||||||
					bind:y={note.y}
 | 
										bind:y={note.y}
 | 
				
			||||||
@ -597,18 +722,40 @@
 | 
				
			|||||||
		height: 100%;
 | 
							height: 100%;
 | 
				
			||||||
		overflow: hidden;
 | 
							overflow: hidden;
 | 
				
			||||||
		cursor: grab;
 | 
							cursor: grab;
 | 
				
			||||||
 | 
							touch-action: none;
 | 
				
			||||||
 | 
							user-select: none;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.game-area:active {
 | 
						.game-area:active {
 | 
				
			||||||
		cursor: grabbing;
 | 
							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 {
 | 
						.canvas-content {
 | 
				
			||||||
		position: relative;
 | 
							position: relative;
 | 
				
			||||||
		width: 500vw;
 | 
							width: 500vw;
 | 
				
			||||||
		height: 500vh;
 | 
							height: 500vh;
 | 
				
			||||||
		min-width: 500vw;
 | 
							min-width: 500vw;
 | 
				
			||||||
		min-height: 500vh;
 | 
							min-height: 500vh;
 | 
				
			||||||
 | 
							transform-origin: 0 0;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.deck-area {
 | 
						.deck-area {
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user