Das Strichmännchen auf unserer Strichmännchen-Seite hat kein Sprite-Sheet und keine Animationsdateien. In jedem Frame berechnen ein paar hundert Zeilen JavaScript, wo Hüften, Knie, Hände und Kopf sein sollen, und zeichnen das Ergebnis auf ein HTML-<canvas>. Im Bewegungslabor geht es, rennt, wirft ohne Waffe, springt und führt eine Speerform aus 17 Posen vor – und du kannst jederzeit ohne sichtbaren Sprung zwischen den Bewegungen wechseln.
Aus der Seite ist inzwischen ein kleines spielbares Spiel geworden, Stickman Spear Run: ein Strichmännchen-Runner, in dem die Figur einen langen Speer trägt und im Laufen mit einem beidhändigen Stoß nach vorn angreift (Leertaste oder ↑ zum Springen, F oder → zum Zustoßen). Das ursprüngliche Bewegungslabor bleibt als zweiter Modus erhalten, sodass du weiterhin durch die Bewegungen blättern kannst – einschließlich des Wurfs ohne Waffe, den es nur dort gibt – und dem Skelett bei der Arbeit zusiehst. Dieser Artikel erklärt die Techniken dahinter anhand echter Ausschnitte aus dem Quellcode: Two-Bone-Inverse-Kinematik mit dem Kosinussatz, Keyframe-Spuren mit Easing, Fußverankerung, Squash and Stretch und ein wenig Verlet-Physik für Sekundärbewegung.
Ein Skelett aus Zahlen
Die ganze Figur besteht aus einer Handvoll Knochenlängen in Canvas-Einheiten:
const SPINE=54, NECK=13, HEADR=12;
const UARM=27, FARM=25, THIGH=37, SHIN=37;
const HIP_H=66; // standing hip height: knees always slightly bent
Beachte, dass HIP_H (66) kürzer ist als THIGH + SHIN (74). Die Figur steht nie mit durchgedrückten Knien – genau das lässt sie selbst im Stand lebendig wirken. Weil diese Maße fest sind, skaliert das Skript stattdessen das Canvas: Es wählt einen Maßstab, sodass die Zeichenfläche immer etwa 440 Einheiten hoch ist, egal wie groß das Element auf dem Bildschirm ist. Komposition und Abstände bleiben auf dem Handy und auf einem 4K-Monitor identisch.
In jedem Frame liefert eine Funktion namens buildPose(dt) eine Reihe von Punkten in Bildschirmkoordinaten – Becken, Brust, Kopf, Schulter, Hüfte, beide Knie, beide Füße, beide Ellbogen und beide Hände –, und ein separater Zeichenschritt rendert sie. „Wo sind die Gelenke“ von „wie zeichnen wir sie“ zu trennen, erlaubt es uns, den Zeichenstil zu ändern, ohne die Bewegung anzufassen.
Two-Bone-IK mit dem Kosinussatz
Inverse Kinematik beantwortet die Frage: Wenn die Hüfte hier ist und der Fuß dorthin soll – wo landet das Knie? Für eine Kette aus nur zwei Knochen gibt es eine exakte, geschlossene Lösung, und sie ist nur wenige Zeilen lang:
function ik(root, target, L1, L2, sign){
let dx=target.x-root.x, dy=target.y-root.y;
let d=Math.hypot(dx,dy);
d=clamp(d, Math.abs(L1-L2)+.01, L1+L2-.01);
const base=Math.atan2(dy,dx);
let ca=clamp((L1*L1+d*d-L2*L2)/(2*L1*d),-1,1);
const a=Math.acos(ca);
const jointAng=base+sign*a;
return V(root.x+Math.cos(jointAng)*L1, root.y+Math.sin(jointAng)*L1);
}
Der komplette Two-Bone-Solver aus stickman.js. Er gibt das mittlere Gelenk zurück (Knie oder Ellbogen).
Die Überlegung dahinter: Der Oberschenkel (L1), das Schienbein (L2) und die gerade Linie von der Hüfte zum Fuß (d) bilden ein Dreieck. Wir kennen alle drei Seiten, also liefert uns der Kosinussatz jeden beliebigen Winkel:
L2² = L1² + d² − 2·L1·d·cos(a) ⇒ cos(a) = (L1² + d² − L2²) / (2·L1·d)
a ist der Winkel an der Hüfte zwischen der Linie Hüfte–Fuß und dem Oberschenkel. Die Linie Hüfte–Fuß zeigt in Richtung base = atan2(dy, dx), der Oberschenkel also in Richtung base ± a. Geh von der Hüfte aus L1 in diese Richtung, und du bist am Knie. Das Argument sign wählt, welche der beiden spiegelbildlichen Lösungen du bekommst – Knie beugen sich nach vorn, und wenn beide Hände den Speer halten, verwenden der nahe und der ferne Ellbogen entgegengesetzte Vorzeichen, damit sie sich voneinander weg beugen.
Zwei Begrenzungen (Clamps) machen den Solver robust:
dwird begrenzt auf knapp innerhalb von[|L1 − L2|, L1 + L2]. Liegt das Ziel außer Reichweite, existiert kein Dreieck, undacoswürdeNaNliefern. Mit der Begrenzung heißt es stattdessen: „Streck dich so weit wie möglich in diese Richtung.“- Der Kosinus wird begrenzt auf
[−1, 1]. Gleitkommafehler können ihn auf 1,0000001 schieben – auch das istNaN-Gebiet.
Der Bein-Code fügt vor dem Aufruf von ik() noch ein Sicherheitsnetz hinzu: Ist ein Fußziel weiter von der Hüfte entfernt als THIGH + SHIN − 1.5, wird es auf derselben Linie zurückgezogen. Das Bein kann sich fast strecken, rastet aber nie ganz ein – so vermeidet man das sichtbare „Schnappen“, das entsteht, wenn eine Zwei-Knochen-Kette die volle Streckung erreicht.
Beine mit IK, Arme meist mit FK
Es ist verlockend, alles mit IK zu lösen. Wir tun das nicht. Füße müssen an exakten Positionen sein – auf dem Boden –, also werden Beine per IK gelöst. Arme, die beim Gehen schwingen, haben kein Ziel; sie haben einen Rhythmus. Dafür ist Vorwärtskinematik (Winkel wählen, Positionen berechnen) einfacher und sieht natürlicher aus:
function armFK(aUp, flex){
const e=add(shoulder,{x:Math.sin(aUp)*UARM, y:Math.cos(aUp)*UARM});
const h=add(e, {x:Math.sin(aUp+flex)*FARM, y:Math.cos(aUp+flex)*FARM});
return {elbow:e, hand:h};
}
Die Winkelkonvention lautet hier „0 zeigt senkrecht nach unten, positiv schwingt nach vorn“, was das Nachdenken über Gehposen erleichtert. Beim Gehen schwingt jeder Arm gegengleich zum Bein seiner Seite, und der Ellbogen beugt sich beim Vorschwung stärker und streckt sich beim Rückschwung – ein kleines Detail, das den roboterhaften Pendel-Look verschwinden lässt. Auf IK wechseln die Arme nur, wenn die Hände an einer bestimmten Stelle sein müssen: am Speerschaft.
Ein Gangzyklus in einer einzigen Phasenvariable
Gehen und Rennen werden von einer einzigen Zahl gesteuert, phase, die von 0 bis 1 läuft und dann von vorn beginnt. Der nahe Fuß verwendet phase, der ferne phase + 0.5, sodass sie immer einen halben Zyklus auseinanderliegen. Eine zweite Zahl, gait, blendet von 0 (Gehen) bis 1 (Rennen), und jeder Gangparameter wird damit interpoliert:
| Parameter | Gehen (gait = 0) | Rennen (gait = 1) |
|---|---|---|
Kadenz cyc (Zyklen pro Sekunde) | 1,5 | 2,55 |
| Schrittlänge | 44 | 60 |
| Schritthöhe | 15 | 26 |
| Duty-Faktor (Anteil des Zyklus, in dem ein Fuß am Boden ist) | 0,62 | 0,40 |
| Amplitude des Hüftwippens | 5 | 12 |
Der Duty-Faktor ist der entscheidende. Über 0,5 sind in einem Teil jedes Zyklus beide Füße am Boden – das ist die Definition von Gehen. Unter 0,5 gibt es Momente, in denen kein Fuß den Boden berührt: eine Flugphase, und genau die lässt Rennen wie Rennen aussehen. Weil gait weich geblendet wird (es nähert sich seinem Zielwert in jedem Frame an), gibt es beim Wechsel zwischen Gehen und Rennen nie einen Sprung.
Die Bahn jedes Fußes ist zweigeteilt. Während der Standphase (dem ersten duty-Anteil seines Zyklus) gleitet er linear von einer halben Schrittlänge vor der Hüfte bis eine halbe Schrittlänge dahinter, flach auf dem Boden. Während der Schwungphase wandert er entlang einer Smoothstep-Kurve wieder nach vorn, angehoben von einer halben Sinuswelle:
if(pph<duty){ const u=pph/duty; fx=lerp(stride/2,-stride/2,u); fy=groundY; }
else{ const u=(pph-duty)/(1-duty); fx=lerp(-stride/2,stride/2,smooth(u));
fy=groundY - stepH*Math.sin(PI*u); }
Die Hüfte hebt und senkt sich zweimal pro Zyklus (einmal pro Schritt) mit -|sin(phase·2π)|; beim Rennen ist diese Kurve phasenverschoben, damit der höchste Punkt mit der Flugphase zusammenfällt. Außerdem werden die Füße proportional zur Körperneigung leicht nach vorn geschoben, damit sie unter dem Schwerpunkt bleiben.
Fußverankerung: Schluss mit dem Moonwalk
Die Figur bleibt an einer festen Stelle auf dem Bildschirm, während der Boden vorbeiscrollt. Stimmt die Scrollgeschwindigkeit nicht, rutscht der aufgesetzte Fuß über den Boden – der klassische Moonwalk-Bug. Die Lösung ist eine Zeile:
const scroll = stride*cyc/duty; // ground speed = stride × cadence ÷ duty factor
Warum funktioniert das? Während der Standphase legt ein Fuß relativ zur Hüfte eine volle stride zurück. Die Standphase dauert duty eines Zyklus, und ein Zyklus dauert 1/cyc Sekunden, also dauert die Standphase duty/cyc Sekunden. Der Fuß bewegt sich relativ zum Körper demnach rückwärts mit
stride ÷ (duty ÷ cyc) = stride × cyc ÷ duty
Bewegt sich der Boden mit genau dieser Geschwindigkeit rückwärts, bewegen sich Fuß und Boden gemeinsam, und der Fuß steht fest. Mit den Zahlen aus der Tabelle scrollt das Gehen mit 44 × 1,5 ÷ 0,62 ≈ 106 Einheiten pro Sekunde und das Rennen mit 60 × 2,55 ÷ 0,40 ≈ 383. Das Rennen ist also mehr als dreimal so schnell, obwohl die Schrittlänge nur ein Drittel größer ist: Den Großteil erledigt der kürzere Duty-Faktor. Ein häufiger Fehler ist, stride × cadence zu verwenden – das ignoriert den Duty-Faktor und lässt den Standfuß wegdriften.
Der Boden in der Szene ist eine Reihe weicher Punkte, die sich in jedem Frame um scroll × dt verschieben. Hört die Figur auf zu laufen, um zu werfen, zu springen oder die Speerform auszuführen, fällt scroll auf null, und die Punkte bleiben ebenfalls stehen.
Keyframe-Spuren mit Easing
Werfen und Springen sind keine Zyklen, sondern einmalige Bewegungen mit Ausholen, Aktion und Erholung. Dafür nutzen wir winzige Keyframe-Spuren: Arrays aus [Zeit, Wert]-Paaren, abgetastet bei einer normierten Zeit p von 0 bis 1:
function track(t, keys, ease){
ease=ease||smooth;
if(t<=keys[0][0]) return keys[0][1];
for(let i=0;i<keys.length-1;i++){
const [t0,v0]=keys[i],[t1,v1]=keys[i+1];
if(t<=t1){ let u=(t-t0)/((t1-t0)||1); return lerp(v0,v1,ease(u)); }
}
return keys[keys.length-1][1];
}
Jede animierte Eigenschaft bekommt ihre eigene Spur. Für den Sprung lautet der vertikale Versatz des Körpers:
rootY = track(p,[[0,0],[0.2,10],[0.32,-6],[0.5,-160],[0.68,-70],[0.82,4],[0.9,-2],[1,0]],
(p<0.5?easeOutCubic:easeInCubic));
Lies es wie eine Geschichte: in die Hocke gehen (+10 – zur Erinnerung: die y-Achse des Canvas zeigt nach unten), abdrücken, auf 160 Einheiten am Scheitelpunkt steigen, fallen, die Landung etwas unterhalb des Bodenniveaus abfedern, zur Ruhe kommen. Das Easing wechselt nach der Hälfte: easeOutCubic auf dem Weg nach oben (schneller Absprung, zum Scheitel hin langsamer) und easeInCubic auf dem Weg nach unten (langsamer Beginn, dann beschleunigend). Eine billige, aber überzeugende Nachahmung der Wurfparabel.
Die Speerform treibt dieselbe Idee weiter: 18 Wegpunkte (die 17 Posen plus ein Schluss-Key, der dem ersten entspricht, damit die Form nahtlos loopt), die jeweils den ganzen Körper beschreiben – Speerwinkel, Griffposition, Neigung, Gewichtsverlagerung, Hocke, Standbreite, Anheben und Stauchung –, mit optionalem Easing pro Segment. Zwei Tricks sorgen dafür, dass sie gut lesbar ist. Zwei identische Wegpunkte hintereinander erzeugen ein Halten, den eingefrorenen „Posen“-Moment einer Kampfkunstform. Und Stöße verwenden easeOutCubic, sodass der Speer schnell herausschießt und in der Pose abbremst – das wirkt knackig und kraftvoll.
Squash and Stretch
Der Sprung animiert außerdem eine vertikale Skalierung, sy: in der Hocke auf 0,84 gestaucht, beim Absprung auf 1,14 gestreckt, bei der Landung wieder auf 0,82 gestaucht. Die horizontale Skalierung wird daraus abgeleitet, damit die Figur ihr Volumen zu behalten scheint:
sx = 1+(1-sy)*0.7; // squash down → get wider; stretch up → get thinner
const pivot=V(anchorX, groundY);
const S=p=>V(pivot.x+(p.x-pivot.x)*sx, pivot.y+(p.y-pivot.y)*sy);
Jedes Gelenk läuft am Ende von buildPose durch S(). Skaliert man um den Punkt am Boden unter der Figur (statt um ihre Mitte), bleiben die Füße auf dem Boden, während der Körper gestaucht wird – genau so, wie es ein Animator von Hand zeichnen würde. Der Kopfradius wird mit dem Mittelwert aus sx und sy skaliert.
Sekundärbewegung: eine Feder und eine Verlet-Kette
Primärbewegung ist das, was die Figur tut. Sekundärbewegung ist das, was folgt – Haare, Stoff, ein Kopf, der einen Moment hinter dem Körper herhinkt. Sie ist billig einzubauen und macht einen riesigen Unterschied. Unsere Figur hat zwei Arten davon.
Eine gedämpfte Feder am Kopf
Der Kopf sitzt nicht starr auf dem Hals. Eine Feder (Steifigkeit 260, Dämpfung 26) zieht ihn zu seiner Idealposition, sodass er bei einem Ruck des Körpers hinterherhinkt und mit einem Hauch von Überschwingen zur Ruhe kommt. Die Dämpfung ist niedrig genug, um ein wenig Cartoon-Wackeln übrig zu lassen.
Verlet-Haare
Der Haarschopf ist eine Kette aus vier Punkten im Abstand von 9 Einheiten. Verlet-Integration speichert für jeden Punkt die aktuelle und die vorherige Position; die Geschwindigkeit ist einfach die Differenz. Dadurch ist das Update sehr kurz:
const s=scarf[i], vx=(s.x-s.px)*0.86, vy=(s.y-s.py)*0.86;
s.px=s.x; s.py=s.y;
s.x+=vx+windX*dt*dt*60; s.y+=vy+g*dt*dt*60;
Der Faktor 0,86 ist der Luftwiderstand. windX ist ein Schub nach hinten, der mit der Laufgeschwindigkeit wächst (und bei Sprüngen einen zusätzlichen Stoß bekommt), und g ist die Schwerkraft. Nachdem die Punkte bewegt wurden, ziehen sechs Iterationen einer Abstandsbedingung jedes Paar wieder auf genau 9 Einheiten zusammen, wobei der erste Punkt am Hinterkopf fixiert ist. Fixierung plus Bedingungen – mehr braucht es nicht: Die Haare wehen beim Rennen nach hinten, hängen beim Gehen herab und schnippen bei Sprüngen hoch, ohne dass irgendetwas davon von Hand animiert wurde.
Weiche Gliedmaßen zeichnen
Ein Strichmännchen aus geraden Linien wirkt steif. Bei unserem laufen die drei Punkte jeder Gliedmaße (zum Beispiel Hüfte, Knie, Fuß) durch einen Catmull-Rom-Spline, werden zu einer glatten Kurve verdichtet und mit einem sich verjüngenden Band umhüllt – dick an der Hüfte, dünner am Fuß – mit runden Enden. Das Ergebnis sieht aus wie ein Gummischlauch-Arm aus alten Cartoons, der sich biegt, statt zu knicken. Gliedmaßen auf der abgewandten Seite werden zuerst gezeichnet, dünner, dunkler und mit 42 % Deckkraft, was ganz ohne 3D Tiefe vermittelt.
Zum Schluss ist die Schleife rücksichtsvoll. Das dt jedes Frames ist auf 33 ms begrenzt, damit ein Ruckler die Haare nicht quer über den Bildschirm schleudert. Die Animation pausiert, wenn die Bühne aus dem Sichtbereich scrollt oder der Tab verborgen ist, und wenn das System des Besuchers reduzierte Bewegung wünscht, startet sie pausiert auf einem einzelnen Standbild.
Was du dir abschauen solltest
- Two-Bone-IK sind zehn Zeilen. Begrenze Abstand und Kosinus, und es entsteht nie ein
NaN. - IK, wo es ein Ziel gibt, FK, wo es einen Rhythmus gibt.
- Bodengeschwindigkeit = Schrittlänge × Kadenz ÷ Duty-Faktor. Stimmt das, kleben die Füße bei jeder Gangart am Boden.
- Keyframe-Spuren mit Easing pro Segment reichen für ausdrucksstarke Einzelbewegungen. Dupliziere einen Key, um ein Halten zu bekommen.
- Stauche um den Kontaktpunkt, nicht um die Mitte, und leite die Breite aus der Höhe ab, um das Volumen zu erhalten.
- Eine Feder und eine Verlet-Kette aus vier Punkten liefern den Großteil des „lebendigen“ Gefühls für fast keinen Code.
All das kannst du in Bewegung auf der Strichmännchen-Seite sehen, die jetzt Stickman Spear Run ist – laufen, springen und mit dem Speer zustoßen –, mit dem Bewegungslabor als zweitem Modus. Wenn du Strichmännchen magst: Auch unser kommendes Physik-Puzzlespiel Ink Left hat eines in der Hauptrolle – gezeichnet in ein unvollendetes Skizzenbuch.