Spaces:
Running
Running
| <html> | |
| <style> | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100vh; | |
| margin: 0; | |
| background-color: #f0f0f0; | |
| font-family: Arial, sans-serif; | |
| } | |
| .container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| max-width: 800px; | |
| width: 100%; | |
| } | |
| .canvas-container { | |
| position: relative; | |
| width: 100%; | |
| } | |
| canvas { | |
| border: 1px solid #000; | |
| cursor: crosshair; | |
| background-color: white; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
| width: 100%; | |
| height: auto; | |
| } | |
| .toolbar { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| background-color: #ffffff; | |
| padding: 12px; | |
| border-radius: 5px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
| } | |
| .tool-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px; | |
| border-radius: 4px; | |
| background-color: #f7f7f7; | |
| flex-wrap: wrap; | |
| } | |
| button { | |
| padding: 8px 12px; | |
| font-size: 14px; | |
| border: none; | |
| border-radius: 4px; | |
| background-color: #4a90e2; | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| button:hover { | |
| background-color: #357abd; | |
| transform: translateY(-1px); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| button.active { | |
| background-color: #2c5990; | |
| } | |
| button.danger { | |
| background-color: #e74c3c; | |
| } | |
| button.danger:hover { | |
| background-color: #c0392b; | |
| } | |
| button.secondary { | |
| background-color: #95a5a6; | |
| } | |
| button.secondary:hover { | |
| background-color: #7f8c8d; | |
| } | |
| input[type="color"] { | |
| width: 40px; | |
| height: 40px; | |
| padding: 0; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| input[type="range"] { | |
| width: 100px; | |
| } | |
| .color-preview { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| border: 2px solid #ddd; | |
| } | |
| .tool-option { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| label { | |
| font-size: 14px; | |
| } | |
| .size-display { | |
| min-width: 30px; | |
| text-align: center; | |
| } | |
| .layer-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| padding: 12px; | |
| background-color: #ffffff; | |
| border-radius: 5px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
| } | |
| .layer-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .layer-item { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 6px; | |
| background-color: #f7f7f7; | |
| border-radius: 4px; | |
| } | |
| .layer-item.active { | |
| background-color: #d3e8ff; | |
| } | |
| .layer-buttons { | |
| display: flex; | |
| gap: 5px; | |
| } | |
| .status-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| width: 100%; | |
| padding: 6px 12px; | |
| background-color: #ffffff; | |
| border-radius: 5px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
| font-size: 12px; | |
| color: #555; | |
| } | |
| #colorPalette { | |
| display: flex; | |
| gap: 5px; | |
| flex-wrap: wrap; | |
| } | |
| .palette-color { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| border: 1px solid #ddd; | |
| } | |
| .palette-color:hover { | |
| transform: scale(1.1); | |
| } | |
| .brush-preview { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 4px; | |
| background-color: #f7f7f7; | |
| position: relative; | |
| } | |
| .brush-preview-dot { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| border-radius: 50%; | |
| background-color: #000; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| z-index: 1000; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .modal-content { | |
| background-color: white; | |
| padding: 20px; | |
| border-radius: 5px; | |
| max-width: 400px; | |
| width: 100%; | |
| } | |
| .modal-title { | |
| margin-top: 0; | |
| } | |
| .modal-footer { | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .image-preview { | |
| max-width: 100%; | |
| margin-top: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| } | |
| .loading { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| } | |
| .loading:after { | |
| content: " "; | |
| display: block; | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| border: 6px solid #5D5CDE; | |
| border-color: #5D5CDE transparent #5D5CDE transparent; | |
| animation: loading 1.2s linear infinite; | |
| } | |
| @keyframes loading { | |
| 0% { | |
| transform: rotate(0deg); | |
| } | |
| 100% { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .transform-modal { | |
| max-width: 90%; | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| } | |
| .transform-preview { | |
| text-align: center; | |
| margin-top: 15px; | |
| } | |
| .transform-preview img { | |
| max-width: 100%; | |
| max-height: 60vh; | |
| border-radius: 4px; | |
| border: 1px solid #ddd; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| body { | |
| background-color: #181818; | |
| color: #e0e0e0; | |
| } | |
| .toolbar, .status-bar, .modal-content, .layer-panel { | |
| background-color: #292929; | |
| color: #e0e0e0; | |
| } | |
| .tool-group, .layer-item { | |
| background-color: #383838; | |
| } | |
| .layer-item.active { | |
| background-color: #3a4d64; | |
| } | |
| canvas { | |
| border-color: #444; | |
| } | |
| .palette-color { | |
| border-color: #444; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="toolbar"> | |
| <div class="tool-group"> | |
| <button id="pencilTool" class="active" title="Pencil (P)">Pencil</button> | |
| <button id="brushTool" title="Brush (B)">Brush</button> | |
| <button id="eraserTool" title="Eraser (E)">Eraser</button> | |
| <button id="fillTool" title="Fill (F)">Fill</button> | |
| <button id="lineTool" title="Line (L)">Line</button> | |
| <button id="rectangleTool" title="Rectangle (R)">Rectangle</button> | |
| <button id="circleTool" title="Circle (C)">Circle</button> | |
| </div> | |
| <div class="tool-group"> | |
| <div class="tool-option"> | |
| <input type="color" id="colorPicker" value="#000000"> | |
| <div id="currentColor" class="color-preview" style="background-color: #000000;"></div> | |
| </div> | |
| <div class="tool-option"> | |
| <label for="brushSize">Size:</label> | |
| <input type="range" id="brushSize" min="1" max="50" value="5"> | |
| <span id="sizeDisplay" class="size-display">5px</span> | |
| </div> | |
| <div class="tool-option"> | |
| <label for="opacityRange">Opacity:</label> | |
| <input type="range" id="opacityRange" min="1" max="100" value="100"> | |
| <span id="opacityDisplay" class="size-display">100%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="canvas-container"> | |
| <canvas id="paintCanvas" width="800" height="600"></canvas> | |
| </div> | |
| <div class="toolbar"> | |
| <div class="tool-group"> | |
| <button id="undoButton" title="Undo (Ctrl+Z)">Undo</button> | |
| <button id="redoButton" title="Redo (Ctrl+Y)">Redo</button> | |
| <button id="clearButton" class="danger" title="Clear">Clear</button> | |
| </div> | |
| <div class="tool-group"> | |
| <button id="saveButton" title="Save (Ctrl+S)">Save</button> | |
| <button id="loadButton" title="Load">Load</button> | |
| <button id="exportButton" title="Export as PNG">Export</button> | |
| <button id="downloadButton" title="Download Image">Download Image</button> | |
| <button id="transformButton" title="Transform with Image-Photo">Transform Image</button> | |
| </div> | |
| <div class="tool-group"> | |
| <div id="colorPalette"> | |
| <div class="palette-color" style="background-color: #000000;"></div> | |
| <div class="palette-color" style="background-color: #ffffff;"></div> | |
| <div class="palette-color" style="background-color: #ff0000;"></div> | |
| <div class="palette-color" style="background-color: #00ff00;"></div> | |
| <div class="palette-color" style="background-color: #0000ff;"></div> | |
| <div class="palette-color" style="background-color: #ffff00;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="status-bar"> | |
| <span id="positionDisplay">Position: 0, 0</span> | |
| <span id="toolInfo">Pencil Tool | Size: 5px | Color: #000000</span> | |
| </div> | |
| </div> | |
| <div id="saveModal" class="modal"> | |
| <div class="modal-content"> | |
| <h3 class="modal-title">Save Drawing</h3> | |
| <div> | |
| <label for="saveFilename">Filename:</label> | |
| <input type="text" id="saveFilename" value="my-drawing" style="width: 100%; margin-top: 5px; padding: 5px;"> | |
| </div> | |
| <div class="modal-footer"> | |
| <button id="cancelSave" class="secondary">Cancel</button> | |
| <button id="confirmSave">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="transformModal" class="modal"> | |
| <div class="modal-content transform-modal"> | |
| <h3 class="modal-title">Transform Image</h3> | |
| <div> | |
| <label for="transformPrompt">Enter a prompt for @Image-Photo:</label> | |
| <input type="text" id="transformPrompt" placeholder="Describe how to transform the image..." style="width: 100%; margin-top: 5px; padding: 5px; font-size: 16px;"> | |
| </div> | |
| <div id="transformLoading" class="loading" style="display: none;"></div> | |
| <div id="transformPreview" class="transform-preview"></div> | |
| <div class="modal-footer"> | |
| <button id="cancelTransform" class="secondary">Cancel</button> | |
| <button id="confirmTransform">Send to Image-Photo</button> | |
| <button id="downloadTransformed" style="display: none;">Download Transformed</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Check if dark mode is preferred | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { | |
| if (event.matches) { | |
| document.documentElement.classList.add('dark'); | |
| } else { | |
| document.documentElement.classList.remove('dark'); | |
| } | |
| }); | |
| const canvas = document.getElementById('paintCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const colorPicker = document.getElementById('colorPicker'); | |
| const currentColor = document.getElementById('currentColor'); | |
| const brushSize = document.getElementById('brushSize'); | |
| const sizeDisplay = document.getElementById('sizeDisplay'); | |
| const opacityRange = document.getElementById('opacityRange'); | |
| const opacityDisplay = document.getElementById('opacityDisplay'); | |
| const positionDisplay = document.getElementById('positionDisplay'); | |
| const toolInfo = document.getElementById('toolInfo'); | |
| const pencilTool = document.getElementById('pencilTool'); | |
| const brushTool = document.getElementById('brushTool'); | |
| const eraserTool = document.getElementById('eraserTool'); | |
| const fillTool = document.getElementById('fillTool'); | |
| const lineTool = document.getElementById('lineTool'); | |
| const rectangleTool = document.getElementById('rectangleTool'); | |
| const circleTool = document.getElementById('circleTool'); | |
| const undoButton = document.getElementById('undoButton'); | |
| const redoButton = document.getElementById('redoButton'); | |
| const clearButton = document.getElementById('clearButton'); | |
| const saveButton = document.getElementById('saveButton'); | |
| const loadButton = document.getElementById('loadButton'); | |
| const exportButton = document.getElementById('exportButton'); | |
| const downloadButton = document.getElementById('downloadButton'); | |
| const transformButton = document.getElementById('transformButton'); | |
| const saveModal = document.getElementById('saveModal'); | |
| const saveFilename = document.getElementById('saveFilename'); | |
| const cancelSave = document.getElementById('cancelSave'); | |
| const confirmSave = document.getElementById('confirmSave'); | |
| const transformModal = document.getElementById('transformModal'); | |
| const transformPrompt = document.getElementById('transformPrompt'); | |
| const transformLoading = document.getElementById('transformLoading'); | |
| const transformPreview = document.getElementById('transformPreview'); | |
| const cancelTransform = document.getElementById('cancelTransform'); | |
| const confirmTransform = document.getElementById('confirmTransform'); | |
| const downloadTransformed = document.getElementById('downloadTransformed'); | |
| let isDrawing = false; | |
| let lastX = 0; | |
| let lastY = 0; | |
| let currentTool = 'pencil'; | |
| let undoStack = []; | |
| let redoStack = []; | |
| let startX = 0; | |
| let startY = 0; | |
| let transformedImageUrl = null; | |
| function initCanvas() { | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| saveState(); | |
| } | |
| function saveState() { | |
| redoStack = []; | |
| undoStack.push(canvas.toDataURL()); | |
| updateButtons(); | |
| } | |
| function updateButtons() { | |
| undoButton.disabled = undoStack.length <= 1; | |
| redoButton.disabled = redoStack.length === 0; | |
| } | |
| function undo() { | |
| if (undoStack.length <= 1) return; | |
| redoStack.push(undoStack.pop()); | |
| const img = new Image(); | |
| img.src = undoStack[undoStack.length - 1]; | |
| img.onload = () => { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| updateButtons(); | |
| }; | |
| } | |
| function redo() { | |
| if (redoStack.length === 0) return; | |
| const img = new Image(); | |
| img.src = redoStack.pop(); | |
| img.onload = () => { | |
| undoStack.push(img.src); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| updateButtons(); | |
| }; | |
| } | |
| function clearCanvas() { | |
| saveState(); | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| function startDrawing(e) { | |
| isDrawing = true; | |
| const rect = canvas.getBoundingClientRect(); | |
| lastX = e.clientX - rect.left; | |
| lastY = e.clientY - rect.top; | |
| startX = lastX; | |
| startY = lastY; | |
| if (['line', 'rectangle', 'circle'].includes(currentTool)) { | |
| saveState(); | |
| return; | |
| } | |
| ctx.beginPath(); | |
| if (currentTool === 'fill') { | |
| floodFill(Math.floor(lastX), Math.floor(lastY), hexToRgb(colorPicker.value)); | |
| isDrawing = false; | |
| saveState(); | |
| return; | |
| } | |
| setupContext(); | |
| if (['pencil', 'brush'].includes(currentTool)) { | |
| ctx.arc(lastX, lastY, 0.5, 0, Math.PI * 2); | |
| ctx.fillStyle = ctx.strokeStyle; | |
| ctx.fill(); | |
| } | |
| saveState(); | |
| } | |
| function draw(e) { | |
| if (!isDrawing) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| const currentX = e.clientX - rect.left; | |
| const currentY = e.clientY - rect.top; | |
| positionDisplay.textContent = `Position: ${Math.floor(currentX)}, ${Math.floor(currentY)}`; | |
| if (currentTool === 'pencil' || currentTool === 'brush' || currentTool === 'eraser') { | |
| ctx.beginPath(); | |
| ctx.moveTo(lastX, lastY); | |
| ctx.lineTo(currentX, currentY); | |
| ctx.stroke(); | |
| lastX = currentX; | |
| lastY = currentY; | |
| } else if (['line', 'rectangle', 'circle'].includes(currentTool)) { | |
| const img = new Image(); | |
| img.src = undoStack[undoStack.length - 1]; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| setupContext(); | |
| if (currentTool === 'line') { | |
| ctx.beginPath(); | |
| ctx.moveTo(startX, startY); | |
| ctx.lineTo(currentX, currentY); | |
| ctx.stroke(); | |
| } else if (currentTool === 'rectangle') { | |
| const width = currentX - startX; | |
| const height = currentY - startY; | |
| ctx.strokeRect(startX, startY, width, height); | |
| } else if (currentTool === 'circle') { | |
| const radius = Math.sqrt(Math.pow(currentX - startX, 2) + Math.pow(currentY - startY, 2)); | |
| ctx.beginPath(); | |
| ctx.arc(startX, startY, radius, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| function endDrawing() { | |
| if (!isDrawing) return; | |
| isDrawing = false; | |
| if (['line', 'rectangle', 'circle'].includes(currentTool)) { | |
| saveState(); | |
| } | |
| } | |
| function setupContext() { | |
| const size = parseInt(brushSize.value); | |
| const opacity = parseInt(opacityRange.value) / 100; | |
| ctx.lineWidth = size; | |
| ctx.lineCap = 'round'; | |
| ctx.lineJoin = 'round'; | |
| if (currentTool === 'eraser') { | |
| ctx.globalCompositeOperation = 'destination-out'; | |
| ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`; | |
| } else { | |
| ctx.globalCompositeOperation = 'source-over'; | |
| const color = colorPicker.value; | |
| ctx.strokeStyle = `${color}${Math.round(opacity * 255).toString(16).padStart(2, '0')}`; | |
| } | |
| } | |
| function floodFill(x, y, targetColor) { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| const width = canvas.width; | |
| const height = canvas.height; | |
| const index = (y * width + x) * 4; | |
| const clickedColor = { r: data[index], g: data[index + 1], b: data[index + 2], a: data[index + 3] }; | |
| if (colorMatch(clickedColor, targetColor)) return; | |
| const stack = [{x, y}]; | |
| while (stack.length > 0) { | |
| const pixel = stack.pop(); | |
| const px = pixel.x; | |
| const py = pixel.y; | |
| if (px < 0 || px >= width || py < 0 || py >= height) continue; | |
| const currentIndex = (py * width + px) * 4; | |
| const currentColor = { | |
| r: data[currentIndex], | |
| g: data[currentIndex + 1], | |
| b: data[currentIndex + 2], | |
| a: data[currentIndex + 3] | |
| }; | |
| if (colorMatch(currentColor, clickedColor)) { | |
| data[currentIndex] = targetColor.r; | |
| data[currentIndex + 1] = targetColor.g; | |
| data[currentIndex + 2] = targetColor.b; | |
| data[currentIndex + 3] = 255; | |
| stack.push({x: px + 1, y: py}); | |
| stack.push({x: px - 1, y: py}); | |
| stack.push({x: px, y: py + 1}); | |
| stack.push({x: px, y: py - 1}); | |
| } | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| } | |
| function colorMatch(color1, color2, tolerance = 10) { | |
| return Math.abs(color1.r - color2.r) <= tolerance && | |
| Math.abs(color1.g - color2.g) <= tolerance && | |
| Math.abs(color1.b - color2.b) <= tolerance; | |
| } | |
| function hexToRgb(hex) { | |
| const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
| return result ? { | |
| r: parseInt(result[1], 16), | |
| g: parseInt(result[2], 16), | |
| b: parseInt(result[3], 16) | |
| } : {r: 0, g: 0, b: 0}; | |
| } | |
| function saveDrawing() { | |
| const filename = saveFilename.value.trim() || 'my-drawing'; | |
| const dataURL = canvas.toDataURL(); | |
| try { | |
| localStorage.setItem(`painting-${filename}`, dataURL); | |
| alert(`Drawing saved as "${filename}"`); | |
| } catch (e) { | |
| alert('Error saving drawing. Local storage might be full or disabled.'); | |
| } | |
| saveModal.style.display = 'none'; | |
| } | |
| function loadDrawing() { | |
| const savedDrawings = []; | |
| for (let i = 0; i < localStorage.length; i++) { | |
| const key = localStorage.key(i); | |
| if (key.startsWith('painting-')) { | |
| savedDrawings.push(key.replace('painting-', '')); | |
| } | |
| } | |
| if (savedDrawings.length === 0) { | |
| alert('No saved drawings found.'); | |
| return; | |
| } | |
| const drawing = prompt(`Enter filename to load (available: ${savedDrawings.join(', ')})`); | |
| if (!drawing) return; | |
| const dataURL = localStorage.getItem(`painting-${drawing}`); | |
| if (!dataURL) { | |
| alert(`Drawing "${drawing}" not found.`); | |
| return; | |
| } | |
| const img = new Image(); | |
| img.src = dataURL; | |
| img.onload = () => { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| saveState(); | |
| }; | |
| } | |
| function exportDrawing() { | |
| const link = document.createElement('a'); | |
| link.download = 'drawing.png'; | |
| link.href = canvas.toDataURL('image/png'); | |
| link.click(); | |
| } | |
| // New function for downloading image | |
| function downloadImage() { | |
| const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); | |
| const link = document.createElement('a'); | |
| link.download = `drawing-${timestamp}.png`; | |
| link.href = canvas.toDataURL('image/png'); | |
| link.click(); | |
| } | |
| // New function to open transform modal | |
| function openTransformModal() { | |
| transformPrompt.value = ''; | |
| transformPreview.innerHTML = ''; | |
| downloadTransformed.style.display = 'none'; | |
| transformModal.style.display = 'flex'; | |
| } | |
| // New function to transform image with Image-Photo | |
| async function transformImage() { | |
| const prompt = transformPrompt.value.trim(); | |
| if (!prompt) { | |
| alert('Please enter a prompt for the transformation'); | |
| return; | |
| } | |
| // Show loading state | |
| transformLoading.style.display = 'block'; | |
| confirmTransform.disabled = true; | |
| transformPreview.innerHTML = ''; | |
| downloadTransformed.style.display = 'none'; | |
| transformedImageUrl = null; | |
| try { | |
| // Convert canvas to File | |
| const dataUrl = canvas.toDataURL('image/png'); | |
| const res = await fetch(dataUrl); | |
| const blob = await res.blob(); | |
| const file = new File([blob], 'drawing.png', { type: 'image/png' }); | |
| // Register handler for Image-Photo response | |
| const handlerId = `image-transform-${Date.now()}`; | |
| window.Poe.registerHandler(handlerId, (result) => { | |
| const response = result.responses[0]; | |
| if (response.status === "error") { | |
| transformLoading.style.display = 'none'; | |
| transformPreview.innerHTML = `<p style="color: red;">Error: ${response.statusText || 'Failed to transform image'}</p>`; | |
| confirmTransform.disabled = false; | |
| } | |
| else if (response.status === "complete") { | |
| transformLoading.style.display = 'none'; | |
| confirmTransform.disabled = false; | |
| if (response.attachments?.length > 0) { | |
| const imageAttachment = response.attachments[0]; | |
| transformedImageUrl = imageAttachment.url; | |
| transformPreview.innerHTML = ` | |
| <img src="${imageAttachment.url}" alt="Transformed image"> | |
| <p>${response.content}</p> | |
| `; | |
| downloadTransformed.style.display = 'inline-block'; | |
| } else { | |
| transformPreview.innerHTML = `<p>${response.content}</p> | |
| <p style="color: #f59e0b;">No image was returned. Try a different prompt.</p>`; | |
| } | |
| } | |
| }); | |
| // Send message to Image-Photo | |
| await window.Poe.sendUserMessage( | |
| "@Image-Photo " + prompt, | |
| { | |
| handler: handlerId, | |
| stream: false, | |
| openChat: false, | |
| attachments: [file] | |
| } | |
| ); | |
| await window.Poe.sendUserMessage( | |
| "@Free-Thinking-Server " + prompt, | |
| { | |
| handler: handlerId, | |
| stream: false, | |
| openChat: false, | |
| attachments: [file] | |
| } | |
| ); | |
| } catch (err) { | |
| transformLoading.style.display = 'none'; | |
| confirmTransform.disabled = false; | |
| transformPreview.innerHTML = `<p style="color: red;">Error: ${err.message || 'Failed to send image'}</p>`; | |
| console.error("Error:", err); | |
| } | |
| } | |
| // Function to download transformed image | |
| function downloadTransformedImage() { | |
| if (!transformedImageUrl) return; | |
| const link = document.createElement('a'); | |
| link.href = transformedImageUrl; | |
| link.download = `transformed-${Date.now()}.png`; | |
| link.click(); | |
| } | |
| function setCurrentTool(tool) { | |
| currentTool = tool; | |
| document.querySelectorAll('.tool-group button').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| document.getElementById(`${tool}Tool`).classList.add('active'); | |
| updateToolInfo(); | |
| } | |
| function updateToolInfo() { | |
| const size = brushSize.value; | |
| const color = colorPicker.value; | |
| const opacity = opacityRange.value; | |
| let toolName; | |
| switch (currentTool) { | |
| case 'pencil': toolName = 'Pencil'; break; | |
| case 'brush': toolName = 'Brush'; break; | |
| case 'eraser': toolName = 'Eraser'; break; | |
| case 'fill': toolName = 'Fill'; break; | |
| case 'line': toolName = 'Line'; break; | |
| case 'rectangle': toolName = 'Rectangle'; break; | |
| case 'circle': toolName = 'Circle'; break; | |
| default: toolName = 'Unknown'; | |
| } | |
| toolInfo.textContent = `${toolName} Tool | Size: ${size}px | Color: ${color} | Opacity: ${opacity}%`; | |
| } | |
| function handleKeyDown(e) { | |
| if (document.activeElement.tagName === 'INPUT') return; | |
| if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { | |
| e.preventDefault(); | |
| undo(); | |
| } else if (e.key === 'y' && (e.ctrlKey || e.metaKey)) { | |
| e.preventDefault(); | |
| redo(); | |
| } else if (e.key === 's' && (e.ctrlKey || e.metaKey)) { | |
| e.preventDefault(); | |
| saveModal.style.display = 'flex'; | |
| } else if (e.key === 'p' || e.key === 'b' || e.key === 'e' || e.key === 'f' || e.key === 'l' || e.key === 'r' || e.key === 'c') { | |
| switch (e.key) { | |
| case 'p': setCurrentTool('pencil'); break; | |
| case 'b': setCurrentTool('brush'); break; | |
| case 'e': setCurrentTool('eraser'); break; | |
| case 'f': setCurrentTool('fill'); break; | |
| case 'l': setCurrentTool('line'); break; | |
| case 'r': setCurrentTool('rectangle'); break; | |
| case 'c': setCurrentTool('circle'); break; | |
| } | |
| } | |
| } | |
| function trackMousePosition(e) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| positionDisplay.textContent = `Position: ${Math.floor(x)}, ${Math.floor(y)}`; | |
| } | |
| function rgbToHex(rgb) { | |
| const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); | |
| if (match) { | |
| return "#" + ((1 << 24) + (parseInt(match[1]) << 16) + (parseInt(match[2]) << 8) + parseInt(match[3])).toString(16).slice(1); | |
| } | |
| return rgb; | |
| } | |
| // Event listeners | |
| canvas.addEventListener('mousedown', startDrawing); | |
| canvas.addEventListener('mousemove', draw); | |
| canvas.addEventListener('mouseup', endDrawing); | |
| canvas.addEventListener('mouseout', endDrawing); | |
| canvas.addEventListener('mousemove', trackMousePosition); | |
| pencilTool.addEventListener('click', () => setCurrentTool('pencil')); | |
| brushTool.addEventListener('click', () => setCurrentTool('brush')); | |
| eraserTool.addEventListener('click', () => setCurrentTool('eraser')); | |
| fillTool.addEventListener('click', () => setCurrentTool('fill')); | |
| lineTool.addEventListener('click', () => setCurrentTool('line')); | |
| rectangleTool.addEventListener('click', () => setCurrentTool('rectangle')); | |
| circleTool.addEventListener('click', () => setCurrentTool('circle')); | |
| colorPicker.addEventListener('input', () => { | |
| currentColor.style.backgroundColor = colorPicker.value; | |
| updateToolInfo(); | |
| }); | |
| brushSize.addEventListener('input', () => { | |
| sizeDisplay.textContent = `${brushSize.value}px`; | |
| updateToolInfo(); | |
| }); | |
| opacityRange.addEventListener('input', () => { | |
| opacityDisplay.textContent = `${opacityRange.value}%`; | |
| updateToolInfo(); | |
| }); | |
| undoButton.addEventListener('click', undo); | |
| redoButton.addEventListener('click', redo); | |
| clearButton.addEventListener('click', clearCanvas); | |
| saveButton.addEventListener('click', () => saveModal.style.display = 'flex'); | |
| loadButton.addEventListener('click', loadDrawing); | |
| exportButton.addEventListener('click', exportDrawing); | |
| // New button event listeners | |
| downloadButton.addEventListener('click', downloadImage); | |
| transformButton.addEventListener('click', openTransformModal); | |
| cancelTransform.addEventListener('click', () => transformModal.style.display = 'none'); | |
| confirmTransform.addEventListener('click', transformImage); | |
| downloadTransformed.addEventListener('click', downloadTransformedImage); | |
| cancelSave.addEventListener('click', () => saveModal.style.display = 'none'); | |
| confirmSave.addEventListener('click', saveDrawing); | |
| document.querySelectorAll('.palette-color').forEach(colorEl => { | |
| colorEl.addEventListener('click', () => { | |
| const color = colorEl.style.backgroundColor; | |
| const hex = rgbToHex(color); | |
| colorPicker.value = hex; | |
| currentColor.style.backgroundColor = color; | |
| updateToolInfo(); | |
| }); | |
| }); | |
| document.addEventListener('keydown', handleKeyDown); | |
| initCanvas(); | |
| updateButtons(); | |
| </script> | |
| </html> |