Getting started
Add Pylinka to a PixiJS v8 app and mount your first effect.
Install
pnpm add @pylinka/core Run it on a canvas
The shortest path needs a canvas and a project, no pixi and no WebGPU. The simulation runs on the GPU through transform feedback, which every WebGL2 browser has. Grab a ready-made effect from the recipes gallery or bend one into shape in the editor.
import { createParticles } from '@pylinka/core/webgl';
import project from './sparks.pylinka.json';
const fx = createParticles(canvas, project);
function frame(dt) {
fx.setEmitter(mouseX, mouseY); // where new particles are born
fx.update(dt); // step the sim and render
requestAnimationFrame(() => frame(1 / 60));
}
requestAnimationFrame(() => frame(1 / 60));
fx.setKnob('windPower', 40); // live, zero recompile
fx.setKnob('cursor', x, y); // vec2 knobs take a second component
fx.spawnBurst(300); This entry point interprets the common node patterns into a fixed GPU model: spawn shape, random velocity and life, gravity, wind, drag, colour and scale over life, obstacles and colliders. An effect that uses nodes outside that set still runs, and the extra nodes are quietly ignored. Convenient right up until the moment you wonder why a node did nothing, so reach for the compiled backends below once an effect gets serious.
The pixi v8 path
To interleave particles inside a PixiJS scene graph, the runtime peers on
pixi.js@^8 and installs itself as a render pipe. A Pylinka project is a
JSON document holding one or more particle systems. Author it in the editor or start from a
recipe, then hand it to your renderer:
import { Application } from 'pixi.js';
import { createPylinka } from '@pylinka/core';
import project from './sparks.pylinka.json';
const app = new Application();
await app.init({ preference: 'webgpu', resizeTo: window });
const pylinka = await createPylinka(project, { renderer: app.renderer });
// add the view to a STATIC layer, never to a moving sprite (see below)
const vfx = new Container();
app.stage.addChild(vfx);
vfx.addChild(pylinka.systems.sparks.view);
app.ticker.add((ticker) => pylinka.update(ticker.deltaMS / 1000));
The backend comes from the renderer you pass. A WebGPU renderer shares its device, a WebGL
renderer gets the WebGL2 simulation in the same context. For headless use, pass an explicit
device instead of a renderer.
Move the emitter, not the view
Simulation happens in world space and the emitter position is read only at spawn. So a trail that follows a moving object comes from driving the emitter. Reparenting the view transforms every live particle along with the sprite, which turns a trail into a rigid clump.
// good: the view sits in a static layer, the emitter follows the target
pylinka.systems.sparks.follow(coinSprite);
// also good: set the position yourself each frame
pylinka.systems.sparks.setEmitterPosition(x, y);
// never: this drags every live particle along with the coin
coinSprite.addChild(pylinka.systems.sparks.view); Tweak values live
Any value promoted to a knob is adjustable at runtime with no recompilation. The write lands on the next frame:
pylinka.params.set('windPower', 40);
pylinka.params.set('windDir', Math.PI * 0.5);
pylinka.params.set('cursor', pointerX, pointerY); // vec2 knob Bursts and lifecycle
const sparks = pylinka.systems.sparks;
sparks.spawnBurst(200); // add 200 particles next frame
sparks.restart(); // reset the pool, plus prewarm if configured
sparks.destroy(); // release GPU resources Core concepts covers systems, the graph and knobs in more depth. The API page has the full runtime surface.