6.4 Animated Sprites and Camera

Aligned outcomes:

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:

In this section, you will learn to:
  • Load a sprite sheet animation using sprite.addAni().
  • Switch between animations using sprite.changeAni().
  • Control animation speed with ani.frameDelay.
  • Move the camera to follow a sprite around a world larger than the canvas.
  • Animate a sprite procedurally with frameCount and trig functions, with no image assets.
  • Give a sprite an emoji or image appearance with .image.

Every sketch through §6.3 kept its whole world inside one 400×400 canvas. This section widens the level — the ground, coins, and bombs now stretch across 900 pixels — which means something has to decide which part of it is on screen. That is what the camera is for, and it is the last piece this section adds to player before it becomes a real, animated, on-screen character.

6.4.1 Sprite Sheet Animation

moSHion supports minimal sprite-sheet animation. sprite.addAni(name, sheetUrl, frameCount) loads a horizontal frame strip from an image URL and registers it under the given name. The first addAni call auto-activates that animation.

A sprite sheet is a single image file that contains multiple frames of an animation arranged in a row. The engine cuts it into individual frames based on the frameCount you provide.

Definition 6.4.1: Sprite Sheet

A single image file containing multiple animation frames arranged horizontally. The engine splits it into individual frames based on the frame count you specify.

Try It Now 6.4.1

Here's §6.3's player, still a plain blue square. Register (in comments, since you don't have real sprite-sheet files yet) an 'idle' animation with 4 frames and a 'walk' animation with 6 frames on player.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Nothing looks different yet — the addAni calls are commented out because there are no real sprite-sheet images to load. The player is ready to receive them, and §6.4.2 wires up the logic that would switch between them once you do have art.

6.4.2 Switching Animations

sprite.changeAni(name) switches to a previously registered animation. If the name isn't found, it's a silent no-op — nothing happens, so register an animation before you try to switch to it.

The typical pattern is to call changeAni based on what the player is doing. Moving? Switch to 'walk'. Standing still? Switch to 'idle'.

changeAni is a silent no-op on an unknown name — it fails without an error message. A typo like changeAni('idle') when you registered 'idle' as 'idel' will just do nothing. Double-check your animation names.

Try It Now 6.4.2

Add the changeAni calls (still commented, matching §6.4.1) to the movement branch of draw: 'walk' when either arrow key is held, 'idle' otherwise.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

The movement logic hasn't changed — the player still walks the same way it did in §6.3. Once real art is loaded, this same branching would flip between a walk cycle and an idle pose automatically, driven by exactly the if/else you already wrote for movement.

6.4.3 Controlling Animation Speed

ani.frameDelay controls how many game frames each animation frame is held for (default 4). Higher values mean a slower cycle.

Definition 6.4.2: frameDelay

The number of game frames each animation frame is displayed before advancing to the next. Default is 4. Higher values = slower animation.

Try It Now 6.4.3

If player's 'walk' animation had 6 frames and you wanted the full cycle to take about half a second at 60 frames per second, what frameDelay would you set? Show your math, then say what player.ani.frameDelay = 10 would look like by comparison.

Solution

Half a second at 60 fps is 30 frames total, split across 6 animation frames: 30 ÷ 6 = frameDelay = 5. With frameDelay = 10, each of the 6 frames would hold for 10 game frames — 60 frames total, a full second per walk cycle — noticeably more sluggish than the snappy half-second cycle frameDelay = 5 gives you.

6.4.4 Following with the Camera

camera.x and camera.y are the world coordinates at the center of the visible canvas. Everything drawn inside draw() shifts as the camera moves, as if the world were scrolling underneath a fixed window rather than the window moving over a fixed world.

A soft follow eases the camera toward its target with a catch-up factor instead of snapping to it outright:

camera.x += (player.x - camera.x) * 0.1;

Smaller factors feel floaty and lag further behind; larger factors feel snappy and catch up almost instantly. 0.1 is a good starting point.

Definition 6.4.3: Camera

The viewport into the game world. camera.x and camera.y are the world coordinates at the center of the visible canvas; moving the camera scrolls everything drawn in draw().

Moving the camera scrolls everything drawn inside draw() — including the score text. If you want a HUD element to stay fixed in the corner of the screen no matter where the camera is looking, draw it before you update the camera position each frame.

Try It Now 6.4.4

Widen your level: stretch ground to 900 pixels, spread the 8 coins and 2 bombs across that full width instead of the first 400 pixels, and add a soft-follow camera so the player stays visible while walking the whole stretch. Draw the score text before moving the camera so it stays pinned to the corner.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Walk right and the world scrolls past instead of the player hitting the edge of the canvas — the level is now more than twice as wide as one screen. The score stays pinned in the top-left corner because text('Score: ...') runs before the camera update, exactly as the Insight Note above describes.

6.4.5 Procedural Animation

Not every animation needs a sprite sheet. frameCount (§6.9 formalizes it fully; you've been reading it since §6.4.1's Try It Now) increments every frame on its own — combining it with Math.sin, Math.cos, and the modulo operator is already enough for bobbing, spinning, pulsing, and color-cycling, with zero image assets.

Math.sin(frameCount / period) oscillates smoothly between -1 and 1. Multiply by an amplitude and add a baseline to shift that into whatever range you want — a coin that bobs 6 pixels up and down, say. frameCount % n cycles through the integers 0 to n - 1 every n frames, which is the same flashing/phase-change trick §6.9's blinking-light example uses.

Definition: Procedural Animation

Motion or visual change driven by a formula evaluated every frame — typically Math.sin/Math.cos for smooth oscillation or % for cyclic phase changes — rather than by swapping between pre-drawn image frames.

Try It Now 6.4.5

Make one of your coins bob up and down in place, using Math.sin(frameCount / period) to offset its y from a fixed baseline. No sprite sheet, no addAni — just math run every frame.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Every coin bobs together, 6 pixels above and below y = 200, forever — a single formula run each frame, no image files, no addAni registration. Give each coin its own phase offset (Math.sin(frameCount / 20 + c.x), say) and they'd bob out of sync with each other instead, which often reads as more alive than moving in lockstep.

6.4.6 Images and Emoji on Sprites

sprite.image = 'player.png' loads an image from a URL and draws it in place of the sprite's default colored shape, stretched to fill its width and height. But if the string has no dot in it, moSHion treats it as an emoji instead: sprite.image = '🏃' draws that character centered on the sprite, using the sprite's height as the font size — a real, visible character with zero image files to host or load.

Setting .image clears any active animation, and vice versa — .image and .ani are mutually exclusive on a given sprite. Use addAni/changeAni (§6.4.1-6.4.2) for state-driven sprite-sheet animation once you have real art; use .image with an emoji for a quick, appealing placeholder — or a finished look — without any art pipeline at all.

Definition: .image

A sprite property that replaces its default colored shape with either an image (a string containing a dot, treated as a URL) or an emoji (a string with no dot, drawn centered at the sprite's height as font size). Mutually exclusive with .ani — setting one clears the other.

Try It Now 6.4.6

Give your player an emoji face instead of a plain colored square, and give each coins member its own emoji too. Confirm both still move and collect exactly as before — .image only changes how a sprite draws, not how it behaves.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

The runner emoji falls, lands, and moves exactly like the plain blue square did — .image is purely visual. Setting it per-coin inside the spawn loop (c.image = '🪙') means each coin carries its own emoji independently, the same instance-independence rule from §5.3.2, just applied to a new property.

6.4.7 Challenge: Extend It Yourself

No starter code this time — you build it. Widen the level further, to 1400 pixels, and spread twice as many coins and bombs across it. Add a second HUD element — a "Coins left" counter — that also stays pinned on screen regardless of camera position. Then try changing the soft-follow factor from 0.1 to 0.3 and to 0.03, and describe in a comment how each feels different to play.

Hint (try the Challenge yourself first!)

coins.length already tells you how many coins remain in the group at any moment (§6.2.4) — no extra tracking variable needed. Draw both HUD lines before the camera.x += ... line, same reasoning as the score text.

Problem Set 6.4

Problem 1. What does sprite.addAni(name, sheetUrl, frameCount) do? What do each of the three arguments mean?

Solution

Step 1 — Recall what the method does: sprite.addAni(name, sheetUrl, frameCount) loads a horizontal frame strip from an image file and registers it on the sprite as a named animation, ready to be played or switched to later with changeAni. (The first addAni call on a sprite also auto-activates that animation.)

Step 2 — First argument, name: A string label you choose, such as 'idle' or 'walk'. This is the handle the animation is registered under — it's what you'll pass to changeAni later, so it's how you refer to the animation from then on.

Step 3 — Second argument, sheetUrl: The URL (or file path) of the sprite sheet image — the single image file containing all the animation frames arranged in a horizontal row.

Step 4 — Third argument, frameCount: How many frames the strip contains. The engine needs this number to know how to slice the image into individual frames.

Answer: addAni registers a named animation on a sprite from a horizontal sprite-sheet image: name is the label you'll use to switch to it, sheetUrl is the location of the frame-strip image, and frameCount is how many frames that strip contains.

Problem 2. What is a sprite sheet? How does the engine know where one frame ends and the next begins?

Solution

Step 1 — Define a sprite sheet: A sprite sheet is a single image file containing multiple frames of an animation arranged in a horizontal row, instead of one separate image file per frame.

Step 2 — Explain how frame boundaries are found: The engine doesn't detect frame edges from the picture content — you tell it how many frames the strip holds, via the frameCount argument of addAni. The engine then divides the image's total width by that count and cuts the strip into that many equal horizontal slices, one per animation frame.

Answer: A sprite sheet is one image containing several animation frames laid out in a row; the engine knows where frames begin and end because you supply the frame count, and it slices the strip into equal-width pieces accordingly.

Problem 3. What does sprite.changeAni(name) do? What happens if you call it with a name that hasn't been registered?

Solution

Step 1 — What changeAni does: sprite.changeAni(name) switches the sprite to a previously registered animation — for example, player.changeAni('walk') makes the sprite start playing the 'walk' animation that an earlier addAni call registered.

Step 2 — What happens with an unregistered name: Nothing at all — it's a silent no-op. No error is thrown and no warning is printed, which is why a typo like changeAni('idel') when you registered 'idle' quietly does nothing. That's also why you must register an animation before trying to switch to it.

Answer: changeAni switches the sprite to a registered animation by name; if the name was never registered, the call does nothing silently — no error, no change.

Problem 4. What does ani.frameDelay control? What is the default value? What happens if you set it to 1?

Solution

Step 1 — What frameDelay controls: ani.frameDelay is the number of game frames each animation frame is held on screen before advancing to the next one — it's the speed dial for sprite-sheet animation.

Step 2 — The default value: The default is 4, so each animation frame displays for 4 game frames. At 60 fps that means \( 60 \div 4 = 15 \) animation frames per second.

Step 3 — Setting it to 1: Each animation frame would display for exactly one game frame before advancing — the fastest playback possible, with no holding at all. The animation runs at full game speed (60 animation frames per second), which is 4× faster than the default and will usually look like a rapid blur.

Answer: frameDelay is how many game frames each animation frame is shown for; the default is 4, and setting it to 1 plays the animation at maximum speed — one animation frame per game frame.

Problem 5. Write code that registers two animations ('walk' and 'jump') and switches between them based on whether the player is on the ground or in the air.

Solution

Step 1 — Register both animations first: Registration has to happen before any changeAni can find them (an unregistered name is a silent no-op), so the two addAni calls come first:

player.addAni('walk', 'player-walk.png', 6);
player.addAni('jump', 'player-jump.png', 4);

Step 2 — Test whether the player is grounded: The same player.colliding(ground) test from §6.3's jump logic tells you whether the player is on the ground or in the air each frame.

Step 3 — Switch based on the test:

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

The first addAni auto-activates 'walk', which is conveniently the correct animation for a player standing on the ground at the start.

Answer: The code above — register 'walk' and 'jump' with addAni, then each frame call changeAni('walk') when player.colliding(ground) is true and changeAni('jump') otherwise.

Problem 6. Why is the first addAni call special? What happens automatically?

Solution

Step 1 — Identify the special behavior: The first addAni call on a sprite auto-activates the animation it registers — the sprite immediately starts displaying that animation with no changeAni call needed.

Step 2 — Explain why it matters: This guarantees a sprite has a visible animation the moment its first sheet is loaded, instead of sitting with nothing active. It also means the auto-activation only happens once: every later addAni call merely registers, and you must call changeAni explicitly to switch to those.

Answer: The first addAni call is special because it auto-activates the animation it registers — the sprite starts playing it automatically, so you never need a changeAni for the first animation.

Problem 7. If you have a sprite sheet with 8 frames and you want the animation to loop through all 8 frames in about 1 second (at 60 fps), what frameDelay would you use? Show your math.

Solution

Step 1 — Convert the time budget to game frames: At 60 frames per second, one second is 60 game frames total — that's the whole time budget the cycle has to fit in.

Step 2 — Divide the budget among the 8 animation frames: Each of the 8 animation frames must occupy an equal share of those 60 game frames:

$$60 \div 8 = 7.5 \text{ game frames per animation frame}$$

Step 3 — Set frameDelay:

player.ani.frameDelay = 7.5;

If you'd rather keep whole numbers, 7 completes the cycle in \( 8 \times 7 = 56 \) frames (just under a second) and 8 completes it in \( 8 \times 8 = 64 \) frames (just over) — both read as "about 1 second."

Answer: frameDelay = 7.5 (60 game frames ÷ 8 frames = 7.5); integer alternatives are 7 or 8 for a cycle of roughly one second.

Problem 8. What do camera.x and camera.y represent? What happens on screen when you change them?

Solution

Step 1 — What they represent: camera.x and camera.y are the world coordinates at the center of the visible canvas. The camera is the viewport into the game world — the window through which you see the level.

Step 2 — What changing them does: Changing them scrolls everything drawn inside draw(), as if the world were sliding underneath a fixed window rather than the window moving over a fixed world. For example, increasing camera.x moves the view to the right, so on-screen sprites appear to slide to the left.

Answer: They are the world coordinates at the center of the visible canvas; changing them scrolls the entire scene in draw(), panning the viewport around a world larger than the canvas.

Problem 9. Write the two lines of a soft-follow camera with a catch-up factor of 0.2.

Solution

Step 1 — Recall the soft-follow pattern: Each frame, the camera moves a fraction of the gap between itself and its target — the gap (player.x - camera.x) scaled by the catch-up factor and added back onto the camera's position, so it eases toward the player instead of jumping to it.

Step 2 — Apply a factor of 0.2 to both axes:

camera.x += (player.x - camera.x) * 0.2;
camera.y += (player.y - camera.y) * 0.2;

Each frame the camera closes 20% of the remaining distance, catching up quickly but smoothly.

Answer: The two lines above — one easing camera.x toward player.x, the other easing camera.y toward player.y, both with a catch-up factor of 0.2.

Problem 10. Why does snapping the camera directly to the player (camera.x = player.x) feel worse than a soft follow, even though both keep the player on screen?

Solution

Step 1 — Consider what snapping does: camera.x = player.x copies the player's position to the camera exactly, every frame. Every twitch in the player's motion — sudden starts and stops, landing from a jump, tiny physics jitter — is passed straight through to the camera with zero smoothing.

Step 2 — Compare with a soft follow: A soft follow only closes a fraction of the gap each frame, so quick jitters get averaged out and the camera glides with a sense of weight and inertia. Both approaches keep the player on screen, but snapping makes the whole world jerk in lockstep with the player's velocity changes, which feels rigid and harsh, while easing feels smooth and polished.

Answer: Snapping transmits every abrupt change in the player's motion directly to the camera, so the whole world jolts with each stop, start, and landing; the soft follow's catch-up factor smooths those changes out, making the same on-screen result feel far more natural.

Problem 11. If you want a score display to stay fixed in the corner of the screen instead of scrolling with the world, where in draw() should you draw it relative to the camera update — before or after? Why?

Solution

Step 1 — Recall the rule: Moving the camera scrolls everything drawn inside draw() — the score text included — so placement relative to the camera update determines whether the text scrolls or stays put.

Step 2 — Answer the ordering question: Draw it before the camera update. Draw commands issued before the camera's position changes that frame are placed at fixed canvas coordinates, so the text stays pinned to the corner no matter where the camera ends up looking that frame.

Step 3 — See what goes wrong the other way: If the score is drawn after the camera update, it's placed relative to the camera's new position, so it drifts and scrolls along with the world instead of staying put.

Answer: Before — because the camera update affects everything drawn after it in that frame, drawing the HUD first keeps it fixed to the screen while the world scrolls.

Problem 12. In the widened-level solution, why does ground.friction from §6.3 still work the same way even though the ground sprite is now 900 pixels wide instead of 400?

Solution

Step 1 — Recall what friction is: friction is a physical property of a sprite's surface — it describes how much contact with that sprite resists sliding. It's about the material's behavior, not the sprite's size.

Step 2 — Compare what changed and what didn't: Widening the ground from 400 to 900 pixels changed its geometry — how much of the world it covers — but not its surface behavior. Wherever along the 900 pixels the player stands, the player-to-ground contact uses the same friction value, so walking, stopping, and skidding feel identical to §6.3.

Answer: Friction is a per-surface property, not a per-pixel one — the width only extends how far the surface reaches, so the same ground.friction value applies to every point along it and the movement feel is unchanged.

Problem 13. Write the one line that would make a sprite pulse its .diameter (not its position) smoothly between roughly 14 and 20 pixels, using Math.sin.

Solution

Step 1 — Find the center and amplitude of the pulse: Math.sin oscillates smoothly between \(-1\) and \(1\), so the line has the shape baseline + amplitude * Math.sin(...). The middle of the range 14–20 is \( (14 + 20) \div 2 = 17 \), and the half-width of the range is \( (20 - 14) \div 2 = 3 \).

Step 2 — Write the line:

sprite.diameter = 17 + 3 * Math.sin(frameCount / 20);

The divisor 20 sets the pace of the pulse — larger values pulse more slowly, smaller values faster.

Step 3 — Check the extremes: When Math.sin(...) equals 1, the diameter is \( 17 + 3 = 20 \); when it equals -1, it's \( 17 - 3 = 14 \); when it equals 0, the sprite sits at its 17-pixel midpoint. That's a smooth pulse across exactly the requested range.

Answer: sprite.diameter = 17 + 3 * Math.sin(frameCount / 20); — baseline 17, amplitude 3, so the diameter sweeps smoothly between 14 and 20 pixels.

Problem 14. What decides whether sprite.image = value is treated as a URL or an emoji? What happens to a sprite's .ani if you set .image on it afterward?

Solution

Step 1 — State the decision rule: Whether the string contains a dot. A string with a dot in it (like 'player.png') is treated as a URL to an image file; a string with no dot (like '🏃') is treated as an emoji, drawn centered on the sprite with the sprite's height used as the font size.

Step 2 — State the effect on .ani: Setting .image clears any active animation — .image and .ani are mutually exclusive on a given sprite, and assigning either one wipes out the other.

Answer: A dot in the string means "treat it as an image URL," and no dot means "treat it as an emoji"; setting .image afterward clears the sprite's .ani, since the two are mutually exclusive.

Problem 15. Why doesn't giving player and every coins member an .image in Try It Now change anything about how player.overlaps(coins, ...) behaves?

Solution

Step 1 — Recall what .image changes: .image is purely cosmetic — it only changes how a sprite is drawn, replacing the default colored shape with an image or emoji. It does not touch the sprite's position, size, velocity, or collider.

Step 2 — Recall what overlaps depends on: player.overlaps(coins, ...) is decided by the sprites' colliders and positions — the coins are still 24-pixel-diameter circles and the player is still a 40×40 sprite, whether they draw as emoji or as plain shapes.

Step 3 — Put the two together: Since neither sprite's physics changed, overlap detection fires at exactly the same moments and with exactly the same callback behavior as before; only the pixels on screen are different.

Answer: .image changes appearance only, while overlap detection runs on colliders and positions — which are untouched — so collecting coins behaves identically with or without the emoji.

Key Terms

Term Definition
addAni A method that loads a horizontal sprite sheet strip and registers it as a named animation on a sprite
Camera The viewport into the game world; camera.x/camera.y are the world coordinates at the center of the visible canvas
changeAni A method that switches a sprite to a previously registered animation by name
frameDelay The number of game frames each animation frame is displayed before advancing (default 4)
Soft follow A camera-following technique that eases toward its target by a catch-up factor each frame, rather than snapping to it directly
Procedural animation Motion or visual change driven by a per-frame formula (typically Math.sin/Math.cos/%) rather than swapped image frames
.image A sprite property replacing its shape with an image (URL string, has a dot) or an emoji (no dot); mutually exclusive with .ani
Sprite sheet A single image containing multiple animation frames arranged horizontally