# Chapter 4: Animation, pixel art, and sprites (/guides/handmade-web-games/animation)





Animations usually aren't built from just one technique, but are made up of a combination of patterns that can be assembled in different ways. So rather than just showing you isolated code examples, we'll take a different approach in this chapter. We'll build four small projects to teach the fundamental animation techniques you'll use frequently in web games.

1. **Bouncing ball.** <span className="opacity-75">Velocity, acceleration, and basic collisions.</span>
2. **Bouncing DVD logo.** <span className="opacity-75">Separating simulation from animation.</span>
3. **Animated grid-based movement.** <span className="opacity-75">Animated vs logical positions, lerp, springs, easing functions.</span>
4. **Sprite animation.** <span className="opacity-75">Animations with images, plus some ways to upscale pixel art.</span>

<Callout type="info">
  The code for all of these examples runs in a game loop (see [Chapter
  3](/guides/handmade-web-games/game-loops)), which means anything in a `draw` function will
  run synchronized with the monitor's refresh rate, and any code in a `fixedUpdate` function will
  run at a fixed 8ms interval.
</Callout>

## A bouncing ball [#a-bouncing-ball]

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

  <slot path="steps">
    Before animating anything, we'll set the ball's starting position and draw it on the canvas using the `arc` technique from [Chapter
    2](/guides/handmade-web-games/intro-to-canvasrenderingcontext2d).
  </slot>

  <slot path="steps">
    <>
      Every frame, add the `y` velocity to the current `y` position.

      Since the ball's `y` position is based on its *center*, we know it is touching the ground when its `y` equals `bounds.height - ball.r`.

      <Callout type="info">
        Since the ball would rapidly fall out of frame forever if we did nothing when it collides with the
        ground, we'll temporarily wrap back to `y=0` on each collision. The next step will make this less
        jarring!
      </Callout>
    </>
  </slot>

  <slot path="steps">
    <>
      **Ground collision:**
      We're already checking to see whether `ball.y` exceeds `bounds.height - ball.r`. Now we need to flip the `y` velocity's sign to make it go in the other direction when we collide.

      **Ceiling collision:**
      The `y` coordinate of the top of the canvas is `0`, so the ball is touching the top when its `y` equals `ball.r`.

      <Callout type="warn" title="You can't only flip the velocity on collision.">
        Unlike in real life, there is no perfectly continuous motion in games. The ball is actually teleporting some number of pixels each frame. This means it's pretty unlikely that the ball ever perfectly hits a `y` position of `bounds.height - ball.r`. It'll more likely overshoot that value by a little bit, and we'll know to reflect its velocity for all values *greater than* that position. But if our logic only looked like this...

        ```ts
        if (ball.y > bottomY) {
          ball.vy *= -1; // flip velocity
        }
        ```

        ...then the ball would still be positioned below the floor after the velocity flip. Since the collision condition would still be true on the next frame, the velocity would keep flipping back and forth every update, causing the ball to be stuck inside the floor. So the solution is to move the ball back to the exact collision boundary before reflecting it:

        ```ts
        if (ball.y > bottomY) {
          // move ball to collision boundary
          ball.y = bottomY;
          // then flip velocity
          ball.vy *= -1;
        }
        ```
      </Callout>
    </>
  </slot>

  <slot path="steps">
    Acceleration makes the animation feel a lot more physically grounded. We can define a `gravity` acceleration value and apply it to the velocity each frame, remembering to incorporate `dt` into the update since this is a change over time. And that's it! With only a couple of new lines of code, we now have a physics-based animation.
  </slot>

  <slot path="steps">
    <>
      When we set `ball.y = bottomY` in the previous two steps, we ended up throwing away part of the frame. If gravity pulls the ball 3 pixels past the floor, setting `ball.y = bottomY` deletes those 3 pixels of downward movement. As a result, each bounce loses a tiny bit of energy, which you can see if you leave the gravity example running for a long time.

      So how can we account for this overshoot?

      <Callout type="success">
        Measure how far the ball crossed the boundary, then reflect it back by that same amount:

        ```ts
        const overshoot = ball.y - bottomY;
        ball.y = bottomY - overshoot;
        ```

        This simplifies to a one-liner:

        ```ts
        ball.y = 2 * bottomY - ball.y;
        ```
      </Callout>
    </>
  </slot>

  <slot path="steps">
    To add further realism, the ball should lose some energy on each bounce. When we update the velocity on each impact, in addition to flipping the direction with `*= -1` we'll now apply an energy loss factor as well. To lose 20% of the current energy each bounce, try this:
  </slot>

  <slot path="steps">
    The ball in the previous demo never actually comes to a complete stop. It's doing tons of microscopic bounces as it gets ever closer to being at rest. To address that, if the ball's velocity ever dips below a threshold value during a collision (we've gone with 30), we now snap it to zero.
  </slot>

  <slot path="steps">
    <>
      The first of [Disney's twelve principles of animation](https://en.wikipedia.org/wiki/Twelve_basic_principles_of_animation) <small className="opacity-75">\[Wikipedia]</small>, *squash and stretch* adds interest to the animation as well as some realism since a perfectly rigid ball would not be so bouncy. A key part of the principle is to keep the ball's volume constant. So if we are squashing by some factor vertically, then we need to stretch by that same factor horizontally. Otherwise it will look like the ball is changing size when it hits the ground.

      The new `squash` value deforms the ball proportionally to its impact speed and decays back to zero.
    </>
  </slot>
</Scrollycoding>

## Bouncing DVD logo [#bouncing-dvd-logo]

What we built in the bouncing ball example is a **simulation**. Each frame reads the previous frame's state, then uses that to compute where it's going next. You could easily use those same techniques to build the [bouncing DVD logo animation](https://www.youtube.com/watch?v=5mGuCdlCcNM) <small className="opacity-75">\[YouTube]</small> but in this section we're going to do something totally different.

You can probably already picture what the state would look like for an animation like this. Maybe:

```ts
const logo = { x, y, dx, dy, numberOfBounces };
```

But what if I told you that the only state you need for that animation is this:

```ts
const state = { time }; // elapsed seconds since animation start
```

*All* of the other properties--the location, movement direction, and number of bounces--can be derived from time.

<Callout type="idea">
  Every now and then you read something that permanently changes the way you approach certain
  problems. One post that did that for me was &#x2A;["Your Simulation Might Not Need
  State"](https://www.onsclom.net/posts/simulator-state)*, which introduced me to the concept of
  animation states derived from time. This upcoming section is a canvas extension to that post, so
  go give it a read, too!
</Callout>

There's an analogy to this concept that you sometimes see in other coding problems: using an iterative solution vs a direct formula or "closed-form solution".

```ts twoslash
function countDaysBetweenDates(start: Date, end: Date) {
  let days = 0;
  let currentDay = new Date(start);
  while (currentDay < end) {
    currentDay.setDate(currentDay.getDate() + 1);
    days++;
  }
  return days;
}
```

You *could* count the number of days between two dates one-by-one, like the above code. But we know we're just looking at two timestamps, and we know the duration of a day, so we have all the information we need to do this without the loop:

```ts twoslash
function countDaysBetweenDates(start: Date, end: Date) {
  const ONE_DAY_MS = 24 * 60 * 60 * 1000;
  const days = Math.floor((end.getTime() - start.getTime()) / ONE_DAY_MS);
  return days;
}
```

Besides the fact that it *feels elegant* to use formulas to do in O(1) time what previously was done in O(N), there's a lot more to this "iterative vs closed-form" or "simulation vs animation" mental model than the aesthetics of the code or microoptimizations. If you can derive the current state without having to look back to the previous state, it becomes possible to massively parallelize these computations and run them on the GPU, which is how games are able to animate things like the movement of foliage so efficiently. But even if you're not planning to have millions of blades of grass in your browser game or use shaders or do any GPU optimization, if you can avoid storing state for every blinking light or rotating coin or idle breathing animation, you'll be doing future you a favor by reducing the complexity of your game code.

Let's look at a real example.

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

  <slot path="steps">
    We'll start with the naïve, stateful solution where we need to store and update the logo's position, direction, and number of bounces. This should look pretty familiar if you made it to Step 3 (Collisions) in the bouncing ball code--only now, we're working in two axes.
  </slot>

  <slot path="steps">
    <>
      Now for the magic. We'll strip out almost everything from `fixedUpdate` and just use it to increment the elapsed seconds value (`time`). Then we'll use a formula to derive the current `x` and `y` positions from the `time`--no direction or velocity needed!

      Now that we've removed all the physics simulation code, there's no longer any need to run this in a `fixedUpdate` loop. The variable-rate `update` function that runs less frequently is well suited to this type of task.

      <details>
        <summary>
          <strong>If you want the formula explained...</strong>
        </summary>

        <Callout type="info">
          Understanding the math behind the bounce position formula isn't essential, but it is an
          interesting puzzle regardless. Most of the time, I've found time-based animations only need a
          simple formula such as a plain sine wave or just `time % value`.
        </Callout>

        To derive the logo's `x` position from state, we'll first build a sawtooth pattern where `x` increases over the available width before wrapping back to zero:

        ```ts
        const speed = 80; // px/s
        const width = 300;
        const x = (time * speed) % width;
        ```

        <DvdSawtoothGraphDemo />

        We also need to subtract the logo's own width from the horizontal distance we're allowing it to travel:

        ```ts
        const speed = 80; // px/s
        const canvasWidth = 300;
        const logoWidth = 100;
        const width = canvasWidth - logoWidth;
        const x = (time * speed) % width;
        ```

        <DvdLogoWidthSawtoothGraphDemo />

        But we don't want the logo to teleport back to the origin after it reaches the far edge, so to make it ping pong we need to add some logic to reflect the shape. To mirror the whole animation we would do this:

        ```ts
        const speed = 80; // px/s
        const canvasWidth = 300;
        const logoWidth = 100;
        const width = canvasWidth - logoWidth;
        const x = (time * speed) % width; // [!code focus]
        const invertedX = width - x; // [!code highlight] [!code focus]
        ```

        <DvdInvertedSawtoothGraphDemo />

        So to make it reflect halfway, we'll need conditional logic. If we're in the first half, use `x` directly. If we're in the second half, use the mirrored `x`.

        ```ts
        const speed = 80; // px/s
        const canvasWidth = 300;
        const logoWidth = 100;
        const width = canvasWidth - logoWidth;
        const x = (time * speed) % width;
        const invertedX = width - x;
        const displayX = // [!code focus]
          x < width / 2 // [!code focus]
            ? x // [!code focus]
            : invertedX; // [!code focus]
        ```

        <DvdOverlaidSawtoothGraphDemo />

        But this is bouncing after it only hits the halfway point in the `x` direction. Let's double the width to fix it.

        ```ts
        const speed = 80; // px/s
        const canvasWidth = 300;
        const logoWidth = 100;
        const width = canvasWidth - logoWidth;
        const x = (time * speed) % (width * 2); // [!code highlight]
        const invertedX = width * 2 - x; // [!code highlight]
        const displayX = // [!code highlight]
          // [!code highlight]
          x < width // double the half-width
            ? x // [!code highlight]
            : invertedX; // [!code highlight]
        ```

        <DvdFullBounceSawtoothGraphDemo />

        Now `x` bounces back and forth across the `width` of the canvas at a configurable `speed`. The exact same approach will work for the `y` and `height` values. And that is how we're able to bounce the DVD logo in both axes without needing to save the previous state. As a bonus, this approach is already framerate-independent and automatically scales to any canvas size responsively, too!
      </details>
    </>
  </slot>

  <slot path="steps">
    <>
      Surely we need state to count each bounce, right? &#x2A;Wrong!* The number of bounces is easily derived given `time` and the dimensions.

      ```ts
      xBounces = floor((time * speed) / (canvas.w - logo.w));
      yBounces = floor((time * speed) / (canvas.h - logo.h));
      bounces = xBounces + yBounces;
      color = colors[bounces % colors.length];
      ```
    </>
  </slot>
</Scrollycoding>

Just remember, &#x2A;*relying on state is not bad!** And actually, our "stateless" animation still relies on some state: *time* is state, too. Sometimes it's just more pragmatic to build a simple stateful solution than it is to use a clever formula. So our aim is not to eliminate statefulness everywhere we can. Instead, identify where it is practical to derive it rather than simulate it, and apply these techniques there. Because the fewer states your game can be in at any given time, the fewer branches you have to go down when debugging or reasoning about your code. And if that sounds appealing to you, there are a few more examples where we derive positions and other typically-stateful properties using only the current `time` in the section on sine waves in [Chapter 5 →](/guides/handmade-web-games/math-for-games#sine-waves).

## Animated grid-based movement [#animated-grid-based-movement]

In grid-based games you want your logical game state to be in discrete, integer steps. So if the player is at `x=1 y=2` and moves right, then they snap to `x=2 y=2` on the next update. There are no in-between states, so your game rules will never have to deal with a player at `x=1.333 y=2.5`.

While snapping to a grid is great for game logic, it's rarely how you want to display movement to players. In this section we'll show how to decouple the logical grid position from the visual position.

<GridSnapVsLerpDemo />

### Lerp [#lerp]

Lerp is a strange portmanteau for "<strong><u>L</u></strong>inear Int<strong><u>erp</u></strong>olation", which blends between two numbers `a` and `b`. How far between? Some amount `t`--a value between `0` and `1`.

```ts
function lerp(a: number, b: number, t: number) {
  return a + (b - a) * t;
}

// lerp(0, 1, 0.3) => 0.3
// lerp(0, 100, 0.3) => 30
// lerp(50, 100, 0.5) => 75
// lerp(123, 456, 0.789) => 385.737
```

<LerpSamplesDemo />

When the `t` value updates over time, you get an animation.

```ts
const t = (time % 2) / 2;
const x = lerp(startX, endX, t);
```

<LerpDemo />

And the same blend factor `t` can update several properties at once.

```ts
const t = (time % 2) / 2;

const x = lerp(startX, endX, t);
const opacity = lerp(0, 1, t);
const size = lerp(0, 40, t);
const rotation = lerp(-0.45, 0.45, t);
```

<LerpPropertiesDemo />

### Easing functions [#easing-functions]

{/* prettier-ignore */}

<blockquote className="font-fancy italic">
  <q>
    You mostly see unaccelerated motion in machinery. An automated factory is chock-full of linear tweens—robotic arms, conveyor belts, assembly lines. It is extremely difficult for humans to move in a linear fashion. Breakdancers who do “robot”-style moves have achieved their linearity with much practice. In this sense, you could say Michael Jackson is the master of the linear tween.
  </q>

  <br />

  <cite className="opacity-75">
    {"- Robert Penner on the "}

    <a href="https://robertpenner.com/easing/penner_chapter7_tweening.pdf">
      Aesthetics of Linear Motion
    </a>
  </cite>
</blockquote>

The `L` in "lerp" stands for "linear". But in games and in life, movement tends not to be linear. So if you apply an *easing function* to the `t` parameter in your `lerp`, it will still take the same amount of time to get from `a` to `b`, but you can shape the acceleration and deceleration as you move between those fixed points for more natural-looking motion.

```ts twoslash
/**
 * quadratic ease-out curve
 * @param t - normalized progress from 0 to 1
 * @returns eased progress value
 */
function easeOut(t: number) {
  return 1 - (1 - t) ** 2;
}

/**
 * linearly interpolates between two values
 * @param a - start value
 * @param b - end value
 * @param t - interpolation factor between 0 and 1
 * @returns interpolated value
 */
function lerp(a: number, b: number, t: number) {
  return a + (b - a) * t;
}

declare let t: number;
declare let startX: number;
declare let endX: number;
// ---cut---
const eased = easeOut(t);
const x = lerp(startX, endX, eased);
```

```ts
function easeOut(t: number) {
  return 1 - (1 - t) ** 2;
}
```

<EasingDemo />

There are a bunch of other easing curves, with examples and TypeScript functions you can copy at [easings.net](https://easings.net/).

### Fixed tweens vs follow behaviors [#fixed-tweens-vs-follow-behaviors]

The lerp we've used so far is a *fixed tween*, measuring progress between a known start and end point. What do you do when you have a target that can move again before the previous move has finished?

*Follows*, unlike tweens, do not need to know the original starting position in order to smoothly close the gap between the current value and the latest target each frame.

#### Exponential smoothing [#exponential-smoothing]

Exponential smoothing is a follow technique that brings a value asymptotically close to a target point, but never actually reaches it. No matter how fast you make the speed factor here, blue will always win.

<ExponentialEaseDemo />

The formula looks like this. You may notice something funny in it:

```ts twoslash
/**
 * linearly interpolates between two values
 * @param a - start value
 * @param b - end value
 * @param t - interpolation factor between 0 and 1
 * @returns interpolated value
 */
declare function lerp(a: number, b: number, t: number): number;
/** deltaTime (the time between updates) */
declare const dt: number;
/** exponential decay factor */
declare const speed: number;
/** current x position of the object */
declare let x: number;
/** x position to move towards */
declare const targetX: number;
/** interpolation factor between 0 and 1 */
declare let t: number;
// ---cut---
t = 1 - Math.exp(-speed * dt);
x = lerp(x, targetX, t);
```

That's right, follows can still use lerp! We just need to reframe the thinking a bit: let `a` in the `lerp(a, b, t)` function be the *current* position rather than the original starting position.

<ExponentialSmoothingDemo />

<Callout type="warning" title="Be careful with lerp smoothing!">
  Seeing that formula, you might be tempted to update a position by simply lerping towards it each frame using the current value as the starting value in the lerp function and some fixed `t`, like this:

  ```ts twoslash
  /**
   * linearly interpolates between two values
   * @param a - start value
   * @param b - end value
   * @param t - interpolation factor between 0 and 1
   * @returns interpolated value
   */
  declare function lerp(a: number, b: number, t: number): number;
  /** current x position of the object */
  declare let x: number;
  /** x position to move towards */
  declare const targetX: number;
  /** a fixed blend factor, e.g. 0.1 */
  declare let t: number;
  // ---cut---
  t = 0.1;
  // @warn: avoid doing this
  x = lerp(x, targetX, t);
  ```

  This is often referred to as lerp smoothing, and the main problem with it is how its behavior changes at different frame rates. Unfortunately, it's not as simple as just multiplying `t` by `dt` to correct the frame rate issue. To see why, check out the talk by Freya Holmér at the 2024 Guadalindie conference called [Lerp smoothing is broken](https://www.youtube.com/watch?v=LSNQuFEDOyQ) <small className="opacity-75">\[YouTube]</small>.

  The solution is to incorporate `dt` the way our exponential smoothing function does, with `t = 1 - Math.exp(-speed * dt)`. This is essentially the same exponential decay formula that Freya uses in the talk. You may prefer her rewrite of the same formula which doesn't require `lerp`:

  ```ts twoslash
  /** current x position of the object */
  declare let x: number;
  /** x position to move towards */
  declare const targetX: number;
  /** exponential decay factor */
  declare const speed: number;
  /** deltaTime (the time between updates) */
  declare const dt: number;
  // ---cut---
  x = targetX + (x - targetX) * Math.exp(-speed * dt);
  ```
</Callout>

#### Springs [#springs]

Springs are a physics-based follow function with configurable `stiffness` (how much the target pulls the value towards it) and `damping` (how much resistance the velocity faces). Like exponential follows, springs can also follow a moving target. But unlike the exponential follow, springs also carry a `velocity` value and can overshoot the target.

```ts
function stepSpring(
  value: number,
  velocity: number,
  target: number,
  stiffness: number,
  damping: number,
  dt: number,
) {
  const acceleration = -stiffness * (value - target) - damping * velocity;
  velocity += acceleration * dt;
  value += velocity * dt;
  return { value, velocity };
}
```

<SpringDemo />

For more on springs, see Josh Comeau's [A Friendly Introduction to Spring Physics](https://www.joshwcomeau.com/animation/a-friendly-introduction-to-spring-physics/) and Glenn Fiedler's [Spring Physics](https://gafferongames.com/post/spring_physics/).

### Mini animated grid movement project [#mini-animated-grid-movement-project]

The goal in this section will be to code a grid movement system with an exponential follow. An integer `row` and `col` grid position will always be available in the game state, but an animated visual position will also be available for drawing.

<GridLogicalVisualDemo />

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

  <slot path="steps">
    Our starting code will draw the player at `row=0 col=0` in a small grid.
  </slot>

  <slot path="steps">
    We'll advance the logical cell on a timer without any animations yet.
  </slot>

  <slot path="steps">
    Keep `col` and `row` for game logic, but now we will draw from pixel `x` and `y` to prep for animating those values.
  </slot>

  <slot path="steps">
    Implement exponential smoothing to make `x` and `y` chase the `row` and `col`.
  </slot>
</Scrollycoding>

## Sprites [#sprites]

People have been producing the illusion of motion through still images for hundreds of years. Analog mechanical animation media, like film, flip books, and zoëtropes all rely on the fact that if you flip through still frames fast enough, viewers will perceive motion.

<MuybridgeRaceHorseDualPreview />

Sprite animations, such as this one, also rely on that principle.

<SpriteAnimationPreview />

<small>
  *This sprite came from the free [Pixel Planet
  Generator](https://deep-fold.itch.io/pixel-planet-generator) on itch.io.*
</small>

A sprite (sometimes called a texture atlas or sprite sheet) is simply a single image with other images in it. Here, for example, we have a sprite sheet with all of the frames of our planet animation in it.

<SpriteSheetsPreview />

You can play through the animation by cropping to a frame, then adjusting the crop window on an interval.

<SpriteSheetFrameWindowPreview />

Sprite sheets are used for much more than animation--they're often used for background tiles and textures as well since you can easily pack dozens of textures into a single file. The exact same techniques for indexing into a small window within a larger image can be used both for sprite animations and for static uses of sprites.

<TilesetSheetPreview />

<small>
  *This tileset is from the [Brackeys Platformer
  Bundle](https://brackeysgames.itch.io/brackeys-platformer-bundle) on itch.io.*
</small>

Sprite sheets are especially useful in web games because they can significantly reduce the number of network roundtrips your browser needs to make to fetch your game's assets.

Let's start with how to draw a single frame in a sprite. We'll need to use the 9-argument form of `drawImage` (see [Chapter 2 →](/guides/handmade-web-games/intro-to-canvasrenderingcontext2d#images)). The code below pulls from the source rectangle (our "crop window") within the image, then draws it to the destination rectangle on the canvas.

```ts twoslash
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
declare const sprite: HTMLImageElement;
// ---cut---
function drawSprite({ index, destX, destY }: { index: number; destX: number; destY: number }) {
  const frameWidth = 50;
  const frameHeight = 50;
  const frameX = index * frameWidth;
  const frameY = 0;
  ctx.drawImage(
    sprite,
    // source rectangle
    frameX,
    frameY,
    frameWidth,
    frameHeight,
    // destination rectangle
    destX,
    destY,
    frameWidth,
    frameHeight,
  );
}

drawSprite({ index: 5, destX: 0, destY: 0 });
```

<details>
  <summary>
    <strong>Scaling up for pixel art</strong>
  </summary>

  Pixel art is normally displayed larger than its original size. You have two ways to do this scaling:

  <PixelArtCanvasScalingDemo />

  If you're not scaling the entire canvas up, you'll need to update `drawSprite` to handle in-canvas scaling:

  ```ts twoslash
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d")!;
  declare const sprite: HTMLImageElement;
  // ---cut---
  function drawSprite({
    index,
    destX,
    destY,
    scale, // [!code ++]
  }: {
    index: number;
    destX: number;
    destY: number;
    scale: number; // [!code ++]
  }) {
    const frameWidth = 50;
    const frameHeight = 50;
    const frameX = index * frameWidth;
    const frameY = 0;
    ctx.imageSmoothingEnabled = false; // [!code ++]
    ctx.drawImage(
      sprite,
      frameX,
      frameY,
      frameWidth,
      frameHeight,
      destX,
      destY,
      frameWidth * scale, // [!code ++]
      frameHeight * scale, // [!code ++]
    );
  }

  drawSprite({ index: 5, destX: 0, destY: 0, scale: 4 });
  ```
</details>

To turn that into an animation, increment the `index` on an interval in the update cycle of your game loop.

```ts
const state = {
  frameTime: 0,
  index: 0,
};

function update(dt: number) {
  const totalFrames = 40;
  const frameDuration = 0.1;
  state.frameTime += dt;
  if (state.frameTime >= frameDuration) {
    state.index = (state.index + 1) % totalFrames;
    state.frameTime -= frameDuration;
  }
}
```

<details>
  <summary>
    <strong>What about sprites with several rows?</strong>
  </summary>

  Lining everything up in one row is convenient for animations, but makes less sense for background tiles or when using the same spritesheet for several animations. Luckily, the 9-argument form of `drawImage` is already close to perfect for the job.

  ```ts twoslash
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d")!;
  declare const sprite: HTMLImageElement;
  // ---cut---
  function drawSpriteTile(row: number, col: number, destX: number, destY: number) {
    const frameWidth = 32;
    const frameHeight = 32;

    ctx.drawImage(
      // your image asset (usually an HTMLImageElement)
      sprite,

      // the source rectangle (x, y, w, h)
      col * frameWidth, // startX
      row * frameHeight, // startY
      frameWidth, // source width
      frameHeight, // source height

      // the destination rectangle (x, y, w, h)
      destX,
      destY,
      frameWidth,
      frameHeight,
    );
  }
  ```
</details>

If you want to create your own sprite sheets, [Aseprite](https://www.aseprite.org/) and [Tilesetter](https://www.tilesetter.org/) are good (paid) options. [Piskel](https://www.piskelapp.com/) and [Tiled](https://www.mapeditor.org/) are good free alternatives.

### Performance improvements for upscaled pixel art [#performance-improvements-for-upscaled-pixel-art]

Scaling images every frame is rather expensive. We can instead pre-scale them in an [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas), which is just like a `<canvas>` element except that it can't interact with the DOM, so you'll only use it to compute bitmaps and then directly transfer those onto your main `canvas`.

```ts twoslash
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
declare const sprite: HTMLImageElement;
// ---cut---
const scale = 4;
const frameWidth = 50;
const frameHeight = 50;
const totalFrames = 40;

// create an offscreen canvas with the exact scaled dimensions for our sprite
const scaled = new OffscreenCanvas(
  totalFrames * frameWidth * scale, // canvas width
  frameHeight * scale, // canvas height
);

// pre-scale the entire sprite sheet one time up front
function prescale() {
  const scaledCtx = scaled.getContext("2d")!;
  scaledCtx.imageSmoothingEnabled = false;
  scaledCtx.drawImage(sprite, 0, 0, scaled.width, scaled.height);
}

// only run the one-time scaling once the sprite is fully downloaded
if (sprite.complete) prescale();
else sprite.onload = prescale;

function drawSprite(index: number, destX: number, destY: number) {
  ctx.drawImage(
    scaled,
    index * frameWidth * scale, // previously `index * frameWidth`
    0,
    frameWidth * scale, // previously just `frameWidth`
    frameHeight * scale, // previously just `frameHeight`
    destX,
    destY,
    frameWidth * scale,
    frameHeight * scale,
  );
}
```

`OffscreenCanvas`es can even be used in Web Workers, allowing you to run expensive computations in a separate thread.

## Up next [#up-next]

In [Chapter 5 →](/guides/handmade-web-games/math-for-games) we'll cover the practical math that shows up all the time in 2d games, including ways to handle movement, randomness, aiming, and collisions.

## See also [#see-also]

* [An In-Depth look at Lerp, Smoothstep, and Shaping Functions](https://www.youtube.com/watch?v=YJB1QnEmlTs) <small className="opacity-75">\[YouTube]</small> — SimonDev on linear interpolation
* [Puzzle Game Movement Systems](https://www.youtube.com/watch?v=_tMb7OS2TOU) <small className="opacity-75">\[YouTube]</small> — Jonathan Blow and Sean Barret discussion
* [The 12 Principles of Animation](https://en.wikipedia.org/wiki/Twelve_basic_principles_of_animation) <small className="opacity-75">\[Wikipedia]</small> — Disney's foundational animation rules (also on [YouTube](https://www.youtube.com/watch?v=uDqjIdI4bF4) by AlanBeckerTutorials)

### Tools [#tools]

* [Aseprite](https://www.aseprite.org/) — animated sprite editor and pixel art tool
* [easings.net](https://easings.net/) — interactive easing curve reference
* [Piskel](https://www.piskelapp.com/) — free sprite and pixel art editor
* [Paint of Persia](https://dunin.itch.io/ptop) — like tracing paper for pixel art, with animation support
* [Tiled](https://www.mapeditor.org/) — free 2D level and tile map editor
* [Tilesetter](https://www.tilesetter.org/) — can export a png + json file with coordinates for sprite sheets
