Photo by Harpal Singh on Unsplash
Using SVG Icons with Pixi.JS
Step by step guide on integrating SVG icons efficiently in Pixi.js in Javascript
π Pre-requisites
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>
elementThe
<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.
βοΈ The Challenge
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.
- The problem with this approach is that these conversions don't translate 1:1 to original SVG due to lack of certain features. So this option can be used if your SVG is not using any of the unsupported features listed by the 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
π The Action Plan
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.
π Code Walkthroughs
π§π½βπ» Spritesheet generation
spritesheet.json consists of the following properties
spritesheetSVGData
: stringified SVG Data of the spritesheet.iconSequence
: An ordered array of distinctsvg-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 thespriteSheet.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 whatindex.html
looks like.- Note: The script,
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;
- Note: The script,
<!--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);
π¨πΌβπ» CodeSandbox
Here's a live example of our above implementation.
Conclusion
So let's look at the aspects we discussed before starting the blog.
Icon Rendering Performance
- Now, we do not calculate textures for every icon. Instead, we calculate it once for the spritesheet, and then we crop the spritesheet based on the icon we need.
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
We are hiring! π
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!