Guides
Ink tanks and force fields
The solver has two kinds of solid boundary besides its floor. A container is a box the fluid can't leave, which the ink effect uses as a water tank. An obstacle is a moving sphere the fluid must flow around, which the force field uses to part smoke.
#Ink in water
ink: a drop every 0.95 s, cycling through five colours. The drops sink at 2.4 m/s and billow as they slow, lit by a bright white key light.const [ink] = magic.loadPreset('ink'); // physics for water: weight, no buoyancy
ink.drop(); // random spot under the surface, next colour
ink.drop([0.3, 2.2, 0], '#ff4f8e'); // your spot and colour
ink.move([0.3, 1.31, 0]); // moves the whole tank (position = tank centre)The ink effect does two things:
- It sets the frame's container to
container { min, max }. The fluid can't leave that box, andmax.yis the water surface. - Every
intervalseconds it callsdrop(). A drop is animpulse()pushed down atdrop.sinkwith no glow, no heat and no sparks.
Automatic drops land at a random spot within spread [0.95, 0.4] (half-extents in x and z around the tank centre), depth 0.12 m below the surface. Every drop, yours included, is clamped to at least 0.3 m from the side walls.
| Option | Default | |
|---|---|---|
container | { min: [-1.5, 0.12, -0.95], max: [1.5, 2.5, 0.95] } | The tank interior, world space |
interval | 0.95 | Seconds between automatic drops (0 = manual only) |
colors | rose, blue, gold, violet, green | Cycled drop by drop |
spread, depth | [0.95, 0.4], 0.12 | Where automatic drops land |
drop | { radius: 0.2, density: 60, glow: 0, sink: 2.4, duration: 0.22, expansion: 0.05 } | Passed to impulse() for each drop |
#Why it looks like ink
The effect on its own only drops dye. The ink preset supplies the rest:
| Setting | Preset | Default | Why |
|---|---|---|---|
controls.buoyancy | 0 | 1 | Dye doesn't rise |
physics.weight | 1.15 | 0 | Dense dye sinks |
physics.dissipation | 0.02 | 0.85 | Ink doesn't fade |
render.colorAbsorption | 1 | 0.35 | Dye absorbs its complement, so red ink looks red against the light |
render.densityPower | 1.35 | 1 | Sharper edges on the billows |
lighting.keyColor | [2.6, 2.55, 2.45] | [0.16, 0.18, 0.3] | Bright white light on the dye instead of dim moonlight |
post.bloom | 0.08 | 0.5 | Almost none: ink doesn't glow |
#Force fields
forceField: three vents (rose, cyan, gold) whose plumes part around the energy sphere as it wanders.const field = magic.addEffect('forceField', { position: [0, 2.4, 0], radius: 0.5 });
field.move([0.8, 2, 0]); // pauses the wander path for wander.holdTime (2.5 s)
field.velocity; // measured from its motion, clamped to 6 m/s
field.pickTarget; // the shell mesh, for raycast draggingA force field is the frame's obstacle: a solid sphere of radius the fluid must flow around. The field's own velocity is written into the cells it covers, so moving it drags the smoke along with it. A radial push (5 m/s²) acts in a shell out to 2.3 × the radius, so plumes part before they touch it.
When nobody moves it, the field follows a wander path: a Lissajous curve around wander.center with per-axis amplitude and speed. It eases toward the path at follow. Each move() pauses the wander for holdTime seconds, after which it drifts back.
The visual is a fresnel shell with a moving interference pattern and a bright core sphere, added to your scene as MagicForceField. It draws additively and leaves destination alpha alone. Set visual: null for no mesh; the obstacle still works.
| Option | Default | |
|---|---|---|
position, radius | [0, 2.4, 0], 0.5 | |
push | 5 | Radial push (m/s²), scaled by the swirl control and the intensity |
wander | { enabled: true, center: [0, 2.4, 0], amplitude: [1.2, 0.5, 0.3], speed: [0.55, 0.8, 0.37], follow: 2, holdTime: 2.5 } | Idle path |
visual | { color: [0.6, 0.8, 1.6], core: [1.2, 1.6, 3.0] } | Linear HDR colours; setColor() tints the shell |
#Drag it with the mouse
The demo raycasts against pickTarget and calls move() while you drag. The wander pauses as long as you keep moving it.
const hit = raycaster.intersectObject(field.pickTarget, false)[0];
if (hit) dragging = true;
// on pointermove, with a plane through the hit point:
field.move(pointOnPlane);