# Animation

Write animation as atTime(time, duration), a function of time. animate() is the fallback. Keep fontSize a positive constant.

Write the animation of a custom element as a function of time. This is the recommended way. The timeline calls `atTime(time, duration)` for every frame it shows, and the method sets every animated property for that time.

```tsx
import { clamp, easeOutCubic, map } from '@vidova/core';

/** Clip length when nothing gives one, such as a thumbnail. */
public readonly defaultDuration = 3;

public atTime(time: number, duration: number): void {
  const enter = easeOutCubic(clamp(0, 1, time / 0.4));
  const leave = easeOutCubic(clamp(0, 1, (time - (duration - 0.4)) / 0.4));
  this.opacity(enter * (1 - leave));
  this.y(map(40, 0, enter));
}
```

- `time` is the seconds since the clip start, from 0 to `duration`.
- `duration` is the clip length in seconds.
- `defaultDuration` is the length used when there is no clip. Thumbnails pose the element at 65% of it.

## Why `atTime`

The timeline can show any frame in any order: play, scrub, jump back. With `atTime`, each frame costs one call, and the same time always gives the same frame.

`animate()` is a thread. To show an earlier frame, the timeline makes a new instance and replays `animate()` from the clip start. Scrubbing back through a long clip gets slower the further in you are.

## Rules for `atTime`

1. **Set every animated property on every call.** A property you set only in some branches keeps the value from the last frame shown.
2. **Keep no state between calls.** Do not count calls or keep the previous frame. Compute each value from `time`, `duration` and the inputs.
3. **Do not use unseeded random numbers or the wall clock.** Use a seeded hash of an index for noise.
4. **Do not wait or measure.** `atTime` cannot `yield` or `await`. Do not read `width()`, `height()` or `glyphs()` in it, because the font can still be loading. Read sizes in reactive props instead.

## A schedule of steps

Give each step a start time and a length. `clamp(0, 1, (time - start) / seconds)` goes from 0 to 1 over the step. Pass it through an easing function, then `map` it to the values.

```tsx
public atTime(time: number): void {
  const badge = this.badge();
  const title = this.title();
  // Badge pops in at 0.2 s for 0.3 s. The title slides in when the badge is done.
  badge.scale(easeOutBack(clamp(0, 1, (time - 0.2) / 0.3)));
  title.x(map(-200, 0, easeOutCubic(clamp(0, 1, (time - 0.5) / 0.6))));
}
```

A step that runs after another starts at the end of the one before it. Steps that run together share a start time.

## One clock signal

When many properties depend on text layout, keep one clock signal and read it in reactive props. Set the clock from `atTime`:

```tsx
private readonly clock = createSignal(0);

public constructor(props?: TickerProps) {
  super({ ...props });
  this.add(<Txt ref={this.label} x={() => this.xAt(this.clock())} text={() => this.text()} />);
}

public atTime(time: number): void {
  this.clock(time);
}
```

Reactive props update when the font loads, so they can read sizes.

## Repeating motion

A loop is a function of time too. Use the remainder of a division, or a sine:

```tsx
const phase = (time % 0.8) / 0.8;
this.dot().y(Math.sin(phase * Math.PI * 2) * -40);
```

## `fontSize` stays positive

`fontSize` must be a positive number on every frame. Do not drive it from a value that starts at 0. Keep `fontSize` constant and animate `scale`.

```tsx
this.scale(easeOutCubic(clamp(0, 1, time / 0.4)));
```

A `fontSize` of 0 compiles and then fails at render. The clip shows nothing.

## Refs

Hold a child when `atTime` needs to set it:

```tsx
import { createRef } from '@vidova/core';
import { Rect, Txt } from '@vidova/2d';

private readonly title = createRef<Txt>();

public constructor(props?: HelloTitleProps) {
  super({ ...props });
  this.add(
    <Rect>
      <Txt ref={this.title} text={() => this.label()} fontFamily="Inter Variable" />
    </Rect>,
  );
}

public atTime(time: number): void {
  this.title().opacity(easeOutCubic(clamp(0, 1, time / 0.4)));
}
```

Class fields for refs are fine. Class fields for `@signal` props must not collide with `Node` members. See [Layout](/docs/custom-elements/layout).

## Fallback: `animate()`

Use `animate()` only when the motion cannot be a function of time. It is a generator. `yield` pauses until the next frame. `yield*` runs another generator to completion, such as a tween.

```tsx
public *animateIn(duration: number = 0.4): ThreadGenerator {
  this.opacity(0);
  yield* this.opacity(1, duration, easeOutCubic);
}

public *animate(duration?: number): ThreadGenerator {
  yield* this.animateIn(duration ?? 0.4);
}
```

- `animate()` starts on the first frame of the clip. A tween with no timing function uses `easeInOutCubic`.
- Keep `animateIn()` separate. A thumbnail of an element without `atTime` plays `animateIn()` before capture.
- Do not yield a promise such as `fetch(...)`. The timeline stops the element and logs an error. `yield node` is allowed. It waits for the fonts and images of that node.
- The same frame must come out the same on every replay. Keep state in the instance, never in module variables.
