Home Reference Source

cables_dev/cables/src/core/anim.js

  1. import { Events, Logger } from "cables-shared-client";
  2. import { logStack, uuid } from "./utils.js";
  3. import { AnimKey } from "./anim_key.js";
  4. import { Op } from "./core_op.js";
  5. import { Port } from "./core_port.js";
  6. import { Patch } from "./core_patch.js";
  7. let counts = {};
  8. /**
  9. * Keyframed interpolated animation.
  10. *
  11. * @class
  12. * @param cfg
  13. * @example
  14. * var anim=new CABLES.Anim();
  15. * anim.setValue(0,0); // set value 0 at 0 seconds
  16. * anim.setValue(10,1); // set value 1 at 10 seconds
  17. * anim.getValue(5); // get value at 5 seconds - this returns 0.5
  18. */
  19. export class Anim extends Events
  20. {
  21. static EVENT_KEY_DELETE = "keyDelete";
  22. static EVENT_CHANGE = "onChange";
  23. static EVENT_UIATTRIB_CHANGE = "uiattrchange";
  24. static LOOP_OFF = 0;
  25. static LOOP_REPEAT = 1;
  26. static LOOP_MIRROR = 2;
  27. static LOOP_OFFSET = 3;
  28. static EASING_LINEAR = 0;
  29. static EASING_ABSOLUTE = 1;
  30. static EASING_SMOOTHSTEP = 2;
  31. static EASING_SMOOTHERSTEP = 3;
  32. static EASING_CUBICSPLINE = 4;
  33. static EASING_CUBIC_IN = 5;
  34. static EASING_CUBIC_OUT = 6;
  35. static EASING_CUBIC_INOUT = 7;
  36. static EASING_EXPO_IN = 8;
  37. static EASING_EXPO_OUT = 9;
  38. static EASING_EXPO_INOUT = 10;
  39. static EASING_SIN_IN = 11;
  40. static EASING_SIN_OUT = 12;
  41. static EASING_SIN_INOUT = 13;
  42. static EASING_BACK_IN = 14;
  43. static EASING_BACK_OUT = 15;
  44. static EASING_BACK_INOUT = 16;
  45. static EASING_ELASTIC_IN = 17;
  46. static EASING_ELASTIC_OUT = 18;
  47. static EASING_BOUNCE_IN = 19;
  48. static EASING_BOUNCE_OUT = 21;
  49. static EASING_QUART_IN = 22;
  50. static EASING_QUART_OUT = 23;
  51. static EASING_QUART_INOUT = 24;
  52. static EASING_QUINT_IN = 25;
  53. static EASING_QUINT_OUT = 26;
  54. static EASING_QUINT_INOUT = 27;
  55. static EASING_CLIP = 28;
  56. static EASINGNAMES = ["linear", "absolute", "smoothstep", "smootherstep", "Cubic In", "Cubic Out", "Cubic In Out", "Expo In", "Expo Out", "Expo In Out", "Sin In", "Sin Out", "Sin In Out", "Quart In", "Quart Out", "Quart In Out", "Quint In", "Quint Out", "Quint In Out", "Back In", "Back Out", "Back In Out", "Elastic In", "Elastic Out", "Bounce In", "Bounce Out", "Clip"];
  57. #tlActive = true;
  58. uiAttribs = {};
  59. loop = 0;
  60. onLooped = null;
  61. _timesLooped = 0;
  62. #needsSort = false;
  63. _cachedIndex = 0;
  64. /** @type {Port} */
  65. port = null;
  66. /** @type {AnimKey[]} */
  67. keys = [];
  68. onChange = null;
  69. stayInTimeline = false;
  70. batchMode = false;
  71. /**
  72. * @param {AnimCfg} [cfg]
  73. */
  74. constructor(cfg = {})
  75. {
  76. super();
  77. cfg = cfg || {};
  78. this.id = uuid();
  79. this._log = new Logger("Anim");
  80. this.name = cfg.name || null;
  81. /** @type {Number} */
  82. this.defaultEasing = cfg.defaultEasing || Anim.EASING_LINEAR;
  83. }
  84. forceChangeCallback()
  85. {
  86. if (this.onChange !== null) this.onChange();
  87. this.emitEvent(Anim.EVENT_CHANGE, this);
  88. }
  89. forceChangeCallbackSoon()
  90. {
  91. if (this.batchMode) return;
  92. if (!this.forcecbto)
  93. this.forcecbto = setTimeout(() =>
  94. {
  95. this.forceChangeCallback();
  96. this.forcecbto = null;
  97. },
  98. 50);
  99. }
  100. getLoop()
  101. {
  102. return this.loop;
  103. }
  104. /**
  105. * @param {number} loopType
  106. */
  107. setLoop(loopType)
  108. {
  109. if (loopType === false)loopType = 0;
  110. if (loopType === true)loopType = 1;
  111. this.loop = loopType;
  112. this.emitEvent(Anim.EVENT_CHANGE, this);
  113. }
  114. /**
  115. * returns true if animation has ended at @time
  116. * checks if last key time is < time
  117. * @param {Number} time
  118. * @returns {Boolean}
  119. * @memberof Anim
  120. * @instance
  121. * @function
  122. */
  123. hasEnded(time)
  124. {
  125. if (this.#needsSort) this.sortKeys();
  126. if (this.keys.length === 0) return true;
  127. if (this.keys[this.keys.length - 1].time <= time) return true;
  128. return false;
  129. }
  130. /**
  131. * @param {number} time
  132. */
  133. hasStarted(time)
  134. {
  135. if (this.#needsSort) this.sortKeys();
  136. if (this.keys.length === 0) return false;
  137. if (time >= this.keys[0].time) return true;
  138. return false;
  139. }
  140. /**
  141. * @param {number} time
  142. */
  143. isRising(time)
  144. {
  145. if (this.#needsSort) this.sortKeys();
  146. if (this.hasEnded(time)) return false;
  147. const ki = this.getKeyIndex(time);
  148. if (this.keys[ki].value < this.keys[ki + 1].value) return true;
  149. return false;
  150. }
  151. /**
  152. * remove all keys from animation before time
  153. * @param {Number} time
  154. * @memberof Anim
  155. * @instance
  156. * @function
  157. */
  158. clearBefore(time)
  159. {
  160. if (this.#needsSort) this.sortKeys();
  161. const v = this.getValue(time);
  162. const ki = this.getKeyIndex(time);
  163. this.setValue(time, v);
  164. if (ki > 1)
  165. {
  166. this.keys.splice(0, ki);
  167. this.#needsSort = true;
  168. }
  169. }
  170. /**
  171. * remove all keys from animation
  172. * @param {Number} [time=0] set a new key at time with the old value at time
  173. * @memberof Anim
  174. * @instance
  175. * @function
  176. */
  177. clear(time)
  178. {
  179. if (this.#needsSort) this.sortKeys();
  180. let v = 0;
  181. if (time) v = this.getValue(time);
  182. for (let i = 0; i < this.keys.length; i++)
  183. this.emitEvent(Anim.EVENT_KEY_DELETE, this.keys[i]);
  184. this.keys.length = 0;
  185. if (time) this.setValue(time, v);
  186. this.#needsSort = true;
  187. if (this.onChange !== null) this.onChange();
  188. this.emitEvent(Anim.EVENT_CHANGE, this);
  189. }
  190. checkIsSorted()
  191. {
  192. let isSorted = true;
  193. for (let i = 0; i < this.keys.length - 1; i++)
  194. if (this.keys[i].time > this.keys[i + 1].time)
  195. {
  196. isSorted = false;
  197. break;
  198. }
  199. return isSorted;
  200. }
  201. sortKeys()
  202. {
  203. if (this.batchMode) return;
  204. if (!this.checkIsSorted())
  205. {
  206. this.keys.sort((a, b) => { return a.time - b.time; });
  207. this.#needsSort = false;
  208. if (this.keys.length > 999 && this.keys.length % 1000 == 0)console.log(this.name, this.keys.length);
  209. this.emitEvent(Anim.EVENT_CHANGE);
  210. }
  211. }
  212. hasDuplicates()
  213. {
  214. const test = {};
  215. let count = 0;
  216. for (let i = 0; i < this.keys.length; i++)
  217. {
  218. test[this.keys[i].time] = 1;
  219. count++;
  220. }
  221. const keys = Object.keys(test);
  222. if (keys.length != count)
  223. {
  224. return true;
  225. }
  226. return false;
  227. }
  228. removeDuplicates()
  229. {
  230. if (this.hasDuplicates())
  231. {
  232. if (this.#needsSort) this.sortKeys();
  233. let count = 0;
  234. while (this.hasDuplicates())
  235. {
  236. for (let i = 0; i < this.keys.length - 1; i++)
  237. {
  238. if (this.keys[i].time == this.keys[i + 1].time)
  239. {
  240. const oldkey = this.keys[i];
  241. this.keys.splice(i, 1);
  242. this.emitEvent(Anim.EVENT_KEY_DELETE, oldkey);
  243. }
  244. count++;
  245. }
  246. }
  247. this.#needsSort = true;
  248. }
  249. }
  250. getLengthLoop()
  251. {
  252. if (this.#needsSort) this.sortKeys();
  253. if (this.keys.length < 2) return 0;
  254. return this.lastKey.time - this.firstKey.time;
  255. }
  256. getLength()
  257. {
  258. if (this.#needsSort) this.sortKeys();
  259. if (this.keys.length === 0) return 0;
  260. return this.lastKey.time;
  261. }
  262. /**
  263. * @param {number} time
  264. */
  265. getKeyIndex(time)
  266. {
  267. if (this.#needsSort) this.sortKeys();
  268. let index = 0;
  269. let start = 0;
  270. if (this._cachedIndex && this.keys.length > this._cachedIndex && time >= this.keys[this._cachedIndex].time) start = this._cachedIndex;
  271. for (let i = start; i < this.keys.length; i++)
  272. {
  273. if (time >= this.keys[i].time) index = i;
  274. if (this.keys[i].time > time)
  275. {
  276. if (time != 0) this._cachedIndex = index;
  277. return index;
  278. }
  279. }
  280. return index;
  281. }
  282. /**
  283. * set value at time
  284. * @param {Number} time
  285. * @param {Number} value
  286. * @param {Function} cb callback
  287. */
  288. setValue(time, value, cb = null)
  289. {
  290. if (isNaN(value))CABLES.logStack();
  291. if (this.#needsSort) this.sortKeys();
  292. let found = null;
  293. if (!this.batchMode)
  294. if (this.keys.length == 0 || time <= this.lastKey.time)
  295. for (let i = 0; i < this.keys.length; i++)
  296. if (this.keys[i].time == time)
  297. {
  298. found = this.keys[i];
  299. this.keys[i].setValue(value);
  300. this.keys[i].cb = cb;
  301. break;
  302. }
  303. if (!found)
  304. {
  305. found = new AnimKey(
  306. {
  307. "time": time,
  308. "value": value,
  309. "e": this.defaultEasing,
  310. "cb": cb,
  311. "anim": this
  312. });
  313. this.keys.push(found);
  314. // if (this.keys.length % 1000 == 0)console.log(this.name, this.keys.length);
  315. }
  316. if (!this.batchMode)
  317. {
  318. if (this.onChange) this.onChange();
  319. this.emitEvent(Anim.EVENT_CHANGE, this);
  320. this.#needsSort = true;
  321. }
  322. return found;
  323. }
  324. /**
  325. * @param {number} index
  326. * @param {number} easing
  327. */
  328. setKeyEasing(index, easing)
  329. {
  330. if (this.keys[index])
  331. {
  332. this.keys[index].setEasing(easing);
  333. this.emitEvent(Anim.EVENT_CHANGE, this);
  334. }
  335. }
  336. /**
  337. * @param {object} obj
  338. * @param {boolean} [clear]
  339. * @param {object} [missingClipAnims]
  340. */
  341. deserialize(obj, clear, missingClipAnims)
  342. {
  343. if (obj.loop) this.loop = obj.loop;
  344. if (obj.tlActive) this.#tlActive = obj.tlActive;
  345. if (obj.height) this.uiAttribs.height = obj.height;
  346. if (clear)
  347. {
  348. while (this.keys.length) this.keys[0].delete();
  349. this.keys.length = 0;
  350. }
  351. for (const ani in obj.keys)
  352. {
  353. let newKey = new AnimKey(obj.keys[ani], this);
  354. this.keys.push(newKey);
  355. if (missingClipAnims)
  356. if (obj.keys[ani].clipId)
  357. {
  358. missingClipAnims[obj.keys[ani].clipId] = missingClipAnims[obj.keys[ani].clipId] || [];
  359. if (missingClipAnims)missingClipAnims[obj.keys[ani].clipId].push(newKey);
  360. }
  361. }
  362. this.sortKeys();
  363. }
  364. /**
  365. * @returns {SerializedAnim}
  366. */
  367. getSerialized()
  368. {
  369. /** @type {SerializedAnim} */
  370. const obj = {};
  371. obj.keys = [];
  372. obj.loop = this.loop;
  373. if (this.#tlActive)obj.tlActive = this.tlActive;
  374. if (this.uiAttribs.height)obj.height = this.uiAttribs.height;
  375. for (let i = 0; i < this.keys.length; i++)
  376. obj.keys.push(this.keys[i].getSerialized());
  377. return obj;
  378. }
  379. /**
  380. * @param {number} time
  381. */
  382. getKey(time)
  383. {
  384. if (this.#needsSort) this.sortKeys();
  385. const index = this.getKeyIndex(time);
  386. return this.keys[index];
  387. }
  388. /**
  389. * @param {number} time
  390. */
  391. getNextKey(time)
  392. {
  393. if (this.#needsSort) this.sortKeys();
  394. let index = this.getKeyIndex(time) + 1;
  395. if (index >= this.keys.length) return null;
  396. return this.keys[index];
  397. }
  398. /**
  399. * @param {number} time
  400. */
  401. getPrevKey(time)
  402. {
  403. if (this.#needsSort) this.sortKeys();
  404. let index = this.getKeyIndex(time) - 1;
  405. if (index < 0) return null;
  406. return this.keys[index];
  407. }
  408. /**
  409. * @param {number} time
  410. */
  411. isFinished(time)
  412. {
  413. if (this.#needsSort) this.sortKeys();
  414. if (this.keys.length <= 0) return true;
  415. return time > this.lastKey.time;
  416. }
  417. /**
  418. * @param {number} time
  419. */
  420. isStarted(time)
  421. {
  422. if (this.#needsSort) this.sortKeys();
  423. if (this.keys.length <= 0) return false;
  424. return time >= this.firstKey.time;
  425. }
  426. /**
  427. * @param {AnimKey} k
  428. * @param {undefined} [events]
  429. */
  430. remove(k, events)
  431. {
  432. for (let i = 0; i < this.keys.length; i++)
  433. {
  434. if (this.keys[i] == k)
  435. {
  436. this.emitEvent(Anim.EVENT_KEY_DELETE, this.keys[i]);
  437. this.keys.splice(i, 1);
  438. this.#needsSort = true;
  439. if (events === undefined)
  440. {
  441. this.emitEvent(Anim.EVENT_CHANGE, this);
  442. }
  443. return;
  444. }
  445. }
  446. }
  447. get lastKey()
  448. {
  449. if (this.#needsSort) this.sortKeys();
  450. return this.keys[this.keys.length - 1];
  451. }
  452. get firstKey()
  453. {
  454. if (this.#needsSort) this.sortKeys();
  455. return this.keys[0];
  456. }
  457. /**
  458. * @param {number} time
  459. */
  460. getLoopIndex(time)
  461. {
  462. if (this.#needsSort) this.sortKeys();
  463. if (this.keys.length < 2) return 0;
  464. return (time - this.firstKey.time) / this.getLengthLoop();
  465. }
  466. /**
  467. * get value at time
  468. * @function getValue
  469. * @memberof Anim
  470. * @instance
  471. * @param {Number} [time] time
  472. * @returns {Number} interpolated value at time
  473. */
  474. getValue(time = 0)
  475. {
  476. // if (isNaN(time))time = 0;
  477. // counts[this.name] = counts[this.name] || 0;
  478. // if (counts[this.name] < 10 && this.port)
  479. // {
  480. // console.log("getvalue", this.name, time, this);
  481. // CABLES.logStack();
  482. // counts[this.name]++;
  483. // }
  484. let valAdd = 0;
  485. if (this.keys.length === 0) return 0;
  486. if (this.#needsSort) this.sortKeys();
  487. if (!this.loop && time > this.lastKey.time)
  488. {
  489. if (this.lastKey.cb && !this.lastKey.cbTriggered) this.lastKey.trigger();
  490. return this.lastKey.value;
  491. }
  492. if (time < this.firstKey.time) return this.keys[0].value;
  493. if (this.loop && this.keys.length > 1 && time > this.lastKey.time)
  494. {
  495. const currentLoop = this.getLoopIndex(time);
  496. if (currentLoop > this._timesLooped)
  497. {
  498. this._timesLooped++;
  499. if (this.onLooped) this.onLooped();
  500. }
  501. time = (time - this.firstKey.time) % (this.getLengthLoop());
  502. if (this.loop == Anim.LOOP_REPEAT) { }
  503. else if (this.loop == Anim.LOOP_MIRROR)
  504. {
  505. if (Math.floor(currentLoop) % 2 == 1)time = this.getLengthLoop() - time;
  506. }
  507. else if (this.loop == Anim.LOOP_OFFSET)
  508. {
  509. valAdd = (this.lastKey.value - this.keys[0].value) * Math.floor(currentLoop);
  510. }
  511. time += this.firstKey.time;
  512. }
  513. const index = this.getKeyIndex(time);
  514. if (index >= this.keys.length - 1)
  515. {
  516. if (this.lastKey.cb && !this.lastKey.cbTriggered) this.lastKey.trigger();
  517. return this.lastKey.value;
  518. }
  519. const index2 = index + 1;
  520. const key1 = this.keys[index];
  521. const key2 = this.keys[index2];
  522. if (key1.cb && !key1.cbTriggered) key1.trigger();
  523. if (!key2) return -1;
  524. const perc = (time - key1.time) / (key2.time - key1.time);
  525. if (key1.getEasing() == Anim.EASING_CLIP)
  526. {
  527. if (!key1.clip && this.port)
  528. {
  529. const patch = this.port.op.patch;
  530. const clip = patch.getVar(key1.clipId)?.getValue();
  531. if (clip) key1.clip = clip;
  532. }
  533. if (key1.clip && key1.clip.getValue)
  534. {
  535. return key1.clip.getValue(perc * key1.clip.getLength());
  536. }
  537. else
  538. {
  539. console.log("no clip found");
  540. }
  541. }
  542. return key1.ease(perc, key2) + valAdd;
  543. }
  544. /**
  545. * @param {AnimKey} k
  546. */
  547. addKey(k)
  548. {
  549. if (k.time === undefined)
  550. {
  551. this._log.warn("key time undefined, ignoring!");
  552. }
  553. else
  554. {
  555. this.keys.push(k);
  556. if (this.onChange !== null) this.onChange();
  557. this.emitEvent(Anim.EVENT_CHANGE, this);
  558. this.#needsSort = true;
  559. }
  560. }
  561. sortSoon()
  562. {
  563. this.#needsSort = true;
  564. }
  565. /**
  566. * @param {string} str
  567. */
  568. easingFromString(str)
  569. {
  570. // todo smarter way to map ?
  571. if (str == "linear") return Anim.EASING_LINEAR;
  572. if (str == "absolute") return Anim.EASING_ABSOLUTE;
  573. if (str == "smoothstep") return Anim.EASING_SMOOTHSTEP;
  574. if (str == "smootherstep") return Anim.EASING_SMOOTHERSTEP;
  575. if (str == "Cubic In") return Anim.EASING_CUBIC_IN;
  576. if (str == "Cubic Out") return Anim.EASING_CUBIC_OUT;
  577. if (str == "Cubic In Out") return Anim.EASING_CUBIC_INOUT;
  578. if (str == "Expo In") return Anim.EASING_EXPO_IN;
  579. if (str == "Expo Out") return Anim.EASING_EXPO_OUT;
  580. if (str == "Expo In Out") return Anim.EASING_EXPO_INOUT;
  581. if (str == "Sin In") return Anim.EASING_SIN_IN;
  582. if (str == "Sin Out") return Anim.EASING_SIN_OUT;
  583. if (str == "Sin In Out") return Anim.EASING_SIN_INOUT;
  584. if (str == "Back In") return Anim.EASING_BACK_IN;
  585. if (str == "Back Out") return Anim.EASING_BACK_OUT;
  586. if (str == "Back In Out") return Anim.EASING_BACK_INOUT;
  587. if (str == "Elastic In") return Anim.EASING_ELASTIC_IN;
  588. if (str == "Elastic Out") return Anim.EASING_ELASTIC_OUT;
  589. if (str == "Bounce In") return Anim.EASING_BOUNCE_IN;
  590. if (str == "Bounce Out") return Anim.EASING_BOUNCE_OUT;
  591. if (str == "Quart Out") return Anim.EASING_QUART_OUT;
  592. if (str == "Quart In") return Anim.EASING_QUART_IN;
  593. if (str == "Quart In Out") return Anim.EASING_QUART_INOUT;
  594. if (str == "Quint Out") return Anim.EASING_QUINT_OUT;
  595. if (str == "Quint In") return Anim.EASING_QUINT_IN;
  596. if (str == "Quint In Out") return Anim.EASING_QUINT_INOUT;
  597. console.log("unknown anim easing?", str);
  598. }
  599. /**
  600. * @param {Op} op
  601. * @param {string} title
  602. * @param {function} cb
  603. * @returns {Port}
  604. */
  605. createPort(op, title, cb)
  606. {
  607. const port = op.inDropDown(title, Anim.EASINGNAMES, "linear");
  608. port.set("linear");
  609. port.defaultValue = 0;
  610. port.onChange = () =>
  611. {
  612. this.defaultEasing = this.easingFromString(port.get());
  613. this.emitEvent("onChangeDefaultEasing", this);
  614. if (cb) cb();
  615. };
  616. return port;
  617. }
  618. get tlActive()
  619. {
  620. return this.#tlActive;
  621. }
  622. set tlActive(b)
  623. {
  624. if (CABLES.UI)
  625. {
  626. this.#tlActive = b;
  627. window.gui.emitEvent("tlActiveChanged", this);
  628. this.forceChangeCallbackSoon();
  629. }
  630. }
  631. /**
  632. * @param {Object} o
  633. */
  634. setUiAttribs(o)
  635. {
  636. for (const i in o)
  637. {
  638. this.uiAttribs[i] = o[i];
  639. if (o[i] === null) delete this.uiAttribs[i];
  640. }
  641. this.emitEvent(Anim.EVENT_UIATTRIB_CHANGE);
  642. }
  643. /**
  644. * @param {number} t
  645. * @param {number} t2
  646. */
  647. hasKeyframesBetween(t, t2)
  648. {
  649. for (let i = 0; i < this.keys.length; i++)
  650. if (this.keys[i].time >= t && this.keys[i].time <= t2) return true;
  651. return false;
  652. }
  653. /**
  654. * @param {Patch} patch
  655. */
  656. static initClipsFromVars(patch)
  657. {
  658. for (const i in patch.missingClipAnims)
  659. {
  660. const v = patch.getVar(i);
  661. for (let j = 0; j < patch.missingClipAnims[i].length; j++)
  662. {
  663. patch.missingClipAnims[i].clip = v.getValue();
  664. delete patch.missingClipAnims[i];
  665. }
  666. }
  667. }
  668. }