How to make a pixel art game with AI
Pixel art drawn at browser resolution is not pixel art. It is small blurry art. Four settings decide which of the two you end up with.
Jump to a section
Pixel art is a resolution decision before it is a drawing style. Ask for a game that renders onto a canvas of 320 by 180 and is then blown up by a whole number. Ask for the same sprites at browser resolution and you get small blurry art, which is a different and much worse thing.
The canvas you draw on is small and never changes size. The canvas the player sees is that one multiplied by 3, 4 or 5. Every fault in this article comes from confusing the two, or from letting the multiplier go fractional.
Four steps to pixels that stay sharp#
Name 320 by 180 before you name the game. Retrofitting it later means touching every draw call in the file.
A single art pixel should be a clean square block. If its edge is a grey halo, smoothing is still on somewhere.
Twelve colours is plenty. The model will offer you a gradient at some point. A gradient has no palette, so say no.
Rooms, enemies and sound are ordinary work. The look is the part that goes wrong quietly and stays wrong.
Build a pixel art game as ONE self-contained .html file. No frameworks
and no image files. Every sprite is filled rectangles drawn in code.
RESOLUTION: keep a second canvas in memory, exactly 320 wide and 180
high, and draw the entire game onto it. The visible canvas only ever
draws that buffer, scaled up.
SCALING: find the largest WHOLE number that still fits the window, with
a floor of 1. Size the visible canvas to 320 and 180 times it. Never a
fractional scale. Centre it and leave the leftover space black.
CRISPNESS: set imageSmoothingEnabled to false on the visible context
after every resize, because changing a canvas size resets it. Add
image-rendering: pixelated to the canvas in the CSS, and also
canvas { touch-action: none; }
PALETTE: one array of exactly 12 hex colours at the top of the file.
Every fill reads a colour out of that array. No gradients, no
shadowBlur, no semi-transparent fills.
GAME: a room of 20 by 11 tiles, 16 pixels each. Arrow keys or WASD walk
a character. Collect five gems, avoid one slime that paces between two
points, and go back to the start on a touch.
DRAWING: entities hold fractional positions. Round with Math.floor at
the moment of drawing and nowhere else.
TIMING: multiply every speed by the seconds that have passed since the
last frame, and clamp that number to 0.05 so a tab left in the
background cannot jump the character through a wall.
PHONE: a swipe anywhere walks the character that way.
CONFIG: one object at the top holding the buffer width and height, the
tile size, the walk speed, the gem count and the palette, each with a
comment saying what it does.
Change the palette, the room and the walk speed freely. Leave the resolution block and the whole number scale alone. Those four lines are the only reason the result reads as pixel art, and they are the first thing a later request will quietly undo.
Why pixel art comes out blurry#
A browser has no idea you meant pixels. Hand it a canvas the size of the window and it draws a 12 pixel sprite as 12 real pixels. At that size a character is a smudge, and no amount of redrawing the sprite will fix it. Four separate settings have to agree before an art pixel stays a square.
The buffer comes first. Create a second canvas in memory at 320 by 180 and draw everything onto it. The visible canvas then has one job per frame, which is to draw that buffer at the scaled size. Nothing in your game logic ever sees a screen pixel, so nothing in it has to care how big the window is.
Smoothing is the setting people turn off once and then lose. Assigning to a canvas's width or height resets its 2D context, and imageSmoothingEnabled resets with it. So a resize handler that changes the size and forgets to switch smoothing off again gives you a sharp game that goes soft the first time the window moves.
The CSS property does the same job one layer out. When a canvas element's CSS size differs from its width and height attributes, the browser scales the result itself, and image-rendering: pixelated tells it to do that without blending. Set both. They cover different stages and the second costs you nothing.
Scale by three, not by 2.4#
A fractional multiplier is the fault that survives a look at the screenshot, because it almost works. At 2.4 times, some art pixels come out two screen pixels wide and some come out three. The sprite develops a limp. One row of a face is fatter than the row above it, and which row is fatter changes with the window size.
The fix is a line of arithmetic. Divide the window width by 320 and the window height by 180, keep the smaller of the two, then floor it. Clamp the result to a minimum of 1 so a narrow phone still gets a playable screen rather than a blank one.
That arithmetic leaves gaps around the canvas, and the gaps are correct. A 320 by 180 buffer at scale 4 is 1280 by 720, and almost no window is exactly that. Centre the canvas and let the page background show through. A letterboxed pixel game reads as deliberate. A stretched one reads as broken.
- 320 by 180. Sixteen by nine, so it fills a modern screen with no awkward bars. A 16 pixel tile gives 20 columns and 11 rows, which is a room you can see all of.
- 256 by 224. Roughly the shape of an early nineties home console. Taller, which suits a playfield you look up and down rather than across.
- 160 by 144. Handheld sized. So few pixels that every sprite has to read at a glance, which is a useful constraint and a hard one.
- Anything you like, chosen once. The number matters less than never changing it. Half the file ends up written in terms of it.
A palette of twelve beats a palette of millions#
Part of what makes pixel art read as pixel art is the small number of colours, and a model will not restrain itself unless you ask. Request an array of twelve hex strings at the top of the file, and require every fill to come out of it by index. A change of mood is then one edited line rather than a search through the drawing code.
Choosing the twelve is easier than it sounds. Two or three ramps of three shades each will cover most of a scene: one for stone, one for plant life, one for metal or skin. Add a near black for outlines and a near white for highlights. Keep one loud colour that appears nowhere in the scenery and spend it only on things the player has to notice.
Two habits quietly break a palette. Gradients, because a gradient is every colour between two others. And partial transparency, because a sprite drawn at half alpha over grass produces a colour you never picked. If you want a ghost, draw it in a pale palette colour, or leave every other pixel empty in a checker pattern. Both were what the old hardware did, and both stay on palette.
| It looks like | What is happening | Ask for |
|---|---|---|
| Soft grey haloes on every edge | Smoothing is on, or the scale is fractional | Smoothing off after resize, and a floored scale |
| Sprites look thin and fiddly | The buffer is far bigger than 320 by 180 | A smaller buffer and fatter tiles |
| Colours that feel muddy together | The palette grew while the game grew | Every fill taken from the twelve by index |
| Text that does not match the art | Browser fonts are not pixel fonts | Letters drawn as rectangles, or HTML text outside the canvas |
| Edges that crawl as things move | Draw positions are fractional | Math.floor at the draw call only |
Say what you can see rather than what you think the line should be. The model can read the whole file and you cannot see it from inside.
Why your sprite shimmers as it walks#
A character moving at 37 pixels a second will sit at x equals 41.37 on some frame. Draw it there and its edges fall between two pixels of the buffer, so the browser blends them. At a scale of 4 that blend is four screen pixels wide, and it changes every frame. That is the shimmer.
Round at the draw call and nowhere else. The entity keeps 41.37 and a floor happens as the number enters drawImage or fillRect. Round the stored position instead and slow movement stops working altogether. A speed that adds 0.4 of a pixel each frame gets rounded straight back to where it started, every frame, forever.
The camera needs the same treatment, and it is where the rule gets forgotten. A scrolling room offset by a fractional amount knocks every tile edge off the grid at once. Then the whole screen crawls rather than one sprite, which is harder to diagnose because nothing looks specifically wrong. Floor the camera offset before anything is drawn with it.
- Rotation. A sprite turned by seven degrees has to be resampled, and resampling invents colours that are not in your twelve. Draw a few pre turned frames instead.
- Half pixel sizes. A rectangle 12.5 pixels wide has a soft edge at the half. Keep every width and height a whole number too.
- Strokes. A one pixel line is centred on the path, so at a whole coordinate it straddles two pixels and both come out grey. Use fillRect rather than strokeRect.
- Scaling inside the buffer. If a sprite is drawn at 1.5 times its own size, you have reinvented the fractional scale at the worst possible layer.
👾Pixel art games written by AI
Common questions#
Can AI make a pixel art game?
Yes, and the sprites are the easy half. A model will draw a readable character out of a few dozen filled rectangles. What it will not do unless asked is pin the resolution, so the usual result is correct code rendering at the wrong size.
Why does my pixel art look blurry?
One of four things. The game is drawing at window resolution instead of to a small buffer. The scale is fractional. Smoothing was never switched off, or was switched off and then reset by a resize. Or the CSS is missing image-rendering: pixelated. Check them in that order.
What resolution should a pixel art game use?
320 by 180 if you want it to fill a widescreen monitor cleanly. 256 by 224 for a taller playfield. The important part is picking one at the start and writing every tile size and sprite size in terms of it.
Do I need two canvases, or will one do?
One can work. Set the canvas width and height attributes to 320 and 180, then give it a CSS size that is a whole multiple of those, with image-rendering: pixelated. The browser does the scaling. You get fewer lines and less say over how the leftover space looks.
Do I need sprite image files?
No, and skipping them keeps the game in one file. At this resolution a sprite is a grid of a few hundred coloured squares, which the code can hold as an array of palette indices and draw with fillRect. It also means a palette change recolours everything at once.
Why do my sprites flicker when they move slowly?
Their draw positions are fractional, so each frame lands on a slightly different blend of two pixels. Floor the position where it is drawn and keep the real value on the entity. Do not round the stored value, or anything moving under a pixel a frame will never move.
Where to go next#
Once the buffer is right, the look is settled and the rest is ordinary game building. Publish it when it is worth a few minutes of somebody else's time. The page records the model and the prompt behind it, which is the part other people learn from.
The method behind all of this is in how to make a game with AI, and the canvas and input details are in the 2D guide. If the look is settled and the game needs a shape, the platformer guide and the roguelike guide both suit a small screen. Fixes for specific faults are in the prompt guide, and the pixel art games here are the fastest way to see which resolutions hold up.
Built something? Put it on Agent Games.
Publishing takes a file and a minute. Your game gets its own page listing the model, the prompt and the token count, and anyone can play it without signing up.
Read next