|
|
<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> |
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
function openTransformModal() { |
|
|
transformPrompt.value = ''; |
|
|
transformPreview.innerHTML = ''; |
|
|
downloadTransformed.style.display = 'none'; |
|
|
transformModal.style.display = 'flex'; |
|
|
} |
|
|
|
|
|
|
|
|
async function transformImage() { |
|
|
const prompt = transformPrompt.value.trim(); |
|
|
if (!prompt) { |
|
|
alert('Please enter a prompt for the transformation'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
transformLoading.style.display = 'block'; |
|
|
confirmTransform.disabled = true; |
|
|
transformPreview.innerHTML = ''; |
|
|
downloadTransformed.style.display = 'none'; |
|
|
transformedImageUrl = null; |
|
|
|
|
|
try { |
|
|
|
|
|
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' }); |
|
|
|
|
|
|
|
|
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>`; |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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 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; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
</html> |