feat: animation depending on timeout

This commit is contained in:
Arnošt Pleskot 2023-09-13 14:54:39 +02:00
parent b99bf74c3d
commit dd9bde5ee7
No known key found for this signature in database
2 changed files with 126 additions and 76 deletions

View file

@ -357,8 +357,8 @@ import { ShapeCache } from "../scene/ShapeCache";
import { import {
calculateConstrainedScrollCenter, calculateConstrainedScrollCenter,
constrainScrollState, constrainScrollState,
isViewportOutsideOfConstrainedArea,
} from "../scene/scrollConstraints"; } from "../scene/scrollConstraints";
import { cancelRender } from "../renderer/renderScene";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@ -428,6 +428,8 @@ let isDraggingScrollBar: boolean = false;
let currentScrollBars: ScrollBars = { horizontal: null, vertical: null }; let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
let touchTimeout = 0; let touchTimeout = 0;
let invalidateContextMenu = false; let invalidateContextMenu = false;
let scrollConstraintsAnimationTimeout: ReturnType<typeof setTimeout> | null =
null;
/** /**
* Map of youtube embed video states * Map of youtube embed video states
@ -2005,58 +2007,6 @@ class App extends React.Component<AppProps, AppState> {
this.files, 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 = ({ private renderInteractiveSceneCallback = ({
@ -2725,6 +2675,9 @@ class App extends React.Component<AppProps, AppState> {
stateUpdate, stateUpdate,
) => { ) => {
this.cancelInProgresAnimation?.(); this.cancelInProgresAnimation?.();
if (scrollConstraintsAnimationTimeout) {
clearTimeout(scrollConstraintsAnimationTimeout);
}
const partialNewState = const partialNewState =
typeof stateUpdate === "function" typeof stateUpdate === "function"
@ -2739,11 +2692,87 @@ class App extends React.Component<AppProps, AppState> {
const newState: AppState = { const newState: AppState = {
...this.state, ...this.state,
...partialNewState, ...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 = ( setToast = (

View file

@ -286,9 +286,9 @@ const constrainScrollValues = ({
* @param state - The application state containing scroll, zoom, and constraint information. * @param state - The application state containing scroll, zoom, and constraint information.
* @returns True if the viewport is outside the constrained area; otherwise undefined. * @returns True if the viewport is outside the constrained area; otherwise undefined.
*/ */
export const isViewportOutsideOfConstrainedArea = (state: AppState) => { const isViewportOutsideOfConstrainedArea = (state: AppState) => {
if (!state.scrollConstraints) { if (!state.scrollConstraints) {
return; return false;
} }
const { scrollX, scrollY, width, height, scrollConstraints } = state; 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. * @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. * @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) { if (!state.scrollConstraints || state.scrollConstraints.isAnimating) {
return state; return { state, shouldAnimate: false, animateTo: null };
} }
const { const { scrollX, scrollY, width, height, scrollConstraints, zoom } = state;
scrollX,
scrollY,
width,
height,
scrollConstraints,
zoom,
cursorButton,
} = state;
const constrainedValues = constrainScrollValues({ const constrainedValues = constrainScrollValues({
...calculateConstraints({ ...calculateConstraints({
@ -327,22 +329,41 @@ export const constrainScrollState = (state: AppState): AppState => {
width, width,
height, height,
zoom, zoom,
cursorButton, cursorButton: "down",
}), }),
scrollX, scrollX,
scrollY, scrollY,
}); });
const isStateChanged = const shouldAnimate = isViewportOutsideOfConstrainedArea(state);
constrainedValues.scrollX !== scrollX ||
constrainedValues.scrollY !== scrollY;
if (isStateChanged) { const animateTo = shouldAnimate
? constrainScrollValues({
...calculateConstraints({
scrollConstraints,
width,
height,
zoom,
cursorButton: "up",
}),
scrollX,
scrollY,
})
: null;
if (
constrainedValues.scrollX !== scrollX ||
constrainedValues.scrollY !== scrollY
) {
return { return {
...state, state: {
...constrainedValues, ...state,
...constrainedValues,
},
shouldAnimate,
animateTo,
}; };
} }
return state; return { state, shouldAnimate, animateTo };
}; };