Spaces:
Running
Running
| 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(); | |
| }); | |
| }); |