Skip to content

Commit

Permalink
Scale canvas to fit viewport
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszmigas committed Mar 2, 2024
1 parent ec5ed8d commit c5d6f30
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 25 deletions.
57 changes: 35 additions & 22 deletions apps/web/src/components/canvasViewport.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Viewport, ViewportManipulator } from "@/utils/manipulation";
import { useEffect, useRef } from "react";
import {
Viewport,
ViewportManipulator,
defaultViewport,
calculateFitViewport,
} from "@/utils/manipulation";
import { useLayoutEffect, useRef } from "react";
import { CanvasHost } from "./canvasHost";

const applyTransform = (viewport: Viewport, element: HTMLElement) => {
Expand All @@ -10,41 +15,49 @@ const applyTransform = (viewport: Viewport, element: HTMLElement) => {
};

export const CanvasViewport = () => {
const hostElement = useRef<HTMLDivElement>(null);
const viewportElement = useRef<HTMLDivElement>(null);
const viewport = useRef<Viewport>({
position: { x: 50, y: 50 },
zoom: 0.5,
});

useEffect(() => {
if (!hostElement.current || !viewportElement.current) return;
const hostElementRef = useRef<HTMLDivElement>(null);
const viewportElementRef = useRef<HTMLDivElement>(null);
const canvasSize = { width: 800, height: 600 };
const viewport = useRef<Viewport | null>(null);

useLayoutEffect(() => {
if (!hostElementRef.current || !viewportElementRef.current) return;

const hostElement = hostElementRef.current;
const viewportElement = viewportElementRef.current;
const manipulator = new ViewportManipulator(
hostElement.current,
() => viewport.current,
hostElement,
() => viewport.current ?? defaultViewport,
(newViewport) => {
viewport.current = newViewport;
applyTransform(viewport.current, viewportElement.current!);
applyTransform(viewport.current, viewportElement!);
}
);
applyTransform(viewport.current, viewportElement.current!);

setTimeout(() => {
viewport.current = calculateFitViewport(
hostElement.getBoundingClientRect(),
{ x: 0, y: 0, ...canvasSize },
50
);
applyTransform(viewport.current, viewportElement!);
viewportElement.classList.remove("hidden");
}, 0);

return () => manipulator.dispose();
}, []);

return (
<div className="relative size-full">
<div
ref={hostElement}
className="absolute size-full overflow-hidden border "
>
<div ref={hostElementRef} className="absolute size-full overflow-hidden">
<div
ref={viewportElement}
className="pointer-events-none origin-top-left"
ref={viewportElementRef}
className="pointer-events-none origin-top-left hidden"
>
<CanvasHost />
</div>
</div>
<div className="absolute p-small">Shift+LMB to move</div>
<div className="absolute p-small">Middle mouse to move/zoom</div>
</div>
);
};
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,21 @@ export type Position = {
x: number;
y: number;
};

export type Size = {
width: number;
height: number;
};

export type Rectangle = Position & Size;

export const scaleRectangle = (
rectangle: Rectangle,
scale: number
): Rectangle => ({
x: rectangle.x * scale,
y: rectangle.y * scale,
width: rectangle.width * scale,
height: rectangle.height * scale,
});

46 changes: 45 additions & 1 deletion apps/web/src/utils/manipulation/viewport.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Position } from "../common";
import { Position, Rectangle, Size, scaleRectangle } from "../common";

export type Viewport = {
position: Position;
zoom: number;
};

export const defaultViewport: Viewport = {
position: { x: 0, y: 0 },
zoom: 1,
};

export const zoomAtPosition = (
currentViewport: Viewport,
zoom: number,
Expand All @@ -16,3 +21,42 @@ export const zoomAtPosition = (
},
zoom: currentViewport.zoom * zoom,
});

export const calculateFitViewport = (
windowSize: Size,
targetArea: Rectangle,
padding: number
): Viewport => {
const windowSizeWithPadding = {
width: windowSize.width - 2 * padding,
height: windowSize.height - 2 * padding,
};
const targetAreaRatio = targetArea.width / targetArea.height;
const windowSizeRatio =
windowSizeWithPadding.width / windowSizeWithPadding.height;
const alignHorizontally = targetAreaRatio > windowSizeRatio;

const newZoom = alignHorizontally
? windowSizeWithPadding.width / targetArea.width
: windowSizeWithPadding.width / (targetArea.height * windowSizeRatio);
const scaledTargetArea = scaleRectangle(targetArea, newZoom);
const newPosition = alignHorizontally
? {
x: -scaledTargetArea.x,
y:
-scaledTargetArea.y +
(windowSizeWithPadding.height - scaledTargetArea.height) / 2,
}
: {
x:
-scaledTargetArea.x +
(windowSizeWithPadding.width - scaledTargetArea.width) / 2,
y: -scaledTargetArea.y,
};

return {
position: { x: newPosition.x + padding, y: newPosition.y + padding },
zoom: newZoom,
};
};

3 changes: 1 addition & 2 deletions apps/web/src/utils/manipulation/viewportManipulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const reducer = (viewport: Viewport, action: ViewportAction): Viewport => {
export class ViewportManipulator extends ThrottleHtmlManipulator {
private pointerPosition = { x: 0, y: 0 };
private isMoving = false;
// private actionReducer: (actions: Action[]) => Viewport;

constructor(
protected element: HTMLElement,
Expand All @@ -58,7 +57,7 @@ export class ViewportManipulator extends ThrottleHtmlManipulator {
};

private onMouseDown = (e: MouseEvent) => {
if (e.button === 0 && e.shiftKey) {
if (e.button === 1) {
this.pointerPosition = { x: e.offsetX, y: e.offsetY };
this.isMoving = true;
}
Expand Down

0 comments on commit c5d6f30

Please sign in to comment.