fix: strange cursor behaviour when zoomed out

This commit is contained in:
Mark Mankarious
2023-07-11 13:50:10 +01:00
parent de0b09b823
commit ccf267dca5
7 changed files with 71 additions and 180 deletions

View File

@@ -8,7 +8,10 @@ interface Props {
}
const COLOR = "grey.900";
const SIZE = 11;
const ARROW = {
size: 11,
top: 8,
};
const ANIMATIONS = {
in: keyframes`
0% {
@@ -27,21 +30,21 @@ export const ContextMenu = ({ position, children }: Props) => {
<Box
sx={{
position: "absolute",
top: position.y,
left: position.x + SIZE,
top: position.y - 20,
left: position.x + ARROW.size * 2,
animation: `${ANIMATIONS.in} 0.2s ease-in-out`,
}}
>
<Box
sx={{
position: "absolute",
left: -(SIZE - 2),
top: 8,
left: -(ARROW.size - 2),
top: ARROW.top,
width: 0,
height: 0,
borderTop: `${SIZE}px solid transparent`,
borderBottom: `${SIZE}px solid transparent`,
borderRight: `${SIZE}px solid`,
borderTop: `${ARROW.size}px solid transparent`,
borderBottom: `${ARROW.size}px solid transparent`,
borderRight: `${ARROW.size}px solid`,
borderRightColor: COLOR,
}}
/>

View File

@@ -1,15 +1,12 @@
import React, { useRef, useEffect, useContext } from "react";
import { observer } from "mobx-react";
import { Renderer } from "../renderer/Renderer";
import { useGlobalState } from "../hooks/useGlobalState";
import { useMouseInput } from "../hooks/useMouseInput";
import { modeManagerContext } from "../contexts/ModeManagerContext";
import { Select } from "../modes/Select";
export const RendererContainer = observer(() => {
const modeManager = useContext(modeManagerContext);
const rendererEl = useRef<HTMLDivElement>(null);
const { setDomEl, setCallbacks } = useMouseInput();
const setRenderer = useGlobalState((state) => state.setRenderer);
const onSceneChange = useGlobalState((state) => state.onSceneChange);
@@ -17,29 +14,10 @@ export const RendererContainer = observer(() => {
if (!rendererEl.current) return;
const renderer = setRenderer(rendererEl.current);
setDomEl(rendererEl.current);
modeManager.setRenderer(renderer);
modeManager.setEventEmitter(renderer.callbacks.emitEvent);
modeManager.activateMode(Select);
setCallbacks({
onMouseMove: (event) => {
modeManager.onMouseEvent("MOUSE_MOVE", event);
},
onMouseDown: (event) => {
modeManager.onMouseEvent("MOUSE_DOWN", event);
},
onMouseUp: (event) => {
modeManager.onMouseEvent("MOUSE_UP", event);
},
onMouseEnter: (event) => {
modeManager.onMouseEvent("MOUSE_ENTER", event);
},
onMouseLeave: (event) => {
modeManager.onMouseEvent("MOUSE_LEAVE", event);
},
});
}, [setRenderer, setDomEl, modeManager, onSceneChange]);
}, [setRenderer, modeManager, onSceneChange]);
return (
<div

View File

@@ -1,120 +0,0 @@
import { useEffect, useState, useCallback } from "react";
import { Coords } from "../renderer/elements/Coords";
interface MousePosition {
position: Coords;
delta: Coords | null;
}
type _MouseEvent = (e: MousePosition) => void;
interface MouseEvents {
onMouseMove: _MouseEvent;
onMouseDown: _MouseEvent;
onMouseUp: _MouseEvent;
onMouseEnter: _MouseEvent;
onMouseLeave: _MouseEvent;
}
const getOffset = (domEl?: HTMLElement) => {
if (!domEl)
return {
top: 0,
left: 0,
};
const box = domEl.getBoundingClientRect();
const body = document.body;
const docEl = document.documentElement;
const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
const clientTop = docEl.clientTop || body.clientTop || 0;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const top = box.top + scrollTop - clientTop;
const left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
};
export const useMouseInput = () => {
const [domEl, setDomEl] = useState<HTMLDivElement>();
const [callbacks, setCallbacks] = useState<MouseEvents>();
const mouseEventToCoords = useCallback(
(e: MouseEvent) => {
const offset = getOffset(domEl);
return new Coords(
e.clientX - offset.left + window.scrollX,
e.clientY - offset.top + window.scrollY
);
},
[domEl]
);
const parseMousePosition = useCallback(
(e: MouseEvent, mousedown: Coords | null) => {
const current = mouseEventToCoords(e);
const delta = mousedown
? new Coords(current.x - mousedown.x, current.y - mousedown.y)
: null;
return {
position: new Coords(current.x, current.y),
delta,
};
},
[mouseEventToCoords]
);
useEffect(() => {
if (!callbacks || !domEl) return;
let lastPosition: Coords | null = null;
const onMouseDown = (e: MouseEvent) => {
callbacks.onMouseDown(parseMousePosition(e, lastPosition));
lastPosition = mouseEventToCoords(e);
};
const onMouseUp = (e: MouseEvent) => {
callbacks.onMouseUp(parseMousePosition(e, lastPosition));
lastPosition = mouseEventToCoords(e);
};
const onMouseMove = (e: MouseEvent) => {
callbacks.onMouseMove(parseMousePosition(e, lastPosition));
lastPosition = mouseEventToCoords(e);
};
const onMouseEnter = (e: MouseEvent) => {
callbacks.onMouseEnter(parseMousePosition(e, lastPosition));
lastPosition = mouseEventToCoords(e);
};
const onMouseLeave = (e: MouseEvent) => {
callbacks.onMouseLeave(parseMousePosition(e, lastPosition));
lastPosition = mouseEventToCoords(e);
};
domEl.addEventListener("mousemove", onMouseMove);
domEl.addEventListener("mousedown", onMouseDown);
domEl.addEventListener("mouseup", onMouseUp);
domEl.addEventListener("mouseenter", onMouseEnter);
domEl.addEventListener("mouseleave", onMouseLeave);
return () => {
domEl.removeEventListener("mousemove", onMouseMove);
domEl.removeEventListener("mousedown", onMouseDown);
domEl.removeEventListener("mouseup", onMouseUp);
domEl.removeEventListener("mouseenter", onMouseEnter);
domEl.removeEventListener("mouseleave", onMouseLeave);
};
}, [callbacks, domEl]);
return { setCallbacks, setDomEl };
};

View File

@@ -1,9 +1,16 @@
import { makeAutoObservable } from "mobx";
import paper from "paper";
import { Renderer } from "../renderer/Renderer";
import { Coords } from "../renderer/elements/Coords";
import { ModeBase } from "./ModeBase";
import type { Mouse, OnSceneChange } from "../types";
const MOUSE_EVENTS = new Map([
["mousemove", "MOUSE_MOVE"],
["mousedown", "MOUSE_DOWN"],
["mouseup", "MOUSE_UP"],
]);
export class ModeManager {
// mobx requires all properties to be initialised explicitly (i.e. prop = undefined)
renderer?: Renderer = undefined;
@@ -17,13 +24,24 @@ export class ModeManager {
delta: null,
};
emitEvent?: OnSceneChange;
tool?: paper.Tool;
constructor() {
makeAutoObservable(this);
this.onMouseEvent = this.onMouseEvent.bind(this);
this.send = this.send.bind(this);
}
setRenderer(renderer: Renderer) {
this.renderer = renderer;
this.tool = new paper.Tool();
this.tool.onMouseMove = this.onMouseEvent;
this.tool.onMouseDown = this.onMouseEvent;
this.tool.onMouseUp = this.onMouseEvent;
this.tool.onKeyDown = this.onMouseEvent;
this.tool.onKeyUp = this.onMouseEvent;
}
setEventEmitter(fn: OnSceneChange) {
@@ -34,6 +52,8 @@ export class ModeManager {
Mode: T,
init?: (instance: InstanceType<T>) => void
) {
console.log("ACTIVATING MODE", Mode.name);
if (!this.renderer) return;
if (this.currentMode) {
@@ -54,10 +74,16 @@ export class ModeManager {
this.currentMode.instance.entry(this.mouse);
}
onMouseEvent(eventName: string, mouse: Mouse) {
this.mouse = mouse;
onMouseEvent(event: paper.ToolEvent) {
const type = MOUSE_EVENTS.get(event.type);
this.send(eventName, mouse);
if (!type) return;
const position = new Coords(event.point.x, event.point.y);
const delta = new Coords(event.delta.x, event.delta.y);
this.mouse = { position, delta };
this.send(type, this.mouse);
}
send(eventName: string, params?: any) {

View File

@@ -33,7 +33,7 @@ export class Pan extends ModeBase {
MOUSE_MOVE(mouse: Mouse) {
if (this.isPanning && mouse.delta !== null) {
this.ctx.renderer.scrollToDelta(mouse.delta.x, mouse.delta.y);
this.ctx.renderer.scrollToDelta(mouse.delta);
}
}
}

View File

@@ -69,7 +69,6 @@ export class Select extends ModeBase {
if (this.dragStartTile && !currentTile.isEqual(this.dragStartTile)) {
this.ctx.activateMode(CreateLasso, (mode) => {
console.log(this.dragStartTile);
this.dragStartTile && mode.setStartTile(this.dragStartTile);
mode.MOUSE_MOVE(mouse);
});

View File

@@ -37,17 +37,14 @@ export class Renderer {
container: HTMLDivElement;
canvas: HTMLCanvasElement;
};
scrollPosition = {
x: 0,
y: 0,
};
scrollPosition = new Coords(0, 0);
rafRef?: number;
constructor(containerEl: HTMLDivElement) {
makeAutoObservable(this);
Paper.settings = {
insertelements: false,
insertItems: false,
applyMatrix: false,
};
@@ -57,7 +54,7 @@ export class Renderer {
this.domElements = {
container: containerEl,
...this.initDOM(containerEl),
canvas: this.initDOM(containerEl).canvas,
};
Paper.setup(this.domElements.canvas);
@@ -83,7 +80,7 @@ export class Renderer {
this.activeLayer = Paper.project.activeLayer;
this.activeLayer.addChild(this.groups.container);
this.scrollTo(0, 0);
this.scrollTo(new Coords(0, 0));
this.render();
@@ -102,6 +99,8 @@ export class Renderer {
scene.nodes.forEach((node) => {
this.sceneElements.nodes.addNode(node);
});
this.setZoom(1);
}
getIconById(id: string) {
@@ -131,13 +130,17 @@ export class Renderer {
const halfW = PROJECTED_TILE_WIDTH / 2;
const halfH = PROJECTED_TILE_HEIGHT / 2;
const mouseX =
(mouse.x - this.groups.elements.position.x) * (1 / this.zoom);
const mouseY =
(mouse.y - this.groups.elements.position.y) * (1 / this.zoom) + halfH;
const canvasPosition = new Coords(
mouse.x - this.groups.elements.position.x,
mouse.y - this.groups.elements.position.y + halfH
);
const row = Math.floor((mouseX / halfW + mouseY / halfH) / 2);
const col = Math.floor((mouseY / halfH - mouseX / halfW) / 2);
const row = Math.floor(
(canvasPosition.x / halfW + canvasPosition.y / halfH) / 2
);
const col = Math.floor(
(canvasPosition.y / halfH - canvasPosition.x / halfW) / 2
);
const halfRowNum = Math.floor(this.sceneElements.grid.size.x * 0.5);
const halfColNum = Math.floor(this.sceneElements.grid.size.y * 0.5);
@@ -208,6 +211,9 @@ export class Renderer {
gsap.to(Paper.view, {
duration: 0.3,
zoom: this.zoom,
onComplete: () => {
this.scrollTo(this.scrollPosition);
},
});
this.emitEvent({
@@ -216,15 +222,15 @@ export class Renderer {
});
}
scrollTo(x: number, y: number) {
this.scrollPosition = { x, y };
scrollTo(coords: Coords) {
this.scrollPosition.set(coords.x, coords.y);
const { center: viewCenter } = Paper.view.bounds;
const newPosition = {
x: x + viewCenter.x,
y: y + viewCenter.y,
};
const newPosition = new Coords(
coords.x + viewCenter.x,
coords.y + viewCenter.y
);
gsap.to(this.groups.elements.position, {
duration: 0,
@@ -232,11 +238,10 @@ export class Renderer {
});
}
scrollToDelta(deltaX: number, deltaY: number) {
this.scrollTo(
this.scrollPosition.x + deltaX * (1 / this.zoom),
this.scrollPosition.y + deltaY * (1 / this.zoom)
);
scrollToDelta(delta: Coords) {
const position = this.scrollPosition.add(delta);
this.scrollTo(position);
}
unfocusAll() {