PixiJS v8 Rendering skill for Codex
Batch-friendly 2D scenes that stay at 60 FPS. Codex writes PixiJS v8 code with the async Application init, loads spritesheets through Assets bundles, keeps the scene graph grouped by texture so the batcher works, and switches to ParticleContainer, BitmapText or cached subtrees when object counts climb. It destroys removed objects without killing shared atlases and translates v7 calls such as drawRect, cacheAsBitmap and BaseTexture.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- pixijs-graphics Installs to ~/.agents/skills/pixijs-graphics/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install pixijs-graphics The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project pixijs-graphics Installs to .agents/skills/pixijs-graphics/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/pixijs-graphics.zipunzip -o pixijs-graphics.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) pixijs-graphics PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $pixijs-graphics, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds fast 2D rendering with PixiJS v8 - async Application init, WebGL and WebGPU renderers, Assets bundles and spritesheets, Container scene graphs, the new Graphics API, Text and BitmapText, ParticleContainer, culling, cacheAsTexture, filters and the ticker - and migrates v7 code. Use when drawing sprites, particles, charts or game scenes on canvas with PixiJS, when a Pixi scene drops frames with many objects, when textures leak memory, or when upgrading from PixiJS v7 (drawRect, cacheAsBitmap, BaseTexture, constructor options).
Use it when
- Drawing sprites, particles or vector shapes on canvas with PixiJS
- A Pixi scene slows down as object counts grow
- Textures or GPU memory leak between screens
- Migrating from PixiJS v7 to v8
Use something else when
- You want scenes, physics and input handled for you: use phaser-games
- You are writing custom shader code for effects: use glsl-shaders
How to check the result
Frame time holds at the target object count in the browser performance panel, and a one-minute spawn/destroy loop returns heap and GPU memory to baseline.
The mistake it prevents
Destroying sprites with texture-destroy options while their frames come from a shared spritesheet, which blanks every other sprite using that atlas.
Done well means: textures load once through Assets and are packed into spritesheets, the
scene graph is shallow and grouped by texture, thousands of identical objects go through
ParticleContainer, off-screen content is culled, static content is cached, and anything
removed from the stage is destroyed so GPU memory stays flat.
Before writing code
- Read
AGENTS.mdandpackage.json(pixi.jsversion). v8 changed most constructors; do not mix v7 snippets into a v8 codebase. - Check the bundler (Vite is typical) and whether code imports from
pixi.js(full) or tree-shaken subpaths. - Note the target: mobile browsers decide texture size limits and particle budgets.
Application setup
import { AnimatedSprite, Application, Assets, Container } from "pixi.js";
const app = new Application();
await app.init({
resizeTo: window,
background: "#101018",
antialias: false, // true costs fill rate; only for vector-heavy scenes
resolution: Math.min(window.devicePixelRatio, 2),
autoDensity: true, // CSS size stays logical while the backbuffer is hi-dpi
preference: "webgl", // or "webgpu"; v8 falls back when unsupported
});
document.getElementById("stage")!.appendChild(app.canvas);
| v7 | v8 |
|---|---|
new Application({ ... }) | new Application() then await app.init({ ... }) |
app.view | app.canvas |
graphics.beginFill(c).drawRect(...).endFill() | graphics.rect(...).fill(c) |
lineStyle(2, c) | .stroke({ width: 2, color: c }) |
new Text("hi", style) | new Text({ text: "hi", style }) |
cacheAsBitmap = true | container.cacheAsTexture(true) |
BaseTexture | TextureSource (ImageSource, CanvasSource, …) |
Texture.from(url) loads | Load with Assets.load(url) first; Texture.from only reads the cache |
Ticker callback gets delta | Callback gets the Ticker; use ticker.deltaTime / ticker.deltaMS |
| Sprites can have children | Only Container can have children; wrap leaves |
interactive = true | eventMode = "static" (default is "passive") |
Assets
await Assets.init({ basePath: "assets" });
Assets.addBundle("level1", {
hero: "hero.json", // spritesheet JSON (TexturePacker); frames become textures
tiles: "tiles.png",
font: "ui-font.fnt", // bitmap font
});
const level = await Assets.loadBundle("level1", (p) => (loadingBar.scale.x = p));
const idle = level.hero.animations["hero_idle"];
- Use spritesheets for anything with more than a handful of frames: one texture source means the batcher can draw many sprites in one call.
- Keep atlas pages at or below 2048x2048 for broad mobile support; 4096 only after checking
app.rendererlimits on target devices. Assets.unload("tiles.png")(orunloadBundle) releases GPU memory for level-scoped assets.
Scene graph
const world = new Container({ label: "world" });
const bg = new Container({ label: "bg" });
const actors = new Container({ label: "actors", sortableChildren: true });
world.addChild(bg, actors);
app.stage.addChild(world);
const hero = new AnimatedSprite(idle);
hero.anchor.set(0.5, 1);
hero.animationSpeed = 0.15;
hero.play();
actors.addChild(hero);
app.ticker.add((ticker) => {
hero.x += 120 * (ticker.deltaMS / 1000); // pixels per second, frame-rate independent
hero.zIndex = hero.y; // y-sort only within the sortable layer
});
- Group by texture and blend mode. Interleaving sprites from different sheets, Graphics and Text breaks batches.
sortableChildrensorts every frame something changes; enable it only on the layer that needs it.- Use a camera Container (
world) and move it, rather than moving every object.
Choosing a display object
| Need | Use |
|---|---|
| Images, animated characters | Sprite, AnimatedSprite |
| 1k-100k+ identical-texture particles | ParticleContainer with Particle objects |
| Dynamic vector shapes (debug, charts) | Graphics; share geometry with GraphicsContext |
| Many identical vector shapes | Draw once, app.renderer.generateTexture(graphics), then Sprites |
| Changing text every frame (score, timers) | BitmapText |
| Rarely changing, styled text | Text |
| Repeating background | TilingSprite |
| Static complex subtree | container.cacheAsTexture(true) |
import { ParticleContainer, Particle, Rectangle } from "pixi.js";
const sparks = new ParticleContainer({
dynamicProperties: { position: true, rotation: false, color: false },
boundsArea: new Rectangle(0, 0, app.screen.width, app.screen.height),
});
for (let i = 0; i < 20000; i++) {
sparks.addParticle(new Particle({ texture: sparkTex, x: Math.random() * 800, y: Math.random() * 600 }));
}
app.stage.addChild(sparks);
Particles live in particleChildren, not children; only the properties marked dynamic are
re-uploaded each frame. Set boundsArea because the container does not compute bounds.
Culling and caching
import { Culler, Rectangle } from "pixi.js";
actors.cullableChildren = true;
for (const c of actors.children) c.cullable = true;
const view = new Rectangle(0, 0, app.screen.width, app.screen.height);
app.ticker.add(() => Culler.shared.cull(world, view));
Or register CullerPlugin with extensions.add(CullerPlugin) for automatic culling. Cache a
subtree with cacheAsTexture(true) only when it rarely changes; call
container.updateCacheTexture() after a change, and turn it off for animated content.
Cleanup
function removeEnemy(enemy: Container) {
enemy.removeFromParent();
enemy.destroy({ children: true }); // keep shared atlas textures alive
}
destroy()removes listeners and GPU resources for the object. Pass texture-destroying options only for textures this object owns (agenerateTextureorRenderTextureresult), never for shared atlas frames.- Remove ticker callbacks you added (
app.ticker.remove(fn)) when a screen closes. - Filters (
BlurFilter, custom filters) render to intermediate textures; reuse filter instances and avoid filters on large containers that change every frame.
Gotchas
- Forgetting
await app.init()leavesapp.canvasundefined. - Fractional positions blur pixel art: round the camera position, set
TextureSource.defaultOptions.scaleMode = "nearest"before loading, or set scale mode per source. Textre-rasterises to a canvas texture on every content change; at 60 changes per second that is a CPU and upload cost. UseBitmapText.getBounds()returns aBounds; use.rectanglewhen you need aRectangle.- WebGPU availability varies by browser; keep a WebGL path and test both if you set
preference. - Losing the WebGL context (mobile tab switching) drops GPU resources; Pixi restores managed textures, but custom raw GL calls need handling.
Verification
- Run the dev server and watch the browser performance panel: steady frame time at the target object count, no long tasks during play.
- Spawn and destroy 1000 objects in a loop for a minute; JS heap and GPU memory (Chrome Task Manager GPU column) should return to baseline.
- Count draw calls with a WebGL inspector extension (Spector.js) or the PixiJS DevTools extension when batches look broken.
npm run buildpasses type checking.
Hand-off summary
**PixiJS:** 8.x, WebGL preference, resizeTo window, resolution capped at 2
**Assets:** level1 bundle (hero spritesheet, tiles, bitmap font); unloaded on level exit
**Rendering:** 20k sparks via ParticleContainer (position dynamic only); actors culled each frame
**Leaks fixed:** enemies destroyed with children; ticker callbacks removed on scene exit
**Measured:** 60 FPS at 20k particles on mid laptop; heap flat after 1 min spawn/destroy loop What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "PixiJS v8 Rendering"
short_description: "Batch-friendly 2D scenes that stay at 60 FPS"
default_prompt: "$pixijs-graphics Render 30,000 particles with PixiJS without dropping frames" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.