Home Reference Source

cables_dev/cables/src/core/anim.js

  1. import { Logger } from "cables-shared-client";
  2. import { Key } from "./anim_key.js";
  3. import { CONSTANTS } from "./constants.js";
  4. import { EventTarget } from "./eventtarget.js";
  5.  
  6. /**
  7. * Keyframed interpolated animation.
  8. *
  9. * Available Easings:
  10. * <code>
  11. * CONSTANTS.ANIM.EASING_LINEAR
  12. * CONSTANTS.ANIM.EASING_ABSOLUTE
  13. * CONSTANTS.ANIM.EASING_SMOOTHSTEP
  14. * CONSTANTS.ANIM.EASING_SMOOTHERSTEP
  15. * CONSTANTS.ANIM.EASING_CUBICSPLINE
  16.  
  17. * CONSTANTS.ANIM.EASING_CUBIC_IN
  18. * CONSTANTS.ANIM.EASING_CUBIC_OUT
  19. * CONSTANTS.ANIM.EASING_CUBIC_INOUT
  20.  
  21. * CONSTANTS.ANIM.EASING_EXPO_IN
  22. * CONSTANTS.ANIM.EASING_EXPO_OUT
  23. * CONSTANTS.ANIM.EASING_EXPO_INOUT
  24.  
  25. * CONSTANTS.ANIM.EASING_SIN_IN
  26. * CONSTANTS.ANIM.EASING_SIN_OUT
  27. * CONSTANTS.ANIM.EASING_SIN_INOUT
  28.  
  29. * CONSTANTS.ANIM.EASING_BACK_IN
  30. * CONSTANTS.ANIM.EASING_BACK_OUT
  31. * CONSTANTS.ANIM.EASING_BACK_INOUT
  32.  
  33. * CONSTANTS.ANIM.EASING_ELASTIC_IN
  34. * CONSTANTS.ANIM.EASING_ELASTIC_OUT
  35.  
  36. * CONSTANTS.ANIM.EASING_BOUNCE_IN
  37. * CONSTANTS.ANIM.EASING_BOUNCE_OUT
  38.  
  39. * CONSTANTS.ANIM.EASING_QUART_IN
  40. * CONSTANTS.ANIM.EASING_QUART_OUT
  41. * CONSTANTS.ANIM.EASING_QUART_INOUT
  42.  
  43. * CONSTANTS.ANIM.EASING_QUINT_IN
  44. * CONSTANTS.ANIM.EASING_QUINT_OUT
  45. * CONSTANTS.ANIM.EASING_QUINT_INOUT
  46. * </code>
  47. * @class
  48. * @param cfg
  49. * @example
  50. * var anim=new CABLES.Anim();
  51. * anim.setValue(0,0); // set value 0 at 0 seconds
  52. * anim.setValue(10,1); // set value 1 at 10 seconds
  53. * anim.getValue(5); // get value at 5 seconds - this returns 0.5
  54. */
  55.  
  56. const Anim = function (cfg)
  57. {
  58. EventTarget.apply(this);
  59.  
  60. cfg = cfg || {};
  61. this.keys = [];
  62. this.onChange = null;
  63. this.stayInTimeline = false;
  64. this.loop = false;
  65. this._log = new Logger("Anim");
  66. this._lastKeyIndex = 0;
  67. this._cachedIndex = 0;
  68. this.name = cfg.name || null;
  69.  
  70. /**
  71. * @member defaultEasing
  72. * @memberof Anim
  73. * @instance
  74. * @type {Number}
  75. */
  76. this.defaultEasing = cfg.defaultEasing || CONSTANTS.ANIM.EASING_LINEAR;
  77. this.onLooped = null;
  78.  
  79. this._timesLooped = 0;
  80. this._needsSort = false;
  81. };
  82.  
  83. Anim.prototype.forceChangeCallback = function ()
  84. {
  85. if (this.onChange !== null) this.onChange();
  86. this.emitEvent("onChange", this);
  87. };
  88.  
  89. Anim.prototype.getLoop = function ()
  90. {
  91. return this.loop;
  92. };
  93.  
  94. Anim.prototype.setLoop = function (target)
  95. {
  96. this.loop = target;
  97. this.emitEvent("onChange", this);
  98. };
  99.  
  100. /**
  101. * returns true if animation has ended at @time
  102. * checks if last key time is < time
  103. * @param {Number} time
  104. * @returns {Boolean}
  105. * @memberof Anim
  106. * @instance
  107. * @function
  108. */
  109. Anim.prototype.hasEnded = function (time)
  110. {
  111. if (this.keys.length === 0) return true;
  112. if (this.keys[this._lastKeyIndex].time <= time) return true;
  113. return false;
  114. };
  115.  
  116. Anim.prototype.isRising = function (time)
  117. {
  118. if (this.hasEnded(time)) return false;
  119. const ki = this.getKeyIndex(time);
  120. if (this.keys[ki].value < this.keys[ki + 1].value) return true;
  121. return false;
  122. };
  123.  
  124. /**
  125. * remove all keys from animation before time
  126. * @param {Number} time
  127. * @memberof Anim
  128. * @instance
  129. * @function
  130. */
  131. Anim.prototype.clearBefore = function (time)
  132. {
  133. const v = this.getValue(time);
  134. const ki = this.getKeyIndex(time);
  135.  
  136. this.setValue(time, v);
  137.  
  138. if (ki > 1) this.keys.splice(0, ki);
  139. this._updateLastIndex();
  140. };
  141. /**
  142. * remove all keys from animation
  143. * @param {Number} [time=0] set a new key at time with the old value at time
  144. * @memberof Anim
  145. * @instance
  146. * @function
  147. */
  148. Anim.prototype.clear = function (time)
  149. {
  150. let v = 0;
  151. if (time) v = this.getValue(time);
  152. this.keys.length = 0;
  153. this._updateLastIndex();
  154. if (time) this.setValue(time, v);
  155. if (this.onChange !== null) this.onChange();
  156. this.emitEvent("onChange", this);
  157. };
  158.  
  159. Anim.prototype.sortKeys = function ()
  160. {
  161. this.keys.sort((a, b) => { return parseFloat(a.time) - parseFloat(b.time); });
  162. this._updateLastIndex();
  163. this._needsSort = false;
  164. if (this.keys.length % 1000 == 0)console.log(this.name, this.keys.length);
  165. };
  166.  
  167. Anim.prototype.getLength = function ()
  168. {
  169. if (this.keys.length === 0) return 0;
  170. return this.keys[this.keys.length - 1].time;
  171. };
  172.  
  173. Anim.prototype.getKeyIndex = function (time)
  174. {
  175. let index = 0;
  176. let start = 0;
  177. if (this._cachedIndex && this.keys.length > this._cachedIndex && time >= this.keys[this._cachedIndex].time) start = this._cachedIndex;
  178. for (let i = start; i < this.keys.length; i++)
  179. {
  180. if (time >= this.keys[i].time) index = i;
  181. if (this.keys[i].time > time)
  182. {
  183. if (time != 0) this._cachedIndex = index;
  184. return index;
  185. }
  186. }
  187.  
  188. return index;
  189. };
  190.  
  191. /**
  192. * set value at time
  193. * @function setValue
  194. * @memberof Anim
  195. * @instance
  196. * @param {Number} time
  197. * @param {Number} value
  198. * @param {Function} cb callback
  199. */
  200. Anim.prototype.setValue = function (time, value, cb)
  201. {
  202. let found = null;
  203.  
  204. if (this.keys.length == 0 || time <= this.keys[this.keys.length - 1].time)
  205. for (let i = 0; i < this.keys.length; i++)
  206. if (this.keys[i].time == time)
  207. {
  208. found = this.keys[i];
  209. this.keys[i].setValue(value);
  210. this.keys[i].cb = cb;
  211. break;
  212. }
  213.  
  214. if (!found)
  215. {
  216. found = new Key(
  217. {
  218. "time": time,
  219. "value": value,
  220. "e": this.defaultEasing,
  221. "cb": cb,
  222. });
  223. this.keys.push(found);
  224.  
  225. // if (this.keys.length % 1000 == 0)console.log(this.name, this.keys.length);
  226. this._updateLastIndex();
  227. }
  228.  
  229. if (this.onChange) this.onChange();
  230. this.emitEvent("onChange", this);
  231. this._needsSort = true;
  232. return found;
  233. };
  234.  
  235. Anim.prototype.setKeyEasing = function (index, e)
  236. {
  237. if (this.keys[index])
  238. {
  239. this.keys[index].setEasing(e);
  240. this.emitEvent("onChange", this);
  241. }
  242. };
  243.  
  244. Anim.prototype.getSerialized = function ()
  245. {
  246. const obj = {};
  247. obj.keys = [];
  248. obj.loop = this.loop;
  249.  
  250. for (let i = 0; i < this.keys.length; i++)
  251. obj.keys.push(this.keys[i].getSerialized());
  252.  
  253. return obj;
  254. };
  255.  
  256. Anim.prototype.getKey = function (time)
  257. {
  258. const index = this.getKeyIndex(time);
  259. return this.keys[index];
  260. };
  261.  
  262. Anim.prototype.getNextKey = function (time)
  263. {
  264. let index = this.getKeyIndex(time) + 1;
  265. if (index >= this.keys.length) index = this.keys.length - 1;
  266.  
  267. return this.keys[index];
  268. };
  269.  
  270. Anim.prototype.isFinished = function (time)
  271. {
  272. if (this.keys.length <= 0) return true;
  273. return time > this.keys[this.keys.length - 1].time;
  274. };
  275.  
  276. Anim.prototype.isStarted = function (time)
  277. {
  278. if (this.keys.length <= 0) return false;
  279. return time >= this.keys[0].time;
  280. };
  281.  
  282. /**
  283. * get value at time
  284. * @function getValue
  285. * @memberof Anim
  286. * @instance
  287. * @param {Number} [time] time
  288. * @returns {Number} interpolated value at time
  289. */
  290. Anim.prototype.getValue = function (time)
  291. {
  292. if (this.keys.length === 0)
  293. {
  294. return 0;
  295. }
  296. if (this._needsSort) this.sortKeys();
  297.  
  298. if (!this.loop && time > this.keys[this._lastKeyIndex].time)
  299. {
  300. if (this.keys[this._lastKeyIndex].cb && !this.keys[this._lastKeyIndex].cbTriggered) this.keys[this._lastKeyIndex].trigger();
  301.  
  302. return this.keys[this._lastKeyIndex].value;
  303. }
  304.  
  305. if (time < this.keys[0].time)
  306. {
  307. // if (this.name)console.log("A");
  308.  
  309. return this.keys[0].value;
  310. }
  311.  
  312. if (this.loop && time > this.keys[this._lastKeyIndex].time)
  313. {
  314. const currentLoop = time / this.keys[this._lastKeyIndex].time;
  315. if (currentLoop > this._timesLooped)
  316. {
  317. this._timesLooped++;
  318. if (this.onLooped) this.onLooped();
  319. }
  320. time = (time - this.keys[0].time) % (this.keys[this._lastKeyIndex].time - this.keys[0].time);
  321. time += this.keys[0].time;
  322. }
  323.  
  324. const index = this.getKeyIndex(time);
  325. if (index >= this._lastKeyIndex)
  326. {
  327. if (this.keys[this._lastKeyIndex].cb && !this.keys[this._lastKeyIndex].cbTriggered) this.keys[this._lastKeyIndex].trigger();
  328.  
  329. return this.keys[this._lastKeyIndex].value;
  330. }
  331.  
  332.  
  333. const index2 = index + 1;
  334. const key1 = this.keys[index];
  335. const key2 = this.keys[index2];
  336.  
  337. if (key1.cb && !key1.cbTriggered) key1.trigger();
  338.  
  339. if (!key2) return -1;
  340.  
  341. const perc = (time - key1.time) / (key2.time - key1.time);
  342.  
  343. if (!key1.ease) this.log._warn("has no ease", key1, key2);
  344.  
  345. return key1.ease(perc, key2);
  346. };
  347.  
  348. Anim.prototype._updateLastIndex = function ()
  349. {
  350. this._lastKeyIndex = this.keys.length - 1;
  351. };
  352.  
  353. Anim.prototype.addKey = function (k)
  354. {
  355. if (k.time === undefined)
  356. {
  357. this.log.warn("key time undefined, ignoring!");
  358. }
  359. else
  360. {
  361. this.keys.push(k);
  362. if (this.onChange !== null) this.onChange();
  363. this.emitEvent("onChange", this);
  364. }
  365. this._updateLastIndex();
  366. };
  367.  
  368. Anim.prototype.easingFromString = function (str)
  369. {
  370. if (str == "linear") return CONSTANTS.ANIM.EASING_LINEAR;
  371. if (str == "absolute") return CONSTANTS.ANIM.EASING_ABSOLUTE;
  372. if (str == "smoothstep") return CONSTANTS.ANIM.EASING_SMOOTHSTEP;
  373. if (str == "smootherstep") return CONSTANTS.ANIM.EASING_SMOOTHERSTEP;
  374.  
  375. if (str == "Cubic In") return CONSTANTS.ANIM.EASING_CUBIC_IN;
  376. if (str == "Cubic Out") return CONSTANTS.ANIM.EASING_CUBIC_OUT;
  377. if (str == "Cubic In Out") return CONSTANTS.ANIM.EASING_CUBIC_INOUT;
  378.  
  379. if (str == "Expo In") return CONSTANTS.ANIM.EASING_EXPO_IN;
  380. if (str == "Expo Out") return CONSTANTS.ANIM.EASING_EXPO_OUT;
  381. if (str == "Expo In Out") return CONSTANTS.ANIM.EASING_EXPO_INOUT;
  382.  
  383. if (str == "Sin In") return CONSTANTS.ANIM.EASING_SIN_IN;
  384. if (str == "Sin Out") return CONSTANTS.ANIM.EASING_SIN_OUT;
  385. if (str == "Sin In Out") return CONSTANTS.ANIM.EASING_SIN_INOUT;
  386.  
  387. if (str == "Back In") return CONSTANTS.ANIM.EASING_BACK_IN;
  388. if (str == "Back Out") return CONSTANTS.ANIM.EASING_BACK_OUT;
  389. if (str == "Back In Out") return CONSTANTS.ANIM.EASING_BACK_INOUT;
  390.  
  391. if (str == "Elastic In") return CONSTANTS.ANIM.EASING_ELASTIC_IN;
  392. if (str == "Elastic Out") return CONSTANTS.ANIM.EASING_ELASTIC_OUT;
  393.  
  394. if (str == "Bounce In") return CONSTANTS.ANIM.EASING_BOUNCE_IN;
  395. if (str == "Bounce Out") return CONSTANTS.ANIM.EASING_BOUNCE_OUT;
  396.  
  397. if (str == "Quart Out") return CONSTANTS.ANIM.EASING_QUART_OUT;
  398. if (str == "Quart In") return CONSTANTS.ANIM.EASING_QUART_IN;
  399. if (str == "Quart In Out") return CONSTANTS.ANIM.EASING_QUART_INOUT;
  400.  
  401. if (str == "Quint Out") return CONSTANTS.ANIM.EASING_QUINT_OUT;
  402. if (str == "Quint In") return CONSTANTS.ANIM.EASING_QUINT_IN;
  403. if (str == "Quint In Out") return CONSTANTS.ANIM.EASING_QUINT_INOUT;
  404. };
  405.  
  406. Anim.prototype.createPort = function (op, title, cb)
  407. {
  408. const port = op.inDropDown(title, CONSTANTS.ANIM.EASINGS, "Cubic Out");
  409.  
  410. // const port = op.addInPort(
  411. // new Port(op, title, CONSTANTS.OP.OP_PORT_TYPE_VALUE, {
  412. // "display": "dropdown",
  413. // "values": CONSTANTS.ANIM.EASINGS,
  414. // }),
  415. // );
  416.  
  417. port.set("linear");
  418. port.defaultValue = "linear";
  419.  
  420. port.onChange = function ()
  421. {
  422. this.defaultEasing = this.easingFromString(port.get());
  423. this.emitEvent("onChangeDefaultEasing", this);
  424.  
  425. if (cb) cb();
  426. }.bind(this);
  427.  
  428. return port;
  429. };
  430.  
  431. // ------------------------------
  432.  
  433. Anim.slerpQuaternion = function (time, q, animx, animy, animz, animw)
  434. {
  435. if (!Anim.slerpQuaternion.q1)
  436. {
  437. Anim.slerpQuaternion.q1 = quat.create();
  438. Anim.slerpQuaternion.q2 = quat.create();
  439. }
  440.  
  441. const i1 = animx.getKeyIndex(time);
  442. let i2 = i1 + 1;
  443. if (i2 >= animx.keys.length) i2 = animx.keys.length - 1;
  444.  
  445. if (i1 == i2)
  446. {
  447. quat.set(q, animx.keys[i1].value, animy.keys[i1].value, animz.keys[i1].value, animw.keys[i1].value);
  448. }
  449. else
  450. {
  451. const key1Time = animx.keys[i1].time;
  452. const key2Time = animx.keys[i2].time;
  453. const perc = (time - key1Time) / (key2Time - key1Time);
  454.  
  455. quat.set(Anim.slerpQuaternion.q1, animx.keys[i1].value, animy.keys[i1].value, animz.keys[i1].value, animw.keys[i1].value);
  456.  
  457. quat.set(Anim.slerpQuaternion.q2, animx.keys[i2].value, animy.keys[i2].value, animz.keys[i2].value, animw.keys[i2].value);
  458.  
  459. quat.slerp(q, Anim.slerpQuaternion.q1, Anim.slerpQuaternion.q2, perc);
  460. }
  461. return q;
  462. };
  463.  
  464. const ANIM = { "Key": Key };
  465.  
  466. export { ANIM };
  467. export { Anim };