mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: follow mode (#6848)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
88a2b286c7
commit
aad8ab0123
28 changed files with 1039 additions and 138 deletions
|
@ -109,6 +109,7 @@ export const actionZoomIn = register({
|
|||
},
|
||||
appState,
|
||||
),
|
||||
userToFollow: null,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
@ -146,6 +147,7 @@ export const actionZoomOut = register({
|
|||
},
|
||||
appState,
|
||||
),
|
||||
userToFollow: null,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
@ -183,6 +185,7 @@ export const actionResetZoom = register({
|
|||
},
|
||||
appState,
|
||||
),
|
||||
userToFollow: null,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
@ -226,22 +229,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;
|
||||
|
||||
|
@ -282,7 +283,7 @@ export const zoomToFit = ({
|
|||
scrollX = (appStateWidth / 2) * (1 / newZoomValue) - centerX;
|
||||
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
|
||||
} else {
|
||||
newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
|
||||
newZoomValue = zoomValueToFitBoundsOnViewport(bounds, {
|
||||
width: appState.width,
|
||||
height: appState.height,
|
||||
});
|
||||
|
@ -311,6 +312,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.
|
||||
|
@ -321,7 +345,10 @@ export const actionZoomToFitSelectionInViewport = register({
|
|||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
return zoomToFit({
|
||||
targetElements: selectedElements.length ? selectedElements : elements,
|
||||
appState,
|
||||
appState: {
|
||||
...appState,
|
||||
userToFollow: null,
|
||||
},
|
||||
fitToViewport: false,
|
||||
});
|
||||
},
|
||||
|
@ -341,7 +368,10 @@ export const actionZoomToFitSelection = register({
|
|||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
return zoomToFit({
|
||||
targetElements: selectedElements.length ? selectedElements : elements,
|
||||
appState,
|
||||
appState: {
|
||||
...appState,
|
||||
userToFollow: null,
|
||||
},
|
||||
fitToViewport: true,
|
||||
});
|
||||
},
|
||||
|
@ -358,7 +388,14 @@ export const actionZoomToFit = register({
|
|||
viewMode: true,
|
||||
trackEvent: { category: "canvas" },
|
||||
perform: (elements, appState) =>
|
||||
zoomToFit({ targetElements: elements, appState, fitToViewport: false }),
|
||||
zoomToFit({
|
||||
targetElements: elements,
|
||||
appState: {
|
||||
...appState,
|
||||
userToFollow: null,
|
||||
},
|
||||
fitToViewport: false,
|
||||
}),
|
||||
keyTest: (event) =>
|
||||
event.code === CODES.ONE &&
|
||||
event.shiftKey &&
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { getClientColor } from "../clients";
|
||||
import { Avatar } from "../components/Avatar";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { Collaborator } from "../types";
|
||||
import { register } from "./register";
|
||||
|
||||
|
@ -9,39 +8,68 @@ export const actionGoToCollaborator = register({
|
|||
viewMode: true,
|
||||
trackEvent: { category: "collab" },
|
||||
perform: (_elements, appState, value) => {
|
||||
const point = value as Collaborator["pointer"];
|
||||
const _value = value as Collaborator;
|
||||
const point = _value.pointer;
|
||||
|
||||
if (!point) {
|
||||
return { appState, commitToHistory: false };
|
||||
}
|
||||
|
||||
if (appState.userToFollow?.socketId === _value.socketId) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
userToFollow: null,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
...centerScrollOn({
|
||||
scenePoint: point,
|
||||
viewportDimensions: {
|
||||
width: appState.width,
|
||||
height: appState.height,
|
||||
},
|
||||
zoom: appState.zoom,
|
||||
}),
|
||||
userToFollow: {
|
||||
socketId: _value.socketId!,
|
||||
username: _value.username || "",
|
||||
},
|
||||
// Close mobile menu
|
||||
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ updateData, data }) => {
|
||||
const [clientId, collaborator] = data as [string, Collaborator];
|
||||
PanelComponent: ({ updateData, data, appState }) => {
|
||||
const [clientId, collaborator, withName] = data as [
|
||||
string,
|
||||
Collaborator,
|
||||
boolean,
|
||||
];
|
||||
|
||||
const background = getClientColor(clientId);
|
||||
|
||||
return (
|
||||
return withName ? (
|
||||
<div
|
||||
className="dropdown-menu-item dropdown-menu-item-base"
|
||||
onClick={() => updateData({ ...collaborator, clientId })}
|
||||
>
|
||||
<Avatar
|
||||
color={background}
|
||||
onClick={() => {}}
|
||||
name={collaborator.username || ""}
|
||||
src={collaborator.avatarUrl}
|
||||
isBeingFollowed={appState.userToFollow?.socketId === clientId}
|
||||
/>
|
||||
{collaborator.username}
|
||||
</div>
|
||||
) : (
|
||||
<Avatar
|
||||
color={background}
|
||||
onClick={() => updateData(collaborator.pointer)}
|
||||
onClick={() => {
|
||||
updateData({ ...collaborator, clientId });
|
||||
}}
|
||||
name={collaborator.username || ""}
|
||||
src={collaborator.avatarUrl}
|
||||
isBeingFollowed={appState.userToFollow?.socketId === clientId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue