|
|
|
|
|
|
|
|
|
|
|
const backgroundInput = document.getElementById('background-input'); |
|
const overlayInput = document.getElementById('overlay-input'); |
|
const canvas = document.getElementById('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
const scaleSlider = document.getElementById('scale-slider'); |
|
const scaleValue = document.getElementById('scale-value'); |
|
const rotationSlider = document.getElementById('rotation-slider'); |
|
const rotationValue = document.getElementById('rotation-value'); |
|
|
|
|
|
const temperatureSlider = document.getElementById('temperature-slider'); |
|
const temperatureValue = document.getElementById('temperature-value'); |
|
const brightnessSlider = document.getElementById('brightness-slider'); |
|
const brightnessValue = document.getElementById('brightness-value'); |
|
const contrastSlider = document.getElementById('contrast-slider'); |
|
const contrastValue = document.getElementById('contrast-value'); |
|
const saturationSlider = document.getElementById('saturation-slider'); |
|
const saturationValue = document.getElementById('saturation-value'); |
|
const resetFilterBtn = document.getElementById('reset-filter-btn'); |
|
|
|
const generateBtn = document.getElementById('generate-btn'); |
|
const resetAllBtn = document.getElementById('reset-all-btn'); |
|
const downloadBtn = document.getElementById('download-btn'); |
|
const deleteLayerBtn = document.getElementById('delete-layer-btn'); |
|
|
|
const previewContainer = document.getElementById('preview-container'); |
|
const previewImg = document.getElementById('preview-img'); |
|
|
|
const layersList = document.getElementById('layers-list'); |
|
|
|
|
|
|
|
const CANVAS_WIDTH = 800; |
|
const CANVAS_HEIGHT = 600; |
|
canvas.width = CANVAS_WIDTH; |
|
canvas.height = CANVAS_HEIGHT; |
|
|
|
|
|
let backgroundImage = null; |
|
let originalFormat = 'png'; |
|
let canvasAdjustedHeight = CANVAS_HEIGHT; |
|
|
|
|
|
let bgDrawProps = { x: 0, y: 0, width: CANVAS_WIDTH, height: CANVAS_HEIGHT }; |
|
|
|
|
|
let overlays = []; |
|
|
|
let activeOverlayIndex = -1; |
|
|
|
|
|
let isDragging = false; |
|
let isRotating = false; |
|
let isResizing = false; |
|
let activeHandle = null; |
|
|
|
|
|
let lastX = 0; |
|
let lastY = 0; |
|
|
|
|
|
let resizeStartDistance = 0; |
|
|
|
|
|
const handleSize = 15; |
|
const rotateHandleDistance = 30; |
|
|
|
|
|
const clickPadding = 5; |
|
|
|
|
|
function initCanvas() { |
|
ctx.fillStyle = '#ffffff'; |
|
ctx.fillRect(0, 0, CANVAS_WIDTH, canvasAdjustedHeight); |
|
updateLayersList(); |
|
} |
|
|
|
function cropTransparentPixels(imageElement, alphaThreshold = 0) { |
|
const tempCanvas = document.createElement('canvas'); |
|
tempCanvas.width = imageElement.naturalWidth || imageElement.width; |
|
tempCanvas.height = imageElement.naturalHeight || imageElement.height; |
|
const tempCtx = tempCanvas.getContext('2d'); |
|
tempCtx.drawImage(imageElement, 0, 0); |
|
|
|
let imageData; |
|
try { |
|
imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); |
|
} catch (e) { |
|
console.error("Error getting ImageData (possibly due to CORS):", e); |
|
|
|
const fallbackCanvas = document.createElement('canvas'); |
|
fallbackCanvas.width = tempCanvas.width; |
|
fallbackCanvas.height = tempCanvas.height; |
|
const fallbackCtx = fallbackCanvas.getContext('2d'); |
|
fallbackCtx.drawImage(imageElement, 0, 0); |
|
return fallbackCanvas; |
|
} |
|
|
|
const data = imageData.data; |
|
let minX = tempCanvas.width, minY = tempCanvas.height, maxX = 0, maxY = 0; |
|
let foundContent = false; |
|
|
|
for (let y = 0; y < tempCanvas.height; y++) { |
|
for (let x = 0; x < tempCanvas.width; x++) { |
|
const alpha = data[(y * tempCanvas.width + x) * 4 + 3]; |
|
if (alpha > alphaThreshold) { |
|
minX = Math.min(minX, x); |
|
minY = Math.min(minY, y); |
|
maxX = Math.max(maxX, x); |
|
maxY = Math.max(maxY, y); |
|
foundContent = true; |
|
} |
|
} |
|
} |
|
|
|
if (!foundContent) { |
|
|
|
const emptyCanvas = document.createElement('canvas'); |
|
emptyCanvas.width = imageElement.naturalWidth || 1; |
|
emptyCanvas.height = imageElement.naturalHeight || 1; |
|
return emptyCanvas; |
|
} |
|
|
|
const cropWidth = maxX - minX + 1; |
|
const cropHeight = maxY - minY + 1; |
|
|
|
const croppedCanvas = document.createElement('canvas'); |
|
croppedCanvas.width = cropWidth; |
|
croppedCanvas.height = cropHeight; |
|
const croppedCtx = croppedCanvas.getContext('2d'); |
|
|
|
|
|
croppedCtx.drawImage(tempCanvas, minX, minY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight); |
|
|
|
return croppedCanvas; |
|
} |
|
|
|
|
|
function loadImage(fileOrBlob, type) { |
|
if (!fileOrBlob) return; |
|
|
|
let fileExtension = 'png'; |
|
let isBlob = fileOrBlob instanceof Blob; |
|
let fileName = 'unknown'; |
|
|
|
if (!isBlob && fileOrBlob.name) { |
|
fileExtension = fileOrBlob.name.split('.').pop().toLowerCase(); |
|
fileName = fileOrBlob.name; |
|
} else if (isBlob && fileOrBlob.type) { |
|
fileExtension = fileOrBlob.type.split('/')[1] ? fileOrBlob.type.split('/')[1].toLowerCase() : 'png'; |
|
fileName = 'processed_image.' + fileExtension; |
|
} |
|
|
|
|
|
if (type === 'background' && !isBlob && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExtension)) { |
|
originalFormat = fileExtension; |
|
} |
|
|
|
const reader = new FileReader(); |
|
reader.onload = function(event) { |
|
const img = new Image(); |
|
img.onload = function() { |
|
if (type === 'background') { |
|
backgroundImage = img; |
|
const aspectRatio = img.width / img.height; |
|
bgDrawProps.width = CANVAS_WIDTH; |
|
bgDrawProps.height = CANVAS_WIDTH / aspectRatio; |
|
bgDrawProps.x = 0; |
|
bgDrawProps.y = 0; |
|
canvasAdjustedHeight = bgDrawProps.height; |
|
canvas.height = canvasAdjustedHeight; |
|
canvas.style.height = 'auto'; |
|
} else if (type === 'overlay') { |
|
let imageToUse = img; |
|
|
|
|
|
|
|
|
|
|
|
if (fileExtension === 'png' || fileExtension === 'webp' || fileExtension === 'gif' || (isBlob && fileOrBlob.type === 'image/png')) { |
|
try { |
|
|
|
|
|
|
|
const croppedImageCanvas = cropTransparentPixels(img); |
|
if (croppedImageCanvas.width > 0 && croppedImageCanvas.height > 0) { |
|
imageToUse = croppedImageCanvas; |
|
} else { |
|
|
|
console.warn("No non-transparent pixels found, using original or empty."); |
|
|
|
const emptyCanvas = document.createElement('canvas'); |
|
emptyCanvas.width = img.naturalWidth || 1; |
|
emptyCanvas.height = img.naturalHeight || 1; |
|
imageToUse = emptyCanvas; |
|
} |
|
} catch (e) { |
|
console.error("Error during image cropping/processing, using original:", e); |
|
imageToUse = img; |
|
} |
|
} |
|
|
|
|
|
const newOverlay = { |
|
image: imageToUse, |
|
|
|
x: CANVAS_WIDTH / 2, |
|
y: canvasAdjustedHeight / 2, |
|
scale: 1, |
|
rotation: 0, |
|
filters: { ...ImageFilters.DEFAULT_FILTERS } |
|
}; |
|
|
|
|
|
const initialScaleRatio = Math.min(CANVAS_WIDTH / newOverlay.image.width, canvasAdjustedHeight / newOverlay.image.height); |
|
if (initialScaleRatio < 1) { |
|
newOverlay.scale = initialScaleRatio * 0.9; |
|
} |
|
|
|
overlays.push(newOverlay); |
|
activeOverlayIndex = overlays.length - 1; |
|
updateControlPanel(); |
|
updateLayersList(); |
|
} |
|
drawCanvas(); |
|
}; |
|
img.onerror = function() { |
|
console.error("Error loading image from reader result for:", fileName); |
|
alert("์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๋ ๋ฐ ์คํจํ์ต๋๋ค. ํ์ผ์ด ์ ํจํ์ง ํ์ธํด์ฃผ์ธ์."); |
|
}; |
|
img.src = event.target.result; |
|
}; |
|
reader.readAsDataURL(fileOrBlob); |
|
} |
|
|
|
|
|
function updateControlPanel() { |
|
const isActive = activeOverlayIndex >= 0; |
|
const activeOverlay = isActive ? overlays[activeOverlayIndex] : null; |
|
|
|
|
|
scaleSlider.disabled = !isActive; |
|
rotationSlider.disabled = !isActive; |
|
temperatureSlider.disabled = !isActive; |
|
brightnessSlider.disabled = !isActive; |
|
contrastSlider.disabled = !isActive; |
|
saturationSlider.disabled = !isActive; |
|
resetFilterBtn.disabled = !isActive; |
|
deleteLayerBtn.disabled = !isActive; |
|
|
|
if (isActive) { |
|
scaleSlider.value = Math.round(activeOverlay.scale * 100); |
|
scaleValue.textContent = scaleSlider.value + '%'; |
|
rotationSlider.value = Math.round(activeOverlay.rotation); |
|
rotationValue.textContent = rotationSlider.value + 'ยฐ'; |
|
|
|
temperatureSlider.value = activeOverlay.filters.temperature; |
|
temperatureValue.textContent = activeOverlay.filters.temperature; |
|
brightnessSlider.value = activeOverlay.filters.brightness; |
|
brightnessValue.textContent = activeOverlay.filters.brightness + '%'; |
|
contrastSlider.value = activeOverlay.filters.contrast; |
|
contrastValue.textContent = activeOverlay.filters.contrast + '%'; |
|
saturationSlider.value = activeOverlay.filters.saturation; |
|
saturationValue.textContent = activeOverlay.filters.saturation + '%'; |
|
} else { |
|
|
|
scaleSlider.value = 100; |
|
scaleValue.textContent = '100%'; |
|
rotationSlider.value = 0; |
|
rotationValue.textContent = '0ยฐ'; |
|
|
|
temperatureSlider.value = 0; |
|
temperatureValue.textContent = '0'; |
|
brightnessSlider.value = 100; |
|
brightnessValue.textContent = '100%'; |
|
contrastSlider.value = 100; |
|
contrastValue.textContent = '100%'; |
|
saturationSlider.value = 100; |
|
saturationValue.textContent = '100%'; |
|
} |
|
|
|
|
|
generateBtn.disabled = overlays.length === 0; |
|
downloadBtn.disabled = overlays.length === 0; |
|
|
|
|
|
if (overlays.length === 0) { |
|
previewContainer.style.display = 'none'; |
|
} |
|
} |
|
|
|
function updateLayersList() { |
|
if (!layersList) return; |
|
layersList.innerHTML = ''; |
|
|
|
|
|
if (deleteLayerBtn) { |
|
deleteLayerBtn.disabled = activeOverlayIndex < 0; |
|
} |
|
|
|
if (overlays.length === 0) { |
|
const emptyMsg = document.createElement('div'); |
|
emptyMsg.style.padding = '8px'; |
|
emptyMsg.style.textAlign = 'center'; |
|
emptyMsg.style.color = '#777'; |
|
emptyMsg.textContent = '๋ ์ด์ด๊ฐ ์์ต๋๋ค'; |
|
layersList.appendChild(emptyMsg); |
|
return; |
|
} |
|
|
|
|
|
for (let i = overlays.length - 1; i >= 0; i--) { |
|
const layerItem = document.createElement('div'); |
|
layerItem.className = 'layer-item'; |
|
if (i === activeOverlayIndex) { |
|
layerItem.classList.add('active'); |
|
} |
|
|
|
const layerName = document.createElement('span'); |
|
layerName.className = 'layer-name'; |
|
layerName.textContent = `๋ ์ด์ด ${i + 1}`; |
|
|
|
const layerControls = document.createElement('div'); |
|
layerControls.className = 'layer-controls'; |
|
|
|
|
|
if (i < overlays.length - 1) { |
|
const upButton = document.createElement('button'); |
|
upButton.className = 'layer-button'; |
|
upButton.textContent = 'โ'; |
|
upButton.title = '์๋ก ์ด๋'; |
|
upButton.addEventListener('click', (e) => { e.stopPropagation(); moveLayerUp(i); }); |
|
layerControls.appendChild(upButton); |
|
} |
|
|
|
|
|
if (i > 0) { |
|
const downButton = document.createElement('button'); |
|
downButton.className = 'layer-button'; |
|
downButton.textContent = 'โ'; |
|
downButton.title = '์๋๋ก ์ด๋'; |
|
downButton.addEventListener('click', (e) => { e.stopPropagation(); moveLayerDown(i); }); |
|
layerControls.appendChild(downButton); |
|
} |
|
|
|
|
|
const deleteButton = document.createElement('button'); |
|
deleteButton.className = 'delete-button'; |
|
deleteButton.textContent = 'ร'; |
|
deleteButton.title = '๋ ์ด์ด ์ญ์ (๋จ์ถํค: Del)'; |
|
deleteButton.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
|
|
activeOverlayIndex = i; |
|
deleteSelectedLayer(); |
|
}); |
|
layerControls.appendChild(deleteButton); |
|
|
|
|
|
layerItem.addEventListener('click', function() { |
|
activeOverlayIndex = i; |
|
updateControlPanel(); |
|
updateLayersList(); |
|
drawCanvas(); |
|
}); |
|
|
|
layerItem.appendChild(layerName); |
|
layerItem.appendChild(layerControls); |
|
|
|
layersList.appendChild(layerItem); |
|
} |
|
} |
|
|
|
|
|
function moveLayerUp(index) { |
|
if (index < overlays.length - 1) { |
|
|
|
const temp = overlays[index]; |
|
overlays[index] = overlays[index + 1]; |
|
overlays[index + 1] = temp; |
|
|
|
|
|
if (activeOverlayIndex === index) activeOverlayIndex = index + 1; |
|
else if (activeOverlayIndex === index + 1) activeOverlayIndex = index; |
|
|
|
updateLayersList(); |
|
drawCanvas(); |
|
} |
|
} |
|
|
|
|
|
function moveLayerDown(index) { |
|
if (index > 0) { |
|
|
|
const temp = overlays[index]; |
|
overlays[index] = overlays[index - 1]; |
|
overlays[index - 1] = temp; |
|
|
|
|
|
if (activeOverlayIndex === index) activeOverlayIndex = index - 1; |
|
else if (activeOverlayIndex === index - 1) activeOverlayIndex = index; |
|
|
|
|
|
updateLayersList(); |
|
drawCanvas(); |
|
} |
|
} |
|
|
|
|
|
function drawCanvas() { |
|
|
|
ctx.clearRect(0, 0, CANVAS_WIDTH, canvasAdjustedHeight); |
|
|
|
|
|
if (backgroundImage) { |
|
ctx.drawImage(backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height, bgDrawProps.x, bgDrawProps.y, bgDrawProps.width, bgDrawProps.height); |
|
} else { |
|
|
|
ctx.fillStyle = '#ffffff'; |
|
ctx.fillRect(0, 0, CANVAS_WIDTH, canvasAdjustedHeight); |
|
} |
|
|
|
|
|
overlays.forEach((overlay, index) => { |
|
|
|
if (!overlay.image || overlay.image.width === 0 || overlay.image.height === 0) return; |
|
|
|
|
|
const filteredCanvas = ImageFilters.createFilteredCanvas(overlay.image, overlay.filters); |
|
|
|
|
|
ctx.save(); |
|
|
|
|
|
ctx.translate(overlay.x, overlay.y); |
|
ctx.rotate(overlay.rotation * Math.PI / 180); |
|
ctx.scale(overlay.scale, overlay.scale); |
|
|
|
|
|
ctx.drawImage(filteredCanvas, -overlay.image.width / 2, -overlay.image.height / 2); |
|
|
|
ctx.restore(); |
|
|
|
|
|
if (index === activeOverlayIndex) { |
|
drawTransformHandles(overlay); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function drawTransformHandles(activeOverlay) { |
|
|
|
if (!activeOverlay || !activeOverlay.image || activeOverlay.image.width === 0 || activeOverlay.image.height === 0) return; |
|
|
|
const currentWidth = activeOverlay.image.width * activeOverlay.scale; |
|
const currentHeight = activeOverlay.image.height * activeOverlay.scale; |
|
const halfWidth = currentWidth / 2; |
|
const halfHeight = currentHeight / 2; |
|
|
|
const angle = activeOverlay.rotation * Math.PI / 180; |
|
|
|
|
|
const handles = { |
|
'tl': { x: -halfWidth, y: -halfHeight, cursor: 'nwse-resize' }, |
|
'tr': { x: halfWidth, y: -halfHeight, cursor: 'nesw-resize' }, |
|
'br': { x: halfWidth, y: halfHeight, cursor: 'nwse-resize' }, |
|
'bl': { x: -halfWidth, y: halfHeight, cursor: 'nesw-resize' } |
|
}; |
|
|
|
|
|
const rotateHandlePos = { x: 0, y: -halfHeight - rotateHandleDistance }; |
|
|
|
|
|
ctx.save(); |
|
|
|
|
|
ctx.translate(activeOverlay.x, activeOverlay.y); |
|
ctx.rotate(angle); |
|
|
|
|
|
const cornerHandleColor = '#e74c3c'; |
|
for (const [, handle] of Object.entries(handles)) { |
|
ctx.beginPath(); |
|
|
|
ctx.rect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize); |
|
ctx.fillStyle = cornerHandleColor; |
|
ctx.fill(); |
|
} |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(0, -halfHeight); |
|
ctx.lineTo(rotateHandlePos.x, rotateHandlePos.y); |
|
ctx.strokeStyle = '#3498db'; |
|
ctx.lineWidth = 2; |
|
ctx.stroke(); |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.arc(rotateHandlePos.x, rotateHandlePos.y, handleSize / 2, 0, Math.PI * 2); |
|
ctx.fillStyle = '#3498db'; |
|
ctx.fill(); |
|
|
|
ctx.restore(); |
|
} |
|
|
|
|
|
|
|
function getHandleAtPosition(canvasX, canvasY, activeOverlay) { |
|
|
|
if (!activeOverlay || !activeOverlay.image || activeOverlay.image.width === 0 || activeOverlay.image.height === 0) return null; |
|
|
|
const currentWidth = activeOverlay.image.width * activeOverlay.scale; |
|
const currentHeight = activeOverlay.image.height * activeOverlay.scale; |
|
const halfWidth = currentWidth / 2; |
|
const halfHeight = currentHeight / 2; |
|
|
|
|
|
const dx = canvasX - activeOverlay.x; |
|
const dy = canvasY - activeOverlay.y; |
|
const angle = -activeOverlay.rotation * Math.PI / 180; |
|
const cos = Math.cos(angle); |
|
const sin = Math.sin(angle); |
|
|
|
|
|
const rotatedX = dx * cos - dy * sin; |
|
const rotatedY = dx * sin + dy * cos; |
|
|
|
|
|
|
|
const handles = { |
|
'tl': { x: -halfWidth, y: -halfHeight, cursor: 'nwse-resize' }, |
|
'tr': { x: halfWidth, y: -halfHeight, cursor: 'nesw-resize' }, |
|
'br': { x: halfWidth, y: halfHeight, cursor: 'nwse-resize' }, |
|
'bl': { x: -halfWidth, y: halfHeight, cursor: 'nesw-resize' } |
|
}; |
|
|
|
|
|
for (const [key, handle] of Object.entries(handles)) { |
|
const dist = Math.sqrt(Math.pow(rotatedX - handle.x, 2) + Math.pow(rotatedY - handle.y, 2)); |
|
|
|
if (dist <= handleSize / 2 + clickPadding) { |
|
return { id: key, cursor: handle.cursor }; |
|
} |
|
} |
|
|
|
|
|
const rotateHandlePos = { x: 0, y: -halfHeight - rotateHandleDistance }; |
|
const rotateDist = Math.sqrt(Math.pow(rotatedX - rotateHandlePos.x, 2) + Math.pow(rotatedY - rotateHandlePos.y, 2)); |
|
|
|
|
|
if (rotateDist <= handleSize / 2 + clickPadding) { |
|
return { id: 'rotate', cursor: 'grab' }; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
|
|
|
|
function isOverOverlayImage(canvasX, canvasY, overlay) { |
|
|
|
if (!overlay || !overlay.image || overlay.image.width === 0 || overlay.image.height === 0) return false; |
|
|
|
const currentWidth = overlay.image.width * overlay.scale; |
|
const currentHeight = overlay.image.height * overlay.scale; |
|
const halfWidth = currentWidth / 2; |
|
const halfHeight = currentHeight / 2; |
|
|
|
|
|
const dx = canvasX - overlay.x; |
|
const dy = canvasY - overlay.y; |
|
const angle = -overlay.rotation * Math.PI / 180; |
|
const cos = Math.cos(angle); |
|
const sin = Math.sin(angle); |
|
|
|
|
|
const rotatedX = dx * cos - dy * sin; |
|
const rotatedY = dx * sin + dy * cos; |
|
|
|
|
|
return (rotatedX >= -halfWidth - clickPadding && rotatedX <= halfWidth + clickPadding && |
|
rotatedY >= -halfHeight - clickPadding && rotatedY <= halfHeight + clickPadding); |
|
} |
|
|
|
|
|
|
|
function updateCursor(canvasX, canvasY) { |
|
|
|
if (isDragging || isRotating || isResizing) return; |
|
|
|
let cursor = 'default'; |
|
let handled = false; |
|
|
|
|
|
if (activeOverlayIndex >= 0) { |
|
const activeOverlay = overlays[activeOverlayIndex]; |
|
const handle = getHandleAtPosition(canvasX, canvasY, activeOverlay); |
|
if (handle) { |
|
cursor = handle.cursor; |
|
handled = true; |
|
} |
|
} |
|
|
|
|
|
if (!handled) { |
|
for (let i = overlays.length - 1; i >= 0; i--) { |
|
if (isOverOverlayImage(canvasX, canvasY, overlays[i])) { |
|
cursor = 'move'; |
|
handled = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
canvas.style.cursor = cursor; |
|
} |
|
|
|
|
|
|
|
function generateMergedImage() { |
|
|
|
if (overlays.length === 0) { |
|
alert('ํฉ์ฑํ ์ค๋ฒ๋ ์ด ์ด๋ฏธ์ง๊ฐ ์์ต๋๋ค.'); |
|
return null; |
|
} |
|
|
|
|
|
const resultCanvas = document.createElement('canvas'); |
|
resultCanvas.width = CANVAS_WIDTH; |
|
resultCanvas.height = canvasAdjustedHeight; |
|
const resultCtx = resultCanvas.getContext('2d'); |
|
|
|
|
|
resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); |
|
if (backgroundImage) { |
|
resultCtx.drawImage(backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height, bgDrawProps.x, bgDrawProps.y, bgDrawProps.width, bgDrawProps.height); |
|
} else { |
|
resultCtx.fillStyle = '#ffffff'; |
|
resultCtx.fillRect(0, 0, CANVAS_WIDTH, canvasAdjustedHeight); |
|
} |
|
|
|
|
|
overlays.forEach(overlay => { |
|
if (!overlay.image || overlay.image.width === 0 || overlay.image.height === 0) return; |
|
|
|
|
|
const filteredCanvas = ImageFilters.createFilteredCanvas(overlay.image, overlay.filters); |
|
|
|
resultCtx.save(); |
|
|
|
|
|
resultCtx.translate(overlay.x, overlay.y); |
|
resultCtx.rotate(overlay.rotation * Math.PI / 180); |
|
resultCtx.scale(overlay.scale, overlay.scale); |
|
|
|
|
|
resultCtx.drawImage(filteredCanvas, -overlay.image.width / 2, -overlay.image.height / 2); |
|
|
|
resultCtx.restore(); |
|
}); |
|
|
|
|
|
previewImg.src = resultCanvas.toDataURL(`image/${originalFormat === 'jpg' ? 'jpeg' : originalFormat}`); |
|
previewContainer.style.display = 'flex'; |
|
|
|
return resultCanvas; |
|
} |
|
|
|
|
|
function deleteSelectedLayer() { |
|
if (activeOverlayIndex >= 0) { |
|
|
|
if (confirm(`๋ ์ด์ด ${activeOverlayIndex + 1}์(๋ฅผ) ์ญ์ ํ์๊ฒ ์ต๋๊น?`)) { |
|
|
|
overlays.splice(activeOverlayIndex, 1); |
|
activeOverlayIndex = -1; |
|
|
|
updateControlPanel(); |
|
updateLayersList(); |
|
drawCanvas(); |
|
|
|
|
|
if (overlays.length === 0) { |
|
previewContainer.style.display = 'none'; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function getCanvasCoordinates(clientX, clientY) { |
|
const rect = canvas.getBoundingClientRect(); |
|
const scaleX = canvas.width / rect.width; |
|
const scaleY = canvas.height / rect.height; |
|
|
|
|
|
const x = (clientX - rect.left) * scaleX; |
|
const y = (clientY - rect.top) * scaleY; |
|
|
|
return { x, y }; |
|
} |
|
|
|
|
|
|
|
function applyCurrentFilters() { |
|
if (activeOverlayIndex < 0) return; |
|
|
|
const filterValues = { |
|
temperature: parseInt(temperatureSlider.value), |
|
brightness: parseInt(brightnessSlider.value), |
|
contrast: parseInt(contrastSlider.value), |
|
saturation: parseInt(saturationSlider.value) |
|
}; |
|
|
|
|
|
ImageFilters.applyFilterToLayer(overlays, activeOverlayIndex, filterValues); |
|
|
|
drawCanvas(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
backgroundInput.addEventListener('change', function(e) { |
|
if (e.target.files.length > 0) { |
|
loadImage(e.target.files[0], 'background'); |
|
} |
|
}); |
|
|
|
|
|
overlayInput.addEventListener('change', function(e) { |
|
if (e.target.files.length > 0) { |
|
loadImage(e.target.files[0], 'overlay'); |
|
this.value = ''; |
|
} |
|
}); |
|
|
|
|
|
const exampleImagesContainer = document.getElementById('example-images-container'); |
|
if (exampleImagesContainer) { |
|
exampleImagesContainer.querySelectorAll('.example-img').forEach(img => { |
|
img.addEventListener('click', function() { |
|
const imageUrl = this.src; |
|
const fileName = this.getAttribute('data-filename') || 'example_image.png'; |
|
|
|
|
|
const exampleImgElement = new Image(); |
|
exampleImgElement.crossOrigin = 'anonymous'; |
|
exampleImgElement.onload = function() { |
|
|
|
|
|
|
|
|
|
|
|
let processedImageSource = exampleImgElement; |
|
|
|
try { |
|
|
|
const croppedCanvas = cropTransparentPixels(exampleImgElement); |
|
if (croppedCanvas.width > 0 && croppedCanvas.height > 0) { |
|
processedImageSource = croppedCanvas; |
|
} else { |
|
console.warn("cropTransparentPixels resulted in empty canvas, using original image."); |
|
processedImageSource = exampleImgElement; |
|
} |
|
} catch (e) { |
|
console.error("Error during cropTransparentPixels, using original image:", e); |
|
processedImageSource = exampleImgElement; |
|
} |
|
|
|
|
|
|
|
|
|
const canvasToProcess = processedImageSource instanceof HTMLCanvasElement ? processedImageSource : null; |
|
|
|
if (canvasToProcess) { |
|
canvasToProcess.toBlob(function(blob) { |
|
if (blob) { |
|
|
|
loadImage(blob, 'overlay'); |
|
} else { |
|
console.error("Failed to create blob from processed canvas."); |
|
alert("์ด๋ฏธ์ง ์ฒ๋ฆฌ์ ์คํจํ์ต๋๋ค."); |
|
} |
|
}, 'image/png'); |
|
} else if (processedImageSource instanceof HTMLImageElement) { |
|
|
|
|
|
const tempCanvas = document.createElement('canvas'); |
|
tempCanvas.width = processedImageSource.width; |
|
tempCanvas.height = processedImageSource.height; |
|
const tempCtx = tempCanvas.getContext('2d'); |
|
tempCtx.drawImage(processedImageSource, 0, 0); |
|
tempCanvas.toBlob(function(blob) { |
|
if (blob) { |
|
loadImage(blob, 'overlay'); |
|
} else { |
|
console.error("Failed to create blob from fallback image."); |
|
alert("์ด๋ฏธ์ง ์ฒ๋ฆฌ์ ์คํจํ์ต๋๋ค."); |
|
} |
|
}, 'image/png'); |
|
} else { |
|
console.error("Processed image source is neither Canvas nor Image."); |
|
alert("์ด๋ฏธ์ง ์ฒ๋ฆฌ์ ์คํจํ์ต๋๋ค."); |
|
} |
|
|
|
}; |
|
exampleImgElement.onerror = function() { |
|
console.error("Error loading example image:", imageUrl); |
|
alert("์์ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๋ ๋ฐ ์คํจํ์ต๋๋ค."); |
|
}; |
|
exampleImgElement.src = imageUrl; |
|
}); |
|
}); |
|
} |
|
|
|
|
|
|
|
document.addEventListener('keydown', function(e) { |
|
|
|
if (e.key === 'Delete' && activeOverlayIndex >= 0) { |
|
e.preventDefault(); |
|
deleteSelectedLayer(); |
|
} |
|
}); |
|
|
|
|
|
deleteLayerBtn.addEventListener('click', function() { |
|
if (activeOverlayIndex >= 0) { |
|
deleteSelectedLayer(); |
|
} |
|
}); |
|
|
|
|
|
scaleSlider.addEventListener('input', function() { |
|
if (activeOverlayIndex >= 0) { |
|
overlays[activeOverlayIndex].scale = parseInt(this.value) / 100; |
|
scaleValue.textContent = this.value + '%'; |
|
drawCanvas(); |
|
} |
|
}); |
|
|
|
|
|
rotationSlider.addEventListener('input', function() { |
|
if (activeOverlayIndex >= 0) { |
|
overlays[activeOverlayIndex].rotation = parseInt(this.value); |
|
rotationValue.textContent = overlays[activeOverlayIndex].rotation + 'ยฐ'; |
|
drawCanvas(); |
|
} |
|
}); |
|
|
|
|
|
temperatureSlider.addEventListener('input', function() { temperatureValue.textContent = this.value; applyCurrentFilters(); }); |
|
brightnessSlider.addEventListener('input', function() { brightnessValue.textContent = this.value + '%'; applyCurrentFilters(); }); |
|
contrastSlider.addEventListener('input', function() { contrastValue.textContent = this.value + '%'; applyCurrentFilters(); }); |
|
saturationSlider.addEventListener('input', function() { saturationValue.textContent = this.value + '%'; applyCurrentFilters(); }); |
|
|
|
|
|
resetFilterBtn.addEventListener('click', function() { |
|
|
|
const defaultFilters = ImageFilters.resetFilters(overlays, activeOverlayIndex); |
|
|
|
temperatureSlider.value = defaultFilters.temperature; temperatureValue.textContent = defaultFilters.temperature; |
|
brightnessSlider.value = defaultFilters.brightness; brightnessValue.textContent = defaultFilters.brightness + '%'; |
|
contrastSlider.value = defaultFilters.contrast; contrastValue.textContent = defaultFilters.contrast + '%'; |
|
saturationSlider.value = defaultFilters.saturation; saturationValue.textContent = defaultFilters.saturation + '%'; |
|
drawCanvas(); |
|
}); |
|
|
|
|
|
|
|
canvas.addEventListener('mousedown', function(e) { |
|
|
|
if (e.button !== 0) return; |
|
|
|
const { x, y } = getCanvasCoordinates(e.clientX, e.clientY); |
|
const isAltPressed = e.altKey; |
|
|
|
let clickedOverlayIndex = -1; |
|
let handle = null; |
|
|
|
|
|
if (activeOverlayIndex >= 0) { |
|
handle = getHandleAtPosition(x, y, overlays[activeOverlayIndex]); |
|
if (handle) { |
|
clickedOverlayIndex = activeOverlayIndex; |
|
} |
|
} |
|
|
|
|
|
if (!handle) { |
|
let overlaysAtPosition = []; |
|
|
|
for (let i = 0; i < overlays.length; i++) { |
|
if (isOverOverlayImage(x, y, overlays[i])) { |
|
overlaysAtPosition.push(i); |
|
} |
|
} |
|
|
|
if (overlaysAtPosition.length > 0) { |
|
|
|
if (isAltPressed && overlaysAtPosition.length > 1) { |
|
|
|
if (activeOverlayIndex >= 0) { |
|
const currentIndexInAtPosition = overlaysAtPosition.indexOf(activeOverlayIndex); |
|
if (currentIndexInAtPosition !== -1) { |
|
const nextIndexInAtPosition = (currentIndexInAtPosition + 1) % overlaysAtPosition.length; |
|
clickedOverlayIndex = overlaysAtPosition[nextIndexInAtPosition]; |
|
} else { |
|
|
|
clickedOverlayIndex = overlaysAtPosition[overlaysAtPosition.length - 1]; |
|
} |
|
} else { |
|
|
|
clickedOverlayIndex = overlaysAtPosition[overlaysAtPosition.length - 1]; |
|
} |
|
} else { |
|
|
|
clickedOverlayIndex = overlaysAtPosition[overlaysAtPosition.length - 1]; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
if (clickedOverlayIndex >= 0) { |
|
const activeOverlay = overlays[clickedOverlayIndex]; |
|
|
|
if (activeOverlayIndex !== clickedOverlayIndex) { |
|
activeOverlayIndex = clickedOverlayIndex; |
|
updateControlPanel(); |
|
updateLayersList(); |
|
} |
|
|
|
|
|
if (handle) { |
|
if (handle.id === 'rotate') { |
|
isRotating = true; |
|
canvas.style.cursor = 'grabbing'; |
|
} else { |
|
isResizing = true; |
|
activeHandle = handle.id; |
|
|
|
const dxMouse = x - activeOverlay.x; |
|
const dyMouse = y - activeOverlay.y; |
|
resizeStartDistance = Math.sqrt(dxMouse * dxMouse + dyMouse * dyMouse); |
|
if (resizeStartDistance < 10) resizeStartDistance = 10; |
|
} |
|
} else { |
|
|
|
isDragging = true; |
|
canvas.style.cursor = 'grabbing'; |
|
} |
|
} else { |
|
|
|
activeOverlayIndex = -1; |
|
updateControlPanel(); |
|
updateLayersList(); |
|
} |
|
|
|
|
|
lastX = x; |
|
lastY = y; |
|
|
|
drawCanvas(); |
|
}); |
|
|
|
canvas.addEventListener('mousemove', function(e) { |
|
const { x, y } = getCanvasCoordinates(e.clientX, e.clientY); |
|
|
|
|
|
if (!isDragging && !isRotating && !isResizing) { |
|
updateCursor(x, y); |
|
} else if (activeOverlayIndex >= 0) { |
|
const activeOverlay = overlays[activeOverlayIndex]; |
|
canvas.style.cursor = 'grabbing'; |
|
|
|
if (isRotating) { |
|
|
|
const dx = x - activeOverlay.x; |
|
const dy = y - activeOverlay.y; |
|
let angle = Math.atan2(dy, dx) * 180 / Math.PI + 90; |
|
angle = (angle % 360 + 360) % 360; |
|
activeOverlay.rotation = angle; |
|
|
|
|
|
rotationSlider.value = Math.round(activeOverlay.rotation); |
|
rotationValue.textContent = Math.round(activeOverlay.rotation) + 'ยฐ'; |
|
|
|
} else if (isResizing && activeOverlay.image) { |
|
|
|
const dxMouse = x - activeOverlay.x; |
|
const dyMouse = y - activeOverlay.y; |
|
const currentDistance = Math.sqrt(dxMouse * dxMouse + dyMouse * dyMouse); |
|
|
|
if (resizeStartDistance > 0 && currentDistance > 0) { |
|
|
|
let newScale = activeOverlay.scale * (currentDistance / resizeStartDistance); |
|
activeOverlay.scale = Math.max(0.01, newScale); |
|
resizeStartDistance = currentDistance; |
|
} |
|
|
|
|
|
const scalePercent = Math.round(activeOverlay.scale * 100); |
|
scaleSlider.value = scalePercent; |
|
scaleValue.textContent = scalePercent + '%'; |
|
|
|
} else if (isDragging && activeOverlay.image) { |
|
|
|
const dx = x - lastX; |
|
const dy = y - lastY; |
|
|
|
activeOverlay.x += dx; |
|
activeOverlay.y += dy; |
|
} |
|
|
|
|
|
if (isRotating || isResizing || isDragging) { |
|
drawCanvas(); |
|
} |
|
} |
|
|
|
|
|
lastX = x; |
|
lastY = y; |
|
}); |
|
|
|
|
|
window.addEventListener('mouseup', function(e) { |
|
isDragging = false; |
|
isRotating = false; |
|
isResizing = false; |
|
activeHandle = null; |
|
|
|
|
|
if (e.clientX !== undefined && e.clientY !== undefined) { |
|
const { x, y } = getCanvasCoordinates(e.clientX, e.clientY); |
|
updateCursor(x, y); |
|
} else { |
|
|
|
canvas.style.cursor = 'default'; |
|
} |
|
}); |
|
|
|
|
|
|
|
canvas.addEventListener('touchstart', function(e) { |
|
e.preventDefault(); |
|
if (e.touches.length > 0) { |
|
const touch = e.touches[0]; |
|
|
|
const mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY, buttons: 1 }); |
|
canvas.dispatchEvent(mouseEvent); |
|
} |
|
}, { passive: false }); |
|
|
|
canvas.addEventListener('touchmove', function(e) { |
|
e.preventDefault(); |
|
if (e.touches.length > 0) { |
|
const touch = e.touches[0]; |
|
|
|
const mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY, buttons: 1 }); |
|
canvas.dispatchEvent(mouseEvent); |
|
} |
|
}, { passive: false }); |
|
|
|
canvas.addEventListener('touchend', function(e) { |
|
e.preventDefault(); |
|
|
|
const mouseEvent = new MouseEvent('mouseup', {}); |
|
window.dispatchEvent(mouseEvent); |
|
}, { passive: false }); |
|
|
|
|
|
|
|
generateBtn.addEventListener('click', function() { |
|
generateMergedImage(); |
|
|
|
|
|
}); |
|
|
|
|
|
resetAllBtn.addEventListener('click', function() { |
|
backgroundImage = null; |
|
overlays = []; |
|
activeOverlayIndex = -1; |
|
backgroundInput.value = ''; |
|
overlayInput.value = ''; |
|
|
|
updateControlPanel(); |
|
updateLayersList(); |
|
|
|
|
|
const defaultFilters = ImageFilters.resetFilters(overlays, activeOverlayIndex); |
|
temperatureSlider.value = defaultFilters.temperature; temperatureValue.textContent = defaultFilters.temperature; |
|
brightnessSlider.value = defaultFilters.brightness; brightnessValue.textContent = defaultFilters.brightness + '%'; |
|
contrastSlider.value = defaultFilters.contrast; contrastValue.textContent = defaultFilters.contrast + '%'; |
|
saturationSlider.value = defaultFilters.saturation; saturationValue.textContent = defaultFilters.saturation + '%'; |
|
|
|
|
|
canvasAdjustedHeight = CANVAS_HEIGHT; |
|
canvas.height = canvasAdjustedHeight; |
|
initCanvas(); |
|
|
|
previewContainer.style.display = 'none'; |
|
}); |
|
|
|
|
|
downloadBtn.addEventListener('click', function() { |
|
const resultCanvas = generateMergedImage(); |
|
if (!resultCanvas) return; |
|
|
|
const link = document.createElement('a'); |
|
|
|
const timestamp = new Date().toISOString().replace(/[-:.]/g, '').substring(0, 14); |
|
|
|
const mimeType = originalFormat === 'jpg' ? 'image/jpeg' : `image/${originalFormat}`; |
|
const fileExt = originalFormat; |
|
|
|
link.download = `merged_image_${timestamp}.${fileExt}`; |
|
|
|
link.href = resultCanvas.toDataURL(mimeType, 1.0); |
|
|
|
link.click(); |
|
}); |
|
|
|
|
|
|
|
initCanvas(); |
|
updateControlPanel(); |
|
|
|
}); |