# Chapter 5: Math for games (/guides/handmade-web-games/math-for-games)





## Diagonal movement [#diagonal-movement]

If you're making a game with 4-way directional movement, it's easy to compute velocity. If the player is pressing <kbd>→</kbd> then have them go `+SPEED` units on the x axis. If they're pressing <kbd>↑</kbd> then have them go `+SPEED` units on the `y` axis. Flip the pluses for minuses for the other two directions. Easy!

```ts
function fixedUpdate(dt: number) {
  const speed = 80;
  if (keysDown.has("ArrowUp")) {
    player.y -= speed * dt;
  } else if (keysDown.has("ArrowDown")) {
    player.y += speed * dt;
  } else if (keysDown.has("ArrowLeft")) {
    player.x -= speed * dt;
  } else if (keysDown.has("ArrowRight")) {
    player.x += speed * dt;
  }
}
```

<FourWayMovementDemo />

Now we want to support 8-way directional movement, so the player can press <kbd>→</kbd> and <kbd>↑</kbd> at the same time to go diagonally. Easy, too, right?

<Callout type="error" title="Wrong.">
  ```ts
  function fixedUpdate(dt: number) {
    const speed = 80;
    if (keysDown.has("ArrowUp")) player.y -= speed * dt;
    if (keysDown.has("ArrowDown")) player.y += speed * dt;
    if (keysDown.has("ArrowLeft")) player.x -= speed * dt;
    if (keysDown.has("ArrowRight")) player.x += speed * dt;
  }
  ```
</Callout>

Now when <kbd>→</kbd> and <kbd>↑</kbd> are both held, the player begins moving *faster* on the diagonal. Why?

<DiagonalMovementUnnormalizedDemo />

Well, if each direction key adds full speed on its own axis, holding two keys adds two full speeds at a right angle.

Put another way, think of a right triangle where the base is your <strong className="font-bold text-sky-600 dark:text-sky-400">`x` velocity</strong> and the height is your <strong className="font-bold text-sky-600 dark:text-sky-400">`y` velocity</strong>. When you move diagonally, you're traveling along its <strong className="font-bold text-rose-700 dark:text-amber-400">hypotenuse</strong>, which covers more distance.

<DiagonalMovementHypotDemo />

So to make the speed consistent, when travelling diagonally you need to divide both the `x` and `y` velocities by that right triangle's hypotenuse.

<Callout type="success">
  <DiagonalMovementNormalizedDemo />

  ```ts
  function fixedUpdate(dt: number) {
    const speed = 80;
    let vx = 0;
    let vy = 0;
    
    if (keysDown.has("ArrowUp")) vy -= speed * dt;
    if (keysDown.has("ArrowDown")) vy += speed * dt;
    if (keysDown.has("ArrowLeft")) vx -= speed * dt;
    if (keysDown.has("ArrowRight")) vx += speed * dt;
    
    const hypot = Math.hypot(vx, vy) || 1;
    player.x += vx / hypot;
    player.y += vy / hypot;
  }
  ```
</Callout>

<Callout type="info">
  We'll revisit this problem in [Chapter 8
  →](/guides/handmade-web-games/controllers-and-haptics) on controllers where we look into
  handling movement using analog sticks.
</Callout>

## Modulo [#modulo]

In languages like Ruby and Python, the `%` symbol is a **modulo operator**. It gives you the remainder when dividing two numbers, and wraps negative results back into a positive range. But in JavaScript and most other C-like languages, `%` is the [**remainder operator**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder#description) <small className="opacity-75">\[MDN]</small> and it does not wrap negative numbers.

<ModArrayWrapDemo />

Things like clocks which are always increasing can be implemented using the `%` operator directly since the numbers are all positive. But games often also need a modulo operator that handles negative number wrapping, for things like world wrapping or item selection in hotbars. Since one isn't built into the language, we have to use our own:

```ts twoslash
function mod(a: number, b: number) {
  return ((a % b) + b) % b;
}
```

## Seeded random numbers [#seeded-random-numbers]

Before computers were effective at generating pseudorandom numbers, people relied on [books filled with random digits](https://en.wikipedia.org/wiki/A_Million_Random_Digits_with_100,000_Normal_Deviates) <small className="opacity-75">\[Wikipedia]</small> in order to pull random numbers for work in fields that required actual randomness, such as cryptography, nuclear physics, or statistics.

With a book of random numbers, you could pick a page and line number, and although the digits you would find at that location would be completely random, you could return to that place any number of times in the future and get the exact same digits.

We get the same guarantee when we use a *seeded random number generator*. A *seed* is like the page and line numbers. It is a way to get back to the same set of random digits.

Why is that useful?

For procedurally generated games like Minecraft or games with deterministic randomness like Balatro, you can enter a specific seed to reproduce an exact (but still randomly-generated) game state. Sometimes this is just for fun--players can share interesting seeds with one another. But seeded randomness is also practical. You might have a bug that only occurs one in a thousand times, and in a truly random game that might be impossible to debug. If a tester shares the seed with the broken state, you can reliably test and reproduce the issue as many times as you need.

In the next demo you can compare the rolls of two dice. The one using a seeded PRNG will produce the same sequence of numbers after each <kbd>Reset</kbd>, which isn't possible with `Math.random()`.

<SeededDiceCompareDemo />

Although the built-in `Math.random()` and `crypto.getRandomValues()` are also seeded random number generators, they're pre-seeded and we don't have an API to reset that seed. So we'll need to find our own seeded random algorithms that allow us to set the seed directly.

A popular <abbr title="Pseudorandom Number Generator">PRNG</abbr> is `splitmix32`, which is short enough to copy into your game's utility functions:

```ts
function splitmix32(seed: number) {
  return function () {
    seed |= 0;
    seed = (seed + 0x9e3779b9) | 0;
    let t = seed ^ (seed >>> 16);
    t = Math.imul(t, 0x21f0aaad);
    t = t ^ (t >>> 15);
    t = Math.imul(t, 0x735a2d97);
    return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;
  };
}

const random = splitmix32(123);

// if you use seed 123 you'll also get these first 3 random numbers:
// random(); => 0.4575126694981009
// random(); => 0.21505506429821253
// random(); => 0.7675276368390769
```

<details>
  <summary>
    <strong>Is `splitmix32` any good? How does it compare to other PRNGs?</strong>
  </summary>

  It's hard to tell based on a series of numbers like `0.4575126694981009`, `0.21505506429821253`, `0.7675276368390769`, ... if our random number generator is actually doing a good job. Instead, we can do a quick visual spot check for patterns by filling a canvas with sequential `random()` values from our PRNG.

  A good PRNG will look like pure static with no visible patterns or streaks.

  <RngStaticSplitmix32Noise />

  Let's see what the noise pattern looks like for an absolutely terrible seeded "random" function:

  ```ts
  function random(seed: number) {
    let t = seed;
    return () => {
      t += 0.1; // the slider changes this value
      return (Math.sin(t * t) + 1) / 2;
    };
  }
  ```

  <RngStaticBadSineStepNoise />

  While they make for interesting generative art, these values are not random.

  Other algorithms result in noise values that seem random at a glance, but still form patterns. (Sometimes these patterns only form for specific seeds and canvas widths, so this type of visual testing can only get us so far.)

  <RngStaticBadSinePhiFractNoise />

  So these approaches clearly don't pass the visual spot check. But they also won't pass empirical analyses based on [statistical](https://prng.di.unimi.it/) [test](https://github.com/lemire/testingRNG) [suites](https://github.com/bryc/code/blob/master/jshash/PRNGs.md).

  Rather than roll our own PRNG, this is a good place to use a tried and true algorithm. Splitmix32 is fast, easy to embed, and random-enough for most practical applications for games. Just note that it isn't cryptographically secure, so it should not be used in sensitive contexts. It's a solid choice overall to use in JavaScript runtimes which use 32-bit floating point numbers.

  Some games need even more robust random number generation, though, and one property of the Splitmix32 algorithm is that it's not too difficult for a computer to predict the next value with that PRNG. So for use cases such as card games where randomness is central to the gameplay, you can use a less predictable algorithm like sfc32.

  ```ts twoslash
  function sfc32(a: number, b: number, c: number, d: number) {
    return function seededRandomSfc32() {
      a |= 0;
      b |= 0;
      c |= 0;
      d |= 0;

      const t = (((a + b) | 0) + d) | 0;
      d = (d + 1) | 0;
      a = b ^ (b >>> 9);
      b = (c + (c << 3)) | 0;
      c = (c << 21) | (c >>> 11);
      c = (c + t) | 0;

      return (t >>> 0) / 4294967296;
    };
  }
  ```

  This PRNG takes in four seed values rather than just one. To come up with four good random seeds, a common technique is to start with a single seed value and pass it to a PRNG like Splitmix32, then run that four times to generate the new seeds. This is the approach we use in the Spud API's `createPrng` function.

  **See also:**

  * [A brief history of random numbers](https://crates.io/crates/oorandom#a-brief-history-of-random-numbers)
  * [Mulberry32: A Tiny, Fast, Deterministic RNG](https://www.4rknova.com/blog/2026/03/01/mulberry32-rng), a similar algorithm to Splitmix32. (Mulberry's creator now [recommends](https://gist.github.com/tommyettinger/46a874533244883189143505d203312c) using Splitmix32.)
  * [PractRand](https://pracrand.sourceforge.net/RNG_engines.txt) evaluation of RNG engines
</details>

<details>
  <summary>
    <strong>Copying code by hand? In 2026?</strong>
  </summary>

  Tempting as it may be to `npm install` a PRNG, I'd like to make the case for copying well-known algorithms like this one into your codebase by hand, the old-fashioned way.

  Honestly, I've never taken the time to understand how `splitmix32` or `sfc32` actually work--and the same goes for plenty of other algorithms I rely on all the time (Perlin noise, pathfinding, and hashing functions to name a few). These snippets pasted into my projects are still black boxes to me, but at least these ones I have a sense of ownership over. They're not buried in `node_modules`. Instead I can carry them from game to game as part of my personal ever-growing library of useful algorithms.

  When an algorithm lives side-by-side with your code, you not only get an intuitive sense for its impact on your game's performance and bundle size, but you can also quickly tell it's not running pathological loops or making network requests or mining for crypto. [Left-pad incidents](https://en.wikipedia.org/wiki/Npm_left-pad_incident) <small className="opacity-75">\[Wikipedia]</small> and supply-chain attacks simply aren't a concern the way they are with an `npm install`.

  Judging by the comments, the Quake III programmers in the 90s probably didn't fully understand the [fast inverse square root](https://en.wikipedia.org/wiki/Fast_inverse_square_root) <small className="opacity-75">\[Wikipedia]</small> they were shipping either--so if some of the best game programmers of all time did it, it's probably fine for you to not understand every line of a well-known algorithm pasted into your projects.

  ```c
  // fast inverse square root
  float Q_rsqrt( float number )
  {
  	long i;
  	float x2, y;
  	const float threehalfs = 1.5F;

  	x2 = number * 0.5F;
  	y  = number;
  	i  = * ( long * ) &y;                       // evil floating point bit level hacking
  	i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
  	y  = * ( float * ) &i;
  	y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
  //	y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

  	return y;
  }
  ```

  I wouldn't go as far as saying...

  {/* prettier-ignore */}

  <blockquote className="font-fancy italic">
    <q>
      Package managers are evil.
    </q>

    <br />

    <cite className="opacity-75">
      <a href="https://www.gingerbill.org/article/2025/09/08/package-managers-are-evil/">
        {"- GingerBill"}
      </a>
    </cite>
  </blockquote>

  ...but pasting a snippet is certainly no worse than hiding the unknown parts inside `node_modules`, where they might never be looked at or might pull in hundreds of their own dependencies that you can't practically audit anyway. Instead of installing code the "modern" way, I'd encourage you to make your games self-contained and dependency-free as much as you reasonably can, which often means inlining someone else's code.

  To get your personal library going, here are a few small helpers you can copy straight into your code and use with `splitmix32` or any other PRNG.
</details>

### Utility functions [#utility-functions]

Once you've created your seeded random number generator, here's how you can use it to pick a random item from an array:

```ts twoslash
function splitmix32(seed: number) {
  return function seededRandomSplitmix32() {
    seed |= 0;
    seed = (seed + 0x9e3779b9) | 0;
    let t = seed ^ (seed >>> 16);
    t = Math.imul(t, 0x21f0aaad);
    t = t ^ (t >>> 15);
    t = Math.imul(t, 0x735a2d97);
    return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;
  };
}
// ---cut---
// an arbitrary seed #
const seed = 123;

// our seeded random number generator
const random = splitmix32(seed);

// selects a random element from an array
function sample<T>(array: T[], rng = Math.random) {
  return array[Math.floor(rng() * array.length)];
}

// pass in the seeded PRNG
sample([5, 2, 7, 9, 1], random); // => 7
```

A handful of utilities that work with seeded random number generators:

```ts twoslash
// pick a random floating point number between min and max
function random(min: number, max: number, rng = Math.random) {
  return min + rng() * (max - min);
}

// pick a random integer between min and max
function randomInt(min: number, max: number, rng = Math.random) {
  return Math.floor(rng() * (max - min + 1)) + min;
}

// select a random element from an array
function sample<T>(array: T[], rng = Math.random) {
  return array[Math.floor(rng() * array.length)];
}

// shuffles an array in-place
function shuffle<T>(array: T[], rng = Math.random) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
  return array;
}
```

## Sine waves [#sine-waves]

A sine wave gives us a number that oscillates between -1 and +1. We can pass it the current time to get a value in `[-1, 1]` that changes over time.

```ts
y = Math.sin(time);
```

<SineWaveDemo />

To change the speed of the oscillation, include a multiplier:

```ts
y = Math.sin(time * speed);
```

<SineWaveSpeedDemo />

Most of the time it's easier to work with values scaled between 0 and 1, instead of the -1 to +1 range we get with `Math.sin`.

```ts
y = (Math.sin(time) + 1) / 2;
```

<NormalizedSineWaveDemo />

And if you want on/off logic, you can either round the value to give you `0` or `1`, or do a range check.

```ts
// rounded number (0 or 1)
y = Math.round((Math.sin(time) + 1) / 2);

// or a boolean value (true 50% of the time)
isOn = Math.sin(time) > 0;
```

<RoundedSineWaveDemo />

<Callout type="error" title="Don't pollute your code with more state than you need">
  ```ts
  const state = {
    isOn: true,
    timeSinceToggled: 0,
  };

  function update(dt: number) {
    state.timeSinceToggled += dt;
    if (state.timeSinceToggled > 1) {
      state.timeSinceToggled = 0;
      state.isOn = !state.isOn;
    }
  }
  ```
</Callout>

<Callout type="success" title="Sine waves to the rescue">
  Remember, [your simulation might not need state](https://www.onsclom.net/posts/simulator-state).

  ```ts
  const state = {
    time: 0,
  };

  function update(dt: number) {
    state.time += dt;
  }

  function isOn() {
    return Math.sin(state.time * Math.PI) > 0;
  }
  ```

  <details>
    <summary>
      <strong>Where did π come from?</strong>
    </summary>

    <PiTauModeProvider>
      Trigonometry. If you draw a dot moving around the unit circle, computing `Math.sin` of the angle from the origin to that dot gives us the dot's `y` position.

      <UnitCirclePiWalkthroughDemo />

      <PiTauPeriodProse />

      <SineWavePeriodWalkthroughDemo />

      <PiTauScaleIntroProse />

      <SineWaveScaleWalkthroughDemo />

      <PiTauScaleResultProse />

      <div className="mb-4 rounded-lg bg-fd-card px-4 pb-4 pt-3 [&_p]:my-0">
        [Tau](https://www.tauday.com/) enjoyers, feel free to use `τ` instead of the *inelegant* `2π` in these equations. ([Why?](https://vimeo.com/147792667) <small className="opacity-75">\[Vimeo]</small>)

        <PiTauToggleButton />
      </div>
    </PiTauModeProvider>
  </details>
</Callout>

If you want the same kind of repeating up-and-down change, but with linear movement instead of smooth easing at the top and bottom, you can use a triangle wave like we did for the DVD bounce in [Chapter 4](/guides/handmade-web-games/animation):

```ts
const period = 2; // seconds for one full up/down cycle
const t = (time % period) / period; // 0..1
const y = t < 0.5 ? t * 2 : 2 - t * 2;
```

<TriangleWaveDemo />

Here's an assortment of practical applications of sine waves for games.

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    <>    </>
  </slot>

  <slot path="steps">
    <>    </>
  </slot>

  <slot path="steps">
    <>    </>
  </slot>

  <slot path="steps">
    The only change from the previous example is the inclusion of `Math.round`.
  </slot>
</Scrollycoding>

## Projectiles [#projectiles]

If you were to draw an arrow from the origin `(0, 0)` to `(x, y)`, how would you determine the angle of that arrow? From trigonometry we know `tan(θ) = y / x`, so maybe you could use the arctangent `Math.atan(y / x)`?

<Callout type="error" title="Math.atan fails when x is negative">
  `Math.atan(y / x)` is unaware of which quadrant we are in, so it doesn't work for every point.
  Imagine testing the coordinates `(1, 1)` or `(-1, -1)`. `y / x` for both of them will equal `1`.

  <AtanQuadrantProblemDemo />
</Callout>

<Callout type="success" title="Math.atan2 solves this problem">
  To address this limitation of `Math.atan(y / x)`, a similar built-in function `Math.atan2(y, x)` takes in two arguments (hence the `2` in its name). This reliably gets us an angle in radians from `(0, 0)` to any `(x, y)`.

  <Atan2QuadrantSolutionDemo />
</Callout>

But how do we get an angle between any two points on our canvas, not just from `(0, 0)`? The trick is to translate so that one of the points is at the origin `(0, 0)`, then make the second point's coordinates relative to that new origin. From there, `atan2` will give you the angle from one point to the other in radians.

<Atan2AimDemo />

To pull out the `x` and `y` components from this angle, use the cosine and sine functions.

```ts twoslash
const origin = { x: 10, y: 20 };
const target = { x: 30, y: 40 };
const targetRelativeToOrigin = {
  x: target.x - origin.x,
  y: target.y - origin.y,
};
const angle = Math.atan2(
  targetRelativeToOrigin.y, // targetY - originY
  targetRelativeToOrigin.x, // targetX - originX
);
// ---cut---
const speed = 100;
const vx = Math.cos(angle) * speed; // x velocity
const vy = Math.sin(angle) * speed; // y velocity
```

<details>
  <summary>
    <strong>Slightly simpler option if you don't need the `angle`</strong>
  </summary>

  If all you need are the `x` and `y` velocities without first knowing the `angle`, you can avoid some trigonometry.

  ```ts twoslash
  const origin = { x: 10, y: 20 };
  const target = { x: 30, y: 40 };
  const targetRelativeToOrigin = {
    x: target.x - origin.x,
    y: target.y - origin.y,
  };
  const length = Math.hypot(
    targetRelativeToOrigin.x, // targetX - originX
    targetRelativeToOrigin.y, // targetY - originY
  );
  const speed = 100;
  const vx = (targetRelativeToOrigin.x / length) * speed;
  const vy = (targetRelativeToOrigin.y / length) * speed;
  ```
</details>

<Atan2ProjectileDemo />

From here you can add gravity, explosions, or anything else to the projectiles.

<Atan2ProjectileGroundDemo />

### Utility functions [#utility-functions-1]

```ts twoslash
// how long is the line from the origin (0, 0) to the point at (x, y)
function magnitude(x: number, y: number) {
  return Math.hypot(x, y);
}

// how long is the line between (x1, y1) and (x2, y2)
function distance(x1: number, y1: number, x2: number, y2: number) {
  return Math.hypot(x2 - x1, y2 - y1);
}

// what is the angle from the origin (0, 0) to the point at (x, y)
function angle(x: number, y: number) {
  return Math.atan2(y, x); // in radians
}

// what is the angle from (x1, y1) to (x2, y2)
function angleBetween(x1: number, y1: number, x2: number, y2: number) {
  return Math.atan2(y2 - y1, x2 - x1); // in radians
}

// if you need to convert the radians from `angle(x, y)` to degrees:
function radiansToDegrees(rad: number) {
  return (rad * 180) / Math.PI;
}

// or convert back from degrees to radians:
function degreesToRadians(deg: number) {
  return (deg * Math.PI) / 180;
}

// pull out the x and y components of the unit vector pointing
// in the direction of the `angle` in radians
function direction(angle: number) {
  return {
    x: Math.cos(angle),
    y: Math.sin(angle),
  };
}
```

## Collisions & intersections [#collisions--intersections]

Detecting when in-game objects overlap is essential not just for things like `didProjectileHitPlayer`, but also to keep the player from falling through platforms or even just to make it possible to click on stuff.

This section is less a guide and more a collection of common code snippets for collision detection algorithms between various shapes.

The functions in the next sections will tell you when two objects collide, but you'll need to compare the current and past collision state if you want to act on the instant the collision occurred:

```ts twoslash
type Point = { x: number; y: number };
type Circle = { x: number; y: number; radius: number };

/** returns true when the point is inside or on the edge of the circle */
function isPointInCircle(point: Point, circle: Circle): boolean {
  const { radius } = circle;
  const distX = point.x - circle.x; // (x – h)
  const distY = point.y - circle.y; // (y – k)
  return distX ** 2 + distY ** 2 <= radius ** 2;
}

/** an example function that might play a sound effect given its name */
function playSound(effect: string) {}

/** do something in state just on the frame the collision occurred */
function handleCollision(state: {
  wasColliding: boolean;
  player: { x: number; y: number };
  circle: { x: number; y: number; radius: number };
}) {}

// ---cut---
const state = {
  wasColliding: false, // stale state from previous frame
  player: { x: 20, y: 50 },
  circle: { x: 30, y: 40, radius: 50 },
};

function update(dt: number) {
  const isColliding = isPointInCircle(state.player, state.circle);
  const justEnteredCircle = isColliding && !state.wasColliding;
  if (justEnteredCircle) {
    playSound("whoosh");
    handleCollision(state);
  }
  state.wasColliding = isColliding;
}
```

### Point in shape [#point-in-shape]

<PointCollisionDemo />

<details>
  <summary>
    <strong>Code snippets for point-in-shape checks</strong>
  </summary>

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];
  type Circle = { x: number; y: number; radius: number };
  type Rect = { x: number; y: number; w: number; h: number }; // aka an axis-aligned bounding box (AABB)
  type RotatedRect = { x: number; y: number; w: number; h: number; angle: number }; // aka an oriented bounding box (OBB)
  type Polygon = Point[];

  function isPointOnSegment(point: Point, segment: Segment): boolean {
    const tolerance = 0.000001; // avoid floating point inaccuracy near the segment

    if (Math.abs(signedArea(segment[0], segment[1], point)) > tolerance) {
      return false;
    }

    const [a, b] = segment;
    return (
      Math.min(a.x, b.x) - point.x <= tolerance &&
      point.x - Math.max(a.x, b.x) <= tolerance &&
      Math.min(a.y, b.y) - point.y <= tolerance &&
      point.y - Math.max(a.y, b.y) <= tolerance
    );
  }

  function isPointInCircle(point: Point, circle: Circle): boolean {
    const { radius } = circle;
    const distX = point.x - circle.x; // (x – h)
    const distY = point.y - circle.y; // (y – k)
    return distX ** 2 + distY ** 2 <= radius ** 2;
  }

  function isPointInRect(point: Point, rect: Rect): boolean {
    return (
      point.x >= rect.x &&
      point.x <= rect.x + rect.w &&
      point.y >= rect.y &&
      point.y <= rect.y + rect.h
    );
  }

  function isPointInRotatedRect(point: Point, rect: RotatedRect): boolean {
    const localPoint = toRotatedRectLocalPoint(point, rect);
    return isPointInRect(localPoint, { x: -rect.w / 2, y: -rect.h / 2, w: rect.w, h: rect.h });
  }

  function isPointInPolygon(ctx: CanvasRenderingContext2D, point: Point, polygon: Polygon): boolean {
    return ctx.isPointInPath(polygonPath(polygon), point.x, point.y);
  }

  // helpers

  function signedArea(a: Point, b: Point, c: Point): number {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  }

  function rotatePointAround(point: Point, origin: Point, angle: number): Point {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const dx = point.x - origin.x;
    const dy = point.y - origin.y;

    return {
      x: origin.x + dx * cos - dy * sin,
      y: origin.y + dx * sin + dy * cos,
    };
  }

  function rotatedRectCenter(rect: RotatedRect): Point {
    return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
  }

  function toRotatedRectLocalPoint(point: Point, rect: RotatedRect): Point {
    const center = rotatedRectCenter(rect);
    const local = rotatePointAround(point, center, -rect.angle);
    return {
      x: local.x - center.x,
      y: local.y - center.y,
    };
  }

  function polygonPath(polygon: Polygon): Path2D {
    const path = new Path2D();
    const [first, ...rest] = polygon;

    path.moveTo(first.x, first.y);
    for (const point of rest) {
      path.lineTo(point.x, point.y);
    }
    path.closePath();

    return path;
  }
  ```
</details>

### Segment collisions [#segment-collisions]

<SegmentCollisionDemo />

<details>
  <summary>
    <strong>Code snippets for segment collision checks</strong>
  </summary>

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];
  type Circle = { x: number; y: number; radius: number };
  type Rect = { x: number; y: number; w: number; h: number }; // aka an axis-aligned bounding box (AABB)
  type RotatedRect = { x: number; y: number; w: number; h: number; angle: number }; // aka an oriented bounding box (OBB)
  type Polygon = Point[];

  function segmentsOverlap(a: Segment, b: Segment): boolean {
    const [a1, a2] = a;
    const [b1, b2] = b;
    const area1 = signedArea(a1, a2, b1);
    const area2 = signedArea(a1, a2, b2);
    const area3 = signedArea(b1, b2, a1);
    const area4 = signedArea(b1, b2, a2);

    if (signsDiffer(area1, area2) && signsDiffer(area3, area4)) {
      return true;
    }

    return (
      isPointOnSegment(b1, a) ||
      isPointOnSegment(b2, a) ||
      isPointOnSegment(a1, b) ||
      isPointOnSegment(a2, b)
    );
  }

  function segmentOverlapsCircle(segment: Segment, circle: Circle): boolean {
    return squaredDistanceFromPointToSegment(circle, segment) <= circle.radius ** 2;
  }

  function segmentOverlapsRect(segment: Segment, rect: Rect): boolean {
    const [a, b] = segment;
    if (isPointInRect(a, rect) || isPointInRect(b, rect)) {
      return true;
    }

    return rectEdges(rect).some((edge) => segmentsOverlap(segment, edge));
  }

  function segmentOverlapsRotatedRect(segment: Segment, rect: RotatedRect): boolean {
    const localSegment: Segment = [
      toRotatedRectLocalPoint(segment[0], rect),
      toRotatedRectLocalPoint(segment[1], rect),
    ];
    const localRect = { x: -rect.w / 2, y: -rect.h / 2, w: rect.w, h: rect.h };
    return segmentOverlapsRect(localSegment, localRect);
  }

  function segmentOverlapsPolygon(
    ctx: CanvasRenderingContext2D,
    segment: Segment,
    polygon: Polygon,
  ): boolean {
    const [a, b] = segment;
    if (isPointInPolygon(ctx, a, polygon) || isPointInPolygon(ctx, b, polygon)) {
      return true;
    }

    return polygonEdges(polygon).some((edge) => segmentsOverlap(segment, edge));
  }

  // helpers

  function clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
  }

  function squaredDistance(a: Point, b: Point): number {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return dx * dx + dy * dy;
  }

  function closestPointOnSegment(point: Point, [a, b]: Segment): Point {
    const segment = { x: b.x - a.x, y: b.y - a.y };
    const lengthSquared = segment.x * segment.x + segment.y * segment.y;

    if (lengthSquared === 0) {
      return a;
    }

    const projection = ((point.x - a.x) * segment.x + (point.y - a.y) * segment.y) / lengthSquared;
    const t = clamp(projection, 0, 1);

    return {
      x: a.x + t * segment.x,
      y: a.y + t * segment.y,
    };
  }

  function squaredDistanceFromPointToSegment(point: Point, segment: Segment): number {
    return squaredDistance(point, closestPointOnSegment(point, segment));
  }

  function isPointInRect(point: Point, rect: Rect): boolean {
    return (
      point.x >= rect.x &&
      point.x <= rect.x + rect.w &&
      point.y >= rect.y &&
      point.y <= rect.y + rect.h
    );
  }

  function signedArea(a: Point, b: Point, c: Point): number {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  }

  function isPointOnSegment(point: Point, [a, b]: Segment): boolean {
    const tolerance = 0.000001; // epsilon to avoid floating point inaccuracy

    if (Math.abs(signedArea(a, b, point)) > tolerance) {
      return false;
    }

    return (
      point.x >= Math.min(a.x, b.x) - tolerance &&
      point.x <= Math.max(a.x, b.x) + tolerance &&
      point.y >= Math.min(a.y, b.y) - tolerance &&
      point.y <= Math.max(a.y, b.y) + tolerance
    );
  }

  function signsDiffer(a: number, b: number): boolean {
    return (a < 0 && b > 0) || (a > 0 && b < 0);
  }

  function rectEdges(rect: Rect): Segment[] {
    const topLeft = { x: rect.x, y: rect.y };
    const topRight = { x: rect.x + rect.w, y: rect.y };
    const bottomRight = { x: rect.x + rect.w, y: rect.y + rect.h };
    const bottomLeft = { x: rect.x, y: rect.y + rect.h };

    return [
      [topLeft, topRight],
      [topRight, bottomRight],
      [bottomRight, bottomLeft],
      [bottomLeft, topLeft],
    ];
  }

  function rotatePointAround(point: Point, origin: Point, angle: number): Point {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const dx = point.x - origin.x;
    const dy = point.y - origin.y;

    return {
      x: origin.x + dx * cos - dy * sin,
      y: origin.y + dx * sin + dy * cos,
    };
  }

  function rotatedRectCenter(rect: RotatedRect): Point {
    return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
  }

  function toRotatedRectLocalPoint(point: Point, rect: RotatedRect): Point {
    const center = rotatedRectCenter(rect);
    const local = rotatePointAround(point, center, -rect.angle);
    return {
      x: local.x - center.x,
      y: local.y - center.y,
    };
  }

  function isPointInPolygon(ctx: CanvasRenderingContext2D, point: Point, polygon: Polygon): boolean {
    return ctx.isPointInPath(polygonPath(polygon), point.x, point.y);
  }

  function polygonPath(polygon: Polygon): Path2D {
    const path = new Path2D();
    const [first, ...rest] = polygon;

    path.moveTo(first.x, first.y);
    for (const point of rest) {
      path.lineTo(point.x, point.y);
    }
    path.closePath();

    return path;
  }

  function polygonEdges(polygon: Polygon): Segment[] {
    return polygon.map((point, index) => [point, polygon[(index + 1) % polygon.length]]);
  }
  ```
</details>

### Ray collisions [#ray-collisions]

<RayCollisionDemo />

<details>
  <summary>
    <strong>Code snippets for ray intersection checks</strong>
  </summary>

  For rays it's often useful to know where the collision occurred, too--not just *that* there was a collision. So all the `rayIntersects*` functions return a `number`, not a `boolean`, with the distance along the ray to the detected collision (or `Infinity` if no collision).

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];
  type Ray = { origin: Point; direction: Point };
  type Circle = { x: number; y: number; radius: number };
  type Rect = { x: number; y: number; w: number; h: number }; // aka an axis-aligned bounding box (AABB)
  type RotatedRect = { x: number; y: number; w: number; h: number; angle: number }; // aka an oriented bounding box (OBB)
  type Polygon = Point[];

  function rayIntersectsSegment(ray: Ray, segment: Segment): number {
    const [a, b] = segment;
    const segmentDirection = { x: b.x - a.x, y: b.y - a.y };
    const originToSegment = { x: a.x - ray.origin.x, y: a.y - ray.origin.y };
    const denominator = cross(ray.direction, segmentDirection);
    const tolerance = 0.000001;

    if (Math.abs(denominator) < tolerance) {
      if (Math.abs(cross(originToSegment, ray.direction)) > tolerance) {
        return Infinity;
      }

      const rayLengthSquared = dot(ray.direction, ray.direction);
      const t1 = dot(originToSegment, ray.direction) / rayLengthSquared;
      const t2 =
        dot({ x: b.x - ray.origin.x, y: b.y - ray.origin.y }, ray.direction) / rayLengthSquared;

      if (t1 < 0 && t2 < 0) {
        return Infinity;
      }

      return Math.max(0, Math.min(t1, t2));
    }

    const t = cross(originToSegment, segmentDirection) / denominator;
    const u = cross(originToSegment, ray.direction) / denominator;

    return t >= 0 && u >= 0 && u <= 1 ? t : Infinity;
  }

  function rayIntersectsCircle(ray: Ray, circle: Circle): number {
    const toCircle = { x: ray.origin.x - circle.x, y: ray.origin.y - circle.y };
    const a = dot(ray.direction, ray.direction);
    const b = 2 * dot(toCircle, ray.direction);
    const c = dot(toCircle, toCircle) - circle.radius ** 2;
    const discriminant = b * b - 4 * a * c;

    if (discriminant < 0) {
      return Infinity;
    }

    const sqrtDiscriminant = Math.sqrt(discriminant);
    const t1 = (-b - sqrtDiscriminant) / (2 * a);
    const t2 = (-b + sqrtDiscriminant) / (2 * a);

    if (t1 >= 0) return t1;
    if (t2 >= 0) return 0;
    return Infinity;
  }

  function rayIntersectsRect(ray: Ray, rect: Rect): number {
    let tMin = 0;
    let tMax = Infinity;
    const axes = [
      { origin: ray.origin.x, direction: ray.direction.x, min: rect.x, max: rect.x + rect.w },
      { origin: ray.origin.y, direction: ray.direction.y, min: rect.y, max: rect.y + rect.h },
    ];

    for (const axis of axes) {
      if (axis.direction === 0) {
        if (axis.origin < axis.min || axis.origin > axis.max) {
          return Infinity;
        }
        continue;
      }

      const t1 = (axis.min - axis.origin) / axis.direction;
      const t2 = (axis.max - axis.origin) / axis.direction;
      tMin = Math.max(tMin, Math.min(t1, t2));
      tMax = Math.min(tMax, Math.max(t1, t2));

      if (tMin > tMax) {
        return Infinity;
      }
    }

    return tMin;
  }

  function rayIntersectsRotatedRect(ray: Ray, rect: RotatedRect): number {
    const center = rotatedRectCenter(rect);
    const localOrigin = toRotatedRectLocalPoint(ray.origin, rect);
    const localDirectionEnd = rotatePointAround(
      { x: center.x + ray.direction.x, y: center.y + ray.direction.y },
      center,
      -rect.angle,
    );
    const localDirection = { x: localDirectionEnd.x - center.x, y: localDirectionEnd.y - center.y };

    return rayIntersectsRect(
      { origin: localOrigin, direction: localDirection },
      { x: -rect.w / 2, y: -rect.h / 2, w: rect.w, h: rect.h },
    );
  }

  function rayIntersectsPolygon(ctx: CanvasRenderingContext2D, ray: Ray, polygon: Polygon): number {
    if (isPointInPolygon(ctx, ray.origin, polygon)) {
      return 0;
    }

    return Math.min(...polygonEdges(polygon).map((edge) => rayIntersectsSegment(ray, edge)));
  }

  // helpers

  function dot(a: Point, b: Point): number {
    return a.x * b.x + a.y * b.y;
  }

  function cross(a: Point, b: Point): number {
    return a.x * b.y - a.y * b.x;
  }

  function pointOnRay(ray: Ray, t: number): Point {
    return {
      x: ray.origin.x + ray.direction.x * t,
      y: ray.origin.y + ray.direction.y * t,
    };
  }

  function rotatePointAround(point: Point, origin: Point, angle: number): Point {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const dx = point.x - origin.x;
    const dy = point.y - origin.y;

    return {
      x: origin.x + dx * cos - dy * sin,
      y: origin.y + dx * sin + dy * cos,
    };
  }

  function rotatedRectCenter(rect: RotatedRect): Point {
    return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
  }

  function toRotatedRectLocalPoint(point: Point, rect: RotatedRect): Point {
    const center = rotatedRectCenter(rect);
    const local = rotatePointAround(point, center, -rect.angle);
    return {
      x: local.x - center.x,
      y: local.y - center.y,
    };
  }

  function isPointInPolygon(ctx: CanvasRenderingContext2D, point: Point, polygon: Polygon): boolean {
    return ctx.isPointInPath(polygonPath(polygon), point.x, point.y);
  }

  function polygonPath(polygon: Polygon): Path2D {
    const path = new Path2D();
    const [first, ...rest] = polygon;

    path.moveTo(first.x, first.y);
    for (const point of rest) {
      path.lineTo(point.x, point.y);
    }
    path.closePath();

    return path;
  }

  function polygonEdges(polygon: Polygon): Segment[] {
    return polygon.map((point, index) => [point, polygon[(index + 1) % polygon.length]]);
  }
  ```
</details>

### Circle collisions [#circle-collisions]

<CircleCollisionDemo />

<details>
  <summary>
    <strong>Code snippets for circle collision checks</strong>
  </summary>

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];
  type Circle = { x: number; y: number; radius: number };
  type Rect = { x: number; y: number; w: number; h: number };
  type RotatedRect = { x: number; y: number; w: number; h: number; angle: number };
  type Polygon = Point[];

  function circlesOverlap(a: Circle, b: Circle): boolean {
    return squaredDistance(a, b) <= (a.radius + b.radius) ** 2;
  }

  // circleOverlapsSegment: see segmentOverlapsCircle

  function circleOverlapsRect(circle: Circle, rect: Rect): boolean {
    return circleOverlapsAabb(circle.x, circle.y, circle.radius, rect.x, rect.y, rect.w, rect.h);
  }

  function circleOverlapsRotatedRect(circle: Circle, rect: RotatedRect): boolean {
    const localCenter = toRotatedRectLocalPoint(circle, rect);
    return circleOverlapsAabb(
      localCenter.x,
      localCenter.y,
      circle.radius,
      -rect.w / 2,
      -rect.h / 2,
      rect.w,
      rect.h,
    );
  }

  function circleOverlapsPolygon(
    ctx: CanvasRenderingContext2D,
    circle: Circle,
    polygon: Polygon,
  ): boolean {
    if (isPointInPolygon(ctx, circle, polygon)) {
      return true;
    }

    return polygonEdges(polygon).some(
      (edge) => squaredDistanceFromPointToSegment(circle, edge) <= circle.radius ** 2,
    );
  }

  // helpers

  function clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
  }

  function squaredDistance(a: Point, b: Point): number {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return dx * dx + dy * dy;
  }

  function squaredDistanceFromPointToSegment(point: Point, segment: Segment): number {
    return squaredDistance(point, closestPointOnSegment(point, segment));
  }

  function circleOverlapsAabb(
    circleX: number,
    circleY: number,
    radius: number,
    rectX: number,
    rectY: number,
    rectW: number,
    rectH: number,
  ): boolean {
    const closestX = clamp(circleX, rectX, rectX + rectW);
    const closestY = clamp(circleY, rectY, rectY + rectH);
    const dx = circleX - closestX;
    const dy = circleY - closestY;
    return dx * dx + dy * dy <= radius ** 2;
  }

  function closestPointOnSegment(point: Point, [a, b]: Segment): Point {
    const segment = { x: b.x - a.x, y: b.y - a.y };
    const lengthSquared = segment.x * segment.x + segment.y * segment.y;

    if (lengthSquared === 0) {
      return a;
    }

    const projection = ((point.x - a.x) * segment.x + (point.y - a.y) * segment.y) / lengthSquared;
    const t = clamp(projection, 0, 1);

    return {
      x: a.x + t * segment.x,
      y: a.y + t * segment.y,
    };
  }

  function rotatePointAround(point: Point, origin: Point, angle: number): Point {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const dx = point.x - origin.x;
    const dy = point.y - origin.y;

    return {
      x: origin.x + dx * cos - dy * sin,
      y: origin.y + dx * sin + dy * cos,
    };
  }

  function rotatedRectCenter(rect: RotatedRect): Point {
    return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
  }

  function toRotatedRectLocalPoint(point: Point, rect: RotatedRect): Point {
    const center = rotatedRectCenter(rect);
    const local = rotatePointAround(point, center, -rect.angle);
    return {
      x: local.x - center.x,
      y: local.y - center.y,
    };
  }

  function isPointInPolygon(ctx: CanvasRenderingContext2D, point: Point, polygon: Polygon): boolean {
    return ctx.isPointInPath(polygonPath(polygon), point.x, point.y);
  }

  function polygonPath(polygon: Polygon): Path2D {
    const path = new Path2D();
    const [first, ...rest] = polygon;

    path.moveTo(first.x, first.y);
    for (const point of rest) {
      path.lineTo(point.x, point.y);
    }
    path.closePath();

    return path;
  }

  function polygonEdges(polygon: Polygon): Segment[] {
    return polygon.map((point, index) => [point, polygon[(index + 1) % polygon.length]]);
  }
  ```
</details>

### Rect collisions (AABB) [#rect-collisions-aabb]

When a rectangle is in the same orientation as the game canvas itself it's considered to be "axis-aligned". Collision detection is simpler with axis-aligned rectangles than rotated ones. A common abbreviation you'll see in many game engines for this is AABB (**axis-aligned bounding box*&#x2A;). The alternative, for rotated rectangles, is an &#x2A;*"oriented bounding box"**, which we'll show in the next section.

<RectCollisionDemo />

<details>
  <summary>
    <strong>Code snippets for rect collision checks</strong>
  </summary>

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];
  type Rect = { x: number; y: number; w: number; h: number };
  type RotatedRect = { x: number; y: number; w: number; h: number; angle: number };
  type Polygon = Point[];

  function rectsOverlap(a: Rect, b: Rect): boolean {
    return (
      a.x <= b.x + b.w &&
      a.x + a.w >= b.x && // X-axis overlap
      a.y <= b.y + b.h &&
      a.y + a.h >= b.y // Y-axis overlap
    );
  }

  // rectOverlapsSegment: see segmentOverlapsRect
  // rectOverlapsCircle: see circleOverlapsRect

  function rectOverlapsRotatedRect(rect: Rect, rotatedRect: RotatedRect): boolean {
    return convexPolygonsOverlap(rectCorners(rect), rotatedRectCorners(rotatedRect));
  }

  function rectOverlapsPolygon(ctx: CanvasRenderingContext2D, rect: Rect, polygon: Polygon): boolean {
    const corners = rectCorners(rect);
    return (
      corners.some((corner) => isPointInPolygon(ctx, corner, polygon)) ||
      polygon.some((point) => isPointInRect(point, rect)) ||
      polygonEdges(polygon).some((polygonEdge) =>
        rectEdges(rect).some((rectEdge) => segmentsOverlap(polygonEdge, rectEdge)),
      )
    );
  }

  // helpers

  function isPointInRect(point: Point, rect: Rect): boolean {
    return (
      point.x >= rect.x &&
      point.x <= rect.x + rect.w &&
      point.y >= rect.y &&
      point.y <= rect.y + rect.h
    );
  }

  function rectCorners(rect: Rect): Point[] {
    return [
      { x: rect.x, y: rect.y },
      { x: rect.x + rect.w, y: rect.y },
      { x: rect.x + rect.w, y: rect.y + rect.h },
      { x: rect.x, y: rect.y + rect.h },
    ];
  }

  function rectEdges(rect: Rect): Segment[] {
    return polygonEdges(rectCorners(rect));
  }

  function rotatedRectCorners(rect: RotatedRect): Point[] {
    const center = rotatedRectCenter(rect);
    const halfWidth = rect.w / 2;
    const halfHeight = rect.h / 2;
    const localCorners = [
      { x: -halfWidth, y: -halfHeight },
      { x: halfWidth, y: -halfHeight },
      { x: halfWidth, y: halfHeight },
      { x: -halfWidth, y: halfHeight },
    ];

    return localCorners.map((point) =>
      rotatePointAround({ x: center.x + point.x, y: center.y + point.y }, center, rect.angle),
    );
  }

  function rotatedRectCenter(rect: RotatedRect): Point {
    return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
  }

  function rotatePointAround(point: Point, origin: Point, angle: number): Point {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const dx = point.x - origin.x;
    const dy = point.y - origin.y;

    return {
      x: origin.x + dx * cos - dy * sin,
      y: origin.y + dx * sin + dy * cos,
    };
  }

  function convexPolygonsOverlap(a: Polygon, b: Polygon): boolean {
    const axes = [...polygonAxes(a), ...polygonAxes(b)];

    for (const axis of axes) {
      if (!projectionsOverlap(projectPointsOntoAxis(a, axis), projectPointsOntoAxis(b, axis))) {
        return false;
      }
    }

    return true;
  }

  function polygonAxes(polygon: Polygon): Point[] {
    return polygonEdges(polygon).map(([a, b]) => normalFromEdge(a, b));
  }

  function normalFromEdge(a: Point, b: Point): Point {
    return { x: a.y - b.y, y: b.x - a.x };
  }

  function projectPointsOntoAxis(points: Point[], axis: Point): { min: number; max: number } {
    let min = Infinity;
    let max = -Infinity;

    for (const point of points) {
      const projection = point.x * axis.x + point.y * axis.y;
      min = Math.min(min, projection);
      max = Math.max(max, projection);
    }

    return { min, max };
  }

  function projectionsOverlap(
    a: { min: number; max: number },
    b: { min: number; max: number },
  ): boolean {
    return a.min <= b.max && a.max >= b.min;
  }

  function segmentsOverlap(a: Segment, b: Segment): boolean {
    const [a1, a2] = a;
    const [b1, b2] = b;
    const area1 = signedArea(a1, a2, b1);
    const area2 = signedArea(a1, a2, b2);
    const area3 = signedArea(b1, b2, a1);
    const area4 = signedArea(b1, b2, a2);

    if (signsDiffer(area1, area2) && signsDiffer(area3, area4)) {
      return true;
    }

    return (
      isPointOnSegment(b1, a) ||
      isPointOnSegment(b2, a) ||
      isPointOnSegment(a1, b) ||
      isPointOnSegment(a2, b)
    );
  }

  function isPointOnSegment(point: Point, [a, b]: Segment): boolean {
    const tolerance = 0.000001;

    if (Math.abs(signedArea(a, b, point)) > tolerance) {
      return false;
    }

    return (
      Math.min(a.x, b.x) - point.x <= tolerance &&
      point.x - Math.max(a.x, b.x) <= tolerance &&
      Math.min(a.y, b.y) - point.y <= tolerance &&
      point.y - Math.max(a.y, b.y) <= tolerance
    );
  }

  function signsDiffer(a: number, b: number): boolean {
    return (a < 0 && b > 0) || (a > 0 && b < 0);
  }

  function signedArea(a: Point, b: Point, c: Point): number {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  }

  function isPointInPolygon(ctx: CanvasRenderingContext2D, point: Point, polygon: Polygon): boolean {
    return ctx.isPointInPath(polygonPath(polygon), point.x, point.y);
  }

  function polygonPath(polygon: Polygon): Path2D {
    const path = new Path2D();
    const [first, ...rest] = polygon;

    path.moveTo(first.x, first.y);
    for (const point of rest) {
      path.lineTo(point.x, point.y);
    }
    path.closePath();

    return path;
  }

  function polygonEdges(polygon: Polygon): Segment[] {
    return polygon.map((point, index) => [point, polygon[(index + 1) % polygon.length]]);
  }
  ```
</details>

### Rotated rect collisions (OBB) [#rotated-rect-collisions-obb]

<RotatedRectCollisionDemo />

<details>
  <summary>
    <strong>Code snippets for rotated rect collision checks</strong>
  </summary>

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];
  type RotatedRect = { x: number; y: number; w: number; h: number; angle: number };
  type Polygon = Point[];

  // rotatedRectOverlapsSegment: see segmentOverlapsRotatedRect
  // rotatedRectOverlapsCircle: see circleOverlapsRotatedRect
  // rotatedRectOverlapsRect: see rectOverlapsRotatedRect

  function rotatedRectsOverlap(a: RotatedRect, b: RotatedRect): boolean {
    return convexPolygonsOverlap(rotatedRectCorners(a), rotatedRectCorners(b));
  }

  function rotatedRectOverlapsPolygon(
    ctx: CanvasRenderingContext2D,
    rect: RotatedRect,
    polygon: Polygon,
  ): boolean {
    const corners = rotatedRectCorners(rect);
    return (
      corners.some((corner) => isPointInPolygon(ctx, corner, polygon)) ||
      polygon.some((point) => isPointInRotatedRect(point, rect)) ||
      polygonEdges(polygon).some((polygonEdge) =>
        polygonEdges(corners).some((rectEdge) => segmentsOverlap(polygonEdge, rectEdge)),
      )
    );
  }

  // helpers

  function isPointInRotatedRect(point: Point, rect: RotatedRect): boolean {
    const localPoint = toRotatedRectLocalPoint(point, rect);
    return (
      localPoint.x >= -rect.w / 2 &&
      localPoint.x <= rect.w / 2 &&
      localPoint.y >= -rect.h / 2 &&
      localPoint.y <= rect.h / 2
    );
  }

  function rotatedRectCorners(rect: RotatedRect): Point[] {
    const center = rotatedRectCenter(rect);
    const halfWidth = rect.w / 2;
    const halfHeight = rect.h / 2;
    const localCorners = [
      { x: -halfWidth, y: -halfHeight },
      { x: halfWidth, y: -halfHeight },
      { x: halfWidth, y: halfHeight },
      { x: -halfWidth, y: halfHeight },
    ];

    return localCorners.map((point) =>
      rotatePointAround({ x: center.x + point.x, y: center.y + point.y }, center, rect.angle),
    );
  }

  function rotatedRectCenter(rect: RotatedRect): Point {
    return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
  }

  function toRotatedRectLocalPoint(point: Point, rect: RotatedRect): Point {
    const center = rotatedRectCenter(rect);
    const local = rotatePointAround(point, center, -rect.angle);
    return {
      x: local.x - center.x,
      y: local.y - center.y,
    };
  }

  function rotatePointAround(point: Point, origin: Point, angle: number): Point {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const dx = point.x - origin.x;
    const dy = point.y - origin.y;

    return {
      x: origin.x + dx * cos - dy * sin,
      y: origin.y + dx * sin + dy * cos,
    };
  }

  function convexPolygonsOverlap(a: Polygon, b: Polygon): boolean {
    const axes = [...polygonAxes(a), ...polygonAxes(b)];

    for (const axis of axes) {
      if (!projectionsOverlap(projectPointsOntoAxis(a, axis), projectPointsOntoAxis(b, axis))) {
        return false;
      }
    }

    return true;
  }

  function polygonAxes(polygon: Polygon): Point[] {
    return polygonEdges(polygon).map(([a, b]) => normalFromEdge(a, b));
  }

  function normalFromEdge(a: Point, b: Point): Point {
    return { x: a.y - b.y, y: b.x - a.x };
  }

  function projectPointsOntoAxis(points: Point[], axis: Point): { min: number; max: number } {
    let min = Infinity;
    let max = -Infinity;

    for (const point of points) {
      const projection = point.x * axis.x + point.y * axis.y;
      min = Math.min(min, projection);
      max = Math.max(max, projection);
    }

    return { min, max };
  }

  function projectionsOverlap(
    a: { min: number; max: number },
    b: { min: number; max: number },
  ): boolean {
    return a.min <= b.max && a.max >= b.min;
  }

  function segmentsOverlap(a: Segment, b: Segment): boolean {
    const [a1, a2] = a;
    const [b1, b2] = b;
    const area1 = signedArea(a1, a2, b1);
    const area2 = signedArea(a1, a2, b2);
    const area3 = signedArea(b1, b2, a1);
    const area4 = signedArea(b1, b2, a2);

    if (signsDiffer(area1, area2) && signsDiffer(area3, area4)) {
      return true;
    }

    return (
      isPointOnSegment(b1, a) ||
      isPointOnSegment(b2, a) ||
      isPointOnSegment(a1, b) ||
      isPointOnSegment(a2, b)
    );
  }

  function isPointOnSegment(point: Point, [a, b]: Segment): boolean {
    const tolerance = 0.000001;

    if (Math.abs(signedArea(a, b, point)) > tolerance) {
      return false;
    }

    return (
      Math.min(a.x, b.x) - point.x <= tolerance &&
      point.x - Math.max(a.x, b.x) <= tolerance &&
      Math.min(a.y, b.y) - point.y <= tolerance &&
      point.y - Math.max(a.y, b.y) <= tolerance
    );
  }

  function signsDiffer(a: number, b: number): boolean {
    return (a < 0 && b > 0) || (a > 0 && b < 0);
  }

  function signedArea(a: Point, b: Point, c: Point): number {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  }

  function isPointInPolygon(ctx: CanvasRenderingContext2D, point: Point, polygon: Polygon): boolean {
    return ctx.isPointInPath(polygonPath(polygon), point.x, point.y);
  }

  function polygonPath(polygon: Polygon): Path2D {
    const path = new Path2D();
    const [first, ...rest] = polygon;

    path.moveTo(first.x, first.y);
    for (const point of rest) {
      path.lineTo(point.x, point.y);
    }
    path.closePath();

    return path;
  }

  function polygonEdges(polygon: Polygon): Segment[] {
    return polygon.map((point, index) => [point, polygon[(index + 1) % polygon.length]]);
  }
  ```
</details>

### Polygon collisions [#polygon-collisions]

<Callout type="warning">
  Polygon collisions are expensive to compute and often not actually necessary. Instead, see if you
  can get away with simplifying your hit boxes to simpler shapes (rectangles, points, and circles).
</Callout>

<PolygonCollisionDemo />

<details>
  <summary>
    <strong>Code snippets for polygon collision checks</strong>
  </summary>

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];
  type Polygon = Point[];

  // polygonOverlapsSegment: see segmentOverlapsPolygon
  // polygonOverlapsCircle: see circleOverlapsPolygon
  // polygonOverlapsRect: see rectOverlapsPolygon
  // polygonOverlapsRotatedRect: see rotatedRectOverlapsPolygon

  function polygonsOverlap(ctx: CanvasRenderingContext2D, a: Polygon, b: Polygon): boolean {
    return (
      a.some((point) => isPointInPolygon(ctx, point, b)) ||
      b.some((point) => isPointInPolygon(ctx, point, a)) ||
      polygonEdges(a).some((edgeA) => polygonEdges(b).some((edgeB) => segmentsOverlap(edgeA, edgeB)))
    );
  }

  // helpers

  function isPointInPolygon(ctx: CanvasRenderingContext2D, point: Point, polygon: Polygon): boolean {
    return ctx.isPointInPath(polygonPath(polygon), point.x, point.y);
  }

  function polygonPath(polygon: Polygon): Path2D {
    const path = new Path2D();
    const [first, ...rest] = polygon;

    path.moveTo(first.x, first.y);
    for (const point of rest) {
      path.lineTo(point.x, point.y);
    }
    path.closePath();

    return path;
  }

  function polygonEdges(polygon: Polygon): Segment[] {
    return polygon.map((point, index) => [point, polygon[(index + 1) % polygon.length]]);
  }

  function segmentsOverlap(a: Segment, b: Segment): boolean {
    const [a1, a2] = a;
    const [b1, b2] = b;
    const area1 = signedArea(a1, a2, b1);
    const area2 = signedArea(a1, a2, b2);
    const area3 = signedArea(b1, b2, a1);
    const area4 = signedArea(b1, b2, a2);

    if (signsDiffer(area1, area2) && signsDiffer(area3, area4)) {
      return true;
    }

    return (
      isPointOnSegment(b1, a) ||
      isPointOnSegment(b2, a) ||
      isPointOnSegment(a1, b) ||
      isPointOnSegment(a2, b)
    );
  }

  function isPointOnSegment(point: Point, [a, b]: Segment): boolean {
    const tolerance = 0.000001;

    if (Math.abs(signedArea(a, b, point)) > tolerance) {
      return false;
    }

    return (
      Math.min(a.x, b.x) - point.x <= tolerance &&
      point.x - Math.max(a.x, b.x) <= tolerance &&
      Math.min(a.y, b.y) - point.y <= tolerance &&
      point.y - Math.max(a.y, b.y) <= tolerance
    );
  }

  function signsDiffer(a: number, b: number): boolean {
    return (a < 0 && b > 0) || (a > 0 && b < 0);
  }

  function signedArea(a: Point, b: Point, c: Point): number {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  }
  ```
</details>

### Distance checks [#distance-checks]

<DistanceChecksDemo />

<details>
  <summary>
    <strong>Code snippets for distance checks</strong>
  </summary>

  ```ts twoslash
  type Point = { x: number; y: number };
  type Segment = [Point, Point];

  function distance(a: Point, b: Point): number {
    return Math.sqrt(squaredDistance(a, b));
  }

  // runs faster than Math.sqrt, so if you're doing a ton of distance
  // calculations in a hot loop, use squared values
  function squaredDistance(a: Point, b: Point): number {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return dx * dx + dy * dy;
  }

  function squaredDistanceFromPointToSegment(point: Point, segment: Segment): number {
    return squaredDistance(point, closestPointOnSegment(point, segment));
  }

  // returns where segment a first hits segment b (0..1), or Infinity if they don't overlap
  function segmentIntersectionT(a: Segment, b: Segment): number {
    const [a1, a2] = a;
    const [b1, b2] = b;
    const dx1 = a2.x - a1.x;
    const dy1 = a2.y - a1.y;
    const dx2 = b2.x - b1.x;
    const dy2 = b2.y - b1.y;
    const denominator = dx1 * dy2 - dy1 * dx2;

    if (denominator === 0) {
      return Infinity;
    }

    const t = ((b1.x - a1.x) * dy2 - (b1.y - a1.y) * dx2) / denominator;
    const u = ((b1.x - a1.x) * dy1 - (b1.y - a1.y) * dx1) / denominator;

    if (t < 0 || t > 1 || u < 0 || u > 1) {
      return Infinity;
    }

    return t;
  }

  // helpers

  function clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
  }

  function closestPointOnSegment(point: Point, [a, b]: Segment): Point {
    const segment = { x: b.x - a.x, y: b.y - a.y };
    const lengthSquared = segment.x * segment.x + segment.y * segment.y;

    if (lengthSquared === 0) {
      return a;
    }

    const projection = ((point.x - a.x) * segment.x + (point.y - a.y) * segment.y) / lengthSquared;
    const t = clamp(projection, 0, 1);

    return {
      x: a.x + t * segment.x,
      y: a.y + t * segment.y,
    };
  }
  ```
</details>

## Dot product [#dot-product]

The dot product of two vectors tells you how much they're pointed in the same direction.

* 1 means they're pointed exactly the same way
* 0 means they're perpendicular to one another
* -1 means they're pointed exactly opposite ways

```ts twoslash
type Vec2 = { x: number; y: number };

function dot(a: Vec2, b: Vec2) {
  return a.x * b.x + a.y * b.y;
}
```

If you need to determine whether an object is in front of vs behind something else, or within some enemy vision cone in a stealth game, for example, then the dot product is a useful tool.

<DotProductUnitCircleDemo />

You can also use `dot` to find which <strong className="font-bold text-rose-700 dark:text-amber-400">target</strong> best matches the player's <strong className="font-bold text-sky-600 dark:text-sky-400">aim</strong>, or in a sports game to determine which player to pass to. Just note that the dot product only tells you how close the angles line up--it doesn't incorporate the distance on its own.

<DotProductAimAssistDemo />

The dot product is typically used on normalized vectors.

```ts twoslash
type Vec2 = { x: number; y: number };
function dot(a: Vec2, b: Vec2) {
  return a.x * b.x + a.y * b.y;
}

// ---cut---
function normalize(x: number, y: number): Vec2 {
  const length = Math.hypot(x, y);
  if (length === 0) {
    return { x: 0, y: 0 };
  }
  return { x: x / length, y: y / length };
}

const move = normalize(0.4, -0.9);
const aim = normalize(0.2, -0.2);

const alignment = dot(move, aim); // => 0.933...
```

## Up next [#up-next]

So far, we've drawn directly in canvas coordinates. In the next chapter, we'll add a camera so your game can have a world larger than the screen. [Chapter 6 →](/guides/handmade-web-games/cameras-and-viewports) covers cameras, viewports, and when to work in world space vs screen space.

## See also [#see-also]

* [How to Turn a Few Numbers into Worlds](https://www.youtube.com/watch?v=ZsEnnB2wrbI) <small className="opacity-75">\[YouTube]</small> — fractal Perlin noise explained, by The Taylor Series
* [Making Randomness](https://www.youtube.com/watch?v=bTA2LWTv5co) <small className="opacity-75">\[YouTube]</small> — pseudorandom number generators explained, by Jorge Rodriguez
* [Math for Game Devs](https://www.youtube.com/watch?v=MOYiVLEnhrw) <small className="opacity-75">\[YouTube]</small> — four-part lecture series by Freya Holmér (+ [Part 2](https://www.youtube.com/watch?v=XiwEyopOMqg), [Part 3](https://www.youtube.com/watch?v=1NLekEd770w), [Part 4](https://www.youtube.com/watch?v=-Ii3MrJFBkQ))
* [Math Visualizations](https://cat.gay/mathvis/index.html) — interactive visual math references by Freya Holmér
* [Tools of the Trade](https://www.youtube.com/watch?v=YNtoDGS4uak) <small className="opacity-75">\[YouTube]</small> — 2025 mathematics talk by GingerBill
* [The Ultimate Guide to Cross Product and Dot Product](https://devforum.roblox.com/t/the-ultimate-guide-to-vector3cross-and-vector3dot/953984) — a post by Moonvane on the Roblox developer forums
* [Trigonometry](https://www.youtube.com/watch?v=-dGi2Ffdiuk\&list=PLFt_AvWsXl0fnA91TcmkRyhhixX9CO3Lw\&index=23) <small className="opacity-75">\[YouTube]</small> — from Sebastian Lague's *Introduction to Game Development* series
* [What Kind of Math Should Game Developers Know?](https://www.youtube.com/watch?v=eRVRioN4GwA) <small className="opacity-75">\[YouTube]</small> — overview of common math concepts for game development, by SimonDev
