DEV Community

monkeymore studio
monkeymore studio

Posted on

Building a Browser-Based Pixel Beads Pattern Generator

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;
}
Enter fullscreen mode Exit fullscreen mode

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
    ]
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
}, []);
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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' },
];
Enter fullscreen mode Exit fullscreen mode

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'
];
Enter fullscreen mode Exit fullscreen mode

PDF Output Structure

The generated PDF includes:

  1. Preview Page: Full pattern preview
  2. Color Chart: All colors with identifiers, hex codes, and bead counts
  3. Layout Overview: Visual section numbering guide
  4. 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

  1. Real-time Preview: See changes instantly as you adjust settings
  2. Multiple Brands: Support for Hama, Perler, Artkal, Nabbi, IKEA
  3. Adjustable Grid: Control bead size (5-30px)
  4. Color Quantization: Limit to specific number of colors
  5. Grayscale Mode: Convert to grayscale for simpler patterns
  6. Grid Toggle: Show/hide grid lines
  7. PDF Export: Multi-page PDF with color chart and sectioned patterns
  8. Bead Styles: 8 different visualization styles

Performance Characteristics

  1. Processing Time: <1 second for typical images
  2. Memory Usage: Low (canvas-based processing)
  3. PDF Generation: 2-10 seconds depending on pattern size

Use Cases

  1. Create Patterns: Convert photos to bead patterns
  2. Pixel Art: Create perler bead versions of pixel art
  3. Cross-stitch Convert: Alternative to cross-stitch
  4. Gift Ideas: Create personalized bead patterns
  5. 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)