This is my attempt at simplifying the explanation of what is pixi.js, and how to use it.
Pixi.js is basically a JavaScript library for rendering graphics with good performance.
Pixi.js is supposed to be easier and more performant.
Pixi.js is basically a tree of objects. The pixi people are calling the tree a "scene graph" and the objects "containers". At the top of the tree (the root node) you have the application container which they call a "Stage".
This guide will cover usage of Pixi v7.2.1
Pixi is divided to core and extra packages, to make sure we can use them all with the least amount of headache we need to use a modern buildtool, we'll be using vite.
Create a vite app, choose whatever framework you want in the prompts, I'll be using React (if you're using a framework other than React or Vanilla JS you'll have to figure where to put the pixi code yourself).
npm create vite@latest
or
yarn create vite
or
pnpm create vite
*using a buildtool prevents a lot of headache dealing with the pixi extra packages.
to get a stage on screen just make sure you have access to the PIXI library through installation:
npm i pixi.js
or the CDN <script src="https://pixijs.download/releas/pixi.js"></script>
and then type or copy the following:
react
/*
for the rest of this guide I'll assume usage of this template for react and just write the PIXI code.
*/
import { useEffect, useState, useRef } from "react";
import * as PIXI from "pixi.js";
export default function App() {
const [pixiApp, setPixiApp] = useState(null);
const appDiv = useRef(null);
useEffect(() => {
if (!pixiApp && appDiv.current) {
// PIXI CODE GOES HERE
const app = new PIXI.Application({width: 800,height: 600,});
appDiv.current.appendChild(app.view);
// PIXI CODE ENDS HERE
setPixiApp(app);
}
return () => {
pixiApp?.destroy(true, true);
setPixiApp(null);
};
}, []);
return <div ref={appDiv}></div>;
}
vanilla js
const app = PIXI.Application({width: 800, height:600});
document.body.appendChild(app.view);
You should see a black box apear on your screen.\
Any descendant of the stage which is a DisplayObject
is what will get rendered to the screen.
The main objects that can get rendered to the screen are:
Container
- boxes to hold and group stuffGraphics
- basic shapes (circle, square, line, polygon, ...)Sprite
- image filesText
- using any font and css style you want (expensive to change at runtime)BitmapText
- letters are taken from an image file (very cheap to change at runtime but can't be styled anyway you want).
You can see the full list of DisplayObject
s here.
*You can also see the properties and methods that all DisplayObject
s share.
If you nest objects within other objects their display properties become relative to the ones of their parents so for example: If you rotate the parent you rotate the child. If you move the parent you move the child. If you change the opacity of the parent you change the opacity of the child. And so on...
Now that we have a stage setup let's add a Graphics object to the stage, say a circle/s?
const circles = new PIXI.Graphics()
circles.beginFill(0x00ff00); // hex-color to fill the circle
circles.drawCircle(400,300,50); // x, y, radius
circles.drawCircle(300,300,20);
circles.endFill(); // stop color from leaking to other shapes.
circles.beginFill(0x0000ff);
circles.drawCircle(500,300,35);
app.stage.addChild(circles)
a single Graphics object can make more than one shape.
a little note* - the drawCircle
function we just used to create the circles doesn't actually draws them to the screen. Instead, think of it as a "buildCircle" function.
The Graphics object has a few more of these "draw" functions and you should treat them all as "build" functions instead.
So what actually draws them to the screen? that's what we talk about next.
As you just saw, to add anything to the stage we just use the app.stage.addChild
function and this is what made the circles appear on screen.
So when things get added to the stage, thats when they get rendered.
Also addChild
is not special to the stage, it's a Container
function, and since all DisplayObjects
extend the Container
class anytime you want to nest one object in another just use its addChild
function.
To add basic animations to your display objects (changing their display properties over time) we use the 'Ticker' class. When you initialize a ticker instance you pass it a callback function that runs on every 'tick'. in that function you change the display properties of whatever DisplayObject you want. The callback gets as an argument the "delta-time" (I'll expand on that in a bit). lets make our circles rotate:
let angle = 0
circles.position.set(400,300); // set our Graphics container position to that of our middle circle
circles.pivot.set(400,300); // set the origin of our container to be the same as our position
// add the ticker
app.ticker.add((delta) => {
angle = angle < 360? angle + 1: 0; //just to keep the number from climbing (if it will climb too high it can cause a crash)
circles.angle = angle; //update the angle of the container every tick.
})
delta-time is a misleading name, the actual value it holds is the number of frames
that have passed since the last time the callback was invoked.
what? shouldn't the callback be invoked on every frame?
Yes! it should!, unfortunately, sometimes due technical issues or overload the
GPU can't keep up with the screen's refresh rate and ends up missing a frame
or more. To avoid your animations lagging behind you can multiply your update values by the delta argument.
app.ticker.add(delta => {
circle.angle += 1 * delta;
})
usually it returns 1 or 0.99XX... which means no frames were skipped and it wouldn't change your update value. but in case some frames were skipped it will compensate for it by increasing your update value.
By default the ticker tries to match your screen's refresh rate, for some screens its 60 fps but for other screens this can go up to 120 fps, 144 fps and even 240 fps. This means that you might want to limit the ticker's maximum fps as otherwise people with higher refresh rate might experience your game at 2 or even 4 times the speed it's supposed to be played at. To do it all you need to do is:
app.ticker.maxFPS = 60;
Let's say you have bunch of assets in your game (or whatever it is you're making). If you don't load them ahead of time your app will not function right, images will not appear on screen and then jump out of nowhere, sounds will not play and things will generally won't work properly and hurt the user experience.
use the Pixi 'Assets' class and its functions: Assets.init({manifest})
, Assets.load()
, Assets.loadBundle()
, Assets.backgroundLoad()
, Assets.backgroundLoadBundle()
.
All of the above functions return a promise so you need to use either "async await" or the ".then" syntax.
Assets.init({manifest})
/Assets.addBundle(bundleId,assets)
: bundles allow you to load a bunch of assets in one go, but to load bundles you first have to give them an ID. UsingAssets.init
you can define all your bundles when the app starts or if you want to add bundles later you can useaddBundle
.
AbundleId
can be any string you want.
assets
is an object{name1: resource, name2: resource2, ... }
Amanifest
is an object or JSON file of the following structure:
const manifest = {
bundles: [
{
name: "game-bundle",
assets: [
{
name: "player",
srcs: "player.png"
},
{
name: "enemy",
srcs: "enemy.png"
},
]
},
{
name: "bundle-2",
assets: [
{
name: "loading-bar",
srcs: "loadingbar.png"
},
{
name: "loading-font",
srcs: "loading.font"
},
]
}
]
}
preloading example with Assets.init()
:
const app = new PIXI.Application({
width: 800,
height: 600,
backgroundColor: 0xaaaaaa,
});
document.body.appendChild(app.view);
PIXI.Assests.init({manifest: 'manifest.json'})
.then(() => {
PIXI.Assets.loadBundle('game-bundle')
.then(loaded_resources => {
// the code in here will run only once our bundle has loaded.
console.log(loaded_resources) // { player: Texture, enemy: Texture }
//The loadBundle function made Texture objects out of our .png files.
//we can now safely add our sprites to the stage knowing they won't
// cause any problems
const sprite1 = new PIXI.Sprite(loaded_resources.player);
const sprite2 = new PIXI.Sprite(loaded_resources.enemy);
app.stage.addChild(sprite1, sprite2);
})
})
create an instance of the AnimatedSprite
class with an array of textures
in the order you want it to play.
you can change the speed of the animation by assigning a value to the animationSpeed
property (0 - 1).
const animation = new AnimatedSprite([PIXI.Texture.from('sprite1.png'), PIXI.Texture.from('sprite2.png')]);
app.stage.addChild(animation);
animation.play();
Containers in PIXI are basically boxes to put other stuff in such as sprites, text, and graphics (basic shapes or composites). and they serve 3 main purposes:
- grouping
- masking
- filtering
when you want a few elements to move or change appearance all together (like how a weapon sprite might move with the player sprite) you can nest them in a container using the addChild
function we mentioned before and then make the changes to the container instead of each of them separately:
const manWithSword = new PIXI.Container()
const man = new PIXI.Sprite.from('man.png');
const sword = new PIXI.Sprite.from('sword.png');
manWithSword.addChild(man, sword);
manWithSword.x = 300;
Remember, children's "display properties" (properties that all display objects have) are relative to those of their parents so if the parent moves the children move with it.
To "mask" something in PIXI means to make it visible only through the mask it has put on. To illustrate, if I make a Graphics
instance that renders a circle and I set it as the mask for a container with a sprite in it. I will only be able to see the sprite where the circle is overlapping the container.
here is a somewhat more elaborate example:
// create the mask
const mask = new PIXI.Graphics();
mask.beginFill(0xffffff);
mask.drawCircle(400, 300, 50);
// create the container that will be masked
const maskedContainer = new PIXI.Container();
maskedContainer.mask = mask;
// create some stuff to put in the container
const circles = new PIXI.Graphics();
circles.beginFill(0x0000ff);
circles.drawCircle(400, 200, 35);
circles.beginFill(0x00ff00);
circles.drawCircle(300, 300, 35);
circles.beginFill(0xff00ff);
circles.drawCircle(500, 300, 35);
circles.beginFill(0x00ffff);
circles.drawCircle(400, 400, 35);
// more stuff to put in the container (to make the difference more obvious)
const background = new PIXI.Graphics();
background.beginFill(0xaa2222);
background.drawRect(0,0,app.screen.width,app.screen.height)
// put the stuff in the container
maskedContainer.addChild(background, circles);
// add the container to the stage
app.stage.addChild(maskedContainer);
// animate the mask (to help illustrate)
let angle = 0;
app.ticker.add(delta => {
angle = (angle + 1 * delta) % 360;
const radians = angle * Math.PI/180;
mask.clear();
mask.beginFill(0xffffff);
mask.drawCircle(400, 300, 100 + Math.cos(radians) * 34);
})
Filters are special effects that can be applied to DisplayObjects
like sprites and text. Filters can change the appearance of objects in many ways, for example by adding a blur effect, adjusting the color saturation and much more!
pixi has two types of filters: core filters and community filters. Core filters are included in the PIXI
library and are maintained by the pixi team. Community filters are created by members of the pixi community.
To see a list of all the available filters in pixi and live examples of them check out this repo. It's really cool!\
Note* - most of the filters in this list are community filters that are not part of the core PIXI
library which means they need to be installed or downloaded separately.
for the Built-in Filters list, just scroll to the end of the list and you'll see them.
Let's see how to use a built-in filter (BlurFilter
):
// Create a filter instance
const blurFilter = new PIXI.BlurFilter();
// create something to blur
const circle = new PIXI.Graphics();
circle.beginFill(0x0000ff);
circle.drawCircle(400,300,50);
// create a container to applay the filter to
const container = new PIXI.Container()
// add the circle to the container
container.addChild(circle);
// apply the filter to the container
container.filters = [blurFilter];
// put the container on the stage
app.stage.addChild(container);
Now every child of the container will have a blur filter applied to it. *You don't have to apply filters to containers, you can also apply them directly to other display objects (sprites, graphics, shapes, text, etc...)
To use community filters is pretty much the same except you have to first install or download them and when you instantiate them you do it directly instead of through PIXI
so instead of:
const filter = new PIXI.BloomFilter()
you'll code:
const filter = new BloomFilter()
To learn how to install the community filters check the repo I mentioned earlier here and go all the way to the bottom.
You may have seen Texture
appear in the above code here and there but I still didn't explain it, let's fix that now!
when dealing with images in pixi there are 4 things in play:
- the image file that contains all the data of the image
- the
BaseTexture
which takes all the data from the image file and loads it to the GPU - the
Texture
which references all or a portion of the data from theBaseTexture
- the
Sprite
which is aDisplayObject
that "wears" aTexture
as its "face" and can move and be seen in the 'game world'/stage\
Now you might be thinking to yourself (I know I did), why so complicated?
In short, performance. In length, maybe later in this guide, but in case you don't really care why and just want to understand how (to use it), think about it this way:
The BaseTexture
is a piece of paper with a drawing on it.
The Texture
is a smaller piece of paper that we cut from the BaseTexture
.
The Sprite
is a piece of plastic that we stick the small piece of paper to.
Now we can use our piece of plastic with a picture stuck to it as a 'character' on our board game.\
Thankfully, because this is all digital, we can cut as many small pieces of paper from the bigger piece as we want, even if we already cut that area before.
And in the same way we can also stick the small piece of paper to as many pieces of plasic as we want.\
So now hopefully even if you don't understand the technical details underneath, you at least understand the relationship between all the parts of this system and why we use it like we do.\
Ok, so using the preloading techniques we used earlier is the best way to load images into your project, but if you want to be more quick and dirty (for example when prototyping or during development) you can use the following:
to load a single image to a sprite
// when served through the internet this will cause a popin effect as
// it takes time for the image to download to the browser then loaded into
// the GPU
const sprite = new PIXI.Sprite.from('image.png')
Once you're done with your sprites and textures (you have no more need for them in your app) you should free up the memory they take:
sprite.destroy();
texture.destroy();
baseTexture.destroy();
// or alternatively
sprite.destroy({
children: true, // destroy children if there are any [default is false]
texture: true, // destroy the texture the sprite has been using [default is false]
baseTexture: true, // destroy the baseTexture used by the sprite's texture [default is false]
})
this removes references to the texture in your app and lets the garbage collector remove it from memory. When all textures of a BaseTexture
have been destroyed the BaseTexture
will also be removed from memory and from the GPU.
As we mentioned before Graphics
in pixi is the name of an Object that lets you 'draw'/build simple shapes. A single Graphics
instance can build one or more shapes that can be combined to create composites. here is a list of the shapes a Graphics
object can build,
Built in shapes:
- Line -
lineTo(x,y)
(usemoveTo(x,y)
to where you want the line to start) - Rectangle -
drawRect(x,y,width,height)
- Round Rectangle -
drawRoundedRect(x,y,width,height,radius)
- Circle -
drawCircle(x,y,radius)
- Ellipse -
drawEllipse(x,y,width,height)
- Polygon -
drawPolygon(โฆpath)
- Arc -
arc(cx, cy, radius, startAngle, endAngle, anticlockwise)
- Bezier Curve -
bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY)
- Quadratic Curve -
quadraticCurveTo(cpX, cpY, toX, toY)
\
Extra shapes that come with the @pixi/graphics-extras
package:
- Torus -
drawTorus(x, y, innerRadius, outerRadius, startArc, endArc)
- Chamfer Rectangle -
drawChamferRect(x, y, width, height, chamfer)
- Fillet Rectangle -
drawFilletRect(x, y, width, height, fillet)
- Regular Polygon -
drawRegularPolygon(x, y, radius, sides, rotation)
- Rounded Polygon -
drawRoundedPolygon(x, y, radius, sides, corner, rotation)
- Star -
drawStar(x, y, points, radius, innerRadius, rotation)
\
You use the extra graphics exactly like you would the built-in graphics except you have to add an import of the @pixi/graphics-extras
:
import * as PIXI from "pixi.js";
import "@pixi/graphics-extras"; // <-- add extras import
// const app = new PIXI.Applic...
// create a Graphics instance
const graphics = new PIXI.Graphics();
graphics.beginFill(0xffff00);
// create the star in graphics
graphics.drawStar(400, 300, 5, 50, 30) // <-- use extras like built-in graphics
// add graphics to the stage
app.stage.addChild(graphics);
Note* - the draw functions in the @pixi/graphics-extras
package can cause trouble with TypeScript, you can try to fix it yourself or use the hacky way I used:
graphics.drawStar ? graphics.drawStar(400, 300, 5, 50, 30) : null;
Note* - If you are not using a modern build tool (such as vite or webpack) the Pixi packages that are not built-in to Pixi can cause a lot of trouble.
To change the geometries (shapes) of a Graphics
instance you have to use the clear
method and then rebuild the shape/s the way you want it to be.
const graphics = new PIXI.Graphics();
graphics.beginFill(0x00ff00);
graphics.drawCircle(400,300,50);
app.stage.add(graphics);
// then some event happnes and I want chnage the circle to be a square
graphics.clear();
graphics.beginFill(0x00ff00);
graphics.drawRect(375,275,50,50);
You could also use this method Inside a ticker callback to create an animation but the pixi docs warn about performance issues that could arise from that. Instead, for shapes that need to be changed every frame, they recommend using the PIXI.Mesh
class which requires some WebGL knowledge (this will be covered later).
The pixi docs advise you to not put too many shapes in one Graphics
instance as it can hinder performance. Instead, they suggest that you create more Graphics
instances and spread your shapes between them.
Also, you can create instances of the Graphics
object by passing the geometry of another Graphics
instance into their constructor, doing this will make both instances share the geometry of the first instance:
// create the instance that will own the geometry
const geometryOwner = new PIXI.Graphics();
geometryOwner.beginFill(0xffff00);
geometryOwner.drawCircle(400, 300, 50);
// create the instance that will refrence the geometry
const geometryUser = new PIXI.Graphics(geometryOwner.geometry);
// the geometry object of both instances is the same one
geometryOwner.geometry === geometryUser.geometry // true
Just make sure to call geometryUser.destroy()
when you're done with it, otherwise the geometry
object will not be garbage collected even if the geometryOwner is destroyed, causing a memory leak.
To use particles in pixi we'll use the ParticleContainer
class, it is basically a regular Container
but with some limitations which makes it more performant (more on the limitations later).
Using particles can feel very complicated but I'll show you it's not too bad.
pixi has an online particle editor that you can play with and use to customize your particles.
Once you are happy with the particles, you can download a JSON config file which we'll feed into the particle emitter to replicate the effect in our project.
Particle editor.
Note* that the particle is using an image, you can see it and download it in the "particle properties" section of the editor, or you can upload and use your own.
Note** the particle editor generates an old version of the emitter configuration but don't worry, they made a function that accepts that old configuration and converts it to the new one so don't get confused when you see it.\
The first step to be able to use particles is to install the @pixi/particle-emitter
package.
npm i @pixi/particle-emitter
with the package installed go through the following steps:
- download the particle JSON file from the online editor
- download the particle image from the online editor (if you didn't use your own)
- add the image file to your public folder -
puclic/images/particle.png
- change the JSON file to .js or .ts (whatever you are using) and export the JSON object
// src/data/emitter.js
export const emitterConfig = {
// json data
}
- import all the things we need and use the emitter in the project
// src/app.js
import { Emitter, upgradeConfig } from "@pixi/particle-emitter";
import { emitterConfig } from "./data/emitter";
const particleContainer = new PIXI.ParticleContainer();
const emitter = new Emitter(particleContainer,upgradeConfig(emitterConfig,"images/particle.png"));
emitter.autoUpdate = true; // you can have that flase but then you need to control the timings of emittion
emitter.updateSpawnPos(400, 300); // the position of the emitter
emitter.emit = true; // a switch to stop and start emittion
app.stage.addChild(particleContainer);
That's it, it should be working now.
Regarding the limitations of the ParticleContainer
it cannot be masked or have filters applied to it and some more advanced features won't work with it. I would tell you exactly what these features are but the documentation is quite vague about it so I have no idea.
Be aware that if you go too far with the effects (too many particles or complicated behavior) it can significantly hurt performance so don't overdo it.
- what are they?
- how you create them?
- how you use them?
Sprite sheets are basically just a bunch of individual images bunched up into one image file.
The benefits of this is faster initial load time for your project and better batch rendering at runtime.
The initial load time improvement is due to less requests to the server as you only need one file instead of many. This benefit becomes more obvious the more images you have.
The improvement in batch rendering helps WebGL draw our sprites faster and is due to all of our sprites coming from a single BaseTexture
.
You may think or want to create sprite sheets manually by yourself but you don't need to! and it's better that you don't!
You see, putting a bunch of images in a file is nice but you then need to also map each sprite to its location on the image and you need to do it in a JSON structure that the pixi spritesheet parser can understand.
Well instead of doing all that, if you have a bunch of individual images you want to turn into a sprite sheet you can use a spritesheet packer such as one of these:
sprite sheet packer, Shoebox, spritesheet.js.
These packers will take your images and arrange them in a single file and they'll also generate the JSON file with all the information about each image's location and other meta data. You just need to download the generated image file (the sprite sheet) and the json that hold all the meta data and load them to your project.
You can do it in 2 ways:
using the Assets
class:
import { Assets } from 'pixi.js';
const sheet = await Assets.load('images/spritesheet.json');
or directly with the Spritesheet
class:
import { Spritesheet } from 'pixi.js';
// texture - needs to be a Texture or BaseTexture object
// spritesheetData - is the json file or js object with the meta data
const sheet = new Spritesheet(texture, spritesheetData);
await sheet.parse(); //<-- notice it returns a promise
console.log('Spritesheet ready to use!');
Note* - both ways are asynchronous!
With the sheet.textures
you can create Sprite objects, and with sheet.animations
you can create an AnimatedSprite
:
await sheet.parse() // assume this is called inside an async function
const sprite = new PIXI.Sprite(sheet.textures.textureName);
const animation = new PIXI.AnimatedSprite(sheet.animations.animationName);
animation.animationSpeed = 0.05; // speed in seconds
app.stage.addChild(animation, sprite);
animation.play();
Sprite sheets can define animations, anchors for each individual sprite and more, here is an example of what a spritesheet.json
can look like:
{
"frames": {
"enemy1.png":
{
"frame": {"x":103,"y":1,"w":32,"h":32},
"spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
"sourceSize": {"w":32,"h":32},
"anchor": {"x":16,"y":16}
},
"enemy2.png":
{
"frame": {"x":103,"y":35,"w":32,"h":32},
"spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
"sourceSize": {"w":32,"h":32},
"anchor": {"x":16,"y":16}
},
"button.png":
{
"frame": {"x":1,"y":1,"w":100,"h":100},
"spriteSourceSize": {"x":0,"y":0,"w":100,"h":100},
"sourceSize": {"w":100,"h":100},
"anchor": {"x":0,"y":0},
"borders": {"left":35,"top":35,"right":35,"bottom":35}
}
},
"animations": {
"enemy": ["enemy1.png","enemy2.png"]
},
"meta": {
"image": "sheet.png",
"format": "RGBA8888",
"size": {"w":136,"h":102},
"scale": "1"
}
}
There are two kinds of text in Pixi:
Text
BitmapText
in short:
Text
- Good for dynamically styled text that doesn't change too often.
BitmapText
- Good for statically styled text that needs to be changed very frequently.
in length:
Text
- is basically taking the browser's normal text rendering and rasterizes it (turning vector data into pixel data) into an image. once rasterized it acts like a Sprite
, meaning you can scale, rotate, position and tint it as you like. Because of the rasterizing, changing the text (content or style) is a somewhat expensive procedure, that is why it is not meant to change often.
That being said, don't be afraid to change Text
, You can possibly get away with changing one Text
object every frame but if you try to do it with a few at once you'll likely get performance issues.
Test your project and see what's your limit, and take in consideration that your users might have weaker machines than yours.
Because Text
is using the browser's normal text rendering you could encounter a situation where the font you defined is not being applied to your Text
. That's because fonts need to be loaded and if the browser is ready to display text on the screen before the font is loaded it will default to a different font until the font loading is done. Because Text
is rasterized into an image it won't change along with the browser's normal text rendering once the font is loaded and you'll get stuck with the default font (unless you re-generate your Text
after the font loads, but that will cause it to pop-in which is not an ideal user experience).
To ensure the font is loaded you'll need to use a 3rd party library such as FontFaceObserver.
Unfortunately, Pixi's Assets
class is not able to do it for us.
If you want to visually style your font you can use an online tool that will both style it and generate the necessary code for you to supply Pixi with.
here is the tool: pixi-text-style.
Once you're done styling just copy/download the generated JSON or JavaScript to your project.
using JSON
import textStyle from "./textStyle.json";
const text = new PIXI.Text('Styled text content', textStyle);
text.position.set(app.screen.width / 2 - text.width / 2, 30);
app.stage.addChild(text);
using JavaScript
// textStyle.js
export const textStyle = {
// styles copied from pixi-text-style online tool
}
// App.js
import { textStyle } from "./textStyle.js";
const text = new PIXI.Text('Styled text content', textStyle);
text.position.set(app.screen.width / 2 - text.width / 2, 30);
app.stage.addChild(text);
to change the text content of a Text
after it has been instantiated:
textInstance.text = 'new text';
Changing the regular display properties of Text
(position, scale, rotation, etc... ) does not regenerate the text so don't worry about using those.
One thing to note though is that scaling Text
up past its default style-size (scale > 1) will cause it to get pixely/blurry. A solution for this could be making the default style as big as you need it to get and then scaling it down to the other sizes you need. It takes more memory but your text will maintain a clean look.
BitmapText
- this type of text is basically generated from a sprite sheet of all the letters and symbols you are going to use so unlike Text
it does not need to go through rasterizing. Because of this, changing the text content is much faster so this is a very good option if you need a lot of changing text at once. Of course, because it is an image you cannot change its style without editing/changing the image.
I still need to test this one but seems like another benefit BitmapText
has is that unlike Text
it does not need to wait for a font to load, you just need to pre-load its image and meta data with the Assets
class.
To create a BitmapText
you don't need to create a spritesheet, in this case pixi makes our lives easier. To style your BitmapText
you can once again use the online tool pixi-text-style just like we did with Text
and get the JavaScript or JSON files to use in your project. then in your code just do:
import textStyle from "./textStyle.json";
// create a bitmap font
PIXI.BitmapFont.from('myFont', textStyle);
// create the BitmapText object with your text and the font we just created
const text = new PIXI.BitmapText('My styled BitmapText', {fontName: 'myFont'});
// center the font on the screen
text.position.set(app.screen.width / 2 - text.width / 2, app.screen.height / 2);
app.stage.addChild(text)
If you want to create a custom font BitmapText
you can do so with 3rd party tools such as: Snowb.
To use Snowb go to their online tool and use their default font or upload your own then design it however you want. Once done click the export button and it will give you the option to name your font and save it in a few different formats.
Choose the "fileName.txt (BMFont TEXT)
" format and make sure to remember the name you give the font. This name is what you need to pass to the BitmapText
constructor. The Pixi docs say that you can also use a .xml
file (instead of the .txt
we're using) but I couldn't get it to work.
Now that you chose the right format and you remember the font name, you can save the export (should download as a .zip
file) .
Inside you'll find a .txt
with the meta data and a .png
image spritesheet, add them both to your project files.
The last thing we need before using it in the project is to convert the .txt
to a .js
and export its entire contents as a single string
.
rename yourFileName.txt -> yourFileName.js
// then inside yourFileName.js wrap the entire text with backticks
// and export it
export const fontData = `
file contents...
`
Finally, we can now use our custom font like this:
import { fontData } from "./yourFileName.js";
// create a texture from the font spritesheet (in production use PIXI.Assets.load('path/to/file.png') and remember it returns a Promise)
const fontTexture = PIXI.Texture.from('images/yourFileName.png')
// this adds the font to the available fonts list under font-name you gave when you exported it
PIXI.BitmapFont.install(fontData,fontTexture);
// create the actual BitmapText instance that will apear on screen
const customFontText = new PIXI.BitmapText('My custom BitmapText', {fontName: 'theFontNameThatYouRemember'});
// center the font on the screen
customFontText.position.set(app.screen.width / 2 - customFontText.width / 2, app.screen.height / 2 - customFontText.height / 2);
app.stage.addChild(customFontText)
to change the text content of a BitmapText
after it has been instantiated:
bitmapTextInstance.text = 'new text';
- I didn't test this but I think you could use custom fonts using the simpler builtin pixi way, the thing to note about it is that you'll need to ensure your font loads like in the case of using custom fonts with
Text
.
playing sounds in pixi is actually surprisingly easy!
npm i @pixi/sound
then
import { sound } from "@pixi/sound";
// add sound to list of sounds available and give it a name
sound.add('soundName', 'path/to/sound.mp3');
// play the sound using the name you gave it
sound.play('soundName')
The example above works but it doesn't preloads your sound. But preloading is also very easy:
import { sound } from "@pixi/sound";
// add sound with a name, pass an options object with preload: true
sound.add('soundName', {
url: 'path/to/sound.mp3',
preload: true,
loaded: (err, sound) => {
// code to run when loaded
sound.volume = 0.5;
sound.play();
}
});
// button to play the sound
const button = new PIXI.Graphics()
button.beginFill(0x0000ff);
button.drawRect(350,275,100,50);
button.interactive = true;
button.on('pointerdown', () => {
sound.play('soundName', {
volume: 0.7,
start: 2, // start playing from 2 seconds from the begining of the track
end: 5 // stops playing 5 seconds from the begining of the track
});
})
app.stage.addChild(button);
a list of sound properties you can change (there are more):
- volume: number - volume (1 is max)
- loop: boolean - should the sound loop
- filters: Filter[] - filters applied to the sound
- speed: number - playback speed (1 is normal)
- muted: boolean - mute state
- paused: boolean - paused state
- singleInstance: boolean -
true
to disallow playing multiple layered instances at once. - url: string - path to sound file
There's actually a lot more that can be done with sounds such as:
- sound sprites - like spritesheets but for audio!
- adding all kinds of filters/effects
- audio events - calling functions at specific progression points of the track
- using fallback sound formats (for devices/browsers that don't support one)
- utilities
- streaming
but we won't get into these now.
Written partly with StackEdit.