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
- 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, // Smooth edges 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
- Sprites & Textures
Core visual elements loaded from images:
import { Assets, Sprite } from 'pixi.js';
// Load texture const texture = await Assets.load('path/to/image.png');
// Create sprite const sprite = new Sprite(texture); sprite.anchor.set(0.5); // Center pivot sprite.position.set(400, 300); sprite.scale.set(2); // 2x scale sprite.rotation = Math.PI / 4; // 45 degrees sprite.alpha = 0.8; // 80% opacity sprite.tint = 0xff0000; // Red tint
app.stage.addChild(sprite);
Quick Creation:
const sprite = Sprite.from('path/to/image.png');
- Graphics API
Draw vector shapes programmatically:
import { Graphics } from 'pixi.js';
const graphics = new Graphics();
// Rectangle graphics.rect(50, 50, 100, 100).fill('blue');
// Circle with stroke graphics.circle(200, 100, 50).fill('red').stroke({ width: 2, color: 'white' });
// Complex path 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>');
- 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, // Allow position updates scale: false, // Static scale rotation: false, // Static rotation color: false // Static color } });
// Add 10,000 particles 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.
- Filters
Apply per-pixel effects using WebGL shaders:
import { BlurFilter, DisplacementFilter, ColorMatrixFilter } from 'pixi.js';
// Blur const blurFilter = new BlurFilter({ strength: 8, quality: 4 }); sprite.filters = [blurFilter];
// Multiple filters sprite.filters = [ new BlurFilter({ strength: 4 }), new ColorMatrixFilter() // Color transforms ];
// Custom filter area for performance 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
- Text Rendering
Display text with styling:
import { Text, BitmapText, TextStyle } from 'pixi.js';
// Standard Text const style = new TextStyle({ fontFamily: 'Arial', fontSize: 36, fill: '#ffffff', stroke: { color: '#000000', width: 4 }, filters: [new BlurFilter()] // Bake filter into texture });
const text = new Text({ text: 'Hello PixiJS!', style }); text.position.set(100, 100);
// BitmapText (faster for dynamic text) 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'; // Enable interactivity bunny.cursor = 'pointer';
// Events bunny.on('pointerdown', () => { bunny.scale.set(1.2); });
bunny.on('pointerup', () => { bunny.scale.set(1.0); });
bunny.on('pointerover', () => { bunny.tint = 0xff0000; // Red on hover });
bunny.on('pointerout', () => { bunny.tint = 0xffffff; // Reset });
app.stage.addChild(bunny);
// Animation loop 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();
// Rectangle with gradient graphics.rect(50, 50, 200, 100).fill({ color: 0x3399ff, alpha: 0.8 });
// Circle with stroke graphics.circle(400, 300, 80) .fill('yellow') .stroke({ width: 4, color: 'orange' });
// Star shape graphics.star(600, 300, 5, 50, 0).fill({ color: 0xffdf00, alpha: 0.9 });
// Custom path graphics .moveTo(100, 400) .bezierCurveTo(150, 300, 250, 300, 300, 400) .stroke({ width: 3, color: 'white' });
// Holes graphics .rect(450, 400, 150, 100).fill('red') .beginHole() .circle(525, 450, 30) .endHole();
app.stage.addChild(graphics);
// Dynamic drawing (animation) 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, // Update positions every frame scale: true, // Fade out by scaling rotation: true, // Rotate particles color: false // Static color } });
const particleData = [];
// Create particles 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, // Slight upward bias life: 1.0 }); }
app.stage.addChild(particles);
// Update loop app.ticker.add((ticker) => { particleData.forEach(data => { // Physics data.particle.x += data.vx * ticker.deltaTime; data.particle.y += data.vy * ticker.deltaTime; data.vy += 0.1 * ticker.deltaTime; // Gravity
// Fade out
data.life -= 0.01 * ticker.deltaTime;
data.particle.scaleX = data.life * 0.5;
data.particle.scaleY = data.life * 0.5;
// Reset particle
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);
// Blur filter const blurFilter = new BlurFilter({ strength: 5, quality: 4 });
// Displacement filter (wavy effect) const displacementTexture = await Assets.load('displacement.jpg'); const displacementSprite = Sprite.from(displacementTexture); const displacementFilter = new DisplacementFilter({ sprite: displacementSprite, scale: 50 });
// Apply multiple filters photo.filters = [blurFilter, displacementFilter];
// Optimize with filterArea photo.filterArea = new Rectangle(0, 0, photo.width, photo.height);
app.stage.addChild(photo);
// Animate displacement 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];
// Update uniform 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);
// Load sprite sheet await Assets.load('spritesheet.json');
// Create animation from frames
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; // ~10 FPS animation.play();
app.stage.addChild(animation);
// Control playback 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 = [];
// Pre-create sprites
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 = []; } }
// Usage const bulletTexture = Texture.from('bullet.png'); const bulletPool = new SpritePool(bulletTexture, 50);
// Spawn bullet const bullet = bulletPool.spawn(100, 200); app.stage.addChild(bullet);
// Despawn after 2 seconds 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;
// Setup scene
// ... add sprites, graphics, etc.
};
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';
// Three.js scene 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);
// PixiJS overlay const pixiApp = new Application(); await pixiApp.init({ width: window.innerWidth, height: window.innerHeight, backgroundAlpha: 0 // Transparent background });
pixiApp.canvas.style.position = 'absolute'; pixiApp.canvas.style.top = '0'; pixiApp.canvas.style.left = '0'; pixiApp.canvas.style.pointerEvents = 'none'; // Click through document.body.appendChild(pixiApp.canvas);
// Add UI elements const scoreText = new Text({ text: 'Score: 0', style: { fontSize: 24, fill: 'white' } }); scoreText.position.set(20, 20); pixiApp.stage.addChild(scoreText);
// Render loop function animate() { requestAnimationFrame(animate);
renderer.render(scene, camera); // 3D scene pixiApp.renderer.render(pixiApp.stage); // 2D overlay }
animate();
Performance Best Practices
- Use ParticleContainer for Large Sprite Counts
// DON'T: Regular Container (slow for 1000+ sprites) const container = new Container(); for (let i = 0; i < 10000; i++) { container.addChild(new Sprite(texture)); }
// DO: ParticleContainer (10x faster) const particles = new ParticleContainer({ dynamicProperties: { position: true } }); for (let i = 0; i < 10000; i++) { particles.addParticle(new Particle({ texture })); }
- Optimize Filter Usage
// Set filterArea to avoid runtime measurement sprite.filterArea = new Rectangle(0, 0, 200, 100);
// Release filters when not needed sprite.filters = null;
// Bake filters into Text at creation const style = new TextStyle({ filters: [new BlurFilter()] // Applied once at texture creation });
- Manage Texture Memory
// Destroy textures when done texture.destroy();
// Batch destruction with delays to prevent frame drops textures.forEach((tex, i) => { setTimeout(() => tex.destroy(), Math.random() * 100); });
- Enable Culling for Off-Screen Objects
sprite.cullable = true; // Skip rendering if outside viewport
// Use CullerPlugin import { CullerPlugin } from 'pixi.js';
- Cache Static Graphics as Bitmaps
// Convert complex graphics to texture for faster rendering const complexShape = new Graphics(); // ... draw many shapes
complexShape.cacheAsBitmap = true; // Renders to texture once
- Optimize Renderer Settings
const app = new Application(); await app.init({ antialias: false, // Disable on mobile for performance resolution: 1, // Lower resolution on low-end devices autoDensity: true });
- Use BitmapText for Dynamic Text
// DON'T: Standard Text (expensive updates)
const text = new Text({ text: Score: ${score} });
app.ticker.add(() => {
text.text = Score: ${++score}; // Re-renders texture each frame
});
// DO: BitmapText (much faster)
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:
// Always destroy sprites and textures sprite.destroy({ children: true, texture: true, baseTexture: true });
// Destroy filters sprite.filters = null;
// Destroy graphics 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, // Enable if you need to update rotation: true, color: true } });
// If properties are static but you change them, call update: container.update();
Pitfall 3: Excessive Filter Usage
Problem: Filters are expensive; too many cause performance issues.
Solution:
// Limit filter usage sprite.filters = [blurFilter]; // 1-2 filters max
// Use filterArea to constrain processing sprite.filterArea = new Rectangle(0, 0, sprite.width, sprite.height);
// Bake filters into textures when possible const filteredTexture = renderer.filters.generateFilteredTexture({ texture, filters: [blurFilter] });
Pitfall 4: Frequent Text Updates
Problem: Updating Text re-generates texture every time.
Solution:
// Use BitmapText for frequently changing text const bitmapText = new BitmapText({ text: 'Score: 0' });
// Reduce resolution for less memory text.resolution = 1; // Lower than device pixel ratio
Pitfall 5: Graphics Clear() Without Redraw
Problem: Calling clear() removes all geometry but doesn't automatically redraw.
Solution:
graphics.clear(); // Remove all shapes
// Redraw new shapes graphics.rect(0, 0, 100, 100).fill('blue');
Pitfall 6: Not Using Asset Loading
Problem: Creating sprites from URLs causes async issues.
Solution:
// DON'T: const sprite = Sprite.from('image.png'); // May load asynchronously
// DO: const texture = await Assets.load('image.png'); const sprite = new Sprite(texture);
Resources
-
Official Site: https://pixijs.com
-
API Documentation: https://pixijs.download/release/docs/
-
Examples: https://pixijs.io/examples/
-
GitHub: https://github.com/pixijs/pixijs
-
Filters Library: @pixi/filter-* packages
-
Community: https://github.com/pixijs/pixijs/discussions
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.