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:
| Step | What | ai helper |
|---|---|---|
| Perceive | Read the environment: cursor, canvas size, nearby agents | ai.mouse(), ai.sense(agent, w, h, time), ai.neighbors(agents, agent, radius) |
| Think | Decide a steering force { fx, fy } | your brain(agent, sense) function, or ai.seek, ai.flock, ... |
| Act | Apply the force, clamp speed, move | ai.think(agent, sense, dt) |
Demo: a particle that chases your cursor
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
aiacts 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.
| Behavior | What it does | Returns |
|---|---|---|
ai.seek(agent, tx, ty, maxForce) | Steer toward a point | force |
ai.flee(agent, tx, ty, maxForce, radius) | Steer away; no force beyond radius | force |
ai.arrive(agent, tx, ty, slowRadius, maxForce) | Seek, then ease in near the target | force |
ai.pursue(agent, target, maxForce) | Chase a moving target, leading it | force |
ai.evade(agent, target, maxForce, radius) | Flee a moving target's future position | force |
ai.wander(agent, maxForce, radius, distance, jitter) | Smooth, random wandering | force |
ai.separate / align / cohesion | The three flocking rules | force |
ai.flock(agent, neighbors, opts) | All three combined + clamped | force |
ai.avoid(agent, obstacles, opts) | Steer around { x, y, r } obstacles | force |
The math is readable on purpose — that's the lesson. seek is just normalize(target − position) × maxForce:
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:
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:
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
// 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
| Function | Defaults | Notes |
|---|---|---|
ai.agent({ x, y, vx, vy, maxSpeed, maxForce, brain }) | vx:0, vy:0, maxSpeed:100, maxForce:60 | Create 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:0 | Build 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:100 | Clamp the agent's velocity magnitude. ai.think calls it automatically |
ai.wrap(agent, w, h, margin = 0) | margin:0 | Toroidal edge wrap — leaving one edge enters the opposite one |
ai.keepInBounds(agent, w, h, margin = 40, maxForce = 60) | margin:40, maxForce:60 | Returns 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.
| Function | Defaults | Behavior |
|---|---|---|
ai.seek(agent, tx, ty, maxForce = 60) | maxForce:60 | Steer toward a point at full maxForce |
ai.flee(agent, tx, ty, maxForce = 60, radius = Infinity) | maxForce:60, radius:Infinity | Steer away. Returns { fx:0, fy:0 } once farther than radius |
ai.arrive(agent, tx, ty, slowRadius = 80, maxForce = 60) | slowRadius:80, maxForce:60 | Seek, but ease the force down linearly as the agent enters slowRadius; stops at the target |
ai.pursue(agent, target, maxForce = 60) | maxForce:60 | Chase 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:Infinity | Flee 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.4 | Smooth random walk. Mutates agent.wanderAngle to keep the turn continuous. Needs a nonzero velocity to produce interesting motion |
Flocking
| Function | Defaults | Notes |
|---|---|---|
ai.neighbors(agents, agent, radius = 60) | radius:60 | Agents within radius px (excludes self). O(n²) — fine to ~100 agents |
ai.separate(agent, neighbors, opts) | perception:60, factor:1.5, maxForce:60 | Push away from neighbors — avoid crowding |
ai.align(agent, neighbors, opts) | factor:1, maxForce:60 | Steer toward the neighbors' average velocity |
ai.cohesion(agent, neighbors, opts) | factor:1, maxForce:60 | Steer toward the neighbors' average position |
ai.flock(agent, neighbors, opts) | sep:1.5, ali:1, coh:1, maxForce:60 | All three rules summed, then clamped to maxForce. One call per frame per agent |
Obstacles
| Function | Defaults | Notes |
|---|---|---|
ai.avoid(agent, obstacles, opts) | perception:60, lookAhead:60, maxForce:80 | Look 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.
aiis a built-in module, not a global — addimport 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().activeisfalseuntil the pointer is over the canvas, so agents can ignore a missing cursor.dtmatters. Always pass the framedttoai.think— never integrate with raw numbers.- Neighbor scans are O(n²).
ai.neighborsis 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 withai.wrapor bound withai.keepInBounds.
Importing ai
ai is a built-in module — import it in any file that uses it. All of these work:
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 directlyEvery 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.