IA-animate / app.js
Andro0s's picture
Update app.js
91ef932 verified
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;
});
});