parent
facde7ace0
commit
566e6a5ede
@ -4,14 +4,16 @@ import { getDefaultAppState } from "../appState";
|
|||||||
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getNormalizedZoom, normalizeScroll } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { AppState, FlooredNumber } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds } from "../element";
|
||||||
|
import { getNewZoom } from "../scene/zoom";
|
||||||
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@ -84,7 +86,11 @@ export const actionZoomIn = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
zoom: getNormalizedZoom(appState.zoom + ZOOM_STEP),
|
zoom: getNewZoom(
|
||||||
|
getNormalizedZoom(appState.zoom.value + ZOOM_STEP),
|
||||||
|
appState.zoom,
|
||||||
|
{ x: appState.width / 2, y: appState.height / 2 },
|
||||||
|
),
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -111,7 +117,11 @@ export const actionZoomOut = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
zoom: getNormalizedZoom(appState.zoom - ZOOM_STEP),
|
zoom: getNewZoom(
|
||||||
|
getNormalizedZoom(appState.zoom.value - ZOOM_STEP),
|
||||||
|
appState.zoom,
|
||||||
|
{ x: appState.width / 2, y: appState.height / 2 },
|
||||||
|
),
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -138,7 +148,10 @@ export const actionResetZoom = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
zoom: 1,
|
zoom: getNewZoom(1 as NormalizedZoomValue, appState.zoom, {
|
||||||
|
x: appState.width / 2,
|
||||||
|
y: appState.height / 2,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -159,40 +172,23 @@ export const actionResetZoom = register({
|
|||||||
(event[KEYS.CTRL_OR_CMD] || event.shiftKey),
|
(event[KEYS.CTRL_OR_CMD] || event.shiftKey),
|
||||||
});
|
});
|
||||||
|
|
||||||
const calculateZoom = (
|
const zoomValueToFitBoundsOnViewport = (
|
||||||
commonBounds: number[],
|
bounds: [number, number, number, number],
|
||||||
currentZoom: number,
|
viewportDimensions: { width: number; height: number },
|
||||||
{
|
) => {
|
||||||
scrollX,
|
const [x1, y1, x2, y2] = bounds;
|
||||||
scrollY,
|
const commonBoundsWidth = x2 - x1;
|
||||||
}: {
|
const zoomValueForWidth = viewportDimensions.width / commonBoundsWidth;
|
||||||
scrollX: FlooredNumber;
|
const commonBoundsHeight = y2 - y1;
|
||||||
scrollY: FlooredNumber;
|
const zoomValueForHeight = viewportDimensions.height / commonBoundsHeight;
|
||||||
},
|
const smallestZoomValue = Math.min(zoomValueForWidth, zoomValueForHeight);
|
||||||
): number => {
|
const zoomAdjustedToSteps =
|
||||||
const { innerWidth, innerHeight } = window;
|
Math.floor(smallestZoomValue / ZOOM_STEP) * ZOOM_STEP;
|
||||||
const [x, y] = commonBounds;
|
const clampedZoomValueToFitElements = Math.min(
|
||||||
const zoomX = -innerWidth / (2 * scrollX + 2 * x - innerWidth);
|
Math.max(zoomAdjustedToSteps, ZOOM_STEP),
|
||||||
const zoomY = -innerHeight / (2 * scrollY + 2 * y - innerHeight);
|
1,
|
||||||
const margin = 0.01;
|
);
|
||||||
let newZoom;
|
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
||||||
|
|
||||||
if (zoomX < zoomY) {
|
|
||||||
newZoom = zoomX - margin;
|
|
||||||
} else if (zoomY <= zoomX) {
|
|
||||||
newZoom = zoomY - margin;
|
|
||||||
} else {
|
|
||||||
newZoom = currentZoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newZoom <= 0.1) {
|
|
||||||
return 0.1;
|
|
||||||
}
|
|
||||||
if (newZoom >= 1) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newZoom;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionZoomToFit = register({
|
export const actionZoomToFit = register({
|
||||||
@ -200,22 +196,29 @@ export const actionZoomToFit = register({
|
|||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const nonDeletedElements = elements.filter((element) => !element.isDeleted);
|
const nonDeletedElements = elements.filter((element) => !element.isDeleted);
|
||||||
const commonBounds = getCommonBounds(nonDeletedElements);
|
const commonBounds = getCommonBounds(nonDeletedElements);
|
||||||
|
|
||||||
|
const zoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
});
|
||||||
|
const newZoom = getNewZoom(zoomValue, appState.zoom);
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = commonBounds;
|
const [x1, y1, x2, y2] = commonBounds;
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
const scrollX = normalizeScroll(appState.width / 2 - centerX);
|
|
||||||
const scrollY = normalizeScroll(appState.height / 2 - centerY);
|
|
||||||
const zoom = calculateZoom(commonBounds, appState.zoom, {
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
scrollX,
|
...centerScrollOn({
|
||||||
scrollY,
|
scenePoint: { x: centerX, y: centerY },
|
||||||
zoom,
|
viewportDimensions: {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
},
|
||||||
|
zoom: newZoom,
|
||||||
|
}),
|
||||||
|
zoom: newZoom,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { Avatar } from "../components/Avatar";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getClientColors, getClientInitials } from "../clients";
|
import { getClientColors, getClientInitials } from "../clients";
|
||||||
import { Collaborator } from "../types";
|
import { Collaborator } from "../types";
|
||||||
import { normalizeScroll } from "../scene";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
|
|
||||||
export const actionGoToCollaborator = register({
|
export const actionGoToCollaborator = register({
|
||||||
name: "goToCollaborator",
|
name: "goToCollaborator",
|
||||||
@ -16,8 +16,14 @@ export const actionGoToCollaborator = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
scrollX: normalizeScroll(appState.width / 2 - point.x),
|
...centerScrollOn({
|
||||||
scrollY: normalizeScroll(appState.height / 2 - point.y),
|
scenePoint: point,
|
||||||
|
viewportDimensions: {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
},
|
||||||
|
zoom: appState.zoom,
|
||||||
|
}),
|
||||||
// Close mobile menu
|
// Close mobile menu
|
||||||
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu,
|
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu,
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { AppState, FlooredNumber } from "./types";
|
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
||||||
import { getDateTime } from "./utils";
|
import { getDateTime } from "./utils";
|
||||||
import { t } from "./i18n";
|
import { t } from "./i18n";
|
||||||
import {
|
import {
|
||||||
@ -53,7 +53,10 @@ export const getDefaultAppState = (): Omit<
|
|||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
zoom: 1,
|
zoom: {
|
||||||
|
value: 1 as NormalizedZoomValue,
|
||||||
|
translation: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
openMenu: null,
|
openMenu: null,
|
||||||
lastPointerDownWith: "mouse",
|
lastPointerDownWith: "mouse",
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import {
|
import {
|
||||||
@ -183,14 +183,16 @@ export const ZoomActions = ({
|
|||||||
zoom,
|
zoom,
|
||||||
}: {
|
}: {
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
zoom: number;
|
zoom: Zoom;
|
||||||
}) => (
|
}) => (
|
||||||
<Stack.Col gap={1}>
|
<Stack.Col gap={1}>
|
||||||
<Stack.Row gap={1} align="center">
|
<Stack.Row gap={1} align="center">
|
||||||
{renderAction("zoomIn")}
|
{renderAction("zoomIn")}
|
||||||
{renderAction("zoomOut")}
|
{renderAction("zoomOut")}
|
||||||
{renderAction("resetZoom")}
|
{renderAction("resetZoom")}
|
||||||
<div style={{ marginInlineStart: 4 }}>{(zoom * 100).toFixed(0)}%</div>
|
<div style={{ marginInlineStart: 4 }}>
|
||||||
|
{(zoom.value * 100).toFixed(0)}%
|
||||||
|
</div>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
);
|
||||||
|
@ -180,6 +180,7 @@ import {
|
|||||||
saveToFirebase,
|
saveToFirebase,
|
||||||
isSavedToFirebase,
|
isSavedToFirebase,
|
||||||
} from "../data/firebase";
|
} from "../data/firebase";
|
||||||
|
import { getNewZoom } from "../scene/zoom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param func handler taking at most single parameter (event).
|
* @param func handler taking at most single parameter (event).
|
||||||
@ -935,8 +936,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
sceneY: user.pointer.y,
|
sceneY: user.pointer.y,
|
||||||
},
|
},
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
cursorButton[socketId] = user.button;
|
cursorButton[socketId] = user.button;
|
||||||
});
|
});
|
||||||
@ -1146,8 +1145,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const { x, y } = viewportCoordsToSceneCoords(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
{ clientX, clientY },
|
{ clientX, clientY },
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const dx = x - elementsCenterX;
|
const dx = x - elementsCenterX;
|
||||||
@ -1205,8 +1202,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const { x, y } = viewportCoordsToSceneCoords(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
{ clientX: cursorX, clientY: cursorY },
|
{ clientX: cursorX, clientY: cursorY },
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const element = newTextElement({
|
const element = newTextElement({
|
||||||
@ -1719,15 +1714,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
});
|
});
|
||||||
gesture.initialScale = this.state.zoom;
|
gesture.initialScale = this.state.zoom.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
private onGestureChange = withBatchedUpdates((event: GestureEvent) => {
|
private onGestureChange = withBatchedUpdates((event: GestureEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const gestureCenter = getCenter(gesture.pointers);
|
||||||
this.setState({
|
this.setState(({ zoom }) => ({
|
||||||
zoom: getNormalizedZoom(gesture.initialScale! * event.scale),
|
zoom: getNewZoom(
|
||||||
});
|
getNormalizedZoom(gesture.initialScale! * event.scale),
|
||||||
|
zoom,
|
||||||
|
gestureCenter,
|
||||||
|
),
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
|
private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
|
||||||
@ -1771,8 +1770,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
sceneY: y,
|
sceneY: y,
|
||||||
},
|
},
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
return [viewportX, viewportY];
|
return [viewportX, viewportY];
|
||||||
},
|
},
|
||||||
@ -1990,8 +1987,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
event,
|
event,
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedGroupIds = getSelectedGroupIds(this.state);
|
const selectedGroupIds = getSelectedGroupIds(this.state);
|
||||||
@ -2051,12 +2046,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const distance = getDistance(Array.from(gesture.pointers.values()));
|
const distance = getDistance(Array.from(gesture.pointers.values()));
|
||||||
const scaleFactor = distance / gesture.initialDistance!;
|
const scaleFactor = distance / gesture.initialDistance!;
|
||||||
|
|
||||||
this.setState({
|
this.setState(({ zoom, scrollX, scrollY }) => ({
|
||||||
scrollX: normalizeScroll(this.state.scrollX + deltaX / this.state.zoom),
|
scrollX: normalizeScroll(scrollX + deltaX / zoom.value),
|
||||||
scrollY: normalizeScroll(this.state.scrollY + deltaY / this.state.zoom),
|
scrollY: normalizeScroll(scrollY + deltaY / zoom.value),
|
||||||
zoom: getNormalizedZoom(gesture.initialScale! * scaleFactor),
|
zoom: getNewZoom(
|
||||||
|
getNormalizedZoom(gesture.initialScale! * scaleFactor),
|
||||||
|
zoom,
|
||||||
|
center,
|
||||||
|
),
|
||||||
shouldCacheIgnoreZoom: true,
|
shouldCacheIgnoreZoom: true,
|
||||||
});
|
}));
|
||||||
this.resetShouldCacheIgnoreZoomDebounced();
|
this.resetShouldCacheIgnoreZoomDebounced();
|
||||||
} else {
|
} else {
|
||||||
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
|
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
|
||||||
@ -2079,12 +2078,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scenePointer = viewportCoordsToSceneCoords(
|
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
||||||
event,
|
|
||||||
this.state,
|
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
|
||||||
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -2453,8 +2447,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollX: normalizeScroll(this.state.scrollX - deltaX / this.state.zoom),
|
scrollX: normalizeScroll(
|
||||||
scrollY: normalizeScroll(this.state.scrollY - deltaY / this.state.zoom),
|
this.state.scrollX - deltaX / this.state.zoom.value,
|
||||||
|
),
|
||||||
|
scrollY: normalizeScroll(
|
||||||
|
this.state.scrollY - deltaY / this.state.zoom.value,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const teardown = withBatchedUpdates(
|
const teardown = withBatchedUpdates(
|
||||||
@ -2491,7 +2489,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
if (gesture.pointers.size === 2) {
|
if (gesture.pointers.size === 2) {
|
||||||
gesture.lastCenter = getCenter(gesture.pointers);
|
gesture.lastCenter = getCenter(gesture.pointers);
|
||||||
gesture.initialScale = this.state.zoom;
|
gesture.initialScale = this.state.zoom.value;
|
||||||
gesture.initialDistance = getDistance(
|
gesture.initialDistance = getDistance(
|
||||||
Array.from(gesture.pointers.values()),
|
Array.from(gesture.pointers.values()),
|
||||||
);
|
);
|
||||||
@ -2501,12 +2499,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
private initialPointerDownState(
|
private initialPointerDownState(
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
): PointerDownState {
|
): PointerDownState {
|
||||||
const origin = viewportCoordsToSceneCoords(
|
const origin = viewportCoordsToSceneCoords(event, this.state);
|
||||||
event,
|
|
||||||
this.state,
|
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
this.scene.getElements(),
|
this.scene.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
@ -2790,7 +2783,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// How many pixels off the shape boundary we still consider a hit
|
// How many pixels off the shape boundary we still consider a hit
|
||||||
const threshold = 10 / this.state.zoom;
|
const threshold = 10 / this.state.zoom.value;
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||||
return (
|
return (
|
||||||
point.x > x1 - threshold &&
|
point.x > x1 - threshold &&
|
||||||
@ -2985,12 +2978,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointerCoords = viewportCoordsToSceneCoords(
|
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
||||||
event,
|
|
||||||
this.state,
|
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
@ -3212,7 +3200,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
mutateElement(draggingElement, {
|
mutateElement(draggingElement, {
|
||||||
points: simplify(
|
points: simplify(
|
||||||
[...(points as Point[]), [dx, dy]],
|
[...(points as Point[]), [dx, dy]],
|
||||||
0.7 / this.state.zoom,
|
0.7 / this.state.zoom.value,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -3300,7 +3288,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const dx = x - pointerDownState.lastCoords.x;
|
const dx = x - pointerDownState.lastCoords.x;
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
|
scrollX: normalizeScroll(
|
||||||
|
this.state.scrollX - dx / this.state.zoom.value,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.x = x;
|
pointerDownState.lastCoords.x = x;
|
||||||
return true;
|
return true;
|
||||||
@ -3310,7 +3300,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
const dy = y - pointerDownState.lastCoords.y;
|
const dy = y - pointerDownState.lastCoords.y;
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
|
scrollY: normalizeScroll(
|
||||||
|
this.state.scrollY - dy / this.state.zoom.value,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.y = y;
|
pointerDownState.lastCoords.y = y;
|
||||||
return true;
|
return true;
|
||||||
@ -3387,8 +3379,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const pointerCoords = viewportCoordsToSceneCoords(
|
const pointerCoords = viewportCoordsToSceneCoords(
|
||||||
childEvent,
|
childEvent,
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -3808,8 +3798,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const { x, y } = viewportCoordsToSceneCoords(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
{ clientX, clientY },
|
{ clientX, clientY },
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const elements = this.scene.getElements();
|
const elements = this.scene.getElements();
|
||||||
@ -3885,7 +3873,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
const { deltaX, deltaY } = event;
|
const { deltaX, deltaY } = event;
|
||||||
const { selectedElementIds, previousSelectedElementIds } = this.state;
|
const { selectedElementIds, previousSelectedElementIds } = this.state;
|
||||||
|
|
||||||
// note that event.ctrlKey is necessary to handle pinch zooming
|
// note that event.ctrlKey is necessary to handle pinch zooming
|
||||||
if (event.metaKey || event.ctrlKey) {
|
if (event.metaKey || event.ctrlKey) {
|
||||||
const sign = Math.sign(deltaY);
|
const sign = Math.sign(deltaY);
|
||||||
@ -3903,8 +3890,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(({ zoom }) => ({
|
this.setState(({ zoom }) => ({
|
||||||
zoom: getNormalizedZoom(zoom - delta / 100),
|
zoom: getNewZoom(getNormalizedZoom(zoom.value - delta / 100), zoom, {
|
||||||
|
x: cursorX,
|
||||||
|
y: cursorY,
|
||||||
|
}),
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
previousSelectedElementIds:
|
previousSelectedElementIds:
|
||||||
Object.keys(selectedElementIds).length !== 0
|
Object.keys(selectedElementIds).length !== 0
|
||||||
@ -3920,14 +3911,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.setState(({ zoom, scrollX }) => ({
|
this.setState(({ zoom, scrollX }) => ({
|
||||||
// on Mac, shift+wheel tends to result in deltaX
|
// on Mac, shift+wheel tends to result in deltaX
|
||||||
scrollX: normalizeScroll(scrollX - (deltaY || deltaX) / zoom),
|
scrollX: normalizeScroll(scrollX - (deltaY || deltaX) / zoom.value),
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(({ zoom, scrollX, scrollY }) => ({
|
this.setState(({ zoom, scrollX, scrollY }) => ({
|
||||||
scrollX: normalizeScroll(scrollX - deltaX / zoom),
|
scrollX: normalizeScroll(scrollX - deltaX / zoom.value),
|
||||||
scrollY: normalizeScroll(scrollY - deltaY / zoom),
|
scrollY: normalizeScroll(scrollY - deltaY / zoom.value),
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3960,8 +3951,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: elementCenterX, sceneY: elementCenterY },
|
{ sceneX: elementCenterX, sceneY: elementCenterY },
|
||||||
appState,
|
appState,
|
||||||
canvas,
|
|
||||||
scale,
|
|
||||||
);
|
);
|
||||||
return { viewportX, viewportY, elementCenterX, elementCenterY };
|
return { viewportX, viewportY, elementCenterX, elementCenterY };
|
||||||
}
|
}
|
||||||
@ -3975,8 +3964,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const pointer = viewportCoordsToSceneCoords(
|
const pointer = viewportCoordsToSceneCoords(
|
||||||
{ clientX: x, clientY: y },
|
{ clientX: x, clientY: y },
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNaN(pointer.x) || isNaN(pointer.y)) {
|
if (isNaN(pointer.x) || isNaN(pointer.y)) {
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
FontFamily,
|
FontFamily,
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { DataState, ImportedDataState } from "./types";
|
import { DataState, ImportedDataState } from "./types";
|
||||||
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
|
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
|
||||||
import { isLinearElementType } from "../element/typeChecks";
|
import { isLinearElementType } from "../element/typeChecks";
|
||||||
@ -161,6 +161,14 @@ const restoreAppState = (
|
|||||||
...nextAppState,
|
...nextAppState,
|
||||||
offsetLeft: appState.offsetLeft || 0,
|
offsetLeft: appState.offsetLeft || 0,
|
||||||
offsetTop: appState.offsetTop || 0,
|
offsetTop: appState.offsetTop || 0,
|
||||||
|
/* Migrates from previous version where appState.zoom was a number */
|
||||||
|
zoom:
|
||||||
|
typeof appState.zoom === "number"
|
||||||
|
? {
|
||||||
|
value: appState.zoom as NormalizedZoomValue,
|
||||||
|
translation: defaultAppState.zoom.translation,
|
||||||
|
}
|
||||||
|
: appState.zoom || defaultAppState.zoom,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export const hitTest = (
|
|||||||
y: number,
|
y: number,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// How many pixels off the shape boundary we still consider a hit
|
// How many pixels off the shape boundary we still consider a hit
|
||||||
const threshold = 10 / appState.zoom;
|
const threshold = 10 / appState.zoom.value;
|
||||||
const point: Point = [x, y];
|
const point: Point = [x, y];
|
||||||
|
|
||||||
if (isElementSelected(appState, element)) {
|
if (isElementSelected(appState, element)) {
|
||||||
@ -60,7 +60,7 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
|
|||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const threshold = 10 / appState.zoom;
|
const threshold = 10 / appState.zoom.value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!isHittingElementNotConsideringBoundingBox(element, appState, [x, y]) &&
|
!isHittingElementNotConsideringBoundingBox(element, appState, [x, y]) &&
|
||||||
@ -73,7 +73,7 @@ const isHittingElementNotConsideringBoundingBox = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
point: Point,
|
point: Point,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const threshold = 10 / appState.zoom;
|
const threshold = 10 / appState.zoom.value;
|
||||||
|
|
||||||
const check =
|
const check =
|
||||||
element.type === "text"
|
element.type === "text"
|
||||||
|
@ -384,7 +384,7 @@ export class LinearElementEditor {
|
|||||||
while (--idx > -1) {
|
while (--idx > -1) {
|
||||||
const point = pointHandles[idx];
|
const point = pointHandles[idx];
|
||||||
if (
|
if (
|
||||||
distance2d(x, y, point[0], point[1]) * zoom <
|
distance2d(x, y, point[0], point[1]) * zoom.value <
|
||||||
// +1px to account for outline stroke
|
// +1px to account for outline stroke
|
||||||
this.POINT_HANDLE_SIZE / 2 + 1
|
this.POINT_HANDLE_SIZE / 2 + 1
|
||||||
) {
|
) {
|
||||||
|
@ -148,7 +148,6 @@ const getAdjustedDimensions = (
|
|||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
baseline: nextBaseline,
|
baseline: nextBaseline,
|
||||||
} = measureText(nextText, getFontString(element));
|
} = measureText(nextText, getFontString(element));
|
||||||
|
|
||||||
const { textAlign, verticalAlign } = element;
|
const { textAlign, verticalAlign } = element;
|
||||||
|
|
||||||
let x, y;
|
let x, y;
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
TransformHandle,
|
TransformHandle,
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { AppState } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
|
|
||||||
const isInsideTransformHandle = (
|
const isInsideTransformHandle = (
|
||||||
transformHandle: TransformHandle,
|
transformHandle: TransformHandle,
|
||||||
@ -29,7 +29,7 @@ export const resizeTest = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
zoom: number,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
): MaybeTransformHandleType => {
|
): MaybeTransformHandleType => {
|
||||||
if (!appState.selectedElementIds[element.id]) {
|
if (!appState.selectedElementIds[element.id]) {
|
||||||
@ -70,7 +70,7 @@ export const getElementWithTransformHandleType = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
zoom: number,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
) => {
|
) => {
|
||||||
return elements.reduce((result, element) => {
|
return elements.reduce((result, element) => {
|
||||||
@ -93,7 +93,7 @@ export const getTransformHandleTypeFromCoords = (
|
|||||||
[x1, y1, x2, y2]: readonly [number, number, number, number],
|
[x1, y1, x2, y2]: readonly [number, number, number, number],
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
zoom: number,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
): MaybeTransformHandleType => {
|
): MaybeTransformHandleType => {
|
||||||
const transformHandles = getTransformHandlesFromCoords(
|
const transformHandles = getTransformHandlesFromCoords(
|
||||||
|
@ -26,9 +26,9 @@ const getTransform = (
|
|||||||
const degree = (180 * angle) / Math.PI;
|
const degree = (180 * angle) / Math.PI;
|
||||||
// offsets must be multiplied by 2 to account for the division by 2 of
|
// offsets must be multiplied by 2 to account for the division by 2 of
|
||||||
// the whole expression afterwards
|
// the whole expression afterwards
|
||||||
return `translate(${((width - offsetLeft * 2) * (zoom - 1)) / 2}px, ${
|
return `translate(${((width - offsetLeft * 2) * (zoom.value - 1)) / 2}px, ${
|
||||||
((height - offsetTop * 2) * (zoom - 1)) / 2
|
((height - offsetTop * 2) * (zoom.value - 1)) / 2
|
||||||
}px) scale(${zoom}) rotate(${degree}deg)`;
|
}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const textWysiwyg = ({
|
export const textWysiwyg = ({
|
||||||
|
@ -2,6 +2,7 @@ import { ExcalidrawElement, PointerType } from "./types";
|
|||||||
|
|
||||||
import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
|
import { Zoom } from "../types";
|
||||||
|
|
||||||
export type TransformHandleType =
|
export type TransformHandleType =
|
||||||
| "n"
|
| "n"
|
||||||
@ -76,25 +77,25 @@ const generateTransformHandle = (
|
|||||||
export const getTransformHandlesFromCoords = (
|
export const getTransformHandlesFromCoords = (
|
||||||
[x1, y1, x2, y2]: Bounds,
|
[x1, y1, x2, y2]: Bounds,
|
||||||
angle: number,
|
angle: number,
|
||||||
zoom: number,
|
zoom: Zoom,
|
||||||
pointerType: PointerType = "mouse",
|
pointerType: PointerType = "mouse",
|
||||||
omitSides: { [T in TransformHandleType]?: boolean } = {},
|
omitSides: { [T in TransformHandleType]?: boolean } = {},
|
||||||
): TransformHandles => {
|
): TransformHandles => {
|
||||||
const size = transformHandleSizes[pointerType];
|
const size = transformHandleSizes[pointerType];
|
||||||
const handleWidth = size / zoom;
|
const handleWidth = size / zoom.value;
|
||||||
const handleHeight = size / zoom;
|
const handleHeight = size / zoom.value;
|
||||||
|
|
||||||
const handleMarginX = size / zoom;
|
const handleMarginX = size / zoom.value;
|
||||||
const handleMarginY = size / zoom;
|
const handleMarginY = size / zoom.value;
|
||||||
|
|
||||||
const width = x2 - x1;
|
const width = x2 - x1;
|
||||||
const height = y2 - y1;
|
const height = y2 - y1;
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
|
|
||||||
const dashedLineMargin = 4 / zoom;
|
const dashedLineMargin = 4 / zoom.value;
|
||||||
|
|
||||||
const centeringOffset = (size - 8) / (2 * zoom);
|
const centeringOffset = (size - 8) / (2 * zoom.value);
|
||||||
|
|
||||||
const transformHandles: TransformHandles = {
|
const transformHandles: TransformHandles = {
|
||||||
nw: omitSides["nw"]
|
nw: omitSides["nw"]
|
||||||
@ -149,7 +150,7 @@ export const getTransformHandlesFromCoords = (
|
|||||||
dashedLineMargin -
|
dashedLineMargin -
|
||||||
handleMarginY +
|
handleMarginY +
|
||||||
centeringOffset -
|
centeringOffset -
|
||||||
ROTATION_RESIZE_HANDLE_GAP / zoom,
|
ROTATION_RESIZE_HANDLE_GAP / zoom.value,
|
||||||
handleWidth,
|
handleWidth,
|
||||||
handleHeight,
|
handleHeight,
|
||||||
cx,
|
cx,
|
||||||
@ -159,7 +160,7 @@ export const getTransformHandlesFromCoords = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// We only want to show height handles (all cardinal directions) above a certain size
|
// We only want to show height handles (all cardinal directions) above a certain size
|
||||||
const minimumSizeForEightHandles = (5 * size) / zoom;
|
const minimumSizeForEightHandles = (5 * size) / zoom.value;
|
||||||
if (Math.abs(width) > minimumSizeForEightHandles) {
|
if (Math.abs(width) > minimumSizeForEightHandles) {
|
||||||
if (!omitSides["n"]) {
|
if (!omitSides["n"]) {
|
||||||
transformHandles["n"] = generateTransformHandle(
|
transformHandles["n"] = generateTransformHandle(
|
||||||
@ -214,7 +215,7 @@ export const getTransformHandlesFromCoords = (
|
|||||||
|
|
||||||
export const getTransformHandles = (
|
export const getTransformHandles = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
zoom: number,
|
zoom: Zoom,
|
||||||
pointerType: PointerType = "mouse",
|
pointerType: PointerType = "mouse",
|
||||||
): TransformHandles => {
|
): TransformHandles => {
|
||||||
let omitSides: { [T in TransformHandleType]?: boolean } = {};
|
let omitSides: { [T in TransformHandleType]?: boolean } = {};
|
||||||
|
@ -23,6 +23,10 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { isPathALoop } from "../math";
|
import { isPathALoop } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
import { Zoom } from "../types";
|
||||||
|
import { getDefaultAppState } from "../appState";
|
||||||
|
|
||||||
|
const defaultAppState = getDefaultAppState();
|
||||||
|
|
||||||
const CANVAS_PADDING = 20;
|
const CANVAS_PADDING = 20;
|
||||||
|
|
||||||
@ -32,14 +36,14 @@ const DASHARRAY_DOTTED = [3, 6];
|
|||||||
export interface ExcalidrawElementWithCanvas {
|
export interface ExcalidrawElementWithCanvas {
|
||||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
canvasZoom: number;
|
canvasZoom: Zoom["value"];
|
||||||
canvasOffsetX: number;
|
canvasOffsetX: number;
|
||||||
canvasOffsetY: number;
|
canvasOffsetY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateElementCanvas = (
|
const generateElementCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
zoom: number,
|
zoom: Zoom,
|
||||||
): ExcalidrawElementWithCanvas => {
|
): ExcalidrawElementWithCanvas => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
@ -50,9 +54,11 @@ const generateElementCanvas = (
|
|||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element)) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
canvas.width =
|
canvas.width =
|
||||||
distance(x1, x2) * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
distance(x1, x2) * window.devicePixelRatio * zoom.value +
|
||||||
|
CANVAS_PADDING * 2;
|
||||||
canvas.height =
|
canvas.height =
|
||||||
distance(y1, y2) * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
distance(y1, y2) * window.devicePixelRatio * zoom.value +
|
||||||
|
CANVAS_PADDING * 2;
|
||||||
|
|
||||||
canvasOffsetX =
|
canvasOffsetX =
|
||||||
element.x > x1
|
element.x > x1
|
||||||
@ -62,25 +68,35 @@ const generateElementCanvas = (
|
|||||||
element.y > y1
|
element.y > y1
|
||||||
? Math.floor(distance(element.y, y1)) * window.devicePixelRatio
|
? Math.floor(distance(element.y, y1)) * window.devicePixelRatio
|
||||||
: 0;
|
: 0;
|
||||||
context.translate(canvasOffsetX * zoom, canvasOffsetY * zoom);
|
context.translate(canvasOffsetX * zoom.value, canvasOffsetY * zoom.value);
|
||||||
} else {
|
} else {
|
||||||
canvas.width =
|
canvas.width =
|
||||||
element.width * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
element.width * window.devicePixelRatio * zoom.value + CANVAS_PADDING * 2;
|
||||||
canvas.height =
|
canvas.height =
|
||||||
element.height * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
|
element.height * window.devicePixelRatio * zoom.value +
|
||||||
|
CANVAS_PADDING * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.translate(CANVAS_PADDING, CANVAS_PADDING);
|
context.translate(CANVAS_PADDING, CANVAS_PADDING);
|
||||||
context.scale(window.devicePixelRatio * zoom, window.devicePixelRatio * zoom);
|
context.scale(
|
||||||
|
window.devicePixelRatio * zoom.value,
|
||||||
|
window.devicePixelRatio * zoom.value,
|
||||||
|
);
|
||||||
|
|
||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
drawElementOnCanvas(element, rc, context);
|
drawElementOnCanvas(element, rc, context);
|
||||||
context.translate(-CANVAS_PADDING, -CANVAS_PADDING);
|
context.translate(-CANVAS_PADDING, -CANVAS_PADDING);
|
||||||
context.scale(
|
context.scale(
|
||||||
1 / (window.devicePixelRatio * zoom),
|
1 / (window.devicePixelRatio * zoom.value),
|
||||||
1 / (window.devicePixelRatio * zoom),
|
1 / (window.devicePixelRatio * zoom.value),
|
||||||
);
|
);
|
||||||
return { element, canvas, canvasZoom: zoom, canvasOffsetX, canvasOffsetY };
|
return {
|
||||||
|
element,
|
||||||
|
canvas,
|
||||||
|
canvasZoom: zoom.value,
|
||||||
|
canvasOffsetX,
|
||||||
|
canvasOffsetY,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawElementOnCanvas = (
|
const drawElementOnCanvas = (
|
||||||
@ -352,11 +368,11 @@ const generateElementWithCanvas = (
|
|||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
sceneState?: SceneState,
|
sceneState?: SceneState,
|
||||||
) => {
|
) => {
|
||||||
const zoom = sceneState ? sceneState.zoom : 1;
|
const zoom: Zoom = sceneState ? sceneState.zoom : defaultAppState.zoom;
|
||||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||||
const shouldRegenerateBecauseZoom =
|
const shouldRegenerateBecauseZoom =
|
||||||
prevElementWithCanvas &&
|
prevElementWithCanvas &&
|
||||||
prevElementWithCanvas.canvasZoom !== zoom &&
|
prevElementWithCanvas.canvasZoom !== zoom.value &&
|
||||||
!sceneState?.shouldCacheIgnoreZoom;
|
!sceneState?.shouldCacheIgnoreZoom;
|
||||||
if (!prevElementWithCanvas || shouldRegenerateBecauseZoom) {
|
if (!prevElementWithCanvas || shouldRegenerateBecauseZoom) {
|
||||||
const elementWithCanvas = generateElementCanvas(element, zoom);
|
const elementWithCanvas = generateElementCanvas(element, zoom);
|
||||||
|
@ -2,7 +2,7 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
|||||||
import { RoughSVG } from "roughjs/bin/svg";
|
import { RoughSVG } from "roughjs/bin/svg";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
|
|
||||||
import { FlooredNumber, AppState } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@ -47,6 +47,7 @@ import {
|
|||||||
TransformHandles,
|
TransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
|
import { viewportCoordsToSceneCoords } from "../utils";
|
||||||
|
|
||||||
const strokeRectWithRotation = (
|
const strokeRectWithRotation = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@ -147,7 +148,7 @@ const renderLinearPointHandles = (
|
|||||||
context.translate(sceneState.scrollX, sceneState.scrollY);
|
context.translate(sceneState.scrollX, sceneState.scrollY);
|
||||||
const origStrokeStyle = context.strokeStyle;
|
const origStrokeStyle = context.strokeStyle;
|
||||||
const lineWidth = context.lineWidth;
|
const lineWidth = context.lineWidth;
|
||||||
context.lineWidth = 1 / sceneState.zoom;
|
context.lineWidth = 1 / sceneState.zoom.value;
|
||||||
|
|
||||||
LinearElementEditor.getPointsGlobalCoordinates(element).forEach(
|
LinearElementEditor.getPointsGlobalCoordinates(element).forEach(
|
||||||
(point, idx) => {
|
(point, idx) => {
|
||||||
@ -162,7 +163,7 @@ const renderLinearPointHandles = (
|
|||||||
context,
|
context,
|
||||||
point[0],
|
point[0],
|
||||||
point[1],
|
point[1],
|
||||||
POINT_HANDLE_SIZE / 2 / sceneState.zoom,
|
POINT_HANDLE_SIZE / 2 / sceneState.zoom.value,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -226,36 +227,36 @@ export const renderScene = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
const zoomTranslationX = (-normalizedCanvasWidth * (sceneState.zoom - 1)) / 2;
|
const zoomTranslationX = sceneState.zoom.translation.x;
|
||||||
const zoomTranslationY =
|
const zoomTranslationY = sceneState.zoom.translation.y;
|
||||||
(-normalizedCanvasHeight * (sceneState.zoom - 1)) / 2;
|
|
||||||
context.translate(zoomTranslationX, zoomTranslationY);
|
context.translate(zoomTranslationX, zoomTranslationY);
|
||||||
context.scale(sceneState.zoom, sceneState.zoom);
|
context.scale(sceneState.zoom.value, sceneState.zoom.value);
|
||||||
|
|
||||||
// Grid
|
// Grid
|
||||||
if (renderGrid && appState.gridSize) {
|
if (renderGrid && appState.gridSize) {
|
||||||
strokeGrid(
|
strokeGrid(
|
||||||
context,
|
context,
|
||||||
appState.gridSize,
|
appState.gridSize,
|
||||||
-Math.ceil(zoomTranslationX / sceneState.zoom / appState.gridSize) *
|
-Math.ceil(zoomTranslationX / sceneState.zoom.value / appState.gridSize) *
|
||||||
appState.gridSize +
|
appState.gridSize +
|
||||||
(sceneState.scrollX % appState.gridSize),
|
(sceneState.scrollX % appState.gridSize),
|
||||||
-Math.ceil(zoomTranslationY / sceneState.zoom / appState.gridSize) *
|
-Math.ceil(zoomTranslationY / sceneState.zoom.value / appState.gridSize) *
|
||||||
appState.gridSize +
|
appState.gridSize +
|
||||||
(sceneState.scrollY % appState.gridSize),
|
(sceneState.scrollY % appState.gridSize),
|
||||||
normalizedCanvasWidth / sceneState.zoom,
|
normalizedCanvasWidth / sceneState.zoom.value,
|
||||||
normalizedCanvasHeight / sceneState.zoom,
|
normalizedCanvasHeight / sceneState.zoom.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint visible elements
|
// Paint visible elements
|
||||||
const visibleElements = elements.filter((element) =>
|
const visibleElements = elements.filter((element) =>
|
||||||
isVisibleElement(
|
isVisibleElement(element, normalizedCanvasWidth, normalizedCanvasHeight, {
|
||||||
element,
|
zoom: sceneState.zoom,
|
||||||
normalizedCanvasWidth,
|
offsetLeft: appState.offsetLeft,
|
||||||
normalizedCanvasHeight,
|
offsetTop: appState.offsetTop,
|
||||||
sceneState,
|
scrollX: sceneState.scrollX,
|
||||||
),
|
scrollY: sceneState.scrollY,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
visibleElements.forEach((element) => {
|
visibleElements.forEach((element) => {
|
||||||
@ -378,13 +379,13 @@ export const renderScene = (
|
|||||||
locallySelectedElements[0].angle,
|
locallySelectedElements[0].angle,
|
||||||
);
|
);
|
||||||
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
||||||
const dashedLinePadding = 4 / sceneState.zoom;
|
const dashedLinePadding = 4 / sceneState.zoom.value;
|
||||||
context.fillStyle = oc.white;
|
context.fillStyle = oc.white;
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
|
const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
|
||||||
const initialLineDash = context.getLineDash();
|
const initialLineDash = context.getLineDash();
|
||||||
context.setLineDash([2 / sceneState.zoom]);
|
context.setLineDash([2 / sceneState.zoom.value]);
|
||||||
const lineWidth = context.lineWidth;
|
const lineWidth = context.lineWidth;
|
||||||
context.lineWidth = 1 / sceneState.zoom;
|
context.lineWidth = 1 / sceneState.zoom.value;
|
||||||
strokeRectWithRotation(
|
strokeRectWithRotation(
|
||||||
context,
|
context,
|
||||||
x1 - dashedLinePadding,
|
x1 - dashedLinePadding,
|
||||||
@ -410,7 +411,7 @@ export const renderScene = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset zoom
|
// Reset zoom
|
||||||
context.scale(1 / sceneState.zoom, 1 / sceneState.zoom);
|
context.scale(1 / sceneState.zoom.value, 1 / sceneState.zoom.value);
|
||||||
context.translate(-zoomTranslationX, -zoomTranslationY);
|
context.translate(-zoomTranslationX, -zoomTranslationY);
|
||||||
|
|
||||||
// Paint remote pointers
|
// Paint remote pointers
|
||||||
@ -556,7 +557,7 @@ const renderTransformHandles = (
|
|||||||
const transformHandle = transformHandles[key as TransformHandleType];
|
const transformHandle = transformHandles[key as TransformHandleType];
|
||||||
if (transformHandle !== undefined) {
|
if (transformHandle !== undefined) {
|
||||||
const lineWidth = context.lineWidth;
|
const lineWidth = context.lineWidth;
|
||||||
context.lineWidth = 1 / sceneState.zoom;
|
context.lineWidth = 1 / sceneState.zoom.value;
|
||||||
if (key === "rotation") {
|
if (key === "rotation") {
|
||||||
fillCircle(
|
fillCircle(
|
||||||
context,
|
context,
|
||||||
@ -610,11 +611,11 @@ const renderSelectionBorder = (
|
|||||||
const lineDashOffset = context.lineDashOffset;
|
const lineDashOffset = context.lineDashOffset;
|
||||||
const strokeStyle = context.strokeStyle;
|
const strokeStyle = context.strokeStyle;
|
||||||
|
|
||||||
const dashedLinePadding = 4 / sceneState.zoom;
|
const dashedLinePadding = 4 / sceneState.zoom.value;
|
||||||
const dashWidth = 8 / sceneState.zoom;
|
const dashWidth = 8 / sceneState.zoom.value;
|
||||||
const spaceWidth = 4 / sceneState.zoom;
|
const spaceWidth = 4 / sceneState.zoom.value;
|
||||||
|
|
||||||
context.lineWidth = 1 / sceneState.zoom;
|
context.lineWidth = 1 / sceneState.zoom.value;
|
||||||
|
|
||||||
context.translate(sceneState.scrollX, sceneState.scrollY);
|
context.translate(sceneState.scrollX, sceneState.scrollY);
|
||||||
|
|
||||||
@ -749,32 +750,30 @@ const renderBindingHighlightForSuggestedPointBinding = (
|
|||||||
|
|
||||||
const isVisibleElement = (
|
const isVisibleElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
viewportWidth: number,
|
canvasWidth: number,
|
||||||
viewportHeight: number,
|
canvasHeight: number,
|
||||||
{
|
viewTransformations: {
|
||||||
scrollX,
|
zoom: Zoom;
|
||||||
scrollY,
|
offsetLeft: number;
|
||||||
zoom,
|
offsetTop: number;
|
||||||
}: {
|
scrollX: number;
|
||||||
scrollX: FlooredNumber;
|
scrollY: number;
|
||||||
scrollY: FlooredNumber;
|
|
||||||
zoom: number;
|
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementBounds(element);
|
const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates
|
||||||
|
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||||
// Apply zoom
|
{ clientX: 0, clientY: 0 },
|
||||||
const viewportWidthWithZoom = viewportWidth / zoom;
|
viewTransformations,
|
||||||
const viewportHeightWithZoom = viewportHeight / zoom;
|
);
|
||||||
|
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
|
||||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
{ clientX: canvasWidth, clientY: canvasHeight },
|
||||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
viewTransformations,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
x2 + scrollX - viewportWidthDiff / 2 >= 0 &&
|
topLeftSceneCoords.x <= x2 &&
|
||||||
x1 + scrollX - viewportWidthDiff / 2 <= viewportWidthWithZoom &&
|
topLeftSceneCoords.y <= y2 &&
|
||||||
y2 + scrollY - viewportHeightDiff / 2 >= 0 &&
|
bottomRightSceneCoords.x >= x1 &&
|
||||||
y1 + scrollY - viewportHeightDiff / 2 <= viewportHeightWithZoom
|
bottomRightSceneCoords.y >= y1
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import { normalizeScroll } from "./scroll";
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_ALIGN } from "../constants";
|
import { DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_ALIGN } from "../constants";
|
||||||
|
import { getDefaultAppState } from "../appState";
|
||||||
|
|
||||||
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
const WATERMARK_HEIGHT = 16;
|
const WATERMARK_HEIGHT = 16;
|
||||||
@ -60,7 +61,7 @@ export const exportToCanvas = (
|
|||||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||||
scrollX: normalizeScroll(-minX + exportPadding),
|
scrollX: normalizeScroll(-minX + exportPadding),
|
||||||
scrollY: normalizeScroll(-minY + exportPadding),
|
scrollY: normalizeScroll(-minY + exportPadding),
|
||||||
zoom: 1,
|
zoom: getDefaultAppState().zoom,
|
||||||
remotePointerViewportCoords: {},
|
remotePointerViewportCoords: {},
|
||||||
remoteSelectedElementIds: {},
|
remoteSelectedElementIds: {},
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
|
@ -16,4 +16,4 @@ export {
|
|||||||
hasText,
|
hasText,
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
} from "./comparisons";
|
} from "./comparisons";
|
||||||
export { getZoomOrigin, getNormalizedZoom } from "./zoom";
|
export { normalizeZoomValue as getNormalizedZoom, getNewZoom } from "./zoom";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AppState, FlooredNumber } from "../types";
|
import { AppState, FlooredNumber, PointerCoords, Zoom } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds, getClosestElementBounds } from "../element";
|
import { getCommonBounds, getClosestElementBounds } from "../element";
|
||||||
|
|
||||||
@ -19,14 +19,10 @@ function isOutsideViewPort(
|
|||||||
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: x1, sceneY: y1 },
|
{ sceneX: x1, sceneY: y1 },
|
||||||
appState,
|
appState,
|
||||||
canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords(
|
const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: x2, sceneY: y2 },
|
{ sceneX: x2, sceneY: y2 },
|
||||||
appState,
|
appState,
|
||||||
canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
viewportX2 - viewportX1 > appState.width ||
|
viewportX2 - viewportX1 > appState.width ||
|
||||||
@ -34,6 +30,29 @@ function isOutsideViewPort(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const centerScrollOn = ({
|
||||||
|
scenePoint,
|
||||||
|
viewportDimensions,
|
||||||
|
zoom,
|
||||||
|
}: {
|
||||||
|
scenePoint: PointerCoords;
|
||||||
|
viewportDimensions: { height: number; width: number };
|
||||||
|
zoom: Zoom;
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
scrollX: normalizeScroll(
|
||||||
|
(viewportDimensions.width / 2) * (1 / zoom.value) -
|
||||||
|
scenePoint.x -
|
||||||
|
zoom.translation.x * (1 / zoom.value),
|
||||||
|
),
|
||||||
|
scrollY: normalizeScroll(
|
||||||
|
(viewportDimensions.height / 2) * (1 / zoom.value) -
|
||||||
|
scenePoint.y -
|
||||||
|
zoom.translation.y * (1 / zoom.value),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const calculateScrollCenter = (
|
export const calculateScrollCenter = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
@ -45,7 +64,6 @@ export const calculateScrollCenter = (
|
|||||||
scrollY: normalizeScroll(0),
|
scrollY: normalizeScroll(0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const scale = window.devicePixelRatio;
|
|
||||||
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||||
|
|
||||||
if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
|
if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
|
||||||
@ -54,8 +72,6 @@ export const calculateScrollCenter = (
|
|||||||
viewportCoordsToSceneCoords(
|
viewportCoordsToSceneCoords(
|
||||||
{ clientX: appState.scrollX, clientY: appState.scrollY },
|
{ clientX: appState.scrollX, clientY: appState.scrollY },
|
||||||
appState,
|
appState,
|
||||||
canvas,
|
|
||||||
scale,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -63,8 +79,9 @@ export const calculateScrollCenter = (
|
|||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
|
||||||
return {
|
return centerScrollOn({
|
||||||
scrollX: normalizeScroll(appState.width / 2 - centerX),
|
scenePoint: { x: centerX, y: centerY },
|
||||||
scrollY: normalizeScroll(appState.height / 2 - centerY),
|
viewportDimensions: { width: appState.width, height: appState.height },
|
||||||
};
|
zoom: appState.zoom,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds } from "../element";
|
||||||
import { FlooredNumber } from "../types";
|
import { FlooredNumber, Zoom } from "../types";
|
||||||
import { ScrollBars } from "./types";
|
import { ScrollBars } from "./types";
|
||||||
import { getGlobalCSSVariable } from "../utils";
|
import { getGlobalCSSVariable } from "../utils";
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
@ -20,7 +20,7 @@ export const getScrollBars = (
|
|||||||
}: {
|
}: {
|
||||||
scrollX: FlooredNumber;
|
scrollX: FlooredNumber;
|
||||||
scrollY: FlooredNumber;
|
scrollY: FlooredNumber;
|
||||||
zoom: number;
|
zoom: Zoom;
|
||||||
},
|
},
|
||||||
): ScrollBars => {
|
): ScrollBars => {
|
||||||
// This is the bounding box of all the elements
|
// This is the bounding box of all the elements
|
||||||
@ -32,8 +32,8 @@ export const getScrollBars = (
|
|||||||
] = getCommonBounds(elements);
|
] = getCommonBounds(elements);
|
||||||
|
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
const viewportWidthWithZoom = viewportWidth / zoom;
|
const viewportWidthWithZoom = viewportWidth / zoom.value;
|
||||||
const viewportHeightWithZoom = viewportHeight / zoom;
|
const viewportHeightWithZoom = viewportHeight / zoom.value;
|
||||||
|
|
||||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
||||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { ExcalidrawTextElement } from "../element/types";
|
import { ExcalidrawTextElement } from "../element/types";
|
||||||
import { FlooredNumber } from "../types";
|
import { FlooredNumber, Zoom } from "../types";
|
||||||
|
|
||||||
export type SceneState = {
|
export type SceneState = {
|
||||||
scrollX: FlooredNumber;
|
scrollX: FlooredNumber;
|
||||||
scrollY: FlooredNumber;
|
scrollY: FlooredNumber;
|
||||||
// null indicates transparent bg
|
// null indicates transparent bg
|
||||||
viewBackgroundColor: string | null;
|
viewBackgroundColor: string | null;
|
||||||
zoom: number;
|
zoom: Zoom;
|
||||||
shouldCacheIgnoreZoom: boolean;
|
shouldCacheIgnoreZoom: boolean;
|
||||||
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
||||||
remotePointerButton?: { [id: string]: string | undefined };
|
remotePointerButton?: { [id: string]: string | undefined };
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
export const getZoomOrigin = (
|
import { NormalizedZoomValue, PointerCoords, Zoom } from "../types";
|
||||||
canvas: HTMLCanvasElement | null,
|
|
||||||
scale: number,
|
|
||||||
) => {
|
|
||||||
if (canvas === null) {
|
|
||||||
return { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
if (context === null) {
|
|
||||||
return { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedCanvasWidth = canvas.width / scale;
|
|
||||||
const normalizedCanvasHeight = canvas.height / scale;
|
|
||||||
|
|
||||||
|
export const getNewZoom = (
|
||||||
|
newZoomValue: NormalizedZoomValue,
|
||||||
|
prevZoom: Zoom,
|
||||||
|
zoomOnViewportPoint: PointerCoords = { x: 0, y: 0 },
|
||||||
|
): Zoom => {
|
||||||
return {
|
return {
|
||||||
x: normalizedCanvasWidth / 2,
|
value: newZoomValue,
|
||||||
y: normalizedCanvasHeight / 2,
|
translation: {
|
||||||
|
x:
|
||||||
|
zoomOnViewportPoint.x -
|
||||||
|
(zoomOnViewportPoint.x - prevZoom.translation.x) *
|
||||||
|
(newZoomValue / prevZoom.value),
|
||||||
|
y:
|
||||||
|
zoomOnViewportPoint.y -
|
||||||
|
(zoomOnViewportPoint.y - prevZoom.translation.y) *
|
||||||
|
(newZoomValue / prevZoom.value),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNormalizedZoom = (zoom: number): number => {
|
export const normalizeZoomValue = (zoom: number): NormalizedZoomValue => {
|
||||||
const normalizedZoom = parseFloat(zoom.toFixed(2));
|
const normalizedZoom = parseFloat(zoom.toFixed(2));
|
||||||
const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2));
|
const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2));
|
||||||
return clampedZoom;
|
return clampedZoom as NormalizedZoomValue;
|
||||||
};
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -364,15 +364,15 @@ describe("regression tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("pinch-to-zoom works", () => {
|
it("pinch-to-zoom works", () => {
|
||||||
expect(h.state.zoom).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
finger1.down(50, 50);
|
finger1.down(50, 50);
|
||||||
finger2.down(60, 50);
|
finger2.down(60, 50);
|
||||||
finger1.move(-10, 0);
|
finger1.move(-10, 0);
|
||||||
expect(h.state.zoom).toBeGreaterThan(1);
|
expect(h.state.zoom.value).toBeGreaterThan(1);
|
||||||
const zoomed = h.state.zoom;
|
const zoomed = h.state.zoom.value;
|
||||||
finger1.move(5, 0);
|
finger1.move(5, 0);
|
||||||
finger2.move(-5, 0);
|
finger2.move(-5, 0);
|
||||||
expect(h.state.zoom).toBeLessThan(zoomed);
|
expect(h.state.zoom.value).toBeLessThan(zoomed);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("two-finger scroll works", () => {
|
it("two-finger scroll works", () => {
|
||||||
@ -500,13 +500,13 @@ describe("regression tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("zoom hotkeys", () => {
|
it("zoom hotkeys", () => {
|
||||||
expect(h.state.zoom).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
fireEvent.keyDown(document, { code: "Equal", ctrlKey: true });
|
fireEvent.keyDown(document, { code: "Equal", ctrlKey: true });
|
||||||
fireEvent.keyUp(document, { code: "Equal", ctrlKey: true });
|
fireEvent.keyUp(document, { code: "Equal", ctrlKey: true });
|
||||||
expect(h.state.zoom).toBeGreaterThan(1);
|
expect(h.state.zoom.value).toBeGreaterThan(1);
|
||||||
fireEvent.keyDown(document, { code: "Minus", ctrlKey: true });
|
fireEvent.keyDown(document, { code: "Minus", ctrlKey: true });
|
||||||
fireEvent.keyUp(document, { code: "Minus", ctrlKey: true });
|
fireEvent.keyUp(document, { code: "Minus", ctrlKey: true });
|
||||||
expect(h.state.zoom).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rerenders UI on language change", async () => {
|
it("rerenders UI on language change", async () => {
|
||||||
|
12
src/types.ts
12
src/types.ts
@ -73,7 +73,7 @@ export type AppState = {
|
|||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
isResizing: boolean;
|
isResizing: boolean;
|
||||||
isRotating: boolean;
|
isRotating: boolean;
|
||||||
zoom: number;
|
zoom: Zoom;
|
||||||
openMenu: "canvas" | "shape" | null;
|
openMenu: "canvas" | "shape" | null;
|
||||||
lastPointerDownWith: PointerType;
|
lastPointerDownWith: PointerType;
|
||||||
selectedElementIds: { [id: string]: boolean };
|
selectedElementIds: { [id: string]: boolean };
|
||||||
@ -99,6 +99,16 @@ export type AppState = {
|
|||||||
fileHandle: import("browser-nativefs").FileSystemHandle | null;
|
fileHandle: import("browser-nativefs").FileSystemHandle | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
|
||||||
|
|
||||||
|
export type Zoom = Readonly<{
|
||||||
|
value: NormalizedZoomValue;
|
||||||
|
translation: Readonly<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type PointerCoords = Readonly<{
|
export type PointerCoords = Readonly<{
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
62
src/utils.ts
62
src/utils.ts
@ -1,5 +1,4 @@
|
|||||||
import { AppState } from "./types";
|
import { Zoom } from "./types";
|
||||||
import { getZoomOrigin } from "./scene";
|
|
||||||
import {
|
import {
|
||||||
CURSOR_TYPE,
|
CURSOR_TYPE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
@ -183,42 +182,47 @@ export const getShortcutKey = (shortcut: string): string => {
|
|||||||
}
|
}
|
||||||
return `${shortcut.replace(/\bCtrlOrCmd\b/i, "Ctrl")}`;
|
return `${shortcut.replace(/\bCtrlOrCmd\b/i, "Ctrl")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewportCoordsToSceneCoords = (
|
export const viewportCoordsToSceneCoords = (
|
||||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
appState: AppState,
|
{
|
||||||
canvas: HTMLCanvasElement | null,
|
zoom,
|
||||||
scale: number,
|
offsetLeft,
|
||||||
|
offsetTop,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
}: {
|
||||||
|
zoom: Zoom;
|
||||||
|
offsetLeft: number;
|
||||||
|
offsetTop: number;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const zoomOrigin = getZoomOrigin(canvas, scale);
|
const invScale = 1 / zoom.value;
|
||||||
const clientXWithZoom =
|
const x = (clientX - zoom.translation.x - offsetLeft) * invScale - scrollX;
|
||||||
zoomOrigin.x +
|
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
||||||
(clientX - zoomOrigin.x - appState.offsetLeft) / appState.zoom;
|
|
||||||
const clientYWithZoom =
|
|
||||||
zoomOrigin.y +
|
|
||||||
(clientY - zoomOrigin.y - appState.offsetTop) / appState.zoom;
|
|
||||||
|
|
||||||
const x = clientXWithZoom - appState.scrollX;
|
|
||||||
const y = clientYWithZoom - appState.scrollY;
|
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sceneCoordsToViewportCoords = (
|
export const sceneCoordsToViewportCoords = (
|
||||||
{ sceneX, sceneY }: { sceneX: number; sceneY: number },
|
{ sceneX, sceneY }: { sceneX: number; sceneY: number },
|
||||||
appState: AppState,
|
{
|
||||||
canvas: HTMLCanvasElement | null,
|
zoom,
|
||||||
scale: number,
|
offsetLeft,
|
||||||
|
offsetTop,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
}: {
|
||||||
|
zoom: Zoom;
|
||||||
|
offsetLeft: number;
|
||||||
|
offsetTop: number;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const zoomOrigin = getZoomOrigin(canvas, scale);
|
const x = (sceneX + scrollX + offsetLeft) * zoom.value + zoom.translation.x;
|
||||||
const x =
|
const y = (sceneY + scrollY + offsetTop) * zoom.value + zoom.translation.y;
|
||||||
zoomOrigin.x -
|
|
||||||
(zoomOrigin.x - sceneX - appState.scrollX - appState.offsetLeft) *
|
|
||||||
appState.zoom;
|
|
||||||
const y =
|
|
||||||
zoomOrigin.y -
|
|
||||||
(zoomOrigin.y - sceneY - appState.scrollY - appState.offsetTop) *
|
|
||||||
appState.zoom;
|
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user