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