feat: splitting logic, memoization
This commit is contained in:
parent
71eb3023b2
commit
132750f753
@ -223,6 +223,7 @@ import {
|
|||||||
FrameNameBoundsCache,
|
FrameNameBoundsCache,
|
||||||
SidebarName,
|
SidebarName,
|
||||||
SidebarTabName,
|
SidebarTabName,
|
||||||
|
ScrollConstraints,
|
||||||
NormalizedZoomValue,
|
NormalizedZoomValue,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
@ -251,6 +252,7 @@ import {
|
|||||||
easeToValuesRAF,
|
easeToValuesRAF,
|
||||||
muteFSAbortError,
|
muteFSAbortError,
|
||||||
easeOut,
|
easeOut,
|
||||||
|
isShallowEqual,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@ -457,6 +459,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
lastPointerDown: React.PointerEvent<HTMLElement> | null = null;
|
lastPointerDown: React.PointerEvent<HTMLElement> | null = null;
|
||||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
||||||
lastViewportPosition = { x: 0, y: 0 };
|
lastViewportPosition = { x: 0, y: 0 };
|
||||||
|
private memoizedScrollConstraints: {
|
||||||
|
input: {
|
||||||
|
scrollConstraints: AppState["scrollConstraints"];
|
||||||
|
values: Omit<Partial<AppState>, "zoom"> & { zoom: NormalizedZoomValue };
|
||||||
|
};
|
||||||
|
result: ReturnType<App["calculateConstraints"]>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -1632,9 +1641,74 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const constraintedScroll = this.constrainScroll(prevState);
|
let constraintedScrollState;
|
||||||
|
if (
|
||||||
|
this.state.scrollConstraints &&
|
||||||
|
!this.state.scrollConstraints.isAnimating
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
scrollConstraints,
|
||||||
|
zoom,
|
||||||
|
cursorButton,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
this.renderScene(constraintedScroll);
|
// TODO: this could be replaced with memoization function like _.memoize()
|
||||||
|
const calculatedConstraints =
|
||||||
|
isShallowEqual(
|
||||||
|
scrollConstraints,
|
||||||
|
this.memoizedScrollConstraints?.input.scrollConstraints ?? {},
|
||||||
|
) &&
|
||||||
|
isShallowEqual(
|
||||||
|
{ width, height, zoom: zoom.value, cursorButton },
|
||||||
|
this.memoizedScrollConstraints?.input.values ?? {},
|
||||||
|
) &&
|
||||||
|
this.memoizedScrollConstraints
|
||||||
|
? this.memoizedScrollConstraints.result
|
||||||
|
: this.calculateConstraints({
|
||||||
|
scrollConstraints,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
zoom,
|
||||||
|
cursorButton,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.memoizedScrollConstraints = {
|
||||||
|
input: {
|
||||||
|
scrollConstraints,
|
||||||
|
values: { width, height, zoom: zoom.value, cursorButton },
|
||||||
|
},
|
||||||
|
result: calculatedConstraints,
|
||||||
|
};
|
||||||
|
|
||||||
|
const constrainedScrollValues = this.constrainScrollValues({
|
||||||
|
...calculatedConstraints,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isViewportOutsideOfConstrainedArea =
|
||||||
|
this.isViewportOutsideOfConstrainedArea({
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
scrollConstraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
constraintedScrollState = this.handleConstrainedScrollStateChange({
|
||||||
|
...constrainedScrollValues,
|
||||||
|
shouldAnimate:
|
||||||
|
isViewportOutsideOfConstrainedArea &&
|
||||||
|
this.state.cursorButton !== "down" &&
|
||||||
|
prevState.zoom.value === this.state.zoom.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderScene(constraintedScrollState);
|
||||||
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
||||||
|
|
||||||
// Do not notify consumers if we're still loading the scene. Among other
|
// Do not notify consumers if we're still loading the scene. Among other
|
||||||
@ -7629,55 +7703,59 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
* @param scrollConstraints - The new scroll constraints.
|
* @param scrollConstraints - The new scroll constraints.
|
||||||
*/
|
*/
|
||||||
public setScrollConstraints = (
|
public setScrollConstraints = (
|
||||||
scrollConstraints: AppState["scrollConstraints"],
|
scrollConstraints: ScrollConstraints | null,
|
||||||
) => {
|
) => {
|
||||||
this.setState({
|
if (scrollConstraints) {
|
||||||
|
const { scrollX, scrollY, width, height, zoom, cursorButton } =
|
||||||
|
this.state;
|
||||||
|
const constrainedScrollValues = this.constrainScrollValues({
|
||||||
|
...this.calculateConstraints({
|
||||||
scrollConstraints,
|
scrollConstraints,
|
||||||
viewModeEnabled: !!scrollConstraints,
|
zoom,
|
||||||
});
|
cursorButton,
|
||||||
};
|
width,
|
||||||
|
height,
|
||||||
/**
|
}),
|
||||||
* Constrains the scroll position of the app state within the defined scroll constraints.
|
|
||||||
* The scroll position is adjusted based on the application's current state including zoom level and viewport dimensions.
|
|
||||||
*
|
|
||||||
* @param nextState - The next state of the application, or a subset of the application state.
|
|
||||||
* @returns The modified next state with scrollX and scrollY constrained to the scroll constraints.
|
|
||||||
*/
|
|
||||||
private constrainScroll = (prevState: AppState): ConstrainedScrollValues => {
|
|
||||||
const {
|
|
||||||
scrollX,
|
scrollX,
|
||||||
scrollY,
|
scrollY,
|
||||||
|
});
|
||||||
|
this.animateConstainedScroll({
|
||||||
|
...constrainedScrollValues,
|
||||||
|
opts: {
|
||||||
|
onEndCallback: () => {
|
||||||
|
this.setState({
|
||||||
|
scrollConstraints,
|
||||||
|
viewModeEnabled: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
scrollConstraints: null,
|
||||||
|
viewModeEnabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private calculateConstraints = ({
|
||||||
scrollConstraints,
|
scrollConstraints,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
zoom: currentZoom,
|
zoom,
|
||||||
cursorButton,
|
cursorButton,
|
||||||
} = this.state;
|
}: {
|
||||||
|
scrollConstraints: ScrollConstraints;
|
||||||
if (!scrollConstraints || scrollConstraints.isAnimating) {
|
width: AppState["width"];
|
||||||
return null;
|
height: AppState["height"];
|
||||||
}
|
zoom: AppState["zoom"];
|
||||||
|
cursorButton: AppState["cursorButton"];
|
||||||
// Check if the state has changed since the last render
|
}) => {
|
||||||
const stateUnchanged =
|
|
||||||
currentZoom.value === prevState.zoom.value &&
|
|
||||||
scrollX === prevState.scrollX &&
|
|
||||||
scrollY === prevState.scrollY &&
|
|
||||||
width === prevState.width &&
|
|
||||||
height === prevState.height &&
|
|
||||||
cursorButton === prevState.cursorButton;
|
|
||||||
|
|
||||||
// If the state hasn't changed and scrollConstraints didn't just get defined, return null
|
|
||||||
if (stateUnchanged && prevState.scrollConstraints) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the overscroll allowance percentage
|
// Set the overscroll allowance percentage
|
||||||
const OVERSCROLL_ALLOWANCE_PERCENTAGE = 0.2;
|
const OVERSCROLL_ALLOWANCE_PERCENTAGE = 0.2;
|
||||||
const lockZoom = scrollConstraints.opts?.lockZoom ?? false;
|
const lockZoom = scrollConstraints.lockZoom ?? false;
|
||||||
const viewportZoomFactor = scrollConstraints.opts?.viewportZoomFactor
|
const viewportZoomFactor = scrollConstraints.viewportZoomFactor
|
||||||
? Math.min(1, Math.max(scrollConstraints.opts.viewportZoomFactor, 0.1))
|
? Math.min(1, Math.max(scrollConstraints.viewportZoomFactor, 0.1))
|
||||||
: 0.9;
|
: 0.9;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7776,6 +7854,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return { maxScrollX, minScrollX, maxScrollY, minScrollY };
|
return { maxScrollX, minScrollX, maxScrollY, minScrollY };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { zoomLevelX, zoomLevelY, maxZoomLevel } = calculateZoomLevel();
|
||||||
|
const constrainedZoom = getNormalizedZoom(
|
||||||
|
maxZoomLevel ? Math.max(maxZoomLevel, zoom.value) : zoom.value,
|
||||||
|
);
|
||||||
|
const { constrainedScrollCenterX, constrainedScrollCenterY } =
|
||||||
|
calculateConstrainedScrollCenter(constrainedZoom);
|
||||||
|
const { overscrollAllowanceX, overscrollAllowanceY } =
|
||||||
|
calculateOverscrollAllowance();
|
||||||
|
const shouldAdjustForCenteredView =
|
||||||
|
constrainedZoom <= zoomLevelX || constrainedZoom <= zoomLevelY;
|
||||||
|
const { maxScrollX, minScrollX, maxScrollY, minScrollY } =
|
||||||
|
calculateMinMaxScrollValues(
|
||||||
|
shouldAdjustForCenteredView,
|
||||||
|
overscrollAllowanceX,
|
||||||
|
overscrollAllowanceY,
|
||||||
|
constrainedScrollCenterX,
|
||||||
|
constrainedScrollCenterY,
|
||||||
|
constrainedZoom,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxScrollX,
|
||||||
|
minScrollX,
|
||||||
|
maxScrollY,
|
||||||
|
minScrollY,
|
||||||
|
constrainedZoom: {
|
||||||
|
value: constrainedZoom,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constrains the scroll values within the constrained area.
|
* Constrains the scroll values within the constrained area.
|
||||||
* @param maxScrollX - The maximum scroll value for the X axis.
|
* @param maxScrollX - The maximum scroll value for the X axis.
|
||||||
@ -7784,12 +7893,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
* @param minScrollY - The minimum scroll value for the Y axis.
|
* @param minScrollY - The minimum scroll value for the Y axis.
|
||||||
* @returns The constrained scroll values for the X and Y axes.
|
* @returns The constrained scroll values for the X and Y axes.
|
||||||
*/
|
*/
|
||||||
const constrainScrollValues = (
|
private constrainScrollValues = ({
|
||||||
maxScrollX: number,
|
scrollX,
|
||||||
minScrollX: number,
|
scrollY,
|
||||||
maxScrollY: number,
|
maxScrollX,
|
||||||
minScrollY: number,
|
minScrollX,
|
||||||
) => {
|
maxScrollY,
|
||||||
|
minScrollY,
|
||||||
|
constrainedZoom,
|
||||||
|
}: {
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
maxScrollX: number;
|
||||||
|
minScrollX: number;
|
||||||
|
maxScrollY: number;
|
||||||
|
minScrollY: number;
|
||||||
|
constrainedZoom: AppState["zoom"];
|
||||||
|
}) => {
|
||||||
const constrainedScrollX = Math.min(
|
const constrainedScrollX = Math.min(
|
||||||
maxScrollX,
|
maxScrollX,
|
||||||
Math.max(scrollX, minScrollX),
|
Math.max(scrollX, minScrollX),
|
||||||
@ -7798,33 +7918,32 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
maxScrollY,
|
maxScrollY,
|
||||||
Math.max(scrollY, minScrollY),
|
Math.max(scrollY, minScrollY),
|
||||||
);
|
);
|
||||||
return { constrainedScrollX, constrainedScrollY };
|
return { constrainedScrollX, constrainedScrollY, constrainedZoom };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the state change based on the constrained scroll values.
|
* Animate the scroll values to the constrained area
|
||||||
* Also handles the animation to the constrained area when the viewport is outside of constrained area.
|
|
||||||
* @param constrainedScrollX - The constrained scroll value for the X axis.
|
|
||||||
* @param constrainedScrollY - The constrained scroll value for the Y axis.
|
|
||||||
* @returns The constrained state if the state has changed, when needs to be passed into render function, otherwise null.
|
|
||||||
*/
|
*/
|
||||||
const handleStateChange = (
|
private animateConstainedScroll = ({
|
||||||
constrainedScrollX: number,
|
constrainedScrollX,
|
||||||
constrainedScrollY: number,
|
constrainedScrollY,
|
||||||
) => {
|
opts,
|
||||||
const isStateChanged =
|
}: {
|
||||||
constrainedScrollX !== scrollX || constrainedScrollY !== scrollY;
|
constrainedScrollX: number;
|
||||||
|
constrainedScrollY: number;
|
||||||
|
opts?: {
|
||||||
|
onStartCallback?: () => void;
|
||||||
|
onEndCallback?: () => void;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { scrollX, scrollY, scrollConstraints } = this.state;
|
||||||
|
|
||||||
|
const { onStartCallback, onEndCallback } = opts || {};
|
||||||
|
|
||||||
|
if (!scrollConstraints) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (isStateChanged) {
|
|
||||||
if (
|
|
||||||
(scrollX < scrollConstraints.x ||
|
|
||||||
scrollX + width > scrollConstraints.x + scrollConstraints.width ||
|
|
||||||
scrollY < scrollConstraints.y ||
|
|
||||||
scrollY + height >
|
|
||||||
scrollConstraints.y + scrollConstraints.height) &&
|
|
||||||
cursorButton !== "down" &&
|
|
||||||
currentZoom.value === prevState.zoom.value
|
|
||||||
) {
|
|
||||||
easeToValuesRAF({
|
easeToValuesRAF({
|
||||||
fromValues: { scrollX, scrollY },
|
fromValues: { scrollX, scrollY },
|
||||||
toValues: {
|
toValues: {
|
||||||
@ -7838,28 +7957,77 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
scrollConstraints: { ...scrollConstraints, isAnimating: true },
|
scrollConstraints: { ...scrollConstraints, isAnimating: true },
|
||||||
});
|
});
|
||||||
|
onStartCallback && onStartCallback();
|
||||||
},
|
},
|
||||||
onEnd: () => {
|
onEnd: () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollConstraints: { ...scrollConstraints, isAnimating: false },
|
scrollConstraints: { ...scrollConstraints, isAnimating: false },
|
||||||
});
|
});
|
||||||
|
onEndCallback && onEndCallback();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private isViewportOutsideOfConstrainedArea = ({
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
scrollConstraints,
|
||||||
|
}: {
|
||||||
|
scrollX: AppState["scrollX"];
|
||||||
|
scrollY: AppState["scrollY"];
|
||||||
|
width: AppState["width"];
|
||||||
|
height: AppState["height"];
|
||||||
|
scrollConstraints: AppState["scrollConstraints"];
|
||||||
|
}) => {
|
||||||
|
if (!scrollConstraints) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
scrollX < scrollConstraints.x ||
|
||||||
|
scrollX + width > scrollConstraints.x + scrollConstraints.width ||
|
||||||
|
scrollY < scrollConstraints.y ||
|
||||||
|
scrollY + height > scrollConstraints.y + scrollConstraints.height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the state change based on the constrained scroll values.
|
||||||
|
* Also handles the animation to the constrained area when the viewport is outside of constrained area.
|
||||||
|
* @param constrainedScrollX - The constrained scroll value for the X axis.
|
||||||
|
* @param constrainedScrollY - The constrained scroll value for the Y axis.
|
||||||
|
* @returns The constrained state if the state has changed, when needs to be passed into render function, otherwise null.
|
||||||
|
*/
|
||||||
|
private handleConstrainedScrollStateChange = ({
|
||||||
|
constrainedScrollX,
|
||||||
|
constrainedScrollY,
|
||||||
|
constrainedZoom,
|
||||||
|
shouldAnimate,
|
||||||
|
}: {
|
||||||
|
constrainedScrollX: number;
|
||||||
|
constrainedScrollY: number;
|
||||||
|
constrainedZoom: AppState["zoom"];
|
||||||
|
shouldAnimate?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { scrollX, scrollY } = this.state;
|
||||||
|
const isStateChanged =
|
||||||
|
constrainedScrollX !== scrollX || constrainedScrollY !== scrollY;
|
||||||
|
|
||||||
|
if (isStateChanged) {
|
||||||
|
if (shouldAnimate) {
|
||||||
|
this.animateConstainedScroll({
|
||||||
|
constrainedScrollX,
|
||||||
|
constrainedScrollY,
|
||||||
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const constrainedState = {
|
const constrainedState = {
|
||||||
scrollX: constrainedScrollX,
|
scrollX: constrainedScrollX,
|
||||||
scrollY: constrainedScrollY,
|
scrollY: constrainedScrollY,
|
||||||
zoom: {
|
zoom: constrainedZoom,
|
||||||
value: maxZoomLevel
|
|
||||||
? (Math.max(
|
|
||||||
currentZoom.value,
|
|
||||||
maxZoomLevel,
|
|
||||||
) as NormalizedZoomValue)
|
|
||||||
: currentZoom.value,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState(constrainedState);
|
this.setState(constrainedState);
|
||||||
@ -7868,36 +8036,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compute the constrained scroll values.
|
|
||||||
const { zoomLevelX, zoomLevelY, maxZoomLevel } = calculateZoomLevel();
|
|
||||||
const zoom = maxZoomLevel
|
|
||||||
? Math.max(maxZoomLevel, currentZoom.value)
|
|
||||||
: currentZoom.value;
|
|
||||||
const { constrainedScrollCenterX, constrainedScrollCenterY } =
|
|
||||||
calculateConstrainedScrollCenter(zoom);
|
|
||||||
const { overscrollAllowanceX, overscrollAllowanceY } =
|
|
||||||
calculateOverscrollAllowance();
|
|
||||||
const shouldAdjustForCenteredView =
|
|
||||||
zoom <= zoomLevelX || zoom <= zoomLevelY;
|
|
||||||
const { maxScrollX, minScrollX, maxScrollY, minScrollY } =
|
|
||||||
calculateMinMaxScrollValues(
|
|
||||||
shouldAdjustForCenteredView,
|
|
||||||
overscrollAllowanceX,
|
|
||||||
overscrollAllowanceY,
|
|
||||||
constrainedScrollCenterX,
|
|
||||||
constrainedScrollCenterY,
|
|
||||||
zoom,
|
|
||||||
);
|
|
||||||
const { constrainedScrollX, constrainedScrollY } = constrainScrollValues(
|
|
||||||
maxScrollX,
|
|
||||||
minScrollX,
|
|
||||||
maxScrollY,
|
|
||||||
minScrollY,
|
|
||||||
);
|
|
||||||
|
|
||||||
return handleStateChange(constrainedScrollX, constrainedScrollY);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
@ -701,7 +701,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: 2560,
|
width: 2560,
|
||||||
height: 1300,
|
height: 1300,
|
||||||
opts: { lockZoom: true, viewportZoomFactor: 0.1 },
|
lockZoom: true,
|
||||||
|
viewportZoomFactor: 0.1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppMainMenu
|
<AppMainMenu
|
||||||
|
22
src/types.ts
22
src/types.ts
@ -223,17 +223,7 @@ export type AppState = {
|
|||||||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||||
showHyperlinkPopup: false | "info" | "editor";
|
showHyperlinkPopup: false | "info" | "editor";
|
||||||
selectedLinearElement: LinearElementEditor | null;
|
selectedLinearElement: LinearElementEditor | null;
|
||||||
scrollConstraints: {
|
scrollConstraints: ScrollConstraints | null;
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
isAnimating?: boolean;
|
|
||||||
opts?: {
|
|
||||||
viewportZoomFactor?: number;
|
|
||||||
lockZoom?: boolean;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
@ -590,3 +580,13 @@ export type FrameNameBoundsCache = {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScrollConstraints = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
isAnimating?: boolean;
|
||||||
|
viewportZoomFactor?: number;
|
||||||
|
lockZoom?: boolean;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user