image-editor / script.js
soiz1's picture
Update script.js
4421753 verified
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();
});
});