Skip to content

Rendering ​

The framework provides two renderers (PixiRenderer for WebGL/WebGPU and CanvasRenderer for Canvas 2D), GameLoop for the update/render cycle, and Camera for world-space transforms.

PixiRenderer ​

The recommended renderer for new games. Uses PixiJS 8 for hardware-accelerated rendering. pixi.js is an optional peer dependency, so install it first:

bash
npm install pixi.js
typescript
import { PixiRenderer, GameLoop } from "quantum-forge-engine/rendering";

class MyRenderer extends PixiRenderer {
  protected draw(state: GameState) {
    // this.graphics is a PixiJS Graphics object, cleared each frame
    this.graphics.circle(state.player.x, state.player.y, 10).fill("#fff");

    // drawText manages PixiJS Text objects by key
    this.drawText("score", `Score: ${state.score}`, 10, 10, {
      fill: "#fff",
      fontSize: 16,
    });

    // Persistent display objects (sprites, containers) go on this.stage
  }
}

const renderer = new MyRenderer({
  canvas: document.getElementById("game-canvas") as HTMLCanvasElement,
  width: 800,
  height: 600,
  backgroundColor: 0x000000,
  maxDpr: 2,
  logger,
});

// PixiJS requires async init
await renderer.init();

There is no this.app. PixiRenderer owns the renderer and its root container, so persistent objects are added with this.stage.addChild(...) and immediate-mode shapes go through this.graphics. getMetrics() reports canvas size, DPR, and how many text objects are active or pooled.

WARNING

await renderer.init() must complete before starting the game loop. PixiJS initializes the WebGL/WebGPU context asynchronously.

CanvasRenderer ​

For simpler games or Canvas 2D use cases:

typescript
import { CanvasRenderer, drawCircle } from "quantum-forge-engine/rendering";

class MyRenderer extends CanvasRenderer {
  render(state: GameState) {
    this.clear("#000");
    drawCircle(this.ctx, state.player.x, state.player.y, 10, "#fff");
  }
}

const renderer = new MyRenderer({ canvas, width: 800, height: 600, maxDpr: 2, logger });

render(state, deltaTime?) is abstract, so every subclass implements it. The base class gives you this.ctx and this.canvas as protected members, clear(color?), resize(cssWidth, cssHeight), and getMetrics(). Passing width and height runs an initial resize, and maxDpr caps the backing-store scale on high-DPI screens.

The same subpath exports plain drawing helpers: drawCircle, drawRect, drawLine, drawText, lerp, clamp.

GameLoop ​

Manages the update/render cycle at a target frame rate:

typescript
const loop = new GameLoop({
  update: (deltaTime: number) => {
    input.poll();
    // Update game logic
  },
  render: () => {
    renderer.render(engine.getState());
  },
  targetFps: 60,
  logger,
});

loop.start();
loop.isRunning();  // true
loop.getFps();     // measured, for a debug overlay
// loop.stop();

deltaTime is in seconds. At 60 FPS, deltaTime ≈ 0.0167.

For games that drive their own frames, DeltaTimeTracker gives the same delta without the loop: getDelta() returns seconds since the last call, capped at 0.1.

Camera ​

Camera converts between world and screen space. x and y are the top-left corner of the view, width and height are required, and there is no zoom.

typescript
import { Camera } from "quantum-forge-engine/rendering";

const camera = new Camera({
  width: 800,
  height: 600,
  bounds: { minX: 0, minY: 0, maxX: 3200, maxY: 2400 },
});

camera.follow(state.player);        // smoothing defaults to 0.1
camera.centerOn(state.player.x, state.player.y);  // hard snap

if (camera.isVisible(enemy.x, enemy.y, 32)) {
  const p = camera.worldToScreen(enemy.x, enemy.y);
  this.graphics.circle(p.x, p.y, enemy.radius).fill(enemy.color);
}

bounds clamps the view to the world rectangle. For Canvas 2D, applyTransform(ctx) and resetTransform(ctx) translate the context instead of converting each point. For PixiJS, applyToContainer(this.stage) does the same for a container.

Rendering from state ​

Renderers derive visuals from state. Game logic and its results stay in the engine.

typescript
// Correct: read state, draw
protected draw(state: GameState) {
  for (const enemy of state.enemies) {
    this.graphics.circle(enemy.x, enemy.y, enemy.radius).fill(enemy.color);
  }
}

// Wrong: deciding and remembering in the renderer
protected draw(state: GameState) {
  enemy.health -= 1;                      // game logic belongs in a helper
  this.lastKnownPositions[enemy.id] = enemy.x;  // don't keep a second source of truth
}

Camera transforms are the exception. Converting world coordinates to screen coordinates is a rendering concern, so camera.worldToScreen() inside draw() is fine.

Quantum visualization ​

For quantum objects, map probability to visual properties such as opacity:

typescript
protected draw(state: GameState) {
  for (const ball of state.balls) {
    const alpha = ball.isQuantum ? ball.existenceProbability : 1.0;
    this.graphics.circle(ball.x, ball.y, ball.radius).fill({
      color: ball.color,
      alpha,
    });
  }
}

The existenceProbability comes from the engine state. An engine helper reads it once per update with ball.exists.probability(true), which never collapses anything, and the renderer only draws what the state holds.

Powered by Quantum Forge