rustmotion_components/
avatar.rs1use rustmotion_core::css::CssStyle;
2use rustmotion_core::error::Result;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use skia_safe::{Canvas, Paint, PaintStyle, RRect, Rect};
6
7use rustmotion_core::engine::animator::AnimatedProperties;
8use rustmotion_core::engine::layout_pass::BoxLayout;
9use rustmotion_core::engine::renderer::{asset_cache, paint_from_hex};
10use rustmotion_core::error::RustmotionError;
11use rustmotion_core::schema::TimelineStep;
12use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
13
14#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
15#[serde(rename_all = "snake_case")]
16#[derive(Default)]
17pub enum AvatarStatus {
18 Online,
19 Offline,
20 Away,
21 #[serde(rename = "none")]
22 #[default]
23 NoStatus,
24}
25
26impl AvatarStatus {
27 fn default_color(&self) -> &str {
28 match self {
29 AvatarStatus::Online => "#22C55E",
30 AvatarStatus::Offline => "#9CA3AF",
31 AvatarStatus::Away => "#F59E0B",
32 AvatarStatus::NoStatus => "",
33 }
34 }
35}
36
37#[derive(Debug, Serialize, Deserialize, JsonSchema)]
38pub struct Avatar {
39 pub src: String,
40 #[serde(default = "default_avatar_size")]
41 pub size: f32,
42 #[serde(default)]
43 pub border_color: Option<String>,
44 #[serde(default)]
45 pub border_width: Option<f32>,
46 #[serde(default)]
47 pub status: AvatarStatus,
48 #[serde(default)]
49 pub status_color: Option<String>,
50 #[serde(flatten)]
51 pub timing: TimingConfig,
52 #[serde(default)]
53 pub style: CssStyle,
54 #[serde(default)]
55 pub timeline: Vec<TimelineStep>,
56 #[serde(default)]
57 pub stagger: Option<f32>,
58}
59
60fn default_avatar_size() -> f32 {
61 64.0
62}
63
64rustmotion_core::impl_traits!(Avatar {
65 Animatable => animation,
66 Timed => timing,
67 Styled => style,
68});
69
70impl Avatar {
71 fn paint(&self, canvas: &Canvas, layout_w: f32, layout_h: f32) -> Result<()> {
72 let w = layout_w;
73 let h = layout_h;
74
75 let cache = asset_cache();
77 let img = if let Some(cached) = cache.get(&self.src) {
78 cached.clone()
79 } else {
80 let data = std::fs::read(&self.src).map_err(|e| RustmotionError::ImageLoad {
81 path: self.src.clone(),
82 reason: e.to_string(),
83 })?;
84 let skia_data = skia_safe::Data::new_copy(&data);
85 let decoded = skia_safe::Image::from_encoded(skia_data).ok_or_else(|| {
86 RustmotionError::ImageDecode {
87 path: self.src.clone(),
88 }
89 })?;
90 cache.insert(self.src.clone(), decoded.clone());
91 decoded
92 };
93
94 let oval_rect = Rect::from_xywh(0.0, 0.0, w, h);
96 let oval_rrect = RRect::new_oval(oval_rect);
97
98 canvas.save();
99 canvas.clip_rrect(oval_rrect, skia_safe::ClipOp::Intersect, true);
100
101 let img_w = img.width() as f32;
102 let img_h = img.height() as f32;
103 let scale = (w / img_w).max(h / img_h);
104 let draw_w = img_w * scale;
105 let draw_h = img_h * scale;
106 let offset_x = (w - draw_w) / 2.0;
107 let offset_y = (h - draw_h) / 2.0;
108
109 let dst = Rect::from_xywh(offset_x, offset_y, draw_w, draw_h);
110 canvas.draw_image_rect(img, None, dst, &Paint::default());
111
112 canvas.restore();
113
114 let border_width = self.border_width.unwrap_or(0.0);
116 if border_width > 0.0 {
117 if let Some(border_color) = &self.border_color {
118 let mut border_paint = paint_from_hex(border_color);
119 border_paint.set_style(PaintStyle::Stroke);
120 border_paint.set_stroke_width(border_width);
121 border_paint.set_anti_alias(true);
122
123 let inset = border_width / 2.0;
124 let border_rect = Rect::from_xywh(inset, inset, w - border_width, h - border_width);
125 canvas.draw_oval(border_rect, &border_paint);
126 }
127 }
128
129 if !matches!(self.status, AvatarStatus::NoStatus) {
131 let dot_radius = w * 0.15;
132 let dot_color = self
133 .status_color
134 .as_deref()
135 .unwrap_or_else(|| self.status.default_color());
136
137 let mut dot_paint = paint_from_hex(dot_color);
138 dot_paint.set_style(PaintStyle::Fill);
139 dot_paint.set_anti_alias(true);
140
141 let cx = w - dot_radius;
142 let cy = h - dot_radius;
143 canvas.draw_circle((cx, cy), dot_radius, &dot_paint);
144
145 let mut border_paint = paint_from_hex("#FFFFFF");
147 border_paint.set_style(PaintStyle::Stroke);
148 border_paint.set_stroke_width(2.0);
149 border_paint.set_anti_alias(true);
150 canvas.draw_circle((cx, cy), dot_radius, &border_paint);
151 }
152
153 Ok(())
154 }
155}
156
157impl Painter for Avatar {
158 fn paint_content(
159 &self,
160 canvas: &Canvas,
161 layout: &BoxLayout,
162 _props: &AnimatedProperties,
163 _ctx: &PaintCtx,
164 ) {
165 let _ = self.paint(canvas, layout.width, layout.height);
166 }
167}