All articles
13 min read

Pixel Art With Code: How to Create Retro Graphics Programmatically

pixel artgenerative artcreative codingjavascripttutorialretrocanvas

Pixel art isn't just nostalgia — it's a living art form. From indie games to NFT collections to creative coding sketches, the deliberate placement of individual pixels creates a warmth and charm that high-resolution rendering can't replicate. And when you create pixel art with code instead of by hand, something magical happens: you can generate infinite variations, animate complex scenes, and build entire worlds procedurally.

This guide covers everything from rendering your first pixel grid to advanced techniques like dithering, palette cycling, and generative landscapes — all in plain JavaScript and Canvas. No frameworks, no pixel-art editors. Just code.

The pixel grid: your canvas within a canvas

The core idea is simple: divide the canvas into a grid of large "pixels" (cells), each filled with a single color. A 64×64 pixel art piece on a 512×512 canvas means each pixel is 8×8 screen pixels.

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

const GRID = 64;  // pixel art resolution
const CELL = canvas.width / GRID; // screen pixels per art pixel

function setPixel(x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
}

// Draw a simple smiley
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);

const yellow = '#f4d03f';
const dark = '#1a1a2e';

// Face circle (midpoint circle algorithm)
function circle(cx, cy, r, color) {
  for (let y = -r; y <= r; y++) {
    for (let x = -r; x <= r; x++) {
      if (x * x + y * y <= r * r) {
        setPixel(cx + x, cy + y, color);
      }
    }
  }
}

circle(32, 32, 14, yellow);
// Eyes
setPixel(27, 28, dark); setPixel(28, 28, dark);
setPixel(36, 28, dark); setPixel(37, 28, dark);
// Mouth
for (let x = 27; x <= 37; x++) {
  const curve = Math.round(Math.sin((x - 27) / 10 * Math.PI) * 3);
  setPixel(x, 35 + curve, dark);
}

That's the foundation. Everything in pixel art comes down to setPixel(x, y, color). The constraint — one color per cell — is what gives pixel art its character.

Palette management: the soul of pixel art

Great pixel art lives or dies by its palette. Classic systems had strict limits: the NES had 54 colors, the Game Boy had 4 shades of green. These constraints forced artists to be creative, and the results were iconic.

const canvas = document.createElement('canvas');
canvas.width = canvas.height = 512;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const GRID = 64, CELL = canvas.width / GRID;

// Classic palettes
const PALETTES = {
  gameboy:  ['#0f380f', '#306230', '#8bac0f', '#9bbc0f'],
  nes:      ['#000000', '#fcfcfc', '#f8f8f8', '#bcbcbc', '#7c7c7c', '#a4e4fc', '#3cbcfc', '#0078f8', '#0000fc', '#b8b8f8', '#6888fc', '#0058f8', '#0000bc', '#d8b8f8', '#9878f8', '#6844fc'],
  pico8:    ['#000000', '#1d2b53', '#7e2553', '#008751', '#ab5236', '#5f574f', '#c2c3c7', '#fff1e8', '#ff004d', '#ffa300', '#ffec27', '#00e436', '#29adff', '#83769c', '#ff77a8', '#ffccaa'],
  sunset:   ['#1a1a2e', '#16213e', '#0f3460', '#e94560', '#f8b500', '#ff6b6b', '#ffd93d', '#533483'],
};

// Draw palette swatches
let palette = PALETTES.pico8;
function drawPalette() {
  const sw = Math.ceil(Math.sqrt(palette.length));
  for (let i = 0; i < palette.length; i++) {
    const px = 2 + (i % sw) * 4;
    const py = 2 + Math.floor(i / sw) * 4;
    for (let dy = 0; dy < 3; dy++)
      for (let dx = 0; dx < 3; dx++) {
        ctx.fillStyle = palette[i];
        ctx.fillRect((px + dx) * CELL, (py + dy) * CELL, CELL, CELL);
      }
  }
}

// Generate a landscape using only the palette
function generateLandscape() {
  ctx.fillStyle = palette[0];
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Sky gradient (map palette indices to rows)
  for (let y = 0; y < GRID / 2; y++) {
    const colorIdx = Math.floor((y / (GRID / 2)) * 3) + 1;
    for (let x = 0; x < GRID; x++) {
      ctx.fillStyle = palette[colorIdx % palette.length];
      ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
    }
  }

  // Hills using sine waves
  for (let layer = 0; layer < 3; layer++) {
    const baseY = 28 + layer * 8;
    const color = palette[3 + layer * 2] || palette[3 + layer];
    for (let x = 0; x < GRID; x++) {
      const hillY = baseY + Math.round(Math.sin(x * 0.15 + layer * 2) * 4
                    + Math.sin(x * 0.08 + layer) * 3);
      for (let y = hillY; y < GRID; y++) {
        ctx.fillStyle = color;
        ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
      }
    }
  }
}

generateLandscape();
drawPalette();

The palette defines the mood. Swap PALETTES.pico8 for PALETTES.sunset and the entire landscape transforms — same shapes, completely different feeling.

Dithering: creating depth with limited colors

Dithering simulates gradients and in-between colors by alternating pixels in patterns. It's the technique that gave Game Boy games their sense of depth despite having only 4 shades. The most common pattern is ordered dithering using a Bayer matrix.

const canvas = document.createElement('canvas');
canvas.width = canvas.height = 512;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const GRID = 128, CELL = canvas.width / GRID;

// 4x4 Bayer matrix for ordered dithering
const BAYER = [
  [ 0,  8,  2, 10],
  [12,  4, 14,  6],
  [ 3, 11,  1,  9],
  [15,  7, 13,  5],
];

const palette = ['#0f380f', '#306230', '#8bac0f', '#9bbc0f']; // Game Boy

function dither(x, y, value) {
  // value: 0.0 to 1.0
  const threshold = BAYER[y % 4][x % 4] / 16;
  const scaled = value * (palette.length - 1);
  const idx = scaled + threshold > Math.floor(scaled) + 0.5
    ? Math.ceil(scaled) : Math.floor(scaled);
  return palette[Math.min(idx, palette.length - 1)];
}

// Draw a gradient sphere with dithering
const cx = GRID / 2, cy = GRID / 2, r = GRID * 0.4;
for (let y = 0; y < GRID; y++) {
  for (let x = 0; x < GRID; x++) {
    const dx = x - cx, dy = y - cy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist < r) {
      // Fake 3D lighting
      const nx = dx / r, ny = dy / r;
      const light = Math.max(0, -nx * 0.5 - ny * 0.7 + 0.5);
      ctx.fillStyle = dither(x, y, light);
    } else {
      ctx.fillStyle = palette[0];
    }
    ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
  }
}

The Bayer matrix creates that distinctive cross-hatch pattern you recognize from retro games. With just 4 colors, you get smooth-looking gradients. That sphere looks 3D despite being made of giant pixels — all through clever dithering.

Sprite animation: bringing pixels to life

Pixel art animation works frame-by-frame. Each frame is a small grid of pixels, and you cycle through them. The key to good pixel animation is "squash and stretch" — even with a few pixels, you can convey weight and energy.

const canvas = document.createElement('canvas');
canvas.width = canvas.height = 512;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const GRID = 32, CELL = canvas.width / GRID;

const palette = ['#1a1a2e', '#e94560', '#f8b500', '#fff1e8', '#0f3460'];

// Define a walking character as frame data (8x8 sprites)
// Each frame: 2D array of palette indices (-1 = transparent)
const frames = [
  // Frame 0: standing
  [[-1,-1, 3, 3, 3,-1,-1,-1],
   [-1, 3, 3, 3, 3, 3,-1,-1],
   [-1, 3, 0, 3, 0, 3,-1,-1],
   [-1, 3, 3, 1, 3, 3,-1,-1],
   [-1,-1, 1, 1, 1,-1,-1,-1],
   [-1, 2, 1, 1, 1, 2,-1,-1],
   [-1,-1, 4, 4, 4,-1,-1,-1],
   [-1,-1, 4,-1, 4,-1,-1,-1]],
  // Frame 1: step left
  [[-1,-1, 3, 3, 3,-1,-1,-1],
   [-1, 3, 3, 3, 3, 3,-1,-1],
   [-1, 3, 0, 3, 0, 3,-1,-1],
   [-1, 3, 3, 1, 3, 3,-1,-1],
   [-1,-1, 1, 1, 1,-1,-1,-1],
   [-1, 2, 1, 1, 1, 2,-1,-1],
   [-1, 4,-1, 4,-1,-1,-1,-1],
   [ 4,-1,-1,-1, 4,-1,-1,-1]],
  // Frame 2: step right
  [[-1,-1, 3, 3, 3,-1,-1,-1],
   [-1, 3, 3, 3, 3, 3,-1,-1],
   [-1, 3, 0, 3, 0, 3,-1,-1],
   [-1, 3, 3, 1, 3, 3,-1,-1],
   [-1,-1, 1, 1, 1,-1,-1,-1],
   [-1, 2, 1, 1, 1, 2,-1,-1],
   [-1,-1,-1, 4,-1, 4,-1,-1],
   [-1,-1, 4,-1,-1,-1, 4,-1]],
];

let frame = 0, charX = 4;

function drawFrame() {
  ctx.fillStyle = palette[0];
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Ground
  for (let x = 0; x < GRID; x++) {
    ctx.fillStyle = '#2d5a27';
    ctx.fillRect(x * CELL, 24 * CELL, CELL, CELL * 8);
  }

  // Character
  const sprite = frames[frame];
  for (let y = 0; y < sprite.length; y++) {
    for (let x = 0; x < sprite[y].length; x++) {
      if (sprite[y][x] >= 0) {
        ctx.fillStyle = palette[sprite[y][x]];
        ctx.fillRect((charX + x) * CELL, (16 + y) * CELL, CELL, CELL);
      }
    }
  }

  charX = (charX + 0.15) % (GRID - 8);
  frame = Math.floor(Date.now() / 200) % frames.length;
  requestAnimationFrame(drawFrame);
}
drawFrame();

Three frames, five colors, and you have a walking character. The secret to pixel animation is subtlety — move just 1-2 pixels per frame. The brain fills in the rest.

Generative pixel landscapes

This is where code and pixel art truly shine together. Instead of placing each pixel by hand, you use algorithms to generate entire worlds. Here's a procedural landscape generator that creates unique terrains every time you run it:

const canvas = document.createElement('canvas');
canvas.width = 640; canvas.height = 480;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const GW = 160, GH = 120, CW = canvas.width / GW, CH = canvas.height / GH;

const pal = {
  sky:    ['#1a1a2e', '#16213e', '#0f3460', '#533483', '#e94560', '#f8b500'],
  ground: ['#2d5a27', '#3a7d32', '#4a9e3f', '#5ab84d'],
  water:  ['#0f3460', '#1a5276', '#2e86c1'],
  tree:   ['#5a3825', '#2d5a27', '#3a7d32'],
};

// Simple 1D noise
function noise1D(x) {
  const i = Math.floor(x);
  const f = x - i;
  const a = Math.sin(i * 127.1 + i * 311.7) * 43758.5453 % 1;
  const b = Math.sin((i+1) * 127.1 + (i+1) * 311.7) * 43758.5453 % 1;
  const t = f * f * (3 - 2 * f); // smoothstep
  return Math.abs(a + (b - a) * t);
}

function generateWorld(seed) {
  // Sky
  for (let y = 0; y < GH * 0.5; y++) {
    const idx = Math.floor((y / (GH * 0.5)) * pal.sky.length);
    for (let x = 0; x < GW; x++) {
      ctx.fillStyle = pal.sky[Math.min(idx, pal.sky.length - 1)];
      ctx.fillRect(x * CW, y * CH, CW, CH);
    }
  }

  // Stars
  for (let i = 0; i < 40; i++) {
    const sx = Math.floor(noise1D(i + seed) * GW);
    const sy = Math.floor(noise1D(i + seed + 100) * GH * 0.35);
    ctx.fillStyle = '#fff1e8';
    ctx.fillRect(sx * CW, sy * CH, CW, CH);
  }

  // Mountains (background)
  for (let x = 0; x < GW; x++) {
    const h = noise1D(x * 0.03 + seed) * 20 + noise1D(x * 0.08 + seed) * 10;
    for (let y = GH * 0.5 - h; y < GH * 0.5; y++) {
      ctx.fillStyle = '#533483';
      ctx.fillRect(x * CW, y * CH, CW, CH);
    }
  }

  // Terrain
  for (let x = 0; x < GW; x++) {
    const terrainH = Math.floor(noise1D(x * 0.05 + seed * 2) * 15 + GH * 0.55);
    const waterLevel = GH * 0.7;
    for (let y = terrainH; y < GH; y++) {
      if (y > waterLevel && terrainH > waterLevel) {
        const wi = Math.min(Math.floor((y - waterLevel) / 5), pal.water.length - 1);
        ctx.fillStyle = pal.water[wi];
      } else {
        const gi = Math.min(Math.floor((y - terrainH) / 4), pal.ground.length - 1);
        ctx.fillStyle = pal.ground[gi];
      }
      ctx.fillRect(x * CW, y * CH, CW, CH);
    }
    // Trees
    if (terrainH < waterLevel && noise1D(x * 0.5 + seed * 3) > 0.55) {
      const th = Math.floor(noise1D(x + seed * 4) * 5 + 4);
      for (let ty = 0; ty < th; ty++) {
        ctx.fillStyle = pal.tree[0];
        ctx.fillRect(x * CW, (terrainH - ty) * CH, CW, CH);
      }
      for (let dy = -2; dy <= 2; dy++)
        for (let dx = -2; dx <= 2; dx++)
          if (Math.abs(dx) + Math.abs(dy) <= 2) {
            ctx.fillStyle = pal.tree[1 + (Math.abs(dx) + Math.abs(dy) > 1 ? 1 : 0)];
            ctx.fillRect((x + dx) * CW, (terrainH - th + dy) * CH, CW, CH);
          }
    }
  }
}

generateWorld(Math.random() * 1000);

Every refresh creates a unique landscape — different mountains, different tree placements, different water features. This is generative pixel art: the code is the artist, and randomness is the brush.

Scaling algorithms: nearest neighbor vs. smooth

When you display pixel art at larger sizes, the scaling algorithm matters enormously. The browser's default imageSmoothingEnabled = true blurs pixels into mush. For pixel art, you want hard edges — nearest-neighbor interpolation.

const canvas = document.createElement('canvas');
canvas.width = 512; canvas.height = 256;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

// Draw a tiny 16x16 pixel art icon
const tiny = document.createElement('canvas');
tiny.width = tiny.height = 16;
const tctx = tiny.getContext('2d');

const icon = [
  '................',
  '....11111111....',
  '...1333333331...',
  '..133333333331..',
  '.13330033003331.',
  '.13330033003331.',
  '.13333333333331.',
  '.13331111113331.',
  '.13330000003331.',
  '..133333333331..',
  '...1333333331...',
  '....11111111....',
  '......1..1......',
  '.....11..11.....',
  '....111..111....',
  '................',
];
const colors = { '.': '#1a1a2e', '0': '#0f3460', '1': '#e94560', '3': '#f8b500' };

icon.forEach((row, y) => {
  [...row].forEach((c, x) => {
    tctx.fillStyle = colors[c];
    tctx.fillRect(x, y, 1, 1);
  });
});

// Left: blurry (default smoothing)
ctx.imageSmoothingEnabled = true;
ctx.drawImage(tiny, 0, 0, 256, 256);

// Right: crisp (nearest neighbor)
ctx.imageSmoothingEnabled = false;
ctx.drawImage(tiny, 256, 0, 256, 256);

// Labels
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.fillText('Smoothed (blurry)', 60, 246);
ctx.fillText('Nearest Neighbor (crisp)', 300, 246);

Always set ctx.imageSmoothingEnabled = false when rendering pixel art. You can also add image-rendering: pixelated in CSS for the canvas element. This single line makes or breaks pixel art display on the web.

Palette cycling: the secret animation trick

One of the most elegant pixel art techniques is palette cycling — instead of changing pixel positions, you rotate the colors in the palette. This creates fluid animation (water, lava, aurora) with zero per-pixel updates.

const canvas = document.createElement('canvas');
canvas.width = canvas.height = 512;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
const GRID = 64, CELL = canvas.width / GRID;

// Water palette that will cycle
let waterColors = ['#0a1628', '#0f3460', '#1a5276', '#2e86c1', '#5dade2', '#85c1e9', '#aed6f1', '#d4e6f1'];
const sandColor = '#f4d03f';
const skyColor = '#1a1a2e';
const groundColors = ['#5a3825', '#6b4226', '#8b6914'];

// Draw static scene once
const pixels = [];
for (let y = 0; y < GRID; y++) {
  pixels[y] = [];
  for (let x = 0; x < GRID; x++) {
    if (y < GRID * 0.3) {
      pixels[y][x] = { type: 'sky' };
    } else if (y < GRID * 0.45) {
      const waveH = Math.sin(x * 0.2) * 2 + Math.sin(x * 0.1) * 3;
      if (y < GRID * 0.35 + waveH) pixels[y][x] = { type: 'sky' };
      else pixels[y][x] = { type: 'water', idx: Math.floor(Math.sin(x * 0.3 + y * 0.5) * 4 + 4) % 8 };
    } else if (y < GRID * 0.7) {
      pixels[y][x] = { type: 'water', idx: Math.floor(Math.sin(x * 0.2 + y * 0.3) * 4 + 4) % 8 };
    } else if (y < GRID * 0.73) {
      pixels[y][x] = { type: 'sand' };
    } else {
      pixels[y][x] = { type: 'ground', idx: Math.floor(Math.random() * 3) };
    }
  }
}

function render() {
  for (let y = 0; y < GRID; y++) {
    for (let x = 0; x < GRID; x++) {
      const p = pixels[y][x];
      if (p.type === 'sky') ctx.fillStyle = skyColor;
      else if (p.type === 'water') ctx.fillStyle = waterColors[p.idx];
      else if (p.type === 'sand') ctx.fillStyle = sandColor;
      else ctx.fillStyle = groundColors[p.idx];
      ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
    }
  }

  // Rotate the water palette
  waterColors.push(waterColors.shift());
  setTimeout(() => requestAnimationFrame(render), 120);
}
render();

The water animates smoothly, but we never change which pixel gets which palette index — we only rotate the colors themselves. This technique was used in classic Amiga demos and games like The Secret of Monkey Island. It's absurdly efficient: zero CPU cost for animation beyond one array rotation per frame.

Generative pixel art characters

Procedurally generated pixel art characters use symmetry and randomness to create infinite variations. This technique powers many indie games and avatar generators:

const canvas = document.createElement('canvas');
canvas.width = 640; canvas.height = 480;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);

const palettes = [
  ['#e94560', '#f8b500', '#fff1e8'],
  ['#29adff', '#00e436', '#fff1e8'],
  ['#ff77a8', '#ffccaa', '#fff1e8'],
  ['#83769c', '#ff004d', '#fff1e8'],
  ['#ab5236', '#ffa300', '#ffec27'],
];

function generateCharacter(seed) {
  const rng = (n) => {
    seed = (seed * 16807 + 0) % 2147483647;
    return seed % n;
  };

  // 5x7 half-sprite (mirrored for symmetry)
  const w = 5, h = 7;
  const half = [];
  for (let y = 0; y < h; y++) {
    half[y] = [];
    for (let x = 0; x < w; x++) {
      // Higher chance of fill in center columns
      const chance = x < 2 ? 0.6 : x < 3 ? 0.5 : 0.35;
      // Top and bottom rows: less fill
      const yChance = (y === 0 || y === h - 1) ? chance * 0.5 : chance;
      half[y][x] = Math.random() < yChance ? 1 : 0;
    }
  }

  const pal = palettes[rng(palettes.length)];
  const full = [];
  for (let y = 0; y < h; y++) {
    full[y] = [];
    for (let x = 0; x < w; x++) full[y][x] = half[y][x];
    for (let x = 0; x < w - 1; x++) full[y][w + x] = half[y][w - 2 - x]; // mirror
  }

  return { pixels: full, width: w * 2 - 1, height: h, palette: pal };
}

// Generate a grid of characters
const SCALE = 6;
const PAD = 4;
const cols = 12, rows = 8;

for (let row = 0; row < rows; row++) {
  for (let col = 0; col < cols; col++) {
    const char = generateCharacter(Date.now() + row * cols + col);
    const ox = 30 + col * (char.width * SCALE + PAD * SCALE);
    const oy = 30 + row * (char.height * SCALE + PAD * SCALE);

    for (let y = 0; y < char.height; y++) {
      for (let x = 0; x < char.width; x++) {
        if (char.pixels[y][x]) {
          // Outline
          ctx.fillStyle = '#000';
          ctx.fillRect(ox + x * SCALE - 1, oy + y * SCALE - 1, SCALE + 2, SCALE + 2);
        }
      }
    }
    for (let y = 0; y < char.height; y++) {
      for (let x = 0; x < char.width; x++) {
        if (char.pixels[y][x]) {
          const ci = (y < 2) ? 2 : (y < 5) ? 0 : 1;
          ctx.fillStyle = char.palette[ci];
          ctx.fillRect(ox + x * SCALE, oy + y * SCALE, SCALE, SCALE);
        }
      }
    }
  }
}

Each character is unique but follows the same rules: vertical symmetry, higher fill probability in the center, less fill at the edges. The result looks like a fleet of alien invaders — which is exactly how the original Space Invaders sprites were designed, just by hand rather than code.

Pixel art tips and techniques

  • Limit your palette — 4-16 colors is the sweet spot. Constraints breed creativity.
  • Avoid black outlines everywhere — use dark variants of the fill color instead ("sel-out" or selective outlining).
  • Anti-aliasing by hand — place intermediate colors at diagonal edges for smoother curves (called "manual AA" or jaggies removal).
  • Sub-pixel animation — shift colors rather than positions for micro-movements that feel smooth at low resolution.
  • Hue shifting — when making darker or lighter versions of a color, shift the hue slightly (darks go blue/purple, lights go yellow). This makes pixel art feel painterly rather than flat.
  • Canvas image-rendering: pixelated — always add this CSS rule so the browser doesn't blur your pixels when the canvas is resized.

Tools and resources

  • Aseprite — the industry-standard pixel art editor (paid, open-source build available)
  • Piskel — free browser-based pixel art editor with animation support
  • Lospec — curated palette library with hundreds of pixel art palettes
  • PICO-8 — fantasy console that enforces 128×128 resolution and 16 colors
  • Plain Canvas — as shown in this tutorial, you can create everything from scratch with <canvas> and JavaScript

For inspiration, explore the Lumitree branches — several micro-worlds use pixel art techniques, from retro game tributes to generative pixel landscapes. Each one fits in under 50KB, proving that constraints and pixels are a perfect match.

From pixels to worlds

Pixel art with code is about embracing limitations as creative fuel. A 64×64 grid with 8 colors sounds restrictive, but as we've seen, it's enough for landscapes, characters, animations, and entire procedural worlds. The retro aesthetic isn't a compromise — it's a design choice that connects your work to decades of game art history while making it uniquely yours.

The techniques in this guide — grid rendering, palette management, dithering, sprite animation, generative landscapes, and palette cycling — are the building blocks. Combine them. Layer a dithered sky over a generated terrain, populate it with procedural characters, and animate the water with palette cycling. In 100 lines of code, you'll have a living pixel world.

That's the spirit of creative coding: small tools, infinite possibilities. Start with the grid. Place your first pixel. See what grows.

Related articles