# uPixi — UbuntuPlay Visual Library

**Version:** 1.0.0  
**Size:** 28 KB (9 KB minified + gzipped)  
**Dependency:** PixiJS 7.x (`/js/pixi.min.js`, loaded first)  
**Path:** `/js/upixi/upixi.min.js` (also at `classroom/public/js/upixi/upixi.min.js`)

uPixi is a first-party visual library for the UbuntuPlay ecosystem. It wraps PixiJS 7 with convenience layers for responsive scaling, unified input, grid rendering, particles, audio synthesis, UI components, tweening, state management, and platform integrations (analytics, i18n, storage, score sync).

All games deployed on the platform (Assegai, Chenjela, Madzi, Mankwala, Nzelu, Rangi) should use uPixi for consistent rendering and platform access.

---

## Table of Contents

1. [Getting Started](#getting-started)
2. [uPixi.App](#upixiapp)
3. [uPixi.Stage](#upixistage)
4. [uPixi.Input](#upixiinput)
5. [uPixi.GridRenderer](#upixigridrenderer)
6. [uPixi.Particles](#upixiparticles)
7. [uPixi.Audio](#upixiaudio)
8. [uPixi.UI](#upixiui)
9. [uPixi.Platform](#upixiplatform)
10. [uPixi.State](#upixistate)
11. [uPixi.Tween](#upixitween)
12. [Game Migration Guide](#game-migration-guide)
13. [Architecture Diagram](#architecture-diagram)
14. [Best Practices](#best-practices)

---

## Getting Started

```html
<!-- Load PixiJS first, then uPixi -->
<script src="/js/pixi.min.js"></script>
<script src="/js/upixi/upixi.min.js"></script>
```

**Minimal app:**
```javascript
const app = new uPixi.App({ width: 600, height: 800 });
const gfx = new PIXI.Graphics();
app.stage.add(gfx.value);
gfx.beginFill(0x40916C);
gfx.drawRect(100, 100, 50, 50);
gfx.endFill();

app.tick(() => {
  // game loop runs every frame
});
```

**Full game setup with all modules:**
```javascript
const app = new uPixi.App({ width: 600, height: 800 });
const stage = app.stage;
const input = app.input;
const audio = new uPixi.Audio();
const particles = new uPixi.Particles(stage);

// Graphics layers
const bg = stage.layer();
const gameLayer = stage.layer();

// Register tick callback
app.tick(function gameLoop(dt) {
  particles.update();
  particles.draw();
  renderScene();
});
```

---

## uPixi.App

Creates a PixiJS application with a managed, responsive game stage. The stage automatically scales to fit the viewport while maintaining the aspect ratio.

### Constructor

```javascript
const app = new uPixi.App(opts);
```

| Option | Default | Description |
|--------|---------|-------------|
| `width` | `600` | Logical game width (in-game coordinate space) |
| `height` | `800` | Logical game height |
| `backgroundColor` | `0x0a1214` | Background color (hex) |
| `antialias` | `true` | Enable WebGL antialiasing |
| `parent` | `document.body` | Container element or CSS selector |

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `app.canvas` | `HTMLCanvasElement` | The created canvas element |
| `app.stage` | `uPixi.Stage` | The managed, scaled stage |
| `app.input` | `uPixi.Input` | Unified input manager (lazy-initialized on first access) |
| `app.pixi` | `PIXI.Application` | The underlying PixiJS application |
| `app.logicalWidth` | `number` | Logical game width |
| `app.logicalHeight` | `number` | Logical game height |

### Methods

```javascript
app.tick(fn)        // Register a frame callback (added to PIXI ticker)
app.untick(fn)      // Remove a frame callback
app.destroy()       // Destroy app, canvas, and input, clean up resize listener
```

### Example

```javascript
const app = new uPixi.App({
  width: 600,
  height: 800,
  backgroundColor: 0x0a1214,
  parent: document.body,
});

app.tick((delta) => {
  // delta is PIXI ticker delta (1 = 60fps)
  updateGame(delta);
});
```

---

## uPixi.Stage

The stage manages a `PIXI.Container` that is scaled to fit the viewport while maintaining aspect ratio. It automatically centers with letterbox bars.

### Constructor

```javascript
const stage = new uPixi.Stage(root, logicalWidth, logicalHeight);
```

| Parameter | Description |
|-----------|-------------|
| `root` | The PIXI stage or parent container |
| `logicalWidth` | Logical game width |
| `logicalHeight` | Logical game height |

You generally do not construct a Stage manually. `new uPixi.App()` creates one for you at `app.stage`.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `stage.root` | `PIXI.Container` | Root PIXI container |
| `stage.container` | `PIXI.Container` | The scaled child container |
| `stage.scale` | `number` | Current scale factor |
| `stage.offsetX` | `number` | Current horizontal letterbox offset |
| `stage.offsetY` | `number` | Current vertical letterbox offset |
| `stage.logicalWidth` | `number` | Logical game width |
| `stage.logicalHeight` | `number` | Logical game height |

### Methods

```javascript
// Recalculate scale and position on resize (called automatically by App)
stage.resize(pixiApp)

// Convert screen pixel coordinates to game logical coordinates
const { x, y } = stage.screenToGame(clientX, clientY)

// Add/remove child display objects to the scaled container
stage.add(child)
stage.remove(child)

// Create a persistent Graphics layer (automatically added to container)
const gfx = stage.layer()

// Convenience: draw to stage via helper
stage.add(somePIXIObject)
```

### Coordinate System

When using `uPixi.App`, all game logic uses the *logical* coordinate system (e.g., 600×800). The Stage handles:
- Scaling to fit the viewport
- Centering with letterbox bars
- Screen-to-game coordinate conversion (used by Input)

---

## uPixi.Input

Unified touch + mouse input with coordinate mapping through the stage. Handles multi-touch tracking and coordinate transformation automatically.

### Constructor

```javascript
const input = new uPixi.Input(canvas, stage);
```

You generally access input via `app.input` (lazy-initialized on first use).

### Events

```javascript
app.input.on('down', (x, y, nativeEvent) => {
  // Called on mousedown or touchstart
  // x, y are in game logical coordinates
});

app.input.on('move', (x, y, nativeEvent) => {
  // Called on mousemove (while down) or touchmove (while down)
});

app.input.on('up', (x, y) => {
  // Called on mouseup or touchend
});
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `input.isDown` | `boolean` | Whether a pointer is currently pressed |
| `input.lastX` | `number` | Last recorded X in game coordinates |
| `input.lastY` | `number` | Last recorded Y in game coordinates |

### Methods

```javascript
// Register/unregister event listeners
input.on('down', handlerFn)
input.off('down', handlerFn)
```

### Example — Drag Drawing

```javascript
app.input.on('down', (x, y) => {
  isDrawing = true;
  addPoint(x, y);
});

app.input.on('move', (x, y) => {
  if (isDrawing) addPoint(x, y);
});

app.input.on('up', () => {
  isDrawing = false;
});
```

### How Coordinate Mapping Works

1. Browser event provides `clientX`/`clientY` (viewport pixels)
2. Subtract canvas bounding rect to get canvas-local pixels
3. Pass through `stage.screenToGame()` to convert to logical coordinates
4. Result: coordinates in your game's logical space (e.g., 0–600, 0–800)

---

## uPixi.GridRenderer

Efficient tile-based rendering for grid games (fluid simulation, tile maps, Sokoban, etc.). Batches all draw calls per frame.

### Constructor

```javascript
const grid = new uPixi.GridRenderer(parent, opts);
```

| Option | Default | Description |
|--------|---------|-------------|
| `cols` | `60` | Number of columns |
| `rows` | `80` | Number of rows |
| `cellSize` | `10` | Pixel size of each cell |

`parent` can be a `uPixi.Stage`, a `PIXI.Container`, or any container.

### Methods

```javascript
// Full batch render with a draw function
grid.render((gfx, col, row, cellSize) => {
  const cellValue = myGrid[col][row];
  if (cellValue === 1) {
    uPixi.GridRenderer.rect(gfx, col, row, cellSize, 0x8B5E3C);
  } else if (cellValue === 2) {
    uPixi.GridRenderer.circle(gfx, col, row, cellSize, 0x495057, 4);
  }
});

// Mark dirty (for future optimised updates)
grid.dirty();

// Clear all cells
grid.clearCells();
```

### Static Helpers

```javascript
// Draw a filled rectangle at cell (col, row)
uPixi.GridRenderer.rect(gfx, col, row, cellSize, color, alpha?)

// Draw a filled circle at cell center
uPixi.GridRenderer.circle(gfx, col, row, cellSize, color, radius?, alpha?)
```

### Example

```javascript
const grid = new uPixi.GridRenderer(app.stage, {
  cols: 30, rows: 40, cellSize: 20
});

function renderGrid() {
  grid.render((g, c, r, s) => {
    const val = map[c][r];
    if (val === 0) return;
    uPixi.GridRenderer.rect(g, c, r, s, val === 1 ? 0x40916C : 0x168AAD);
  });
}
```

---

## uPixi.Particles

Lightweight particle system that draws circles on a shared `PIXI.Graphics` layer. Automatically manages a particle pool with a cap.

### Constructor

```javascript
const particles = new uPixi.Particles(parent);
```

| Parameter | Description |
|-----------|-------------|
| `parent` | `uPixi.Stage` or `PIXI.Container` to attach the graphics layer to |

### Properties

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `particles.gfx` | `PIXI.Graphics` | — | The shared graphics layer |
| `particles.maxParticles` | `number` | `300` | Maximum particles before culling |
| `particles.particles` | `Array` | `[]` | Active particle objects |

### Methods

```javascript
// Emit a single particle
particles.emit(x, y, {
  vx: 0, vy: 0,          // velocity
  size: 2,               // radius in pixels
  color: '#FFFFFF',       // hex color string
  gravity: 0.12,          // vertical acceleration per frame
  decay: 0.025,           // life reduction per frame
  life: 1.0,              // starting life (0-1)
});

// Emit a burst of particles
particles.burst(x, y, count = 10, {
  spread: 2,              // random velocity spread
  sizeRange: [1, 3],      // [min, max] size
  color: '#FFFFFF',
  gravity: 0.12,
  decay: 0.025,
});

// Update all particles (call each frame in game loop)
particles.update();

// Draw all particles to the graphics layer (call each frame)
particles.draw();
```

### Example — Dirt Particles on Dig

```javascript
function onDig(x, y) {
  particles.burst(x, y, 5, {
    color: '#A47551',
    spread: 1.5,
    sizeRange: [1, 3],
    gravity: 0.15,
    decay: 0.03,
  });
}

// In game loop:
app.tick(() => {
  particles.update();
  particles.draw();
});
```

---

## uPixi.Audio

WebAudio tone synthesis engine. No audio files needed — generates sounds programmatically. Includes mute persistence via `localStorage`.

### Constructor

```javascript
const audio = new uPixi.Audio();
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `audio.muted` | `boolean` | Whether audio is muted |

### Methods

```javascript
// Core tone synthesis
audio.tone(freq, duration = 0.1, type = 'sine', volume = 0.12)
audio.chime()          // Gem/chime collect sound (double-tone)
audio.click()          // UI click feedback
audio.buzz()           // Error/wrong sound (sawtooth)
audio.fanfare()        // Level complete fanfare (4-note ascending)
audio.trickle()        // Random trickle/water sound

// Toggle mute (persists to localStorage)
const isMuted = audio.toggleMute()

// Direct mute control
audio.muted = true
audio.muted = false
```

All methods return `this` for chaining.

### Example

```javascript
const audio = new uPixi.Audio();

// In game:
audio.click();                  // UI click
audio.chime();                  // collect item
audio.fanfare();                // level complete
audio.buzz();                   // wrong answer
audio.trickle();                // ambient effect
audio.pop();                    // bubble/pop effect

// Mute toggle button:
muteBtn.onclick = () => {
  const muted = audio.toggleMute();
  muteBtn.textContent = muted ? '' : '';
};

// Direct mute control:
audio.setMuted(true);
const isMuted = audio.isMuted();
```

### Tone Reference

| Sound | Frequency | Duration | Type | Volume | Method |
|-------|-----------|----------|------|--------|--------|
| Click | 550 Hz | 60 ms | sine | 0.08 | `click()` |
| Chime | 880 → 1320 Hz | 100+150 ms | sine | 0.1 | `chime()` |
| Buzz | 160 Hz | 350 ms | sawtooth | 0.12 | `buzz()` |
| Fanfare | 523→659→784→1047 Hz | 4×280 ms | sine | 0.1 | `fanfare()` |
| Trickle | 180–400 Hz | 60 ms | sine | 0.02 | `trickle()` |
| Pop | 600–1000 Hz | 80 ms | sine | 0.07 | `pop()` |
| Custom | any | any | any | any | `tone(freq, dur, type, vol)` |

---

## uPixi.UI

DOM-based UI components: toast notifications, confetti celebration, Gogo helper bubble. Creates elements on first use and reuses them.

### Methods

```javascript
// Toast notification (auto-dismisses)
uPixi.UI.toast(message, type = 'success', duration = 2200)
// type: 'success' (green border) or 'bad' (red border)

// Confetti celebration
uPixi.UI.confetti(count = 40)

// Gogo mascot helper bubble (auto-dismisses)
uPixi.UI.gogo(message, duration = 4000)

// Hide Gogo bubble early
uPixi.UI.hideGogo()
```

### Example

```javascript
// Level complete
uPixi.UI.toast('Level Complete!');
uPixi.UI.confetti(50);

// Error
uPixi.UI.toast('Not enough water collected', 'bad');

// Gogo hint
uPixi.UI.gogo('Try digging deeper to find the water source!');
```

### Styling

The UI elements are auto-generated DOM elements with inline styles. They use `position: fixed` and appear at standard locations:
- **Toast:** Top center (below any header)
- **Confetti:** Full viewport (animated CSS particles)
- **Gogo bubble:** Bottom center

---

## uPixi.Platform

Integration layer for UbuntuPlay platform services: analytics, internationalization, progress storage, and score syncing.

### Methods

```javascript
// Track analytics event
uPixi.Platform.track(event, category, data?)
//   event: string (e.g., 'game_open', 'level_complete', 'quiz_attempt')
//   category: string (e.g., 'madzi', 'nzelu')
//   data: object (optional, e.g., { level: 3, stars: 2 })
//
// Delegates to window.UbuntuAnalytics.track() if available.
// Gracefully degrades if analytics is not loaded.

// Translate a key via i18n
uPixi.Platform.t(key)
//   key: string (e.g., 'nzelu.ask_gogo')
//   Returns: translated string or key if i18n not loaded

// Save level progress to localStorage
uPixi.Platform.saveProgress(gameId, data)
//   gameId: string (e.g., 'madzi', 'assegai')
//   data: object (any JSON-serializable data)
//   Storage key: upixi_prog_{gameId}

// Load level progress from localStorage
const progress = uPixi.Platform.loadProgress(gameId)
//   Returns: parsed object or {} on failure

// Sync score to server
uPixi.Platform.syncScore(token, game, score, metadata?)
//   token: auth token (from localStorage or sessionStorage)
//   game: string identifier
//   score: number
//   metadata: optional extra data
```

### Example

```javascript
// Track game open
uPixi.Platform.track('game_open', 'nzelu');

// Track level attempt
uPixi.Platform.track('quiz_attempt', 'madzi', {
  questionId: 'madzi_c1_l3',
  category: 'Science',
  zone: 'Chapter 1'
});

// Save progress
uPixi.Platform.saveProgress('rangi', {
  levelsCompleted: [1, 2, 3],
  highScore: 1500
});

// Load progress
const saved = uPixi.Platform.loadProgress('rangi');

// Sync score
const token = localStorage.getItem('up_student_token');
uPixi.Platform.syncScore(token, 'rangi', totalScore, {
  level: 5,
  timeSec: 120
});
```

---

## uPixi.State

Lightweight finite state machine for managing game screens.

### Constructor

```javascript
const fsm = new uPixi.State('menu');
```

### Methods

```javascript
// Register a state with enter/exit handlers
fsm.add('menu', {
  enter() { showMenuScreen(); },
  exit() { hideMenuScreen(); },
});

fsm.add('play', {
  enter(levelNum) {
    startLevel(levelNum);
  },
  exit() {
    cleanupLevel();
  },
});

// Transition to a state (calls exit on current, enter on next)
// Additional args are passed to the enter handler
fsm.go('play', 1);

// Properties
console.log(fsm.current);   // 'play'
console.log(fsm.previous);  // 'menu'
```

### Example

```javascript
const state = new uPixi.State('loading');

state.add('loading', {
  enter() { showLoadingBar(); },
  exit() { hideLoadingBar(); },
});

state.add('menu', {
  enter() { showMainMenu(); },
});

state.add('playing', {
  enter(level) { loadLevel(level); },
  exit() { cleanupLevel(); },
});

state.add('gameover', {
  enter(score) { showGameOver(score); },
});

// Usage
state.go('playing', 3);
// → exits 'menu', enters 'playing' with arg 3
```

---

## uPixi.Tween

Simple numeric tweening for animations. Interpolates a value over time with easing, yo-yo, and repeat support.

### Constructor

```javascript
const tween = new uPixi.Tween(from, to, duration = 500, opts);
```

| Option | Default | Description |
|--------|---------|-------------|
| `from` | — | Starting value |
| `to` | — | Ending value |
| `duration` | `500` | Duration in milliseconds |
| `ease` | `'linear'` | Easing function: `'linear'`, `'easeOut'`, `'easeIn'`, `'easeInOut'` |
| `yoyo` | `false` | Ping-pong animation |
| `repeat` | `0` | Number of repeats (0 = once) |

### Methods

```javascript
tween.onUpdate(value => { /* called each frame with interpolated value */ })
tween.onComplete(() => { /* called when animation finishes */ })
tween.start()
tween.stop()

// In game loop:
app.tick((dt) => {
  tween.update(dt);  // dt in milliseconds (PIXI ticker delta)
});

// Read current value
console.log(tween.value);
```

### Example — Fade In

```javascript
const fade = new uPixi.Tween(0, 1, 500, { ease: 'easeOut' });
fade.onUpdate(v => { sprite.alpha = v; });
fade.onComplete(() => { sprite.interactive = true; });
fade.start();

// In game loop:
app.tick((dt) => fade.update(dt));
```

### Example — Pulse (yo-yo)

```javascript
const pulse = new uPixi.Tween(0.8, 1.0, 400, {
  ease: 'easeInOut',
  yoyo: true,
  repeat: 3,
});
pulse.onUpdate(v => { btn.scale.set(v); });
pulse.start();
```

---

## Game Migration Guide

### Mankwala (DOM-based literacy game)

**Replace:**
| Old pattern | New pattern |
|-------------|-------------|
| `<script src="/js/ai-helper.js">` | Keep (for AI) — add `<script src="/js/upixi/upixi.min.js">` after PixiJS |
| `const AC = new AudioContext(); playSound()` | `const audio = new uPixi.Audio(); audio.chime()`/`audio.buzz()`/`audio.fanfare()` |
| `localStorage.setItem('mankwala_X')` | `uPixi.Platform.saveProgress('mankwala', data)` |
| `if (UbuntuAnalytics) UbuntuAnalytics.track(...)` | `uPixi.Platform.track('event', 'mankwala', data)` |
| `document.getElementById('toast')` inline | `uPixi.UI.toast(msg)` |

**Add script tag:**
```html
<script src="/js/pixi.min.js"></script>
<script src="/js/upixi/upixi.min.js"></script>
<script src="/js/ai-helper.js"></script>
```

### Rangi (DOM-based color/music game)

**Replace:**
| Old pattern | New pattern |
|-------------|-------------|
| `AC = new AudioContext(); playNote()` | `audio = new uPixi.Audio(); audio.tone(freq, dur)` |
| `localStorage` scores | `uPixi.Platform.saveProgress('rangi', data)` |
| `UbuntuAnalytics.track(...)` | `uPixi.Platform.track(...)` |

### Chenjela (DOM-exam game)

**Replace:**
| Old pattern | New pattern |
|-------------|-------------|
| `AC = new AudioContext(); tone()` | `audio = new uPixi.Audio(); audio.click()`/`audio.buzz()` |
| `localStorage` | `uPixi.Platform.saveProgress('chenjela', data)` |
| `UbuntuAnalytics.track(...)` | `uPixi.Platform.track(...)` |

### Nzelu (canvas2D + DOM sorting game)

**Replace:**
| Old pattern | New pattern |
|-------------|-------------|
| `AC = new AudioContext(); tone()` | `audio = new uPixi.Audio()` |
| Canvas input (touch/mouse) | `app.input.on('down', (x,y) => ...)` |
| Background canvas rendering | Use PixiJS Graphics via `stage.layer()` |
| `document.getElementById('toast')` | `uPixi.UI.toast()` |
| `gogo()` inline | `uPixi.UI.gogo()` |
| `UbuntuAnalytics.track(...)` | `uPixi.Platform.track(...)` |

### Assegai (canvas2D strategy game)

Assegai has a complex custom `AudioEngine` class with beat/melody sequencers. The core strategy:

**Replace:**
| Old pattern | New pattern |
|-------------|-------------|
| `AudioEngine` class (PATS, beat sequencer) | `audio = new uPixi.Audio()` for simple sounds; keep custom sequencer if needed |
| `canvas.addEventListener` input | Use `app.input.on()` for unified touch handling |
| Canvas rendering | Consider migrating to PixiJS layers via `stage.layer()` |
| `UbuntuAnalytics.track(...)` | `uPixi.Platform.track(...)` |
| `localStorage` | `uPixi.Platform.saveProgress()` |
| Toast/Confetti inline | `uPixi.UI.toast()`, `uPixi.UI.confetti()` |

**Note:** Assegai is the most complex game (3,425 lines). See game-specific patterns below for a practical migration.

---

## Architecture Diagram

```
┌────────────────────────────────────────────────────────┐
│  Game HTML File (single-file, self-contained)          │
│                                                        │
│  ┌──────────────────────────────────────────────┐     │
│  │  HTML Structure                               │     │
│  │  • Screens (title, map, game, result)         │     │
│  │  • HUD elements (score, timer, gems)          │     │
│  │  • UI overlays (toast, confetti, gogo)        │     │
│  └──────────────┬───────────────────────────────┘     │
│                 │                                       │
│  ┌──────────────▼──────────────────────────────┐     │
│  │  uPixi Library                              │     │
│  │                                             │     │
│  │  uPixi.App ───── uPixi.Stage               │     │
│  │      │              │                        │     │
│  │      ├──→ input ──→ screenToGame()          │     │
│  │      │              │                        │     │
│  │      └──→ tick()    ├── layer() → Graphics  │     │
│  │                     ├── add()   → Container │     │
│  │                     └── remove()            │     │
│  │                                             │     │
│  │  uPixi.Audio        uPixi.Particles         │     │
│  │  uPixi.UI           uPixi.GridRenderer      │     │
│  │  uPixi.Platform     uPixi.State             │     │
│  │  uPixi.Tween                                │     │
│  └──────┬────────────────────────────────┬────┘     │
│         │                                │           │
│  ┌──────▼──────────┐    ┌───────────────▼──────┐  │
│  │  PixiJS 7.x     │    │  Platform Services   │  │
│  │  (WebGL/Canvas) │    │  • Analytics          │  │
│  │                 │    │  • i18n translation   │  │
│  │  PIXI.Graphics  │    │  • localStorage       │  │
│  │  PIXI.Container │    │  • Score sync API     │  │
│  │  PIXI.Filter    │    └───────────────────────┘  │
│  └─────────────────┘                                │
└────────────────────────────────────────────────────────┘
```

---

## Best Practices

### 1. Always use uPixi for platform features

Never access `UbuntuAnalytics`, `UbuntuI18n`, or raw `localStorage` directly in game code. Use `uPixi.Platform.track()`, `uPixi.Platform.t()`, and `uPixi.Platform.loadProgress()`/`saveProgress()` for consistent behavior and graceful degradation.

### 2. One Audio instance per game

```javascript
const audio = new uPixi.Audio();  // Create once at top level
```

Do not create multiple AudioContext instances. Reuse `audio` everywhere.

### 3. Use app.input, not raw DOM events

```javascript
// ✅ Correct
app.input.on('down', (x, y) => { /* x,y in game coordinates */ });

// ❌ Avoid
canvas.addEventListener('touchstart', e => {
  // Must manually handle coordinate mapping, multi-touch, etc.
});
```

### 4. Particles lifecycle in game loop

```javascript
app.tick(() => {
  particles.update();
  particles.draw();
  // ... other game logic
});
```

### 5. Stage coordinate awareness

All coordinates in `app.input.on()` and `stage.screenToGame()` are in logical game coordinates (e.g., 0–600 × 0–800). Draw to PIXI Graphics objects using these same coordinates — the Stage handles the transform.

### 6. Loading order

```html
<!-- Order matters -->
<script src="/js/pixi.min.js"></script>           <!-- 1. PixiJS -->
<script src="/js/upixi/upixi.min.js"></script>     <!-- 2. uPixi -->
<script src="/js/i18n.js"></script>                 <!-- 3. i18n -->
<script src="/analytics.js"></script>               <!-- 4. Analytics -->
```

### 7. Handle resize

`uPixi.App` handles window resize automatically. Do not add your own `resize` listener — the built-in handler calls `stage.resize()` which re-scales the container and re-centers letterbox.

### 8. Destroy on unmount

If a game can be re-loaded without page refresh, call `app.destroy()` during cleanup to remove the resize listener and destroy the PixiJS application.

### 9. Mute button pattern

```javascript
muteBtn.onclick = () => {
  const muted = audio.toggleMute();
  muteBtn.textContent = muted ? '' : '';
};
```

The mute preference is automatically persisted to localStorage by `uPixi.Audio`.

### 10. Testing with uPixi

uPixi will not load if PixiJS is not present. It gracefully sets `window.uPixi = undefined` if the PIXI global is missing. In test environments, ensure PixiJS is mocked or loaded before uPixi.

---

## API Quick Reference

```javascript
// App
const app = new uPixi.App({ width: 600, height: 800, backgroundColor: 0x0a1214 });
app.tick(fn);
app.untick(fn);
app.destroy();

// Stage (accessed via app.stage)
stage.add(child);
stage.remove(child);
stage.layer();              // → PIXI.Graphics
stage.screenToGame(x, y);   // → { x, y }

// Input (accessed via app.input)
app.input.on('down', (x, y) => {});
app.input.on('move', (x, y) => {});
app.input.on('up', () => {});

// Audio
const audio = new uPixi.Audio();
audio.tone(440, 0.1, 'sine', 0.12);
audio.chime();
audio.click();
audio.buzz();
audio.fanfare();
audio.trickle();
audio.pop();
audio.toggleMute();
audio.setMuted(true);
audio.isMuted();

// Particles
const ps = new uPixi.Particles(stage);
ps.emit(x, y, { color: '#FFF', size: 2, gravity: 0.12 });
ps.burst(x, y, 10, { color: '#40916C', spread: 2 });
ps.update();
ps.draw();

// Grid Renderer
const grid = new uPixi.GridRenderer(stage, { cols: 30, rows: 40, cellSize: 20 });
grid.render((g, c, r, s) => { /* draw cells */ });

// UI
uPixi.UI.toast('Hello!', 'success');
uPixi.UI.gogo('Try again!');
uPixi.UI.confetti(40);
uPixi.UI.hideGogo();

// Platform
uPixi.Platform.track('event', 'game', { data });
uPixi.Platform.t('key');
uPixi.Platform.saveProgress('game', data);
const data = uPixi.Platform.loadProgress('game');
uPixi.Platform.syncScore(token, 'game', score, metadata);

// State Machine
const state = new uPixi.State('menu');
state.add('play', { enter() {}, exit() {} });
state.go('play');

// Tween
const tween = new uPixi.Tween(0, 1, 500, { ease: 'easeOut' });
tween.onUpdate(v => {});
tween.start();
tween.update(dt);
```

---

## License

MIT — Part of the UbuntuPlay platform by Zipangile.
