1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::gradient::{self, Colors, Gradient};
4use skia_safe::{Canvas, Color, Color4f, PaintStyle, Point, Rect};
5
6use rustmotion_core::css::CssStyle;
7use rustmotion_core::engine::animator::AnimatedProperties;
8use rustmotion_core::engine::layout_pass::BoxLayout;
9use rustmotion_core::engine::renderer::paint_from_hex;
10use rustmotion_core::schema::TimelineStep;
11use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
12
13fn default_base_color() -> String {
14 "#1E293B".to_string()
15}
16
17fn default_shimmer_color() -> String {
18 "#334155".to_string()
19}
20
21fn default_border_radius() -> f32 {
22 8.0
23}
24
25fn default_speed() -> f32 {
26 1.5
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
30#[serde(rename_all = "snake_case")]
31#[derive(Default)]
32pub enum SkeletonVariant {
33 #[default]
34 Rectangle,
35 Circle,
36 Text,
37}
38
39#[derive(Debug, Serialize, Deserialize, JsonSchema)]
40pub struct Skeleton {
41 #[serde(default)]
42 pub variant: SkeletonVariant,
43 #[serde(default = "default_base_color")]
44 pub base_color: String,
45 #[serde(default = "default_shimmer_color")]
46 pub shimmer_color: String,
47 #[serde(default = "default_border_radius")]
48 pub border_radius: f32,
49 #[serde(default = "default_speed")]
50 pub speed: f32,
51 #[serde(default = "default_lines")]
53 pub lines: u32,
54 #[serde(default = "default_line_height")]
56 pub line_height: f32,
57 #[serde(default = "default_line_gap")]
59 pub line_gap: f32,
60 #[serde(flatten)]
61 pub timing: TimingConfig,
62 #[serde(default)]
63 pub style: CssStyle,
64 #[serde(default)]
65 pub timeline: Vec<TimelineStep>,
66 #[serde(default)]
67 pub stagger: Option<f32>,
68}
69
70fn default_lines() -> u32 {
71 3
72}
73
74fn default_line_height() -> f32 {
75 16.0
76}
77
78fn default_line_gap() -> f32 {
79 12.0
80}
81
82rustmotion_core::impl_traits!(Skeleton {
83 Animatable => animation,
84 Timed => timing,
85 Styled => style,
86});
87
88impl Skeleton {
89 fn draw_shimmer_rect(&self, canvas: &Canvas, rect: Rect, radius: f32, time: f64) {
90 let w = rect.width();
91
92 let mut base_paint = paint_from_hex(&self.base_color);
94 base_paint.set_style(PaintStyle::Fill);
95 base_paint.set_anti_alias(true);
96 let rrect = skia_safe::RRect::new_rect_xy(rect, radius, radius);
97 canvas.draw_rrect(rrect, &base_paint);
98
99 let cycle = (time as f32 / self.speed).fract();
101 let shimmer_w = w * 0.4;
103 let shimmer_x = rect.left + (w + shimmer_w) * cycle - shimmer_w;
104
105 let (r, g, b, _) = rustmotion_core::engine::renderer::parse_hex_color(&self.shimmer_color);
106 let transparent = Color::from_argb(0, r, g, b);
107 let highlight = Color::from_argb(180, r, g, b);
108
109 let colors4f = [
110 Color4f::from(transparent),
111 Color4f::from(highlight),
112 Color4f::from(transparent),
113 ];
114 let stops = Colors::new(&colors4f, None, skia_safe::TileMode::Clamp, None);
115 let grad = Gradient::new(stops, gradient::Interpolation::default());
116 let shader = gradient::shaders::linear_gradient(
117 (
118 Point::new(shimmer_x, 0.0),
119 Point::new(shimmer_x + shimmer_w, 0.0),
120 ),
121 &grad,
122 None,
123 );
124
125 if let Some(shader) = shader {
126 canvas.save();
127 canvas.clip_rrect(rrect, skia_safe::ClipOp::Intersect, true);
128 let mut shimmer_paint = skia_safe::Paint::default();
129 shimmer_paint.set_style(PaintStyle::Fill);
130 shimmer_paint.set_anti_alias(true);
131 shimmer_paint.set_shader(shader);
132 canvas.draw_rect(rect, &shimmer_paint);
133 canvas.restore();
134 }
135 }
136}
137
138impl Skeleton {
139 fn paint(&self, canvas: &Canvas, layout_w: f32, layout_h: f32, time: f64) {
140 let w = layout_w;
141 let h = layout_h;
142
143 match self.variant {
144 SkeletonVariant::Rectangle => {
145 let rect = Rect::from_xywh(0.0, 0.0, w, h);
146 self.draw_shimmer_rect(canvas, rect, self.border_radius, time);
147 }
148 SkeletonVariant::Circle => {
149 let diameter = w.min(h);
150 let rect = Rect::from_xywh(0.0, 0.0, diameter, diameter);
151 self.draw_shimmer_rect(canvas, rect, diameter / 2.0, time);
152 }
153 SkeletonVariant::Text => {
154 let total_lines = self.lines.max(1);
155 for i in 0..total_lines {
156 let y = i as f32 * (self.line_height + self.line_gap);
157 let line_w = if i == total_lines - 1 { w * 0.6 } else { w };
158 let rect = Rect::from_xywh(0.0, y, line_w, self.line_height);
159 self.draw_shimmer_rect(canvas, rect, self.border_radius * 0.5, time);
160 }
161 }
162 }
163 }
164}
165
166impl Painter for Skeleton {
167 fn paint_content(
168 &self,
169 canvas: &Canvas,
170 layout: &BoxLayout,
171 _props: &AnimatedProperties,
172 ctx: &PaintCtx,
173 ) {
174 self.paint(canvas, layout.width, layout.height, ctx.time);
175 }
176}