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 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'); // カーブ用のキャンバス const redCurveCanvas = document.getElementById('red-curve'); const greenCurveCanvas = document.getElementById('green-curve'); const blueCurveCanvas = document.getElementById('blue-curve'); const luminanceCurveCanvas = document.getElementById('luminance-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 luminanceCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; let camanInstance = null; let originalImageData = null; let isApplyingFilters = false; const applyRgbCurves = function() { const redCurve = createCurveData(redCurvePoints); const greenCurve = createCurveData(greenCurvePoints); const blueCurve = createCurveData(blueCurvePoints); this.process("rgbCurve", function(rgba) { rgba.r = redCurve[rgba.r]; rgba.g = greenCurve[rgba.g]; rgba.b = blueCurve[rgba.b]; return rgba; }); }; const applyLuminanceCurve = function() { const luminanceCurve = createCurveData(luminanceCurvePoints); const shadowAmount = parseInt(shadowsSlider.value) / 100 * 50; const highlightAmount = parseInt(highlightsSlider.value) / 100 * 50; this.process("luminanceAdjustment", function(rgba) { const luminance = 0.299 * rgba.r + 0.587 * rgba.g + 0.114 * rgba.b; const curveAdjustment = (luminanceCurve[luminance] - luminance) / 255 * 100; let adjustment = 0; if (shadowAmount !== 0) { const shadowFactor = 1 - (luminance / 255); adjustment += shadowAmount * shadowFactor; } if (highlightAmount !== 0) { const highlightFactor = luminance / 255; adjustment += highlightAmount * highlightFactor; } adjustment += curveAdjustment; rgba.r = Math.min(255, Math.max(0, rgba.r + adjustment)); rgba.g = Math.min(255, Math.max(0, rgba.g + adjustment)); rgba.b = Math.min(255, Math.max(0, rgba.b + adjustment)); return rgba; }); }; // カーブデータを作成する関数 const createCurveData = function(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; }; // フィルターを適用する関数 const applyFilters = function() { if (!camanInstance || isApplyingFilters) return; isApplyingFilters = true; try { camanInstance.revert(false); // 基本調整を適用 camanInstance.brightness(parseInt(brightnessSlider.value)); camanInstance.contrast(parseInt(contrastSlider.value) * 1.5); camanInstance.saturation(parseInt(saturationSlider.value)); // RGBカーブ調整を適用 const applyRgbCurves = function() { const redCurve = createCurveData(redCurvePoints); const greenCurve = createCurveData(greenCurvePoints); const blueCurve = createCurveData(blueCurvePoints); this.process("rgbCurve", function(rgba) { rgba.r = Math.min(255, Math.max(0, redCurve[rgba.r])); rgba.g = Math.min(255, Math.max(0, greenCurve[rgba.g])); rgba.b = Math.min(255, Math.max(0, blueCurve[rgba.b])); return rgba; }); }; // 輝度カーブ調整を適用 const applyLuminanceCurve = function() { const luminanceCurve = createCurveData(luminanceCurvePoints); const shadowAmount = parseInt(shadowsSlider.value) / 2; // 調整量を減らす const highlightAmount = parseInt(highlightsSlider.value) / 2; this.process("luminanceAdjustment", function(rgba) { const luminance = 0.299 * rgba.r + 0.587 * rgba.g + 0.114 * rgba.b; const normalizedLum = luminance / 255; // カーブ調整(0-255範囲に収める) const curveAdjustment = (luminanceCurve[luminance] - luminance) / 3; // シャドウ調整(低輝度ほど強く適用) const shadowAdjust = shadowAmount * (1 - normalizedLum) / 2; // ハイライト調整(高輝度ほど強く適用) const highlightAdjust = highlightAmount * normalizedLum / 2; // 合計調整量(より控えめに) const totalAdjust = curveAdjustment + shadowAdjust + highlightAdjust; rgba.r = Math.min(255, Math.max(0, rgba.r + totalAdjust)); rgba.g = Math.min(255, Math.max(0, rgba.g + totalAdjust)); rgba.b = Math.min(255, Math.max(0, rgba.b + totalAdjust)); return rgba; }); }; // カーブ調整を適用 applyRgbCurves.call(camanInstance); applyLuminanceCurve.call(camanInstance); camanInstance.render(); } catch (e) { console.error("Error applying filters:", e); } finally { isApplyingFilters = false; } }; // カーブを描画する関数 const drawCurve = function(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(); }); }; // カーブのインタラクションを設定 const setupCurveInteraction = function(canvas, points, color) { let isDragging = false; let draggedPoint = null; let lastUpdateTime = 0; const handleUpdate = function() { drawCurve(canvas, points, color); const now = Date.now(); if (now - lastUpdateTime > 200) { // 200msごとに更新 applyFilters(); lastUpdateTime = now; } }; 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; break; } } if (!isDragging && x > 0 && x < 255 && y > 0 && y < 255) { points.push({x, y}); points.sort((a, b) => a.x - b.x); isDragging = true; draggedPoint = points.find(p => p.x === x && p.y === y); } handleUpdate(); }); 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)); if (points.indexOf(draggedPoint) === 0) { x = 0; } else if (points.indexOf(draggedPoint) === points.length - 1) { x = 255; } draggedPoint.x = x; draggedPoint.y = y; handleUpdate(); }); canvas.addEventListener('mouseup', function() { if (isDragging) { isDragging = false; applyFilters(); // 最後に確実に適用 } }); canvas.addEventListener('mouseleave', function() { if (isDragging) { isDragging = false; applyFilters(); // 最後に確実に適用 } }); 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); applyFilters(); break; } } }); }; // 初期化関数 const initCurves = function() { drawCurve(redCurveCanvas, redCurvePoints, 'red'); drawCurve(greenCurveCanvas, greenCurvePoints, 'green'); drawCurve(blueCurveCanvas, blueCurvePoints, 'blue'); drawCurve(luminanceCurveCanvas, luminanceCurvePoints, '#888'); setupCurveInteraction(redCurveCanvas, redCurvePoints, 'red'); setupCurveInteraction(greenCurveCanvas, greenCurvePoints, 'green'); setupCurveInteraction(blueCurveCanvas, blueCurvePoints, 'blue'); setupCurveInteraction(luminanceCurveCanvas, luminanceCurvePoints, '#888'); }; // 画像アップロードの処理 imageUpload.addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; 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; Caman(imageCanvas, function() { this.revert(false); this.render(); camanInstance = this; originalImageData = this.canvas.toDataURL(); initCurves(); }); 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', applyFilters); // リセットボタン resetBtn.addEventListener('click', function() { if (!camanInstance) return; brightnessSlider.value = 0; contrastSlider.value = 0; saturationSlider.value = 0; shadowsSlider.value = 0; highlightsSlider.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}]; luminanceCurvePoints = [{x: 0, y: 0}, {x: 255, y: 255}]; initCurves(); 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; }); }; img.src = originalImageData; }); // ダウンロードボタン 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(); }); });