Animation
Animate transforms and values with tweens and frame updates.
Animation
ExoJS gives you three layers of animation control, each suited to a different kind of motion:
- Manual frame-loop animation — you write
update(delta)logic that mutates transforms or values directly. Covered in Scenes & lifecycle. - Tweens — automated interpolation of numeric properties over time, with easing, delay, repeat, yoyo, chaining, and lifecycle callbacks. Driven by the application’s
TweenManager(app.tweens). - Animated sprites — frame-based sprite animation from named clips (
AnimatedSprite), optionally sourced from aSpritesheet.
All three can coexist in one scene. Use tweens for UI transitions and canned motion paths; use manual updates for physics, input-driven movement, and any logic where the next position depends on something other than time alone; use animated sprites for character animation playback.
Tweens
A Tween interpolates numeric properties on any target object from their current value to configured end values over a duration in seconds. Create one through app.tweens.create(target), configure it fluently, and call start():
init(loader) {
this.sprite = new Sprite(loader.get(Texture, 'hero'));
this.sprite.setAnchor(0.5);
this.sprite.setPosition(100, 300);
this.app.tweens.create(this.sprite.position)
.to({ x: 700 }, 1.5)
.easing(Ease.cubicInOut)
.start();
}
Start values are captured lazily on the first update after start(), so you can configure a tween in init and mutate the target between construction and start() without affecting the baseline.
Tween lifecycle
| Method | Effect |
|---|---|
.to(properties, duration) | Set end values and duration. Replaces any prior to(). |
.delay(seconds) | Wait before interpolation begins. Default 0. |
.easing(fn) | Easing function. Default Ease.linear. |
.repeat(count) | Extra cycles after the first. -1 = infinite. |
.yoyo(enabled) | Reverse direction on each repeat. |
.start() | Begin playback. |
.pause() | Freeze at current position. |
.resume() | Continue from where it paused. |
.stop() | Halt without finishing. onComplete does not fire. |
.chain(nextTween) | Automatically start nextTween when this one completes. |
State is readable at tween.state: 'idle', 'active', 'paused', 'stopped', or 'complete'. Progress is readable at tween.progress (0–1, already eased).
Callbacks
this.app.tweens.create(sprite)
.to({ rotation: 360 }, 2)
.onStart(() => console.log('started'))
.onUpdate(t => console.log(`progress: ${t}`))
.onComplete(() => console.log('finished'))
.onRepeat(() => console.log('cycled'))
.repeat(2)
.start();
onStartfires once when interpolation begins (after any delay).onUpdatefires every active frame, receiving the eased progress 0–1.onRepeatfires at each repeat cycle boundary.onCompletefires when all cycles finish naturally. Does not fire onstop().
Easing functions
Ease provides 31 standard Penner easing functions (including linear). All accept normalized time t in [0, 1] and return an eased value that starts at 0 and ends at 1:
import { Ease } from '@codexo/exojs';
Ease.linear
Ease.quadIn Ease.quadOut Ease.quadInOut
Ease.cubicIn Ease.cubicOut Ease.cubicInOut
Ease.quartIn Ease.quartOut Ease.quartInOut
Ease.quintIn Ease.quintOut Ease.quintInOut
Ease.sineIn Ease.sineOut Ease.sineInOut
Ease.expoIn Ease.expoOut Ease.expoInOut
Ease.circIn Ease.circOut Ease.circInOut
Ease.backIn Ease.backOut Ease.backInOut
Ease.bounceIn Ease.bounceOut Ease.bounceInOut
Ease.elasticIn Ease.elasticOut Ease.elasticInOut
In variants start slow and accelerate. Out variants start fast and decelerate. InOut combines both — slow at both ends, fast in the middle.
Tween chains
Chain tweens to build sequenced motion without nesting callbacks:
const moveRight = this.app.tweens.create(sprite.position)
.to({ x: 700 }, 1.0)
.easing(Ease.cubicInOut);
const moveDown = this.app.tweens.create(sprite.position)
.to({ y: 500 }, 0.8)
.easing(Ease.cubicOut);
const fadeOut = this.app.tweens.create(sprite)
.to({ alpha: 0 }, 0.5);
moveRight.chain(moveDown);
moveDown.chain(fadeOut);
moveRight.start();
Because chain() returns the next tween, keep a reference to the first tween and start that first tween:
const first = this.app.tweens.create(sprite.position)
.to({ x: 700 }, 1.0)
const second = this.app.tweens.create(sprite.position)
.to({ y: 500 }, 0.8);
const third = this.app.tweens.create(sprite)
.to({ alpha: 0 }, 0.5);
first.chain(second);
second.chain(third);
first.start();
Only the first tween in the chain needs a start() call — each chained tween automatically starts when its predecessor completes.
Sequencing with TweenSequencer
chain() links exactly one tween to exactly one successor — fine for a short relay, but it can’t express “run these two tweens together, then move on” or “pause for a beat between steps” without extra bookkeeping. TweenSequencer covers those cases. Create one with app.tweens.createSequencer() and build it up with .then() and .wait():
const fadeIn = this.app.tweens.create(sprite)
.to({ alpha: 1 }, 0.4);
const moveLeft = this.app.tweens.create(sprite.position)
.to({ x: 200 }, 0.6)
.easing(Ease.cubicOut);
const scaleUp = this.app.tweens.create(sprite.scale)
.to({ x: 1.2, y: 1.2 }, 0.6)
.easing(Ease.cubicOut);
const fadeOut = this.app.tweens.create(sprite)
.to({ alpha: 0 }, 0.4);
this.app.tweens.createSequencer()
.then(fadeIn)
.wait(0.5)
.then([moveLeft, scaleUp])
.then(fadeOut)
.onComplete(() => console.log('sequence done'))
.start();
Each .then() call adds a stage. Pass a single tween to run it alone, or an array of tweens to run them in parallel — the stage only advances once every tween in it has completed, which is how moveLeft and scaleUp above end up running side by side before fadeOut begins. .wait(seconds) inserts a timed pause as its own stage, without needing a dummy tween to hold up the sequence.
The sequencer also has lifecycle controls of its own, independent of any individual tween’s: .repeat(count) and .yoyo() replay the whole stage sequence (optionally reversed), .onStart() / .onComplete() fire once for the sequence as a whole, and .pause() / .resume() / .stop() act on whichever stage is currently active. createSequencer() binds the sequencer to app.tweens, so — like tweens created via app.tweens.create() — it’s ticked automatically once start() is called; no manual update() calls needed.
Reach for .chain() when you already have two tween references and just need one to follow the other. Reach for TweenSequencer when a step in the sequence involves multiple simultaneous tweens, needs a timed gap, or the sequence itself needs repeat/yoyo/callback control.
Interruption and replacement
Calling start() on an active tween restarts it from zero. To replace a running animation cleanly, stop the old tween and start a new one on the same target:
// Interrupt the current move and slide to a new position
this._activeTween?.stop();
this._activeTween = this.app.tweens.create(sprite.position)
.to({ x: newTarget }, 0.3)
.easing(Ease.cubicOut)
.start();
Only numeric properties interpolate
Tween interpolation is purely numeric. Attempting to tween a non-number property logs a warning and skips it. Use tween targets whose properties are numbers (for example sprite.position with { x, y }), and animate complex objects by their numeric fields.
Animated sprites
AnimatedSprite extends Sprite with frame-based animation playback from named clips. Each clip is a sequence of texture-frame rectangles with a playback speed and repeat count.
Defining clips
import { AnimatedSprite, Rectangle } from '@codexo/exojs';
const player = new AnimatedSprite(texture);
player.defineClip('walk', {
frames: [
new Rectangle(0, 0, 32, 32),
new Rectangle(32, 0, 32, 32),
new Rectangle(64, 0, 32, 32),
new Rectangle(96, 0, 32, 32),
],
fps: 10, // frames per second (default 12)
repeat: -1, // full cycles to play; -1 loops indefinitely (default)
});
repeat counts full cycles through every frame: -1 (the default when omitted) loops indefinitely, 1 plays the clip once and stops on its last frame, and any other positive integer plays exactly that many cycles before onComplete fires. This is the same -1 = infinite convention Tween.repeat() uses above — AnimatedSprite.repeat now agrees with Tween on what “loop forever” means.
Multiple clips can be registered on the same AnimatedSprite:
player.defineClip('idle', { frames: [new Rectangle(0, 32, 32, 32)], fps: 1 }); // repeat: -1 (default) — loops forever
player.defineClip('jump', { frames: [new Rectangle(128, 0, 32, 32)], fps: 1, repeat: 1 }); // plays once
Call setClips() to replace all clips at once with a map.
Per-frame timing and offsets
Two optional clip fields cover cases a single fps value can’t express:
frameDurations— a per-frame hold time in milliseconds, parallel toframes. When present it drives playback timing directly andfpsis ignored while advancing frames (still accepted as a display-only average). Use it for clips with intentionally uneven pacing, such as an impact frame that lingers longer than the rest.frameOffsets— a per-frame local draw offset ({ x, y }in local pixels), parallel toframes. Use it to keep frames anchored to a stable point when the source frames are trimmed to different sizes — without it, differently-trimmed frames jitter around their own top-left corner.
player.defineClip('hit', {
frames: [frame0, frame1, frame2],
frameDurations: [80, 40, 200], // ms per frame — frame1 is a quick flash, frame2 lingers
frameOffsets: [{ x: 0, y: 0 }, { x: -2, y: 1 }, { x: 0, y: 0 }], // keep the trimmed frame1 anchored
});
Both arrays are usually populated automatically rather than written by hand: the Aseprite guide (@codexo/exojs-aseprite) fills frameDurations from each tag’s per-frame export duration and frameOffsets from trimmed spriteSourceSize data. Set them yourself when building clips directly from a Spritesheet or raw Rectangles and you need the same fidelity.
Playback control
player.play('walk'); // start from frame 0
player.play('walk', { restart: false }); // resume if same clip
player.play('walk', { repeat: 1 }); // override repeat count for this play
player.pause(); // freeze playback
player.resume(); // continue
player.stop(); // stop and rewind to frame 0
The onFrame signal fires on every frame advance; onComplete fires when the clip completes its final repeat cycle:
player.onFrame.add((clip, frame) => {
console.log(`Frame ${frame} of clip "${clip}"`);
});
player.onComplete.add(clip => {
player.play('idle');
});
Call player.update(delta) each frame. The delta can be a Time object (from the scene’s update) or a number in milliseconds:
update(delta) {
this.player.update(delta);
}
From a Spritesheet
If a Spritesheet has animation data, AnimatedSprite.fromSpritesheet() creates a configured instance directly:
import { AnimatedSprite, Spritesheet } from '@codexo/exojs';
const sheet = new Spritesheet(texture, spritesheetJson);
const hero = AnimatedSprite.fromSpritesheet(sheet);
hero.play('walk');
The spritesheet’s animations map — ordered lists of frame names — becomes named AnimatedSprite clips (repeating indefinitely, since repeat isn’t set), each frame rectangle sourced from the spritesheet’s frames map.
When to use
Use AnimatedSprite when you have frame strips from a texture atlas and want clip-based playback with names, speeds, and repeat counts. Use manual sprite.setTextureFrame() calls in update when you need per-frame logic beyond simple clip playback — responsive frame selection based on game state, for example.
Standing tween vs. frame-loop animation
Tweens are driven by the TweenManager on every frame via app.tweens.update(delta), which happens automatically. You never call update on individual tweens unless you created them standalone (without a manager).
Frame-loop animation — the update(delta) pattern where you read delta.seconds and multiply transforms — is the right choice for continuous motion that doesn’t have a defined endpoint: character movement, physics, any state that evolves based on input or simulation. Tweens are the right choice for motion with a clear start and end: UI transitions, camera pans, scripted sequences, one-shot effects.
Examples
A sprite sliding between two positions with chained tweens and on-screen status text.
Manual frame-loop animation without tweens — comparing delta-based vs. fixed-step motion.
Where to go next
The next chapter, Render targets, covers off-screen rendering — how to draw into a RenderTexture and reuse it as a sprite texture for compositing, caching, or post-processing.