follow scroll + zoom => follow scrollAndZoom

This commit is contained in:
barnabasmolnar 2023-07-25 01:11:05 +02:00
parent 42a90def41
commit f255a0835f
8 changed files with 97 additions and 138 deletions

View File

@ -225,22 +225,20 @@ const zoomValueToFitBoundsOnViewport = (
return clampedZoomValueToFitElements as NormalizedZoomValue;
};
export const zoomToFit = ({
targetElements,
export const zoomToFitBounds = ({
bounds,
appState,
fitToViewport = false,
viewportZoomFactor = 0.7,
}: {
targetElements: readonly ExcalidrawElement[];
bounds: readonly [number, number, number, number];
appState: Readonly<AppState>;
/** whether to fit content to viewport (beyond >100%) */
fitToViewport: boolean;
/** zoom content to cover X of the viewport, when fitToViewport=true */
viewportZoomFactor?: number;
}) => {
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
const [x1, y1, x2, y2] = commonBounds;
const [x1, y1, x2, y2] = bounds;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
@ -267,7 +265,7 @@ export const zoomToFit = ({
scrollX = (appState.width / 2) * (1 / newZoomValue) - centerX;
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
} else {
newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
newZoomValue = zoomValueToFitBoundsOnViewport(bounds, {
width: appState.width,
height: appState.height,
});
@ -296,6 +294,29 @@ export const zoomToFit = ({
};
};
export const zoomToFit = ({
targetElements,
appState,
fitToViewport,
viewportZoomFactor,
}: {
targetElements: readonly ExcalidrawElement[];
appState: Readonly<AppState>;
/** whether to fit content to viewport (beyond >100%) */
fitToViewport: boolean;
/** zoom content to cover X of the viewport, when fitToViewport=true */
viewportZoomFactor?: number;
}) => {
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
return zoomToFitBounds({
bounds: commonBounds,
appState,
fitToViewport,
viewportZoomFactor,
});
};
// Note, this action differs from actionZoomToFitSelection in that it doesn't
// zoom beyond 100%. In other words, if the content is smaller than viewport
// size, it won't be zoomed in.

View File

@ -1500,18 +1500,21 @@ class App extends React.Component<AppProps, AppState> {
this.refreshDeviceState(this.excalidrawContainerRef.current);
}
// TODO follow-participant
// add zoom change
if (prevState.zoom.value !== this.state.zoom.value) {
this.props?.onZoomChange?.(this.state.zoom);
this.props?.onScrollAndZoomChange?.({
zoom: this.state.zoom,
scroll: { x: this.state.scrollX, y: this.state.scrollY },
});
}
if (
prevState.scrollX !== this.state.scrollX ||
prevState.scrollY !== this.state.scrollY
) {
this.props?.onScrollChange?.(this.state.scrollX, this.state.scrollY);
this.props?.onScrollAndZoomChange?.({
zoom: this.state.zoom,
scroll: { x: this.state.scrollX, y: this.state.scrollY },
});
}
if (

View File

@ -1,6 +1,6 @@
import throttle from "lodash.throttle";
import { PureComponent } from "react";
import { ExcalidrawImperativeAPI } from "../../types";
import { AppState, ExcalidrawImperativeAPI } from "../../types";
import { ErrorDialog } from "../../components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../constants";
import { ImportedDataState } from "../../data/types";
@ -16,6 +16,7 @@ import { Collaborator, Gesture } from "../../types";
import {
preventUnload,
resolvablePromise,
viewportCoordsToSceneCoords,
withBatchedUpdates,
} from "../../utils";
import {
@ -71,6 +72,7 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai";
import { appJotaiStore } from "../app-jotai";
import { zoomToFitBounds } from "../../actions/actionCanvas";
export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false);
@ -89,8 +91,7 @@ export interface CollabAPI {
/** function so that we can access the latest value from stale callbacks */
isCollaborating: () => boolean;
onPointerUpdate: CollabInstance["onPointerUpdate"];
onScrollChange: CollabInstance["onScrollChange"];
onZoomChange: CollabInstance["onZoomChange"];
onScrollAndZoomChange: CollabInstance["onScrollAndZoomChange"];
startCollaboration: CollabInstance["startCollaboration"];
stopCollaboration: CollabInstance["stopCollaboration"];
syncElements: CollabInstance["syncElements"];
@ -164,8 +165,7 @@ class Collab extends PureComponent<Props, CollabState> {
const collabAPI: CollabAPI = {
isCollaborating: this.isCollaborating,
onPointerUpdate: this.onPointerUpdate,
onScrollChange: this.onScrollChange,
onZoomChange: this.onZoomChange,
onScrollAndZoomChange: this.onScrollAndZoomChange,
startCollaboration: this.startCollaboration,
syncElements: this.syncElements,
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
@ -510,8 +510,6 @@ class Collab extends PureComponent<Props, CollabState> {
break;
}
case WS_SCENE_EVENT_TYPES.UPDATE:
console.log("received update", decryptedData);
console.log(this.excalidrawAPI.getAppState());
this.handleRemoteSceneUpdate(
this.reconcileElements(decryptedData.payload.elements),
);
@ -520,8 +518,6 @@ class Collab extends PureComponent<Props, CollabState> {
const { pointer, button, username, selectedElementIds } =
decryptedData.payload;
// console.log({ decryptedData });
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
decryptedData.payload.socketId ||
// @ts-ignore legacy, see #2094 (#2097)
@ -539,50 +535,22 @@ class Collab extends PureComponent<Props, CollabState> {
});
break;
}
// TODO follow-participant
// case "SCROLL_LOCATION"
// case "ZOOM_VALUE"
// if following someone, update scroll and zoom
case "SCROLL_LOCATION": {
const {
scroll: { x, y },
} = decryptedData.payload;
case "SCROLL_AND_ZOOM": {
const { bounds } = decryptedData.payload;
const socketId: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["socketId"] =
const socketId: SocketUpdateDataSource["SCROLL_AND_ZOOM"]["payload"]["socketId"] =
decryptedData.payload.socketId;
console.log({ decryptedData });
const appState = this.excalidrawAPI.getAppState();
console.log({ appState });
if (appState.userToFollow === socketId) {
this.excalidrawAPI.updateScene({
appState: {
scrollX: x,
scrollY: y,
},
});
}
break;
}
case "ZOOM_VALUE": {
const { zoom } = decryptedData.payload;
console.log({ decryptedData });
const socketId: SocketUpdateDataSource["ZOOM_VALUE"]["payload"]["socketId"] =
decryptedData.payload.socketId;
const appState = this.excalidrawAPI.getAppState();
if (appState.userToFollow === socketId) {
this.excalidrawAPI.updateScene({
appState: { zoom },
const _appState = this.excalidrawAPI.getAppState();
if (_appState.userToFollow === socketId) {
const { appState } = zoomToFitBounds({
appState: _appState,
bounds,
fitToViewport: true,
viewportZoomFactor: 1,
});
this.excalidrawAPI.updateScene({ appState });
}
break;
@ -814,7 +782,6 @@ class Collab extends PureComponent<Props, CollabState> {
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
pointersMap: Gesture["pointers"];
}) => {
// console.log({ payload });
payload.pointersMap.size < 2 &&
this.portal.socket &&
this.portal.broadcastMouseLocation(payload);
@ -822,26 +789,34 @@ class Collab extends PureComponent<Props, CollabState> {
CURSOR_SYNC_TIMEOUT,
);
// TODO follow-participant
// - onScrollChange
// -- broadCastScrollLocation
// - onZoomChange
// -- broadCastZoomValue
onScrollAndZoomChange = throttle(
(payload: { zoom: AppState["zoom"]; scroll: { x: number; y: number } }) => {
const appState = this.excalidrawAPI.getAppState();
onZoomChange = throttle(
(payload: {
zoom: SocketUpdateDataSource["ZOOM_VALUE"]["payload"]["zoom"];
}) => {
this.portal.socket && this.portal.broadcastZoomValue(payload);
},
);
const { x: x1, y: y1 } = viewportCoordsToSceneCoords(
{ clientX: 0, clientY: 0 },
{
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
scrollX: payload.scroll.x,
scrollY: payload.scroll.y,
zoom: payload.zoom,
},
);
onScrollChange = throttle(
(payload: {
scrollX: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["x"];
scrollY: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["y"];
}) => {
this.portal.socket && this.portal.broadcastScrollLocation(payload);
const { x: x2, y: y2 } = viewportCoordsToSceneCoords(
{ clientX: appState.width, clientY: appState.height },
{
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
scrollX: payload.scroll.x,
scrollY: payload.scroll.y,
zoom: payload.zoom,
},
);
this.portal.socket &&
this.portal.broadcastScrollAndZoom({ bounds: [x1, y1, x2, y2] });
},
);

View File

@ -214,8 +214,6 @@ class Portal {
},
};
// console.log("broadcastMouseLocation data", data);
return this._broadcastSocketData(
data as SocketUpdateData,
true, // volatile
@ -223,48 +221,19 @@ class Portal {
}
};
// TODO follow-participant
// - broadCastScrollLocation
// - broadCastZoomValue
broadcastScrollLocation = (payload: {
scrollX: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["x"];
scrollY: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["y"];
broadcastScrollAndZoom = (payload: {
bounds: [number, number, number, number];
}) => {
if (this.socket?.id) {
const data: SocketUpdateDataSource["SCROLL_LOCATION"] = {
type: "SCROLL_LOCATION",
const data: SocketUpdateDataSource["SCROLL_AND_ZOOM"] = {
type: "SCROLL_AND_ZOOM",
payload: {
socketId: this.socket.id,
scroll: { x: payload.scrollX, y: payload.scrollY },
username: this.collab.state.username,
bounds: payload.bounds,
},
};
console.log("broadcastScrollLocation data", data);
return this._broadcastSocketData(
data as SocketUpdateData,
true, // volatile
);
}
};
broadcastZoomValue = (payload: {
zoom: SocketUpdateDataSource["ZOOM_VALUE"]["payload"]["zoom"];
}) => {
if (this.socket?.id) {
const data: SocketUpdateDataSource["ZOOM_VALUE"] = {
type: "ZOOM_VALUE",
payload: {
socketId: this.socket.id,
zoom: payload.zoom,
username: this.collab.state.username,
},
};
console.log("broadcastZoomValue data", data);
return this._broadcastSocketData(
data as SocketUpdateData,
true, // volatile

View File

@ -113,20 +113,12 @@ export type SocketUpdateDataSource = {
username: string;
};
};
SCROLL_LOCATION: {
type: "SCROLL_LOCATION";
SCROLL_AND_ZOOM: {
type: "SCROLL_AND_ZOOM";
payload: {
socketId: string;
scroll: { x: number; y: number };
username: string;
};
};
ZOOM_VALUE: {
type: "ZOOM_VALUE";
payload: {
socketId: string;
zoom: AppState["zoom"];
username: string;
bounds: [number, number, number, number];
};
};
IDLE_STATUS: {

View File

@ -649,16 +649,8 @@ const ExcalidrawWrapper = () => {
})}
>
<Excalidraw
// TODO follow-participant
// add onZoomChange
onZoomChange={(zoom) => {
console.log({ zoom });
collabAPI?.onZoomChange({ zoom });
}}
onScrollChange={(x, y) => {
// console.log({ x, y });
collabAPI?.onScrollChange({ scrollX: x, scrollY: y });
onScrollAndZoomChange={({ zoom, scroll }) => {
collabAPI?.onScrollAndZoomChange({ zoom, scroll });
}}
ref={excalidrawRefCallback}
onChange={onChange}

View File

@ -41,7 +41,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
onLinkOpen,
onPointerDown,
onScrollChange,
onZoomChange,
onScrollAndZoomChange,
children,
} = props;
@ -116,7 +116,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
onLinkOpen={onLinkOpen}
onPointerDown={onPointerDown}
onScrollChange={onScrollChange}
onZoomChange={onZoomChange}
onScrollAndZoomChange={onScrollAndZoomChange}
>
{children}
</App>

View File

@ -362,6 +362,13 @@ export interface ExcalidrawProps {
) => void;
onScrollChange?: (scrollX: number, scrollY: number) => void;
onZoomChange?: (zoom: Zoom) => void;
onScrollAndZoomChange?: ({
zoom,
scroll,
}: {
zoom: Zoom;
scroll: { x: number; y: number };
}) => void;
children?: React.ReactNode;
}