Claude-skill-registry dev-phaser-animations

Sprite animations, tweens, animation chains, and timeline sequences

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/dev-phaser-animations" ~/.claude/skills/majiayu000-claude-skill-registry-dev-phaser-animations && rm -rf "$T"
manifest: skills/data/dev-phaser-animations/SKILL.md
source content

Phaser Animations

"Bring your sprites to life with smooth animations and tweens."

Before/After: Manual Animation vs Phaser Animation System

❌ Before: Manual Frame Animation

// Manual sprite animation without Phaser
class SpriteAnimator {
  private currentFrame = 0;
  private frameTimer = 0;
  private frameDuration = 83; // ~12 FPS
  private isPlaying = false;
  private frames: HTMLImageElement[] = [];

  constructor(private sprite: HTMLElement) {
    // Load all frames manually
    for (let i = 0; i < 8; i++) {
      const img = document.createElement('img');
      img.src = `assets/walk/frame${i}.png`;
      this.frames.push(img);
    }
  }

  play(animationName: string) {
    this.isPlaying = true;
    this.currentFrame = 0;
  }

  update(dt: number) {
    if (!this.isPlaying) return;

    this.frameTimer += dt;

    if (this.frameTimer >= this.frameDuration) {
      this.frameTimer = 0;
      this.currentFrame = (this.currentFrame + 1) % this.frames.length;
      this.sprite.style.backgroundImage = `url(${this.frames[this.currentFrame].src})`;
    }
  }

  // Manual tween for position
  moveTo(targetX: number, targetY: number, duration: number) {
    const startX = parseFloat(this.sprite.style.left) || 0;
    const startY = parseFloat(this.sprite.style.top) || 0;
    const startTime = performance.now();

    const animate = (currentTime: number) => {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);

      // Linear interpolation only - no easing options
      this.sprite.style.left = (startX + (targetX - startX) * progress) + 'px';
      this.sprite.style.top = (startY + (targetY - startY) * progress) + 'px';

      if (progress < 1) {
        requestAnimationFrame(animate);
      }
    };

    requestAnimationFrame(animate);
  }
}

// Problems:
// - Manual frame timing is error-prone
// - No built-in easing functions
// - Manual sprite sheet management
// - No animation state management
// - Chained animations require nested callbacks
// - No timeline support

✅ After: Phaser Animation System

// Phaser handles all animation automatically
export class GameScene extends Phaser.Scene {
  create() {
    const player = this.add.sprite(400, 300, 'player');

    // Create sprite sheet animation - ONE config!
    this.anims.create({
      key: 'walk',
      frames: this.anims.generateFrameNumbers('player', {
        start: 0,
        end: 7
      }),
      frameRate: 12,
      repeat: -1 // Infinite loop
    });

    // Play animation - ONE line!
    player.play('walk');

    // Tween with easing - ONE call!
    this.tweens.add({
      targets: player,
      x: 600,
      y: 400,
      duration: 1000,
      ease: 'Power2', // Built-in easing!
      onComplete: () => {
        console.log('Movement complete!');
      }
    });

    // Timeline for sequences - ONE config!
    this.tweens.timeline({
      targets: player,
      tweens: [
        { alpha: 0, duration: 500, ease: 'Linear' },
        { x: 200, duration: 500, offset: '-=250' }, // Overlap!
        { scale: 2, duration: 300, ease: 'Back.easeOut' }
      ]
    });
  }
}

// Benefits:
// - Automatic frame timing
// - 20+ built-in easing functions
// - Sprite sheet generation helpers
// - Animation state management (isPlaying, currentAnim)
// - Timeline support for complex sequences
// - Event callbacks (onComplete, animationcomplete)

When to Use This Skill

Use when:

  • Creating sprite animations
  • Building tween sequences
  • Implementing timeline-based animations
  • Animating UI elements
  • Creating visual effects and transitions

Quick Start

create() {
  // Create animation from sprite sheet
  this.anims.create({
    key: 'walk',
    frames: this.anims.generateFrameNumbers('player', { start: 0, end: 7 }),
    frameRate: 12,
    repeat: -1
  });

  // Play animation
  const player = this.add.sprite(400, 300, 'player');
  player.play('walk');
}

Decision Framework

NeedUse
Frame-based animation
anims.create()
+
sprite.play()
Property animation
tweens.add()
Sequenced animations
timeline
Single tween
tweens.add()
once
Delayed action
time.delayedCall()

Progressive Guide

Level 1: Sprite Animations

export class GameScene extends Phaser.Scene {
  create() {
    // Create animations from sprite sheet
    this.anims.create({
      key: "idle",
      frames: this.anims.generateFrameNumbers("player", {
        start: 0,
        end: 3,
      }),
      frameRate: 8,
      repeat: -1, // Infinite loop
      yoyo: false, // Don't play backwards
    });

    this.anims.create({
      key: "walk",
      frames: this.anims.generateFrameNumbers("player", {
        start: 4,
        end: 11,
      }),
      frameRate: 12,
      repeat: -1,
    });

    this.anims.create({
      key: "attack",
      frames: this.anims.generateFrameNumbers("player", {
        start: 12,
        end: 17,
      }),
      frameRate: 15,
      repeat: 0, // Play once
    });

    // Create sprite and play animation
    const player = this.add.sprite(400, 300, "player");
    player.play("idle");

    // Change animation
    player.play("walk", true); // true = ignore if playing
  }
}

Level 2: Tweening Properties

create() {
  const box = this.add.rectangle(400, 300, 50, 50, 0xff0000);

  // Basic tween
  this.tweens.add({
    targets: box,
    x: 600,
    duration: 1000,
    ease: 'Power2'
  });

  // Multiple properties
  this.tweens.add({
    targets: box,
    x: 200,
    y: 400,
    alpha: 0.5,
    angle: 180,
    scale: 2,
    duration: 2000,
    ease: 'Elastic.easeOut',
    onComplete: () => {
      console.log('Tween complete!');
    }
  });

  // Yoyo tween (ping-pong)
  this.tweens.add({
    targets: box,
    y: 100,
    duration: 1000,
    yoyo: true,
    repeat: -1
  });
}

Level 3: Tween Chains and Callbacks

create() {
  const sprite = this.add.sprite(400, 300, 'player');

  // Chain tweens
  this.tweens.add({
    targets: sprite,
    y: 200,
    duration: 500,
    ease: 'Linear',
    onComplete: () => {
      // Second tween
      this.tweens.add({
        targets: sprite,
        x: 600,
        duration: 500,
        ease: 'Linear',
        onComplete: () => {
          // Third tween
          this.tweens.add({
            targets: sprite,
            scale: 2,
            duration: 300,
            ease: 'Back.easeOut'
          });
        }
      });
    }
  });

  // Better approach: Timeline
  const timeline = this.tweens.timeline({
    targets: sprite,
    tweens: [
      {
        y: 200,
        duration: 500,
        ease: 'Linear'
      },
      {
        x: 600,
        duration: 500,
        ease: 'Linear'
      },
      {
        scale: 2,
        duration: 300,
        ease: 'Back.easeOut'
      }
    ]
  });

  // Timeline with delay
  this.tweens.timeline({
    tweens: [
      {
        targets: sprite,
        alpha: 0,
        duration: 500,
        offset: 0 // Start immediately
      },
      {
        targets: otherSprite,
        y: 400,
        duration: 500,
        offset: '-=250' // Start 250ms before previous ends
      }
    ]
  });
}

Level 4: Animation State Management

class AnimationController {
  private sprite: Phaser.GameObjects.Sprite;
  private currentAnim = '';

  constructor(sprite: Phaser.GameObjects.Sprite) {
    this.sprite = sprite;

    // Listen for animation completion
    this.sprite.on('animationcomplete', this.onAnimComplete, this);
  }

  play(animKey: string, ignoreIfPlaying = false) {
    if (ignoreIfPlaying && this.currentAnim === animKey) {
      return;
    }

    // Don't interrupt non-looping animations
    const current = this.sprite.anims.currentAnim;
    if (current && !current.repeat && current.isPlaying) {
      return;
    }

    this.sprite.play(animKey);
    this.currentAnim = animKey;
  }

  onAnimComplete(event: any) {
    if (event.key === 'attack') {
      this.play('idle');
    } else if (event.key === 'death') {
      this.sprite.setVisible(false);
    }
  }

  isPlaying(animKey: string): boolean {
    return this.sprite.anims.isPlaying && this.sprite.anims.currentAnim?.key === animKey;
  }

  getProgress(): number {
    return this.sprite.anims.getProgress();
  }
}

// In scene
create() {
  this.player = this.add.sprite(400, 300, 'player');
  this.animController = new AnimationController(this.player);
  this.animController.play('idle');
}

update() {
  if (this.cursors.left.isDown || this.cursors.right.isDown) {
    this.animController.play('walk');
  } else {
    this.animController.play('idle');
  }

  if (this.attackKey.isDown && !this.animController.isPlaying('attack')) {
    this.animController.play('attack');
  }
}

Level 5: Advanced Animation Systems

export class GameScene extends Phaser.Scene {
  private animManager!: AnimationManager;

  create() {
    this.animManager = new AnimationManager(this);

    // Define animation states
    this.animManager.registerState("player", "idle", {
      frames: { start: 0, end: 3 },
      frameRate: 8,
      loop: true,
    });

    this.animManager.registerState("player", "walk", {
      frames: { start: 4, end: 11 },
      frameRate: 12,
      loop: true,
    });

    this.animManager.registerState("player", "jump", {
      frames: { start: 12, end: 15 },
      frameRate: 10,
      loop: false,
      next: "idle",
    });

    this.animManager.registerState("player", "attack", {
      frames: { start: 16, end: 21 },
      frameRate: 15,
      loop: false,
      next: "idle",
      callback: this.onAttackComplete,
    });

    // Set up state transitions
    this.animManager.addTransition("player", "idle", "walk", () =>
      this.isMoving(),
    );
    this.animManager.addTransition(
      "player",
      "walk",
      "idle",
      () => !this.isMoving(),
    );
    this.animManager.addTransition("player", "walk", "jump", () =>
      this.justJumped(),
    );

    // Initialize player
    this.player = this.add.sprite(400, 300, "player");
    this.animManager.attach("player", this.player);
  }

  update() {
    this.animController.update("player");
  }

  isMoving() {
    return this.cursors.left.isDown || this.cursors.right.isDown;
  }

  justJumped() {
    return Phaser.Input.Keyboard.JustDown(this.jumpKey);
  }

  onAttackComplete() {
    console.log("Attack finished");
  }
}

class AnimationManager {
  private states = new Map<string, Map<string, AnimationState>>();
  private sprites = new Map<string, Phaser.GameObjects.Sprite>();
  private transitions: Array<{
    sprite: string;
    from: string;
    to: string;
    condition: () => boolean;
  }> = [];

  constructor(private scene: Phaser.Scene) {}

  registerState(sprite: string, key: string, config: any) {
    if (!this.states.has(sprite)) {
      this.states.set(sprite, new Map());
    }

    const stateConfig = {
      ...config,
      key: `${sprite}_${key}`,
    };

    // Create Phaser animation
    this.scene.anims.create({
      key: stateConfig.key,
      frames: this.scene.anims.generateFrameNumbers(sprite, config.frames),
      frameRate: config.frameRate,
      repeat: config.loop ? -1 : 0,
      yoyo: config.yoyo || false,
    });

    this.states.get(sprite)!.set(key, stateConfig);
  }

  attach(spriteKey: string, sprite: Phaser.GameObjects.Sprite) {
    this.sprites.set(spriteKey, sprite);
    sprite.play(`${spriteKey}_idle`);
  }

  addTransition(
    sprite: string,
    from: string,
    to: string,
    condition: () => boolean,
  ) {
    this.transitions.push({ sprite, from, to, condition });
  }

  update(spriteKey: string) {
    const sprite = this.sprites.get(spriteKey);
    if (!sprite) return;

    const currentAnim = sprite.anims.currentAnim?.key.replace(
      `${spriteKey}_`,
      "",
    );
    const states = this.states.get(spriteKey);

    // Check transitions
    for (const transition of this.transitions) {
      if (
        transition.sprite === spriteKey &&
        transition.from === currentAnim &&
        transition.condition()
      ) {
        this.play(spriteKey, transition.to);
        break;
      }
    }
  }

  play(spriteKey: string, stateKey: string) {
    const sprite = this.sprites.get(spriteKey);
    const state = this.states.get(spriteKey)?.get(stateKey);
    if (sprite && state) {
      sprite.play(state.key);
      if (state.callback) {
        sprite.once("animationcomplete-" + state.key, state.callback);
      }
    }
  }
}

Anti-Patterns

DON'T:

  • Create animations in update() - do it once in create()
  • Override playing animation without checking
  • Use tweens for simple value changes
  • Forget to clean up event listeners
  • Use
    tweens.chain()
    for simple sequences - use timeline
  • Ignore animation repeat/yoyo properties

DO:

  • Create animations once in create()
  • Check current animation before changing
  • Use tweens for property animation
  • Clean up listeners on shutdown
  • Use timeline for sequences
  • Configure repeat and yoyo appropriately

Code Patterns

Animation Playback Control

// Check if animation is playing
if (sprite.anims.isPlaying) {
  console.log("Playing:", sprite.anims.currentAnim?.key);
}

// Get animation progress (0-1)
const progress = sprite.anims.getProgress();

// Pause/resume animation
sprite.anims.pause();
sprite.anims.resume();

// Stop animation
sprite.anims.stop();

// Restart animation
sprite.anims.restart();

Tween Easing Functions

// Linear
this.tweens.add({ targets: obj, x: 100, ease: "Linear" });

// Power easing
this.tweens.add({ targets: obj, x: 100, ease: "Power0" }); // Linear
this.tweens.add({ targets: obj, x: 100, ease: "Power1" }); // Smooth
this.tweens.add({ targets: obj, x: 100, ease: "Power2" }); // Accelerating
this.tweens.add({ targets: obj, x: 100, ease: "Power3" }); // More accelerating
this.tweens.add({ targets: obj, x: 100, ease: "Power4" }); // Most accelerating

// Special easing
this.tweens.add({ targets: obj, x: 100, ease: "Elastic.easeOut" });
this.tweens.add({ targets: obj, x: 100, ease: "Bounce.easeOut" });
this.tweens.add({ targets: obj, x: 100, ease: "Back.easeOut" });
this.tweens.add({ targets: obj, x: 100, ease: "Cubic.easeOut" });

Tween Controls

const tween = this.tweens.add({
  targets: sprite,
  alpha: 0,
  duration: 1000,
});

// Pause tween
tween.pause();

// Resume tween
tween.resume();

// Stop tween
tween.stop();

// Complete tween immediately
tween.complete();

Common Easing Functions

EaseDescription
Linear
Constant speed
Power0
Same as Linear
Power1
Gentle ease
Power2
Medium ease
Power3
Strong ease
Power4
Strongest ease
Elastic
Bouncy effect
Bounce
Bounce at end
Back
Overshoot slightly
Sine
Smooth sine curve

Checklist

  • Animations created in create()
  • Frame rates appropriate for smoothness
  • Repeat/yoyo configured correctly
  • Transitions handled gracefully
  • Tweens use proper easing
  • Event listeners cleaned up
  • Animation state managed properly

Reference