import {compose} from '@avocado/core'; import {AnimationView} from '@avocado/graphics'; import {Rectangle, Vector} from '@avocado/math'; import {Animation} from '@avocado/timing'; import {StateProperty, Trait} from '../trait'; const decorate = compose( StateProperty('currentAnimation', { track: true, }), ); class AnimatedBase extends Trait { static defaultParams() { return { animations: {}, }; } static defaultState() { return { currentAnimation: 'idle', }; } initialize() { this._animations = this.params.get('animations').toJS(); this.animations = {}; this.animationViews = undefined; this.animationsPromise = undefined; } destroy() { if (this.animationViews) { for (const key in this.animationViews) { this.hideAnimation(key); const animationView = this.animationViews[key]; animationView.destroy(); } } for (const key in this.animations) { const animation = this.animations[key]; animation.destroy(); } } hideAnimation(key) { if (!this.animationViews) { return; } const animationView = this.animationViews[key]; if (!animationView) { return; } this.entity.container.removeChild(animationView); } loadAnimations() { if (this.animationsPromise) { return; } const animationPromises = []; // Load all animations. for (const key in this._animations) { const {uri} = this._animations[key]; const promise = Animation.load(uri).then((animation) => { // Zip with key to make populating animations and views trivial. return {animation, key}; }); animationPromises.push(promise); } this.animationsPromise = Promise.all(animationPromises); // Store keyed animations. this.animations = {}; this.animationsPromise.then((animations) => { animations.forEach(({animation, key}) => { this.animations[key] = animation; // Set direction upfront. animation.direction = this.entity.direction; }); // Bounding box update. this.entity.emit('visibleBoundingBoxesUpdated'); }); } loadAnimationImagesIfPossible() { if (!this.entity.container) { return; } if (!this.animationsPromise) { return; } if (this.animationViews) { return; } // Store keyed animation views. this.animationViews = {}; this.animationsPromise.then((animations) => { animations.forEach(({animation, key}) => { this.animationViews[key] = new AnimationView(animation); // Calculate any offset. const animationView = this.animationViews[key]; animationView.position = this.viewPositionFor(key); // Ensure animation is made visible upfront. const isCurrentAnimation = key === this.entity.currentAnimation; if (isCurrentAnimation) { this.showAnimation(key); } }); }); } offsetFor(key) { if (!this._animations[key] || !this._animations[key].offset) { return [0, 0]; } return this._animations[key].offset; } showAnimation(key) { if (!this.animationViews) { return; } const animationView = this.animationViews[key]; if (!animationView) { return; } this.entity.container.addChild(animationView); } viewPositionFor(key) { const animation = this.animations[key]; if (!animation) { return [0, 0]; } const size = animation.frameSize; const halfway = Vector.scale(size, -0.5); const offset = this.offsetFor(key); return Vector.add(halfway, offset); } hooks() { return { visibleBoundingBoxes: () => { const key = this.entity.currentAnimation; const animation = this.animations[key]; if (!animation) { return [0, 0, 0, 0]; } const viewPosition = this.viewPositionFor(key); const position = Vector.add(this.entity.position, viewPosition); return Rectangle.compose(position, animation.frameSize); }, } } listeners() { return { currentAnimationChanged: (oldKey) => { // Reset old animation. const oldAnimation = this.animations[oldKey]; if (oldAnimation) { oldAnimation.reset(); } // Bounding box update. this.entity.emit('visibleBoundingBoxesUpdated'); // Only client/graphics. if (!this.animationViews) { return; } // Swap the animation. this.hideAnimation(oldKey); this.showAnimation(this.entity.currentAnimation); }, directionChanged: () => { // All animations track direction. for (const key in this.animations) { const animation = this.animations[key]; animation.direction = this.entity.direction; } }, traitAdded: (type) => { if (-1 === [ 'animated', 'visible', ].indexOf(type)) { return; } this.loadAnimations(); this.loadAnimationImagesIfPossible(); }, }; } tick(elapsed) { // Only tick current animation. const animation = this.animations[this.entity.currentAnimation]; if (!animation) { return; } animation.tick(elapsed); } } export class Animated extends decorate(AnimatedBase) {}