Overview
This guide walks you through creating a new toy for the Stims Webtoys Library. Each toy is a self-contained module that exports astart function and integrates with the shared audio, rendering, and input systems.
Core Requirements
When building a toy, follow these essential conventions:- Place new toy modules under
assets/js/toys/and export astart(options)entry point - Export
start({ container, canvas?, audioContext? })—containeris the preferred target for rendering - Register the toy in
assets/data/toys.jsonwith a unique slug, label, and any default parameters - Load toys through
toy.html?toy=<slug>or a dedicated HTML entry point - Keep assets (textures, JSON data, audio snippets) in
assets/data/and reference them with relative paths - Run
bun run generate:toysafter metadata edits to regenerate derived artifacts - Run
bun run check:toysto verify schema validity and consistency - Run
bun run check:quickto validate types and code quality before opening a PR
Metadata Workflow
The toy registry follows a source-of-truth pattern: Authoritative metadata:assets/data/toys.json
Generated from metadata:
assets/js/data/toy-manifest.tspublic/toys.json
Toy Lifecycle Stages
Treat toys like live game content with explicit lifecycle stages intoys.json:
prototype: New or experimental toys that need fast iterationfeatured: Curated experiences that get the most attention and polish (5-8 toys max)archived: Stable toys that stay available but are not actively promoted
Featured Rotation
- Use
featuredRank(lower = higher priority) to drive the default “Featured” sort - Revisit the featured set every 4-6 weeks
- Schedule periodic quality passes on
featuredtoys (monthly) andprototypetoys (as needed) - Each pass should include performance verification, accessibility checks, and UI convention review
Quality Preset Guidelines
Quality presets are global and persist across toys. Toys should respond consistently:- Honor shared settings: Call
toy.updateRendererSettings()when settings change - Avoid hard-coded pixel ratios: Use preset-provided
maxPixelRatioandrenderScale - Scale expensive effects: Map presets to particle counts, post-processing strength, or shader iterations
- Battery saver preset: Keep under ~65% of default costs
- Hi-fi visuals preset: Increase costs modestly (~135%) without exceeding target frame times
- Persist in session: Rely on shared settings panel so users don’t reconfigure per toy
Custom Quality Presets
Use custom presets only when a toy needs additional fidelity tiers:Starter Template
Use this skeleton for new toys to standardize lifecycle hooks and cleanup:toy-template.ts
Adding a New Toy: Step-by-Step
Pick a slug
Choose a short, kebab-case slug (e.g.,
pocket-pulse) that becomes the toy.html?toy=<slug> route.Scaffold the module (recommended)
Use the scaffold script to automate setup:This creates:
assets/js/toys/<slug>.tswith typedToyStartFunctionstarter- Metadata entry in
assets/data/toys.json - Updated
docs/TOY_SCRIPT_INDEX.md - Minimal test in
tests/
docs/toys.md for contributor notes.Manual alternative (if skipping scaffold)
If you skip the scaffold script:
- Create
assets/js/toys/<slug>.tsand exportstart({ container, canvas?, audioContext? }) - Add entry to
assets/data/toys.json(includetitle,description,module,type,lifecycleStage) - Add slug row to
docs/TOY_SCRIPT_INDEX.md - Add short section to
docs/toys.md - Create
toys/<slug>.htmlonly if using a standalone page
Wire the runtime
Use Keep all DOM work scoped to the provided
createToyRuntimeStarter or createToyRuntime for audio, renderer, input, and settings panel behavior:container.Verify locally
Run
bun run dev and load http://localhost:5173/toy.html?toy=<slug> to confirm the toy loads, starts audio, and cleans up on exit.Manual spot-checks
- Confirm “Back to Library” control returns to grid and removes DOM nodes
- Verify microphone permission flows (granted, denied, sample-audio fallback)
- For WebGPU toys, test in both WebGPU-capable and non-WebGPU browsers
- Test on mobile devices or emulators for touch interactions
Audio Reactivity Tips
Normalizing Microphone Input
Use analyzer helpers to derive frequency bins, waveform data, and energy levels:Microphone Permission Flow
Reuse the centralized UI helper for mic access:- Granted: Start audio shows success, sample-audio action hidden
- Denied/timed out: Status shows error, sample-audio action visible
Rendering Patterns
- Keep animation state outside Three.js objects where possible
- Mutate objects inside a
tickloop driven byrequestAnimationFrame - Throttle expensive operations on resize
- Use
maxPixelRatiooption to keep frame times stable on high-DPI screens - Reuse materials and geometries instead of recreating each frame
- Dispose buffers and textures in the cleanup function:
Mobile and Interaction
- Test on touch devices or emulators — avoid hover-only interactions
- Gate device motion logic behind feature detection (
window.DeviceMotionEvent) - Ensure controls are keyboard-focusable with visible focus states
- Use
.control-panel__modefor custom buttons to guarantee 44×44px touch targets - Normalize pointer handling with
assets/js/utils/pointer-input.ts:
- Apply
.toy-canvasclass to fullscreen canvases - Include viewport meta tag:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> - Test at common breakpoints: 320×568, 375×812, 768×1024
Debugging Checklist
Visuals are blank
Visuals are blank
- Confirm canvas is attached to DOM
- Check renderer size matches
clientWidth/clientHeight - Verify camera position and lookAt target
- Check scene contains visible objects
Audio-driven behavior seems off
Audio-driven behavior seems off
- Log analyzer samples to verify data ranges
- Check microphone permissions in browser
- Verify
fftSizematches expected frequency resolution - Test with demo audio to isolate mic issues
Toy doesn't appear in selection UI
Toy doesn't appear in selection UI
- Validate entry in
assets/data/toys.json - Run
bun run generate:toysto regenerate manifest - Visit
toy.html?toy=<slug>directly to test loading
Settings Panel Checklist
When exposing toy-specific controls:- Quality controls: Respond to shared presets; expose additional toggles only when necessary
- Audio controls: Surface mic/demo audio status; avoid conflicting with shell-level controls
- Performance cues: Note performance-impacting toggles with helper text (e.g., “Lower GPU load”)
- Accessibility: Keep controls keyboard-focusable, label every toggle, maintain visible focus styles
Documentation
- Update
README.mdandCONTRIBUTING.mdif introducing new scripts or global expectations - Describe user-facing controls in the HTML entry point if they deviate from patterns
- Include inline comments for novel shader parameters, math tricks, or input quirks
- When wrapping HTML pages, expose through
startPageToyand add slug toassets/data/toys.json
Next Steps
Toy Interface
Learn about TypeScript interfaces for toy lifecycle
Testing Toys
Write automated tests for your toy
Toy Manifest
Understand toys.json structure and metadata
Audio System
Deep dive into audio analysis utilities