Basic elements with Pixi.js: Primitives, Text and Sprites
Tags: javascript, webgl, dataviz, pixijs
Published: 2020-06-22 18:23:17

## Creating primitives

Primitives are basic geometric shapes that we can draw directly using instructions. In Pixi.js the instructions used to create these graphics are very similar (but not the same) to the ones used to draw an [HTML Canvas element using pure Javascript](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes).

#### Setting up the stage

The first thing will be to create a PIXI application as in the previous section, but with some minor changes:

```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);
```

The only changes are the addition of one more parameter in the `Aplication` function, called `antialias`, which improves the display of the edges for elements on the screen.

Also now the width and height of the stage are declared as variables, so that these values ​​can be reused in different parts of our code.

#### A first circle

To create a graphic called `myCircle` we use the [Graphics](https://pixijs.download/release/docs/PIXI.Graphics.html) constructor, which allows you to draw lines, circles, rectangles, polygons, among other shapes. Thus we obtain an object in which we can draw in addition to manipulating freely, changing its properties.

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

To make our circle we use a sequence of 5 instructions:

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

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

myCircle.endFill();
```

And each of those lines have a task:

- `lineStyle` set the style of the line: thickness 2 pixels and border color `0x993333`
- `beginFill` fills the geometric shape, with the color` 0xCC3333`
- `drawCircle` draws the circle itself, entering the `x` and `y` coordinates where the center of the circle will be located, followed by the desired radius, in pixels.
- `endFill` ends the filling process

Those are all the steps required to draw our circle. However, the drawing process has been holded  out inside `myCircle`, which is a variable. That is to say, all the time we have been drawing in the memory of the computer. It takes one more step to see our circle on the screen.

#### Adding items to the stage

The final step is to call the `addChild` function of the application `stage`, which will make the  `myCircle` element visible on screen: 

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

Thus, the complete code needed to draw a circle and display it on the screen is as follows:

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

The result is a circle with a radius of 40 pixels and located in the center of the stage:

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

Note that the coordinates of the object `myCircle` will be (0, 0) and the circle drawn inside that object has an offset to the coordinates (240, 120). This could be confusing in some cases and for that reason we will explore this topic further in a future post.

#### How about a rectangle?

Following a similar procedure, we can create and insert a yellow rectangle, but this time at the stage origin (0, 0), that is, the upper left corner:

```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;" />

#### Changing visual properties

The thickness of the border can affect the exact size and position of an item. It can be seen that, despite having been created at the point (0, 0), part of the border is outside the visible space. This is due to the way the instructions draw the edges of the figures. This behavior, of course, is configurable and we can modify it later.

After adding the graphic on the stage, we will manipulate the properties of the rectangle, taking it to the center of the stage and changing its original dimensions so that it now measures twice, that is 96 pixels on each side:

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

So we obtain the following result:

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

#### Creating text

The fastest way to create [text](https://pixijs.download/release/docs/PIXI.Text.html) is similar:

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

However, this text will have a default style (font, color, weight, etc.). To improve the appearance of our text, it is necessary to create a [text style object](https://pixijs.download/release/docs/PIXI.TextStyle.html), that allows us to control each characteristic:

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

Assigning the style to our text element, we will display a much more personalized message on the screen. We will place it in the center of the stage and will assign the `anchor` property, which allows us to control the element's anchor point:

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

From what we get:

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

Here is a live version where all the basic elements are put together:

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

## Adding Sprites

[Sprites](https://en.wikipedia.org/wiki/Sprite_(computer_graphics)) are 2D visual elements that can be inserted into the stage of any graphic environment of interactive applications or video games. They are the simplest graphic resources that we can put on screen and control from the code of our application, by manipulating properties such as its size, rotation or position, among others.

Sprites are generally created from bitmaps. The easiest way, although not necessarily the best in all cases, is to create it *directly* from an image file:

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

After which we would see the following:

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

Although this method is simple, it is inconvenient if the image file is too large, since loading will take longer than expected and the following instructions related to the sprite could produce  unexpected behaviors. 

## Creating Sprites by pre-loading textures

The best way to load one or more external resources is by using the `Loader` class offered by Pixi.js. For our convenience, the `PIXI` object offers a pre-built loader instance that can be used without further configuration.

```JS
const loader = PIXI.Loader.shared;
```

After the instantiation of this utility, we can load the same file but with the new method:

```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
    })
```

In the previous code we use the `add` function to add elements to the loading queue, with a name that we want to assign to it (in this case _coffee_), in addition to the path to the image file.

We can [chain](https://dev.to/nedsoft/method-chaining-in-javascript-3klb) the `load` and `use`  functions to do tasks with the loaded elements. The first is executed when the loading of *all* the elements has been completed. The second works as a _middleware_ after *each* item has been loaded.

The power of the `Loader` class shines when we want to load multiple files at the same time. For convenience, we will use the object `sprites` to store the loaded elements, instead of having a variable for each one of them.

```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
    })
```

Remember that `use` runs multiple times, once for each item added to the load queue (and subsequently loaded). This will result in the following:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/bnwxs3cnlo8qjfkx8tdb.png" alt="sprites from textures" />

In addition, the `loader` instance sends various signals during the loading process, which we can take advantage of to obtain additional information about the loading process. The following code would display messages on the console:

```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!');
});
```
Check out a live version here:

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