Source: anime/action.js

/**
 * 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,
};