NatureGL Magicv1.0.0

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

js
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.

TypeWhat it pushes into the fluidGuide
portalTwo torus emitters, a swirl field, rim sparksPortals and spell orbs
spellOrbcount orbiting emitters with trails, point lights and core meshesPortals and spell orbs
genieLampA glowing jet, a swirl column above it, a smoke ring every 3.6 sGenie smoke and vortex rings
vortexRingTravelling rings with a confinement boost and an analytic torusGenie smoke and vortex rings
dragonBreathA pulsing fuel jet, the flame palette, embers, glow flickerDragon breath
inkA solid tank and sinking dye dropsInk tanks and force fields
forceFieldA moving solid sphere with a radial pushInk tanks and force fields
emitterOne sphere or torus of dye, glow, heat and fuelCustom 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.

js
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, intensityoptions is plain JSON: vectors and colours are stored as arrays
positionA 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
The portal preset with its own palette
The same portal after setColor([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

js
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:

js
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.

js
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:

SlotPer frameUsed by
emitters8Portal (2), genie lamp (1), spell orbs (1 per orb), dragon (1), emitter (1)
impulses4impulse() and ink drops
rings4Vortex rings and genie-lamp rings in flight
swirls2Portal, genie lamp
spawners4Spark 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.

js
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().

js
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.