Geometry & UX Logic
「比率の自動補正」と「Canvasレイアウトエンジン」の設計
2枚の画像を単にくっつけるだけなら、CSSのFlexboxで十分です。しかし、ダウンロード可能な1枚の「作品」として出力するためには、Canvasを用いた厳密な幾何学計算が必要になります。
1. 基準軸によるアスペクト比維持アルゴリズム
ユーザーがアップロードする写真は、サイズもアスペクト比もバラバラです。本ツールでは、横並びの場合は「高さ」を、縦並びの場合は「幅」を共通の基準軸(Master Axis)として固定し、もう一方の辺を比率計算によって導き出しています。
メリット: 片方の画像を無理に引き伸ばしたり、歪ませたりすることなく、2枚をピタッと揃えることができます。ユーザーが事前にトリミングする手間を完全に排除しています。
// 横並び(Horizontal)時のサイズ補正ロジック
// 1枚目の高さを基準に、2枚目の幅を比率計算で算出する
const h1 = img1.height;
const w1 = img1.width;
// 2枚目の計算後サイズ
const h2 = h1; // 高さを揃える
const w2 = Math.floor(img2.width * (h1 / img2.height));
// 最終的なキャンバス幅の決定(境界線も考慮)
canvas.width = w1 + w2 + // 独自の境界線サイズ;
canvas.height = h1; 2. 解像度ガードレールとダウンサンプリング
現代のスマートフォンで撮影された写真は、長辺が4000pxを超えることも珍しくありません。これを2枚そのまま連結すると、Canvasの面積は1600万ピクセルを超え、モバイルブラウザのメモリ上限に達してクラッシュ(タブの強制リロード)を引き起こします。
メリット: 入力時に MAX_DIM というガードレールを設け、アスペクト比を維持したまま、処理に最適な解像度へ動的にスケールダウンさせています。これにより、SNS投稿に十分な画質を保ちつつ、低スペックなデバイスでも安定した動作を保証しています。
// 巨大画像に対するメモリ保護ロジック
const MAX_SAFE_DIMENSION = // 独自の閾値;
let scale = 1.0;
if (Math.max(w1, h1) > MAX_SAFE_DIMENSION) {
// 独自の計算式でスケーリング係数を算出
scale = MAX_SAFE_DIMENSION / Math.max(w1, h1);
// この係数を基に全ての描画座標をスケーリング
w1 = Math.floor(w1 * scale);
h1 = Math.floor(h1 * scale);
// ...
} 3. 動的なセパレーター(境界線)の計算
画像の間に引く「白い線」の太さは、固定のピクセル値ではなく、画像全体の解像度に対して相対的に算出されるように設計されています。
メリット: 小さなスクリーンショットでも、大きな風景写真でも、常に「ちょうど良い太さ」の境界線が引かれます。また、Canvas全体を背景色で塗りつぶしてから画像を配置する手法をとることで、境界線部分の透過を防ぎ、JPEG出力時の品質を安定させています。
// 解像度に比例した境界線サイズの動的算出
const dividerWeight = // 独自の比率係数;
const dividerSize = showDivider ? Math.floor(Math.max(w1, h1) * dividerWeight) : 0;
// Canvasの初期化(背景色を境界線として利用)
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 画像の描画(境界線分をオフセットさせて配置)
// ここに位置計算を伴う drawImage 処理が入ります Developer's Note
Before/After画像は、ダイエットの記録やフリマアプリの出品など、極めて個人的、あるいはビジネスに直結する場面で使われます。
そのため、このツールでは「利便性」と同じくらい「安心感」を重視しました。HEIC形式(iPhone独自の高効率画像)をブラウザ内でJPEGへ変換するパイプラインを実装したのも、サーバーサイドの重い変換処理を介さず、ユーザーの端末の中だけで全てを完結させるためです。
「一枚も写真を外に出さない」という、ブラウザネイティブな実装にこだわった結果、プライバシーとパフォーマンスを高い次元で両立させることができました。