| name | pixijs-2d |
| description | Fast, lightweight 2D rendering engine for creating interactive graphics, particle effects, and canvas-based applications using WebGL/WebGPU. Use this skill when building 2D games, particle systems, interactive canvases, sprite animations, or UI overlays on 3D scenes. Triggers on tasks involving PixiJS, 2D rendering, sprite sheets, particle effects, filters, or high-performance canvas graphics. Alternative to Canvas2D with WebGL acceleration for rendering thousands of sprites at 60 FPS. |
PixiJS 2D Rendering Skill
Fast, lightweight 2D rendering engine for creating interactive graphics, particle effects, and canvas-based applications using WebGL/WebGPU.
When to Use This Skill
Trigger this skill when you encounter:
- "Create 2D particle effects" or "animated particles"
- "2D sprite animation" or "sprite sheet handling"
- "Interactive canvas graphics" or "2D game"
- "UI overlays on 3D scenes" or "HUD layer"
- "Draw shapes programmatically" or "vector graphics API"
- "Optimize rendering performance" or "thousands of sprites"
- "Apply visual filters" or "blur/displacement effects"
- "Lightweight 2D engine" or "alternative to Canvas2D"
Use PixiJS for: High-performance 2D rendering (up to 100,000+ sprites), particle systems, interactive UI, 2D games, data visualization with WebGL acceleration.
Don't use for: 3D graphics (use Three.js/R3F), simple animations (use Motion/GSAP), basic DOM manipulation.
Core Concepts
1. Application & Renderer
The entry point for PixiJS applications:
import { Application } from 'pixi.js';
const app = new Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1099bb,
antialias: true,
resolution: window.devicePixelRatio || 1
});
document.body.appendChild(app.canvas);
Key Properties:
app.stage: Root container for all display objects
app.renderer: WebGL/WebGPU renderer instance
app.ticker: Update loop for animations
app.screen: Canvas dimensions
2. Sprites & Textures
Core visual elements loaded from images:
import { Assets, Sprite } from 'pixi.js';
const texture = await Assets.load('path/to/image.png');
const sprite = new Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(400, 300);
sprite.scale.set(2);
sprite.rotation = Math.PI / 4;
sprite.alpha = 0.8;
sprite.tint = 0xff0000;
app.stage.addChild(sprite);
Quick Creation:
const sprite = Sprite.from('path/to/image.png');
3. Graphics API
Draw vector shapes programmatically:
import { Graphics } from 'pixi.js';
const graphics = new Graphics();
graphics.rect(50, 50, 100, 100).fill('blue');
graphics.circle(200, 100, 50).fill('red').stroke({ width: 2, color: 'white' });
graphics
.moveTo(300, 100)
.lineTo(350, 150)
.lineTo(250, 150)
.closePath()
.fill({ color: 0x00ff00, alpha: 0.5 });
app.stage.addChild(graphics);
SVG Support:
graphics.svg('<svg><path d="M 100 350 q 150 -300 300 0" /></svg>');
4. ParticleContainer
Optimized container for rendering thousands of sprites:
import { ParticleContainer, Particle, Texture } from 'pixi.js';
const texture = Texture.from('particle.png');
const container = new ParticleContainer({
dynamicProperties: {
position: true,
scale: false,
rotation: false,
color: false
}
});
for (let i = 0; i < 10000; i++) {
const particle = new Particle({
texture,
x: Math.random() * 800,
y: Math.random() * 600
});
container.addParticle(particle);
}
app.stage.addChild(container);
Performance: Up to 10x faster than regular Container for static properties.
5. Filters
Apply per-pixel effects using WebGL shaders:
import { BlurFilter, DisplacementFilter, ColorMatrixFilter } from 'pixi.js';
const blurFilter = new BlurFilter({ strength: 8, quality: 4 });
sprite.filters = [blurFilter];
sprite.filters = [
new BlurFilter({ strength: 4 }),
new ColorMatrixFilter()
];
sprite.filterArea = new Rectangle(0, 0, 200, 100);
Available Filters:
BlurFilter: Gaussian blur
ColorMatrixFilter: Color transformations (sepia, grayscale, etc.)
DisplacementFilter: Warp/distort pixels
AlphaFilter: Flatten alpha across children
NoiseFilter: Random grain effect
FXAAFilter: Anti-aliasing
6. Text Rendering
Display text with styling:
import { Text, BitmapText, TextStyle } from 'pixi.js';
const style = new TextStyle({
fontFamily: 'Arial',
fontSize: 36,
fill: '#ffffff',
stroke: { color: '#000000', width: 4 },
filters: [new BlurFilter()]
});
const text = new Text({ text: 'Hello PixiJS!', style });
text.position.set(100, 100);
const bitmapText = new BitmapText({
text: 'Score: 0',
style: { fontFamily: 'MyBitmapFont', fontSize: 24 }
});
Performance Tip: Use BitmapText for frequently changing text (scores, counters).
Common Patterns
Pattern 1: Basic Interactive Sprite
import { Application, Assets, Sprite } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
const texture = await Assets.load('bunny.png');
const bunny = new Sprite(texture);
bunny.anchor.set(0.5);
bunny.position.set(400, 300);
bunny.eventMode = 'static';
bunny.cursor = 'pointer';
bunny.on('pointerdown', () => {
bunny.scale.set(1.2);
});
bunny.on('pointerup', () => {
bunny.scale.set(1.0);
});
bunny.on('pointerover', () => {
bunny.tint = 0xff0000;
});
bunny.on('pointerout', () => {
bunny.tint = 0xffffff;
});
app.stage.addChild(bunny);
app.ticker.add((ticker) => {
bunny.rotation += 0.01 * ticker.deltaTime;
});
Pattern 2: Drawing with Graphics
import { Graphics, Application } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
const graphics = new Graphics();
graphics.rect(50, 50, 200, 100).fill({
color: 0x3399ff,
alpha: 0.8
});
graphics.circle(400, 300, 80)
.fill('yellow')
.stroke({ width: 4, color: 'orange' });
graphics.star(600, 300, 5, 50, 0).fill({ color: 0xffdf00, alpha: 0.9 });
graphics
.moveTo(100, 400)
.bezierCurveTo(150, 300, 250, 300, 300, 400)
.stroke({ width: 3, color: 'white' });
graphics
.rect(450, 400, 150, 100).fill('red')
.beginHole()
.circle(525, 450, 30)
.endHole();
app.stage.addChild(graphics);
app.ticker.add(() => {
graphics.clear();
const time = Date.now() * 0.001;
const x = 400 + Math.cos(time) * 100;
const y = 300 + Math.sin(time) * 100;
graphics.circle(x, y, 20).fill('cyan');
});
Pattern 3: Particle System with ParticleContainer
import { Application, ParticleContainer, Particle, Texture } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600, backgroundColor: 0x000000 });
document.body.appendChild(app.canvas);
const texture = Texture.from('spark.png');
const particles = new ParticleContainer({
dynamicProperties: {
position: true,
scale: true,
rotation: true,
color: false
}
});
const particleData = [];
for (let i = 0; i < 5000; i++) {
const particle = new Particle({
texture,
x: 400,
y: 300,
scaleX: 0.5,
scaleY: 0.5
});
particles.addParticle(particle);
particleData.push({
particle,
vx: (Math.random() - 0.5) * 5,
vy: (Math.random() - 0.5) * 5 - 2,
life: 1.0
});
}
app.stage.addChild(particles);
app.ticker.add((ticker) => {
particleData.forEach(data => {
data.particle.x += data.vx * ticker.deltaTime;
data.particle.y += data.vy * ticker.deltaTime;
data.vy += 0.1 * ticker.deltaTime;
data.life -= 0.01 * ticker.deltaTime;
data.particle.scaleX = data.life * 0.5;
data.particle.scaleY = data.life * 0.5;
if (data.life <= 0) {
data.particle.x = 400;
data.particle.y = 300;
data.vx = (Math.random() - 0.5) * 5;
data.vy = (Math.random() - 0.5) * 5 - 2;
data.life = 1.0;
}
});
});
Pattern 4: Applying Filters
import { Application, Sprite, Assets, BlurFilter, DisplacementFilter } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
const texture = await Assets.load('photo.jpg');
const photo = new Sprite(texture);
photo.position.set(100, 100);
const blurFilter = new BlurFilter({ strength: 5, quality: 4 });
const displacementTexture = await Assets.load('displacement.jpg');
const displacementSprite = Sprite.from(displacementTexture);
const displacementFilter = new DisplacementFilter({
sprite: displacementSprite,
scale: 50
});
photo.filters = [blurFilter, displacementFilter];
photo.filterArea = new Rectangle(0, 0, photo.width, photo.height);
app.stage.addChild(photo);
app.ticker.add((ticker) => {
displacementSprite.x += 1 * ticker.deltaTime;
displacementSprite.y += 0.5 * ticker.deltaTime;
});
Pattern 5: Custom Filter with Shaders
import { Filter, GlProgram } from 'pixi.js';
const vertex = `
in vec2 aPosition;
out vec2 vTextureCoord;
uniform vec4 uInputSize;
uniform vec4 uOutputFrame;
uniform vec4 uOutputTexture;
vec4 filterVertexPosition() {
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
position.y = position.y * (2.0*uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
return vec4(position, 0.0, 1.0);
}
vec2 filterTextureCoord() {
return aPosition * (uOutputFrame.zw * uInputSize.zw);
}
void main() {
gl_Position = filterVertexPosition();
vTextureCoord = filterTextureCoord();
}
`;
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uTime;
void main() {
vec2 uv = vTextureCoord;
// Wave distortion
float wave = sin(uv.y * 10.0 + uTime) * 0.05;
vec4 color = texture(uTexture, vec2(uv.x + wave, uv.y));
gl_FragColor = color;
}
`;
const customFilter = new Filter({
glProgram: new GlProgram({ fragment, vertex }),
resources: {
timeUniforms: {
uTime: { value: 0.0, type: 'f32' }
}
}
});
sprite.filters = [customFilter];
app.ticker.add((ticker) => {
customFilter.resources.timeUniforms.uniforms.uTime += 0.04 * ticker.deltaTime;
});
Pattern 6: Sprite Sheet Animation
import { Application, Assets, AnimatedSprite } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
await Assets.load('spritesheet.json');
const frames = [];
for (let i = 0; i < 10; i++) {
frames.push(Texture.from(`frame_${i}.png`));
}
const animation = new AnimatedSprite(frames);
animation.anchor.set(0.5);
animation.position.set(400, 300);
animation.animationSpeed = 0.16;
animation.play();
app.stage.addChild(animation);
animation.stop();
animation.gotoAndPlay(0);
animation.onComplete = () => {
console.log('Animation completed!');
};
Pattern 7: Object Pooling for Performance
class SpritePool {
constructor(texture, initialSize = 100) {
this.texture = texture;
this.available = [];
this.active = [];
for (let i = 0; i < initialSize; i++) {
this.createSprite();
}
}
createSprite() {
const sprite = new Sprite(this.texture);
sprite.visible = false;
this.available.push(sprite);
return sprite;
}
spawn(x, y) {
let sprite = this.available.pop();
if (!sprite) {
sprite = this.createSprite();
}
sprite.position.set(x, y);
sprite.visible = true;
this.active.push(sprite);
return sprite;
}
despawn(sprite) {
sprite.visible = false;
const index = this.active.indexOf(sprite);
if (index > -1) {
this.active.splice(index, 1);
this.available.push(sprite);
}
}
reset() {
this.active.forEach(sprite => {
sprite.visible = false;
this.available.push(sprite);
});
this.active = [];
}
}
const bulletTexture = Texture.from('bullet.png');
const bulletPool = new SpritePool(bulletTexture, 50);
const bullet = bulletPool.spawn(100, 200);
app.stage.addChild(bullet);
setTimeout(() => {
bulletPool.despawn(bullet);
}, 2000);
Integration Patterns
React Integration
import { useEffect, useRef } from 'react';
import { Application } from 'pixi.js';
function PixiCanvas() {
const canvasRef = useRef(null);
const appRef = useRef(null);
useEffect(() => {
const init = async () => {
const app = new Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1099bb
});
canvasRef.current.appendChild(app.canvas);
appRef.current = app;
};
init();
return () => {
if (appRef.current) {
appRef.current.destroy(true, { children: true });
}
};
}, []);
return <div ref={canvasRef} />;
}
Three.js Overlay (2D UI on 3D)
import * as THREE from 'three';
import { Application, Sprite, Text } from 'pixi.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight);
const renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);
const pixiApp = new Application();
await pixiApp.init({
width: window.innerWidth,
height: window.innerHeight,
backgroundAlpha: 0
});
pixiApp.canvas.style.position = 'absolute';
pixiApp.canvas.style.top = '0';
pixiApp.canvas.style.left = '0';
pixiApp.canvas.style.pointerEvents = 'none';
document.body.appendChild(pixiApp.canvas);
const scoreText = new Text({ text: 'Score: 0', style: { fontSize: 24, fill: 'white' } });
scoreText.position.set(20, 20);
pixiApp.stage.addChild(scoreText);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
pixiApp.renderer.render(pixiApp.stage);
}
animate();
Performance Best Practices
1. Use ParticleContainer for Large Sprite Counts
const container = new Container();
for (let i = 0; i < 10000; i++) {
container.addChild(new Sprite(texture));
}
const particles = new ParticleContainer({
dynamicProperties: { position: true }
});
for (let i = 0; i < 10000; i++) {
particles.addParticle(new Particle({ texture }));
}
2. Optimize Filter Usage
sprite.filterArea = new Rectangle(0, 0, 200, 100);
sprite.filters = null;
const style = new TextStyle({
filters: [new BlurFilter()]
});
3. Manage Texture Memory
texture.destroy();
textures.forEach((tex, i) => {
setTimeout(() => tex.destroy(), Math.random() * 100);
});
4. Enable Culling for Off-Screen Objects
sprite.cullable = true;
import { CullerPlugin } from 'pixi.js';
5. Cache Static Graphics as Bitmaps
const complexShape = new Graphics();
complexShape.cacheAsBitmap = true;
6. Optimize Renderer Settings
const app = new Application();
await app.init({
antialias: false,
resolution: 1,
autoDensity: true
});
7. Use BitmapText for Dynamic Text
const text = new Text({ text: `Score: ${score}` });
app.ticker.add(() => {
text.text = `Score: ${++score}`;
});
const bitmapText = new BitmapText({ text: `Score: ${score}` });
app.ticker.add(() => {
bitmapText.text = `Score: ${++score}`;
});
Common Pitfalls
Pitfall 1: Not Destroying Objects
Problem: Memory leaks from unreleased GPU resources.
Solution:
sprite.destroy({ children: true, texture: true, baseTexture: true });
sprite.filters = null;
graphics.destroy();
Pitfall 2: Updating Static ParticleContainer Properties
Problem: Changing scale when dynamicProperties.scale = false has no effect.
Solution:
const container = new ParticleContainer({
dynamicProperties: {
position: true,
scale: true,
rotation: true,
color: true
}
});
container.update();
Pitfall 3: Excessive Filter Usage
Problem: Filters are expensive; too many cause performance issues.
Solution:
sprite.filters = [blurFilter];
sprite.filterArea = new Rectangle(0, 0, sprite.width, sprite.height);
const filteredTexture = renderer.filters.generateFilteredTexture({
texture,
filters: [blurFilter]
});
Pitfall 4: Frequent Text Updates
Problem: Updating Text re-generates texture every time.
Solution:
const bitmapText = new BitmapText({ text: 'Score: 0' });
text.resolution = 1;
Pitfall 5: Graphics Clear() Without Redraw
Problem: Calling clear() removes all geometry but doesn't automatically redraw.
Solution:
graphics.clear();
graphics.rect(0, 0, 100, 100).fill('blue');
Pitfall 6: Not Using Asset Loading
Problem: Creating sprites from URLs causes async issues.
Solution:
const sprite = Sprite.from('image.png');
const texture = await Assets.load('image.png');
const sprite = new Sprite(texture);
Resources
Related Skills
- threejs-webgl: For 3D graphics; PixiJS can provide 2D UI overlays
- gsap-scrolltrigger: For animating PixiJS properties with scroll
- motion-framer: For React component animations alongside PixiJS canvas
- react-three-fiber: Similar React integration patterns
Summary
PixiJS excels at high-performance 2D rendering with WebGL acceleration. Key strengths:
- Performance: Render 100,000+ sprites at 60 FPS
- ParticleContainer: 10x faster for static properties
- Filters: WebGL-powered visual effects
- Graphics API: Intuitive vector drawing
- Asset Management: Robust texture and sprite sheet handling
Use for particle systems, 2D games, data visualizations, and interactive canvas applications where performance is critical.