Nobody wants to see blurry pixel art rendered in a game. So let's investigate how to render crisp pixel art images on a canvas element.

Full source code for this article is available at BrianDouglasIE/canvas-pixel-art

The task at hand

So for this demo let's say we want to render the sprite of our game's hero character on a canvas. The sprite sheet that I will be using is from KenneyNL, it's the 1-Bit monochrome asset pack. Each sprite in the canvas is 16x16 pixels in dimension. This will be scaled up to around 4 times it's original size.

I want my canvas to be 640x480 pixels in dimension. If I were to draw a sprite directly on the canvas it would look miniscule. If I were to draw it at 4 times it's original size, it would look blurry. So the question is, how do I draw my scaled up pixel art character without causing it to blur.

I bet this complex problem requires a complex solution I bet the solution is a one liner

Some boiler plate

Before I even get as far as rendering my pixel art character on a canvas there is some initial boilerplate code that is needed.

Firstly I'll need to create a canvas element to render the game on. I can do that with the following code. Which creates the canvas and appends it to the document's body. I also set the canvas' width and height to be a quarter of the intended size. I will scale this up later.

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
document.body.appendChild(canvas)
canvas.height = 120
canvas.width = 160

Once this is in place I will add some functions to render the sprite sheet and extract the image data of the sprite that I want to render. To do this I create a separate canvas. This canvas will not be appended to the body of the html document as the user will never see it. Instead it will be used to draw the sprite sheet in it's entirety. I will then grab the individual sprites from it as and when they are needed. I also set the canvas to be scaled up the target size using CSS. The code for this is below.

const spriteCellSize = 16
const spriteSheet = new Image()
spriteSheet.crossOrigin = "anonymous"
spriteSheet.addEventListener("load", onLoad, false)
spriteSheet.src = "./monochrome-transparent.png"

const scale = 4
canvas.style.width = `${canvas.width * scale}px`
canvas.style.height = `${canvas.height * scale}px`

const spriteSheetCanvas = document.createElement('canvas')

let heroSprite = undefined

function onLoad() {
  if (!heroSprite) {
    spriteSheetCanvas.width = spriteSheet.width
    spriteSheetCanvas.height = spriteSheet.height
    const spriteSheetContext = spriteSheetCanvas.getContext('2d')
    clearScreen(spriteSheetContext, spriteSheetCanvas.width, spriteSheetCanvas.height)
    drawSpriteSheet(spriteSheetContext, spriteSheet)
    heroSprite = getSpriteSheetCell(spriteSheetContext, spriteCellSize, 26, 0)
  }

  clearScreen(ctx, canvas.width, canvas.height)
  ctx.putImageData(heroSprite, canvas.width / 2 - spriteCellSize / 2, canvas.height / 2 - spriteCellSize / 2)
}

function clearScreen(ctx, w, h) {
  ctx.save()
  ctx.fillStyle = "black"
  ctx.fillRect(0, 0, w, h)
  ctx.restore()
}

function drawSpriteSheet(ctx, spriteSheet) {
  ctx.drawImage(spriteSheet, 0, 0)
}

function getSpriteSheetCell(ctx, spriteCellSize, col, row) {
  const x = spriteCellSize * col
  const y = spriteCellSize * row
  return ctx.getImageData(x, y, spriteCellSize, spriteCellSize)
}

Here is what is rendered on the canvas. As you can see it is super blurry.

blurry sprite render

The solution

One line of CSS fixes this. Setting the image-rendering property on the canvas to be pixelated. In the demo code this can be done by adding the following statement at line 10.

canvas.style.imageRendering = "pixelated"

The canvas now renders a crisp sprite.

crisp sprite render