DEV Community

hirosys for AWS Community Builders

Posted on • Edited on

I Built a Fuse Bead Pattern Generator Using Kiro and Claude Opus, Deployed on AWS Amplify

Hello everyone!

I built a web application that automatically generates fuse bead (Perler/Nano beads) patterns from any uploaded image using Kiro and Claude Opus. I love making fuse bead crafts, but creating custom patterns manually is always a hassle—so I decided to automate it!

Application Screenshot

Development Environment

Kiro + Claude Opus 4.8

Kiro is an AI-powered IDE provided by AWS. For this project, I used Anthropic's Claude Opus 4.8 as the backend.

Kiro features an excellent workflow called Spec-Driven Development. It allows you to build software systematically by solidifying documents in order: Requirements (requirements.md) → Technical Design (design.md) → Task List (tasks.md). Because the "what to build" phase is completely locked in early on, your specifications rarely drift during implementation.

My workflow was the same as usual: I brainstormed ideas and discussed them with Kiro, and Kiro handled the code implementation.


What I Built

  • Image Upload: Supports JPEG, PNG, GIF, and WebP up to 10MB, with drag-and-drop.
  • Auto-Quantization: Converts images into Perler Bead (100 colors) or Nano Bead (55 colors) palettes.
  • Pegboard Configuration: Customizable grid configurations from 1×1 up to 10×10 boards, featuring an "optimal size recommendation" engine.
  • Advanced Adjustments: Background removal, color limiting/reduction, resizing method selection, and fit-mode options.
  • Manual Editor: Click-and-drag continuous drawing to manually fix individual pixels.
  • Color Inventory: Lists the exact bead count per color and supports PNG pattern exporting.
  • AI Prompt Challenges Using Gemini (Bonus Feature)

Tech Stack

Layer Technology
Language Vanilla JavaScript (ES Modules)
Build Tool Vite
Rendering HTML5 Canvas API
Testing Vitest + fast-check (Property-Based Testing)
Hosting AWS Amplify Hosting

Everything runs entirely on the client side inside the browser!


Architectural Deep Dive & Key Features

1. Nearest Neighbor Color Matching via CIE76 Color Difference

This is the core engine of the pattern generator. It translates every pixel of an uploaded image into the "closest matching color" available in the actual physical bead palette.

Measuring distance using standard Euclidean distance in the RGB space often fails because colors that look completely distinct to human eyes (e.g., deep blue vs. purple) can be mathematically classified as "close". To fix this, the application transforms the RGB values into the CIE Lab color space before calculating the distance, ensuring visually accurate matching.

While I considered CIEDE2000 (which offers higher precision), CIE76 provided practically perfect visual results for a ~100-color palette mapping a few thousand pixels. Plus, the implementation is significantly simpler and much less error-prone.

// RGB → sRGB Linearization → XYZ → Lab (D65 Reference White)
export function rgbToLab(r, g, b) {
  const toLinear = (c) => {
    const s = c / 255;
    return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  };
  const lr = toLinear(r), lg = toLinear(g), lb = toLinear(b);
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.0721750;
  const z = lr * 0.0193339 + lg * 0.1191920 + lb * 0.9503041;
  const f = (t) => t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
  const fx = f(x / 0.95047), fy = f(y), fz = f(z / 1.08883);
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
}

export function deltaE(lab1, lab2) {
  return Math.sqrt(
    (lab1.L - lab2.L) ** 2 + (lab1.a - lab2.a) ** 2 + (lab1.b - lab2.b) ** 2
  );
}

Enter fullscreen mode Exit fullscreen mode

To optimize performance, the Lab values of all official palette colors are cached at application startup using initializePalette. This means during image processing, we only compute the conversion for the incoming image pixels. Mapping thousands of pixels against 100 colors completes instantly.

2. Processing Pipeline Design

Image processing flows inside LocalConversionStrategy using the following precise execution order:

Fit/Resize 
  → Transparent Pixel Detection (alpha < 128 → Unplaced 'null')
  → White Background Blending (128 ≤ alpha < 255 translucent blending)
  → Color Reduction (Only if max color limit is specified)
  → Palette Nearest-Neighbor Matching (CIE76)
  → Background Removal (If toggled ON)

Enter fullscreen mode Exit fullscreen mode

This specific sequence is intentional. If you do not isolate transparent pixels first, transparent regions get filled with arbitrary nearest matches like solid white or black. Furthermore, background removal runs after palette matching because the engine detects backgrounds by matching "which bead color represents the background" within the standardized palette space. Swapping these steps would mix raw pixel color spaces with palette spaces, introducing visual artifacts.

3. Decoupling with the Strategy Pattern

Initially, I wanted to leverage Amazon Bedrock for image-to-pattern processing. The idea was to send images to a multimodal LLM and have it directly return a structured JSON grid of bead color IDs.

However, considering API latency, cost, and strict pixel-perfect precision requirements, I opted to deliver a bulletproof client-side local pipeline first. To keep the Bedrock implementation open as a future enhancement, I abstracted the execution engine using the Strategy Pattern.

// ConversionStrategy.js — Interface Definition
/**
 * @typedef {Object} ConversionStrategy
 * @property {function(HTMLImageElement, ConversionOptions): PatternGrid} convert
 */

Enter fullscreen mode Exit fullscreen mode

Thanks to this decoupling, introducing a BedrockConversionStrategy in the future won't require modifying any of the core UI components or orchestrating code.

LocalConversionStrategy (Current)
  ↓
BedrockConversionStrategy (Future Extension)
  ↓
Alternative Custom Algorithms

Enter fullscreen mode Exit fullscreen mode

4. Replicating the Official Palette

The application includes data structures for 100 Perler Bead colors and 55 Nano Bead colors. While the color names strictly align with official Kawada color charts, exact RGB values are not published. I generated closely approximated RGB definitions based on physical reference charts.

export const PARLER_PALETTE = [
  { id: 'P01', name: 'White',  r: 241, g: 241, b: 241 },
  { id: 'P02', name: 'Cream',  r: 255, g: 246, b: 207 },
  // ... 100 colors
];

Enter fullscreen mode Exit fullscreen mode

Each color is modeled as a flat record { id, name, r, g, b }. If more accurate spectroradiometer measurements become available, I can simply update the RGB fields without breaking the application's underlying structural identifiers.

5. Property-Based Testing

For functions dealing with pure mathematical properties—such as color distance and coordinate mappings—I implemented Property-Based Testing using fast-check.

Unlike traditional unit tests that assert "Given input A, assert output B", property-based testing validates that a given invariant property holds true for an infinite set of randomized inputs. It is incredible at catching edge cases you would easily overlook manually, such as negative indices, out-of-bound coordinates, or zero-alpha pixels.

I defined 23 different mathematical invariants, including the non-negativity and symmetry properties of deltaE, the bounds-guarantees of findClosestColor, and grid dimension constraints.

6. Security Review Based on OWASP Top 10

Once the core features were working, I ran a security review structured around the OWASP Top 10 — the widely used list of the most critical web application security risks. I referenced the 2021 edition, which was the long-standing stable version (a 2025 edition is now emerging, but the top-level categories — access control, cryptographic failures, injection, and so on — are largely consistent across editions, so the review still applies). This is a client-side-only app with no backend, but the AI prompt feature (generating patterns from text via Gemini) means it now handles a user-supplied API key, so this was worth doing carefully.

The main areas I checked:

  • Injection: No innerHTML or eval anywhere in the codebase — all DOM updates go through textContent. The Gemini response is only ever interpreted as numeric palette indices, so there's no path where it gets evaluated as markup or code.
  • Secrets handling: The API key lives in session memory only. A codebase-wide search confirmed there's no write to localStorage, sessionStorage, or cookies. Debug logging that could include the key is gated behind import.meta.env.DEV and never runs in production builds.
  • Dependencies: npm audit reported zero vulnerabilities. Runtime dependencies are effectively zero (everything is a devDependency), which keeps supply-chain risk low.
  • Defense in depth on AI input: Grid-shape handling is intentionally lenient (it reshapes whatever dimensions Gemini returns), but I added row-count and per-row string-length limits before parsing, so an unexpectedly huge response can't blow up memory or parsing time.

Based on the findings, I also re-tuned the security headers — including the Content-Security-Policy — via a customHttp.yml file:

customHeaders:
  - pattern: '**/*'
    headers:
      - key: 'Content-Security-Policy'
        value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://generativelanguage.googleapis.com; object-src 'none'; frame-ancestors 'none'"
Enter fullscreen mode Exit fullscreen mode

The policy is tuned to how the app actually behaves: connect-src has to explicitly allow-list the Gemini endpoint, or the AI feature would silently break in production, and style-src needs unsafe-inline because color swatches are rendered via inline style assignments rather than CSS classes.


Development Experience with Kiro

When you explain an idea to Kiro, it translates your vision into requirements.md, followed by design.md, and finally tasks.md. You review and approve the documents at each milestone before any implementation begins.

For this project, we solidified 13 concrete requirements in requirements.md and settled the entire color space conversion strategy and processing pipeline hierarchy inside design.md. Because the constraints were ironed out beforehand, the AI didn't encounter any mid-development dead-ends.

The generated tasks.md listed items divided into architectural waves based on dependency trees. Kiro can run independent tasks in parallel, automatically writing implementation code and driving the project forward until all 264 unit and property tests successfully passed.

The primary difference between Kiro and standard conversational chat AI models is that architectural intent remains perfectly documented. If you ever look at a function down the road and wonder why it was constructed a certain way, you can just open design.md to see the exact reasoning behind it.


Deploying to AWS Amplify Hosting

Monorepo Workaround

The project repository uses a monorepo setup where the web application lives inside a subfolder named bead-pattern-maker/. To make this work seamlessly on AWS Amplify, I placed a custom amplify.yml at the root of the repository to specify the appRoot:

version: 1
applications:
  - appRoot: bead-pattern-maker
    frontend:
      phases:
        preBuild:
          commands:
            - npm ci
        build:
          commands:
            - npm run build
      artifacts:
        baseDirectory: dist
        files:
          - '**/*'
      cache:
        paths:
          - node_modules/**/*

Enter fullscreen mode Exit fullscreen mode

Note: Although the AWS Amplify Console features a checkbox for "My app is a monorepo," if you already explicitly provide an appRoot inside your custom amplify.yml, you do not need to toggle it on. Amplify discovers and parses it automatically.

Configuring Security Headers

I wanted proper security headers even for a static deployment, so I set up CSP, HSTS, X-Frame-Options, and friends. I initially configured them through the Amplify Console's custom-headers UI, but since console settings don't live in the repository, I moved them into a customHttp.yml file at the repo root instead.

For a monorepo, the file-based approach works too — you just use the appRoot-scoped format. The customHttp.yml lives at the repository root, and its headers take precedence over anything set in the console. With this in place, the headers apply from the very first deploy without ever touching the console.

applications:
  - appRoot: bead-pattern-maker
    customHeaders:
      - pattern: '**/*'
        headers:
          - key: 'Content-Security-Policy'
            value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://generativelanguage.googleapis.com; object-src 'none'; frame-ancestors 'none'"
          # plus Strict-Transport-Security / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy
Enter fullscreen mode Exit fullscreen mode

The CSP is tuned to how the app actually behaves. In particular, connect-src has to explicitly allow-list the Gemini API endpoint (generativelanguage.googleapis.com), or the AI prompt feature silently breaks in production. The app still scores a perfect A+ on securityheaders.com!


Key Takeaways

  1. Lab space calculations are vastly superior to RGB for human vision. Using direct RGB calculations often leads to awkward mismatches, whereas shifting to CIE76 instantly aligns colors with human perception.
  2. Lock down your data pipeline early. Deciding the sequence of transformations (especially executing background isolation after palette mapping) saved a massive amount of refactoring effort later on.
  3. Use the Strategy Pattern for future-proofing. It allows you to build a reliable local processing engine right now while leaving the doors wide open for seamless integration with heavy-duty LLM microservices (like Amazon Bedrock) later.
  4. Amplify monorepos are effortless with amplify.yml. Explicitly setting the appRoot keeps console configurations clean (just remember it only respects the .yml file extension). Keep security headers in a separate customHttp.yml (also at the repo root, with the same appRoot format) so everything stays in the repository without touching the console.
  5. Spec-Driven Development reduces engineering friction. Having a permanent architectural reference point (requirements.md / design.md) to look back on keeps both human developers and generative AI engines completely aligned throughout a project.

Disclaimer

This application is an unofficial, personal fan-made project. It is entirely unaffiliated with Kawada Co., Ltd. or any of its subsidiaries. "Perler Beads" and "Nano Beads" are registered trademarks of Kawada Co., Ltd. Please do not contact official support lines regarding this application.

All other company names, product names, and logos are trademarks or registered trademarks of their respective owners.

Reference Links

Top comments (0)