File

src/managers/three-manager/animations-manager.ts

Description

Manager for managing animation related operations using three.js and tween.js.

Index

Methods

Constructor

constructor(scene: Scene, activeCamera: Camera, rendererManager: RendererManager)

Constructor for the animation manager.

Parameters :
Name Type Optional Description
scene Scene No

Three.js scene containing all the objects and event data.

activeCamera Camera No

Currently active camera.

rendererManager RendererManager No

Manager for managing event display's renderer related functions.

Methods

Public animateClippingWithCollision
animateClippingWithCollision(tweenDuration: number, onEnd?: () => void)

Animate the propagation and generation of event data using clipping planes after particle collison.

Parameters :
Name Type Optional Description
tweenDuration number No

Duration of the animation tween.

onEnd function Yes

Function to call when all animations have ended.

Returns : void
Public animateEvent
animateEvent(tweenDuration: number, onEnd?: () => void, onAnimationStart?: () => void)

Animate the propagation and generation of event data.

Parameters :
Name Type Optional Description
tweenDuration number No

Duration of the animation tween.

onEnd function Yes

Callback when all animations have ended.

onAnimationStart function Yes

Callback when the first animation starts.

Returns : void
Public animateEventWithClipping
animateEventWithClipping(tweenDuration: number, onEnd?: () => void, onAnimationStart?: () => void, clippingConstant: number)

Animate the propagation and generation of event data using clipping planes.

Parameters :
Name Type Optional Default value Description
tweenDuration number No

Duration of the animation tween.

onEnd function Yes

Function to call when all animations have ended.

onAnimationStart function Yes

Callback when the first animation starts.

clippingConstant number No 11000

Constant for the clipping planes for distance from the origin.

Returns : void
Public animateEventWithCollision
animateEventWithCollision(tweenDuration: number, onEnd?: () => void)

Animate the propagation and generation of event data with particle collison.

Parameters :
Name Type Optional Description
tweenDuration number No

Duration of the animation tween.

onEnd function Yes

Function to call when all animations have ended.

Returns : void
Public animatePreset
animatePreset(animationPreset: AnimationPreset, onEnd?: () => void)

Animate scene by animating camera through the scene and animating event collision. event collision animation options.

Parameters :
Name Type Optional Description
animationPreset AnimationPreset No

Preset for animation including positions to go through and event collision animation options.

onEnd function Yes

Function to call when the animation ends.

Returns : void
Public animateThroughEvent
animateThroughEvent(startPos: number[], tweenDuration: number, onAnimationEnd?: () => void)

Animate the camera through the event scene.

Parameters :
Name Type Optional Description
startPos number[] No

Start position of the translation animation.

tweenDuration number No

Duration of each tween in the translation animation.

onAnimationEnd function Yes

Callback when the last animation ends.

Returns : void
Public animateWithCollision
animateWithCollision(animationFunction: (tweenDuration: number,onEnd: function,onAnimationStart: function) => void, tweenDuration: number, onEnd?: () => void)

Animate the propagation and generation of event data with particle collison.

Parameters :
Name Type Optional Description
animationFunction function No

Animation function to call after collision.

tweenDuration number No

Duration of the animation tween.

onEnd function Yes

Function to call when all animations have ended.

Returns : void
Public collideParticles
collideParticles(tweenDuration: number, particleSize: number, distanceFromOrigin: number, particleColor: Color, onEnd?: () => void)

Animate the collision of two particles.

Parameters :
Name Type Optional Default value Description
tweenDuration number No

Duration of the particle collision animation tween.

particleSize number No 10

Size of the particles.

distanceFromOrigin number No 5000

Distance of the particles (along z-axes) from the origin.

particleColor Color No new Color(0xffffff)

Color of the particles.

onEnd function Yes

Callback to call when the particle collision ends.

Returns : void
Public getCameraTween
getCameraTween(pos: number[], duration: number, easing?)

Get the camera tween for animating camera to a position.

Parameters :
Name Type Optional Default value Description
pos number[] No

End position of the camera tween.

duration number No 1000

Duration of the tween.

easing Yes

Animation easing of the tween if any.

Returns : any

Tween object of the camera animation.

Private getHitsPositions
getHitsPositions(positions: number[])

Get the positions of hits in a multidimensional array from a single dimensional array.

Parameters :
Name Type Optional Description
positions number[] No

Positions of hits in a single dimensional array.

Returns : [][]

Positions of hits in a multidimensional array.

import { Easing, Tween } from '@tweenjs/tween.js';
import {
  TubeGeometry,
  BufferGeometry,
  Vector3,
  Color,
  MeshBasicMaterial,
  Mesh,
  SphereGeometry,
  Sphere,
  Object3D,
  BufferAttribute,
  Scene,
  Camera,
  Plane,
  Group,
} from 'three';
import { SceneManager } from './scene-manager';
import { RendererManager } from './renderer-manager';
import { TracksMesh } from '../../loaders/objects/tracks';

/** Type for animation preset. */
export interface AnimationPreset {
  /** Positions with duration and easing of each tween forming a path. */
  positions: { position: number[]; duration: number; easing?: any }[];
  /** Time after which to start the event collision animation. */
  animateEventAfterInterval?: number;
  /** Duration of the event collision. */
  collisionDuration?: number;
  /** Name of the Animation */
  name: string;
}

/**
 * Manager for managing animation related operations using three.js and tween.js.
 */
export class AnimationsManager {
  /**
   * Constructor for the animation manager.
   * @param scene Three.js scene containing all the objects and event data.
   * @param activeCamera Currently active camera.
   * @param rendererManager Manager for managing event display's renderer related functions.
   */
  constructor(
    private scene: Scene,
    private activeCamera: Camera,
    private rendererManager: RendererManager,
  ) {
    this.animateEvent = this.animateEvent.bind(this);
    this.animateEventWithClipping = this.animateEventWithClipping.bind(this);
  }

  /**
   * Get the camera tween for animating camera to a position.
   * @param pos End position of the camera tween.
   * @param duration Duration of the tween.
   * @param easing Animation easing of the tween if any.
   * @returns Tween object of the camera animation.
   */
  public getCameraTween(
    pos: number[],
    duration: number = 1000,
    easing?: typeof Easing.Linear.None,
  ) {
    const tween = new Tween(this.activeCamera.position).to(
      { x: pos[0], y: pos[1], z: pos[2] },
      duration,
    );

    if (easing) {
      tween.easing(easing);
    }

    return tween;
  }

  /**
   * Animate the camera through the event scene.
   * @param startPos Start position of the translation animation.
   * @param tweenDuration Duration of each tween in the translation animation.
   * @param onAnimationEnd Callback when the last animation ends.
   */
  public animateThroughEvent(
    startPos: number[],
    tweenDuration: number,
    onAnimationEnd?: () => void,
  ) {
    // Move to start
    const start = this.getCameraTween(startPos, 1000, Easing.Cubic.Out);
    // Move to position along the detector axis
    const alongAxisPosition = [0, 0, startPos[2]];
    const startXAxis = this.getCameraTween(alongAxisPosition, tweenDuration);

    const radius = 500;
    const numOfSteps = 24;
    const angle = 3 * Math.PI;
    const step = angle / numOfSteps;

    const rotationPositions = [];
    for (let i = 1; i <= numOfSteps; i++) {
      rotationPositions.push([
        radius * Math.sin(step * i), // x
        0, // y
        radius * Math.cos(step * i), // z
      ]);
    }

    // Go to origin
    const rotateStart = this.getCameraTween(
      [0, 0, radius],
      tweenDuration,
      Easing.Cubic.Out,
    );

    let rotate = rotateStart;
    const rotationTime = tweenDuration * 4;
    const singleRotationTime = rotationTime / numOfSteps;
    // Rotating around the event
    for (const pos of rotationPositions) {
      const animation = this.getCameraTween(pos, singleRotationTime);
      rotate.chain(animation);
      rotate = animation;
    }

    // Go to the end position and then back to the starting point
    const endPos = [0, 0, -startPos[2]];
    const end = this.getCameraTween(endPos, tweenDuration, Easing.Cubic.In);
    const startClone = this.getCameraTween(
      startPos,
      tweenDuration,
      Easing.Cubic.Out,
    );
    startClone.onComplete(() => onAnimationEnd?.());
    startClone.delay(500);

    start.chain(startXAxis);
    startXAxis.chain(rotateStart);
    rotate.chain(end);
    end.chain(startClone);

    start.start();
  }

  /**
   * Animate the propagation and generation of event data.
   * @param tweenDuration Duration of the animation tween.
   * @param onEnd Callback when all animations have ended.
   * @param onAnimationStart Callback when the first animation starts.
   */
  public animateEvent(
    tweenDuration: number,
    onEnd?: () => void,
    onAnimationStart?: () => void,
  ) {
    const extraAnimationSphereDuration = tweenDuration * 0.25;
    tweenDuration *= 0.75;

    const eventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);

    const animationSphere = new Sphere(new Vector3(), 0);
    const objectsToAnimateWithSphere: {
      eventObject: Object3D;
      position: any;
    }[] = [];

    const allTweens = [];
    // Traverse over all event data
    eventData.traverse((eventObject: any) => {
      if (eventObject.geometry) {
        // Animation for extrapolating tracks without changing scale
        if (eventObject.name === 'Track' || eventObject.name === 'LineHit') {
          // Check if geometry drawRange count exists
          let geometryPosCount =
            eventObject.geometry?.attributes?.position?.count;
          if (geometryPosCount) {
            // WORKAROUND
            // Changing position count for TubeGeometry because
            // what we get is not the actual and it has Infinity drawRange count
            if (eventObject.geometry instanceof TubeGeometry) {
              geometryPosCount *= 6;
            }

            if (eventObject.geometry instanceof TracksMesh) {
              eventObject.material.progress = 0;
              const eventObjectTween = new Tween(eventObject.material).to(
                {
                  progress: 1,
                },
                tweenDuration,
              );
              eventObjectTween.onComplete(() => {
                eventObject.material.progress = 1;
              });
              allTweens.push(eventObjectTween);
            } else if (eventObject.geometry instanceof BufferGeometry) {
              const oldDrawRangeCount = eventObject.geometry.drawRange.count;
              eventObject.geometry.setDrawRange(0, 0);
              const eventObjectTween = new Tween(
                eventObject.geometry.drawRange,
              ).to(
                {
                  count: geometryPosCount,
                },
                tweenDuration,
              );
              eventObjectTween.onComplete(() => {
                eventObject.geometry.drawRange.count = oldDrawRangeCount;
              });
              allTweens.push(eventObjectTween);
            }
          }
        }
        // Animation for scaling out objects with or without position
        else if (eventObject.name === 'Jet') {
          const scaleTween = new Tween({
            x: 0.01,
            y: 0.01,
            z: 0.01,
          }).to(
            {
              x: eventObject.scale.x,
              y: eventObject.scale.y,
              z: eventObject.scale.z,
            },
            tweenDuration,
          );
          // Manually updating scale since we need to change position
          scaleTween.onUpdate((updatedScale: Vector3) => {
            const previousScale = eventObject.scale.x;
            eventObject.scale.setScalar(updatedScale.x);
            // Restoring to original position and then moving again with the current value
            eventObject.position
              .divideScalar(previousScale)
              .multiplyScalar(updatedScale.x);
          });
          allTweens.push(scaleTween);
        } else {
          const hasPosition = !eventObject.position.equals(
            new Vector3(0, 0, 0),
          );
          let position = hasPosition
            ? eventObject.position
            : eventObject.geometry.boundingSphere.center;

          // Edit geometry for hits
          if (eventObject.name === 'Hit') {
            position = Array.from(
              eventObject.geometry.attributes['position'].array,
            );
            eventObject.geometry.deleteAttribute('position');
            eventObject.geometry.computeBoundingSphere();
          } else {
            // Making the object invisible and will make visible
            // once the animation sphere reaches the object
            eventObject.visible = false;
          }

          objectsToAnimateWithSphere.push({
            eventObject: eventObject,
            position: position,
          });
        }
      }
    });

    // Tween for the animation sphere
    const animationSphereTween = new Tween(animationSphere).to(
      { radius: 3000 },
      tweenDuration,
    );

    const onAnimationSphereUpdate = (updateAnimationSphere: Sphere) => {
      objectsToAnimateWithSphere.forEach((obj) => {
        if (obj.eventObject.name === 'Hit') {
          const geometry = (obj.eventObject as any).geometry;

          const hitsPositions = this.getHitsPositions(obj.position);
          const reachedHits = hitsPositions.filter((hitPosition) =>
            updateAnimationSphere.containsPoint(
              new Vector3().fromArray(hitPosition),
            ),
          );

          if (reachedHits.length > 0) {
            geometry.setAttribute(
              'position',
              new BufferAttribute(
                new Float32Array([].concat(...reachedHits)),
                3,
              ),
            );
            geometry.computeBoundingSphere();
          }
        } else if (updateAnimationSphere.containsPoint(obj.position)) {
          obj.eventObject.visible = true;
        }
      });
    };

    animationSphereTween.onUpdate(onAnimationSphereUpdate);

    // Animation sphere tween after covering the tracks
    const animationSphereTweenClone = new Tween(animationSphere).to(
      { radius: 10000 },
      extraAnimationSphereDuration,
    );
    animationSphereTweenClone.onUpdate(onAnimationSphereUpdate);

    animationSphereTween.chain(animationSphereTweenClone);

    allTweens.push(animationSphereTween);

    // Call onAnimationStart when the first tween starts
    allTweens[0].onStart(() => onAnimationStart?.());

    // Start all tweens
    for (const tween of allTweens) {
      tween.easing(Easing.Quartic.Out).start();
    }

    // Call onEnd when the last tween completes
    animationSphereTweenClone.onComplete(() => {
      // Restore all remaining event data items
      onAnimationSphereUpdate(new Sphere(new Vector3(), Infinity));
      onEnd?.();
    });
  }

  /**
   * Animate the propagation and generation of event data using clipping planes.
   * @param tweenDuration Duration of the animation tween.
   * @param onEnd Function to call when all animations have ended.
   * @param onAnimationStart Callback when the first animation starts.
   * @param clippingConstant Constant for the clipping planes for distance from the origin.
   */
  public animateEventWithClipping(
    tweenDuration: number,
    onEnd?: () => void,
    onAnimationStart?: () => void,
    clippingConstant: number = 11000,
  ) {
    const allEventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);

    // Sphere to get spherical set of clipping planes from
    const sphere = new SphereGeometry(1, 8, 8);
    // Clipping planes for animation
    const animationClipPlanes: Plane[] = [];

    // Get clipping planes from the vertices of sphere
    const position = sphere.attributes.position;
    const vertex = new Vector3();
    for (let i = 0; i < position.count; i++) {
      vertex.fromBufferAttribute(position as BufferAttribute, i);
      animationClipPlanes.push(new Plane(vertex.clone(), 0));
    }

    // Save the previous clipping setting of the renderer
    const prevLocalClipping =
      this.rendererManager.getMainRenderer().localClippingEnabled;
    if (!prevLocalClipping) {
      this.rendererManager.setLocalClippingEnabled(true);
    }

    // Apply clipping planes to all the event data objects' material
    allEventData.traverse((eventObject: any) => {
      if (eventObject.geometry && eventObject.material) {
        eventObject.material.clippingPlanes = animationClipPlanes;
      }
    });

    const allTweens = [];
    // Create tweens for the animation clipping planes
    for (const animationClipPlane of animationClipPlanes) {
      animationClipPlane.constant = 0;
      const tween = new Tween(animationClipPlane).to(
        { constant: clippingConstant },
        tweenDuration,
      );
      allTweens.push(tween);
    }

    allTweens[0].onStart(() => onAnimationStart?.());

    // Start all the tweens
    for (const tween of allTweens) {
      tween.start();
    }

    allTweens[allTweens.length - 1].onComplete(() => {
      // Revert local clipping of the renderer
      if (!prevLocalClipping) {
        this.rendererManager.getMainRenderer().localClippingEnabled =
          prevLocalClipping /* false */;
      }
      // Remove the applied clipping planes from the event data objects
      allEventData.traverse((eventObject: any) => {
        if (eventObject.geometry && eventObject.material) {
          eventObject.material.clippingPlanes = null;
        }
      });
      onEnd?.();
    });
  }

  /**
   * Animate the collision of two particles.
   * @param tweenDuration Duration of the particle collision animation tween.
   * @param particleSize Size of the particles.
   * @param distanceFromOrigin Distance of the particles (along z-axes) from the origin.
   * @param particleColor Color of the particles.
   * @param onEnd Callback to call when the particle collision ends.
   */
  public collideParticles(
    tweenDuration: number,
    particleSize: number = 10,
    distanceFromOrigin: number = 5000,
    particleColor: Color = new Color(0xffffff),
    onEnd?: () => void,
  ) {
    const particleGeometry = new SphereGeometry(particleSize, 32, 32);
    const particleMaterial = new MeshBasicMaterial({
      color: particleColor,
      transparent: true,
      opacity: 0,
    });

    const particle1 = new Mesh(particleGeometry, particleMaterial);
    const particle2 = particle1.clone() as Mesh;

    particle1.position.setZ(distanceFromOrigin);
    particle2.position.setZ(-distanceFromOrigin);

    const particles = [particle1, particle2];

    this.scene.add(...particles);

    const particleTweens = [];

    for (const particle of particles) {
      new Tween(particle.material)
        .to(
          {
            opacity: 1,
          },
          300,
        )
        .start();

      const particleToOrigin = new Tween(particle.position)
        .to(
          {
            z: 0,
          },
          tweenDuration,
        )
        .start();

      particleTweens.push(particleToOrigin);
    }

    particleTweens[0].onComplete(() => {
      this.scene.remove(...particles);
      onEnd?.();
    });
  }

  /**
   * Animate the propagation and generation of event data with particle collison.
   * @param animationFunction Animation function to call after collision.
   * @param tweenDuration Duration of the animation tween.
   * @param onEnd Function to call when all animations have ended.
   */
  public animateWithCollision(
    animationFunction: (
      tweenDuration: number,
      onEnd?: () => void,
      onAnimationStart?: () => void,
    ) => void,
    tweenDuration: number,
    onEnd?: () => void,
  ) {
    const allEventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);

    // Get the color of the first track to use for colliding particles
    const track = allEventData.getObjectByName('Track');
    let trackColor: Color;
    if (track instanceof Group) {
      trackColor = (track.children[0] as any)?.material?.color;
    } else {
      trackColor = (track as any)?.material.color;
    }

    // Hide event data to show particles collision
    if (allEventData) {
      allEventData.visible = false;
    }

    this.collideParticles(1500, 30, 5000, trackColor, () => {
      animationFunction(tweenDuration, onEnd, () => {
        if (allEventData) {
          allEventData.visible = true;
        }
      });
    });
  }

  /**
   * Animate the propagation and generation of event data with particle collison.
   * @param tweenDuration Duration of the animation tween.
   * @param onEnd Function to call when all animations have ended.
   */
  public animateEventWithCollision(tweenDuration: number, onEnd?: () => void) {
    this.animateWithCollision(this.animateEvent, tweenDuration, onEnd);
  }

  /**
   * Animate the propagation and generation of event data
   * using clipping planes after particle collison.
   * @param tweenDuration Duration of the animation tween.
   * @param onEnd Function to call when all animations have ended.
   */
  public animateClippingWithCollision(
    tweenDuration: number,
    onEnd?: () => void,
  ) {
    this.animateWithCollision(
      this.animateEventWithClipping,
      tweenDuration,
      onEnd,
    );
  }

  /**
   * Get the positions of hits in a multidimensional array
   * from a single dimensional array.
   * @param positions Positions of hits in a single dimensional array.
   * @returns Positions of hits in a multidimensional array.
   */
  private getHitsPositions(positions: number[]): number[][] {
    const hitsPositions: number[][] = [];
    for (let i = 0; i < positions.length; i += 3) {
      hitsPositions.push(positions.slice(i, i + 3));
    }
    return hitsPositions;
  }

  /**
   * Animate scene by animating camera through the scene and animating event collision.
   * @param animationPreset Preset for animation including positions to go through and
   * event collision animation options.
   * @param onEnd Function to call when the animation ends.
   */
  public animatePreset(animationPreset: AnimationPreset, onEnd?: () => void) {
    const { positions, animateEventAfterInterval, collisionDuration } =
      animationPreset;

    if (animateEventAfterInterval && collisionDuration) {
      // Will be made visible after collision animation ends.
      this.scene.getObjectByName(SceneManager.EVENT_DATA_ID).visible = false;
      setTimeout(() => {
        this.animateEventWithCollision(collisionDuration);
      }, animateEventAfterInterval);
    }

    const firstTween = this.getCameraTween(
      positions[0].position,
      positions[0].duration ?? 2000,
      positions[0].easing,
    );

    let previousTween = firstTween;
    positions.slice(1).forEach(({ position, duration, easing }) => {
      const tween = this.getCameraTween(position, duration ?? 2000, easing);
      previousTween.chain(tween);
      previousTween = tween;
    });
    previousTween.onComplete(onEnd);

    firstTween.start();
  }
}

results matching ""

    No results matching ""