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.
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.
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)
loop: true, // default true
});
Multiple clips can be registered on the same AnimatedSprite:
player.defineClip('idle', { frames: [new Rectangle(0, 32, 32, 32)], fps: 1, loop: true });
player.defineClip('jump', { frames: [new Rectangle(128, 0, 32, 32)], fps: 1, loop: false });
Call setClips() to replace all clips at once with a map.
Playback control
player.play('walk'); // start from frame 0
player.play('walk', { restart: false }); // resume if same clip
player.play('walk', { loop: false }); // override loop setting 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 a non-looping clip reaches its last frame:
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, 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 looping. 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.