Implementing Onion Skinning with PixiJS
GrokBenard
Sign in to confirm0 confirmations
Question
How can I implement onion skinning using PixiJS to display previous and next animation frames behind and in front of the current frame?
Answer
To achieve onion skinning in PixiJS, you can use containers to group and layer objects, with the rendering order following the child order. Create a main stage container, an animation container, an onion skin container, and a current frame container. Store each frame as a Sprite and add them to their respective containers with adjusted alpha and tint for onion skinning. Utilize PixiJS v8's Render Layers for advanced control over rendering order.
typescript
import { Application, Container, Sprite, Texture, Assets } from 'pixi.js';
const app = new Application();
await app.init({ background: '#111', resizeTo: window });
document.body.appendChild(app.canvas);
// Main stage container
const stage = app.stage;
// Container for the entire animation
const animationContainer = new Container();
stage.addChild(animationContainer);
// Onion skin container (renders behind current frame)
const onionContainer = new Container();
animationContainer.addChild(onionContainer);
// Current frame container (on top)
const currentContainer = new Container();
animationContainer.addChild(currentContainer);typescript
class OnionSkinAnimation {
private frames: Sprite[] = [];
private currentIndex = 0;
private prevCount = 2;
private nextCount = 2;
private baseOpacity = 0.35;
constructor(private onionContainer: Container, private currentContainer: Container) {}
async addFrame(texture: Texture): Promise<Sprite> {
const sprite = new Sprite(texture);
sprite.anchor.set(0.5); // Center for easy positioning
this.frames.push(sprite);
return sprite;
}
updateOnionSkin() {
// Clear previous onion sprites
this.onionContainer.removeChildren();
const currentSprite = this.frames[this.currentIndex];
if (!currentSprite) return;
// Previous frames (blue tint)
for (let i = 1; i <= this.prevCount; i++) {
const idx = this.currentIndex - i;
if (idx < 0) break;
const prevSprite = this.frames[idx].clone(); // or reuse with visibility
prevSprite.alpha = this.baseOpacity * (1 - (i - 1) * 0.25);
prevSprite.tint = 0x88aaff; // blue-ish
this.onionContainer.addChild(prevSprite);
}
// Current frame
this.currentContainer.removeChildren();
const currClone = currentSprite.clone();
currClone.alpha = 1;
currClone.tint = 0xffffff;
this.currentContainer.addChild(currClone);
// Next frames (red tint)
for (let i = 1; i <= this.nextCount; i++) {
const idx = this.currentIndex + i;
if (idx >= this.frames.length) break;
const nextSprite = this.frames[idx].clone();
nextSprite.alpha = this.baseOpacity * (1 - (i - 1) * 0.25);
nextSprite.tint = 0xff88aa; // red-ish
this.onionContainer.addChild(nextSprite);
}
}
setCurrentFrame(index: number) {
this.currentIndex = Math.max(0, Math.min(index, this.frames.length - 1));
this.updateOnionSkin();
}
}typescript
import { RenderLayer } from 'pixi.js';
// Create dedicated render layers
const backgroundLayer = new RenderLayer();
const onionLayer = new RenderLayer();
const currentLayer = new RenderLayer();
const uiLayer = new RenderLayer();
app.stage.addChild(backgroundLayer, onionLayer, currentLayer, uiLayer);
// Attach sprites to layers instead of normal parenting
onionLayer.attach(onionSprite);
currentLayer.attach(currentSprite);typescript
container.sortableChildren = true;
sprite.zIndex = 10; // higher = on top
container.sortChildren();typescriptpixijsonionskinning