assets/examples/README.md
# PixiJS Examples and Patterns
Comprehensive real-world examples and patterns for building production-ready PixiJS applications.
## Table of Contents
1. [Basic Applications](#basic-applications)
2. [Interactive Elements](#interactive-elements)
3. [Particle Systems](#particle-systems)
4. [Filters and Effects](#filters-and-effects)
5. [Custom Shaders](#custom-shaders)
6. [Animation Patterns](#animation-patterns)
7. [Performance Optimization](#performance-optimization)
8. [Game Development](#game-development)
9. [UI Components](#ui-components)
10. [Framework Integration](#framework-integration)
11. [Mobile Optimization](#mobile-optimization)
12. [Advanced Techniques](#advanced-techniques)
---
## Basic Applications
### 1. Minimal PixiJS Application
The absolute minimum code to get started with PixiJS v8+:
```javascript
(async () => {
const app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1099bb
});
document.body.appendChild(app.canvas);
// Create a sprite
const texture = await PIXI.Assets.load('bunny.png');
const bunny = new PIXI.Sprite(texture);
bunny.anchor.set(0.5);
bunny.position.set(400, 300);
app.stage.addChild(bunny);
// Animation loop
app.ticker.add((ticker) => {
bunny.rotation += 0.05 * ticker.deltaTime;
});
})();
```
### 2. Responsive Canvas Application
Canvas that automatically resizes to fill the window:
```javascript
(async () => {
const app = new PIXI.Application();
await app.init({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 0x1a1a2e,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true
});
document.body.appendChild(app.canvas);
// Handle window resize
window.addEventListener('resize', () => {
app.renderer.resize(window.innerWidth, window.innerHeight);
// Reposition elements if needed
updateLayout();
});
function updateLayout() {
// Center content or adjust layout based on new dimensions
const centerX = app.screen.width / 2;
const centerY = app.screen.height / 2;
// Update element positions...
}
})();
```
### 3. Asset Loading with Progress
Load multiple assets with loading screen:
```javascript
(async () => {
const app = new PIXI.Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
// Create loading text
const loadingText = new PIXI.Text({
text: 'Loading: 0%',
style: { fontSize: 32, fill: 0xffffff }
});
loadingText.anchor.set(0.5);
loadingText.position.set(400, 300);
app.stage.addChild(loadingText);
// Assets to load
const assets = [
{ alias: 'bunny', src: 'bunny.png' },
{ alias: 'spritesheet', src: 'spritesheet.json' },
{ alias: 'background', src: 'background.jpg' }
];
// Load assets with progress
PIXI.Assets.load(assets, (progress) => {
loadingText.text = `Loading: ${Math.round(progress * 100)}%`;
}).then((textures) => {
// Remove loading screen
app.stage.removeChild(loadingText);
// Start application with loaded assets
startApp(textures);
});
function startApp(textures) {
const bunny = new PIXI.Sprite(textures.bunny);
bunny.position.set(400, 300);
app.stage.addChild(bunny);
}
})();
```
### 4. Multi-Scene Application
Manage multiple scenes/screens:
```javascript
class SceneManager {
constructor(app) {
this.app = app;
this.scenes = new Map();
this.currentScene = null;
}
addScene(name, scene) {
this.scenes.set(name, scene);
scene.visible = false;
this.app.stage.addChild(scene);
}
switchTo(name) {
if (this.currentScene) {
this.currentScene.visible = false;
this.currentScene.onExit?.();
}
const scene = this.scenes.get(name);
if (scene) {
scene.visible = true;
scene.onEnter?.();
this.currentScene = scene;
}
}
}
// Usage
(async () => {
const app = new PIXI.Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
const sceneManager = new SceneManager(app);
// Create scenes
const menuScene = new PIXI.Container();
menuScene.onEnter = () => console.log('Entered menu');
menuScene.onExit = () => console.log('Exited menu');
const gameScene = new PIXI.Container();
gameScene.onEnter = () => console.log('Game started');
gameScene.onExit = () => console.log('Game paused');
// Add menu content
const menuText = new PIXI.Text({
text: 'Main Menu',
style: { fontSize: 48, fill: 0xffffff }
});
menuText.position.set(300, 250);
menuScene.addChild(menuText);
const startButton = new PIXI.Text({
text: 'Start Game',
style: { fontSize: 32, fill: 0x00ff00 }
});
startButton.position.set(320, 350);
startButton.eventMode = 'static';
startButton.cursor = 'pointer';
startButton.on('pointerdown', () => sceneManager.switchTo('game'));
menuScene.addChild(startButton);
// Add game content
const gameText = new PIXI.Text({
text: 'Game Scene',
style: { fontSize: 48, fill: 0xffffff }
});
gameText.position.set(300, 300);
gameScene.addChild(gameText);
// Register scenes
sceneManager.addScene('menu', menuScene);
sceneManager.addScene('game', gameScene);
// Start with menu
sceneManager.switchTo('menu');
})();
```
---
## Interactive Elements
### 1. Draggable Sprites
Implement drag-and-drop functionality:
```javascript
function makeDraggable(sprite) {
sprite.eventMode = 'static';
sprite.cursor = 'pointer';
let dragData = null;
sprite.on('pointerdown', (event) => {
dragData = event.global.clone();
sprite.alpha = 0.5;
app.stage.on('pointermove', onDragMove);
});
sprite.on('pointerup', onDragEnd);
sprite.on('pointerupoutside', onDragEnd);
function onDragMove(event) {
if (dragData) {
const newPosition = event.global;
sprite.x += newPosition.x - dragData.x;
sprite.y += newPosition.y - dragData.y;
dragData = newPosition.clone();
}
}
function onDragEnd() {
if (dragData) {
sprite.alpha = 1;
dragData = null;
app.stage.off('pointermove', onDragMove);
}
}
}
// Usage
const sprite = new PIXI.Sprite(texture);
sprite.position.set(100, 100);
app.stage.addChild(sprite);
makeDraggable(sprite);
```
### 2. Hover Effects
Add visual feedback on mouse hover:
```javascript
function addHoverEffect(sprite, options = {}) {
const {
hoverScale = 1.2,
hoverTint = 0xffff00,
animationSpeed = 0.1
} = options;
sprite.eventMode = 'static';
sprite.cursor = 'pointer';
const originalScale = sprite.scale.x;
const originalTint = sprite.tint;
sprite.on('pointerover', () => {
animateScale(sprite, hoverScale, animationSpeed);
sprite.tint = hoverTint;
});
sprite.on('pointerout', () => {
animateScale(sprite, originalScale, animationSpeed);
sprite.tint = originalTint;
});
}
function animateScale(sprite, targetScale, speed) {
const ticker = (delta) => {
const currentScale = sprite.scale.x;
const diff = targetScale - currentScale;
if (Math.abs(diff) < 0.01) {
sprite.scale.set(targetScale);
app.ticker.remove(ticker);
} else {
const newScale = currentScale + diff * speed * delta.deltaTime;
sprite.scale.set(newScale);
}
};
app.ticker.add(ticker);
}
// Usage
const sprite = new PIXI.Sprite(texture);
addHoverEffect(sprite, { hoverScale: 1.3, hoverTint: 0x00ff00 });
```
### 3. Button Component
Reusable button with states:
```javascript
class Button extends PIXI.Container {
constructor(text, options = {}) {
super();
const {
width = 200,
height = 60,
backgroundColor = 0x4CAF50,
hoverColor = 0x45a049,
textColor = 0xffffff,
fontSize = 24,
borderRadius = 10
} = options;
// Background
this.background = new PIXI.Graphics();
this.drawBackground(backgroundColor, width, height, borderRadius);
this.addChild(this.background);
// Text
this.label = new PIXI.Text({
text,
style: {
fontSize,
fill: textColor,
fontWeight: 'bold'
}
});
this.label.anchor.set(0.5);
this.label.position.set(width / 2, height / 2);
this.addChild(this.label);
// Interactive
this.eventMode = 'static';
this.cursor = 'pointer';
// Store colors
this.normalColor = backgroundColor;
this.hoverColor = hoverColor;
this.width = width;
this.height = height;
this.borderRadius = borderRadius;
// Events
this.on('pointerover', () => {
this.drawBackground(this.hoverColor, this.width, this.height, this.borderRadius);
});
this.on('pointerout', () => {
this.drawBackground(this.normalColor, this.width, this.height, this.borderRadius);
});
this.on('pointerdown', () => {
this.scale.set(0.95);
});
this.on('pointerup', () => {
this.scale.set(1);
});
}
drawBackground(color, width, height, borderRadius) {
this.background.clear();
this.background.roundRect(0, 0, width, height, borderRadius).fill(color);
}
setText(text) {
this.label.text = text;
}
onClick(callback) {
this.on('pointerdown', callback);
return this;
}
}
// Usage
const playButton = new Button('Play Game', {
width: 250,
height: 70,
backgroundColor: 0x3498db,
hoverColor: 0x2980b9
});
playButton.position.set(275, 400);
playButton.onClick(() => {
console.log('Play button clicked!');
startGame();
});
app.stage.addChild(playButton);
```
### 4. Click Detection with Shapes
Detect clicks on custom shapes (not just sprites):
```javascript
const graphics = new PIXI.Graphics();
graphics.circle(400, 300, 100).fill(0xff0000);
graphics.eventMode = 'static';
graphics.cursor = 'pointer';
graphics.hitArea = new PIXI.Circle(400, 300, 100);
graphics.on('pointerdown', (event) => {
console.log('Circle clicked!');
graphics.tint = Math.random() * 0xffffff;
});
app.stage.addChild(graphics);
// Custom polygon hit area
const star = new PIXI.Graphics();
star.star(400, 300, 5, 50, 25).fill(0xffff00);
star.eventMode = 'static';
star.cursor = 'pointer';
// Define hit area as polygon
const starPoints = [
400, 250, // Top
420, 290,
460, 300,
430, 330,
440, 370,
400, 350,
360, 370,
370, 330,
340, 300,
380, 290
];
star.hitArea = new PIXI.Polygon(starPoints);
star.on('pointerdown', () => {
console.log('Star clicked!');
});
app.stage.addChild(star);
```
---
## Particle Systems
### 1. Basic Particle Emitter
Simple particle emitter with gravity:
```javascript
class ParticleEmitter {
constructor(app, texture) {
this.app = app;
this.texture = texture;
this.particles = [];
this.maxParticles = 1000;
this.container = new PIXI.ParticleContainer(this.maxParticles, {
position: true,
rotation: true,
scale: true,
tint: true,
alpha: true
});
app.stage.addChild(this.container);
app.ticker.add((ticker) => this.update(ticker));
}
emit(x, y, count = 10) {
for (let i = 0; i < count; i++) {
if (this.particles.length >= this.maxParticles) break;
const particle = new PIXI.Sprite(this.texture);
particle.anchor.set(0.5);
particle.position.set(x, y);
// Random velocity
particle.vx = (Math.random() - 0.5) * 10;
particle.vy = -Math.random() * 10;
// Lifetime
particle.life = 1.0;
particle.maxLife = 1.0;
this.container.addChild(particle);
this.particles.push(particle);
}
}
update(ticker) {
const gravity = 0.5;
const delta = ticker.deltaTime;
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
// Apply physics
p.vy += gravity * delta;
p.x += p.vx * delta;
p.y += p.vy * delta;
// Update life
p.life -= 0.02 * delta;
p.alpha = p.life / p.maxLife;
p.scale.set(p.alpha);
// Remove dead particles
if (p.life <= 0) {
this.container.removeChild(p);
p.destroy();
this.particles.splice(i, 1);
}
}
}
}
// Usage
const particleTexture = createCircleTexture(10, 0xffffff);
const emitter = new ParticleEmitter(app, particleTexture);
// Emit on click
app.stage.eventMode = 'static';
app.stage.on('pointerdown', (event) => {
emitter.emit(event.global.x, event.global.y, 50);
});
function createCircleTexture(radius, color) {
const graphics = new PIXI.Graphics();
graphics.circle(radius, radius, radius).fill(color);
return app.renderer.generateTexture(graphics);
}
```
### 2. Fire Effect
Realistic fire particle effect:
```javascript
class FireEmitter {
constructor(app, x, y) {
this.app = app;
this.x = x;
this.y = y;
this.particles = [];
this.container = new PIXI.ParticleContainer(2000, {
position: true,
scale: true,
tint: true,
alpha: true
});
app.stage.addChild(this.container);
// Create particle texture
const graphics = new PIXI.Graphics();
graphics.circle(8, 8, 8).fill(0xffffff);
this.texture = app.renderer.generateTexture(graphics);
// Emit continuously
app.ticker.add((ticker) => this.update(ticker));
}
update(ticker) {
const delta = ticker.deltaTime;
// Emit new particles
for (let i = 0; i < 5; i++) {
this.emitParticle();
}
// Update existing particles
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
// Rise upward with slight horizontal drift
p.y += p.vy * delta;
p.x += p.vx * delta;
// Update life
p.life -= 0.02 * delta;
// Color transition: yellow -> orange -> red -> transparent
const t = 1 - p.life;
if (t < 0.33) {
// Yellow to orange
const localT = t / 0.33;
p.tint = this.colorLerp(0xffff00, 0xff8800, localT);
} else if (t < 0.66) {
// Orange to red
const localT = (t - 0.33) / 0.33;
p.tint = this.colorLerp(0xff8800, 0xff0000, localT);
} else {
// Red to dark red
const localT = (t - 0.66) / 0.34;
p.tint = this.colorLerp(0xff0000, 0x880000, localT);
}
p.alpha = p.life;
p.scale.set(p.life * 1.5);
// Remove dead particles
if (p.life <= 0) {
this.container.removeChild(p);
p.destroy();
this.particles.splice(i, 1);
}
}
}
emitParticle() {
const particle = new PIXI.Sprite(this.texture);
particle.anchor.set(0.5);
particle.position.set(
this.x + (Math.random() - 0.5) * 20,
this.y
);
particle.vx = (Math.random() - 0.5) * 2;
particle.vy = -2 - Math.random() * 3;
particle.life = 1.0;
this.container.addChild(particle);
this.particles.push(particle);
}
colorLerp(color1, color2, t) {
const r1 = (color1 >> 16) & 0xff;
const g1 = (color1 >> 8) & 0xff;
const b1 = color1 & 0xff;
const r2 = (color2 >> 16) & 0xff;
const g2 = (color2 >> 8) & 0xff;
const b2 = color2 & 0xff;
const r = Math.floor(r1 + (r2 - r1) * t);
const g = Math.floor(g1 + (g2 - g1) * t);
const b = Math.floor(b1 + (b2 - b1) * t);
return (r << 16) | (g << 8) | b;
}
}
// Usage
const fireEmitter = new FireEmitter(app, 400, 500);
```
### 3. Trail Effect
Create motion trails behind moving objects:
```javascript
class TrailEffect {
constructor(app, target, options = {}) {
this.app = app;
this.target = target;
this.trailSegments = [];
this.maxSegments = options.maxSegments || 20;
this.segmentLife = options.segmentLife || 0.5;
this.container = new PIXI.Container();
app.stage.addChildAt(this.container, 0); // Behind target
app.ticker.add((ticker) => this.update(ticker));
}
update(ticker) {
const delta = ticker.deltaTime;
// Create new segment at target position
const segment = new PIXI.Graphics();
segment.circle(0, 0, this.target.width / 2).fill(this.target.tint);
segment.position.set(this.target.x, this.target.y);
segment.life = this.segmentLife;
segment.maxLife = this.segmentLife;
this.container.addChild(segment);
this.trailSegments.push(segment);
// Remove old segments
if (this.trailSegments.length > this.maxSegments) {
const old = this.trailSegments.shift();
this.container.removeChild(old);
old.destroy();
}
// Update existing segments
for (const seg of this.trailSegments) {
seg.life -= 0.016 * delta;
seg.alpha = seg.life / seg.maxLife;
}
}
destroy() {
for (const seg of this.trailSegments) {
seg.destroy();
}
this.container.destroy();
}
}
// Usage
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(400, 300);
app.stage.addChild(sprite);
const trail = new TrailEffect(app, sprite, {
maxSegments: 30,
segmentLife: 0.8
});
// Move sprite with mouse
app.stage.eventMode = 'static';
app.stage.on('pointermove', (event) => {
sprite.position.set(event.global.x, event.global.y);
});
```
### 4. Explosion Effect
One-shot explosion particle effect:
```javascript
class Explosion {
constructor(app, x, y, particleCount = 50) {
this.app = app;
this.particles = [];
const graphics = new PIXI.Graphics();
graphics.circle(5, 5, 5).fill(0xffffff);
const texture = app.renderer.generateTexture(graphics);
const container = new PIXI.ParticleContainer(particleCount, {
position: true,
rotation: true,
scale: true,
tint: true,
alpha: true
});
app.stage.addChild(container);
// Create particles
for (let i = 0; i < particleCount; i++) {
const angle = (Math.PI * 2 * i) / particleCount;
const speed = 5 + Math.random() * 5;
const particle = new PIXI.Sprite(texture);
particle.anchor.set(0.5);
particle.position.set(x, y);
particle.vx = Math.cos(angle) * speed;
particle.vy = Math.sin(angle) * speed;
particle.life = 1.0;
particle.tint = Math.random() > 0.5 ? 0xff8800 : 0xffff00;
container.addChild(particle);
this.particles.push(particle);
}
// Update and cleanup
const ticker = (delta) => {
let allDead = true;
for (const p of this.particles) {
p.x += p.vx * delta.deltaTime;
p.y += p.vy * delta.deltaTime;
p.vy += 0.3 * delta.deltaTime; // Gravity
p.life -= 0.02 * delta.deltaTime;
p.alpha = p.life;
p.scale.set(p.life);
if (p.life > 0) allDead = false;
}
if (allDead) {
app.ticker.remove(ticker);
container.destroy({ children: true });
}
};
app.ticker.add(ticker);
}
}
// Usage - trigger on click
app.stage.eventMode = 'static';
app.stage.on('pointerdown', (event) => {
new Explosion(app, event.global.x, event.global.y, 100);
});
```
---
## Filters and Effects
### 1. Glow Effect
Add glowing outline to sprites:
```javascript
import { BlurFilter, ColorMatrixFilter } from 'pixi.js';
function addGlowEffect(sprite, options = {}) {
const {
glowColor = [1, 1, 0], // Yellow
glowStrength = 2,
blurStrength = 10
} = options;
// Create blur filter
const blurFilter = new BlurFilter();
blurFilter.strength = blurStrength;
// Create color filter for glow
const colorFilter = new ColorMatrixFilter();
colorFilter.brightness(glowStrength, false);
// Apply filters
sprite.filters = [blurFilter, colorFilter];
return { blurFilter, colorFilter };
}
// Usage
const sprite = new PIXI.Sprite(texture);
sprite.position.set(400, 300);
app.stage.addChild(sprite);
const glow = addGlowEffect(sprite, {
glowColor: [0, 1, 1], // Cyan
glowStrength: 3,
blurStrength: 15
});
// Animate glow intensity
let time = 0;
app.ticker.add((ticker) => {
time += 0.05 * ticker.deltaTime;
const intensity = Math.sin(time) * 0.5 + 1.5;
glow.blurFilter.strength = 10 + intensity * 5;
});
```
### 2. Chromatic Aberration
RGB color separation effect:
```javascript
import { Filter, GlProgram } from 'pixi.js';
class ChromaticAberrationFilter extends Filter {
constructor(offset = 5) {
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform vec2 uOffset;
out vec4 finalColor;
void main() {
vec2 coord = vTextureCoord;
float r = texture(uTexture, coord + uOffset).r;
float g = texture(uTexture, coord).g;
float b = texture(uTexture, coord - uOffset).b;
float a = texture(uTexture, coord).a;
finalColor = vec4(r, g, b, a);
}
`;
super({
glProgram: new GlProgram({
fragment,
vertex: GlProgram.defaultVertexSrc
}),
resources: {
chromaUniforms: {
uOffset: {
value: new Float32Array([offset / 800, 0]),
type: 'vec2<f32>'
}
}
}
});
}
set offset(value) {
this.resources.chromaUniforms.uniforms.uOffset[0] = value / 800;
}
}
// Usage
const sprite = new PIXI.Sprite(texture);
sprite.position.set(400, 300);
app.stage.addChild(sprite);
const chromaFilter = new ChromaticAberrationFilter(10);
sprite.filters = [chromaFilter];
// Animate offset
let time = 0;
app.ticker.add((ticker) => {
time += 0.1 * ticker.deltaTime;
chromaFilter.offset = Math.sin(time) * 20;
});
```
### 3. CRT Monitor Effect
Old-school CRT screen effect with scanlines and curvature:
```javascript
class CRTFilter extends Filter {
constructor() {
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uTime;
out vec4 finalColor;
void main() {
vec2 uv = vTextureCoord;
// Screen curvature
vec2 dc = uv - 0.5;
float dist = dot(dc, dc);
uv = uv + dc * dist * 0.1;
// Scanlines
float scanline = sin(uv.y * 800.0) * 0.04;
// Vignette
float vignette = 1.0 - dist * 1.5;
// Color aberration on edges
float r = texture(uTexture, uv + vec2(0.001, 0.0)).r;
float g = texture(uTexture, uv).g;
float b = texture(uTexture, uv - vec2(0.001, 0.0)).b;
vec3 color = vec3(r, g, b);
color -= scanline;
color *= vignette;
// Flicker
color *= 0.95 + 0.05 * sin(uTime * 100.0);
finalColor = vec4(color, 1.0);
}
`;
super({
glProgram: new GlProgram({
fragment,
vertex: GlProgram.defaultVertexSrc
}),
resources: {
crtUniforms: {
uTime: { value: 0, type: 'f32' }
}
}
});
this.time = 0;
}
update(deltaTime) {
this.time += deltaTime * 0.016;
this.resources.crtUniforms.uniforms.uTime = this.time;
}
}
// Usage
const crtFilter = new CRTFilter();
app.stage.filters = [crtFilter];
app.ticker.add((ticker) => {
crtFilter.update(ticker.deltaTime);
});
```
### 4. Displacement Map Effect
Create water ripple or distortion effects:
```javascript
import { DisplacementFilter } from 'pixi.js';
(async () => {
// Create displacement sprite
const displacementSprite = PIXI.Sprite.from('displacement_map.png');
displacementSprite.texture.baseTexture.wrapMode = PIXI.WRAP_MODES.REPEAT;
// Create displacement filter
const displacementFilter = new DisplacementFilter({
sprite: displacementSprite,
scale: 50
});
// Apply to stage or specific sprite
app.stage.filters = [displacementFilter];
// Animate displacement
app.ticker.add((ticker) => {
displacementSprite.x += 1 * ticker.deltaTime;
displacementSprite.y += 0.5 * ticker.deltaTime;
});
})();
// Generate displacement map programmatically
function createDisplacementMap(size = 512) {
const graphics = new PIXI.Graphics();
for (let i = 0; i < 50; i++) {
const x = Math.random() * size;
const y = Math.random() * size;
const radius = Math.random() * 50 + 20;
graphics.circle(x, y, radius)
.fill({ color: 0xffffff, alpha: Math.random() * 0.5 });
}
const texture = app.renderer.generateTexture(graphics, {
resolution: 1,
multisample: PIXI.MSAA_QUALITY.NONE
});
return PIXI.Sprite.from(texture);
}
```
---
## Custom Shaders
### 1. Wave Distortion Shader
Create animated wave distortion:
```javascript
import { Filter, GlProgram } from 'pixi.js';
class WaveFilter extends Filter {
constructor() {
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uTime;
uniform float uAmplitude;
uniform float uFrequency;
out vec4 finalColor;
void main() {
vec2 coord = vTextureCoord;
// Apply wave distortion
coord.x += sin(coord.y * uFrequency + uTime) * uAmplitude;
coord.y += cos(coord.x * uFrequency + uTime) * uAmplitude;
finalColor = texture(uTexture, coord);
}
`;
super({
glProgram: new GlProgram({
fragment,
vertex: GlProgram.defaultVertexSrc
}),
resources: {
waveUniforms: {
uTime: { value: 0, type: 'f32' },
uAmplitude: { value: 0.01, type: 'f32' },
uFrequency: { value: 10.0, type: 'f32' }
}
}
});
}
set time(value) {
this.resources.waveUniforms.uniforms.uTime = value;
}
set amplitude(value) {
this.resources.waveUniforms.uniforms.uAmplitude = value;
}
set frequency(value) {
this.resources.waveUniforms.uniforms.uFrequency = value;
}
}
// Usage
const sprite = new PIXI.Sprite(texture);
sprite.position.set(400, 300);
app.stage.addChild(sprite);
const waveFilter = new WaveFilter();
sprite.filters = [waveFilter];
let time = 0;
app.ticker.add((ticker) => {
time += 0.05 * ticker.deltaTime;
waveFilter.time = time;
});
```
### 2. Pixelate Shader
Dynamic pixelation effect:
```javascript
class PixelateFilter extends Filter {
constructor(pixelSize = 10) {
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform vec2 uSize;
uniform float uPixelSize;
out vec4 finalColor;
void main() {
vec2 coord = vTextureCoord * uSize;
vec2 pixelCoord = floor(coord / uPixelSize) * uPixelSize;
vec2 pixelUV = pixelCoord / uSize;
finalColor = texture(uTexture, pixelUV);
}
`;
super({
glProgram: new GlProgram({
fragment,
vertex: GlProgram.defaultVertexSrc
}),
resources: {
pixelateUniforms: {
uSize: {
value: new Float32Array([800, 600]),
type: 'vec2<f32>'
},
uPixelSize: { value: pixelSize, type: 'f32' }
}
}
});
}
set pixelSize(value) {
this.resources.pixelateUniforms.uniforms.uPixelSize = value;
}
setSize(width, height) {
this.resources.pixelateUniforms.uniforms.uSize[0] = width;
this.resources.pixelateUniforms.uniforms.uSize[1] = height;
}
}
// Usage with animated pixelation
const pixelateFilter = new PixelateFilter(1);
app.stage.filters = [pixelateFilter];
let pixelSize = 1;
let direction = 1;
app.ticker.add(() => {
pixelSize += direction * 0.5;
if (pixelSize >= 20) direction = -1;
if (pixelSize <= 1) direction = 1;
pixelateFilter.pixelSize = pixelSize;
});
```
### 3. Color Grading Shader
Professional color grading with adjustable parameters:
```javascript
class ColorGradingFilter extends Filter {
constructor(options = {}) {
const {
brightness = 0,
contrast = 0,
saturation = 0,
temperature = 0
} = options;
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uBrightness;
uniform float uContrast;
uniform float uSaturation;
uniform float uTemperature;
out vec4 finalColor;
void main() {
vec4 color = texture(uTexture, vTextureCoord);
// Brightness
color.rgb += uBrightness;
// Contrast
color.rgb = (color.rgb - 0.5) * (1.0 + uContrast) + 0.5;
// Saturation
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(vec3(gray), color.rgb, 1.0 + uSaturation);
// Temperature (blue/orange tint)
color.r += uTemperature * 0.1;
color.b -= uTemperature * 0.1;
finalColor = color;
}
`;
super({
glProgram: new GlProgram({
fragment,
vertex: GlProgram.defaultVertexSrc
}),
resources: {
colorGradingUniforms: {
uBrightness: { value: brightness, type: 'f32' },
uContrast: { value: contrast, type: 'f32' },
uSaturation: { value: saturation, type: 'f32' },
uTemperature: { value: temperature, type: 'f32' }
}
}
});
}
set brightness(value) {
this.resources.colorGradingUniforms.uniforms.uBrightness = value;
}
set contrast(value) {
this.resources.colorGradingUniforms.uniforms.uContrast = value;
}
set saturation(value) {
this.resources.colorGradingUniforms.uniforms.uSaturation = value;
}
set temperature(value) {
this.resources.colorGradingUniforms.uniforms.uTemperature = value;
}
}
// Usage with UI controls
const colorFilter = new ColorGradingFilter({
brightness: 0.1,
contrast: 0.2,
saturation: 0.3,
temperature: 0.5
});
app.stage.filters = [colorFilter];
// Interactive controls
document.getElementById('brightness').addEventListener('input', (e) => {
colorFilter.brightness = parseFloat(e.target.value);
});
document.getElementById('contrast').addEventListener('input', (e) => {
colorFilter.contrast = parseFloat(e.target.value);
});
```
### 4. Outline Shader
Add colored outline to sprites:
```javascript
class OutlineFilter extends Filter {
constructor(thickness = 2, color = 0xffffff) {
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform vec2 uTextureSize;
uniform float uThickness;
uniform vec3 uOutlineColor;
out vec4 finalColor;
void main() {
vec2 pixelSize = 1.0 / uTextureSize;
// Sample surrounding pixels
float alpha = 0.0;
for (float x = -uThickness; x <= uThickness; x++) {
for (float y = -uThickness; y <= uThickness; y++) {
vec2 offset = vec2(x, y) * pixelSize;
alpha = max(alpha, texture(uTexture, vTextureCoord + offset).a);
}
}
vec4 original = texture(uTexture, vTextureCoord);
// If pixel is transparent but surrounded by opaque, it's an outline
if (original.a < 0.5 && alpha > 0.5) {
finalColor = vec4(uOutlineColor, 1.0);
} else {
finalColor = original;
}
}
`;
super({
glProgram: new GlProgram({
fragment,
vertex: GlProgram.defaultVertexSrc
}),
resources: {
outlineUniforms: {
uTextureSize: {
value: new Float32Array([256, 256]),
type: 'vec2<f32>'
},
uThickness: { value: thickness, type: 'f32' },
uOutlineColor: {
value: new Float32Array([
((color >> 16) & 0xff) / 255,
((color >> 8) & 0xff) / 255,
(color & 0xff) / 255
]),
type: 'vec3<f32>'
}
}
}
});
}
set thickness(value) {
this.resources.outlineUniforms.uniforms.uThickness = value;
}
set color(value) {
this.resources.outlineUniforms.uniforms.uOutlineColor[0] = ((value >> 16) & 0xff) / 255;
this.resources.outlineUniforms.uniforms.uOutlineColor[1] = ((value >> 8) & 0xff) / 255;
this.resources.outlineUniforms.uniforms.uOutlineColor[2] = (value & 0xff) / 255;
}
}
// Usage
const sprite = new PIXI.Sprite(texture);
sprite.position.set(400, 300);
app.stage.addChild(sprite);
const outlineFilter = new OutlineFilter(3, 0xffff00);
sprite.filters = [outlineFilter];
// Animate outline color
let hue = 0;
app.ticker.add(() => {
hue = (hue + 1) % 360;
outlineFilter.color = hslToHex(hue, 100, 50);
});
function hslToHex(h, s, l) {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color);
};
return (f(0) << 16) | (f(8) << 8) | f(4);
}
```
---
## Animation Patterns
### 1. Sprite Sheet Animation
Load and play sprite sheet animations:
```javascript
(async () => {
// Load sprite sheet
await PIXI.Assets.load('spritesheet.json');
// Get animation frames
const frames = [];
for (let i = 0; i < 30; i++) {
frames.push(PIXI.Texture.from(`run_${i}.png`));
}
// Create animated sprite
const animatedSprite = new PIXI.AnimatedSprite(frames);
animatedSprite.anchor.set(0.5);
animatedSprite.position.set(400, 300);
animatedSprite.animationSpeed = 0.5;
animatedSprite.play();
app.stage.addChild(animatedSprite);
// Control playback
document.getElementById('play').addEventListener('click', () => {
animatedSprite.play();
});
document.getElementById('pause').addEventListener('click', () => {
animatedSprite.stop();
});
document.getElementById('speed').addEventListener('input', (e) => {
animatedSprite.animationSpeed = parseFloat(e.target.value);
});
})();
```
### 2. Tweening Library Integration (GSAP)
Smooth animations with GSAP:
```javascript
import gsap from 'gsap';
import { PixiPlugin } from 'gsap/PixiPlugin';
// Register PIXI plugin
gsap.registerPlugin(PixiPlugin);
PixiPlugin.registerPIXI(PIXI);
// Create sprite
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(100, 300);
app.stage.addChild(sprite);
// Animate with GSAP
gsap.to(sprite, {
pixi: {
x: 700,
rotation: 360,
scale: 1.5,
tint: 0xff0000
},
duration: 2,
ease: 'elastic.out(1, 0.5)',
onComplete: () => {
console.log('Animation complete!');
}
});
// Complex timeline
const timeline = gsap.timeline({ repeat: -1, yoyo: true });
timeline
.to(sprite, { pixi: { y: 100 }, duration: 1, ease: 'power2.out' })
.to(sprite, { pixi: { x: 700 }, duration: 1, ease: 'power2.inOut' })
.to(sprite, { pixi: { y: 500 }, duration: 1, ease: 'power2.in' })
.to(sprite, { pixi: { x: 100 }, duration: 1, ease: 'power2.inOut' });
```
### 3. Custom Easing Functions
Implement custom easing without external libraries:
```javascript
class Tween {
constructor(target, to, duration, easing = 'linear') {
this.target = target;
this.from = {};
this.to = to;
this.duration = duration;
this.elapsed = 0;
this.easing = this.easingFunctions[easing] || this.easingFunctions.linear;
this.complete = false;
this.onCompleteCallback = null;
// Store starting values
for (const key in to) {
this.from[key] = target[key];
}
}
easingFunctions = {
linear: t => t,
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
elastic: t => {
if (t === 0 || t === 1) return t;
const p = 0.3;
return -Math.pow(2, 10 * (t - 1)) * Math.sin(((t - 1 - p / 4) * (2 * Math.PI)) / p);
},
bounce: t => {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75;
} else if (t < 2.5 / 2.75) {
return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375;
} else {
return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375;
}
}
};
update(deltaTime) {
if (this.complete) return;
this.elapsed += deltaTime;
const progress = Math.min(this.elapsed / this.duration, 1);
const easedProgress = this.easing(progress);
// Update target properties
for (const key in this.to) {
const from = this.from[key];
const to = this.to[key];
this.target[key] = from + (to - from) * easedProgress;
}
// Check completion
if (progress >= 1) {
this.complete = true;
if (this.onCompleteCallback) {
this.onCompleteCallback();
}
}
}
onComplete(callback) {
this.onCompleteCallback = callback;
return this;
}
}
// Tween manager
class TweenManager {
constructor(app) {
this.app = app;
this.tweens = [];
app.ticker.add((ticker) => this.update(ticker.deltaTime));
}
to(target, to, duration, easing) {
const tween = new Tween(target, to, duration, easing);
this.tweens.push(tween);
return tween;
}
update(deltaTime) {
for (let i = this.tweens.length - 1; i >= 0; i--) {
const tween = this.tweens[i];
tween.update(deltaTime * 0.016); // Convert to seconds
if (tween.complete) {
this.tweens.splice(i, 1);
}
}
}
}
// Usage
const tweenManager = new TweenManager(app);
const sprite = new PIXI.Sprite(texture);
sprite.position.set(100, 300);
app.stage.addChild(sprite);
tweenManager
.to(sprite.position, { x: 700 }, 2, 'elastic')
.onComplete(() => {
console.log('Move complete!');
tweenManager
.to(sprite, { rotation: Math.PI * 2 }, 1, 'easeOutCubic')
.onComplete(() => {
console.log('Rotation complete!');
});
});
```
### 4. Path Following Animation
Make sprites follow curved paths:
```javascript
class PathFollower {
constructor(sprite, path, duration) {
this.sprite = sprite;
this.path = path; // Array of {x, y} points
this.duration = duration;
this.elapsed = 0;
}
update(deltaTime) {
this.elapsed += deltaTime;
const progress = Math.min(this.elapsed / this.duration, 1);
// Get position along path
const position = this.getPositionAtProgress(progress);
this.sprite.position.set(position.x, position.y);
// Get rotation based on direction
if (progress < 1) {
const futureProgress = Math.min(progress + 0.01, 1);
const futurePosition = this.getPositionAtProgress(futureProgress);
const angle = Math.atan2(
futurePosition.y - position.y,
futurePosition.x - position.x
);
this.sprite.rotation = angle;
}
return progress >= 1;
}
getPositionAtProgress(t) {
// Catmull-Rom spline interpolation
const points = this.path;
const segmentCount = points.length - 1;
const segment = Math.floor(t * segmentCount);
const localT = (t * segmentCount) - segment;
const p0 = points[Math.max(0, segment - 1)];
const p1 = points[segment];
const p2 = points[Math.min(points.length - 1, segment + 1)];
const p3 = points[Math.min(points.length - 1, segment + 2)];
const t2 = localT * localT;
const t3 = t2 * localT;
const x = 0.5 * (
(2 * p1.x) +
(-p0.x + p2.x) * localT +
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3
);
const y = 0.5 * (
(2 * p1.y) +
(-p0.y + p2.y) * localT +
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3
);
return { x, y };
}
}
// Usage
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
app.stage.addChild(sprite);
const path = [
{ x: 100, y: 300 },
{ x: 300, y: 100 },
{ x: 500, y: 300 },
{ x: 700, y: 500 },
{ x: 400, y: 550 }
];
const pathFollower = new PathFollower(sprite, path, 5000);
app.ticker.add((ticker) => {
const complete = pathFollower.update(ticker.deltaTime * 16);
if (complete) {
console.log('Path complete!');
}
});
// Visualize path
const graphics = new PIXI.Graphics();
graphics.moveTo(path[0].x, path[0].y);
for (let i = 1; i < path.length; i++) {
graphics.lineTo(path[i].x, path[i].y);
}
graphics.stroke({ width: 2, color: 0xffffff, alpha: 0.3 });
app.stage.addChild(graphics);
```
---
## Performance Optimization
### 1. Object Pooling System
Reuse objects to avoid garbage collection:
```javascript
class ObjectPool {
constructor(createFunc, resetFunc, initialSize = 100) {
this.createFunc = createFunc;
this.resetFunc = resetFunc;
this.available = [];
this.active = new Set();
// Pre-create objects
for (let i = 0; i < initialSize; i++) {
this.available.push(this.createFunc());
}
}
acquire() {
let obj;
if (this.available.length > 0) {
obj = this.available.pop();
} else {
obj = this.createFunc();
console.warn('Pool exhausted, creating new object');
}
this.active.add(obj);
return obj;
}
release(obj) {
if (this.active.has(obj)) {
this.active.delete(obj);
this.resetFunc(obj);
this.available.push(obj);
}
}
releaseAll() {
for (const obj of this.active) {
this.resetFunc(obj);
this.available.push(obj);
}
this.active.clear();
}
getStats() {
return {
available: this.available.length,
active: this.active.size,
total: this.available.length + this.active.size
};
}
}
// Usage
const texture = await PIXI.Assets.load('bullet.png');
const bulletPool = new ObjectPool(
// Create function
() => {
const bullet = new PIXI.Sprite(texture);
bullet.anchor.set(0.5);
return bullet;
},
// Reset function
(bullet) => {
bullet.visible = false;
bullet.position.set(0, 0);
bullet.rotation = 0;
},
50 // Initial size
);
// Shoot bullet
function shootBullet(x, y, angle) {
const bullet = bulletPool.acquire();
bullet.position.set(x, y);
bullet.rotation = angle;
bullet.visible = true;
bullet.vx = Math.cos(angle) * 5;
bullet.vy = Math.sin(angle) * 5;
app.stage.addChild(bullet);
}
// Update bullets
app.ticker.add((ticker) => {
for (const bullet of bulletPool.active) {
bullet.x += bullet.vx * ticker.deltaTime;
bullet.y += bullet.vy * ticker.deltaTime;
// Release off-screen bullets
if (bullet.x < 0 || bullet.x > 800 || bullet.y < 0 || bullet.y > 600) {
app.stage.removeChild(bullet);
bulletPool.release(bullet);
}
}
});
// Monitor pool
setInterval(() => {
console.log('Bullet pool:', bulletPool.getStats());
}, 1000);
```
### 2. Viewport Culling
Only render visible sprites:
```javascript
class ViewportCuller {
constructor(app, margin = 100) {
this.app = app;
this.margin = margin;
this.trackedSprites = new Set();
}
track(sprite) {
this.trackedSprites.add(sprite);
}
untrack(sprite) {
this.trackedSprites.delete(sprite);
}
update() {
const bounds = {
left: -this.margin,
right: this.app.screen.width + this.margin,
top: -this.margin,
bottom: this.app.screen.height + this.margin
};
for (const sprite of this.trackedSprites) {
const spriteBounds = sprite.getBounds();
const visible = (
spriteBounds.x + spriteBounds.width >= bounds.left &&
spriteBounds.x <= bounds.right &&
spriteBounds.y + spriteBounds.height >= bounds.top &&
spriteBounds.y <= bounds.bottom
);
sprite.renderable = visible;
}
}
}
// Usage
const culler = new ViewportCuller(app, 50);
// Create many sprites
for (let i = 0; i < 1000; i++) {
const sprite = new PIXI.Sprite(texture);
sprite.x = Math.random() * 2000 - 500;
sprite.y = Math.random() * 2000 - 500;
app.stage.addChild(sprite);
culler.track(sprite);
}
app.ticker.add(() => {
culler.update();
});
// Camera/viewport movement
let cameraX = 0;
let cameraY = 0;
window.addEventListener('keydown', (e) => {
const speed = 10;
switch (e.key) {
case 'ArrowLeft': cameraX += speed; break;
case 'ArrowRight': cameraX -= speed; break;
case 'ArrowUp': cameraY += speed; break;
case 'ArrowDown': cameraY -= speed; break;
}
app.stage.position.set(cameraX, cameraY);
});
```
### 3. Texture Atlases
Combine multiple textures into one for reduced draw calls:
```javascript
// Generate texture atlas programmatically
class TextureAtlas {
constructor(app, textures, padding = 2) {
this.app = app;
this.textures = textures;
this.padding = padding;
this.atlas = null;
this.frames = {};
this.generate();
}
generate() {
// Calculate atlas size using bin packing
const rects = Object.entries(this.textures).map(([name, texture]) => ({
name,
width: texture.width + this.padding * 2,
height: texture.height + this.padding * 2,
texture
}));
// Sort by height (descending) for better packing
rects.sort((a, b) => b.height - a.height);
// Simple shelf packing algorithm
let atlasWidth = 0;
let atlasHeight = 0;
let shelfY = 0;
let shelfX = 0;
let shelfHeight = 0;
const packed = [];
for (const rect of rects) {
// Check if we need a new shelf
if (shelfX + rect.width > 2048) { // Max texture size
shelfY += shelfHeight;
shelfX = 0;
shelfHeight = 0;
}
rect.x = shelfX + this.padding;
rect.y = shelfY + this.padding;
shelfX += rect.width;
shelfHeight = Math.max(shelfHeight, rect.height);
atlasWidth = Math.max(atlasWidth, shelfX);
atlasHeight = Math.max(atlasHeight, shelfY + shelfHeight);
packed.push(rect);
}
// Round up to power of 2
atlasWidth = Math.pow(2, Math.ceil(Math.log2(atlasWidth)));
atlasHeight = Math.pow(2, Math.ceil(Math.log2(atlasHeight)));
// Create canvas for atlas
const canvas = document.createElement('canvas');
canvas.width = atlasWidth;
canvas.height = atlasHeight;
const ctx = canvas.getContext('2d');
// Draw all textures to canvas
for (const rect of packed) {
const source = rect.texture.source;
ctx.drawImage(
source.resource,
rect.x,
rect.y,
rect.texture.width,
rect.texture.height
);
// Store frame data
this.frames[rect.name] = {
x: rect.x,
y: rect.y,
width: rect.texture.width,
height: rect.texture.height
};
}
// Create PixiJS texture from canvas
this.atlas = PIXI.Texture.from(canvas);
}
getFrame(name) {
const frame = this.frames[name];
if (!frame) return null;
return new PIXI.Texture({
source: this.atlas.source,
frame: new PIXI.Rectangle(frame.x, frame.y, frame.width, frame.height)
});
}
}
// Usage
const textures = {
'player': await PIXI.Assets.load('player.png'),
'enemy': await PIXI.Assets.load('enemy.png'),
'bullet': await PIXI.Assets.load('bullet.png'),
'powerup': await PIXI.Assets.load('powerup.png')
};
const atlas = new TextureAtlas(app, textures);
// Use frames from atlas
const player = new PIXI.Sprite(atlas.getFrame('player'));
const enemy = new PIXI.Sprite(atlas.getFrame('enemy'));
// All sprites now share one texture = one draw call!
```
### 4. Spatial Hashing for Collisions
Optimize collision detection from O(n²) to O(n):
```javascript
class SpatialHash {
constructor(cellSize = 100) {
this.cellSize = cellSize;
this.cells = new Map();
}
clear() {
this.cells.clear();
}
insert(obj) {
const bounds = obj.getBounds();
const cells = this.getCellsForBounds(bounds);
for (const cellKey of cells) {
if (!this.cells.has(cellKey)) {
this.cells.set(cellKey, new Set());
}
this.cells.get(cellKey).add(obj);
}
}
getCellsForBounds(bounds) {
const cells = [];
const minX = Math.floor(bounds.x / this.cellSize);
const maxX = Math.floor((bounds.x + bounds.width) / this.cellSize);
const minY = Math.floor(bounds.y / this.cellSize);
const maxY = Math.floor((bounds.y + bounds.height) / this.cellSize);
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
cells.push(`${x},${y}`);
}
}
return cells;
}
getNearby(obj) {
const bounds = obj.getBounds();
const cells = this.getCellsForBounds(bounds);
const nearby = new Set();
for (const cellKey of cells) {
const cell = this.cells.get(cellKey);
if (cell) {
for (const other of cell) {
if (other !== obj) {
nearby.add(other);
}
}
}
}
return nearby;
}
}
// Usage
const spatialHash = new SpatialHash(100);
const sprites = [];
// Create many sprites
for (let i = 0; i < 500; i++) {
const sprite = new PIXI.Sprite(texture);
sprite.position.set(
Math.random() * 800,
Math.random() * 600
);
sprite.vx = (Math.random() - 0.5) * 3;
sprite.vy = (Math.random() - 0.5) * 3;
app.stage.addChild(sprite);
sprites.push(sprite);
}
app.ticker.add((ticker) => {
// Update positions
for (const sprite of sprites) {
sprite.x += sprite.vx * ticker.deltaTime;
sprite.y += sprite.vy * ticker.deltaTime;
// Bounce off edges
if (sprite.x < 0 || sprite.x > 800) sprite.vx *= -1;
if (sprite.y < 0 || sprite.y > 600) sprite.vy *= -1;
}
// Rebuild spatial hash
spatialHash.clear();
for (const sprite of sprites) {
spatialHash.insert(sprite);
}
// Check collisions (only nearby sprites)
for (const sprite of sprites) {
const nearby = spatialHash.getNearby(sprite);
for (const other of nearby) {
if (checkCollision(sprite, other)) {
// Handle collision
sprite.tint = 0xff0000;
other.tint = 0xff0000;
} else {
sprite.tint = 0xffffff;
}
}
}
});
function checkCollision(a, b) {
const ab = a.getBounds();
const bb = b.getBounds();
return ab.x < bb.x + bb.width &&
ab.x + ab.width > bb.x &&
ab.y < bb.y + bb.height &&
ab.y + ab.height > bb.y;
}
```
---
## Game Development
### 1. Simple Platformer Physics
Basic 2D platformer with jumping and gravity:
```javascript
class Player {
constructor(app, x, y) {
this.app = app;
// Create sprite
const graphics = new PIXI.Graphics();
graphics.rect(0, 0, 32, 48).fill(0x3498db);
const texture = app.renderer.generateTexture(graphics);
this.sprite = new PIXI.Sprite(texture);
this.sprite.position.set(x, y);
app.stage.addChild(this.sprite);
// Physics
this.vx = 0;
this.vy = 0;
this.isGrounded = false;
this.gravity = 0.5;
this.jumpPower = -12;
this.moveSpeed = 5;
// Input
this.keys = {};
this.setupInput();
}
setupInput() {
window.addEventListener('keydown', (e) => {
this.keys[e.key] = true;
if (e.key === ' ' && this.isGrounded) {
this.vy = this.jumpPower;
this.isGrounded = false;
}
});
window.addEventListener('keyup', (e) => {
this.keys[e.key] = false;
});
}
update(platforms) {
// Horizontal movement
this.vx = 0;
if (this.keys['ArrowLeft']) this.vx = -this.moveSpeed;
if (this.keys['ArrowRight']) this.vx = this.moveSpeed;
// Apply gravity
if (!this.isGrounded) {
this.vy += this.gravity;
}
// Update position
this.sprite.x += this.vx;
this.sprite.y += this.vy;
// Check platform collisions
this.isGrounded = false;
for (const platform of platforms) {
if (this.checkCollision(platform)) {
// Resolve collision
const playerBottom = this.sprite.y + this.sprite.height;
const platformTop = platform.y;
if (this.vy > 0 && playerBottom <= platformTop + this.vy) {
// Landing on platform
this.sprite.y = platformTop - this.sprite.height;
this.vy = 0;
this.isGrounded = true;
}
}
}
// Keep in bounds
if (this.sprite.x < 0) this.sprite.x = 0;
if (this.sprite.x > 800 - this.sprite.width) {
this.sprite.x = 800 - this.sprite.width;
}
}
checkCollision(platform) {
return this.sprite.x < platform.x + platform.width &&
this.sprite.x + this.sprite.width > platform.x &&
this.sprite.y < platform.y + platform.height &&
this.sprite.y + this.sprite.height > platform.y;
}
}
// Usage
const player = new Player(app, 100, 100);
// Create platforms
const platforms = [
{ x: 0, y: 550, width: 800, height: 50 }, // Ground
{ x: 200, y: 450, width: 150, height: 20 },
{ x: 450, y: 350, width: 150, height: 20 },
{ x: 100, y: 250, width: 150, height: 20 }
];
// Draw platforms
for (const platform of platforms) {
const graphics = new PIXI.Graphics();
graphics.rect(platform.x, platform.y, platform.width, platform.height)
.fill(0x2ecc71);
app.stage.addChild(graphics);
}
// Game loop
app.ticker.add(() => {
player.update(platforms);
});
```
### 2. Top-Down Movement
8-direction movement with collision:
```javascript
class TopDownCharacter {
constructor(app, x, y) {
this.app = app;
const graphics = new PIXI.Graphics();
graphics.circle(16, 16, 16).fill(0xe74c3c);
const texture = app.renderer.generateTexture(graphics);
this.sprite = new PIXI.Sprite(texture);
this.sprite.anchor.set(0.5);
this.sprite.position.set(x, y);
app.stage.addChild(this.sprite);
this.speed = 3;
this.keys = {};
window.addEventListener('keydown', (e) => this.keys[e.key] = true);
window.addEventListener('keyup', (e) => this.keys[e.key] = false);
}
update(obstacles = []) {
let dx = 0;
let dy = 0;
if (this.keys['w'] || this.keys['ArrowUp']) dy -= this.speed;
if (this.keys['s'] || this.keys['ArrowDown']) dy += this.speed;
if (this.keys['a'] || this.keys['ArrowLeft']) dx -= this.speed;
if (this.keys['d'] || this.keys['ArrowRight']) dx += this.speed;
// Normalize diagonal movement
if (dx !== 0 && dy !== 0) {
dx *= 0.707; // 1/√2
dy *= 0.707;
}
// Try horizontal movement
this.sprite.x += dx;
if (this.checkCollisions(obstacles)) {
this.sprite.x -= dx;
}
// Try vertical movement
this.sprite.y += dy;
if (this.checkCollisions(obstacles)) {
this.sprite.y -= dy;
}
// Face movement direction
if (dx !== 0 || dy !== 0) {
this.sprite.rotation = Math.atan2(dy, dx);
}
}
checkCollisions(obstacles) {
for (const obstacle of obstacles) {
const bounds = this.sprite.getBounds();
if (bounds.x < obstacle.x + obstacle.width &&
bounds.x + bounds.width > obstacle.x &&
bounds.y < obstacle.y + obstacle.height &&
bounds.y + bounds.height > obstacle.y) {
return true;
}
}
return false;
}
}
// Usage
const character = new TopDownCharacter(app, 400, 300);
const obstacles = [
{ x: 200, y: 200, width: 100, height: 100 },
{ x: 500, y: 300, width: 80, height: 120 }
];
// Draw obstacles
for (const obstacle of obstacles) {
const graphics = new PIXI.Graphics();
graphics.rect(obstacle.x, obstacle.y, obstacle.width, obstacle.height)
.fill(0x95a5a6);
app.stage.addChild(graphics);
}
app.ticker.add(() => {
character.update(obstacles);
});
```
### 3. Health Bar Component
Reusable health bar UI:
```javascript
class HealthBar extends PIXI.Container {
constructor(maxHealth = 100, width = 100, height = 10) {
super();
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
this.barWidth = width;
this.barHeight = height;
// Background (red)
this.background = new PIXI.Graphics();
this.background.rect(0, 0, width, height).fill(0xff0000);
this.addChild(this.background);
// Foreground (green)
this.foreground = new PIXI.Graphics();
this.foreground.rect(0, 0, width, height).fill(0x00ff00);
this.addChild(this.foreground);
// Border
this.border = new PIXI.Graphics();
this.border.rect(0, 0, width, height)
.stroke({ width: 2, color: 0x000000 });
this.addChild(this.border);
}
setHealth(value) {
this.currentHealth = Math.max(0, Math.min(value, this.maxHealth));
this.updateBar();
}
damage(amount) {
this.setHealth(this.currentHealth - amount);
}
heal(amount) {
this.setHealth(this.currentHealth + amount);
}
updateBar() {
const percentage = this.currentHealth / this.maxHealth;
const newWidth = this.barWidth * percentage;
// Animate width
this.foreground.clear();
this.foreground.rect(0, 0, newWidth, this.barHeight).fill(this.getHealthColor());
}
getHealthColor() {
const percentage = this.currentHealth / this.maxHealth;
if (percentage > 0.5) return 0x00ff00; // Green
if (percentage > 0.25) return 0xffff00; // Yellow
return 0xff0000; // Red
}
}
// Usage
const enemy = new PIXI.Sprite(texture);
enemy.position.set(400, 300);
app.stage.addChild(enemy);
const healthBar = new HealthBar(100, 80, 8);
healthBar.position.set(enemy.x - 40, enemy.y - 50);
app.stage.addChild(healthBar);
// Damage over time
setInterval(() => {
healthBar.damage(10);
if (healthBar.currentHealth === 0) {
console.log('Enemy defeated!');
}
}, 1000);
```
### 4. State Machine for Game Logic
Manage game states (menu, playing, paused, game over):
```javascript
class StateMachine {
constructor() {
this.states = new Map();
this.currentState = null;
}
addState(name, state) {
this.states.set(name, state);
}
setState(name) {
if (this.currentState) {
this.currentState.exit?.();
}
const newState = this.states.get(name);
if (newState) {
this.currentState = newState;
newState.enter?.();
}
}
update(deltaTime) {
this.currentState?.update?.(deltaTime);
}
render() {
this.currentState?.render?.();
}
}
// Game states
class MenuState {
constructor(app, stateMachine) {
this.app = app;
this.stateMachine = stateMachine;
this.container = new PIXI.Container();
const title = new PIXI.Text({
text: 'Main Menu',
style: { fontSize: 64, fill: 0xffffff }
});
title.anchor.set(0.5);
title.position.set(400, 200);
this.container.addChild(title);
const startButton = new PIXI.Text({
text: 'Start Game',
style: { fontSize: 32, fill: 0x00ff00 }
});
startButton.anchor.set(0.5);
startButton.position.set(400, 350);
startButton.eventMode = 'static';
startButton.cursor = 'pointer';
startButton.on('pointerdown', () => {
stateMachine.setState('playing');
});
this.container.addChild(startButton);
}
enter() {
this.app.stage.addChild(this.container);
}
exit() {
this.app.stage.removeChild(this.container);
}
}
class PlayingState {
constructor(app, stateMachine) {
this.app = app;
this.stateMachine = stateMachine;
this.container = new PIXI.Container();
this.score = 0;
this.scoreText = new PIXI.Text({
text: 'Score: 0',
style: { fontSize: 24, fill: 0xffffff }
});
this.scoreText.position.set(10, 10);
this.container.addChild(this.scoreText);
// Setup pause key
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.stateMachine.currentState === this) {
this.stateMachine.setState('paused');
}
});
}
enter() {
this.app.stage.addChild(this.container);
this.score = 0;
}
exit() {
// Keep container for paused state
}
update(deltaTime) {
// Game logic here
this.score++;
this.scoreText.text = `Score: ${Math.floor(this.score / 60)}`;
}
}
class PausedState {
constructor(app, stateMachine) {
this.app = app;
this.stateMachine = stateMachine;
this.container = new PIXI.Container();
const overlay = new PIXI.Graphics();
overlay.rect(0, 0, 800, 600).fill({ color: 0x000000, alpha: 0.7 });
this.container.addChild(overlay);
const pausedText = new PIXI.Text({
text: 'PAUSED',
style: { fontSize: 64, fill: 0xffffff }
});
pausedText.anchor.set(0.5);
pausedText.position.set(400, 250);
this.container.addChild(pausedText);
const resumeButton = new PIXI.Text({
text: 'Resume',
style: { fontSize: 32, fill: 0x00ff00 }
});
resumeButton.anchor.set(0.5);
resumeButton.position.set(400, 350);
resumeButton.eventMode = 'static';
resumeButton.cursor = 'pointer';
resumeButton.on('pointerdown', () => {
stateMachine.setState('playing');
});
this.container.addChild(resumeButton);
}
enter() {
this.app.stage.addChild(this.container);
}
exit() {
this.app.stage.removeChild(this.container);
}
}
// Usage
const stateMachine = new StateMachine();
stateMachine.addState('menu', new MenuState(app, stateMachine));
stateMachine.addState('playing', new PlayingState(app, stateMachine));
stateMachine.addState('paused', new PausedState(app, stateMachine));
stateMachine.setState('menu');
app.ticker.add((ticker) => {
stateMachine.update(ticker.deltaTime);
});
```
---
## UI Components
### 1. Progress Bar
Animated progress bar with percentage:
```javascript
class ProgressBar extends PIXI.Container {
constructor(width = 200, height = 30, options = {}) {
super();
const {
backgroundColor = 0x333333,
fillColor = 0x3498db,
borderColor = 0xffffff,
borderWidth = 2,
showPercentage = true
} = options;
this.barWidth = width;
this.barHeight = height;
this.progress = 0;
this.targetProgress = 0;
this.animationSpeed = 0.05;
// Background
this.background = new PIXI.Graphics();
this.background.roundRect(0, 0, width, height, 5).fill(backgroundColor);
this.addChild(this.background);
// Fill
this.fill = new PIXI.Graphics();
this.addChild(this.fill);
// Border
this.border = new PIXI.Graphics();
this.border.roundRect(0, 0, width, height, 5)
.stroke({ width: borderWidth, color: borderColor });
this.addChild(this.border);
// Percentage text
if (showPercentage) {
this.percentageText = new PIXI.Text({
text: '0%',
style: {
fontSize: height * 0.6,
fill: 0xffffff,
fontWeight: 'bold'
}
});
this.percentageText.anchor.set(0.5);
this.percentageText.position.set(width / 2, height / 2);
this.addChild(this.percentageText);
}
this.fillColor = fillColor;
this.updateBar();
}
setProgress(value, animate = true) {
this.targetProgress = Math.max(0, Math.min(1, value));
if (!animate) {
this.progress = this.targetProgress;
this.updateBar();
}
}
update() {
if (Math.abs(this.targetProgress - this.progress) > 0.001) {
this.progress += (this.targetProgress - this.progress) * this.animationSpeed;
this.updateBar();
}
}
updateBar() {
const fillWidth = this.barWidth * this.progress;
this.fill.clear();
if (fillWidth > 0) {
this.fill.roundRect(0, 0, fillWidth, this.barHeight, 5).fill(this.fillColor);
}
if (this.percentageText) {
this.percentageText.text = `${Math.round(this.progress * 100)}%`;
}
}
}
// Usage
const loadingBar = new ProgressBar(400, 40, {
backgroundColor: 0x2c3e50,
fillColor: 0x27ae60,
showPercentage: true
});
loadingBar.position.set(200, 280);
app.stage.addChild(loadingBar);
// Simulate loading
let loaded = 0;
const interval = setInterval(() => {
loaded += 0.1;
loadingBar.setProgress(loaded);
if (loaded >= 1) {
clearInterval(interval);
console.log('Loading complete!');
}
}, 100);
app.ticker.add(() => {
loadingBar.update();
});
```
### 2. Modal Dialog
Reusable modal dialog component:
```javascript
class Modal extends PIXI.Container {
constructor(title, message, buttons = ['OK']) {
super();
// Dark overlay
this.overlay = new PIXI.Graphics();
this.overlay.rect(0, 0, 800, 600).fill({ color: 0x000000, alpha: 0.7 });
this.overlay.eventMode = 'static'; // Block clicks
this.addChild(this.overlay);
// Modal background
this.modal = new PIXI.Graphics();
this.modal.roundRect(200, 150, 400, 300, 10).fill(0xffffff);
this.modal.roundRect(200, 150, 400, 300, 10)
.stroke({ width: 3, color: 0x3498db });
this.addChild(this.modal);
// Title
const titleText = new PIXI.Text({
text: title,
style: {
fontSize: 32,
fill: 0x2c3e50,
fontWeight: 'bold'
}
});
titleText.anchor.set(0.5, 0);
titleText.position.set(400, 170);
this.addChild(titleText);
// Message
const messageText = new PIXI.Text({
text: message,
style: {
fontSize: 20,
fill: 0x34495e,
align: 'center',
wordWrap: true,
wordWrapWidth: 350
}
});
messageText.anchor.set(0.5);
messageText.position.set(400, 280);
this.addChild(messageText);
// Buttons
const buttonContainer = new PIXI.Container();
const buttonWidth = 120;
const buttonSpacing = 20;
const totalWidth = buttons.length * buttonWidth + (buttons.length - 1) * buttonSpacing;
buttons.forEach((label, index) => {
const button = this.createButton(label, buttonWidth, 40);
button.position.set(
index * (buttonWidth + buttonSpacing),
0
);
buttonContainer.addChild(button);
});
buttonContainer.position.set(400 - totalWidth / 2, 380);
this.addChild(buttonContainer);
this.visible = false;
}
createButton(label, width, height) {
const button = new PIXI.Container();
const bg = new PIXI.Graphics();
bg.roundRect(0, 0, width, height, 5).fill(0x3498db);
button.addChild(bg);
const text = new PIXI.Text({
text: label,
style: {
fontSize: 18,
fill: 0xffffff,
fontWeight: 'bold'
}
});
text.anchor.set(0.5);
text.position.set(width / 2, height / 2);
button.addChild(text);
button.eventMode = 'static';
button.cursor = 'pointer';
button.on('pointerover', () => {
bg.clear();
bg.roundRect(0, 0, width, height, 5).fill(0x2980b9);
});
button.on('pointerout', () => {
bg.clear();
bg.roundRect(0, 0, width, height, 5).fill(0x3498db);
});
button.on('pointerdown', () => {
this.visible = false;
this.emit('buttonClick', label);
});
return button;
}
show() {
this.visible = true;
}
hide() {
this.visible = false;
}
}
// Usage
const modal = new Modal(
'Game Over',
'You scored 1,234 points!\nWould you like to play again?',
['Play Again', 'Main Menu', 'Quit']
);
app.stage.addChild(modal);
modal.on('buttonClick', (button) => {
console.log(`Clicked: ${button}`);
if (button === 'Play Again') {
startGame();
} else if (button === 'Main Menu') {
showMenu();
} else if (button === 'Quit') {
quitGame();
}
});
// Show modal after 3 seconds
setTimeout(() => {
modal.show();
}, 3000);
```
### 3. Slider Component
Interactive slider for settings:
```javascript
class Slider extends PIXI.Container {
constructor(min = 0, max = 100, value = 50, width = 200) {
super();
this.min = min;
this.max = max;
this.value = value;
this.width = width;
this.isDragging = false;
// Track
this.track = new PIXI.Graphics();
this.track.roundRect(0, -2, width, 4, 2).fill(0x7f8c8d);
this.addChild(this.track);
// Fill
this.fill = new PIXI.Graphics();
this.addChild(this.fill);
// Handle
this.handle = new PIXI.Graphics();
this.handle.circle(0, 0, 10).fill(0x3498db);
this.handle.circle(0, 0, 10).stroke({ width: 2, color: 0xffffff });
this.handle.eventMode = 'static';
this.handle.cursor = 'pointer';
this.addChild(this.handle);
// Events
this.handle.on('pointerdown', this.onDragStart.bind(this));
this.handle.on('pointerup', this.onDragEnd.bind(this));
this.handle.on('pointerupoutside', this.onDragEnd.bind(this));
this.updateVisuals();
}
onDragStart(event) {
this.isDragging = true;
this.handle.on('globalpointermove', this.onDragMove.bind(this));
}
onDragMove(event) {
if (!this.isDragging) return;
const localX = event.global.x - this.getGlobalPosition().x;
const percentage = Math.max(0, Math.min(1, localX / this.width));
this.setValue(this.min + (this.max - this.min) * percentage);
}
onDragEnd() {
this.isDragging = false;
this.handle.off('globalpointermove');
}
setValue(value) {
this.value = Math.max(this.min, Math.min(this.max, value));
this.updateVisuals();
this.emit('change', this.value);
}
updateVisuals() {
const percentage = (this.value - this.min) / (this.max - this.min);
const handleX = this.width * percentage;
// Update fill
this.fill.clear();
this.fill.roundRect(0, -2, handleX, 4, 2).fill(0x3498db);
// Update handle position
this.handle.position.set(handleX, 0);
}
}
// Usage
const volumeSlider = new Slider(0, 100, 75, 250);
volumeSlider.position.set(275, 300);
app.stage.addChild(volumeSlider);
const label = new PIXI.Text({
text: 'Volume: 75',
style: { fontSize: 20, fill: 0xffffff }
});
label.position.set(275, 270);
app.stage.addChild(label);
volumeSlider.on('change', (value) => {
label.text = `Volume: ${Math.round(value)}`;
console.log('Volume changed to:', value);
});
```
---
(Continuing with Framework Integration, Mobile Optimization, and Advanced Techniques in next message due to length constraints...)
## Framework Integration
### 1. React Integration with @pixi/react
Use PixiJS components in React:
```javascript
// Install: npm install @pixi/react pixi.js
import { Stage, Container, Sprite, useApp, useTick } from '@pixi/react';
import { useState, useCallback } from 'react';
import * as PIXI from 'pixi.js';
// Animated sprite component
function RotatingSprite({ x, y, texture }) {
const [rotation, setRotation] = useState(0);
useTick((delta) => {
setRotation((prev) => prev + 0.05 * delta);
});
return (
<Sprite
texture={texture}
x={x}
y={y}
anchor={0.5}
rotation={rotation}
/>
);
}
// Interactive sprite
function DraggableSprite({ initialX, initialY, texture }) {
const [position, setPosition] = useState({ x: initialX, y: initialY });
const [isDragging, setIsDragging] = useState(false);
const handlePointerDown = useCallback(() => {
setIsDragging(true);
}, []);
const handlePointerUp = useCallback(() => {
setIsDragging(false);
}, []);
const handlePointerMove = useCallback((event) => {
if (isDragging) {
setPosition({
x: event.global.x,
y: event.global.y
});
}
}, [isDragging]);
return (
<Sprite
texture={texture}
x={position.x}
y={position.y}
anchor={0.5}
interactive
pointerdown={handlePointerDown}
pointerup={handlePointerUp}
pointerupoutside={handlePointerUp}
pointermove={handlePointerMove}
cursor="pointer"
/>
);
}
// Main app component
function PixiApp() {
const [bunnyTexture] = useState(() => PIXI.Texture.from('bunny.png'));
const [spriteCount, setSpriteCount] = useState(5);
return (
<div>
<button onClick={() => setSpriteCount(prev => prev + 5)}>
Add Sprites
</button>
<Stage width={800} height={600} options={{ backgroundColor: 0x1099bb }}>
<Container>
{Array.from({ length: spriteCount }, (_, i) => (
<RotatingSprite
key={i}
x={Math.random() * 800}
y={Math.random() * 600}
texture={bunnyTexture}
/>
))}
<DraggableSprite
initialX={400}
initialY={300}
texture={bunnyTexture}
/>
</Container>
</Stage>
</div>
);
}
export default PixiApp;
```
### 2. Vue Integration
```javascript
// Install: npm install vue pixi.js
<template>
<div>
<div ref="pixiContainer"></div>
<div class="controls">
<button @click="addSprite">Add Sprite</button>
<button @click="clearSprites">Clear</button>
<p>Sprites: {{ sprites.length }}</p>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
import * as PIXI from 'pixi.js';
export default {
setup() {
const pixiContainer = ref(null);
let app = null;
const sprites = ref([]);
onMounted(async () => {
app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1099bb
});
pixiContainer.value.appendChild(app.canvas);
// Start ticker
app.ticker.add((ticker) => {
sprites.value.forEach(sprite => {
sprite.rotation += 0.05 * ticker.deltaTime;
});
});
});
onUnmounted(() => {
app?.destroy(true, { children: true });
});
const addSprite = async () => {
if (!app) return;
const texture = await PIXI.Assets.load('bunny.png');
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(
Math.random() * 800,
Math.random() * 600
);
app.stage.addChild(sprite);
sprites.value.push(sprite);
};
const clearSprites = () => {
sprites.value.forEach(sprite => {
app.stage.removeChild(sprite);
sprite.destroy();
});
sprites.value = [];
};
return {
pixiContainer,
sprites,
addSprite,
clearSprites
};
}
};
</script>
<style scoped>
.controls {
margin-top: 20px;
}
button {
margin-right: 10px;
padding: 10px 20px;
}
</style>
```
---
## Mobile Optimization
### 1. Touch Controls
Implement mobile-friendly touch controls:
```javascript
class TouchJoystick {
constructor(app, options = {}) {
this.app = app;
this.container = new PIXI.Container();
this.isActive = false;
this.centerX = 0;
this.centerY = 0;
this.touchId = null;
const { radius = 60, color = 0x3498db } = options;
// Outer circle (base)
this.base = new PIXI.Graphics();
this.base.circle(0, 0, radius).fill({ color: 0x000000, alpha: 0.3 });
this.base.circle(0, 0, radius).stroke({ width: 3, color, alpha: 0.5 });
this.container.addChild(this.base);
// Inner circle (stick)
this.stick = new PIXI.Graphics();
this.stick.circle(0, 0, radius / 2).fill({ color, alpha: 0.7 });
this.container.addChild(this.stick);
this.radius = radius;
this.container.position.set(100, app.screen.height - 100);
this.container.alpha = 0.5;
app.stage.addChild(this.container);
// Touch events
app.stage.eventMode = 'static';
app.stage.on('pointerdown', this.onTouchStart.bind(this));
app.stage.on('pointermove', this.onTouchMove.bind(this));
app.stage.on('pointerup', this.onTouchEnd.bind(this));
app.stage.on('pointerupoutside', this.onTouchEnd.bind(this));
this.direction = { x: 0, y: 0 };
this.magnitude = 0;
}
onTouchStart(event) {
const pos = event.global;
const dist = this.distance(
pos.x,
pos.y,
this.container.x,
this.container.y
);
if (dist < this.radius * 1.5) {
this.isActive = true;
this.touchId = event.pointerId;
this.container.alpha = 1;
}
}
onTouchMove(event) {
if (!this.isActive || event.pointerId !== this.touchId) return;
const pos = event.global;
const dx = pos.x - this.container.x;
const dy = pos.y - this.container.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > this.radius) {
// Clamp to radius
this.stick.position.set(
(dx / dist) * this.radius,
(dy / dist) * this.radius
);
this.magnitude = 1;
} else {
this.stick.position.set(dx, dy);
this.magnitude = dist / this.radius;
}
// Normalized direction
if (dist > 0) {
this.direction.x = dx / dist;
this.direction.y = dy / dist;
}
}
onTouchEnd(event) {
if (event.pointerId === this.touchId) {
this.isActive = false;
this.touchId = null;
this.stick.position.set(0, 0);
this.container.alpha = 0.5;
this.direction = { x: 0, y: 0 };
this.magnitude = 0;
}
}
distance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
getDirection() {
return {
x: this.direction.x * this.magnitude,
y: this.direction.y * this.magnitude
};
}
}
// Usage
const joystick = new TouchJoystick(app, { radius: 70, color: 0xe74c3c });
const player = new PIXI.Sprite(texture);
player.anchor.set(0.5);
player.position.set(400, 300);
app.stage.addChild(player);
app.ticker.add((ticker) => {
const dir = joystick.getDirection();
const speed = 5;
player.x += dir.x * speed * ticker.deltaTime;
player.y += dir.y * speed * ticker.deltaTime;
// Keep in bounds
player.x = Math.max(0, Math.min(app.screen.width, player.x));
player.y = Math.max(0, Math.min(app.screen.height, player.y));
});
```
### 2. Device Detection and Adaptive Settings
```javascript
class DeviceOptimizer {
constructor(app) {
this.app = app;
this.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
this.isLowEnd = this.detectLowEndDevice();
this.applyOptimizations();
}
detectLowEndDevice() {
// Check hardware concurrency (CPU cores)
const cores = navigator.hardwareConcurrency || 2;
// Check device memory (if available)
const memory = navigator.deviceMemory || 4;
// Low-end: < 4 cores or < 4GB RAM
return cores < 4 || memory < 4 || this.isMobile;
}
applyOptimizations() {
if (this.isLowEnd) {
console.log('Applying low-end device optimizations');
// Reduce resolution
this.app.renderer.resolution = 1;
// Disable antialiasing
this.app.renderer.antialias = false;
// Reduce max FPS
this.app.ticker.maxFPS = 30;
return {
maxSprites: 50,
particleLimit: 100,
enableFilters: false,
textureQuality: 'low'
};
} else {
console.log('Using high-quality settings');
return {
maxSprites: 500,
particleLimit: 5000,
enableFilters: true,
textureQuality: 'high'
};
}
}
getOptimalSettings() {
return this.applyOptimizations();
}
}
// Usage
(async () => {
const app = new PIXI.Application();
await app.init({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 0x1a1a2e
});
const optimizer = new DeviceOptimizer(app);
const settings = optimizer.getOptimalSettings();
console.log('Device settings:', settings);
// Use settings to configure game
for (let i = 0; i < settings.maxSprites; i++) {
const sprite = new PIXI.Sprite(texture);
// ... create sprites based on limit
}
})();
```
---
## Advanced Techniques
### 1. Custom Render Texture
Render to texture for post-processing or caching:
```javascript
class RenderTextureEffect {
constructor(app) {
this.app = app;
// Create render texture
this.renderTexture = PIXI.RenderTexture.create({
width: app.screen.width,
height: app.screen.height
});
// Sprite to display the rendered texture
this.outputSprite = new PIXI.Sprite(this.renderTexture);
// Container for objects to render
this.sceneContainer = new PIXI.Container();
}
render() {
// Render scene to texture
this.app.renderer.render({
container: this.sceneContainer,
target: this.renderTexture,
clear: true
});
}
getSprite() {
return this.outputSprite;
}
getContainer() {
return this.sceneContainer;
}
}
// Usage - Create trail effect
const effect = new RenderTextureEffect(app);
const sprite = new PIXI.Sprite(texture);
sprite.position.set(400, 300);
effect.getContainer().addChild(sprite);
// Display render texture with alpha
const outputSprite = effect.getSprite();
outputSprite.alpha = 0.9; // Fade effect
app.stage.addChild(outputSprite);
// Move sprite with mouse
app.stage.eventMode = 'static';
app.stage.on('pointermove', (event) => {
sprite.position.set(event.global.x, event.global.y);
});
// Render each frame
app.ticker.add(() => {
effect.render();
});
```
### 2. Multi-Pass Shader Effects
Chain multiple shader passes:
```javascript
class MultiPassFilter {
constructor(app) {
this.app = app;
this.passes = [];
this.renderTextures = [];
}
addPass(filter) {
this.passes.push(filter);
// Create render texture for this pass
const rt = PIXI.RenderTexture.create({
width: this.app.screen.width,
height: this.app.screen.height
});
this.renderTextures.push(rt);
}
render(source) {
let currentSource = source;
for (let i = 0; i < this.passes.length; i++) {
const filter = this.passes[i];
const target = this.renderTextures[i];
// Apply filter
currentSource.filters = [filter];
// Render to texture
this.app.renderer.render({
container: currentSource,
target,
clear: true
});
// Use output as input for next pass
const sprite = new PIXI.Sprite(target);
currentSource = sprite;
}
return currentSource;
}
}
// Usage - Blur then chromatic aberration
const multiPass = new MultiPassFilter(app);
const blurFilter = new PIXI.BlurFilter();
blurFilter.strength = 5;
const chromaFilter = new ChromaticAberrationFilter(10);
multiPass.addPass(blurFilter);
multiPass.addPass(chromaFilter);
const scene = new PIXI.Container();
const sprite = new PIXI.Sprite(texture);
sprite.position.set(400, 300);
scene.addChild(sprite);
const result = multiPass.render(scene);
app.stage.addChild(result);
```
### 3. WebWorker Integration
Offload heavy calculations to worker threads:
```javascript
// worker.js
self.onmessage = function(e) {
const { type, data } = e.data;
if (type === 'calculateParticles') {
const { count, width, height } = data;
const particles = [];
for (let i = 0; i < count; i++) {
particles.push({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2
});
}
self.postMessage({ type: 'particlesCalculated', particles });
} else if (type === 'updateParticles') {
const { particles, width, height, deltaTime } = data;
particles.forEach(p => {
p.x += p.vx * deltaTime;
p.y += p.vy * deltaTime;
if (p.x < 0 || p.x > width) p.vx *= -1;
if (p.y < 0 || p.y > height) p.vy *= -1;
});
self.postMessage({ type: 'particlesUpdated', particles });
}
};
// main.js
const worker = new Worker('worker.js');
const particles = [];
const sprites = [];
worker.onmessage = function(e) {
const { type, particles: updatedParticles } = e.data;
if (type === 'particlesCalculated') {
// Create sprites for particles
updatedParticles.forEach(p => {
const sprite = new PIXI.Sprite(texture);
sprite.position.set(p.x, p.y);
app.stage.addChild(sprite);
sprites.push(sprite);
});
particles.push(...updatedParticles);
} else if (type === 'particlesUpdated') {
// Update sprite positions
updatedParticles.forEach((p, i) => {
sprites[i].position.set(p.x, p.y);
});
particles.length = 0;
particles.push(...updatedParticles);
}
};
// Initial creation
worker.postMessage({
type: 'calculateParticles',
data: {
count: 10000,
width: 800,
height: 600
}
});
// Update loop
app.ticker.add((ticker) => {
if (particles.length > 0) {
worker.postMessage({
type: 'updateParticles',
data: {
particles,
width: 800,
height: 600,
deltaTime: ticker.deltaTime
}
});
}
});
```
---
## Conclusion
These examples cover the most common PixiJS patterns and use cases for production applications:
- **Basic Applications**: Foundation for any PixiJS project
- **Interactive Elements**: User input and engagement
- **Particle Systems**: Visual effects and ambiance
- **Filters & Effects**: Visual polish and style
- **Custom Shaders**: Advanced visual customization
- **Animation**: Smooth motion and transitions
- **Performance**: Optimization for 60 FPS
- **Game Development**: Core gameplay mechanics
- **UI Components**: Professional user interfaces
- **Framework Integration**: React, Vue compatibility
- **Mobile Optimization**: Touch controls and adaptive settings
- **Advanced Techniques**: Render textures, multi-pass effects, WebWorkers
For more examples, visit:
- [PixiJS Examples](https://pixijs.com/examples)
- [PixiJS Playground](https://pixijs.io/playground)
- [GitHub PixiJS Demos](https://github.com/pixijs/pixijs/tree/dev/examples)
assets/starter_pixijs/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Starter</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- UI Overlay -->
<div id="ui-overlay">
<div id="info-panel">
<h1>PixiJS Starter</h1>
<p>High-performance 2D rendering</p>
</div>
<div id="stats-panel">
<div class="stat">
<span class="label">FPS:</span>
<span id="fps">--</span>
</div>
<div class="stat">
<span class="label">Sprites:</span>
<span id="sprite-count">--</span>
</div>
<div class="stat">
<span class="label">Draw Calls:</span>
<span id="draw-calls">--</span>
</div>
</div>
<div id="controls-panel">
<button id="toggle-stats">Toggle Stats</button>
<button id="add-sprites">Add Sprites</button>
<button id="clear-sprites">Clear</button>
</div>
</div>
<!-- PixiJS CDN -->
<script src="https://pixijs.download/release/pixi.js"></script>
<script src="main.js"></script>
</body>
</html>
assets/starter_pixijs/main.js
/**
* PixiJS Starter Project
* Main application initialization
*/
(async () => {
// Create PixiJS application
const app = new PIXI.Application();
await app.init({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 0x1a1a2e,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true
});
document.body.appendChild(app.canvas);
// Create sprite container
const spriteContainer = new PIXI.Container();
app.stage.addChild(spriteContainer);
// Sprite collection
const sprites = [];
// Create sprite texture
function createSpriteTexture(color) {
const graphics = new PIXI.Graphics();
graphics.circle(25, 25, 25).fill(color);
return app.renderer.generateTexture(graphics);
}
const textures = [
createSpriteTexture(0xe74c3c),
createSpriteTexture(0x3498db),
createSpriteTexture(0x2ecc71),
createSpriteTexture(0xf39c12),
createSpriteTexture(0x9b59b6)
];
// Add sprites function
function addSprites(count = 10) {
for (let i = 0; i < count; i++) {
const texture = textures[Math.floor(Math.random() * textures.length)];
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(
Math.random() * app.screen.width,
Math.random() * app.screen.height
);
sprite.scale.set(Math.random() * 0.5 + 0.5);
// Velocity
sprite.vx = (Math.random() - 0.5) * 2;
sprite.vy = (Math.random() - 0.5) * 2;
// Interactive
sprite.eventMode = 'static';
sprite.cursor = 'pointer';
sprite.on('pointerdown', () => {
sprite.tint = Math.random() * 0xffffff;
});
spriteContainer.addChild(sprite);
sprites.push(sprite);
}
updateSpriteCount();
}
// Clear sprites function
function clearSprites() {
sprites.forEach(sprite => sprite.destroy());
sprites.length = 0;
spriteContainer.removeChildren();
updateSpriteCount();
}
// Update sprite count display
function updateSpriteCount() {
document.getElementById('sprite-count').textContent = sprites.length;
}
// Setup UI controls
let statsVisible = true;
document.getElementById('toggle-stats').addEventListener('click', () => {
statsVisible = !statsVisible;
document.getElementById('stats-panel').classList.toggle('hidden');
});
document.getElementById('add-sprites').addEventListener('click', () => {
addSprites(20);
});
document.getElementById('clear-sprites').addEventListener('click', () => {
clearSprites();
});
// Update loop
app.ticker.add((ticker) => {
// Move sprites
sprites.forEach(sprite => {
sprite.x += sprite.vx * ticker.deltaTime;
sprite.y += sprite.vy * ticker.deltaTime;
// Bounce off edges
if (sprite.x < 0 || sprite.x > app.screen.width) {
sprite.vx *= -1;
}
if (sprite.y < 0 || sprite.y > app.screen.height) {
sprite.vy *= -1;
}
// Keep within bounds
sprite.x = Math.max(0, Math.min(app.screen.width, sprite.x));
sprite.y = Math.max(0, Math.min(app.screen.height, sprite.y));
// Rotate
sprite.rotation += 0.01 * ticker.deltaTime;
});
// Update stats
if (statsVisible) {
document.getElementById('fps').textContent = Math.round(app.ticker.FPS);
document.getElementById('draw-calls').textContent = app.renderer.stats.drawCalls.total || 0;
}
});
// Handle window resize
window.addEventListener('resize', () => {
app.renderer.resize(window.innerWidth, window.innerHeight);
});
// Initial sprites
addSprites(50);
console.log('PixiJS application initialized');
console.log('Canvas size:', app.screen.width, 'x', app.screen.height);
console.log('Resolution:', app.renderer.resolution);
})();
assets/starter_pixijs/README.md
# PixiJS Starter Template
A modern, production-ready PixiJS starter template with interactive sprites, real-time performance monitoring, and responsive UI controls.
## Features
- **High-Performance Rendering**: Uses PixiJS v8+ with WebGL/WebGPU
- **Interactive Sprites**: Click to change colors, drag and interact
- **Physics Simulation**: Bouncing sprites with velocity and collision detection
- **Performance Monitoring**: Real-time FPS, sprite count, and draw call tracking
- **Responsive Design**: Mobile-friendly UI with glassmorphism effects
- **Modern UI**: Clean, professional interface with gradient accents
- **Easy Customization**: Well-structured code for quick modifications
## Quick Start
### 1. Local Development
Simply open `index.html` in a modern web browser:
```bash
# Using Python's built-in server (recommended)
python3 -m http.server 8000
# Or using Node.js http-server
npx http-server -p 8000
# Then open http://localhost:8000
```
### 2. Live Server (VS Code)
If using VS Code with the Live Server extension:
1. Right-click on `index.html`
2. Select "Open with Live Server"
### 3. Production Deployment
For production, serve the files through any static hosting:
- **Vercel**: `vercel --prod`
- **Netlify**: Drag and drop the folder
- **GitHub Pages**: Push to repository and enable Pages
- **AWS S3**: Upload as static website
## Project Structure
```
starter_pixijs/
├── index.html # Main HTML structure
├── styles.css # Responsive styling with glassmorphism
├── main.js # PixiJS application logic
└── README.md # This file
```
## Usage
### Controls
- **Toggle Stats**: Show/hide performance statistics panel
- **Add Sprites**: Add 20 random sprites to the canvas
- **Clear**: Remove all sprites from the canvas
### Interactions
- **Click Sprites**: Change sprite color randomly
- **Watch Physics**: Sprites bounce off edges automatically
## Customization
### Change Background Color
In `main.js` line 10:
```javascript
await app.init({
backgroundColor: 0x1a1a2e, // Change this hex color
// ...
});
```
### Modify Sprite Colors
In `main.js` lines 35-40, edit the color palette:
```javascript
const textures = [
createSpriteTexture(0xe74c3c), // Red
createSpriteTexture(0x3498db), // Blue
createSpriteTexture(0x2ecc71), // Green
createSpriteTexture(0xf39c12), // Orange
createSpriteTexture(0x9b59b6) // Purple
];
```
### Adjust Sprite Size
In `main.js` line 31, change the circle radius:
```javascript
graphics.circle(25, 25, 25).fill(color); // Last parameter is radius
```
### Change Initial Sprite Count
In `main.js` line 140:
```javascript
addSprites(50); // Change from 50 to your desired count
```
### Modify Physics Behavior
In `main.js` lines 57-58, adjust velocity ranges:
```javascript
sprite.vx = (Math.random() - 0.5) * 2; // Horizontal speed
sprite.vy = (Math.random() - 0.5) * 2; // Vertical speed
```
### Disable Rotation
In `main.js`, comment out or remove line 124:
```javascript
// sprite.rotation += 0.01 * ticker.deltaTime;
```
## Advanced Customization
### Add Sprite Textures from Images
Replace the procedural graphics with image textures:
```javascript
// Load texture from image
const texture = await PIXI.Assets.load('path/to/sprite.png');
// Create sprite
const sprite = new PIXI.Sprite(texture);
```
### Add Filters and Effects
Apply blur, glow, or other effects:
```javascript
import { BlurFilter } from 'pixi.js';
const blurFilter = new BlurFilter();
blurFilter.strength = 8;
sprite.filters = [blurFilter];
```
### Implement Sprite Pooling
For better performance with many sprites:
```javascript
class SpritePool {
constructor(texture, size = 100) {
this.available = [];
this.active = [];
for (let i = 0; i < size; i++) {
const sprite = new PIXI.Sprite(texture);
sprite.visible = false;
this.available.push(sprite);
}
}
spawn(x, y) {
let sprite = this.available.pop();
if (!sprite) {
sprite = new PIXI.Sprite(this.texture);
}
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);
}
}
}
const pool = new SpritePool(texture);
const sprite = pool.spawn(100, 100);
// Later: pool.despawn(sprite);
```
### Add Particle Effects
Use ParticleContainer for thousands of sprites:
```javascript
const particles = new PIXI.ParticleContainer(10000, {
position: true,
rotation: true,
scale: true,
tint: true
});
for (let i = 0; i < 10000; i++) {
const particle = new PIXI.Sprite(texture);
particle.x = Math.random() * app.screen.width;
particle.y = Math.random() * app.screen.height;
particles.addChild(particle);
}
app.stage.addChild(particles);
```
### Add Custom Shaders
Create custom visual effects with GLSL:
```javascript
import { Filter, GlProgram } from 'pixi.js';
const fragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uTime;
void main() {
vec2 coord = vTextureCoord;
coord.x += sin(coord.y * 10.0 + uTime) * 0.01;
gl_FragColor = texture(uTexture, coord);
}
`;
const waveFilter = new Filter({
glProgram: new GlProgram({ fragment }),
resources: {
waveUniforms: {
uTime: { value: 0, type: 'f32' }
}
}
});
// Update in ticker
app.ticker.add(() => {
waveFilter.resources.waveUniforms.uniforms.uTime += 0.1;
});
sprite.filters = [waveFilter];
```
## Performance Tips
### For Desktop (High-End)
- Increase sprite count for stress testing
- Enable antialiasing for smoother edges
- Use higher resolution textures
- Add complex filters and effects
```javascript
await app.init({
antialias: true,
resolution: 2, // Higher resolution
// ...
});
addSprites(500); // More sprites
```
### For Mobile (Low-End)
- Reduce sprite count
- Disable antialiasing
- Use lower resolution
- Avoid heavy filters
```javascript
await app.init({
antialias: false,
resolution: 1,
// ...
});
addSprites(50); // Fewer sprites
```
### General Optimization
1. **Use ParticleContainer** for static sprites (10x faster)
2. **Enable cacheAsBitmap** for complex static graphics
3. **Minimize draw calls** with texture atlases
4. **Cull off-screen objects** for large scenes
5. **Pool objects** to avoid garbage collection
6. **Limit filters** to specific areas with `filterArea`
## Troubleshooting
### Issue: Black screen or no rendering
**Solution**: Check browser console for errors. Ensure:
- PixiJS CDN is loading correctly
- No JavaScript errors in console
- Browser supports WebGL (check `https://get.webgl.org/`)
### Issue: Low FPS on mobile
**Solution**: Reduce sprite count and disable antialiasing:
```javascript
await app.init({
antialias: false,
resolution: 1
});
addSprites(25); // Fewer sprites
```
### Issue: Sprites disappearing at edges
**Solution**: Ensure sprites are kept within bounds (lines 120-121 in main.js handle this)
### Issue: Memory leaks over time
**Solution**: Properly destroy sprites when clearing:
```javascript
function clearSprites() {
sprites.forEach(sprite => {
sprite.destroy({ texture: false }); // Keep texture
});
sprites.length = 0;
spriteContainer.removeChildren();
}
```
## Browser Support
- **Chrome/Edge**: Full support (recommended)
- **Firefox**: Full support
- **Safari**: Full support (iOS 15+)
- **Mobile browsers**: Supported with reduced features
Requires WebGL support. Check compatibility at [caniuse.com/webgl](https://caniuse.com/webgl).
## Next Steps
### Learning Resources
- [PixiJS Official Documentation](https://pixijs.com/docs)
- [PixiJS Examples](https://pixijs.com/examples)
- [PixiJS Playground](https://pixijs.io/playground)
- [WebGL Fundamentals](https://webglfundamentals.org/)
### Extend the Template
1. **Add Sprite Sheet Animations**: Use `AnimatedSprite` for frame-based animation
2. **Implement Collision Detection**: Check sprite overlaps and interactions
3. **Add Sound Effects**: Integrate Howler.js or Web Audio API
4. **Create Game Logic**: Add scoring, levels, or gameplay mechanics
5. **Integrate with React**: Use `@pixi/react` for component-based approach
### Example Extensions
**Collision Detection**:
```javascript
function checkCollision(sprite1, sprite2) {
const bounds1 = sprite1.getBounds();
const bounds2 = sprite2.getBounds();
return bounds1.x < bounds2.x + bounds2.width &&
bounds1.x + bounds1.width > bounds2.x &&
bounds1.y < bounds2.y + bounds2.height &&
bounds1.y + bounds1.height > bounds2.y;
}
app.ticker.add(() => {
for (let i = 0; i < sprites.length; i++) {
for (let j = i + 1; j < sprites.length; j++) {
if (checkCollision(sprites[i], sprites[j])) {
// Handle collision
}
}
}
});
```
**Sprite Sheet Animation**:
```javascript
// Load sprite sheet
const sheet = await PIXI.Assets.load('spritesheet.json');
// Create animated sprite
const animatedSprite = new PIXI.AnimatedSprite(sheet.animations['run']);
animatedSprite.animationSpeed = 0.1;
animatedSprite.play();
app.stage.addChild(animatedSprite);
```
**React Integration**:
```javascript
import { Stage, Container, Sprite } from '@pixi/react';
function App() {
return (
<Stage width={800} height={600}>
<Container>
<Sprite texture={texture} x={100} y={100} />
</Container>
</Stage>
);
}
```
## Deployment
### Static Hosting
**Vercel**:
```bash
npm install -g vercel
vercel --prod
```
**Netlify**:
```bash
npm install -g netlify-cli
netlify deploy --prod --dir .
```
**GitHub Pages**:
```bash
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/username/repo.git
git push -u origin main
# Enable Pages in repository settings
```
### CDN Considerations
The template uses PixiJS from CDN:
```html
<script src="https://pixijs.download/release/pixi.js"></script>
```
For production, consider:
1. **Self-hosting** for better caching and control
2. **npm installation** for bundled builds
3. **Specific version** to avoid breaking changes
**npm approach**:
```bash
npm install pixi.js
```
```javascript
// main.js
import * as PIXI from 'pixi.js';
// Use bundler like Vite or Webpack
```
## License
This starter template is provided as-is for learning and development purposes.
PixiJS is MIT licensed. See [PixiJS GitHub](https://github.com/pixijs/pixijs) for details.
## Support
For PixiJS questions:
- [PixiJS Discord](https://discord.gg/CPTjeb28nH)
- [PixiJS GitHub Discussions](https://github.com/pixijs/pixijs/discussions)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/pixi.js)
---
**Happy Coding!** 🎨✨
assets/starter_pixijs/styles.css
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #1a1a2e;
color: #fff;
}
/* Canvas Styles */
canvas {
display: block;
width: 100%;
height: 100%;
}
/* UI Overlay */
#ui-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
#ui-overlay > * {
pointer-events: auto;
}
/* Info Panel */
#info-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#info-panel h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
#info-panel p {
font-size: 14px;
opacity: 0.8;
}
/* Stats Panel */
#stats-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
min-width: 180px;
}
#stats-panel.hidden {
display: none;
}
.stat {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.stat:last-child {
margin-bottom: 0;
}
.stat .label {
opacity: 0.7;
margin-right: 15px;
}
.stat span:last-child {
font-weight: 600;
font-variant-numeric: tabular-nums;
}
/* Controls Panel */
#controls-panel {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
}
button {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
/* Mobile Responsive */
@media (max-width: 768px) {
#info-panel {
top: 10px;
left: 10px;
padding: 15px;
}
#info-panel h1 {
font-size: 20px;
}
#info-panel p {
font-size: 12px;
}
#stats-panel {
top: 10px;
right: 10px;
padding: 10px;
min-width: 150px;
}
.stat {
font-size: 12px;
}
#controls-panel {
bottom: 10px;
flex-direction: column;
gap: 8px;
}
button {
padding: 10px 20px;
font-size: 13px;
}
}
references/api_reference.md
# PixiJS API Reference
Complete API reference for PixiJS v8+ core classes and methods.
---
## Table of Contents
1. [Application](#application)
2. [Sprite](#sprite)
3. [Texture](#texture)
4. [Graphics](#graphics)
5. [Container](#container)
6. [ParticleContainer](#particlecontainer)
7. [Filters](#filters)
8. [Text](#text)
9. [Assets](#assets)
10. [Renderer](#renderer)
11. [DisplayObject](#displayobject)
12. [Events](#events)
---
## Application
Core application class that manages the renderer, stage, and update loop.
### Constructor
```typescript
new Application()
```
### Methods
#### `init(options)`
Initialize the application with configuration options.
```typescript
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1099bb,
backgroundAlpha: 1,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
powerPreference: 'high-performance',
hello: true // Show PixiJS banner in console
});
```
**Options**:
- `width: number` - Canvas width (default: 800)
- `height: number` - Canvas height (default: 600)
- `backgroundColor: number` - Background color (default: 0x000000)
- `backgroundAlpha: number` - Background alpha 0-1 (default: 1)
- `antialias: boolean` - Enable antialiasing (default: false)
- `resolution: number` - Device pixel ratio (default: 1)
- `autoDensity: boolean` - Adjust CSS pixel size automatically
- `powerPreference: string` - 'high-performance' | 'low-power' | 'default'
- `hello: boolean` - Show PixiJS banner (default: false)
#### `destroy(removeView, stageOptions)`
Destroy the application and release resources.
```typescript
app.destroy(true, { children: true, texture: true });
```
**Parameters**:
- `removeView: boolean` - Remove canvas from DOM (default: false)
- `stageOptions: object` - Options for stage destruction
- `children: boolean` - Destroy all children
- `texture: boolean` - Destroy textures
- `baseTexture: boolean` - Destroy base textures
#### `resizeCanvas()`
Resize canvas to fill window.
```typescript
window.addEventListener('resize', () => {
app.resizeCanvas();
});
```
### Properties
```typescript
app.stage: Container // Root display object container
app.renderer: Renderer // WebGL/WebGPU renderer instance
app.ticker: Ticker // Update loop manager
app.canvas: HTMLCanvasElement // Canvas element
app.screen: Rectangle // Screen dimensions
app.view: HTMLCanvasElement // Alias for canvas (deprecated)
```
### Plugins
```typescript
// Ticker Plugin - manages update loop
app.ticker.add((ticker) => {
// Update logic
sprite.rotation += 0.01 * ticker.deltaTime;
});
app.ticker.stop();
app.ticker.start();
app.ticker.speed = 0.5; // Half speed
// Resize Plugin
app.resizeTo = window; // Auto-resize to window
// Culler Plugin - automatic viewport culling
app.cullable = true;
```
**API References**:
- TickerPlugin: https://pixijs.download/release/docs/app.TickerPlugin.html
- ResizePlugin: https://pixijs.download/release/docs/app.ResizePlugin.html
- CullerPlugin: https://pixijs.download/release/docs/app.CullerPlugin.html
---
## Sprite
Visual element that displays a texture.
### Constructor
```typescript
new Sprite(texture: Texture)
Sprite.from(source: string | Texture) // Convenience method
```
### Properties
```typescript
sprite.texture: Texture // The texture to display
sprite.anchor: ObservablePoint // Pivot point (0-1, default: 0,0)
sprite.tint: number // Color tint (0xRRGGBB)
sprite.blendMode: BLEND_MODES // How sprite blends with background
// Transform properties (inherited from DisplayObject)
sprite.position: ObservablePoint // x, y position
sprite.scale: ObservablePoint // x, y scale
sprite.rotation: number // Rotation in radians
sprite.pivot: ObservablePoint // Rotation pivot point
sprite.skew: ObservablePoint // x, y skew
// Visibility
sprite.alpha: number // Opacity (0-1)
sprite.visible: boolean // Show/hide
sprite.renderable: boolean // Should render
// Interaction
sprite.eventMode: string // 'none' | 'passive' | 'static' | 'dynamic'
sprite.cursor: string // CSS cursor
sprite.hitArea: Rectangle | Circle | Polygon // Custom hit area
// Performance
sprite.cullable: boolean // Enable viewport culling
sprite.cacheAsBitmap: boolean // Convert to texture for performance
```
### Methods
```typescript
// Anchor
sprite.anchor.set(x, y)
sprite.anchor.set(0.5) // Center (shorthand)
// Position
sprite.position.set(x, y)
sprite.setTransform(x, y, scaleX, scaleY, rotation, skewX, skewY, pivotX, pivotY)
// Bounds
sprite.getBounds()
sprite.getLocalBounds()
// Destroy
sprite.destroy({ children: true, texture: false, baseTexture: false })
```
### Example
```typescript
import { Sprite, Texture } from 'pixi.js';
const texture = Texture.from('bunny.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.tint = 0xff0000;
sprite.alpha = 0.8;
app.stage.addChild(sprite);
```
---
## Texture
Image data that can be rendered by Sprites and Graphics.
### Static Methods
```typescript
Texture.from(source: string | HTMLImageElement | HTMLCanvasElement)
Texture.fromURL(url: string, options?: object)
Texture.fromBuffer(buffer: Uint8Array, width: number, height: number)
```
### Properties
```typescript
texture.width: number // Texture width
texture.height: number // Texture height
texture.baseTexture: BaseTexture // Underlying GPU texture
texture.frame: Rectangle // Region of baseTexture to use
texture.source: TextureSource // Source data
```
### Methods
```typescript
texture.destroy(destroyBase?: boolean)
texture.update() // Update from source
texture.clone() // Create copy
```
### Example
```typescript
import { Texture, Assets } from 'pixi.js';
// Load texture
const texture = await Assets.load('image.png');
// From URL
const tex = Texture.from('https://example.com/image.png');
// From canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// ... draw on canvas
const canvasTex = Texture.from(canvas);
// Destroy
texture.destroy(true); // Also destroy baseTexture
```
**API Reference**: https://pixijs.download/release/docs/rendering.Texture.html
---
## Graphics
API for drawing vector shapes programmatically.
### Constructor
```typescript
new Graphics()
new Graphics(context: GraphicsContext) // Share geometry
```
### Shape Methods
All shape methods return `this` for chaining.
```typescript
graphics.rect(x, y, width, height)
graphics.circle(x, y, radius)
graphics.ellipse(x, y, radiusX, radiusY)
graphics.roundRect(x, y, width, height, radius)
graphics.poly(points: number[] | Point[])
graphics.star(x, y, points, radius, innerRadius?, rotation?)
```
### Path Methods
```typescript
graphics.moveTo(x, y)
graphics.lineTo(x, y)
graphics.bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY)
graphics.quadraticCurveTo(cpX, cpY, toX, toY)
graphics.arcTo(x1, y1, x2, y2, radius)
graphics.arc(x, y, radius, startAngle, endAngle, anticlockwise?)
graphics.closePath()
```
### Fill & Stroke
```typescript
// Fill
graphics.fill(color: number | string)
graphics.fill({ color, alpha })
graphics.fill(texture: Texture)
// Stroke
graphics.stroke({ width, color, alpha, alignment, cap, join })
// Options
{
width: number, // Line width
color: number | string, // Line color
alpha: number, // Line opacity (0-1)
alignment: number, // 0=inner, 0.5=middle, 1=outer
cap: string, // 'butt' | 'round' | 'square'
join: string // 'miter' | 'round' | 'bevel'
}
```
### Holes
```typescript
graphics.rect(0, 0, 100, 100).fill('red')
.beginHole()
.circle(50, 50, 20)
.endHole();
```
### SVG Support
```typescript
graphics.svg('<svg><path d="M 100 350 q 150 -300 300 0" /></svg>');
```
### Other Methods
```typescript
graphics.clear() // Remove all shapes
graphics.clone() // Create copy
graphics.destroy(options) // Destroy and release memory
// Context sharing
const context = new GraphicsContext().circle(50, 50, 30).fill('red');
const g1 = new Graphics(context);
const g2 = new Graphics(context); // Shares same geometry
```
### Properties
```typescript
graphics.context: GraphicsContext // Drawing instructions
graphics.pixelLine: boolean // Force 1px line width regardless of scale
graphics.fillStyle: FillStyle // Current fill style
graphics.lineStyle: StrokeStyle // Current line style
```
### Example
```typescript
import { Graphics } from 'pixi.js';
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
graphics.star(600, 300, 5, 50).fill(0xffdf00);
// Custom path
graphics
.moveTo(100, 400)
.bezierCurveTo(150, 300, 250, 300, 300, 400)
.stroke({ width: 3, color: 'white' });
// Hole
graphics.rect(450, 400, 150, 100).fill('red')
.beginHole()
.circle(525, 450, 30)
.endHole();
app.stage.addChild(graphics);
```
**API References**:
- Graphics: https://pixijs.download/release/docs/scene.Graphics.html
- GraphicsContext: https://pixijs.download/release/docs/scene.GraphicsContext.html
- FillStyle: https://pixijs.download/release/docs/scene.FillStyle.html
- StrokeStyle: https://pixijs.download/release/docs/scene.StrokeStyle.html
---
## Container
Display object that can contain children (like a group).
### Constructor
```typescript
new Container()
```
### Children Management
```typescript
container.addChild(child: DisplayObject)
container.addChildAt(child: DisplayObject, index: number)
container.removeChild(child: DisplayObject)
container.removeChildAt(index: number)
container.removeChildren(beginIndex?, endIndex?)
container.getChildAt(index: number)
container.getChildIndex(child: DisplayObject)
container.setChildIndex(child: DisplayObject, index: number)
container.swapChildren(child1: DisplayObject, child2: DisplayObject)
```
### Properties
```typescript
container.children: DisplayObject[] // Array of children
container.width: number // Combined width of children
container.height: number // Combined height of children
container.sortableChildren: boolean // Enable z-index sorting
container.interactiveChildren: boolean // Enable child interaction
```
### Filters
```typescript
container.filters: Filter[] // Array of filters
container.filterArea: Rectangle // Filter bounding box
```
### Iteration
```typescript
for (const child of container.children) {
// Process child
}
container.children.forEach(child => {
// Process child
});
```
### Example
```typescript
import { Container, Sprite } from 'pixi.js';
const container = new Container();
container.position.set(100, 100);
// Add children
const sprite1 = Sprite.from('image1.png');
const sprite2 = Sprite.from('image2.png');
sprite2.x = 50;
container.addChild(sprite1, sprite2);
// Z-index sorting
container.sortableChildren = true;
sprite1.zIndex = 2;
sprite2.zIndex = 1; // Renders behind sprite1
app.stage.addChild(container);
```
---
## ParticleContainer
Optimized container for rendering thousands of sprites with limited transform capabilities.
### Constructor
```typescript
new ParticleContainer(options?: ParticleContainerOptions)
```
**Options**:
```typescript
{
maxSize: number, // Max particles (default: 1500)
dynamicProperties: {
position: boolean, // Allow position updates (default: true)
scale: boolean, // Allow scale updates (default: false)
rotation: boolean, // Allow rotation updates (default: false)
color: boolean // Allow tint/alpha updates (default: false)
}
}
```
### Methods
```typescript
container.addParticle(particle: Particle)
container.removeParticle(particle: Particle)
container.update() // Call if changing static properties
container.destroy()
```
### Properties
```typescript
container.maxSize: number // Maximum particle count
container.dynamicProperties: object // Which properties can change
container.particleChildren: Particle[] // Array of particles
```
### Particle Interface
```typescript
interface IParticle {
x: number;
y: number;
scaleX: number;
scaleY: number;
anchorX: number;
anchorY: number;
rotation: number;
color: number; // Tint
texture: Texture;
}
// Create particle
const particle = new Particle({
texture: Texture.from('spark.png'),
x: 100,
y: 200,
scaleX: 0.5,
scaleY: 0.5,
rotation: 0,
tint: 0xffffff,
alpha: 1.0
});
```
### Example
```typescript
import { ParticleContainer, Particle, Texture } from 'pixi.js';
const texture = Texture.from('particle.png');
const particles = new ParticleContainer({
maxSize: 10000,
dynamicProperties: {
position: true, // Update positions
scale: true, // Update scale
rotation: false, // Static rotation
color: false // Static color
}
});
for (let i = 0; i < 10000; i++) {
const particle = new Particle({
texture,
x: Math.random() * 800,
y: Math.random() * 600
});
particles.addParticle(particle);
}
app.stage.addChild(particles);
// Update loop
app.ticker.add(() => {
particles.particleChildren.forEach(p => {
p.y += 1; // Move down
if (p.y > 600) p.y = 0;
});
});
```
---
## Filters
WebGL shader-based effects applied to display objects.
### Built-in Filters
#### BlurFilter
```typescript
import { BlurFilter } from 'pixi.js';
const blur = new BlurFilter({
strength: 8, // Blur amount (default: 8)
quality: 4, // Iterations (default: 4)
kernelSize: 5 // Sample size: 5, 7, 9, 11, 13, 15
});
sprite.filters = [blur];
```
#### ColorMatrixFilter
```typescript
import { ColorMatrixFilter } from 'pixi.js';
const colorMatrix = new ColorMatrixFilter();
// Presets
colorMatrix.greyscale(0.5); // 0-1
colorMatrix.sepia();
colorMatrix.blackAndWhite();
colorMatrix.contrast(1.5); // >1 increases
colorMatrix.saturate(2); // -1 to 1
colorMatrix.brightness(1.2); // >1 brightens
colorMatrix.hue(45); // Rotate hue (degrees)
colorMatrix.negative();
colorMatrix.kodachrome();
colorMatrix.technicolor();
colorMatrix.polaroid();
colorMatrix.vintage();
sprite.filters = [colorMatrix];
```
#### DisplacementFilter
```typescript
import { DisplacementFilter, Sprite } from 'pixi.js';
const displacementSprite = Sprite.from('displacement.jpg');
const displacementFilter = new DisplacementFilter({
sprite: displacementSprite,
scale: 50 // Displacement amount
});
sprite.filters = [displacementFilter];
// Animate displacement
app.ticker.add(() => {
displacementSprite.x += 1;
});
```
#### AlphaFilter
```typescript
import { AlphaFilter } from 'pixi.js';
const alphaFilter = new AlphaFilter(0.5); // 0-1
container.filters = [alphaFilter]; // Flattens alpha across children
```
#### NoiseFilter
```typescript
import { NoiseFilter } from 'pixi.js';
const noise = new NoiseFilter({
noise: 0.5, // Amount (0-1)
seed: Math.random()
});
sprite.filters = [noise];
```
#### FXAAFilter
```typescript
import { FXAAFilter } from 'pixi.js';
const fxaa = new FXAAFilter();
sprite.filters = [fxaa]; // Anti-aliasing
```
### Custom Filters
```typescript
import { Filter, GlProgram } from 'pixi.js';
const vertex = `...`; // Vertex shader
const fragment = `...`; // Fragment shader
const customFilter = new Filter({
glProgram: new GlProgram({ vertex, fragment }),
resources: {
customUniforms: {
uTime: { value: 0.0, type: 'f32' },
uColor: { value: [1.0, 0.0, 0.0], type: 'vec3<f32>' }
}
}
});
sprite.filters = [customFilter];
// Update uniforms
app.ticker.add((ticker) => {
customFilter.resources.customUniforms.uniforms.uTime += 0.01 * ticker.deltaTime;
});
```
### Filter Optimization
```typescript
// Specify filterArea to avoid runtime measurement
sprite.filterArea = new Rectangle(0, 0, sprite.width, sprite.height);
// Release filters
sprite.filters = null;
// Generate filtered texture (apply once)
const filteredTexture = renderer.filters.generateFilteredTexture({
texture: originalTexture,
filters: [blurFilter]
});
```
**API References**:
- BlurFilter: @pixi/filter-blur
- ColorMatrixFilter: @pixi/filter-color-matrix
- DisplacementFilter: @pixi/filter-displacement
- AlphaFilter: @pixi/filter-alpha
- NoiseFilter: @pixi/filter-noise
- FXAAFilter: @pixi/filter-fxaa
---
## Text
Render styled text as a texture.
### Text
Standard text rendering:
```typescript
import { Text, TextStyle } from 'pixi.js';
const style = new TextStyle({
fontFamily: 'Arial',
fontSize: 36,
fontStyle: 'italic',
fontWeight: 'bold',
fill: '#ffffff',
stroke: { color: '#000000', width: 4 },
dropShadow: {
alpha: 0.5,
angle: Math.PI / 6,
blur: 4,
color: '#000000',
distance: 6
},
wordWrap: true,
wordWrapWidth: 400,
align: 'center',
filters: [new BlurFilter()] // Bake filter into texture
});
const text = new Text({
text: 'Hello PixiJS!',
style
});
text.position.set(100, 100);
app.stage.addChild(text);
// Update text
text.text = 'New text';
// Adjust resolution
text.resolution = 2; // Higher = sharper but more memory
```
### BitmapText
High-performance text for dynamic content:
```typescript
import { BitmapText } from 'pixi.js';
// Requires bitmap font asset
const bitmapText = new BitmapText({
text: 'Score: 0',
style: {
fontFamily: 'MyBitmapFont',
fontSize: 24,
tint: 0xff0000
}
});
app.stage.addChild(bitmapText);
// Update frequently (very fast)
app.ticker.add(() => {
bitmapText.text = `Score: ${++score}`;
});
```
### TextStyle Options
```typescript
interface TextStyleOptions {
// Font
fontFamily: string | string[];
fontSize: number | string;
fontStyle: 'normal' | 'italic' | 'oblique';
fontWeight: 'normal' | 'bold' | '100-900';
// Fill
fill: number | string | string[] | number[]; // Gradient support
fillGradientStops: number[];
// Stroke
stroke: { color: number | string, width: number, alpha?: number };
// Shadow
dropShadow: {
alpha: number,
angle: number,
blur: number,
color: number | string,
distance: number
};
// Layout
align: 'left' | 'center' | 'right' | 'justify';
wordWrap: boolean;
wordWrapWidth: number;
breakWords: boolean;
lineHeight: number;
letterSpacing: number;
leading: number;
// Other
padding: number;
trim: boolean;
whiteSpace: 'normal' | 'pre' | 'pre-line';
}
```
---
## Assets
Asset loading and management system.
### Loading Assets
```typescript
import { Assets } from 'pixi.js';
// Load single asset
const texture = await Assets.load('image.png');
const spritesheet = await Assets.load('spritesheet.json');
// Load multiple assets
const assets = await Assets.load([
'image1.png',
'image2.png',
'sound.mp3'
]);
// Load with aliases
await Assets.add({ alias: 'hero', src: 'hero.png' });
const heroTexture = await Assets.load('hero');
// Load bundle
Assets.addBundle('game', {
player: 'player.png',
enemy: 'enemy.png',
background: 'bg.jpg'
});
const bundle = await Assets.loadBundle('game');
// Access loaded assets
const playerTexture = Assets.get('player');
```
### Progress Tracking
```typescript
Assets.load('large-file.png', (progress) => {
console.log(`Loading: ${Math.round(progress * 100)}%`);
});
// Or with promises
const promise = Assets.load(['file1.png', 'file2.png']);
promise.progress = (progress) => {
console.log(`Progress: ${progress * 100}%`);
};
await promise;
```
### Background Loading
```typescript
// Load in background (non-blocking)
Assets.backgroundLoad(['asset1.png', 'asset2.png']);
// Check if loaded
if (Assets.cache.has('asset1.png')) {
const texture = Assets.get('asset1.png');
}
```
### Unloading Assets
```typescript
// Unload single asset
await Assets.unload('image.png');
// Unload bundle
await Assets.unloadBundle('game');
// Clear cache
Assets.reset();
```
---
## Renderer
Low-level rendering system (WebGL/WebGPU).
### Properties
```typescript
renderer.type: string // 'webgl' | 'webgpu'
renderer.width: number
renderer.height: number
renderer.resolution: number
renderer.backgroundColor: number
renderer.backgroundAlpha: number
```
### Methods
```typescript
// Manual rendering
renderer.render(container);
// Resize
renderer.resize(width, height);
// Clear
renderer.clear();
// Generate texture from display object
const texture = renderer.generateTexture(displayObject, {
resolution: 1,
frame: new Rectangle(0, 0, 100, 100)
});
// Destroy
renderer.destroy();
```
---
## DisplayObject
Base class for all renderable objects (Sprite, Graphics, Container, etc.).
### Transform Properties
```typescript
displayObject.position: ObservablePoint // x, y
displayObject.scale: ObservablePoint // x, y scale
displayObject.rotation: number // Radians
displayObject.pivot: ObservablePoint // Rotation pivot
displayObject.skew: ObservablePoint // x, y skew
displayObject.angle: number // Degrees (converts to rotation)
```
### Visibility
```typescript
displayObject.alpha: number // 0-1 opacity
displayObject.visible: boolean // Show/hide
displayObject.renderable: boolean // Render flag
displayObject.cullable: boolean // Viewport culling
displayObject.mask: Graphics | Sprite // Masking
```
### Hierarchy
```typescript
displayObject.parent: Container
displayObject.children: DisplayObject[] // If Container
displayObject.zIndex: number // Render order (if sortableChildren enabled)
displayObject.removeFromParent()
displayObject.destroy(options)
```
### Bounds
```typescript
displayObject.getBounds() // Global bounds
displayObject.getLocalBounds() // Local bounds
displayObject.width: number // Bounding width
displayObject.height: number // Bounding height
```
### Interaction
```typescript
displayObject.eventMode: string // 'none' | 'passive' | 'static' | 'dynamic'
displayObject.cursor: string // CSS cursor
displayObject.hitArea: Shape // Custom hit detection area
displayObject.interactive: boolean // Enable events (deprecated, use eventMode)
```
---
## Events
Interactive event system.
### Event Modes
```typescript
sprite.eventMode = 'static'; // Enable interaction
sprite.eventMode = 'dynamic'; // Enable + propagate to children
sprite.eventMode = 'passive'; // Receive events but don't block
sprite.eventMode = 'none'; // No interaction (default)
```
### Mouse Events
```typescript
sprite.on('pointerdown', (event) => {
console.log('Clicked at:', event.global.x, event.global.y);
});
sprite.on('pointerup', handler);
sprite.on('pointermove', handler);
sprite.on('pointerover', handler); // Mouse enter
sprite.on('pointerout', handler); // Mouse leave
sprite.on('pointerupoutside', handler); // Released outside
// Once
sprite.once('pointerdown', handler);
// Remove
sprite.off('pointerdown', handler);
sprite.removeAllListeners();
```
### Touch Events
```typescript
sprite.on('touchstart', handler);
sprite.on('touchend', handler);
sprite.on('touchmove', handler);
sprite.on('tap', handler);
```
### Event Object
```typescript
interface FederatedPointerEvent {
global: Point; // Global coordinates
client: Point; // Client coordinates
screen: Point; // Screen coordinates
movement: Point; // Delta movement
page: Point; // Page coordinates
button: number; // Mouse button (0=left, 1=middle, 2=right)
buttons: number; // Bitmask of pressed buttons
target: DisplayObject; // Event target
currentTarget: DisplayObject;
type: string; // Event type
preventDefault(): void;
stopPropagation(): void;
}
```
### Custom Cursor
```typescript
sprite.cursor = 'pointer';
sprite.cursor = 'grab';
sprite.cursor = 'help';
```
### Hit Area
```typescript
import { Rectangle, Circle, Polygon } from 'pixi.js';
// Rectangle hit area
sprite.hitArea = new Rectangle(0, 0, 100, 100);
// Circle hit area
sprite.hitArea = new Circle(50, 50, 30);
// Polygon hit area
sprite.hitArea = new Polygon([0,0, 100,0, 100,100, 0,100]);
```
---
## Utility Classes
### Rectangle
```typescript
const rect = new Rectangle(x, y, width, height);
rect.contains(x, y);
rect.intersects(otherRect);
```
### Circle
```typescript
const circle = new Circle(x, y, radius);
circle.contains(x, y);
```
### Point
```typescript
const point = new Point(x, y);
point.set(x, y);
point.clone();
point.equals(otherPoint);
```
### ObservablePoint
```typescript
const observable = new ObservablePoint(callback, scope);
observable.set(x, y);
observable.x = 100; // Triggers callback
```
---
## Performance APIs
### CacheAsBitmap
```typescript
// Convert to texture for faster rendering
displayObject.cacheAsBitmap = true;
// Disable when updating frequently
displayObject.cacheAsBitmap = false;
```
### Ticker
```typescript
import { Ticker } from 'pixi.js';
const ticker = Ticker.shared;
ticker.add((delta) => {
// Update logic
// delta = time since last frame
});
ticker.speed = 0.5; // Half speed
ticker.maxFPS = 30; // Cap at 30 FPS
ticker.minFPS = 10; // Min for deltaTime calculation
ticker.stop();
ticker.start();
```
---
## Constants
### Blend Modes
```typescript
import { BLEND_MODES } from 'pixi.js';
sprite.blendMode = BLEND_MODES.NORMAL;
sprite.blendMode = BLEND_MODES.ADD;
sprite.blendMode = BLEND_MODES.MULTIPLY;
sprite.blendMode = BLEND_MODES.SCREEN;
sprite.blendMode = BLEND_MODES.OVERLAY;
sprite.blendMode = BLEND_MODES.DARKEN;
sprite.blendMode = BLEND_MODES.LIGHTEN;
sprite.blendMode = BLEND_MODES.COLOR_DODGE;
sprite.blendMode = BLEND_MODES.COLOR_BURN;
sprite.blendMode = BLEND_MODES.HARD_LIGHT;
sprite.blendMode = BLEND_MODES.SOFT_LIGHT;
sprite.blendMode = BLEND_MODES.DIFFERENCE;
sprite.blendMode = BLEND_MODES.EXCLUSION;
sprite.blendMode = BLEND_MODES.HUE;
sprite.blendMode = BLEND_MODES.SATURATION;
sprite.blendMode = BLEND_MODES.COLOR;
sprite.blendMode = BLEND_MODES.LUMINOSITY;
```
### Scale Modes
```typescript
import { SCALE_MODES } from 'pixi.js';
texture.baseTexture.scaleMode = SCALE_MODES.LINEAR; // Smooth (default)
texture.baseTexture.scaleMode = SCALE_MODES.NEAREST; // Pixelated
```
---
## TypeScript Support
PixiJS is written in TypeScript and provides full type definitions.
```typescript
import { Application, Sprite, Texture, Container } from 'pixi.js';
const app: Application = new Application();
const sprite: Sprite = new Sprite(Texture.WHITE);
const container: Container = new Container();
```
---
## Official API Documentation
- **Main Docs**: https://pixijs.download/release/docs/
- **Examples**: https://pixijs.io/examples/
- **GitHub**: https://github.com/pixijs/pixijs
---
This API reference covers PixiJS v8+ core functionality. For advanced features, plugins, and detailed shader programming, consult the official documentation.
references/filters_effects.md
# PixiJS Filters & Visual Effects Guide
Comprehensive guide to using and creating visual effects with PixiJS filters and shaders.
---
## Table of Contents
1. [Filter Basics](#filter-basics)
2. [Built-in Filters](#built-in-filters)
3. [Custom Filters](#custom-filters)
4. [Shader Programming](#shader-programming)
5. [Effect Combinations](#effect-combinations)
6. [Performance Tips](#performance-tips)
---
## Filter Basics
### Applying Filters
Filters are WebGL/WebGPU shader programs applied to display objects after rendering.
```javascript
import { BlurFilter, Sprite } from 'pixi.js';
const sprite = Sprite.from('image.png');
// Single filter
sprite.filters = [new BlurFilter()];
// Multiple filters (applied in order)
sprite.filters = [
new BlurFilter({ strength: 4 }),
new ColorMatrixFilter()
];
// Remove filters
sprite.filters = null;
```
### Filter Area Optimization
```javascript
import { Rectangle } from 'pixi.js';
// Specify filter bounds for performance
sprite.filterArea = new Rectangle(0, 0, sprite.width, sprite.height);
// PixiJS won't need to measure bounds at runtime
```
### Filter on Containers
```javascript
import { Container } from 'pixi.js';
const container = new Container();
container.addChild(sprite1, sprite2, sprite3);
// Filter applied to entire container
container.filters = [new BlurFilter()];
```
---
## Built-in Filters
### BlurFilter
Gaussian blur effect for depth of field, motion blur, or soft focus.
```javascript
import { BlurFilter } from 'pixi.js';
const blur = new BlurFilter({
strength: 8, // Blur radius (default: 8)
quality: 4, // Number of passes (1-5, default: 4)
kernelSize: 5 // Sample size: 5, 7, 9, 11, 13, 15 (default: 5)
});
sprite.filters = [blur];
// Adjust blur dynamically
app.ticker.add(() => {
blur.blur = 5 + Math.sin(Date.now() * 0.001) * 5; // Pulsing blur
});
```
**Use Cases**:
- Depth of field effects
- Focus/unfocus transitions
- Motion blur
- Background blur (foreground sharp)
**Performance**: Higher quality and kernelSize = slower. Use lower values for real-time effects.
---
### ColorMatrixFilter
Transform colors using matrix multiplication. Includes preset effects.
```javascript
import { ColorMatrixFilter } from 'pixi.js';
const colorMatrix = new ColorMatrixFilter();
// Grayscale
colorMatrix.greyscale(0.5); // 0 = color, 1 = full grayscale
// Sepia tone
colorMatrix.sepia();
// Black and white
colorMatrix.blackAndWhite();
// Adjust contrast
colorMatrix.contrast(1.5); // >1 = more contrast
// Adjust saturation
colorMatrix.saturate(0.5); // <1 = desaturate, >1 = supersaturate
// Adjust brightness
colorMatrix.brightness(1.2); // >1 = brighter
// Hue rotation
colorMatrix.hue(45); // Rotate hue in degrees
// Negative (invert)
colorMatrix.negative();
// Vintage film effects
colorMatrix.kodachrome();
colorMatrix.technicolor();
colorMatrix.polaroid();
colorMatrix.vintage();
sprite.filters = [colorMatrix];
```
**Chaining Effects**:
```javascript
colorMatrix.greyscale(0.3).contrast(1.2).brightness(1.1);
```
**Custom Color Matrix**:
```javascript
// 5x4 color matrix [R, G, B, A, offset]
const matrix = [
1, 0, 0, 0, 0, // Red
0, 1, 0, 0, 0, // Green
0, 0, 1, 0, 0, // Blue
0, 0, 0, 1, 0 // Alpha
];
colorMatrix.matrix = matrix;
```
**Use Cases**:
- Photo filters (Instagram-style)
- Color grading
- Night vision effect
- Damage/flash effects
---
### DisplacementFilter
Warp/distort pixels based on a displacement map texture.
```javascript
import { DisplacementFilter, Sprite } from 'pixi.js';
// Create displacement sprite (usually perlin noise or cloud texture)
const displacementSprite = Sprite.from('displacement.jpg');
displacementSprite.texture.baseTexture.wrapMode = WRAP_MODES.REPEAT;
const displacementFilter = new DisplacementFilter({
sprite: displacementSprite,
scale: 50 // Displacement amount
});
sprite.filters = [displacementFilter];
app.stage.addChild(displacementSprite);
// Animate displacement
app.ticker.add(() => {
displacementSprite.x += 1;
displacementSprite.y += 0.5;
});
```
**Parameters**:
- `sprite`: Displacement map (red channel = X offset, green channel = Y offset)
- `scale`: Displacement intensity (default: 20)
**Use Cases**:
- Water ripple effects
- Heat distortion
- Portal effects
- Liquid/jelly animations
- Flag waving
**Creating Displacement Maps**:
```javascript
// Generate noise texture
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
// Draw perlin noise or clouds
// ... (use simplex-noise library or draw manually)
const displacementTexture = Texture.from(canvas);
const displacementSprite = new Sprite(displacementTexture);
```
---
### AlphaFilter
Flatten alpha across all children in a container.
```javascript
import { AlphaFilter, Container } from 'pixi.js';
const container = new Container();
container.addChild(sprite1, sprite2, sprite3);
const alphaFilter = new AlphaFilter(0.5); // 50% opacity
container.filters = [alphaFilter];
// Without filter: each sprite has individual alpha
// With filter: entire container rendered at 50% alpha
```
**Use Cases**:
- Fade entire UI panel
- Composite transparency
- Layer blending
---
### NoiseFilter
Add random grain/noise for film grain or static effects.
```javascript
import { NoiseFilter } from 'pixi.js';
const noise = new NoiseFilter({
noise: 0.5, // Amount (0-1, default: 0.5)
seed: Math.random() // Random seed
});
sprite.filters = [noise];
// Animated noise
app.ticker.add(() => {
noise.seed = Math.random();
});
```
**Use Cases**:
- Film grain
- Old TV static
- Analog distortion
- Glitch effects
---
### FXAAFilter
Fast approximate anti-aliasing for smooth edges.
```javascript
import { FXAAFilter } from 'pixi.js';
const fxaa = new FXAAFilter();
sprite.filters = [fxaa];
```
**Use Cases**:
- Smooth jagged edges
- Improve visual quality on low-res displays
- Reduce aliasing artifacts
---
## Custom Filters
### Creating a Custom Filter
Custom filters use GLSL shaders for GPU-accelerated effects.
```javascript
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 waveFilter = new Filter({
glProgram: new GlProgram({ vertex, fragment }),
resources: {
timeUniforms: {
uTime: { value: 0.0, type: 'f32' }
}
}
});
sprite.filters = [waveFilter];
// Update uniform
app.ticker.add((ticker) => {
waveFilter.resources.timeUniforms.uniforms.uTime += 0.04 * ticker.deltaTime;
});
```
---
### Example: Pixelate Filter
```javascript
const pixelateVertex = `
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 pixelateFragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform vec2 uSize;
uniform float uPixelSize;
void main() {
vec2 coord = vTextureCoord * uSize;
vec2 pixelCoord = floor(coord / uPixelSize) * uPixelSize;
vec2 pixelUV = pixelCoord / uSize;
gl_FragColor = texture(uTexture, pixelUV);
}
`;
class PixelateFilter extends Filter {
constructor(pixelSize = 10) {
super({
glProgram: new GlProgram({
vertex: pixelateVertex,
fragment: pixelateFragment
}),
resources: {
pixelateUniforms: {
uSize: { value: new Float32Array([800, 600]), type: 'vec2<f32>' },
uPixelSize: { value: pixelSize, type: 'f32' }
}
}
});
}
get pixelSize() {
return this.resources.pixelateUniforms.uniforms.uPixelSize;
}
set pixelSize(value) {
this.resources.pixelateUniforms.uniforms.uPixelSize = value;
}
}
// Usage
const pixelate = new PixelateFilter(5);
sprite.filters = [pixelate];
// Animate
app.ticker.add(() => {
pixelate.pixelSize = 5 + Math.sin(Date.now() * 0.001) * 4;
});
```
---
### Example: Chromatic Aberration
```javascript
const chromaticFragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uAmount;
void main() {
vec2 uv = vTextureCoord;
// Offset RGB channels
float r = texture(uTexture, uv + vec2(uAmount, 0.0)).r;
float g = texture(uTexture, uv).g;
float b = texture(uTexture, uv - vec2(uAmount, 0.0)).b;
gl_FragColor = vec4(r, g, b, 1.0);
}
`;
class ChromaticAberrationFilter extends Filter {
constructor(amount = 0.005) {
super({
glProgram: new GlProgram({
vertex: defaultVertex, // Use default vertex shader
fragment: chromaticFragment
}),
resources: {
chromaticUniforms: {
uAmount: { value: amount, type: 'f32' }
}
}
});
}
get amount() {
return this.resources.chromaticUniforms.uniforms.uAmount;
}
set amount(value) {
this.resources.chromaticUniforms.uniforms.uAmount = value;
}
}
// Usage
const chromatic = new ChromaticAberrationFilter(0.01);
sprite.filters = [chromatic];
```
---
### Example: Vignette Filter
```javascript
const vignetteFragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uIntensity;
uniform float uSoftness;
void main() {
vec4 color = texture(uTexture, vTextureCoord);
vec2 uv = vTextureCoord - 0.5;
float dist = length(uv);
float vignette = smoothstep(uIntensity, uIntensity - uSoftness, dist);
gl_FragColor = vec4(color.rgb * vignette, color.a);
}
`;
class VignetteFilter extends Filter {
constructor(intensity = 0.5, softness = 0.3) {
super({
glProgram: new GlProgram({
vertex: defaultVertex,
fragment: vignetteFragment
}),
resources: {
vignetteUniforms: {
uIntensity: { value: intensity, type: 'f32' },
uSoftness: { value: softness, type: 'f32' }
}
}
});
}
get intensity() {
return this.resources.vignetteUniforms.uniforms.uIntensity;
}
set intensity(value) {
this.resources.vignetteUniforms.uniforms.uIntensity = value;
}
get softness() {
return this.resources.vignetteUniforms.uniforms.uSoftness;
}
set softness(value) {
this.resources.vignetteUniforms.uniforms.uSoftness = value;
}
}
```
---
## Shader Programming
### GLSL Basics
**Data Types**:
```glsl
float x = 1.0;
vec2 position = vec2(0.5, 0.5);
vec3 color = vec3(1.0, 0.0, 0.0); // RGB
vec4 rgba = vec4(1.0, 0.0, 0.0, 1.0); // RGBA
sampler2D texture; // Texture sampler
```
**Built-in Functions**:
```glsl
// Math
sin(x), cos(x), tan(x)
abs(x), sign(x)
floor(x), ceil(x), fract(x)
min(a, b), max(a, b), clamp(x, min, max)
mix(a, b, t) // Linear interpolation
smoothstep(edge0, edge1, x) // Smooth interpolation
// Vector
length(v) // Vector length
distance(a, b) // Distance between vectors
dot(a, b) // Dot product
normalize(v) // Unit vector
// Texture sampling
texture(sampler, uv) // Sample texture at UV coordinates
```
**Vertex Shader Template**:
```glsl
in vec2 aPosition;
out vec2 vTextureCoord;
uniform vec4 uInputSize;
uniform vec4 uOutputFrame;
uniform vec4 uOutputTexture;
void main() {
gl_Position = filterVertexPosition();
vTextureCoord = filterTextureCoord();
}
```
**Fragment Shader Template**:
```glsl
in vec2 vTextureCoord;
uniform sampler2D uTexture;
void main() {
vec4 color = texture(uTexture, vTextureCoord);
// Modify color
color.rgb *= 0.5; // Darken
gl_FragColor = color;
}
```
---
### Uniforms
Pass data from JavaScript to shaders.
```javascript
const filter = new Filter({
glProgram: new GlProgram({ vertex, fragment }),
resources: {
customUniforms: {
uTime: { value: 0.0, type: 'f32' },
uColor: { value: [1.0, 0.0, 0.0], type: 'vec3<f32>' },
uPosition: { value: new Float32Array([0.5, 0.5]), type: 'vec2<f32>' },
uTexture2: { value: secondTexture, type: 'sampler2D' }
}
}
});
// Access uniforms
filter.resources.customUniforms.uniforms.uTime = 5.0;
filter.resources.customUniforms.uniforms.uColor = [0.0, 1.0, 0.0];
```
**In Shader**:
```glsl
uniform float uTime;
uniform vec3 uColor;
uniform vec2 uPosition;
uniform sampler2D uTexture2;
void main() {
// Use uniforms
float wave = sin(vTextureCoord.y * 10.0 + uTime);
vec4 color = texture(uTexture, vTextureCoord) * vec4(uColor, 1.0);
gl_FragColor = color;
}
```
---
### Multi-Pass Filters
Apply multiple shader passes for complex effects.
```javascript
class MultiPassFilter extends Filter {
constructor() {
// First pass: Blur horizontal
const pass1 = new Filter({
glProgram: new GlProgram({ vertex: defaultVertex, fragment: blurHorizontalFragment })
});
// Second pass: Blur vertical
const pass2 = new Filter({
glProgram: new GlProgram({ vertex: defaultVertex, fragment: blurVerticalFragment })
});
// Combine passes
super({
glProgram: new GlProgram({ vertex: defaultVertex, fragment: combineFragment }),
resources: {
pass1Texture: { value: null, type: 'sampler2D' },
pass2Texture: { value: null, type: 'sampler2D' }
}
});
}
}
```
---
## Effect Combinations
### Glow Effect
Blur + Additive Blend
```javascript
import { BlurFilter, BLEND_MODES } from 'pixi.js';
// Original sprite
const sprite = Sprite.from('star.png');
// Glow sprite (blurred copy)
const glowSprite = new Sprite(sprite.texture);
glowSprite.filters = [new BlurFilter({ strength: 15 })];
glowSprite.blendMode = BLEND_MODES.ADD;
glowSprite.alpha = 0.8;
const container = new Container();
container.addChild(glowSprite, sprite); // Glow behind
app.stage.addChild(container);
```
---
### Outline Effect
Multiple displacement passes
```javascript
const outlineFragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uThickness;
uniform vec3 uColor;
void main() {
vec4 color = texture(uTexture, vTextureCoord);
float alpha = color.a;
// Sample neighboring pixels
alpha += texture(uTexture, vTextureCoord + vec2(uThickness, 0.0)).a;
alpha += texture(uTexture, vTextureCoord - vec2(uThickness, 0.0)).a;
alpha += texture(uTexture, vTextureCoord + vec2(0.0, uThickness)).a;
alpha += texture(uTexture, vTextureCoord - vec2(0.0, uThickness)).a;
// Create outline
float outline = step(0.1, alpha) * (1.0 - color.a);
vec3 finalColor = mix(color.rgb, uColor, outline);
float finalAlpha = max(color.a, outline);
gl_FragColor = vec4(finalColor, finalAlpha);
}
`;
class OutlineFilter extends Filter {
constructor(thickness = 0.01, color = [1, 1, 1]) {
super({
glProgram: new GlProgram({ vertex: defaultVertex, fragment: outlineFragment }),
resources: {
outlineUniforms: {
uThickness: { value: thickness, type: 'f32' },
uColor: { value: color, type: 'vec3<f32>' }
}
}
});
}
}
```
---
### CRT Monitor Effect
Scanlines + chromatic aberration + curve
```javascript
const crtFragment = `
in vec2 vTextureCoord;
uniform sampler2D uTexture;
uniform float uTime;
void main() {
vec2 uv = vTextureCoord;
// Curve screen
uv = uv * 2.0 - 1.0;
uv *= 1.0 + 0.1 * dot(uv, uv);
uv = (uv + 1.0) * 0.5;
// Chromatic aberration
float r = texture(uTexture, uv + vec2(0.002, 0.0)).r;
float g = texture(uTexture, uv).g;
float b = texture(uTexture, uv - vec2(0.002, 0.0)).b;
// Scanlines
float scanline = sin(uv.y * 800.0) * 0.1 + 0.9;
// Flicker
float flicker = sin(uTime * 50.0) * 0.02 + 0.98;
vec3 color = vec3(r, g, b) * scanline * flicker;
gl_FragColor = vec4(color, 1.0);
}
`;
```
---
### Film Grain + Vignette
```javascript
sprite.filters = [
new NoiseFilter({ noise: 0.2 }),
new VignetteFilter(0.5, 0.3),
new ColorMatrixFilter().sepia()
];
```
---
## Performance Tips
### 1. Minimize Filter Count
```javascript
// ❌ BAD: Too many filters
sprite.filters = [blur1, blur2, colorMatrix, noise, vignette];
// ✅ GOOD: Combine into single custom filter
sprite.filters = [combinedFilter];
```
---
### 2. Set Filter Area
```javascript
sprite.filters = [blurFilter];
sprite.filterArea = new Rectangle(0, 0, sprite.width, sprite.height);
```
---
### 3. Bake Static Filters
```javascript
// Apply filter once, generate texture
const filteredTexture = renderer.filters.generateFilteredTexture({
texture: originalTexture,
filters: [blurFilter, colorMatrix]
});
const sprite = new Sprite(filteredTexture);
// No runtime filter cost
```
---
### 4. Use Lower Quality
```javascript
const blur = new BlurFilter({
strength: 8,
quality: 2, // Lower = faster (1-5)
kernelSize: 5 // Smaller = faster
});
```
---
### 5. Toggle Filters Based on Performance
```javascript
let filtersEnabled = true;
app.ticker.add(() => {
const fps = Math.round(1000 / app.ticker.deltaMS);
if (fps < 30 && filtersEnabled) {
sprite.filters = null; // Disable filters
filtersEnabled = false;
} else if (fps > 55 && !filtersEnabled) {
sprite.filters = [blurFilter]; // Re-enable
filtersEnabled = true;
}
});
```
---
## Filter Examples Library
### Glass/Frosted Effect
```javascript
sprite.filters = [
new BlurFilter({ strength: 10 }),
new ColorMatrixFilter().brightness(1.2)
];
sprite.alpha = 0.8;
```
### Night Vision
```javascript
const nightVision = new ColorMatrixFilter();
nightVision.greyscale(1);
nightVision.contrast(1.5);
nightVision.brightness(1.5);
sprite.filters = [nightVision];
sprite.tint = 0x00ff00; // Green tint
```
### X-Ray
```javascript
const xray = new ColorMatrixFilter();
xray.negative();
xray.contrast(2);
sprite.filters = [xray];
```
### Underwater
```javascript
sprite.filters = [
new DisplacementFilter({ sprite: waveSprite, scale: 20 }),
new ColorMatrixFilter().saturate(0.7)
];
sprite.tint = 0x88ccff;
```
---
This guide provides comprehensive coverage of PixiJS filters, from built-in options to custom shader programming for advanced visual effects.
references/performance_guide.md
# PixiJS Performance Optimization Guide
Comprehensive guide to optimizing PixiJS applications for maximum performance and smooth 60 FPS rendering.
---
## Table of Contents
1. [Performance Profiling](#performance-profiling)
2. [Rendering Optimization](#rendering-optimization)
3. [Texture Management](#texture-management)
4. [Container Optimization](#container-optimization)
5. [Filter Performance](#filter-performance)
6. [Text Rendering](#text-rendering)
7. [Memory Management](#memory-management)
8. [Mobile Optimization](#mobile-optimization)
9. [Advanced Techniques](#advanced-techniques)
---
## Performance Profiling
### Built-in Stats
```javascript
import { Application } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600 });
// Access stats
app.ticker.add(() => {
const stats = app.renderer.stats;
console.log('FPS:', Math.round(1000 / app.ticker.deltaMS));
console.log('Draw calls:', stats.drawCalls);
console.log('Texture bind count:', stats.textureCount);
console.log('Shader bind count:', stats.shaderCount);
});
```
### Custom Performance Monitor
```javascript
class PerformanceMonitor {
constructor(app) {
this.app = app;
this.frameCount = 0;
this.lastTime = performance.now();
this.fps = 60;
this.drawCalls = 0;
this.createDisplay();
this.app.ticker.add(this.update.bind(this));
}
createDisplay() {
this.container = document.createElement('div');
this.container.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.8);
color: #0f0;
padding: 10px;
font-family: monospace;
font-size: 12px;
z-index: 10000;
`;
document.body.appendChild(this.container);
}
update() {
this.frameCount++;
const now = performance.now();
if (now - this.lastTime >= 1000) {
this.fps = Math.round(this.frameCount * 1000 / (now - this.lastTime));
this.frameCount = 0;
this.lastTime = now;
const stats = this.app.renderer.stats;
this.drawCalls = stats.drawCalls.total;
this.render();
}
}
render() {
const color = this.fps >= 55 ? '#0f0' : this.fps >= 30 ? '#ff0' : '#f00';
this.container.style.color = color;
this.container.innerHTML = `
FPS: ${this.fps}<br>
Draw Calls: ${this.drawCalls}<br>
Sprites: ${this.countSprites()}<br>
Memory: ${this.getMemoryUsage()}
`;
}
countSprites() {
let count = 0;
const traverse = (container) => {
if (container.children) {
container.children.forEach(child => {
count++;
traverse(child);
});
}
};
traverse(this.app.stage);
return count;
}
getMemoryUsage() {
if (performance.memory) {
const mb = (performance.memory.usedJSHeapSize / 1048576).toFixed(2);
return `${mb} MB`;
}
return 'N/A';
}
}
// Usage
const monitor = new PerformanceMonitor(app);
```
---
## Rendering Optimization
### 1. Use ParticleContainer for Large Sprite Counts
**Problem**: Rendering 1,000+ sprites with regular Container is slow.
**Solution**: Use ParticleContainer with static properties.
```javascript
import { ParticleContainer, Particle, Texture } from 'pixi.js';
// ❌ BAD: Regular container (slow)
const container = new Container();
for (let i = 0; i < 10000; i++) {
const sprite = new Sprite(texture);
sprite.x = Math.random() * 800;
sprite.y = Math.random() * 600;
container.addChild(sprite);
}
// ✅ GOOD: ParticleContainer (10x faster)
const particles = new ParticleContainer({
maxSize: 10000,
dynamicProperties: {
position: true, // Only if you need to update positions
scale: false, // Static scale
rotation: false, // Static rotation
color: false // Static color
}
});
for (let i = 0; i < 10000; i++) {
const particle = new Particle({
texture,
x: Math.random() * 800,
y: Math.random() * 600
});
particles.addParticle(particle);
}
app.stage.addChild(particles);
```
**Performance Gain**: Up to 10x faster rendering for static properties.
---
### 2. Minimize Draw Calls
**Problem**: Each texture/shader switch triggers a new draw call.
**Solution**: Batch sprites with the same texture and blend mode.
```javascript
// ❌ BAD: Different textures interspersed
const sprites = [];
for (let i = 0; i < 100; i++) {
const tex = i % 2 === 0 ? texture1 : texture2;
sprites.push(new Sprite(tex));
}
// ✅ GOOD: Group by texture
const group1 = new Container();
const group2 = new Container();
for (let i = 0; i < 50; i++) {
group1.addChild(new Sprite(texture1));
group2.addChild(new Sprite(texture2));
}
app.stage.addChild(group1, group2);
```
**Tip**: Use sprite atlases (texture packing) to combine multiple images into one texture.
---
### 3. Enable Culling for Off-Screen Objects
**Problem**: Rendering objects outside viewport wastes GPU cycles.
**Solution**: Enable viewport culling.
```javascript
import { Application } from 'pixi.js';
const app = new Application();
await app.init({
width: 800,
height: 600,
cullable: true // Enable automatic culling
});
// Or per-object
sprite.cullable = true;
// Manual culling
app.ticker.add(() => {
const bounds = app.screen;
sprites.forEach(sprite => {
const spriteBounds = sprite.getBounds();
// Check if sprite is in viewport
sprite.renderable = (
spriteBounds.x < bounds.width &&
spriteBounds.x + spriteBounds.width > 0 &&
spriteBounds.y < bounds.height &&
spriteBounds.y + spriteBounds.height > 0
);
});
});
```
**CullerPlugin**:
```javascript
// Automatic viewport culling plugin
import { CullerPlugin } from 'pixi.js';
// Enabled by default in Application
app.cullable = true;
```
**Performance Gain**: Up to 50% for scenes with many off-screen objects.
---
### 4. Cache Static Graphics as Bitmaps
**Problem**: Complex vector graphics re-render every frame.
**Solution**: Convert to texture using `cacheAsBitmap`.
```javascript
import { Graphics } from 'pixi.js';
const complexShape = new Graphics();
// Draw many shapes
for (let i = 0; i < 100; i++) {
complexShape.circle(
Math.random() * 200,
Math.random() * 200,
Math.random() * 10
).fill(Math.random() * 0xffffff);
}
// ✅ Cache as bitmap for static graphics
complexShape.cacheAsBitmap = true;
// ❌ Don't use for frequently changing graphics
// complexShape.cacheAsBitmap = false; // If updating often
```
**When to Use**:
- ✅ Static UI elements
- ✅ Backgrounds
- ✅ Complex shapes that don't change
- ❌ Animated graphics
- ❌ Frequently updated elements
---
### 5. Reduce Resolution on Low-End Devices
**Problem**: High-resolution rendering on mobile drains battery and causes lag.
**Solution**: Adjust resolution based on device capabilities.
```javascript
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
const resolution = isMobile ? 1 : window.devicePixelRatio || 1;
const app = new Application();
await app.init({
width: 800,
height: 600,
resolution,
autoDensity: true
});
// Dynamic resolution scaling
function adjustResolution() {
const fps = Math.round(1000 / app.ticker.deltaMS);
if (fps < 30 && app.renderer.resolution > 1) {
app.renderer.resolution *= 0.9;
} else if (fps > 55 && app.renderer.resolution < window.devicePixelRatio) {
app.renderer.resolution = Math.min(app.renderer.resolution * 1.1, window.devicePixelRatio);
}
}
app.ticker.add(adjustResolution);
```
---
## Texture Management
### 1. Destroy Unused Textures
**Problem**: Textures consume GPU memory even when not displayed.
**Solution**: Explicitly destroy textures when done.
```javascript
import { Texture } from 'pixi.js';
const texture = Texture.from('image.png');
const sprite = new Sprite(texture);
// When done
sprite.destroy({ texture: true, baseTexture: true });
// Or destroy texture directly
texture.destroy(true); // true = also destroy baseTexture
```
**Batch Destruction with Delay**:
```javascript
// Prevent frame drops by staggering destruction
const textures = [tex1, tex2, tex3, tex4];
textures.forEach((tex, index) => {
setTimeout(() => {
tex.destroy(true);
}, index * 50 + Math.random() * 50);
});
```
---
### 2. Use Texture Atlases (Sprite Sheets)
**Problem**: Loading many individual images causes numerous HTTP requests and draw calls.
**Solution**: Pack images into sprite sheets.
```javascript
import { Assets, Sprite } from 'pixi.js';
// Load sprite sheet
await Assets.load('spritesheet.json');
// Access individual frames
const texture1 = Texture.from('frame1.png');
const texture2 = Texture.from('frame2.png');
const sprite1 = new Sprite(texture1);
const sprite2 = new Sprite(texture2);
// All batched in single draw call
app.stage.addChild(sprite1, sprite2);
```
**Tools for Creating Sprite Sheets**:
- TexturePacker: https://www.codeandweb.com/texturepacker
- ShoeBox: https://renderhjs.net/shoebox/
- Free Texture Packer: https://free-tex-packer.com/
---
### 3. Optimize Texture Sizes
**Problem**: Large textures consume excessive memory.
**Solution**: Use appropriate sizes and compression.
```javascript
// ❌ BAD: 4096x4096 texture (64MB RGBA)
const hugeTexture = Texture.from('huge-image-4k.png');
// ✅ GOOD: 1024x1024 texture (4MB RGBA)
const optimizedTexture = Texture.from('optimized-image-1k.png');
// Power-of-2 sizes for best performance
// Good sizes: 256, 512, 1024, 2048
// Avoid odd sizes: 300, 500, 1500
// Use NEAREST for pixel art
texture.baseTexture.scaleMode = SCALE_MODES.NEAREST;
// Use LINEAR for photos
texture.baseTexture.scaleMode = SCALE_MODES.LINEAR;
```
---
### 4. Lazy Load Assets
**Problem**: Loading all assets upfront delays game start.
**Solution**: Load assets on-demand.
```javascript
import { Assets } from 'pixi.js';
// Preload critical assets
const criticalAssets = await Assets.load([
'ui/background.png',
'ui/logo.png'
]);
// Background load game assets
Assets.backgroundLoad([
'characters/hero.png',
'characters/enemy.png',
'levels/level1.jpg'
]);
// Check if asset is loaded
if (Assets.cache.has('characters/hero.png')) {
const heroTexture = Assets.get('characters/hero.png');
const hero = new Sprite(heroTexture);
}
// Load on-demand
async function showLevel(levelNumber) {
const levelTexture = await Assets.load(`levels/level${levelNumber}.jpg`);
// Use texture
}
```
---
## Container Optimization
### 1. Disable Unnecessary Features
```javascript
import { Container } from 'pixi.js';
const container = new Container();
// ❌ Don't enable unless needed
container.sortableChildren = false; // Z-index sorting (expensive)
container.interactiveChildren = false; // Child interaction (expensive)
// ✅ Enable only when required
if (needsSorting) {
container.sortableChildren = true;
}
```
---
### 2. Use Object Pooling
**Problem**: Creating/destroying objects causes garbage collection pauses.
**Solution**: Reuse objects via pooling.
```javascript
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 = [];
}
}
// Usage
const bulletPool = new SpritePool(bulletTexture, 50);
// Spawn
const bullet = bulletPool.spawn(100, 200);
app.stage.addChild(bullet);
// Despawn
bulletPool.despawn(bullet);
```
**Performance Gain**: Eliminates GC pauses, smoother frame times.
---
### 3. Flatten Hierarchy
**Problem**: Deep nesting requires traversing many containers.
**Solution**: Keep hierarchy shallow when possible.
```javascript
// ❌ BAD: Deep nesting
const root = new Container();
const level1 = new Container();
const level2 = new Container();
const level3 = new Container();
root.addChild(level1);
level1.addChild(level2);
level2.addChild(level3);
level3.addChild(sprite);
// ✅ GOOD: Flat structure
const root = new Container();
root.addChild(sprite);
// Use position offsets instead of nested containers
sprite.x = parentX + childX;
sprite.y = parentY + childY;
```
---
## Filter Performance
### 1. Limit Filter Usage
**Problem**: Filters are expensive WebGL operations.
**Solution**: Use sparingly, optimize where possible.
```javascript
import { BlurFilter } from 'pixi.js';
// ❌ BAD: Filter on every sprite
sprites.forEach(sprite => {
sprite.filters = [new BlurFilter()];
});
// ✅ GOOD: Filter on container
const container = new Container();
sprites.forEach(sprite => container.addChild(sprite));
container.filters = [new BlurFilter()];
// ✅ BETTER: Bake filter into texture
const filteredTexture = renderer.filters.generateFilteredTexture({
texture: originalTexture,
filters: [new BlurFilter({ strength: 5 })]
});
const sprite = new Sprite(filteredTexture);
```
---
### 2. Specify Filter Area
**Problem**: PixiJS measures filter bounds at runtime (expensive).
**Solution**: Manually specify `filterArea`.
```javascript
import { BlurFilter, Rectangle } from 'pixi.js';
const sprite = new Sprite(texture);
sprite.filters = [new BlurFilter()];
// ✅ Specify filter area for performance
sprite.filterArea = new Rectangle(0, 0, sprite.width, sprite.height);
// Update if sprite resizes
sprite.on('resize', () => {
sprite.filterArea = new Rectangle(0, 0, sprite.width, sprite.height);
});
```
**Performance Gain**: Avoids runtime bounds calculation.
---
### 3. Release Filters When Not Needed
```javascript
// Enable filter
sprite.filters = [new BlurFilter()];
// Disable filter (releases GPU memory)
sprite.filters = null;
// Toggle based on game state
if (gameState === 'paused') {
sprite.filters = [new BlurFilter()];
} else {
sprite.filters = null;
}
```
---
## Text Rendering
### 1. Use BitmapText for Dynamic Text
**Problem**: Standard Text re-renders texture on every update.
**Solution**: Use BitmapText for frequently changing text.
```javascript
import { Text, BitmapText } from 'pixi.js';
// ❌ BAD: Standard Text (expensive updates)
const scoreText = new Text({ text: 'Score: 0' });
app.ticker.add(() => {
scoreText.text = `Score: ${++score}`; // Re-renders texture every frame
});
// ✅ GOOD: BitmapText (much faster)
const scoreBitmap = new BitmapText({
text: 'Score: 0',
style: { fontFamily: 'MyBitmapFont', fontSize: 24 }
});
app.ticker.add(() => {
scoreBitmap.text = `Score: ${++score}`; // Fast glyph updates
});
```
**Performance**: BitmapText is 10-50x faster for dynamic text.
---
### 2. Reduce Text Resolution
**Problem**: High-resolution text consumes memory.
**Solution**: Lower resolution for less critical text.
```javascript
import { Text, TextStyle } from 'pixi.js';
const style = new TextStyle({ fontSize: 36 });
const text = new Text({ text: 'Hello', style });
// Default resolution matches renderer (e.g., 2 on Retina)
text.resolution = 1; // Reduce to 1 for memory savings
// Still looks good, uses less memory
```
---
### 3. Bake Filters into Text
**Problem**: Runtime filters on text are expensive.
**Solution**: Apply filters at texture creation.
```javascript
import { Text, TextStyle, BlurFilter } from 'pixi.js';
const style = new TextStyle({
fontFamily: 'Arial',
fontSize: 36,
fill: '#ffffff',
filters: [new BlurFilter()] // Baked into texture at creation
});
const text = new Text({ text: 'Glowing Text', style });
// Filter applied once at creation, not every frame
```
---
## Memory Management
### 1. Destroy Display Objects Properly
```javascript
import { Sprite, Container } from 'pixi.js';
const sprite = new Sprite(texture);
const container = new Container();
// ✅ GOOD: Destroy with options
sprite.destroy({
children: true, // Destroy children
texture: false, // Keep texture (if used elsewhere)
baseTexture: false // Keep baseTexture
});
// Destroy container and all children
container.destroy({ children: true });
// Destroy texture when completely done
texture.destroy(true); // true = also destroy baseTexture
```
---
### 2. Clear Event Listeners
```javascript
const sprite = new Sprite(texture);
sprite.on('pointerdown', onPointerDown);
sprite.on('pointermove', onPointerMove);
// ✅ Remove listeners before destroying
sprite.off('pointerdown', onPointerDown);
sprite.off('pointermove', onPointerMove);
// Or remove all
sprite.removeAllListeners();
sprite.destroy();
```
---
### 3. Monitor Memory Usage
```javascript
function logMemoryUsage() {
if (performance.memory) {
const used = (performance.memory.usedJSHeapSize / 1048576).toFixed(2);
const total = (performance.memory.totalJSHeapSize / 1048576).toFixed(2);
const limit = (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2);
console.log(`Memory: ${used}MB / ${total}MB (Limit: ${limit}MB)`);
}
}
setInterval(logMemoryUsage, 5000);
```
---
## Mobile Optimization
### 1. Disable Anti-Aliasing
```javascript
const app = new Application();
await app.init({
width: 800,
height: 600,
antialias: false, // Faster on mobile
resolution: 1 // Lower resolution
});
```
---
### 2. Reduce Particle Count
```javascript
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
const particleCount = isMobile ? 1000 : 5000;
for (let i = 0; i < particleCount; i++) {
particles.addParticle(new Particle({ texture }));
}
```
---
### 3. Limit Frame Rate on Battery
```javascript
import { Ticker } from 'pixi.js';
function checkBattery() {
if ('getBattery' in navigator) {
navigator.getBattery().then(battery => {
if (battery.charging === false && battery.level < 0.2) {
Ticker.shared.maxFPS = 30; // Reduce to 30 FPS
} else {
Ticker.shared.maxFPS = 60;
}
});
}
}
checkBattery();
```
---
## Advanced Techniques
### 1. Use WebWorkers for Heavy Calculations
```javascript
// worker.js
self.onmessage = function(e) {
const { particles, delta } = e.data;
// Update particle physics
particles.forEach(p => {
p.vx += (Math.random() - 0.5) * 0.1;
p.vy += 0.05;
p.x += p.vx * delta;
p.y += p.vy * delta;
});
self.postMessage(particles);
};
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
const updatedParticles = e.data;
// Apply to PixiJS particles
updatedParticles.forEach((data, i) => {
particles.particleChildren[i].x = data.x;
particles.particleChildren[i].y = data.y;
});
};
app.ticker.add((ticker) => {
const particleData = particles.particleChildren.map(p => ({
x: p.x,
y: p.y,
vx: p.vx || 0,
vy: p.vy || 0
}));
worker.postMessage({ particles: particleData, delta: ticker.deltaTime });
});
```
---
### 2. Implement Spatial Hashing for Collision Detection
```javascript
class SpatialHash {
constructor(cellSize) {
this.cellSize = cellSize;
this.grid = new Map();
}
clear() {
this.grid.clear();
}
insert(sprite) {
const cells = this.getCells(sprite);
cells.forEach(cell => {
const key = `${cell.x},${cell.y}`;
if (!this.grid.has(key)) {
this.grid.set(key, []);
}
this.grid.get(key).push(sprite);
});
}
getCells(sprite) {
const bounds = sprite.getBounds();
const cells = [];
const minX = Math.floor(bounds.x / this.cellSize);
const maxX = Math.floor((bounds.x + bounds.width) / this.cellSize);
const minY = Math.floor(bounds.y / this.cellSize);
const maxY = Math.floor((bounds.y + bounds.height) / this.cellSize);
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
cells.push({ x, y });
}
}
return cells;
}
getNearby(sprite) {
const cells = this.getCells(sprite);
const nearby = new Set();
cells.forEach(cell => {
const key = `${cell.x},${cell.y}`;
const sprites = this.grid.get(key);
if (sprites) {
sprites.forEach(s => {
if (s !== sprite) nearby.add(s);
});
}
});
return Array.from(nearby);
}
}
// Usage
const spatialHash = new SpatialHash(100);
app.ticker.add(() => {
spatialHash.clear();
// Insert all sprites
sprites.forEach(sprite => spatialHash.insert(sprite));
// Check collisions only with nearby sprites
sprites.forEach(sprite => {
const nearby = spatialHash.getNearby(sprite);
nearby.forEach(other => {
if (checkCollision(sprite, other)) {
handleCollision(sprite, other);
}
});
});
});
```
**Performance**: O(n) instead of O(n²) for collision detection.
---
## Performance Checklist
✅ **Rendering**
- [ ] Use ParticleContainer for 1,000+ sprites
- [ ] Batch sprites by texture
- [ ] Enable culling for off-screen objects
- [ ] Cache static graphics as bitmaps
- [ ] Minimize draw calls
✅ **Textures**
- [ ] Destroy unused textures
- [ ] Use sprite atlases
- [ ] Optimize texture sizes (power-of-2)
- [ ] Lazy load non-critical assets
✅ **Containers**
- [ ] Disable sortableChildren unless needed
- [ ] Use object pooling
- [ ] Keep hierarchy shallow
✅ **Filters**
- [ ] Limit filter usage (1-2 per scene)
- [ ] Specify filterArea
- [ ] Release filters when not needed
- [ ] Bake filters into textures
✅ **Text**
- [ ] Use BitmapText for dynamic text
- [ ] Reduce text resolution
- [ ] Bake filters into TextStyle
✅ **Memory**
- [ ] Destroy objects properly
- [ ] Clear event listeners
- [ ] Monitor memory usage
✅ **Mobile**
- [ ] Disable anti-aliasing
- [ ] Reduce particle counts
- [ ] Lower resolution
- [ ] Limit frame rate on battery
---
## Debugging Performance Issues
### Identify Bottlenecks
```javascript
// Measure specific operations
console.time('particleUpdate');
updateParticles();
console.timeEnd('particleUpdate');
// Profile draw calls
console.log('Draw calls:', app.renderer.stats.drawCalls.total);
// Check texture count
console.log('Textures bound:', app.renderer.stats.textureCount);
```
### Common Issues
| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
| Low FPS | Too many draw calls | Batch sprites, use atlases |
| Stuttering | GC pauses | Use object pooling |
| High memory | Texture leaks | Destroy textures properly |
| Slow filters | Too many filters | Limit usage, bake into textures |
| Laggy text | Text updates | Use BitmapText |
---
This guide provides comprehensive strategies for optimizing PixiJS applications to achieve smooth 60 FPS performance across devices.
scripts/particle_builder.py
#!/usr/bin/env python3
"""
PixiJS Particle Builder
Generates high-performance particle systems using ParticleContainer.
Usage:
Interactive mode:
python particle_builder.py
CLI mode:
python particle_builder.py --type fountain --count 5000 --output ./
python particle_builder.py -t fire -c 2000 -o ./my-project/
"""
import argparse
import os
import sys
from typing import Dict, Tuple
def generate_fountain_particles(count: int = 5000) -> str:
"""Generate fountain particle system"""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Fountain Particles</title>
<style>
body {{
margin: 0;
padding: 0;
overflow: hidden;
background: #0a0a0a;
}}
#info {{
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}}
</style>
</head>
<body>
<div id="info">
<h3>Fountain Particles</h3>
<p>Particles: {count}</p>
<p>FPS: <span id="fps">--</span></p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {{
const app = new PIXI.Application();
await app.init({{
width: 800,
height: 600,
backgroundColor: 0x0a0a0a,
antialias: true
}});
document.body.appendChild(app.canvas);
// Create particle texture
const graphics = new PIXI.Graphics();
graphics.circle(5, 5, 5).fill(0xffffff);
const particleTexture = app.renderer.generateTexture(graphics);
// Create particle container
const particles = new PIXI.ParticleContainer({{
maxSize: {count},
dynamicProperties: {{
position: true,
scale: true,
rotation: false,
color: true
}}
}});
app.stage.addChild(particles);
// Particle data
const particleData = [];
for (let i = 0; i < {count}; i++) {{
const particle = new PIXI.Particle({{
texture: particleTexture,
x: 400,
y: 550
}});
particles.addParticle(particle);
particleData.push({{
particle,
vx: (Math.random() - 0.5) * 8,
vy: -(Math.random() * 12 + 8),
life: 1.0,
gravity: 0.2
}});
}}
// 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 += data.gravity * ticker.deltaTime;
// Fade out
data.life -= 0.01 * ticker.deltaTime;
if (data.life > 0) {{
const color = Math.floor(data.life * 255);
data.particle.tint = (color << 16) | (color << 8) | 255;
data.particle.alpha = data.life;
}} else {{
// Reset particle
data.particle.x = 400;
data.particle.y = 550;
data.vx = (Math.random() - 0.5) * 8;
data.vy = -(Math.random() * 12 + 8);
data.life = 1.0;
}}
}});
// Update FPS
document.getElementById('fps').textContent = Math.round(app.ticker.FPS);
}});
}})();
</script>
</body>
</html>"""
def generate_fire_particles(count: int = 2000) -> str:
"""Generate fire particle system"""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Fire Particles</title>
<style>
body {{
margin: 0;
padding: 0;
overflow: hidden;
background: #1a0a00;
}}
#info {{
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}}
</style>
</head>
<body>
<div id="info">
<h3>Fire Particles</h3>
<p>Particles: {count}</p>
<p>FPS: <span id="fps">--</span></p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {{
const app = new PIXI.Application();
await app.init({{
width: 800,
height: 600,
backgroundColor: 0x1a0a00,
antialias: true
}});
document.body.appendChild(app.canvas);
// Create particle texture
const graphics = new PIXI.Graphics();
graphics.circle(8, 8, 8).fill(0xffffff);
const particleTexture = app.renderer.generateTexture(graphics);
// Create particle container
const particles = new PIXI.ParticleContainer({{
maxSize: {count},
dynamicProperties: {{
position: true,
scale: true,
rotation: true,
color: true
}}
}});
app.stage.addChild(particles);
// Particle data
const particleData = [];
for (let i = 0; i < {count}; i++) {{
const particle = new PIXI.Particle({{
texture: particleTexture,
x: 400,
y: 550
}});
particles.addParticle(particle);
particleData.push({{
particle,
vx: (Math.random() - 0.5) * 2,
vy: -(Math.random() * 3 + 2),
life: Math.random(),
maxLife: Math.random() * 0.5 + 0.5
}});
}}
// Update loop
app.ticker.add((ticker) => {{
particleData.forEach(data => {{
// Physics
data.particle.x += data.vx * ticker.deltaTime;
data.particle.y += data.vy * ticker.deltaTime;
// Wind
data.vx += (Math.random() - 0.5) * 0.1 * ticker.deltaTime;
// Rise
data.vy -= 0.05 * ticker.deltaTime;
// Fade
data.life -= 0.01 * ticker.deltaTime;
if (data.life > 0) {{
const t = data.life / data.maxLife;
// Color gradient: yellow -> orange -> red -> black
let r, g, b;
if (t > 0.66) {{
// Yellow to orange
const localT = (t - 0.66) / 0.34;
r = 255;
g = Math.floor(255 * localT);
b = 0;
}} else if (t > 0.33) {{
// Orange to red
const localT = (t - 0.33) / 0.33;
r = 255;
g = Math.floor(128 * localT);
b = 0;
}} else {{
// Red to black
r = Math.floor(255 * (t / 0.33));
g = 0;
b = 0;
}}
data.particle.tint = (r << 16) | (g << 8) | b;
data.particle.alpha = t;
data.particle.scaleX = t * 1.5;
data.particle.scaleY = t * 1.5;
data.particle.rotation += 0.1 * ticker.deltaTime;
}} else {{
// Reset particle
data.particle.x = 400 + (Math.random() - 0.5) * 100;
data.particle.y = 550;
data.vx = (Math.random() - 0.5) * 2;
data.vy = -(Math.random() * 3 + 2);
data.life = data.maxLife;
}}
}});
document.getElementById('fps').textContent = Math.round(app.ticker.FPS);
}});
}})();
</script>
</body>
</html>"""
def generate_snow_particles(count: int = 3000) -> str:
"""Generate snow particle system"""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Snow Particles</title>
<style>
body {{
margin: 0;
padding: 0;
overflow: hidden;
background: linear-gradient(to bottom, #2c3e50, #34495e);
}}
#info {{
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}}
</style>
</head>
<body>
<div id="info">
<h3>Snow Particles</h3>
<p>Particles: {count}</p>
<p>FPS: <span id="fps">--</span></p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {{
const app = new PIXI.Application();
await app.init({{
width: 800,
height: 600,
backgroundAlpha: 0,
antialias: true
}});
document.body.appendChild(app.canvas);
// Create snowflake texture
const graphics = new PIXI.Graphics();
graphics.circle(3, 3, 3).fill(0xffffff);
const snowTexture = app.renderer.generateTexture(graphics);
// Create particle container
const particles = new PIXI.ParticleContainer({{
maxSize: {count},
dynamicProperties: {{
position: true,
scale: true,
rotation: true,
color: false
}}
}});
app.stage.addChild(particles);
// Particle data
const particleData = [];
for (let i = 0; i < {count}; i++) {{
const particle = new PIXI.Particle({{
texture: snowTexture,
x: Math.random() * 800,
y: Math.random() * 600,
scaleX: Math.random() * 0.5 + 0.5,
scaleY: Math.random() * 0.5 + 0.5,
alpha: Math.random() * 0.5 + 0.5
}});
particles.addParticle(particle);
particleData.push({{
particle,
speed: Math.random() * 1 + 0.5,
sway: Math.random() * Math.PI * 2,
swaySpeed: Math.random() * 0.02 + 0.01
}});
}}
// Update loop
app.ticker.add((ticker) => {{
particleData.forEach(data => {{
// Fall down
data.particle.y += data.speed * ticker.deltaTime;
// Sway left and right
data.sway += data.swaySpeed * ticker.deltaTime;
data.particle.x += Math.sin(data.sway) * 0.5 * ticker.deltaTime;
// Rotate
data.particle.rotation += 0.01 * ticker.deltaTime;
// Reset if below screen
if (data.particle.y > 600) {{
data.particle.y = -10;
data.particle.x = Math.random() * 800;
}}
}});
document.getElementById('fps').textContent = Math.round(app.ticker.FPS);
}});
}})();
</script>
</body>
</html>"""
def generate_explosion_particles(count: int = 1000) -> str:
"""Generate explosion particle system"""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Explosion Particles</title>
<style>
body {{
margin: 0;
padding: 0;
overflow: hidden;
background: #0a0a0a;
}}
#info {{
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}}
</style>
</head>
<body>
<div id="info">
<h3>Explosion Particles</h3>
<p>Click to explode</p>
<p>FPS: <span id="fps">--</span></p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {{
const app = new PIXI.Application();
await app.init({{
width: 800,
height: 600,
backgroundColor: 0x0a0a0a,
antialias: true
}});
document.body.appendChild(app.canvas);
// Create particle texture
const graphics = new PIXI.Graphics();
graphics.circle(4, 4, 4).fill(0xffffff);
const particleTexture = app.renderer.generateTexture(graphics);
// Create particle container
const particles = new PIXI.ParticleContainer({{
maxSize: {count},
dynamicProperties: {{
position: true,
scale: true,
rotation: false,
color: true
}}
}});
app.stage.addChild(particles);
// Particle pool
const particleData = [];
for (let i = 0; i < {count}; i++) {{
const particle = new PIXI.Particle({{
texture: particleTexture,
x: -100,
y: -100
}});
particle.alpha = 0;
particles.addParticle(particle);
particleData.push({{
particle,
vx: 0,
vy: 0,
life: 0,
active: false
}});
}}
// Explosion function
function explode(x, y) {{
let spawned = 0;
particleData.forEach(data => {{
if (!data.active && spawned < 100) {{
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 8 + 4;
data.particle.x = x;
data.particle.y = y;
data.vx = Math.cos(angle) * speed;
data.vy = Math.sin(angle) * speed;
data.life = 1.0;
data.active = true;
data.particle.alpha = 1;
spawned++;
}}
}});
}}
// Click to explode
app.canvas.addEventListener('click', (e) => {{
const rect = app.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
explode(x, y);
}});
// Update loop
app.ticker.add((ticker) => {{
particleData.forEach(data => {{
if (data.active) {{
// Physics
data.particle.x += data.vx * ticker.deltaTime;
data.particle.y += data.vy * ticker.deltaTime;
// Gravity
data.vy += 0.2 * ticker.deltaTime;
// Drag
data.vx *= 0.98;
data.vy *= 0.98;
// Fade
data.life -= 0.02 * ticker.deltaTime;
if (data.life > 0) {{
// Color: white -> yellow -> orange -> red
let r, g, b;
if (data.life > 0.75) {{
r = 255;
g = 255;
b = 255;
}} else if (data.life > 0.5) {{
r = 255;
g = 255;
b = Math.floor((data.life - 0.5) * 4 * 255);
}} else if (data.life > 0.25) {{
r = 255;
g = Math.floor((data.life - 0.25) * 4 * 255);
b = 0;
}} else {{
r = Math.floor(data.life * 4 * 255);
g = 0;
b = 0;
}}
data.particle.tint = (r << 16) | (g << 8) | b;
data.particle.alpha = data.life;
data.particle.scaleX = data.life;
data.particle.scaleY = data.life;
}} else {{
data.active = false;
data.particle.alpha = 0;
}}
}}
}});
document.getElementById('fps').textContent = Math.round(app.ticker.FPS);
}});
// Initial explosion
explode(400, 300);
}})();
</script>
</body>
</html>"""
def generate_stars_particles(count: int = 5000) -> str:
"""Generate starfield particle system"""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Starfield</title>
<style>
body {{
margin: 0;
padding: 0;
overflow: hidden;
background: #000000;
}}
#info {{
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}}
</style>
</head>
<body>
<div id="info">
<h3>Starfield</h3>
<p>Stars: {count}</p>
<p>FPS: <span id="fps">--</span></p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {{
const app = new PIXI.Application();
await app.init({{
width: 800,
height: 600,
backgroundColor: 0x000000,
antialias: true
}});
document.body.appendChild(app.canvas);
// Create star texture
const graphics = new PIXI.Graphics();
graphics.circle(2, 2, 2).fill(0xffffff);
const starTexture = app.renderer.generateTexture(graphics);
// Create particle container
const particles = new PIXI.ParticleContainer({{
maxSize: {count},
dynamicProperties: {{
position: true,
scale: true,
rotation: false,
color: false
}}
}});
app.stage.addChild(particles);
// Particle data
const particleData = [];
for (let i = 0; i < {count}; i++) {{
const particle = new PIXI.Particle({{
texture: starTexture,
x: Math.random() * 800,
y: Math.random() * 600,
scaleX: Math.random(),
scaleY: Math.random(),
alpha: Math.random()
}});
particles.addParticle(particle);
particleData.push({{
particle,
z: Math.random() * 1000,
speed: Math.random() * 2 + 1
}});
}}
// Update loop
const centerX = 400;
const centerY = 300;
app.ticker.add((ticker) => {{
particleData.forEach(data => {{
// Move toward camera
data.z -= data.speed * ticker.deltaTime;
if (data.z <= 0) {{
data.z = 1000;
}}
// Project 3D to 2D
const scale = 1000 / data.z;
const x = (data.particle.x - centerX) * scale + centerX;
const y = (data.particle.y - centerY) * scale + centerY;
data.particle.x = x;
data.particle.y = y;
data.particle.scaleX = scale;
data.particle.scaleY = scale;
data.particle.alpha = Math.min(scale, 1);
// Reset if off screen
if (x < 0 || x > 800 || y < 0 || y > 600) {{
data.particle.x = Math.random() * 800;
data.particle.y = Math.random() * 600;
data.z = 1000;
}}
}});
document.getElementById('fps').textContent = Math.round(app.ticker.FPS);
}});
}})();
</script>
</body>
</html>"""
# Particle type registry
PARTICLE_TYPES: Dict[str, Dict] = {
'fountain': {
'name': 'Fountain',
'description': 'Particles shooting upward with gravity',
'default_count': 5000,
'generator': generate_fountain_particles
},
'fire': {
'name': 'Fire',
'description': 'Fire effect with color gradient',
'default_count': 2000,
'generator': generate_fire_particles
},
'snow': {
'name': 'Snow',
'description': 'Falling snowflakes with sway',
'default_count': 3000,
'generator': generate_snow_particles
},
'explosion': {
'name': 'Explosion',
'description': 'Click-triggered explosions',
'default_count': 1000,
'generator': generate_explosion_particles
},
'stars': {
'name': 'Starfield',
'description': '3D starfield effect',
'default_count': 5000,
'generator': generate_stars_particles
}
}
def interactive_mode():
"""Run interactive particle builder"""
print("\n" + "="*60)
print("PixiJS Particle Builder - Interactive Mode")
print("="*60)
# Show particle types
print("\nAvailable particle systems:")
print("-" * 60)
for idx, (key, info) in enumerate(PARTICLE_TYPES.items(), 1):
print(f"{idx}. {info['name']:15} - {info['description']} ({info['default_count']} particles)")
# Get particle type
while True:
try:
choice = input(f"\nSelect particle type (1-{len(PARTICLE_TYPES)}): ").strip()
idx = int(choice)
if 1 <= idx <= len(PARTICLE_TYPES):
particle_type = list(PARTICLE_TYPES.keys())[idx - 1]
break
print(f"Error: Please enter a number between 1 and {len(PARTICLE_TYPES)}")
except ValueError:
print("Error: Please enter a valid number")
# Get particle count
default_count = PARTICLE_TYPES[particle_type]['default_count']
count_input = input(f"\nParticle count (default: {default_count}): ").strip()
count = int(count_input) if count_input else default_count
# Get output directory
output_dir = input("\nOutput directory (default: current directory): ").strip()
if not output_dir:
output_dir = "."
# Generate particles
print("\n" + "-"*60)
print("Generating particle system...")
try:
generator = PARTICLE_TYPES[particle_type]['generator']
html = generator(count)
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Write HTML file
filename = f"{particle_type}_particles.html"
html_path = os.path.join(output_dir, filename)
with open(html_path, 'w') as f:
f.write(html)
print(f"\n✓ Particle system created: {html_path}")
print(f"\nParticle type: {PARTICLE_TYPES[particle_type]['name']}")
print(f"Particle count: {count}")
print(f"Lines of code: {len(html.splitlines())}")
print("\nTo view:")
print(f" Open {html_path} in a web browser")
print(f" Or run: python -m http.server 8000")
except Exception as e:
print(f"\nError: Failed to generate particle system: {e}")
return 1
return 0
def cli_mode(args):
"""Run CLI particle builder"""
particle_type = args.type
count = args.count
output_dir = args.output
# Validate particle type
if particle_type not in PARTICLE_TYPES:
print(f"Error: Unknown particle type '{particle_type}'")
print(f"Available types: {', '.join(PARTICLE_TYPES.keys())}")
return 1
# Use default count if not specified
if count is None:
count = PARTICLE_TYPES[particle_type]['default_count']
# Generate particles
try:
generator = PARTICLE_TYPES[particle_type]['generator']
html = generator(count)
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Write HTML file
filename = f"{particle_type}_particles.html"
html_path = os.path.join(output_dir, filename)
with open(html_path, 'w') as f:
f.write(html)
print(f"✓ Particle system created: {html_path}")
return 0
except Exception as e:
print(f"Error: Failed to generate particle system: {e}")
return 1
def main():
parser = argparse.ArgumentParser(
description='PixiJS Particle Builder',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Interactive mode:
python particle_builder.py
Generate fountain particles:
python particle_builder.py --type fountain --count 5000 --output ./
Generate fire particles:
python particle_builder.py -t fire -c 2000 -o ./effects/
Generate snow particles:
python particle_builder.py -t snow -c 3000
Available particle types:
fountain - Particles shooting upward with gravity
fire - Fire effect with color gradient
snow - Falling snowflakes with sway
explosion - Click-triggered explosions
stars - 3D starfield effect
"""
)
parser.add_argument(
'-t', '--type',
choices=list(PARTICLE_TYPES.keys()),
help='Particle system type'
)
parser.add_argument(
'-c', '--count',
type=int,
help='Number of particles'
)
parser.add_argument(
'-o', '--output',
default='.',
help='Output directory (default: current directory)'
)
args = parser.parse_args()
# Run interactive mode if no particle type specified
if not args.type:
return interactive_mode()
return cli_mode(args)
if __name__ == '__main__':
sys.exit(main())
scripts/sprite_generator.py
#!/usr/bin/env python3
"""
PixiJS Sprite Generator
Generates PixiJS sprite-based applications with various templates.
Usage:
Interactive mode:
python sprite_generator.py
CLI mode:
python sprite_generator.py --type basic --output ./
python sprite_generator.py -t interactive -o ./my-project/
python sprite_generator.py -t animated --name MySprite
"""
import argparse
import os
import sys
from typing import Dict, Tuple
def generate_basic_sprite() -> Tuple[str, str]:
"""Generate basic sprite example"""
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Basic Sprite</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #1a1a2e;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="info">
<h3>Basic Sprite</h3>
<p>Click the sprite to change color</p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {
// Create application
const app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1a1a2e,
antialias: true
});
document.body.appendChild(app.canvas);
// Create sprite
const graphics = new PIXI.Graphics();
graphics.rect(0, 0, 100, 100).fill(0x3498db);
const texture = app.renderer.generateTexture(graphics);
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(400, 300);
sprite.eventMode = 'static';
sprite.cursor = 'pointer';
app.stage.addChild(sprite);
// Rotate animation
app.ticker.add((ticker) => {
sprite.rotation += 0.01 * ticker.deltaTime;
});
// Click event
const colors = [0x3498db, 0xe74c3c, 0x2ecc71, 0xf39c12, 0x9b59b6];
let colorIndex = 0;
sprite.on('pointerdown', () => {
colorIndex = (colorIndex + 1) % colors.length;
const newGraphics = new PIXI.Graphics();
newGraphics.rect(0, 0, 100, 100).fill(colors[colorIndex]);
sprite.texture = app.renderer.generateTexture(newGraphics);
});
})();
</script>
</body>
</html>"""
js = """// Standalone JavaScript version
import { Application, Sprite, Graphics } from 'pixi.js';
(async () => {
const app = new Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1a1a2e,
antialias: true
});
document.body.appendChild(app.canvas);
// Create sprite
const graphics = new Graphics();
graphics.rect(0, 0, 100, 100).fill(0x3498db);
const texture = app.renderer.generateTexture(graphics);
const sprite = new Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(400, 300);
sprite.eventMode = 'static';
sprite.cursor = 'pointer';
app.stage.addChild(sprite);
// Rotate animation
app.ticker.add((ticker) => {
sprite.rotation += 0.01 * ticker.deltaTime;
});
// Click event
const colors = [0x3498db, 0xe74c3c, 0x2ecc71, 0xf39c12, 0x9b59b6];
let colorIndex = 0;
sprite.on('pointerdown', () => {
colorIndex = (colorIndex + 1) % colors.length;
const newGraphics = new Graphics();
newGraphics.rect(0, 0, 100, 100).fill(colors[colorIndex]);
sprite.texture = app.renderer.generateTexture(newGraphics);
});
})();
"""
return html, js
def generate_interactive_sprite() -> Tuple[str, str]:
"""Generate interactive sprite with drag and hover"""
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Interactive Sprite</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #0f0f23;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="info">
<h3>Interactive Sprite</h3>
<p>Drag to move • Hover to scale</p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {
const app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x0f0f23,
antialias: true
});
document.body.appendChild(app.canvas);
// Create draggable sprite
function createDraggableSprite(x, y, color) {
const graphics = new PIXI.Graphics();
graphics.circle(50, 50, 50).fill(color);
const texture = app.renderer.generateTexture(graphics);
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
sprite.position.set(x, y);
sprite.eventMode = 'static';
sprite.cursor = 'pointer';
// Drag state
sprite.dragging = false;
sprite.dragData = null;
sprite.originalScale = 1.0;
// Pointer events
sprite.on('pointerdown', onDragStart);
sprite.on('pointerup', onDragEnd);
sprite.on('pointerupoutside', onDragEnd);
sprite.on('pointermove', onDragMove);
sprite.on('pointerover', onHoverStart);
sprite.on('pointerout', onHoverEnd);
function onDragStart(event) {
sprite.dragging = true;
sprite.dragData = event.data;
sprite.alpha = 0.7;
}
function onDragEnd() {
sprite.dragging = false;
sprite.dragData = null;
sprite.alpha = 1.0;
}
function onDragMove() {
if (sprite.dragging) {
const newPosition = sprite.dragData.global;
sprite.position.set(newPosition.x, newPosition.y);
}
}
function onHoverStart() {
app.canvas.style.cursor = 'grab';
if (!sprite.dragging) {
sprite.scale.set(1.2);
}
}
function onHoverEnd() {
app.canvas.style.cursor = 'default';
if (!sprite.dragging) {
sprite.scale.set(sprite.originalScale);
}
}
return sprite;
}
// Create multiple draggable sprites
const colors = [0xe74c3c, 0x3498db, 0x2ecc71, 0xf39c12, 0x9b59b6];
for (let i = 0; i < 5; i++) {
const x = 150 + i * 120;
const y = 300;
const sprite = createDraggableSprite(x, y, colors[i]);
app.stage.addChild(sprite);
}
})();
</script>
</body>
</html>"""
js = "" # Standalone JS same as embedded
return html, js
def generate_animated_sprite() -> Tuple[str, str]:
"""Generate sprite sheet animation"""
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Animated Sprite</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #2c3e50;
}
#controls {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
button {
margin: 5px;
padding: 5px 10px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="controls">
<h3>Animated Sprite</h3>
<button id="play">Play</button>
<button id="stop">Stop</button>
<button id="faster">Faster</button>
<button id="slower">Slower</button>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {
const app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x2c3e50,
antialias: true
});
document.body.appendChild(app.canvas);
// Generate sprite sheet frames
const frames = [];
for (let i = 0; i < 12; i++) {
const graphics = new PIXI.Graphics();
// Animate a growing/shrinking circle
const scale = 0.5 + Math.sin(i / 12 * Math.PI * 2) * 0.5;
const radius = 50 * scale;
graphics.circle(50, 50, radius).fill(0x3498db);
const texture = app.renderer.generateTexture(graphics);
frames.push(texture);
}
// Create animated sprite
const animation = new PIXI.AnimatedSprite(frames);
animation.anchor.set(0.5);
animation.position.set(400, 300);
animation.animationSpeed = 0.16;
animation.play();
app.stage.addChild(animation);
// Controls
document.getElementById('play').addEventListener('click', () => {
animation.play();
});
document.getElementById('stop').addEventListener('click', () => {
animation.stop();
});
document.getElementById('faster').addEventListener('click', () => {
animation.animationSpeed *= 1.5;
});
document.getElementById('slower').addEventListener('click', () => {
animation.animationSpeed /= 1.5;
});
// Rotate animation
app.ticker.add((ticker) => {
animation.rotation += 0.01 * ticker.deltaTime;
});
})();
</script>
</body>
</html>"""
js = ""
return html, js
def generate_tiled_sprites() -> Tuple[str, str]:
"""Generate tiled sprite pattern"""
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Tiled Sprites</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #34495e;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="info">
<h3>Tiled Sprites</h3>
<p>Scrolling background pattern</p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {
const app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x34495e,
antialias: true
});
document.body.appendChild(app.canvas);
// Create tile texture
const graphics = new PIXI.Graphics();
graphics.rect(0, 0, 64, 64).fill(0x3498db);
graphics.rect(4, 4, 56, 56).fill(0x2980b9);
const tileTexture = app.renderer.generateTexture(graphics);
// Create tiling sprite
const tilingSprite = new PIXI.TilingSprite({
texture: tileTexture,
width: app.screen.width,
height: app.screen.height
});
app.stage.addChild(tilingSprite);
// Scroll animation
app.ticker.add((ticker) => {
tilingSprite.tilePosition.x += 1 * ticker.deltaTime;
tilingSprite.tilePosition.y += 0.5 * ticker.deltaTime;
});
})();
</script>
</body>
</html>"""
js = ""
return html, js
def generate_spritesheet_atlas() -> Tuple[str, str]:
"""Generate sprite sheet with texture atlas"""
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Sprite Sheet</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #1a1a2e;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="info">
<h3>Sprite Sheet Atlas</h3>
<p>Multiple sprites from texture atlas</p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {
const app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x1a1a2e,
antialias: true
});
document.body.appendChild(app.canvas);
// Create texture atlas (sprite sheet)
const atlas = {};
const shapes = ['circle', 'square', 'triangle', 'star', 'hexagon'];
const colors = [0xe74c3c, 0x3498db, 0x2ecc71, 0xf39c12, 0x9b59b6];
shapes.forEach((shape, index) => {
const graphics = new PIXI.Graphics();
switch(shape) {
case 'circle':
graphics.circle(25, 25, 25).fill(colors[index]);
break;
case 'square':
graphics.rect(0, 0, 50, 50).fill(colors[index]);
break;
case 'triangle':
graphics.poly([25, 0, 50, 50, 0, 50]).fill(colors[index]);
break;
case 'star':
graphics.star(25, 25, 5, 25).fill(colors[index]);
break;
case 'hexagon':
graphics.poly([
25, 0, 45, 12.5, 45, 37.5, 25, 50, 5, 37.5, 5, 12.5
]).fill(colors[index]);
break;
}
atlas[shape] = app.renderer.generateTexture(graphics);
});
// Create sprites from atlas
shapes.forEach((shape, index) => {
const sprite = new PIXI.Sprite(atlas[shape]);
sprite.anchor.set(0.5);
sprite.position.set(150 + index * 120, 300);
sprite.eventMode = 'static';
sprite.cursor = 'pointer';
// Hover effect
sprite.on('pointerover', () => {
sprite.scale.set(1.2);
});
sprite.on('pointerout', () => {
sprite.scale.set(1.0);
});
// Click to rotate
sprite.on('pointerdown', () => {
sprite.rotation += Math.PI / 4;
});
app.stage.addChild(sprite);
// Floating animation
const startY = sprite.y;
app.ticker.add(() => {
sprite.y = startY + Math.sin(Date.now() * 0.001 + index) * 20;
});
});
})();
</script>
</body>
</html>"""
js = ""
return html, js
def generate_masked_sprite() -> Tuple[str, str]:
"""Generate sprite with mask"""
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PixiJS Masked Sprite</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #2c3e50;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="info">
<h3>Masked Sprite</h3>
<p>Circular mask reveals gradient</p>
</div>
<script src="https://pixijs.download/release/pixi.js"></script>
<script>
(async () => {
const app = new PIXI.Application();
await app.init({
width: 800,
height: 600,
backgroundColor: 0x2c3e50,
antialias: true
});
document.body.appendChild(app.canvas);
// Create gradient texture
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 400;
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 400, 400);
gradient.addColorStop(0, '#e74c3c');
gradient.addColorStop(0.5, '#3498db');
gradient.addColorStop(1, '#2ecc71');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 400, 400);
const gradientTexture = PIXI.Texture.from(canvas);
const sprite = new PIXI.Sprite(gradientTexture);
sprite.anchor.set(0.5);
sprite.position.set(400, 300);
// Create circular mask
const mask = new PIXI.Graphics();
mask.circle(400, 300, 150).fill(0xffffff);
sprite.mask = mask;
app.stage.addChild(mask, sprite);
// Animate mask
let growing = true;
let radius = 150;
app.ticker.add((ticker) => {
sprite.rotation += 0.01 * ticker.deltaTime;
// Pulse mask
if (growing) {
radius += 1;
if (radius > 200) growing = false;
} else {
radius -= 1;
if (radius < 100) growing = true;
}
mask.clear();
mask.circle(400, 300, radius).fill(0xffffff);
});
})();
</script>
</body>
</html>"""
js = ""
return html, js
# Sprite type registry
SPRITE_TYPES: Dict[str, Dict] = {
'basic': {
'name': 'Basic Sprite',
'description': 'Simple rotating sprite with color change on click',
'generator': generate_basic_sprite
},
'interactive': {
'name': 'Interactive Sprite',
'description': 'Draggable sprites with hover effects',
'generator': generate_interactive_sprite
},
'animated': {
'name': 'Animated Sprite',
'description': 'Sprite sheet animation with playback controls',
'generator': generate_animated_sprite
},
'tiled': {
'name': 'Tiled Sprite',
'description': 'Scrolling background with tiling sprite',
'generator': generate_tiled_sprites
},
'atlas': {
'name': 'Sprite Sheet Atlas',
'description': 'Multiple sprites from texture atlas',
'generator': generate_spritesheet_atlas
},
'masked': {
'name': 'Masked Sprite',
'description': 'Sprite with animated circular mask',
'generator': generate_masked_sprite
}
}
def interactive_mode():
"""Run interactive sprite generator"""
print("\n" + "="*60)
print("PixiJS Sprite Generator - Interactive Mode")
print("="*60)
# Show sprite types
print("\nAvailable sprite types:")
print("-" * 60)
for idx, (key, info) in enumerate(SPRITE_TYPES.items(), 1):
print(f"{idx}. {info['name']:25} - {info['description']}")
# Get sprite type
while True:
try:
choice = input(f"\nSelect sprite type (1-{len(SPRITE_TYPES)}): ").strip()
idx = int(choice)
if 1 <= idx <= len(SPRITE_TYPES):
sprite_type = list(SPRITE_TYPES.keys())[idx - 1]
break
print(f"Error: Please enter a number between 1 and {len(SPRITE_TYPES)}")
except ValueError:
print("Error: Please enter a valid number")
# Get output directory
output_dir = input("\nOutput directory (default: current directory): ").strip()
if not output_dir:
output_dir = "."
# Get filename
filename = input("\nFilename (default: sprite.html): ").strip()
if not filename:
filename = "sprite.html"
if not filename.endswith('.html'):
filename += '.html'
# Generate sprite
print("\n" + "-"*60)
print("Generating sprite...")
try:
generator = SPRITE_TYPES[sprite_type]['generator']
html, js = generator()
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Write HTML file
html_path = os.path.join(output_dir, filename)
with open(html_path, 'w') as f:
f.write(html)
print(f"\n✓ Sprite created: {html_path}")
print(f"\nSprite type: {SPRITE_TYPES[sprite_type]['name']}")
print(f"Lines of code: {len(html.splitlines())}")
print("\nTo view:")
print(f" Open {html_path} in a web browser")
print(f" Or run: python -m http.server 8000")
except Exception as e:
print(f"\nError: Failed to generate sprite: {e}")
return 1
return 0
def cli_mode(args):
"""Run CLI sprite generator"""
sprite_type = args.type
output_dir = args.output
name = args.name or 'sprite'
# Validate sprite type
if sprite_type not in SPRITE_TYPES:
print(f"Error: Unknown sprite type '{sprite_type}'")
print(f"Available types: {', '.join(SPRITE_TYPES.keys())}")
return 1
# Generate sprite
try:
generator = SPRITE_TYPES[sprite_type]['generator']
html, js = generator()
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Write HTML file
filename = f"{name}.html"
html_path = os.path.join(output_dir, filename)
with open(html_path, 'w') as f:
f.write(html)
print(f"✓ Sprite created: {html_path}")
return 0
except Exception as e:
print(f"Error: Failed to generate sprite: {e}")
return 1
def main():
parser = argparse.ArgumentParser(
description='PixiJS Sprite Generator',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Interactive mode:
python sprite_generator.py
Generate basic sprite:
python sprite_generator.py --type basic --output ./
Generate interactive sprite:
python sprite_generator.py -t interactive -o ./my-project/
Generate animated sprite:
python sprite_generator.py -t animated --name MyAnimation
Available sprite types:
basic - Simple rotating sprite with color change
interactive - Draggable sprites with hover effects
animated - Sprite sheet animation with controls
tiled - Scrolling background pattern
atlas - Multiple sprites from texture atlas
masked - Sprite with animated mask
"""
)
parser.add_argument(
'-t', '--type',
choices=list(SPRITE_TYPES.keys()),
help='Sprite type'
)
parser.add_argument(
'-o', '--output',
default='.',
help='Output directory (default: current directory)'
)
parser.add_argument(
'-n', '--name',
help='Output filename (default: sprite)'
)
args = parser.parse_args()
# Run interactive mode if no sprite type specified
if not args.type:
return interactive_mode()
return cli_mode(args)
if __name__ == '__main__':
sys.exit(main())
SKILL.md
---
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:
```javascript
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
---
### 2. Sprites & Textures
Core visual elements loaded from images:
```javascript
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**:
```javascript
const sprite = Sprite.from('path/to/image.png');
```
---
### 3. Graphics API
Draw vector shapes programmatically:
```javascript
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**:
```javascript
graphics.svg('<svg><path d="M 100 350 q 150 -300 300 0" /></svg>');
```
---
### 4. ParticleContainer
Optimized container for rendering thousands of sprites:
```javascript
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.
---
### 5. Filters
Apply per-pixel effects using WebGL shaders:
```javascript
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
---
### 6. Text Rendering
Display text with styling:
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```jsx
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)
```javascript
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
### 1. Use ParticleContainer for Large Sprite Counts
```javascript
// 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 }));
}
```
---
### 2. Optimize Filter Usage
```javascript
// 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
});
```
---
### 3. Manage Texture Memory
```javascript
// Destroy textures when done
texture.destroy();
// Batch destruction with delays to prevent frame drops
textures.forEach((tex, i) => {
setTimeout(() => tex.destroy(), Math.random() * 100);
});
```
---
### 4. Enable Culling for Off-Screen Objects
```javascript
sprite.cullable = true; // Skip rendering if outside viewport
// Use CullerPlugin
import { CullerPlugin } from 'pixi.js';
```
---
### 5. Cache Static Graphics as Bitmaps
```javascript
// Convert complex graphics to texture for faster rendering
const complexShape = new Graphics();
// ... draw many shapes
complexShape.cacheAsBitmap = true; // Renders to texture once
```
---
### 6. Optimize Renderer Settings
```javascript
const app = new Application();
await app.init({
antialias: false, // Disable on mobile for performance
resolution: 1, // Lower resolution on low-end devices
autoDensity: true
});
```
---
### 7. Use BitmapText for Dynamic Text
```javascript
// 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**:
```javascript
// 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**:
```javascript
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**:
```javascript
// 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**:
```javascript
// 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**:
```javascript
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**:
```javascript
// 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:
1. **Performance**: Render 100,000+ sprites at 60 FPS
2. **ParticleContainer**: 10x faster for static properties
3. **Filters**: WebGL-powered visual effects
4. **Graphics API**: Intuitive vector drawing
5. **Asset Management**: Robust texture and sprite sheet handling
Use for particle systems, 2D games, data visualizations, and interactive canvas applications where performance is critical.