Core Concepts /Animation & Time

Core Concepts

Animation & Time

Make things move smoothly at any frame rate with delta time, elapsed time and a little trigonometry.

Animation means changing a value a little every frame. The catch is that frames aren't always the same length: a 120 Hz screen calls export function update()Screen.OnUpdate()def update() twice as often as a 60 Hz one, and a busy frame can take longer.

#Use delta time

deltaTime()Screen.DeltaTimedeltaTime() is the number of seconds since the last frame, usually about 0.016. Multiply speeds by it so they mean "per second" instead of "per frame":

let x: f64 = 0
const speed: f64 = 300 // pixels per second

export function update(): void {
  x += 5                      // ✗ speed depends on the frame rate
  x += speed * deltaTime()    // ✓ 300 px/s everywhere
}
double x = 0;
double speed = 300; // pixels per second

Screen.OnUpdate(() => {
    x += 5;                          // ✗ speed depends on the frame rate
    x += speed * Screen.DeltaTime;   // ✓ 300 px/s everywhere
});
x = 0.0
speed = 300.0  # pixels per second

def update():
    global x
    x += 5                     # ✗ speed depends on the frame rate
    x += speed * deltaTime()   # ✓ 300 px/s everywhere

The same goes for anything that changes over time: rotation (radians per second), fading (alpha per second) and timers.

#Velocity and bouncing

Store a velocity, add it to the position each frame, and flip it when you hit a wall:

import { fill, rect, circle } from "codetoy/canvas"
import * as screen from "codetoy/screen"
import { deltaTime } from "codetoy/time"

let x: f64 = 100, y: f64 = 100
let vx: f64 = 220, vy: f64 = 160
const r: f64 = 20

export function update(): void {
  const dt = deltaTime()
  x += vx * dt
  y += vy * dt

  if (x < r || x > screen.width() - r) vx = -vx
  if (y < r || y > screen.height() - r) vy = -vy

  fill(20, 20, 30)
  rect(0, 0, screen.width(), screen.height())
  fill(100, 200, 255)
  circle(x, y, r)
}
double x = 100, y = 100;
double vx = 220, vy = 160;
const double r = 20;

Screen.OnUpdate(() => {
    double dt = Screen.DeltaTime;
    x += vx * dt;
    y += vy * dt;

    if (x < r || x > Screen.Width - r) vx = -vx;
    if (y < r || y > Screen.Height - r) vy = -vy;

    Canvas.Fill(20, 20, 30);
    Canvas.Rect(0, 0, Screen.Width, Screen.Height);
    Canvas.Fill(100, 200, 255);
    Canvas.Circle(x, y, r);
});
from codetoy.canvas import fill, rect, circle
from codetoy.screen import width, height
from codetoy.time import deltaTime

x, y = 100.0, 100.0
vx, vy = 220.0, 160.0
r = 20

def update():
    global x, y, vx, vy
    dt = deltaTime()
    x += vx * dt
    y += vy * dt

    if x < r or x > width() - r: vx = -vx
    if y < r or y > height() - r: vy = -vy

    fill(20, 20, 30)
    rect(0, 0, width(), height())
    fill(100, 200, 255)
    circle(x, y, r)

Add gravity by increasing vy a little every frame (vy += 900 * dt).

#Elapsed time and waves

elapsedTime()not in C#elapsedTime() is the total number of seconds since the program started. Feed it to Math.sin()Math.Sin()math.sin() or Math.cos()Math.Cos()math.cos() for smooth back-and-forth motion, pulsing and orbits:

const t = elapsedTime()
const pulse = 40 + Math.sin(t * 3) * 15             // radius between 25 and 55
const orbitX = screen.centerX() + Math.cos(t) * 120
const orbitY = screen.centerY() + Math.sin(t) * 120
circle(orbitX, orbitY, pulse)
// C# has no ElapsedTime yet, so add up DeltaTime yourself.
double t = 0;

Screen.OnUpdate(() => {
    t += Screen.DeltaTime;
    double pulse = 40 + Math.Sin(t * 3) * 15;            // radius between 25 and 55
    double orbitX = Screen.CenterX + Math.Cos(t) * 120;
    double orbitY = Screen.CenterY + Math.Sin(t) * 120;
    Canvas.Circle(orbitX, orbitY, pulse);
});
import math

t = elapsedTime()
pulse = 40 + math.sin(t * 3) * 15             # radius between 25 and 55
orbit_x = centerX() + math.cos(t) * 120
orbit_y = centerY() + math.sin(t) * 120
circle(orbit_x, orbit_y, pulse)

#Timers

To do something every N seconds, count down with delta time:

let spawnTimer: f64 = 0

export function update(): void {
  spawnTimer -= deltaTime()
  if (spawnTimer <= 0) {
    spawnTimer = 1.5   // seconds until the next spawn
    // spawn an enemy...
  }
}
double spawnTimer = 0;

Screen.OnUpdate(() => {
    spawnTimer -= Screen.DeltaTime;
    if (spawnTimer <= 0) {
        spawnTimer = 1.5;   // seconds until the next spawn
        // spawn an enemy...
    }
});
spawn_timer = 0.0

def update():
    global spawn_timer
    spawn_timer -= deltaTime()
    if spawn_timer <= 0:
        spawn_timer = 1.5   # seconds until the next spawn
        # spawn an enemy...

#Easing toward a target

For smooth following (cameras, cursors, UI), move a fraction of the remaining distance each frame:

const k = 1.0 - Math.exp(-10.0 * deltaTime())   // frame-rate independent smoothing
x += (input.mouseX() - x) * k
y += (input.mouseY() - y) * k
double k = 1.0 - Math.Exp(-10.0 * Screen.DeltaTime);   // frame-rate independent smoothing
x += (Input.MouseX - x) * k;
y += (Input.MouseY - y) * k;
k = 1.0 - math.exp(-10.0 * deltaTime())   # frame-rate independent smoothing
x += (mouseX() - x) * k
y += (mouseY() - y) * k

Higher numbers than 10 follow more tightly; lower numbers feel floatier.

Esc