or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

animation-composition.mdindex.mdprogrammatic-control.mdreusable-animations.mdstyling-timing.mdtriggers.md
tile.json

programmatic-control.mddocs/

Programmatic Animation Control

Classes and interfaces for controlling animations programmatically through code rather than declarative templates, enabling dynamic animation creation and precise control over animation playback.

Capabilities

AnimationBuilder Service

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();
  }
}

AnimationFactory Class

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;
}

AnimationPlayer Interface

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;
  }
}

NoopAnimationPlayer Class

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 callback

Advanced Programmatic Patterns

Animation Queue Manager

import { 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();
  }
}

Dynamic Animation Generator

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())
    };
  }
}

Animation State Manager

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 
    });
  }
}

Integration with Reactive Patterns

Observable-driven Animations

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();
  }
}

Performance Considerations

Player Management

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();
  }
}

Memory Management

// 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());
}