Using SVG Icons with Pixi.JS
Step by step guide on integrating SVG icons efficiently in Pixi.js in Javascript

Search for a command to run...
Step by step guide on integrating SVG icons efficiently in Pixi.js in Javascript

No comments yet. Be the first to comment.
A Real-World Guide to Avoiding Dependency Pitfalls in a Monorepo

7 Practical Tips for Faster Frontend Performance

It was just another Monday at Certa, where a team of engineers had completed a significant deployment over the weekend. The mood was upbeat as everyone settled in for the start of the workweek. But the serenity was deceptive; a subtle issue brewing w...

Problem In our codebase, we had a very specific parser responsible for converting meta JSON created on front-end into a format ingestible by our backend. The parser was exclusively utilized by the frontend until recently. However, while we were build...

Background As development teams rely on CI/CD tools and workflows more and more to deploy their applications, testing has become a key integral part of the SDLC. By using automated testing tools like CircleCI or GitHub Actions, we can ensure our code...

1. Basic understanding of PixiJS
PixiJS is an open-source, web-based rendering system that provides blazing-fast performance for graphics-intensive projects.
This blog assumes the user has set up a basic Pixi.js application. If not, it's highly recommended to go through the official Pixi.js tutorials
2. SVG
SVG is an XML-based vector image format for defining two-dimensional graphics, having support for interactivity and animation.
Basic understanding of <symbol> and <use> element
The <symbol> element is used to define graphical template objects which can be instantiated by a <use> element. Refer to MDN for reference.
3. How to use spritesheet
In layman's terms, spritesheet is a collection of small images, like icons in this case. Instead of keeping each picture separately, you tile them on a single large document. To view an image from this spritesheet, you just need the position of the image.
If you directly use SVG icons within Pixi JS, they get "Rasterized" (meaning they lose their vector data). Which means if you scale them, they get blurred out. To avoid this, we can:
Either convert them into native Pixi Objects using something like pixi-svg library.
Or we can set the SVG scale beforehand so we can rasterize them at higher resolution, before they get converted to textures.
For the scope of this blog post, we will be implementing the solution based on the second approach, while making sure that we have:
Efficient Icon Rendering Performance - Icons are loaded quickly and rendered without any hassle.
Smoother Icon Management - Adding new icons should not require significant extra effort
Let's get started, with our action plan.
NodeJS Script to generate Spritesheet
The first step is to create a spritesheet from available SVG icons in our workspace. The advantage of this tooling is rich DX and a better way to manage SVG icons.
Assumption is that we already have a set of SVG icons with us
The script will read these icons and compile them into a single spritesheet.json file, which we'll use within our code.
The script itself will be responsible for:
loading SVG icons as strings
Wrapping <symbol> around them so they become "reusable".
Finally, using them by <use> and laying them horizontally. (with the assumption that every icon is of the same size and aspect ratio)
Following will be format for spritesheet.json
// Spritesheet.json
{
"spriteSheetSVGData": "..." // Entire SVG as string
"iconSequence":[] // sequence of icons in the spritesheet.
}
Using SVGs in Pixi.JS
Now coming to our main logic, we will use the generated spritesheet JSON to:
Create texture from the spritesheet
Create a function to crop the spritesheet based on the provided iconName and return the resultant sprite.
Render the icon in a PixiJS Stage.
π§π½βπ» Spritesheet generation
spritesheet.json consists of the following properties
spritesheetSVGData: stringified SVG Data of the spritesheet.
iconSequence : An ordered array of distinct svg-ids used in the spritesheet. This is a critical attribute, whose index will be used to fetch an Icon, which is going to be used in Pixi.js app.
/**
* Note:
* The script runs in node-js environment.
*/
import path from "path";
import fs from "fs-extra";
/**
* Converts string to PascalCase
*/
const toPascalCase = (str) => {
return `${str}`
.replace(/[-_]+/g, " ")
.replace(/[^\w\s]/g, "")
.replace(
/\s+(.)(\w*)/g,
($1, $2, $3) => `${$2.toUpperCase() + $3.toLowerCase()}`,
)
.replace(/\w/, (s) => s.toUpperCase());
};
// Directory where spritesheet.json will be generated
const spriteSheetDirectoryPath = "";
// spritesheet object, to be loaded in JSON.
const spriteSheet = {
spriteSheetSVGData: "",
iconSequence: [],
};
// Utility function to replace SVG with <symbol>
const replaceSvgWithSymbol = (svg, id) => {
// Replace <svg> opening tag with <symbol> opening tag
let modifiedSvg = svg.replace(/<svg([^>]*)>/, `<symbol$1 id="${id}">`);
// Replace </svg> closing tag with </symbol> closing tag
modifiedSvg = modifiedSvg.replace("</svg>", "</symbol>");
// Remove xmlns property from <symbol>
modifiedSvg = modifiedSvg.replace(' xmlns="http://www.w3.org/2000/svg"', "");
return modifiedSvg;
};
export const createSpriteSheet = (svgList) => {
/*
Used to store <symbol>(s).
Every root <symbol> represents a svg icon.
For example:
<symbol id="iconName">...</symbol>
*/
let symbols = "";
// conversion of SVG to <symbol>, and
// updating IconSequence in spritesheet
svgList.forEach((svg) => {
const svgCode = svg.data;
const iconName = toPascalCase(svg.name);
const symbolCode = replaceSvgWithSymbol(svgCode, iconName);
symbols += symbolCode;
spriteSheet.iconSequence.push(iconName);
});
/*
Create SpriteSheet SVG Data
Our icons are of size: 16 X 16
A linear spritesheet having a
height: 16 units (based on icon height).
width: 16 * total icons
gap between icons: 8px
The data part of spritesheet svg
We use entity reference method, to put images in the spritesheet.
<use href="symbol-id" y=0 x={index * 24}/>
Finally write the spriteSheet to the respective JSON file
*/
spriteSheet.spriteSheetSVGData = `<svg xmlns="http://www.w3.org/2000/svg"
height="16" width="${16 * svgList.length}" viewbox="0 0 ${
16 * svgList.length
} 16">${symbols}${spriteSheet.iconSequence
.map((symbolID, index) => {
return `<use href="#${symbolID}" x="${index * 24}" y="0"/>`;
})
.join(" ")}</svg>`;
// Create Spritesheet.json
fs.writeFileSync(
path.resolve(spriteSheetDirectoryPath, "spritesheet.json"),
JSON.stringify(spriteSheet),
);
// Create Spritesheet.svg
fs.writeFileSync(
path.resolve(spriteSheetDirectoryPath, "spritesheet.svg"),
JSON.stringify(spriteSheet.spriteSheetSVGData),
);
};
βοΈ Using SVGs in Pixi.JS
Assumptions
spritesheet.svg is generated from the above script.
iconSequence is extracted from the spriteSheet.json
Example App Structure
We will be using Vanilla Javascript as an example for simplicity and easier for understanding
app/
ββ index.html
ββ js/
β ββ app.js
ββ static/
β ββspritesheet.json
As we are using vanilla javascript. We will be using the native DOM to load the Pixi.JS. Henceforth, here is what index.html looks like.
app.js is loaded as a module. This is intentionally done to prevent scope ambiguity issues with global context. Henceforth, we have to explicitly mention what variables we want to add to the global context. For example: globalThis.__PIXI_APP__ = app; <!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>PixiJS and Icons Integration</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/pixi.js@6.x/dist/browser/pixi.min.js"></script>
<script type="module" src="js/app.js"></script>
</body>
</html>
app.js performs the following operations.
Initialize PixiJS
Load SVG Resource for spritesheet
Create texture from the SVG resource.
A function that takes input as icon name, and returns the relevant icon sprite.
Create a sprite from the texture.
Crop the sprite generated, based on the required index found in iconSequence
If no such icon exists, return undefined.
// App.js
// File where our svg is stored.
const SVG_URL = "/images/spriteSheet.svg";
const ICON_SEQUENCE = ["House", "Tasks", "Chart"];
// Initializing the PIXIJS
let app = new PIXI.Application({
width: 400,
height: 300,
resolution: window.devicePixelRatio,
antialias: true,
backgroundColor: 0xeeeeee,
autoDensity: true
});
globalThis.__PIXI_APP__ = app;
document.body.appendChild(app.view);
// Created SVG Resource from the SVG
const svgRes = new PIXI.SVGResource(SVG_URL, { scale: 3 });
// Created Texture from the SVG Resource
const texture = PIXI.Texture.from(svgRes);
/* Returns the relevant Icon Sprite
* @param iconName: string
* @param options: {x:number, y:number, height:number, width:number}
*/
function getIcon(iconName, options) {
const sprite = new PIXI.Sprite(texture);
const iconIndex = ICON_SEQUENCE.findIndex(
(iName) => iName === iconName);
if (iconIndex < 0) {
console.error("No icon found with name:", iconName);
return undefined;
}
texture.on("update", () => {
if (texture.valid) {
texture.frame = new PIXI.Rectangle(70 * iconIndex, 0, 50, 48);
sprite.width = options?.width || 16;
sprite.height = options?.height || 16;
sprite.x = options?.x || 175;
sprite.y = options?.y || 125;
}
});
return sprite;
}
const iconSprite = getIcon("Tasks", { height: 30, width: 30, x: 25, y: 25 });
app.stage.addChild(iconSprite);
Here's a live example of our above implementation.
So let's look at the aspects we discussed before starting the blog.
Icon Rendering Performance
Icon Management
Spritesheets can be now easily generated and updated with new icons from our new script.
And every generated spritesheet, can be versioned via git.
In the end, the whole spritesheet is just a big SVG file, and we need not worry about having multiple files for multiple SVG icons, providing a smoother icon management experience, without any overhead of fetching individual resources.
I hope you found the blog helpful and got to learn something new today. Plus give a read to our detailed blog on building a layout engine in Pixi.JS here
Solving challenging problems at scale in a fully remote team interests you, head to our careers page and apply for the position of your liking!