Skip to main content

Overview

Every toy in the Stims library implements a standard interface that defines how it starts, runs, and cleans up. This page documents the exact TypeScript types exported from assets/js/core/toy-interface.ts.

ToyStartFunction

The main entry point for a toy module. Each toy exports a start function that matches this signature:
export type ToyStartFunction = (
  options?: ToyStartOptions,
) => Promise<ToyInstance> | ToyInstance;
Key points:
  • Can return either a ToyInstance directly or a Promise<ToyInstance>
  • Takes an optional ToyStartOptions parameter
  • Synchronous or asynchronous execution supported

Usage Example

import type { ToyStartFunction } from '../core/toy-interface';

export const start: ToyStartFunction = ({ container, canvas, audioContext }) => {
  // Initialize your toy
  
  return {
    dispose: () => {
      // Clean up resources
    },
  };
};

ToyStartOptions

Configuration object passed to the toy’s start function:
container
HTMLElement | null
default:"undefined"
The container element where the toy should render its canvas and UI.If not provided, the toy may default to full-screen behavior or throw an error depending on implementation.Best practice: Always use the provided container for DOM operations to ensure proper cleanup.
canvas
HTMLCanvasElement | null
default:"undefined"
An optional existing canvas element to use for rendering.If provided, the toy should respect its size or the container’s size. Most toys create their own canvas within the container.
audioContext
AudioContext
default:"undefined"
Optional Web Audio API context to share across multiple toys or with the main app.Sharing a context allows audio to continue seamlessly when switching toys and avoids browser limits on concurrent audio contexts.

Complete Interface

toy-interface.ts
export interface ToyStartOptions {
  /**
   * The container element where the toy should render its canvas and UI.
   * If not provided, the toy may default to a full-screen behavior or throw an error depending on implementation.
   */
  container?: HTMLElement | null;

  /**
   * An optional existing canvas to use. If provided, the toy should likely respect its size or the container's size.
   */
  canvas?: HTMLCanvasElement | null;

  /**
   * Optional AudioContext to share across multiple toys or with the main app.
   */
  audioContext?: AudioContext;
}

ToyInstance

The object returned by a toy’s start function. Defines methods for controlling and cleaning up the toy:
dispose
() => void
required
Cleans up all resources (audio, WebGL, event listeners).After calling this, the toy should not be used again.Must be implemented. This is the only required method on a ToyInstance.
pause
() => void
Pauses the animation loop and audio processing.Optional. Implement this if your toy supports pausing without full disposal.
resume
() => void
Resumes the animation loop and audio processing.Optional. Implement alongside pause for pause/resume support.
updateOptions
(options: Record<string, unknown>) => void
Updates configuration parameters dynamically.Optional. Use this to allow external controls to modify toy behavior at runtime without restarting.

Complete Interface

toy-interface.ts
export interface ToyInstance {
  /**
   * Cleans up all resources (audio, webgl, event listeners).
   * After calling this, the toy should not be used again.
   */
  dispose(): void;

  /**
   * Optional: Pauses the animation loop and audio processing.
   */
  pause?(): void;

  /**
   * Optional: Resumes the animation loop and audio processing.
   */
  resume?(): void;

  /**
   * Optional: Updates configuration parameters dynamically.
   */
  updateOptions?(options: Record<string, unknown>): void;
}

Implementation Patterns

Basic Implementation

Minimal toy with just dispose:
import type { ToyStartFunction, ToyInstance } from '../core/toy-interface';

export const start: ToyStartFunction = ({ container }): ToyInstance => {
  const canvas = document.createElement('canvas');
  container?.appendChild(canvas);
  
  let running = true;
  
  function animate() {
    if (!running) return;
    // Animation logic
    requestAnimationFrame(animate);
  }
  
  animate();
  
  return {
    dispose: () => {
      running = false;
      canvas.remove();
    },
  };
};

With Pause/Resume

Toy that supports pausing:
import type { ToyStartFunction, ToyInstance } from '../core/toy-interface';

export const start: ToyStartFunction = ({ container }): ToyInstance => {
  let animationId: number | null = null;
  let isPaused = false;
  
  function animate(time: number) {
    if (isPaused) return;
    
    // Animation logic
    
    animationId = requestAnimationFrame(animate);
  }
  
  animationId = requestAnimationFrame(animate);
  
  return {
    pause: () => {
      isPaused = true;
      if (animationId !== null) {
        cancelAnimationFrame(animationId);
        animationId = null;
      }
    },
    
    resume: () => {
      if (!isPaused) return;
      isPaused = false;
      animationId = requestAnimationFrame(animate);
    },
    
    dispose: () => {
      isPaused = true;
      if (animationId !== null) {
        cancelAnimationFrame(animationId);
      }
      // Additional cleanup
    },
  };
};

With Dynamic Options

Toy that accepts runtime configuration updates:
import type { ToyStartFunction, ToyInstance } from '../core/toy-interface';

interface MyToyOptions {
  speed?: number;
  color?: string;
  particleCount?: number;
}

export const start: ToyStartFunction = ({ container }): ToyInstance => {
  let options: MyToyOptions = {
    speed: 1,
    color: '#ffffff',
    particleCount: 100,
  };
  
  function rebuildParticles() {
    // Recreate particle system with new count
  }
  
  function animate(time: number) {
    // Use options.speed and options.color
    requestAnimationFrame(animate);
  }
  
  animate(0);
  
  return {
    updateOptions: (newOptions: Record<string, unknown>) => {
      const oldCount = options.particleCount;
      options = { ...options, ...newOptions } as MyToyOptions;
      
      // Rebuild if particle count changed
      if (options.particleCount !== oldCount) {
        rebuildParticles();
      }
    },
    
    dispose: () => {
      // Cleanup
    },
  };
};

Async Initialization

Toy that loads assets before starting:
import type { ToyStartFunction, ToyInstance } from '../core/toy-interface';

export const start: ToyStartFunction = async ({ container }): Promise<ToyInstance> => {
  // Load textures, models, or other assets
  const texture = await loadTexture('/assets/texture.png');
  const model = await loadModel('/assets/model.gltf');
  
  function animate(time: number) {
    // Use loaded assets
    requestAnimationFrame(animate);
  }
  
  animate(0);
  
  return {
    dispose: () => {
      texture.dispose();
      model.dispose();
      // Additional cleanup
    },
  };
};

Using with Runtime Starters

Most toys use createToyRuntimeStarter or createAudioToyStarter which handle the boilerplate:
import { createToyRuntimeStarter } from '../utils/toy-runtime-starter';
import type { ToyStartFunction } from '../core/toy-interface';

export const start: ToyStartFunction = ({ container }) => {
  let runtime: ToyRuntimeInstance;
  
  const startRuntime = createToyRuntimeStarter({
    toyOptions: {
      cameraOptions: { position: { z: 50 } },
    },
    audio: { fftSize: 256 },
    plugins: [
      {
        name: 'my-toy',
        setup: (runtimeInstance) => {
          runtime = runtimeInstance;
          // Initialize scene
        },
        update: ({ frequencyData, time }) => {
          // Animation loop
        },
        dispose: () => {
          // Cleanup
        },
      },
    ],
  });
  
  runtime = startRuntime({ container });
  
  return {
    dispose: () => {
      runtime.dispose();
    },
  };
};
The runtime starters automatically return a ToyInstance with proper dispose implementation, so you can return runtime directly or wrap it with additional cleanup logic.

Type Safety Tips

Enforce Return Type

Explicitly type your start function to catch return type errors:
import type { ToyStartFunction } from '../core/toy-interface';

// TypeScript will error if you forget to return dispose
export const start: ToyStartFunction = ({ container }) => {
  return {
    dispose: () => {
      // Required!
    },
  };
};

Custom Instance Extensions

Extend ToyInstance for toy-specific methods:
import type { ToyInstance } from '../core/toy-interface';

interface MyToyInstance extends ToyInstance {
  setMode(mode: 'burst' | 'bloom' | 'vortex'): void;
  getCurrentMode(): string;
}

export const start = ({ container }): MyToyInstance => {
  let currentMode = 'burst';
  
  return {
    dispose: () => {},
    setMode: (mode) => { currentMode = mode; },
    getCurrentMode: () => currentMode,
  };
};

Cleanup Best Practices

Always implement dispose properly to avoid memory leaks and dangling event listeners.
Common cleanup tasks:
dispose: () => {
  // 1. Stop animation loops
  if (animationId) cancelAnimationFrame(animationId);
  
  // 2. Remove event listeners
  window.removeEventListener('resize', handleResize);
  canvas.removeEventListener('click', handleClick);
  
  // 3. Close audio context (if owned by toy)
  audioContext?.close();
  
  // 4. Dispose Three.js resources
  disposeGeometry(geometry);
  disposeMaterial(material);
  renderer?.dispose();
  
  // 5. Remove DOM elements
  canvas?.remove();
  
  // 6. Clear references
  runtime = null;
  particles = null;
}

Next Steps

Toy Development

Complete guide to building toys

Testing Toys

Write tests for toy lifecycle

Toy Lifecycle

Understand runtime behavior

Audio System

Integrate audio reactivity

Build docs developers (and LLMs) love