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