🎲 Building a PNG to Dice Art Converter in TypeScript

dice art

Learn how to convert images into artistic dice patterns using TypeScript and HTML5 Canvas

Table of Contents

1. Project Overview

In this tutorial, we'll build a web application that converts PNG images into dice art. Each pixel of the input image will be represented by a dice face (1-6 dots) based on its brightness.

💡 What You'll Learn

2. Core Concepts

How It Works

  1. Load Image: User uploads a PNG/JPG file
  2. Extract Pixels: Read pixel RGB values using Canvas API
  3. Calculate Brightness: Convert RGB to brightness (0-255)
  4. Map to Dice: Map brightness to dice values (1-6)
  5. Draw Result: Draw dice patterns on output canvas
Key Insight: Brighter pixels get fewer dots (dice 1), darker pixels get more dots (dice 6). This creates contrast in the final artwork.

3. Creating the Interface

Define TypeScript Types

Start by defining the configuration interface for our converter:

interface DiceConverterConfig {
  diceSize: number;    // Size of each dice in pixels
  maxWidth: number;   // Maximum width in dice units
}
Why interfaces? They provide type safety and make our code self-documenting. TypeScript will catch errors at compile time.

4. Building the Converter Class

Class Structure

We'll use a class to encapsulate all dice conversion logic:

class DiceArtConverter {
  private hiddenCanvas: HTMLCanvasElement;
  private outputCanvas: HTMLCanvasElement;
  private currentImage: HTMLImageElement | null = null;
  private config: DiceConverterConfig;

  constructor(
    hiddenCanvas: HTMLCanvasElement,
    outputCanvas: HTMLCanvasElement,
    initialConfig: DiceConverterConfig
  ) {
    this.hiddenCanvas = hiddenCanvas;
    this.outputCanvas = outputCanvas;
    this.config = initialConfig;
  }
}
Important: We use two canvases - one hidden for processing the original image, one visible for displaying the dice art.

5. Drawing Dice

Understanding Dice Patterns

Each dice face (1-6) has a specific dot pattern. Let's implement the drawing logic:

private drawDice(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  size: number,
  value: number
): void {
  const dotSize = size * 0.12;

  // Draw dice background
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(x, y, size, size);
  ctx.strokeStyle = '#333';
  ctx.lineWidth = 1;
  ctx.strokeRect(x, y, size, size);

  // Calculate center and offset for dots
  const cx = x + size / 2;
  const cy = y + size / 2;
  const offset = size * 0.3;

  // Helper function to draw a single dot
  const drawDot = (dx: number, dy: number): void => {
    ctx.beginPath();
    ctx.arc(cx + dx, cy + dy, dotSize, 0, Math.PI * 2);
    ctx.fill();
  };

  // Draw dots based on dice value
  ctx.fillStyle = '#000';
  switch (value) {
    case 1:
      drawDot(0, 0);  // Center dot
      break;
    case 2:
      drawDot(-offset, -offset);  // Top-left
      drawDot(offset, offset);    // Bottom-right
      break;
    case 3:
      drawDot(-offset, -offset);
      drawDot(0, 0);
      drawDot(offset, offset);
      break;
    // ... cases 4, 5, 6
  }
}

🎯 Dice Dot Positions

Dice 1: Center

Dice 2: Top-left, Bottom-right (diagonal)

Dice 3: Top-left, Center, Bottom-right

Dice 4: All four corners

Dice 5: Four corners + center

Dice 6: Three dots on left, three on right

6. Image Conversion Logic

Step 1: Calculate Brightness

Convert RGB values to a single brightness value:

private calculateBrightness(r: number, g: number, b: number): number {
  // Simple average method
  return (r + g + b) / 3;
  
  // Alternative: Weighted luminosity (more accurate)
  // return 0.299 * r + 0.587 * g + 0.114 * b;
}

Step 2: Map Brightness to Dice Value

Convert brightness (0-255) to dice value (1-6):

private brightnessToDiceValue(brightness: number): number {
  // Invert: darker pixels = more dots (higher value)
  const inverted = 255 - brightness;
  
  // Map 0-255 to 1-6
  const diceValue = Math.ceil((inverted / 255) * 5) + 1;
  
  // Ensure value is between 1 and 6
  return Math.max(1, Math.min(6, diceValue));
}
Math Explanation:

Step 3: Main Conversion Method

Process the entire image pixel by pixel:

public convertToDice(img: HTMLImageElement): void {
  this.currentImage = img;

  const hiddenCtx = this.hiddenCanvas.getContext('2d');
  const outputCtx = this.outputCanvas.getContext('2d');

  // Calculate dimensions
  const aspectRatio = img.height / img.width;
  const width = Math.min(this.config.maxWidth, img.width);
  const height = Math.floor(width * aspectRatio);

  // Draw original to hidden canvas
  this.hiddenCanvas.width = width;
  this.hiddenCanvas.height = height;
  hiddenCtx.drawImage(img, 0, 0, width, height);

  // Get pixel data
  const imageData = hiddenCtx.getImageData(0, 0, width, height);
  const data = imageData.data;

  // Setup output canvas
  this.outputCanvas.width = width * this.config.diceSize;
  this.outputCanvas.height = height * this.config.diceSize;

  // Process each pixel
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const i = (y * width + x) * 4;  // 4 bytes per pixel (RGBA)
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];

      const brightness = this.calculateBrightness(r, g, b);
      const diceValue = this.brightnessToDiceValue(brightness);

      this.drawDice(
        outputCtx,
        x * this.config.diceSize,
        y * this.config.diceSize,
        this.config.diceSize,
        diceValue
      );
    }
  }
}

7. Connecting the UI

File Upload Handler

Load images from user's computer:

public loadImage(file: File): Promise<void> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (event: ProgressEvent<FileReader>) => {
      const img = new Image();
      img.onload = () => {
        this.convertToDice(img);
        resolve();
      };
      img.onerror = () => reject(new Error('Failed to load image'));
      img.src = event.target.result as string;
    };

    reader.onerror = () => reject(new Error('Failed to read file'));
    reader.readAsDataURL(file);
  });
}

Initialize Application

function initializeDiceArtApp(): void {
  const hiddenCanvas = document.createElement('canvas');
  const outputCanvas = document.getElementById('outputCanvas') as HTMLCanvasElement;
  const fileInput = document.getElementById('fileInput') as HTMLInputElement;

  const converter = new DiceArtConverter(hiddenCanvas, outputCanvas, {
    diceSize: 20,
    maxWidth: 50
  });

  fileInput.addEventListener('change', async (e) => {
    const file = (e.target as HTMLInputElement).files?.[0];
    if (file) {
      await converter.loadImage(file);
    }
  });
}

// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initializeDiceArtApp);

8. Complete Example

Upload an image to get started

🎓 Key Takeaways

Next Steps

More TypeScript tutorials:
Fractal graphics zoom
Animated plasma effect
Happy Coding! 🎲✨