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