Skip to content

Motion engine ​

How walking, sitting down and standing up are computed. You do not need this to build an app, but it helps when you make assets, plug in another renderer, or fix odd-looking motion.

Flow ​

plan (extended every tick, deterministic)                              draw (every frame)
createBehaviorWorld ── frameAt(t) ─┬─ overrides   position·facing·pose  ─┐
  routines·commands·paths·booking  ├─ transitions sit/stand progress     ├─ behaviorActors ─ actorPartBoxes ─ composeRender ─ SVG
                                   └─ strides     distance walked       ─┘                   (part boxes)     (depth sorting)
  • Planning decides who goes where, when, and what they do there. It chains segments (idle, walk, wait, stand-up, sit-down, act, away). It depends only on time, so you can ask about any t.
  • Drawing computes that moment's part boxes in world space and depth-sorts them. Nothing is slid around in screen coordinates, so
    • walking people do not jitter (speed is not uneven from bridging between ticks), and
    • nobody appears to pass through furniture (each part is ordered front-to-back from its real position).

Walking ​

Paths are horizontal/vertical A* on a 0.25-tile grid (buildNavGrid, findPath). Walking people reserve nodes in time slots, so they detour or wait for each other.

Legs and arms swing with the distance walked. Feet do not slide even at higher playback speeds.

Value (GAIT)
cycle0.9 tilesDistance for one left step and one right step
swing0.06How far legs swing forward and back
arm0.05Arm swing (opposite to the legs)
bob0.02How much the body rises when the feet pass each other
ease0.15 tilesDistance over which the swing grows after starting and shrinks before stopping
  • Swinging parts are found by name: leg|shoe|shorts-l/r are legs, arm|forearm|hand-l/r are arms (gaitLimb).
  • Speed: WALK_SPEED is 1.6 tiles/s. Change it with createBehaviorWorld(…, { speed }) or useBehaviorPlayback(…, { walkSpeed }).
  • Facing changes in 90° steps, turning instantly at corners.

Sitting down and standing up ​

Sitting takes POSE_SECONDS (0.45 s), in two stages.

  ① Fold (0–45%)                               ② Move (45–100%)
  Fold the body into the seated shape          Move that shape, unchanged, onto the seat
  where they stand
  - First turn to face the seat                - The shape never changes, so if the seated
  - Height first (lift knees, raise hands),      pose clears the furniture and the path is
    reaching forward (legs, hands) later         clear, nothing passes through furniture
  - Thighs and shins come from the upper       - Onto a higher seat: match the height
    and lower halves of the legs; feet are       first, then move
    shaped from the shoes                      - Onto a lower seat: move, then lower
  • Standing up runs the same path in reverse: slide out of the seat, then unfold.
  • Lying down (beds) lays the body down at the height of the lying surface. Parts swing back in an arc (90°) around the feet, then that shape moves onto the bed unchanged. The standing place is searched beside the bed (at the hip), stepping outward 0.25 at a time, or at the foot if blocked. On a bed for two, each person climbs in from their own side.
  • Parts that exist only when standing (a bag in hand, a skirt hem) shrink away while folding; parts that exist only when seated (forearms reaching forward) grow from a point.
  • When entering from the front, the fold is pulled toward the seat so the folded toes do not go past the front edge of the standing place (so feet do not hit a coffee table in front of a sofa).
  • actorPartBoxes(object, asset, { transition: { from, progress } }) gives this shape. from is the pose before the transition (position, facing, pose).

Which side to enter from ​

For each seat, four standing places are considered (standUpCorners): front → left → right → back.

  1. It must be standable (canStand: no overlap with furniture or walls, and on floor tiles).
  2. Among those, pick the first where the whole sitting motion passes through no furniture (sitPathClear). It is measured with every person asset in the scene.
  3. If there is none, the first standable place.

That is why a chair pushed up to a desk is entered from the side (desk in front, chair back behind), and a sofa from the front. The editor's "stand up" picks the standing place by the same rule.

Known limitation: when no side is clear — a chair tight against table legs, or a wheelchair with wheels at the sides — feet briefly brush the legs or wheels as the person sits.

Collision check API ​

FunctionMeaning
transitionClear(seated, asset, from, obstacles)Whether one person sitting from from to seated stays clear of obstacles (boxes). The fold is measured in slices, the move over its whole swept area. Contact already present in the start or end pose is not counted
sitPathClear(seated, standing, sitters, obstacles)Measures with several person assets, using only nearby obstacles
seatObstacles(scene, registry, ignore?)Obstacle boxes in the scene (without floor tiles and rugs, and without the people in ignore)
flatOnFloor(asset)Whether the asset is a thin floor covering drawn before everything else, like floor tiles and rugs (not an obstacle)

Drawing ​

  • prepareRender draws static things once and measures front/back edges between parts. It sorts by screen x and only compares pairs whose x ranges overlap, so a 600-part scene takes about 1.7 ms.
  • composeRender slots only the moving people's parts in between those edges every frame and sorts topologically.
    • It re-measures only the static parts that overlap a person's silhouette, so 8 people take around 1 ms.
  • Front/back is decided between part boxes: two boxes that overlap on screen are separated along some axis, and that axis decides which is in front (isBehind). Floor tiles and coverings are drawn first, separately.
  • SceneLayer keeps React children in a fixed order and moves as few DOM nodes as possible to apply the draw order (longest increasing subsequence). In browsers with moveBefore, moved nodes keep their screen and typing CSS animations running instead of restarting.
  • The clock's timeAt(wall) returns the same value anywhere within one frame, so several sources drawn in one frame never fall out of step.

Numbers ​

Walking speed1.6 tiles/s (WALK_SPEED)
Sitting down / standing up0.45 s (POSE_SECONDS)
Pathfinding grid0.25 tiles (NAV_STEP), person footprint 0.5×0.5 (PERSON_SIZE)
Tick80 ms (TIMELINE_TICK_MS) — how often plans, activities and bubbles change
Sitting templatefootprint 0.5×0.75, back y 0.1, knees y 0.46, hips z 0.49 (SITTING)
Lying templatefootprint 0.5×1.4, head end y 0, back z 0 (LYING)
Building a worldstartup office (8 people, 20 spots) about 6 ms — including the entry-side collision checks

Released under the MIT License.