Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoom #44

Merged
merged 10 commits into from
Jun 13, 2024
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