animation/Blend2DClip.js

// 2D Blend clip of blend tree
// http://docs.unity3d.com/Documentation/Manual/2DBlending.html
import Clip from './Clip';
import delaunay from '../util/delaunay';
import Vector2 from '../math/Vector2';

/**
 * @typedef {Object} clay.animation.Blend2DClip.IClipInput
 * @property {clay.Vector2} position
 * @property {clay.animation.Clip} clip
 * @property {number} offset
 */

/**
 * 2d blending node in animation blend tree.
 * output clip must have blend2D method
 * @constructor
 * @alias clay.animation.Blend2DClip
 * @extends clay.animation.Clip
 *
 * @param {Object} [opts]
 * @param {string} [opts.name]
 * @param {Object} [opts.target]
 * @param {number} [opts.life]
 * @param {number} [opts.delay]
 * @param {number} [opts.gap]
 * @param {number} [opts.playbackRatio]
 * @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation
 * @param {string|Function} [opts.easing]
 * @param {Function} [opts.onframe]
 * @param {Function} [opts.onfinish]
 * @param {Function} [opts.onrestart]
 * @param {object[]} [opts.inputs]
 * @param {clay.Vector2} [opts.position]
 * @param {clay.animation.Clip} [opts.output]
 */
var Blend2DClip = function (opts) {

    opts = opts || {};

    Clip.call(this, opts);
    /**
     * Output clip must have blend2D method
     * @type {clay.animation.Clip}
     */
    this.output = opts.output || null;
    /**
     * @type {clay.animation.Blend2DClip.IClipInput[]}
     */
    this.inputs = opts.inputs || [];
    /**
     * @type {clay.Vector2}
     */
    this.position = new Vector2();

    this._cacheTriangle = null;

    this._triangles = [];

    this._updateTriangles();
};

Blend2DClip.prototype = new Clip();
Blend2DClip.prototype.constructor = Blend2DClip;
/**
 * @param {clay.Vector2} position
 * @param {clay.animation.Clip} inputClip
 * @param {number} [offset]
 * @return {clay.animation.Blend2DClip.IClipInput}
 */
Blend2DClip.prototype.addInput = function (position, inputClip, offset) {
    var obj = {
        position : position,
        clip : inputClip,
        offset : offset || 0
    };
    this.inputs.push(obj);
    this.life = Math.max(inputClip.life, this.life);
    // TODO Change to incrementally adding
    this._updateTriangles();

    return obj;
};

// Delaunay triangulate
Blend2DClip.prototype._updateTriangles = function () {
    var inputs = this.inputs.map(function (a) {
        return a.position;
    });
    this._triangles = delaunay.triangulate(inputs, 'array');
};

Blend2DClip.prototype.step = function (time, dTime, silent) {

    var ret = Clip.prototype.step.call(this, time);

    if (ret !== 'finish') {
        this.setTime(this.getElapsedTime());
    }

    // PENDING Schedule
    if (!silent && ret !== 'paused') {
        this.fire('frame');
    }
    return ret;
};

Blend2DClip.prototype.setTime = function (time) {
    var res = this._findTriangle(this.position);
    if (!res) {
        return;
    }
    // In Barycentric
    var a = res[1]; // Percent of clip2
    var b = res[2]; // Percent of clip3

    var tri = res[0];

    var in1 = this.inputs[tri.indices[0]];
    var in2 = this.inputs[tri.indices[1]];
    var in3 = this.inputs[tri.indices[2]];
    var clip1 = in1.clip;
    var clip2 = in2.clip;
    var clip3 = in3.clip;

    clip1.setTime((time + in1.offset) % clip1.life);
    clip2.setTime((time + in2.offset) % clip2.life);
    clip3.setTime((time + in3.offset) % clip3.life);

    var c1 = clip1.output instanceof Clip ? clip1.output : clip1;
    var c2 = clip2.output instanceof Clip ? clip2.output : clip2;
    var c3 = clip3.output instanceof Clip ? clip3.output : clip3;

    this.output.blend2D(c1, c2, c3, a, b);
};

/**
 * Clone a new Blend2D clip
 * @param {boolean} cloneInputs True if clone the input clips
 * @return {clay.animation.Blend2DClip}
 */
Blend2DClip.prototype.clone = function (cloneInputs) {
    var clip = Clip.prototype.clone.call(this);
    clip.output = this.output.clone();
    for (var i = 0; i < this.inputs.length; i++) {
        var inputClip = cloneInputs ? this.inputs[i].clip.clone(true) : this.inputs[i].clip;
        clip.addInput(this.inputs[i].position, inputClip, this.inputs[i].offset);
    }
    return clip;
};

Blend2DClip.prototype._findTriangle = function (position) {
    if (this._cacheTriangle) {
        var res = delaunay.contains(this._cacheTriangle.vertices, position.array);
        if (res) {
            return [this._cacheTriangle, res[0], res[1]];
        }
    }
    for (var i = 0; i < this._triangles.length; i++) {
        var tri = this._triangles[i];
        var res = delaunay.contains(tri.vertices, this.position.array);
        if (res) {
            this._cacheTriangle = tri;
            return [tri, res[0], res[1]];
        }
    }
};

export default Blend2DClip;