Skip to content

Commit

Permalink
Generic prop transition (#3443)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed Aug 27, 2019
1 parent 259bedb commit 1c0005d
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 112 deletions.
18 changes: 11 additions & 7 deletions dev-docs/RFCs/v7.x/property-transition-rfc.md
Expand Up @@ -2,8 +2,8 @@

> Note: This RFC Makes a strong distinction between "animation" and "transitions". For information the distinction, see the introduction, for more information aboutabout property animation, see the complementary RFC about that topic.
* **Authors**: Ib Green, ...
* **Date**: Aug 2018 (Initial version Aug 2017)
* **Authors**: Ib Green, Xiaoji Chen
* **Date**: Aug 2019 (Initial version Aug 2017)
* **Status**: **Draft**


Expand Down Expand Up @@ -41,11 +41,15 @@ The following types of animations are not included in this RFC.

### Background - Current State

In deck.gl v4.1:
* layers are only redrawn when the dirty flag of at least one layer is set, or when the viewport changes.
* layer dirty flags are only set when new layers are actually provided by the application (which typically happens when application state changes).

In deck.gl v7.2:

* layers support attribute transition with the `transitions` prop, where accessor names are used as keys.
* layers are redrawn if:
- user provided a new layer instance with changed props (as defined by the prop type)
- the viewport changes
- there is an ongoing attribute transition
- an async prop is loaded
- layer state has changed via calling `setState` internally

## Proposal: Automatic interpolation of properties

Expand All @@ -66,7 +70,7 @@ Options:

### Issue: Dependence on a PropTypes System

Reference: See the separate [prop-types RFC]()
Reference: See the separate [prop-types RFC](/dev-docs/RFCs/v6.3/prop-types-rfc.md)

A prop-types system would allow deck.gl to see if a certain property is interpolatable at all. If the prop is an integer or a float, or a color the interpolation strategy is clear, if a function or string we should not attempt interpolation. And deck.gl could start small, and gradually add interpolation support for more types as the system was built out.

Expand Down
53 changes: 6 additions & 47 deletions examples/website/3d-heatmap/app.js
@@ -1,4 +1,3 @@
/* global window */
import React, {Component} from 'react';
import {render} from 'react-dom';
import {StaticMap} from 'react-map-gl';
Expand Down Expand Up @@ -72,50 +71,6 @@ export class App extends Component {
this.state = {
elevationScale: elevationScale.min
};

this.startAnimationTimer = null;
this.intervalTimer = null;

this._startAnimate = this._startAnimate.bind(this);
this._animateHeight = this._animateHeight.bind(this);
}

componentDidMount() {
this._animate();
}

componentWillReceiveProps(nextProps) {
if (nextProps.data && this.props.data && nextProps.data.length !== this.props.data.length) {
this._animate();
}
}

componentWillUnmount() {
this._stopAnimate();
}

_animate() {
this._stopAnimate();

// wait 1.5 secs to start animation so that all data are loaded
this.startAnimationTimer = window.setTimeout(this._startAnimate, 1500);
}

_startAnimate() {
this.intervalTimer = window.setInterval(this._animateHeight, 20);
}

_stopAnimate() {
window.clearTimeout(this.startAnimationTimer);
window.clearTimeout(this.intervalTimer);
}

_animateHeight() {
if (this.state.elevationScale === elevationScale.max) {
this._stopAnimate();
} else {
this.setState({elevationScale: this.state.elevationScale + 1});
}
}

_renderLayers() {
Expand All @@ -128,15 +83,19 @@ export class App extends Component {
coverage,
data,
elevationRange: [0, 3000],
elevationScale: this.state.elevationScale,
elevationScale: data && data.length ? 50 : 0,
extruded: true,
getPosition: d => d,
onHover: this.props.onHover,
opacity: 1,
pickable: Boolean(this.props.onHover),
radius,
upperPercentile,
material
material,

transitions: {
elevationScale: 3000
}
})
];
}
Expand Down
Expand Up @@ -65,7 +65,9 @@ export default class TriangleLayer extends Layer {
draw({uniforms}) {
const {model} = this.state;
const {texture, maxTexture, colorTexture, intensity, threshold} = this.props;
model.setUniforms({texture, maxTexture, colorTexture, intensity, threshold}).draw();
model
.setUniforms({...uniforms, texture, maxTexture, colorTexture, intensity, threshold})
.draw();
}
}

Expand Down
1 change: 1 addition & 0 deletions modules/core/package.json
Expand Up @@ -33,6 +33,7 @@
"@loaders.gl/core": "^1.2.0-beta.4",
"@loaders.gl/images": "^1.2.0-beta.4",
"@luma.gl/core": "^7.3.0-alpha.1",
"@luma.gl/addons": "^7.3.0-alpha.1",
"gl-matrix": "^3.0.0",
"math.gl": "^2.3.0",
"mjolnir.js": "^2.1.2",
Expand Down
13 changes: 1 addition & 12 deletions modules/core/src/lib/attribute-transition-manager.js
Expand Up @@ -7,15 +7,6 @@ import Transition from '../transitions/transition';
import log from '../utils/log';
import assert from '../utils/assert';

const noop = () => {};
const DEFAULT_TRANSITION_SETTINGS = {
duration: 0,
easing: t => t,
onStart: noop,
onEnd: noop,
onInterrupt: noop
};

export default class AttributeTransitionManager {
constructor(gl, {id}) {
this.id = id;
Expand Down Expand Up @@ -289,11 +280,9 @@ export default class AttributeTransitionManager {

this.needsRedraw = true;

const transitionSettings = Object.assign({}, DEFAULT_TRANSITION_SETTINGS, settings);

// Attribute descriptor to transition from
transition.start(
Object.assign({}, this._getNextTransitionStates(transition, settings), transitionSettings)
Object.assign({}, this._getNextTransitionStates(transition, settings), settings)
);
}
}
22 changes: 22 additions & 0 deletions modules/core/src/lib/attribute-transition-utils.js
Expand Up @@ -8,6 +8,28 @@ const ATTRIBUTE_MAPPING = {
4: 'vec4'
};

const noop = () => {};
const DEFAULT_TRANSITION_SETTINGS = {
duration: 0,
easing: t => t,
onStart: noop,
onEnd: noop,
onInterrupt: noop
};

export function normalizeTransitionSettings(settings) {
if (!settings) {
return null;
}
if (Number.isFinite(settings)) {
settings = {duration: settings};
}
if (!settings.duration) {
return null;
}
return Object.assign({}, DEFAULT_TRANSITION_SETTINGS, settings);
}

export function getShaders(transitions) {
// Build shaders
const varyings = [];
Expand Down
7 changes: 3 additions & 4 deletions modules/core/src/lib/attribute.js
Expand Up @@ -9,6 +9,7 @@ import log from '../utils/log';
import BaseAttribute from './base-attribute';
import typedArrayManager from '../utils/typed-array-manager';
import {toDoublePrecisionArray} from '../utils/math-utils';
import {normalizeTransitionSettings} from './attribute-transition-utils';

const DEFAULT_STATE = {
isExternalBuffer: false,
Expand Down Expand Up @@ -194,11 +195,9 @@ export default class Attribute extends BaseAttribute {
let settings = Array.isArray(accessor) ? opts[accessor.find(a => opts[a])] : opts[accessor];

// Shorthand: use duration instead of parameter object
if (Number.isFinite(settings)) {
settings = {duration: settings};
}
settings = normalizeTransitionSettings(settings);

if (settings && settings.duration > 0) {
if (settings) {
return Object.assign({}, transition, settings);
}

Expand Down
10 changes: 9 additions & 1 deletion modules/core/src/lib/deck.js
Expand Up @@ -36,6 +36,7 @@ import {
setParameters,
lumaStats
} from '@luma.gl/core';
import {Timeline} from '@luma.gl/addons';
import {Stats} from 'probe.gl';
import {EventManager} from 'mjolnir.js';

Expand Down Expand Up @@ -624,11 +625,18 @@ export default class Deck {
// layerManager depends on viewport created by viewManager.
assert(this.viewManager);
const viewport = this.viewManager.getViewports()[0];

// timeline for transitions
const timeline = new Timeline();
timeline.play();
this.animationLoop.attachTimeline(timeline);

// Note: avoid React setState due GL animation loop / setState timing issue
this.layerManager = new LayerManager(gl, {
deck: this,
stats: this.stats,
viewport
viewport,
timeline
});

this.effectManager = new EffectManager();
Expand Down
12 changes: 5 additions & 7 deletions modules/core/src/lib/layer-manager.js
Expand Up @@ -20,6 +20,7 @@

import assert from '../utils/assert';
import {_ShaderCache as ShaderCache} from '@luma.gl/core';
import {Timeline} from '@luma.gl/addons';
import seer from 'seer';
import Layer from './layer';
import {LIFECYCLE} from '../lifecycle/constants';
Expand All @@ -45,7 +46,6 @@ const INITIAL_CONTEXT = Object.seal({
layerManager: null,
deck: null,
gl: null,
time: -1,

// Settings
useDevicePixels: true, // Exposed in case custom layers need to adjust sizes
Expand All @@ -67,7 +67,7 @@ const layerName = layer => (layer instanceof Layer ? `${layer}` : !layer ? 'null

export default class LayerManager {
// eslint-disable-next-line
constructor(gl, {deck, stats, viewport = null} = {}) {
constructor(gl, {deck, stats, viewport = null, timeline = null} = {}) {
// Currently deck.gl expects the DeckGL.layers array to be different
// whenever React rerenders. If the same layers array is used, the
// LayerManager's diffing algorithm will generate a fatal error and
Expand All @@ -88,7 +88,8 @@ export default class LayerManager {
shaderCache: gl && new ShaderCache({gl, _cachePrograms: true}),
stats: stats || new Stats({id: 'deck.gl'}),
// Make sure context.viewport is not empty on the first layer initialization
viewport: viewport || new Viewport({id: 'DEFAULT-INITIAL-VIEWPORT'}) // Current viewport, exposed to layers for project* function
viewport: viewport || new Viewport({id: 'DEFAULT-INITIAL-VIEWPORT'}), // Current viewport, exposed to layers for project* function
timeline: timeline || new Timeline()
});

this._needsRedraw = 'Initial render';
Expand Down Expand Up @@ -207,9 +208,6 @@ export default class LayerManager {

// Update layers from last cycle if `setNeedsUpdate()` has been called
updateLayers(animationProps = {}) {
if ('time' in animationProps) {
this.context.time = animationProps.time;
}
// NOTE: For now, even if only some layer has changed, we update all layers
// to ensure that layer id maps etc remain consistent even if different
// sublayers are rendered
Expand Down Expand Up @@ -292,7 +290,7 @@ export default class LayerManager {
// Finalize unmatched layers
const error2 = this._finalizeOldLayers(oldLayerMap);

this._needsUpdate = false;
this._needsUpdate = generatedLayers.some(layer => layer.hasUniformTransition());

const firstError = error || error2;
return {error: firstError, generatedLayers};
Expand Down

0 comments on commit 1c0005d

Please sign in to comment.