Introduction
In this article, we'll explore how to implement a browser-based tool that converts images into fuse bead (perler bead) patterns. This tool helps crafters create bead patterns from any image, supporting multiple bead brands with their specific color palettes, and generating professional PDF patterns ready for crafting.
Why Browser-Based Implementation?
1. Privacy Protection
When users convert images to bead patterns in the browser, their photos never leave their device.
2. No Server Costs
All image processing happens locally in the browser, eliminating server costs.
3. Instant Preview
Users can see the bead pattern immediately and adjust settings in real-time.
Technical Architecture
Core Implementation
1. Data Structures
type BeadStyle =
| 'symbolsWithColor'
| 'symbols'
| 'lettersNumbersWithColor'
| 'lettersNumbers'
| 'numbersWithColor'
| 'numbers'
| 'coloredBoxes'
| 'coloredCircles';
interface ImageFile {
id: string;
file: File;
previewUrl: string;
}
2. Bead Palette Definitions
The tool includes pre-defined color palettes for popular bead brands:
const BEAD_PALETTES: Record<string, { name: string; colors: [number, number, number][] }> = {
hama: {
name: "Hama Midi (53 colors)",
colors: [
[0, 0, 0], [255, 255, 255], [255, 0, 0], [200, 0, 0], [150, 0, 0],
[0, 0, 255], [0, 0, 200], [0, 0, 150], [255, 255, 0], [200, 200, 0],
[0, 255, 0], [0, 200, 0], [0, 150, 0], [255, 165, 0], [255, 140, 0],
[255, 105, 180], [255, 20, 147], [199, 21, 133], [220, 20, 60], [178, 34, 34],
[139, 69, 19], [160, 82, 45], [205, 133, 63], [222, 184, 135], [210, 180, 140],
// ... more colors
]
},
perler: {
name: "Perler (92 colors)",
colors: [
// Perler-specific colors
]
},
artkal: {
name: "Artkal Midi (156 colors)",
colors: [
// 156 colors
]
},
nabbi: {
name: "Nabbi (30 colors)",
colors: [
// 30 colors
]
},
ikea: {
name: "Ikea Pyssla (18 colors)",
colors: [
// 18 colors
]
}
};
3. Color Mapping Algorithm
Convert image colors to the nearest palette color using Euclidean distance:
const mapToPalette = useCallback((r: number, g: number, b: number, palette: [number, number, number][]) => {
let minDistance = Infinity;
let closestColor = palette[0];
for (const color of palette) {
const distance = Math.sqrt(
Math.pow(r - color[0], 2) +
Math.pow(g - color[1], 2) +
Math.pow(b - color[2], 2)
);
if (distance < minDistance) {
minDistance = distance;
closestColor = color;
}
}
return closestColor;
}, []);
4. Generating Color Identifiers
Create unique identifiers (symbols/letters/numbers) for each color:
const generateColorMapping = (colorCounts: { color: [number, number, number]; count: number }[]) => {
const symbols = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,./<>?'.split('');
const mapping = new Map<string, { symbol: string; letter: string; number: number }>();
colorCounts.forEach((item, index) => {
const colorKey = `${item.color[0]},${item.color[1]},${item.color[2]}`;
mapping.set(colorKey, {
symbol: symbols[index % symbols.length],
letter: String.fromCharCode(65 + (index % 26)), // A-Z
number: index + 1
});
});
return mapping;
};
5. Image Processing Pipeline
const processImage = useCallback(async () => {
if (!originalImage || !canvasRef.current || !sourceCanvasRef.current) return;
const img = new Image();
img.crossOrigin = "anonymous";
img.src = originalImage;
await new Promise((resolve) => { img.onload = resolve; });
// Calculate bead grid dimensions
const beadsX = Math.floor(img.width / beadSize);
const beadsY = Math.floor(img.height / beadSize);
// Create source canvas (small, for pixel data)
sourceCanvas.width = beadsX;
sourceCanvas.height = beadsY;
// Downscale image to bead grid
sourceCtx.drawImage(tempCanvas, 0, 0, beadsX, beadsY);
const imageData = sourceCtx.getImageData(0, 0, beadsX, beadsY);
const data = imageData.data;
const palette = BEAD_PALETTES[selectedPalette]?.colors.slice(0, paletteSize);
// Map each pixel to nearest palette color
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Optional grayscale conversion
if (grayscale) {
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
r = g = b = gray;
}
const [nr, ng, nb] = mapToPalette(r, g, b, palette);
data[i] = nr;
data[i + 1] = ng;
data[i + 2] = nb;
// Track color usage
const colorKey = `${nr},${ng},${nb}`;
const existing = colorCountMap.get(colorKey);
if (existing) {
existing.count++;
} else {
colorCountMap.set(colorKey, {color: [nr, ng, nb], count: 1});
}
}
// Sort colors by usage count
const sortedColorCounts = Array.from(colorCountMap.values())
.sort((a, b) => b.count - a.count);
setColorCounts(sortedColorCounts);
setTotalBeads(beadsX * beadsY);
// Render to output canvas with grid
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const padding = beadSize * 0.1;
const drawSize = beadSize - padding * 2;
for (let y = 0; y < beadsY; y++) {
for (let x = 0; x < beadsX; x++) {
const i = (y * beadsX + x) * 4;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(x * beadSize + padding, y * beadSize + padding, drawSize, drawSize);
}
}
// Draw grid lines
if (gridVisible) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1;
for (let x = 0; x <= beadsX; x++) {
ctx.beginPath();
ctx.moveTo(x * beadSize, 0);
ctx.lineTo(x * beadSize, canvas.height);
ctx.stroke();
}
for (let y = 0; y <= beadsY; y++) {
ctx.beginPath();
ctx.moveTo(0, y * beadSize);
ctx.lineTo(canvas.width, y * beadSize);
ctx.stroke();
}
}
setProcessedImage(canvas.toDataURL('image/png'));
}, [originalImage, beadSize, grayscale, selectedPalette, paletteSize, gridVisible]);
6. PDF Generation
Generate a multi-page PDF with pattern sections:
const handleDownloadPDF = useCallback(() => {
const doc = new jsPDF('p', 'mm', 'a4');
const pageWidth = 210;
const pageHeight = 297;
const margin = 10;
// Page 1: Preview
doc.setFontSize(16);
doc.text('Pixel Beads Pattern - Preview', margin, margin + 5);
// Add image preview...
// Page 2: Color Chart
doc.addPage();
doc.setFontSize(16);
doc.text('Color Chart', margin, margin + 5);
const colorMapping = generateColorMapping(colorCounts);
colorCounts.forEach((item, idx) => {
const colorKey = `${item.color[0]},${item.color[1]},${item.color[2]}`;
const mapping = colorMapping.get(colorKey);
// Draw color swatch
doc.setFillColor(item.color[0], item.color[1], item.color[2]);
doc.rect(xPos, yPos, 6, 6, 'F');
doc.rect(xPos, yPos, 6, 6, 'S'); // border
// Show identifier and bead count
let identifier = mapping?.symbol || '?';
if (beadStyle.includes('lettersNumbers')) {
identifier = mapping?.letter || '?';
} else if (beadStyle.includes('numbers')) {
identifier = (mapping?.number || 0).toString();
}
doc.text(`${identifier} ${item.hex.toUpperCase()}`, xPos + 8, yPos + 4);
doc.text(`${item.count} beads (${((item.count / totalBeads) * 100).toFixed(1)}%)`, xPos + 8, yPos + 9);
});
// Page 3: Section Layout Overview
doc.addPage();
doc.text('Section Layout Overview', margin, margin + 5);
// Calculate sections
const cellsPerRow = Math.floor(usableWidth / pdfCellSize);
const cellsPerCol = Math.floor(usableHeight / pdfCellSize);
const sectionsX = Math.ceil(beadsX / cellsPerRow);
const sectionsY = Math.ceil(beadsY / cellsPerCol);
// Draw section boxes with numbers...
// Pages 4+: Detailed Section Pages
for (let sy = 0; sy < sectionsY; sy++) {
for (let sx = 0; sx < sectionsX; sx++) {
doc.addPage();
// Create high-res canvas for this section
const tempCanvas = document.createElement('canvas');
tempCanvas.width = sectionWidth * PIXELS_PER_CELL;
tempCanvas.height = sectionHeight * PIXELS_PER_CELL;
// Draw beads based on beadStyle
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
// Get color
const i = (y * beadsX + x) * 4;
const r = data[i], g = data[i+1], b = data[i+2];
const colorKey = `${r},${g},${b}`;
const mapping = colorMapping.get(colorKey);
if (isPureTextStyle) {
// White background + black text
tempCtx.fillStyle = 'white';
tempCtx.fillRect(pixelX, pixelY, PIXELS_PER_CELL, PIXELS_PER_CELL);
tempCtx.fillStyle = 'black';
tempCtx.font = `bold ${PIXELS_PER_CELL * 0.7}px monospace`;
tempCtx.textAlign = 'center';
tempCtx.textBaseline = 'middle';
let text = '';
if (beadStyle === 'symbols') text = mapping?.symbol || '?';
else if (beadStyle === 'lettersNumbers') text = mapping?.letter || '?';
else if (beadStyle === 'numbers') text = (mapping?.number || 0).toString();
tempCtx.fillText(text, pixelX + PIXELS_PER_CELL/2, pixelY + PIXELS_PER_CELL/2);
}
else if (isColoredTextStyle) {
// Colored background + text with contrast
tempCtx.fillStyle = `rgb(${r}, ${g}, ${b})`;
tempCtx.fillRect(pixelX, pixelY, PIXELS_PER_CELL, PIXELS_PER_CELL);
// Adjust text color based on brightness
const brightness = r + g + b;
tempCtx.fillStyle = brightness > 380 ? 'black' : 'white';
// Draw text...
}
else {
// Pure color (coloredBoxes/coloredCircles)
tempCtx.fillStyle = `rgb(${r}, ${g}, ${b})`;
tempCtx.fillRect(pixelX, pixelY, PIXELS_PER_CELL, PIXELS_PER_CELL);
}
}
}
// Add to PDF
const sectionImgData = tempCanvas.toDataURL('image/png');
doc.addImage(sectionImgData, 'PNG', margin, margin + 15, drawWidth, drawHeight);
}
}
doc.save('pixel-beads-pattern.pdf');
}, [processedImage, beadSize, pdfCellSize, colorCounts, beadStyle]);
7. Bead Style Options
const BEAD_STYLE_OPTIONS = [
{ value: 'symbolsWithColor', label: 'Symbols With Colored Boxes' },
{ value: 'symbols', label: 'Symbols' },
{ value: 'lettersNumbersWithColor', label: 'Letters/Numbers With Colored Boxes' },
{ value: 'lettersNumbers', label: 'Letters/Numbers' },
{ value: 'numbersWithColor', label: 'Numbers With Colored Boxes' },
{ value: 'numbers', label: 'Numbers' },
{ value: 'coloredBoxes', label: 'Colored Boxes' },
{ value: 'coloredCircles', label: 'Colored Circles' },
];
8. Advanced Options
The tool includes advanced options for fine-tuning:
// Dithering methods
const DITHERING_METHODS = [
'none',
'atkinson',
'floydSteinberg',
'stucki',
'burkes',
'sierra'
];
// Color distance calculation methods
const DISTANCE_METHODS = [
'weightedEuclidean',
'euclidean',
'cie76',
'cie94',
'ciede2000'
];
// Color reduction algorithms
const COLOR_REDUCTION = [
'none',
'mmcq',
'rgbQuant'
];
PDF Output Structure
The generated PDF includes:
- Preview Page: Full pattern preview
- Color Chart: All colors with identifiers, hex codes, and bead counts
- Layout Overview: Visual section numbering guide
- Section Pages: Detailed patterns split into manageable sections
Supported Bead Brands
| Brand | Colors | Description |
|---|---|---|
| Hama Midi | 53 | Classic Hama beads |
| Perler | 92 | US Perler beads |
| Artkal Midi | 156 | Artkal with most colors |
| Nabbi | 30 | Nabbi BIO beads |
| IKEA Pyssla | 18 | Budget IKEA option |
Processing Flow
Key Features
- Real-time Preview: See changes instantly as you adjust settings
- Multiple Brands: Support for Hama, Perler, Artkal, Nabbi, IKEA
- Adjustable Grid: Control bead size (5-30px)
- Color Quantization: Limit to specific number of colors
- Grayscale Mode: Convert to grayscale for simpler patterns
- Grid Toggle: Show/hide grid lines
- PDF Export: Multi-page PDF with color chart and sectioned patterns
- Bead Styles: 8 different visualization styles
Performance Characteristics
- Processing Time: <1 second for typical images
- Memory Usage: Low (canvas-based processing)
- PDF Generation: 2-10 seconds depending on pattern size
Use Cases
- Create Patterns: Convert photos to bead patterns
- Pixel Art: Create perler bead versions of pixel art
- Cross-stitch Convert: Alternative to cross-stitch
- Gift Ideas: Create personalized bead patterns
- Educational: Teach color theory and pattern making
Conclusion
This browser-based pixel beads pattern generator makes it easy for crafters to create professional bead patterns from any image. The implementation uses:
- Canvas API for image processing and color mapping
- jsPDF for generating multi-page PDF patterns
- Color Distance Algorithms for accurate palette matching
Users can select from multiple bead brands, adjust patterns in real-time, and export complete PDF patterns with color charts and sectioned layouts - all without uploading their images to any server.
Try it yourself at Free Image Tools
Create beautiful fuse bead patterns from your photos. No upload required - your images stay on your device!



Top comments (0)