cables_dev/cables/src/core/anim.js
import { Logger } from "cables-shared-client";
import { Key } from "./anim_key.js";
import { CONSTANTS } from "./constants.js";
import { EventTarget } from "./eventtarget.js";
/**
* Keyframed interpolated animation.
*
* Available Easings:
* <code>
* CONSTANTS.ANIM.EASING_LINEAR
* CONSTANTS.ANIM.EASING_ABSOLUTE
* CONSTANTS.ANIM.EASING_SMOOTHSTEP
* CONSTANTS.ANIM.EASING_SMOOTHERSTEP
* CONSTANTS.ANIM.EASING_CUBICSPLINE
* CONSTANTS.ANIM.EASING_CUBIC_IN
* CONSTANTS.ANIM.EASING_CUBIC_OUT
* CONSTANTS.ANIM.EASING_CUBIC_INOUT
* CONSTANTS.ANIM.EASING_EXPO_IN
* CONSTANTS.ANIM.EASING_EXPO_OUT
* CONSTANTS.ANIM.EASING_EXPO_INOUT
* CONSTANTS.ANIM.EASING_SIN_IN
* CONSTANTS.ANIM.EASING_SIN_OUT
* CONSTANTS.ANIM.EASING_SIN_INOUT
* CONSTANTS.ANIM.EASING_BACK_IN
* CONSTANTS.ANIM.EASING_BACK_OUT
* CONSTANTS.ANIM.EASING_BACK_INOUT
* CONSTANTS.ANIM.EASING_ELASTIC_IN
* CONSTANTS.ANIM.EASING_ELASTIC_OUT
* CONSTANTS.ANIM.EASING_BOUNCE_IN
* CONSTANTS.ANIM.EASING_BOUNCE_OUT
* CONSTANTS.ANIM.EASING_QUART_IN
* CONSTANTS.ANIM.EASING_QUART_OUT
* CONSTANTS.ANIM.EASING_QUART_INOUT
* CONSTANTS.ANIM.EASING_QUINT_IN
* CONSTANTS.ANIM.EASING_QUINT_OUT
* CONSTANTS.ANIM.EASING_QUINT_INOUT
* </code>
* @class
* @param cfg
* @example
* var anim=new CABLES.Anim();
* anim.setValue(0,0); // set value 0 at 0 seconds
* anim.setValue(10,1); // set value 1 at 10 seconds
* anim.getValue(5); // get value at 5 seconds - this returns 0.5
*/
const Anim = function (cfg)
{
EventTarget.apply(this);
cfg = cfg || {};
this.keys = [];
this.onChange = null;
this.stayInTimeline = false;
this.loop = false;
this._log = new Logger("Anim");
this._lastKeyIndex = 0;
this._cachedIndex = 0;
this.name = cfg.name || null;
/**
* @member defaultEasing
* @memberof Anim
* @instance
* @type {Number}
*/
this.defaultEasing = cfg.defaultEasing || CONSTANTS.ANIM.EASING_LINEAR;
this.onLooped = null;
this._timesLooped = 0;
this._needsSort = false;
};
Anim.prototype.forceChangeCallback = function ()
{
if (this.onChange !== null) this.onChange();
this.emitEvent("onChange", this);
};
Anim.prototype.getLoop = function ()
{
return this.loop;
};
Anim.prototype.setLoop = function (target)
{
this.loop = target;
this.emitEvent("onChange", this);
};
/**
* returns true if animation has ended at @time
* checks if last key time is < time
* @param {Number} time
* @returns {Boolean}
* @memberof Anim
* @instance
* @function
*/
Anim.prototype.hasEnded = function (time)
{
if (this.keys.length === 0) return true;
if (this.keys[this._lastKeyIndex].time <= time) return true;
return false;
};
Anim.prototype.isRising = function (time)
{
if (this.hasEnded(time)) return false;
const ki = this.getKeyIndex(time);
if (this.keys[ki].value < this.keys[ki + 1].value) return true;
return false;
};
/**
* remove all keys from animation before time
* @param {Number} time
* @memberof Anim
* @instance
* @function
*/
Anim.prototype.clearBefore = function (time)
{
const v = this.getValue(time);
const ki = this.getKeyIndex(time);
this.setValue(time, v);
if (ki > 1) this.keys.splice(0, ki);
this._updateLastIndex();
};
/**
* remove all keys from animation
* @param {Number} [time=0] set a new key at time with the old value at time
* @memberof Anim
* @instance
* @function
*/
Anim.prototype.clear = function (time)
{
let v = 0;
if (time) v = this.getValue(time);
this.keys.length = 0;
this._updateLastIndex();
if (time) this.setValue(time, v);
if (this.onChange !== null) this.onChange();
this.emitEvent("onChange", this);
};
Anim.prototype.sortKeys = function ()
{
this.keys.sort((a, b) => { return parseFloat(a.time) - parseFloat(b.time); });
this._updateLastIndex();
this._needsSort = false;
if (this.keys.length % 1000 == 0)console.log(this.name, this.keys.length);
};
Anim.prototype.getLength = function ()
{
if (this.keys.length === 0) return 0;
return this.keys[this.keys.length - 1].time;
};
Anim.prototype.getKeyIndex = function (time)
{
let index = 0;
let start = 0;
if (this._cachedIndex && this.keys.length > this._cachedIndex && time >= this.keys[this._cachedIndex].time) start = this._cachedIndex;
for (let i = start; i < this.keys.length; i++)
{
if (time >= this.keys[i].time) index = i;
if (this.keys[i].time > time)
{
if (time != 0) this._cachedIndex = index;
return index;
}
}
return index;
};
/**
* set value at time
* @function setValue
* @memberof Anim
* @instance
* @param {Number} time
* @param {Number} value
* @param {Function} cb callback
*/
Anim.prototype.setValue = function (time, value, cb)
{
let found = null;
if (this.keys.length == 0 || time <= this.keys[this.keys.length - 1].time)
for (let i = 0; i < this.keys.length; i++)
if (this.keys[i].time == time)
{
found = this.keys[i];
this.keys[i].setValue(value);
this.keys[i].cb = cb;
break;
}
if (!found)
{
found = new Key(
{
"time": time,
"value": value,
"e": this.defaultEasing,
"cb": cb,
});
this.keys.push(found);
// if (this.keys.length % 1000 == 0)console.log(this.name, this.keys.length);
this._updateLastIndex();
}
if (this.onChange) this.onChange();
this.emitEvent("onChange", this);
this._needsSort = true;
return found;
};
Anim.prototype.setKeyEasing = function (index, e)
{
if (this.keys[index])
{
this.keys[index].setEasing(e);
this.emitEvent("onChange", this);
}
};
Anim.prototype.getSerialized = function ()
{
const obj = {};
obj.keys = [];
obj.loop = this.loop;
for (let i = 0; i < this.keys.length; i++)
obj.keys.push(this.keys[i].getSerialized());
return obj;
};
Anim.prototype.getKey = function (time)
{
const index = this.getKeyIndex(time);
return this.keys[index];
};
Anim.prototype.getNextKey = function (time)
{
let index = this.getKeyIndex(time) + 1;
if (index >= this.keys.length) index = this.keys.length - 1;
return this.keys[index];
};
Anim.prototype.isFinished = function (time)
{
if (this.keys.length <= 0) return true;
return time > this.keys[this.keys.length - 1].time;
};
Anim.prototype.isStarted = function (time)
{
if (this.keys.length <= 0) return false;
return time >= this.keys[0].time;
};
/**
* get value at time
* @function getValue
* @memberof Anim
* @instance
* @param {Number} [time] time
* @returns {Number} interpolated value at time
*/
Anim.prototype.getValue = function (time)
{
if (this.keys.length === 0)
{
return 0;
}
if (this._needsSort) this.sortKeys();
if (!this.loop && time > this.keys[this._lastKeyIndex].time)
{
if (this.keys[this._lastKeyIndex].cb && !this.keys[this._lastKeyIndex].cbTriggered) this.keys[this._lastKeyIndex].trigger();
return this.keys[this._lastKeyIndex].value;
}
if (time < this.keys[0].time)
{
// if (this.name)console.log("A");
return this.keys[0].value;
}
if (this.loop && time > this.keys[this._lastKeyIndex].time)
{
const currentLoop = time / this.keys[this._lastKeyIndex].time;
if (currentLoop > this._timesLooped)
{
this._timesLooped++;
if (this.onLooped) this.onLooped();
}
time = (time - this.keys[0].time) % (this.keys[this._lastKeyIndex].time - this.keys[0].time);
time += this.keys[0].time;
}
const index = this.getKeyIndex(time);
if (index >= this._lastKeyIndex)
{
if (this.keys[this._lastKeyIndex].cb && !this.keys[this._lastKeyIndex].cbTriggered) this.keys[this._lastKeyIndex].trigger();
return this.keys[this._lastKeyIndex].value;
}
const index2 = index + 1;
const key1 = this.keys[index];
const key2 = this.keys[index2];
if (key1.cb && !key1.cbTriggered) key1.trigger();
if (!key2) return -1;
const perc = (time - key1.time) / (key2.time - key1.time);
if (!key1.ease) this.log._warn("has no ease", key1, key2);
return key1.ease(perc, key2);
};
Anim.prototype._updateLastIndex = function ()
{
this._lastKeyIndex = this.keys.length - 1;
};
Anim.prototype.addKey = function (k)
{
if (k.time === undefined)
{
this.log.warn("key time undefined, ignoring!");
}
else
{
this.keys.push(k);
if (this.onChange !== null) this.onChange();
this.emitEvent("onChange", this);
}
this._updateLastIndex();
};
Anim.prototype.easingFromString = function (str)
{
if (str == "linear") return CONSTANTS.ANIM.EASING_LINEAR;
if (str == "absolute") return CONSTANTS.ANIM.EASING_ABSOLUTE;
if (str == "smoothstep") return CONSTANTS.ANIM.EASING_SMOOTHSTEP;
if (str == "smootherstep") return CONSTANTS.ANIM.EASING_SMOOTHERSTEP;
if (str == "Cubic In") return CONSTANTS.ANIM.EASING_CUBIC_IN;
if (str == "Cubic Out") return CONSTANTS.ANIM.EASING_CUBIC_OUT;
if (str == "Cubic In Out") return CONSTANTS.ANIM.EASING_CUBIC_INOUT;
if (str == "Expo In") return CONSTANTS.ANIM.EASING_EXPO_IN;
if (str == "Expo Out") return CONSTANTS.ANIM.EASING_EXPO_OUT;
if (str == "Expo In Out") return CONSTANTS.ANIM.EASING_EXPO_INOUT;
if (str == "Sin In") return CONSTANTS.ANIM.EASING_SIN_IN;
if (str == "Sin Out") return CONSTANTS.ANIM.EASING_SIN_OUT;
if (str == "Sin In Out") return CONSTANTS.ANIM.EASING_SIN_INOUT;
if (str == "Back In") return CONSTANTS.ANIM.EASING_BACK_IN;
if (str == "Back Out") return CONSTANTS.ANIM.EASING_BACK_OUT;
if (str == "Back In Out") return CONSTANTS.ANIM.EASING_BACK_INOUT;
if (str == "Elastic In") return CONSTANTS.ANIM.EASING_ELASTIC_IN;
if (str == "Elastic Out") return CONSTANTS.ANIM.EASING_ELASTIC_OUT;
if (str == "Bounce In") return CONSTANTS.ANIM.EASING_BOUNCE_IN;
if (str == "Bounce Out") return CONSTANTS.ANIM.EASING_BOUNCE_OUT;
if (str == "Quart Out") return CONSTANTS.ANIM.EASING_QUART_OUT;
if (str == "Quart In") return CONSTANTS.ANIM.EASING_QUART_IN;
if (str == "Quart In Out") return CONSTANTS.ANIM.EASING_QUART_INOUT;
if (str == "Quint Out") return CONSTANTS.ANIM.EASING_QUINT_OUT;
if (str == "Quint In") return CONSTANTS.ANIM.EASING_QUINT_IN;
if (str == "Quint In Out") return CONSTANTS.ANIM.EASING_QUINT_INOUT;
};
Anim.prototype.createPort = function (op, title, cb)
{
const port = op.inDropDown(title, CONSTANTS.ANIM.EASINGS, "Cubic Out");
// const port = op.addInPort(
// new Port(op, title, CONSTANTS.OP.OP_PORT_TYPE_VALUE, {
// "display": "dropdown",
// "values": CONSTANTS.ANIM.EASINGS,
// }),
// );
port.set("linear");
port.defaultValue = "linear";
port.onChange = function ()
{
this.defaultEasing = this.easingFromString(port.get());
this.emitEvent("onChangeDefaultEasing", this);
if (cb) cb();
}.bind(this);
return port;
};
// ------------------------------
Anim.slerpQuaternion = function (time, q, animx, animy, animz, animw)
{
if (!Anim.slerpQuaternion.q1)
{
Anim.slerpQuaternion.q1 = quat.create();
Anim.slerpQuaternion.q2 = quat.create();
}
const i1 = animx.getKeyIndex(time);
let i2 = i1 + 1;
if (i2 >= animx.keys.length) i2 = animx.keys.length - 1;
if (i1 == i2)
{
quat.set(q, animx.keys[i1].value, animy.keys[i1].value, animz.keys[i1].value, animw.keys[i1].value);
}
else
{
const key1Time = animx.keys[i1].time;
const key2Time = animx.keys[i2].time;
const perc = (time - key1Time) / (key2Time - key1Time);
quat.set(Anim.slerpQuaternion.q1, animx.keys[i1].value, animy.keys[i1].value, animz.keys[i1].value, animw.keys[i1].value);
quat.set(Anim.slerpQuaternion.q2, animx.keys[i2].value, animy.keys[i2].value, animz.keys[i2].value, animw.keys[i2].value);
quat.slerp(q, Anim.slerpQuaternion.q1, Anim.slerpQuaternion.q2, perc);
}
return q;
};
const ANIM = { "Key": Key };
export { ANIM };
export { Anim };