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

View File

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

View File

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

View File

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

View File

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

View File

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