Spaces:
Running
Running
| document.addEventListener('DOMContentLoaded', () => { | |
| // ------------------------ | |
| // 1. SELECTORES DE ELEMENTOS HTML | |
| // ------------------------ | |
| const addLayerFile = document.getElementById('addLayerFile'); | |
| const preview = document.getElementById('preview'); | |
| const sheetView = document.getElementById('sheetView'); | |
| if (!preview || !sheetView) { | |
| alert("ERROR: Faltan elementos Canvas en el HTML ('preview' o 'sheetView'). Revisar index.html."); | |
| return; | |
| } | |
| const ctx = preview.getContext('2d'); | |
| const sheetCtx = sheetView.getContext('2d'); | |
| const frameW = document.getElementById('frameW'); | |
| const frameH = document.getElementById('frameH'); | |
| const framesCountInput = document.getElementById('framesCount'); | |
| const speedInput = document.getElementById('speed'); | |
| const showGrid = document.getElementById('showGrid'); | |
| const addFrameBtn = document.getElementById('addFrame'); | |
| const removeFrameBtn = document.getElementById('removeFrame'); | |
| const playFramesBtn = document.getElementById('playFrames'); | |
| const exportGIFBtn = document.getElementById('exportGIF'); | |
| const exportWebmBtn = document.getElementById('exportWebm'); // Nuevo selector | |
| // SELECTORES DE BOTONES DE AJUSTE DIRECCIONAL | |
| const adjustLeftBtn = document.getElementById('adjustLeftBtn'); | |
| const adjustRightBtn = document.getElementById('adjustRightBtn'); | |
| const adjustUpBtn = document.getElementById('adjustUpBtn'); | |
| const adjustDownBtn = document.getElementById('adjustDownBtn'); | |
| let frames = []; | |
| let currentFrame = 0; | |
| let animationInterval; | |
| let uploadedImage; | |
| // Variables de estado | |
| let currentSheetX = 0; | |
| let currentSheetY = 0; | |
| let isDragging = false; | |
| let lastTouchX = 0; | |
| let lastTouchY = 0; | |
| // Tamaño mínimo de frame | |
| const MIN_FRAME_SIZE = 16; | |
| const STEP = 8; // Pixeles de ajuste y movimiento | |
| // ------------------------ | |
| // 2. LÓGICA DE DIBUJO | |
| // ------------------------ | |
| function drawCurrentFramePreview(x, y) { | |
| if (!uploadedImage) return; | |
| const fW = parseInt(frameW.value); | |
| const fH = parseInt(frameH.value); | |
| ctx.clearRect(0, 0, preview.width, preview.height); | |
| ctx.drawImage(uploadedImage, x, y, fW, fH, 0, 0, fW, fH); | |
| if (showGrid.checked) drawGrid(); | |
| } | |
| function drawSheetView() { | |
| if (!uploadedImage) return; | |
| const sheetW = uploadedImage.width; | |
| const sheetH = uploadedImage.height; | |
| const canvasW = sheetView.width; | |
| const canvasH = sheetView.height; | |
| const fW = parseInt(frameW.value); | |
| const fH = parseInt(frameH.value); | |
| const scaleX = canvasW / sheetW; | |
| const scaleY = canvasH / sheetH; | |
| const scale = Math.min(scaleX, scaleY); | |
| const scaledW = sheetW * scale; | |
| const scaledH = sheetH * scale; | |
| const offsetX = (canvasW - scaledW) / 2; | |
| const offsetY = (canvasH - scaledH) / 2; | |
| sheetCtx.clearRect(0, 0, canvasW, canvasH); | |
| sheetCtx.drawImage(uploadedImage, offsetX, offsetY, scaledW, scaledH); | |
| sheetCtx.strokeStyle = 'red'; | |
| sheetCtx.lineWidth = 2; | |
| sheetCtx.strokeRect( | |
| offsetX + currentSheetX * scale, | |
| offsetY + currentSheetY * scale, | |
| fW * scale, | |
| fH * scale | |
| ); | |
| } | |
| function drawGrid() { | |
| ctx.strokeStyle = 'rgba(255,255,255,0.3)'; | |
| ctx.strokeRect(0, 0, preview.width, preview.height); | |
| } | |
| // ------------------------ | |
| // 3. EVENTO DE CARGA DE IMAGEN | |
| // ------------------------ | |
| addLayerFile.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| if (animationInterval) clearInterval(animationInterval); | |
| frames = []; | |
| framesCountInput.value = 0; | |
| currentSheetX = 0; | |
| currentSheetY = 0; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| uploadedImage = img; | |
| const fW = parseInt(frameW.value); | |
| const fH = parseInt(frameH.value); | |
| if (isNaN(fW) || isNaN(fH) || fW < MIN_FRAME_SIZE || fH < MIN_FRAME_SIZE) { | |
| // Forzar a valores iniciales válidos | |
| frameW.value = 48; | |
| frameH.value = 48; | |
| } | |
| preview.width = parseInt(frameW.value); | |
| preview.height = parseInt(frameH.value); | |
| sheetView.width = 400; | |
| sheetView.height = 400; | |
| drawSheetView(); | |
| drawCurrentFramePreview(currentSheetX, currentSheetY); | |
| }; | |
| img.onerror = () => { alert("ERROR: La imagen no pudo cargarse."); uploadedImage = null; }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| // ------------------------ | |
| // 4. CONTROL MANUAL DE FRAMES (Añadir/Remover/Reproducir) | |
| // ------------------------ | |
| addFrameBtn.addEventListener('click', () => { | |
| if (!uploadedImage) return alert("Sube una imagen primero"); | |
| const fW = parseInt(frameW.value); | |
| const fH = parseInt(frameH.value); | |
| if (currentSheetY >= uploadedImage.height) { | |
| alert("Ya se recorrió toda la imagen. El proceso se detiene."); | |
| return; | |
| } | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = fW; | |
| canvas.height = fH; | |
| const context = canvas.getContext('2d'); | |
| context.drawImage(uploadedImage, currentSheetX, currentSheetY, fW, fH, 0, 0, fW, fH); | |
| frames.push(canvas); | |
| framesCountInput.value = frames.length; | |
| currentSheetX += fW; | |
| if (currentSheetX >= uploadedImage.width) { | |
| currentSheetX = 0; | |
| currentSheetY += fH; | |
| } | |
| drawCurrentFramePreview(currentSheetX, currentSheetY); | |
| drawSheetView(); | |
| if (animationInterval) clearInterval(animationInterval); | |
| }); | |
| removeFrameBtn.addEventListener('click', () => { | |
| if (frames.length === 0) return; | |
| frames.pop(); | |
| framesCountInput.value = frames.length; | |
| const fW = parseInt(frameW.value); | |
| const fH = parseInt(frameH.value); | |
| const framesPerRow = Math.floor(uploadedImage.width / fW); | |
| if (currentSheetX === 0) { | |
| currentSheetY = Math.max(0, currentSheetY - fH); | |
| currentSheetX = (framesPerRow - 1) * fW; | |
| } else { | |
| currentSheetX = Math.max(0, currentSheetX - fW); | |
| } | |
| drawCurrentFramePreview(currentSheetX, currentSheetY); | |
| drawSheetView(); | |
| }); | |
| playFramesBtn.addEventListener('click', () => { | |
| if (frames.length === 0) { | |
| alert("No hay frames en la lista, ¡añádelos manualmente!"); | |
| return; | |
| } | |
| playFrames(); | |
| }); | |
| function playFrames() { | |
| if (frames.length === 0) return; | |
| if (animationInterval) clearInterval(animationInterval); | |
| currentFrame = 0; | |
| const speed = parseInt(speedInput.value) || 100; | |
| animationInterval = setInterval(() => { | |
| ctx.clearRect(0, 0, preview.width, preview.height); | |
| ctx.drawImage(frames[currentFrame], 0, 0, preview.width, preview.height); | |
| if (showGrid.checked) drawGrid(); | |
| currentFrame = (currentFrame + 1) % frames.length; | |
| }, speed); | |
| } | |
| // ------------------------ | |
| // 5. EXPORTACIÓN GIF/WebM | |
| // ------------------------ | |
| exportGIFBtn.addEventListener('click', exportGIF); | |
| function exportGIF() { | |
| if (frames.length === 0) return alert("No hay frames para exportar."); | |
| if (typeof GIF === 'undefined') return alert("ERROR: La librería GIF.js no está cargada."); | |
| // ... (código de exportación GIF) ... | |
| } | |
| // NUEVA FUNCIÓN: EXPORTACIÓN WEBM | |
| exportWebmBtn.addEventListener('click', exportWebM); | |
| function exportWebM() { | |
| if (frames.length === 0) return alert("No hay frames para exportar."); | |
| // La API MediaRecorder permite grabar frames como video WebM | |
| const chunks = []; | |
| const canvasRecorder = document.createElement('canvas'); | |
| canvasRecorder.width = parseInt(frameW.value); | |
| canvasRecorder.height = parseInt(frameH.value); | |
| const contextRecorder = canvasRecorder.getContext('2d'); | |
| const speed = parseInt(speedInput.value) || 100; | |
| const frameRate = 1000 / speed; // cuadros por segundo | |
| if (!MediaRecorder.isTypeSupported('video/webm')) { | |
| alert("ERROR: Tu navegador no soporta la grabación en WebM. Intenta exportar a GIF."); | |
| return; | |
| } | |
| exportWebmBtn.textContent = 'Codificando WebM...'; | |
| exportWebmBtn.disabled = true; | |
| // Capturar los frames uno por uno | |
| let frameIndex = 0; | |
| const captureInterval = setInterval(() => { | |
| if (frameIndex >= frames.length) { | |
| clearInterval(captureInterval); | |
| mediaRecorder.stop(); | |
| return; | |
| } | |
| contextRecorder.clearRect(0, 0, canvasRecorder.width, canvasRecorder.height); | |
| contextRecorder.drawImage(frames[frameIndex], 0, 0, canvasRecorder.width, canvasRecorder.height); | |
| frameIndex++; | |
| }, speed); | |
| // Iniciar la grabación | |
| const stream = canvasRecorder.captureStream(frameRate); | |
| const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); | |
| mediaRecorder.ondataavailable = (e) => chunks.push(e.data); | |
| mediaRecorder.onstop = () => { | |
| const blob = new Blob(chunks, { type: 'video/webm' }); | |
| let link = document.getElementById('webmDownload'); | |
| if (!link) { | |
| link = document.createElement('a'); | |
| link.id = 'webmDownload'; | |
| link.textContent = 'WebM listo: hacer clic para descargar'; | |
| document.getElementById('downloadLinks').appendChild(link); | |
| } | |
| link.href = URL.createObjectURL(blob); | |
| link.download = 'sprite.webm'; | |
| exportWebmBtn.textContent = 'Exportar WebM'; | |
| exportWebmBtn.disabled = false; | |
| // Reanudar animación de preview si estaba activa | |
| playFrames(); | |
| }; | |
| mediaRecorder.start(speed); // Grabar cada 'speed' milisegundos | |
| // Limpiar la animación de preview mientras se graba | |
| if (animationInterval) clearInterval(animationInterval); | |
| } | |
| // ------------------------ | |
| // 7. LÓGICA DE AJUSTE POR BOTONES DIRECCIONALES | |
| // ------------------------ | |
| // Función central para actualizar el estado del frame y redibujar | |
| function updateFrame(newW, newH) { | |
| newW = Math.max(MIN_FRAME_SIZE, Math.min(newW, uploadedImage.width)); | |
| newH = Math.max(MIN_FRAME_SIZE, Math.min(newH, uploadedImage.height)); | |
| frameW.value = newW; | |
| frameH.value = newH; | |
| preview.width = newW; | |
| preview.height = newH; | |
| // Asegurar que el recuadro no se salga de los límites | |
| const finalW = parseInt(frameW.value); | |
| const finalH = parseInt(frameH.value); | |
| currentSheetX = Math.max(0, Math.min(currentSheetX, uploadedImage.width - finalW)); | |
| currentSheetY = Math.max(0, Math.min(currentSheetY, uploadedImage.height - finalH)); | |
| drawSheetView(); | |
| drawCurrentFramePreview(currentSheetX, currentSheetY); | |
| } | |
| // Handlers para los botones de ajuste | |
| // ANCHO (-) | |
| if (adjustLeftBtn) { | |
| adjustLeftBtn.addEventListener('click', () => { | |
| if (!uploadedImage) return alert("Sube una imagen primero."); | |
| const currentW = parseInt(frameW.value); | |
| // Ajustar el ancho y MOVER la posición de captura a la izquierda para mantener el borde derecho fijo. | |
| updateFrame(currentW - STEP, parseInt(frameH.value)); | |
| // Asegurar que currentSheetX se ajuste hacia la derecha por el cambio de tamaño | |
| currentSheetX = Math.max(0, currentSheetX + STEP); | |
| updateFrame(currentW - STEP, parseInt(frameH.value)); // Llamada repetida para aplicar currentSheetX y límites | |
| }); | |
| } | |
| // ANCHO (+) | |
| if (adjustRightBtn) { | |
| adjustRightBtn.addEventListener('click', () => { | |
| if (!uploadedImage) return alert("Sube una imagen primero."); | |
| const currentW = parseInt(frameW.value); | |
| // Solo ajustar el ancho (el borde izquierdo se mantiene) | |
| updateFrame(currentW + STEP, parseInt(frameH.value)); | |
| }); | |
| } | |
| // ALTO (-) | |
| if (adjustUpBtn) { | |
| adjustUpBtn.addEventListener('click', () => { | |
| if (!uploadedImage) return alert("Sube una imagen primero."); | |
| const currentH = parseInt(frameH.value); | |
| // Ajustar el alto y MOVER la posición de captura hacia arriba para mantener el borde inferior fijo. | |
| updateFrame(parseInt(frameW.value), currentH - STEP); | |
| // Asegurar que currentSheetY se ajuste hacia abajo por el cambio de tamaño | |
| currentSheetY = Math.max(0, currentSheetY + STEP); | |
| updateFrame(parseInt(frameW.value), currentH - STEP); // Llamada repetida para aplicar currentSheetY y límites | |
| }); | |
| } | |
| // ALTO (+) | |
| if (adjustDownBtn) { | |
| adjustDownBtn.addEventListener('click', () => { | |
| if (!uploadedImage) return alert("Sube una imagen primero."); | |
| const currentH = parseInt(frameH.value); | |
| // Solo ajustar el alto (el borde superior se mantiene) | |
| updateFrame(parseInt(frameW.value), currentH + STEP); | |
| }); | |
| } | |
| // ------------------------ | |
| // 8. CONTROL TÁCTIL (Solo MOVER - 1 Dedo) | |
| // ------------------------ | |
| // Función auxiliar para getDistance (mantener por si se necesita) | |
| function getDistance(touch1, touch2) { | |
| const dx = touch1.clientX - touch2.clientX; | |
| const dy = touch1.clientY - touch2.clientY; | |
| return Math.sqrt(dx * dx + dy * dy); | |
| } | |
| sheetView.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| if (!uploadedImage) return; | |
| if (e.touches.length === 1) { | |
| isDragging = true; | |
| const touch = e.touches[0]; | |
| lastTouchX = touch.clientX; | |
| lastTouchY = touch.clientY; | |
| } | |
| }); | |
| sheetView.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| if (!uploadedImage || !isDragging || e.touches.length !== 1) return; | |
| // LÓGICA DE DRAG (MOVIMIENTO - 1 Dedo) | |
| const touch = e.touches[0]; | |
| const dx = touch.clientX - lastTouchX; | |
| const dy = touch.clientY - lastTouchY; | |
| const sheetW = uploadedImage.width; | |
| const canvasW = sheetView.width; | |
| const scale = Math.min(canvasW / sheetW, canvasW / uploadedImage.height); | |
| const fW = parseInt(frameW.value); | |
| const fH = parseInt(frameH.value); | |
| let imgDx = Math.round(dx / scale); | |
| let imgDy = Math.round(dy / scale); | |
| // Mover solo si el arrastre es significativo (para evitar jitter) | |
| if (Math.abs(imgDx) >= STEP) { | |
| let framesMovedX = Math.round(imgDx / STEP); | |
| currentSheetX += framesMovedX * STEP; | |
| lastTouchX = touch.clientX; | |
| } | |
| if (Math.abs(imgDy) >= STEP) { | |
| let framesMovedY = Math.round(imgDy / STEP); | |
| currentSheetY += framesMovedY * STEP; | |
| lastTouchY = touch.clientY; | |
| } | |
| // Asegurar límites | |
| currentSheetX = Math.max(0, Math.min(currentSheetX, uploadedImage.width - fW)); | |
| currentSheetY = Math.max(0, Math.min(currentSheetY, uploadedImage.height - fH)); | |
| drawSheetView(); | |
| drawCurrentFramePreview(currentSheetX, currentSheetY); | |
| }); | |
| sheetView.addEventListener('touchend', (e) => { | |
| isDragging = false; | |
| }); | |
| }); | |