Cómo tomar capturas de pantalla
Este tutorial fue escrito en febrero de 2026, para la v2 del motor.
Tu juego se renderiza en un canvas, pero no hay una forma integrada de guardar lo que aparece en pantalla. Quizás los jugadores quieran compartir una puntuación alta, o estás construyendo una herramienta que necesita un botón de exportar. screenshot() captura el canvas como una URL de datos PNG — una llamada y tienes una imagen que puedes descargar, mostrar o enviar donde quieras.
Vamos a construir una pequeña app de dibujo para ver cómo funciona. Si quieres un editor de píxeles completo, el editor de sprites tiene todo lo que necesitas — mantenemos este simple para que el foco esté en las capturas de pantalla.
Un canvas en blanco
Primero necesitamos algo que valga la pena capturar. Vamos a crear una cuadrícula de 16x16 píxeles donde puedas dibujar con el ratón. Cada celda mide 7 píxeles de ancho, así que la cuadrícula entera cabe cómodamente en el canvas de 128x128:
engine.scope(({ start, cls, rectfill, rect, mouse, mdown, dragging }) => {
const gridSize = 16;
const pixelSize = 7;
const gridX = 1;
const gridY = 1;
const canvas = new Array(gridSize * gridSize).fill(-1);
function update() {
if (mdown() || dragging()) {
const pos = mouse();
const col = Math.floor((pos.x - gridX) / pixelSize);
const row = Math.floor((pos.y - gridY) / pixelSize);
if (col >= 0 && col < gridSize && row >= 0 && row < gridSize) {
canvas[row * gridSize + col] = 7;
}
}
}
function draw() {
cls(0);
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < gridSize; col++) {
const x = gridX + col * pixelSize;
const y = gridY + row * pixelSize;
const color = canvas[row * gridSize + col];
if (color >= 0) {
rectfill(x, y, x + pixelSize - 1, y + pixelSize - 1, color);
}
}
}
rect(gridX - 1, gridY - 1, gridX + gridSize * pixelSize, gridY + gridSize * pixelSize, 5);
}
start({ sprites: {}, sounds: {}, update, draw, target });
});
Guardamos los colores de los píxeles en un array plano — 256 entradas, una por celda. -1 significa vacío, 0–15 corresponde a un color de la paleta. En cada fotograma comprobamos si el ratón está pulsado o arrastrando, averiguamos sobre qué celda está, y la ponemos en blanco.
Añadir una paleta de colores
Blanco sobre negro es un poco limitado. Vamos a añadir una fila de 16 muestras de color en la parte inferior para que puedas elegir cualquier color de la paleta antes de dibujar:
engine.scope(({ start, cls, rectfill, rect, mouse, mdown, click, dragging }) => {
const gridSize = 16;
const pixelSize = 7;
const gridX = 1;
const gridY = 1;
const canvas = new Array(gridSize * gridSize).fill(-1);
let selectedColor = 7;
const paletteY = 118;
const swatchSize = 8;
function update() {
const pos = mouse();
if (click()) {
if (pos.y >= paletteY && pos.y < paletteY + swatchSize) {
const swatch = Math.floor(pos.x / swatchSize);
if (swatch >= 0 && swatch < 16) {
selectedColor = swatch;
return;
}
}
}
if (mdown() || dragging()) {
const col = Math.floor((pos.x - gridX) / pixelSize);
const row = Math.floor((pos.y - gridY) / pixelSize);
if (col >= 0 && col < gridSize && row >= 0 && row < gridSize) {
canvas[row * gridSize + col] = selectedColor;
}
}
}
function draw() {
cls(0);
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < gridSize; col++) {
const x = gridX + col * pixelSize;
const y = gridY + row * pixelSize;
const color = canvas[row * gridSize + col];
if (color >= 0) {
rectfill(x, y, x + pixelSize - 1, y + pixelSize - 1, color);
}
}
}
rect(gridX - 1, gridY - 1, gridX + gridSize * pixelSize, gridY + gridSize * pixelSize, 5);
for (let i = 0; i < 16; i++) {
const x = i * swatchSize;
rectfill(x, paletteY, x + swatchSize - 1, paletteY + swatchSize - 1, i);
if (i === selectedColor) {
rect(x, paletteY, x + swatchSize - 1, paletteY + swatchSize - 1, 7);
}
}
}
start({ sprites: {}, sounds: {}, update, draw, target });
});
Al hacer clic en una muestra se selecciona ese color. Comprobamos si el clic cayó en la fila de la paleta antes de comprobar la cuadrícula de dibujo. La muestra seleccionada tiene un borde blanco para que siempre sepas cuál está activa.
Cómo funciona screenshot()
Ahora la parte divertida. Llama a screenshot() y devuelve una URL de datos PNG — una cadena larga que empieza con data:image/png;base64,... y contiene toda la imagen del canvas codificada en base64:
// screenshot() returns a PNG data URL string
const dataUrl = screenshot();
// it looks like this:
// "data:image/png;base64,iVBORw0KGgo..."
// you can use it as an image source
const img = new Image();
img.src = dataUrl;
Esa cadena funciona en cualquier lugar donde usarías una URL de imagen. Ponla como src de un elemento <img>, envíala a un servidor con POST, o — como haremos a continuación — activa una descarga de archivo.
Consejo: screenshot() captura todo lo que hay en el canvas — incluyendo elementos de la interfaz como la paleta y el botón. Si quieres una captura limpia solo del dibujo, podrías limpiar y redibujar solo los píxeles del canvas antes de llamar a screenshot(), y luego redibujar la interfaz completa en el siguiente fotograma.
Activar una descarga
La forma estándar de activar una descarga de archivo desde JavaScript es crear un elemento <a> invisible, establecer su href con la URL de datos, darle un nombre de archivo con download, y hacer clic programáticamente. Vamos a conectarlo a la tecla S:
// inside update()
if (btnp('s')) {
const dataUrl = screenshot();
const link = document.createElement('a');
link.download = 'my-drawing.png';
link.href = dataUrl;
link.click();
}
Pulsa S y el navegador guarda un archivo my-drawing.png. La imagen será de 128x128 píxeles — esa es la resolución real del canvas, independientemente de lo grande que se vea en pantalla.
Añadir un botón de descarga
Un atajo de teclado está bien, pero un botón visible es mucho más fácil de descubrir. Vamos a dibujar un botón "SAVE" en la esquina superior derecha y detectar clics sobre él:
// draw a "SAVE" button
const btnX = 104;
const btnY = 1;
const btnW = 23;
const btnH = 9;
rectfill(btnX, btnY, btnX + btnW, btnY + btnH, 1);
rect(btnX, btnY, btnX + btnW, btnY + btnH, 13);
text('SAVE', btnX + 3, btnY + 2, 13);
// in update, detect click on the button
const pos = mouse();
if (click()) {
if (pos.x >= btnX && pos.x <= btnX + btnW && pos.y >= btnY && pos.y <= btnY + btnH) {
const dataUrl = screenshot();
const link = document.createElement('a');
link.download = 'drawing.png';
link.href = dataUrl;
link.click();
}
}
Dibujamos el botón con rectfill para el fondo, rect para el borde y text para la etiqueta. En update comprobamos si un clic cayó dentro del área del botón — si es así, tomamos la captura y activamos la descarga.
Juntándolo todo
Aquí está la app de dibujo completa con la paleta de colores, el botón SAVE y el atajo de teclado S, todo conectado. También añadimos un pequeño mensaje "Saved!" que parpadea durante un segundo después de cada descarga:
engine.scope(
({ start, cls, rectfill, rect, text, mouse, mdown, mup, click, dragging, btnp, screenshot }) => {
const gridSize = 16;
const pixelSize = 7;
const gridX = 1;
const gridY = 1;
const canvas = new Array(gridSize * gridSize).fill(-1);
let selectedColor = 7;
const paletteY = 118;
const swatchSize = 8;
const btnX = 104;
const btnY = 1;
const btnW = 23;
const btnH = 9;
let statusText = '';
let statusTimer = 0;
function save() {
const dataUrl = screenshot();
const link = document.createElement('a');
link.download = 'drawing.png';
link.href = dataUrl;
link.click();
statusText = 'Saved!';
statusTimer = 60;
}
function update() {
if (statusTimer > 0) {
statusTimer--;
if (statusTimer === 0) {
statusText = '';
}
}
if (btnp('s')) {
save();
return;
}
const pos = mouse();
if (click()) {
if (pos.x >= btnX && pos.x <= btnX + btnW && pos.y >= btnY && pos.y <= btnY + btnH) {
save();
return;
}
if (pos.y >= paletteY && pos.y < paletteY + swatchSize) {
const swatch = Math.floor(pos.x / swatchSize);
if (swatch >= 0 && swatch < 16) {
selectedColor = swatch;
return;
}
}
}
if (mdown() || dragging()) {
const col = Math.floor((pos.x - gridX) / pixelSize);
const row = Math.floor((pos.y - gridY) / pixelSize);
if (col >= 0 && col < gridSize && row >= 0 && row < gridSize) {
canvas[row * gridSize + col] = selectedColor;
}
}
}
function draw() {
cls(0);
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < gridSize; col++) {
const x = gridX + col * pixelSize;
const y = gridY + row * pixelSize;
const color = canvas[row * gridSize + col];
if (color >= 0) {
rectfill(x, y, x + pixelSize - 1, y + pixelSize - 1, color);
}
}
}
rect(gridX - 1, gridY - 1, gridX + gridSize * pixelSize, gridY + gridSize * pixelSize, 5);
for (let i = 0; i < 16; i++) {
const x = i * swatchSize;
rectfill(x, paletteY, x + swatchSize - 1, paletteY + swatchSize - 1, i);
if (i === selectedColor) {
rect(x, paletteY, x + swatchSize - 1, paletteY + swatchSize - 1, 7);
}
}
rectfill(btnX, btnY, btnX + btnW, btnY + btnH, 1);
rect(btnX, btnY, btnX + btnW, btnY + btnH, 13);
text('SAVE', btnX + 3, btnY + 2, 13);
if (statusText) {
text(statusText, btnX + 2, btnY + btnH + 3, 11);
}
}
start({ sprites: {}, sounds: {}, update, draw, target });
},
);
Para ir más allá
Algunas direcciones que podrías tomar:
- Capturas temporizadas — captura cada N segundos y une las imágenes para hacer un timelapse de tu juego
- Capturas limpias — redibuja solo el contenido del juego (sin interfaz) antes de llamar a
screenshot(), y luego restaura la interfaz en el siguiente fotograma - Botón de compartir — envía la URL de datos a un servidor con POST, o conviértela en un Blob y usa la Web Share API
- GIFs animados — captura varios fotogramas y combínalos con una librería como gif.js