rustmotion_components/
rating.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::{Canvas, PaintStyle, Path, PathBuilder};
4
5use rustmotion_core::css::CssStyle;
6use rustmotion_core::engine::animator::AnimatedProperties;
7use rustmotion_core::engine::layout_pass::BoxLayout;
8use rustmotion_core::engine::renderer::paint_from_hex;
9use rustmotion_core::schema::TimelineStep;
10use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
11
12fn default_max() -> u32 {
13 5
14}
15fn default_star_size() -> f32 {
16 32.0
17}
18fn default_gap() -> f32 {
19 4.0
20}
21fn default_filled_color() -> String {
22 "#F59E0B".to_string()
23}
24fn default_empty_color() -> String {
25 "#374151".to_string()
26}
27fn default_animated() -> bool {
28 true
29}
30fn default_animation_duration() -> f64 {
31 1.0
32}
33
34#[derive(Debug, Serialize, Deserialize, JsonSchema)]
35pub struct Rating {
36 #[serde(default)]
37 pub value: f64,
38 #[serde(default = "default_max")]
39 pub max: u32,
40 #[serde(default = "default_star_size")]
41 pub size: f32,
42 #[serde(default = "default_gap")]
43 pub gap: f32,
44 #[serde(default = "default_filled_color")]
45 pub filled_color: String,
46 #[serde(default = "default_empty_color")]
47 pub empty_color: String,
48 #[serde(default = "default_animated")]
49 pub animated: bool,
50 #[serde(default = "default_animation_duration")]
51 pub animation_duration: f64,
52 #[serde(flatten)]
53 pub timing: TimingConfig,
54 #[serde(default)]
55 pub style: CssStyle,
56 #[serde(default)]
57 pub timeline: Vec<TimelineStep>,
58 #[serde(default)]
59 pub stagger: Option<f32>,
60}
61
62rustmotion_core::impl_traits!(Rating {
63 Animatable => animation,
64 Timed => timing,
65 Styled => style,
66});
67
68impl Rating {
69 fn ease_out_cubic(t: f64) -> f64 {
70 1.0 - (1.0 - t).powi(3)
71 }
72
73 fn displayed_value_at(&self, time: f64) -> f64 {
74 if !self.animated {
75 return self.value;
76 }
77 let progress = (time / self.animation_duration).clamp(0.0, 1.0);
78 let eased = Self::ease_out_cubic(progress);
79 self.value * eased
80 }
81
82 fn star_path(cx: f32, cy: f32, outer_radius: f32) -> Path {
83 let inner_radius = outer_radius * 0.4;
84 let mut path = PathBuilder::new();
85
86 for i in 0..10 {
87 let angle = -std::f32::consts::FRAC_PI_2 + i as f32 * std::f32::consts::PI / 5.0;
88 let r = if i % 2 == 0 {
89 outer_radius
90 } else {
91 inner_radius
92 };
93 let x = cx + r * angle.cos();
94 let y = cy + r * angle.sin();
95
96 if i == 0 {
97 path.move_to((x, y));
98 } else {
99 path.line_to((x, y));
100 }
101 }
102 path.close();
103 path.detach()
104 }
105}
106
107impl Rating {
108 fn paint(&self, canvas: &Canvas, time: f64) {
109 let displayed = self.displayed_value_at(time);
110 let outer_radius = self.size / 2.0;
111
112 for i in 0..self.max {
113 let cx = outer_radius + i as f32 * (self.size + self.gap);
114 let cy = outer_radius;
115 let star_fill = displayed - i as f64;
116
117 if star_fill >= 1.0 {
118 let path = Self::star_path(cx, cy, outer_radius);
119 let mut paint = paint_from_hex(&self.filled_color);
120 paint.set_style(PaintStyle::Fill);
121 paint.set_anti_alias(true);
122 canvas.draw_path(&path, &paint);
123 } else if star_fill > 0.0 {
124 let path = Self::star_path(cx, cy, outer_radius);
125 let mut empty_paint = paint_from_hex(&self.empty_color);
126 empty_paint.set_style(PaintStyle::Fill);
127 empty_paint.set_anti_alias(true);
128 canvas.draw_path(&path, &empty_paint);
129
130 let fill_fraction = star_fill as f32;
131 let clip_x = cx - outer_radius;
132 let clip_w = self.size * fill_fraction;
133 let clip_rect = skia_safe::Rect::from_xywh(clip_x, 0.0, clip_w, self.size);
134
135 canvas.save();
136 canvas.clip_rect(clip_rect, skia_safe::ClipOp::Intersect, true);
137 let path = Self::star_path(cx, cy, outer_radius);
138 let mut fill_paint = paint_from_hex(&self.filled_color);
139 fill_paint.set_style(PaintStyle::Fill);
140 fill_paint.set_anti_alias(true);
141 canvas.draw_path(&path, &fill_paint);
142 canvas.restore();
143 } else {
144 let path = Self::star_path(cx, cy, outer_radius);
145 let mut paint = paint_from_hex(&self.empty_color);
146 paint.set_style(PaintStyle::Fill);
147 paint.set_anti_alias(true);
148 canvas.draw_path(&path, &paint);
149 }
150 }
151 }
152}
153
154impl Painter for Rating {
155 fn paint_content(
156 &self,
157 canvas: &Canvas,
158 _layout: &BoxLayout,
159 _props: &AnimatedProperties,
160 ctx: &PaintCtx,
161 ) {
162 self.paint(canvas, ctx.time);
163 }
164}