Learn how to convert images into artistic dice patterns using TypeScript and HTML5 Canvas
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.
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
}
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;
}
}
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 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
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;
}
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));
}
(inverted / 255) → Normalize to 0-1* 5 → Scale to 0-5+ 1 → Shift to 1-6Math.ceil() → Round up to nearest integerProcess 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
);
}
}
}
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);
});
}
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);
Upload an image to get started
More TypeScript tutorials:
Fractal graphics zoom
Animated plasma effect
Happy Coding! 🎲✨