image-editor / script.js
soiz1's picture
Update script.js
f08a4ef verified
raw
history blame
17.8 kB
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();
}
});