Spaces:
Running
Running
| // PixelForge Studio - Main JavaScript | |
| // Canvas and context | |
| let canvas = document.getElementById('photo-canvas'); | |
| let ctx = canvas.getContext('2d'); | |
| let isDrawing = false; | |
| let currentTool = 'brush'; | |
| let currentColor = '#000000'; | |
| let brushSize = 5; | |
| let zoomLevel = 1; | |
| let history = []; | |
| let historyStep = -1; | |
| let layers = []; | |
| let activeLayer = 0; | |
| // Initialize canvas with white background | |
| function initCanvas() { | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| saveHistory(); | |
| } | |
| // Tool selection | |
| document.querySelectorAll('.tool-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active')); | |
| this.classList.add('active'); | |
| currentTool = this.dataset.tool; | |
| updateCursor(); | |
| }); | |
| }); | |
| // Update cursor based on tool | |
| function updateCursor() { | |
| switch(currentTool) { | |
| case 'brush': | |
| case 'eraser': | |
| canvas.style.cursor = 'crosshair'; | |
| break; | |
| case 'move': | |
| canvas.style.cursor = 'move'; | |
| break; | |
| case 'eyedropper': | |
| canvas.style.cursor = 'copy'; | |
| break; | |
| case 'text': | |
| canvas.style.cursor = 'text'; | |
| break; | |
| case 'zoom': | |
| canvas.style.cursor = 'zoom-in'; | |
| break; | |
| default: | |
| canvas.style.cursor = 'default'; | |
| } | |
| } | |
| // Drawing functions | |
| canvas.addEventListener('mousedown', startDrawing); | |
| canvas.addEventListener('mousemove', draw); | |
| canvas.addEventListener('mouseup', stopDrawing); | |
| canvas.addEventListener('mouseout', stopDrawing); | |
| function startDrawing(e) { | |
| if (currentTool === 'brush' || currentTool === 'eraser') { | |
| isDrawing = true; | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| ctx.beginPath(); | |
| ctx.moveTo(x, y); | |
| } | |
| } | |
| function draw(e) { | |
| if (!isDrawing) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| if (currentTool === 'brush') { | |
| ctx.globalCompositeOperation = 'source-over'; | |
| ctx.strokeStyle = currentColor; | |
| ctx.lineWidth = brushSize; | |
| ctx.lineCap = 'round'; | |
| ctx.lineJoin = 'round'; | |
| ctx.lineTo(x, y); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(x, y); | |
| } else if (currentTool === 'eraser') { | |
| ctx.globalCompositeOperation = 'destination-out'; | |
| ctx.lineWidth = brushSize * 2; | |
| ctx.lineCap = 'round'; | |
| ctx.lineJoin = 'round'; | |
| ctx.lineTo(x, y); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(x, y); | |
| } | |
| updateCursorPosition(e); | |
| } | |
| function stopDrawing() { | |
| if (isDrawing) { | |
| isDrawing = false; | |
| ctx.beginPath(); | |
| saveHistory(); | |
| } | |
| } | |
| // Color picker functionality | |
| let rSlider = document.getElementById('color-r'); | |
| let gSlider = document.getElementById('color-g'); | |
| let bSlider = document.getElementById('color-b'); | |
| let rVal = document.getElementById('r-val'); | |
| let gVal = document.getElementById('g-val'); | |
| let bVal = document.getElementById('b-val'); | |
| let fgColor = document.getElementById('fg-color'); | |
| function updateColor() { | |
| const r = rSlider.value; | |
| const g = gSlider.value; | |
| const b = bSlider.value; | |
| currentColor = `rgb(${r}, ${g}, ${b})`; | |
| fgColor.style.backgroundColor = currentColor; | |
| rVal.textContent = r; | |
| gVal.textContent = g; | |
| bVal.textContent = b; | |
| } | |
| rSlider.addEventListener('input', updateColor); | |
| gSlider.addEventListener('input', updateColor); | |
| bSlider.addEventListener('input', updateColor); | |
| // Opacity control | |
| let opacitySlider = document.getElementById('opacity'); | |
| let opacityVal = document.getElementById('opacity-val'); | |
| opacitySlider.addEventListener('input', function() { | |
| ctx.globalAlpha = this.value / 100; | |
| opacityVal.textContent = this.value + '%'; | |
| }); | |
| // Zoom controls | |
| document.getElementById('zoom-in').addEventListener('click', function() { | |
| if (zoomLevel < 3) { | |
| zoomLevel += 0.25; | |
| updateZoom(); | |
| } | |
| }); | |
| document.getElementById('zoom-out').addEventListener('click', function() { | |
| if (zoomLevel > 0.25) { | |
| zoomLevel -= 0.25; | |
| updateZoom(); | |
| } | |
| }); | |
| function updateZoom() { | |
| let mainCanvas = document.getElementById('main-canvas'); | |
| mainCanvas.style.transform = `scale(${zoomLevel})`; | |
| document.getElementById('zoom-level').textContent = Math.round(zoomLevel * 100) + '%'; | |
| } | |
| // History management | |
| function saveHistory() { | |
| historyStep++; | |
| if (historyStep < history.length) { | |
| history.length = historyStep; | |
| } | |
| history.push(canvas.toDataURL()); | |
| } | |
| function undo() { | |
| if (historyStep > 0) { | |
| historyStep--; | |
| restoreCanvas(); | |
| } | |
| } | |
| function redo() { | |
| if (historyStep < history.length - 1) { | |
| historyStep++; | |
| restoreCanvas(); | |
| } | |
| } | |
| function restoreCanvas() { | |
| let img = new Image(); | |
| img.src = history[historyStep]; | |
| img.onload = function() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| }; | |
| } | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', function(e) { | |
| if (e.ctrlKey || e.metaKey) { | |
| switch(e.key) { | |
| case 'z': | |
| e.preventDefault(); | |
| undo(); | |
| break; | |
| case 'y': | |
| e.preventDefault(); | |
| redo(); | |
| break; | |
| case 's': | |
| e.preventDefault(); | |
| saveImage(); | |
| break; | |
| case 'o': | |
| e.preventDefault(); | |
| openImage(); | |
| break; | |
| case 'n': | |
| e.preventDefault(); | |
| document.getElementById('new-doc-modal').classList.remove('hidden'); | |
| break; | |
| } | |
| } | |
| // Tool shortcuts | |
| switch(e.key) { | |
| case 'b': | |
| document.querySelector('[data-tool="brush"]').click(); | |
| break; | |
| case 'e': | |
| document.querySelector('[data-tool="eraser"]').click(); | |
| break; | |
| case 'm': | |
| document.querySelector('[data-tool="move"]').click(); | |
| break; | |
| case 't': | |
| document.querySelector('[data-tool="text"]').click(); | |
| break; | |
| case 'c': | |
| document.querySelector('[data-tool="crop"]').click(); | |
| break; | |
| case 'i': | |
| document.querySelector('[data-tool="eyedropper"]').click(); | |
| break; | |
| case 'w': | |
| document.querySelector('[data-tool="wand"]').click(); | |
| break; | |
| case 'g': | |
| document.querySelector('[data-tool="gradient"]').click(); | |
| break; | |
| case 'z': | |
| if (!e.ctrlKey && !e.metaKey) { | |
| document.querySelector('[data-tool="zoom"]').click(); | |
| } | |
| break; | |
| } | |
| }); | |
| // Image save and load | |
| function saveImage() { | |
| const link = document.createElement('a'); | |
| link.download = 'pixelforge-image.png'; | |
| link.href = canvas.toDataURL(); | |
| link.click(); | |
| } | |
| function openImage() { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.accept = 'image/*'; | |
| input.onchange = function(e) { | |
| const file = e.target.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| ctx.drawImage(img, 0, 0); | |
| saveHistory(); | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }; | |
| input.click(); | |
| } | |
| // Update cursor position | |
| function updateCursorPosition(e) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = Math.round(e.clientX - rect.left); | |
| const y = Math.round(e.clientY - rect.top); | |
| document.getElementById('cursor-position').textContent = `X: ${x}, Y: ${y}`; | |
| } | |
| canvas.addEventListener('mousemove', updateCursorPosition); | |
| // Layer management | |
| function addLayer() { | |
| const layersList = document.getElementById('layers-list'); | |
| const layerCount = layersList.children.length; | |
| const layerItem = document.createElement('div'); | |
| layerItem.className = 'layer-item bg-gray-700 rounded p-2 mb-2 flex items-center space-x-2 cursor-pointer'; | |
| layerItem.innerHTML = ` | |
| <i data-feather="eye" class="w-4 h-4"></i> | |
| <i data-feather="unlock" class="w-4 h-4"></i> | |
| <div class="w-8 h-8 bg-gray-600 rounded"></div> | |
| <span class="flex-1 text-sm">Layer ${layerCount}</span> | |
| <span class="text-xs text-gray-400">100%</span> | |
| `; | |
| layersList.insertBefore(layerItem, layersList.firstChild); | |
| feather.replace(); | |
| // Add click handler for layer selection | |
| layerItem.addEventListener('click', function() { | |
| document.querySelectorAll('.layer-item').forEach(item => { | |
| item.classList.remove('active'); | |
| }); | |
| this.classList.add('active'); | |
| activeLayer = layerCount; | |
| }); | |
| } | |
| // Modal functions | |
| function closeModal() { | |
| document.getElementById('new-doc-modal').classList.add('hidden'); | |
| } | |
| // Fill slider | |
| document.getElementById('fill').addEventListener('input', function() { | |
| document.getElementById('fill-val').textContent = this.value + '%'; | |
| }); | |
| // Eyedropper tool | |
| canvas.addEventListener('click', function(e) { | |
| if (currentTool === 'eyedropper') { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const pixel = ctx.getImageData(x, y, 1, 1).data; | |
| rSlider.value = pixel[0]; | |
| gSlider.value = pixel[1]; | |
| bSlider.value = pixel[2]; | |
| updateColor(); | |
| } | |
| }); | |
| // Text tool | |
| canvas.addEventListener('click', function(e) { | |
| if (currentTool === 'text') { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const text = prompt('Enter text:'); | |
| if (text) { | |
| ctx.font = '20px Arial'; | |
| ctx.fillStyle = currentColor; | |
| ctx.fillText(text, x, y); | |
| saveHistory(); | |
| } | |
| } | |
| }); | |
| // Initialize | |
| initCanvas(); | |
| document.querySelector('[data-tool="brush"]').classList.add('active'); | |
| // Add initial layer | |
| layers.push({ name: 'Background', visible: true, opacity: 1 }); | |
| // Drag and drop support | |
| document.body.addEventListener('dragover', function(e) { | |
| e.preventDefault(); | |
| document.body.classList.add('dragover'); | |
| }); | |
| document.body.addEventListener('dragleave', function() { | |
| document.body.classList.remove('dragover'); | |
| }); | |
| document.body.addEventListener('drop', function(e) { | |
| e.preventDefault(); | |
| document.body.classList.remove('dragover'); | |
| const file = e.dataTransfer.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| ctx.drawImage(img, 0, 0); | |
| saveHistory(); | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| // Filter functions | |
| function applyBlur() { | |
| ctx.filter = 'blur(5px)'; | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| ctx.putImageData(imageData, 0, 0); | |
| ctx.filter = 'none'; | |
| saveHistory(); | |
| } | |
| function applyGrayscale() { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114; | |
| data[i] = gray; | |
| data[i + 1] = gray; | |
| data[i + 2] = gray; | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| saveHistory(); | |
| } | |
| function applyInvert() { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| data[i] = 255 - data[i]; | |
| data[i + 1] = 255 - data[i + 1]; | |
| data[i + 2] = 255 - data[i + 2]; | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| saveHistory(); | |
| } | |
| // Brush size control (mouse wheel) | |
| canvas.addEventListener('wheel', function(e) { | |
| if (e.ctrlKey) { | |
| e.preventDefault(); | |
| if (e.deltaY < 0) { | |
| brushSize = Math.min(brushSize + 1, 50); | |
| } else { | |
| brushSize = Math.max(brushSize - 1, 1); | |
| } | |
| console.log('Brush size:', brushSize); | |
| } | |
| }); | |
| // Touch support for mobile devices | |
| let touchDrawing = false; | |
| canvas.addEventListener('touchstart', function(e) { | |
| if (e.touches.length === 1) { | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousedown', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| canvas.dispatchEvent(mouseEvent); | |
| touchDrawing = true; | |
| } | |
| }); | |
| canvas.addEventListener('touchmove', function(e) { | |
| if (touchDrawing && e.touches.length === 1) { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousemove', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| canvas.dispatchEvent(mouseEvent); | |
| } | |
| }); | |
| canvas.addEventListener('touchend', function(e) { | |
| const mouseEvent = new MouseEvent('mouseup', {}); | |
| canvas.dispatchEvent(mouseEvent); | |
| touchDrawing = false; | |
| }); |