Back to the tutorial. Code: rig.gait_pose, rig.leg (the "ikb" mode), poses.py, anim.py.
The elk student's gaits are Muybridge's timing turned into tables: a phase offset per leg, a duty factor, a stride, eased swings, IK feet. I kept that machine. What changed is where the numbers came from, because for one of my animals the plates don't exist.
if ph < G["duty"]: # stance: the foot slides back on the ground
s = ph / G["duty"]
x = ax + S * (0.5 - s); z = 0.0
...
else: # swing: eased forward, lifted on a sine
s = (ph - G["duty"]) / (1 - G["duty"])
e = 0.5 - 0.5 * np.cos(np.pi * s)
x = ax + S * (-0.5 + e)
z = lift * np.sin(np.pi * s ** (0.8 if fore else 1.1))
legs[name] = ("ik", (x, z), meta, dig) # foot target, metapodial angle, digit angle
The new part is the last line. Each leg also gets a metapodial angle and a digit angle through the stride, because in these animals the foot changes shape as it moves (how-to 2).
With no plate to copy, I used the biomechanics.
0.76 − 0.12 v. The forefoot lands medially and the carpus rotates inward in the swing, "resulting in a 'waddle' from side to side".The tables:
"walk": dict(off={"LH": 0.0, "LF": 0.22, "RH": 0.5, "RF": 0.72}, duty=0.64, stride=0.66, lf=0.12, lh=0.10, bob=0.015, neck=-12.0, head=-42.0),
"pace": dict(off={"LH": 0.0, "LF": 0.08, "RH": 0.5, "RF": 0.58}, duty=0.46, stride=0.80, lf=0.16, lh=0.12, bob=0.03, neck=-6.0, head=-34.0),
"run": dict(off={"LH": 0.0, "RH": 0.10, "RF": 0.46, "LF": 0.56}, duty=0.32, stride=0.95, lf=0.26, lh=0.20, bob=0.07, neck=0.0, head=-24.0),
The walk's duty of 0.64 is Shine's formula at about 1 m/s. In the pace, each fore leg follows its own side's hind leg by only 0.08 of a stride. The bear has no trot entry at all.
The foot does two things only a bear's does:
if bear:
# heel lifts late in stance (the foot peels from the heel)
meta = meta0 - 40 * max(0, (s - 0.75) / 0.25) ** 1.5
...
if bear:
# the bear's forepaw flips: the wrist flexes and the palm turns up and back in the
# swing; the hind foot hangs from the ankle, toes down
meta = meta0 * (1 - sw) + (-70 if fore else 20) * sw
dig = 90 * (1 - sw) + (-40 if fore else 20) * sw
And the head carries low and swings. The naturalist says bears walk "with a rolling, swaying gait and the head swinging low":
p.neck_turn = 6 * np.sin(2 * np.pi * phase) # the low head swings side to side
The head was the bug. After I had fixed the standing head against the Whistler photo, the walking bear on the board still carried its head level, like a dog. The walk table had its own neck and head entries (4° and −26°, copied in spirit from the elk), and they overrode the species default. At −12° and −42° the nose sits at about 0.42 H, where the photo has it.


study GIFs (walk, pace, run) and fig, the pace in six frames. In the pace the two legs on one side swing together; watch the far pair.
The felid plates wouldn't download. Wikimedia's media host returned HTTP 429 to every request from the laptop for two hours. At 14:50 I sent the request from a Modal container instead (a 1-CPU function, a polite one-second sleep between files), and fourteen plates arrived in a few minutes:
@app.function(image=image, timeout=3000, cpu=1)
def fetch(titles, width=960):
...
q = urllib.parse.urlencode({"action": "query", "titles": "|".join(batch), "prop": "imageinfo",
"iiprop": "url|extmetadata", "iiurlwidth": width, "format": "json"})

nb075. Plate 721, a lion walking. The head at or just below the withers line, the forearm vertical at mid-stance, the hind foot set down in the fore print.
The walk and stalk tables were already right against 721 (the stalk is the walk slowed, lowered and with the head dropped):
"walk": dict(off={"LH": 0.0, "LF": 0.25, "RH": 0.5, "RF": 0.75}, duty=0.66, stride=0.55, lf=0.12, lh=0.12, bob=0.012, neck=-4.0, head=-22.0),
"stalk": dict(off={"LH": 0.0, "LF": 0.25, "RH": 0.5, "RF": 0.75}, duty=0.76, stride=0.34, lf=0.09, lh=0.08, bob=0.006, neck=-20.0, head=-6.0),
...
if gait == "stalk":
# belly low, legs flexed, head below the shoulder line, the scapulae standing up
p.body_z += -0.20
p.scap = 0.14
scap raises the two scapula tubes above the back line: the naturalist's "the shoulder blades, which stand up like two peaks". The critic's round 2 said my stalk's head was level with the shoulders, not below them. The neck went from −6° to −20° and the scapulae from 0.08 to 0.14 H.

nb076. Plate 720, a cat galloping, 24 frames.
Plate 720 was the correction. The first critic had called my gallop "a rocking horse". Against the plate, my cat galloped high on straight legs with a splayed stride and its tail held up like a flag. Muybridge's cat stays low through the whole stride: legs bunched under an arched back in the gathered frames, long and low in the extended ones, and the tail streaming back.
"run": dict(off={"LH": 0.0, "RH": 0.12, "RF": 0.48, "LF": 0.60}, duty=0.28, stride=0.95, lf=0.22, lh=0.20, bob=0.06, neck=2.0, head=-10.0),
...
p.flex = (22 if not bear else 12) * np.cos(2 * np.pi * (phase - 0.9)) # rotary gallop: gathered at 0.9
p.tail = (-12, 0.05, 0.2) if bear else (-8, 0.1, 0.3) # streaming back at the back line
if not bear:
p.body_z -= 0.07 # a galloping cat stays low: legs flexed, never stilted (pl. 720)
Stride 1.15 → 0.95 H, lifts 0.30 → 0.22, spine flex 16° → 22°, the body 0.07 H lower.

fig. The final gallop in ten frames. The gathered frames (the two ends) are right. The extended frames in the middle are still wrong: the legs splay stiffly and the tail rides up. The gallop's pitch and flex add to the tail's root angle, and I never printed the tail height for this pose the way I did for standing.
Most postures are a few angles and four foot targets: graze, sniff, alert, stand, the bear's fishing stance with one forepaw lifted to pin, the cat's crouch with its hind feet gathered under it. Three didn't fit that mould.
The pounce: keyframes. My first pounce used one formula for the whole leap and gave a cat on stilts (nb039). A pounce is three different shapes, so I made it three keyframes, interpolated by phase. They came from Seton's plate XV (the crouch) and a Commons photo of a cougar leaping with its claws out. (The code comment also cites "NPS leaping-cougar frames"; I never looked at those, and the comment is wrong.)
# 0.0 the launch: crouched low, body pitched up, hind legs driving, forelegs lifting;
# 0.5 the flight: body stretched level and high, forelegs reaching out ahead, hind legs
# trailing straight back, tail streaming;
# 1.0 the strike: forelegs down and forward, claws out, body pitched down, hinds coming under.
K = [dict(flex=4.0, pitch=22.0, z=-0.08, ..., LF=(20, 10, 60, 120), ..., LH=(-30, -38, -60, -80), ...),
dict(flex=5.0, pitch=4.0, z=0.46, ..., LF=(72, 84, 100, 116), ..., LH=(-100, -110, -120, -140), ...),
dict(flex=-4.0, pitch=-16.0, z=0.18, ..., LF=(55, 30, 30, 60), ..., LH=(-20, -60, -40, 20), ...)]
t = float(np.clip(phase, 0, 1)) * 2
i = min(int(t), 1); w = t - i
p.legs = {n: ("fk", tuple(lerp(np.array(A[n], float), np.array(B[n], float)))) for n in LEG_NAMES}
if phase < 0.12:
p.legs["LH"] = ("ik", (hx - 0.32, 0.0), 40, 100) # the launch: the hind paws still on the ground

fig. The pounce. The critic called the flight frame "an otter": the forelegs were reaching straight out level. I angled them forward and down and arched the back 5°. It still leans toward a weasel.
The climb: targets in body coordinates. I pitched the bear 80° about its hip and gave the paws world-space targets on a trunk. The IK had nothing sensible to reach, because the pitch had moved every root, and the bear floated beside nothing (nb039). The fix is a leg mode, "ikb", whose targets are given in the body's frame and transformed with it, plus a trunk whose face touches the belly:
if kind == "ikb":
(bx, bz), meta, dig = args
w = body_xf(np.array([bx, side * ry0, bz + rp * 0.55])) # the target turns with the body
ball = np.array([w[0], w[2]])
meta, dig = meta + pose.pitch, dig + pose.pitch
...
# in gait_pose("climb"):
zt = min(st[2] for st in sp.torso) - 0.01 # the trunk face, against the belly
p.legs = {"LF": ("ikb", (fr[0] + 0.30 + 0.10 * g, zt), 60, 100), ...
"LH": ("ikb", (hr[0] + 0.20 - 0.10 * g, zt), 70, 95), ...}
The trunk is a real tree (0.25 m radius), not one scaled to the climber, so a cub on the same trunk looks right beside its mother (sheet G).

fig. The climb in eight frames. It reads as "a bear on a tree", which it didn't before, but it's a lump pressed against a column. No limb wraps the trunk, and the bear bobs in place rather than rising. Seton's "corkscrew" bounds aren't in it.
Rearing: a slouch, not a soldier. My first rearing bear stood bolt upright with straight legs: "a man in a black coat", the critic said. Seton's plate XVII bears rear with the back bent, the belly out, and the forepaws hanging limp in front of the chest:
if gait == "stand_up": # bear rearing on its hind legs to see and smell (not aggression)
# a slouch, not a soldier: the back at ~65 deg, the belly out, the forelegs hanging in front
# of the chest with the paws limp, the head forward and up testing the air
p.pitch, p.pivot = 64.0, (sp.hind_root[0], sp.hind_root[1])
p.flex = 8.0
p.neck_ang, p.head_ang = -44.0, -52.0
p.legs = {..., "LF": ("fk", (30, 10, 50, 80)), "RF": ("fk", (22, 2, 44, 74))}
Round 1's pose notes are the most useful thing I can hand on here, because each one names what the pose read as instead:
| pose | read as | fix |
|---|---|---|
| bear rearing | a man in a black coat | slouch 64°, belly out, paws limp (above) |
| bear lying | a rug | head up on the paws (neck +4°), not flat |
| bear climbing | a bag stuck to a capsule | body-frame targets, a real trunk |
| salmon in the jaws | a speck | 0.85 H long; later carried by its back half, as in a photo (nb085) |
| bear pace | identical to the walk | lateral pairs 0.08 apart (above) |
| cougar run | a rocking horse | Muybridge 720 (above) |
| pounce flight | an otter | forelegs forward-down, back arched |
| cougar sit | too tall | body dropped 0.50 H, tibia folded forward |
| cougar trot | identical to the walk | (not fixed: cats rarely trot) |

nb039, nb060. The pose sheet before round 1 and after it.