DEV Community

loading...

Elementos básicos con Pixi.js: Primitivas y Sprites

Gustavo Ojeda-P
Data Scientist & Visualization Engineer, #DS4A Colombia Fellow 2019. Researcher on Social Studies of Science and Technology.
・7 min read

Elementos básicos con Pixi.js: Primitivas y Sprites

Creación de primitivas

Las primitivas son formas geométricas básicas que podemos dibujar directamente mediante instrucciones. En Pixi.js las instrucciones utilizadas para crear estos gráficos son muy similares (pero no iguales) a las que se utilizan para dibjuar en un elemento HTML Canvas mediante Javascript puro.

Preparación del escenario

Lo primero será crear una aplicación PIXI tal como en la sección anterior, pero con algunos cambios menores:

// the size of the stage, as variables
let stageWidth = 480;
let stageHeight = 240;

// create app
let app = new PIXI.Application({
  width: stageWidth,
  height: stageHeight,
  antialias: true,
  backgroundColor: 0xEEEEEE
});

// add canvas to HTML document
document.body.appendChild(app.view);

Los únicos cambios son la adición de un parametro más en la función Aplication, llamado antialias, el cual mejora la visualización de los bordes de los elementos en pantalla.

Además ahora el ancho y el alto del escenario son declarados como variables, para que esos valores puedan ser reutilizados en distintas partes de nuestro código.

Un primer círculo

Para crear un gráfico llamado myCircle usamos el constructor Graphics, que permite dibujar líneas, círculos, rectángulos, polígonos, entre otras formas. Así obtenemos un objeto en el que podremos dibujar además de manipular libremente, cambiando sus propiedades.

// draw a circle
let myCircle = new PIXI.Graphics();

Para hacer nuestro círculo utilizamos una secuencia de 5 instrucciones:

myCircle.lineStyle(2, 0x993333);
myCircle.beginFill(0xCC3333);

// params: pos x, pos y, radius
myCircle.drawCircle(100, 100, 25);

myCircle.endFill();

Y cada una de esas líneas tienen esta misión:

  • con lineStyle asignamos el estilo de la línea, de grosor 2 pixeles y color de borde 0x993333
  • con beginFill comenzamos a rellenar la forma geométrica, con el color 0xCC3333
  • con drawCircle dibujamos el círculo propiamente dicho, ingresando las coordenadas x y y donde se ubicará el centro del círculo, seguidas del radio deseado, en pixeles.
  • con endFill terminamos el proceso de rellenado

Esos son todos los pasos requeridos para dibujar nuestro círculo. Sin embargo, el proceso de dibujo se ha llevado a cabo dentro del gráfico myCircle, que es una variable. Es decir todo el tiempo hemos estado dibujando en la memoria del computador. Hace falta un paso más para poder ver nuestro círulo en pantalla.

Poner elementos en el escenario

El paso final consiste en llamar la función addChild del escenario de nuestra aplicación, lo cual hará que nuestro gráfico myCircle quede visualmente incorporado también en pantalla y no solamente en la memoria:

app.stage.addChild(myRect);

Así las cosas, el código completo necesario para dibujar un círculo y mostrarlo en pantalla es el siguiente:

let myCircle = new PIXI.Graphics();
myCircle.lineStyle(2, 0x993333);
myCircle.beginFill(0xCC3333);
myCircle.drawCircle(240, 120, 40);
myCircle.endFill();
app.stage.addChild(myCircle);

y el resultado es un círculo con radio de 40 pixeles y ubicado en el centro del escenario:

A simple circle in the stage

Debe tenerse en cuenta que las coordenadas del objeto myCircle en el escenario siguen siendo (0, 0) y que el círculo dibujado dentro de ese objeto es el que está desplazado hasta las coordenadas (240, 120). Esto podría ser problemático en algunos casos y por esa razón exploraremos más este tema en la sección dedicada a contenedores.

Qué tal un rectángulo?

Siguiendo un procedimiento similar, podemos crear e insertar un rectángulo amarillo, pero esta vez en el origen de escenario (0, 0), es decir la esquina superior izquierda:

let myRect = new PIXI.Graphics();
myRect.lineStyle(4, 0xEEBB00);
myRect.drawRect(0, 0, 48, 48); // x, y, width, height
app.stage.addChild(myRect);

A rectangle in the stage

Cambiando las propiedades del gráfico

El grosor del borde puede afectar la posición y el tamaño exacto de un elemento. Puede observarse que, a pesar de haberse creado en el punto (0, 0), parte del borde está fuera del espacio visible. Esto se debe a la forma en que las instrucciones dibujan los bordes de las figuras. Este comportamiento, por supuesto, es configurable y podremos modificarlo más adelante.

Después de haber agregado el gráfico en el escenario, haremos una manipulación de las propiedades del rectángulo, llevándolo al centro del escenario y cambiando sus dimensiones originales para que ahora mida el doble, es decir 96 pixeles de cada lado:

myRect.width = 96;
myRect.height = 96;
myRect.x = (stageWidth - myRect.width) / 2;
myRect.y = (stageHeight - myRect.height) / 2;

Con lo cual obtenemos el siguiente resultado:

A centered rectangle

Crear texto

La forma más rápida de crear texto es similar:

let myText = new PIXI.Text('Morning Coffee!')
app.stage.addChild(tagline);

Sin embargo, el texto creado tendrá un estilo (fuente, color, peso, etc.) por defecto. Para mejorar la apariencia de nuestro texto, es necesario crear un objeto de estilo de texto, que nos permita controlar cada característica:

let textStyle = new PIXI.TextStyle({
  fill: '#DD3366',
  fontFamily: 'Open Sans',
  fontWeight: 300,
  fontSize: 14
});

Entonces asignando el estilo a nuestro elemento de texto, mostraremos en pantalla un mensaje mucho más personalizado. Lo ubicaremos en el centro de la pantalla, usando también la propiedad anchor, que nos permite controlar el punto de anclaje del elemento, para que sirva como referencia a la hora de ubicarlo:

let myText = new PIXI.Text('Morning Coffee!', textStyle) // <-
myText.anchor.set(0.5);
myText.x = 240;
myText.y = 120;
app.stage.addChild(myText);

De lo que obtenemos:

text in stage

A continuación una versión en vivo donde se combinan todos los elementos básicos:

Agregando Sprites

Los Sprites son elementos visuales en 2D que pueden insertarse dentro del escenario de cualquier ambiente gráfico de aplicaciones interactivas o videojuegos. Son los recursos gráficos más simples quepodemos poner en pantalla y controlar desde el código de nuestra aplicación, mediante la manipulación de propiedades como sus dimensiones, rotación o poscición, entre otras.

En general los sprites se crean a partir de imágenes o mapas de bits. La forma más fácil, aunque no necesariamente la mejor en todos los casos, es crearla directamente de un archivo:

let coffee = new PIXI.Sprite.from('images/coffee-cup.png');
app.stage.addChild(coffee);

Tras lo cual veríamos lo siguiente:

sprite in stage

Aunque este método es simple, es inconveniente si el archivo de imagen es grande, pues la carga demorará más de lo esperado y las instrucciones siguientes relacionadas con el sprite podrían tener resultados inesperados.

Sprites mediante la carga de texturas

La mejor forma de cargar uno o varios recursos externos es mediante el uso de la clase Loader ofrecida por Pixi.js. Para nuestra conveniencia, el objeto PIXI ofrece una instancia del cargador pre-fabricada que se puede usar sin mayor configuración.

const loader = PIXI.Loader.shared;

Después de la instanciación de esta utilidad, podemos cargar el mismo archivo pero con el nuevo método:

let myCoffee; // it will store the sprite

loader
    .add('coffee', 'images/coffee-cup.png')
    .load((loader, resources) => {
        // this callback function is optional
        // it is called once all resources have loaded.
        // similar to onComplete, but triggered after
        console.log('All elements loaded!');
    })
    .use((resource, next) => {
        // middleware to process each resource
        console.log('resource' + resource.name + ' loaded');
        myCoffee = new PIXI.Sprite(resource.texture);
        app.stage.addChild(myCoffee);
        next(); // <- mandatory
    })

En el código anterior utilizamos la función add para agregar a la cola los elementos que deseamos cargar, con un nombre que le queremos asignar (en este caso coffee), además de la ruta al archivo de imagen.

Podemos encadenar las funciones load y use para hacer tareas con los elementos cargados. La primera se ejecuta cuando la carga de todos los elementos se ha completado. La segunda funciona como un middleware después de que cada elemento ha sido cargado.

Sin embargo, el verdadero poder de la clase Loader se hace evidente cuando queremos cargar múltiples archivos al mismo tiempo. Para esto necesitaremos un objeto, que llamaremos sprites, que pueda alojar a todos los elementos que serán cargados, en lugar de tener una variable para cada uno de ellos.

let sprites = {};
let xpos = 16;

loader
    .add('coffee', 'images/coffee-cup.png')
    .add('muffin', 'images/muffin.png')
    .add('icecream', 'images/ice-cream.png')
    .add('croissant', 'images/lollipop.png')
    .use((resource, next) => {
        // create new sprite from loaded resource
        sprites[resource.name] = new PIXI.Sprite(resource.texture);

        // set in a different position
        sprites[resource.name].y = 16;
        sprites[resource.name].x = xpos;

        // add the sprite to the stage
        app.stage.addChild(sprites[resource.name]);

        // increment the position for the next sprite
        xpos += 72;
        next(); // <- mandatory
    })

Recordemos que use se ejecuta varias veces, una por cada elemento agregado a la cola de carga. Esto dará como resultado lo siguiente:

sprites from textures

Pero además, la instancia loader envía diversas señales durante del proceso de carga, que podemos aprovechar para obtener información adicional sobre cómo avanza la carga. El código siguiente mostraría mensajes en la consola:

loader.onProgress.add((loader, resource) => {
    // called once for each file
    console.log('progress: ' + loader.progress + '%');
});
loader.onError.add((message, loader, resource) => {
    // called once for each file, if error
    console.log('Error: ' + resource.name + ' ' + message);
});
loader.onLoad.add((loader, resource) => {
    // called once per loaded file
    console.log(resource.name + ' loaded');
});
loader.onComplete.add((loader, resources) => {
    // called once all queued resources has been loaded
    // triggered before load method callback
    console.log('loading complete!');
});

Míralo en vivo aquí:

Discussion (0)

Forem Open with the Forem app