Tweens and transitions
A tween moves a value from where it is to a target over a set time, along an easing curve. A transition blends one part of the scene into another, like a crossfade between two screens.
Tweening a node
TweenGroup has one factory per field. Each takes a TweenConfig with the duration in seconds, an optional easing curve and an optional callback for the end.
ts
import { Sprite, TweenGroup, TweenConfig } from "willow";
class Pickup {
static main(): void {
const gem: Sprite = new Sprite("assets/gem.png");
gem.setAnchor(0.5, 0.5);
gem.setPosition(400, 500);
engine.root2d.add(gem);
TweenGroup.position(gem, 400, 300, new TweenConfig(0.6, Ease.OUT_BACK));
TweenGroup.scale(gem, 1.5, 1.5, new TweenConfig(0.6, Ease.OUT_CUBIC));
TweenGroup.alpha(gem, 0, new TweenConfig(1.2, Ease.IN_QUAD, (): void => {
gem.dispose();
}));
}
}| Factory | Animates |
|---|---|
TweenGroup.position(node, x, y, cfg) | x and y |
TweenGroup.scale(node, sx, sy, cfg) | scaleX and scaleY |
TweenGroup.rotation(node, radians, cfg) | rotation |
TweenGroup.alpha(node, alpha, cfg) | alpha |
TweenGroup.color(node, color, cfg) | tint on a sprite, color on a quad, text or polygon |
TweenGroup.value(from, to, cfg, onStep) | A plain number, passed to onStep(v) each step |
Things to know:
- Tweens run themselves. willow advances every live tween once per frame. You don't call
update. (update(dt)exists for stepping a tween by hand in tests, and it adds to the automatic step.) cancel()stops a tween where it is.doneis true once it has finished.- If the node is disposed, its tweens end quietly. The
onDonecallback does not run when a tween is cancelled or its node is disposed. - A tween on a node inside a paused subtree waits while the subtree is paused.
TweenGroup.activeCount()counts unfinished tweens, which helps when looking for leaks.
The easing curve is a built-in Ease constant such as Ease.LINEAR (the default), Ease.OUT_CUBIC or Ease.OUT_BACK. You can also pass your own function (t, b, c, d): float, where t is the elapsed time, b the start value, c the change and d the duration.
Tweening a number
ts
import { Text, TweenGroup, TweenConfig } from "willow";
class Counter {
static main(): void {
const score: Text = new Text("0");
score.fontSize = 40;
engine.root2d.add(score);
TweenGroup.value(0, 1500, new TweenConfig(1.0, Ease.OUT_QUAD), (v: float): void => {
score.text = "" + Math.round(v);
});
}
}Waiting and timers
Each node has its own ticker, and the app has App.ticker. Use them to wait or to repeat without writing your own timer:
ts
import { Quad } from "willow";
class Blink {
static async main(): Promise<void> {
const warning: Quad = new Quad(800, 40, Color.hex("#C0302A"));
engine.root2d.add(warning);
warning.ticker.every(0.25, (): void => {
warning.visible = !warning.visible;
});
await warning.ticker.wait(3);
warning.dispose();
}
}A node's ticker pauses and scales with the node. Disposing the node cancels its pending waits.
Transitions between screens
engine.scene.playTransition(transition, from, to) blends one subtree into another. Both are normally children of the world root. The promise resolves when the transition ends; the scene never removes either node, so you retire from yourself.
ts
import { Node, Quad, Text, Transition } from "willow";
class Screens {
static async main(): Promise<void> {
const title: Node = new Node("title");
title.add(new Quad(800, 600, Color.hex("#1B2A4A")));
title.add(new Text("Press Space"));
engine.root2d.add(title);
const level: Node = new Node("level");
level.add(new Quad(800, 600, Color.hex("#2F6B3A")));
const off: Function = engine.scene.onKeyDown(async (e: KeyEvent): Promise<void> => {
if (e.keyCode === Key.SPACE) {
off();
engine.root2d.add(level);
await engine.scene.playTransition(Transition.fade(0.5), title, level);
title.dispose();
}
});
}
}Transition.fade(seconds)is the built-in crossfade.- Optional fourth and fifth arguments pass each side's screen-root subtree (its menu or dialog box) so it fades with that side, while other screen nodes stay on top.
- Only one transition plays at a time; a second
playTransitionrejects.cancelTransition()stops the current one, and its promise rejects with aCancellationError.
Writing your own transition
Extend Transition, call super(seconds) and override render. willow has already drawn both sides into regions; you draw them onto the canvas.
ts
import { Node, Transition, TextureRegion, TransitionCanvas } from "willow";
class LeftWipe extends Transition {
constructor(seconds: float) {
super(seconds);
}
render(progress: float, outgoing: TextureRegion, incoming: TextureRegion, canvas: TransitionCanvas): void {
canvas.draw(outgoing, 0.0, 0.0);
canvas.drawPart(incoming, 0.0, 0.0, progress * canvas.width, canvas.height, 0.0, 0.0);
}
}
class WipeDemo {
static async main(): Promise<void> {
const a: Node = new Node("a");
const b: Node = new Node("b");
engine.root2d.add(a);
engine.root2d.add(b);
await engine.scene.playTransition(new LeftWipe(0.4), a, b);
a.dispose();
}
}progress runs from 0 to 1. Anything you don't draw lets the scene show through, so draw outgoing first. The regions and the canvas are only valid during the call.
Next
- Lifecycle and time
- Reference: TweenGroup, Transition, Scene