<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: hirosys</title>
    <description>The latest articles on DEV Community by hirosys (@hirosys).</description>
    <link>https://dev.to/hirosys</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1171891%2Ffa36e96a-976d-4eb6-840c-0fdcfc889c23.png</url>
      <title>DEV Community: hirosys</title>
      <link>https://dev.to/hirosys</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hirosys"/>
    <language>en</language>
    <item>
      <title>I Built a Fuse Bead Pattern Generator Using Kiro and Claude Opus, Deployed on AWS Amplify</title>
      <dc:creator>hirosys</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:27:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-built-a-fuse-bead-pattern-generator-using-kiro-and-claude-opus-deployed-on-aws-amplify-1p9o</link>
      <guid>https://dev.to/aws-builders/i-built-a-fuse-bead-pattern-generator-using-kiro-and-claude-opus-deployed-on-aws-amplify-1p9o</guid>
      <description>&lt;p&gt;Hello everyone! &lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://main.d3jfjapbrozdy8.amplifyapp.com/" rel="noopener noreferrer"&gt;https://main.d3jfjapbrozdy8.amplifyapp.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/hiro-sys/iron-beads-designer" rel="noopener noreferrer"&gt;https://github.com/hiro-sys/iron-beads-designer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fpnoqu9pc3vbta86q8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fpnoqu9pc3vbta86q8b.png" alt="Application Screenshot" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Environment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Kiro + Claude Opus 4.8
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; is an AI-powered IDE provided by AWS. For this project, I used Anthropic's Claude Opus 4.8 as the backend.&lt;/p&gt;

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

&lt;p&gt;My workflow was the same as usual: I brainstormed ideas and discussed them with Kiro, and Kiro handled the code implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vanilla JavaScript (ES Modules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build Tool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rendering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML5 Canvas API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vitest + fast-check (Property-Based Testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS Amplify Hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything runs entirely on the client side inside the browser!&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural Deep Dive &amp;amp; Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Nearest Neighbor Color Matching via CIE76 Color Difference
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Measuring distance using standard Euclidean distance in the &lt;strong&gt;RGB&lt;/strong&gt; 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 &lt;strong&gt;CIE Lab color space&lt;/strong&gt; before calculating the distance, ensuring visually accurate matching.&lt;/p&gt;

&lt;p&gt;While I considered &lt;strong&gt;CIEDE2000&lt;/strong&gt; (which offers higher precision), &lt;strong&gt;CIE76&lt;/strong&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RGB → sRGB Linearization → XYZ → Lab (D65 Reference White)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rgbToLab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toLinear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.04045&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.92&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toLinear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toLinear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toLinear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.4124564&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3575761&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1804375&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2126729&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.7151522&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0721750&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0193339&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1191920&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9503041&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.008856&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cbrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.787&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;116&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.95047&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;fz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.08883&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;L&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;116&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deltaE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  2. Processing Pipeline Design
&lt;/h3&gt;

&lt;p&gt;Image processing flows inside &lt;code&gt;LocalConversionStrategy&lt;/code&gt; using the following precise execution order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fit/Resize 
  → Transparent Pixel Detection (alpha &amp;lt; 128 → Unplaced 'null')
  → White Background Blending (128 ≤ alpha &amp;lt; 255 translucent blending)
  → Color Reduction (Only if max color limit is specified)
  → Palette Nearest-Neighbor Matching (CIE76)
  → Background Removal (If toggled ON)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;em&gt;after&lt;/em&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Decoupling with the Strategy Pattern
&lt;/h3&gt;

&lt;p&gt;Initially, I wanted to leverage &lt;strong&gt;Amazon Bedrock&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;Strategy Pattern&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ConversionStrategy.js — Interface Definition&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * @typedef {Object} ConversionStrategy
 * @property {function(HTMLImageElement, ConversionOptions): PatternGrid} convert
 */&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks to this decoupling, introducing a &lt;code&gt;BedrockConversionStrategy&lt;/code&gt; in the future won't require modifying any of the core UI components or orchestrating code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LocalConversionStrategy (Current)
  ↓
BedrockConversionStrategy (Future Extension)
  ↓
Alternative Custom Algorithms

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Replicating the Official Palette
&lt;/h3&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARLER_PALETTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;White&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;241&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;241&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;241&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P02&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;246&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;207&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 100 colors&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  5. Property-Based Testing
&lt;/h3&gt;

&lt;p&gt;For functions dealing with pure mathematical properties—such as color distance and coordinate mappings—I implemented &lt;strong&gt;Property-Based Testing&lt;/strong&gt; using &lt;code&gt;fast-check&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;I defined 23 different mathematical invariants, including the non-negativity and symmetry properties of &lt;code&gt;deltaE&lt;/code&gt;, the bounds-guarantees of &lt;code&gt;findClosestColor&lt;/code&gt;, and grid dimension constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Security Review Based on OWASP Top 10
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The main areas I checked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Injection&lt;/strong&gt;: No &lt;code&gt;innerHTML&lt;/code&gt; or &lt;code&gt;eval&lt;/code&gt; anywhere in the codebase — all DOM updates go through &lt;code&gt;textContent&lt;/code&gt;. The Gemini response is only ever interpreted as numeric palette indices, so there's no path where it gets evaluated as markup or code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets handling&lt;/strong&gt;: The API key lives in session memory only. A codebase-wide search confirmed there's no write to &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, or cookies. Debug logging that could include the key is gated behind &lt;code&gt;import.meta.env.DEV&lt;/code&gt; and never runs in production builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies&lt;/strong&gt;: &lt;code&gt;npm audit&lt;/code&gt; reported zero vulnerabilities. Runtime dependencies are effectively zero (everything is a devDependency), which keeps supply-chain risk low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defense in depth on AI input&lt;/strong&gt;: 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Based on the findings, I also re-tuned the security headers — including the Content-Security-Policy — via a &lt;code&gt;customHttp.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;customHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*'&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Security-Policy'&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;script-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;style-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'unsafe-inline';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;img-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data:;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;connect-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://generativelanguage.googleapis.com;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;object-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'none';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;frame-ancestors&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'none'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  Development Experience with Kiro
&lt;/h2&gt;

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

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

&lt;p&gt;The generated &lt;code&gt;tasks.md&lt;/code&gt; 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.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Deploying to AWS Amplify Hosting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Monorepo Workaround
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;applications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bead-pattern-maker&lt;/span&gt;
    &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;preBuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;baseDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*'&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/**/*&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Configuring Security Headers
&lt;/h3&gt;

&lt;p&gt;I wanted proper security headers even for a static deployment, so I set up CSP, HSTS, &lt;code&gt;X-Frame-Options&lt;/code&gt;, 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 &lt;code&gt;customHttp.yml&lt;/code&gt; file at the repo root instead.&lt;/p&gt;

&lt;p&gt;For a monorepo, the file-based approach works too — you just use the &lt;code&gt;appRoot&lt;/code&gt;-scoped format. The &lt;code&gt;customHttp.yml&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;applications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bead-pattern-maker&lt;/span&gt;
    &lt;span class="na"&gt;customHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*'&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Security-Policy'&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;script-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;style-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'unsafe-inline';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;img-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data:;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;connect-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://generativelanguage.googleapis.com;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;object-src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'none';&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;frame-ancestors&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'none'"&lt;/span&gt;
          &lt;span class="c1"&gt;# plus Strict-Transport-Security / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSP is tuned to how the app actually behaves. In particular, &lt;code&gt;connect-src&lt;/code&gt; has to explicitly allow-list the Gemini API endpoint (&lt;code&gt;generativelanguage.googleapis.com&lt;/code&gt;), or the AI prompt feature silently breaks in production. The app still scores a perfect &lt;strong&gt;A+&lt;/strong&gt; on &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;securityheaders.com&lt;/a&gt;!&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lab space calculations are vastly superior to RGB for human vision.&lt;/strong&gt; Using direct RGB calculations often leads to awkward mismatches, whereas shifting to CIE76 instantly aligns colors with human perception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock down your data pipeline early.&lt;/strong&gt; Deciding the sequence of transformations (especially executing background isolation &lt;em&gt;after&lt;/em&gt; palette mapping) saved a massive amount of refactoring effort later on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Strategy Pattern for future-proofing.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amplify monorepos are effortless with &lt;code&gt;amplify.yml&lt;/code&gt;.&lt;/strong&gt; Explicitly setting the &lt;code&gt;appRoot&lt;/code&gt; keeps console configurations clean (just remember it only respects the &lt;code&gt;.yml&lt;/code&gt; file extension). Keep security headers in a separate &lt;code&gt;customHttp.yml&lt;/code&gt; (also at the repo root, with the same &lt;code&gt;appRoot&lt;/code&gt; format) so everything stays in the repository without touching the console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec-Driven Development reduces engineering friction.&lt;/strong&gt; Having a permanent architectural reference point (&lt;code&gt;requirements.md&lt;/code&gt; / &lt;code&gt;design.md&lt;/code&gt;) to look back on keeps both human developers and generative AI engines completely aligned throughout a project.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;All other company names, product names, and logos are trademarks or registered trademarks of their respective owners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro - AI-powered IDE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/claude" rel="noopener noreferrer"&gt;Anthropic Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kawada-toys.com/brand/perlerbeads/colorlist/" rel="noopener noreferrer"&gt;Kawada Perler Beads Official Catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fast-check.dev/" rel="noopener noreferrer"&gt;fast-check - Property Based Testing Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/amplify/hosting/" rel="noopener noreferrer"&gt;AWS Amplify Hosting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>amplify</category>
      <category>kiro</category>
      <category>claude</category>
    </item>
    <item>
      <title>I Built a Fuse Bead Pattern Generator Using Kiro and Claude Opus, Deployed on AWS Amplify</title>
      <dc:creator>hirosys</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:27:06 +0000</pubDate>
      <link>https://dev.to/hirosys/i-built-a-fuse-bead-pattern-generator-using-kiro-and-claude-opus-deployed-on-aws-amplify-474n</link>
      <guid>https://dev.to/hirosys/i-built-a-fuse-bead-pattern-generator-using-kiro-and-claude-opus-deployed-on-aws-amplify-474n</guid>
      <description>&lt;p&gt;Hello everyone! &lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://main.d3jfjapbrozdy8.amplifyapp.com/" rel="noopener noreferrer"&gt;https://main.d3jfjapbrozdy8.amplifyapp.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/hiro-sys/iron-beads-designer" rel="noopener noreferrer"&gt;https://github.com/hiro-sys/iron-beads-designer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fpnoqu9pc3vbta86q8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fpnoqu9pc3vbta86q8b.png" alt="Application Screenshot" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Environment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Kiro + Claude Opus 4.8
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; is an AI-powered IDE provided by AWS. For this project, I used Anthropic's Claude Opus 4.8 as the backend.&lt;/p&gt;

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

&lt;p&gt;My workflow was the same as usual: I brainstormed ideas and discussed them with Kiro, and Kiro handled the code implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vanilla JavaScript (ES Modules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build Tool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rendering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML5 Canvas API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vitest + fast-check (Property-Based Testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS Amplify Hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything runs entirely on the client side inside the browser!&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural Deep Dive &amp;amp; Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Nearest Neighbor Color Matching via CIE76 Color Difference
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Measuring distance using standard Euclidean distance in the &lt;strong&gt;RGB&lt;/strong&gt; 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 &lt;strong&gt;CIE Lab color space&lt;/strong&gt; before calculating the distance, ensuring visually accurate matching.&lt;/p&gt;

&lt;p&gt;While I considered &lt;strong&gt;CIEDE2000&lt;/strong&gt; (which offers higher precision), &lt;strong&gt;CIE76&lt;/strong&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RGB → sRGB Linearization → XYZ → Lab (D65 Reference White)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rgbToLab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toLinear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.04045&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.92&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toLinear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toLinear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toLinear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.4124564&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3575761&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1804375&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2126729&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.7151522&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0721750&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0193339&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1191920&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9503041&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.008856&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cbrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.787&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;116&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.95047&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;fz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.08883&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;L&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;116&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deltaE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lab1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lab2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  2. Processing Pipeline Design
&lt;/h3&gt;

&lt;p&gt;Image processing flows inside &lt;code&gt;LocalConversionStrategy&lt;/code&gt; using the following precise execution order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fit/Resize 
  → Transparent Pixel Detection (alpha &amp;lt; 128 → Unplaced 'null')
  → White Background Blending (128 ≤ alpha &amp;lt; 255 translucent blending)
  → Color Reduction (Only if max color limit is specified)
  → Palette Nearest-Neighbor Matching (CIE76)
  → Background Removal (If toggled ON)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;em&gt;after&lt;/em&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Decoupling with the Strategy Pattern
&lt;/h3&gt;

&lt;p&gt;Initially, I wanted to leverage &lt;strong&gt;Amazon Bedrock&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;Strategy Pattern&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ConversionStrategy.js — Interface Definition&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * @typedef {Object} ConversionStrategy
 * @property {function(HTMLImageElement, ConversionOptions): PatternGrid} convert
 */&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks to this decoupling, introducing a &lt;code&gt;BedrockConversionStrategy&lt;/code&gt; in the future won't require modifying any of the core UI components or orchestrating code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LocalConversionStrategy (Current)
  ↓
BedrockConversionStrategy (Future Extension)
  ↓
Alternative Custom Algorithms

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Replicating the Official Palette
&lt;/h3&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARLER_PALETTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;White&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;241&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;241&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;241&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P02&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;246&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;207&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 100 colors&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  5. Property-Based Testing
&lt;/h3&gt;

&lt;p&gt;For functions dealing with pure mathematical properties—such as color distance and coordinate mappings—I implemented &lt;strong&gt;Property-Based Testing&lt;/strong&gt; using &lt;code&gt;fast-check&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;I defined 23 different mathematical invariants, including the non-negativity and symmetry properties of &lt;code&gt;deltaE&lt;/code&gt;, the bounds-guarantees of &lt;code&gt;findClosestColor&lt;/code&gt;, and grid dimension constraints.&lt;/p&gt;




&lt;h2&gt;
  
  
  Development Experience with Kiro
&lt;/h2&gt;

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

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

&lt;p&gt;The generated &lt;code&gt;tasks.md&lt;/code&gt; 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.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Deploying to AWS Amplify Hosting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Monorepo Workaround
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;applications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bead-pattern-maker&lt;/span&gt;
    &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;preBuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;baseDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*'&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/**/*&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Configuring Security Headers
&lt;/h3&gt;

&lt;p&gt;To ensure enterprise-grade security for this static deployment, I configured custom security headers directly in the Amplify Console. This includes tuning Content Security Policy (CSP), HTTP Strict Transport Security (HSTS), and &lt;code&gt;X-Frame-Options&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keep in mind that within monorepo setups, headers defined inside text configurations like &lt;code&gt;customHttp.yml&lt;/code&gt; might occasionally be skipped depending on your build override parameters. Applying them via the Amplify Console UI is the most robust method. The app currently scores a perfect &lt;strong&gt;A+&lt;/strong&gt; ranking on &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;securityheaders.com&lt;/a&gt;!&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lab space calculations are vastly superior to RGB for human vision.&lt;/strong&gt; Using direct RGB calculations often leads to awkward mismatches, whereas shifting to CIE76 instantly aligns colors with human perception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock down your data pipeline early.&lt;/strong&gt; Deciding the sequence of transformations (especially executing background isolation &lt;em&gt;after&lt;/em&gt; palette mapping) saved a massive amount of refactoring effort later on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Strategy Pattern for future-proofing.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amplify monorepos are effortless with &lt;code&gt;amplify.yml&lt;/code&gt;.&lt;/strong&gt; Explicitly setting the &lt;code&gt;appRoot&lt;/code&gt; keeps console configurations clean. Just remember that it only respects the &lt;code&gt;.yml&lt;/code&gt; file extension!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec-Driven Development reduces engineering friction.&lt;/strong&gt; Having a permanent architectural reference point (&lt;code&gt;requirements.md&lt;/code&gt; / &lt;code&gt;design.md&lt;/code&gt;) to look back on keeps both human developers and generative AI engines completely aligned throughout a project.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;All other company names, product names, and logos are trademarks or registered trademarks of their respective owners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro - AI-powered IDE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/claude" rel="noopener noreferrer"&gt;Anthropic Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kawada-toys.com/brand/perlerbeads/colorlist/" rel="noopener noreferrer"&gt;Kawada Perler Beads Official Catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fast-check.dev/" rel="noopener noreferrer"&gt;fast-check - Property Based Testing Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/amplify/hosting/" rel="noopener noreferrer"&gt;AWS Amplify Hosting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>amplify</category>
      <category>kiro</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
