6.8 Joints
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
Learning Objectives
By the end of this section, you will be able to:
- Explain what a joint is and why it is not the same as putting two sprites next to each other.
- Connect two sprites with a
HingeJointso they pivot around a shared point. - Connect two sprites with a
DistanceJointso they stay a fixed distance apart. - Change a
DistanceJoint's length while the program runs. - Release a joint with
delete()to launch something. - Name the other joint types and say what each is for.
- Aim a force at a target using a direction vector, and build a slingshot from a joint, a drag, and an aimed impulse together.
Every sprite in the courtyard game so far has been on its own — nothing has ever been attached to anything. This section adds a swinging hazard partway down the level: a pendulum the player has to time a run past, built from one joint and four lines of setup.
6.8.1 What a Joint Is
A joint is a permanent relationship between two sprites. The physics engine enforces it on every frame: whatever else happens, these two stay connected in the way you specified. A door stays on its hinge, a wrecking ball stays on its chain, a wheel stays on its axle.
A constraint connecting two sprites so the physics engine keeps a fixed relationship between them — a shared pivot, a fixed distance, a sliding track. Created with new SomeJoint(spriteA, spriteB) and removed with joint.delete().
The important word is enforced. You are not moving the sprites yourself each frame; you state the rule once, and physics does the rest. Every joint connects exactly two sprites, and the order matters: the first is usually the anchor and the second is the thing that moves.
6.8.2 Hinges
A HingeJoint pins two sprites together at a point and lets them rotate around it. This is a door hinge, a pendulum, a see-saw.
A joint that pins two sprites at a single point and allows rotation around it. By default the pivot is at the centre of the first sprite.
A hinge is the joint people reach for when they want something to dangle or swing. The anchor sprite being static is what stops the whole assembly falling out of the sky — a hinge holds two sprites together, it does not hold them up.
Add a swinging bar to the courtyard level, hinged from a static post above the ground between the two bombs (around x = 450, matching the widened 900-pixel level from §6.4). Give it a starting horizontal position so it swings once released.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
The bar starts horizontal, then swings down and settles into hanging straight below the post — a pendulum from four lines of setup and zero motion code. This is the obstacle the player will need to time a run past.
6.8.3 Ropes and Springs
A DistanceJoint keeps two sprites a fixed distance apart. Think of a rope: the two ends can swing anywhere, but they cannot get further apart than the rope allows.
A joint holding two sprites at a fixed separation. The distance defaults to however far apart they already are when the joint is created, and can be read or changed afterwards through the joint's length property.
Add a weight to the free end of the swinging bar from §6.8.2, connected by a short DistanceJoint instead of being glued rigidly — this makes the hazard feel heavier and lag slightly behind the bar's own swing.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
The weight trails the swinging bar on its own short rope instead of being rigidly attached — watch it lag slightly behind the bar at the bottom of each swing, exactly the way a chain would.
6.8.4 Releasing a Joint
A joint is not permanent. joint.delete() breaks the connection and the sprites go their separate ways, keeping whatever motion they had at that instant. Two details worth remembering: joints have delete() and no remove() (sprites have both, §6.2.4); and set the variable to null after deleting, since calling delete() twice on an already-deleted joint is an error.
Add a "cut the rope" feature: pressing X removes the HingeJoint connecting the bar to the post, sending the whole swinging assembly flying off with whatever momentum it had.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Press X while the bar is mid-swing and the whole assembly flies off on a tangent instead of continuing to pivot — the exact motion it had at the moment of release, now unconstrained.
6.8.5 The Other Joints
Four more joint types exist, all created the same way (new WheelJoint(a, b)) and removed with delete():
| Joint | What it does | Typical use |
|---|---|---|
SliderJoint |
Constrains one sprite to slide along a line relative to the other | A lift, a piston, a drawer |
WheelJoint |
A wheel on an axle with suspension | A vehicle |
GrabberJoint |
Attaches a sprite to a point so it can be dragged | Mouse-dragging a physics object |
GlueJoint |
Fuses two sprites rigidly at their current positions | Building one solid shape out of several sprites |
SliderJoint and WheelJoint accept an axis, given as { x: 1, y: 0 } for horizontal.
GrabberJoint's pull point is fixed at the moment you create it — moving the anchor sprite afterward does not drag the target along with it. For a drag that tracks the mouse continuously, the manual position-snap pattern from §6.7.3 (set the sprite's .x/.y directly to mouse.x/mouse.y every frame while dragging) is the reliable choice; that's why this book uses that approach instead of GrabberJoint for its own drag examples.
Use a GlueJoint instead of the DistanceJoint from §6.8.3 to fuse the weight rigidly to the end of the bar. Compare how the swing feels — does the weight still lag behind, or move exactly with the bar now?
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
With GlueJoint, the weight moves exactly with the bar — no lag, because it's now rigidly fused rather than trailing on a rope. The two joints answer different questions: DistanceJoint says "stay this far apart, however you get there"; GlueJoint says "become one object."
6.8.6 Impulses and the Slingshot Pattern
§5.2/§6.3 already used applyForce(fx, fy) — but always with fixed numbers, pushing in one constant direction. Real aimed shots need a direction vector: dx = target.x - source.x, dy = target.y - source.y gives you a vector pointing from one sprite toward another, and scaling that into applyForce launches something at a target instead of just away from wherever it happened to start. Skipping this vector step is the single most common bug in force-based aiming — without it, every launch fires in the same constant direction no matter where the player aimed.
This composes with everything else in this chapter into the slingshot pattern: a DistanceJoint at length = 0 anchors a ball to a fixed point; dragging the ball (§6.7.3) stretches that anchor out; releasing the mouse deletes the joint and applies a force along the vector from the ball back to the anchor, scaled up. Pull back, let go, launch — the same idea behind slingshots and grappling mechanics across dozens of games, built from three things you already know.
Definition: Aimed Impulse
A force computed from the direction between two sprites (dx = target.x - source.x, dy = target.y - source.y, scaled and passed to applyForce) rather than a fixed constant — the push points at something instead of in a hardcoded direction.
Build a slingshot: a ball anchored to a fixed point by a zero-length DistanceJoint. Dragging the ball stretches the anchor; releasing the mouse deletes the joint and launches the ball backward along the stretch, scaled up into a real shot.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Drag the ball away from the gray anchor and release — it launches back along exactly the line you stretched it on, harder the further you pulled. dx/dy here point from the ball toward the anchor (the recoil direction), not the other way around; get that sign backward and the ball would keep sailing away from the anchor when you let go instead of launching back through it.
6.8.7 Challenge: Extend It Yourself
No starter code this time — you build it. Add the pendulum hazard (post, bar, weight) into the actual courtyard game from §6.7, positioned between the two bombs. Give the player a way to lose points on contact with the swinging weight — reuse the same overlaps()-callback pattern the bombs already use, treating weight as a one-sprite hazard group of its own, or grouping it if you'd rather.
Hint (try the Challenge yourself first!)
overlaps() works on a lone Sprite just as well as a Group — player.overlaps(weight, () => { score -= 5; }) is valid without wrapping weight in anything, though you'll want a cooldown or a hit flag so contact during a long swing doesn't dock points 60 times a second.
Problem Set 6.8
Problem 1. What is a joint, and what does it mean to say the engine enforces it?
Solution
Step 1 — Recall the definition:
A joint is a permanent relationship between two sprites — a constraint such as a shared pivot (HingeJoint), a fixed separation (DistanceJoint), or a sliding track (SliderJoint). Every joint connects exactly two sprites and is created once with new SomeJoint(spriteA, spriteB).
Step 2 — Explain what "enforced" means: Enforced means you state the rule a single time, at creation, and the physics engine applies it on every frame from then on. You never re-position the sprites yourself to maintain the connection — whatever else happens (gravity, collisions, other forces), the engine keeps the two sprites connected exactly as specified: the door stays on its hinge, the wrecking ball stays on its chain.
Answer: A joint is a constraint connecting two sprites so the physics engine keeps a fixed relationship between them (a shared pivot, a fixed distance, a slide track). "Enforced" means the rule is applied automatically every frame after being stated once — you never do per-frame work to maintain it.
Problem 2. Where is a HingeJoint's pivot by default, and how do you put it somewhere else?
Solution
Step 1 — State the default:
By default, a HingeJoint's pivot is at the centre of the first sprite — the one written first in new HingeJoint(spriteA, spriteB).
Step 2 — Explain how to move it:
Since the pivot is the first sprite's centre, you control the pivot by positioning that sprite: place the first sprite's centre exactly where you want the hinge point. That is why the chapter's examples hinge the bar to a small static post placed at the desired hinge location (e.g., post = new Sprite(450, 150, 12) puts the pivot at (450, 150)).
Answer: The pivot defaults to the centre of the first sprite passed to the joint. To pivot somewhere else, place the first sprite so its centre sits at that point — typically a small static "post" sprite at the spot you want the hinge.
Problem 3. Why is the anchor sprite of a hinge usually static?
Solution
Step 1 — Recall what a hinge does and does not do:
A HingeJoint pins two sprites together and allows rotation — it enforces the connection, but it provides no support. A hinge holds two sprites together; it does not hold them up.
Step 2 — Apply that to a dynamic anchor: If the anchor sprite were dynamic, gravity would act on it too. The whole assembly — anchor plus bar — would stay connected but fall out of the sky together, swinging as it dropped.
Step 3 — Conclude why static is used:
Making the anchor static fixes it in place, giving the moving sprite a fixed point to pivot around. That is what "stops the whole assembly falling out of the sky."
Answer: Because the hinge only connects the sprites — it doesn't support them against gravity. A static anchor is what holds the assembly up; without it, the entire hinged pair falls.
Problem 4. What determines a DistanceJoint's length if you do not specify one?
Solution
Step 1 — Recall the default rule:
A DistanceJoint's length defaults to however far apart the two sprites already are at the moment the joint is created.
Step 2 — Spell out the consequence:
Create the joint while the sprites are 80 pixels apart and the rope's length is 80; create them touching and it's 0. The length can then be read or changed afterwards through the joint's length property.
Answer: The current separation between the two sprites at the instant the joint is created — whatever gap they already have becomes the joint's length.
Problem 5. What is the difference between how a DistanceJoint and a GlueJoint connect two sprites?
Solution
Step 1 — Describe the DistanceJoint:
It keeps two sprites a fixed distance apart — like a rope. The ends can swing, rotate, and move anywhere at all, but they cannot separate further than the joint's length allows. Relative motion between the two remains free.
Step 2 — Describe the GlueJoint:
It fuses the two sprites rigidly at their current positions — they become one solid object. There is no relative motion of any kind: the pair translates and rotates together as a single shape.
Step 3 — State the one-line contrast from the chapter:
DistanceJoint says "stay this far apart, however you get there"; GlueJoint says "become one object." That's why the weight on a DistanceJoint lags behind the swinging bar, while a GlueJoint-fused weight moves exactly with it.
Answer: A DistanceJoint fixes only the separation, leaving both sprites free to swing and rotate relative to each other (a rope); a GlueJoint rigidly fuses them into a single object with no relative motion at all.
Problem 6. Write the two lines that lengthen a rope while the down arrow is held.
Solution
Step 1 — Identify what needs to change:
The rope's length is the joint's length property, which is settable while the program runs. "Lengthen while the key is held" therefore means increasing joint.length on every frame the down arrow is down.
Step 2 — Pick the right keyboard check:
kb.pressing('down') is true on every frame the key is held (unlike kb.presses('down'), which fires once per press) — exactly what "while the down arrow is held" requires.
Answer:
if (kb.pressing('down'))
joint.length += 1;
where joint is the variable holding the DistanceJoint. Each frame the key is held, the length grows a little, so the rope continuously lengthens.
Problem 7. What does joint.delete() do to the motion the sprites already had?
Solution
Step 1 — Identify what delete() affects:
joint.delete() breaks the connection — nothing more. It does not reset, stop, or redirect either sprite.
Step 2 — State what survives: Both sprites keep exactly the velocity (and rotation) they had at the instant of deletion and continue moving unconstrained. A bar cut loose mid-swing flies off on a tangent rather than continuing to pivot.
Answer: Nothing — delete() only removes the constraint. The sprites carry on with whatever motion they had at the instant of release, now unconstrained.
Problem 8. Why is joint.remove() an error when sprite.remove() is fine?
Solution
Step 1 — Check what each class actually defines:
Sprites define both remove() and delete() — two names for the same cleanup (§6.2.4). Joints define only delete().
Step 2 — Explain the error:
Calling joint.remove() attempts to call a method that does not exist on joint objects, so JavaScript throws an error (joint.remove is not a function). Nothing is wrong with the idea of removing a joint — the method simply has a different name.
Answer: Because joints have no remove() method — only delete(). Sprites have both (as aliases), so sprite.remove() works, but joint.remove() calls an undefined method and errors.
Problem 9. Why should a variable holding a joint be set to null after deleting it?
Solution
Step 1 — Recall the danger:
Calling delete() on an already-deleted joint is an error. A variable that still points at a deleted joint is a trap waiting for a second delete.
Step 2 — See what null buys you:
Setting the variable to null does two things: it stops you reusing a dead joint reference, and it makes truthiness guards work correctly — if (kb.presses('x') && hinge) passes only while a live joint exists, so the delete() line is skipped once hinge is null. If the stale reference were left in place, the guard would still pass and the second delete() would crash the sketch.
Answer: So the variable stops referencing a dead joint. Since a second delete() on the same joint is an error — and a stale reference would still pass checks like if (hinge) — setting hinge = null after deleting makes the guard reliable and prevents the double-delete crash.
Problem 10. Build a pendulum: a static pivot and a bar hinged to it.
Solution
Step 1 — Create a static pivot sprite:
A small sprite whose collider is 'static', so it holds the assembly up — remember, the hinge connects the sprites but does not hold them up.
Step 2 — Create the bar offset to one side: Place the bar's centre half its length to the right of the pivot, so its end sits at the pivot and it starts horizontal. A horizontal start means gravity has leverage on the free end, so it swings once released — a bar started hanging straight below the pivot would just sit there.
Step 3 — Join them with a HingeJoint:
new HingeJoint(pivot, bar) — the first sprite is the anchor, and the pivot defaults to its centre.
Step 4 — Run it: The bar swings down and settles hanging straight below the pivot — a working pendulum from one joint and no motion code.
▶ Press Run to see the output…
Answer: The sketch above: a static pivot at (200, 80) and a 160-pixel bar hinged to it with new HingeJoint(pivot, bar). Released from horizontal, the bar swings down and settles hanging straight below the pivot.
Problem 11. In the Challenge, why might checking overlaps() on the weight every frame dock points too fast without a cooldown?
Solution
Step 1 — Recall how often the callback can fire:
draw() runs about 60 times per second, and an overlaps() callback fires on every frame the two sprites are overlapping — not once per touch.
Step 2 — Apply that to the swinging weight: The weight sweeps through the player's space, so a single pass can keep the two overlapping for many consecutive frames — a slow, long swing means a long overlap window.
Step 3 — Do the arithmetic: At 5 points docked per callback, even half a second of contact is roughly 30 callbacks — about 150 points gone for one brush past the hazard, instead of the intended 5.
Answer: Because overlaps() fires every frame of contact (~60 per second), one slow pass through the weight could dock the 5 points dozens of times — turning a 5-point penalty into 100 or more. A cooldown timer or a hit flag ensures the penalty applies once per contact.
Problem 12. Name the four other joint types and give a use for each.
Solution
Step 1 — List the four types with their jobs:
All four are created the same way (new SomeJoint(a, b)) and removed with delete():
| Joint | What it does | Typical use |
|---|---|---|
SliderJoint |
Constrains one sprite to slide along a line relative to the other | A lift, a piston, a drawer |
WheelJoint |
A wheel on an axle with suspension | A vehicle |
GrabberJoint |
Attaches a sprite to a point so it can be dragged | Mouse-dragging a physics object |
GlueJoint |
Fuses two sprites rigidly at their current positions | Building one solid shape out of several sprites |
Answer: SliderJoint (a lift, piston, or drawer), WheelJoint (a vehicle's wheel on an axle), GrabberJoint (mouse-dragging a physics object), and GlueJoint (fusing several sprites into one solid shape).
Problem 13. The swinging bar fires straight up instead of swinging down when the sketch starts. Which part of the setup is most likely wrong, and is it a joint problem or a positioning problem?
Solution
Step 1 — Recall what a hinge controls — and what it doesn't:
A HingeJoint only pins the two sprites at a point and allows rotation around it. It does not decide which way the bar swings; the swing direction comes from gravity acting on where the bar's mass starts relative to the pivot.
Step 2 — Locate the likely culprit:
If the bar fires upward, its mass is starting on the wrong side of the pivot: the bar's new Sprite(x, y, ...) line has placed it above (or overlapping) the post instead of beside it at the same height. The physics solver starts from that bad configuration, and the correction plus gravity launches the bar up instead of letting it rotate down into a hang.
Step 3 — Classify the bug:
The joint line, the argument order, and the static collider can all be perfectly correct — this is a positioning problem, not a joint problem. (A genuinely joint problem — swapping the arguments to new HingeJoint(bar, post) — would instead yank the assembly toward the post, since the pivot would jump to the bar's centre.)
Step 4 — State the fix: Match Try It Now 6.8.1's geometry: post at (450, 150), bar at (530, 150) — the bar's centre offset horizontally by half its length so its end sits at the pivot and the free end hangs down.
Answer: Most likely the bar's starting position — its new Sprite(...) coordinates put it above/overlapping the post rather than beside it at the same height. It's a positioning problem, not a joint problem: the hinge is created correctly, but the bar's mass starts on the wrong side of the pivot.
Problem 14. In the slingshot code, what would happen if dx/dy were computed as ball.x - anchor.x / ball.y - anchor.y instead (the reversed sign)?
Solution
Step 1 — Recall what the correct vector points at:
In the working code, dx = anchor.x - ball.x and dy = anchor.y - ball.y form the vector pointing from the ball toward the anchor — the recoil direction. Applying the force along it snaps the ball back through (and past) the anchor, like a released slingshot.
Step 2 — Work out what the reversed version computes:
ball.x - anchor.x and ball.y - anchor.y point from the anchor to the ball — the same direction the player just dragged it. The magnitude is unchanged (it's the same stretch distance), but the direction flips by 180 degrees.
Step 3 — Predict the behaviour: On release, the force would push the ball further away from the anchor, continuing the direction of the drag: pull back and it sails away from you instead of launching forward. The slingshot fires backwards.
Answer: The ball would keep flying away from the anchor in the direction you dragged it, instead of launching back through the anchor — the shot is flipped 180 degrees. The launch speed would be the same; only the direction reverses.
Problem 15. Why does the slingshot pattern set joint.length = 0 specifically, rather than leaving it at whatever default distance the two sprites started at?
Solution
Step 1 — Consider what the default length would be:
Left at its default, the joint's length is whatever gap the ball and anchor had when the joint was created. Any nonzero value means the ball's resting position is a ring of that radius around the anchor, not the anchor itself.
Step 2 — See why that breaks the slingshot: The launch strength comes entirely from how far the player stretches the ball away from the anchor. With a nonzero resting length, small drags fall within the resting length — the joint isn't stretched, so nothing pulls back and releasing barely moves the ball. Only drags beyond the default would load the "band," and the ball would settle back onto a ring around the anchor rather than sitting on it.
Step 3 — Note why it's set explicitly, not just inherited:
In the sketch the ball and anchor are both created at (100, 300), so the default distance happens to be 0 anyway — but writing joint.length = 0 makes the requirement explicit and robust: if you later nudge the ball's start position (say, so it's visible beside the anchor), the pattern still behaves as a zero-length tether where every pixel of drag is stretch.
Answer: Because the whole mechanism depends on the resting state being "ball exactly on the anchor." With length = 0, every bit of drag is pure stretch and the release force scales with the full pull-back; a leftover default length would give the ball a nonzero resting orbit, so small drags would store no pull and the launch would be weak or nonexistent.
Key Terms
| Term | Definition |
|---|---|
| Joint | A constraint connecting two sprites, enforced by the physics engine every frame |
| HingeJoint | Pins two sprites at a point and allows rotation around it; the pivot defaults to the first sprite's centre |
| DistanceJoint | Holds two sprites a fixed distance apart; the distance defaults to the gap when the joint was made |
length |
A DistanceJoint's separation, readable and settable while the program runs |
delete() |
Breaks a joint, leaving both sprites with whatever motion they had. Joints have no remove() |
| Anchor sprite | The first sprite passed to a joint, usually static, whose position most joints anchor to |
| GlueJoint | Fuses two sprites rigidly into one object; takes no options |
| Aimed impulse | A force computed from the direction between two sprites, rather than a fixed constant push |
| Slingshot pattern | A zero-length DistanceJoint combined with drag-to-stretch and a release-triggered aimed impulse |
| SliderJoint / WheelJoint | Constrain motion to a line or model a wheel on an axle; both accept an axis |