applyTo: "**"
Bubbles — Copilot Instructions
Project Overview
Silly web app: phone becomes a water glass. Canvas 2D physics, no framework, no server. Hosted on GitLab Pages. TypeScript + Vite + pnpm.
Hard Rules
- No UI framework (no React, Vue, Svelte, etc.) — vanilla TypeScript only.
- Canvas 2D only — no WebGL, no Three.js, no PixiJS.
- pnpm only — never suggest npm or yarn commands.
- No server — everything must work as a static site.
strict: trueTypeScript — noanywithout explicit justification.
Architecture
src/physics/ — pure simulation (no DOM, no canvas)
src/rendering/ — Canvas 2D drawing only (reads from physics, never writes)
src/input/ — DOM event adapters that produce values (no side effects on world directly)
src/icons/ — static data (icon registry)
src/App.ts — game loop: wires physics ← input, renderer ← physics
src/main.ts — DOM entry point only
scripts/ — Node.js tooling (sharp asset pipeline)
Physics Conventions
- Coordinates: canvas space, origin top-left, +Y downward.
- Units: pixels for distance, seconds for time, px/s for velocity, px/s² for acceleration.
g ≈ 980 px/s²(treats 100 px ≈ 1 metre).Watersimulation approach is open — column-based, SPH particles, or any other model that runs well on mobile Canvas 2D.- Prefer approaches that stay ≥ 30fps on a mid-range phone; profile before committing to a heavier model.
- max 12
RigidBodyobjects by default;World.addBody()enforces this by dropping oldest non-permanent. - Fixed physics timestep:
1/60s (set inApp.ts). - Container is sealed — no spilling, volume is always conserved.
- Gravity is a full 360°
Vector2(device beta + gamma viaTiltInput). Buoyancy, constraints, andWater.getSurfaceY()all work correctly when the phone is upside-down (water.isFlipped).
Rendering Conventions
Rendereralways draws in this order: clear → glass clip → water → icons → glass outline.- Shimmer and refraction are animated via
WaterRenderer.update(dt)— call every frame. - Emoji rendered with
ctx.fillText— never rasterise to offscreen canvas unless profiling shows it's needed.
Input Conventions
TiltInput: pure producer of agravity: Vector2— no direct world mutation.TouchInput: spawnsRigidBodyviaWorld.addBody()— the only input that mutates world state.- iOS orientation permission must be requested inside a user-gesture handler.
File Conventions
- One class per file, filename matches class name.
- Physics files import from
./Vector2.jsetc. (.jsextension required for ESM bundler resolution). - No barrel (
index.ts) files — import directly.