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.

  • Game Development
  • $pixijs-graphics
  • 188 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- pixijs-graphics

Installs to ~/.agents/skills/pixijs-graphics/ for every project on this machine.

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.

~/.agents/skills/pixijs-graphics/SKILL.md

--- name: pixijs-graphics description: Builds fast 2D rendering with PixiJS v8 - async Application init, WebGL and WebGPU renderers, Assets bundles and spritesheets, Container scene… license: MIT ---

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

  1. Read AGENTS.md and package.json (pixi.js version). v8 changed most constructors; do not mix v7 snippets into a v8 codebase.
  2. Check the bundler (Vite is typical) and whether code imports from pixi.js (full) or tree-shaken subpaths.
  3. 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);
v7v8
new Application({ ... })new Application() then await app.init({ ... })
app.viewapp.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 = truecontainer.cacheAsTexture(true)
BaseTextureTextureSource (ImageSource, CanvasSource, …)
Texture.from(url) loadsLoad with Assets.load(url) first; Texture.from only reads the cache
Ticker callback gets deltaCallback gets the Ticker; use ticker.deltaTime / ticker.deltaMS
Sprites can have childrenOnly Container can have children; wrap leaves
interactive = trueeventMode = "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.renderer limits on target devices.
  • Assets.unload("tiles.png") (or unloadBundle) 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.
  • sortableChildren sorts 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

NeedUse
Images, animated charactersSprite, AnimatedSprite
1k-100k+ identical-texture particlesParticleContainer with Particle objects
Dynamic vector shapes (debug, charts)Graphics; share geometry with GraphicsContext
Many identical vector shapesDraw once, app.renderer.generateTexture(graphics), then Sprites
Changing text every frame (score, timers)BitmapText
Rarely changing, styled textText
Repeating backgroundTilingSprite
Static complex subtreecontainer.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 (a generateTexture or RenderTexture result), 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() leaves app.canvas undefined.
  • Fractional positions blur pixel art: round the camera position, set TextureSource.defaultOptions.scaleMode = "nearest" before loading, or set scale mode per source.
  • Text re-rasterises to a canvas texture on every content change; at 60 changes per second that is a CPU and upload cost. Use BitmapText.
  • getBounds() returns a Bounds; use .rectangle when you need a Rectangle.
  • 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 build passes 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.yamlgenerated from this page

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.

esc