こんにちは。
Kiro と Claude Opus だけで、画像からアイロンビーズの図案を生成する Web アプリを作りました。アイロンビーズ(パーラービーズ)の図案作りが面倒で、自分で作ることにしたやつです。
デモ: https://main.d3jfjapbrozdy8.amplifyapp.com/
コード: https://github.com/hiro-sys/iron-beads-designer
開発環境
Kiro + Claude Opus 4.8
Kiro は AWS が提供する AI 搭載 IDE です。今回は Anthropic の Claude Opus 4.8 をバックエンドに使いました。
Spec 駆動開発という仕組みがあって、要件定義(requirements.md)→ 技術設計(design.md)→ タスク一覧(tasks.md)の順に文書を固めながら開発を進められます。「何を作るか」が先に確定しているので、実装中に仕様が揺れるということが起きにくいです。
今回も自分でアイデアを出して相談し、Kiro がコードを書く、といういつもどおりのスタイルで進めました。
作ったもの
- 画像アップロード(JPEG/PNG/GIF/WebP、最大 10MB、ドラッグ&ドロップ対応)
- パーラービーズ(100色)/ ナノビーズ(55色)への自動変換
- プレート構成の指定(1×1 〜 10×10)とおすすめサイズ提案
- 背景除外、使用色の制限・減色、リサイズ方式とフィットモードの選択
- 図案の手動編集(ドラッグによる連続塗り対応)
- 使用色一覧の表示と PNG エクスポート
- Gemini を使ったAIお題(おまけ機能)
- i18n 対応
技術スタック:
| 領域 | 内容 |
|---|---|
| 言語 | Vanilla JavaScript(ES モジュール) |
| ビルド | Vite |
| レンダリング | HTML5 Canvas API |
| テスト | Vitest + fast-check(プロパティベーステスト) |
| ホスティング | AWS Amplify Hosting |
すべてブラウザで完結します。
実装でこだわったところ
CIE76 色差計算による最近傍色マッチング
図案生成の核になる部分です。各ピクセルを「パレットの中で一番近い色」に変換します。
RGB のままユークリッド距離を測ると、人間の目には違う色に見えるのに「近い」と判定されやすいケースが出ます(濃い青と紫など)。CIE Lab 色空間に変換してから距離を計算することで、視覚的に近い色へのマッチングになります。
CIE76 より精度の高い CIEDE2000 も検討しましたが、100色程度のパレットで数千ピクセルを変換する用途では、CIE76 でも目で見て十分実用的な結果が得られ、実装がシンプルな分バグも潜りにくいので採用しました。
// RGB → sRGB リニア化 → XYZ → Lab(D65 白色点基準)
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
);
}
Lab 値は起動時に initializePalette で全色分キャッシュします。変換時に毎回計算するのは対象ピクセル側だけになるので、100色 × 数千ピクセルでも十分実用的な速度です。
変換パイプラインの設計
変換処理は LocalConversionStrategy の中でこの順序で流れます。
フィット/リサイズ
→ 透明ピクセル判定(alpha < 128 → 未配置 null)
→ 白背景合成(128 ≤ alpha < 255 の半透明)
→ 減色(最大色数指定時のみ)
→ パレット最近傍色マッチング(CIE76)
→ 背景除外(ON の場合)
この順序にはいくつか理由があります。透明ピクセルの判定を最初に行わないと、透明部分が白や黒に変換されます。背景除外をパレットマッチングの後に置いているのは、「どのビーズ色が背景に近いか」をパレット色空間で判定するためです。逆にすると生ピクセル色とパレット色の色空間が混在してズレが起きます。
Strategy パターンで変換エンジンを差し替え可能にした
当初、Amazon Bedrock による変換も検討していました。画像をマルチモーダルで投げ、AI に直接「ビーズ色のグリッド」を JSON で出力させようとしました。
API コストやレスポンス速度、1 マス単位の精度の観点から、まずは確実に動くローカルパイプラインで完成させることにしました。Bedrock の利用は「将来の課題」として、変換エンジンを Strategy パターンで抽象化しています。
// ConversionStrategy.js — インターフェース定義
/**
* @typedef {Object} ConversionStrategy
* @property {function(HTMLImageElement, ConversionOptions): PatternGrid} convert
*/
将来 BedrockConversionStrategy を追加しても、呼び出し元のコードは変えなくていい構造です。
LocalConversionStrategy(現在)
↓
BedrockConversionStrategy(将来)
↓
その他のアルゴリズム
カラーパレット 100色 / 55色
パーラービーズは 100色、ナノビーズは 55色を定義しています。カワダの公式カラーリストに色名を準拠させていますが、RGB 値は数値で公開されていないため、カラーチャートの見た目から近似値を作っています。
export const PARLER_PALETTE = [
{ id: 'P01', name: 'しろ', r: 241, g: 241, b: 241 },
{ id: 'P02', name: 'クリーム', r: 255, g: 246, b: 207 },
// ... 100色
];
各色は { id, name, r, g, b } の独立したレコードです。将来 RGB 値だけ実測値に差し替えても、id と色名はそのまま維持できる構造にしています。Lab 値はここには書かず、実行時に initializePalette で付与します。
プロパティベーステスト
色差計算や座標変換など「数学的な性質を持つ関数」は、fast-check でプロパティテストを書きました。通常のテストが「この入力に対してこの出力」を検証するのに対し、プロパティテストは「任意の入力に対してこの性質が常に成り立つ」を検証します。手書きのテストケースでは気づきにくいエッジケース(座標が負のケース、alpha=0 のピクセルなど)も自動的に試してくれます。
deltaE の非負性・対称性、findClosestColor の最小保証、グリッドサイズの計算など、23 個のプロパティを定義しています。
セキュリティチェック(OWASP Top 10 ベース)
一通り動くようになった後、OWASP Top 10(Web アプリの代表的なセキュリティリスク集)を軸にセキュリティレビューも行いました。参照したのは長らく最新の安定版だった 2021 年版です(2025 年版も登場しつつありますが、アクセス制御・暗号化の不備・インジェクションといった大分類は概ね共通なので、レビューの観点はそのまま通用します)。バックエンドを持たないクライアントサイド完結のアプリですが、後から追加した「Gemini でお題からドット絵を生成する」機能で利用者自身の API キーを扱うようになったため、ここは丁寧に見ておきたいポイントでした。
主な確認観点は次のとおりです。
-
インジェクション:
innerHTMLやevalは一切使わず、DOM 操作はすべてtextContentで統一。Gemini の応答も「色インデックスの数値」としてのみ解釈し、コードやマークアップとして評価される経路が無いことをコード全体で確認 -
機密情報の扱い: API キーはセッションメモリにのみ保持し、
localStorage/sessionStorage/ Cookie への書き込みが無いことを全ファイル検索で確認。デバッグログはimport.meta.env.DEVで分岐させ、本番ビルドでは API キーを含みうる出力を一切行わない -
依存パッケージ:
npm auditで脆弱性 0 件を確認。ランタイム依存はゼロ(すべて devDependencies)なのでサプライチェーンのリスクも小さい - AI 応答に対する多層防御: グリッドの寸法は多少ズレても寛容に整形する設計にしている一方で、行数・1行あたりの文字列長にも上限チェックを追加し、AI が想定外に巨大な応答を返した場合の防御を入れた
指摘事項を踏まえて、customHttp.yml で CSP(Content-Security-Policy)などのセキュリティヘッダーも設定し直しました。
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'"
connect-src に Gemini のエンドポイントを明示的に許可しないとAI機能が本番で動かなくなる点や、色見本表示でインラインスタイルを多用しているため style-src に unsafe-inline が要る点など、実装の実態に合わせて内容を調整しています。
多言語対応(i18n): 実行時にロケールを判定して英語へフォールバック
GitHub で公開するにあたり、日本語話者以外にも使ってもらえるよう UI の多言語対応(英語フォールバック)を追加しました。方針はシンプルで、ビルド時ではなく実行時に navigator.language を見てロケールを判定するというものです。
-
navigator.languages(無ければnavigator.language)の先頭が"ja"で始まれば日本語(ja) - それ以外・取得不能・
navigatorが無い環境(SSR/テスト)はすべて英語(en)にフォールバック
const DEFAULT_LOCALE = 'en';
export function detectLocale() {
if (typeof navigator === 'undefined') return DEFAULT_LOCALE;
const raw =
(Array.isArray(navigator.languages) && navigator.languages[0]) ||
navigator.language || '';
return String(raw).toLowerCase().startsWith('ja') ? 'ja' : DEFAULT_LOCALE;
}
文言は「コンポーネント名.項目名」をキーにした辞書に集約し、t('beadType.legend') のように引きます。{count} のようなプレースホルダは第2引数で差し込みます。辞書に無いキーはキー文字列そのものを返すので、未翻訳箇所を開発時に見つけやすくしています。
色名は「日本語を唯一の出典」にする
ここが設計上いちばん気をつけたところです。カラーパレットは日本語名(カワダ公式準拠)を内部キーの唯一の出典とし、英語表示のための nameEn(参考訳)は追加するだけにしました。id・name・RGB 値は一切変更していません。
export const PARLER_PALETTE = [
{ id: 'P01', name: 'しろ', nameEn: 'White', r: 241, g: 241, b: 241 },
// ...
];
こうすることで、色マッチング(CIE76)や AI お題生成が使う内部ロジックは日本語名(=不変のキー)のまま動き、表示だけ getColorName() でロケールに応じて出し分けられます。表示専用の追加なので、既存の変換ロジックへの影響はゼロです。
AI エラーメッセージはキーで受け渡す
後から入れた Gemini お題生成のエラー表示も i18n 化しました。ポイントは、エラー分類の関数が「日本語の文字列」ではなく「辞書キー」を返すようにしたことです。
// エラー種別 → 辞書キー(表示文言そのものは返さない)
getAiErrorKey(error); // => 'aiError.auth' など
// 表示側でロケールに応じて解決する
showMessage(t(getAiErrorKey(error)), 'error');
これでエラーの「分類」と「表示文言・言語」が分離され、翻訳の追加やロケール切替が表示側だけで完結します。
テストでは、getAiErrorKey が返しうる全キーが辞書に実在することを ja/en 双方でクロスチェックしています。将来エラー種別を足して辞書キーを足し忘れると、このテストが落ちて気づけます。
Kiro を使った開発体験
Spec 駆動開発の流れ
「何を作りたいか」を話すと、Kiro が requirements.md → design.md → tasks.md の順に文書を作ります。各フェーズでレビューして確認してから次へ進む流れです。
今回は requirements.md で 13 要件を確定させ、design.md でパイプラインの順序や色空間整合性の設計を固めました。「背景除外はパレットマッチングの後」という実装の制約も、設計段階で決まっていたので実装中に迷うことがなかったようです。
tasks.md には依存関係に基づいて Wave に分割されたタスクが並んでいます。独立したものは並列実行してくれます。今回は合計 264 テストが全パスする状態まで自動実装が進みました。
通常のチャット型 AI との違いは、「なぜそう設計するか」が文書として残る点だと思います。実装を進めながら「この関数はなんのためにあるんだっけ」となったときに、design.md を見れば理由が書いてあります。
AWS Amplify へのデプロイ
モノレポ構成での落とし穴
リポジトリルートに bead-pattern-maker/ というサブフォルダがあるモノレポ構成です。ルートに amplify.yml を置いて 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/**/*
Amplify コンソールには「私のアプリはモノレポです」というチェックボックスがありますが、amplify.yml に appRoot が書いてある場合はチェック不要で、自動で読み込まれます。
セキュリティヘッダー
静的サイトでもセキュリティヘッダーはちゃんと設定したかったので、CSP・HSTS・X-Frame-Options などをまとめて設定しました。当初は Amplify コンソールのカスタムヘッダー画面から設定していましたが、コンソール設定はリポジトリに残らないため、リポジトリのルートに customHttp.yml を置く方式へ移行しました。
モノレポの場合は、コンソールと同じ「ファイルに書く」方式でも appRoot を指定した専用フォーマットを使えばきちんと適用されます(customHttp.yml はリポジトリのルートに置き、コンソール側の設定より優先されます)。この方式なら、コンソールを一度も触らなくても初回デプロイからヘッダーが効きます。
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'"
# 他に Strict-Transport-Security / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy も設定
CSP はアプリの実装に合わせて調整しています。とくに connect-src に Gemini API のエンドポイント(generativelanguage.googleapis.com)を明示的に許可しておかないと、AI お題生成機能が本番環境で動かなくなってしまう点は要注意でした。ちなみに、securityheaders.com で A+ 評価でした。
学び
- Lab 色空間での距離計算は体感で違いがわかる。RGB のままだと「なんか色がズレる」が起きやすく、CIE76 に切り替えると整う
- 変換パイプラインの順序は設計段階で決めておくと楽。特に「背景除外はパレットマッチングの後」という制約は、後から変えると影響範囲が広い
- Strategy パターンは「将来の拡張」を保留するのに便利。Bedrock 統合を先送りしつつ、インターフェースだけ定義しておける
-
AWS Amplify のモノレポ対応は
amplify.ymlで完結する。appRootを書いておけばコンソールの設定は最小限でいい(ただし拡張子は.ymlのみ)。セキュリティヘッダーはビルド設定とは別のcustomHttp.yml(同じくルートに配置・appRoot指定)に分離すると、コンソールを触らずリポジトリ側で完結できる - Spec 駆動開発は、設計上の判断が文書として残る。「なぜこの順序か」「なぜこのアーキテクチャか」が requirements.md / design.md に書かれていて、実装中に戻れる場所がある。チャット型の AI 開発とはここが一番異なる
免責事項
本アプリはカワダ株式会社および関係各社とは一切関係のない、個人制作の非公式ファンメイドツールです。「パーラービーズ」「ナノビーズ」はカワダ株式会社の登録商標です。本アプリについての問い合わせをカワダ公式窓口へ行うことは絶対にしないでください。
記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。
