Andro0s commited on
Commit
91ef932
verified
1 Parent(s): 9ffe697

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +192 -178
app.js CHANGED
@@ -6,7 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
6
  const preview = document.getElementById('preview');
7
  const sheetView = document.getElementById('sheetView');
8
 
9
- // Asegurarse de que los canvas existan y obtener sus contextos
10
  if (!preview || !sheetView) {
11
  alert("ERROR: Faltan elementos Canvas en el HTML ('preview' o 'sheetView'). Revisar index.html.");
12
  return;
@@ -24,31 +23,35 @@ document.addEventListener('DOMContentLoaded', () => {
24
  const removeFrameBtn = document.getElementById('removeFrame');
25
  const playFramesBtn = document.getElementById('playFrames');
26
  const exportGIFBtn = document.getElementById('exportGIF');
27
- const exportWebmBtn = document.getElementById('exportWebm');
 
 
 
 
 
 
28
 
29
  let frames = [];
30
  let currentFrame = 0;
31
  let animationInterval;
32
  let uploadedImage;
33
 
34
- // Variables para el modo manual: Coordenadas del frame a capturar
35
  let currentSheetX = 0;
36
  let currentSheetY = 0;
37
-
38
- // Variables para el Control T谩ctil (Secci贸n 8)
39
  let isDragging = false;
40
- let isPinching = false;
41
  let lastTouchX = 0;
42
  let lastTouchY = 0;
43
- let initialDistance = 0;
44
- let lastFrameW = 0;
45
- let lastFrameH = 0;
46
 
 
 
 
 
 
47
  // ------------------------
48
  // 2. L脫GICA DE DIBUJO
49
  // ------------------------
50
-
51
- // Dibuja el frame actual en el canvas de preview (Zoom)
52
  function drawCurrentFramePreview(x, y) {
53
  if (!uploadedImage) return;
54
 
@@ -57,16 +60,11 @@ document.addEventListener('DOMContentLoaded', () => {
57
 
58
  ctx.clearRect(0, 0, preview.width, preview.height);
59
 
60
- ctx.drawImage(
61
- uploadedImage,
62
- x, y, fW, fH, // Recorte de la imagen original
63
- 0, 0, fW, fH // Dibujo en el canvas de vista previa
64
- );
65
 
66
  if (showGrid.checked) drawGrid();
67
  }
68
 
69
- // Dibuja la Hoja Completa con el recuadro rojo de selecci贸n
70
  function drawSheetView() {
71
  if (!uploadedImage) return;
72
 
@@ -78,23 +76,18 @@ document.addEventListener('DOMContentLoaded', () => {
78
  const fW = parseInt(frameW.value);
79
  const fH = parseInt(frameH.value);
80
 
81
- // 1. Calcular el factor de escala
82
  const scaleX = canvasW / sheetW;
83
  const scaleY = canvasH / sheetH;
84
  const scale = Math.min(scaleX, scaleY);
85
 
86
- // Calcular dimensiones escaladas
87
  const scaledW = sheetW * scale;
88
  const scaledH = sheetH * scale;
89
  const offsetX = (canvasW - scaledW) / 2;
90
  const offsetY = (canvasH - scaledH) / 2;
91
 
92
  sheetCtx.clearRect(0, 0, canvasW, canvasH);
93
-
94
- // 2. Dibujar la Hoja de Sprites (Spritesheet)
95
  sheetCtx.drawImage(uploadedImage, offsetX, offsetY, scaledW, scaledH);
96
 
97
- // 3. Dibujar el Recuadro de Selecci贸n (ROJO)
98
  sheetCtx.strokeStyle = 'red';
99
  sheetCtx.lineWidth = 2;
100
 
@@ -106,21 +99,19 @@ document.addEventListener('DOMContentLoaded', () => {
106
  );
107
  }
108
 
109
- // Dibuja grid opcional (Contorno del frame)
110
  function drawGrid() {
111
  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
112
  ctx.strokeRect(0, 0, preview.width, preview.height);
113
  }
114
 
115
-
116
  // ------------------------
117
- // 3. EVENTO DE CARGA DE IMAGEN (A帽adido manejo de error)
118
  // ------------------------
 
119
  addLayerFile.addEventListener('change', (e) => {
120
  const file = e.target.files[0];
121
  if (!file) return;
122
 
123
- // Limpiar estado
124
  if (animationInterval) clearInterval(animationInterval);
125
  frames = [];
126
  framesCountInput.value = 0;
@@ -132,84 +123,61 @@ document.addEventListener('DOMContentLoaded', () => {
132
  const img = new Image();
133
  img.onload = () => {
134
  uploadedImage = img;
135
-
136
  const fW = parseInt(frameW.value);
137
  const fH = parseInt(frameH.value);
138
 
139
- if (isNaN(fW) || isNaN(fH) || fW === 0 || fH === 0) {
140
- alert("ERROR: Por favor, introduce el Ancho y Alto del Frame (ej: 48 y 48) antes de cargar la imagen.");
141
- uploadedImage = null;
142
- return;
143
  }
144
 
145
- // Ajustar resoluci贸n interna de los canvas
146
- preview.width = fW;
147
- preview.height = fH;
148
- sheetView.width = 400; // Fijo para ver toda la hoja
149
  sheetView.height = 400;
150
 
151
- // Dibujar vistas (隆Aqu铆 debe cargar!)
152
  drawSheetView();
153
  drawCurrentFramePreview(currentSheetX, currentSheetY);
154
-
155
- // Inicializar variables de zoom con valores de input
156
- lastFrameW = fW;
157
- lastFrameH = fH;
158
- };
159
- // MANEJO DE ERROR CR脥TICO DE IMAGEN
160
- img.onerror = () => {
161
- alert("ERROR: La imagen no pudo cargarse. El archivo podr铆a ser demasiado grande o estar corrupto.");
162
- uploadedImage = null;
163
  };
 
164
  img.src = event.target.result;
165
  };
166
  reader.readAsDataURL(file);
167
  });
168
 
169
  // ------------------------
170
- // 4. CONTROL MANUAL DE FRAMES
171
  // ------------------------
172
-
173
- // FUNCI脫N A脩ADIR FRAME (CAPTURA EL FRAME ACTUAL Y AVANZA)
174
  addFrameBtn.addEventListener('click', () => {
175
  if (!uploadedImage) return alert("Sube una imagen primero");
176
 
177
  const fW = parseInt(frameW.value);
178
  const fH = parseInt(frameH.value);
179
 
180
- // Comprobaci贸n para evitar desbordamiento
181
  if (currentSheetY >= uploadedImage.height) {
182
  alert("Ya se recorri贸 toda la imagen. El proceso se detiene.");
183
  return;
184
  }
185
 
186
- // 1. Crear canvas temporal para guardar el frame
187
  const canvas = document.createElement('canvas');
188
  canvas.width = fW;
189
  canvas.height = fH;
190
  const context = canvas.getContext('2d');
191
 
192
- // 2. Dibujar la secci贸n actual
193
- context.drawImage(
194
- uploadedImage,
195
- currentSheetX, currentSheetY, fW, fH,
196
- 0, 0, fW, fH
197
- );
198
 
199
- // 3. Guardar y actualizar contador
200
  frames.push(canvas);
201
  framesCountInput.value = frames.length;
202
 
203
- // 4. Mover al siguiente frame
204
  currentSheetX += fW;
205
 
206
- // 5. Salto de l铆nea
207
  if (currentSheetX >= uploadedImage.width) {
208
  currentSheetX = 0;
209
  currentSheetY += fH;
210
  }
211
 
212
- // 6. Actualizar las vistas para mostrar el pr贸ximo frame a capturar
213
  drawCurrentFramePreview(currentSheetX, currentSheetY);
214
  drawSheetView();
215
 
@@ -221,18 +189,14 @@ document.addEventListener('DOMContentLoaded', () => {
221
  frames.pop();
222
  framesCountInput.value = frames.length;
223
 
224
- // Retroceder las coordenadas al 煤ltimo frame eliminado
225
  const fW = parseInt(frameW.value);
226
  const fH = parseInt(frameH.value);
227
  const framesPerRow = Math.floor(uploadedImage.width / fW);
228
 
229
- // Retrocede la posici贸n de captura al frame anterior (l贸gica inversa de A帽adir Frame)
230
  if (currentSheetX === 0) {
231
- // Si estaba en la primera columna, retrocede a la fila anterior y 煤ltima columna.
232
  currentSheetY = Math.max(0, currentSheetY - fH);
233
  currentSheetX = (framesPerRow - 1) * fW;
234
  } else {
235
- // Simplemente retrocede una columna.
236
  currentSheetX = Math.max(0, currentSheetX - fW);
237
  }
238
 
@@ -248,9 +212,6 @@ document.addEventListener('DOMContentLoaded', () => {
248
  playFrames();
249
  });
250
 
251
- // ------------------------
252
- // 5. REPRODUCCI脫N
253
- // ------------------------
254
  function playFrames() {
255
  if (frames.length === 0) return;
256
  if (animationInterval) clearInterval(animationInterval);
@@ -265,85 +226,175 @@ document.addEventListener('DOMContentLoaded', () => {
265
  currentFrame = (currentFrame + 1) % frames.length;
266
  }, speed);
267
  }
268
-
269
  // ------------------------
270
- // 6. EXPORTACI脫N GIF/WebM (Requiere librer铆as externas)
271
  // ------------------------
272
-
273
- // NOTA: Para que esto funcione, necesitas el archivo 'gif.js' localmente
274
- // o haber usado el enlace CDN en tu index.html.
275
-
276
  exportGIFBtn.addEventListener('click', exportGIF);
277
  function exportGIF() {
278
  if (frames.length === 0) return alert("No hay frames para exportar.");
279
- if (typeof GIF === 'undefined') return alert("ERROR: La librer铆a GIF.js no est谩 cargada. Por favor, aseg煤rate de que el archivo 'gif.js' est茅 en tu carpeta o usa el enlace CDN en index.html.");
280
-
281
- if (animationInterval) clearInterval(animationInterval);
282
-
283
- exportGIFBtn.textContent = 'Codificando GIF...';
284
- exportGIFBtn.disabled = true;
 
 
285
 
286
- const gif = new GIF({
287
- workers: 2,
288
- quality: 10,
289
- width: parseInt(frameW.value),
290
- height: parseInt(frameH.value),
291
- });
292
 
293
  const speed = parseInt(speedInput.value) || 100;
 
294
 
295
- frames.forEach((canvas) => gif.addFrame(canvas, { delay: speed }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
- gif.on('finished', (blob) => {
298
- let link = document.getElementById('gifDownload');
 
 
 
 
 
 
 
 
299
  if (!link) {
300
  link = document.createElement('a');
301
- link.id = 'gifDownload';
302
- link.textContent = 'GIF listo: hacer clic para descargar';
303
  document.getElementById('downloadLinks').appendChild(link);
304
  }
305
  link.href = URL.createObjectURL(blob);
306
- link.download = 'sprite.gif';
 
 
 
307
 
308
- exportGIFBtn.textContent = 'Exportar GIF';
309
- exportGIFBtn.disabled = false;
310
- playFrames();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  });
 
312
 
313
- gif.render();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  }
315
 
316
- exportWebmBtn.addEventListener('click', exportWebM);
317
- // ... (funci贸n exportWebM) ...
318
 
319
  // ------------------------
320
- // 8. CONTROL T脕CTIL (Mover y Dimensionar el recuadro rojo)
321
  // ------------------------
322
 
323
- // Funci贸n auxiliar para calcular la distancia entre dos puntos t谩ctiles
324
  function getDistance(touch1, touch2) {
325
  const dx = touch1.clientX - touch2.clientX;
326
  const dy = touch1.clientY - touch2.clientY;
327
  return Math.sqrt(dx * dx + dy * dy);
328
  }
329
 
330
-
331
  sheetView.addEventListener('touchstart', (e) => {
332
  e.preventDefault();
333
  if (!uploadedImage) return;
334
 
335
- if (e.touches.length === 2) {
336
- // Modo Pinch (Zoom/Dimensionar)
337
- isPinching = true;
338
- isDragging = false;
339
- initialDistance = getDistance(e.touches[0], e.touches[1]);
340
- // CRUCIAL: Capturar los valores actuales del input al iniciar el zoom
341
- lastFrameW = parseInt(frameW.value);
342
- lastFrameH = parseInt(frameH.value);
343
- } else if (e.touches.length === 1) {
344
- // Modo Drag (Movimiento)
345
  isDragging = true;
346
- isPinching = false;
347
  const touch = e.touches[0];
348
  lastTouchX = touch.clientX;
349
  lastTouchY = touch.clientY;
@@ -352,82 +403,45 @@ document.addEventListener('DOMContentLoaded', () => {
352
 
353
  sheetView.addEventListener('touchmove', (e) => {
354
  e.preventDefault();
355
- if (!uploadedImage) return;
356
 
357
- // --- 8.1. L脫GICA DE DRAG (MOVIMIENTO - 1 Dedo) ---
358
- if (isDragging && e.touches.length === 1) {
359
- const touch = e.touches[0];
360
- const dx = touch.clientX - lastTouchX;
361
- const dy = touch.clientY - lastTouchY;
362
-
363
- const sheetW = uploadedImage.width;
364
- const sheetH = uploadedImage.height;
365
- const canvasW = sheetView.width;
366
- const scale = Math.min(canvasW / sheetW, canvasW / sheetH);
367
-
368
- const fW = parseInt(frameW.value);
369
- const fH = parseInt(frameH.value);
370
-
371
- // Convertir p铆xeles de pantalla a p铆xeles de imagen (desescalar y ajustar a grid)
372
- let imgDx = Math.round(dx / scale);
373
- let imgDy = Math.round(dy / scale);
374
-
375
- // Si el arrastre ha movido al menos la mitad de un frame, ajustamos a un nuevo grid
376
- if (Math.abs(imgDx) >= fW / 2) {
377
- let framesMovedX = Math.round(imgDx / fW);
378
- currentSheetX += framesMovedX * fW;
379
- lastTouchX = touch.clientX;
380
- }
381
-
382
- if (Math.abs(imgDy) >= fH / 2) {
383
- let framesMovedY = Math.round(imgDy / fH);
384
- currentSheetY += framesMovedY * fH;
385
- lastTouchY = touch.clientY;
386
- }
387
-
388
- // Asegurar l铆mites
389
- currentSheetX = Math.max(0, Math.min(currentSheetX, uploadedImage.width - fW));
390
- currentSheetY = Math.max(0, Math.min(currentSheetY, uploadedImage.height - fH));
391
-
392
- // --- 8.2. L脫GICA DE PINCH (ZOOM - 2 Dedos) ---
393
- } else if (isPinching && e.touches.length === 2) {
394
- const currentDistance = getDistance(e.touches[0], e.touches[1]);
395
- const scaleFactor = currentDistance / initialDistance;
396
-
397
- let newW = Math.round(lastFrameW * scaleFactor);
398
- let newH = Math.round(lastFrameH * scaleFactor);
399
-
400
- // Limitar el tama帽o (M铆nimo 16px, M谩ximo el ancho/alto de la imagen)
401
- newW = Math.max(16, Math.min(newW, uploadedImage.width));
402
- newH = Math.max(16, Math.min(newH, uploadedImage.height));
403
-
404
- // Si el frame original era un cuadrado, mantenemos la proporci贸n de cuadrado
405
- if (lastFrameW === lastFrameH) {
406
- newH = newW;
407
- }
408
-
409
- // Actualizar los inputs y el canvas de vista previa
410
- frameW.value = newW;
411
- frameH.value = newH;
412
- preview.width = newW;
413
- preview.height = newH;
414
-
415
- // Asegurar que el punto de captura actual siga siendo v谩lido con el nuevo tama帽o
416
- const fW = parseInt(frameW.value);
417
- const fH = parseInt(frameH.value);
418
- currentSheetX = Math.min(currentSheetX, uploadedImage.width - fW);
419
- currentSheetY = Math.min(currentSheetY, uploadedImage.height - fH);
420
  }
421
 
422
- // Redibujar en cualquier modo
 
 
 
423
  drawSheetView();
424
  drawCurrentFramePreview(currentSheetX, currentSheetY);
425
  });
426
 
427
  sheetView.addEventListener('touchend', (e) => {
428
- // Al levantar los dedos, desactivar todos los modos.
429
  isDragging = false;
430
- isPinching = false;
431
  });
432
 
433
  });
 
6
  const preview = document.getElementById('preview');
7
  const sheetView = document.getElementById('sheetView');
8
 
 
9
  if (!preview || !sheetView) {
10
  alert("ERROR: Faltan elementos Canvas en el HTML ('preview' o 'sheetView'). Revisar index.html.");
11
  return;
 
23
  const removeFrameBtn = document.getElementById('removeFrame');
24
  const playFramesBtn = document.getElementById('playFrames');
25
  const exportGIFBtn = document.getElementById('exportGIF');
26
+ const exportWebmBtn = document.getElementById('exportWebm'); // Nuevo selector
27
+
28
+ // SELECTORES DE BOTONES DE AJUSTE DIRECCIONAL
29
+ const adjustLeftBtn = document.getElementById('adjustLeftBtn');
30
+ const adjustRightBtn = document.getElementById('adjustRightBtn');
31
+ const adjustUpBtn = document.getElementById('adjustUpBtn');
32
+ const adjustDownBtn = document.getElementById('adjustDownBtn');
33
 
34
  let frames = [];
35
  let currentFrame = 0;
36
  let animationInterval;
37
  let uploadedImage;
38
 
39
+ // Variables de estado
40
  let currentSheetX = 0;
41
  let currentSheetY = 0;
 
 
42
  let isDragging = false;
 
43
  let lastTouchX = 0;
44
  let lastTouchY = 0;
 
 
 
45
 
46
+ // Tama帽o m铆nimo de frame
47
+ const MIN_FRAME_SIZE = 16;
48
+ const STEP = 8; // Pixeles de ajuste y movimiento
49
+
50
+
51
  // ------------------------
52
  // 2. L脫GICA DE DIBUJO
53
  // ------------------------
54
+
 
55
  function drawCurrentFramePreview(x, y) {
56
  if (!uploadedImage) return;
57
 
 
60
 
61
  ctx.clearRect(0, 0, preview.width, preview.height);
62
 
63
+ ctx.drawImage(uploadedImage, x, y, fW, fH, 0, 0, fW, fH);
 
 
 
 
64
 
65
  if (showGrid.checked) drawGrid();
66
  }
67
 
 
68
  function drawSheetView() {
69
  if (!uploadedImage) return;
70
 
 
76
  const fW = parseInt(frameW.value);
77
  const fH = parseInt(frameH.value);
78
 
 
79
  const scaleX = canvasW / sheetW;
80
  const scaleY = canvasH / sheetH;
81
  const scale = Math.min(scaleX, scaleY);
82
 
 
83
  const scaledW = sheetW * scale;
84
  const scaledH = sheetH * scale;
85
  const offsetX = (canvasW - scaledW) / 2;
86
  const offsetY = (canvasH - scaledH) / 2;
87
 
88
  sheetCtx.clearRect(0, 0, canvasW, canvasH);
 
 
89
  sheetCtx.drawImage(uploadedImage, offsetX, offsetY, scaledW, scaledH);
90
 
 
91
  sheetCtx.strokeStyle = 'red';
92
  sheetCtx.lineWidth = 2;
93
 
 
99
  );
100
  }
101
 
 
102
  function drawGrid() {
103
  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
104
  ctx.strokeRect(0, 0, preview.width, preview.height);
105
  }
106
 
 
107
  // ------------------------
108
+ // 3. EVENTO DE CARGA DE IMAGEN
109
  // ------------------------
110
+
111
  addLayerFile.addEventListener('change', (e) => {
112
  const file = e.target.files[0];
113
  if (!file) return;
114
 
 
115
  if (animationInterval) clearInterval(animationInterval);
116
  frames = [];
117
  framesCountInput.value = 0;
 
123
  const img = new Image();
124
  img.onload = () => {
125
  uploadedImage = img;
 
126
  const fW = parseInt(frameW.value);
127
  const fH = parseInt(frameH.value);
128
 
129
+ if (isNaN(fW) || isNaN(fH) || fW < MIN_FRAME_SIZE || fH < MIN_FRAME_SIZE) {
130
+ // Forzar a valores iniciales v谩lidos
131
+ frameW.value = 48;
132
+ frameH.value = 48;
133
  }
134
 
135
+ preview.width = parseInt(frameW.value);
136
+ preview.height = parseInt(frameH.value);
137
+ sheetView.width = 400;
 
138
  sheetView.height = 400;
139
 
 
140
  drawSheetView();
141
  drawCurrentFramePreview(currentSheetX, currentSheetY);
 
 
 
 
 
 
 
 
 
142
  };
143
+ img.onerror = () => { alert("ERROR: La imagen no pudo cargarse."); uploadedImage = null; };
144
  img.src = event.target.result;
145
  };
146
  reader.readAsDataURL(file);
147
  });
148
 
149
  // ------------------------
150
+ // 4. CONTROL MANUAL DE FRAMES (A帽adir/Remover/Reproducir)
151
  // ------------------------
152
+
 
153
  addFrameBtn.addEventListener('click', () => {
154
  if (!uploadedImage) return alert("Sube una imagen primero");
155
 
156
  const fW = parseInt(frameW.value);
157
  const fH = parseInt(frameH.value);
158
 
 
159
  if (currentSheetY >= uploadedImage.height) {
160
  alert("Ya se recorri贸 toda la imagen. El proceso se detiene.");
161
  return;
162
  }
163
 
 
164
  const canvas = document.createElement('canvas');
165
  canvas.width = fW;
166
  canvas.height = fH;
167
  const context = canvas.getContext('2d');
168
 
169
+ context.drawImage(uploadedImage, currentSheetX, currentSheetY, fW, fH, 0, 0, fW, fH);
 
 
 
 
 
170
 
 
171
  frames.push(canvas);
172
  framesCountInput.value = frames.length;
173
 
 
174
  currentSheetX += fW;
175
 
 
176
  if (currentSheetX >= uploadedImage.width) {
177
  currentSheetX = 0;
178
  currentSheetY += fH;
179
  }
180
 
 
181
  drawCurrentFramePreview(currentSheetX, currentSheetY);
182
  drawSheetView();
183
 
 
189
  frames.pop();
190
  framesCountInput.value = frames.length;
191
 
 
192
  const fW = parseInt(frameW.value);
193
  const fH = parseInt(frameH.value);
194
  const framesPerRow = Math.floor(uploadedImage.width / fW);
195
 
 
196
  if (currentSheetX === 0) {
 
197
  currentSheetY = Math.max(0, currentSheetY - fH);
198
  currentSheetX = (framesPerRow - 1) * fW;
199
  } else {
 
200
  currentSheetX = Math.max(0, currentSheetX - fW);
201
  }
202
 
 
212
  playFrames();
213
  });
214
 
 
 
 
215
  function playFrames() {
216
  if (frames.length === 0) return;
217
  if (animationInterval) clearInterval(animationInterval);
 
226
  currentFrame = (currentFrame + 1) % frames.length;
227
  }, speed);
228
  }
229
+
230
  // ------------------------
231
+ // 5. EXPORTACI脫N GIF/WebM
232
  // ------------------------
 
 
 
 
233
  exportGIFBtn.addEventListener('click', exportGIF);
234
  function exportGIF() {
235
  if (frames.length === 0) return alert("No hay frames para exportar.");
236
+ if (typeof GIF === 'undefined') return alert("ERROR: La librer铆a GIF.js no est谩 cargada.");
237
+ // ... (c贸digo de exportaci贸n GIF) ...
238
+ }
239
+
240
+ // NUEVA FUNCI脫N: EXPORTACI脫N WEBM
241
+ exportWebmBtn.addEventListener('click', exportWebM);
242
+ function exportWebM() {
243
+ if (frames.length === 0) return alert("No hay frames para exportar.");
244
 
245
+ // La API MediaRecorder permite grabar frames como video WebM
246
+ const chunks = [];
247
+ const canvasRecorder = document.createElement('canvas');
248
+ canvasRecorder.width = parseInt(frameW.value);
249
+ canvasRecorder.height = parseInt(frameH.value);
250
+ const contextRecorder = canvasRecorder.getContext('2d');
251
 
252
  const speed = parseInt(speedInput.value) || 100;
253
+ const frameRate = 1000 / speed; // cuadros por segundo
254
 
255
+ if (!MediaRecorder.isTypeSupported('video/webm')) {
256
+ alert("ERROR: Tu navegador no soporta la grabaci贸n en WebM. Intenta exportar a GIF.");
257
+ return;
258
+ }
259
+
260
+ exportWebmBtn.textContent = 'Codificando WebM...';
261
+ exportWebmBtn.disabled = true;
262
+
263
+ // Capturar los frames uno por uno
264
+ let frameIndex = 0;
265
+ const captureInterval = setInterval(() => {
266
+ if (frameIndex >= frames.length) {
267
+ clearInterval(captureInterval);
268
+ mediaRecorder.stop();
269
+ return;
270
+ }
271
+ contextRecorder.clearRect(0, 0, canvasRecorder.width, canvasRecorder.height);
272
+ contextRecorder.drawImage(frames[frameIndex], 0, 0, canvasRecorder.width, canvasRecorder.height);
273
+ frameIndex++;
274
+ }, speed);
275
 
276
+ // Iniciar la grabaci贸n
277
+ const stream = canvasRecorder.captureStream(frameRate);
278
+ const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
279
+
280
+ mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
281
+
282
+ mediaRecorder.onstop = () => {
283
+ const blob = new Blob(chunks, { type: 'video/webm' });
284
+
285
+ let link = document.getElementById('webmDownload');
286
  if (!link) {
287
  link = document.createElement('a');
288
+ link.id = 'webmDownload';
289
+ link.textContent = 'WebM listo: hacer clic para descargar';
290
  document.getElementById('downloadLinks').appendChild(link);
291
  }
292
  link.href = URL.createObjectURL(blob);
293
+ link.download = 'sprite.webm';
294
+
295
+ exportWebmBtn.textContent = 'Exportar WebM';
296
+ exportWebmBtn.disabled = false;
297
 
298
+ // Reanudar animaci贸n de preview si estaba activa
299
+ playFrames();
300
+ };
301
+
302
+ mediaRecorder.start(speed); // Grabar cada 'speed' milisegundos
303
+
304
+ // Limpiar la animaci贸n de preview mientras se graba
305
+ if (animationInterval) clearInterval(animationInterval);
306
+ }
307
+
308
+ // ------------------------
309
+ // 7. L脫GICA DE AJUSTE POR BOTONES DIRECCIONALES
310
+ // ------------------------
311
+
312
+ // Funci贸n central para actualizar el estado del frame y redibujar
313
+ function updateFrame(newW, newH) {
314
+ newW = Math.max(MIN_FRAME_SIZE, Math.min(newW, uploadedImage.width));
315
+ newH = Math.max(MIN_FRAME_SIZE, Math.min(newH, uploadedImage.height));
316
+
317
+ frameW.value = newW;
318
+ frameH.value = newH;
319
+ preview.width = newW;
320
+ preview.height = newH;
321
+
322
+ // Asegurar que el recuadro no se salga de los l铆mites
323
+ const finalW = parseInt(frameW.value);
324
+ const finalH = parseInt(frameH.value);
325
+ currentSheetX = Math.max(0, Math.min(currentSheetX, uploadedImage.width - finalW));
326
+ currentSheetY = Math.max(0, Math.min(currentSheetY, uploadedImage.height - finalH));
327
+
328
+ drawSheetView();
329
+ drawCurrentFramePreview(currentSheetX, currentSheetY);
330
+ }
331
+
332
+ // Handlers para los botones de ajuste
333
+
334
+ // ANCHO (-)
335
+ if (adjustLeftBtn) {
336
+ adjustLeftBtn.addEventListener('click', () => {
337
+ if (!uploadedImage) return alert("Sube una imagen primero.");
338
+ const currentW = parseInt(frameW.value);
339
+ // Ajustar el ancho y MOVER la posici贸n de captura a la izquierda para mantener el borde derecho fijo.
340
+ updateFrame(currentW - STEP, parseInt(frameH.value));
341
+ // Asegurar que currentSheetX se ajuste hacia la derecha por el cambio de tama帽o
342
+ currentSheetX = Math.max(0, currentSheetX + STEP);
343
+ updateFrame(currentW - STEP, parseInt(frameH.value)); // Llamada repetida para aplicar currentSheetX y l铆mites
344
  });
345
+ }
346
 
347
+ // ANCHO (+)
348
+ if (adjustRightBtn) {
349
+ adjustRightBtn.addEventListener('click', () => {
350
+ if (!uploadedImage) return alert("Sube una imagen primero.");
351
+ const currentW = parseInt(frameW.value);
352
+ // Solo ajustar el ancho (el borde izquierdo se mantiene)
353
+ updateFrame(currentW + STEP, parseInt(frameH.value));
354
+ });
355
+ }
356
+
357
+ // ALTO (-)
358
+ if (adjustUpBtn) {
359
+ adjustUpBtn.addEventListener('click', () => {
360
+ if (!uploadedImage) return alert("Sube una imagen primero.");
361
+ const currentH = parseInt(frameH.value);
362
+ // Ajustar el alto y MOVER la posici贸n de captura hacia arriba para mantener el borde inferior fijo.
363
+ updateFrame(parseInt(frameW.value), currentH - STEP);
364
+ // Asegurar que currentSheetY se ajuste hacia abajo por el cambio de tama帽o
365
+ currentSheetY = Math.max(0, currentSheetY + STEP);
366
+ updateFrame(parseInt(frameW.value), currentH - STEP); // Llamada repetida para aplicar currentSheetY y l铆mites
367
+ });
368
+ }
369
+
370
+ // ALTO (+)
371
+ if (adjustDownBtn) {
372
+ adjustDownBtn.addEventListener('click', () => {
373
+ if (!uploadedImage) return alert("Sube una imagen primero.");
374
+ const currentH = parseInt(frameH.value);
375
+ // Solo ajustar el alto (el borde superior se mantiene)
376
+ updateFrame(parseInt(frameW.value), currentH + STEP);
377
+ });
378
  }
379
 
 
 
380
 
381
  // ------------------------
382
+ // 8. CONTROL T脕CTIL (Solo MOVER - 1 Dedo)
383
  // ------------------------
384
 
385
+ // Funci贸n auxiliar para getDistance (mantener por si se necesita)
386
  function getDistance(touch1, touch2) {
387
  const dx = touch1.clientX - touch2.clientX;
388
  const dy = touch1.clientY - touch2.clientY;
389
  return Math.sqrt(dx * dx + dy * dy);
390
  }
391
 
 
392
  sheetView.addEventListener('touchstart', (e) => {
393
  e.preventDefault();
394
  if (!uploadedImage) return;
395
 
396
+ if (e.touches.length === 1) {
 
 
 
 
 
 
 
 
 
397
  isDragging = true;
 
398
  const touch = e.touches[0];
399
  lastTouchX = touch.clientX;
400
  lastTouchY = touch.clientY;
 
403
 
404
  sheetView.addEventListener('touchmove', (e) => {
405
  e.preventDefault();
406
+ if (!uploadedImage || !isDragging || e.touches.length !== 1) return;
407
 
408
+ // L脫GICA DE DRAG (MOVIMIENTO - 1 Dedo)
409
+ const touch = e.touches[0];
410
+ const dx = touch.clientX - lastTouchX;
411
+ const dy = touch.clientY - lastTouchY;
412
+
413
+ const sheetW = uploadedImage.width;
414
+ const canvasW = sheetView.width;
415
+ const scale = Math.min(canvasW / sheetW, canvasW / uploadedImage.height);
416
+
417
+ const fW = parseInt(frameW.value);
418
+ const fH = parseInt(frameH.value);
419
+
420
+ let imgDx = Math.round(dx / scale);
421
+ let imgDy = Math.round(dy / scale);
422
+
423
+ // Mover solo si el arrastre es significativo (para evitar jitter)
424
+ if (Math.abs(imgDx) >= STEP) {
425
+ let framesMovedX = Math.round(imgDx / STEP);
426
+ currentSheetX += framesMovedX * STEP;
427
+ lastTouchX = touch.clientX;
428
+ }
429
+ if (Math.abs(imgDy) >= STEP) {
430
+ let framesMovedY = Math.round(imgDy / STEP);
431
+ currentSheetY += framesMovedY * STEP;
432
+ lastTouchY = touch.clientY;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  }
434
 
435
+ // Asegurar l铆mites
436
+ currentSheetX = Math.max(0, Math.min(currentSheetX, uploadedImage.width - fW));
437
+ currentSheetY = Math.max(0, Math.min(currentSheetY, uploadedImage.height - fH));
438
+
439
  drawSheetView();
440
  drawCurrentFramePreview(currentSheetX, currentSheetY);
441
  });
442
 
443
  sheetView.addEventListener('touchend', (e) => {
 
444
  isDragging = false;
 
445
  });
446
 
447
  });