animationGroup.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  1. import { Animation } from "./animation.js";
  2. import { Observable } from "../Misc/observable.js";
  3. import { EngineStore } from "../Engines/engineStore.js";
  4. import { Tags } from "../Misc/tags.js";
  5. /**
  6. * This class defines the direct association between an animation and a target
  7. */
  8. export class TargetedAnimation {
  9. /**
  10. * Returns the string "TargetedAnimation"
  11. * @returns "TargetedAnimation"
  12. */
  13. getClassName() {
  14. return "TargetedAnimation";
  15. }
  16. /**
  17. * Serialize the object
  18. * @returns the JSON object representing the current entity
  19. */
  20. serialize() {
  21. const serializationObject = {};
  22. serializationObject.animation = this.animation.serialize();
  23. serializationObject.targetId = this.target.id;
  24. return serializationObject;
  25. }
  26. }
  27. /**
  28. * Use this class to create coordinated animations on multiple targets
  29. */
  30. export class AnimationGroup {
  31. /**
  32. * Gets or sets the mask associated with this animation group. This mask is used to filter which objects should be animated.
  33. */
  34. get mask() {
  35. return this._mask;
  36. }
  37. set mask(value) {
  38. if (this._mask === value) {
  39. return;
  40. }
  41. this._mask = value;
  42. this.syncWithMask(true);
  43. }
  44. /**
  45. * Makes sure that the animations are either played or stopped according to the animation group mask.
  46. * Note however that the call won't have any effect if the animation group has not been started yet.
  47. * @param forceUpdate If true, forces to loop over the animatables even if no mask is defined (used internally, you shouldn't need to use it). Default: false.
  48. */
  49. syncWithMask(forceUpdate = false) {
  50. if (!this.mask && !forceUpdate) {
  51. this._numActiveAnimatables = this._targetedAnimations.length;
  52. return;
  53. }
  54. this._numActiveAnimatables = 0;
  55. for (let i = 0; i < this._animatables.length; ++i) {
  56. const animatable = this._animatables[i];
  57. if (!this.mask || this.mask.disabled || this.mask.retainsTarget(animatable.target.name)) {
  58. this._numActiveAnimatables++;
  59. if (animatable.paused) {
  60. animatable.restart();
  61. }
  62. }
  63. else {
  64. if (!animatable.paused) {
  65. animatable.pause();
  66. }
  67. }
  68. }
  69. }
  70. /**
  71. * Removes all animations for the targets not retained by the animation group mask.
  72. * Use this function if you know you won't need those animations anymore and if you want to free memory.
  73. */
  74. removeUnmaskedAnimations() {
  75. if (!this.mask || this.mask.disabled) {
  76. return;
  77. }
  78. // Removes all animatables (in case the animation group has already been started)
  79. for (let i = 0; i < this._animatables.length; ++i) {
  80. const animatable = this._animatables[i];
  81. if (!this.mask.retainsTarget(animatable.target.name)) {
  82. animatable.stop();
  83. this._animatables.splice(i, 1);
  84. --i;
  85. }
  86. }
  87. // Removes the targeted animations
  88. for (let index = 0; index < this._targetedAnimations.length; index++) {
  89. const targetedAnimation = this._targetedAnimations[index];
  90. if (!this.mask.retainsTarget(targetedAnimation.target.name)) {
  91. this._targetedAnimations.splice(index, 1);
  92. --index;
  93. }
  94. }
  95. }
  96. /**
  97. * Gets or sets the first frame
  98. */
  99. get from() {
  100. return this._from;
  101. }
  102. set from(value) {
  103. if (this._from === value) {
  104. return;
  105. }
  106. this._from = value;
  107. for (let index = 0; index < this._animatables.length; index++) {
  108. const animatable = this._animatables[index];
  109. animatable.fromFrame = this._from;
  110. }
  111. }
  112. /**
  113. * Gets or sets the last frame
  114. */
  115. get to() {
  116. return this._to;
  117. }
  118. set to(value) {
  119. if (this._to === value) {
  120. return;
  121. }
  122. this._to = value;
  123. for (let index = 0; index < this._animatables.length; index++) {
  124. const animatable = this._animatables[index];
  125. animatable.toFrame = this._to;
  126. }
  127. }
  128. /**
  129. * Define if the animations are started
  130. */
  131. get isStarted() {
  132. return this._isStarted;
  133. }
  134. /**
  135. * Gets a value indicating that the current group is playing
  136. */
  137. get isPlaying() {
  138. return this._isStarted && !this._isPaused;
  139. }
  140. /**
  141. * Gets or sets the speed ratio to use for all animations
  142. */
  143. get speedRatio() {
  144. return this._speedRatio;
  145. }
  146. /**
  147. * Gets or sets the speed ratio to use for all animations
  148. */
  149. set speedRatio(value) {
  150. if (this._speedRatio === value) {
  151. return;
  152. }
  153. this._speedRatio = value;
  154. for (let index = 0; index < this._animatables.length; index++) {
  155. const animatable = this._animatables[index];
  156. animatable.speedRatio = this._speedRatio;
  157. }
  158. }
  159. /**
  160. * Gets or sets if all animations should loop or not
  161. */
  162. get loopAnimation() {
  163. return this._loopAnimation;
  164. }
  165. set loopAnimation(value) {
  166. if (this._loopAnimation === value) {
  167. return;
  168. }
  169. this._loopAnimation = value;
  170. for (let index = 0; index < this._animatables.length; index++) {
  171. const animatable = this._animatables[index];
  172. animatable.loopAnimation = this._loopAnimation;
  173. }
  174. }
  175. /**
  176. * Gets or sets if all animations should be evaluated additively
  177. */
  178. get isAdditive() {
  179. return this._isAdditive;
  180. }
  181. set isAdditive(value) {
  182. if (this._isAdditive === value) {
  183. return;
  184. }
  185. this._isAdditive = value;
  186. for (let index = 0; index < this._animatables.length; index++) {
  187. const animatable = this._animatables[index];
  188. animatable.isAdditive = this._isAdditive;
  189. }
  190. }
  191. /**
  192. * Gets or sets the weight to apply to all animations of the group
  193. */
  194. get weight() {
  195. return this._weight;
  196. }
  197. set weight(value) {
  198. if (this._weight === value) {
  199. return;
  200. }
  201. this._weight = value;
  202. this.setWeightForAllAnimatables(this._weight);
  203. }
  204. /**
  205. * Gets the targeted animations for this animation group
  206. */
  207. get targetedAnimations() {
  208. return this._targetedAnimations;
  209. }
  210. /**
  211. * returning the list of animatables controlled by this animation group.
  212. */
  213. get animatables() {
  214. return this._animatables;
  215. }
  216. /**
  217. * Gets the list of target animations
  218. */
  219. get children() {
  220. return this._targetedAnimations;
  221. }
  222. /**
  223. * Gets or sets the order of play of the animation group (default: 0)
  224. */
  225. get playOrder() {
  226. return this._playOrder;
  227. }
  228. set playOrder(value) {
  229. if (this._playOrder === value) {
  230. return;
  231. }
  232. this._playOrder = value;
  233. if (this._animatables.length > 0) {
  234. for (let i = 0; i < this._animatables.length; i++) {
  235. this._animatables[i].playOrder = this._playOrder;
  236. }
  237. this._scene.sortActiveAnimatables();
  238. }
  239. }
  240. /**
  241. * Allows the animations of the animation group to blend with current running animations
  242. * Note that a null value means that each animation will use their own existing blending configuration (Animation.enableBlending)
  243. */
  244. get enableBlending() {
  245. return this._enableBlending;
  246. }
  247. set enableBlending(value) {
  248. if (this._enableBlending === value) {
  249. return;
  250. }
  251. this._enableBlending = value;
  252. if (value !== null) {
  253. for (let i = 0; i < this._targetedAnimations.length; ++i) {
  254. this._targetedAnimations[i].animation.enableBlending = value;
  255. }
  256. }
  257. }
  258. /**
  259. * Gets or sets the animation blending speed
  260. * Note that a null value means that each animation will use their own existing blending configuration (Animation.blendingSpeed)
  261. */
  262. get blendingSpeed() {
  263. return this._blendingSpeed;
  264. }
  265. set blendingSpeed(value) {
  266. if (this._blendingSpeed === value) {
  267. return;
  268. }
  269. this._blendingSpeed = value;
  270. if (value !== null) {
  271. for (let i = 0; i < this._targetedAnimations.length; ++i) {
  272. this._targetedAnimations[i].animation.blendingSpeed = value;
  273. }
  274. }
  275. }
  276. /**
  277. * Gets the length (in seconds) of the animation group
  278. * This function assumes that all animations are played at the same framePerSecond speed!
  279. * Note: you can only call this method after you've added at least one targeted animation!
  280. * @param from Starting frame range (default is AnimationGroup.from)
  281. * @param to Ending frame range (default is AnimationGroup.to)
  282. * @returns The length in seconds
  283. */
  284. getLength(from, to) {
  285. from = from ?? this._from;
  286. to = to ?? this._to;
  287. const fps = this.targetedAnimations[0].animation.framePerSecond * this._speedRatio;
  288. return (to - from) / fps;
  289. }
  290. /**
  291. * Merge the array of animation groups into a new animation group
  292. * @param animationGroups List of animation groups to merge
  293. * @param disposeSource If true, animation groups will be disposed after being merged (default: true)
  294. * @param normalize If true, animation groups will be normalized before being merged, so that all animations have the same "from" and "to" frame (default: false)
  295. * @param weight Weight for the new animation group. If not provided, it will inherit the weight from the first animation group of the array
  296. * @returns The new animation group or null if no animation groups were passed
  297. */
  298. static MergeAnimationGroups(animationGroups, disposeSource = true, normalize = false, weight) {
  299. if (animationGroups.length === 0) {
  300. return null;
  301. }
  302. weight = weight ?? animationGroups[0].weight;
  303. let beginFrame = Number.MAX_VALUE;
  304. let endFrame = -Number.MAX_VALUE;
  305. if (normalize) {
  306. for (const animationGroup of animationGroups) {
  307. if (animationGroup.from < beginFrame) {
  308. beginFrame = animationGroup.from;
  309. }
  310. if (animationGroup.to > endFrame) {
  311. endFrame = animationGroup.to;
  312. }
  313. }
  314. }
  315. const mergedAnimationGroup = new AnimationGroup(animationGroups[0].name + "_merged", animationGroups[0]._scene, weight);
  316. for (const animationGroup of animationGroups) {
  317. if (normalize) {
  318. animationGroup.normalize(beginFrame, endFrame);
  319. }
  320. for (const targetedAnimation of animationGroup.targetedAnimations) {
  321. mergedAnimationGroup.addTargetedAnimation(targetedAnimation.animation, targetedAnimation.target);
  322. }
  323. if (disposeSource) {
  324. animationGroup.dispose();
  325. }
  326. }
  327. return mergedAnimationGroup;
  328. }
  329. /**
  330. * Instantiates a new Animation Group.
  331. * This helps managing several animations at once.
  332. * @see https://doc.babylonjs.com/features/featuresDeepDive/animation/groupAnimations
  333. * @param name Defines the name of the group
  334. * @param scene Defines the scene the group belongs to
  335. * @param weight Defines the weight to use for animations in the group (-1.0 by default, meaning "no weight")
  336. * @param playOrder Defines the order of play of the animation group (default is 0)
  337. */
  338. constructor(
  339. /** The name of the animation group */
  340. name, scene = null, weight = -1, playOrder = 0) {
  341. this.name = name;
  342. this._targetedAnimations = new Array();
  343. this._animatables = new Array();
  344. this._from = Number.MAX_VALUE;
  345. this._to = -Number.MAX_VALUE;
  346. this._speedRatio = 1;
  347. this._loopAnimation = false;
  348. this._isAdditive = false;
  349. this._weight = -1;
  350. this._playOrder = 0;
  351. this._enableBlending = null;
  352. this._blendingSpeed = null;
  353. this._numActiveAnimatables = 0;
  354. /** @internal */
  355. this._parentContainer = null;
  356. /**
  357. * This observable will notify when one animation have ended
  358. */
  359. this.onAnimationEndObservable = new Observable();
  360. /**
  361. * Observer raised when one animation loops
  362. */
  363. this.onAnimationLoopObservable = new Observable();
  364. /**
  365. * Observer raised when all animations have looped
  366. */
  367. this.onAnimationGroupLoopObservable = new Observable();
  368. /**
  369. * This observable will notify when all animations have ended.
  370. */
  371. this.onAnimationGroupEndObservable = new Observable();
  372. /**
  373. * This observable will notify when all animations have paused.
  374. */
  375. this.onAnimationGroupPauseObservable = new Observable();
  376. /**
  377. * This observable will notify when all animations are playing.
  378. */
  379. this.onAnimationGroupPlayObservable = new Observable();
  380. /**
  381. * Gets or sets an object used to store user defined information for the node
  382. */
  383. this.metadata = null;
  384. this._mask = null;
  385. this._animationLoopFlags = [];
  386. this._scene = scene || EngineStore.LastCreatedScene;
  387. this._weight = weight;
  388. this._playOrder = playOrder;
  389. this.uniqueId = this._scene.getUniqueId();
  390. this._scene.addAnimationGroup(this);
  391. }
  392. /**
  393. * Add an animation (with its target) in the group
  394. * @param animation defines the animation we want to add
  395. * @param target defines the target of the animation
  396. * @returns the TargetedAnimation object
  397. */
  398. addTargetedAnimation(animation, target) {
  399. const targetedAnimation = new TargetedAnimation();
  400. targetedAnimation.animation = animation;
  401. targetedAnimation.target = target;
  402. const keys = animation.getKeys();
  403. if (this._from > keys[0].frame) {
  404. this._from = keys[0].frame;
  405. }
  406. if (this._to < keys[keys.length - 1].frame) {
  407. this._to = keys[keys.length - 1].frame;
  408. }
  409. if (this._enableBlending !== null) {
  410. animation.enableBlending = this._enableBlending;
  411. }
  412. if (this._blendingSpeed !== null) {
  413. animation.blendingSpeed = this._blendingSpeed;
  414. }
  415. this._targetedAnimations.push(targetedAnimation);
  416. return targetedAnimation;
  417. }
  418. /**
  419. * Remove an animation from the group
  420. * @param animation defines the animation we want to remove
  421. */
  422. removeTargetedAnimation(animation) {
  423. for (let index = this._targetedAnimations.length - 1; index > -1; index--) {
  424. const targetedAnimation = this._targetedAnimations[index];
  425. if (targetedAnimation.animation === animation) {
  426. this._targetedAnimations.splice(index, 1);
  427. }
  428. }
  429. }
  430. /**
  431. * This function will normalize every animation in the group to make sure they all go from beginFrame to endFrame
  432. * It can add constant keys at begin or end
  433. * @param beginFrame defines the new begin frame for all animations or the smallest begin frame of all animations if null (defaults to null)
  434. * @param endFrame defines the new end frame for all animations or the largest end frame of all animations if null (defaults to null)
  435. * @returns the animation group
  436. */
  437. normalize(beginFrame = null, endFrame = null) {
  438. if (beginFrame == null) {
  439. beginFrame = this._from;
  440. }
  441. if (endFrame == null) {
  442. endFrame = this._to;
  443. }
  444. for (let index = 0; index < this._targetedAnimations.length; index++) {
  445. const targetedAnimation = this._targetedAnimations[index];
  446. const keys = targetedAnimation.animation.getKeys();
  447. const startKey = keys[0];
  448. const endKey = keys[keys.length - 1];
  449. if (startKey.frame > beginFrame) {
  450. const newKey = {
  451. frame: beginFrame,
  452. value: startKey.value,
  453. inTangent: startKey.inTangent,
  454. outTangent: startKey.outTangent,
  455. interpolation: startKey.interpolation,
  456. };
  457. keys.splice(0, 0, newKey);
  458. }
  459. if (endKey.frame < endFrame) {
  460. const newKey = {
  461. frame: endFrame,
  462. value: endKey.value,
  463. inTangent: endKey.inTangent,
  464. outTangent: endKey.outTangent,
  465. interpolation: endKey.interpolation,
  466. };
  467. keys.push(newKey);
  468. }
  469. }
  470. this._from = beginFrame;
  471. this._to = endFrame;
  472. return this;
  473. }
  474. _processLoop(animatable, targetedAnimation, index) {
  475. animatable.onAnimationLoop = () => {
  476. this.onAnimationLoopObservable.notifyObservers(targetedAnimation);
  477. if (this._animationLoopFlags[index]) {
  478. return;
  479. }
  480. this._animationLoopFlags[index] = true;
  481. this._animationLoopCount++;
  482. if (this._animationLoopCount === this._numActiveAnimatables) {
  483. this.onAnimationGroupLoopObservable.notifyObservers(this);
  484. this._animationLoopCount = 0;
  485. this._animationLoopFlags.length = 0;
  486. }
  487. };
  488. }
  489. /**
  490. * Start all animations on given targets
  491. * @param loop defines if animations must loop
  492. * @param speedRatio defines the ratio to apply to animation speed (1 by default)
  493. * @param from defines the from key (optional)
  494. * @param to defines the to key (optional)
  495. * @param isAdditive defines the additive state for the resulting animatables (optional)
  496. * @returns the current animation group
  497. */
  498. start(loop = false, speedRatio = 1, from, to, isAdditive) {
  499. if (this._isStarted || this._targetedAnimations.length === 0) {
  500. return this;
  501. }
  502. this._loopAnimation = loop;
  503. this._animationLoopCount = 0;
  504. this._animationLoopFlags.length = 0;
  505. for (let index = 0; index < this._targetedAnimations.length; index++) {
  506. const targetedAnimation = this._targetedAnimations[index];
  507. const animatable = this._scene.beginDirectAnimation(targetedAnimation.target, [targetedAnimation.animation], from !== undefined ? from : this._from, to !== undefined ? to : this._to, loop, speedRatio, undefined, undefined, isAdditive !== undefined ? isAdditive : this._isAdditive);
  508. animatable.weight = this._weight;
  509. animatable.playOrder = this._playOrder;
  510. animatable.onAnimationEnd = () => {
  511. this.onAnimationEndObservable.notifyObservers(targetedAnimation);
  512. this._checkAnimationGroupEnded(animatable);
  513. };
  514. this._processLoop(animatable, targetedAnimation, index);
  515. this._animatables.push(animatable);
  516. }
  517. this.syncWithMask();
  518. this._scene.sortActiveAnimatables();
  519. this._speedRatio = speedRatio;
  520. this._isStarted = true;
  521. this._isPaused = false;
  522. this.onAnimationGroupPlayObservable.notifyObservers(this);
  523. return this;
  524. }
  525. /**
  526. * Pause all animations
  527. * @returns the animation group
  528. */
  529. pause() {
  530. if (!this._isStarted) {
  531. return this;
  532. }
  533. this._isPaused = true;
  534. for (let index = 0; index < this._animatables.length; index++) {
  535. const animatable = this._animatables[index];
  536. animatable.pause();
  537. }
  538. this.onAnimationGroupPauseObservable.notifyObservers(this);
  539. return this;
  540. }
  541. /**
  542. * Play all animations to initial state
  543. * This function will start() the animations if they were not started or will restart() them if they were paused
  544. * @param loop defines if animations must loop
  545. * @returns the animation group
  546. */
  547. play(loop) {
  548. // only if all animatables are ready and exist
  549. if (this.isStarted && this._animatables.length === this._targetedAnimations.length) {
  550. if (loop !== undefined) {
  551. this.loopAnimation = loop;
  552. }
  553. this.restart();
  554. }
  555. else {
  556. this.stop();
  557. this.start(loop, this._speedRatio);
  558. }
  559. this._isPaused = false;
  560. return this;
  561. }
  562. /**
  563. * Reset all animations to initial state
  564. * @returns the animation group
  565. */
  566. reset() {
  567. if (!this._isStarted) {
  568. this.play();
  569. this.goToFrame(0);
  570. this.stop();
  571. return this;
  572. }
  573. for (let index = 0; index < this._animatables.length; index++) {
  574. const animatable = this._animatables[index];
  575. animatable.reset();
  576. }
  577. return this;
  578. }
  579. /**
  580. * Restart animations from key 0
  581. * @returns the animation group
  582. */
  583. restart() {
  584. if (!this._isStarted) {
  585. return this;
  586. }
  587. for (let index = 0; index < this._animatables.length; index++) {
  588. const animatable = this._animatables[index];
  589. animatable.restart();
  590. }
  591. this.syncWithMask();
  592. this.onAnimationGroupPlayObservable.notifyObservers(this);
  593. return this;
  594. }
  595. /**
  596. * Stop all animations
  597. * @returns the animation group
  598. */
  599. stop() {
  600. if (!this._isStarted) {
  601. return this;
  602. }
  603. const list = this._animatables.slice();
  604. for (let index = 0; index < list.length; index++) {
  605. list[index].stop(undefined, undefined, true);
  606. }
  607. // We will take care of removing all stopped animatables
  608. let curIndex = 0;
  609. for (let index = 0; index < this._scene._activeAnimatables.length; index++) {
  610. const animatable = this._scene._activeAnimatables[index];
  611. if (animatable._runtimeAnimations.length > 0) {
  612. this._scene._activeAnimatables[curIndex++] = animatable;
  613. }
  614. }
  615. this._scene._activeAnimatables.length = curIndex;
  616. this._isStarted = false;
  617. return this;
  618. }
  619. /**
  620. * Set animation weight for all animatables
  621. *
  622. * @since 6.12.4
  623. * You can pass the weight to the AnimationGroup constructor, or use the weight property to set it after the group has been created,
  624. * making it easier to define the overall animation weight than calling setWeightForAllAnimatables() after the animation group has been started
  625. * @param weight defines the weight to use
  626. * @returns the animationGroup
  627. * @see https://doc.babylonjs.com/features/featuresDeepDive/animation/advanced_animations#animation-weights
  628. */
  629. setWeightForAllAnimatables(weight) {
  630. for (let index = 0; index < this._animatables.length; index++) {
  631. const animatable = this._animatables[index];
  632. animatable.weight = weight;
  633. }
  634. return this;
  635. }
  636. /**
  637. * Synchronize and normalize all animatables with a source animatable
  638. * @param root defines the root animatable to synchronize with (null to stop synchronizing)
  639. * @returns the animationGroup
  640. * @see https://doc.babylonjs.com/features/featuresDeepDive/animation/advanced_animations#animation-weights
  641. */
  642. syncAllAnimationsWith(root) {
  643. for (let index = 0; index < this._animatables.length; index++) {
  644. const animatable = this._animatables[index];
  645. animatable.syncWith(root);
  646. }
  647. return this;
  648. }
  649. /**
  650. * Goes to a specific frame in this animation group. Note that the animation group must be in playing or paused status
  651. * @param frame the frame number to go to
  652. * @returns the animationGroup
  653. */
  654. goToFrame(frame) {
  655. if (!this._isStarted) {
  656. return this;
  657. }
  658. for (let index = 0; index < this._animatables.length; index++) {
  659. const animatable = this._animatables[index];
  660. animatable.goToFrame(frame);
  661. }
  662. return this;
  663. }
  664. /**
  665. * Dispose all associated resources
  666. */
  667. dispose() {
  668. this._targetedAnimations.length = 0;
  669. this._animatables.length = 0;
  670. // Remove from scene
  671. const index = this._scene.animationGroups.indexOf(this);
  672. if (index > -1) {
  673. this._scene.animationGroups.splice(index, 1);
  674. }
  675. if (this._parentContainer) {
  676. const index = this._parentContainer.animationGroups.indexOf(this);
  677. if (index > -1) {
  678. this._parentContainer.animationGroups.splice(index, 1);
  679. }
  680. this._parentContainer = null;
  681. }
  682. this.onAnimationEndObservable.clear();
  683. this.onAnimationGroupEndObservable.clear();
  684. this.onAnimationGroupPauseObservable.clear();
  685. this.onAnimationGroupPlayObservable.clear();
  686. this.onAnimationLoopObservable.clear();
  687. this.onAnimationGroupLoopObservable.clear();
  688. }
  689. _checkAnimationGroupEnded(animatable) {
  690. // animatable should be taken out of the array
  691. const idx = this._animatables.indexOf(animatable);
  692. if (idx > -1) {
  693. this._animatables.splice(idx, 1);
  694. }
  695. // all animatables were removed? animation group ended!
  696. if (this._animatables.length === 0) {
  697. this._isStarted = false;
  698. this.onAnimationGroupEndObservable.notifyObservers(this);
  699. }
  700. }
  701. /**
  702. * Clone the current animation group and returns a copy
  703. * @param newName defines the name of the new group
  704. * @param targetConverter defines an optional function used to convert current animation targets to new ones
  705. * @param cloneAnimations defines if the animations should be cloned or referenced
  706. * @returns the new animation group
  707. */
  708. clone(newName, targetConverter, cloneAnimations = false) {
  709. const newGroup = new AnimationGroup(newName || this.name, this._scene, this._weight, this._playOrder);
  710. newGroup._from = this.from;
  711. newGroup._to = this.to;
  712. newGroup._speedRatio = this.speedRatio;
  713. newGroup._loopAnimation = this.loopAnimation;
  714. newGroup._isAdditive = this.isAdditive;
  715. newGroup._enableBlending = this.enableBlending;
  716. newGroup._blendingSpeed = this.blendingSpeed;
  717. newGroup.metadata = this.metadata;
  718. newGroup.mask = this.mask;
  719. for (const targetAnimation of this._targetedAnimations) {
  720. newGroup.addTargetedAnimation(cloneAnimations ? targetAnimation.animation.clone() : targetAnimation.animation, targetConverter ? targetConverter(targetAnimation.target) : targetAnimation.target);
  721. }
  722. return newGroup;
  723. }
  724. /**
  725. * Serializes the animationGroup to an object
  726. * @returns Serialized object
  727. */
  728. serialize() {
  729. const serializationObject = {};
  730. serializationObject.name = this.name;
  731. serializationObject.from = this.from;
  732. serializationObject.to = this.to;
  733. serializationObject.speedRatio = this.speedRatio;
  734. serializationObject.loopAnimation = this.loopAnimation;
  735. serializationObject.isAdditive = this.isAdditive;
  736. serializationObject.weight = this.weight;
  737. serializationObject.playOrder = this.playOrder;
  738. serializationObject.enableBlending = this.enableBlending;
  739. serializationObject.blendingSpeed = this.blendingSpeed;
  740. serializationObject.targetedAnimations = [];
  741. for (let targetedAnimationIndex = 0; targetedAnimationIndex < this.targetedAnimations.length; targetedAnimationIndex++) {
  742. const targetedAnimation = this.targetedAnimations[targetedAnimationIndex];
  743. serializationObject.targetedAnimations[targetedAnimationIndex] = targetedAnimation.serialize();
  744. }
  745. if (Tags && Tags.HasTags(this)) {
  746. serializationObject.tags = Tags.GetTags(this);
  747. }
  748. // Metadata
  749. if (this.metadata) {
  750. serializationObject.metadata = this.metadata;
  751. }
  752. return serializationObject;
  753. }
  754. // Statics
  755. /**
  756. * Returns a new AnimationGroup object parsed from the source provided.
  757. * @param parsedAnimationGroup defines the source
  758. * @param scene defines the scene that will receive the animationGroup
  759. * @returns a new AnimationGroup
  760. */
  761. static Parse(parsedAnimationGroup, scene) {
  762. const animationGroup = new AnimationGroup(parsedAnimationGroup.name, scene, parsedAnimationGroup.weight, parsedAnimationGroup.playOrder);
  763. for (let i = 0; i < parsedAnimationGroup.targetedAnimations.length; i++) {
  764. const targetedAnimation = parsedAnimationGroup.targetedAnimations[i];
  765. const animation = Animation.Parse(targetedAnimation.animation);
  766. const id = targetedAnimation.targetId;
  767. if (targetedAnimation.animation.property === "influence") {
  768. // morph target animation
  769. const morphTarget = scene.getMorphTargetById(id);
  770. if (morphTarget) {
  771. animationGroup.addTargetedAnimation(animation, morphTarget);
  772. }
  773. }
  774. else {
  775. const targetNode = scene.getNodeById(id);
  776. if (targetNode != null) {
  777. animationGroup.addTargetedAnimation(animation, targetNode);
  778. }
  779. }
  780. }
  781. if (Tags) {
  782. Tags.AddTagsTo(animationGroup, parsedAnimationGroup.tags);
  783. }
  784. if (parsedAnimationGroup.from !== null && parsedAnimationGroup.to !== null) {
  785. animationGroup.normalize(parsedAnimationGroup.from, parsedAnimationGroup.to);
  786. }
  787. if (parsedAnimationGroup.speedRatio !== undefined) {
  788. animationGroup._speedRatio = parsedAnimationGroup.speedRatio;
  789. }
  790. if (parsedAnimationGroup.loopAnimation !== undefined) {
  791. animationGroup._loopAnimation = parsedAnimationGroup.loopAnimation;
  792. }
  793. if (parsedAnimationGroup.isAdditive !== undefined) {
  794. animationGroup._isAdditive = parsedAnimationGroup.isAdditive;
  795. }
  796. if (parsedAnimationGroup.weight !== undefined) {
  797. animationGroup._weight = parsedAnimationGroup.weight;
  798. }
  799. if (parsedAnimationGroup.playOrder !== undefined) {
  800. animationGroup._playOrder = parsedAnimationGroup.playOrder;
  801. }
  802. if (parsedAnimationGroup.enableBlending !== undefined) {
  803. animationGroup._enableBlending = parsedAnimationGroup.enableBlending;
  804. }
  805. if (parsedAnimationGroup.blendingSpeed !== undefined) {
  806. animationGroup._blendingSpeed = parsedAnimationGroup.blendingSpeed;
  807. }
  808. if (parsedAnimationGroup.metadata !== undefined) {
  809. animationGroup.metadata = parsedAnimationGroup.metadata;
  810. }
  811. return animationGroup;
  812. }
  813. /** @internal */
  814. static MakeAnimationAdditive(sourceAnimationGroup, referenceFrameOrOptions, range, cloneOriginal = false, clonedName) {
  815. let options;
  816. if (typeof referenceFrameOrOptions === "object") {
  817. options = referenceFrameOrOptions;
  818. }
  819. else {
  820. options = {
  821. referenceFrame: referenceFrameOrOptions,
  822. range: range,
  823. cloneOriginalAnimationGroup: cloneOriginal,
  824. clonedAnimationName: clonedName,
  825. };
  826. }
  827. let animationGroup = sourceAnimationGroup;
  828. if (options.cloneOriginalAnimationGroup) {
  829. animationGroup = sourceAnimationGroup.clone(options.clonedAnimationGroupName || animationGroup.name);
  830. }
  831. const targetedAnimations = animationGroup.targetedAnimations;
  832. for (let index = 0; index < targetedAnimations.length; index++) {
  833. const targetedAnimation = targetedAnimations[index];
  834. targetedAnimation.animation = Animation.MakeAnimationAdditive(targetedAnimation.animation, options);
  835. }
  836. animationGroup.isAdditive = true;
  837. if (options.clipKeys) {
  838. // We need to recalculate the from/to frames for the animation group because some keys may have been removed
  839. let from = Number.MAX_VALUE;
  840. let to = -Number.MAX_VALUE;
  841. const targetedAnimations = animationGroup.targetedAnimations;
  842. for (let index = 0; index < targetedAnimations.length; index++) {
  843. const targetedAnimation = targetedAnimations[index];
  844. const animation = targetedAnimation.animation;
  845. const keys = animation.getKeys();
  846. if (from > keys[0].frame) {
  847. from = keys[0].frame;
  848. }
  849. if (to < keys[keys.length - 1].frame) {
  850. to = keys[keys.length - 1].frame;
  851. }
  852. }
  853. animationGroup._from = from;
  854. animationGroup._to = to;
  855. }
  856. return animationGroup;
  857. }
  858. /**
  859. * Creates a new animation, keeping only the keys that are inside a given key range
  860. * @param sourceAnimationGroup defines the animation group on which to operate
  861. * @param fromKey defines the lower bound of the range
  862. * @param toKey defines the upper bound of the range
  863. * @param name defines the name of the new animation group. If not provided, use the same name as animationGroup
  864. * @param dontCloneAnimations defines whether or not the animations should be cloned before clipping the keys. Default is false, so animations will be cloned
  865. * @returns a new animation group stripped from all the keys outside the given range
  866. */
  867. static ClipKeys(sourceAnimationGroup, fromKey, toKey, name, dontCloneAnimations) {
  868. const animationGroup = sourceAnimationGroup.clone(name || sourceAnimationGroup.name);
  869. return AnimationGroup.ClipKeysInPlace(animationGroup, fromKey, toKey, dontCloneAnimations);
  870. }
  871. /**
  872. * Updates an existing animation, keeping only the keys that are inside a given key range
  873. * @param animationGroup defines the animation group on which to operate
  874. * @param fromKey defines the lower bound of the range
  875. * @param toKey defines the upper bound of the range
  876. * @param dontCloneAnimations defines whether or not the animations should be cloned before clipping the keys. Default is false, so animations will be cloned
  877. * @returns the animationGroup stripped from all the keys outside the given range
  878. */
  879. static ClipKeysInPlace(animationGroup, fromKey, toKey, dontCloneAnimations) {
  880. return AnimationGroup.ClipInPlace(animationGroup, fromKey, toKey, dontCloneAnimations, false);
  881. }
  882. /**
  883. * Creates a new animation, keeping only the frames that are inside a given frame range
  884. * @param sourceAnimationGroup defines the animation group on which to operate
  885. * @param fromFrame defines the lower bound of the range
  886. * @param toFrame defines the upper bound of the range
  887. * @param name defines the name of the new animation group. If not provided, use the same name as animationGroup
  888. * @param dontCloneAnimations defines whether or not the animations should be cloned before clipping the frames. Default is false, so animations will be cloned
  889. * @returns a new animation group stripped from all the frames outside the given range
  890. */
  891. static ClipFrames(sourceAnimationGroup, fromFrame, toFrame, name, dontCloneAnimations) {
  892. const animationGroup = sourceAnimationGroup.clone(name || sourceAnimationGroup.name);
  893. return AnimationGroup.ClipFramesInPlace(animationGroup, fromFrame, toFrame, dontCloneAnimations);
  894. }
  895. /**
  896. * Updates an existing animation, keeping only the frames that are inside a given frame range
  897. * @param animationGroup defines the animation group on which to operate
  898. * @param fromFrame defines the lower bound of the range
  899. * @param toFrame defines the upper bound of the range
  900. * @param dontCloneAnimations defines whether or not the animations should be cloned before clipping the frames. Default is false, so animations will be cloned
  901. * @returns the animationGroup stripped from all the frames outside the given range
  902. */
  903. static ClipFramesInPlace(animationGroup, fromFrame, toFrame, dontCloneAnimations) {
  904. return AnimationGroup.ClipInPlace(animationGroup, fromFrame, toFrame, dontCloneAnimations, true);
  905. }
  906. /**
  907. * Updates an existing animation, keeping only the keys that are inside a given key or frame range
  908. * @param animationGroup defines the animation group on which to operate
  909. * @param start defines the lower bound of the range
  910. * @param end defines the upper bound of the range
  911. * @param dontCloneAnimations defines whether or not the animations should be cloned before clipping the keys. Default is false, so animations will be cloned
  912. * @param useFrame defines if the range is defined by frame numbers or key indices (default is false which means use key indices)
  913. * @returns the animationGroup stripped from all the keys outside the given range
  914. */
  915. static ClipInPlace(animationGroup, start, end, dontCloneAnimations, useFrame = false) {
  916. let from = Number.MAX_VALUE;
  917. let to = -Number.MAX_VALUE;
  918. const targetedAnimations = animationGroup.targetedAnimations;
  919. for (let index = 0; index < targetedAnimations.length; index++) {
  920. const targetedAnimation = targetedAnimations[index];
  921. const animation = dontCloneAnimations ? targetedAnimation.animation : targetedAnimation.animation.clone();
  922. if (useFrame) {
  923. // Make sure we have keys corresponding to the bounds of the frame range
  924. animation.createKeyForFrame(start);
  925. animation.createKeyForFrame(end);
  926. }
  927. const keys = animation.getKeys();
  928. const newKeys = [];
  929. let startFrame = Number.MAX_VALUE;
  930. for (let k = 0; k < keys.length; k++) {
  931. const key = keys[k];
  932. if ((!useFrame && k >= start && k <= end) || (useFrame && key.frame >= start && key.frame <= end)) {
  933. const newKey = {
  934. frame: key.frame,
  935. value: key.value.clone ? key.value.clone() : key.value,
  936. inTangent: key.inTangent,
  937. outTangent: key.outTangent,
  938. interpolation: key.interpolation,
  939. lockedTangent: key.lockedTangent,
  940. };
  941. if (startFrame === Number.MAX_VALUE) {
  942. startFrame = newKey.frame;
  943. }
  944. newKey.frame -= startFrame;
  945. newKeys.push(newKey);
  946. }
  947. }
  948. if (newKeys.length === 0) {
  949. targetedAnimations.splice(index, 1);
  950. index--;
  951. continue;
  952. }
  953. if (from > newKeys[0].frame) {
  954. from = newKeys[0].frame;
  955. }
  956. if (to < newKeys[newKeys.length - 1].frame) {
  957. to = newKeys[newKeys.length - 1].frame;
  958. }
  959. animation.setKeys(newKeys, true);
  960. targetedAnimation.animation = animation; // in case the animation has been cloned
  961. }
  962. animationGroup._from = from;
  963. animationGroup._to = to;
  964. return animationGroup;
  965. }
  966. /**
  967. * Returns the string "AnimationGroup"
  968. * @returns "AnimationGroup"
  969. */
  970. getClassName() {
  971. return "AnimationGroup";
  972. }
  973. /**
  974. * Creates a detailed string about the object
  975. * @param fullDetails defines if the output string will support multiple levels of logging within scene loading
  976. * @returns a string representing the object
  977. */
  978. toString(fullDetails) {
  979. let ret = "Name: " + this.name;
  980. ret += ", type: " + this.getClassName();
  981. if (fullDetails) {
  982. ret += ", from: " + this._from;
  983. ret += ", to: " + this._to;
  984. ret += ", isStarted: " + this._isStarted;
  985. ret += ", speedRatio: " + this._speedRatio;
  986. ret += ", targetedAnimations length: " + this._targetedAnimations.length;
  987. ret += ", animatables length: " + this._animatables;
  988. }
  989. return ret;
  990. }
  991. }
  992. //# sourceMappingURL=animationGroup.js.map