DEV Community

Gustavo Ojeda-P
Gustavo Ojeda-P

Posted on

Basic elements with Pixi.js: Primitives, Text and Sprites

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.

Setting up the stage

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

// 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 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.

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

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

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 color0xCC3333
  • 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:

app.stage.addChild(myRect);

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

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:

A simple circle in the stage

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:

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

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:

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

So we obtain the following result:

A centered rectangle

Creating text

The fastest way to create text is similar:

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, that allows us to control each characteristic:

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:

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:

text in stage

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

Adding Sprites

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

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

After which we would see the following:

sprite in stage

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.

const loader = PIXI.Loader.shared;

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

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 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.

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:

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:

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:

Latest comments (1)

Collapse
 
drsensor profile image
૮༼⚆︿⚆༽つ

Nice tutorial!
Is there a reason why you put CSS * { font-family } in that Codepen?