/**
* Action module provides functionalities to create Flash/Blender like
* key based animation.
*
* @module engine/anime/action
*
* @requires engine/EventEmitter
* @requires engine/anime/utils
* @requires engine/anime/easing
*/
const EventEmitter = require('engine/EventEmitter');
const { getTargetAndKey } = require('./utils');
const { Easing } = require('./easing');
/**
* Type of channels
* @enum {number}
*/
const CHANNEL_TYPE = {
VALUE: 0,
EVENT: 1,
};
/**
* A single key of an action.
* @class Key
*/
class Key {
/**
* @constructor
* @param {number} time At which time.
* @param {*} value Value for this key.
* @param {function} easing Easing function.
*/
constructor(time, value, easing = Easing.Linear.None) {
/**
* Time of this key
* @type {number}
*/
this.time = time;
/**
* Value of this key.
* @type {*}
*/
this.value = value;
let easingFn = easing;
if (typeof(easing) === 'string') {
easing = easing.split('.');
easingFn = Easing[easing[0]][easing[1]];
}
/**
* Easing of this key.
* @type {function}
*/
this.easing = easingFn;
}
}
/**
* Channel represents a specific path of variable. Say we have an
* action that modifies `position` and two channels will be created
* for that as `position.x` and `position.y`.
* @class Channel
*/
class Channel {
/**
* @constructor
* @param {string} path Path of the target variable.
* @param {Action} owner Which action this channel is in.
* @param {CHANNEL_TYPE} type Type of this channel.
*/
constructor(path, owner, type = CHANNEL_TYPE.VALUE) {
/**
* Which action is this channel belongs to
* @type {module:engine/anime/action~Action}
*/
this.owner = owner;
/**
* Full path to the property
* @type {string}
*/
this.path = path;
var theType = type;
if (typeof(type) === 'string') {
theType = CHANNEL_TYPE[type.toUpperCase()];
}
/**
* Type of this channel, valid types are:
* - VALUE
* - EVENT
* @type {string|number}
*/
this.type = theType;
/**
* Key list.
* @type {array<module:engine/anime/action~Key>}
*/
this.keys = [];
/**
* Time between the first and last key.
* @type {number}
*/
this.duration = 0;
}
/**
* Insert a new key.
* @memberof Channel#
* @method insert
* @param {module:engine/anime/action~Key} key Key to insert.
*/
insert(key) {
this.keys.push(key);
// Sort keys based on their times
let i, tmpKey;
for (i = this.keys.length - 1; i > 0; i--) {
if (this.keys[i].time < this.keys[i - 1].time) {
tmpKey = this.keys[i - 1];
this.keys[i - 1] = this.keys[i];
this.keys[i] = tmpKey;
}
else {
break;
}
}
// Update duration
this.duration = this.keys[this.keys.length - 1].time;
// Update duration of owner Action
this.owner.duration = Math.max(this.owner.duration, this.duration);
}
/**
* Find the key at a specific time.
* @param {number} time Time to seek for.
* @return {module:engine/anime/action~Key} Will return undefined if no one can be found.
*/
findKey(time) {
// Empty channel
if (this.keys.length === 0) {
return undefined;
}
// Return the last key if time is
// larger than channel duration
if (time > this.duration) {
return this.keys[this.keys.length - 1];
}
// Find the key and return
let i, key = this.keys[0];
for (i = 0; i < this.keys.length - 1; i++) {
key = this.keys[i];
if (this.keys[i + 1].time > time) {
return key;
}
}
// Return the last key
return key;
}
}
/**
* Action is a data structure that represents an independent
* animation clip.
*
* `Action.create()` is prefered to create a new Action.
*
* @class Action
*/
class Action {
/**
* @constructor
*/
constructor() {
/**
* ID of this action
* @type {number}
*/
this.id = Action.nextId++;
/**
* Duration of this action (time between first and last key).
* @type {number}
*/
this.duration = 0;
/**
* Channel list.
* @type {array<module:engine/anime/action~Channel>}
*/
this.channels = [];
/**
* <Path, Channel> map
* @type {object}
*/
this.channelMap = {};
// Internal caches
this._latestChannel = null;
}
/**
* Insert a new channel.
* @memberof Action#
* @method channel
* @see Channel
* @param {string} path Path of the channel.
* @param {CHANNEL_TYPE} type Type of this channel.
* @return {module:engine/anime/action~Action} Self for chaining.
*/
channel(path, type) {
let channel = new Channel(path, this, type);
this.channels.push(channel);
this.channelMap[path] = channel;
this._latestChannel = channel;
return this;
}
/**
* Insert a new key to the last inserted channel.
* @memberof Action#
* @method key
* @param {number} time Time of the key.
* @param {*} value Value of the key.
* @param {function} easing Easing function.
* @return {module:engine/anime/action~Action} Self for chaining
*/
key(time, value, easing) {
if (!this._latestChannel) {
console.log('[Warning]: can not insert key without a channel!');
return this;
}
this._latestChannel.insert(new Key(time, value, easing));
return this;
}
/**
* Find a channel by path.
* @memberof Action#
* @method findChannel
* @param {string} path Path to a specific channel.
* @return {module:engine/anime/action~Channel} Channel with the path.
*/
findChannel(path) {
return this.channelMap[path];
}
}
Action.nextId = 0;
/**
* Create a new action.
* @return {module:engine/anime/action~Action} Action instance.
*/
Action.create = function create() {
return new Action();
};
/**
* Action player controls and plays an action defination.
*
* Usually you only need to run an action using {@link Scene#runAction}.
*
* @class ActionPlayer
*/
class ActionPlayer extends EventEmitter {
/**
* @constructor
* @param {module:engine/anime/action~Action} action Action to play.
* @param {object} target Target object this action is apply to.
*/
constructor(action, target) {
super();
this.action = action;
this.context = target;
this.channelCache = [];
/**
* Play speed (-1: reverse, 0: stop, 1: forward)
* @type {number}
* @default 1
*/
this.speed = 1;
/**
* Current time
* @type {number}
* @default 0
*/
this.time = 0;
/**
* Whether this action is finished
* @type {boolean}
* @default false
*/
this.finished = false;
/**
* Loop the action or not
* @type {boolean}
* @default false
*/
this.looped = true;
let channel, channels = this.action.channels;
for (let i = 0; i < channels.length; i++) {
// Channel: [propContext, propKey]
channel = getTargetAndKey(this.context, channels[i].path);
// Channel: [propContext, propKey, keys[], currKeyIdx]
channel.push(channels[i].keys, 0);
this.channelCache.push(channel);
}
}
/**
* Update.
* @memberof ActionPlayer
* @private
* @method _step
* @param {number} delta Delta time.
*/
_step(delta) {
let c, channel;
let keys, keyIdx, key, nextKey;
let length, progress, mod, change;
// Update time
this.time += delta * this.speed;
// Forward
if (this.speed > 0) {
// Reached the last frame?
if (this.time >= this.action.duration) {
if (this.looped) {
this.time = this.time % this.action.duration;
// Reset channels to their first keys
for (c = 0; c < this.channelCache.length; c++) {
channel = this.channelCache[c];
channel[3] = 0;
}
this.emit('loop', this);
}
else {
this.time = this.action.duration;
this.finished = true;
this.emit('finish', this);
return;
}
}
// Update animated channels
for (c = 0; c < this.channelCache.length; c++) {
channel = this.channelCache[c];
// Already passed the last key?
if (this.time > channel.duration) {
continue;
}
keys = channel[2];
keyIdx = channel[3];
// Reached next key?
if (keyIdx < channel[2].length - 2 && this.time >= channel[2][keyIdx + 1].time) {
keyIdx += 1;
channel[3] = keyIdx;
}
// Calculate progress of current key
key = keys[keyIdx];
nextKey = keys[keyIdx + 1];
length = nextKey.time - key.time;
change = nextKey.value - key.value;
progress = (this.time - key.time) / length;
mod = key.easing(progress);
// Update action target
channel[0][channel[1]] = key.value + change * mod;
// TODO: event keys
}
}
// Backward
else if (this.speed < 0) {
// Reached the first frame?
if (this.time < 0) {
if (this.looped) {
this.time += this.action.duration;
// Reset channels to their last keys
for (c = 0; c < this.channelCache.length; c++) {
channel = this.channelCache[c];
channel[3] = Math.max(channel[2].length - 2, 0);
}
this.emit('loop', this);
}
else {
this.time = 0;
this.finished = true;
this.emit('finish', this);
return;
}
}
// Update animated channels
for (c = 0; c < this.channelCache.length; c++) {
channel = this.channelCache[c];
keys = channel[2];
keyIdx = channel[3];
// Reached previous key?
if (keyIdx > 0 && this.time < channel[2][keyIdx].time) {
keyIdx -= 1;
channel[3] = keyIdx;
}
// Calculate progress of current key
key = keys[keyIdx];
nextKey = keys[keyIdx + 1];
length = nextKey.time - key.time;
change = nextKey.value - key.value;
progress = (this.time - key.time) / length;
mod = key.easing(progress);
// Update action target
channel[0][channel[1]] = key.value + change * mod;
// TODO: event keys
}
}
}
/**
* Go to a specific time
* @memberof ActionPlayer#
* @method goto
* @param {number} time The moment to go to.
*/
goto(/* time*/) {}
}
/**
* Create a player for actor
* @param {module:engine/anime/action~Action} action Action to play.
* @param {object} target Object this action will target.
* @return {module:engine/anime/action~ActionPlayer} ActionPlayer instance.
*/
ActionPlayer.create = function(action, target) {
return new ActionPlayer(action, target);
};
module.exports = {
CHANNEL_TYPE: CHANNEL_TYPE,
Key: Key,
Channel: Channel,
Action: Action,
ActionPlayer: ActionPlayer,
};