diff --git a/web/LED-Wall-Website/src/components/Canvas.tsx b/web/LED-Wall-Website/src/components/Canvas.tsx index 4d52da1..20a8028 100644 --- a/web/LED-Wall-Website/src/components/Canvas.tsx +++ b/web/LED-Wall-Website/src/components/Canvas.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useRef } from "react"; import styles from "../Styles.module.css"; import uploadFile from "./Upload.tsx"; import { useDispatch, useSelector } from "react-redux"; @@ -11,33 +11,67 @@ type Props = { sizeMultiplier: number; }; -//Canvas that holds all elements and allows drag and drop of image files onto it +const ZOOM_IN_SCALE = 2.5; + function Canvas(props: Props) { const dispatch = useDispatch(); const configState = useSelector((state: RootState) => state.config); - //Sets the file and calls upload with the current mouse location + const [scale, setScale] = useState(1); + const [pan, setPan] = useState({ x: 0, y: 0 }); + const isPanning = useRef(false); + const panStart = useRef({ x: 0, y: 0 }); + const panOrigin = useRef({ x: 0, y: 0 }); + + const isZoomedIn = scale > 1; + + function handleZoomIn() { + setScale(ZOOM_IN_SCALE); + setPan({ x: 0, y: 0 }); + } + + function handleZoomOut() { + setScale(1); + setPan({ x: 0, y: 0 }); + } + + // Only pan when the user clicks directly on the canvas background, + // not when clicking on a child element (image/text). + function handleMouseDown(e: React.MouseEvent) { + if (!isZoomedIn) return; + // e.target is the exact element clicked; e.currentTarget is this div. + // If they differ, the click landed on a child (an Element), so don't pan. + if (e.target !== e.currentTarget) return; + if (e.button !== 0) return; + isPanning.current = true; + panStart.current = { x: e.clientX, y: e.clientY }; + panOrigin.current = { x: pan.x, y: pan.y }; + } + + function handleMouseMove(e: React.MouseEvent) { + if (!isPanning.current) return; + const dx = e.clientX - panStart.current.x; + const dy = e.clientY - panStart.current.y; + setPan({ x: panOrigin.current.x + dx, y: panOrigin.current.y + dy }); + } + + function handleMouseUp() { + isPanning.current = false; + } + + function handleMouseLeave() { + isPanning.current = false; + } + async function handleDrop(e: React.DragEvent) { e.preventDefault(); - if (!e.dataTransfer.files || e.dataTransfer.files.length === 0) { - return; - } - //Gets the canvas position + if (!e.dataTransfer.files || e.dataTransfer.files.length === 0) return; const canvasRect = e.currentTarget.getBoundingClientRect(); - //Calculates position relative to canvas - let relativeX = e.clientX - canvasRect.left; - relativeX = Math.trunc(relativeX / props.sizeMultiplier); - let relativeY = e.clientY - canvasRect.top; - relativeY = Math.trunc(relativeY / props.sizeMultiplier); - uploadFile( - [relativeX, relativeY], - e.dataTransfer.files[0], - dispatch, - configState - ); + const relativeX = Math.trunc((e.clientX - canvasRect.left) / props.sizeMultiplier); + const relativeY = Math.trunc((e.clientY - canvasRect.top) / props.sizeMultiplier); + uploadFile([relativeX, relativeY], e.dataTransfer.files[0], dispatch, configState); } - //Prevents browser not allowing dragging the image function handleDragOver(e: React.DragEvent) { e.preventDefault(); } @@ -75,18 +109,96 @@ function Canvas(props: Props) { } return ( + // Outer viewport — fixed size, clips overflow, holds border
handleDrop(e)} - onDragOver={(e) => handleDragOver(e)} style={{ - cursor: "grab", + position: "fixed", + top: "53%", + left: "50%", + transform: "translate(-50%, -50%)", width: props.canvasDimensions[0], height: props.canvasDimensions[1], + border: "4px solid rgb(29, 41, 58)", + overflow: "hidden", }} > - {configState.elements.map(createJSXElement)} + {/* Zoom buttons — sit above the canvas, stopPropagation so they don't trigger pan */} + {props.canvasDimensions[1] > 0 && ( +
e.stopPropagation()} + > + + + {isZoomedIn ? `Zoom: ${ZOOM_IN_SCALE}×` : "Zoom: 1×"} + + +
+ )} + + {/* Inner canvas — receives scale+pan transform, listens for background pan drags */} +
+ {configState.elements.map(createJSXElement)} +
); } -export default Canvas; + +export default Canvas; \ No newline at end of file