iced 图形绘制实战:用 Canvas 与 Path 绘制旋转圆弧(arc 示例全解析)
【免费下载链接】icedA cross-platform GUI library for Rust, inspired by Elm项目地址: https://gitcode.com/GitHub_Trending/ic/iced
导读
本文以 iced 仓库中examples/arc示例为核心,完整解析如何基于Canvas组件实现一个实时旋转的圆弧动画:从cargo run --package arc的运行方式、Canvas组件的初始化与布局,到Path构建器中的arc_to圆弧绘制原理、Stroke描边配置,再到基于window::frames()订阅驱动的逐帧重绘与Cache缓存失效机制。读完本文,你将掌握使用 iced 的 2D 绘图框架(iced_canvas/graphics)编写自绘组件并驱动其动画刷新的完整实战路径。
示例概览与运行方式
arc是 iced 官方仓库中的一个最小可运行示例,它的全部代码集中在examples/arc/src/main.rs一个文件中。该示例的用途正如examples/arc/README.md所述:
An application that uses the
Canvaswidget to draw a rotating arc.
即:使用Canvas组件绘制一条不断旋转的圆弧。它在画布上呈现一个半径为10.0的圆点沿着圆弧运动,同时圆弧本身也随角度变化而转动,形成一条“挥舞”的弧线动画。该示例最初是 iced-rs/iced#1358 的演示程序,用于展示圆弧绘制 API 的使用方式。
运行方式非常简单,在仓库根目录执行:
cargo run --package arc对应的examples/arc/Cargo.toml中声明了示例的依赖与特性:
[package] name = "arc" version = "0.1.0" edition = "2024" publish = false [dependencies] iced.workspace = true iced.features = ["canvas", "tokio", "debug"]三个特性各有分工:
canvas:启用 2D 画布绘图能力,提供Canvas、Frame、Path、Stroke等核心类型;tokio:启用 tokio 作为异步运行时后端,配合window::frames()订阅驱动动画循环;debug:启用调试工具(如iced_debug/iced_devtools),方便开发阶段查看组件树与性能信息。
应用骨架:从入口到 Canvas 视图
main.rs的入口使用 iced 的函数式 API 构建应用:
pub fn main() -> iced::Result { iced::application(Arc::new, Arc::update, Arc::view) .subscription(Arc::subscription) .theme(Theme::Dark) .run() }iced::application接收三个函数:new构造应用状态、update处理消息、view构建界面元素;.subscription(Arc::subscription)挂载帧订阅,为动画提供时钟源;.theme(Theme::Dark)指定深色主题(后续绘图会从主题调色板取色);.run()启动事件循环,返回iced::Result。
应用状态Arc只保存两份数据:
struct Arc { start: Instant, cache: Cache, }start:记录应用启动时刻,用于计算动画的当前角度;cache:canvas::Cache,缓存已生成的几何体,避免每帧重复构建路径。
视图层把状态本身作为Canvas的程序载体:
fn view(&self) -> Element<'_, Message> { Canvas::new(self).width(Fill).height(Fill).into() }这里有两个值得注意的细节:
Canvas::new(program)的构造器在widget/src/canvas.rs中实现,其默认尺寸为Length::Fixed(100.0)(源码中const DEFAULT_SIZE: f32 = 100.0),因此这里必须通过.width(Fill).height(Fill)显式扩展到整个窗口;impl<Message> canvas::Program<Message> for Arc让Arc自身成为画布绘制程序,这是 icedCanvas的典型用法:应用状态同时扮演Program,在draw中根据内部数据绘制内容。
Canvas在布局阶段通过layout::atomic按limits与给定宽高完成尺寸计算,绘图时则会将自身 bounds 平移后交给Program::draw生成的几何图层逐层渲染(见widget/src/canvas.rs)。
驱动动画:window::frames() 订阅与每帧重绘
动画不能只画一帧静态图像,arc采用订阅驱动的重绘模型:
fn subscription(&self) -> Subscription<Message> { window::frames().map(|_| Message::Tick) }window::frames()定义于runtime/src/window.rs,其文档明确指出:
The resulting
Subscriptionwill produce items at a rate equal to the refresh rate of the first application window. Note that this rate may be variable, as it is normally managed by the graphics driver and/or the OS.
即该订阅以首个窗口的刷新率(通常与显示器垂直同步一致,且可能随驱动/系统波动)产生事件,每次触发产生一条Message::Tick。其底层实现是:
pub fn frames() -> Subscription<Instant> { event::listen_raw(|event, _status, _window| match event { crate::core::Event::Window(Event::RedrawRequested(at)) => Some(at), _ => None, }) }它监听窗口的RedrawRequested事件,把重绘请求时间点转换成订阅项。因此这套机制天然与渲染管线同步,适合做平滑动画且不丢帧。
收到Message::Tick后,update做一件关键的事——清空缓存:
fn update(&mut self, _: Message) { self.cache.clear(); }结合graphics/src/geometry/cache.rs中Cache::clear的定义(“Clears the Cache, forcing a redraw the next time it is used”),可知:每帧 Tick 使缓存失效,下一帧绘制时就会重新执行绘图闭包,从而让角度随时间推进、产生旋转动画。若不清空缓存,Cache::draw在尺寸未变时会直接返回缓存的几何体,动画将静止(参见 Cache::draw 文档)。
核心绘制:Path 构建器与 arc_to 圆弧
draw是Programtrait 的核心方法(trait 定义见widget/src/canvas/program.rs),其签名包含渲染器、主题、画布 bounds 与光标位置,返回一组Geometry。arc示例在其中完成了全部绘制逻辑:
fn draw( &self, _state: &Self::State, renderer: &Renderer, theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec<Geometry> { let geometry = self.cache.draw(renderer, bounds.size(), |frame| { // ...绘图闭包... }); vec![geometry] }闭包内的绘制分四步展开。
1. 取色与几何参数
let palette = theme.seed(); let center = frame.center(); let radius = frame.width().min(frame.height()) / 5.0; let start = Point::new(center.x, center.y - radius);theme.seed()返回当前主题的Palette(结构定义见core/src/theme/palette.rs,包含background、primary、secondary、success、warning、danger与is_dark等字段)。示例中使用palette.text作为线条与圆点的颜色,因此换主题时画面颜色会自动跟随,实现“主题自适应绘图”;frame.center()与frame.width()/height()由graphics/src/geometry/frame.rs提供,分别返回画布中心点与宽高;start为圆弧起点,位于圆心正上方radius处。
2. 计算旋转角度
let angle = (self.start.elapsed().as_millis() % 10_000) as f32 / 10_000.0 * 2.0 * PI; let end = Point::new( center.x + radius * angle.cos(), center.y + radius * angle.sin(), );- 以
start(Instant::now()记录的应用启动时刻)为基准计算已运行毫秒数,对10_000(10 秒)取模,再映射到[0, 2π),实现每 10 秒循环一周的旋转; - 终点
end通过三角函数cos/sin在圆上取点,angle即当前时刻的旋转角。
3. 绘制端点圆点
let circles = Path::new(|b| { b.circle(start, 10.0); b.move_to(end); b.circle(end, 10.0); }); frame.fill(&circles, palette.text);Path::new接收一个闭包形式的Builder。示例调用b.circle(center, radius)分别在起点与终点画出半径10.0的小圆(move_to(end)用于在终点另起一个子路径),再用frame.fill以主题文字色填充,作为圆弧两端的可视端点。Builder::circle在graphics/src/geometry/path/builder.rs中实现,其本质是构建一个start_angle = 0、end_angle = 2π的完整Arc(圆心 + 半径)。
4. 描边旋转圆弧
let path = Path::new(|b| { b.move_to(start); b.arc_to(center, end, 50.0); b.line_to(end); }); frame.stroke( &path, Stroke { style: stroke::Style::Solid(palette.text), width: 10.0, ..Stroke::default() }, );这是整个示例的核心——用arc_to绘制圆弧:
move_to(start):将当前点移动到圆弧起点(圆心正上方);arc_to(center, end, 50.0):从当前点到控制点center再到end之间,以50.0半径拟合一段圆形圆弧;line_to(end):从弧的结束点画直线到end点,封闭视觉上的“弦”效果,让弧线更像一段带角度的“弯刀”。
Builder::arc_to的语义与 HTML5 Canvas 的arcTo一致,其完整实现见graphics/src/geometry/path/builder.rs:
- 先从当前点画直线到
arc_start(切点),随后以radius绘制与两条线段相切的圆弧到arc_end; - 内部通过计算两向量夹角(
inner_angle)、切点(origin_adjacent)与扫掠方向(sweep)来定位圆弧,最终委托给lyon_path的raw.arc_to(...)完成; - 当三点共线、起点等于中点或
radius == 0.0时,arc_to会退化为直接画直线到中点(源码中的兜底分支,见builder.rs)。
由于arc_to的圆弧方向由扫掠角决定,当end点随angle在圆上移动时,圆弧会自动在“内侧凹”与“外侧凸”之间切换,形成示例中弧线围绕圆心旋转、两端圆点随之运动的视觉效果。
frame.stroke使用Stroke结构体(定义见graphics/src/geometry/stroke.rs)配置线条样式:
| 字段 | 示例取值 | 说明(默认值) |
|---|---|---|
style | stroke::Style::Solid(palette.text) | 线条颜色或渐变,默认Style::Solid(Color::BLACK) |
width | 10.0 | 描边宽度,即两条边缘之间的间距 |
line_cap | LineCap::default()(继承默认) | 开放子路径端点的形状 |
line_join | LineJoin::default()(继承默认) | 路径转角处的连接形状 |
line_dash | LineDash::default()(继承默认) | 虚线模式 |
示例中通过结构体更新语法..Stroke::default()仅覆盖style与width,其余保持默认。Stroke还提供with_color、with_width等链式便捷方法(见stroke.rs),代码中也可等价写作Stroke::default().with_color(palette.text).with_width(10.0)。
渲染管线与缓存机制
draw返回的Vec<Geometry>最终被Canvas组件逐个交给渲染器绘制。在widget/src/canvas.rs中,Canvas先按自身 bounds 平移坐标系,再调用Program::draw获取图层并依次renderer.draw_geometry(layer)输出。
关于缓存,需要强调的是Cache::draw的“惰性”语义(见graphics/src/geometry/cache.rs文档):
The closure will only be called when the size has changed since the previous draw call, or the Cache is empty or has been explicitly cleared.
即绘图闭包只在尺寸变化、缓存为空或被显式清空时执行;否则直接返回已存储的几何体。这正是arc示例必须每帧调用self.cache.clear()的原因——不清缓存,几何体就永远停留在第一帧。此外Cache还支持with_group(Group)分组(同组缓存可复用内部渲染存储,适合会一起变化的几何体,见cache.rs),以及在tiny_skia/wgpu后端间共享几何缓存的数据结构。
从示例到实践:可复用的动画绘图模式
arc示例虽小,却完整覆盖了 iced 自绘动画应用的五大环节,可作为自定义控件开发的模板:
- 状态建模:
Arc { start: Instant, cache: Cache }—— 时间基准驱动角度,缓存控制重绘; - 帧订阅:
window::frames().map(|_| Message::Tick)—— 以窗口刷新率为节拍,天然同步渲染管线; - 缓存失效:
update中self.cache.clear()—— 让每帧重新执行绘图闭包; - 几何构建:
Path::new闭包 +move_to/arc_to/line_to/circle等构建器方法(完整方法清单见graphics/src/geometry/path/builder.rs顶部),辅以frame.fill与frame.stroke渲染; - 主题适配:
theme.seed()取色,让自绘内容随Theme::Dark等主题自动调整。
若需进一步实验,可在本示例基础上尝试:将arc_to的半径50.0改为变量并随角度变化、用Stroke的line_dash做出虚线弧、或把start.elapsed()换成外部传入的时间基准来实现可控动画。相关源码与文档均位于仓库对应路径:示例入口、Canvas 组件、Path 构建器、几何缓存。
【免费下载链接】icedA cross-platform GUI library for Rust, inspired by Elm项目地址: https://gitcode.com/GitHub_Trending/ic/iced
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考