|
<!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> |