Algorithm & UI/UX
ブラウザだけで完結する
背景切り抜き実装のハック
「写真をサーバーに送りたくない」というニーズに応えるため、すべての画像処理をクライアントサイドのCanvas APIで完結させています。今回はその中核となるロジックを抽出して解説します。
1. destination-out による「透明なペン」の実装
Canvasで背景を消す際、一般的な「白や黒で塗る」方法では透明化はできません。このツールでは globalCompositeOperation プロパティに destination-out を設定することで、なぞった部分を「削り取る(透明にする)」処理を実現しています。
この設定により、描画済みのピクセルが新しい描画と重なった部分だけ完全に除去されるため、直感的な消しゴム機能が作れます。
// 消しゴムモードの中核ロジック
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = // UIから取得したブラシサイズ
// 重なった部分を透明にする特殊設定
ctx.globalCompositeOperation = 'destination-out';
// なぞった軌跡をパスとして描画
ctx.beginPath();
// ここに座標計算と描画処理が入ります
ctx.stroke();
} 2. 端末負荷を抑えるスマート・リサイズ
近年のスマホ写真は数千万画素に及びますが、そのままCanvasに展開するとメモリ不足でブラウザがクラッシュするリスクがあります。本ツールでは、アクスタ制作やSNS投稿に十分な品質を保ちつつ、処理速度を犠牲にしない「独自の動的リサイズアルゴリズム」を採用しています。
読み込み時にアスペクト比を維持したまま、最大長辺を特定の閾値に制限することで、古い端末でもサクサク動く操作感を実現しました。
// 高解像度画像の最適化処理
img.onload = () => {
const LIMIT = // 独自のパフォーマンス閾値
let w = img.width;
let h = img.height;
if (w > LIMIT || h > LIMIT) {
const ratio = LIMIT / Math.max(w, h);
// 浮動小数点による端数が出ないよう整数化
w = Math.floor(w * ratio);
h = Math.floor(h * ratio);
}
canvas.width = w;
canvas.height = h;
// リサイズしてCanvasに転写
ctx.drawImage(img, 0, 0, w, h);
}; 3. HEIC/HEIF形式のオンザフライ変換
iPhoneで撮影された写真は標準でHEIC形式ですが、Canvasはこれを直接扱うことができません。UXを損なわないよう、ダイナミック・インポートを活用して変換ライブラリを必要な時だけ読み込み、ブラウザ内でPNGに変換するフローを組み込んでいます。
// 拡張子判定による動的変換
if (file.name.toLowerCase().match(/\.(heic|heif)$/)) {
// 必要な時だけモジュールをロード(軽量化ハック)
const converter = await import('conversion-module');
const convertedBlob = await converter.process({
blob: file,
to: "image/png"
});
// 変換後データをImageオブジェクトとして処理
processBlob = Array.isArray(convertedBlob) ? convertedBlob[0] : convertedBlob;
} Developer's Note
最近はAIによる自動切り抜きツールが主流ですが、あえて「なぞって消す」というアナログな体験にこだわりました。それは、推しの写真の境界線を自分の手でなぞる時間もまた、推し活という「愛でる行為」の一部だと考えているからです。
実装面では、いかに「Webブラウザという制限の多い環境で、巨大な画像データをストレスなく扱うか」に心血を注ぎました。特にUndo機能(1つ戻る)は、メモリ消費を抑えるために履歴の深さを動的に制御するなど、見えない部分でチューニングを行っています。