Skip to content

Commit

Permalink
Merge pull request #44 from KirillSerg/zoom
Browse files Browse the repository at this point in the history
Zoom
  • Loading branch information
KirillSerg authored Jun 13, 2024
2 parents af35955 + 827703f commit 2e6be74
Show file tree
Hide file tree
Showing 19 changed files with 452 additions and 90 deletions.
82 changes: 82 additions & 0 deletions UItests/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,72 @@ test("Create curved line", async ({ page }) => {
await checkNumberOfElementsInLocalStorage(page, 0)
})

test("Zoom", async ({ page }) => {
await page.goto('http://localhost:5173/');
// zoom by clicking zoomBar
const zoomDownBtn = page.locator('[id=zoomdown]')
const zoomUpBtn = page.locator('[id=zoomup]')
const zoomReset = page.locator('[id=zoomreset]')
await zoomDownBtn.click()
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "width", value: 2112 }) //1920px+192px(10%)
await zoomUpBtn.click()
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "percentage", value: 100 })
await page.pause()
// zoom by keyPress ctrl/meta + "+"/"-"
await page.locator('#canvas').press('Control++')
await page.keyboard.up("+")
await page.keyboard.up("Control")
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "height", value: 972 }) //1080px - 108px(10%)
await page.locator('#canvas').press('Meta+-')
await page.keyboard.up("-")
await page.keyboard.up("Meta")
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "percentage", value: 100 })
// zoom by keyPress + wheel scroll
await page.mouse.move(700, 400);
await page.keyboard.down('Control')
await page.mouse.wheel(0, 500);
await page.keyboard.up("Control")
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "percentage", value: 90 })
// reset zoom (centered)
await zoomReset.click()
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "percentage", value: 100 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "width", value: 1920 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "height", value: 1080 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "x", value: 0 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "y", value: 0 })
})

test("Grab canvas", async ({ page }) => {
await page.goto('http://localhost:5173/');
const zoomReset = page.locator('[id=zoomreset]')
const toolbarGrab = page.locator('header > [id=canvasGrabBtn]')
await toolbarGrab.click()
// await page.locator('#canvas').click() // it's need if testing in --headed mode
await page.mouse.move(600, 600);
await page.mouse.down();
await page.mouse.move(700, 700);
await page.mouse.up();
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "x", value: -1 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "y", value: -1 })
// reset grab (centered)
await zoomReset.click()
// await page.locator('#canvas').click() // it's need if testing in --headed mode
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "percentage", value: 100 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "width", value: 1920 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "height", value: 1080 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "x", value: 0 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "y", value: 0 })
// grab by keyPress + mouse move
await page.mouse.move(600, 600);
await page.keyboard.down('Control')
await page.mouse.down();
await page.mouse.move(400, 400);
await page.mouse.up();
await page.keyboard.up("Control")
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "x", value: 1 })
await checkCanvasViewBoxParametersInLocalStorage(page, { key: "y", value: 1 })
})

async function checkElementInLocalStorage(page: Page, elementType: string) {
return await page.waitForFunction(type => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -141,6 +207,22 @@ async function checkNumberOfElementsInLocalStorage(page: Page, expected: number)
}, expected);
}

async function checkCanvasViewBoxParametersInLocalStorage(page: Page, param: { key: string; value: number }) {
return await page.waitForFunction(param => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (param.key === "x" || param.key === "y") {
if (param.value <= 0) {
return JSON.parse(localStorage['canvasViewBox'])[param.key] <= param.value;
}
if (param.value >= 0) {
return JSON.parse(localStorage['canvasViewBox'])[param.key] >= param.value;
}
} else {
return JSON.parse(localStorage['canvasViewBox'])[param.key] === param.value;
}
}, param);
}



// let context;
Expand Down
22 changes: 21 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
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 (
<div className="flex flex-col h-screen relative">
<div
className="h-screen flex flex-col items-center relative"
onKeyDown={(e) => {
if (e.key === '+' || e.key === '-') {
e.preventDefault();
}
onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: e.key });
}}
onKeyUp={(e) => {
if (e.key === '+' || e.key === '-') {
e.preventDefault();
}
onKeyPress({ ctrlKey: e.ctrlKey || e.metaKey, key: '' });
}}
>
<Toolbar />
<Canvas />
<Inspector />
<Layers />
<Zoom />
</div>
);
};
Expand Down
62 changes: 50 additions & 12 deletions src/components/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,10 +9,14 @@ import {
onMouseMoveAtom,
isDraggingAtom,
isDrawingAtom,
onKeyPressAtom,
initialElementAtom,
keyPressedAtom,
canvasViewBoxAtom,
zoomCanvasAtom,
creationInitialElementAtom,
selectingAreaAtom,
grabCanvasAtom,
} from '../store/store';
import { ElemenEvent } from '../types/CommonTypes';
import { ElemenEvent, ZoomCanvasFn } from '../types/CommonTypes';
import { transformCoordinates } from '../assets/utilities';

const Canvas = () => {
Expand All @@ -22,8 +26,12 @@ const Canvas = () => {
const [, onMouseUp] = useAtom(onMouseUpAtom);
const [, onMouseDown] = useAtom(onMouseDownAtom);
const [, onMouseMove] = useAtom(onMouseMoveAtom);
const [, onKeyPress] = useAtom(onKeyPressAtom);
const initialElement = useAtomValue(initialElementAtom);
const keyPressed = useAtomValue(keyPressedAtom);
const canvasViewBox = useAtomValue(canvasViewBoxAtom);
const [, zoomCanvas] = useAtom(zoomCanvasAtom);
const [, grabCanvas] = useAtom(grabCanvasAtom);
const creationInitialElement = useAtomValue(creationInitialElementAtom);
const selectingArea = useAtomValue(selectingAreaAtom);

const svgContainerRef = useRef<SVGSVGElement>(null);

Expand Down Expand Up @@ -51,16 +59,44 @@ const Canvas = () => {
});
};

// for prevent default browser zoom
useEffect(() => {
const handleOnWheel = (e: WheelEvent) => {
e.preventDefault();
if (keyPressed.ctrlKey && e.deltaY < 0) {
zoomCanvas(ZoomCanvasFn.ZOOMUP);
}
if (keyPressed.ctrlKey && e.deltaY > 0) {
zoomCanvas(ZoomCanvasFn.ZOOMDOWN);
}
if (!keyPressed.ctrlKey) {
grabCanvas({ x: 0, y: e.deltaY });
}
};

const containerElement = svgContainerRef.current;

if (containerElement) {
containerElement.addEventListener('wheel', handleOnWheel, {
passive: false,
});

return () => {
containerElement.removeEventListener('wheel', handleOnWheel);
};
}
}, [keyPressed.ctrlKey, zoomCanvas, grabCanvas]);

return (
<svg
className={`h-screen focus:outline-none ${selectingArea && creationInitialElement.type_name === 'grab' ? 'cursor-grabbing' : creationInitialElement.type_name === 'grab' ? 'cursor-grab' : ''}`}
id="canvas"
ref={svgContainerRef}
onMouseDown={(e) => handleMouseDown(e)}
onMouseMove={(e) => handleMouseMove(e)}
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={`${canvasViewBox.x} ${canvasViewBox.y} ${canvasViewBox.width} ${canvasViewBox.height}`}
width="100%"
height="100%"
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -87,12 +123,14 @@ const Canvas = () => {
/>
))}

{!isDragging && !isDrawing && <SelectingArea />}
{!isDragging &&
!isDrawing &&
creationInitialElement.type_name !== 'grab' && <SelectingArea />}

{initialElement.type_name === 'image' && (
{creationInitialElement.type_name === 'image' && (
<SingleElement
key={initialElement.id}
element={initialElement}
key={creationInitialElement.id}
element={creationInitialElement}
svgContainerRef={svgContainerRef.current}
/>
)}
Expand Down
7 changes: 6 additions & 1 deletion src/components/EllipseIconBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ interface Props {
const EllipseIconBtn = ({ className, handlerClick }: Props) => {
return (
<button className={`${className}`} onClick={() => handlerClick('ellipse')}>
<svg viewBox="0 0 24 24" height="100%" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 24 24"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
>
<ellipse
cx="12"
cy="12"
Expand Down
2 changes: 1 addition & 1 deletion src/components/FreeIconBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const FreeIconBtn = ({ className, handlerClick }: Props) => {
<button className={`${className}`} onClick={() => handlerClick('free')}>
<svg
fill="#000000"
height="20.73"
height="100%"
width="20.73"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 203.079 203.079"
Expand Down
31 changes: 31 additions & 0 deletions src/components/GrabIconBtn copy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ElementsTypeName } from '../types/CommonTypes';

interface Props {
className: string;
handlerClick: (typeName: ElementsTypeName) => void;
}

const GrabIconBtn = ({ className, handlerClick }: Props) => {
return (
<button
id="canvasGrabBtn"
className={`${className}`}
onClick={() => handlerClick('grab')}
>
<svg
fill="#000000"
width="20.73"
height="100%"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M28.09,9.74a4,4,0,0,0-1.16.19c-.19-1.24-1.55-2.18-3.27-2.18A4,4,0,0,0,22.13,8,3.37,3.37,0,0,0,19,6.3a3.45,3.45,0,0,0-2.87,1.32,3.65,3.65,0,0,0-1.89-.51A3.05,3.05,0,0,0,11,9.89v.91c-1.06.4-4.11,1.8-4.91,4.84s.34,8,2.69,11.78a25.21,25.21,0,0,0,5.9,6.41.9.9,0,0,0,.53.17H25.55a.92.92,0,0,0,.55-.19,13.13,13.13,0,0,0,3.75-6.13A25.8,25.8,0,0,0,31.41,18v-5.5A3.08,3.08,0,0,0,28.09,9.74ZM29.61,18a24,24,0,0,1-1.47,9.15A12.46,12.46,0,0,1,25.2,32.2H15.47a23.75,23.75,0,0,1-5.2-5.72c-2.37-3.86-3-8.23-2.48-10.39A5.7,5.7,0,0,1,11,12.76v7.65a.9.9,0,0,0,1.8,0V9.89c0-.47.59-1,1.46-1s1.49.52,1.49,1v5.72h1.8V8.81c0-.28.58-.71,1.46-.71s1.53.48,1.53.75v6.89h1.8V10l.17-.12a2.1,2.1,0,0,1,1.18-.32c.93,0,1.5.44,1.5.68l0,6.5H27V11.87a1.91,1.91,0,0,1,1.12-.33c.86,0,1.52.51,1.52.94Z"
className="clr-i-outline clr-i-outline-path-1"
></path>
</svg>
</button>
);
};

export default GrabIconBtn;
36 changes: 18 additions & 18 deletions src/components/ImageIconBtn.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { useRef } from 'react';
import { useAtom } from 'jotai';
import { ElementsTypeName } from '../types/CommonTypes';
import { initialElementAtom } from '../store/store';
import { creationInitialElementAtom } from '../store/store';

interface Props {
className: string;
handlerClick: (typeName: ElementsTypeName) => void;
}

const ImageIconBtn = ({ className, handlerClick }: Props) => {
const [, setInitialElement] = useAtom(initialElementAtom);
const [, setCreationInitialElement] = useAtom(creationInitialElementAtom);

const fileInputRef = useRef<HTMLInputElement>(null);

const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.result !== null)
setInitialElement((prev) => {
return { ...prev, href: reader.result };
});
// if (reader.result !== null)
setCreationInitialElement((prev) => {
return { ...prev, href: reader.result };
});
// Reset the file input value
if (fileInputRef.current) {
fileInputRef.current.value = '';
Expand All @@ -32,21 +32,21 @@ const ImageIconBtn = ({ className, handlerClick }: Props) => {

return (
<button className={className} onClick={() => handlerClick('image')}>
<div className="absolute">
<label htmlFor="image" className="relative cursor-pointer">
<div className="w-5 h-5"></div>
</label>
<input
ref={fileInputRef}
onChange={(e) => handleImageChange(e)}
id="image"
type="file"
className="hidden"
/>
</div>
<label
htmlFor="image"
className="absolute top-0 left-0 w-full h-full cursor-pointer"
></label>
<input
ref={fileInputRef}
onChange={(e) => handleImageChange(e)}
id="image"
type="file"
className="hidden"
/>
<svg
viewBox="0 0 24 24"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
fillRule="evenodd"
clipRule="evenodd"
Expand Down
6 changes: 4 additions & 2 deletions src/components/Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useAtom } from 'jotai';
import deleteIcon from '../assets/icons/trash.svg';
import {
deleteElementsAtom,
initialElementAtom,
creationInitialElementAtom,
selectedElementAtom,
updateElementsAtom,
} from '../store/store';
Expand All @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/components/LineArrowIconBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const LineArrowIconBtn = ({ className, handlerClick }: Props) => {
className={`${className}`}
onClick={() => handlerClick('arrow_line')}
>
<svg viewBox="0 0 24 24" height="100%" xmlns="http://www.w3.org/2000/svg">
<svg
viewBox="0 0 24 24"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<marker
id="arrow"
Expand Down
Loading

0 comments on commit 2e6be74

Please sign in to comment.