| <!DOCTYPE html> |
| <html lang="pt-PT"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Editor de Imagens Completo</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .image-container { |
| max-width: 100%; |
| max-height: 80vh; |
| overflow: auto; |
| border: 2px dashed #ccc; |
| position: relative; |
| } |
| #canvas { |
| max-width: 100%; |
| max-height: 80vh; |
| display: block; |
| } |
| .slider-container { |
| width: 100%; |
| } |
| .slider { |
| -webkit-appearance: none; |
| width: 100%; |
| height: 8px; |
| border-radius: 5px; |
| background: #d3d3d3; |
| outline: none; |
| } |
| .slider::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| appearance: none; |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| background: #4f46e5; |
| cursor: pointer; |
| } |
| .slider::-moz-range-thumb { |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| background: #4f46e5; |
| cursor: pointer; |
| } |
| .filter-option { |
| transition: all 0.2s; |
| } |
| .filter-option:hover { |
| transform: scale(1.05); |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| } |
| .filter-option.active { |
| border: 2px solid #4f46e5; |
| } |
| .tooltip { |
| position: relative; |
| display: inline-block; |
| } |
| .tooltip .tooltiptext { |
| visibility: hidden; |
| width: 120px; |
| background-color: #333; |
| color: #fff; |
| text-align: center; |
| border-radius: 6px; |
| padding: 5px; |
| position: absolute; |
| z-index: 1; |
| bottom: 125%; |
| left: 50%; |
| margin-left: -60px; |
| opacity: 0; |
| transition: opacity 0.3s; |
| font-size: 12px; |
| } |
| .tooltip:hover .tooltiptext { |
| visibility: visible; |
| opacity: 1; |
| } |
| .tab-content { |
| display: none; |
| } |
| .tab-content.active { |
| display: block; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 font-sans"> |
| <div class="container mx-auto px-4 py-8"> |
| <h1 class="text-3xl font-bold text-center text-indigo-700 mb-8">Editor de Imagens Completo</h1> |
| |
| <div class="bg-white rounded-lg shadow-lg overflow-hidden"> |
| |
| <div class="bg-indigo-600 text-white px-6 py-4 flex justify-between items-center"> |
| <h2 class="text-xl font-semibold">Editar Imagem</h2> |
| <div class="flex space-x-2"> |
| <button id="resetBtn" class="bg-indigo-700 hover:bg-indigo-800 px-4 py-2 rounded-md text-sm"> |
| <i class="fas fa-undo-alt mr-1"></i> Reiniciar |
| </button> |
| <button id="downloadBtn" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-md text-sm"> |
| <i class="fas fa-download mr-1"></i> Descarregar |
| </button> |
| </div> |
| </div> |
| |
| <div class="flex flex-col md:flex-row"> |
| |
| <div class="w-full md:w-1/4 bg-gray-50 p-4 border-r"> |
| <div class="mb-6"> |
| <h3 class="font-medium text-gray-700 mb-2">Carregar Imagem</h3> |
| <div class="flex flex-col space-y-2"> |
| <input type="file" id="fileInput" accept="image/*" class="hidden"> |
| <label for="fileInput" class="bg-indigo-100 text-indigo-700 hover:bg-indigo-200 px-4 py-2 rounded-md text-center cursor-pointer"> |
| <i class="fas fa-folder-open mr-2"></i> Escolher Imagem |
| </label> |
| </div> |
| </div> |
| |
| <div class="tabs mb-6"> |
| <div class="flex border-b"> |
| <button class="tab-btn active px-4 py-2 text-indigo-600 border-b-2 border-indigo-600" data-tab="filters">Filtros</button> |
| <button class="tab-btn px-4 py-2 text-gray-600" data-tab="adjust">Ajustes</button> |
| <button class="tab-btn px-4 py-2 text-gray-600" data-tab="tools">Ferramentas</button> |
| </div> |
| |
| <div id="filters" class="tab-content active mt-4"> |
| <div class="grid grid-cols-3 gap-2"> |
| <div class="filter-option p-2 bg-white rounded cursor-pointer" data-filter="none"> |
| <div class="w-full h-16 bg-gray-200 rounded mb-1"></div> |
| <p class="text-xs text-center">Original</p> |
| </div> |
| <div class="filter-option p-2 bg-white rounded cursor-pointer" data-filter="grayscale"> |
| <div class="w-full h-16 bg-gray-400 rounded mb-1"></div> |
| <p class="text-xs text-center">Preto e Branco</p> |
| </div> |
| <div class="filter-option p-2 bg-white rounded cursor-pointer" data-filter="sepia"> |
| <div class="w-full h-16 bg-yellow-300 rounded mb-1"></div> |
| <p class="text-xs text-center">Sépia</p> |
| </div> |
| <div class="filter-option p-2 bg-white rounded cursor-pointer" data-filter="invert"> |
| <div class="w-full h-16 bg-indigo-200 rounded mb-1"></div> |
| <p class="text-xs text-center">Inverter</p> |
| </div> |
| <div class="filter-option p-2 bg-white rounded cursor-pointer" data-filter="blur"> |
| <div class="w-full h-16 bg-blue-100 rounded mb-1"></div> |
| <p class="text-xs text-center">Desfocado</p> |
| </div> |
| <div class="filter-option p-2 bg-white rounded cursor-pointer" data-filter="hue-rotate"> |
| <div class="w-full h-16 bg-purple-200 rounded mb-1"></div> |
| <p class="text-xs text-center">Matiz</p> |
| </div> |
| </div> |
| </div> |
| |
| <div id="adjust" class="tab-content mt-4"> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Brilho</label> |
| <div class="slider-container"> |
| <input type="range" min="-100" max="100" value="0" class="slider" id="brightnessSlider"> |
| </div> |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> |
| <span>-100</span> |
| <span>0</span> |
| <span>100</span> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Contraste</label> |
| <div class="slider-container"> |
| <input type="range" min="-100" max="100" value="0" class="slider" id="contrastSlider"> |
| </div> |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> |
| <span>-100</span> |
| <span>0</span> |
| <span>100</span> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Saturação</label> |
| <div class="slider-container"> |
| <input type="range" min="0" max="200" value="100" class="slider" id="saturationSlider"> |
| </div> |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> |
| <span>0</span> |
| <span>100</span> |
| <span>200</span> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Temperatura</label> |
| <div class="slider-container"> |
| <input type="range" min="-100" max="100" value="0" class="slider" id="temperatureSlider"> |
| </div> |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> |
| <span>Frio</span> |
| <span>Neutro</span> |
| <span>Quente</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="tools" class="tab-content mt-4"> |
| <div class="grid grid-cols-2 gap-3"> |
| <button id="cropBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-md text-sm"> |
| <i class="fas fa-crop mr-1"></i> Cortar |
| </button> |
| <button id="rotateBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-md text-sm"> |
| <i class="fas fa-undo mr-1"></i> Rodar |
| </button> |
| <button id="flipHBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-md text-sm"> |
| <i class="fas fa-arrows-alt-h mr-1"></i> Inverter H |
| </button> |
| <button id="flipVBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-md text-sm"> |
| <i class="fas fa-arrows-alt-v mr-1"></i> Inverter V |
| </button> |
| <button id="textBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-md text-sm"> |
| <i class="fas fa-font mr-1"></i> Texto |
| </button> |
| <button id="drawBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-md text-sm"> |
| <i class="fas fa-pencil-alt mr-1"></i> Desenhar |
| </button> |
| </div> |
| |
| <div id="textOptions" class="mt-4 hidden"> |
| <input type="text" id="textInput" placeholder="Introduza o texto" class="w-full px-3 py-2 border rounded mb-2"> |
| <div class="flex space-x-2 mb-2"> |
| <input type="color" id="textColor" value="#000000" class="w-8 h-8"> |
| <select id="textFont" class="flex-1 px-2 py-1 border rounded"> |
| <option value="Arial">Arial</option> |
| <option value="Times New Roman">Times New Roman</option> |
| <option value="Courier New">Courier New</option> |
| <option value="Georgia">Georgia</option> |
| <option value="Verdana">Verdana</option> |
| </select> |
| </div> |
| <div class="slider-container mb-2"> |
| <label class="block text-xs text-gray-700 mb-1">Tamanho</label> |
| <input type="range" min="10" max="72" value="24" class="slider" id="textSizeSlider"> |
| </div> |
| <button id="addTextBtn" class="w-full bg-indigo-600 text-white px-3 py-2 rounded-md text-sm"> |
| Adicionar Texto |
| </button> |
| </div> |
| |
| <div id="drawOptions" class="mt-4 hidden"> |
| <div class="flex space-x-2 mb-2"> |
| <input type="color" id="drawColor" value="#000000" class="w-8 h-8"> |
| <div class="slider-container flex-1"> |
| <label class="block text-xs text-gray-700 mb-1">Espessura</label> |
| <input type="range" min="1" max="20" value="5" class="slider" id="drawSizeSlider"> |
| </div> |
| </div> |
| <div class="grid grid-cols-3 gap-2"> |
| <button id="drawStartBtn" class="bg-indigo-600 text-white px-3 py-2 rounded-md text-sm"> |
| Começar |
| </button> |
| <button id="drawClearBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-md text-sm"> |
| Limpar |
| </button> |
| <button id="drawSaveBtn" class="bg-green-600 text-white px-3 py-2 rounded-md text-sm"> |
| Guardar |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="w-full md:w-3/4 p-6"> |
| <div class="image-container mx-auto bg-gray-100 flex items-center justify-center" id="imageContainer"> |
| <canvas id="canvas"></canvas> |
| <p class="text-gray-500 text-center p-8" id="placeholderText">Carregue uma imagem para começar a editar</p> |
| </div> |
| |
| <div class="mt-4 flex justify-center space-x-4" id="cropControls" style="display: none;"> |
| <button id="cropConfirmBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md"> |
| <i class="fas fa-check mr-1"></i> Confirmar |
| </button> |
| <button id="cropCancelBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md"> |
| <i class="fas fa-times mr-1"></i> Cancelar |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const fileInput = document.getElementById('fileInput'); |
| const canvas = document.getElementById('canvas'); |
| const ctx = canvas.getContext('2d'); |
| const imageContainer = document.getElementById('imageContainer'); |
| const placeholderText = document.getElementById('placeholderText'); |
| const resetBtn = document.getElementById('resetBtn'); |
| const downloadBtn = document.getElementById('downloadBtn'); |
| |
| |
| const filterOptions = document.querySelectorAll('.filter-option'); |
| |
| |
| const brightnessSlider = document.getElementById('brightnessSlider'); |
| const contrastSlider = document.getElementById('contrastSlider'); |
| const saturationSlider = document.getElementById('saturationSlider'); |
| const temperatureSlider = document.getElementById('temperatureSlider'); |
| |
| |
| const cropBtn = document.getElementById('cropBtn'); |
| const rotateBtn = document.getElementById('rotateBtn'); |
| const flipHBtn = document.getElementById('flipHBtn'); |
| const flipVBtn = document.getElementById('flipVBtn'); |
| const textBtn = document.getElementById('textBtn'); |
| const drawBtn = document.getElementById('drawBtn'); |
| |
| |
| const textOptions = document.getElementById('textOptions'); |
| const textInput = document.getElementById('textInput'); |
| const textColor = document.getElementById('textColor'); |
| const textFont = document.getElementById('textFont'); |
| const textSizeSlider = document.getElementById('textSizeSlider'); |
| const addTextBtn = document.getElementById('addTextBtn'); |
| |
| |
| const drawOptions = document.getElementById('drawOptions'); |
| const drawColor = document.getElementById('drawColor'); |
| const drawSizeSlider = document.getElementById('drawSizeSlider'); |
| const drawStartBtn = document.getElementById('drawStartBtn'); |
| const drawClearBtn = document.getElementById('drawClearBtn'); |
| const drawSaveBtn = document.getElementById('drawSaveBtn'); |
| |
| |
| const cropControls = document.getElementById('cropControls'); |
| const cropConfirmBtn = document.getElementById('cropConfirmBtn'); |
| const cropCancelBtn = document.getElementById('cropCancelBtn'); |
| |
| |
| const tabBtns = document.querySelectorAll('.tab-btn'); |
| const tabContents = document.querySelectorAll('.tab-content'); |
| |
| |
| let originalImage = null; |
| let currentImage = null; |
| let isDrawing = false; |
| let isCropping = false; |
| let cropStartX, cropStartY, cropEndX, cropEndY; |
| let rotationAngle = 0; |
| let flipH = false; |
| let flipV = false; |
| let filters = { |
| brightness: 0, |
| contrast: 0, |
| saturation: 100, |
| temperature: 0, |
| filter: 'none' |
| }; |
| |
| |
| fileInput.addEventListener('change', handleImageUpload); |
| resetBtn.addEventListener('click', resetImage); |
| downloadBtn.addEventListener('click', downloadImage); |
| |
| |
| filterOptions.forEach(option => { |
| option.addEventListener('click', () => { |
| document.querySelector('.filter-option.active')?.classList.remove('active'); |
| option.classList.add('active'); |
| filters.filter = option.dataset.filter; |
| applyFilters(); |
| }); |
| }); |
| |
| |
| brightnessSlider.addEventListener('input', () => { |
| filters.brightness = parseInt(brightnessSlider.value); |
| applyFilters(); |
| }); |
| |
| contrastSlider.addEventListener('input', () => { |
| filters.contrast = parseInt(contrastSlider.value); |
| applyFilters(); |
| }); |
| |
| saturationSlider.addEventListener('input', () => { |
| filters.saturation = parseInt(saturationSlider.value); |
| applyFilters(); |
| }); |
| |
| temperatureSlider.addEventListener('input', () => { |
| filters.temperature = parseInt(temperatureSlider.value); |
| applyFilters(); |
| }); |
| |
| |
| cropBtn.addEventListener('click', startCropping); |
| rotateBtn.addEventListener('click', rotateImage); |
| flipHBtn.addEventListener('click', flipHorizontal); |
| flipVBtn.addEventListener('click', flipVertical); |
| textBtn.addEventListener('click', toggleTextOptions); |
| drawBtn.addEventListener('click', toggleDrawOptions); |
| |
| |
| addTextBtn.addEventListener('click', addTextToImage); |
| |
| |
| drawStartBtn.addEventListener('click', startDrawing); |
| drawClearBtn.addEventListener('click', clearDrawing); |
| drawSaveBtn.addEventListener('click', saveDrawing); |
| |
| |
| cropConfirmBtn.addEventListener('click', confirmCrop); |
| cropCancelBtn.addEventListener('click', cancelCrop); |
| |
| |
| tabBtns.forEach(btn => { |
| btn.addEventListener('click', () => { |
| const tabId = btn.dataset.tab; |
| |
| |
| tabBtns.forEach(b => b.classList.remove('active', 'text-indigo-600', 'border-indigo-600')); |
| tabContents.forEach(c => c.classList.remove('active')); |
| |
| |
| btn.classList.add('active', 'text-indigo-600', 'border-indigo-600'); |
| document.getElementById(tabId).classList.add('active'); |
| }); |
| }); |
| |
| |
| canvas.addEventListener('mousedown', startDraw); |
| canvas.addEventListener('mousemove', draw); |
| canvas.addEventListener('mouseup', endDraw); |
| canvas.addEventListener('mouseout', endDraw); |
| |
| |
| canvas.addEventListener('touchstart', handleTouchStart); |
| canvas.addEventListener('touchmove', handleTouchMove); |
| canvas.addEventListener('touchend', handleTouchEnd); |
| |
| |
| canvas.addEventListener('mousedown', startCrop); |
| canvas.addEventListener('mousemove', moveCrop); |
| canvas.addEventListener('mouseup', endCrop); |
| |
| |
| function handleImageUpload(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = function(event) { |
| const img = new Image(); |
| img.onload = function() { |
| originalImage = img; |
| currentImage = img; |
| setupCanvas(img); |
| placeholderText.style.display = 'none'; |
| }; |
| img.src = event.target.result; |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| function setupCanvas(img) { |
| |
| const maxWidth = imageContainer.clientWidth - 40; |
| const maxHeight = imageContainer.clientHeight - 40; |
| |
| let width = img.width; |
| let height = img.height; |
| |
| if (width > maxWidth) { |
| const ratio = maxWidth / width; |
| width = maxWidth; |
| height = height * ratio; |
| } |
| |
| if (height > maxHeight) { |
| const ratio = maxHeight / height; |
| height = maxHeight; |
| width = width * ratio; |
| } |
| |
| canvas.width = width; |
| canvas.height = height; |
| |
| |
| rotationAngle = 0; |
| flipH = false; |
| flipV = false; |
| filters = { |
| brightness: 0, |
| contrast: 0, |
| saturation: 100, |
| temperature: 0, |
| filter: 'none' |
| }; |
| |
| |
| brightnessSlider.value = 0; |
| contrastSlider.value = 0; |
| saturationSlider.value = 100; |
| temperatureSlider.value = 0; |
| |
| |
| document.querySelector('.filter-option.active')?.classList.remove('active'); |
| document.querySelector('.filter-option[data-filter="none"]').classList.add('active'); |
| filters.filter = 'none'; |
| |
| drawImage(); |
| } |
| |
| function drawImage() { |
| if (!currentImage) return; |
| |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| if (flipH || flipV) { |
| ctx.save(); |
| if (flipH && flipV) { |
| ctx.scale(-1, -1); |
| ctx.drawImage(currentImage, -canvas.width, -canvas.height, canvas.width, canvas.height); |
| } else if (flipH) { |
| ctx.scale(-1, 1); |
| ctx.drawImage(currentImage, -canvas.width, 0, canvas.width, canvas.height); |
| } else if (flipV) { |
| ctx.scale(1, -1); |
| ctx.drawImage(currentImage, 0, -canvas.height, canvas.width, canvas.height); |
| } |
| ctx.restore(); |
| } |
| |
| else if (rotationAngle !== 0) { |
| ctx.save(); |
| ctx.translate(canvas.width / 2, canvas.height / 2); |
| ctx.rotate(rotationAngle * Math.PI / 180); |
| ctx.drawImage(currentImage, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height); |
| ctx.restore(); |
| } |
| |
| else { |
| ctx.drawImage(currentImage, 0, 0, canvas.width, canvas.height); |
| } |
| |
| applyFilters(); |
| } |
| |
| function applyFilters() { |
| if (!currentImage) return; |
| |
| let filterString = ''; |
| |
| |
| if (filters.brightness !== 0) { |
| filterString += `brightness(${100 + filters.brightness}%) `; |
| } |
| |
| |
| if (filters.contrast !== 0) { |
| filterString += `contrast(${100 + filters.contrast}%) `; |
| } |
| |
| |
| if (filters.saturation !== 100) { |
| filterString += `saturate(${filters.saturation}%) `; |
| } |
| |
| |
| if (filters.temperature !== 0) { |
| const temp = filters.temperature; |
| if (temp > 0) { |
| |
| filterString += `sepia(${temp}%) hue-rotate(-${temp/2}deg) saturate(${100 + temp}%) `; |
| } else { |
| |
| filterString += `sepia(${Math.abs(temp)}%) hue-rotate(${Math.abs(temp)}deg) saturate(${100 + Math.abs(temp)}%) `; |
| } |
| } |
| |
| |
| if (filters.filter !== 'none') { |
| switch(filters.filter) { |
| case 'grayscale': |
| filterString += 'grayscale(100%) '; |
| break; |
| case 'sepia': |
| filterString += 'sepia(100%) '; |
| break; |
| case 'invert': |
| filterString += 'invert(100%) '; |
| break; |
| case 'blur': |
| filterString += 'blur(2px) '; |
| break; |
| case 'hue-rotate': |
| filterString += 'hue-rotate(90deg) '; |
| break; |
| } |
| } |
| |
| canvas.style.filter = filterString.trim(); |
| } |
| |
| function resetImage() { |
| if (!originalImage) return; |
| |
| currentImage = originalImage; |
| setupCanvas(originalImage); |
| drawImage(); |
| |
| |
| textOptions.classList.add('hidden'); |
| drawOptions.classList.add('hidden'); |
| isDrawing = false; |
| isCropping = false; |
| cropControls.style.display = 'none'; |
| } |
| |
| function downloadImage() { |
| if (!currentImage) { |
| alert('Por favor, carregue uma imagem primeiro.'); |
| return; |
| } |
| |
| const link = document.createElement('a'); |
| link.download = 'imagem-editada.png'; |
| link.href = canvas.toDataURL('image/png'); |
| link.click(); |
| } |
| |
| function rotateImage() { |
| rotationAngle += 90; |
| if (rotationAngle >= 360) rotationAngle = 0; |
| |
| |
| if (rotationAngle % 180 === 90) { |
| const temp = canvas.width; |
| canvas.width = canvas.height; |
| canvas.height = temp; |
| } |
| |
| drawImage(); |
| } |
| |
| function flipHorizontal() { |
| flipH = !flipH; |
| drawImage(); |
| } |
| |
| function flipVertical() { |
| flipV = !flipV; |
| drawImage(); |
| } |
| |
| function toggleTextOptions() { |
| if (textOptions.classList.contains('hidden')) { |
| textOptions.classList.remove('hidden'); |
| drawOptions.classList.add('hidden'); |
| isDrawing = false; |
| } else { |
| textOptions.classList.add('hidden'); |
| } |
| } |
| |
| function toggleDrawOptions() { |
| if (drawOptions.classList.contains('hidden')) { |
| drawOptions.classList.remove('hidden'); |
| textOptions.classList.add('hidden'); |
| } else { |
| drawOptions.classList.add('hidden'); |
| isDrawing = false; |
| } |
| } |
| |
| function addTextToImage() { |
| if (!textInput.value.trim()) { |
| alert('Por favor, introduza um texto.'); |
| return; |
| } |
| |
| ctx.font = `${textSizeSlider.value}px ${textFont.value}`; |
| ctx.fillStyle = textColor.value; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| |
| const x = canvas.width / 2; |
| const y = canvas.height / 2; |
| |
| ctx.fillText(textInput.value, x, y); |
| |
| |
| const img = new Image(); |
| img.src = canvas.toDataURL(); |
| img.onload = function() { |
| currentImage = img; |
| }; |
| } |
| |
| function startDrawing() { |
| isDrawing = true; |
| canvas.style.cursor = 'crosshair'; |
| } |
| |
| function clearDrawing() { |
| drawImage(); |
| } |
| |
| function saveDrawing() { |
| isDrawing = false; |
| canvas.style.cursor = 'default'; |
| |
| |
| const img = new Image(); |
| img.src = canvas.toDataURL(); |
| img.onload = function() { |
| currentImage = img; |
| }; |
| } |
| |
| function startDraw(e) { |
| if (!isDrawing) return; |
| |
| isDrawing = true; |
| ctx.beginPath(); |
| ctx.moveTo(e.offsetX, e.offsetY); |
| ctx.strokeStyle = drawColor.value; |
| ctx.lineWidth = drawSizeSlider.value; |
| ctx.lineCap = 'round'; |
| ctx.lineJoin = 'round'; |
| } |
| |
| function draw(e) { |
| if (!isDrawing) return; |
| |
| ctx.lineTo(e.offsetX, e.offsetY); |
| ctx.stroke(); |
| } |
| |
| function endDraw() { |
| if (!isDrawing) return; |
| |
| isDrawing = false; |
| } |
| |
| |
| function handleTouchStart(e) { |
| if (!isDrawing) return; |
| e.preventDefault(); |
| |
| const touch = e.touches[0]; |
| const mouseEvent = new MouseEvent('mousedown', { |
| clientX: touch.clientX, |
| clientY: touch.clientY |
| }); |
| canvas.dispatchEvent(mouseEvent); |
| } |
| |
| function handleTouchMove(e) { |
| if (!isDrawing) return; |
| e.preventDefault(); |
| |
| const touch = e.touches[0]; |
| const mouseEvent = new MouseEvent('mousemove', { |
| clientX: touch.clientX, |
| clientY: touch.clientY |
| }); |
| canvas.dispatchEvent(mouseEvent); |
| } |
| |
| function handleTouchEnd(e) { |
| if (!isDrawing) return; |
| e.preventDefault(); |
| |
| const mouseEvent = new MouseEvent('mouseup', {}); |
| canvas.dispatchEvent(mouseEvent); |
| } |
| |
| |
| function startCropping() { |
| isCropping = true; |
| cropControls.style.display = 'flex'; |
| canvas.style.cursor = 'crosshair'; |
| } |
| |
| function startCrop(e) { |
| if (!isCropping) return; |
| |
| const rect = canvas.getBoundingClientRect(); |
| cropStartX = e.clientX - rect.left; |
| cropStartY = e.clientY - rect.top; |
| } |
| |
| function moveCrop(e) { |
| if (!isCropping) return; |
| |
| const rect = canvas.getBoundingClientRect(); |
| cropEndX = e.clientX - rect.left; |
| cropEndY = e.clientY - rect.top; |
| |
| |
| drawImage(); |
| ctx.strokeStyle = '#4f46e5'; |
| ctx.lineWidth = 2; |
| ctx.setLineDash([5, 5]); |
| ctx.strokeRect( |
| cropStartX, |
| cropStartY, |
| cropEndX - cropStartX, |
| cropEndY - cropStartY |
| ); |
| ctx.setLineDash([]); |
| } |
| |
| function endCrop(e) { |
| if (!isCropping) return; |
| |
| const rect = canvas.getBoundingClientRect(); |
| cropEndX = e.clientX - rect.left; |
| cropEndY = e.clientY - rect.top; |
| } |
| |
| function confirmCrop() { |
| if (!isCropping) return; |
| |
| |
| const x = Math.min(cropStartX, cropEndX); |
| const y = Math.min(cropStartY, cropEndY); |
| const width = Math.abs(cropEndX - cropStartX); |
| const height = Math.abs(cropEndY - cropStartY); |
| |
| |
| const tempCanvas = document.createElement('canvas'); |
| const tempCtx = tempCanvas.getContext('2d'); |
| tempCanvas.width = width; |
| tempCanvas.height = height; |
| |
| |
| tempCtx.drawImage( |
| canvas, |
| x, y, width, height, |
| 0, 0, width, height |
| ); |
| |
| |
| canvas.width = width; |
| canvas.height = height; |
| ctx.drawImage(tempCanvas, 0, 0); |
| |
| |
| const img = new Image(); |
| img.src = canvas.toDataURL(); |
| img.onload = function() { |
| currentImage = img; |
| }; |
| |
| |
| cancelCrop(); |
| } |
| |
| function cancelCrop() { |
| isCropping = false; |
| cropControls.style.display = 'none'; |
| canvas.style.cursor = 'default'; |
| drawImage(); |
| } |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=ocirema/imagem" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |