From 84b19a77d7b319fc81b372182fa4f06a6d5d6edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arno=C5=A1t=20Pleskot?= Date: Mon, 11 Sep 2023 17:07:07 +0200 Subject: [PATCH] feat: make constrained scroll working with split canvases --- src/components/App.tsx | 85 +++++++++++-- src/scene/scrollConstraints.ts | 225 ++++++--------------------------- 2 files changed, 115 insertions(+), 195 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 963f4fb340..199f3f31ba 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -355,8 +355,9 @@ import { StaticCanvas, InteractiveCanvas } from "./canvases"; import { Renderer } from "../scene/Renderer"; import { ShapeCache } from "../scene/ShapeCache"; import { - setScrollConstraints, calculateConstrainedScrollCenter, + constrainScrollState, + isViewportOutsideOfConstrainedArea, } from "../scene/scrollConstraints"; const AppContext = React.createContext(null!); @@ -2004,6 +2005,58 @@ class App extends React.Component { this.files, ); } + + if ( + this.state.scrollConstraints && + isViewportOutsideOfConstrainedArea(this.state) && + prevState.cursorButton === "down" && + this.state.cursorButton !== "down" && + !this.state.isLoading + ) { + const state = constrainScrollState(this.state); + const cancel = easeToValuesRAF({ + fromValues: { + scrollX: this.state.scrollX, + scrollY: this.state.scrollY, + zoom: this.state.zoom.value, + }, + toValues: { + scrollX: state.scrollX, + scrollY: state.scrollY, + zoom: state.zoom.value, + }, + interpolateValue: (from, to, progress, key) => { + // for zoom, use different easing + if (key === "zoom") { + return from * Math.pow(to / from, easeOut(progress)); + } + // handle using default + return undefined; + }, + onStep: ({ scrollX, scrollY, zoom }) => { + this.setState({ + scrollX, + scrollY, + zoom: { value: getNormalizedZoom(zoom) }, + }); + }, + onStart: () => { + this.setState({ shouldCacheIgnoreZoom: true }); + }, + onEnd: () => { + this.setState({ shouldCacheIgnoreZoom: false }); + }, + onCancel: () => { + this.setState({ shouldCacheIgnoreZoom: false }); + }, + duration: 500, + }); + + this.cancelInProgresAnimation = () => { + cancel(); + this.cancelInProgresAnimation = null; + }; + } } private renderInteractiveSceneCallback = ({ @@ -2669,9 +2722,27 @@ class App extends React.Component { /** use when changing scrollX/scrollY/zoom based on user interaction */ private translateCanvas: React.Component["setState"] = ( - state, + stateUpdate, ) => { this.cancelInProgresAnimation?.(); + + const partialNewState = + typeof stateUpdate === "function" + ? ( + stateUpdate as ( + prevState: Readonly, + props: Readonly, + ) => AppState + )(this.state, this.props) + : stateUpdate; + + const newState: AppState = { + ...this.state, + ...partialNewState, + }; + + const state = constrainScrollState(newState); + this.setState(state); }; @@ -8136,12 +8207,10 @@ class App extends React.Component { scrollConstraints: ScrollConstraints | null, ) => { if (scrollConstraints) { - setScrollConstraints(scrollConstraints, this.state, () => - this.setState({ - scrollConstraints, - viewModeEnabled: true, - }), - ); + this.setState({ + scrollConstraints, + viewModeEnabled: true, + }); } else { this.setState({ scrollConstraints: null, diff --git a/src/scene/scrollConstraints.ts b/src/scene/scrollConstraints.ts index 9b440c0fd9..e5895cf3ae 100644 --- a/src/scene/scrollConstraints.ts +++ b/src/scene/scrollConstraints.ts @@ -1,5 +1,4 @@ import { AppState, ScrollConstraints } from "../types"; -import { easeToValuesRAF, isShallowEqual } from "../utils"; import { getNormalizedZoom } from "./zoom"; /** @@ -274,162 +273,41 @@ const constrainScrollValues = ({ maxScrollY, Math.max(scrollY, minScrollY), ); - return { constrainedScrollX, constrainedScrollY, constrainedZoom }; + return { + scrollX: constrainedScrollX, + scrollY: constrainedScrollY, + zoom: constrainedZoom, + }; }; /** - * Animate the scroll values to the constrained area + * Determines whether the current viewport is outside the constrained area defined in the AppState. + * + * @param state - The application state containing scroll, zoom, and constraint information. + * @returns True if the viewport is outside the constrained area; otherwise undefined. */ -const animateConstrainedScroll = ({ - state, - constrainedScrollX, - constrainedScrollY, - opts, -}: { - state: AppState; - constrainedScrollX: number; - constrainedScrollY: number; - opts?: { - onStartCallback?: () => void; - onEndCallback?: () => void; - }; -}) => { - const { scrollX, scrollY, scrollConstraints } = state; - - const { onStartCallback, onEndCallback } = opts || {}; - - if (!scrollConstraints) { - return null; +export const isViewportOutsideOfConstrainedArea = (state: AppState) => { + if (!state.scrollConstraints) { + return; } - easeToValuesRAF({ - fromValues: { scrollX, scrollY }, - toValues: { - scrollX: constrainedScrollX, - scrollY: constrainedScrollY, - }, - onStep: ({ scrollX, scrollY }) => { - // TODO: this.setState({ scrollX, scrollY }); - }, - onStart: () => { - // TODO: this.setState({ - // scrollConstraints: { ...scrollConstraints, isAnimating: true }, - // }); - onStartCallback && onStartCallback(); - }, - onEnd: () => { - // TODO: this.setState({ - // scrollConstraints: { ...scrollConstraints, isAnimating: false }, - // }); - onEndCallback && onEndCallback(); - }, - }); -}; - -const 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; - } + const { scrollX, scrollY, width, height, scrollConstraints } = state; return ( - scrollX < scrollConstraints.x || - scrollX + width > scrollConstraints.x + scrollConstraints.width || + 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. + * Constrains the AppState scroll values within the defined scroll constraints. + * + * @param state - The original AppState with the current scroll position, dimensions, and constraints. + * @returns A new AppState object with scroll values constrained as per the defined constraints. */ -const handleConstrainedScrollStateChange = ({ - state, - constrainedScrollX, - constrainedScrollY, - constrainedZoom, - shouldAnimate, -}: { - constrainedScrollX: number; - constrainedScrollY: number; - constrainedZoom: AppState["zoom"]; - shouldAnimate?: boolean; - state: AppState; -}) => { - const { scrollX, scrollY } = state; - const isStateChanged = - constrainedScrollX !== scrollX || constrainedScrollY !== scrollY; - - if (isStateChanged) { - if (shouldAnimate) { - animateConstrainedScroll({ - state, - constrainedScrollX, - constrainedScrollY, - }); - - return null; - } - const constrainedState = { - scrollX: constrainedScrollX, - scrollY: constrainedScrollY, - zoom: constrainedZoom, - }; - - // TODO: this.setState(constrainedState); - return constrainedState; - } - - return null; -}; - -export const setScrollConstraints = ( - scrollConstraints: ScrollConstraints, - state: AppState, - onAnimteEndCallback?: () => void, -) => { - const { scrollX, scrollY, width, height, zoom, cursorButton } = state; - const constrainedScrollValues = constrainScrollValues({ - ...calculateConstraints({ - scrollConstraints, - zoom, - cursorButton, - width, - height, - }), - scrollX, - scrollY, - }); - animateConstrainedScroll({ - state, - ...constrainedScrollValues, - opts: { - onEndCallback: () => { - onAnimteEndCallback && onAnimteEndCallback(); - }, - }, - }); -}; - -let memoizedScrollConstraints: ReturnType | null = - null; - -export const constrainScrollState = (state: AppState, prevState: AppState) => { +export const constrainScrollState = (state: AppState): AppState => { if (!state.scrollConstraints || state.scrollConstraints.isAnimating) { return state; } @@ -443,55 +321,28 @@ export const constrainScrollState = (state: AppState, prevState: AppState) => { cursorButton, } = state; - const canUseMemoizedConstraints = - isShallowEqual(scrollConstraints, prevState.scrollConstraints ?? {}) && - isShallowEqual( - { width, height, zoom: zoom.value, cursorButton }, - { - width: prevState.width, - height: prevState.height, - zoom: prevState.zoom.value, - cursorButton: prevState.cursorButton, - } ?? {}, - ); - - const calculatedConstraints = - canUseMemoizedConstraints && !!memoizedScrollConstraints - ? memoizedScrollConstraints - : calculateConstraints({ - scrollConstraints, - width, - height, - zoom, - cursorButton, - }); - - memoizedScrollConstraints = calculatedConstraints; - - const constrainedScrollValues = constrainScrollValues({ - ...calculatedConstraints, + const constrainedValues = constrainScrollValues({ + ...calculateConstraints({ + scrollConstraints, + width, + height, + zoom, + cursorButton, + }), scrollX, scrollY, }); - const viewportOutsideOfConstrainedArea = isViewportOutsideOfConstrainedArea({ - scrollX, - scrollY, - width, - height, - scrollConstraints, - }); + const isStateChanged = + constrainedValues.scrollX !== scrollX || + constrainedValues.scrollY !== scrollY; - const shouldAnimate = - viewportOutsideOfConstrainedArea && - state.cursorButton !== "down" && - prevState.cursorButton === "down" && - prevState.zoom.value === state.zoom.value && - !state.isLoading; // Do not animate when app is initialized but scene is empty - it would cause flickering + if (isStateChanged) { + return { + ...state, + ...constrainedValues, + }; + } - return handleConstrainedScrollStateChange({ - state, - ...constrainedScrollValues, - shouldAnimate, - }); + return state; };