Classes and interfaces for controlling animations programmatically through code rather than declarative templates, enabling dynamic animation creation and precise control over animation playback.
Injectable service for building animation sequences programmatically.
/**
* Injectable service for producing animation sequences programmatically
* Provided by BrowserAnimationsModule or NoopAnimationsModule
*/
abstract class AnimationBuilder {
/**
* Builds a factory for producing a defined animation
* @param animation - Reusable animation definition or array of definitions
* @returns AnimationFactory that can create players for the animation
*/
abstract build(animation: AnimationMetadata | AnimationMetadata[]): AnimationFactory;
}Usage Examples:
import { Component, ElementRef, ViewChild, inject } from '@angular/core';
import { AnimationBuilder, style, animate, group } from '@angular/animations';
@Component({
selector: 'app-dynamic',
template: `
<div #animatedElement class="box">
Animated Element
</div>
<button (click)="startAnimation()">Animate</button>
`
})
export class DynamicAnimationComponent {
@ViewChild('animatedElement', { static: true })
animatedElement!: ElementRef;
private animationBuilder = inject(AnimationBuilder);
startAnimation() {
// Build animation dynamically
const factory = this.animationBuilder.build([
style({ transform: 'translateX(0)', backgroundColor: 'blue' }),
group([
animate('500ms ease-out', style({ transform: 'translateX(200px)' })),
animate('300ms', style({ backgroundColor: 'red' }))
])
]);
// Create and play animation
const player = factory.create(this.animatedElement.nativeElement);
player.play();
}
}Factory object returned from AnimationBuilder.build() that creates animation players.
/**
* Factory for creating AnimationPlayer instances
* Returned by AnimationBuilder.build()
*/
abstract class AnimationFactory {
/**
* Creates AnimationPlayer instance for the defined animation
* @param element - DOM element to attach the animation to
* @param options - Optional timing and parameter configuration
* @returns AnimationPlayer for controlling the animation
*/
abstract create(element: any, options?: AnimationOptions): AnimationPlayer;
}Usage Examples:
import { AnimationBuilder, style, animate } from '@angular/animations';
// In component
buildAndCreateAnimation(element: HTMLElement) {
// Build the factory
const factory = this.animationBuilder.build([
style({ opacity: 0, transform: 'scale(0.5)' }),
animate('400ms cubic-bezier(0.25, 0.46, 0.45, 0.94)',
style({ opacity: 1, transform: 'scale(1)' })
)
]);
// Create player with options
const player = factory.create(element, {
delay: '100ms',
params: { duration: 500 }
});
return player;
}Interface for controlling animation playback programmatically.
/**
* Interface for programmatic control of animation sequences
* Created by AnimationFactory.create()
*/
interface AnimationPlayer {
/** Parent player if this player is part of a group */
parentPlayer: AnimationPlayer | null;
/** Total duration of the animation in milliseconds */
readonly totalTime: number;
/** Optional callback invoked before the animation is destroyed */
beforeDestroy?: () => any;
/**
* Sets callback to invoke when animation finishes
* @param fn - Callback function to execute
*/
onDone(fn: () => void): void;
/**
* Sets callback to invoke when animation starts
* @param fn - Callback function to execute
*/
onStart(fn: () => void): void;
/**
* Sets callback to invoke after animation is destroyed
* @param fn - Callback function to execute
*/
onDestroy(fn: () => void): void;
/** Initializes the animation */
init(): void;
/**
* Reports whether the animation has started
* @returns True if animation has started
*/
hasStarted(): boolean;
/** Starts playing the animation */
play(): void;
/** Pauses the animation */
pause(): void;
/** Restarts the paused animation */
restart(): void;
/** Finishes the animation immediately */
finish(): void;
/** Destroys the animation and cleans up resources */
destroy(): void;
/** Resets the animation to its initial state */
reset(): void;
/**
* Sets the current position of the animation
* @param position - Fractional position (0 to 1)
*/
setPosition(position: number): void;
/**
* Gets the current position of the animation
* @returns Fractional position (0 to 1)
*/
getPosition(): number;
}Usage Examples:
import { Component, ElementRef, ViewChild } from '@angular/core';
import { AnimationBuilder, AnimationPlayer, style, animate } from '@angular/animations';
@Component({
selector: 'app-player-control',
template: `
<div #target class="animated-box">Target Element</div>
<div class="controls">
<button (click)="play()">Play</button>
<button (click)="pause()">Pause</button>
<button (click)="restart()">Restart</button>
<button (click)="finish()">Finish</button>
<button (click)="reset()">Reset</button>
<input type="range" min="0" max="1" step="0.01"
[value]="currentPosition"
(input)="setPosition($event)">
</div>
<div class="info">
<p>Duration: {{ player?.totalTime }}ms</p>
<p>Position: {{ currentPosition }}</p>
<p>Started: {{ player?.hasStarted() }}</p>
</div>
`
})
export class PlayerControlComponent {
@ViewChild('target', { static: true }) target!: ElementRef;
player?: AnimationPlayer;
currentPosition = 0;
constructor(private animationBuilder: AnimationBuilder) {}
ngOnInit() {
this.createAnimation();
}
createAnimation() {
const factory = this.animationBuilder.build([
style({ transform: 'translateX(0) rotate(0deg)', backgroundColor: 'blue' }),
animate('2000ms ease-in-out', style({
transform: 'translateX(300px) rotate(360deg)',
backgroundColor: 'red'
}))
]);
this.player = factory.create(this.target.nativeElement);
// Set up callbacks
this.player.onStart(() => {
console.log('Animation started');
});
this.player.onDone(() => {
console.log('Animation completed');
});
this.player.onDestroy(() => {
console.log('Animation destroyed');
});
// Track position changes
this.trackPosition();
}
trackPosition() {
const updatePosition = () => {
if (this.player) {
this.currentPosition = this.player.getPosition();
if (this.player.hasStarted() && this.currentPosition < 1) {
requestAnimationFrame(updatePosition);
}
}
};
updatePosition();
}
play() {
this.player?.play();
}
pause() {
this.player?.pause();
}
restart() {
this.player?.restart();
}
finish() {
this.player?.finish();
}
reset() {
this.player?.reset();
this.currentPosition = 0;
}
setPosition(event: any) {
const position = parseFloat(event.target.value);
this.player?.setPosition(position);
this.currentPosition = position;
}
}Empty implementation of AnimationPlayer used when animations are disabled.
/**
* Empty animation player implementation
* Used when animations are disabled to avoid null checks
*/
class NoopAnimationPlayer implements AnimationPlayer {
parentPlayer: AnimationPlayer | null = null;
readonly totalTime: number;
/**
* Creates a no-op animation player
* @param duration - Total duration in milliseconds
* @param delay - Delay before animation starts in milliseconds
*/
constructor(duration?: number, delay?: number);
onStart(fn: () => void): void;
onDone(fn: () => void): void;
onDestroy(fn: () => void): void;
hasStarted(): boolean;
init(): void;
play(): void;
pause(): void;
restart(): void;
finish(): void;
destroy(): void;
reset(): void;
setPosition(position: number): void;
getPosition(): number;
}Usage Example:
// NoopAnimationPlayer is typically used internally, but can be useful for testing
import { NoopAnimationPlayer } from '@angular/animations';
// Create a no-op player for testing
const testPlayer = new NoopAnimationPlayer(1000, 100);
testPlayer.onDone(() => console.log('Done'));
testPlayer.play(); // Will immediately trigger done callbackimport { Injectable } from '@angular/core';
import { AnimationBuilder, AnimationPlayer, AnimationMetadata } from '@angular/animations';
@Injectable()
export class AnimationQueueService {
private queue: Array<{
element: HTMLElement;
animation: AnimationMetadata[];
options?: any;
}> = [];
private currentPlayer?: AnimationPlayer;
constructor(private animationBuilder: AnimationBuilder) {}
enqueue(element: HTMLElement, animation: AnimationMetadata[], options?: any) {
this.queue.push({ element, animation, options });
if (!this.currentPlayer || !this.currentPlayer.hasStarted()) {
this.playNext();
}
}
private playNext() {
if (this.queue.length === 0) return;
const { element, animation, options } = this.queue.shift()!;
const factory = this.animationBuilder.build(animation);
this.currentPlayer = factory.create(element, options);
this.currentPlayer.onDone(() => {
this.playNext(); // Play next animation in queue
});
this.currentPlayer.play();
}
clear() {
this.queue.length = 0;
this.currentPlayer?.destroy();
}
}import { Injectable } from '@angular/core';
import { AnimationBuilder, style, animate, group, sequence } from '@angular/animations';
@Injectable()
export class DynamicAnimationService {
constructor(private animationBuilder: AnimationBuilder) {}
createBounceAnimation(
element: HTMLElement,
intensity: number = 1,
duration: number = 600
) {
const bounceHeight = 20 * intensity;
const factory = this.animationBuilder.build([
sequence([
animate(`${duration * 0.2}ms ease-out`,
style({ transform: `translateY(-${bounceHeight}px)` })
),
animate(`${duration * 0.3}ms ease-in`,
style({ transform: 'translateY(0)' })
),
animate(`${duration * 0.2}ms ease-out`,
style({ transform: `translateY(-${bounceHeight * 0.5}px)` })
),
animate(`${duration * 0.3}ms ease-in`,
style({ transform: 'translateY(0)' })
)
])
]);
return factory.create(element);
}
createParallaxAnimation(
elements: HTMLElement[],
speeds: number[],
distance: number = 100
) {
const animations = elements.map((element, index) => {
const speed = speeds[index] || 1;
return this.animationBuilder.build([
animate(`${1000 / speed}ms linear`,
style({ transform: `translateX(${distance}px)` })
)
]).create(element);
});
return {
play: () => animations.forEach(player => player.play()),
pause: () => animations.forEach(player => player.pause()),
destroy: () => animations.forEach(player => player.destroy())
};
}
}import { Injectable } from '@angular/core';
import { AnimationBuilder, AnimationPlayer } from '@angular/animations';
import { BehaviorSubject, Observable } from 'rxjs';
interface AnimationState {
isPlaying: boolean;
progress: number;
currentAnimation?: string;
}
@Injectable()
export class AnimationStateManager {
private stateSubject = new BehaviorSubject<AnimationState>({
isPlaying: false,
progress: 0
});
public state$: Observable<AnimationState> = this.stateSubject.asObservable();
private activePlayer?: AnimationPlayer;
constructor(private animationBuilder: AnimationBuilder) {}
playAnimation(
element: HTMLElement,
animation: any[],
name: string
): Promise<void> {
return new Promise((resolve) => {
// Stop current animation if running
if (this.activePlayer) {
this.activePlayer.destroy();
}
const factory = this.animationBuilder.build(animation);
this.activePlayer = factory.create(element);
// Update state
this.updateState({
isPlaying: true,
progress: 0,
currentAnimation: name
});
// Track progress
this.trackProgress();
this.activePlayer.onDone(() => {
this.updateState({
isPlaying: false,
progress: 1,
currentAnimation: undefined
});
resolve();
});
this.activePlayer.play();
});
}
private trackProgress() {
const updateProgress = () => {
if (this.activePlayer && this.activePlayer.hasStarted()) {
const progress = this.activePlayer.getPosition();
this.updateState({
isPlaying: true,
progress,
currentAnimation: this.stateSubject.value.currentAnimation
});
if (progress < 1) {
requestAnimationFrame(updateProgress);
}
}
};
updateProgress();
}
private updateState(newState: Partial<AnimationState>) {
this.stateSubject.next({
...this.stateSubject.value,
...newState
});
}
pause() {
this.activePlayer?.pause();
this.updateState({ isPlaying: false });
}
resume() {
this.activePlayer?.play();
this.updateState({ isPlaying: true });
}
stop() {
this.activePlayer?.destroy();
this.updateState({
isPlaying: false,
progress: 0,
currentAnimation: undefined
});
}
}import { Component, ElementRef, ViewChild } from '@angular/core';
import { AnimationBuilder } from '@angular/animations';
import { interval, fromEvent, merge } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-reactive-animation',
template: `
<div #animatedElement
class="reactive-box"
(mouseenter)="onMouseEnter()"
(mouseleave)="onMouseLeave()">
Hover me!
</div>
`
})
export class ReactiveAnimationComponent {
@ViewChild('animatedElement', { static: true })
element!: ElementRef;
constructor(private animationBuilder: AnimationBuilder) {}
onMouseEnter() {
// Create hover animation
const factory = this.animationBuilder.build([
animate('200ms ease-out', style({
transform: 'scale(1.1)',
backgroundColor: 'lightblue'
}))
]);
const player = factory.create(this.element.nativeElement);
player.play();
}
onMouseLeave() {
// Create leave animation
const factory = this.animationBuilder.build([
animate('200ms ease-in', style({
transform: 'scale(1)',
backgroundColor: 'white'
}))
]);
const player = factory.create(this.element.nativeElement);
player.play();
}
}class OptimizedAnimationController {
private players = new Set<AnimationPlayer>();
createAndTrackPlayer(factory: AnimationFactory, element: HTMLElement) {
const player = factory.create(element);
this.players.add(player);
player.onDone(() => {
this.players.delete(player);
player.destroy();
});
return player;
}
destroyAllPlayers() {
this.players.forEach(player => player.destroy());
this.players.clear();
}
}// Always clean up players to prevent memory leaks
ngOnDestroy() {
if (this.activePlayer) {
this.activePlayer.destroy();
}
// Clean up any stored references
this.players.forEach(player => player.destroy());
}