ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用 Iced Canvas 画动画:实现 Program,分清图层缓存

2026/9/3 14:46:39 拓冰建站 浏览量
用 Iced Canvas 画动画:实现 Program,分清图层缓存 用 Iced Canvas 画动画实现 Program分清图层缓存【免费下载链接】icedA cross-platform GUI library for Rust, inspired by Elm项目地址: https://gitcode.com/GitHub_Trending/ic/icedIced 是一个 Rust 跨平台 GUI 库Canvas 是它内置的二维画布你只要实现draw方法就能在声明式组件树里画自己的图形。我第一次把它放到窗口上时得到的是一个指甲盖大的灰色小方块我画的 500 半径的圆完全没显示——没有报错只是被裁掉了。原因在源码里Canvas::new的默认宽高都是100.0见 widget/src/canvas.rs而Frame会把画界外的几何体全部裁掉。这个默认值是 Canvas 的第一个坑先记住它。draw 不碰屏幕一帧的渲染链路第一件要明白的事是你写的draw闭包不直接画屏幕它只是在写图纸。一帧的流程是运行时调用Program::draw→ 你把绘制指令交给Frame→ Frame 把指令打包成Geometry→ 具体后端wgpu 或 tiny-skia负责真正光栅化并输出到窗口。这像把图纸交给照相馆怎么画和谁来冲印是两件事。分离的好处是仓库的太阳系示例可以用 pick_list 在运行时热切换 wgpu/tiny-skia 后端——你画的图纸层不变换的只是照相馆。三步跑通最小画布实现 Program 并画圆最小形态分三块一个存参数的 struct、一个Program实现、view 里的canvas(...)。核心是draw按画布尺寸建一个Frame建一条Path填色implMessage canvas::ProgramMessage for Circle { type State (); fn draw( self, _state: (), renderer: Renderer, _theme: Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) - VecGeometry { let mut frame canvas::Frame::new(renderer, bounds.size()); let circle canvas::Path::circle(frame.center(), self.radius); frame.fill(circle, Color::BLACK); vec![frame.into_geometry()] } }注意坐标系draw里 (0,0) 是画布左上角X 向右、Y 向下frame.center()直接给你中心点。跑起来把radius从 50 改成 190圆会填满画布改成 300界外部分被裁掉——记得给 canvas 加上.width(400).height(400)否则又变回那个 100x100 的小方块。要让圆动起来加一个每帧重画的订阅和一个带时间戳的timed入口fn update(mut self, message: Message, now: Instant) - TaskMessage { if matches!(message, Message::Tick) { /* 用 now 推进状态 */ } Task::none() } fn subscription(self) - SubscriptionMessage { window::frames().map(|_| Message::Tick) }window::frames()不保证固定帧率它只是发生了一次重画的信号所以下面避坑一节会讲为什么角度要按时间算。仓库里的 color_palette 示例是同一条路径的静态版本画布内容与普通组件按布局并排放置为什么 draw 返回 Vec 图层与 Cache 的分工这个 API 最反直觉的一点是draw返回图层而不是一张画每个Frame是一层返回多个就按顺序合成。这样设计是为了让静态部分缓存、动态部分重算成为可能。canvas::Cache只有在尺寸变了或显式调用clear()时才会执行闭包里的绘制否则直接返回存好的几何体graphics/src/geometry/cache.rs。太阳系示例正好把画面拆成两层let background self.space_cache.draw(renderer, bounds.size(), |frame| { frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK); frame.fill(stars, Color::WHITE); // 100 颗随机星点 }); let system self.system_cache.draw(renderer, bounds.size(), |frame| { frame.draw_image(Rectangle::with_radius(SUN_RADIUS), self.sun); // 轨道虚线、地球、月球…… }); vec![background, system]星空层只算一次轨道层在每帧的update里调用system_cache.clear()强制重画且只重画这一层。如果把星星和绕转的行星塞进同一层每帧都得重算整个星空性能省掉一半。绕转运动本身靠变换栈完成平移到中心 → 旋转 → 平移到轨道半径Frame的with_save会在闭包结束后自动恢复原坐标系不用担心污染下一帧的状态。如果……那么……四条高频踩坑路径⚠️如果忘了设置.width()和.height()那么你会得到 100x100 的画布大图被静默裁剪——这是画布上什么都没有的第一大原因。如果在draw里每帧重画密集矢量路径上百颗星点、网格线那么帧率会明显下降——把静态部分拆成canvas::Cache图层只算一次。如果把window::frames()当成固定 60fps 的定时器那么动画速度会随重画速率漂移——正确做法是太阳系示例的写法用Instant时间戳累计 elapsed 时间算角度。如果指望fill_text画出的文字垫在图形下面那么会失望——源码注释明确写着文字绘制在 Canvas 所有图层之上它只适合做覆盖层。延伸路径Canvas 组件与Programtrait 定义含完整圆示例文档widget/src/canvas.rs太阳系动画完整源码双层缓存、变换栈与后端热切换examples/solar_system/其余 Canvas 相关示例game of life、pick_list、delineate 等examples/两个后端的渲染实现wgpu/src/ 与 tiny_skia/src/下一步你可以试自己写一个弹跳小球——window::frames()订阅发 Tick用 elapsed 时间算位置用一个不缓存的Frame画出来卡住时先打印bounds.size()看看画布到底给了你多大。【免费下载链接】icedA cross-platform GUI library for Rust, inspired by Elm项目地址: https://gitcode.com/GitHub_Trending/ic/iced创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考