From 18494d1234eb121dd59fe8c3d5bf44932b8ada61 Mon Sep 17 00:00:00 2001 From: KirillSerg Date: Thu, 6 Jun 2024 22:56:35 +0300 Subject: [PATCH 1/9] feat: added zoom component with simple zoom logic --- src/App.tsx | 2 ++ src/components/Canvas.tsx | 4 +++- src/components/Zoom.tsx | 40 +++++++++++++++++++++++++++++++++++++++ src/store/store.ts | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/components/Zoom.tsx diff --git a/src/App.tsx b/src/App.tsx index 77dc439..0bf88ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import Canvas from './components/Canvas'; import Inspector from './components/Inspector'; import Layers from './components/Layers'; import Toolbar from './components/Toolbar'; +import Zoom from './components/Zoom'; const App = () => { return ( @@ -10,6 +11,7 @@ const App = () => { + ); }; diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 628a8ab..cdea85f 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -10,6 +10,7 @@ import { isDraggingAtom, isDrawingAtom, onKeyPressAtom, + zoomSizeAtom, } from '../store/store'; import { ElemenEvent } from '../types/CommonTypes'; import { transformCoordinates } from '../assets/utilities'; @@ -22,6 +23,7 @@ const Canvas = () => { const [, onMouseDown] = useAtom(onMouseDownAtom); const [, onMouseMove] = useAtom(onMouseMoveAtom); const [, onKeyPress] = useAtom(onKeyPressAtom); + const zoomSize = useAtomValue(zoomSizeAtom); const svgContainerRef = useRef(null); @@ -58,7 +60,7 @@ const Canvas = () => { onMouseUp={onMouseUp} onKeyDown={(e) => onKeyPress(e.key)} preserveAspectRatio="xMinYMin meet" //for the SVG container to be on the entire screen, while the elements inside kept the proportions and x=0, y=0 viewBox started from the upper left corner - viewBox="0 0 1920 1080" + viewBox={`0 0 ${zoomSize.width} ${zoomSize.height}`} width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" diff --git a/src/components/Zoom.tsx b/src/components/Zoom.tsx new file mode 100644 index 0000000..9de1f6a --- /dev/null +++ b/src/components/Zoom.tsx @@ -0,0 +1,40 @@ +import { useAtom } from 'jotai'; +import { zoomSizeAtom } from '../store/store'; + +const Zoom = () => { + const [zoomSize, setZoomSize] = useAtom(zoomSizeAtom); + return ( +
+ + {`${zoomSize.percentage} %`} + +
+ ); +}; + +export default Zoom; diff --git a/src/store/store.ts b/src/store/store.ts index b84a0e1..4c41e7f 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -37,6 +37,7 @@ export const isDraggingAtom = atom(false) export const isDrawingAtom = atom( (get) => get(initialElementAtom).type === "free" ? false : true ) +export const zoomSizeAtom = atom<{ percentage: number, width: number, height: number }>({ percentage: 100, width: 1920, height: 1080 }) export const updateElementsAtom = atom( null, From ee479f0ef29fb61fc12d44045b098e74cab1b96e Mon Sep 17 00:00:00 2001 From: KirillSerg Date: Sat, 8 Jun 2024 11:56:16 +0300 Subject: [PATCH 2/9] refact: zoom component --- src/App.tsx | 2 +- src/components/Canvas.tsx | 7 +-- src/components/EllipseIconBtn.tsx | 7 ++- src/components/FreeIconBtn.tsx | 2 +- src/components/LineArrowIconBtn.tsx | 7 ++- src/components/LineIconBtn.tsx | 1 + src/components/PencilIconBtn.tsx | 1 + src/components/RectIconBtn.tsx | 11 +++-- src/components/TextIconBtn.tsx | 8 +++- src/components/Toolbar.tsx | 18 ++++---- src/components/TriangleIconBtn.tsx | 7 ++- src/components/Zoom.tsx | 67 +++++++++++++++-------------- src/store/store.ts | 46 ++++++++++++++++++-- src/types/CommonTypes.ts | 18 ++++++++ 14 files changed, 146 insertions(+), 56 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0bf88ab..fb0dd44 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import Zoom from './components/Zoom'; const App = () => { return ( -
+
diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index cdea85f..964ba10 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -10,7 +10,7 @@ import { isDraggingAtom, isDrawingAtom, onKeyPressAtom, - zoomSizeAtom, + canvasViewBoxAtom, } from '../store/store'; import { ElemenEvent } from '../types/CommonTypes'; import { transformCoordinates } from '../assets/utilities'; @@ -23,7 +23,7 @@ const Canvas = () => { const [, onMouseDown] = useAtom(onMouseDownAtom); const [, onMouseMove] = useAtom(onMouseMoveAtom); const [, onKeyPress] = useAtom(onKeyPressAtom); - const zoomSize = useAtomValue(zoomSizeAtom); + const zoomSize = useAtomValue(canvasViewBoxAtom); const svgContainerRef = useRef(null); @@ -53,6 +53,7 @@ const Canvas = () => { return ( handleMouseDown(e)} @@ -60,7 +61,7 @@ const Canvas = () => { onMouseUp={onMouseUp} onKeyDown={(e) => onKeyPress(e.key)} preserveAspectRatio="xMinYMin meet" //for the SVG container to be on the entire screen, while the elements inside kept the proportions and x=0, y=0 viewBox started from the upper left corner - viewBox={`0 0 ${zoomSize.width} ${zoomSize.height}`} + viewBox={`${zoomSize.x} ${zoomSize.y} ${zoomSize.width} ${zoomSize.height}`} width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" diff --git a/src/components/EllipseIconBtn.tsx b/src/components/EllipseIconBtn.tsx index 2278825..ed0aed1 100644 --- a/src/components/EllipseIconBtn.tsx +++ b/src/components/EllipseIconBtn.tsx @@ -8,7 +8,12 @@ interface Props { const EllipseIconBtn = ({ className, handlerClick }: Props) => { return (
- - {`${zoomSize.percentage} %`} - +
+
+ + + + + +
); }; diff --git a/src/store/store.ts b/src/store/store.ts index 4c41e7f..3cc3ac9 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,5 +1,5 @@ import { atom } from "jotai"; -import { Area, Coordinates, Element } from "../types/CommonTypes"; +import { Area, CanvasViewBox, Coordinates, Element, UpdateCanvasViewBoxFn } from "../types/CommonTypes"; import { atomWithStorage } from 'jotai/utils' import { getPencilPointsArrFromString, getTrianglePointsArrFromString, useUpdateXYAndDistance } from "../assets/utilities"; @@ -25,9 +25,16 @@ const initialElement: Element = { markerEnd: "", stroke: 'black', strokeWidth: 4, - fill: 'transparent', + fill: 'none', fontSize: "28px", } +const initialCanvasViewBox = { + x: 0, + y: 0, + percentage: 100, + width: 1920, + height: 1080, +} export const initialElementAtom = atom(initialElement) export const elementsAtom = atomWithStorage("elements", []) @@ -37,7 +44,40 @@ export const isDraggingAtom = atom(false) export const isDrawingAtom = atom( (get) => get(initialElementAtom).type === "free" ? false : true ) -export const zoomSizeAtom = atom<{ percentage: number, width: number, height: number }>({ percentage: 100, width: 1920, height: 1080 }) +export const canvasViewBoxAtom = atomWithStorage("canvasViewBox", initialCanvasViewBox) + +export const updateCanvasViewBoxAtom = atom( + null, + (get, set, updateFn: UpdateCanvasViewBoxFn) => { + const canvasViewBox = get(canvasViewBoxAtom) + + switch (updateFn) { + case UpdateCanvasViewBoxFn.ZOOMDOWN: + if (canvasViewBox.percentage === 0) return; + set(canvasViewBoxAtom, { + ...canvasViewBox, + percentage: canvasViewBox.percentage - 10, + width: canvasViewBox.width + 1920 * 0.1, + height: canvasViewBox.height + 1080 * 0.1, + }) + break; + + case UpdateCanvasViewBoxFn.ZOOMUP: + if (canvasViewBox.percentage === 200) return; + set(canvasViewBoxAtom, { + ...canvasViewBox, + percentage: canvasViewBox.percentage + 10, + width: canvasViewBox.width - 1920 * 0.1, + height: canvasViewBox.height - 1080 * 0.1, + }) + break; + + case UpdateCanvasViewBoxFn.ZOOMRESET: + set(canvasViewBoxAtom, initialCanvasViewBox) + break; + } + } +) export const updateElementsAtom = atom( null, diff --git a/src/types/CommonTypes.ts b/src/types/CommonTypes.ts index e930a68..5929c50 100644 --- a/src/types/CommonTypes.ts +++ b/src/types/CommonTypes.ts @@ -57,3 +57,21 @@ export const ELEMENT_TYPE_VARIANTS = { }; export type ElementsTypeName = keyof typeof ELEMENT_TYPE_VARIANTS; + +export type CanvasViewBox = { + x: number, + y: number, + percentage: number, + width: number, + height: number +} + +export enum UpdateCanvasViewBoxFn { + ZOOMUP, + ZOOMDOWN, + DRAGUP, + DRAGRIGHT, + DRAGDOWN, + DRAGLEFT, + ZOOMRESET, +} From bdb1821863a4dfda63f402ce409d4317e60c3170 Mon Sep 17 00:00:00 2001 From: KirillSerg Date: Sat, 8 Jun 2024 14:52:33 +0300 Subject: [PATCH 3/9] feat: added more variant to zoom --- src/components/Canvas.tsx | 46 ++++++++++++++++++++++++++++++++++----- src/store/store.ts | 16 +++++++++++--- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 964ba10..0f0ba0d 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -1,4 +1,4 @@ -import { useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import SingleElement from './SingleElement'; import SelectingArea from './SelectingArea'; @@ -9,10 +9,12 @@ import { onMouseMoveAtom, isDraggingAtom, isDrawingAtom, + keyPressedAtom, onKeyPressAtom, canvasViewBoxAtom, + updateCanvasViewBoxAtom, } from '../store/store'; -import { ElemenEvent } from '../types/CommonTypes'; +import { ElemenEvent, UpdateCanvasViewBoxFn } from '../types/CommonTypes'; import { transformCoordinates } from '../assets/utilities'; const Canvas = () => { @@ -22,8 +24,10 @@ const Canvas = () => { const [, onMouseUp] = useAtom(onMouseUpAtom); const [, onMouseDown] = useAtom(onMouseDownAtom); const [, onMouseMove] = useAtom(onMouseMoveAtom); + const keyPressed = useAtomValue(keyPressedAtom); const [, onKeyPress] = useAtom(onKeyPressAtom); - const zoomSize = useAtomValue(canvasViewBoxAtom); + const canvasViewBox = useAtomValue(canvasViewBoxAtom); + const [, updateCanvasViewBox] = useAtom(updateCanvasViewBoxAtom); const svgContainerRef = useRef(null); @@ -51,6 +55,30 @@ const Canvas = () => { }); }; + useEffect(() => { + const handleOnWheel = (e: WheelEvent) => { + e.preventDefault(); + if (keyPressed.ctrlKey && e.deltaY < 0) { + updateCanvasViewBox(UpdateCanvasViewBoxFn.ZOOMUP); + } + if (keyPressed.ctrlKey && e.deltaY > 0) { + updateCanvasViewBox(UpdateCanvasViewBoxFn.ZOOMDOWN); + } + }; + + const containerElement = svgContainerRef.current; + + if (containerElement) { + containerElement.addEventListener('wheel', handleOnWheel, { + passive: false, + }); + + return () => { + containerElement.removeEventListener('wheel', handleOnWheel); + }; + } + }, [keyPressed.ctrlKey, updateCanvasViewBox]); + return ( { onMouseDown={(e) => handleMouseDown(e)} onMouseMove={(e) => handleMouseMove(e)} onMouseUp={onMouseUp} - onKeyDown={(e) => onKeyPress(e.key)} + onKeyDown={(e) => { + e.preventDefault(); + onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: e.key }); + }} + onKeyUp={(e) => { + e.preventDefault(); + onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: '' }); + }} + // onWheel={(e) => handleOnWheel(e)} preserveAspectRatio="xMinYMin meet" //for the SVG container to be on the entire screen, while the elements inside kept the proportions and x=0, y=0 viewBox started from the upper left corner - viewBox={`${zoomSize.x} ${zoomSize.y} ${zoomSize.width} ${zoomSize.height}`} + viewBox={`${canvasViewBox.x} ${canvasViewBox.y} ${canvasViewBox.width} ${canvasViewBox.height}`} width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" diff --git a/src/store/store.ts b/src/store/store.ts index 3cc3ac9..341455f 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -45,6 +45,7 @@ export const isDrawingAtom = atom( (get) => get(initialElementAtom).type === "free" ? false : true ) export const canvasViewBoxAtom = atomWithStorage("canvasViewBox", initialCanvasViewBox) +export const keyPressedAtom = atom<{ ctrlKey: boolean; key: string }>({ ctrlKey: false, key: "" }) export const updateCanvasViewBoxAtom = atom( null, @@ -253,14 +254,23 @@ export const onMouseUpAtom = atom( export const onKeyPressAtom = atom( null, - (_get, set, key: string) => { + (_get, set, key: { ctrlKey: boolean; key: string }) => { + console.info(key) + set(keyPressedAtom, key) + switch (true) { - case key === "Delete": + case key.key === "Delete": set(deleteElementsAtom) break; - case (key === "Escape"): + case (key.key === "Escape"): set(initialElementAtom, initialElement) break; + case (key.key === "+" && key.ctrlKey): + set(updateCanvasViewBoxAtom, UpdateCanvasViewBoxFn.ZOOMUP) + break; + case (key.key === "-" && key.ctrlKey): + set(updateCanvasViewBoxAtom, UpdateCanvasViewBoxFn.ZOOMDOWN) + break; } } ) From 354c44411c812a3eded6b7ef7d524dad3fb08448 Mon Sep 17 00:00:00 2001 From: KirillSerg Date: Sun, 9 Jun 2024 00:10:04 +0300 Subject: [PATCH 4/9] feat: added grabbin of canvas: replace onKeypress fn to the App component --- src/App.tsx | 16 ++++- src/components/Canvas.tsx | 39 +++++------ src/components/GrabIconBtn copy.tsx | 27 ++++++++ src/components/Inspector.tsx | 6 +- src/components/SingleElement.tsx | 2 +- src/components/Toolbar.tsx | 14 +++- src/components/Zoom.tsx | 12 ++-- src/store/store.ts | 101 ++++++++++++++++++---------- src/types/CommonTypes.ts | 10 ++- 9 files changed, 155 insertions(+), 72 deletions(-) create mode 100644 src/components/GrabIconBtn copy.tsx diff --git a/src/App.tsx b/src/App.tsx index fb0dd44..02b5368 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,26 @@ +import { useAtom } from 'jotai'; import Canvas from './components/Canvas'; import Inspector from './components/Inspector'; import Layers from './components/Layers'; import Toolbar from './components/Toolbar'; import Zoom from './components/Zoom'; +import { onKeyPressAtom } from './store/store'; const App = () => { + const [, onKeyPress] = useAtom(onKeyPressAtom); + return ( -
+
{ + e.preventDefault(); + onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: e.key }); + }} + onKeyUp={(e) => { + e.preventDefault(); + onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: '' }); + }} + > diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 0f0ba0d..4fb7fb4 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -10,11 +10,13 @@ import { isDraggingAtom, isDrawingAtom, keyPressedAtom, - onKeyPressAtom, canvasViewBoxAtom, - updateCanvasViewBoxAtom, + zoomCanvasAtom, + creationInitialElementAtom, + selectingAreaAtom, + grabCanvasAtom, } from '../store/store'; -import { ElemenEvent, UpdateCanvasViewBoxFn } from '../types/CommonTypes'; +import { ElemenEvent, ZoomCanvasFn } from '../types/CommonTypes'; import { transformCoordinates } from '../assets/utilities'; const Canvas = () => { @@ -25,9 +27,11 @@ const Canvas = () => { const [, onMouseDown] = useAtom(onMouseDownAtom); const [, onMouseMove] = useAtom(onMouseMoveAtom); const keyPressed = useAtomValue(keyPressedAtom); - const [, onKeyPress] = useAtom(onKeyPressAtom); const canvasViewBox = useAtomValue(canvasViewBoxAtom); - const [, updateCanvasViewBox] = useAtom(updateCanvasViewBoxAtom); + const [, zoomCanvas] = useAtom(zoomCanvasAtom); + const creationInitialElement = useAtomValue(creationInitialElementAtom); + const selectingArea = useAtomValue(selectingAreaAtom); + const [canasViewBox, grabCanvas] = useAtom(grabCanvasAtom); const svgContainerRef = useRef(null); @@ -55,14 +59,18 @@ const Canvas = () => { }); }; + // for prevent default browser zoom useEffect(() => { const handleOnWheel = (e: WheelEvent) => { e.preventDefault(); if (keyPressed.ctrlKey && e.deltaY < 0) { - updateCanvasViewBox(UpdateCanvasViewBoxFn.ZOOMUP); + zoomCanvas(ZoomCanvasFn.ZOOMUP); } if (keyPressed.ctrlKey && e.deltaY > 0) { - updateCanvasViewBox(UpdateCanvasViewBoxFn.ZOOMDOWN); + zoomCanvas(ZoomCanvasFn.ZOOMDOWN); + } + if (!keyPressed.ctrlKey) { + grabCanvas({ ...canasViewBox, y: e.deltaY }); } }; @@ -77,25 +85,16 @@ const Canvas = () => { containerElement.removeEventListener('wheel', handleOnWheel); }; } - }, [keyPressed.ctrlKey, updateCanvasViewBox]); + }, [keyPressed.ctrlKey, canasViewBox, zoomCanvas, grabCanvas]); return ( handleMouseDown(e)} onMouseMove={(e) => handleMouseMove(e)} onMouseUp={onMouseUp} - onKeyDown={(e) => { - e.preventDefault(); - onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: e.key }); - }} - onKeyUp={(e) => { - e.preventDefault(); - onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: '' }); - }} - // onWheel={(e) => handleOnWheel(e)} preserveAspectRatio="xMinYMin meet" //for the SVG container to be on the entire screen, while the elements inside kept the proportions and x=0, y=0 viewBox started from the upper left corner viewBox={`${canvasViewBox.x} ${canvasViewBox.y} ${canvasViewBox.width} ${canvasViewBox.height}`} width="100%" @@ -124,7 +123,9 @@ const Canvas = () => { /> ))} - {!isDragging && !isDrawing && } + {!isDragging && + !isDrawing && + creationInitialElement.type_name !== 'grab' && } ); }; diff --git a/src/components/GrabIconBtn copy.tsx b/src/components/GrabIconBtn copy.tsx new file mode 100644 index 0000000..3de15e0 --- /dev/null +++ b/src/components/GrabIconBtn copy.tsx @@ -0,0 +1,27 @@ +import { ElementsTypeName } from '../types/CommonTypes'; + +interface Props { + className: string; + handlerClick: (typeName: ElementsTypeName) => void; +} + +const GrabIconBtn = ({ className, handlerClick }: Props) => { + return ( + + ); +}; + +export default GrabIconBtn; diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index d72f9c8..483113c 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -3,7 +3,7 @@ import { useAtom } from 'jotai'; import deleteIcon from '../assets/icons/trash.svg'; import { deleteElementsAtom, - initialElementAtom, + creationInitialElementAtom, selectedElementAtom, updateElementsAtom, } from '../store/store'; @@ -19,7 +19,9 @@ const Inspector = () => { const [, deleteElements] = useAtom(deleteElementsAtom); const [, updateElements] = useAtom(updateElementsAtom); const [selectedElement] = useAtom(selectedElementAtom); - const [initialElement, setInitialElement] = useAtom(initialElementAtom); + const [initialElement, setInitialElement] = useAtom( + creationInitialElementAtom, + ); const element = useMemo(() => { if (selectedElement !== null) { diff --git a/src/components/SingleElement.tsx b/src/components/SingleElement.tsx index c7ceeec..49162df 100644 --- a/src/components/SingleElement.tsx +++ b/src/components/SingleElement.tsx @@ -30,7 +30,7 @@ const SingleElement = ({ element, svgContainerRef }: Props) => { return ( <> - {element.type !== 'free' && ( + {element.type !== 'free' && element.type !== 'grab' && ( { - const [initialElement, setInitialElement] = useAtom(initialElementAtom); + const [initialElement, setInitialElement] = useAtom( + creationInitialElementAtom, + ); const [, setSelectedElement] = useAtom(selectedElementAtom); const handlerSelectElement = (typeName: ElementsTypeName) => { @@ -32,6 +38,10 @@ const Toolbar = () => { return (
+ { const canvasViewBox = useAtomValue(canvasViewBoxAtom); - const [, updateCanvasViewBox] = useAtom(updateCanvasViewBoxAtom); + const [, zoomCanvas] = useAtom(zoomCanvasAtom); return (
@@ -12,7 +12,7 @@ const Zoom = () => {