A tiny, fast, TypeScript-first 2D web game engine.
~88 KB minified, ~27 KB gzip - zero runtime dependencies. Lightweight. Performant. Reliable.
Load the pre-bundled global build directly via a <script> tag.
All engine exports are automatically mapped to the browser's global namespace:
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<script src="dist/tinyts.js"></script>
<script>
const pos = vec2(100, 100);
engineStart({
size: { width: 480, height: 270 },
pixelated: true,
update(dt) {
if (keyDown("KeyD")) pos.x += 120 * dt;
if (keyDown("KeyA")) pos.x -= 120 * dt;
},
render() {
clear(Color.fromHSL(235, 0.45, 0.05));
drawRect(pos, vec2(16, 16), "#e94560");
drawText("USE A/D TO MOVE", vec2(240, 30), {
color: "#fff",
align: "center",
size: 16,
});
},
});
</script>
</body>
</html>Install via npm and import components into your build system:
import { engineStart, vec2, clear, drawRect, Color } from "@ujjwalvivek/tinyts";
engineStart({
size: { width: 640, height: 360 },
render() {
clear("#08080f");
drawRect(vec2(320, 180), vec2(32, 32), "#f0c040");
},
});Follow these steps to scaffold a project from scratch:
-
Initialize your project:
npm create vite@latest my-tinyts-game -- --template vanilla-ts cd my-tinyts-game npm install @ujjwalvivek/tinyts -
Configure your files:
Update
src/main.tswith this setup template:import { engineStart, vec2, clear, drawRect, keyDown } from "@ujjwalvivek/tinyts"; const pos = vec2(100, 100); const engine = engineStart({ size: { width: 480, height: 270 }, pixelated: true, update(dt) { // Input tracking if (keyDown("KeyD") || keyDown("ArrowRight")) pos.x += 120 * dt; if (keyDown("KeyA") || keyDown("ArrowLeft")) pos.x -= 120 * dt; }, render() { clear("#11111b"); // Clear background drawRect(pos, vec2(16, 16), "#89b4fa"); // Draw player }, }); // Mount engine canvas to index.html container const container = document.querySelector("#app"); if (container) { container.appendChild(engine.canvasManager.canvas); if (engine.overlayCanvas) { container.appendChild(engine.overlayCanvas); } }
Update
index.html:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>My TinyTS Game</title> <style> body { margin: 0; padding: 0; background: #1e1e2e; display: flex; align-items: center; justify-content: center; width: 100vw; height: 100vh; overflow: hidden; } #app { position: relative; width: 100%; height: 100%; } #app canvas { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); max-width: 95%; max-height: 95%; aspect-ratio: 16/9; } </style> </head> <body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> </body> </html>
-
Run the development server:
npm run dev
| Category | Description |
|---|---|
| Overall Architecture | Singleton-free, instance-bound, clean module boundaries |
| Game Loop | Fixed-timestep with accumulator, alpha interpolation, frame clamping |
| Rendering | Unified interface, optional WebGPU, WebGL2 batcher, Canvas2D fallback |
| Input | Bitmask state tracking, action mapping, gamepad, touch, keyboard and mouse |
| Audio | Full ADSR synth, voice stealing, groups, spatial audio, sequencer |
| Physics | Swept AABB, tilemap collision, spatial grid, verlet |
| ECS | View caching, component pooling, hierarchy, serialization, event hooks |
| Particles | Object pooling, emitter lifecycle, shapes, additive blending, prewarm |
| API Ergonomics | Dead-simple global helpers plus full OOP access for power users |
TinyTS implements a highly disciplined engine design thats fun to work with:
- Zero memory leaks on restart -
engineStop()truly dismantles everything. - Embeddable - can run inside an iframe or component without polluting the host page.
- Quad batching - 8192-quad batch buffer with pre-built index buffer
- High-Performance WebGPU backend - opt in with
webgpu: true; utilizes instanced rendering and a texture atlas. Falls back through WebGL2/Canvas2D when unavailable - State-cached uniforms - texture, shape, and shape params are tracked to avoid redundant GL calls
- GPU-batched lines -
drawLine()renders through the WebGL2 batcher as transformed quads - Canvas2D overlay - text and debug overlays use a synchronized Canvas2D layer
- Sprite batching - rotated/flipped sprites are pre-transformed on CPU and batched, avoiding per-sprite draw calls
- Renderer stats -
getRendererStats()and the debug overlay expose draw calls, batch flushes, switches, quads, and overlay calls - Default font - ships and loads the MIT-licensed TinyTS font from
dist/font/tinyTS.woff2 - View caching with reactive
updateEntityInCacheson add/remove - Component pooling via
obtain()withinit()/reset()hooks - Entity free list for ID recycling
- Serialization via
registerComponentType/serialize/deserialize - Event hooks via
onAdded/onRemovedwith unsubscribe handles - Voice pool with configurable max voices and voice stealing (oldest non-looping first)
- Audio groups (sfx, music, ambient) with per-group volume
- Spatial audio via
playSoundAtwith distance falloff and stereo panning Sequencerfor pattern-based music with MIDI-to-frequency conversion- Clip caching and
AudioClipabstraction
# Ensure Node.js is installed
# Clone the repository
git clone https://github.com/ujjwalvivek/tinyts.git
cd tinyts
npm install # Install package dependencies
npm run check # Typecheck, run ES module / CJS builds, and execute tests
npm run docs # Generate the API Documentation site locally
npm run serve # Start a local HTTP utility server to test examples| Scenario | Reliable? |
|---|---|
Drop a <script> tag, call engineStart(), draw stuff |
✅ Yes |
| Use as ES module with bundler | ✅ Yes |
| Pixel art games with integer scaling | ✅ Yes |
| Smooth HD games with fractional scaling | ✅ Yes |
| Start/stop/restart engine multiple times | ✅ Yes, zero leaks |
| Mobile browser with touch | ✅ Yes, on-screen touch controls work |
| Gamepad support | ✅ Yes, with deadzone |
| WebGPU/WebGL2 not available | ✅ Yes, auto-falls back to Canvas2D |
| Mix Canvas2D calls with engine rendering | ✅ Yes, getContext() exposes the raw context |
| Platform | Support |
|---|---|
| Chrome / Edge (desktop) | ✅ Full WebGL2 + Canvas2D |
| Firefox (desktop) | ✅ Full WebGL2 + Canvas2D |
| Safari 15+ (desktop) | ✅ WebGL2 + Canvas2D |
| Chrome / Firefox (Android) | ✅ WebGL2 + Canvas2D + Touch |
| Safari (iOS 15+) | ✅ WebGL2 + Canvas2D + Touch |
| Older browsers without WebGL2 | ✅ Canvas2D fallback |
| Node.js (SSR / testing) | ✅ Headless (math, ECS, physics work; rendering needs DOM) |
| Optimization | Present? |
|---|---|
| WebGL2 quad batching (single draw call per texture + shape) | ✅ |
| High-Performance WebGPU backend (instanced rendering + texture atlas) | ✅ |
| Pre-allocated batch Float32Array (no per-frame alloc) | ✅ |
| Pre-built static index buffer | ✅ |
| GPU-batched lines | ✅ |
| Texture caching | ✅ |
| State-cached uniforms (avoid redundant GL calls) | ✅ |
| Renderer instrumentation counters | ✅ |
| Object pooling (particles, ECS components) | ✅ |
| SDF shapes (no per-circle geometry) | ✅ |
imageSmoothingEnabled = false for pixel art |
✅ |
Framerate-independent damping (Math.pow(damp, dt * 60)) |
✅ |
| Spatial grid for broad-phase collision | ✅ |
MIT License.
TinyTS - Designed with zero bloat, maximum performance, and straightforward readability.
