ついでに初期画像として使う汎用的なコラージュ生成プログラムを作った。
pc用とスマホ用コラージュ
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>万能コラージュメーカー</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f4f4f9;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
margin: 0;
}
.container {
background: #fff;
padding: 30px;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
max-width: 900px;
width: 100%;
text-align: center;
}
h1 {
font-size: 1.5rem;
margin-top: 0;
}
.controls {
margin: 20px 0;
display: flex;
justify-content: center;
flex-wrap: wrap; /* スマホなどで折り返すように */
gap: 15px;
}
input[type="file"] {
display: none;
}
.btn, .select-ratio {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background 0.3s;
font-family: inherit;
}
.select-ratio {
background-color: #495057; /* 少し色を変えて区別 */
appearance: none; /* デフォルトの矢印を消す */
-webkit-appearance: none;
}
.select-ratio:hover {
background-color: #343a40;
}
.btn:hover {
background-color: #0056b3;
}
.btn-download {
background-color: #28a745;
}
.btn-download:hover {
background-color: #1e7e34;
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* キャンバス(プレビュー)の枠。縦長になっても画面に収まるよう工夫 */
.canvas-wrapper {
background-color: #eee;
padding: 10px;
border-radius: 8px;
margin-top: 20px;
}
canvas {
max-width: 100%;
max-height: 65vh; /* 画面の高さを超えないように制限 */
width: auto;
height: auto;
border: 1px solid #ddd;
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
display: block;
margin: 0 auto;
}
.instruction {
font-size: 0.9rem;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>自動コラージュメーカー</h1>
<p class="instruction">画像を複数選択し、用途に合わせてアスペクト比を変更してください。</p>
<div class="controls">
<select id="ratioSelect" class="select-ratio">
<option value="16:9" selected>PC用 (16:9)</option>
<option value="9:16">スマホ用 (9:16)</option>
<option value="4:3">標準写真 (4:3)</option>
<option value="1:1">正方形 (1:1)</option>
</select>
<label class="btn">
画像を選ぶ
<input type="file" id="imageInput" multiple accept="image/*">
</label>
<button id="downloadBtn" class="btn btn-download" disabled>コラージュを保存</button>
</div>
<div class="canvas-wrapper">
<canvas id="collageCanvas"></canvas>
</div>
</div>
<script>
const imageInput = document.getElementById('imageInput');
const ratioSelect = document.getElementById('ratioSelect');
const canvas = document.getElementById('collageCanvas');
const ctx = canvas.getContext('2d');
const downloadBtn = document.getElementById('downloadBtn');
let currentImages = []; // アップロードされた画像を記憶しておく変数
// 選ばれた比率に合わせてキャンバスの実際のサイズ(画素数)を設定する関数
function updateCanvasSize() {
const ratio = ratioSelect.value;
if (ratio === '16:9') {
canvas.width = 1920;
canvas.height = 1080;
} else if (ratio === '9:16') {
canvas.width = 1080;
canvas.height = 1920;
} else if (ratio === '4:3') {
canvas.width = 1600;
canvas.height = 1200;
} else if (ratio === '1:1') {
canvas.width = 1200;
canvas.height = 1200;
}
}
// キャンバスを白紙に戻す関数
function clearCanvas() {
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 画像が選択された時の処理
imageInput.addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
if (files.length === 0) return;
downloadBtn.disabled = true;
// 選択されたファイルをImageオブジェクトとして読み込み、記憶する
currentImages = await Promise.all(files.map(file => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = URL.createObjectURL(file);
});
}));
// キャンバスのサイズを合わせてから描画
updateCanvasSize();
clearCanvas();
drawCollage(currentImages);
downloadBtn.disabled = false;
});
// アスペクト比のプルダウンが変更された時の処理
ratioSelect.addEventListener('change', () => {
updateCanvasSize();
clearCanvas();
// すでに画像が読み込まれていれば、新しい比率で再描画する
if (currentImages.length > 0) {
drawCollage(currentImages);
}
});
// キャンバスに画像を配置して描画する関数
function drawCollage(images) {
const numImages = images.length;
if (numImages === 0) return;
// 画像の枚数に合わせてグリッド(行と列)を計算
const cols = Math.ceil(Math.sqrt(numImages));
const rows = Math.ceil(numImages / cols);
const cellWidth = canvas.width / cols;
const cellHeight = canvas.height / rows;
images.forEach((img, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
const x = col * cellWidth;
const y = row * cellHeight;
// 画像を枠いっぱいにトリミングして描画
drawImageCover(ctx, img, x, y, cellWidth, cellHeight);
// 画像の間に白い枠線を引く
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 10;
ctx.strokeRect(x, y, cellWidth, cellHeight);
});
}
// 画像の比率を保ったまま枠にトリミング(cover)する関数
function drawImageCover(ctx, img, x, y, w, h) {
const imgRatio = img.width / img.height;
const targetRatio = w / h;
let sx, sy, sWidth, sHeight;
if (imgRatio > targetRatio) {
sHeight = img.height;
sWidth = img.height * targetRatio;
sx = (img.width - sWidth) / 2;
sy = 0;
} else {
sWidth = img.width;
sHeight = img.width / targetRatio;
sx = 0;
sy = (img.height - sHeight) / 2;
}
ctx.drawImage(img, sx, sy, sWidth, sHeight, x, y, w, h);
}
// ダウンロードボタンの処理
downloadBtn.addEventListener('click', () => {
// 現在の比率名をファイル名に組み込む(例: collage_16-9.jpg)
const ratioName = ratioSelect.value.replace(':', '-');
const fileName = `collage_${ratioName}.jpg`;
const dataURL = canvas.toDataURL('image/jpeg', 0.9);
const link = document.createElement('a');
link.download = fileName;
link.href = dataURL;
link.click();
});
// 最初に画面を開いた時の初期設定
updateCanvasSize();
clearCanvas();
</script>
</body>
</html>
スマホでスクロールしながら画面いっぱいに背景画像として表示すると予想以上に没入感があり、Geminiが最初にNYTのサイト構成手法で優れているとしていたのに納得する。