Tabla de contenido
Una vez más aquí. Hoy vengo a dejar un pequeño artículo sobre una pequeña aplicación que hace una par de semanas pensé. El caso es que alguien me pidió que le creara una web utilizando Pixel Art. Y la verdad es que no tenía ni idea de que era esto, aun que resultó ser algo conocido por todos aquellos que teníamos videoconsolas o pc’s en los años 90. Pues bueno, pensando un poco, me decidí a crear una pequeña aplicación web a la que he llamado Pixeleand0, que permite a todo el que quiera poder crear pequeñas imágenes pixel a pixel. Y buscando un poco, me encontré una comunidad de bastante más grande de lo que me esperaba. Además encontré el canal de Youtube de un hombre que me parece un fenómeno de este tipo de arte, @Elias_Lozano. Sé que no será el único, pero sus vídeos me han llamado mucho la atención.
Bueno, como decía, el arte pixelado es un estilo visual único que ha perdurado a través de las décadas, desde los primeros videojuegos hasta las modernas obras de diseño. Es por eso que he creado Pixeland0, que viene siendo una pequeña y básica herramienta para que el usuario explore su creatividad y pueda crear imágenes pixel a pixel desde el navegador web, sin necesidad de meterse en programas más complicados. Aun que evidentemente, si quieres conseguir algo realmente bueno, tendrás que utilizar otras herramientas mucho más completas que la que vengo a presentar hoy aquí.
¿Qué es Pixeleand0?
Como decía líneas más arriba, Pixeleand0 es una aplicación web básica e intuitiva que te permite crear tus imágenes pixeladas de forma sencilla y sin conocimientos previos de ningún tipo. Con una interfaz fácil de usar y una pequeñisima gama de herramientas, puedes dar vida a tus ideas con solo unos pocos clics.
Características principales
- Generación de píxeles dinámica: El programa genera una cuadrícula de píxeles que puedes personalizar según tus necesidades.
- Amplia paleta de colores: Escoge entre una variedad de colores predefinidos o selecciona tu propio color usando el selector de color integrado. Podrás ir guardando tu paleta de cinco colores en los selectores disponibles.
- Herramientas de dibujo versátiles: Utiliza el pincel para dibujar libremente, el cubo de pintura para rellenar áreas con un solo clic o el cuentagotas para seleccionar colores de la imagen.
- Importación de imágenes: Importa tus propias imágenes para usarlas como referencia o como base para tus creaciones pixeladas.
- Descarga fácil: Descarga tus obras de arte en formato PNG para compartirlas con el mundo o para utilizarlas en tus proyectos personales.
- Botón deshacer: Si te equivocas en uno o varios pixels, no importará. Siempre tendrás la posibilidad de deshacer los cambios con este botón.
El código de Pixeleand0
Todo este código que vamos a ver a continuación, está colgado en el repositorio de GitHub en el que he alojado el proyecto. Ahí será donde publicaré actualizaciones (si tengo tiempo para ello).
Esta aplicación se compone de tan solo tres archivos. Uno .html para crear el entorno, un .css para darle un poco de estilo a la aplicación y otro .js en el que se estará toda la lógica de la aplicación. A continuación veremos el código archivo por archivo.
index.html
Pixeleand0
script.js
// Esperar a que se cargue el DOM antes de ejecutar el script document.addEventListener("DOMContentLoaded", function() { // Obtener referencias a los elementos del DOM const container = document.querySelector(".container"); // Contenedor de píxeles const colorPicker = document.querySelector("#colorPicker"); // Selector de color const sizeSelector = document.querySelector("#sizeSelector"); // Selector de tamaño const clearButton = document.querySelector("#clearButton"); // Botón de limpiar const downloadButton = document.querySelector("#downloadButton"); // Botón de descargar const fileNameInput = document.querySelector("#fileNameInput"); // Entrada de nombre de archivo const paintBucketButton = document.querySelector("#paintBucketButton"); // Botón de cubo de pintura const brushButton = document.querySelector("#brushButton"); // Botón de pincel const zoomInButton = document.querySelector("#zoomInButton"); // Botón de hacer zoom const zoomOutButton = document.querySelector("#zoomOutButton"); // Botón de alejar const colorButtons = document.querySelectorAll(".color-button"); // Botones de color const openButton = document.querySelector("#openButton"); // Botón de abrir imagen const imageUpload = document.querySelector("#imageUpload"); // Entrada de carga de imagen const eyedropperButton = document.querySelector("#eyedropperButton"); // Botón de cuentagotas const undoButton = document.querySelector("#undoButton"); // Botón de deshacer // Variables de estado y configuración inicial let isPaintBucketActive = false; // Estado de activación del cubo de pintura let isBrushActive = true; // Estado de activación del pincel let isEyedropperActive = false; // Estado de activación del cuentagotas let isDrawing = false; // Estado de dibujo let activeColor = "#000000"; // Color activo inicial let pixelSize = 15; // Tamaño de píxel inicial let maxSize = parseInt(sizeSelector.value); // Tamaño máximo inicial let history = []; // Historial de acciones // Generar los píxeles en el contenedor function generatePixels(size) { container.innerHTML = ''; // Limpiar el contenedor container.style.gridTemplateColumns = `repeat(${size}, ${pixelSize}px)`; // Establecer el número de columnas for (let i = 0; i < size * size; i++) { const pixel = document.createElement("div"); // Crear un píxel pixel.classList.add("pixel"); pixel.style.width = `${pixelSize}px`; // Establecer el tamaño pixel.style.height = `${pixelSize}px`; pixel.style.backgroundColor = "#fff"; // Color inicial container.appendChild(pixel); // Agregar píxel al contenedor } } // Obtener los píxeles adyacentes del mismo color function getAdjacentPixels(pixel, color) { const pixels = Array.from(container.children); // Obtener todos los píxeles const size = Math.sqrt(pixels.length); // Calcular el tamaño de la cuadrícula const index = pixels.indexOf(pixel); // Obtener el índice del píxel const queue = [index]; // Inicializar la cola de píxeles const result = []; // Inicializar el resultado while (queue.length > 0) { const current = queue.shift(); // Obtener el próximo píxel de la cola if (result.includes(current)) continue; // Si ya está en el resultado, continuar const x = current % size; // Calcular la coordenada x const y = Math.floor(current / size); // Calcular la coordenada y if (pixels[current].style.backgroundColor === color) { // Si el color del píxel es el mismo que el color dado result.push(current); // Agregar el índice al resultado // Agregar los píxeles adyacentes a la cola if (x > 0) queue.push(current - 1); // Izquierda if (x < size - 1) queue.push(current + 1); // Derecha if (y > 0) queue.push(current - size); // Arriba if (y < size - 1) queue.push(current + size); // Abajo } } return result; // Devolver los píxeles adyacentes del mismo color } // Pintar los píxeles adyacentes del mismo color function paintAdjacentPixels(pixel, color) { const pixels = Array.from(container.children); // Obtener todos los píxeles const originalColor = pixel.style.backgroundColor; // Obtener el color original del píxel const adjacentPixels = getAdjacentPixels(pixel, originalColor); // Obtener los píxeles adyacentes del mismo color // Iterar sobre los píxeles adyacentes adjacentPixels.forEach(index => { const targetPixel = pixels[index]; // Obtener el píxel objetivo // Agregar la acción al historial history.push({ pixel: targetPixel, previousColor: targetPixel.style.backgroundColor, newColor: color }); targetPixel.style.backgroundColor = color; // Pintar el píxel }); } // Manejador de eventos: clic en el contenedor de píxeles container.addEventListener("mousedown", function(event) { if (event.target.classList.contains("pixel")) { isDrawing = true; // Iniciar el dibujo if (isPaintBucketActive) { // Si el cubo de pintura está activo paintAdjacentPixels(event.target, activeColor); // Pintar píxeles adyacentes del mismo color } else if (isEyedropperActive) { // Si el cuentagotas está activo activeColor = event.target.style.backgroundColor; // Obtener el color del píxel const activeButton = document.querySelector(".color-button.active"); // Obtener el botón de color activo activeButton.style.backgroundColor = activeColor; // Establecer el color activo en el botón colorPicker.value = rgbToHex(activeColor); // Establecer el valor del selector de color isEyedropperActive = false; // Desactivar el cuentagotas eyedropperButton.classList.remove("active"); // Eliminar la clase activa del botón de cuentagotas container.classList.remove("eyedropper-active"); // Eliminar la clase activa del contenedor } else { // Si se está utilizando el pincel const targetPixel = event.target; // Obtener el píxel objetivo // Agregar la acción al historial history.push({ pixel: targetPixel, previousColor: targetPixel.style.backgroundColor, newColor: activeColor }); targetPixel.style.backgroundColor = activeColor; } } }); // Manejador de eventos: movimiento del ratón sobre el contenedor de píxeles container.addEventListener("mouseover", function(event) { if (isDrawing && event.target.classList.contains("pixel")) { if (isBrushActive) { const targetPixel = event.target; // Obtener el píxel objetivo // Agregar la acción al historial history.push({ pixel: targetPixel, previousColor: targetPixel.style.backgroundColor, newColor: activeColor }); targetPixel.style.backgroundColor = activeColor; // Pintar el píxel } } }); // Manejador de eventos: liberación del botón del ratón sobre el contenedor de píxeles container.addEventListener("mouseup", function() { isDrawing = false; // Finalizar el dibujo }); // Manejador de eventos: salida del ratón del contenedor de píxeles container.addEventListener("mouseleave", function() { isDrawing = false; // Finalizar el dibujo }); // Manejador de eventos: clic en el botón "Paint Bucket" paintBucketButton.addEventListener("click", function() { isPaintBucketActive = true; // Activar el cubo de pintura isBrushActive = false; // Desactivar el pincel isEyedropperActive = false; // Desactivar el cuentagotas paintBucketButton.classList.add("active"); // Agregar clase activa al botón de cubo de pintura brushButton.classList.remove("active"); // Eliminar clase activa del botón de pincel eyedropperButton.classList.remove("active"); // Eliminar clase activa del botón de cuentagotas container.classList.remove("eyedropper-active"); // Eliminar clase activa del contenedor }); // Manejador de eventos: clic en el botón "Brush" brushButton.addEventListener("click", function() { isBrushActive = true; // Activar el pincel isPaintBucketActive = false; // Desactivar el cubo de pintura isEyedropperActive = false; // Desactivar el cuentagotas brushButton.classList.add("active"); // Agregar clase activa al botón de pincel paintBucketButton.classList.remove("active"); // Eliminar clase activa del botón de cubo de pintura eyedropperButton.classList.remove("active"); // Eliminar clase activa del botón de cuentagotas container.classList.remove("eyedropper-active"); // Eliminar clase activa del contenedor }); // Manejador de eventos: clic en el botón "Eyedropper" eyedropperButton.addEventListener("click", function() { isEyedropperActive = true; // Activar el cuentagotas isBrushActive = false; // Desactivar el pincel isPaintBucketActive = false; // Desactivar el cubo de pintura eyedropperButton.classList.add("active"); // Agregar clase activa al botón de cuentagotas brushButton.classList.remove("active"); // Eliminar clase activa del botón de pincel paintBucketButton.classList.remove("active"); // Eliminar clase activa del botón de cubo de pintura container.classList.add("eyedropper-active"); // Agregar clase activa al contenedor }); // Manejador de eventos: clic en el botón "Clear" clearButton.addEventListener("click", function() { const pixels = document.querySelectorAll(".pixel"); // Obtener todos los píxeles pixels.forEach(pixel => { pixel.style.backgroundColor = "#fff"; // Restablecer el color a blanco }); fileNameInput.value = ""; // Limpiar el valor de la entrada de nombre de archivo history = []; // Limpiar el historial de acciones }); // Manejador de eventos: clic en el botón "Download" downloadButton.addEventListener("click", async function() { const size = parseInt(sizeSelector.value); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = size; canvas.height = size; ctx.imageSmoothingEnabled = false; const pixels = document.querySelectorAll(".pixel"); // Obtener todos los píxeles for (let i = 0; i < pixels.length; i++) { const x = i % size; // Calcular la coordenida x const y = Math.floor(i / size); // Calcular la coordenada y const color = pixels[i].style.backgroundColor; // Obtener el color del píxel ctx.fillStyle = color; // Establecer el color de relleno ctx.fillRect(x, y, 1, 1); // Dibujar un píxel } const link = document.createElement("a"); // Crear un elemento de enlace link.download = fileNameInput.value ? fileNameInput.value + ".png" : "pixeleand0.png"; // Establecer el nombre de archivo para descargar link.href = canvas.toDataURL(); // Establecer el enlace de descarga link.click(); // Simular clic en el enlace }); // Manejador de eventos: cambio en el selector de tamaño sizeSelector.addEventListener("change", function() { maxSize = parseInt(sizeSelector.value); // Actualizar el tamaño máximo generatePixels(maxSize); // Generar los píxeles history = []; // Limpiar el historial de acciones }); // Manejador de eventos: clic en el botón "Zoom In" zoomInButton.addEventListener("click", function() { pixelSize += 5; // Aumentar el tamaño de píxel adjustPixelSize(); // Ajustar el tamaño de los píxeles }); // Manejador de eventos: clic en el botón "Zoom Out" zoomOutButton.addEventListener("click", function() { if (pixelSize > 5) { // Verificar que el tamaño de píxel no sea menor que 5 pixelSize -= 5; // Disminuir el tamaño de píxel adjustPixelSize(); // Ajustar el tamaño de los píxeles } }); // Función para ajustar el tamaño de los píxeles function adjustPixelSize() { const pixels = document.querySelectorAll(".pixel"); // Obtener todos los píxeles pixels.forEach(pixel => { pixel.style.width = `${pixelSize}px`; // Establecer el ancho del píxel pixel.style.height = `${pixelSize}px`; // Establecer la altura del píxel }); container.style.gridTemplateColumns = `repeat(${maxSize}, ${pixelSize}px)`; // Establecer las columnas de la cuadrícula container.style.gridTemplateRows = `repeat(${maxSize}, ${pixelSize}px)`; // Establecer las filas de la cuadrícula } // Cargar imagen desde el botón "Abrir" openButton.addEventListener("click", function() { imageUpload.click(); }); imageUpload.addEventListener("change", function(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { const img = new Image(); img.onload = function() { // Ajustar el tamaño del lienzo al tamaño de la imagen maxSize = img.width > img.height ? img.width : img.height; pixelSize = container.clientWidth / maxSize; container.style.gridTemplateColumns = `repeat(${img.width}, ${pixelSize}px)`; container.style.gridTemplateRows = `repeat(${img.height}, ${pixelSize}px)`; // Dibujar la imagen en la cuadrícula de píxeles const ctx = document.createElement("canvas").getContext("2d"); ctx.drawImage(img, 0, 0, img.width, img.height); const imageData = ctx.getImageData(0, 0, img.width, img.height).data; const pixels = container.querySelectorAll(".pixel"); for (let i = 0; i < pixels.length; i++) { const r = imageData[i * 4]; const g = imageData[i * 4 + 1]; const b = imageData[i * 4 + 2]; const a = imageData[i * 4 + 3] / 255; pixels[i].style.backgroundColor = `rgba(${r}, ${g}, ${b}, ${a})`; } }; img.src = e.target.result; }; reader.readAsDataURL(file); } }); // Manejador de eventos: cambio en el selector de color colorPicker.addEventListener("input", function() { activeColor = colorPicker.value; // Actualizar el color activo const activeButton = document.querySelector(".color-button.active"); // Obtener el botón de color activo if (activeButton) { activeButton.style.backgroundColor = activeColor; // Establecer el color activo en el botón } }); // Función para convertir RGB a código hexadecimal function rgbToHex(rgb) { const rgbArray = rgb.match(/\d+/g).map(Number); // Obtener los componentes RGB como números const hex = rgbArray.map(value => { const hexValue = value.toString(16); // Convertir a hexadecimal return hexValue.length === 1 ? "0" + hexValue : hexValue; // Agregar ceros a la izquierda si es necesario }); return `#${hex.join("")}`; // Devolver el código hexadecimal } // Verificar si hay botones de color if (colorButtons.length > 0) { colorButtons[0].classList.add("active"); activeColor = colorButtons[0].style.backgroundColor; colorPicker.value = rgbToHex(activeColor); } // Asignar manejadores de eventos a los botones de color colorButtons.forEach(button => { button.addEventListener("click", function() { colorButtons.forEach(btn => btn.classList.remove("active")); // Desactivar todos los botones de color button.classList.add("active"); // Activar el botón seleccionado activeColor = button.style.backgroundColor; // Actualizar el color activo colorPicker.value = rgbToHex(activeColor); // Actualizar el selector de color }); }); // Manejador de eventos: clic en el botón "Undo" undoButton.addEventListener("click", function() { if (history.length > 0) { // Verificar si hay acciones en el historial const lastAction = history.pop(); // Obtener la última acción del historial lastAction.pixel.style.backgroundColor = lastAction.previousColor; // Restaurar el color anterior del píxel } }); // Generar los píxeles iniciales generatePixels(maxSize); });
styles.css
body { background-color: #f0f0f0; /* Color de fondo */ display: flex; flex-direction: column; align-items: center; justify-content: flex-start; /* Alinear al inicio para el menú fijo */ height: 100vh; margin: 0; } #hr{ width: 85%; } .menu { position: fixed; top: 0; width: 100%; display: flex; justify-content: center; background-color: #444; padding: 10px; z-index: 1000; color: #fff; font-weight: bold; font-family: Arial, Helvetica, sans-serif; } .menu button, .menu input { margin-right: 10px; margin-left: 10px; background-color: #444; border: none; padding: 10px; cursor: pointer; color: #fff; font-weight: bold; } .menu input[type="text"] { flex-grow: 1; max-width: 200px; } .menu button:hover, .menu input:hover { background-color: #555; } .container { display: grid; gap: 1px; border: 1px solid #ccc; margin: 20px auto; width: fit-content; background-color: #ddd; } .pixel { box-sizing: border-box; border: 1px solid #ccc; cursor: pointer; } button.active { background-color: #666; color: #fff; } .controls button { padding: 8px 16px; font-size: 16px; background-color: #007bff; color: #fff; border: none; cursor: pointer; } .controls input[type="color"] { padding: 8px; border: none; cursor: pointer; } .controls { margin: 20px; display: flex; align-items: center; } .color-palette { display: flex; margin-left: 10px; } .color-button { width: 30px; height: 30px; border: none; cursor: pointer; margin-right: 5px; } .color-button:focus, .color-button.active { outline: 2px solid #000; } .eyedropper-active { cursor: crosshair; }
¿Cómo empezar con Pixeleand0?
Para empezar a crear tus propias imágenes pixeladas, simplemente abre Pixeland0 en tu navegador web desde la sección de ejemplos de esta web.
Utiliza las herramientas disponibles para dibujar, pintar y experimentar con diferentes estilos y técnicas. Una vez que hayas terminado tu obra maestra y descárgala.
Como decía al principio del artículo, Pixeleand0es una herramienta simple de dibujo que he creado para todos aquellos que empezamos en esto del Pixel Art. Supongo que todos aquellos que saben del tema, utilizarán herramientas más complejas como Libresprite, que permiten realizar muchas más acciones que esta aplicación. También quiero aclarar que esta aplicación ha salido del tirón, por lo que es posible que falle en algunos casos.