猫の手ツール

Geometry & UX Logic

「比率の自動補正」と「Canvasレイアウトエンジン」の設計

2枚の画像を単にくっつけるだけなら、CSSのFlexboxで十分です。しかし、ダウンロード可能な1枚の「作品」として出力するためには、Canvasを用いた厳密な幾何学計算が必要になります。

このツールの入力から出力まで

現行コードの確認日:

入力
比較したい2枚を別々の欄へ読み込み、横・縦の配置と境界線の有無を選びます。
処理
横なら高さ、縦なら幅を合わせます。2枚と境界線を含む全体寸法を計算してから、長辺3000px・900万画素以内へ縮小します。
出力
2枚の画像を横または縦に並べ、JPEG(品質指定 0.9)で保存します。

まず確認する点:片方を拡大することもあるため、低解像度画像の粗さを確認します。位置合わせは写真の比率を揃える工程で、被写体の自動追跡ではありません。

仕上がりの例

猫の写真2枚の高さをそろえ、白い境界線を付けて横につないだ例です。

加工前・加工後
左は縦長の猫写真と愛猫2匹が並ぶ横長の写真。右は2枚を同じ高さで横に連結した画像。

加工前・加工後

左は縦長の猫写真と愛猫2匹が並ぶ横長の写真。右は2枚を同じ高さで横に連結した画像。

掲載画像はサイトにある既存の公開作例です。保存済みの資料では運営者の愛猫の写真を使用しています。今回の確認日が、この作例を実行した日を表すわけではありません。

保存済み検証記録の条件を見る

資料の種類は「ツールの出力画像を保存した記録」です。公開作例と保存済み記録で素材や色などが異なる場合があるため、下の数値は保存済み検証記録の条件として読んでください。

記録の入力画像
800×1222px (JPEG)、800×667px (JPEG)
記録の出力画像
2277×1222px (JPEG)
保存済み記録の主な操作・出力条件
画面の項目記録の値現行コードの初期値・範囲
記録の配置方向横並び—
記録の境界線あり—

資料ファイルの作成日は実行日として扱っていません。個別端末の処理速度や、ほかの写真に対する検出・加工の精度を測定した資料でもありません。

実装の確認箇所:src/pages/image/combine/index.astro。以下のコード抜粋と出力条件は、この本番実装に基づいて説明します。

仕組みを読み進めるときは、Canvas APIと画像処理(画像寸法、drawImage、画素と透明度の扱い)、File APIとObject URL(選んだ画像の読み込みとBlob URLの解放)が参考になります。

1. 基準軸によるアスペクト比維持アルゴリズム

ユーザーがアップロードする写真は、サイズもアスペクト比もバラバラです。本ツールでは、横並びの場合は「高さ」を、縦並びの場合は「幅」を共通の基準軸(Master Axis)として固定し、もう一方の辺を比率計算によって導き出しています。

メリット: 片方の画像を無理に引き伸ばしたり、歪ませたりすることなく、2枚をピタッと揃えることができます。縦横比を保ったまま比較用の画像を作れます。被写体の位置や大きさを一致させるには、元画像側で構図をそろえる必要があります。

横並びのレイアウトを求める抜粋です。2枚目の比率を保った後、境界線を含む全体を制限内へ縮小します。boundedDimensionsは共通ヘルパーで、長辺と総画素数から寸法を制限します。

function horizontalSize(firstWidth, firstHeight, secondWidth, secondHeight, divider = 0) {
  const w2 = secondWidth * firstHeight / secondHeight;
  const totalWidth = firstWidth + w2 + divider;
  const totalHeight = firstHeight;
  const output = boundedDimensions(totalWidth, totalHeight, 3000, 9_000_000);
  const sx = output.width / totalWidth;
  const sy = output.height / totalHeight;
  return {
    width: output.width, height: output.height,
    first: { x: 0, y: 0, width: firstWidth * sx, height: firstHeight * sy },
    second: { x: (firstWidth + divider) * sx, y: 0, width: w2 * sx, height: firstHeight * sy }
  };
}

2. 解像度ガードレールとダウンサンプリング

2枚を並べた全体の幅と高さは、写真の比率と配置方向で変わります。4000×3000pxの写真を縮小せず横に2枚置けば、境界線を除いて8000×3000px、2400万画素になります。Canvasの画素バッファだけでも約96MBとなり、ほかの画像やプレビューを含めた負荷はさらに増えます。端末によっては失敗するため、現行実装は合成全体の寸法を制限します。

メリット: 連結する全体に boundedDimensions(totalWidth, totalHeight, 3000, 9_000_000) を適用し、長辺3000px・総画素数900万以内にします。1枚目・2枚目・境界線の描画座標も、最終寸法に合わせて縮小します。これは後続の画素数を減らす処理であり、入力のデコード時を含む端末全体のメモリ使用量を保証するものではありません。

3. 動的なセパレーター(境界線)の計算

画像の間に引く「白い線」の太さは、固定のピクセル値ではなく、縮小前の1枚目の長辺の 1% として算出されるように設計されています。

メリット: 境界線の初期幅は1枚目の長辺の1%(最低1px)で、合成全体を縮小すると線も同じ割合で縮みます。Canvas全体は境界線の有無にかかわらず白く塗ってから画像を配置するため、透過画像の透明部分もJPEGでは白になります。

出力の仕様と確認する点

2枚の画像を横または縦に並べ、JPEG(品質指定 0.9)で保存します。横は高さ、縦は幅を合わせます。写真同士の順序を入れ替える場合は、読み込み欄の割り当てを確認します。

3000px の制限は合成後の全体に対するものです。比率合わせの段階では2枚目が拡大される場合もあるため、低解像度の画像の粗さを確認します。全体をさらに縮小しても、元画像の細部が増えるわけではありません。

比較位置の自動検出、Before/After の文字入れ、自由なトリミングは行いません。撮影角度や余白をそろえた2枚を用意すると比較しやすくなります。