Browse documentation

Quadruped kinematics

The dependency-free pose solver behind the quadruped. Four legs, three footfall patterns, and bounded controls that keep every foot reachable.

QUAD / 11
variant
gait
phase
65%
height
50%
stride
60%
lift
50%

Scrub the cycle to inspect footfall timing. Coloured marks identify feet on the ground.

Install

pnpm dlx shadcn@latest add https://robocn.dev/r/quadruped-kinematics.json

Notes

  • The solver has no React or three.js dependency and does not mutate its inputs. Each side can be projected independently for an SVG or 3D renderer.
  • These are illustrative gait trajectories, not a stability or dynamics model. Walk staggers quarter-cycle swings; trot pairs opposite corners.

Usage

import { solveQuadruped } from "@/lib/robocn/quadruped"

const pose = solveQuadruped({ gait: "walk", phase: 0.4, height: 0.6 })
pose.legs // id, side, hip, knee, foot, contact
pose.height // hip height in world units

API

exports
PropTypeDefaultDescription
solveQuadruped(options?: QuadrupedOptions) => QuadrupedPoseSolves hip/knee/foot coordinates for front-left, front-right, rear-left, and rear-right, in that order.
QuadrupedOptions{ gait?, phase?, height?, stride?, lift? }Same gait and normalized stance controls as RobotQuadruped. Defaults to a neutral standing pose.
QuadrupedLeg{ id, side, hip: Vec2, knee: Vec2, foot: Vec2, contact }Coordinates in a sagittal plane: x points forward and y up. Upper links are 30 units, lower links 28. Contact means foot y is within 1e-7 of zero.

Source

src/lib/robocn/quadruped.ts
/** Planar two-link legs and illustrative footfall cycles for a quadruped. */
import { solveChain2, type Vec2 } from "@/lib/robocn/kinematics"

export type QuadrupedGait = "stand" | "walk" | "trot"
export type QuadrupedLegId = "front-left" | "front-right" | "rear-left" | "rear-right"

export interface QuadrupedOptions {
  gait?: QuadrupedGait
  /** Cycle fraction; wraps in either direction. */
  phase?: number
  /** Normalized body height, stride length, and foot lift, each clamped to 0–1. */
  height?: number
  stride?: number
  lift?: number
}

export interface QuadrupedLeg {
  id: QuadrupedLegId
  side: "left" | "right"
  hip: Vec2
  knee: Vec2
  foot: Vec2
  /** Whether the solved foot is on the ground plane (y = 0). */
  contact: boolean
}

export interface QuadrupedPose {
  gait: QuadrupedGait
  /** Hip height in world units, 36–54. */
  height: number
  legs: QuadrupedLeg[]
}

const unit = (value: number, fallback: number) => Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : fallback
const wrap = (value: number) => Number.isFinite(value) ? ((value % 1) + 1) % 1 : 0
const ids: QuadrupedLegId[] = ["front-left", "front-right", "rear-left", "rear-right"]

/**
 * Each leg solves in its own sagittal plane. x points toward the front, y up.
 * Links are 30 and 28 units; bounded controls keep every requested foot reachable.
 * Walk uses staggered quarter-cycle swings; trot moves diagonal pairs together.
 * This computes poses, not body balance, dynamics, or terrain contact forces.
 */
export function solveQuadruped({ gait = "stand", phase = 0, height = 0.5, stride = 0.6, lift = 0.5 }: QuadrupedOptions = {}): QuadrupedPose {
  const hipHeight = 36 + 18 * unit(height, 0.5)
  const reach = 12 * unit(stride, 0.6)
  const clearance = 14 * unit(lift, 0.5)
  const cycle = wrap(phase)
  const offsets = gait === "trot" ? [0, 0.5, 0.5, 0] : [0, 0.5, 0.75, 0.25]
  const duty = gait === "trot" ? 0.5 : 0.75

  const legs = ids.map((id, index): QuadrupedLeg => {
    const front = index < 2
    const hip = { x: front ? 28 : -28, y: hipHeight }
    const t = wrap(cycle + offsets[index])
    let x = 0
    let y = 0
    if (gait !== "stand") {
      if (t < duty) {
        x = reach * (1 - 2 * t / duty)
      } else {
        const swing = (t - duty) / (1 - duty)
        x = -reach * Math.cos(Math.PI * swing)
        y = clearance * Math.sin(Math.PI * swing)
      }
    }
    const [root, knee, foot] = solveChain2(hip, { x: hip.x + x, y }, [30, 28], { bend: front ? "down" : "up" })
    return { id, side: index % 2 === 0 ? "left" : "right", hip: root, knee, foot, contact: foot.y < 1e-7 }
  })
  return { gait, height: hipHeight, legs }
}