Skip to content

AI Mode — Steering & Smart Particles

Give your simulation a brain. Import it — import ai from 'ai'; at the top of main.js — then call ai.* to give particles intelligence. The ai module is built in, in the editor and on published pages, so the same code behaves identically everywhere. It turns ordinary particles into agents that can perceive the canvas, decide what to do, and act — the classic "behavioral AI" used in games (steering behaviors and Craig Reynolds' flocking).

Paste the demo below into main.js and hit Restart.

The Perceive → Think → Act model

An agent is just a particle with a velocity — { x, y, vx, vy }. Intelligence comes from three steps each frame:

StepWhatai helper
PerceiveRead the environment: cursor, canvas size, nearby agentsai.mouse(), ai.sense(agent, w, h, time), ai.neighbors(agents, agent, radius)
ThinkDecide a steering force { fx, fy }your brain(agent, sense) function, or ai.seek, ai.flock, ...
ActApply the force, clamp speed, moveai.think(agent, sense, dt)

Demo: a particle that chases your cursor

javascript
import ai from 'ai';

if (!state.p) {
  state.p = ai.agent({
    x: 100, y: 100,
    maxSpeed: 180, maxForce: 90,
    brain: (me, sense) => ai.seek(me, sense.mouse.x, sense.mouse.y, 90),
  });
}

ai.think(state.p, ai.sense(state.p, w, h, time), dt);
ai.wrap(state.p, w, h);

return {
  particles: [{ x: state.p.x, y: state.p.y, r: 10, color: '#8B5CF6', label: 'seeker' }],
  vectors: [{ x: state.p.vx * 0.3, y: state.p.vy * 0.3, ox: state.p.x, oy: state.p.y, color: '#22C55E' }],
};

Move the mouse over the canvas — the particle pursues it. sense.mouse is the live cursor position; it's a new capability, previously simulations only knew where you clicked.

Because ai acts on ordinary particles, the analysis toolbar still works on them: enable velocity vectors or the value graphs and watch the seeker's motion measured live.

Steering behaviors

Each behavior returns a force { fx, fy } that ai.think applies as acceleration. The magnitude is clamped to maxForce (how strongly the agent reacts); speed is capped at maxSpeed.

BehaviorWhat it doesReturns
ai.seek(agent, tx, ty, maxForce)Steer toward a pointforce
ai.flee(agent, tx, ty, maxForce, radius)Steer away; no force beyond radiusforce
ai.arrive(agent, tx, ty, slowRadius, maxForce)Seek, then ease in near the targetforce
ai.pursue(agent, target, maxForce)Chase a moving target, leading itforce
ai.evade(agent, target, maxForce, radius)Flee a moving target's future positionforce
ai.wander(agent, maxForce, radius, distance, jitter)Smooth, random wanderingforce
ai.separate / align / cohesionThe three flocking rulesforce
ai.flock(agent, neighbors, opts)All three combined + clampedforce
ai.avoid(agent, obstacles, opts)Steer around { x, y, r } obstaclesforce

The math is readable on purpose — that's the lesson. seek is just normalize(target − position) × maxForce:

javascript
const dx = tx - me.x, dy = ty - me.y;
const d = Math.hypot(dx, dy) || 1;
return { fx: (dx / d) * maxForce, fy: (dy / d) * maxForce };

Flocking: 40 agents that think as one

A whole flock of agents, each perceiving its neighbors and applying all three rules, produces emergent group behavior — no central controller:

javascript
import ai from 'ai';

if (!state.flock) {
  state.flock = Array.from({ length: 40 }, () =>
    ai.agent({
      x: Math.random() * w, y: Math.random() * h,
      maxSpeed: 110, maxForce: 55,
      brain: (me, sense) => {
        const near = ai.neighbors(state.flock, me, 55);
        const f = ai.flock(me, near, { perception: 55, sep: 1.6, ali: 1, coh: 1, maxForce: 55 });
        const m = ai.seek(me, sense.mouse.x, sense.mouse.y, 12);
        return { fx: f.fx + m.fx, fy: f.fy + m.fy };
      },
    })
  );
}

for (const b of state.flock) {
  ai.think(b, ai.sense(b, w, h, time), dt);
  ai.wrap(b, w, h);
}

return {
  particles: state.flock.map((b) => ({ x: b.x, y: b.y, r: 4, color: '#8B5CF6' })),
  vectors: state.flock.filter((_, i) => i % 4 === 0).map((b) => ({
    x: b.vx * 0.15, y: b.vy * 0.15, ox: b.x, oy: b.y, color: '#22C55E',
  })),
};

The flock drifts on its own and also reacts to your cursor. Tune the weights: raise sep to spread out, raise coh to clump, raise ali to march in formation.

Intelligence for the whole simulation, not just particles

ai.mouse() isn't only for agents — any part of your simulation can react to the user. Adapt global parameters on the fly:

javascript
import ai from 'ai';

// gravity follows your cursor's half of the screen
const g = ai.mouse().active && ai.mouse().y < h / 2 ? 900 : 300;

Turn a static scene into a living one: gravity, spawn rates, spring constants, track shapes — anything can be driven by ai.mouse(), ai.sense(), or your own state.

API Reference

Types

javascript
// A steering force. Every behavior returns one; your brain returns one.
{ fx: number, fy: number }

// What a brain receives.
{
  mouse:   { x: number, y: number, active: boolean }, // live cursor
  bounds:  { x: 0, y: 0, w: number, h: number },      // canvas rect
  time:    number,                                    // the frame time you passed to sense
  w:       number,                                    // canvas width
  h:       number,                                    // canvas height
}

// An agent. Created by ai.agent; the fields are plain properties you can read/write.
{
  x: number, y: number, vx: number, vy: number,  // position & velocity (px, px/s)
  maxSpeed: number,      // default 100 — speed cap applied by ai.think and ai.limitSpeed
  maxForce: number,      // default 60  — steering-force cap
  brain?: (agent, sense) => Steer,  // optional; ai.think calls it each frame
  wanderAngle?: number,  // internal, auto-managed by ai.wander — don't set by hand
}

// Options for the flocking helpers. All optional.
{
  perception?: number,  // only affects separate() — neighbor scan radius
  sep?: number,         // separation weight, default 1.5
  ali?: number,         // alignment weight,  default 1
  coh?: number,         // cohesion weight,   default 1
  factor?: number,      // single weight used by separate/align/cohesion when called alone
  maxForce?: number,    // default 60
}

// An obstacle for ai.avoid.
{ x: number, y: number, r: number }

Create & simulate

FunctionDefaultsNotes
ai.agent({ x, y, vx, vy, maxSpeed, maxForce, brain })vx:0, vy:0, maxSpeed:100, maxForce:60Create an agent. Only x and y are required
ai.think(agent, sense, dt)Run brain (if set), clamp force to maxForce, integrate vx/vy into x/y, clamp speed to maxSpeed. Returns the agent. With no brain, returns the agent unchanged
ai.sense(agent, w, h, time = 0)time:0Build the { mouse, bounds, time, w, h } object for a brain
ai.mouse()Live cursor { x, y, active }. active is false until the pointer is over the canvas
ai.limitSpeed(agent, maxSpeed = 100)maxSpeed:100Clamp the agent's velocity magnitude. ai.think calls it automatically
ai.wrap(agent, w, h, margin = 0)margin:0Toroidal edge wrap — leaving one edge enters the opposite one
ai.keepInBounds(agent, w, h, margin = 40, maxForce = 60)margin:40, maxForce:60Returns a force steering back toward the center when near an edge. Call it from a brain or add its force manually

Steering behaviors

All return a Steer force; pass it to ai.think via a brain, or add several forces together.

FunctionDefaultsBehavior
ai.seek(agent, tx, ty, maxForce = 60)maxForce:60Steer toward a point at full maxForce
ai.flee(agent, tx, ty, maxForce = 60, radius = Infinity)maxForce:60, radius:InfinitySteer away. Returns { fx:0, fy:0 } once farther than radius
ai.arrive(agent, tx, ty, slowRadius = 80, maxForce = 60)slowRadius:80, maxForce:60Seek, but ease the force down linearly as the agent enters slowRadius; stops at the target
ai.pursue(agent, target, maxForce = 60)maxForce:60Chase a moving agent, aiming at where it will be (leads it by distance / maxSpeed seconds)
ai.evade(agent, target, maxForce = 60, radius = Infinity)maxForce:60, radius:InfinityFlee a moving agent's predicted position
ai.wander(agent, maxForce = 30, radius = 40, distance = 80, jitter = 0.4)maxForce:30, radius:40, distance:80, jitter:0.4Smooth random walk. Mutates agent.wanderAngle to keep the turn continuous. Needs a nonzero velocity to produce interesting motion

Flocking

FunctionDefaultsNotes
ai.neighbors(agents, agent, radius = 60)radius:60Agents within radius px (excludes self). O(n²) — fine to ~100 agents
ai.separate(agent, neighbors, opts)perception:60, factor:1.5, maxForce:60Push away from neighbors — avoid crowding
ai.align(agent, neighbors, opts)factor:1, maxForce:60Steer toward the neighbors' average velocity
ai.cohesion(agent, neighbors, opts)factor:1, maxForce:60Steer toward the neighbors' average position
ai.flock(agent, neighbors, opts)sep:1.5, ali:1, coh:1, maxForce:60All three rules summed, then clamped to maxForce. One call per frame per agent

Obstacles

FunctionDefaultsNotes
ai.avoid(agent, obstacles, opts)perception:60, lookAhead:60, maxForce:80Look lookAhead px along the current heading; steer around any { x, y, r } obstacle within r + perception. Returns { fx:0, fy:0 } when clear

Rules & performance

  • Import it. ai is a built-in module, not a global — add import ai from 'ai'; to every file (entry or module) that uses it. Forgetting the import throws "ai is not defined", exactly like any other undeclared name.
  • ai.mouse().active is false until the pointer is over the canvas, so agents can ignore a missing cursor.
  • dt matters. Always pass the frame dt to ai.think — never integrate with raw numbers.
  • Neighbor scans are O(n²). ai.neighbors is fine for ~100 agents; for larger flocks, partition your agents into grid buckets yourself (the loop guard caps per-frame work).
  • Agents live in state; Restart clears them. Wrap edges with ai.wrap or bound with ai.keepInBounds.

Importing ai

ai is a built-in module — import it in any file that uses it. All of these work:

javascript
import ai from 'ai';          // default import
import { ai } from 'ai';      // named import
import * as ai from 'ai';     // namespace import
import { seek, flock } from 'ai'; // import helpers directly

Every file that calls ai.* needs its own import — the entry file and each module file. ai is not a global: using it without an import throws "ai is not defined". See Injected Globals for the full reference.