Warning, /firebird/firebird-ng/src/app/phoenix-overload/eic-animation-manager.ts is written in an unsupported language. File is not indexed.
0001 import { Easing, Tween } from '@tweenjs/tween.js';
0002 import {
0003 TubeGeometry,
0004 BufferGeometry,
0005 Vector3,
0006 Color,
0007 MeshBasicMaterial,
0008 Mesh,
0009 SphereGeometry,
0010 Sphere,
0011 Object3D,
0012 BufferAttribute,
0013 Scene,
0014 Camera,
0015 Plane,
0016 Group,
0017 } from 'three';
0018 import {RendererManager, SceneManager} from "phoenix-event-display";
0019 import {TracksMesh} from "phoenix-event-display/dist/loaders/objects/tracks";
0020
0021
0022 /** Type for animation preset. */
0023 export interface AnimationPreset {
0024 /** Positions with duration and easing of each tween forming a path. */
0025 positions: { position: number[]; duration: number; easing?: any }[];
0026 /** Time after which to start the event collision animation. */
0027 animateEventAfterInterval?: number;
0028 /** Duration of the event collision. */
0029 collisionDuration?: number;
0030 /** Name of the Animation */
0031 name: string;
0032 }
0033
0034 /**
0035 * Manager for managing animation related operations using three.js and tween.js.
0036 */
0037 export class EicAnimationsManager {
0038 /**
0039 * Constructor for the animation manager.
0040 * @param scene Three.js scene containing all the objects and event data.
0041 * @param activeCamera Currently active camera.
0042 * @param rendererManager Manager for managing event display's renderer related functions.
0043 */
0044 constructor(
0045 private scene: Scene,
0046 private activeCamera: Camera,
0047 private rendererManager: RendererManager,
0048 ) {
0049 this.animateEvent = this.animateEvent.bind(this);
0050 this.animateEventWithClipping = this.animateEventWithClipping.bind(this);
0051 }
0052
0053 /**
0054 * Get the camera tween for animating camera to a position.
0055 * @param pos End position of the camera tween.
0056 * @param duration Duration of the tween.
0057 * @param easing Animation easing of the tween if any.
0058 * @returns Tween object of the camera animation.
0059 */
0060 public getCameraTween(
0061 pos: number[],
0062 duration: number = 1000,
0063 easing?: typeof Easing.Linear.None,
0064 ) {
0065 const tween = new Tween(this.activeCamera.position).to(
0066 { x: pos[0], y: pos[1], z: pos[2] },
0067 duration,
0068 );
0069
0070 if (easing) {
0071 tween.easing(easing);
0072 }
0073
0074 return tween;
0075 }
0076
0077 /**
0078 * Animate the camera through the event scene.
0079 * @param startPos Start position of the translation animation.
0080 * @param tweenDuration Duration of each tween in the translation animation.
0081 * @param onAnimationEnd Callback when the last animation ends.
0082 */
0083 public animateThroughEvent(
0084 startPos: number[],
0085 tweenDuration: number,
0086 onAnimationEnd?: () => void,
0087 ) {
0088 // Move to start
0089 const start = this.getCameraTween(startPos, 1000, Easing.Cubic.Out);
0090 // Move to position along the detector axis
0091 const alongAxisPosition = [0, 0, startPos[2]];
0092 const startXAxis = this.getCameraTween(alongAxisPosition, tweenDuration);
0093
0094 const radius = 500;
0095 const numOfSteps = 24;
0096 const angle = 3 * Math.PI;
0097 const step = angle / numOfSteps;
0098
0099 const rotationPositions = [];
0100 for (let i = 1; i <= numOfSteps; i++) {
0101 rotationPositions.push([
0102 radius * Math.sin(step * i), // x
0103 0, // y
0104 radius * Math.cos(step * i), // z
0105 ]);
0106 }
0107
0108 // Go to origin
0109 const rotateStart = this.getCameraTween(
0110 [0, 0, radius],
0111 tweenDuration,
0112 Easing.Cubic.Out,
0113 );
0114
0115 let rotate = rotateStart;
0116 const rotationTime = tweenDuration * 4;
0117 const singleRotationTime = rotationTime / numOfSteps;
0118 // Rotating around the event
0119 for (const pos of rotationPositions) {
0120 const animation = this.getCameraTween(pos, singleRotationTime);
0121 rotate.chain(animation);
0122 rotate = animation;
0123 }
0124
0125 // Go to the end position and then back to the starting point
0126 const endPos = [0, 0, -startPos[2]];
0127 const end = this.getCameraTween(endPos, tweenDuration, Easing.Cubic.In);
0128 const startClone = this.getCameraTween(
0129 startPos,
0130 tweenDuration,
0131 Easing.Cubic.Out,
0132 );
0133 startClone.onComplete(() => onAnimationEnd?.());
0134 startClone.delay(500);
0135
0136 start.chain(startXAxis);
0137 startXAxis.chain(rotateStart);
0138 rotate.chain(end);
0139 end.chain(startClone);
0140
0141 start.start();
0142 }
0143
0144 /**
0145 * Animate the propagation and generation of event data.
0146 * @param tweenDuration Duration of the animation tween.
0147 * @param onEnd Callback when all animations have ended.
0148 * @param onAnimationStart Callback when the first animation starts.
0149 */
0150 public animateEvent(
0151 tweenDuration: number,
0152 onEnd?: () => void,
0153 onAnimationStart?: () => void,
0154 ) {
0155 const extraAnimationSphereDuration = tweenDuration * 0.25;
0156 tweenDuration *= 0.75;
0157
0158 const eventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);
0159 if(!eventData) {
0160 console.error("this.scene.getObjectByName(SceneManager.EVENT_DATA_ID) returned null or undefined");
0161 return;
0162 }
0163
0164 const animationSphere = new Sphere(new Vector3(), 0);
0165 const objectsToAnimateWithSphere: {
0166 eventObject: Object3D;
0167 position: any;
0168 }[] = [];
0169
0170 const allTweens = [];
0171 // Traverse over all event data
0172 eventData.traverse((eventObject: any) => {
0173 if (eventObject.geometry) {
0174 // Animation for extrapolating tracks without changing scale
0175 if (eventObject.name === 'Track' || eventObject.name === 'LineHit') {
0176 // Check if geometry drawRange count exists
0177 let geometryPosCount =
0178 eventObject.geometry?.attributes?.position?.count;
0179 if (geometryPosCount) {
0180 // WORKAROUND
0181 // Changing position count for TubeGeometry because
0182 // what we get is not the actual and it has Infinity drawRange count
0183 if (eventObject.geometry instanceof TubeGeometry) {
0184 geometryPosCount *= 6;
0185 }
0186
0187 if (eventObject.geometry instanceof TracksMesh) {
0188 eventObject.material.progress = 0;
0189 const eventObjectTween = new Tween(eventObject.material).to(
0190 {
0191 progress: 1,
0192 },
0193 tweenDuration,
0194 );
0195 eventObjectTween.onComplete(() => {
0196 eventObject.material.progress = 1;
0197 });
0198 allTweens.push(eventObjectTween);
0199 } else if (eventObject.geometry instanceof BufferGeometry) {
0200 const oldDrawRangeCount = eventObject.geometry.drawRange.count;
0201 eventObject.geometry.setDrawRange(0, 0);
0202 const eventObjectTween = new Tween(
0203 eventObject.geometry.drawRange,
0204 ).to(
0205 {
0206 count: geometryPosCount,
0207 },
0208 tweenDuration,
0209 );
0210 eventObjectTween.onComplete(() => {
0211 eventObject.geometry.drawRange.count = oldDrawRangeCount;
0212 });
0213 allTweens.push(eventObjectTween);
0214 }
0215 }
0216 }
0217
0218 }
0219 });
0220
0221 // Tween for the animation sphere
0222 const animationSphereTween = new Tween(animationSphere).to(
0223 { radius: 3000 },
0224 tweenDuration,
0225 );
0226
0227 const onAnimationSphereUpdate = (updateAnimationSphere: Sphere) => {
0228 // objectsToAnimateWithSphere.forEach((obj) => {
0229 // if (obj.eventObject.name === 'Hit') {
0230 // const geometry = (obj.eventObject as any).geometry;
0231 //
0232 // const hitsPositions = this.getHitsPositions(obj.position);
0233 // const reachedHits = hitsPositions.filter((hitPosition) =>
0234 // updateAnimationSphere.containsPoint(
0235 // new Vector3().fromArray(hitPosition),
0236 // ),
0237 // );
0238 //
0239 // if (reachedHits.length > 0) {
0240 // geometry.setAttribute(
0241 // 'position',
0242 // new BufferAttribute(
0243 // new Float32Array([].concat(...reachedHits)),
0244 // 3,
0245 // ),
0246 // );
0247 // geometry.computeBoundingSphere();
0248 // }
0249 // } else if (updateAnimationSphere.containsPoint(obj.position)) {
0250 // obj.eventObject.visible = true;
0251 // }
0252 // });
0253 };
0254
0255 animationSphereTween.onUpdate(onAnimationSphereUpdate);
0256
0257 // Animation sphere tween after covering the tracks
0258 const animationSphereTweenClone = new Tween(animationSphere).to(
0259 { radius: 10000 },
0260 extraAnimationSphereDuration,
0261 );
0262 animationSphereTweenClone.onUpdate(onAnimationSphereUpdate);
0263
0264 animationSphereTween.chain(animationSphereTweenClone);
0265
0266 allTweens.push(animationSphereTween);
0267
0268 // Call onAnimationStart when the first tween starts
0269 allTweens[0].onStart(() => onAnimationStart?.());
0270
0271 // Start all tweens
0272 for (const tween of allTweens) {
0273 tween.easing(Easing.Quartic.Out).start();
0274 }
0275
0276 // Call onEnd when the last tween completes
0277 animationSphereTweenClone.onComplete(() => {
0278 // Restore all remaining event data items
0279 onAnimationSphereUpdate(new Sphere(new Vector3(), Infinity));
0280 onEnd?.();
0281 });
0282 }
0283
0284 /**
0285 * Animate the propagation and generation of event data using clipping planes.
0286 * @param tweenDuration Duration of the animation tween.
0287 * @param onEnd Function to call when all animations have ended.
0288 * @param onAnimationStart Callback when the first animation starts.
0289 * @param clippingConstant Constant for the clipping planes for distance from the origin.
0290 */
0291 public animateEventWithClipping(
0292 tweenDuration: number,
0293 onEnd?: () => void,
0294 onAnimationStart?: () => void,
0295 clippingConstant: number = 11000,
0296 ) {
0297 const allEventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);
0298 if(!allEventData) {
0299 console.error("this.scene.getObjectByName(SceneManager.EVENT_DATA_ID) returned null or undefined");
0300 return;
0301 }
0302
0303 // Sphere to get spherical set of clipping planes from
0304 const sphere = new SphereGeometry(1, 8, 8);
0305 // Clipping planes for animation
0306 const animationClipPlanes: Plane[] = [];
0307
0308 // Get clipping planes from the vertices of sphere
0309 const position = sphere.attributes["position"];
0310 const vertex = new Vector3();
0311 for (let i = 0; i < position.count; i++) {
0312 vertex.fromBufferAttribute(position as BufferAttribute, i);
0313 animationClipPlanes.push(new Plane(vertex.clone(), 0));
0314 }
0315
0316 // Save the previous clipping setting of the renderer
0317 const prevLocalClipping =
0318 this.rendererManager.getMainRenderer().localClippingEnabled;
0319 if (!prevLocalClipping) {
0320 this.rendererManager.setLocalClippingEnabled(true);
0321 }
0322
0323 // Apply clipping planes to all the event data objects' material
0324 allEventData.traverse((eventObject: any) => {
0325 if (eventObject.geometry && eventObject.material) {
0326 eventObject.material.clippingPlanes = animationClipPlanes;
0327 }
0328 });
0329
0330 const allTweens = [];
0331 // Create tweens for the animation clipping planes
0332 for (const animationClipPlane of animationClipPlanes) {
0333 animationClipPlane.constant = 0;
0334 const tween = new Tween(animationClipPlane).to(
0335 { constant: clippingConstant },
0336 tweenDuration,
0337 );
0338 allTweens.push(tween);
0339 }
0340
0341 allTweens[0].onStart(() => onAnimationStart?.());
0342
0343 // Start all the tweens
0344 for (const tween of allTweens) {
0345 tween.start();
0346 }
0347
0348 allTweens[allTweens.length - 1].onComplete(() => {
0349 // Revert local clipping of the renderer
0350 if (!prevLocalClipping) {
0351 this.rendererManager.getMainRenderer().localClippingEnabled =
0352 prevLocalClipping /* false */;
0353 }
0354 // Remove the applied clipping planes from the event data objects
0355 allEventData.traverse((eventObject: any) => {
0356 if (eventObject.geometry && eventObject.material) {
0357 eventObject.material.clippingPlanes = null;
0358 }
0359 });
0360 onEnd?.();
0361 });
0362 }
0363
0364 /**
0365 * Animate the collision of two particles.
0366 * @param tweenDuration Duration of the particle collision animation tween.
0367 * @param particleSize Size of the particles.
0368 * @param distanceFromOrigin Distance of the particles (along z-axes) from the origin.
0369 * @param particleColor Color of the particles.
0370 * @param onEnd Callback to call when the particle collision ends.
0371 */
0372 public collideParticles(
0373 tweenDuration: number,
0374 particleSize: number = 10,
0375 distanceFromOrigin: number = 5000,
0376 particleColor: Color = new Color(0xffffff),
0377 onEnd?: () => void,
0378 ) {
0379 const electronGeometry = new SphereGeometry(0.5*particleSize, 32, 32);
0380 const electronMaterial = new MeshBasicMaterial({ color: 0x0000FF, transparent: true, opacity: 0});
0381 const electron = new Mesh(electronGeometry, electronMaterial);
0382
0383 const ionMaterial = new MeshBasicMaterial({ color: 0xFF0000, transparent: true, opacity: 0});
0384 const ionGeometry = new SphereGeometry(2*particleSize, 32, 32);
0385 const ion = new Mesh(ionGeometry, ionMaterial);
0386
0387 electron.position.setZ(distanceFromOrigin);
0388 ion.position.setZ(-distanceFromOrigin);
0389
0390 const particles = [electron, ion];
0391
0392 this.scene.add(...particles);
0393
0394 const particleTweens = [];
0395
0396 for (const particle of particles) {
0397 new Tween(particle.material)
0398 .to(
0399 {
0400 opacity: 1,
0401 },
0402 300,
0403 )
0404 .start();
0405
0406 const particleToOrigin = new Tween(particle.position)
0407 .to(
0408 {
0409 z: 0,
0410 },
0411 tweenDuration,
0412 )
0413 .start();
0414
0415 particleTweens.push(particleToOrigin);
0416 }
0417
0418 particleTweens[0].onComplete(() => {
0419 this.scene.remove(...particles);
0420 onEnd?.();
0421 });
0422 }
0423
0424 /**
0425 * Animate the propagation and generation of event data with particle collison.
0426 * @param animationFunction Animation function to call after collision.
0427 * @param tweenDuration Duration of the animation tween.
0428 * @param onEnd Function to call when all animations have ended.
0429 */
0430 public animateWithCollision(
0431 animationFunction: (
0432 tweenDuration: number,
0433 onEnd?: () => void,
0434 onAnimationStart?: () => void,
0435 ) => void,
0436 tweenDuration: number,
0437 onEnd?: () => void,
0438 ) {
0439 const allEventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);
0440 if(!allEventData) {
0441 console.error("this.scene.getObjectByName(SceneManager.EVENT_DATA_ID) returned null or undefined");
0442 return;
0443 }
0444
0445 // Hide event data to show particles collision
0446 if (allEventData) {
0447 allEventData.visible = false;
0448 }
0449
0450 this.collideParticles(1500, 30, 5000, new Color(0xAAAAAA), () => {
0451 animationFunction(tweenDuration, onEnd, () => {
0452 if (allEventData) {
0453 allEventData.visible = true;
0454 }
0455 });
0456 });
0457 }
0458
0459 /**
0460 * Animate the propagation and generation of event data with particle collison.
0461 * @param tweenDuration Duration of the animation tween.
0462 * @param onEnd Function to call when all animations have ended.
0463 */
0464 public animateEventWithCollision(tweenDuration: number, onEnd?: () => void) {
0465 this.animateWithCollision(this.animateEvent, tweenDuration, onEnd);
0466 }
0467
0468 /**
0469 * Animate the propagation and generation of event data
0470 * using clipping planes after particle collison.
0471 * @param tweenDuration Duration of the animation tween.
0472 * @param onEnd Function to call when all animations have ended.
0473 */
0474 public animateClippingWithCollision(
0475 tweenDuration: number,
0476 onEnd?: () => void,
0477 ) {
0478 this.animateWithCollision(
0479 this.animateEventWithClipping,
0480 tweenDuration,
0481 onEnd,
0482 );
0483 }
0484
0485 /**
0486 * Get the positions of hits in a multidimensional array
0487 * from a single dimensional array.
0488 * @param positions Positions of hits in a single dimensional array.
0489 * @returns Positions of hits in a multidimensional array.
0490 */
0491 private getHitsPositions(positions: number[]): number[][] {
0492 const hitsPositions: number[][] = [];
0493 for (let i = 0; i < positions.length; i += 3) {
0494 hitsPositions.push(positions.slice(i, i + 3));
0495 }
0496 return hitsPositions;
0497 }
0498
0499 /**
0500 * Animate scene by animating camera through the scene and animating event collision.
0501 * @param animationPreset Preset for animation including positions to go through and
0502 * event collision animation options.
0503 * @param onEnd Function to call when the animation ends.
0504 */
0505 public animatePreset(animationPreset: AnimationPreset, onEnd?: () => void) {
0506 const { positions, animateEventAfterInterval, collisionDuration } =
0507 animationPreset;
0508
0509 let allEventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);
0510 if(!allEventData) {
0511 console.error("this.scene.getObjectByName(SceneManager.EVENT_DATA_ID) returned null or undefined");
0512 return;
0513 }
0514
0515 if (animateEventAfterInterval && collisionDuration) {
0516 // Will be made visible after collision animation ends.
0517 allEventData.visible = false;
0518 setTimeout(() => {
0519 this.animateEventWithCollision(collisionDuration);
0520 }, animateEventAfterInterval);
0521 }
0522
0523 const firstTween = this.getCameraTween(
0524 positions[0].position,
0525 positions[0].duration ?? 2000,
0526 positions[0].easing,
0527 );
0528
0529 let previousTween = firstTween;
0530 positions.slice(1).forEach(({ position, duration, easing }) => {
0531 const tween = this.getCameraTween(position, duration ?? 2000, easing);
0532 previousTween.chain(tween);
0533 previousTween = tween;
0534 });
0535 previousTween.onComplete(onEnd);
0536
0537 firstTween.start();
0538 }
0539 }