PixiJS Animation & Interactivity — Ticker, Events, Drag-and-Drop, Filters
PixiJS animation uses a ticker that runs code every frame with delta time, keeping motion smooth. Combined with pointer events, you can build interactive scenes.
What You’ll Learn
- How the
Tickerdrives frame-based animation with delta timing - How to make sprites interactive with
eventModeand pointer events - The drag-and-drop pattern (stage-level tracking)
- How hit areas define clickable regions
- How filters (blur, color matrix) create visual effects
- How to build a custom tween (animation) using the ticker
Why Animation & Interactivity Matter
Static graphics are informative — animated, interactive graphics are engaging. A threat dashboard that animates incoming alerts and lets you drag threat nodes to inspect them is far more usable than a static screenshot.
In Durga Antivirus Pro, PixiJS powers the real-time threat map. Security alerts pulse with animated icons, threat nodes are draggable for inspection, and color filters highlight critical vs. low-priority threats. These interactive features help security analysts spot anomalies in seconds rather than minutes.
Learning Path
flowchart LR
GS["Getting Started"] --> GT["Graphics & Textures"]
GT --> AI["Animation & Interactivity ⬅ You Are Here"]
AI --> TF["Text & Bitmap Fonts"]
TF --> PB["Performance & Best Practices"]
style AI fill:#4a90d9,stroke:#fff,color:#fff
linkStyle default stroke:#4a90d9,stroke-width:2
PixiJS Animation vs CSS Animations vs requestAnimationFrame
| Approach | Control | Performance | Complexity |
|---|---|---|---|
| PixiJS Ticker | Frame-level, delta time | Excellent (GPU) | Low |
| CSS animations | Limited transforms | Good (GPU) | Very low |
| requestAnimationFrame | Full control | Good | Medium |
| GSAP | Tween-based, easing | Good | Medium |
When to use PixiJS Ticker: You need per-frame control of hundreds of objects — position, rotation, scale, alpha all changing simultaneously. The ticker automatically integrates with PixiJS’s render loop.
The Ticker — PixiJS’s Built-in Animation Clock
The Ticker runs a callback function every frame (typically 60 times per second). It provides a delta parameter that represents how much time passed since the last frame.
Think of the ticker like a metronome that ticks 60 times per second. Each tick, you move your objects a little bit. The delta value ensures movement stays smooth even when the frame rate dips.
app.ticker.add((delta) => {
sprite.rotation += 0.1 * delta;
});Why multiply by delta? Without delta, your animation runs slower when the frame rate drops — the sprite moves less because fewer frames execute. With delta, the movement per frame scales up to compensate, keeping the speed consistent at 30fps or 60fps.
Adding and Removing Callbacks
const tickerFn = (delta) => {
sprite.x += 2 * delta;
};
app.ticker.add(tickerFn); // Start
app.ticker.remove(tickerFn); // Stop (must keep reference)
Always keep a reference to your callback. If you pass an anonymous function to add(), you can’t remove() it later. This is a common source of memory leaks.
Controlling the Ticker
app.ticker.stop(); // Pause all updates
app.ticker.start(); // Resume
app.ticker.destroy(); // Clean up and remove all listeners
Making Objects Interactive
In PixiJS 7+, you make an object interactive by setting eventMode:
const sprite = new PIXI.Sprite(texture);
sprite.eventMode = 'static'; // Enable click/touch events
sprite.cursor = 'pointer'; // Show hand cursor on hover
sprite.on('pointerdown', () => {
console.log('Clicked!');
});Why eventMode instead of interactive = true? The old interactive property was deprecated in PixiJS 7. eventMode is more explicit about how events are processed. 'static' means the object doesn’t move in ways that affect hit testing — the most common and fastest mode.
Pointer Events Reference
| Event | When it fires |
|---|---|
pointerdown | Finger/mouse is pressed |
pointerup | Finger/mouse is released |
pointermove | Pointer moves over the object |
pointerover | Pointer enters the object’s hit area |
pointerout | Pointer leaves the object’s hit area |
click | Complete click (down + up within same target) |
rightclick | Right mouse button click |
pointertap | Tap (touch equivalent of click) |
sprite.on('pointerover', () => { sprite.tint = 0x999999; });
sprite.on('pointerout', () => { sprite.tint = 0xffffff; });Drag-and-Drop Pattern
Drag-and-drop is one of the most common interactive patterns. Here’s the key insight: track drag on the stage, not on the dragged object. If you track on the object, you lose events when the pointer moves faster than the object.
let dragTarget = null;
let dragOffset = { x: 0, y: 0 };
function makeDraggable(obj) {
obj.eventMode = 'static';
obj.cursor = 'grab';
obj.on('pointerdown', (e) => startDrag(e, obj));
}
function startDrag(e, obj) {
dragTarget = obj;
// Store the offset between pointer and object position
dragOffset = {
x: e.data.global.x - obj.x,
y: e.data.global.y - obj.y,
};
dragTarget.alpha = 0.6;
dragTarget.cursor = 'grabbing';
}
// Stage-level tracking — always works
app.stage.on('pointermove', (e) => {
if (dragTarget) {
dragTarget.x = e.data.global.x - dragOffset.x;
dragTarget.y = e.data.global.y - dragOffset.y;
}
});
app.stage.on('pointerup', () => endDrag());
app.stage.on('pointerupoutside', () => endDrag());
function endDrag() {
if (dragTarget) {
dragTarget.alpha = 1;
dragTarget.cursor = 'grab';
dragTarget = null;
}
}Why store an offset? Without the offset, the object jumps so its top-left corner is at the pointer position. Storing pointer position - object position on pointerdown lets you maintain the same relative position while dragging.
Hit Areas
By default, the clickable region of a sprite is its rectangular bounding box. You can override this with hitArea:
// Rectangle hit area
sprite.hitArea = new PIXI.Rectangle(0, 0, 64, 64);
// Circle hit area (great for round objects)
sprite.hitArea = new PIXI.Circle(32, 32, 32);
// Polygon hit area (triangular, hexagonal, etc.)
sprite.hitArea = new PIXI.Polygon([
32, 0,
64, 64,
0, 64,
]);When to use custom hit areas: For non-rectangular objects (circles, stars), the default rectangular hit area captures clicks outside the visible shape. A matching hit area improves user experience.
Reset to default: sprite.hitArea = null.
Filters — Visual Effects Made Easy
Filters apply GPU-powered visual effects to display objects. Think of them like Instagram filters for your sprites.
// Blur filter
const blurFilter = new PIXI.BlurFilter();
blurFilter.blur = 4;
// Color matrix filter (hue rotation)
const colorFilter = new PIXI.ColorMatrixFilter();
colorFilter.hue(30, true);
// Apply multiple filters
sprite.filters = [blurFilter, colorFilter];Built-in Filter Gallery
| Filter | Effect | Use Case |
|---|---|---|
BlurFilter | Gaussian blur | Motion blur, focus effects |
ColorMatrixFilter | Color transformation | Sepia, hue rotation, desaturate |
DisplacementFilter | Image distortion | Ripple, heat haze effects |
AlphaFilter | Per-object alpha | Fade effects |
Filter Performance
Filters require rendering to an offscreen texture, which costs GPU time. To limit the affected area:
sprite.filterArea = new PIXI.Rectangle(0, 0, 200, 200);Best practice: Apply filters to a parent Container rather than individual children. This reduces the number of offscreen render passes.
Tweening with the Ticker
A tween (in-between animation) smoothly transitions a property from one value to another over time. PixiJS doesn’t include a built-in tween library, but you can build one with the ticker:
function tweenTo(obj, targetProps, duration = 60) {
const start = {
x: obj.x, y: obj.y,
alpha: obj.alpha,
rotation: obj.rotation,
};
let elapsed = 0;
const update = (delta) => {
elapsed += delta;
const t = Math.min(elapsed / duration, 1);
// easeInOutCubic — smooth acceleration and deceleration
const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
if (targetProps.x !== undefined) obj.x = start.x + (targetProps.x - start.x) * ease;
if (targetProps.y !== undefined) obj.y = start.y + (targetProps.y - start.y) * ease;
if (targetProps.alpha !== undefined) obj.alpha = start.alpha + (targetProps.alpha - start.alpha) * ease;
if (targetProps.rotation !== undefined) obj.rotation = start.rotation + (targetProps.rotation - start.rotation) * ease;
if (t >= 1) app.ticker.remove(update); // Auto-cleanup
};
app.ticker.add(update);
}
// Usage: tween a sprite to x:400 over 90 frames
tweenTo(mySprite, { x: 400 }, 90);Why ease functions? Linear animation looks robotic. Easing functions simulate natural motion — objects accelerate when starting and decelerate when stopping, like real physics.
For complex animations, integrate GSAP — it provides professional-grade tweening, timelines, and easing.
Common Mistakes
| Mistake | Why it happens | How to fix it |
|---|---|---|
Not using eventMode | In PixiJS 7+, interactive = true is deprecated | Set eventMode = 'static' on interactive objects |
| Ticker callback memory leak | Added callback without keeping a reference | Store the function in a variable and call remove() |
| Drag events lost on fast movement | Events attached to dragged object, not stage | Track pointermove and pointerup on the stage |
| Applying filters to every child | Each filter render pass costs GPU time | Apply filters to parent Container instead |
| Filter area too small | Effect gets clipped visibly | Expand filterArea to cover the effect’s bounds |
| Animation speed varies with FPS | No delta multiplication in ticker | Always multiply movement by delta |
Practice Questions
What does the
deltaparameter in the ticker callback represent?- It’s a time multiplier where 1.0 = 60fps. At 30fps, delta is ~2.0, so movements scale up to keep speed consistent.
Why should
pointermovebe handled on the stage for drag-and-drop?- If attached to the dragged object, events are lost when the pointer moves faster than the object. The stage always receives events.
What is the difference between
pointerdownandclick?pointerdownfires immediately on press.clickfires only after down + up on the same target — useful for buttons.
How do you remove a ticker callback?
- Keep a reference to the function and call
app.ticker.remove(myFn). Anonymous functions cannot be removed.
- Keep a reference to the function and call
What happens if you apply too many filters to many objects?
- Each filter requires an offscreen render pass. Performance degrades quickly. Use parent containers and limit
filterArea.
- Each filter requires an offscreen render pass. Performance degrades quickly. Use parent containers and limit
Challenge: Build a particle system where 100 small circles spawn from a center point, fade out while expanding, and are recycled. Use the ticker for position/alpha updates and a Pool for object reuse.
FAQ
Try It Yourself
Open this interactive toy in your browser — drag shapes, toggle blur/color filters, enable auto-rotation, and watch shapes bounce on click.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>PixiJS Interactive Toy</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; font-family: system-ui, sans-serif; display: flex; justify-content: center; padding: 20px; }
.app { display: flex; gap: 20px; flex-wrap: wrap; }
canvas { border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
.panel { background: #16213e; border-radius: 12px; padding: 20px; color: #fff; width: 220px; display: flex; flex-direction: column; gap: 12px; }
.panel h2 { font-size: 18px; }
.toggle { display: flex; align-items: center; gap: 10px; }
.toggle input[type="checkbox"] { width: 20px; height: 20px; accent-color: #4a90d9; cursor: pointer; }
.btn { padding: 10px; border: none; border-radius: 8px; background: #4a90d9; color: #fff; font-weight: 600; cursor: pointer; }
.btn:hover { background: #357abd; }
.btn.success { background: #2d6a4f; }
.stats { font-size: 12px; color: #8892b0; }
</style>
</head>
<body>
<div class="app">
<div id="canvas-container"></div>
<div class="panel">
<h2>Interactive Toy</h2>
<div class="toggle">
<input type="checkbox" id="chkBlur" />
<label for="chkBlur">Blur Filter</label>
</div>
<div class="toggle">
<input type="checkbox" id="chkColor" />
<label for="chkColor">Color Shift</label>
</div>
<div class="toggle">
<input type="checkbox" id="chkRotate" checked />
<label for="chkRotate">Auto Rotate</label>
</div>
<button class="btn" id="addBtn">+ Add Shape</button>
<button class="btn success" id="animateBtn">Bounce All</button>
<div class="stats" id="stats">Shapes: 0</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/pixi.js@7.x/dist/pixi.min.js"></script>
<script>
const COLORS = [0xff6600, 0x9933ff, 0x00cc99, 0xff3366, 0x3399ff];
const SHAPES = ['rect', 'circle', 'star', 'polygon'];
const app = new PIXI.Application({ width: 700, height: 600, backgroundColor: 0x1a1a2e, antialias: true });
document.getElementById('canvas-container').appendChild(app.view);
const shapes = [];
let counter = 0;
const blurFilter = new PIXI.BlurFilter(); blurFilter.blur = 0;
const colorFilter = new PIXI.ColorMatrixFilter(); colorFilter.hue(0, true);
function createShape(x, y) {
const g = new PIXI.Graphics();
g.beginFill(COLORS[counter % COLORS.length], 0.85);
g.lineStyle(2, 0xffffff, 0.5);
const s = 30 + Math.random() * 40;
const t = SHAPES[counter % SHAPES.length]; counter++;
switch (t) {
case 'rect': g.drawRoundedRect(-s/2, -s/2, s, s, 6); break;
case 'circle': g.drawCircle(0, 0, s/2); break;
case 'star': g.drawStar(0, 0, 5, s/2, s/5); break;
case 'polygon': g.drawPolygon([0,-s/2, s/2,s/3, -s/2,s/3]); break;
}
g.endFill();
g.x = x ?? 100 + Math.random() * 500;
g.y = y ?? 100 + Math.random() * 400;
g.eventMode = 'static'; g.cursor = 'grab';
g.on('pointerdown', (e) => {
g.alpha = 0.6; g.cursor = 'grabbing';
g.parent.setChildIndex(g, g.parent.children.length - 1);
g.dragData = { x: e.data.global.x - g.x, y: e.data.global.y - g.y };
});
g.on('pointertap', () => { g.scale.set(1.3); setTimeout(() => g.scale.set(1), 200); });
app.stage.addChild(g); shapes.push(g); document.getElementById('stats').textContent = `Shapes: ${shapes.length}`;
}
app.stage.on('pointermove', (e) => {
shapes.forEach(g => { if (g.dragData) { g.x = e.data.global.x - g.dragData.x; g.y = e.data.global.y - g.dragData.y; } });
});
app.stage.on('pointerup', () => shapes.forEach(g => { if (g.dragData) { g.dragData = null; g.alpha = 1; g.cursor = 'grab'; } }));
app.stage.on('pointerupoutside', () => shapes.forEach(g => { if (g.dragData) { g.dragData = null; g.alpha = 1; g.cursor = 'grab'; } }));
let autoRotate = true;
app.ticker.add((delta) => { if (autoRotate) shapes.forEach(g => g.rotation += 0.02 * delta); });
document.getElementById('chkBlur').addEventListener('change', e => {
blurFilter.blur = e.target.checked ? 6 : 0;
shapes.forEach(g => { g.filters = e.target.checked ? [blurFilter] : []; });
});
document.getElementById('chkColor').addEventListener('change', e => {
colorFilter.hue(e.target.checked ? 60 : 0, true);
shapes.forEach(g => { g.filters = e.target.checked ? [colorFilter] : []; });
});
document.getElementById('chkRotate').addEventListener('change', e => { autoRotate = e.target.checked; });
document.getElementById('addBtn').addEventListener('click', () => createShape());
document.getElementById('animateBtn').addEventListener('click', () => {
shapes.forEach((g, i) => {
setTimeout(() => {
const targetY = g.y < 300 ? 500 : 100;
const fn = (delta) => { g.y += (targetY - g.y) * 0.08 * delta; if (Math.abs(g.y - targetY) < 1) app.ticker.remove(fn); };
app.ticker.add(fn);
}, i * 50);
});
});
createShape(200, 200); createShape(400, 300); createShape(550, 200); createShape(150, 400);
window.addEventListener('resize', () => {
const w = Math.min(window.innerWidth - 280, 700); const h = Math.min(window.innerHeight - 40, 600);
app.renderer.resize(Math.max(w, 200), Math.max(h, 200));
});
</script>
</body>
</html>What’s Next
| Topic | Link |
|---|---|
| Text and bitmap fonts | https://tutorials.dodatech.com/frontend/libraries/pixijs/pixijs-text-fonts/ |
| Performance and best practices | https://tutorials.dodatech.com/frontend/libraries/pixijs/pixijs-performance/ |
| Professional animations with GSAP | GSAP Animation Guide |
| JavaScript event handling | JavaScript Events |
Related Tutorials
- https://tutorials.dodatech.com/frontend/libraries/pixijs/pixijs-getting-started/ — review sprites and stage
- https://tutorials.dodatech.com/frontend/libraries/pixijs/pixijs-graphics-textures/ — drawing shapes to animate
- WebGL for 2D — what’s happening under the hood
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Durga Antivirus Pro uses animated, interactive threat nodes so analysts can drag, inspect, and filter security alerts in real time.
What’s Next
Congratulations on completing this Pixijs Animation Interactivity tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro