rustmotion_components/
image.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::{Canvas, Paint, Rect};
4
5use rustmotion_core::css::CssStyle;
6use rustmotion_core::engine::animator::AnimatedProperties;
7use rustmotion_core::engine::layout_pass::BoxLayout;
8use rustmotion_core::engine::renderer::asset_cache;
9use rustmotion_core::schema::{ImageFit, TimelineStep};
10use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
11
12#[derive(Debug, Serialize, Deserialize, JsonSchema)]
13pub struct Image {
14 pub src: String,
15 #[serde(default)]
16 pub fit: ImageFit,
17 #[serde(flatten)]
18 pub timing: TimingConfig,
19 #[serde(default)]
20 pub style: CssStyle,
21 #[serde(default)]
22 pub timeline: Vec<TimelineStep>,
23 #[serde(default)]
24 pub stagger: Option<f32>,
25}
26
27rustmotion_core::impl_traits!(Image {
28 Animatable => animation,
29 Timed => timing,
30 Styled => style,
31});
32
33impl Painter for Image {
34 fn paint_content(
35 &self,
36 canvas: &Canvas,
37 layout: &BoxLayout,
38 _props: &AnimatedProperties,
39 _ctx: &PaintCtx,
40 ) {
41 let cache = asset_cache();
42 let img = if let Some(cached) = cache.get(&self.src) {
43 cached.clone()
44 } else {
45 let Ok(data) = std::fs::read(&self.src) else {
46 return;
47 };
48 let skia_data = skia_safe::Data::new_copy(&data);
49 let Some(decoded) = skia_safe::Image::from_encoded(skia_data) else {
50 return;
51 };
52 cache.insert(self.src.clone(), decoded.clone());
53 decoded
54 };
55
56 let img_w = img.width() as f32;
57 let img_h = img.height() as f32;
58 let target_w = layout.width;
59 let target_h = layout.height;
60
61 let (draw_w, draw_h, offset_x, offset_y) = match self.fit {
62 ImageFit::Fill => (target_w, target_h, 0.0, 0.0),
63 ImageFit::Contain => {
64 let scale = (target_w / img_w).min(target_h / img_h);
65 let w = img_w * scale;
66 let h = img_h * scale;
67 (w, h, (target_w - w) / 2.0, (target_h - h) / 2.0)
68 }
69 ImageFit::Cover => {
70 let scale = (target_w / img_w).max(target_h / img_h);
71 let w = img_w * scale;
72 let h = img_h * scale;
73 (w, h, (target_w - w) / 2.0, (target_h - h) / 2.0)
74 }
75 };
76
77 let dst = Rect::from_xywh(offset_x, offset_y, draw_w, draw_h);
78 let paint = Paint::default();
79
80 if matches!(self.fit, ImageFit::Cover) {
81 canvas.save();
82 canvas.clip_rect(
83 Rect::from_xywh(0.0, 0.0, target_w, target_h),
84 skia_safe::ClipOp::Intersect,
85 true,
86 );
87 canvas.draw_image_rect(img, None, dst, &paint);
88 canvas.restore();
89 } else {
90 canvas.draw_image_rect(img, None, dst, &paint);
91 }
92 }
93}