Quadruped kinematics
The dependency-free pose solver behind the quadruped. Four legs, three footfall patterns, and bounded controls that keep every foot reachable.
view
variant
drive
Bring the pointer over it and it gets up; click and it sits down.
height50%
stride60%
lift50%
Scrub the cycle to inspect footfall timing. Coloured marks identify feet on the ground.
Theming
Set a role and the same CSS goes in your own app — every robot under it follows.
Install
bunx --bun shadcn@latest add https://robocn.dev/r/quadruped-kinematics.jsonNotes
- 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 unitsAPI
| Prop | Type | Default | Description |
|---|---|---|---|
| solveQuadruped | (options?: QuadrupedOptions) => QuadrupedPose | — | Solves 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 }
}