animationGroup.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. import { Animatable } from "./animatable";
  2. import { Animation } from "./animation";
  3. import { IAnimationKey } from "./animationKey";
  4. import { Scene, IDisposable } from "../scene";
  5. import { Observable } from "../Misc/observable";
  6. import { Nullable } from "../types";
  7. import { EngineStore } from "../Engines/engineStore";
  8. import "./animatable";
  9. /**
  10. * This class defines the direct association between an animation and a target
  11. */
  12. export class TargetedAnimation {
  13. /**
  14. * Animation to perform
  15. */
  16. public animation: Animation;
  17. /**
  18. * Target to animate
  19. */
  20. public target: any;
  21. /**
  22. * Serialize the object
  23. * @returns the JSON object representing the current entity
  24. */
  25. public serialize(): any {
  26. var serializationObject: any = {};
  27. serializationObject.animation = this.animation.serialize();
  28. serializationObject.targetId = this.target.id;
  29. return serializationObject;
  30. }
  31. }
  32. /**
  33. * Use this class to create coordinated animations on multiple targets
  34. */
  35. export class AnimationGroup implements IDisposable {
  36. private _scene: Scene;
  37. private _targetedAnimations = new Array<TargetedAnimation>();
  38. private _animatables = new Array<Animatable>();
  39. private _from = Number.MAX_VALUE;
  40. private _to = -Number.MAX_VALUE;
  41. private _isStarted: boolean;
  42. private _isPaused: boolean;
  43. private _speedRatio = 1;
  44. private _loopAnimation = false;
  45. private _isAdditive = false;
  46. /**
  47. * Gets or sets the unique id of the node
  48. */
  49. public uniqueId: number;
  50. /**
  51. * This observable will notify when one animation have ended
  52. */
  53. public onAnimationEndObservable = new Observable<TargetedAnimation>();
  54. /**
  55. * Observer raised when one animation loops
  56. */
  57. public onAnimationLoopObservable = new Observable<TargetedAnimation>();
  58. /**
  59. * Observer raised when all animations have looped
  60. */
  61. public onAnimationGroupLoopObservable = new Observable<AnimationGroup>();
  62. /**
  63. * This observable will notify when all animations have ended.
  64. */
  65. public onAnimationGroupEndObservable = new Observable<AnimationGroup>();
  66. /**
  67. * This observable will notify when all animations have paused.
  68. */
  69. public onAnimationGroupPauseObservable = new Observable<AnimationGroup>();
  70. /**
  71. * This observable will notify when all animations are playing.
  72. */
  73. public onAnimationGroupPlayObservable = new Observable<AnimationGroup>();
  74. /**
  75. * Gets the first frame
  76. */
  77. public get from(): number {
  78. return this._from;
  79. }
  80. /**
  81. * Gets the last frame
  82. */
  83. public get to(): number {
  84. return this._to;
  85. }
  86. /**
  87. * Define if the animations are started
  88. */
  89. public get isStarted(): boolean {
  90. return this._isStarted;
  91. }
  92. /**
  93. * Gets a value indicating that the current group is playing
  94. */
  95. public get isPlaying(): boolean {
  96. return this._isStarted && !this._isPaused;
  97. }
  98. /**
  99. * Gets or sets the speed ratio to use for all animations
  100. */
  101. public get speedRatio(): number {
  102. return this._speedRatio;
  103. }
  104. /**
  105. * Gets or sets the speed ratio to use for all animations
  106. */
  107. public set speedRatio(value: number) {
  108. if (this._speedRatio === value) {
  109. return;
  110. }
  111. this._speedRatio = value;
  112. for (var index = 0; index < this._animatables.length; index++) {
  113. let animatable = this._animatables[index];
  114. animatable.speedRatio = this._speedRatio;
  115. }
  116. }
  117. /**
  118. * Gets or sets if all animations should loop or not
  119. */
  120. public get loopAnimation(): boolean {
  121. return this._loopAnimation;
  122. }
  123. public set loopAnimation(value: boolean) {
  124. if (this._loopAnimation === value) {
  125. return;
  126. }
  127. this._loopAnimation = value;
  128. for (var index = 0; index < this._animatables.length; index++) {
  129. let animatable = this._animatables[index];
  130. animatable.loopAnimation = this._loopAnimation;
  131. }
  132. }
  133. /**
  134. * Gets or sets if all animations should be evaluated additively
  135. */
  136. public get isAdditive(): boolean {
  137. return this._isAdditive;
  138. }
  139. public set isAdditive(value: boolean) {
  140. if (this._isAdditive === value) {
  141. return;
  142. }
  143. this._isAdditive = value;
  144. for (var index = 0; index < this._animatables.length; index++) {
  145. let animatable = this._animatables[index];
  146. animatable.isAdditive = this._isAdditive;
  147. }
  148. }
  149. /**
  150. * Gets the targeted animations for this animation group
  151. */
  152. public get targetedAnimations(): Array<TargetedAnimation> {
  153. return this._targetedAnimations;
  154. }
  155. /**
  156. * returning the list of animatables controlled by this animation group.
  157. */
  158. public get animatables(): Array<Animatable> {
  159. return this._animatables;
  160. }
  161. /**
  162. * Instantiates a new Animation Group.
  163. * This helps managing several animations at once.
  164. * @see http://doc.babylonjs.com/how_to/group
  165. * @param name Defines the name of the group
  166. * @param scene Defines the scene the group belongs to
  167. */
  168. public constructor(
  169. /** The name of the animation group */
  170. public name: string,
  171. scene: Nullable<Scene> = null) {
  172. this._scene = scene || EngineStore.LastCreatedScene!;
  173. this.uniqueId = this._scene.getUniqueId();
  174. this._scene.addAnimationGroup(this);
  175. }
  176. /**
  177. * Add an animation (with its target) in the group
  178. * @param animation defines the animation we want to add
  179. * @param target defines the target of the animation
  180. * @returns the TargetedAnimation object
  181. */
  182. public addTargetedAnimation(animation: Animation, target: any): TargetedAnimation {
  183. let targetedAnimation = new TargetedAnimation();
  184. targetedAnimation.animation = animation;
  185. targetedAnimation.target = target;
  186. let keys = animation.getKeys();
  187. if (this._from > keys[0].frame) {
  188. this._from = keys[0].frame;
  189. }
  190. if (this._to < keys[keys.length - 1].frame) {
  191. this._to = keys[keys.length - 1].frame;
  192. }
  193. this._targetedAnimations.push(targetedAnimation);
  194. return targetedAnimation;
  195. }
  196. /**
  197. * This function will normalize every animation in the group to make sure they all go from beginFrame to endFrame
  198. * It can add constant keys at begin or end
  199. * @param beginFrame defines the new begin frame for all animations or the smallest begin frame of all animations if null (defaults to null)
  200. * @param endFrame defines the new end frame for all animations or the largest end frame of all animations if null (defaults to null)
  201. * @returns the animation group
  202. */
  203. public normalize(beginFrame: Nullable<number> = null, endFrame: Nullable<number> = null): AnimationGroup {
  204. if (beginFrame == null) { beginFrame = this._from; }
  205. if (endFrame == null) { endFrame = this._to; }
  206. for (var index = 0; index < this._targetedAnimations.length; index++) {
  207. let targetedAnimation = this._targetedAnimations[index];
  208. let keys = targetedAnimation.animation.getKeys();
  209. let startKey = keys[0];
  210. let endKey = keys[keys.length - 1];
  211. if (startKey.frame > beginFrame) {
  212. let newKey: IAnimationKey = {
  213. frame: beginFrame,
  214. value: startKey.value,
  215. inTangent: startKey.inTangent,
  216. outTangent: startKey.outTangent,
  217. interpolation: startKey.interpolation
  218. };
  219. keys.splice(0, 0, newKey);
  220. }
  221. if (endKey.frame < endFrame) {
  222. let newKey: IAnimationKey = {
  223. frame: endFrame,
  224. value: endKey.value,
  225. inTangent: endKey.outTangent,
  226. outTangent: endKey.outTangent,
  227. interpolation: endKey.interpolation
  228. };
  229. keys.push(newKey);
  230. }
  231. }
  232. this._from = beginFrame;
  233. this._to = endFrame;
  234. return this;
  235. }
  236. private _animationLoopCount: number;
  237. private _animationLoopFlags: boolean[];
  238. private _processLoop(animatable: Animatable, targetedAnimation: TargetedAnimation, index: number) {
  239. animatable.onAnimationLoop = () => {
  240. this.onAnimationLoopObservable.notifyObservers(targetedAnimation);
  241. if (this._animationLoopFlags[index]) {
  242. return;
  243. }
  244. this._animationLoopFlags[index] = true;
  245. this._animationLoopCount++;
  246. if (this._animationLoopCount === this._targetedAnimations.length) {
  247. this.onAnimationGroupLoopObservable.notifyObservers(this);
  248. this._animationLoopCount = 0;
  249. this._animationLoopFlags = [];
  250. }
  251. };
  252. }
  253. /**
  254. * Start all animations on given targets
  255. * @param loop defines if animations must loop
  256. * @param speedRatio defines the ratio to apply to animation speed (1 by default)
  257. * @param from defines the from key (optional)
  258. * @param to defines the to key (optional)
  259. * @param isAdditive defines the additive state for the resulting animatables (optional)
  260. * @returns the current animation group
  261. */
  262. public start(loop = false, speedRatio = 1, from?: number, to?: number, isAdditive?: boolean): AnimationGroup {
  263. if (this._isStarted || this._targetedAnimations.length === 0) {
  264. return this;
  265. }
  266. this._loopAnimation = loop;
  267. this._animationLoopCount = 0;
  268. this._animationLoopFlags = [];
  269. for (var index = 0; index < this._targetedAnimations.length; index++) {
  270. const targetedAnimation = this._targetedAnimations[index];
  271. let animatable = this._scene.beginDirectAnimation(
  272. targetedAnimation.target,
  273. [targetedAnimation.animation],
  274. from !== undefined ? from : this._from,
  275. to !== undefined ? to : this._to,
  276. loop,
  277. speedRatio,
  278. undefined,
  279. undefined,
  280. isAdditive !== undefined ? isAdditive : this._isAdditive
  281. );
  282. animatable.onAnimationEnd = () => {
  283. this.onAnimationEndObservable.notifyObservers(targetedAnimation);
  284. this._checkAnimationGroupEnded(animatable);
  285. };
  286. this._processLoop(animatable, targetedAnimation, index);
  287. this._animatables.push(animatable);
  288. }
  289. this._speedRatio = speedRatio;
  290. if (from !== undefined && to !== undefined) {
  291. if (from < to && this._speedRatio < 0) {
  292. let temp = to;
  293. to = from;
  294. from = temp;
  295. } else if (from > to && this._speedRatio > 0) {
  296. this._speedRatio = -speedRatio;
  297. }
  298. }
  299. this._isStarted = true;
  300. this._isPaused = false;
  301. this.onAnimationGroupPlayObservable.notifyObservers(this);
  302. return this;
  303. }
  304. /**
  305. * Pause all animations
  306. * @returns the animation group
  307. */
  308. public pause(): AnimationGroup {
  309. if (!this._isStarted) {
  310. return this;
  311. }
  312. this._isPaused = true;
  313. for (var index = 0; index < this._animatables.length; index++) {
  314. let animatable = this._animatables[index];
  315. animatable.pause();
  316. }
  317. this.onAnimationGroupPauseObservable.notifyObservers(this);
  318. return this;
  319. }
  320. /**
  321. * Play all animations to initial state
  322. * This function will start() the animations if they were not started or will restart() them if they were paused
  323. * @param loop defines if animations must loop
  324. * @returns the animation group
  325. */
  326. public play(loop?: boolean): AnimationGroup {
  327. // only if all animatables are ready and exist
  328. if (this.isStarted && this._animatables.length === this._targetedAnimations.length) {
  329. if (loop !== undefined) {
  330. this.loopAnimation = loop;
  331. }
  332. this.restart();
  333. } else {
  334. this.stop();
  335. this.start(loop, this._speedRatio);
  336. }
  337. this._isPaused = false;
  338. return this;
  339. }
  340. /**
  341. * Reset all animations to initial state
  342. * @returns the animation group
  343. */
  344. public reset(): AnimationGroup {
  345. if (!this._isStarted) {
  346. return this;
  347. }
  348. for (var index = 0; index < this._animatables.length; index++) {
  349. let animatable = this._animatables[index];
  350. animatable.reset();
  351. }
  352. return this;
  353. }
  354. /**
  355. * Restart animations from key 0
  356. * @returns the animation group
  357. */
  358. public restart(): AnimationGroup {
  359. if (!this._isStarted) {
  360. return this;
  361. }
  362. for (var index = 0; index < this._animatables.length; index++) {
  363. let animatable = this._animatables[index];
  364. animatable.restart();
  365. }
  366. this.onAnimationGroupPlayObservable.notifyObservers(this);
  367. return this;
  368. }
  369. /**
  370. * Stop all animations
  371. * @returns the animation group
  372. */
  373. public stop(): AnimationGroup {
  374. if (!this._isStarted) {
  375. return this;
  376. }
  377. var list = this._animatables.slice();
  378. for (var index = 0; index < list.length; index++) {
  379. list[index].stop();
  380. }
  381. this._isStarted = false;
  382. return this;
  383. }
  384. /**
  385. * Set animation weight for all animatables
  386. * @param weight defines the weight to use
  387. * @return the animationGroup
  388. * @see http://doc.babylonjs.com/babylon101/animations#animation-weights
  389. */
  390. public setWeightForAllAnimatables(weight: number): AnimationGroup {
  391. for (var index = 0; index < this._animatables.length; index++) {
  392. let animatable = this._animatables[index];
  393. animatable.weight = weight;
  394. }
  395. return this;
  396. }
  397. /**
  398. * Synchronize and normalize all animatables with a source animatable
  399. * @param root defines the root animatable to synchronize with
  400. * @return the animationGroup
  401. * @see http://doc.babylonjs.com/babylon101/animations#animation-weights
  402. */
  403. public syncAllAnimationsWith(root: Animatable): AnimationGroup {
  404. for (var index = 0; index < this._animatables.length; index++) {
  405. let animatable = this._animatables[index];
  406. animatable.syncWith(root);
  407. }
  408. return this;
  409. }
  410. /**
  411. * Goes to a specific frame in this animation group
  412. * @param frame the frame number to go to
  413. * @return the animationGroup
  414. */
  415. public goToFrame(frame: number): AnimationGroup {
  416. if (!this._isStarted) {
  417. return this;
  418. }
  419. for (var index = 0; index < this._animatables.length; index++) {
  420. let animatable = this._animatables[index];
  421. animatable.goToFrame(frame);
  422. }
  423. return this;
  424. }
  425. /**
  426. * Dispose all associated resources
  427. */
  428. public dispose(): void {
  429. this._targetedAnimations = [];
  430. this._animatables = [];
  431. var index = this._scene.animationGroups.indexOf(this);
  432. if (index > -1) {
  433. this._scene.animationGroups.splice(index, 1);
  434. }
  435. this.onAnimationEndObservable.clear();
  436. this.onAnimationGroupEndObservable.clear();
  437. this.onAnimationGroupPauseObservable.clear();
  438. this.onAnimationGroupPlayObservable.clear();
  439. this.onAnimationLoopObservable.clear();
  440. this.onAnimationGroupLoopObservable.clear();
  441. }
  442. private _checkAnimationGroupEnded(animatable: Animatable) {
  443. // animatable should be taken out of the array
  444. let idx = this._animatables.indexOf(animatable);
  445. if (idx > -1) {
  446. this._animatables.splice(idx, 1);
  447. }
  448. // all animatables were removed? animation group ended!
  449. if (this._animatables.length === 0) {
  450. this._isStarted = false;
  451. this.onAnimationGroupEndObservable.notifyObservers(this);
  452. }
  453. }
  454. /**
  455. * Clone the current animation group and returns a copy
  456. * @param newName defines the name of the new group
  457. * @param targetConverter defines an optional function used to convert current animation targets to new ones
  458. * @returns the new aniamtion group
  459. */
  460. public clone(newName: string, targetConverter?: (oldTarget: any) => any): AnimationGroup {
  461. let newGroup = new AnimationGroup(newName || this.name, this._scene);
  462. for (var targetAnimation of this._targetedAnimations) {
  463. newGroup.addTargetedAnimation(targetAnimation.animation.clone(), targetConverter ? targetConverter(targetAnimation.target) : targetAnimation.target);
  464. }
  465. return newGroup;
  466. }
  467. /**
  468. * Serializes the animationGroup to an object
  469. * @returns Serialized object
  470. */
  471. public serialize(): any {
  472. var serializationObject: any = {};
  473. serializationObject.name = this.name;
  474. serializationObject.from = this.from;
  475. serializationObject.to = this.to;
  476. serializationObject.targetedAnimations = [];
  477. for (var targetedAnimationIndex = 0; targetedAnimationIndex < this.targetedAnimations.length; targetedAnimationIndex++) {
  478. var targetedAnimation = this.targetedAnimations[targetedAnimationIndex];
  479. serializationObject.targetedAnimations[targetedAnimationIndex] = targetedAnimation.serialize();
  480. }
  481. return serializationObject;
  482. }
  483. // Statics
  484. /**
  485. * Returns a new AnimationGroup object parsed from the source provided.
  486. * @param parsedAnimationGroup defines the source
  487. * @param scene defines the scene that will receive the animationGroup
  488. * @returns a new AnimationGroup
  489. */
  490. public static Parse(parsedAnimationGroup: any, scene: Scene): AnimationGroup {
  491. var animationGroup = new AnimationGroup(parsedAnimationGroup.name, scene);
  492. for (var i = 0; i < parsedAnimationGroup.targetedAnimations.length; i++) {
  493. var targetedAnimation = parsedAnimationGroup.targetedAnimations[i];
  494. var animation = Animation.Parse(targetedAnimation.animation);
  495. var id = targetedAnimation.targetId;
  496. if (targetedAnimation.animation.property === "influence") { // morph target animation
  497. let morphTarget = scene.getMorphTargetById(id);
  498. if (morphTarget) {
  499. animationGroup.addTargetedAnimation(animation, morphTarget);
  500. }
  501. }
  502. else {
  503. var targetNode = scene.getNodeByID(id);
  504. if (targetNode != null) {
  505. animationGroup.addTargetedAnimation(animation, targetNode);
  506. }
  507. }
  508. }
  509. if (parsedAnimationGroup.from !== null && parsedAnimationGroup.to !== null) {
  510. animationGroup.normalize(parsedAnimationGroup.from, parsedAnimationGroup.to);
  511. }
  512. return animationGroup;
  513. }
  514. /**
  515. * Convert the keyframes for all animations belonging to the group to be relative to a given reference frame.
  516. * @param sourceAnimationGroup defines the AnimationGroup containing animations to convert
  517. * @param referenceFrame defines the frame that keyframes in the range will be relative to
  518. * @param range defines the name of the AnimationRange belonging to the animations in the group to convert
  519. * @param cloneOriginal defines whether or not to clone the group and convert the clone or convert the original group (default is false)
  520. * @param clonedName defines the name of the resulting cloned AnimationGroup if cloneOriginal is true
  521. * @returns a new AnimationGroup if cloneOriginal is true or the original AnimationGroup if cloneOriginal is false
  522. */
  523. public static MakeAnimationAdditive(sourceAnimationGroup: AnimationGroup, referenceFrame = 0, range?: string, cloneOriginal = false, clonedName?: string): AnimationGroup {
  524. let animationGroup = sourceAnimationGroup;
  525. if (cloneOriginal) {
  526. animationGroup = sourceAnimationGroup.clone(clonedName || animationGroup.name);
  527. }
  528. let targetedAnimations = animationGroup.targetedAnimations;
  529. for (var index = 0; index < targetedAnimations.length; index++) {
  530. let targetedAnimation = targetedAnimations[index];
  531. Animation.MakeAnimationAdditive(targetedAnimation.animation, referenceFrame, range);
  532. }
  533. animationGroup.isAdditive = true;
  534. return animationGroup;
  535. }
  536. /**
  537. * Returns the string "AnimationGroup"
  538. * @returns "AnimationGroup"
  539. */
  540. public getClassName(): string {
  541. return "AnimationGroup";
  542. }
  543. /**
  544. * Creates a detailled string about the object
  545. * @param fullDetails defines if the output string will support multiple levels of logging within scene loading
  546. * @returns a string representing the object
  547. */
  548. public toString(fullDetails?: boolean): string {
  549. var ret = "Name: " + this.name;
  550. ret += ", type: " + this.getClassName();
  551. if (fullDetails) {
  552. ret += ", from: " + this._from;
  553. ret += ", to: " + this._to;
  554. ret += ", isStarted: " + this._isStarted;
  555. ret += ", speedRatio: " + this._speedRatio;
  556. ret += ", targetedAnimations length: " + this._targetedAnimations.length;
  557. ret += ", animatables length: " + this._animatables;
  558. }
  559. return ret;
  560. }
  561. }