✨ 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 do nothing — the whole of math.* and input.* among them. Ask it what it can run with isInterpreted(kind) or unsupportedNodes(system) from @pylinka/core/webgl; the editor uses those to mark such nodes inert while this backend is the one running. Reach for the compiled backends below once an effect gets serious — they evaluate the graph and cover the whole catalog.

Projects with several emitters

A project can hold more than one system, and one system's particles can be born from another's — sparks from a rocket, embers from smoke. That link is part of the project, chosen when the effect was authored, so loading the project is all it takes:

const fx = createParticles(canvas, project);   // every enabled system, linked
fx.update(1 / 60);                             // steps them, parents first

The handle drives the whole project: setKnob and setEmitter reach every system, spawnBurst reaches the ones that are not born from another (bursting a sub-emitter means nothing — its particles come from its parent's), and the clear belongs to the first so the rest composite on top.

Pass systemName to build a single system instead, and drive them yourself; the order and the links are available as systemsInBuildOrder(project), or buildProject(project, create) if you want the walk done for you.

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.