A trapezoidal motion profile simulator for Cycling'74 Max (v8 JavaScript engine).
Simulates the acceleration → cruise → deceleration shape of a real stepper motor, producing smooth position and velocity curves you can use to drive lights, audio parameters, or any time-varying value in your patcher. Behavioral reference: STMicroelectronics L6470 stepper driver.
- Copy
motion-profile.jsandtrapezoid.jsinto your Max project folder (or the Max search path). - Create a
[v8 motion-profile.js]object in your patcher. - Connect the four outlets as described below.
Both
.jsfiles must be in the same folder —motion-profile.jsloadstrapezoid.jsautomatically.
The object has four outlets (left to right):
| # | Type | Contents |
|---|---|---|
| 0 | float | Position — current position during streaming, or total duration (ms) after calc |
| 1 | float | Velocity — current velocity (signed); 0 when idle |
| 2 | message | Status — phase, profile data, state messages, and errors |
| 3 | bang | Done — fires when motion completes (or stops) |
| Parameter | Unit |
|---|---|
position / target |
dimensionless — you decide what it means (steps, mm, degrees…) |
acc / dec |
position units per second² (e.g. 1000 = 1000 units/s²) |
maxSpeed |
position units per second |
| Duration output (outlet 0 / status messages) | milliseconds |
query time argument |
milliseconds |
interval argument |
milliseconds |
All motion math is internally in seconds; durations are converted to milliseconds on output so they integrate naturally with Max's timing ecosystem.
Calculates the motion profile and returns timing immediately. No streaming. Use this to pre-plan timing for other events.
calc <target> <acc> <dec> <maxSpeed>
| Argument | Description |
|---|---|
target |
Destination position (absolute) |
acc |
Acceleration (units/s²) |
dec |
Deceleration (units/s²) |
maxSpeed |
Maximum speed (units/s) |
Outputs:
- Outlet 0: total duration in ms
- Outlet 2:
profile_calc <totalDuration> <accelDuration> <cruiseDuration> <decelDuration> <peakSpeed> <distance>(all durations in ms) - Outlet 3: bang
The current position is advanced to target instantly. peakSpeed may be less than maxSpeed if the distance is too short to reach full speed (triangular profile).
Example:
calc 1000 500 300 800
→ outlet 0: 2083.33 (total duration in ms)
→ outlet 2: profile_calc 2083.33 1600.0 0.0 2666.67 800.0 1000.0
Starts a motion and streams position + velocity at a regular interval (default: every 10 ms).
move <target> <acc> <dec> <maxSpeed>
Same arguments as calc.
Outputs per tick:
- Outlet 0: current position
- Outlet 1: current velocity (positive = moving toward higher positions, negative = lower)
- Outlet 2:
phase accel/phase cruise/phase decel
On completion:
- Outlet 2:
done - Outlet 3: bang
Sending move while already moving (dynamic re-targeting):
The object re-plans intelligently without stopping:
- Same direction → seamless blend. The new profile starts from current position and velocity; no jerk or pause.
- Reverse direction → decelerates to a stop first (using the current decel rate), then starts the new move.
Outlet 2 announces: retarget <newTarget> blend or retarget <newTarget> reverse
Reads position and velocity from the last stored profile at an arbitrary time, without actually running the motion. Use this when you want to drive the motion from Max's transport or an external clock.
query <time>
| Argument | Description |
|---|---|
time |
Time offset from motion start, in milliseconds |
Outputs:
- Outlet 0: position at that time
- Outlet 1: velocity at that time
- Outlet 2:
phase accel/phase cruise/phase decel/phase done
Does not change currentPosition. Values are clamped: times below 0 return the start, times past the end return the target.
Requires a profile to be stored first (via calc or move). If none: error no_profile.
queryis used instead ofevalbecauseevalis reserved by the Max v8 engine.
stop
Decelerates to zero using the current deceleration rate. When fully stopped:
- Outlet 2:
stopped - Outlet 3: bang
currentPositionis updated to the actual stop point
No effect if idle.
hardStop
Freezes position instantly with no deceleration. Use for emergency situations.
- Outlet 2:
hardstop - Outlet 3: bang
setPos <position>
Sets currentPosition to the given value without moving. Only works when idle. Useful for calibration or repositioning the logical origin.
Error if called while running: error cannot_setpos_while_running
getPos
Outputs currentPosition on outlet 0.
getVel
Outputs currentVelocity on outlet 1.
getState
Outlet 2: state <position> <velocity> <isRunning> <phase>
phase is one of: idle accel cruise decel stopping
getProfile
Outlet 2: profile <startPos> <targetPos> <totalDuration> <accelDuration> <cruiseDuration> <decelDuration> <peakSpeed>
All durations in milliseconds. Error if no profile is stored: error no_profile
interval <ms>
Sets the timer interval for move streaming. Default: 10 ms. Minimum: 1 ms.
Takes effect on the next move command (not mid-motion).
reset
- Stops any running motion (hard stop)
- Sets position and velocity to 0
- Clears stored profile
- Resets interval to 10 ms
- Outlet 2:
reset
| Message | When |
|---|---|
profile_calc <totalMs> <accelMs> <cruiseMs> <decelMs> <peakSpeed> <distance> |
After calc |
phase <accel|cruise|decel> |
Each streaming tick |
done |
Motion completed normally |
retarget <target> <blend|reverse> |
New move during active motion |
stopped |
Controlled stop completed |
hardstop |
Hard stop executed |
state <pos> <vel> <running> <phase> |
Response to getState |
profile <startPos> <targetPos> <totalMs> <accelMs> <cruiseMs> <decelMs> <peakSpeed> |
Response to getProfile |
reset |
After reset |
error <code> |
Error (see below) |
| Code | Cause |
|---|---|
no_profile |
query or getProfile called with no stored profile |
cannot_setpos_while_running |
setPos called during active motion |
invalid_params |
Missing or non-numeric arguments |
acc_must_be_positive |
acc ≤ 0 |
dec_must_be_positive |
dec ≤ 0 |
maxspeed_must_be_positive |
maxSpeed ≤ 0 |
- Units are yours to define. Position values are dimensionless — they could be MIDI notes, millimeters, degrees, or DMX levels. Just be consistent with your acc/dec/maxSpeed scale.
- Asymmetric profiles are fine. Acceleration and deceleration can differ independently (e.g. fast ramp-up, slow ramp-down).
- Zero-distance move (
target== current position): completes immediately with adonebang and no streaming. - Pre-plan with
calc, then re-use withquery. Callcalconce to store the profile, then scrub through it withqueryfrom a[transport]or[counter]. - Dynamic re-targeting is intentional. Send
moveagain at any time — the engine re-plans smoothly. There is no command queue; just send a new destination whenever your logic changes.