Guides
Effects and handles
An effect is a small object that pushes sources into the fluid every frame. You add one by type name, get a handle back, and steer it while it runs. The fluid, not the effect, decides what the result looks like.
#Adding effects
const portal = magic.addEffect('portal', { position: [0, 2.2, 0], colors: ['#a66bff', '#35d4ff'] });
const ring = magic.addEffect('vortexRing', { position: [-1.2, 1, 0], direction: [1, 0.35, 0] });
const vent = magic.addEmitter({ position: [0, 0.3, 0], velocity: [0, 1.3, 0], color: '#ff4f8e' });options deep-merge onto the class's static defaults, so you only pass what you change. You can also pass intensity. addEmitter(options) is addEffect('emitter', options) with the vectors and colour normalised first.
| Type | What it pushes into the fluid | Guide |
|---|---|---|
portal | Two torus emitters, a swirl field, rim sparks | Portals and spell orbs |
spellOrb | count orbiting emitters with trails, point lights and core meshes | Portals and spell orbs |
genieLamp | A glowing jet, a swirl column above it, a smoke ring every 3.6 s | Genie smoke and vortex rings |
vortexRing | Travelling rings with a confinement boost and an analytic torus | Genie smoke and vortex rings |
dragonBreath | A pulsing fuel jet, the flame palette, embers, glow flicker | Dragon breath |
ink | A solid tank and sinking dye drops | Ink tanks and force fields |
forceField | A moving solid sphere with a radial push | Ink tanks and force fields |
emitter | One sphere or torus of dye, glow, heat and fuel | Custom spells |
EFFECT_TYPES maps each name to its class (PortalEffect, SpellOrbEffect, … EmitterEffect). An unknown name throws and lists the known ones.
#The handle
Every effect, built in or custom, extends MagicEffect. All the methods that change it return the handle, so they chain.
portal
.move([0.5, 2.2, 0]) // world position
.retarget({ target: camera.position }) // face the camera
.setColor('#3dff8a') // override every colour of the effect
.setIntensity(0.6); // scale glow, density and spark rate
portal.set({ swirl: { speed: 5 } }); // deep-merge options live
portal.enabled = false; // pause it and hide its meshes
portal.setColor(null); // back to its own palette
portal.remove();| Member | |
|---|---|
type, options, enabled, intensity | options is plain JSON: vectors and colours are stored as arrays |
position | A copy of the current position as a Vector3 |
move(position) | Sets options.position. The ink tank moves as a whole, and the force field pauses its wander path |
retarget(direction | { target }) | Aims direction (rings, dragon) or axis (portal). The lamp and emitters keep their speed and change the direction of velocity. Effects without a direction ignore it |
setColor(color | null) | Overrides every colour of the effect. null restores its palette |
setIntensity(k) | A multiplier (clamped at 0) on glow, density and spark rate |
set(partialOptions) | Deep-merges options, with colours normalised |
remove() | Removes it from the system and frees its meshes and lights |
toJSON() | { type, options, intensity? }, the form presets and getParams() use |
portal preset with its own palettesetColor([0.2, 1, 0.5]). The demo tints its rune ring from portal.colors; the rim sparks keep their own sparks.colorA and colorB#Finding and removing
magic.effects; // every live handle, in the order they were added
magic.getEffect('vortexRing'); // the first of that type, or null
magic.removeEffect(handle); // same as handle.remove()
magic.removeAllEffects();loadPreset() removes every effect and adds the preset's, and it returns the new handles:
const [dragon] = magic.loadPreset('dragonBreath');
dragon.retarget([1, 0, 0.2]);#Impulses
impulse() is a one-shot burst rather than an effect: it injects dye, glow, heat and a push for duration seconds, and throws sparks until 0.3 s after it ends. Use it for clicks, hits and spell impacts.
magic.impulse(position, direction = [0, 1, 0], color, {
radius: 0.45, speed: 0.6, density: 26, glow: 8,
temperature: 0.2, fuel: 0, expansion: 0.35, duration: 0.16, sparks: true,
});Those are the defaults. expansion blows the burst outward through the pressure solve, and speed pushes it along direction. The default colour is violet ([0.62, 0.3, 1.0]). At most 4 impulses are in flight: a fifth drops the oldest.
#Per-frame limits
All effects share fixed uniform slots. LIMITS holds the capacities:
| Slot | Per frame | Used by |
|---|---|---|
emitters | 8 | Portal (2), genie lamp (1), spell orbs (1 per orb), dragon (1), emitter (1) |
impulses | 4 | impulse() and ink drops |
rings | 4 | Vortex rings and genie-lamp rings in flight |
swirls | 2 | Portal, genie lamp |
spawners | 4 | Spark sources: effects with sparks, plus each impulse |
There's also one obstacle sphere (a force field) and one container (an ink tank). Anything past a limit is ignored for that frame, with one console.warn per slot type. A portal and a genieLamp together already fill both swirl slots.
#Custom spells
#With emitters
addEmitter() covers most custom work. The fluid inside the emitter is driven toward velocity by blend, and rate is the dye density added per second.
magic.addEmitter({
position: [0, 0.4, 0],
velocity: [0, 1.6, 0], // m/s the fluid inside is driven toward
blend: 0.35, // how hard it is driven (0..1+)
color: '#35d4ff',
radius: 0.2,
rate: 6, // dye density per second
glow: 1.5, // glow per second
temperature: 0.3, // heat, so buoyancy lifts it
fuel: 0, // > 0 burns (see Dragon breath)
ring: { axis: [0, 1, 0], radius: 0.6 }, // optional: a torus instead of a sphere
sparks: { rate: 0.5, radius: 0.2, life: 0.5, jitter: 1.5 }, // optional
});Those are the defaults, except position, velocity, color, ring and sparks. In sparks, radius defaults to the emitter's radius, life to 0.5, jitter to 2.5, colorA to twice the dye colour and colorB to [1.5, 1.5, 1.5].
#With your own effect class
Subclass MagicEffect, give it static defaults, and push world-space sources into the frame in update(t, dt, frame). Register it in EFFECT_TYPES and it works with addEffect, presets and getParams().
import * as THREE from 'three';
import { MagicEffect, EFFECT_TYPES } from 'naturegl-magic';
class Beacon extends MagicEffect {
static defaults = { position: [0, 1, 0], color: [0.3, 1, 0.6] };
update(t, dt, frame) {
frame.emitters.push({
position: this.position, radius: 0.15, ring: null,
color: this._col(this.options.color), // honours setColor()
glow: 3 * this.intensity, density: 2, temperature: 0.4, fuel: 0,
velocity: new THREE.Vector3(0, 2, 0), blend: 0.5,
});
}
}
EFFECT_TYPES.beacon = Beacon;
magic.addEffect('beacon', { position: [1, 0.5, 0] });The frame has emitters, impulses, rings, swirls and spawners lists, plus single obstacle, container, palette and flicker fields. The typedefs are in src/effects/MagicEffect.js. Override onReset() to drop state when the fields are cleared, onDisabled() to hide meshes while enabled is false, and dispose() to free what you added to the scene.