document.addEventListener('DOMContentLoaded', function() { // 要素の取得 const imageUpload = document.getElementById('image-upload'); const imageCanvas = document.getElementById('image-canvas'); const brightnessSlider = document.getElementById('brightness'); const contrastSlider = document.getElementById('contrast'); const saturationSlider = document.getElementById('saturation'); const shadowsSlider = document.getElementById('shadows'); const highlightsSlider = document.getElementById('highlights'); const hueRedSlider = document.getElementById('hue-red'); const hueGreenSlider = document.getElementById('hue-green'); const hueBlueSlider = document.getElementById('hue-blue'); const brightnessValue = document.getElementById('brightness-value'); const contrastValue = document.getElementById('contrast-value'); const saturationValue = document.getElementById('saturation-value'); const shadowsValue = document.getElementById('shadows-value'); const highlightsValue = document.getElementById('highlights-value'); const resetBtn = document.getElementById('reset-btn'); const applyBtn = document.getElementById('apply-btn'); const downloadBtn = document.getElementById('download-btn'); const loadingOverlay = document.getElementById('loading-overlay'); // RGBカーブ用のキャンバス const redCurveCanvas = document.getElementById('red-curve'); const greenCurveCanvas = document.getElementById('green-curve'); const blueCurveCanvas = document.getElementById('blue-curve'); // カーブ制御用の変数 let redCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; let greenCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; let blueCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; let camanInstance = null; let originalImageData = null; let currentFilters = {}; // 画像アップロードの処理 imageUpload.addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; showLoading(); const reader = new FileReader(); reader.onload = function(event) { const img = new Image(); img.onload = function() { // キャンバスのサイズを画像に合わせる const maxWidth = 800; const maxHeight = 600; let width = img.width; let height = img.height; if (width > maxWidth) { height = (maxWidth / width) * height; width = maxWidth; } if (height > maxHeight) { width = (maxHeight / height) * width; height = maxHeight; } imageCanvas.width = width; imageCanvas.height = height; // CamanJSで画像を読み込む Caman(imageCanvas, function() { this.revert(false); this.render(); camanInstance = this; originalImageData = this.canvas.toDataURL(); // RGBカーブを初期化 initCurves(); hideLoading(); }); const ctx = imageCanvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); }; img.src = event.target.result; }; reader.readAsDataURL(file); }); // スライダーイベントの設定 function setupSlider(slider, valueElement) { slider.addEventListener('input', function() { valueElement.textContent = this.value; }); } setupSlider(brightnessSlider, brightnessValue); setupSlider(contrastSlider, contrastValue); setupSlider(saturationSlider, saturationValue); setupSlider(shadowsSlider, shadowsValue); setupSlider(highlightsSlider, highlightsValue); // 適用ボタンのイベントリスナー applyBtn.addEventListener('click', function() { if (!camanInstance) return; showLoading(); // 非同期で処理を行う setTimeout(function() { applyFilters(); hideLoading(); }, 100); }); // フィルターを適用する関数(強化版) function applyFilters() { if (!camanInstance) return; camanInstance.revert(false); // 現在のフィルター設定を保存 currentFilters = { brightness: parseInt(brightnessSlider.value), contrast: parseInt(contrastSlider.value), saturation: parseInt(saturationSlider.value), shadows: parseInt(shadowsSlider.value), highlights: parseInt(highlightsSlider.value), hueRed: parseInt(hueRedSlider.value), hueGreen: parseInt(hueGreenSlider.value), hueBlue: parseInt(hueBlueSlider.value) }; // 明るさ(範囲拡大: -100 to 100 → -150 to 150) camanInstance.brightness(currentFilters.brightness * 1.5); // コントラスト(効果を強化) camanInstance.contrast(currentFilters.contrast * 1.5); // 彩度 camanInstance.saturation(currentFilters.saturation); // シャドウ(効果を強化、範囲拡大) if (currentFilters.shadows > 0) { camanInstance.shadows(currentFilters.shadows * 2); } else { camanInstance.exposure(currentFilters.shadows / 5); } // ハイライト(効果を強化、範囲拡大) if (currentFilters.highlights > 0) { camanInstance.highlights(currentFilters.highlights * 2); } else { camanInstance.exposure(currentFilters.highlights / 5); } // HSL調整 camanInstance.hue(currentFilters.hueRed / 2); camanInstance.hue(currentFilters.hueGreen / 2); camanInstance.hue(currentFilters.hueBlue / 2); // RGBカーブを適用 applyRgbCurves(); // 追加フィルター: シャープネス if (currentFilters.sharpness) { camanInstance.sharpen(currentFilters.sharpness); } // 追加フィルター: ノイズ if (currentFilters.noise) { camanInstance.noise(currentFilters.noise); } // 追加フィルター: ビネット if (currentFilters.vignette) { camanInstance.vignette("10%", currentFilters.vignette * 2); } camanInstance.render(); } // RGBカーブを初期化 function initCurves() { drawCurve(redCurveCanvas, redCurvePoints, 'red'); drawCurve(greenCurveCanvas, greenCurvePoints, 'green'); drawCurve(blueCurveCanvas, blueCurvePoints, 'blue'); setupCurveInteraction(redCurveCanvas, redCurvePoints, 'red'); setupCurveInteraction(greenCurveCanvas, greenCurvePoints, 'green'); setupCurveInteraction(blueCurveCanvas, blueCurvePoints, 'blue'); } // カーブを描画 function drawCurve(canvas, points, color) { const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; // クリア ctx.clearRect(0, 0, width, height); // グリッドを描画 ctx.strokeStyle = '#eee'; ctx.lineWidth = 1; // 水平線 for (let y = 0; y <= height; y += height / 4) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } // 垂直線 for (let x = 0; x <= width; x += width / 4) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } // 対角線 ctx.strokeStyle = '#ccc'; ctx.beginPath(); ctx.moveTo(0, height); ctx.lineTo(width, 0); ctx.stroke(); // カーブを描画 ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); // 点をソート points.sort((a, b) => a.x - b.x); // 最初の点 const firstPoint = points[0]; ctx.moveTo(firstPoint.x / 255 * width, (255 - firstPoint.y) / 255 * height); // 中間の点 for (let i = 1; i < points.length; i++) { const point = points[i]; ctx.lineTo(point.x / 255 * width, (255 - point.y) / 255 * height); } ctx.stroke(); // 制御点を描画 ctx.fillStyle = color; points.forEach(point => { const x = point.x / 255 * width; const y = (255 - point.y) / 255 * height; ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); }); } // カーブのインタラクションを設定 function setupCurveInteraction(canvas, points, color) { let isDragging = false; let draggedPoint = null; canvas.addEventListener('mousedown', function(e) { const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width * 255; const y = 255 - (e.clientY - rect.top) / rect.height * 255; // 既存の点をチェック for (let i = 0; i < points.length; i++) { const point = points[i]; const distance = Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2)); if (distance < 15) { isDragging = true; draggedPoint = point; return; } } // 新しい点を追加 if (x > 0 && x < 255 && y > 0 && y < 255) { points.push({x, y}); points.sort((a, b) => a.x - b.x); isDragging = true; draggedPoint = {x, y}; } drawCurve(canvas, points, color); }); canvas.addEventListener('mousemove', function(e) { if (!isDragging || !draggedPoint) return; const rect = canvas.getBoundingClientRect(); let x = (e.clientX - rect.left) / rect.width * 255; let y = 255 - (e.clientY - rect.top) / rect.height * 255; // 境界チェック x = Math.max(0, Math.min(255, x)); y = Math.max(0, Math.min(255, y)); // 最初と最後の点はx座標を固定 if (points.indexOf(draggedPoint) === 0) { x = 0; } else if (points.indexOf(draggedPoint) === points.length - 1) { x = 255; } draggedPoint.x = x; draggedPoint.y = y; drawCurve(canvas, points, color); }); canvas.addEventListener('mouseup', function() { isDragging = false; draggedPoint = null; }); canvas.addEventListener('mouseleave', function() { isDragging = false; draggedPoint = null; }); // ポイントを削除するダブルクリック canvas.addEventListener('dblclick', function(e) { if (points.length <= 2) return; const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width * 255; const y = 255 - (e.clientY - rect.top) / rect.height * 255; for (let i = 1; i < points.length - 1; i++) { const point = points[i]; const distance = Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2)); if (distance < 15) { points.splice(i, 1); drawCurve(canvas, points, color); return; } } }); } // RGBカーブを適用 function applyRgbCurves() { if (!camanInstance) return; // カーブデータを準備 const redCurve = createCurveData(redCurvePoints); const greenCurve = createCurveData(greenCurvePoints); const blueCurve = createCurveData(blueCurvePoints); // カーブを適用 camanInstance.process("rgbCurve", function(rgba) { rgba.r = redCurve[rgba.r]; rgba.g = greenCurve[rgba.g]; rgba.b = blueCurve[rgba.b]; return rgba; }); } // カーブデータを作成 function createCurveData(points) { points.sort((a, b) => a.x - b.x); const curve = new Array(256); for (let i = 0; i < points.length - 1; i++) { const start = points[i]; const end = points[i + 1]; const x1 = Math.round(start.x); const y1 = Math.round(start.y); const x2 = Math.round(end.x); const y2 = Math.round(end.y); // 線形補間 for (let x = x1; x <= x2; x++) { const t = (x - x1) / (x2 - x1); curve[x] = Math.round(y1 + t * (y2 - y1)); } } return curve; } // リセットボタン resetBtn.addEventListener('click', function() { if (!camanInstance) return; showLoading(); // スライダーをリセット brightnessSlider.value = 0; contrastSlider.value = 0; saturationSlider.value = 0; shadowsSlider.value = 0; highlightsSlider.value = 0; hueRedSlider.value = 0; hueGreenSlider.value = 0; hueBlueSlider.value = 0; brightnessValue.textContent = '0'; contrastValue.textContent = '0'; saturationValue.textContent = '0'; shadowsValue.textContent = '0'; highlightsValue.textContent = '0'; // カーブをリセット redCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; greenCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; blueCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; drawCurve(redCurveCanvas, redCurvePoints, 'red'); drawCurve(greenCurveCanvas, greenCurvePoints, 'green'); drawCurve(blueCurveCanvas, blueCurvePoints, 'blue'); // 画像をリセット const img = new Image(); img.onload = function() { const ctx = imageCanvas.getContext('2d'); ctx.drawImage(img, 0, 0, imageCanvas.width, imageCanvas.height); Caman(imageCanvas, function() { this.revert(false); this.render(); camanInstance = this; hideLoading(); }); }; img.src = originalImageData; // フィルター設定をリセット currentFilters = {}; }); // ダウンロードボタン downloadBtn.addEventListener('click', function() { if (!camanInstance) return; const link = document.createElement('a'); link.download = 'edited-image.png'; link.href = imageCanvas.toDataURL('image/png'); link.click(); }); // 読み込み表示を制御する関数 function showLoading() { loadingOverlay.style.display = 'flex'; } function hideLoading() { loadingOverlay.style.display = 'none'; } // 追加機能: キーボードショートカット document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.key === 'z') { resetBtn.click(); } else if (e.ctrlKey && e.key === 's') { downloadBtn.click(); e.preventDefault(); } else if (e.key === 'Enter') { applyBtn.click(); } }); // 追加機能: プリセットフィルター function applyPreset(presetName) { switch(presetName) { case 'vintage': brightnessSlider.value = 10; contrastSlider.value = 20; saturationSlider.value = -30; shadowsSlider.value = 15; highlightsSlider.value = -10; hueRedSlider.value = 10; break; case 'dramatic': brightnessSlider.value = -10; contrastSlider.value = 50; saturationSlider.value = -20; shadowsSlider.value = 30; highlightsSlider.value = -20; break; case 'bright': brightnessSlider.value = 30; contrastSlider.value = 20; saturationSlider.value = 40; shadowsSlider.value = 20; highlightsSlider.value = 10; break; } // スライダー値を更新 brightnessValue.textContent = brightnessSlider.value; contrastValue.textContent = contrastSlider.value; saturationValue.textContent = saturationSlider.value; shadowsValue.textContent = shadowsSlider.value; highlightsValue.textContent = highlightsSlider.value; applyBtn.click(); } });