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 color0x993333
-
beginFill
fills the geometric shape, with the color0xCC3333
-
drawCircle
draws the circle itself, entering thex
andy
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:
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);
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:
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:
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:
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:
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:
Top comments (1)
Nice tutorial!
Is there a reason why you put CSS
* { font-family }
in that Codepen?