Cline 终端 UI 动画系统:OpenTUI Timeline、缓动函数与实战模式详解
Cline 的 CLI 终端界面基于 OpenTUI 构建,而 OpenTUI 提供了一套基于时间线(Timeline)的动画系统,用于在字符网格的终端环境中实现平滑的属性过渡。本篇围绕仓库中 OpenTUI 技能的动画参考文档 animation/REFERENCE.md 展开,完整覆盖其三大架构组件、React/Solid/Core 三种框架的用法、Timeline 全部配置项与缓动函数表,并对照 Cline CLI 的实际源码说明这套系统如何落地到真实终端界面。读完后你可以掌握:如何在 TUI 中编排多段动画、选择合适的缓动曲线、实现进度条/淡入/滑入等常用动效,并规避终端动画特有的性能与渲染陷阱。
动画系统的三大组成部分
OpenTUI 的动画由三个层次协作完成:
- Timeline(时间线):编排多个动画的播放顺序与起止时间,是动画的"总调度";
- Animation Engine(动画引擎):管理所有时间线并驱动渲染循环,在 core 层中通过全局
engine单例与 renderer 绑定; - Easing Functions(缓动函数):控制动画曲线,决定属性值随时间变化的速率形态(加速、减速、回弹、抖动等)。
适用场景:当你需要时间线驱动的动画、缓动曲线或渐进式属性过渡时,使用本文参考。
基本用法:三种框架入口
OpenTUI 支持三种编程风格,动画 API 各自有对应入口,但核心语义一致:useTimeline(React/Solid Hook)或 Timeline 类(Core 命令式)。
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(命令式)
Core 层不使用 Hook,而是直接实例化 Timeline 类,并通过全局 engine 将其挂接到渲染器上:
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)
这一调用链在 Cline 仓库中有真实印证:CLI 的 TUI 入口 index.tsx 正是先 createCliRenderer(...) 再用 createRoot(renderer) 挂载 React 树,其中 createCliRenderer 来自 @opentui/core、createRoot 来自 @opentui/react,与参考文档描述的 React 框架分层完全一致。
Timeline 配置项(Options)
useTimeline / new Timeline 接受如下选项:
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
})
各选项含义(结合 React API 参考 中的 Hook 文档):
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
duration |
number |
- | 时间线总时长(毫秒) |
loop |
boolean |
false |
是否循环播放 |
autoplay |
boolean |
true |
是否自动开始播放 |
onComplete |
() => void |
- | 时间线播放完成时的回调 |
onPause |
() => void |
- | 时间线暂停时的回调 |
Timeline 方法与动画属性
播放控制与状态
// 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
添加动画的完整参数
timeline.add(target, properties, startTime?) 的三段式签名如下:
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)
)
关键点说明:
- 第一段参数是携带初始值的目标对象,第二段是目标值及该段动画的
duration/ease/delay与回调; - 第三段
startTime?可选,指定该动画在时间线上的起始偏移,用于编排多段动画的先后顺序; onUpdate回调中的anim.targets[0]即为当前帧插值后的目标对象,这是每帧把插值结果写回 UI 状态的唯一入口;- Core 层对应参考见 Core API,其中的
Animation Timeline小节给出了engine.attach(renderer)+engine.addTimeline(timeline)的等价写法;Solid 的 Hook 参考见 Solid API。
完整缓动函数(Easing)参考
OpenTUI 内置八组缓动函数,覆盖幂次、指数、回弹、弹性、反弹五类曲线形态:
Linear(线性)
| 名称 | 说明 |
|---|---|
linear |
匀速(Constant speed) |
Quad(二次幂,Power of 2)
| 名称 | 说明 |
|---|---|
easeInQuad |
慢启动 |
easeOutQuad |
慢结束 |
easeInOutQuad |
首尾都慢 |
Cubic(三次幂,Power of 3)
| 名称 | 说明 |
|---|---|
easeInCubic |
更慢的启动 |
easeOutCubic |
更慢的结束 |
easeInOutCubic |
首尾都更慢 |
Quart(四次幂,Power of 4)
| 名称 | 说明 |
|---|---|
easeInQuart |
启动更平缓 |
easeOutQuart |
结束更平缓 |
easeInOutQuart |
首尾都更平缓 |
Expo(指数)
| 名称 | 说明 |
|---|---|
easeInExpo |
指数式启动 |
easeOutExpo |
指数式结束 |
easeInOutExpo |
指数式首尾 |
Back(过冲回弹)
| 名称 | 说明 |
|---|---|
easeInBack |
先回拉再向前 |
easeOutBack |
冲过头再回稳 |
easeInOutBack |
两者兼具 |
Elastic(弹性)
| 名称 | 说明 |
|---|---|
easeInElastic |
弹性启动 |
easeOutElastic |
弹性结束(带回弹) |
easeInOutElastic |
两者兼具 |
Bounce(弹跳)
| 名称 | 说明 |
|---|---|
easeInBounce |
起始段弹跳 |
easeOutBounce |
结束段弹跳 |
easeInOutBounce |
两者兼具 |
实战模式(Patterns)
参考文档给出了五个可直接复用的模式。以下完整保留并补充注释。
进度条(Progress Bar)
跟随外部 progress 变化,以 300ms 的 easeOutQuad 过渡到目标宽度,实现平滑追赶效果:
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>
)
}
注意这里以当前宽度为起点、以依赖项 [progress] 触发重播,因此进度条在多次更新间是连续过渡而非跳变。类似的简单写法也收录在 React 模式参考 的 "Animation Patterns" 小节(0→100、ease: "linear"、duration: 3000 的单段进度动画)。
淡入(Fade In)
用 500ms 的 easeOutQuad 把 opacity 从 0 过渡到 1,包裹任意子内容:
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>
)
}
与进度条不同,这里直接使用浮点 opacity(终端渲染层支持透明度混合),无需取整。
循环动画(Spinner)
Braille 帧序列配合 80ms 定时器轮转,是加载指示器的经典实现:
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)
按 100ms 间隔逐条揭示列表项,形成"依次进场"的视觉节奏:
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)
用 300ms 的 easeOutCubic 把偏移量从 ±20 归零,支持从左或右滑入:
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>
)
}
性能建议
批量更新
Timeline 会在渲染循环内自动对更新进行批处理(batch),开发者无需手动节流 onUpdate 中的状态写入。
使用整数值
终端定位以字符格为单位,动画数值应取整后再写入布局属性:
onUpdate: (anim) => {
setX(Math.round(anim.targets[0].x))
}
清理时间线
React/Solid 的 Hook 会自动清理;Core 命令式用法则需要在不再使用时主动移除:
// When done with timeline
engine.removeTimeline(timeline)
常见陷阱(Gotchas)
终端刷新率
终端 UI 通常上限为 60 FPS 刷新,过快完成的动画在视觉上可能显得卡顿。选择 duration 时应以"百毫秒级"为基本尺度,参考文档中所有示例的时长都在 300ms~2000ms 区间。
字符网格限制
动画被约束在字符单元格的网格上,不存在亚像素(sub-pixel)定位能力。这解释了为何位移类动画普遍 Math.round 后再赋值。
Effect 中的清理
所有 setInterval 与时间线资源都应在清理函数中释放:
useEffect(() => {
const interval = setInterval(...)
return () => clearInterval(interval)
}, [])
在 Cline CLI 中的真实应用
Cline 仓库的终端界面(apps/cli/src/tui/)是这套 OpenTUI 栈的直接消费方,从源码结构可以看出动画实践的两个侧面:
- 框架组合:index.tsx 中
createCliRenderer({ exitOnCtrlC: false, autoFocus: false, enableMouseMovement: true })(来自@opentui/core)+createRoot(renderer)(来自@opentui/react)的组合,正是参考文档 Core/React 两种用法的工程化落地;auth.ts 中的 onboarding 渲染也复用同一createCliRenderer路径。 - 帧动画的自实现缓动:robot-animation.tsx 实现了一个跟随鼠标光标转向的机器人 ASCII 动画。它没有走
useTimeline,而是用setInterval以 12ms 为周期做帧插值:每一帧以Math.sign(diff) * Math.max(Math.abs(Math.round(diff * 0.5)), 1)计算步进,即"每帧向目标帧推进剩余差距的一半"——这实际上是一个手写的指数缓动(近似easeOutExpo的逐帧衰减),且用Math.round将浮点帧索引钳位到整数帧,完全符合前文"字符网格 + 取整"的性能准则。组件卸载/目标变化时通过return () => clearInterval(interval)清理定时器,也与 Gotchas 一节的清理要求一致。
可以推断:对于这类"离散帧序列"的动画(机器人转向共 160 帧左右),逐帧半距逼近比 Timeline 的连续属性插值更自然;而对于进度条、透明度、偏移量这类连续属性,Timeline + 缓动函数则是首选方案。
小结与延伸阅读
OpenTUI 的动画体系以 Timeline 编排、以 Engine 驱动、以 Easing 塑形,三层职责清晰:timeline.add(target, properties, startTime?) 负责声明"从哪到哪、多久、什么曲线",onUpdate 负责把每帧插值写回 UI,engine 负责把它们接入渲染循环。缓动函数八组二十四种(Linear/Quad/Cubic/Quart/Expo/Back/Elastic/Bounce)覆盖了绝大多数终端动效需求;实践层面则记住三条纪律——数值取整、时长按百毫秒设计、资源随 Effect 清理。
深入阅读可参考仓库内同目录的配套文档:
- React API ——
useTimelineHook 完整参考 - Solid API ——
useTimelineHook 参考(Solid 版) - Core API ——
AnimationEngine与Timeline命令式参考 - Layout Patterns —— 动画定位与过渡的布局配合
- OpenTUI 技能入口 —— 组件、布局、键盘、测试等全部参考的导航
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00