// Walking-in-place animation for the juno humanoid STEP assembly. // // The STEP geometry is baked in the athletic ready stance (see juno.py / // juno_parts/chain.py). This module recomputes full-chain FK each frame at // (athletic pose + gait deltas) and applies, per link subtree, the rigid // delta matrix T_target * inverse(T_athletic) in the fixed model frame // (mm, +X forward, +Y robot-left, +Z up, pelvis waist-yaw center at the // origin). // // Gait design: an in-place march. Each leg swings inside its own window of // the cycle with a raised-cosine envelope that starts and ends at zero, and // is EXACTLY at the athletic pose for the rest of the cycle — so the stance // foot stays planted on the ground with no IK. The pelvis root is // intentionally static for the same reason. Arms swing continuously, // antiphase to their own-side leg; the torso counter-rotates about the // waist with a hint of roll, and the neck compensates so the head keeps // facing forward. const X = [1, 0, 0]; const Y = [0, 1, 0]; const Z = [0, 0, 1]; const HIP_Y = 90; const SHOULDER_Y = 148; // Athletic ready stance angles (deg), mirrored from juno_parts/chain.py. const ATHLETIC = { HIP_PITCH: -16, KNEE: 32, ANKLE_PITCH: -16, HIP_ROLL_ABDUCT: 3, HIP_YAW: 0, WAIST_YAW: 0, SHOULDER_PITCH: -8, SHOULDER_ROLL_ABDUCT: 8, SHOULDER_YAW_INTERNAL: 8, ELBOW: -20, WRIST_ROLL: 0, WRIST_PITCH: -2, NECK_YAW: 0, NECK_PITCH: -2 }; // Gait amplitudes (deg) at parameter scale 1. Sign conventions follow the // chain ledger: pitch about +Y (negative = forward), roll about +X // (positive = toward robot-left), yaw about +Z (positive = turn left). const HIP_LIFT_DEG = 24; // swing-leg thigh raise (forward = negative pitch) const KNEE_FOLD_DEG = 44; // extra swing-leg knee flexion const TOE_POINT_DEG = 8; // slight toes-down of the airborne foot const ARM_SWING_DEG = 12; // shoulder pitch swing, antiphase to own-side leg const ELBOW_EXTRA_DEG = 10; // extra elbow flexion on the forward arm swing const WAIST_SWAY_DEG = 4; // torso counter-rotation about the waist yaw axis const TORSO_ROLL_DEG = 1.2; // weight-shift hint, roll about +X at the waist const NECK_COMPENSATION = 0.8; // head stabilization fraction against waist sway // Swing windows of the normalized cycle (left lifts first), with // double-support dwell between them. const SWING = { left: [0.05, 0.45], right: [0.55, 0.95] }; // Stride gait (treadmill-style walk in place): the stance foot slides // backward flat on the ground while the swing leg passes through the air, // both solved with planar two-link leg IK in each leg's (slightly rolled) // sagittal plane. The athletic stance is exactly the IK solution at stride // center, so the gait converges to plain standing as strideLength -> 0. const THIGH_LEN = 290; // hip-pitch to knee (mm) const SHIN_LEN = 290; // knee to ankle-pitch (mm) const STANCE_FRACTION = 0.58; // ground-contact share of each leg cycle const STRIDE_LIFT_MM = 60; // swing-foot apex clearance at legLift 1 const STRIDE_TOE_OFF_DEG = 14; // toes-down just after toe-off const STRIDE_HEEL_DEG = 10; // toes-up heel-first approach to contact const STRIDE_ARM_FACTOR = 1.5; // arms swing wider with long strides const STRIDE_SWAY_FACTOR = 1.25; const MAX_STRIDE_MM = 300; // keeps two-link IK inside reach at the extremes const DEFAULT_STRIDE_MM = 230; // lift/pitch accents are normalized to this const STRIDE_PHASE = { left: 0, right: 0.5 }; // Mid-swing cycle positions (offset + stance + half the swing window). const STRIDE_MID_SWING = { left: (STRIDE_PHASE.left + STANCE_FRACTION + ((1 - STANCE_FRACTION) / 2)) % 1, right: (STRIDE_PHASE.right + STANCE_FRACTION + ((1 - STANCE_FRACTION) / 2)) % 1 }; // Run gait: short ground contacts with a FLIGHT phase between them (both // feet airborne), a vertical body bounce absorbed by the stance-leg IK // (lowest at mid-stance, highest at mid-flight), toe-pivot heel-off into // push-off, high heel-recovery swing, forward torso lean, and pumping bent // arms. Foot contact lands slightly ahead of the hip and pushes off well // behind it (asymmetric split of the stride travel). const RUN_STANCE_FRACTION = 0.35; const RUN_PHASE = { left: 0, right: 0.5 }; const RUN_MID_STANCE = { left: RUN_STANCE_FRACTION / 2, right: (RUN_PHASE.right + (RUN_STANCE_FRACTION / 2)) % 1 }; const RUN_MID_SWING = { left: (RUN_STANCE_FRACTION + ((1 - RUN_STANCE_FRACTION) / 2)) % 1, right: (RUN_PHASE.right + RUN_STANCE_FRACTION + ((1 - RUN_STANCE_FRACTION) / 2)) % 1 }; const RUN_CONTACT_SHARE = 0.3; // stride share landing ahead of the hip const RUN_BOUNCE_MM = 22; // body bounce amplitude at legLift 1 const RUN_APEX_MM = 140; // swing-ankle apex over flat stance (heel tuck) const RUN_HEEL_OFF_DEG = 26; // toe-pivot heel rise into push-off const RUN_HEEL_OFF_START = 0.62; // stance progress where the heel starts rising const RUN_HEEL_IN_DEG = 8; // toes-up into the next contact const RUN_TOE_PIVOT_X_MM = 112; // sole front edge (foot bbox +x) — pivoting const RUN_ANKLE_HEIGHT_MM = 56; // there keeps the toe from digging in const RUN_LEAN_DEG = 7; // forward torso lean (waist pitch extra) const RUN_ARM_SWING_DEG = 26; // shoulder pump amplitude const RUN_ELBOW_BASE_DEG = -60; // extra constant elbow flexion (runner's arms) const RUN_ELBOW_PUMP_DEG = 15; // extra flexion on the forward pump const RUN_SWAY_DEG = 5; // waist yaw counter-rotation const RUN_ROLL_DEG = 1.6; // lean toward the stance side // Jump gait: countermovement crouch -> push -> ballistic flight with toe // point and leg tuck -> springy landing (underdamped body-height response // absorbed by the leg IK) -> recover. Arms swing back in the crouch, sweep // overhead for the flight ("hands in the air"), dip with the landing // spring, and settle back. Both legs work symmetrically; takeoff/landing // fall out of the IK reach clamp (targets go out of reach -> legs straight // -> feet leave/meet the ground smoothly). const JUMP_TIMING = { crouch: 0.18, push: 0.3, land: 0.62, absorb: 0.74 }; const JUMP_CROUCH_MM = 120; // countermovement depth at legLift 1 const JUMP_TAKEOFF_MM = 20; // body rise at full leg extension (reach limit) const JUMP_APEX_MM = 160; // ballistic apex above standing at legLift 1 const JUMP_TUCK_MM = 250; // mid-flight ankle tuck toward the body const JUMP_TOE_POINT_DEG = 25; // toes point down in the air const JUMP_LAND_DIP_MM = -80; // first landing compression const JUMP_SPRING_DECAY = 3.5; // landing spring: exp decay rate ... const JUMP_SPRING_CYCLES = 1.25; // ... and bounce count (ends at zero crossing) const JUMP_ARM_BACK_DEG = 30; // crouch arm backswing (shoulder pitch) const JUMP_ARM_UP_DEG = -155; // overhead shoulder pitch in flight const JUMP_ARM_V_DEG = 20; // shoulder roll abduction for an overhead V const JUMP_ARM_SPRING_DEG_PER_MM = 0.3; // landing arm dip coupled to body spring const JUMP_LEAN_DEG = 8; // crouch/landing forward lean const JUMP_HEAD_UP_DEG = -8; // look up slightly while airborne // Showpiece animations: karate kick, handstand, and the Elvis dance each // run as their own loop; every one starts and ends in the athletic stance. // Lateral pendulum for weight shifts: the hip-roll joint sits at z=-184 in // the pelvis frame; rolling there (with the ankle roll compensating) moves // the planted sole sideways by ~(soleZ - HIP_ROLL_Z) * sin(roll). const HIP_ROLL_Z_MM = -184; // Kick (segment-local): weight shifts over the left leg, the right leg // chambers and snaps a front kick at hip height with a guard up. const KICK_WEIGHT_SHIFT_MM = 42; const KICK_DIP_MM = 18; const KICK_CHAMBER_HIP_DEG = -70; const KICK_CHAMBER_KNEE_DEG = 115; const KICK_CHAMBER_ANKLE_DEG = 10; const KICK_EXTEND_HIP_DEG = -20; // added on top of the chamber const KICK_EXTEND_KNEE_DEG = -105; // snaps the shin out nearly straight const KICK_EXTEND_ANKLE_DEG = -25; // ball-of-foot strike (toes pulled back) const KICK_GUARD_SHOULDER_DEG = -45; const KICK_GUARD_ELBOW_DEG = -105; const KICK_GUARD_ROLL_OUT_DEG = 18; const KICK_HIP_TURN_DEG = -14; // hips rotate into the kick const KICK_LEAN_BACK_DEG = -7; // counterbalance during the snap // Handstand: keyframed root rotation about +Y with contact anchoring — // toe-pinned while folding, palm-pinned once the hands plant. const HS_PLANT_X_MM = 430; // hand plant line in front of the feet // Palm contact point in the hand frame: the hands plant palm-down with the // fingers pointing forward (total hand pitch -90 at the hold), so contact // is on the local -x palm face — the one extent that is symmetric between // the two hands (the posed finger curl differs in local z). const HS_PALM_LOCAL_MM = [-35, 0, -70]; const HS_TOE_LOCAL_MM = [112, 0, -26]; // sole FRONT EDGE (pivoting there keeps the whole sole above ground) const HS_WOBBLE_DEG = 1.6; // leg wobble while holding the stand // Elvis dance (4 beats): weight over the right leg, left leg kicked out // to the left planted on a pointed toe (closed-form lateral+sagittal leg // IK), rubber-leg shake, right arm pointing up to the right with a beat // pulse, hip swivel, head turned toward the pointing hand. const ELVIS_BEATS = 4; const ELVIS_WEIGHT_SHIFT_MM = -55; // pelvis shifts over the right leg const ELVIS_DIP_MM = 20; // settled crouch on the stance leg const ELVIS_BOUNCE_MM = 8; // beat bounce on top of the dip const ELVIS_TOE_X_MM = 80; // kicked-out toe plant, forward of the hips const ELVIS_TOE_Y_MM = 330; // ... and out to the robot-left const ELVIS_TOE_SHAKE_MM = 28; // rubber-leg lateral toe shake (2x beat) const ELVIS_FOOT_POINT_DEG = 55; // pointed toe of the kicked-out foot const ELVIS_FOOT_POINT_SHAKE_DEG = 7; const ELVIS_TOE_TIP_LOCAL = { x: 112, drop: 56 }; // toe tip rel ankle-pitch (fwd, down) const ELVIS_POINT_ROLL_DEG = -115; // right arm raised out-and-up to the right const ELVIS_POINT_PITCH_DEG = -25; const ELVIS_POINT_ELBOW_DEG = -12; // nearly straight: the finger points const ELVIS_POINT_PULSE_DEG = 8; // beat pulse of the pointing arm const ELVIS_OFF_ARM_PITCH_DEG = -30; // left arm low and bent across const ELVIS_OFF_ARM_ELBOW_DEG = -70; const ELVIS_TWIST_DEG = 10; // hip swivel via the waist const ELVIS_LEAN_RIGHT_DEG = -3; // lean into the point const ELVIS_HEAD_TURN_DEG = -20; // look toward the pointing hand const ELVIS_CHIN_UP_DEG = -5; const SIDES = ["left", "right"]; function sideSign(side) { return side === "left" ? 1 : -1; } // Kinematic chain in root-to-leaf order, mirrored from juno_parts/chain.py. // origin is the joint center in the parent link frame (mm); each child link // frame sits at its joint center. function buildJoints() { const joints = [ { name: "waist_yaw", parent: "pelvis", child: "torso", origin: [0, 0, 0], axis: Z }, { name: "neck_yaw", parent: "torso", child: "neck_collar", origin: [0, 0, 324], axis: Z }, { name: "neck_pitch", parent: "neck_collar", child: "head", origin: [0, 0, 46], axis: Y } ]; for (const side of SIDES) { const s = sideSign(side); joints.push( { name: `hip_yaw_${side}`, parent: "pelvis", child: `hip_bracket_${side}`, origin: [0, s * HIP_Y, -120], axis: Z }, { name: `hip_roll_${side}`, parent: `hip_bracket_${side}`, child: `hip_carrier_${side}`, origin: [0, 0, -64], axis: X }, { name: `hip_pitch_${side}`, parent: `hip_carrier_${side}`, child: `thigh_${side}`, origin: [0, 0, -78], axis: Y }, { name: `knee_${side}`, parent: `thigh_${side}`, child: `shin_${side}`, origin: [0, 0, -290], axis: Y }, { name: `ankle_pitch_${side}`, parent: `shin_${side}`, child: `ankle_link_${side}`, origin: [0, 0, -290], axis: Y }, { name: `ankle_roll_${side}`, parent: `ankle_link_${side}`, child: `foot_${side}`, origin: [0, 0, -30], axis: X } ); } for (const side of SIDES) { const s = sideSign(side); joints.push( { name: `shoulder_pitch_${side}`, parent: "torso", child: `shoulder_pod_${side}`, origin: [0, s * SHOULDER_Y, 290], axis: Y }, { name: `shoulder_roll_${side}`, parent: `shoulder_pod_${side}`, child: `yaw_housing_${side}`, origin: [0, s * 34, -72], axis: X }, { name: `shoulder_yaw_${side}`, parent: `yaw_housing_${side}`, child: `bicep_${side}`, origin: [0, 0, -24], axis: Z }, { name: `elbow_${side}`, parent: `bicep_${side}`, child: `forearm_${side}`, origin: [0, 0, -156], axis: Y }, { name: `wrist_roll_${side}`, parent: `forearm_${side}`, child: `wrist_carrier_${side}`, origin: [0, 0, -150], axis: Z }, { name: `wrist_pitch_${side}`, parent: `wrist_carrier_${side}`, child: `hand_${side}`, origin: [0, 0, -28], axis: Y } ); } return joints; } const JOINTS = buildJoints(); // Optional artistic extras applied on top of joint FK, keyed by joint name: // { rollDeg } adds a small rotation about +X at the same joint center. function athleticAnglesDeg() { const angles = { waist_yaw: ATHLETIC.WAIST_YAW, neck_yaw: ATHLETIC.NECK_YAW, neck_pitch: ATHLETIC.NECK_PITCH }; for (const side of SIDES) { const s = sideSign(side); angles[`hip_yaw_${side}`] = ATHLETIC.HIP_YAW; angles[`hip_roll_${side}`] = s * ATHLETIC.HIP_ROLL_ABDUCT; angles[`hip_pitch_${side}`] = ATHLETIC.HIP_PITCH; angles[`knee_${side}`] = ATHLETIC.KNEE; angles[`ankle_pitch_${side}`] = ATHLETIC.ANKLE_PITCH; angles[`ankle_roll_${side}`] = -s * ATHLETIC.HIP_ROLL_ABDUCT; angles[`shoulder_pitch_${side}`] = ATHLETIC.SHOULDER_PITCH; angles[`shoulder_roll_${side}`] = s * ATHLETIC.SHOULDER_ROLL_ABDUCT; angles[`shoulder_yaw_${side}`] = -s * ATHLETIC.SHOULDER_YAW_INTERNAL; angles[`elbow_${side}`] = ATHLETIC.ELBOW; angles[`wrist_roll_${side}`] = ATHLETIC.WRIST_ROLL; angles[`wrist_pitch_${side}`] = ATHLETIC.WRIST_PITCH; } return angles; } const ATHLETIC_ANGLES = athleticAnglesDeg(); function finite(value, fallback = 0) { const numericValue = Number(value); return Number.isFinite(numericValue) ? numericValue : fallback; } function clamp(value, min, max) { return Math.min(Math.max(finite(value, min), min), max); } const IDENTITY3 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; function rotAxisDeg(axis, deg) { const rad = (deg * Math.PI) / 180; const c = Math.cos(rad); const s = Math.sin(rad); if (axis === X) { return [[1, 0, 0], [0, c, -s], [0, s, c]]; } if (axis === Y) { return [[c, 0, s], [0, 1, 0], [-s, 0, c]]; } return [[c, -s, 0], [s, c, 0], [0, 0, 1]]; } function matMul3(a, b) { const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; for (let i = 0; i < 3; i += 1) { for (let j = 0; j < 3; j += 1) { out[i][j] = (a[i][0] * b[0][j]) + (a[i][1] * b[1][j]) + (a[i][2] * b[2][j]); } } return out; } function matVec3(a, v) { return [ (a[0][0] * v[0]) + (a[0][1] * v[1]) + (a[0][2] * v[2]), (a[1][0] * v[0]) + (a[1][1] * v[1]) + (a[1][2] * v[2]), (a[2][0] * v[0]) + (a[2][1] * v[1]) + (a[2][2] * v[2]) ]; } function matTranspose3(a) { return [ [a[0][0], a[1][0], a[2][0]], [a[0][1], a[1][1], a[2][1]], [a[0][2], a[1][2], a[2][2]] ]; } // FK over the whole chain. extras[jointName] = { pitchDeg, rollDeg } folds // small artistic +Y/+X rotations in at that joint center (torso lean and // weight shift); rootOffset translates the pelvis (body bounce in the air // gaits — the stance-leg IK absorbs it on the ground side); rootPitchDeg // rotates the whole body about +Y at the pelvis (handstand inversion — // callers solve the matching translation so contacts stay put). function fkFrames(anglesDeg, extras = {}, rootOffset = [0, 0, 0], rootPitchDeg = 0) { const rootR = rootPitchDeg ? rotAxisDeg(Y, rootPitchDeg) : IDENTITY3; const frames = { pelvis: { R: rootR, p: [...rootOffset] } }; for (const joint of JOINTS) { const parent = frames[joint.parent]; const offset = matVec3(parent.R, joint.origin); const p = [parent.p[0] + offset[0], parent.p[1] + offset[1], parent.p[2] + offset[2]]; let R = matMul3(parent.R, rotAxisDeg(joint.axis, finite(anglesDeg[joint.name], 0))); const extra = extras[joint.name]; if (extra && finite(extra.pitchDeg, 0) !== 0) { R = matMul3(R, rotAxisDeg(Y, extra.pitchDeg)); } if (extra && finite(extra.rollDeg, 0) !== 0) { R = matMul3(R, rotAxisDeg(X, extra.rollDeg)); } frames[joint.child] = { R, p }; } return frames; } const ATHLETIC_FRAMES = fkFrames(ATHLETIC_ANGLES); // Per-side stride-IK constants derived from the athletic FK: the hip-pitch // joint center stays fixed in the world (static pelvis, athletic hip // yaw/roll), and the compensated ankle roll keeps the foot flat, so the // ankle-pitch target maps exactly onto the rolled sagittal plane. const STRIDE_RIG = (() => { const rig = {}; for (const side of SIDES) { const hip = ATHLETIC_FRAMES[`thigh_${side}`].p; // hip-pitch joint center const foot = ATHLETIC_FRAMES[`foot_${side}`]; const rollRad = (sideSign(side) * ATHLETIC.HIP_ROLL_ABDUCT * Math.PI) / 180; // Sole plane and the flat-foot vertical offset from the ankle-pitch // joint: the 30 mm ankle-roll drop is still rolled by the hip roll, the // 26 mm sole sits below the roll-compensated (world-aligned) foot frame. const soleZ = foot.p[2] + matVec3(foot.R, [0, 0, -26])[2]; const ankleToSole = (30 * Math.cos(rollRad)) + 26; rig[side] = { hipZ: hip[2], cosRoll: Math.cos(rollRad), soleZ, ankleToSole }; } return rig; })(); // Athletic left toe-tip world position (x, y): the Elvis kick-out leg IK // starts and ends its toe-target path here so the dance loop rests // exactly on the athletic stance. const ELVIS_ATHLETIC_TOE = (() => { const foot = ATHLETIC_FRAMES.foot_left; const toe = matVec3(foot.R, [112, 0, -26]).map((v, k) => v + foot.p[k]); return [toe[0], toe[1]]; })(); // Row-major 4x4 mapping the baked athletic placement of a link onto its // target placement: M = [R_t R_o^T | p_t - R_t R_o^T p_o]. function deltaMatrixRowMajor(original, target) { const Rd = matMul3(target.R, matTranspose3(original.R)); const moved = matVec3(Rd, original.p); const t = [target.p[0] - moved[0], target.p[1] - moved[1], target.p[2] - moved[2]]; return [ Rd[0][0], Rd[0][1], Rd[0][2], t[0], Rd[1][0], Rd[1][1], Rd[1][2], t[1], Rd[2][0], Rd[2][1], Rd[2][2], t[2], 0, 0, 0, 1 ]; } // Raised-cosine swing envelope: 0 at lift-off and touch-down, 1 mid-swing, // identically 0 outside the window so the stance foot never moves. function swingEnvelope(phase, [start, end]) { const p = ((finite(phase, 0) % 1) + 1) % 1; if (p <= start || p >= end) { return 0; } const u = (p - start) / (end - start); const lobe = Math.sin(Math.PI * u); return lobe * lobe; } // Cosine forwardness peaking at the given cycle position. function forwardness(phase, peakAt) { return Math.cos(2 * Math.PI * (finite(phase, 0) - peakAt)); } function gaitAnglesDeg(phase, { legLift = 1, armSwing = 1, torsoSway = 1 } = {}) { const angles = { ...ATHLETIC_ANGLES }; for (const side of SIDES) { const env = swingEnvelope(phase, SWING[side]) * legLift; const hipDelta = -HIP_LIFT_DEG * env; const kneeDelta = KNEE_FOLD_DEG * env; // Cancel hip+knee so the airborne foot stays near level, then point // the toes down a touch mid-swing. const ankleDelta = -(hipDelta + kneeDelta) + (TOE_POINT_DEG * env); angles[`hip_pitch_${side}`] += hipDelta; angles[`knee_${side}`] += kneeDelta; angles[`ankle_pitch_${side}`] += ankleDelta; // Arms swing antiphase to the own-side leg: the left arm leads when the // right leg lifts (left swing centered at 0.25, right at 0.75). const ownLegPeak = side === "left" ? 0.25 : 0.75; const armForward = forwardness(phase, ownLegPeak + 0.5) * armSwing; angles[`shoulder_pitch_${side}`] += -ARM_SWING_DEG * armForward; angles[`elbow_${side}`] += -ELBOW_EXTRA_DEG * Math.max(0, armForward); } // Torso counter-rotation: turning left brings the right shoulder forward // exactly while the left leg swings; the neck compensates most of it so // the head keeps facing forward. const sway = WAIST_SWAY_DEG * torsoSway * Math.sin(2 * Math.PI * finite(phase, 0)); angles.waist_yaw += sway; angles.neck_yaw += -sway * NECK_COMPENSATION; return angles; } function torsoRollExtras(phase, torsoSway) { // Lean toward the stance side: negative roll (lean robot-right) while the // left leg is lifted at phase 0.25. const rollDeg = -TORSO_ROLL_DEG * torsoSway * Math.sin(2 * Math.PI * finite(phase, 0)); return { waist_yaw: { rollDeg } }; } function smooth01(value) { const t = clamp(value, 0, 1); return t * t * (3 - (2 * t)); } // Two-link planar leg IK in the rolled sagittal plane. Targets the // ankle-pitch joint at forward offset dx (mm, from the hip-pitch joint) and // plane depth d (mm); footPitchDeg is the desired world pitch of the foot. // Returns hip-pitch / knee / ankle-pitch angles in the chain's conventions // (hip negative = forward, knee positive = flexion). function legPitchIk(dx, d, footPitchDeg) { const dist = clamp(Math.hypot(dx, d), Math.abs(THIGH_LEN - SHIN_LEN) + 1, THIGH_LEN + SHIN_LEN - 1); const phi = Math.atan2(dx, d); // forward angle of the hip->ankle line from straight down const cosInterior = clamp( ((THIGH_LEN * THIGH_LEN) + (SHIN_LEN * SHIN_LEN) - (dist * dist)) / (2 * THIGH_LEN * SHIN_LEN), -1, 1 ); const kneeRad = Math.PI - Math.acos(cosInterior); // knee flexion, >= 0 (knee bends forward) const cosBeta = clamp( ((THIGH_LEN * THIGH_LEN) + (dist * dist) - (SHIN_LEN * SHIN_LEN)) / (2 * THIGH_LEN * dist), -1, 1 ); const thighRad = phi + Math.acos(cosBeta); // thigh angle from straight down, + forward const hipDeg = (-thighRad * 180) / Math.PI; const kneeDeg = (kneeRad * 180) / Math.PI; const ankleDeg = footPitchDeg - (hipDeg + kneeDeg); return { hipDeg, kneeDeg, ankleDeg }; } // Treadmill stride: stance foot slides backward flat on the ground from // +stride/2 to -stride/2; the swing leg returns through the air with toe-off // and heel-strike accents. Both halves meet at zero velocity mismatch in // position, so the loop is seamless. function strideAnglesDeg(phase, { strideLength = 230, legLift = 1, armSwing = 1, torsoSway = 1 } = {}) { const angles = { ...ATHLETIC_ANGLES }; const stride = clamp(strideLength, 0, MAX_STRIDE_MM); for (const side of SIDES) { const rig = STRIDE_RIG[side]; const q = ((finite(phase, 0) + STRIDE_PHASE[side]) % 1 + 1) % 1; let ankleX; let soleTargetZ; let footPitchDeg; if (q < STANCE_FRACTION) { const s = q / STANCE_FRACTION; ankleX = (stride / 2) - (stride * s); // front contact -> push-off behind soleTargetZ = rig.soleZ; footPitchDeg = 0; } else { const u = (q - STANCE_FRACTION) / (1 - STANCE_FRACTION); ankleX = (-stride / 2) + (stride * smooth01(u)); const lobe = Math.sin(Math.PI * u); // Clearance and toe-off/heel accents scale with the stride so a zero // stride degenerates to plain athletic standing. const reach = stride / DEFAULT_STRIDE_MM; soleTargetZ = rig.soleZ + (STRIDE_LIFT_MM * clamp(legLift, 0, 1.4) * lobe * reach); // Toes down right after toe-off, toes up (heel first) into contact. footPitchDeg = ((STRIDE_TOE_OFF_DEG * lobe * (1 - u)) - (STRIDE_HEEL_DEG * lobe * u)) * reach; } const ankleTargetZ = soleTargetZ + rig.ankleToSole; const depth = (rig.hipZ - ankleTargetZ) / rig.cosRoll; // ankleX is authored relative to the hip-pitch joint; the athletic ankle // sits exactly under the hip, so stride 0 reproduces the athletic legs. const ik = legPitchIk(ankleX, depth, footPitchDeg); angles[`hip_pitch_${side}`] = ik.hipDeg; angles[`knee_${side}`] = ik.kneeDeg; angles[`ankle_pitch_${side}`] = ik.ankleDeg; // Arms counter the opposite leg: forward peak at the other side's // mid-swing, wider than the march to match the longer stride. const otherSide = side === "left" ? "right" : "left"; const armForward = forwardness(phase, STRIDE_MID_SWING[otherSide]) * armSwing * STRIDE_ARM_FACTOR; angles[`shoulder_pitch_${side}`] += -ARM_SWING_DEG * armForward; angles[`elbow_${side}`] += -ELBOW_EXTRA_DEG * Math.max(0, armForward); } // Shoulder girdle counters the hips: the right shoulder leads while the // right arm swings forward (right-arm peak == left leg mid-swing). const sway = WAIST_SWAY_DEG * STRIDE_SWAY_FACTOR * torsoSway * forwardness(phase, STRIDE_MID_SWING.left); angles.waist_yaw += sway; angles.neck_yaw += -sway * NECK_COMPENSATION; return angles; } function strideTorsoRollExtras(phase, torsoSway) { // Lean toward the stance side: robot-right lean while the left leg swings. const rollDeg = -TORSO_ROLL_DEG * STRIDE_SWAY_FACTOR * torsoSway * forwardness(phase, STRIDE_MID_SWING.left); return { waist_yaw: { rollDeg } }; } // ------------------------------------------------------------------- run function runBounceMm(phase, legLift) { // Twice per cycle: lowest at each mid-stance, highest at mid-flight. return -RUN_BOUNCE_MM * clamp(legLift, 0, 1.4) * Math.cos(4 * Math.PI * (finite(phase, 0) - RUN_MID_STANCE.left)); } // Stance-foot targets with a toe-pivot heel-off: the foot pivots about the // sole's front edge so the heel rises into push-off without the toe digging // below the ground. function runStanceTarget(s, stride, reach, rig) { const flatAnkleX = (stride * RUN_CONTACT_SHARE) - (stride * s); let pitchDeg = 0; if (s > RUN_HEEL_OFF_START) { pitchDeg = RUN_HEEL_OFF_DEG * reach * smooth01((s - RUN_HEEL_OFF_START) / (1 - RUN_HEEL_OFF_START)); } const pitchRad = (pitchDeg * Math.PI) / 180; const c = Math.cos(pitchRad); const sn = Math.sin(pitchRad); // Ankle relative to the toe pivot when flat: (-RUN_TOE_PIVOT_X_MM, +ankle height). const ankleX = flatAnkleX + (RUN_TOE_PIVOT_X_MM * (1 - c)) + (RUN_ANKLE_HEIGHT_MM * sn); const ankleZ = rig.soleZ + (RUN_TOE_PIVOT_X_MM * sn) + (rig.ankleToSole * c); return { ankleX, ankleZ, pitchDeg }; } function runAnglesDeg(phase, { strideLength = 230, legLift = 1, armSwing = 1, torsoSway = 1 } = {}) { const angles = { ...ATHLETIC_ANGLES }; const stride = clamp(strideLength, 0, MAX_STRIDE_MM); const reach = stride / DEFAULT_STRIDE_MM; const lift = clamp(legLift, 0, 1.4); const bounce = runBounceMm(phase, legLift); for (const side of SIDES) { const rig = STRIDE_RIG[side]; const q = ((finite(phase, 0) + RUN_PHASE[side]) % 1 + 1) % 1; let target; if (q < RUN_STANCE_FRACTION) { target = runStanceTarget(q / RUN_STANCE_FRACTION, stride, reach, rig); } else { const u = (q - RUN_STANCE_FRACTION) / (1 - RUN_STANCE_FRACTION); const t = smooth01(u); const end = runStanceTarget(1, stride, reach, rig); const contactX = stride * RUN_CONTACT_SHARE; const contactZ = rig.soleZ + rig.ankleToSole; target = { ankleX: end.ankleX + ((contactX - end.ankleX) * t), ankleZ: end.ankleZ + ((contactZ - end.ankleZ) * t) + (RUN_APEX_MM * lift * reach * Math.sin(Math.PI * u)), pitchDeg: (end.pitchDeg * (1 - t) * (1 - t)) - (RUN_HEEL_IN_DEG * reach * Math.sin(Math.PI * u) * u) }; } const depth = ((rig.hipZ + bounce) - target.ankleZ) / rig.cosRoll; const ik = legPitchIk(target.ankleX, depth, target.pitchDeg); angles[`hip_pitch_${side}`] = ik.hipDeg; angles[`knee_${side}`] = ik.kneeDeg; angles[`ankle_pitch_${side}`] = ik.ankleDeg; // Runner's arm pump: bent elbows, antiphase to the own-side leg. const otherSide = side === "left" ? "right" : "left"; const armForward = forwardness(phase, RUN_MID_SWING[otherSide]) * armSwing; angles[`shoulder_pitch_${side}`] += -RUN_ARM_SWING_DEG * armForward; angles[`elbow_${side}`] += (RUN_ELBOW_BASE_DEG * clamp(armSwing, 0, 1.5)) - (RUN_ELBOW_PUMP_DEG * Math.max(0, armForward)); } // Shoulder girdle counters the hips; the head keeps level against both // the sway and the forward lean. const sway = RUN_SWAY_DEG * torsoSway * forwardness(phase, RUN_MID_SWING.left); angles.waist_yaw += sway; angles.neck_yaw += -sway * NECK_COMPENSATION; angles.neck_pitch += -RUN_LEAN_DEG * NECK_COMPENSATION; return angles; } function runTorsoExtras(phase, torsoSway) { // Constant forward lean plus a lean toward the stance side. const rollDeg = RUN_ROLL_DEG * torsoSway * forwardness(phase, RUN_MID_STANCE.left); return { waist_yaw: { pitchDeg: RUN_LEAN_DEG, rollDeg } }; } // ------------------------------------------------------------------ jump function jumpBodyZMm(phase, lift) { const p = ((finite(phase, 0) % 1) + 1) % 1; const { crouch, push, land, absorb } = JUMP_TIMING; if (p < crouch) { return -JUMP_CROUCH_MM * lift * smooth01(p / crouch); } if (p < push) { const t = smooth01((p - crouch) / (push - crouch)); return (-JUMP_CROUCH_MM * lift) + (((JUMP_TAKEOFF_MM) + (JUMP_CROUCH_MM * lift)) * t); } if (p < land) { // Ballistic parabola between takeoff and landing at the same height. const t = (p - push) / (land - push); const apex = JUMP_APEX_MM * lift; return JUMP_TAKEOFF_MM + ((apex - JUMP_TAKEOFF_MM) * (1 - ((2 * t) - 1) * ((2 * t) - 1))); } if (p < absorb) { // Impact: ride down into the first compression. const t = smooth01((p - land) / (absorb - land)); return JUMP_TAKEOFF_MM + (((JUMP_LAND_DIP_MM * lift) - JUMP_TAKEOFF_MM) * t); } // Springy recovery: underdamped wobble that ends exactly at zero. const t = (p - absorb) / (1 - absorb); return JUMP_LAND_DIP_MM * lift * Math.exp(-JUMP_SPRING_DECAY * t) * Math.cos(2 * Math.PI * JUMP_SPRING_CYCLES * t); } function jumpFlightProgress(phase) { const p = ((finite(phase, 0) % 1) + 1) % 1; const { push, land } = JUMP_TIMING; if (p <= push || p >= land) { return 0; } return (p - push) / (land - push); } // Arms: backswing in the crouch, sweep overhead for the flight, dip with // the landing spring, settle back to athletic by the end of the cycle. function jumpShoulderPitchDeg(phase, bodyZ) { const p = ((finite(phase, 0) % 1) + 1) % 1; const { crouch, push, land, absorb } = JUMP_TIMING; const athletic = ATHLETIC.SHOULDER_PITCH; if (p < crouch) { return athletic + ((JUMP_ARM_BACK_DEG - athletic) * smooth01(p / crouch)); } if (p < push) { return JUMP_ARM_BACK_DEG + ((JUMP_ARM_UP_DEG - JUMP_ARM_BACK_DEG) * smooth01((p - crouch) / (push - crouch))); } if (p < land) { return JUMP_ARM_UP_DEG; } if (p < absorb) { // Dip with the impact: arms lower in proportion to the body drop. return JUMP_ARM_UP_DEG + (Math.abs(Math.min(0, bodyZ)) * JUMP_ARM_SPRING_DEG_PER_MM); } const t = smooth01((p - absorb) / (1 - absorb)); const dipped = JUMP_ARM_UP_DEG + (Math.abs(Math.min(0, bodyZ)) * JUMP_ARM_SPRING_DEG_PER_MM); return dipped + ((athletic - dipped) * t); } function jumpAnglesDeg(phase, { legLift = 1, armSwing = 1 } = {}) { const angles = { ...ATHLETIC_ANGLES }; const lift = clamp(legLift, 0, 1.4); const bodyZ = jumpBodyZMm(phase, lift); const flight = jumpFlightProgress(phase); const tuckLobe = Math.sin(Math.PI * flight); for (const side of SIDES) { const rig = STRIDE_RIG[side]; // Feet stay planted under the hips; in flight the ankle target rises // (tuck) and the IK reach clamp straightens the legs at takeoff/landing. const ankleZ = rig.soleZ + rig.ankleToSole + (flight > 0 ? JUMP_TUCK_MM * lift * tuckLobe : 0); const pitchDeg = flight > 0 ? JUMP_TOE_POINT_DEG * tuckLobe : 0; const depth = ((rig.hipZ + bodyZ) - ankleZ) / rig.cosRoll; const ik = legPitchIk(0, depth, pitchDeg); angles[`hip_pitch_${side}`] = ik.hipDeg; angles[`knee_${side}`] = ik.kneeDeg; angles[`ankle_pitch_${side}`] = ik.ankleDeg; // Overhead V: pitch sweeps the arms up; roll spreads them outward in // proportion to how raised they are. const s = sideSign(side); const shoulderPitch = jumpShoulderPitchDeg(phase, bodyZ); const raised = clamp( (shoulderPitch - ATHLETIC.SHOULDER_PITCH) / (JUMP_ARM_UP_DEG - ATHLETIC.SHOULDER_PITCH), 0, 1 ); const armScale = clamp(armSwing, 0, 1.5); angles[`shoulder_pitch_${side}`] = ATHLETIC.SHOULDER_PITCH + ((shoulderPitch - ATHLETIC.SHOULDER_PITCH) * armScale); angles[`shoulder_roll_${side}`] += s * JUMP_ARM_V_DEG * raised * armScale; angles[`elbow_${side}`] = ATHLETIC.ELBOW + (5 * raised * armScale); } // Look up a touch while airborne. angles.neck_pitch += JUMP_HEAD_UP_DEG * tuckLobe; return { angles, bodyZ, flight }; } function jumpTorsoExtras(phase, bodyZ, flight, torsoSway) { // Lean into the crouch and the landing compression, upright in the air. const compression = clamp(-Math.min(0, bodyZ) / JUMP_CROUCH_MM, 0, 1.2); const pitchDeg = JUMP_LEAN_DEG * compression * (1 - flight) * clamp(torsoSway, 0, 1.5); return { waist_yaw: { pitchDeg } }; } // --------------------------------------------------------------- routine function wrap01(value) { return ((finite(value, 0) % 1) + 1) % 1; } // Smooth on/off window: ramps up over [inStart, inEnd], back down over // [outStart, outEnd], zero outside. function pulse(t, inStart, inEnd, outStart, outEnd) { return smooth01((t - inStart) / Math.max(1e-9, inEnd - inStart)) * (1 - smooth01((t - outStart) / Math.max(1e-9, outEnd - outStart))); } // Planted-leg solve shared by the kick support leg and the dance: the // pitch IK keeps the sole at ground height under a bobbing pelvis, and a // hip/ankle roll pair absorbs a lateral pelvis shift dy (the second-order // foot lift from the roll arc is under 2 mm at these amplitudes). function plantedLegAngles(side, bodyZMm, pelvisDyMm) { const rig = STRIDE_RIG[side]; const s = sideSign(side); const depth = ((rig.hipZ + bodyZMm) - (rig.soleZ + rig.ankleToSole)) / rig.cosRoll; const ik = legPitchIk(0, depth, 0); const lateralDrop = HIP_ROLL_Z_MM - rig.soleZ; const rollDeltaDeg = (Math.asin(clamp(pelvisDyMm / lateralDrop, -0.5, 0.5)) * 180) / Math.PI; return { [`hip_pitch_${side}`]: ik.hipDeg, [`knee_${side}`]: ik.kneeDeg, [`ankle_pitch_${side}`]: ik.ankleDeg, [`hip_roll_${side}`]: (s * ATHLETIC.HIP_ROLL_ABDUCT) - rollDeltaDeg, [`ankle_roll_${side}`]: -((s * ATHLETIC.HIP_ROLL_ABDUCT) - rollDeltaDeg) }; } // ---- karate front kick (segment-local t): weight onto the left leg, // chamber, snap, re-chamber, plant. function kickPose(t) { const angles = { ...ATHLETIC_ANGLES }; const weight = pulse(t, 0.0, 0.12, 0.86, 0.98); const chamber = pulse(t, 0.14, 0.3, 0.72, 0.88); const extend = pulse(t, 0.36, 0.48, 0.58, 0.7); const guard = pulse(t, 0.04, 0.16, 0.8, 0.94); const dy = KICK_WEIGHT_SHIFT_MM * weight; const bodyZ = -KICK_DIP_MM * chamber; Object.assign(angles, plantedLegAngles("left", bodyZ, dy)); // Kicking leg, authored in joint space (airborne). angles.hip_pitch_right = ATHLETIC.HIP_PITCH + ((KICK_CHAMBER_HIP_DEG - ATHLETIC.HIP_PITCH) * chamber) + (KICK_EXTEND_HIP_DEG * extend); angles.knee_right = ATHLETIC.KNEE + ((KICK_CHAMBER_KNEE_DEG - ATHLETIC.KNEE) * chamber) + (KICK_EXTEND_KNEE_DEG * extend); angles.ankle_pitch_right = ATHLETIC.ANKLE_PITCH + ((KICK_CHAMBER_ANKLE_DEG - ATHLETIC.ANKLE_PITCH) * chamber) + (KICK_EXTEND_ANKLE_DEG * extend); angles.ankle_roll_right = -sideSign("right") * ATHLETIC.HIP_ROLL_ABDUCT; // Guard: both fists up, elbows tight. for (const side of SIDES) { const s = sideSign(side); angles[`shoulder_pitch_${side}`] = ATHLETIC.SHOULDER_PITCH + ((KICK_GUARD_SHOULDER_DEG - ATHLETIC.SHOULDER_PITCH) * guard); angles[`shoulder_roll_${side}`] = (s * ATHLETIC.SHOULDER_ROLL_ABDUCT) + (s * KICK_GUARD_ROLL_OUT_DEG * guard); angles[`elbow_${side}`] = ATHLETIC.ELBOW + ((KICK_GUARD_ELBOW_DEG - ATHLETIC.ELBOW) * guard); } const turn = KICK_HIP_TURN_DEG * ((0.5 * chamber) + (0.5 * extend)); angles.waist_yaw += turn; angles.neck_yaw += -turn * NECK_COMPENSATION; return { angles, extras: { waist_yaw: { pitchDeg: KICK_LEAN_BACK_DEG * extend } }, rootOffset: [0, dy, bodyZ], rootPitchDeg: 0 }; } // ---- handstand: keyframed whole-body rotation about +Y. While folding, // the root is anchored so the sole front edge stays at its athletic spot // (heels rise, toe pivot); once the hands plant, the root is anchored so // the palm contact point stays on the ground at the plant line. Anchors // are solved once at init by evaluating the FK and shifting the root. function anchoredRootOffset(angles, rootPitchDeg, link, localMm, targetMm) { const frames = fkFrames(angles, {}, [0, 0, 0], rootPitchDeg); const f = frames[link]; const world = matVec3(f.R, localMm).map((v, k) => v + f.p[k]); // null target components leave that axis where the pose naturally put it. return targetMm.map((t, k) => (t === null ? 0 : t - world[k])); } function handstandKeyAngles(overrides) { const angles = { ...ATHLETIC_ANGLES }; for (const side of SIDES) { // Square the limbs for the gymnastic line: no abduction or twist. angles[`hip_roll_${side}`] = 0; angles[`ankle_roll_${side}`] = 0; angles[`shoulder_yaw_${side}`] = 0; angles[`wrist_pitch_${side}`] = 0; for (const [name, value] of Object.entries(overrides.legs || {})) { angles[`${name}_${side}`] = value; } for (const [name, value] of Object.entries(overrides.arms || {})) { angles[`${name}_${side}`] = value; } } if (overrides.neckPitch !== undefined) { angles.neck_pitch = overrides.neckPitch; } return angles; } const HANDSTAND_KEYS = (() => { const toeFrame = ATHLETIC_FRAMES.foot_left; const toeWorld = matVec3(toeFrame.R, HS_TOE_LOCAL_MM).map((v, k) => v + toeFrame.p[k]); // Anchor x/z only; the lateral position stays where the pose puts it. const toeTarget = [toeWorld[0], null, toeWorld[2]]; const ground = STRIDE_RIG.left.soleZ; const palmTarget = [HS_PLANT_X_MM, null, ground]; // The arms stay roughly vertical under the rotating body — the world // arm direction is rootPitch + shoulderPitch, so the shoulder tracks // -rootPitch as the body goes over, reaching shoulder -180 / elbow -18 / // wrist -70 at the hold (total hand pitch -90: palms flat, fingers // forward), all inside the URDF joint limits. const HOLD_ARMS = { shoulder_pitch: -180, shoulder_roll: 0, elbow: -18, wrist_roll: 0, wrist_pitch: -70 }; const spec = [ { t: 0.0, pitch: 0, anchor: null, overrides: null }, { t: 0.09, pitch: 12, anchor: "toe", overrides: { legs: { hip_pitch: -30, knee: 20, ankle_pitch: 3 }, // foot pitch +5: heel just off the ground arms: { shoulder_pitch: -52, shoulder_roll: 0, elbow: -10, wrist_roll: 0, wrist_pitch: -20 } } }, { t: 0.2, pitch: 55, anchor: "toe", overrides: { legs: { hip_pitch: -100, knee: 30, ankle_pitch: 27 }, // foot pitch +12: heels rise in the fold arms: { shoulder_pitch: -70, shoulder_roll: 0, elbow: -8, wrist_roll: 0, wrist_pitch: -45 } } }, { // Press position: palms planted, arms vertical, pike fold with the // toes still resting on the ground (hip solved at init below). t: 0.3, pitch: 95, anchor: "palm", solvePressHip: true, overrides: { legs: { hip_pitch: -80, knee: 8, ankle_pitch: 30 }, arms: { shoulder_pitch: -95, shoulder_roll: 0, elbow: -20, wrist_roll: 0, wrist_pitch: -70 } } }, { t: 0.4, pitch: 140, anchor: "palm", overrides: { legs: { hip_pitch: -45, knee: 25, ankle_pitch: 10 }, arms: { shoulder_pitch: -145, shoulder_roll: 0, elbow: -18, wrist_roll: 0, wrist_pitch: -67 } } }, { t: 0.48, pitch: 178, anchor: "palm", overrides: { legs: { hip_pitch: -6, knee: 4, ankle_pitch: 25 }, arms: HOLD_ARMS } }, { t: 0.58, pitch: 178, anchor: "palm", overrides: { legs: { hip_pitch: -6, knee: 4, ankle_pitch: 25 }, arms: HOLD_ARMS } } ]; // Mirror the way back down through the same shapes. const back = [ { ...spec[4], t: 0.66 }, { ...spec[3], t: 0.76 }, { ...spec[2], t: 0.86 }, { ...spec[1], t: 0.94 }, { ...spec[0], t: 1.0 } ]; // Resolve every key with its anchor TYPE plus the toe-line x position // it implies; the root translation itself is solved per frame at // runtime, so contacts stay exact through every interpolated pose. // Palm-typed keys still record their natural toe x so toe<->palm // boundary intervals can slide the toe target continuously. const resolved = [...spec, ...back].map((key) => { const angles = key.overrides ? handstandKeyAngles(key.overrides) : { ...ATHLETIC_ANGLES }; const type = key.anchor === "palm" ? "palm" : "toe"; if (key.solvePressHip) { // Bisect the pike fold so the toes rest exactly on the ground // behind the planted hands (toe height grows monotonically with // hip angle on the toes-behind branch). The palm anchor depends // only on the arm chain, so the hip search does not disturb it. const rootOffset = anchoredRootOffset(angles, key.pitch, "hand_left", HS_PALM_LOCAL_MM, palmTarget); let lo = -95; let hi = -40; for (let i = 0; i < 48; i += 1) { const mid = (lo + hi) / 2; for (const side of SIDES) { angles[`hip_pitch_${side}`] = mid; } const f = fkFrames(angles, {}, rootOffset, key.pitch).foot_left; const toeZ = f.p[2] + matVec3(f.R, HS_TOE_LOCAL_MM)[2]; if (toeZ < ground) { lo = mid; } else { hi = mid; } } } const rootOffset = type === "palm" ? anchoredRootOffset(angles, key.pitch, "hand_left", HS_PALM_LOCAL_MM, palmTarget) : anchoredRootOffset(angles, key.pitch, "foot_left", HS_TOE_LOCAL_MM, toeTarget); const foot = fkFrames(angles, {}, rootOffset, key.pitch).foot_left; const toeX = foot.p[0] + matVec3(foot.R, HS_TOE_LOCAL_MM)[0]; return { t: key.t, pitch: key.pitch, type, toeX, angles }; }); return { keys: resolved.sort((a, b) => a.t - b.t), toeZ: toeWorld[2], palmTarget }; })(); function handstandPose(t) { const { keys, toeZ, palmTarget } = HANDSTAND_KEYS; let a = keys[0]; let b = keys[keys.length - 1]; for (let i = 0; i < keys.length - 1; i += 1) { if (t >= keys[i].t && t <= keys[i + 1].t) { a = keys[i]; b = keys[i + 1]; break; } } const s = smooth01((t - a.t) / Math.max(1e-9, b.t - a.t)); const angles = {}; for (const name of Object.keys(ATHLETIC_ANGLES)) { angles[name] = a.angles[name] + ((b.angles[name] - a.angles[name]) * s); } // A breath of leg wobble during the hold (hips only, so the planted // hands stay put). const hold = pulse(t, 0.46, 0.5, 0.56, 0.6); const wobble = HS_WOBBLE_DEG * hold * Math.sin(2 * Math.PI * ((t - 0.46) / 0.14) * 2); for (const side of SIDES) { angles[`hip_pitch_${side}`] += wobble; angles[`knee_${side}`] += -wobble * 0.6; } const rootPitchDeg = a.pitch + ((b.pitch - a.pitch) * s); // Solve the contact anchor for THIS frame: palm-anchored only while both // bracketing keys are palm keys (the press key satisfies both contacts, // so the anchor handoff there is seamless); otherwise toe-anchored with // the toe target sliding along the ground line between the keys. let rootOffset; if (a.type === "palm" && b.type === "palm") { rootOffset = anchoredRootOffset(angles, rootPitchDeg, "hand_left", HS_PALM_LOCAL_MM, palmTarget); } else { const toeX = a.toeX + ((b.toeX - a.toeX) * s); rootOffset = anchoredRootOffset(angles, rootPitchDeg, "foot_left", HS_TOE_LOCAL_MM, [toeX, null, toeZ]); } return { angles, extras: {}, rootOffset, rootPitchDeg }; } // ---- Elvis dance. The left leg kicks out to the robot-left and plants // on a pointed toe, solved with closed-form leg IK: the hip roll angle // follows directly from requiring the toe target to lie in the leg's // (rolled) pitch plane, then the in-plane two-link IK places the // ankle-pitch joint so the toe tip lands exactly on the target. The // ankle roll stays 0, so the pointed foot rolls onto its outer toe edge // with the leg — the classic look. function elvisKickOutLeg(bodyZMm, pelvisDyMm, toeXMm, toeYMm, footPointDeg, liftMm, edgeScale) { const rig = STRIDE_RIG.left; // Left hip-roll joint center in the world, with the shifted pelvis. const hipRoll = [0, HIP_Y + pelvisDyMm, HIP_ROLL_Z_MM + bodyZMm]; // The foot rolls with the leg (ankle roll 0), so at full kick-out the // contact is the OUTER toe corner: lift the centerline target by the // rolled half foot width (scaled in with the pose; roll barely changes // with the lift, so one extra pass converges). let target = [toeXMm, toeYMm, rig.soleZ + liftMm]; let v = [target[0] - hipRoll[0], target[1] - hipRoll[1], target[2] - hipRoll[2]]; const edgeLift = ((35 * Math.abs(Math.sin(Math.atan2(v[1], -v[2])))) + 1) * edgeScale; target = [toeXMm, toeYMm, rig.soleZ + liftMm + edgeLift]; v = [target[0] - hipRoll[0], target[1] - hipRoll[1], target[2] - hipRoll[2]]; // Roll that brings the target into the leg's pitch plane. const rollRad = Math.atan2(v[1], -v[2]); const planeDepth = Math.hypot(v[1], v[2]); // hip-roll -> target, in-plane // Toe tip relative to the ankle-pitch joint at the pointed foot pitch. const pointRad = (footPointDeg * Math.PI) / 180; const toeForward = (ELVIS_TOE_TIP_LOCAL.x * Math.cos(pointRad)) - (ELVIS_TOE_TIP_LOCAL.drop * Math.sin(pointRad)); const toeDrop = (ELVIS_TOE_TIP_LOCAL.x * Math.sin(pointRad)) + (ELVIS_TOE_TIP_LOCAL.drop * Math.cos(pointRad)); // Ankle-pitch target measured from the hip-PITCH joint (78 mm further // down the plane from the hip-roll joint). const ik = legPitchIk(v[0] - toeForward, planeDepth - 78 - toeDrop, footPointDeg); const rollDeg = (rollRad * 180) / Math.PI; return { hip_pitch_left: ik.hipDeg, knee_left: ik.kneeDeg, ankle_pitch_left: ik.ankleDeg, hip_roll_left: rollDeg, // Flat (roll-compensated) foot at rest, rolling onto the outer toe // edge with the leg as the kick-out engages. ankle_roll_left: -rollDeg * (1 - edgeScale) }; } function dancePose(t) { const angles = { ...ATHLETIC_ANGLES }; const env = pulse(t, 0.0, 0.12, 0.88, 1.0); const beat = t * ELVIS_BEATS; const dy = ELVIS_WEIGHT_SHIFT_MM * env; const bodyZ = (-ELVIS_DIP_MM * env) - (ELVIS_BOUNCE_MM * env * Math.abs(Math.sin(Math.PI * beat))); // Stance leg: planted under the shifted, bobbing pelvis. Object.assign(angles, plantedLegAngles("right", bodyZ, dy)); // Kicked-out leg: toe planted out to the left, shaking with the beat. // The whole transition is IK-driven — the toe target slides from the // athletic toe spot to the kick-out point with a lift bump mid-blend, // so the foot never sweeps through the floor (at env=0 the IK lands // exactly back on the athletic leg). const shake = env * Math.sin(2 * Math.PI * 2 * beat); const toeX = ELVIS_ATHLETIC_TOE[0] + ((ELVIS_TOE_X_MM - ELVIS_ATHLETIC_TOE[0]) * env); const toeY = ELVIS_ATHLETIC_TOE[1] + (((ELVIS_TOE_Y_MM + (ELVIS_TOE_SHAKE_MM * shake)) - ELVIS_ATHLETIC_TOE[1]) * env); const point = (ELVIS_FOOT_POINT_DEG * env) + (ELVIS_FOOT_POINT_SHAKE_DEG * shake); const lift = 45 * Math.sin(Math.PI * env); Object.assign(angles, elvisKickOutLeg(bodyZ, dy, toeX, toeY, point, lift, env)); // The point: right arm up and out to the right, pulsing on the beat. const pulseDeg = ELVIS_POINT_PULSE_DEG * Math.sin(2 * Math.PI * beat); angles.shoulder_roll_right = (sideSign("right") * ATHLETIC.SHOULDER_ROLL_ABDUCT) + (((ELVIS_POINT_ROLL_DEG + pulseDeg) - (sideSign("right") * ATHLETIC.SHOULDER_ROLL_ABDUCT)) * env); angles.shoulder_pitch_right = ATHLETIC.SHOULDER_PITCH + ((ELVIS_POINT_PITCH_DEG - ATHLETIC.SHOULDER_PITCH) * env); angles.elbow_right = ATHLETIC.ELBOW + ((ELVIS_POINT_ELBOW_DEG - ATHLETIC.ELBOW) * env); // Off arm low and bent across the body. angles.shoulder_pitch_left = ATHLETIC.SHOULDER_PITCH + ((ELVIS_OFF_ARM_PITCH_DEG - ATHLETIC.SHOULDER_PITCH) * env); angles.elbow_left = ATHLETIC.ELBOW + ((ELVIS_OFF_ARM_ELBOW_DEG - ATHLETIC.ELBOW) * env); // Hip swivel, lean into the point, head to the pointing hand, chin up. const twist = ELVIS_TWIST_DEG * env * Math.sin(Math.PI * beat); angles.waist_yaw += twist; angles.neck_yaw += (ELVIS_HEAD_TURN_DEG * env) - (twist * 0.6); angles.neck_pitch += ELVIS_CHIN_UP_DEG * env; return { angles, extras: { waist_yaw: { rollDeg: ELVIS_LEAN_RIGHT_DEG * env } }, rootOffset: [0, dy, bodyZ], rootPitchDeg: 0 }; } const FEATURE_BY_LINK = { pelvis: "pelvis", torso: "torso", neck_collar: "neckCollar", head: "head", hip_bracket_left: "hipBracketLeft", hip_carrier_left: "hipCarrierLeft", thigh_left: "thighLeft", shin_left: "shinLeft", ankle_link_left: "ankleLinkLeft", foot_left: "footLeft", shoulder_pod_left: "shoulderPodLeft", yaw_housing_left: "yawHousingLeft", bicep_left: "bicepLeft", forearm_left: "forearmLeft", wrist_carrier_left: "wristCarrierLeft", hand_left: "handLeft", hip_bracket_right: "hipBracketRight", hip_carrier_right: "hipCarrierRight", thigh_right: "thighRight", shin_right: "shinRight", ankle_link_right: "ankleLinkRight", foot_right: "footRight", shoulder_pod_right: "shoulderPodRight", yaw_housing_right: "yawHousingRight", bicep_right: "bicepRight", forearm_right: "forearmRight", wrist_carrier_right: "wristCarrierRight", hand_right: "handRight" }; export default { manifest: { schemaVersion: 1, step: { path: "models/robots/juno/juno.step" }, label: "juno humanoid", description: "Walking-in-place march for the juno humanoid: alternating leg lift with planted stance feet, antiphase arm swing, torso counter-sway, and a head that keeps facing forward.", units: { length: "mm", angle: "deg", time: "s" }, features: { pelvis: { ref: "#o1.1", label: "Pelvis", description: "Root link; static during the in-place march." }, torso: { ref: "#o1.2", label: "Torso" }, neckCollar: { ref: "#o1.3", label: "Neck collar" }, head: { ref: "#o1.4", label: "Head" }, hipBracketLeft: { ref: "#o1.5", label: "Left hip bracket" }, hipCarrierLeft: { ref: "#o1.6", label: "Left hip carrier" }, thighLeft: { ref: "#o1.7", label: "Left thigh" }, shinLeft: { ref: "#o1.8", label: "Left shin" }, ankleLinkLeft: { ref: "#o1.9", label: "Left ankle link" }, footLeft: { ref: "#o1.10", label: "Left foot" }, shoulderPodLeft: { ref: "#o1.11", label: "Left shoulder pod" }, yawHousingLeft: { ref: "#o1.12", label: "Left yaw housing" }, bicepLeft: { ref: "#o1.13", label: "Left bicep" }, forearmLeft: { ref: "#o1.14", label: "Left forearm" }, wristCarrierLeft: { ref: "#o1.15", label: "Left wrist carrier" }, handLeft: { ref: "#o1.16", label: "Left hand" }, hipBracketRight: { ref: "#o1.17", label: "Right hip bracket" }, hipCarrierRight: { ref: "#o1.18", label: "Right hip carrier" }, thighRight: { ref: "#o1.19", label: "Right thigh" }, shinRight: { ref: "#o1.20", label: "Right shin" }, ankleLinkRight: { ref: "#o1.21", label: "Right ankle link" }, footRight: { ref: "#o1.22", label: "Right foot" }, shoulderPodRight: { ref: "#o1.23", label: "Right shoulder pod" }, yawHousingRight: { ref: "#o1.24", label: "Right yaw housing" }, bicepRight: { ref: "#o1.25", label: "Right bicep" }, forearmRight: { ref: "#o1.26", label: "Right forearm" }, wristCarrierRight: { ref: "#o1.27", label: "Right wrist carrier" }, handRight: { ref: "#o1.28", label: "Right hand" } }, parameters: { phase: { type: "number", label: "Gait phase", description: "One full gait cycle; the walk animations drive this 0 -> 1.", default: 0, min: 0, max: 1, step: 0.001 }, gait: { type: "select", label: "Gait", description: "Showpieces (Elvis dance, handstand, karate kick) and in-place gaits (jump, run, stride, march). Each walk animation drives the matching mode.", default: "march", options: [ { value: "dance", label: "Elvis dance" }, { value: "handstand", label: "Handstand" }, { value: "kick", label: "Karate kick" }, { value: "jump", label: "Jump in place" }, { value: "run", label: "Run in place" }, { value: "stride", label: "Stride in place" }, { value: "march", label: "March in place" } ] }, strideLength: { type: "number", label: "Stride length", description: "Front-to-back foot travel (mm) of the stride gait; capped so the leg IK stays inside reach. Ignored by the march.", default: 230, min: 0, max: 300, step: 5 }, legLift: { type: "number", label: "Leg lift", description: "Scales hip raise, knee fold, and toe point of the swing leg. Stance feet stay planted at any value.", default: 1, min: 0, max: 1.4, step: 0.01 }, armSwing: { type: "number", label: "Arm swing", description: "Scales the antiphase shoulder swing and forward-arm elbow flexion.", default: 1, min: 0, max: 1.5, step: 0.01 }, torsoSway: { type: "number", label: "Torso sway", description: "Scales the waist counter-rotation and weight-shift roll; the head compensates to keep facing forward.", default: 1, min: 0, max: 1.5, step: 0.01 } }, // Ordered most exciting first — this is the order the viewer lists them. animations: { danceLoop: { label: "Elvis dance", description: "The King: weight on the right leg, left leg kicked out to the left on a shaking pointed toe, right finger pointing up to the right with a beat pulse, hip swivel and chin up.", duration: 3.2, loop: true, update({ cycle, set }) { set("gait", "dance"); set("phase", ((finite(cycle, 0) % 1) + 1) % 1); } }, handstandLoop: { label: "Handstand", description: "Toe-pivot fold, press to a palms-flat inverted hold with a breathing wobble, and back up to the ready stance.", duration: 4.6, loop: true, update({ cycle, set }) { set("gait", "handstand"); set("phase", ((finite(cycle, 0) % 1) + 1) % 1); } }, kickLoop: { label: "Karate kick", description: "Weight shifts over the left leg, the right leg chambers and snaps a ball-of-foot front kick at hip height behind a fists-up guard.", duration: 2.8, loop: true, update({ cycle, set }) { set("gait", "kick"); set("phase", ((finite(cycle, 0) % 1) + 1) % 1); } }, jumpLoop: { label: "Jump in place", description: "Crouch, leap with hands overhead, and land with a springy knee-absorbed wobble before settling back to the ready stance.", duration: 1.8, loop: true, update({ cycle, set }) { set("gait", "jump"); set("phase", ((finite(cycle, 0) % 1) + 1) % 1); } }, runLoop: { label: "Run in place", description: "Running cadence with flight phases: body bounce, toe-pivot push-off, high heel recovery, forward lean, and pumping bent arms.", duration: 0.8, loop: true, update({ cycle, set }) { set("gait", "run"); set("phase", ((finite(cycle, 0) % 1) + 1) % 1); } }, strideLoop: { label: "Stride in place", description: "Bigger strides: legs sweep back and forward with the stance foot sliding flat on the ground, wider arm swing, stronger torso counter-sway.", duration: 1.9, loop: true, update({ cycle, set }) { set("gait", "stride"); set("phase", ((finite(cycle, 0) % 1) + 1) % 1); } }, walkLoop: { label: "Walk in place", description: "Continuous march loop; scrub the gait phase or scale lift/swing/sway live.", duration: 1.5, loop: true, update({ cycle, set }) { set("gait", "march"); set("phase", ((finite(cycle, 0) % 1) + 1) % 1); } } } }, update({ params, effects }) { const phase = ((finite(params.phase, 0) % 1) + 1) % 1; const scales = { legLift: clamp(params.legLift, 0, 1.4), armSwing: clamp(params.armSwing, 0, 1.5), torsoSway: clamp(params.torsoSway, 0, 1.5), strideLength: clamp(params.strideLength, 0, MAX_STRIDE_MM) }; const gait = String(params.gait || "march"); let targetFrames; if (gait === "stride") { targetFrames = fkFrames( strideAnglesDeg(phase, scales), strideTorsoRollExtras(phase, scales.torsoSway) ); } else if (gait === "run") { targetFrames = fkFrames( runAnglesDeg(phase, scales), runTorsoExtras(phase, scales.torsoSway), [0, 0, runBounceMm(phase, scales.legLift)] ); } else if (gait === "jump") { const jump = jumpAnglesDeg(phase, scales); targetFrames = fkFrames( jump.angles, jumpTorsoExtras(phase, jump.bodyZ, jump.flight, scales.torsoSway), [0, 0, jump.bodyZ] ); } else if (gait === "kick" || gait === "handstand" || gait === "dance") { const pose = gait === "kick" ? kickPose(phase) : gait === "handstand" ? handstandPose(phase) : dancePose(phase); targetFrames = fkFrames(pose.angles, pose.extras, pose.rootOffset, pose.rootPitchDeg); } else { targetFrames = fkFrames( gaitAnglesDeg(phase, scales), torsoRollExtras(phase, scales.torsoSway) ); } // The pelvis root is included: it bobs in the run and jump gaits and // resolves to an identity delta in the march and stride. for (const [link, feature] of Object.entries(FEATURE_BY_LINK)) { effects.transform(feature, { matrix: deltaMatrixRowMajor(ATHLETIC_FRAMES[link], targetFrames[link]) }); } } };