use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use skia_safe::gradient::{self, Colors, Gradient};
use skia_safe::{Canvas, Color, Color4f, PaintStyle, Point, Rect};
use rustmotion_core::css::CssStyle;
use rustmotion_core::engine::animator::AnimatedProperties;
use rustmotion_core::engine::layout_pass::BoxLayout;
use rustmotion_core::engine::renderer::paint_from_hex;
use rustmotion_core::schema::TimelineStep;
use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
fn default_base_color() -> String {
"#1E293B".to_string()
}
fn default_shimmer_color() -> String {
"#334155".to_string()
}
fn default_border_radius() -> f32 {
8.0
}
fn default_speed() -> f32 {
1.5
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum SkeletonVariant {
#[default]
Rectangle,
Circle,
Text,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Skeleton {
#[serde(default)]
pub variant: SkeletonVariant,
#[serde(default = "default_base_color")]
pub base_color: String,
#[serde(default = "default_shimmer_color")]
pub shimmer_color: String,
#[serde(default = "default_border_radius")]
pub border_radius: f32,
#[serde(default = "default_speed")]
pub speed: f32,
#[serde(default = "default_lines")]
pub lines: u32,
#[serde(default = "default_line_height")]
pub line_height: f32,
#[serde(default = "default_line_gap")]
pub line_gap: f32,
#[serde(flatten)]
pub timing: TimingConfig,
#[serde(default)]
pub style: CssStyle,
#[serde(default)]
pub timeline: Vec<TimelineStep>,
#[serde(default)]
pub stagger: Option<f32>,
}
fn default_lines() -> u32 {
3
}
fn default_line_height() -> f32 {
16.0
}
fn default_line_gap() -> f32 {
12.0
}
rustmotion_core::impl_traits!(Skeleton {
Animatable => animation,
Timed => timing,
Styled => style,
});
impl Skeleton {
fn draw_shimmer_rect(&self, canvas: &Canvas, rect: Rect, radius: f32, time: f64) {
let w = rect.width();
let mut base_paint = paint_from_hex(&self.base_color);
base_paint.set_style(PaintStyle::Fill);
base_paint.set_anti_alias(true);
let rrect = skia_safe::RRect::new_rect_xy(rect, radius, radius);
canvas.draw_rrect(rrect, &base_paint);
let cycle = (time as f32 / self.speed).fract();
let shimmer_w = w * 0.4;
let shimmer_x = rect.left + (w + shimmer_w) * cycle - shimmer_w;
let (r, g, b, _) = rustmotion_core::engine::renderer::parse_hex_color(&self.shimmer_color);
let transparent = Color::from_argb(0, r, g, b);
let highlight = Color::from_argb(180, r, g, b);
let colors4f = [
Color4f::from(transparent),
Color4f::from(highlight),
Color4f::from(transparent),
];
let stops = Colors::new(&colors4f, None, skia_safe::TileMode::Clamp, None);
let grad = Gradient::new(stops, gradient::Interpolation::default());
let shader = gradient::shaders::linear_gradient(
(
Point::new(shimmer_x, 0.0),
Point::new(shimmer_x + shimmer_w, 0.0),
),
&grad,
None,
);
if let Some(shader) = shader {
canvas.save();
canvas.clip_rrect(rrect, skia_safe::ClipOp::Intersect, true);
let mut shimmer_paint = skia_safe::Paint::default();
shimmer_paint.set_style(PaintStyle::Fill);
shimmer_paint.set_anti_alias(true);
shimmer_paint.set_shader(shader);
canvas.draw_rect(rect, &shimmer_paint);
canvas.restore();
}
}
}
impl Skeleton {
fn paint(&self, canvas: &Canvas, layout_w: f32, layout_h: f32, time: f64) {
let w = layout_w;
let h = layout_h;
match self.variant {
SkeletonVariant::Rectangle => {
let rect = Rect::from_xywh(0.0, 0.0, w, h);
self.draw_shimmer_rect(canvas, rect, self.border_radius, time);
}
SkeletonVariant::Circle => {
let diameter = w.min(h);
let rect = Rect::from_xywh(0.0, 0.0, diameter, diameter);
self.draw_shimmer_rect(canvas, rect, diameter / 2.0, time);
}
SkeletonVariant::Text => {
let total_lines = self.lines.max(1);
for i in 0..total_lines {
let y = i as f32 * (self.line_height + self.line_gap);
let line_w = if i == total_lines - 1 { w * 0.6 } else { w };
let rect = Rect::from_xywh(0.0, y, line_w, self.line_height);
self.draw_shimmer_rect(canvas, rect, self.border_radius * 0.5, time);
}
}
}
}
}
impl Painter for Skeleton {
fn paint_content(
&self,
canvas: &Canvas,
layout: &BoxLayout,
_props: &AnimatedProperties,
ctx: &PaintCtx,
) {
self.paint(canvas, layout.width, layout.height, ctx.time);
}
}