Skip to content

Instantly share code, notes, and snippets.

@Jeremboo
Last active September 19, 2017 15:14
Show Gist options
  • Save Jeremboo/6b20e8c69a3005a3c1801e26f6f31467 to your computer and use it in GitHub Desktop.
Save Jeremboo/6b20e8c69a3005a3c1801e26f6f31467 to your computer and use it in GitHub Desktop.
Class to manage animations with GreenSock
/**
* V1.0.0
* @author jeremboo https://jeremieboulay.fr
*
* REACT ANIMATED COMPONENT :
* Class to manage react components animated by GreenSock timelines.
* You can show/hide child components and their children.
*
* PARENT: Start to animate components
* -------------
*
* export default class Parent extends AnimComponent {
* constructor(props) {
* super(props);
* }
*
* componentDidMount() {
* // Start animation of Child1 component then on complete, start Child2 animation
* this.updateAnim('child1', true, {}, '+=1')
* .then(() => this.updateAnim('child2', true))
* .then(() => { console.log('animationComplete'); })
* ;
* }
*
* render() {
* return (
* <section className="Parent">
* <Child anim={this.anims.child1} />
* <Child anim={this.anims.child2} />
* </section>
* );
* }
* }
*
*
* CHILD: Build the timelines
* -----------
*
* export default class Child extends AnimComponent {
* constructor(props) {
* super(props);
* this.section = false;
* }
*
* componentDidMount() {
* // Timeline lautched to show the component
* this.timelineIn.fromTo(this.section, 0.3, { opacity: 0 }, { opacity: 1 });
* // Timeline lautched to hide the component
* this.timelineOut.to(this.section, 0.5, { opacity: 0 });
* super.componentDidMount();
* }
*
* componentDidUpdate() { super.componentDidUpdate(); }
*
* render() {
* return (
* <section ref={r => { this.section = r; }}>
* Child1
* </section>
* );
* }
* }
*
* ...
*
*
* OTHER POSIBILITIES:
* ----------------------
*
* You can also overwrite the show/hide methods to do what you whant. Ex :
*
* show() {
* this.updateAnim('childchild', true).then(() => this.timelineIn.play);
* }
*
* hide() {
* this.timelineIn.reverse(0);
* }
*
**/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { TimelineLite } from 'gsap';
const TM_RESOLVER = 'fireComplete';
export default class AnimComponent extends Component {
constructor(props) {
super(props);
this._shown = false;
this._animComplete = true;
this.anims = {};
this.timelineIn = new TimelineLite({ paused: true });
this.timelineOut = new TimelineLite({ paused: true });
this._oldPosition = 0;
this._oldCallback = false;
this.updateAnim = this.updateAnim.bind(this);
this._setAnim = this._setAnim.bind(this);
this._testAnim = this._testAnim.bind(this);
this.show = this.show.bind(this);
this.hide = this.hide.bind(this);
}
componentDidMount() {
this._testAnim();
}
componentDidUpdate() {
this._testAnim();
}
componentWillUnmount() {}
/**
* Fired when the component is updated and test the anim property.
* If this one exist and is different to the last time,
* she updates the timelines to add callback at the good position
* and launch `show` or `hide` method.
*/
_testAnim() {
const { anim } = this.props;
if (!anim || typeof anim.shown !== 'boolean' || typeof anim.callback !== 'function') {
return;
}
if (anim.shown !== this._shown) {
this._shown = anim.shown;
this._animComplete = false;
/** ***********************
* Sets and updates the callback on the two timelines.
* It's for both because `show()` and `hide()` methods can be overwritten by the user.
*/
// Stops and removes the old callback to the timelines
this.timelineIn.pause();
this.timelineOut.pause();
this.timelineIn.remove(this._oldCallback, this._oldPosition);
this.timelineOut.remove(this._oldCallback, this._oldPosition);
// Set the new callback to a new position
this.timelineIn.add(anim.callback, anim.position);
this.timelineOut.add(anim.callback, anim.position);
// Save the params to remove them the next time
this._oldPosition = anim.position;
this._oldCallback = anim.callback;
// TODO the HACK for a timeline.reverse() usage must be remplaced.
this.timelineIn.eventCallback('onReverseComplete', anim.callback);
this.timelineOut.eventCallback('onReverseComplete', anim.callback);
// Launch the animation
if (anim.shown) {
this.show(anim.payload);
} else {
this.hide(anim.payload);
}
}
}
/**
* Update an child anim status
*
* @type {String} name ... The name of the child component.
* @type {Object} object . The data to update
*/
_setAnim(name, object) {
this.anims[name] = Object.assign(this.anims[name] || {}, object);
}
/**
* ************
* PARENT CONTROLS
* ************
*/
/**
* Creates a promise and updates the children animation status managed through `this.anims`
* to start the animation of one animComponent child.
* The Promise is resolved when the child's animation is ended.
*
* @type {String} name ...... The name of the child component.
* @type {Boolean} shown .... The new status of the child component.
* @type {Object} payload ... ???
* @type {String} position .. Same utility as a Timeline animation.
*/
updateAnim(name, shown, payload = {}, position = '+=0') {
return new Promise((resolve) => {
// Callback who resolve the Promise and show the animComplete status.
const callback = (object = {}) => {
this._animComplete = true;
this._setAnim(name, Object.assign(object, { animComplete: true }));
resolve();
};
// TEST IF HE HAVE THE SAME UPDATE
if (
// is never appeared and he want to be hidden
(this.anims[name] === undefined && shown === false) ||
// or is already appeared and have the same update
(this.anims[name] !== undefined && this.anims[name].shown === shown)
) {
callback({ shown, payload });
return;
}
// Launch the animation
this._setAnim(name, { shown, callback, payload, position, animComplete: false });
this.forceUpdate();
});
}
/**
* ************
* SHOW / HIDE
* ************
* Could be overwritten by the extended class
*/
show() {
this.timelineIn.play(0);
}
hide() {
this.timelineOut.play(0);
}
/**
* ************
* TEST
* ************
*/
isHidden() {
return !this._shown && this._animComplete;
}
/**
* If a child is completly hidden after the animation
*/
childIsHidden(name) {
return (this.anims[name] === undefined)
|| (!this.anims[name].shown && this.anims[name].animComplete)
;
}
}
AnimComponent.propTypes = {
anim: PropTypes.shape({
shown: PropTypes.bool,
resolve: PropTypes.func,
}),
};
AnimComponent.defaultProps = {
anim: {},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment