Skip to content

Commit

Permalink
Added simple viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszmigas committed Mar 2, 2024
1 parent 2a7ab29 commit ec5ed8d
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 7 deletions.
7 changes: 2 additions & 5 deletions apps/web/src/components/appContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ColorsPanel } from "./panels/colorsPanel";
import { LayersPanel } from "./panels/layersPanel";
import { HistoryPanel } from "./panels/historyPanel";
import { MetadataPanel } from "./panels/metadataPanel";
import { CanvasViewport } from "./canvasViewport";

const LeftContent = () => {
return (
Expand All @@ -23,10 +24,6 @@ const LeftContent = () => {
);
};

const MiddleContent = () => {
return <div className="p-2">Canvas</div>;
};

const RightContent = () => {
return (
<ResizablePanelGroup direction="vertical">
Expand All @@ -53,7 +50,7 @@ export const AppContent = () => {
</ResizablePanel>
<ResizableHandle />
<ResizablePanel>
<MiddleContent />
<CanvasViewport />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={20}>
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/canvasHost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const CanvasHost = () => {
return <div className="bg-white" style={{ width: 800, height: 600 }}></div>;
};
51 changes: 51 additions & 0 deletions apps/web/src/components/canvasViewport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Viewport, ViewportManipulator } from "@/utils/manipulation";
import { useEffect, useRef } from "react";
import { CanvasHost } from "./canvasHost";

const applyTransform = (viewport: Viewport, element: HTMLElement) => {
element.style.transform = `
translate(${viewport.position.x}px, ${viewport.position.y}px)
scale(${viewport.zoom})
`;
};

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 manipulator = new ViewportManipulator(
hostElement.current,
() => viewport.current,
(newViewport) => {
viewport.current = newViewport;
applyTransform(viewport.current, viewportElement.current!);
}
);
applyTransform(viewport.current, viewportElement.current!);
return () => manipulator.dispose();
}, []);

return (
<div className="relative size-full">
<div
ref={hostElement}
className="absolute size-full overflow-hidden border "
>
<div
ref={viewportElement}
className="pointer-events-none origin-top-left"
>
<CanvasHost />
</div>
</div>
<div className="absolute p-small">Shift+LMB to move</div>
</div>
);
};

4 changes: 2 additions & 2 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@
}
body {
@apply bg-background text-foreground;
@apply w-full h-full;
@apply size-full;
}
html,
#root {
@apply w-full h-full;
@apply size-full;
}
}

4 changes: 4 additions & 0 deletions apps/web/src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Position = {
x: number;
y: number;
};
4 changes: 4 additions & 0 deletions apps/web/src/utils/manipulation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* Based on https://github.com/mateuszmigas/composite-viewer-2d */
export * from "./viewport";
export * from "./viewportManipulator";

47 changes: 47 additions & 0 deletions apps/web/src/utils/manipulation/throttleHtmlManipulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export type EventType = keyof HTMLElementEventMap;
export type EventHandler<T extends Event> = (event: T) => void;

export class ThrottleHtmlManipulator {
animationFrameHandle = 0;
eventListeners = new Map<string, EventHandler<Event>>();
events = new Map<string, Event>();

constructor(protected readonly element: HTMLElement) {
this.requestProcessEvents();
}

dispose() {
this.eventListeners.forEach((_, key) =>
this.element.removeEventListener(key, this.onEventTriggered)
);

cancelAnimationFrame(this.animationFrameHandle);
}

protected registerEvent<T extends Event>(
type: EventType,
handler: EventHandler<T>
) {
this.element.addEventListener(type, this.onEventTriggered);
this.eventListeners.set(type, handler as EventHandler<Event>);
}

private onEventTriggered = (event: Event) => {
this.events.set(event.type, event);
};

private processEventsLoop = () => {
this.events.forEach((value, key) => {
const handler = this.eventListeners.get(key);
handler?.(value);
});
this.events.clear();

this.requestProcessEvents();
};

private requestProcessEvents() {
this.animationFrameHandle = requestAnimationFrame(this.processEventsLoop);
}
}

18 changes: 18 additions & 0 deletions apps/web/src/utils/manipulation/viewport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Position } from "../common";

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

export const zoomAtPosition = (
currentViewport: Viewport,
zoom: number,
position: Position
): Viewport => ({
position: {
x: position.x - (position.x - currentViewport.position.x) * zoom,
y: position.y - (position.y - currentViewport.position.y) * zoom,
},
zoom: currentViewport.zoom * zoom,
});
96 changes: 96 additions & 0 deletions apps/web/src/utils/manipulation/viewportManipulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Position } from "../common";
import { ThrottleHtmlManipulator } from "./throttleHtmlManipulator";
import { Viewport, zoomAtPosition } from "./viewport";

type ViewportAction =
| {
type: "translate";
offsetX: number;
offsetY: number;
}
| { type: "zoomInAtPosition"; position: Position }
| { type: "zoomOutAtPosition"; position: Position };

const reducer = (viewport: Viewport, action: ViewportAction): Viewport => {
switch (action.type) {
case "translate": {
return {
...viewport,
position: {
x: viewport.position.x + action.offsetX,
y: viewport.position.y + action.offsetY,
},
};
}
case "zoomInAtPosition": {
return zoomAtPosition(viewport, 1.1, action.position);
}
case "zoomOutAtPosition": {
return zoomAtPosition(viewport, 0.9, action.position);
}
default:
return viewport;
}
};

export class ViewportManipulator extends ThrottleHtmlManipulator {
private pointerPosition = { x: 0, y: 0 };
private isMoving = false;
// private actionReducer: (actions: Action[]) => Viewport;

constructor(
protected element: HTMLElement,
protected viewport: () => Viewport,
private onViewportChange: (newViewport: Viewport) => void
) {
super(element);

this.registerEvent("mousedown", this.onMouseDown);
this.registerEvent("mousemove", this.onMouseMove);
this.registerEvent("mouseup", this.onMouseUp);
this.registerEvent("mouseleave", this.onMouseLeave);
this.registerEvent("wheel", this.onWheel);
}

private dispatchAction = (action: ViewportAction) => {
const newViewport = reducer(this.viewport(), action);
this.onViewportChange(newViewport);
};

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

private onMouseMove = (e: MouseEvent) => {
if (this.isMoving) {
this.dispatchAction({
type: "translate",
offsetX: e.offsetX - this.pointerPosition.x,
offsetY: e.offsetY - this.pointerPosition.y,
});
}

this.pointerPosition = { x: e.offsetX, y: e.offsetY };
};

private onMouseUp = () => {
this.isMoving = false;
};

private onMouseLeave = () => {
this.isMoving = false;
};

private onWheel = (e: WheelEvent) => {
const action = e.deltaY < 0 ? "zoomInAtPosition" : "zoomOutAtPosition";

this.dispatchAction({
type: action,
position: { x: e.offsetX, y: e.offsetY },
});
};
}

3 changes: 3 additions & 0 deletions apps/web/src/utils/typeGuards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isFunction = (x: any): x is Function => {
return typeof x === "function";
};

0 comments on commit ec5ed8d

Please sign in to comment.