animatable.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import { Animation } from "./animation";
  2. import { RuntimeAnimation } from "./runtimeAnimation";
  3. import { Nullable } from "../types";
  4. import { Observable } from "../Misc/observable";
  5. import { Scene } from "../scene";
  6. /**
  7. * Class used to store an actual running animation
  8. */
  9. export class Animatable {
  10. private _localDelayOffset: Nullable<number> = null;
  11. private _pausedDelay: Nullable<number> = null;
  12. private _runtimeAnimations = new Array<RuntimeAnimation>();
  13. private _paused = false;
  14. private _scene: Scene;
  15. private _speedRatio = 1;
  16. private _weight = -1.0;
  17. private _syncRoot: Animatable;
  18. /**
  19. * Gets or sets a boolean indicating if the animatable must be disposed and removed at the end of the animation.
  20. * This will only apply for non looping animation (default is true)
  21. */
  22. public disposeOnEnd = true;
  23. /**
  24. * Gets a boolean indicating if the animation has started
  25. */
  26. public animationStarted = false;
  27. /**
  28. * Observer raised when the animation ends
  29. */
  30. public onAnimationEndObservable = new Observable<Animatable>();
  31. /**
  32. * Observer raised when the animation loops
  33. */
  34. public onAnimationLoopObservable = new Observable<Animatable>();
  35. /**
  36. * Gets the root Animatable used to synchronize and normalize animations
  37. */
  38. public get syncRoot(): Animatable {
  39. return this._syncRoot;
  40. }
  41. /**
  42. * Gets the current frame of the first RuntimeAnimation
  43. * Used to synchronize Animatables
  44. */
  45. public get masterFrame(): number {
  46. if (this._runtimeAnimations.length === 0) {
  47. return 0;
  48. }
  49. return this._runtimeAnimations[0].currentFrame;
  50. }
  51. /**
  52. * Gets or sets the animatable weight (-1.0 by default meaning not weighted)
  53. */
  54. public get weight(): number {
  55. return this._weight;
  56. }
  57. public set weight(value: number) {
  58. if (value === -1) { // -1 is ok and means no weight
  59. this._weight = -1;
  60. return;
  61. }
  62. // Else weight must be in [0, 1] range
  63. this._weight = Math.min(Math.max(value, 0), 1.0);
  64. }
  65. /**
  66. * Gets or sets the speed ratio to apply to the animatable (1.0 by default)
  67. */
  68. public get speedRatio(): number {
  69. return this._speedRatio;
  70. }
  71. public set speedRatio(value: number) {
  72. for (var index = 0; index < this._runtimeAnimations.length; index++) {
  73. var animation = this._runtimeAnimations[index];
  74. animation._prepareForSpeedRatioChange(value);
  75. }
  76. this._speedRatio = value;
  77. }
  78. /**
  79. * Creates a new Animatable
  80. * @param scene defines the hosting scene
  81. * @param target defines the target object
  82. * @param fromFrame defines the starting frame number (default is 0)
  83. * @param toFrame defines the ending frame number (default is 100)
  84. * @param loopAnimation defines if the animation must loop (default is false)
  85. * @param speedRatio defines the factor to apply to animation speed (default is 1)
  86. * @param onAnimationEnd defines a callback to call when animation ends if it is not looping
  87. * @param animations defines a group of animation to add to the new Animatable
  88. * @param onAnimationLoop defines a callback to call when animation loops
  89. */
  90. constructor(scene: Scene,
  91. /** defines the target object */
  92. public target: any,
  93. /** defines the starting frame number (default is 0) */
  94. public fromFrame: number = 0,
  95. /** defines the ending frame number (default is 100) */
  96. public toFrame: number = 100,
  97. /** defines if the animation must loop (default is false) */
  98. public loopAnimation: boolean = false,
  99. speedRatio: number = 1.0,
  100. /** defines a callback to call when animation ends if it is not looping */
  101. public onAnimationEnd?: Nullable<() => void>,
  102. animations?: Animation[],
  103. /** defines a callback to call when animation loops */
  104. public onAnimationLoop?: Nullable<() => void>) {
  105. this._scene = scene;
  106. if (animations) {
  107. this.appendAnimations(target, animations);
  108. }
  109. this._speedRatio = speedRatio;
  110. scene._activeAnimatables.push(this);
  111. }
  112. // Methods
  113. /**
  114. * Synchronize and normalize current Animatable with a source Animatable
  115. * This is useful when using animation weights and when animations are not of the same length
  116. * @param root defines the root Animatable to synchronize with
  117. * @returns the current Animatable
  118. */
  119. public syncWith(root: Animatable): Animatable {
  120. this._syncRoot = root;
  121. if (root) {
  122. // Make sure this animatable will animate after the root
  123. let index = this._scene._activeAnimatables.indexOf(this);
  124. if (index > -1) {
  125. this._scene._activeAnimatables.splice(index, 1);
  126. this._scene._activeAnimatables.push(this);
  127. }
  128. }
  129. return this;
  130. }
  131. /**
  132. * Gets the list of runtime animations
  133. * @returns an array of RuntimeAnimation
  134. */
  135. public getAnimations(): RuntimeAnimation[] {
  136. return this._runtimeAnimations;
  137. }
  138. /**
  139. * Adds more animations to the current animatable
  140. * @param target defines the target of the animations
  141. * @param animations defines the new animations to add
  142. */
  143. public appendAnimations(target: any, animations: Animation[]): void {
  144. for (var index = 0; index < animations.length; index++) {
  145. var animation = animations[index];
  146. this._runtimeAnimations.push(new RuntimeAnimation(target, animation, this._scene, this));
  147. }
  148. }
  149. /**
  150. * Gets the source animation for a specific property
  151. * @param property defines the propertyu to look for
  152. * @returns null or the source animation for the given property
  153. */
  154. public getAnimationByTargetProperty(property: string): Nullable<Animation> {
  155. var runtimeAnimations = this._runtimeAnimations;
  156. for (var index = 0; index < runtimeAnimations.length; index++) {
  157. if (runtimeAnimations[index].animation.targetProperty === property) {
  158. return runtimeAnimations[index].animation;
  159. }
  160. }
  161. return null;
  162. }
  163. /**
  164. * Gets the runtime animation for a specific property
  165. * @param property defines the propertyu to look for
  166. * @returns null or the runtime animation for the given property
  167. */
  168. public getRuntimeAnimationByTargetProperty(property: string): Nullable<RuntimeAnimation> {
  169. var runtimeAnimations = this._runtimeAnimations;
  170. for (var index = 0; index < runtimeAnimations.length; index++) {
  171. if (runtimeAnimations[index].animation.targetProperty === property) {
  172. return runtimeAnimations[index];
  173. }
  174. }
  175. return null;
  176. }
  177. /**
  178. * Resets the animatable to its original state
  179. */
  180. public reset(): void {
  181. var runtimeAnimations = this._runtimeAnimations;
  182. for (var index = 0; index < runtimeAnimations.length; index++) {
  183. runtimeAnimations[index].reset(true);
  184. }
  185. this._localDelayOffset = null;
  186. this._pausedDelay = null;
  187. }
  188. /**
  189. * Allows the animatable to blend with current running animations
  190. * @see http://doc.babylonjs.com/babylon101/animations#animation-blending
  191. * @param blendingSpeed defines the blending speed to use
  192. */
  193. public enableBlending(blendingSpeed: number): void {
  194. var runtimeAnimations = this._runtimeAnimations;
  195. for (var index = 0; index < runtimeAnimations.length; index++) {
  196. runtimeAnimations[index].animation.enableBlending = true;
  197. runtimeAnimations[index].animation.blendingSpeed = blendingSpeed;
  198. }
  199. }
  200. /**
  201. * Disable animation blending
  202. * @see http://doc.babylonjs.com/babylon101/animations#animation-blending
  203. */
  204. public disableBlending(): void {
  205. var runtimeAnimations = this._runtimeAnimations;
  206. for (var index = 0; index < runtimeAnimations.length; index++) {
  207. runtimeAnimations[index].animation.enableBlending = false;
  208. }
  209. }
  210. /**
  211. * Jump directly to a given frame
  212. * @param frame defines the frame to jump to
  213. */
  214. public goToFrame(frame: number): void {
  215. var runtimeAnimations = this._runtimeAnimations;
  216. if (runtimeAnimations[0]) {
  217. var fps = runtimeAnimations[0].animation.framePerSecond;
  218. var currentFrame = runtimeAnimations[0].currentFrame;
  219. var adjustTime = frame - currentFrame;
  220. var delay = adjustTime * 1000 / (fps * this.speedRatio);
  221. if (this._localDelayOffset === null) {
  222. this._localDelayOffset = 0;
  223. }
  224. this._localDelayOffset -= delay;
  225. }
  226. for (var index = 0; index < runtimeAnimations.length; index++) {
  227. runtimeAnimations[index].goToFrame(frame);
  228. }
  229. }
  230. /**
  231. * Pause the animation
  232. */
  233. public pause(): void {
  234. if (this._paused) {
  235. return;
  236. }
  237. this._paused = true;
  238. }
  239. /**
  240. * Restart the animation
  241. */
  242. public restart(): void {
  243. this._paused = false;
  244. }
  245. private _raiseOnAnimationEnd() {
  246. if (this.onAnimationEnd) {
  247. this.onAnimationEnd();
  248. }
  249. this.onAnimationEndObservable.notifyObservers(this);
  250. }
  251. /**
  252. * Stop and delete the current animation
  253. * @param animationName defines a string used to only stop some of the runtime animations instead of all
  254. * @param targetMask - a function that determines if the animation should be stopped based on its target (all animations will be stopped if both this and animationName are empty)
  255. */
  256. public stop(animationName?: string, targetMask?: (target: any) => boolean): void {
  257. if (animationName || targetMask) {
  258. var idx = this._scene._activeAnimatables.indexOf(this);
  259. if (idx > -1) {
  260. var runtimeAnimations = this._runtimeAnimations;
  261. for (var index = runtimeAnimations.length - 1; index >= 0; index--) {
  262. const runtimeAnimation = runtimeAnimations[index];
  263. if (animationName && runtimeAnimation.animation.name != animationName) {
  264. continue;
  265. }
  266. if (targetMask && !targetMask(runtimeAnimation.target)) {
  267. continue;
  268. }
  269. runtimeAnimation.dispose();
  270. runtimeAnimations.splice(index, 1);
  271. }
  272. if (runtimeAnimations.length == 0) {
  273. this._scene._activeAnimatables.splice(idx, 1);
  274. this._raiseOnAnimationEnd();
  275. }
  276. }
  277. } else {
  278. var index = this._scene._activeAnimatables.indexOf(this);
  279. if (index > -1) {
  280. this._scene._activeAnimatables.splice(index, 1);
  281. var runtimeAnimations = this._runtimeAnimations;
  282. for (var index = 0; index < runtimeAnimations.length; index++) {
  283. runtimeAnimations[index].dispose();
  284. }
  285. this._raiseOnAnimationEnd();
  286. }
  287. }
  288. }
  289. /**
  290. * Wait asynchronously for the animation to end
  291. * @returns a promise which will be fullfilled when the animation ends
  292. */
  293. public waitAsync(): Promise<Animatable> {
  294. return new Promise((resolve, reject) => {
  295. this.onAnimationEndObservable.add(() => {
  296. resolve(this);
  297. }, undefined, undefined, this, true);
  298. });
  299. }
  300. /** @hidden */
  301. public _animate(delay: number): boolean {
  302. if (this._paused) {
  303. this.animationStarted = false;
  304. if (this._pausedDelay === null) {
  305. this._pausedDelay = delay;
  306. }
  307. return true;
  308. }
  309. if (this._localDelayOffset === null) {
  310. this._localDelayOffset = delay;
  311. this._pausedDelay = null;
  312. } else if (this._pausedDelay !== null) {
  313. this._localDelayOffset += delay - this._pausedDelay;
  314. this._pausedDelay = null;
  315. }
  316. if (this._weight === 0) { // We consider that an animation with a weight === 0 is "actively" paused
  317. return true;
  318. }
  319. // Animating
  320. var running = false;
  321. var runtimeAnimations = this._runtimeAnimations;
  322. var index: number;
  323. for (index = 0; index < runtimeAnimations.length; index++) {
  324. var animation = runtimeAnimations[index];
  325. var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame,
  326. this.toFrame, this.loopAnimation, this._speedRatio, this._weight,
  327. () => {
  328. this.onAnimationLoopObservable.notifyObservers(this);
  329. if (this.onAnimationLoop) {
  330. this.onAnimationLoop();
  331. }
  332. }
  333. );
  334. running = running || isRunning;
  335. }
  336. this.animationStarted = running;
  337. if (!running) {
  338. if (this.disposeOnEnd) {
  339. // Remove from active animatables
  340. index = this._scene._activeAnimatables.indexOf(this);
  341. this._scene._activeAnimatables.splice(index, 1);
  342. // Dispose all runtime animations
  343. for (index = 0; index < runtimeAnimations.length; index++) {
  344. runtimeAnimations[index].dispose();
  345. }
  346. }
  347. this._raiseOnAnimationEnd();
  348. if (this.disposeOnEnd) {
  349. this.onAnimationEnd = null;
  350. this.onAnimationLoop = null;
  351. this.onAnimationLoopObservable.clear();
  352. this.onAnimationEndObservable.clear();
  353. }
  354. }
  355. return running;
  356. }
  357. }