Skip to content

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();
        }));
    }
}
FactoryAnimates
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. done is true once it has finished.
  • If the node is disposed, its tweens end quietly. The onDone callback 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 playTransition rejects. cancelTransition() stops the current one, and its promise rejects with a CancellationError.

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 ​