8.1 KiB
8.1 KiB
Animation System
OpenTUI provides a timeline-based animation system for smooth property transitions.
Overview
Animations in OpenTUI use:
- Timeline: Orchestrates multiple animations
- Animation Engine: Manages timelines and rendering
- Easing Functions: Control animation curves
When to Use
Use this reference when you need timeline-driven animations, easing curves, or progressive transitions.
Basic Usage
React
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
function AnimatedBox() {
const [width, setWidth] = useState(0)
const timeline = useTimeline({
duration: 2000,
})
useEffect(() => {
timeline.add(
{ width: 0 },
{
width: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].width))
},
}
)
}, [])
return (
<box
width={width}
height={3}
backgroundColor="#6a5acd"
/>
)
}
Solid
import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"
function AnimatedBox() {
const [width, setWidth] = createSignal(0)
const timeline = useTimeline({
duration: 2000,
})
onMount(() => {
timeline.add(
{ width: 0 },
{
width: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].width))
},
}
)
})
return (
<box
width={width()}
height={3}
backgroundColor="#6a5acd"
/>
)
}
Core
import { createCliRenderer, Timeline, engine } from "@opentui/core"
const renderer = await createCliRenderer()
engine.attach(renderer)
const timeline = new Timeline({
duration: 2000,
autoplay: true,
})
timeline.add(
{ x: 0 },
{
x: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
box.setLeft(Math.round(anim.targets[0].x))
},
}
)
engine.addTimeline(timeline)
Timeline Options
const timeline = useTimeline({
duration: 2000, // Total duration in ms
loop: false, // Loop the timeline
autoplay: true, // Start automatically
onComplete: () => {}, // Called when timeline completes
onPause: () => {}, // Called when timeline pauses
})
Timeline Methods
// Add animation
timeline.add(target, properties, startTime?)
// Control playback
timeline.play() // Start/resume
timeline.pause() // Pause
timeline.restart() // Restart from beginning
// State
timeline.progress // Current progress (0-1)
timeline.duration // Total duration
Animation Properties
timeline.add(
{ value: 0 }, // Target object with initial values
{
value: 100, // Final value
duration: 1000, // Animation duration in ms
ease: "linear", // Easing function
delay: 0, // Delay before starting
onUpdate: (anim) => {
// Called each frame
const current = anim.targets[0].value
},
onComplete: () => {
// Called when this animation completes
},
},
0 // Start time in timeline (optional)
)
Easing Functions
Available easing functions:
Linear
| Name | Description |
|---|---|
linear |
Constant speed |
Quad (Power of 2)
| Name | Description |
|---|---|
easeInQuad |
Slow start |
easeOutQuad |
Slow end |
easeInOutQuad |
Slow start and end |
Cubic (Power of 3)
| Name | Description |
|---|---|
easeInCubic |
Slower start |
easeOutCubic |
Slower end |
easeInOutCubic |
Slower start and end |
Quart (Power of 4)
| Name | Description |
|---|---|
easeInQuart |
Even slower start |
easeOutQuart |
Even slower end |
easeInOutQuart |
Even slower start and end |
Expo (Exponential)
| Name | Description |
|---|---|
easeInExpo |
Exponential start |
easeOutExpo |
Exponential end |
easeInOutExpo |
Exponential start and end |
Back (Overshoot)
| Name | Description |
|---|---|
easeInBack |
Pull back, then forward |
easeOutBack |
Overshoot, then settle |
easeInOutBack |
Both |
Elastic
| Name | Description |
|---|---|
easeInElastic |
Elastic start |
easeOutElastic |
Elastic end (bouncy) |
easeInOutElastic |
Both |
Bounce
| Name | Description |
|---|---|
easeInBounce |
Bounce at start |
easeOutBounce |
Bounce at end |
easeInOutBounce |
Both |
Patterns
Progress Bar
function ProgressBar({ progress }: { progress: number }) {
const [width, setWidth] = useState(0)
const maxWidth = 50
const timeline = useTimeline()
useEffect(() => {
timeline.add(
{ value: width },
{
value: (progress / 100) * maxWidth,
duration: 300,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].value))
},
}
)
}, [progress])
return (
<box flexDirection="column" gap={1}>
<text>Progress: {progress}%</text>
<box width={maxWidth} height={1} backgroundColor="#333">
<box width={width} height={1} backgroundColor="#00FF00" />
</box>
</box>
)
}
Fade In
function FadeIn({ children }) {
const [opacity, setOpacity] = useState(0)
const timeline = useTimeline()
useEffect(() => {
timeline.add(
{ opacity: 0 },
{
opacity: 1,
duration: 500,
ease: "easeOutQuad",
onUpdate: (anim) => {
setOpacity(anim.targets[0].opacity)
},
}
)
}, [])
return (
<box style={{ opacity }}>
{children}
</box>
)
}
Looping Animation
function Spinner() {
const [frame, setFrame] = useState(0)
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
useEffect(() => {
const interval = setInterval(() => {
setFrame(f => (f + 1) % frames.length)
}, 80)
return () => clearInterval(interval)
}, [])
return <text>{frames[frame]} Loading...</text>
}
Staggered Animation
function StaggeredList({ items }) {
const [visibleCount, setVisibleCount] = useState(0)
useEffect(() => {
let count = 0
const interval = setInterval(() => {
count++
setVisibleCount(count)
if (count >= items.length) {
clearInterval(interval)
}
}, 100)
return () => clearInterval(interval)
}, [items.length])
return (
<box flexDirection="column">
{items.slice(0, visibleCount).map((item, i) => (
<text key={i}>{item}</text>
))}
</box>
)
}
Slide In
function SlideIn({ children, from = "left" }) {
const [offset, setOffset] = useState(from === "left" ? -20 : 20)
const timeline = useTimeline()
useEffect(() => {
timeline.add(
{ offset: from === "left" ? -20 : 20 },
{
offset: 0,
duration: 300,
ease: "easeOutCubic",
onUpdate: (anim) => {
setOffset(Math.round(anim.targets[0].offset))
},
}
)
}, [])
return (
<box position="relative" left={offset}>
{children}
</box>
)
}
Performance Tips
Batch Updates
Timeline automatically batches updates within the render loop.
Use Integer Values
Round animated values for character-based positioning:
onUpdate: (anim) => {
setX(Math.round(anim.targets[0].x))
}
Clean Up Timelines
Hooks automatically clean up, but for core:
// When done with timeline
engine.removeTimeline(timeline)
Gotchas
Terminal Refresh Rate
Terminal UIs typically refresh at 60 FPS max. Very fast animations may appear choppy.
Character Grid
Animations are constrained to character cells. Sub-pixel positioning isn't possible.
Cleanup in Effects
Always clean up intervals and timelines:
useEffect(() => {
const interval = setInterval(...)
return () => clearInterval(interval)
}, [])