Documentation

Everything you need to build native games with TypeScript.

Quick Start

A complete Bloom game in 12 lines. This opens a window, clears it each frame, draws text, and exits when the user closes the window.

main.ts
import { initWindow, windowShouldClose, beginDrawing,
         endDrawing, clearBackground, drawText,
         Colors, closeWindow } from "bloom";

initWindow(800, 450, "Hello Bloom");

while (!windowShouldClose()) {
  beginDrawing();
  clearBackground(Colors.RAYWHITE);
  drawText("Hello, Bloom!", 190, 200, 20, Colors.DARKGRAY);
  endDrawing();
}

closeWindow();

Core Concepts

The Game Loop

Every Bloom game follows the same pattern: initialize, loop, clean up. There's no hidden framework — you own the loop.

initWindow(800, 600, "My Game");
setTargetFPS(60);

while (!windowShouldClose()) {
  const dt = getDeltaTime();  // seconds since last frame

  // 1. Update game state
  playerX = playerX + speed * dt;

  // 2. Draw everything
  beginDrawing();
  clearBackground(Colors.BLACK);
  // ... draw calls here ...
  endDrawing();
}

closeWindow();

Delta Time

getDeltaTime() returns the time in seconds since the last frame. Always multiply movement and animation values by delta time to keep behavior consistent regardless of frame rate.

Drawing Lifecycle

All draw calls must happen between beginDrawing() and endDrawing(). Bloom batches and submits GPU commands when you call endDrawing().

No Classes, No Magic

Bloom uses plain interfaces and pure functions. Types like Vec2, Color, and Rect are plain objects. Functions never mutate their inputs — they return new values. There are no classes, no inheritance, no lifecycle hooks.

Project Structure

my-game/
  src/
    main.ts           # Entry point
    player.ts
    enemies.ts
  assets/
    sprites/          # PNG, JPG
    models/           # glTF, OBJ
    sounds/           # WAV, OGG, MP3
    fonts/            # TTF, OTF
  package.json

Bloom modules are independently importable. Use only what you need:

import { initWindow } from "bloom/core";
import { drawRect } from "bloom/shapes";
import { vec3Add } from "bloom/math";

// Or import everything from the barrel export:
import { initWindow, drawRect, vec3Add } from "bloom";

API Reference

Core

bloom/core

Window management, game loop, input handling, timing, and utility functions.

Window

initWindow(width: number, height: number, title: string): void
Creates a window with the given dimensions and title. Call this before anything else.
closeWindow(): void
Closes the window and releases all resources.
windowShouldClose(): boolean
Returns true if the user has requested to close the window (close button or ESC).
getScreenWidth(): number
Returns the current window width in pixels.
getScreenHeight(): number
Returns the current window height in pixels.
toggleFullscreen(): void
Toggles between windowed and fullscreen mode.
setWindowTitle(title: string): void
Changes the window title.
setWindowIcon(path: string): void
Sets the window icon from a file path.

Drawing

beginDrawing(): void
Begins a new drawing frame. All draw calls go between this and endDrawing().
endDrawing(): void
Ends the frame, submits GPU commands, and presents to screen.
clearBackground(color: Color): void
Fills the entire screen with the given color.

Timing

setTargetFPS(fps: number): void
Sets the target frame rate. The engine will sleep to maintain this rate.
getDeltaTime(): number
Returns time elapsed since the last frame, in seconds.
getFPS(): number
Returns the current frames per second.
getTime(): number
Returns total elapsed time since window creation, in seconds.

Keyboard Input

isKeyPressed(key: number): boolean
Returns true if the key was pressed this frame (single event, not held).
isKeyDown(key: number): boolean
Returns true if the key is currently held down.
isKeyReleased(key: number): boolean
Returns true if the key was released this frame.

Mouse Input

getMouseX(): number
Returns current mouse X position.
getMouseY(): number
Returns current mouse Y position.
getMousePosition(): Vec2
Returns current mouse position.
getMouseDeltaX(): number
Returns mouse X movement since last frame.
getMouseDeltaY(): number
Returns mouse Y movement since last frame.
isMouseButtonPressed(button: number): boolean
Returns true if the mouse button was pressed this frame.
isMouseButtonDown(button: number): boolean
Returns true if the mouse button is currently held down.
isMouseButtonReleased(button: number): boolean
Returns true if the mouse button was released this frame.
disableCursor(): void
Hides and locks the cursor (useful for first-person cameras).
enableCursor(): void
Shows and unlocks the cursor.

Touch Input

getTouchX(index: number): number
Returns X position of touch point at index.
getTouchY(index: number): number
Returns Y position of touch point at index.
getTouchPosition(index: number): Vec2
Returns position of touch point at index.
getTouchCount(): number
Returns number of active touch points.

Gamepad Input

isGamepadAvailable(): boolean
Returns true if a gamepad is connected.
getGamepadAxis(axis: number): number
Returns the value of a gamepad axis (-1.0 to 1.0).
getGamepadAxisCount(): number
Returns the number of available gamepad axes.
isGamepadButtonPressed(button: number): boolean
Returns true if gamepad button was pressed this frame.
isGamepadButtonDown(button: number): boolean
Returns true if gamepad button is currently held.
isGamepadButtonReleased(button: number): boolean
Returns true if gamepad button was released this frame.

Camera Modes

beginMode2D(camera: Camera2D): void
Activates 2D camera mode. All subsequent draw calls are transformed by the camera.
endMode2D(): void
Deactivates 2D camera mode.
getScreenToWorld2D(pos: Vec2, camera: Camera2D): Vec2
Converts screen coordinates to world coordinates using the camera transform.
getWorldToScreen2D(pos: Vec2, camera: Camera2D): Vec2
Converts world coordinates to screen coordinates using the camera transform.
beginMode3D(camera: Camera3D): void
Activates 3D camera mode with perspective or orthographic projection.
endMode3D(): void
Deactivates 3D camera mode.

File I/O

writeFile(path: string, data: string): boolean
Writes string data to a file. Returns true on success.
fileExists(path: string): boolean
Returns true if a file exists at the given path.

Shapes

bloom/shapes

2D shape drawing and collision detection.

Drawing

drawLine(startX: number, startY: number, endX: number, endY: number, thickness: number, color: Color): void
Draws a line segment with the given thickness.
drawRect(x: number, y: number, width: number, height: number, color: Color): void
Draws a filled rectangle.
drawRectRec(rec: Rect, color: Color): void
Draws a filled rectangle from a Rect object.
drawRectLines(x: number, y: number, width: number, height: number, thickness: number, color: Color): void
Draws a rectangle outline.
drawCircle(cx: number, cy: number, radius: number, color: Color): void
Draws a filled circle.
drawCircleLines(cx: number, cy: number, radius: number, color: Color): void
Draws a circle outline.
drawTriangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, color: Color): void
Draws a filled triangle.
drawPoly(cx: number, cy: number, sides: number, radius: number, rotation: number, color: Color): void
Draws a filled regular polygon.
drawBezier(startX: number, startY: number, cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, endX: number, endY: number, thickness: number, color: Color): void
Draws a cubic Bezier curve.

2D Collision Detection

checkCollisionRecs(rec1: Rect, rec2: Rect): boolean
Returns true if two rectangles overlap.
checkCollisionCircles(center1: Vec2, r1: number, center2: Vec2, r2: number): boolean
Returns true if two circles overlap.
checkCollisionCircleRec(center: Vec2, radius: number, rec: Rect): boolean
Returns true if a circle and rectangle overlap.
checkCollisionPointRec(point: Vec2, rec: Rect): boolean
Returns true if a point is inside a rectangle.
checkCollisionPointCircle(point: Vec2, center: Vec2, radius: number): boolean
Returns true if a point is inside a circle.
getCollisionRec(rec1: Rect, rec2: Rect): Rect
Returns the overlapping rectangle between two rects, or an empty rect if none.

Textures

bloom/textures

Image loading, texture management, and sprite rendering. Supports PNG, JPG, BMP, and TGA.

Textures

loadTexture(path: string): Texture
Loads an image file and uploads it to the GPU as a texture.
unloadTexture(texture: Texture): void
Frees the GPU texture.
drawTexture(texture: Texture, x: number, y: number, tint: Color): void
Draws a texture at the given position with a color tint. Use Colors.WHITE for no tint.
drawTextureRec(texture: Texture, source: Rect, position: Vec2, tint: Color): void
Draws a region of a texture (useful for sprite sheets).
drawTexturePro(texture: Texture, source: Rect, dest: Rect, origin: Vec2, rotation: number, tint: Color): void
Draws a texture with full control over source rect, destination rect, rotation origin, and rotation angle.
getTextureWidth(texture: Texture): number
Returns the texture width in pixels.
getTextureHeight(texture: Texture): number
Returns the texture height in pixels.

Images (CPU-side)

loadImage(path: string): number
Loads an image into CPU memory. Returns a handle for manipulation before uploading to GPU.
loadTextureFromImage(imageHandle: number): Texture
Uploads a CPU image to the GPU as a texture.
imageResize(imageHandle: number, width: number, height: number): void
Resizes an image.
imageCrop(imageHandle: number, x: number, y: number, width: number, height: number): void
Crops an image to the given rectangle.
imageFlipH(imageHandle: number): void
Flips an image horizontally.
imageFlipV(imageHandle: number): void
Flips an image vertically.

Text

bloom/text

Font loading and text rendering. Supports TTF and OTF fonts.

drawText(text: string, x: number, y: number, size: number, color: Color): void
Draws text with the built-in default font.
measureText(text: string, size: number): number
Returns the width of text in pixels using the default font. Useful for centering.
loadFont(path: string, size: number): Font
Loads a TTF or OTF font at the given size.
loadFontEx(path: string, size: number): Font
Extended font loading (same as loadFont).
unloadFont(font: Font): void
Frees font resources.
drawTextEx(font: Font, text: string, pos: Vec2, size: number, spacing: number, color: Color): void
Draws text with a custom font, size, and letter spacing.
measureTextEx(font: Font, text: string, size: number, spacing: number): Vec2
Returns the dimensions of text with a custom font (width in x, height in y).

Audio

bloom/audio

Sound effects and music streaming. Supports WAV, OGG, and MP3.

System

initAudio(): void
Initializes the audio system. Call before loading any sounds or music.
closeAudio(): void
Shuts down the audio system.
setMasterVolume(volume: number): void
Sets the master volume (0.0 to 1.0).

Sound Effects

loadSound(path: string): Sound
Loads a sound file fully into memory. Best for short sound effects.
playSound(sound: Sound): void
Plays a sound.
stopSound(sound: Sound): void
Stops a sound.
setSoundVolume(sound: Sound, volume: number): void
Sets the volume for a specific sound (0.0 to 1.0).

Music Streaming

loadMusic(path: string): Music
Loads a music file for streaming (not fully loaded into memory).
playMusic(music: Music): void
Starts playing music.
stopMusic(music: Music): void
Stops music playback.
updateMusicStream(music: Music): void
Updates the music stream buffer. Must be called every frame while music is playing.
setMusicVolume(music: Music, volume: number): void
Sets the volume for music (0.0 to 1.0).
isMusicPlaying(music: Music): boolean
Returns true if music is currently playing.

Models

bloom/models

3D model loading, primitive drawing, mesh generation, and shaders. Supports glTF and OBJ.

3D Models

loadModel(path: string): Model
Loads a 3D model from a glTF or OBJ file.
drawModel(model: Model, position: Vec3, scale: number, tint: Color): void
Draws a 3D model at the given position with scale and tint.
unloadModel(model: Model): void
Frees model resources.

3D Primitives

drawCube(pos: Vec3, width: number, height: number, depth: number, color: Color): void
Draws a filled cube.
drawCubeWires(pos: Vec3, width: number, height: number, depth: number, color: Color): void
Draws a wireframe cube.
drawSphere(pos: Vec3, radius: number, color: Color): void
Draws a filled sphere.
drawSphereWires(pos: Vec3, radius: number, color: Color): void
Draws a wireframe sphere.
drawCylinder(pos: Vec3, radiusTop: number, radiusBottom: number, height: number, color: Color): void
Draws a cylinder (can be a cone if one radius is 0).
drawPlane(pos: Vec3, width: number, depth: number, color: Color): void
Draws a flat plane.
drawGrid(slices: number, spacing: number): void
Draws a reference grid on the XZ plane.
drawRay(origin: Vec3, direction: Vec3, color: Color): void
Draws a ray as a line.

Mesh Generation

genMeshCube(width: number, height: number, depth: number): Model
Generates a cube mesh as a Model.
genMeshHeightmap(imageHandle: number, sizeX: number, sizeY: number, sizeZ: number): Model
Generates terrain mesh from a heightmap image.
createMesh(vertices: number[], indices: number[]): Model
Creates a mesh from raw vertex data. Each vertex is 12 floats: x, y, z, nx, ny, nz, r, g, b, a, u, v.

Shaders & Animation

loadShader(wgslSource: string): number
Loads a shader from WGSL source code.
loadModelAnimation(path: string): number
Loads model animation data from a file.
updateModelAnimation(handle: number, animIndex: number, time: number): void
Updates the animation state for a model.

Math

bloom/math

Vectors, matrices, quaternions, easing functions, collision, and utilities. All functions are pure — they return new values and never mutate inputs.

Vector 2D

vec2(x: number, y: number): Vec2
vec2Add(a: Vec2, b: Vec2): Vec2
vec2Sub(a: Vec2, b: Vec2): Vec2
vec2Scale(v: Vec2, s: number): Vec2
vec2Length(v: Vec2): number
vec2LengthSq(v: Vec2): number
vec2Normalize(v: Vec2): Vec2
vec2Dot(a: Vec2, b: Vec2): number
vec2Distance(a: Vec2, b: Vec2): number
vec2Lerp(a: Vec2, b: Vec2, t: number): Vec2

Vector 3D

vec3(x: number, y: number, z: number): Vec3
vec3Add(a: Vec3, b: Vec3): Vec3
vec3Sub(a: Vec3, b: Vec3): Vec3
vec3Scale(v: Vec3, s: number): Vec3
vec3Length(v: Vec3): number
vec3LengthSq(v: Vec3): number
vec3Normalize(v: Vec3): Vec3
vec3Dot(a: Vec3, b: Vec3): number
vec3Cross(a: Vec3, b: Vec3): Vec3
vec3Distance(a: Vec3, b: Vec3): number
vec3Lerp(a: Vec3, b: Vec3, t: number): Vec3

Vector 4D

vec4(x: number, y: number, z: number, w: number): Vec4
vec4Add(a: Vec4, b: Vec4): Vec4
vec4Scale(v: Vec4, s: number): Vec4
vec4Length(v: Vec4): number
vec4Normalize(v: Vec4): Vec4

Matrix 4x4

Mat4 is a number[] with 16 elements in column-major order.

mat4Identity(): Mat4
mat4Multiply(a: Mat4, b: Mat4): Mat4
mat4Translate(m: Mat4, v: Vec3): Mat4
mat4Scale(m: Mat4, v: Vec3): Mat4
mat4RotateX(m: Mat4, angle: number): Mat4
mat4RotateY(m: Mat4, angle: number): Mat4
mat4RotateZ(m: Mat4, angle: number): Mat4
mat4Perspective(fovy: number, aspect: number, near: number, far: number): Mat4
mat4Ortho(left: number, right: number, bottom: number, top: number, near: number, far: number): Mat4
mat4LookAt(eye: Vec3, center: Vec3, up: Vec3): Mat4
mat4Invert(m: Mat4): Mat4

Quaternions

quatIdentity(): Quat
quatFromEuler(pitch: number, yaw: number, roll: number): Quat
quatToMat4(q: Quat): Mat4
quatSlerp(a: Quat, b: Quat, t: number): Quat
quatNormalize(q: Quat): Quat
quatMultiply(a: Quat, b: Quat): Quat

3D Collision & Raycasting

rayIntersectsBox(ray: Ray, box: BoundingBox): boolean
Returns true if a ray intersects an axis-aligned bounding box.
rayIntersectsSphere(ray: Ray, center: Vec3, radius: number): boolean
Returns true if a ray intersects a sphere.
rayIntersectsTriangle(ray: Ray, v0: Vec3, v1: Vec3, v2: Vec3): RayHit
Ray-triangle intersection using the Möller-Trumbore algorithm. Returns hit details.
getRayCollisionBox(ray: Ray, box: BoundingBox): RayHit
Returns detailed ray-box intersection info (hit point, normal, distance).
checkCollisionSpheres(c1: Vec3, r1: number, c2: Vec3, r2: number): boolean
Returns true if two spheres overlap.
checkCollisionBoxes(a: BoundingBox, b: BoundingBox): boolean
Returns true if two bounding boxes overlap.

Frustum Culling

extractFrustumPlanes(mvp: Mat4): FrustumPlanes
Extracts the 6 frustum planes from a model-view-projection matrix.
isBoxInFrustum(box: BoundingBox, frustum: FrustumPlanes): boolean
Returns true if a bounding box is visible within the frustum.

Scalar Utilities

lerp(a: number, b: number, t: number): number
Linear interpolation between a and b.
clamp(value: number, min: number, max: number): number
Constrains a value between min and max.
remap(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number
Remaps a value from one range to another.
randomFloat(min: number, max: number): number
Returns a random float between min and max.
randomInt(min: number, max: number): number
Returns a random integer between min and max (inclusive).

Easing Functions

All easing functions take a t value from 0.0 to 1.0 and return a transformed value.

easeInQuad(t) · easeOutQuad(t) · easeInOutQuad(t)
easeInCubic(t) · easeOutCubic(t) · easeInOutCubic(t)
easeInElastic(t) · easeOutElastic(t)
easeBounce(t)

Reference

Types

All types are plain interfaces. No classes, no constructors — just object literals.

types.ts
// Vectors
interface Vec2  { x: number; y: number }
interface Vec3  { x: number; y: number; z: number }
interface Vec4  { x: number; y: number; z: number; w: number }

// Visual
interface Color { r: number; g: number; b: number; a: number }  // 0-255
interface Rect  { x: number; y: number; width: number; height: number }

// Cameras
interface Camera2D {
  offset: Vec2;    // camera offset (screen center)
  target: Vec2;    // camera target (world position)
  rotation: number; // rotation in degrees
  zoom: number;     // zoom level (1.0 = no zoom)
}

interface Camera3D {
  position: Vec3;   // camera position
  target: Vec3;     // look-at point
  up: Vec3;         // up direction
  fovy: number;     // field of view Y (degrees)
  projection: number; // 0 = perspective, 1 = orthographic
}

// Resources (handle-based)
interface Texture { handle: number; width: number; height: number }
interface Font    { handle: number; size: number }
interface Sound   { handle: number }
interface Music   { handle: number }
interface Model   { handle: number }

// Quaternion
interface Quat { x: number; y: number; z: number; w: number }

// 3D Geometry
interface Ray         { position: Vec3; direction: Vec3 }
interface BoundingBox { min: Vec3; max: Vec3 }
interface RayHit {
  hit: boolean; distance: number;
  point: Vec3; normal: Vec3;
}
interface FrustumPlanes { planes: Vec4[] }

// Matrix (column-major, 16 elements)
type Mat4 = number[];

Colors

Pre-defined colors available via Colors.NAME. Import from bloom/core.

WHITE
BLACK
RED
GREEN
BLUE
YELLOW
ORANGE
PINK
PURPLE
MAGENTA
VIOLET
GOLD
SKYBLUE
DARKBLUE
LIME
DARKGREEN
MAROON
BROWN
BEIGE
LIGHTGRAY
GRAY
DARKGRAY
RAYWHITE
BLANK

Custom colors are plain objects: { r: 255, g: 100, b: 50, a: 255 }

Key Constants

Import Key and MouseButton from bloom/core.

Keyboard

Letters

Key.AKey.Z

Numbers

Key.ZEROKey.NINE

Function Keys

Key.F1Key.F12

Arrows

Key.UP, Key.DOWN, Key.LEFT, Key.RIGHT

Common

Key.SPACE, Key.ENTER, Key.ESCAPE, Key.TAB, Key.BACKSPACE, Key.DELETE

Navigation

Key.INSERT, Key.HOME, Key.END, Key.PAGE_UP, Key.PAGE_DOWN

Modifiers

Key.LEFT_SHIFT, Key.RIGHT_SHIFT, Key.LEFT_CONTROL, Key.RIGHT_CONTROL, Key.LEFT_ALT, Key.RIGHT_ALT, Key.LEFT_SUPER, Key.RIGHT_SUPER

Punctuation

Key.COMMA, Key.PERIOD, Key.SLASH, Key.SEMICOLON, Key.APOSTROPHE, Key.MINUS, Key.EQUAL, Key.LEFT_BRACKET, Key.RIGHT_BRACKET, Key.BACKSLASH, Key.GRAVE

Mouse Buttons

MouseButton.LEFT, MouseButton.RIGHT, MouseButton.MIDDLE

Guides

Input Handling

Bloom distinguishes between pressed (single frame), down (held), and released (single frame) states.

// One-shot actions (menus, jumping, shooting)
if (isKeyPressed(Key.SPACE)) jump();
if (isKeyPressed(Key.ENTER)) selectMenuItem();

// Continuous movement (hold to move)
if (isKeyDown(Key.LEFT))  x -= speed * dt;
if (isKeyDown(Key.RIGHT)) x += speed * dt;
if (isKeyDown(Key.W))     y -= speed * dt;
if (isKeyDown(Key.S))     y += speed * dt;

// Mouse
const mouse = getMousePosition();
if (isMouseButtonPressed(MouseButton.LEFT)) {
  shoot(mouse.x, mouse.y);
}

Cameras

2D Camera

The 2D camera transforms all draw calls between beginMode2D and endMode2D. Set offset to the screen center and target to the world position you want to follow.

const camera: Camera2D = {
  offset: { x: 400, y: 300 },    // center of 800x600 screen
  target: { x: player.x, y: player.y }, // follow player
  rotation: 0,
  zoom: 1.0
};

// Smooth follow
camera.target.x += (player.x - camera.target.x) * 8 * dt;
camera.target.y += (player.y - camera.target.y) * 8 * dt;

beginMode2D(camera);
// World-space drawing here — moves with camera
drawRect(player.x, player.y, 32, 32, Colors.BLUE);
endMode2D();

// Screen-space drawing here — stays fixed (HUD)
drawText("Score: 100", 10, 10, 20, Colors.WHITE);

3D Camera

const camera: Camera3D = {
  position: { x: 10, y: 10, z: 10 }, // eye position
  target: { x: 0, y: 0, z: 0 },     // look-at point
  up: { x: 0, y: 1, z: 0 },         // up direction
  fovy: 45,                       // field of view (degrees)
  projection: 0                   // 0=perspective, 1=orthographic
};

beginMode3D(camera);
drawCube({ x: 0, y: 0, z: 0 }, 2, 2, 2, Colors.RED);
drawGrid(10, 1);
endMode3D();

Collision Detection

Bloom provides both 2D and 3D collision detection. All collision functions are pure and return results without side effects.

// Rectangle vs Rectangle (AABB)
const player: Rect = { x: 100, y: 100, width: 32, height: 32 };
const enemy: Rect  = { x: 120, y: 110, width: 32, height: 32 };

if (checkCollisionRecs(player, enemy)) {
  // Get the overlap area
  const overlap = getCollisionRec(player, enemy);
}

// Circle vs Circle
if (checkCollisionCircles(
  { x: 100, y: 100 }, 20,  // center1, radius1
  { x: 130, y: 110 }, 15   // center2, radius2
)) {
  // collision!
}

// Point in rectangle (mouse clicks, UI)
const mouse = getMousePosition();
if (checkCollisionPointRec(mouse, button)) {
  // hovering over button
}

Audio

Sound effects are fully loaded into memory — use them for short clips. Music is streamed — use it for background tracks. Always call updateMusicStream every frame.

initAudio();

// Sound effects (loaded fully, play instantly)
const hitSound = loadSound("sounds/hit.wav");
const coinSound = loadSound("sounds/coin.ogg");
setSoundVolume(coinSound, 0.7);

// Background music (streamed)
const bgm = loadMusic("music/theme.ogg");
setMusicVolume(bgm, 0.5);
playMusic(bgm);

while (!windowShouldClose()) {
  updateMusicStream(bgm);  // REQUIRED every frame

  if (playerHit) playSound(hitSound);
  if (coinCollected) playSound(coinSound);

  // ... drawing ...
}

closeAudio();

Examples

Pong

~170 lines · Beginner · View source

Classic two-player Pong. Demonstrates the game loop, keyboard input, delta-time movement, 2D shapes, collision detection, text rendering, and score tracking.

What you'll learn

  • Game loop with getDeltaTime() for frame-rate independence
  • Continuous input with isKeyDown() for paddle movement
  • Rectangle collision with checkCollisionRecs()
  • Centered text using measureText()
  • Game state management (pause, scoring, ball reset)
  • Value clamping with clamp() to keep paddles on screen
Key pattern: paddle-ball collision
const ballRect: Rect = {
  x: ballX - BALL_RADIUS, y: ballY - BALL_RADIUS,
  width: BALL_RADIUS * 2, height: BALL_RADIUS * 2
};

if (checkCollisionRecs(ballRect, leftPaddle) && ballVelX < 0) {
  ballVelX = -ballVelX;
  // Angle varies based on where ball hits paddle
  const hitPos = (ballY - leftPaddleY) / PADDLE_HEIGHT;
  ballVelY = BALL_SPEED * (hitPos - 0.5) * 2;
}

Dungeon Crawl

~300 lines · Intermediate · View source

A turn-based dungeon crawler with procedural map generation, fog of war, enemy AI, and 2D camera. Descend through floors of increasing difficulty.

What you'll learn

  • Procedural dungeon generation (rooms + corridors)
  • Raycasting field-of-view with fog of war and exploration memory
  • Turn-based logic using isKeyPressed() (one action per press)
  • 2D camera with smooth follow and zoom (beginMode2D/endMode2D)
  • Flat-array tile maps with y * WIDTH + x indexing
  • Entity AI (enemies chase visible player, attack when adjacent)
  • HUD overlay drawn outside camera mode
Key pattern: smooth camera follow
const camera: Camera2D = {
  offset: { x: SCREEN_WIDTH / 2, y: SCREEN_HEIGHT / 2 },
  target: { x: player.x * TILE_SIZE, y: player.y * TILE_SIZE },
  rotation: 0, zoom: 1.0
};

// Smooth easing toward player each frame
camera.target.x += (targetX - camera.target.x) * 8 * dt;
camera.target.y += (targetY - camera.target.y) * 8 * dt;

// Zoom with +/- keys
if (isKeyDown(Key.EQUAL)) camera.zoom = clamp(camera.zoom + dt, 0.5, 3.0);
if (isKeyDown(Key.MINUS)) camera.zoom = clamp(camera.zoom - dt, 0.5, 3.0);

Space Blaster

~350 lines · Intermediate · View source

A fast-paced vertical shooter with wave-based enemy spawning, particle explosions, object pooling, and a scrolling star field.

What you'll learn

  • Object pooling with fixed-size arrays and active flags (zero GC)
  • Particle system with velocity, lifetime, and alpha fade-out
  • Wave-based difficulty progression (more enemies, new types per wave)
  • Multiple entity types with different behavior (basic, fast, tank)
  • Cooldown-based shooting (bulletCooldown -= dt)
  • Parallax scrolling star background
  • Triangle drawing for the player ship with drawTriangle()
Key pattern: object pool + particle spawn
// Pre-allocate pool (no allocations during gameplay)
const particles: Particle[] = [];
for (let i = 0; i < MAX_PARTICLES; i++) {
  particles.push({ x: 0, y: 0, vx: 0, vy: 0,
    life: 0, maxLife: 0, color: Colors.WHITE, active: false });
}

// Spawn by reusing inactive entries
function spawnExplosion(x: number, y: number) {
  for (let i = 0; i < MAX_PARTICLES; i++) {
    if (!particles[i].active) {
      const angle = randomFloat(0, Math.PI * 2);
      const speed = randomFloat(50, 200);
      particles[i].x = x; particles[i].y = y;
      particles[i].vx = Math.cos(angle) * speed;
      particles[i].vy = Math.sin(angle) * speed;
      particles[i].life = randomFloat(0.3, 0.8);
      particles[i].active = true;
      break;
    }
  }
}