JSPM

  • Created
  • Published
  • Downloads 230
  • Score
    100M100P100Q108857F
  • License O'Saasy

Annie animation player that runs in the browser

Package Exports

  • @effing/annie-player
  • @effing/annie-player/react

Readme

@effing/annie-player

Browser player for Annie animations.

Part of the Effing family — programmatic video creation with TypeScript.

Play TAR archives of PNG or JPEG frames in the browser using canvas. Framework-agnostic core with optional React component.

Installation

npm install @effing/annie-player

Quick Start

React

import { AnniePlayer } from "@effing/annie-player/react";

function App() {
  return (
    <AnniePlayer
      src="https://example.com/animation.tar"
      height={540}
      autoLoad
      autoPlay
      fps={30}
    />
  );
}

The React component includes built-in inline styles for:

  • Wrapper positioning
  • Load/Play/Pause buttons with hover states
  • Status overlay
  • Canvas styling

To customize the root container, use className and style props:

<AnniePlayer
  src="https://example.com/animation.tar"
  height={540}
  className="my-player"
  style={{ border: "2px solid blue" }}
/>

Compound Components

For full control over styling and layout, use compound components:

import { AnniePlayer } from "@effing/annie-player/react";

function CustomPlayer() {
  return (
    <AnniePlayer.Root
      src="https://example.com/animation.tar"
      height={540}
      autoLoad
      autoPlay
    >
      <AnniePlayer.Wrapper style={{ border: "2px solid blue" }}>
        <AnniePlayer.Canvas style={{ backgroundColor: "#000" }} />
        <AnniePlayer.Controls
          style={{ position: "absolute", bottom: 16, top: "auto" }}
        >
          <AnniePlayer.LoadButton />
          <AnniePlayer.PlayButton />
          <AnniePlayer.PauseButton />
        </AnniePlayer.Controls>
        <AnniePlayer.Status />
      </AnniePlayer.Wrapper>
    </AnniePlayer.Root>
  );
}

Headless (Render Function)

For maximum control, use the render function pattern:

import { AnniePlayer } from "@effing/annie-player/react";

function HeadlessPlayer() {
  return (
    <AnniePlayer.Root src="https://example.com/animation.tar" height={540}>
      {({ state, actions, canvasRef, canvasDimensions }) => (
        <div className="my-custom-player">
          <canvas
            ref={canvasRef}
            width={canvasDimensions.width}
            height={canvasDimensions.height}
          />
          <div className="my-controls">
            <button onClick={actions.load} disabled={state.isLoading}>
              {state.isLoading ? "Loading..." : "Load"}
            </button>
            <button
              onClick={actions.play}
              disabled={!state.frameCount || state.isPlaying}
            >
              Play
            </button>
            <button onClick={actions.pause} disabled={!state.isPlaying}>
              Pause
            </button>
          </div>
          <p className="my-status">{state.status}</p>
          {state.error && <p className="my-error">Error: {state.error}</p>}
        </div>
      )}
    </AnniePlayer.Root>
  );
}

Available Sub-components

Component Description
AnniePlayer.Root Provider that manages player state. Accepts src, height, defaultWidth, fps, autoLoad, autoPlay.
AnniePlayer.Wrapper Container with hover detection. Sets data-hovering and data-playing attributes.
AnniePlayer.Canvas The canvas element where frames are rendered.
AnniePlayer.Controls Container for buttons. Auto-hides when playing (unless hovering).
AnniePlayer.LoadButton Load button with default "Load"/"Loading..." text.
AnniePlayer.PlayButton Play button with default "Play" text.
AnniePlayer.PauseButton Pause button with default "Pause" text.
AnniePlayer.Status Status overlay showing current state.

Vanilla JavaScript

import { AnniePlayerCore } from "@effing/annie-player";

const canvas = document.getElementById("canvas") as HTMLCanvasElement;

const player = new AnniePlayerCore({
  src: "https://example.com/animation.tar",
  fps: 30,
  autoPlay: true,
});

player.attachCanvas(canvas);
player.on("load", ({ frameCount, dimensions }) => {
  canvas.width = dimensions.width;
  canvas.height = dimensions.height;
});

await player.load();

API Overview

AnniePlayerCore

class AnniePlayerCore {
  constructor(options: AnniePlayerOptions);

  // Canvas management
  attachCanvas(canvas: HTMLCanvasElement): void;
  detachCanvas(): void;

  // Lifecycle
  load(): Promise<void>;
  destroy(): void;

  // Playback
  play(): void;
  pause(): void;
  stop(): void;

  // State
  getState(): AnniePlayerState;

  // Events
  on<K extends keyof AnniePlayerEvents>(
    event: K,
    callback: (data: AnniePlayerEvents[K]) => void,
  ): () => void;
}

Options:

Option Type Default Description
src string required URL to TAR archive
fps number 30 Playback frame rate
autoPlay boolean false Start playing after load

State:

type AnniePlayerState = {
  status: string;
  error: string | null;
  isLoading: boolean;
  isPlaying: boolean;
  frameCount: number;
  dimensions: { width: number; height: number } | null;
};

Events:

Event Data Description
statechange AnniePlayerState State updated
load { frameCount, dimensions } Frames loaded
error Error Error occurred

AnniePlayer (React)

<AnniePlayer
  src={string} // TAR archive URL
  height={number} // Canvas height
  defaultWidth={number} // Width before load (default: same as height)
  autoLoad={boolean} // Load on mount (default: false)
  autoPlay={boolean} // Play after load (default: false)
  fps={number} // Frame rate (default: 30)
  className={string} // Class name for root container
  style={CSSProperties} // Style for root container
/>

The React component:

  • Manages core lifecycle automatically
  • Adjusts canvas width based on loaded frame aspect ratio
  • Shows load/play/pause buttons with built-in styling
  • Displays status overlay on hover

Examples

Custom Controls

const player = new AnniePlayerCore({ src, fps: 30 });
player.attachCanvas(canvas);

loadButton.onclick = () => player.load();
playButton.onclick = () => player.play();
pauseButton.onclick = () => player.pause();

player.on("statechange", (state) => {
  playButton.disabled = state.isPlaying || state.frameCount === 0;
  pauseButton.disabled = !state.isPlaying;
  statusText.textContent = state.status;
});