From dd9bde5ee7ce2f5ee2a10bd440ecf16055c230e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arno=C5=A1t=20Pleskot?= Date: Wed, 13 Sep 2023 14:54:39 +0200 Subject: [PATCH] feat: animation depending on timeout --- src/components/App.tsx | 139 ++++++++++++++++++++------------- src/scene/scrollConstraints.ts | 63 ++++++++++----- 2 files changed, 126 insertions(+), 76 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 199f3f31ba..0667ef18b8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -357,8 +357,8 @@ import { ShapeCache } from "../scene/ShapeCache"; import { calculateConstrainedScrollCenter, constrainScrollState, - isViewportOutsideOfConstrainedArea, } from "../scene/scrollConstraints"; +import { cancelRender } from "../renderer/renderScene"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); @@ -428,6 +428,8 @@ let isDraggingScrollBar: boolean = false; let currentScrollBars: ScrollBars = { horizontal: null, vertical: null }; let touchTimeout = 0; let invalidateContextMenu = false; +let scrollConstraintsAnimationTimeout: ReturnType | null = + null; /** * Map of youtube embed video states @@ -2005,58 +2007,6 @@ 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 = ({ @@ -2725,6 +2675,9 @@ class App extends React.Component { stateUpdate, ) => { this.cancelInProgresAnimation?.(); + if (scrollConstraintsAnimationTimeout) { + clearTimeout(scrollConstraintsAnimationTimeout); + } const partialNewState = typeof stateUpdate === "function" @@ -2739,11 +2692,87 @@ class App extends React.Component { const newState: AppState = { ...this.state, ...partialNewState, + ...(this.state.scrollConstraints && { + // manually reset if setState in onCancel wasn't committed yet + shouldCacheIgnoreZoom: false, + scrollConstraints: { + ...this.state.scrollConstraints, + isAnimating: false, + }, + }), }; - const state = constrainScrollState(newState); + const { state, shouldAnimate, animateTo } = constrainScrollState(newState); - this.setState(state); + this.setState(state, () => { + if (shouldAnimate && animateTo) { + scrollConstraintsAnimationTimeout = setTimeout(() => { + const cancel = easeToValuesRAF({ + fromValues: { + scrollX: newState.scrollX, + scrollY: newState.scrollY, + zoom: newState.zoom.value, + }, + toValues: { + scrollX: animateTo.scrollX, + scrollY: animateTo.scrollY, + zoom: animateTo.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((inAnimationState) => ({ + shouldCacheIgnoreZoom: true, + scrollConstraints: { + ...inAnimationState.scrollConstraints!, // existance scrollConstraints is checked in test for shouldAnimate + isAnimating: true, + }, + })); + cancelRender(); + }, + onEnd: () => { + this.setState((inAnimationState) => ({ + shouldCacheIgnoreZoom: false, + scrollConstraints: { + ...inAnimationState.scrollConstraints!, + isAnimating: false, + }, + })); + }, + onCancel: () => { + this.setState((inAnimationState) => { + return { + shouldCacheIgnoreZoom: false, + scrollConstraints: { + ...inAnimationState.scrollConstraints!, + isAnimating: false, + }, + }; + }); + }, + duration: 200, + }); + + this.cancelInProgresAnimation = () => { + cancel(); + this.cancelInProgresAnimation = null; + }; + }, 200); + } + }); }; setToast = ( diff --git a/src/scene/scrollConstraints.ts b/src/scene/scrollConstraints.ts index e5895cf3ae..973d2b2f06 100644 --- a/src/scene/scrollConstraints.ts +++ b/src/scene/scrollConstraints.ts @@ -286,9 +286,9 @@ const constrainScrollValues = ({ * @param state - The application state containing scroll, zoom, and constraint information. * @returns True if the viewport is outside the constrained area; otherwise undefined. */ -export const isViewportOutsideOfConstrainedArea = (state: AppState) => { +const isViewportOutsideOfConstrainedArea = (state: AppState) => { if (!state.scrollConstraints) { - return; + return false; } const { scrollX, scrollY, width, height, scrollConstraints } = state; @@ -307,19 +307,21 @@ export const isViewportOutsideOfConstrainedArea = (state: AppState) => { * @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. */ -export const constrainScrollState = (state: AppState): AppState => { +export const constrainScrollState = ( + state: AppState, +): { + state: AppState; + shouldAnimate: boolean; + animateTo: { + scrollX: AppState["scrollX"]; + scrollY: AppState["scrollY"]; + zoom: AppState["zoom"]; + } | null; +} => { if (!state.scrollConstraints || state.scrollConstraints.isAnimating) { - return state; + return { state, shouldAnimate: false, animateTo: null }; } - const { - scrollX, - scrollY, - width, - height, - scrollConstraints, - zoom, - cursorButton, - } = state; + const { scrollX, scrollY, width, height, scrollConstraints, zoom } = state; const constrainedValues = constrainScrollValues({ ...calculateConstraints({ @@ -327,22 +329,41 @@ export const constrainScrollState = (state: AppState): AppState => { width, height, zoom, - cursorButton, + cursorButton: "down", }), scrollX, scrollY, }); - const isStateChanged = - constrainedValues.scrollX !== scrollX || - constrainedValues.scrollY !== scrollY; + const shouldAnimate = isViewportOutsideOfConstrainedArea(state); - if (isStateChanged) { + const animateTo = shouldAnimate + ? constrainScrollValues({ + ...calculateConstraints({ + scrollConstraints, + width, + height, + zoom, + cursorButton: "up", + }), + scrollX, + scrollY, + }) + : null; + + if ( + constrainedValues.scrollX !== scrollX || + constrainedValues.scrollY !== scrollY + ) { return { - ...state, - ...constrainedValues, + state: { + ...state, + ...constrainedValues, + }, + shouldAnimate, + animateTo, }; } - return state; + return { state, shouldAnimate, animateTo }; };