@deck.gl/core
Version:
deck.gl core library
245 lines (205 loc) • 8.38 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import Transition, {TransitionSettings as BaseTransitionSettings} from '../transitions/transition';
import TransitionInterpolator from '../transitions/transition-interpolator';
import type {IViewState} from './view-state';
import type {Timeline} from '@luma.gl/engine';
import type {InteractionState} from './controller';
const noop = () => {};
// Enums cannot be directly exported as they are not transpiled correctly into ES5, see https://github.com/visgl/deck.gl/issues/7130
export const TRANSITION_EVENTS = {
BREAK: 1,
SNAP_TO_END: 2,
IGNORE: 3
} as const;
type TransitionEvent = 1 | 2 | 3;
export type TransitionProps = {
/** Transition duration in milliseconds, default value 0, implies no transition. When using `FlyToInterpolator`, it can also be set to `'auto'`. */
transitionDuration?: number | 'auto';
/** An interpolator object that defines the transition behavior between two viewports. */
transitionInterpolator?: TransitionInterpolator;
/** Easing function that can be used to achieve effects like "Ease-In-Cubic", "Ease-Out-Cubic", etc. Default value performs Linear easing. */
transitionEasing?: (t: number) => number;
/** Controls how to process a new view state change that occurs during an existing transition. */
transitionInterruption?: TransitionEvent;
/** Callback fired when requested transition starts. */
onTransitionStart?: (transition: Transition) => void;
/** Callback fired when requested transition is interrupted. */
onTransitionInterrupt?: (transition: Transition) => void;
/** Callback fired when requested transition ends. */
onTransitionEnd?: (transition: Transition) => void;
};
const DEFAULT_EASING = t => t;
const DEFAULT_INTERRUPTION = TRANSITION_EVENTS.BREAK;
type TransitionSettings = BaseTransitionSettings & {
interpolator: TransitionInterpolator;
easing: (t: number) => number;
interruption: TransitionEvent;
startProps: Record<string, any>;
endProps: Record<string, any>;
};
export default class TransitionManager<ControllerState extends IViewState<ControllerState>> {
getControllerState: (props: any) => ControllerState;
props?: TransitionProps;
propsInTransition: Record<string, any> | null;
transition: Transition;
onViewStateChange: (params: {
viewState: Record<string, any>;
oldViewState: Record<string, any>;
}) => void;
onStateChange: (state: InteractionState) => void;
constructor(opts: {
timeline: Timeline;
getControllerState: (props: any) => ControllerState;
onViewStateChange?: (params: {
viewState: Record<string, any>;
oldViewState: Record<string, any>;
}) => void;
onStateChange?: (state: InteractionState) => void;
}) {
this.getControllerState = opts.getControllerState;
this.propsInTransition = null;
this.transition = new Transition(opts.timeline);
this.onViewStateChange = opts.onViewStateChange || noop;
this.onStateChange = opts.onStateChange || noop;
}
finalize(): void {
this.transition.cancel();
}
// Returns current transitioned viewport.
getViewportInTransition(): Record<string, any> | null {
return this.propsInTransition;
}
// Process the vewiport change, either ignore or trigger a new transition.
// Return true if a new transition is triggered, false otherwise.
processViewStateChange(nextProps: TransitionProps) {
let transitionTriggered = false;
const currentProps = this.props;
// Set this.props here as '_triggerTransition' calls '_updateViewport' that uses this.props.
this.props = nextProps;
// NOTE: Be cautious re-ordering statements in this function.
if (!currentProps || this._shouldIgnoreViewportChange(currentProps, nextProps)) {
return false;
}
if (this._isTransitionEnabled(nextProps)) {
let startProps = currentProps;
if (this.transition.inProgress) {
// @ts-expect-error
const {interruption, endProps} = this.transition.settings as TransitionSettings;
startProps = {
...currentProps,
...(interruption === TRANSITION_EVENTS.SNAP_TO_END
? endProps
: this.propsInTransition || currentProps)
};
}
this._triggerTransition(startProps, nextProps);
transitionTriggered = true;
} else {
this.transition.cancel();
}
return transitionTriggered;
}
updateTransition() {
this.transition.update();
}
// Helper methods
_isTransitionEnabled(props: TransitionProps): boolean {
const {transitionDuration, transitionInterpolator} = props;
return (
((transitionDuration as number) > 0 || transitionDuration === 'auto') &&
Boolean(transitionInterpolator)
);
}
_isUpdateDueToCurrentTransition(props: TransitionProps): boolean {
if (this.transition.inProgress && this.propsInTransition) {
// @ts-expect-error
return (this.transition.settings as TransitionSettings).interpolator.arePropsEqual(
props,
this.propsInTransition
);
}
return false;
}
_shouldIgnoreViewportChange(currentProps: TransitionProps, nextProps: TransitionProps): boolean {
if (this.transition.inProgress) {
// @ts-expect-error
const transitionSettings = this.transition.settings as TransitionSettings;
// Ignore update if it is requested to be ignored
return (
transitionSettings.interruption === TRANSITION_EVENTS.IGNORE ||
// Ignore update if it is due to current active transition.
this._isUpdateDueToCurrentTransition(nextProps)
);
}
if (this._isTransitionEnabled(nextProps)) {
// Ignore if none of the viewport props changed.
return (nextProps.transitionInterpolator as TransitionInterpolator).arePropsEqual(
currentProps,
nextProps
);
}
return true;
}
_triggerTransition(startProps: TransitionProps, endProps: TransitionProps): void {
const startViewstate = this.getControllerState(startProps);
const endViewStateProps = this.getControllerState(endProps).shortestPathFrom(startViewstate);
// update transitionDuration for 'auto' mode
const transitionInterpolator = endProps.transitionInterpolator as TransitionInterpolator;
const duration = transitionInterpolator.getDuration
? transitionInterpolator.getDuration(startProps, endProps)
: (endProps.transitionDuration as number);
if (duration === 0) {
return;
}
const initialProps = transitionInterpolator.initializeProps(startProps, endViewStateProps);
this.propsInTransition = {};
const transitionSettings: TransitionSettings = {
duration,
easing: endProps.transitionEasing || DEFAULT_EASING,
interpolator: transitionInterpolator,
interruption: endProps.transitionInterruption || DEFAULT_INTERRUPTION,
startProps: initialProps.start,
endProps: initialProps.end,
onStart: endProps.onTransitionStart,
onUpdate: this._onTransitionUpdate,
onInterrupt: this._onTransitionEnd(endProps.onTransitionInterrupt),
onEnd: this._onTransitionEnd(endProps.onTransitionEnd)
};
this.transition.start(transitionSettings);
this.onStateChange({inTransition: true});
this.updateTransition();
}
_onTransitionEnd(callback?: (transition: Transition) => void) {
return transition => {
this.propsInTransition = null;
this.onStateChange({
inTransition: false,
isZooming: false,
isPanning: false,
isRotating: false
});
callback?.(transition);
};
}
_onTransitionUpdate = transition => {
// NOTE: Be cautious re-ordering statements in this function.
const {
time,
settings: {interpolator, startProps, endProps, duration, easing}
} = transition;
const t = easing(time / duration);
const viewport = interpolator.interpolateProps(startProps, endProps, t);
// This gurantees all props (e.g. bearing, longitude) are normalized
// So when viewports are compared they are in same range.
this.propsInTransition = this.getControllerState({
...this.props,
...viewport
}).getViewportProps();
this.onViewStateChange({
viewState: this.propsInTransition,
oldViewState: this.props as TransitionProps
});
};
}