playcanvas
Version:
PlayCanvas WebGL game engine
465 lines (462 loc) • 14.9 kB
JavaScript
import { EventHandler } from '../../../core/event-handler.js';
import { math } from '../../../core/math/math.js';
import { Asset } from '../../asset/asset.js';
import { SPRITE_RENDERMODE_SIMPLE } from '../../../scene/constants.js';
/**
* @import { EventHandle } from '../../../core/event-handle.js'
* @import { SpriteComponent } from './component.js'
* @import { Sprite } from '../../../scene/sprite.js'
*/ /**
* Handles playing of sprite animations and loading of relevant sprite assets.
*
* @category Graphics
*/ class SpriteAnimationClip extends EventHandler {
static{
/**
* Fired when the clip starts playing.
*
* @event
* @example
* clip.on('play', () => {
* console.log('Clip started playing');
* });
*/ this.EVENT_PLAY = 'play';
}
static{
/**
* Fired when the clip is paused.
*
* @event
* @example
* clip.on('pause', () => {
* console.log('Clip paused');
* });
*/ this.EVENT_PAUSE = 'pause';
}
static{
/**
* Fired when the clip is resumed.
*
* @event
* @example
* clip.on('resume', () => {
* console.log('Clip resumed');
* });
*/ this.EVENT_RESUME = 'resume';
}
static{
/**
* Fired when the clip is stopped.
*
* @event
* @example
* clip.on('stop', () => {
* console.log('Clip stopped');
* });
*/ this.EVENT_STOP = 'stop';
}
static{
/**
* Fired when the clip stops playing because it reached its end.
*
* @event
* @example
* clip.on('end', () => {
* console.log('Clip ended');
* });
*/ this.EVENT_END = 'end';
}
static{
/**
* Fired when the clip reached the end of its current loop.
*
* @event
* @example
* clip.on('loop', () => {
* console.log('Clip looped');
* });
*/ this.EVENT_LOOP = 'loop';
}
/**
* Create a new SpriteAnimationClip instance.
*
* @param {SpriteComponent} component - The sprite component managing this clip.
* @param {object} data - Data for the new animation clip.
* @param {number} [data.fps] - Frames per second for the animation clip.
* @param {boolean} [data.loop] - Whether to loop the animation clip.
* @param {string} [data.name] - The name of the new animation clip.
* @param {number} [data.spriteAsset] - The id of the sprite asset that this clip will play.
*/ constructor(component, data){
super(), /**
* @type {EventHandle|null}
* @private
*/ this._evtSetMeshes = null;
this._component = component;
this._frame = 0;
this._sprite = null;
this._spriteAsset = null;
this.spriteAsset = data.spriteAsset;
this.name = data.name;
this.fps = data.fps || 0;
this.loop = data.loop || false;
this._playing = false;
this._paused = false;
this._time = 0;
}
/**
* Gets the total duration of the animation in seconds.
*
* @type {number}
*/ get duration() {
if (this._sprite) {
const fps = this.fps || Number.MIN_VALUE;
return this._sprite.frameKeys.length / Math.abs(fps);
}
return 0;
}
/**
* Sets the index of the frame of the {@link Sprite} currently being rendered.
*
* @type {number}
*/ set frame(value) {
this._setFrame(value);
// update time to start of frame
const fps = this.fps || Number.MIN_VALUE;
this._setTime(this._frame / fps);
}
/**
* Gets the index of the frame of the {@link Sprite} currently being rendered.
*
* @type {number}
*/ get frame() {
return this._frame;
}
/**
* Sets whether the animation is currently paused.
*
* @type {boolean}
*/ get isPaused() {
return this._paused;
}
/**
* Sets whether the animation is currently playing.
*
* @type {boolean}
*/ get isPlaying() {
return this._playing;
}
/**
* Sets the current sprite used to play the animation.
*
* @type {Sprite}
*/ set sprite(value) {
if (this._sprite) {
this._evtSetMeshes?.off();
this._evtSetMeshes = null;
this._sprite.off('set:pixelsPerUnit', this._onSpritePpuChanged, this);
this._sprite.off('set:atlas', this._onSpriteMeshesChange, this);
if (this._sprite.atlas) {
this._sprite.atlas.off('set:texture', this._onSpriteMeshesChange, this);
}
}
this._sprite = value;
if (this._sprite) {
this._evtSetMeshes = this._sprite.on('set:meshes', this._onSpriteMeshesChange, this);
this._sprite.on('set:pixelsPerUnit', this._onSpritePpuChanged, this);
this._sprite.on('set:atlas', this._onSpriteMeshesChange, this);
if (this._sprite.atlas) {
this._sprite.atlas.on('set:texture', this._onSpriteMeshesChange, this);
}
}
if (this._component.currentClip === this) {
let mi;
// if we are clearing the sprite clear old mesh instance parameters
if (!value || !value.atlas) {
mi = this._component._meshInstance;
if (mi) {
mi.deleteParameter('texture_emissiveMap');
mi.deleteParameter('texture_opacityMap');
}
this._component._hideModel();
} else {
// otherwise show sprite
// update texture
if (value.atlas.texture) {
mi = this._component._meshInstance;
if (mi) {
mi.setParameter('texture_emissiveMap', value.atlas.texture);
mi.setParameter('texture_opacityMap', value.atlas.texture);
}
if (this._component.enabled && this._component.entity.enabled) {
this._component._showModel();
}
}
// if we have a time then force update
// frame based on the time (check if fps is not 0 otherwise time will be Infinity)
/* eslint-disable no-self-assign */ if (this.time && this.fps) {
this.time = this.time;
} else {
// if we don't have a time
// then force update frame counter
this.frame = this.frame;
}
/* eslint-enable no-self-assign */ }
}
}
/**
* Gets the current sprite used to play the animation.
*
* @type {Sprite}
*/ get sprite() {
return this._sprite;
}
/**
* Sets the id of the sprite asset used to play the animation.
*
* @type {number}
*/ set spriteAsset(value) {
const assets = this._component.system.app.assets;
let id = value;
if (value instanceof Asset) {
id = value.id;
}
if (this._spriteAsset !== id) {
if (this._spriteAsset) {
// clean old event listeners
const prev = assets.get(this._spriteAsset);
if (prev) {
this._unbindSpriteAsset(prev);
}
}
this._spriteAsset = id;
// bind sprite asset
if (this._spriteAsset) {
const asset = assets.get(this._spriteAsset);
if (!asset) {
this.sprite = null;
assets.on(`add:${this._spriteAsset}`, this._onSpriteAssetAdded, this);
} else {
this._bindSpriteAsset(asset);
}
} else {
this.sprite = null;
}
}
}
/**
* Gets the id of the sprite asset used to play the animation.
*
* @type {number}
*/ get spriteAsset() {
return this._spriteAsset;
}
/**
* Sets the current time of the animation in seconds.
*
* @type {number}
*/ set time(value) {
this._setTime(value);
if (this._sprite) {
this.frame = Math.min(this._sprite.frameKeys.length - 1, Math.floor(this._time * Math.abs(this.fps)));
} else {
this.frame = 0;
}
}
/**
* Gets the current time of the animation in seconds.
*
* @type {number}
*/ get time() {
return this._time;
}
// When sprite asset is added bind it
_onSpriteAssetAdded(asset) {
this._component.system.app.assets.off(`add:${asset.id}`, this._onSpriteAssetAdded, this);
if (this._spriteAsset === asset.id) {
this._bindSpriteAsset(asset);
}
}
// Hook up event handlers on sprite asset
_bindSpriteAsset(asset) {
asset.on('load', this._onSpriteAssetLoad, this);
asset.on('remove', this._onSpriteAssetRemove, this);
if (asset.resource) {
this._onSpriteAssetLoad(asset);
} else {
this._component.system.app.assets.load(asset);
}
}
_unbindSpriteAsset(asset) {
if (!asset) {
return;
}
asset.off('load', this._onSpriteAssetLoad, this);
asset.off('remove', this._onSpriteAssetRemove, this);
// unbind atlas
if (asset.resource && !asset.resource.atlas) {
this._component.system.app.assets.off(`load:${asset.data.textureAtlasAsset}`, this._onTextureAtlasLoad, this);
}
}
// When sprite asset is loaded make sure the texture atlas asset is loaded too
// If so then set the sprite, otherwise wait for the atlas to be loaded first
_onSpriteAssetLoad(asset) {
if (!asset.resource) {
this.sprite = null;
} else {
if (!asset.resource.atlas) {
const atlasAssetId = asset.data.textureAtlasAsset;
const assets = this._component.system.app.assets;
assets.off(`load:${atlasAssetId}`, this._onTextureAtlasLoad, this);
assets.once(`load:${atlasAssetId}`, this._onTextureAtlasLoad, this);
} else {
this.sprite = asset.resource;
}
}
}
// When atlas is loaded try to reset the sprite asset
_onTextureAtlasLoad(atlasAsset) {
const spriteAsset = this._spriteAsset;
if (spriteAsset instanceof Asset) {
this._onSpriteAssetLoad(spriteAsset);
} else {
this._onSpriteAssetLoad(this._component.system.app.assets.get(spriteAsset));
}
}
_onSpriteAssetRemove(asset) {
this.sprite = null;
}
// If the meshes are re-created make sure
// we update them in the mesh instance
_onSpriteMeshesChange() {
if (this._component.currentClip === this) {
this._component._showFrame(this.frame);
}
}
// Update frame if ppu changes for 9-sliced sprites
_onSpritePpuChanged() {
if (this._component.currentClip === this) {
if (this.sprite.renderMode !== SPRITE_RENDERMODE_SIMPLE) {
this._component._showFrame(this.frame);
}
}
}
/**
* Advances the animation, looping if necessary.
*
* @param {number} dt - The delta time.
* @private
*/ _update(dt) {
if (this.fps === 0) return;
if (!this._playing || this._paused || !this._sprite) return;
const dir = this.fps < 0 ? -1 : 1;
const time = this._time + dt * this._component.speed * dir;
const duration = this.duration;
const end = time > duration || time < 0;
this._setTime(time);
let frame = this.frame;
if (this._sprite) {
frame = Math.floor(this._sprite.frameKeys.length * this._time / duration);
} else {
frame = 0;
}
if (frame !== this._frame) {
this._setFrame(frame);
}
if (end) {
if (this.loop) {
this.fire('loop');
this._component.fire('loop', this);
} else {
this._playing = false;
this._paused = false;
this.fire('end');
this._component.fire('end', this);
}
}
}
_setTime(value) {
this._time = value;
const duration = this.duration;
if (this._time < 0) {
if (this.loop) {
this._time = this._time % duration + duration;
} else {
this._time = 0;
}
} else if (this._time > duration) {
if (this.loop) {
this._time %= duration;
} else {
this._time = duration;
}
}
}
_setFrame(value) {
if (this._sprite) {
// clamp frame
this._frame = math.clamp(value, 0, this._sprite.frameKeys.length - 1);
} else {
this._frame = value;
}
if (this._component.currentClip === this) {
this._component._showFrame(this._frame);
}
}
_destroy() {
// cleanup events
if (this._spriteAsset) {
const assets = this._component.system.app.assets;
this._unbindSpriteAsset(assets.get(this._spriteAsset));
}
// remove sprite
if (this._sprite) {
this.sprite = null;
}
// remove sprite asset
if (this._spriteAsset) {
this.spriteAsset = null;
}
}
/**
* Plays the animation. If it's already playing then this does nothing.
*/ play() {
if (this._playing) {
return;
}
this._playing = true;
this._paused = false;
this.frame = 0;
this.fire('play');
this._component.fire('play', this);
}
/**
* Pauses the animation.
*/ pause() {
if (!this._playing || this._paused) {
return;
}
this._paused = true;
this.fire('pause');
this._component.fire('pause', this);
}
/**
* Resumes the paused animation.
*/ resume() {
if (!this._paused) return;
this._paused = false;
this.fire('resume');
this._component.fire('resume', this);
}
/**
* Stops the animation and resets the animation to the first frame.
*/ stop() {
if (!this._playing) return;
this._playing = false;
this._paused = false;
this._time = 0;
this.frame = 0;
this.fire('stop');
this._component.fire('stop', this);
}
}
export { SpriteAnimationClip };