Previous 12 Next

Renderer

We will create a renderer that can support different graphics APIs and we will implement support for both CanvasRenderingContext2D and WebGLRenderingContext.

Graphics backend

To support multiple backends we create an abstraction that the backends will implement.

src/GraphicsBackend.ts
import { Rectangle } from "./Rectangle";

export interface GraphicsBackend {
  clear(red: number, green: number, blue: number): void
  draw(dest: Rectangle, src: Rectangle, image: HTMLImageElement, mirrored: boolean): void
  present(): void
}

Renderer

The game will be played on different screen resolutions, therefore we will use a rendering pipeline that will render the game correctly without the game having to know about the actual resolution.

src/Renderer.ts
import { Rectangle } from "./Rectangle";

export interface Renderer {
  image(dest: Rectangle, texture: Rectangle,
    image: HTMLImageElement, mirrored: boolean): void
}
Coordinate system wher Y points downwards and X points right
Rendering pipeline

Clipping

Everything drawn outside the clipping area is discarded.

src/ClipRenderer.ts
import { Rectangle } from "./Rectangle";
import { tRectangle, tVector2 } from "./temporaryObjects";
import { Renderer } from "./Renderer";

export class ClipRenderer implements Renderer {
  isClipping = false
  rectangle = new Rectangle(0, 0, 0, 0)

  constructor(
    private renderer: Renderer
  ) { }

  image(dest: Rectangle, texture: Rectangle,
    image: HTMLImageElement, mirrored: boolean
  ) {
    if (this.isClipping) {
      dest = tRectangle.get(dest.x, dest.y, dest.width, dest.height)
      const overlap = findOverlap(dest, this.rectangle)
      if (!overlap) return


      let sx = (overlap.x - dest.x) / dest.width
      if (mirrored) {
        sx = ((dest.x + dest.width) - (overlap.x + overlap.width)) / (dest.width)
      }
      const sy = (overlap.y - dest.y) / dest.height
      const sw = (dest.width - overlap.width) / dest.width
      const sh = (dest.height - overlap.height) / dest.height
      texture = tRectangle.get(
        texture.x + sx * texture.width,
        texture.y + sy * texture.height,
        texture.width - sw * texture.width,
        texture.height - sh * texture.height
      )

      dest = overlap
    }
    this.renderer.image(dest, texture, image, mirrored)
  }

  clip(rectangle: Rectangle | null) {
    this.isClipping = rectangle != null
    if (rectangle != null) {
      this.rectangle.set(rectangle.x, rectangle.y,
        rectangle.width, rectangle.height)
    }
  }
}

export function findOverlap(a: Rectangle, b: Rectangle) {
  const ap0 = tVector2.get(a.x, a.y)
  const ap1 = tVector2.get(a.x + a.width, a.y + a.height)

  const bp0 = tVector2.get(b.x, b.y)
  const bp1 = tVector2.get(b.x + b.width, b.y + b.height)

  const o0x = Math.max(ap0.x, bp0.x)
  const o0y = Math.max(ap0.y, bp0.y)

  const o1x = Math.min(ap1.x, bp1.x)
  const o1y = Math.min(ap1.y, bp1.y)

  if (o0x >= o1x || o0y >= o1y) return null

  return tRectangle.get(o0x, o0y, o1x - o0x, o1y - o0y)
}

Scaling

Scales images by a scale factor.

src/ScaleRenderer.ts
import { Rectangle } from "./Rectangle";
import { tRectangle } from "./temporaryObjects";
import { Renderer } from "./Renderer";
import { TranslationRenderer } from "./TranslationRenderer";

export class ScaleRenderer implements Renderer {
  scale = 1

  constructor(
    private renderer: TranslationRenderer
  ) { }

  image(dest: Rectangle, texture: Rectangle,
    image: HTMLImageElement, mirrored: boolean
  ) {
    dest = tRectangle.get(
      dest.x * this.scale,
      dest.y * this.scale,
      dest.width * this.scale,
      dest.height * this.scale
    )
    this.renderer.image(dest, texture, image, mirrored)
  }
}

Translation

Moves images by a X and Y offset.

src/TranslationRenderer.ts
import { Renderer } from "./Renderer";
import { Rectangle } from "./Rectangle";
import { tRectangle } from "./temporaryObjects";

export class TranslationRenderer implements Renderer {
  xOffset = 0
  yOffset = 0

  constructor(
    private renderer: Renderer
  ) { }

  image(dest: Rectangle, texture: Rectangle,
    image: HTMLImageElement, mirrored: boolean
  ) {
    dest = tRectangle.get(
      dest.x + this.xOffset,
      dest.y + this.yOffset,
      dest.width, dest.height
    )
    this.renderer.image(dest, texture, image, mirrored)
  }
}

Output

As the last step in our pipeline we hand over the information to the graphics backend.

src/GraphicsRenderer.ts
import { GraphicsBackend } from "./GraphicsBackend"
import { Rectangle } from "./Rectangle";
import { Renderer } from "./Renderer";
import { tRectangle } from "./temporaryObjects";

export class GraphicsRenderer implements Renderer {

  constructor(
    private graphics: GraphicsBackend
  ) { }

  image(dest: Rectangle, src: Rectangle,
    image: HTMLImageElement, mirrored: boolean
  ) {
    dest = tRectangle.get(Math.round(dest.x), Math.round(dest.y),
      Math.round(dest.width), Math.round(dest.height))
    this.graphics.draw(dest, src, image, mirrored)
  }
}
Previous 12 Next