Elementos básicos con Pixi.js: Primitivas y Sprites
Tags: javascript, webgl, pixijs, dataviz
Published: 2020-06-22 18:23:08

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](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes).

#### Preparación del escenario

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

```JS
// 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](https://pixijs.download/release/docs/PIXI.Graphics.html), 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.

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

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

```JS
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: 

```JS
app.stage.addChild(myRect);
```

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

```JS
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:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/fla8gl0l15quuwrf70i5.png" alt="A simple circle in the stage" style="width:480px;" />

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:

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

<img src="https://dev-to-uploads.s3.amazonaws.com/i/ovk8r26b920plx2g1sw8.png" alt="A rectangle in the stage" style="width:480px;" />

#### 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:

```JS
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:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/omh6ockedp3kq2wf99qw.png" alt="A centered rectangle" style="width:480px;" />

#### Crear texto

La forma más rápida de crear [texto](https://pixijs.download/release/docs/PIXI.Text.html) es similar:

```JS
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](https://pixijs.download/release/docs/PIXI.TextStyle.html), que nos permita controlar cada característica:

```JS
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:

```JS
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:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/rk6o751073k6k02rn8qs.png" alt="text in stage" style="width: 480px;" />

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

{% codepen https://codepen.io/ggojedap/pen/BajWKvv default-tab=result %}

## Agregando Sprites

Los [Sprites](https://en.wikipedia.org/wiki/Sprite_(computer_graphics)) 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:

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

Tras lo cual veríamos lo siguiente:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/507dhgz2qgpuwunl0umo.png" alt="sprite in stage" style="width: 480px;" />

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.

```JS
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:

```JS
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](https://dev.to/nedsoft/method-chaining-in-javascript-3klb) 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.

```JS
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:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/bnwxs3cnlo8qjfkx8tdb.png" alt="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:

```JS
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í:

{% codepen https://codepen.io/ggojedap/pen/vYLxrNg default-tab=result %}