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