Skip to main content

Animation

FrameScript controls the overall timing on <TimeLine/>, but it also provides a system for fine-grained animation state.

Overview and example

import { useAnimation, useVariable } from "../src/lib/animation"
import { BEZIER_SMOOTH } from "../src/lib/animation/functions"
import { FillFrame } from "../src/lib/layout/fill-frame"
import { seconds } from "../src/lib/frame"

const CircleScene = () => {
// Store position and opacity as animatable variables
const position = useVariable({ x: -300, y: 0 })
const opacity = useVariable(0)

const { ready } = useAnimation(async (ctx) => {
// Create handles and wait for them in parallel
const move = ctx.move(position).to({ x: 240, y: 0 }, seconds(1.2), BEZIER_SMOOTH)
const fade = ctx.move(opacity).to(1, seconds(0.6), BEZIER_SMOOTH)
await ctx.parallel([move, fade])
}, [])

// Avoid rendering before precomputation finishes
if (!ready) return null

// Read the value for the current frame
const pos = position.use()

return (
<FillFrame style={{ alignItems: "center", justifyContent: "center" }}>
<div
style={{
width: 120,
height: 120,
borderRadius: "999px",
background: "#38bdf8",
opacity: opacity.use(),
transform: `translate(${pos.x}px, ${pos.y}px)`,
boxShadow: "0 20px 60px rgba(56,189,248,0.35)",
}}
/>
</FillFrame>
)
}

In this example, useVariable creates position and opacity, while useAnimation drives a move and a fade at the same time. Use variable.use() in JSX styles to bind the current-frame value.

Supported value types for useVariable are number, Vec2, Vec3, and hex colors (ColorHex):

const opacity = useVariable(0)
const pos2 = useVariable({ x: 0, y: 0 })
const pos3 = useVariable({ x: 0, y: 0, z: 0 })
const color = useVariable("#FFAA33CC")
const colorRgb = useVariable("#FFAA33")

You can reorder awaits

useAnimation uses async/await for flow control. By changing the await order, you can decide when to wait even with the same motions.

useAnimation(async (ctx) => {
// Kick off a motion first
const move = ctx.move(position).to({ x: 300, y: 0 }, seconds(1), BEZIER_SMOOTH)

// Wait for something else first
await ctx.sleep(seconds(0.4))

// If move already progressed, this finishes immediately
await move
}, [])

The key is that you can start motions early and await them later. That makes it easy to layer animations without complex bookkeeping.

Effects: SpeedLines

<SpeedLines /> is a focused-line overlay that reacts to the current frame and adds a subtle jitter.

import { SpeedLines } from "../src/lib/animation/effect/speed-lines"
import { FillFrame } from "../src/lib/layout/fill-frame"

const Impact = () => (
<FillFrame>
<SpeedLines />
</FillFrame>
)

Effects: DrawText

<DrawText /> renders text as animated SVG strokes using a supplied font file.

import { useAnimation, useVariable } from "../src/lib/animation"
import { DrawText } from "../src/lib/animation/effect/draw-text"
import { seconds } from "../src/lib/frame"

const Title = () => {
const progress = useVariable(0)

useAnimation(async (context) => {
await context.move(progress).to(1, seconds(2))
})

return <DrawText text="Hello" fontUrl="assets/Roboto.ttf" fontSize={96} progress={progress} />
}

Effects: DrawTex

<DrawTex /> renders TeX as animated SVG strokes (MathJax SVG output). MathJax must be available in the build (this project uses a static import in draw-text.tsx).

import { useAnimation, useVariable } from "../src/lib/animation"
import { DrawTex } from "../src/lib/animation/effect/draw-text"
import { seconds } from "../src/lib/frame"

const Formula = () => {
const progress = useVariable(0)

useAnimation(async (context) => {
await context.move(progress).to(1, seconds(2))
})

return (
<DrawTex tex={"\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}"} fontSize={96} progress={progress} />
)
}