Docs

Animation

animate() is the timeline entry point. animateIn() is the intro. Use generator functions, yield*, all, and tween. Keep fontSize a positive constant.

View as Markdown

Custom element animation is a generator function. yield pauses until the next frame. yield* runs another generator to completion, such as a tween.

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

Import ThreadGenerator, all, tween, waitFor, and easing from @vidova/core.

animate vs animateIn

animate() is the timeline entry point. The player calls it when the clip is active.

animateIn() is the intro. Thumbnails play animateIn() before capture. Frame 0 of a reveal-driven element is blank, which is why the thumbnail path needs a separate intro.

Keep them as two methods. animate() may call animateIn(), then hold or play the rest:

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);
}

If animate() hides the element at the end, a thumbnail that played only animate() would capture an empty frame. That is the reason for the split.

Tweens and flow

A signal tween takes the next value, a duration in seconds, and an optional timing function:

yield* this.opacity(1, 0.4, easeOutCubic);

Run tweens together with all:

yield* all(
  this.opacity(1, 0.4, easeOutCubic),
  this.scale(1, 0.4, easeOutCubic),
);

Wait with waitFor:

yield* waitFor(0.2);

tween is the low-level helper when you need a callback per frame:

yield* tween(0.4, (value) => {
  this.opacity(value);
});

Prefer signal tweens when the property is already a signal.

fontSize stays positive

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

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

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

Refs

Hold a child when a method needs to tween it later:

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 *animateIn(duration: number = 0.4): ThreadGenerator {
  this.title().opacity(0);
  yield* this.title().opacity(1, duration, easeOutCubic);
}

Class fields for refs are fine. Class fields for @signal props must not collide with Node members. See Layout.