UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

225 lines (195 loc) 7.09 kB
// Copyright (c) 2021 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import {Component} from 'react'; import {bisectLeft} from 'd3-array'; import {requestAnimationFrame, cancelAnimationFrame} from 'global/window'; import Console from 'global/console'; import {BASE_SPEED, FPS, ANIMATION_WINDOW} from 'constants/default-settings'; function AnimationControllerFactory() { /** * 4 Animation Window Types * 1. free * |-> |-> * Current time is a fixed range, animate a moving window that calls next animation frames continuously * The increment id based on domain / BASE_SPEED * SPEED * * 2. incremental * | |-> * Same as free, current time is a growing range, only the max value of range increment during animation. * The increment is also based on domain / BASE_SPEED * SPEED * * 3. point * o -> o * Current time is a point, animate a moving point calls next animation frame continuously * The increment is based on domain / BASE_SPEED * SPEED * * 4. interval * o ~> o * Current time is a point. An array of sorted time steps are provided, * animate a moving point jumps to the next step */ class AnimationController extends Component { // TODO: convert the entire component to use hooks in the next PR static defaultProps = { baseSpeed: BASE_SPEED, speed: 1, steps: null, animationWindow: ANIMATION_WINDOW.free }; state = { isAnimating: false }; componentDidMount() { this._startOrPauseAnimation(); } componentDidUpdate() { this._startOrPauseAnimation(); } componentWillUnmount() { if (this._timer) { cancelAnimationFrame(this._timer); } } _timer = null; _startOrPauseAnimation() { const {isAnimating, speed} = this.props; if (!this._timer && isAnimating && speed > 0) { this._startAnimation(); } else if (this._timer && !isAnimating) { this._pauseAnimation(); } } _animate = delay => { this._startTime = new Date().getTime(); const loop = () => { const current = new Date().getTime(); // @ts-ignore const delta = current - this._startTime; if (delta >= delay) { this._nextFrame(); this._startTime = new Date().getTime(); } else { this._timer = requestAnimationFrame(loop); } }; this._timer = requestAnimationFrame(loop); }; _resetAnimationByDomain = () => { const {domain, value, animationWindow} = this.props; if (Array.isArray(value)) { if (animationWindow === ANIMATION_WINDOW.incremental) { this.props.updateAnimation([value[0], value[0] + 1]); } else { this.props.updateAnimation([domain[0], domain[0] + value[1] - value[0]]); } } else { this.props.updateAnimation(domain[0]); } }; _resetAnimtionByTimeStep = () => { // go to the first steps this.props.updateAnimation([this.props.steps[0], 0]); }; _resetAnimation = () => { if (this.props.animationWindow === ANIMATION_WINDOW.interval) { this._resetAnimtionByTimeStep(); } else { this._resetAnimationByDomain(); } }; _startAnimation = () => { const {speed} = this.props; this._clearTimer(); if (speed > 0) { if (this.props.animationWindow === ANIMATION_WINDOW.interval) { // animate by interval // 30*600 const {steps} = this.props; if (!Array.isArray(steps) || !steps.length) { Console.warn('animation steps should be an array'); return; } // when speed = 1, animation should loop through 600 frames at 60 FPS // calculate delay based on # steps const delay = (BASE_SPEED * (1000 / FPS)) / steps.length / (speed || 1); this._animate(delay); } else { this._timer = requestAnimationFrame(this._nextFrame); } } this.setState({isAnimating: true}); }; _clearTimer = () => { if (this._timer) { cancelAnimationFrame(this._timer); this._timer = null; } }; _pauseAnimation = () => { this._clearTimer(); this.setState({isAnimating: false}); }; _nextFrame = () => { this._timer = null; const nextValue = this.props.animationWindow === ANIMATION_WINDOW.interval ? this._nextFrameByTimeStep() : this._nextFrameByDomain(); this.props.updateAnimation(nextValue); }; _nextFrameByDomain() { const {domain, value, speed, baseSpeed, animationWindow} = this.props; const delta = ((domain[1] - domain[0]) / baseSpeed) * speed; // loop when reaches the end // current time is a range if (Array.isArray(value)) { let value0; let value1; const readEnd = value[1] + delta > domain[1]; if (animationWindow === ANIMATION_WINDOW.incremental) { value0 = value[0]; value1 = readEnd ? value[0] + 1 : value[1] + delta; } else { value0 = readEnd ? domain[0] : value[0] + delta; value1 = value0 + value[1] - value[0]; } return [value0, value1]; } // current time is a point return value + delta > domain[1] ? domain[0] : value + delta; } _nextFrameByTimeStep() { const {steps, value} = this.props; const val = Array.isArray(value) ? value[0] : value; const index = bisectLeft(steps, val); const nextIdx = index >= steps.length - 1 ? 0 : index + 1; return [steps[nextIdx], nextIdx]; } render() { const {isAnimating} = this.state; const {children} = this.props; return typeof children === 'function' ? children(isAnimating, this._startAnimation, this._pauseAnimation, this._resetAnimation) : null; } } return AnimationController; } export default AnimationControllerFactory;