animation/Blend1DClip.js

// 1D Blend clip of blend tree
// http://docs.unity3d.com/Documentation/Manual/1DBlending.html

import Clip from './Clip';

var clipSortFunc = function (a, b) {
    return a.position < b.position;
};

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

/**
 * 1d blending node in animation blend tree.
 * output clip must have blend1D and copy method
 * @constructor
 * @alias clay.animation.Blend1DClip
 * @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 {number} [opts.position]
 * @param {clay.animation.Clip} [opts.output]
 */
var Blend1DClip = function (opts) {

    opts = opts || {};

    Clip.call(this, opts);
    /**
     * Output clip must have blend1D and copy method
     * @type {clay.animation.Clip}
     */
    this.output = opts.output || null;
    /**
     * @type {clay.animation.Blend1DClip.IClipInput[]}
     */
    this.inputs = opts.inputs || [];
    /**
     * @type {number}
     */
    this.position = 0;

    this._cacheKey = 0;
    this._cachePosition = -Infinity;

    this.inputs.sort(clipSortFunc);
};

Blend1DClip.prototype = new Clip();
Blend1DClip.prototype.constructor = Blend1DClip;

/**
 * @param {number} position
 * @param {clay.animation.Clip} inputClip
 * @param {number} [offset]
 * @return {clay.animation.Blend1DClip.IClipInput}
 */
Blend1DClip.prototype.addInput = function (position, inputClip, offset) {
    var obj = {
        position: position,
        clip: inputClip,
        offset: offset || 0
    };
    this.life = Math.max(inputClip.life, this.life);

    if (!this.inputs.length) {
        this.inputs.push(obj);
        return obj;
    }
    var len = this.inputs.length;
    if (this.inputs[0].position > position) {
        this.inputs.unshift(obj);
    } else if (this.inputs[len - 1].position <= position) {
        this.inputs.push(obj);
    } else {
        var key = this._findKey(position);
        this.inputs.splice(key, obj);
    }

    return obj;
};

Blend1DClip.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;
};

Blend1DClip.prototype.setTime = function (time) {
    var position = this.position;
    var inputs = this.inputs;
    var len = inputs.length;
    var min = inputs[0].position;
    var max = inputs[len-1].position;

    if (position <= min || position >= max) {
        var in0 = position <= min ? inputs[0] : inputs[len-1];
        var clip = in0.clip;
        var offset = in0.offset;
        clip.setTime((time + offset) % clip.life);
        // Input clip is a blend clip
        // PENDING
        if (clip.output instanceof Clip) {
            this.output.copy(clip.output);
        } else {
            this.output.copy(clip);
        }
    } else {
        var key = this._findKey(position);
        var in1 = inputs[key];
        var in2 = inputs[key + 1];
        var clip1 = in1.clip;
        var clip2 = in2.clip;
        // Set time on input clips
        clip1.setTime((time + in1.offset) % clip1.life);
        clip2.setTime((time + in2.offset) % clip2.life);

        var w = (this.position - in1.position) / (in2.position - in1.position);

        var c1 = clip1.output instanceof Clip ? clip1.output : clip1;
        var c2 = clip2.output instanceof Clip ? clip2.output : clip2;
        this.output.blend1D(c1, c2, w);
    }
};

/**
 * Clone a new Blend1D clip
 * @param {boolean} cloneInputs True if clone the input clips
 * @return {clay.animation.Blend1DClip}
 */
Blend1DClip.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;
};

// Find the key where position in range [inputs[key].position, inputs[key+1].position)
Blend1DClip.prototype._findKey = function (position) {
    var key = -1;
    var inputs = this.inputs;
    var len = inputs.length;
    if (this._cachePosition < position) {
        for (var i = this._cacheKey; i < len-1; i++) {
            if (position >= inputs[i].position && position < inputs[i+1].position) {
                key = i;
            }
        }
    } else {
        var s = Math.min(len-2, this._cacheKey);
        for (var i = s; i >= 0; i--) {
            if (position >= inputs[i].position && position < inputs[i+1].position) {
                key = i;
            }
        }
    }
    if (key >= 0) {
        this._cacheKey = key;
        this._cachePosition = position;
    }

    return key;
};

export default Blend1DClip;