Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 202 additions & 36 deletions files/en-us/games/techniques/crisp_pixel_art_look/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,72 @@ This article discusses a useful technique for giving your canvas/WebGL games a c

## The concept

Retro [pixel art](https://en.wikipedia.org/wiki/Pixel_art) aesthetics are getting popular, especially in [indie games](https://en.wikipedia.org/wiki/Indie_game) or [game jam](https://en.wikipedia.org/wiki/Game_jam) entries. But since today's screens render content at high resolutions, there is a problem with making sure the pixel art does not look blurry. Developers have been manually scaling up graphics so they are shown with blocks that represent pixels. Two downsides to this method are larger file sizes and [compression artifacts](https://en.wikipedia.org/wiki/Compression_artifact).

<table class="standard-table">
<tbody>
<tr>
<td><img alt="small pixelated man" src="technique_original.png" /></td>
<td><img alt="small pixelated man" src="technique_original.png" /></td>
<td><img alt="larger pixelated man" src="technique_4x.png" /></td>
</tr>
<tr>
<td>original size</td>
<td>4x size</td>
<td>4x size (scaled with an image editor)</td>
</tr>
<tr>
<td>none</td>
<td>vendor's algorithm</td>
<td>
<a href="https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation"
>nearest-neighbor algorithm</a
>
</td>
</tr>
</tbody>
</table>

## A CSS-based solution

The good news is that you can use CSS to automatically do the up-scaling, which not only solves the blur problem, but also allows you to use the images in their original, smaller size, thus saving download time. Also, some game techniques require algorithms that analyze images, which also benefit from working with smaller images.

The CSS property to achieve this scaling is {{cssxref("image-rendering")}}. The steps to achieve this effect are:
Retro [pixel art](https://en.wikipedia.org/wiki/Pixel_art) aesthetics are getting popular, especially in [indie games](https://en.wikipedia.org/wiki/Indie_game) or [game jam](https://en.wikipedia.org/wiki/Game_jam) entries. But since today's screens render content at high resolutions, there is a problem with making sure the pixel art does not look blurry. Here's an original image that an actual arcade game may have used:

- Create a {{htmlelement("canvas")}} element and set its `width` and `height` attributes to the original, smaller resolution.
- Set its CSS {{cssxref("width")}} and {{cssxref("height")}} properties to be 2x or 4x the value of the HTML `width` and `height`. If the canvas was created with a 128 pixel width, for example, we would set the CSS `width` to `512px` if we wanted a 4x scale.
- Set the {{htmlelement("canvas")}} element's `image-rendering` CSS property to `pixelated`, which does not make the image blurry. There are also the `crisp-edges` and `-webkit-optimize-contrast` values that work on some browsers. Check out the {{cssxref("image-rendering")}} article for more information on the differences between these values, and which values to use depending on the browser.
![small pixelated man](technique_original.png)

We can manually scale it up in an image editor, expanding each pixel into a 4x4 block of pixels. The image editor can leverage algorithms like [nearest-neighbor interpolation](https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation) to achieve crisp edges.

![larger pixelated man](technique_4x.png)

Two downsides to this method are larger file sizes and [compression artifacts](https://en.wikipedia.org/wiki/Compression_artifact), because the image actually contains more pixels.

The idea of producing crisp pixel art is simple: we want to have a single pixel in the original image map to a block of pixels on the screen, without any smoothing or blending between them. The example above achieves this by manually doing that mapping in an image editor. But we can also achieve this effect in the browser using CSS.

## Up-scaling \<img> with CSS

An image has an intrinsic size, which is its actual pixel dimensions. It also has a rendered size, which is set by HTML or CSS. If the rendered size is larger than the intrinsic size, the browser will automatically scale up the image to fit the rendered size.

```html
<img
src="technique_original.png"
alt="small pixelated man, upscaled with width and height attributes, appearing blurry" />
```

```css
img {
width: 48px;
height: 136px;
}
```

<img src="technique_original.png" style="width: 48px; height: 136px;" alt="small pixelated man, upscaled with CSS, appearing blurry" />

But as you can see in the image above, the browser's default scaling algorithm makes the image look blurry. This is because it uses a smoothing algorithm that averages the colors of pixels to create a smooth transition between them.

To fix this, we can use the CSS property {{cssxref("image-rendering")}} to tell the browser to use a different scaling algorithm that preserves the hard edges of pixel art.

## An example
```html
<img
src="technique_original.png"
alt="small pixelated man, upscaled with CSS, appearing crisp" />
```

```css
img {
width: 48px;
height: 136px;
image-rendering: pixelated;
}
```

<img src="technique_original.png" style="width: 48px; height: 136px; image-rendering: pixelated;" alt="small pixelated man, upscaled with width and height attributes, appearing crisp" />

There are also the `crisp-edges` and `-webkit-optimize-contrast` values that work on some browsers. Check out the {{cssxref("image-rendering")}} article for more information on the differences between these values, and which values to use depending on the browser.

`image-rendering: pixelated` is not without its problems as a crisp-edge-preservation technique. When CSS pixels don't align with device pixels (if the {{domxref("Window/devicePixelRatio", "devicePixelRatio")}} is not an integer), certain pixels may be drawn larger than others, resulting in a non-uniform appearance. For example, in Chrome and Firefox, when you zoom in or out, the `devicePixelRatio` changes. This can cause the pixel art to appear distorted or uneven. The screenshot below is taken at 110% page zoom in Chrome. If you look closely, you can see that the left edge of the character's face and leg appears uneven.

![Pixelated image with uneven edges](pixelated_uneven.png)

This is not an easy problem to solve, however, because it is impossible to fill device pixels precisely when the CSS pixels cannot accurately map to them.

## Crisp pixel art in canvas

Many games render inside a {{htmlelement("canvas")}} element, which can use the same `image-rendering` technique because canvases are also raster images. The steps to achieve this are:

- Create a {{htmlelement("canvas")}} element and set its `width` and `height` attributes to the original, smaller resolution.
- Set its CSS {{cssxref("width")}} and {{cssxref("height")}} properties to be any value you want, but stretched equally to preserve the aspect ratio. If the canvas was created with a 128 pixel width, for example, we would set the CSS `width` to `512px` if we wanted a 4x scale.
- Set the {{htmlelement("canvas")}} element's `image-rendering` CSS property to `pixelated`.

Let's have a look at an example. The original image we want to upscale looks like this:

Expand Down Expand Up @@ -85,7 +114,144 @@ image.src = "cat.png";

This code used together produces the following result:

{{ EmbedLiveSample('An_example', '100%', 520) }}
{{EmbedLiveSample("Crisp pixel art in canvas", "", 520)}}

> [!NOTE]
> Canvas content is not accessible to screen readers. Include descriptive text as the value of the [`aria-label`](/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) attribute directly on the canvas element itself or include fallback content placed within the opening and closing canvas tag. Canvas content is not part of the DOM, but nested fallback content is.

## Arbitrarily scaling images in canvas

For the character example with a plain `<img>`, you can set the scale factor to any value you want, and `image-rendering: pixelated` will do its best to preserve crisp edges. For example, you can scale the image by 5.7x:

```css
img {
/* 5.7x scale factor */
width: 68.4px;
height: 193.8px;
image-rendering: pixelated;
}
```

<img src="technique_original.png" style="width: 68.4px; height: 193.8px; image-rendering: pixelated;" alt="small pixelated man, upscaled with CSS, appearing crisp" />

Previously, we said that `image-rendering: pixelated` works at the stage of mapping image pixels to CSS pixels. But if we are drawing the image into a canvas, we have two layers of mapping: from image pixels to canvas pixels, and then from canvas pixels to CSS pixels. The second step works the same way as image scaling with `<img>`, so you can also use arbitrary scale factors when scaling the canvas with CSS:

```html hidden live-sample___canvas_arbitrary_scale
<canvas id="game" width="128" height="128">A cat</canvas>
```

```css live-sample___canvas_arbitrary_scale
canvas {
/* 3.7x scale factor */
width: 473.6px;
height: 473.6px;
image-rendering: pixelated;
}
```

```js hidden live-sample___canvas_arbitrary_scale
// Get canvas context
const ctx = document.getElementById("game").getContext("2d");

// Load image
const image = new Image();
image.onload = () => {
// Draw the image into the canvas
ctx.drawImage(image, 0, 0);
};
image.src = "cat.png";
```

{{EmbedLiveSample("Canvas arbitrary scale", "", 520)}}

But we need to be careful with how the image pixels are aligned with the canvas pixels. By default the image pixels are drawn 1:1 to canvas pixels; however, if you use the extra arguments of {{domxref("CanvasRenderingContext2D/drawImage", "drawImage()")}} to draw the image at a different size in the canvas, you may end up with a non-integer scale factor. For example, if you draw a 128x128 pixel image into a 100x100 pixel area on the canvas, each image pixel will be drawn as 0.78x0.78 canvas pixels, which can lead to blurriness.

```html hidden live-sample___canvas_image_scale
<canvas id="game" width="128" height="128">A cat</canvas>
```

```css hidden live-sample___canvas_image_scale
canvas {
width: 512px;
height: 512px;
image-rendering: pixelated;
}
```

```js example-bad live-sample___canvas_image_scale
// Get canvas context
const ctx = document.getElementById("game").getContext("2d");

// Load image
const image = new Image();
image.onload = () => {
// Extract the image pixels from (0,0) to (128,128) (full size)
// and draw them into the canvas at (0,0) to (100,100)
ctx.drawImage(image, 0, 0, 128, 128, 0, 0, 100, 100);
};
image.src = "cat.png";
```

{{EmbedLiveSample("Canvas image scale", "", 520)}}

The same happens if you use {{domxref("CanvasRenderingContext2D/scale", "scale()")}} to scale the canvas grid. In this case, a unit of 1 when calling canvas methods would be interpreted as a non-integer number of canvas pixels, leading to blurriness.

```html hidden live-sample___canvas_context_scale
<canvas id="game" width="128" height="128">A cat</canvas>
```

```css hidden live-sample___canvas_context_scale
canvas {
width: 512px;
height: 512px;
image-rendering: pixelated;
}
```

```js example-bad live-sample___canvas_context_scale
// Get canvas context
const ctx = document.getElementById("game").getContext("2d");
// Scaling the context by 0.8, so each image pixel is drawn as 0.8x0.8 canvas pixels
ctx.scale(0.8, 0.8);

// Load image
const image = new Image();
image.onload = () => {
ctx.drawImage(image, 0, 0);
};
image.src = "cat.png";
```

{{EmbedLiveSample("Canvas context scale", "", 520)}}

To fix this, you have to ensure that the image pixels are always drawn at integer multiples of canvas pixels. That is, when you call `drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)`, `dWidth` needs to be equal to `sWidth / xScale * n`, where `xScale` is the x scale factor for the context (1.0 if you haven't called `scale()`), and `n` is an integer (1, 2, 3, ...). The same applies to `dHeight`. So if you want to draw a 128x128 pixel image on a canvas that has been scaled by 0.8, you can only draw it at sizes like 160 (128 / 0.8 \* 1), 320 (128 / 0.8 \* 2), etc.

```html hidden live-sample___canvas_context_scale_correct
<canvas id="game" width="128" height="128">A cat</canvas>
```

```css hidden live-sample___canvas_context_scale_correct
canvas {
width: 512px;
height: 512px;
image-rendering: pixelated;
}
```

```js example-good live-sample___canvas_context_scale_correct
// Get canvas context
const ctx = document.getElementById("game").getContext("2d");
// Scaling the context by 0.8, so each image pixel is drawn as 0.8x0.8 canvas pixels
ctx.scale(0.8, 0.8);

// Load image
const image = new Image();
image.onload = () => {
ctx.drawImage(image, 0, 0, 128, 128, 0, 0, 128 / 0.8, 128 / 0.8);
};
image.src = "cat.png";
```

{{EmbedLiveSample("Canvas context scale correct", "", 520)}}

See the canvas [drawing shapes](/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#seeing_blurry_edges) guide for more information about how canvas pixels work.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ draw();

{{EmbedLiveSample("Seeing blurry edges", "", "350")}}

In this example, we create our canvas really small (15x15), but then use CSS to scale it up to 300x300 pixels. As a result, each canvas pixel is now represented by a 20x20 block of screen pixels. We draw a stroked rectangle from (2,2) to (12,12) and a filled rectangle from (7,7) to (8,8). It appears _really_ blurry. This is because by default, when the browser scales raster images, it uses a smoothing algorithm to interpolate the extra pixels. This is great for photographs or canvas graphics with curly edges, but not so great for straight-edged shapes. To fix this, we can set {{cssxref("image-rendering")}} to `pixelated`:
In this example, we create our canvas really small (15x15), but then use CSS to scale it up to 300x300 pixels. As a result, each canvas pixel is now represented by a 20x20 block of CSS pixels. We draw a stroked rectangle from (2,2) to (12,12) and a filled rectangle from (7,7) to (8,8). It appears _really_ blurry. This is because by default, when the browser scales raster images, it uses a smoothing algorithm to interpolate the extra pixels. This is great for photographs or canvas graphics with curly edges, but not so great for straight-edged shapes. To fix this, we can set {{cssxref("image-rendering")}} to `pixelated`:

```css live-sample___seeing_blurry_edges_2 live-sample___seeing_blurry_edges_3
#canvas {
Expand Down