playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
493 lines (492 loc) • 13.4 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
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";
class SpriteAnimationClip extends EventHandler {
/**
* 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
*/
__publicField(this, "_evtSetMeshes", null);
/**
* @type {SpriteComponent}
* @private
*/
__publicField(this, "_component");
/** @private */
__publicField(this, "_frame", 0);
/**
* @type {Sprite|null}
* @private
*/
__publicField(this, "_sprite", null);
/**
* @type {number|null}
* @private
*/
__publicField(this, "_spriteAsset", null);
/** @private */
__publicField(this, "_playing", false);
/** @private */
__publicField(this, "_paused", false);
/** @private */
__publicField(this, "_time", 0);
/**
* The name of this animation clip.
*
* @type {string|undefined}
*/
__publicField(this, "name");
/**
* Frames per second for this animation clip. A negative value plays the animation backwards.
*
* @type {number}
*/
__publicField(this, "fps", 0);
/**
* Whether to loop the animation clip when it reaches the end.
*
* @type {boolean}
*/
__publicField(this, "loop", false);
this._component = component;
this.fps = data.fps || 0;
this.loop = data.loop || false;
this.name = data.name;
this.spriteAsset = data.spriteAsset;
}
/**
* 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);
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 (!value || !value.atlas) {
mi = this._component._meshInstance;
if (mi) {
mi.deleteParameter("texture_emissiveMap");
mi.deleteParameter("texture_opacityMap");
}
this._component.removeFromLayers();
} else {
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.addToLayers();
}
}
if (this.time && this.fps) {
this.time = this.time;
} else {
this.frame = this.frame;
}
}
}
}
/**
* 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) {
const prev = assets.get(this._spriteAsset);
if (prev) {
this._unbindSpriteAsset(prev);
}
}
this._spriteAsset = id;
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);
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) {
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() {
if (this._spriteAsset) {
const assets = this._component.system.app.assets;
this._unbindSpriteAsset(assets.get(this._spriteAsset));
}
if (this._sprite) {
this.sprite = null;
}
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);
}
}
/**
* Fired when the clip starts playing.
*
* @event
* @example
* clip.on('play', () => {
* console.log('Clip started playing');
* });
*/
__publicField(SpriteAnimationClip, "EVENT_PLAY", "play");
/**
* Fired when the clip is paused.
*
* @event
* @example
* clip.on('pause', () => {
* console.log('Clip paused');
* });
*/
__publicField(SpriteAnimationClip, "EVENT_PAUSE", "pause");
/**
* Fired when the clip is resumed.
*
* @event
* @example
* clip.on('resume', () => {
* console.log('Clip resumed');
* });
*/
__publicField(SpriteAnimationClip, "EVENT_RESUME", "resume");
/**
* Fired when the clip is stopped.
*
* @event
* @example
* clip.on('stop', () => {
* console.log('Clip stopped');
* });
*/
__publicField(SpriteAnimationClip, "EVENT_STOP", "stop");
/**
* Fired when the clip stops playing because it reached its end.
*
* @event
* @example
* clip.on('end', () => {
* console.log('Clip ended');
* });
*/
__publicField(SpriteAnimationClip, "EVENT_END", "end");
/**
* Fired when the clip reached the end of its current loop.
*
* @event
* @example
* clip.on('loop', () => {
* console.log('Clip looped');
* });
*/
__publicField(SpriteAnimationClip, "EVENT_LOOP", "loop");
export {
SpriteAnimationClip
};