bouncingBehavior.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { Behavior } from "../../Behaviors/behavior";
  2. import { Camera } from "../../Cameras/camera";
  3. import { ArcRotateCamera } from "../../Cameras/arcRotateCamera";
  4. import { BackEase, EasingFunction } from "../../Animations/easing";
  5. import { Nullable } from "../../types";
  6. import { Observer } from "../../Misc/observable";
  7. import { AbstractMesh } from "../../Meshes/abstractMesh";
  8. import { Animatable } from "../../Animations/animatable";
  9. import { Animation } from "../../Animations/animation";
  10. /**
  11. * Add a bouncing effect to an ArcRotateCamera when reaching a specified minimum and maximum radius
  12. * @see https://doc.babylonjs.com/how_to/camera_behaviors#bouncing-behavior
  13. */
  14. export class BouncingBehavior implements Behavior<ArcRotateCamera> {
  15. /**
  16. * Gets the name of the behavior.
  17. */
  18. public get name(): string {
  19. return "Bouncing";
  20. }
  21. /**
  22. * The easing function used by animations
  23. */
  24. public static EasingFunction = new BackEase(0.3);
  25. /**
  26. * The easing mode used by animations
  27. */
  28. public static EasingMode = EasingFunction.EASINGMODE_EASEOUT;
  29. /**
  30. * The duration of the animation, in milliseconds
  31. */
  32. public transitionDuration = 450;
  33. /**
  34. * Length of the distance animated by the transition when lower radius is reached
  35. */
  36. public lowerRadiusTransitionRange = 2;
  37. /**
  38. * Length of the distance animated by the transition when upper radius is reached
  39. */
  40. public upperRadiusTransitionRange = -2;
  41. private _autoTransitionRange = false;
  42. /**
  43. * Gets a value indicating if the lowerRadiusTransitionRange and upperRadiusTransitionRange are defined automatically
  44. */
  45. public get autoTransitionRange(): boolean {
  46. return this._autoTransitionRange;
  47. }
  48. /**
  49. * Sets a value indicating if the lowerRadiusTransitionRange and upperRadiusTransitionRange are defined automatically
  50. * Transition ranges will be set to 5% of the bounding box diagonal in world space
  51. */
  52. public set autoTransitionRange(value: boolean) {
  53. if (this._autoTransitionRange === value) {
  54. return;
  55. }
  56. this._autoTransitionRange = value;
  57. let camera = this._attachedCamera;
  58. if (!camera) {
  59. return;
  60. }
  61. if (value) {
  62. this._onMeshTargetChangedObserver = camera.onMeshTargetChangedObservable.add((mesh) => {
  63. if (!mesh) {
  64. return;
  65. }
  66. mesh.computeWorldMatrix(true);
  67. let diagonal = mesh.getBoundingInfo().diagonalLength;
  68. this.lowerRadiusTransitionRange = diagonal * 0.05;
  69. this.upperRadiusTransitionRange = diagonal * 0.05;
  70. });
  71. } else if (this._onMeshTargetChangedObserver) {
  72. camera.onMeshTargetChangedObservable.remove(this._onMeshTargetChangedObserver);
  73. }
  74. }
  75. // Connection
  76. private _attachedCamera: Nullable<ArcRotateCamera>;
  77. private _onAfterCheckInputsObserver: Nullable<Observer<Camera>>;
  78. private _onMeshTargetChangedObserver: Nullable<Observer<Nullable<AbstractMesh>>>;
  79. /**
  80. * Initializes the behavior.
  81. */
  82. public init(): void {
  83. // Do nothing
  84. }
  85. /**
  86. * Attaches the behavior to its arc rotate camera.
  87. * @param camera Defines the camera to attach the behavior to
  88. */
  89. public attach(camera: ArcRotateCamera): void {
  90. this._attachedCamera = camera;
  91. this._onAfterCheckInputsObserver = camera.onAfterCheckInputsObservable.add(() => {
  92. if (!this._attachedCamera) {
  93. return;
  94. }
  95. // Add the bounce animation to the lower radius limit
  96. if (this._isRadiusAtLimit(this._attachedCamera.lowerRadiusLimit)) {
  97. this._applyBoundRadiusAnimation(this.lowerRadiusTransitionRange);
  98. }
  99. // Add the bounce animation to the upper radius limit
  100. if (this._isRadiusAtLimit(this._attachedCamera.upperRadiusLimit)) {
  101. this._applyBoundRadiusAnimation(this.upperRadiusTransitionRange);
  102. }
  103. });
  104. }
  105. /**
  106. * Detaches the behavior from its current arc rotate camera.
  107. */
  108. public detach(): void {
  109. if (!this._attachedCamera) {
  110. return;
  111. }
  112. if (this._onAfterCheckInputsObserver) {
  113. this._attachedCamera.onAfterCheckInputsObservable.remove(this._onAfterCheckInputsObserver);
  114. }
  115. if (this._onMeshTargetChangedObserver) {
  116. this._attachedCamera.onMeshTargetChangedObservable.remove(this._onMeshTargetChangedObserver);
  117. }
  118. this._attachedCamera = null;
  119. }
  120. // Animations
  121. private _radiusIsAnimating: boolean = false;
  122. private _radiusBounceTransition: Nullable<Animation> = null;
  123. private _animatables = new Array<Animatable>();
  124. private _cachedWheelPrecision: number;
  125. /**
  126. * Checks if the camera radius is at the specified limit. Takes into account animation locks.
  127. * @param radiusLimit The limit to check against.
  128. * @return Bool to indicate if at limit.
  129. */
  130. private _isRadiusAtLimit(radiusLimit: Nullable<number>): boolean {
  131. if (!this._attachedCamera) {
  132. return false;
  133. }
  134. if (this._attachedCamera.radius === radiusLimit && !this._radiusIsAnimating) {
  135. return true;
  136. }
  137. return false;
  138. }
  139. /**
  140. * Applies an animation to the radius of the camera, extending by the radiusDelta.
  141. * @param radiusDelta The delta by which to animate to. Can be negative.
  142. */
  143. private _applyBoundRadiusAnimation(radiusDelta: number): void {
  144. if (!this._attachedCamera) {
  145. return;
  146. }
  147. if (!this._radiusBounceTransition) {
  148. BouncingBehavior.EasingFunction.setEasingMode(BouncingBehavior.EasingMode);
  149. this._radiusBounceTransition = Animation.CreateAnimation("radius", Animation.ANIMATIONTYPE_FLOAT, 60, BouncingBehavior.EasingFunction);
  150. }
  151. // Prevent zoom until bounce has completed
  152. this._cachedWheelPrecision = this._attachedCamera.wheelPrecision;
  153. this._attachedCamera.wheelPrecision = Infinity;
  154. this._attachedCamera.inertialRadiusOffset = 0;
  155. // Animate to the radius limit
  156. this.stopAllAnimations();
  157. this._radiusIsAnimating = true;
  158. let animatable = Animation.TransitionTo("radius", this._attachedCamera.radius + radiusDelta, this._attachedCamera, this._attachedCamera.getScene(), 60,
  159. this._radiusBounceTransition, this.transitionDuration, () => this._clearAnimationLocks());
  160. if (animatable) {
  161. this._animatables.push(animatable);
  162. }
  163. }
  164. /**
  165. * Removes all animation locks. Allows new animations to be added to any of the camera properties.
  166. */
  167. protected _clearAnimationLocks(): void {
  168. this._radiusIsAnimating = false;
  169. if (this._attachedCamera) {
  170. this._attachedCamera.wheelPrecision = this._cachedWheelPrecision;
  171. }
  172. }
  173. /**
  174. * Stops and removes all animations that have been applied to the camera
  175. */
  176. public stopAllAnimations(): void {
  177. if (this._attachedCamera) {
  178. this._attachedCamera.animations = [];
  179. }
  180. while (this._animatables.length) {
  181. this._animatables[0].onAnimationEnd = null;
  182. this._animatables[0].stop();
  183. this._animatables.shift();
  184. }
  185. }
  186. }