pylinka
Menu

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.