1use rustmotion_core::error::Result;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use skia_safe::{Canvas, Paint, PaintStyle, Point};
5
6use rustmotion_core::css::CssStyle;
7use rustmotion_core::engine::animator::AnimatedProperties;
8use rustmotion_core::engine::layout_pass::BoxLayout;
9use rustmotion_core::engine::renderer::{
10 build_shape_path, color4f_from_hex, draw_shape_path, draw_text_with_fallback, emoji_typeface,
11 measure_text_with_fallback, paint_from_hex, typeface_with_fallback, wrap_text_with_tracking,
12};
13use rustmotion_core::schema::{
14 Fill, FontWeight, GradientType, ShapeText, ShapeType, Stroke, TextAlign, TimelineStep,
15};
16use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
17
18#[derive(Debug, Serialize, Deserialize, JsonSchema)]
19pub struct Shape {
20 pub shape: ShapeType,
21 #[serde(default)]
22 pub text: Option<ShapeText>,
23 #[serde(flatten)]
24 pub timing: TimingConfig,
25 #[serde(default)]
26 pub style: CssStyle,
27 #[serde(default)]
28 pub timeline: Vec<TimelineStep>,
29 #[serde(default)]
30 pub stagger: Option<f32>,
31 #[serde(default)]
32 pub fill: Option<Fill>,
33 #[serde(default)]
34 pub stroke: Option<Stroke>,
35}
36
37rustmotion_core::impl_traits!(Shape {
38 Animatable => animation,
39 Timed => timing,
40 Styled => style,
41});
42
43impl Painter for Shape {
44 fn paint_content(
45 &self,
46 canvas: &Canvas,
47 layout: &BoxLayout,
48 props: &AnimatedProperties,
49 _ctx: &PaintCtx,
50 ) {
51 let w = layout.width;
52 let h = layout.height;
53 let corner_radius = self.style.border_radius_px();
54
55 if let Some(fill) = &self.fill {
56 let mut paint = match fill {
57 Fill::Solid(color) => paint_from_hex(color),
58 Fill::Gradient(gradient) => {
59 let colors: Vec<skia_safe::Color4f> = gradient
60 .colors
61 .iter()
62 .map(|c| color4f_from_hex(c))
63 .collect();
64 let stops: Option<Vec<f32>> = gradient
69 .stops
70 .as_ref()
71 .filter(|s| s.len() == colors.len())
72 .cloned();
73 let mut paint = Paint::default();
74 paint.set_anti_alias(true);
75
76 let shader = match gradient.gradient_type {
77 GradientType::Linear => {
78 let angle = gradient.angle.unwrap_or(0.0);
79 let rad = angle.to_radians();
80 let cx = w / 2.0;
81 let cy = h / 2.0;
82 let dx = (w / 2.0) * rad.cos();
83 let dy = (h / 2.0) * rad.sin();
84 let start = Point::new(cx - dx, cy - dy);
85 let end = Point::new(cx + dx, cy + dy);
86 let gradient_colors = skia_safe::gradient::Colors::new(
87 &colors,
88 stops.as_deref(),
89 skia_safe::TileMode::Clamp,
90 Some(skia_safe::ColorSpace::new_srgb()),
91 );
92 let g = skia_safe::gradient::Gradient::new(
93 gradient_colors,
94 skia_safe::gradient::Interpolation::default(),
95 );
96 skia_safe::gradient::shaders::linear_gradient((start, end), &g, None)
97 }
98 GradientType::Radial => {
99 let center = Point::new(w / 2.0, h / 2.0);
100 let radius = w.max(h) / 2.0;
101 let gradient_colors = skia_safe::gradient::Colors::new(
102 &colors,
103 stops.as_deref(),
104 skia_safe::TileMode::Clamp,
105 Some(skia_safe::ColorSpace::new_srgb()),
106 );
107 let g = skia_safe::gradient::Gradient::new(
108 gradient_colors,
109 skia_safe::gradient::Interpolation::default(),
110 );
111 skia_safe::gradient::shaders::radial_gradient(
112 (center, radius),
113 &g,
114 None,
115 )
116 }
117 };
118 if let Some(shader) = shader {
119 paint.set_shader(shader);
120 paint.set_dither(true);
121 }
122 paint
123 }
124 };
125 paint.set_style(PaintStyle::Fill);
126 draw_shape_path(canvas, &self.shape, 0.0, 0.0, w, h, corner_radius, &paint);
127 }
128
129 if let Some(stroke) = &self.stroke {
130 let mut paint = paint_from_hex(&stroke.color);
131 paint.set_style(PaintStyle::Stroke);
132 let stroke_w = if props.stroke_width >= 0.0 {
133 props.stroke_width
134 } else {
135 stroke.width
136 };
137 paint.set_stroke_width(stroke_w);
138
139 if props.draw_progress >= 0.0 && props.draw_progress < 1.0 {
140 if let Some(path) = build_shape_path(&self.shape, 0.0, 0.0, w, h, corner_radius) {
141 let mut measure = skia_safe::PathMeasure::new(&path, false, None);
142 let path_len = measure.length();
143 if path_len > 0.0 {
144 let draw_len = path_len * props.draw_progress.clamp(0.0, 1.0);
145 let intervals = [draw_len, path_len - draw_len + 0.01];
146 if let Some(dash) = skia_safe::PathEffect::dash(&intervals, 0.0) {
147 paint.set_path_effect(dash);
148 }
149 }
150 }
151 }
152
153 draw_shape_path(canvas, &self.shape, 0.0, 0.0, w, h, corner_radius, &paint);
154 }
155
156 if let Some(text) = &self.text {
157 let _ = render_shape_text(canvas, text, 0.0, 0.0, w, h);
158 }
159 }
160}
161
162fn render_shape_text(
163 canvas: &Canvas,
164 text: &ShapeText,
165 shape_x: f32,
166 shape_y: f32,
167 shape_w: f32,
168 shape_h: f32,
169) -> Result<()> {
170 use rustmotion_core::schema::VerticalAlign;
171
172 let pad = text.padding.unwrap_or(0.0);
173 let area_x = shape_x + pad;
174 let area_y = shape_y + pad;
175 let area_w = shape_w - 2.0 * pad;
176 let area_h = shape_h - 2.0 * pad;
177
178 let font_style = match text.font_weight {
179 FontWeight::Bold => skia_safe::FontStyle::bold(),
180 FontWeight::Normal => skia_safe::FontStyle::normal(),
181 FontWeight::Weight(w) => skia_safe::FontStyle::new(
182 skia_safe::font_style::Weight::from(w as i32),
183 skia_safe::font_style::Width::NORMAL,
184 skia_safe::font_style::Slant::Upright,
185 ),
186 };
187
188 let typeface = typeface_with_fallback(&text.font_family, font_style)?;
189
190 let font = skia_safe::Font::from_typeface(typeface, text.font_size);
191 let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, text.font_size));
192 let (_strike_width, metrics) = font.metrics();
193 let ascent = -metrics.ascent;
194 let line_height = match text.line_height {
195 Some(v) if v <= 10.0 => text.font_size * v,
196 Some(v) => v,
197 None => text.font_size * 1.3,
198 };
199 let letter_spacing = text.letter_spacing.unwrap_or(0.0);
200
201 let lines = wrap_text_with_tracking(
208 &text.content,
209 &font,
210 &emoji_font,
211 Some(area_w),
212 letter_spacing,
213 );
214 let descent = metrics.descent;
215 let total_h = if lines.len() > 1 {
216 (lines.len() - 1) as f32 * line_height + ascent + descent
217 } else {
218 ascent + descent
219 };
220
221 let y_start = match text.vertical_align {
222 VerticalAlign::Top => area_y + ascent,
223 VerticalAlign::Middle => area_y + (area_h - total_h) / 2.0 + ascent,
224 VerticalAlign::Bottom => area_y + area_h - total_h + ascent,
225 };
226
227 let mut paint = paint_from_hex(&text.color);
228 paint.set_alpha_f(1.0);
229
230 for (i, line) in lines.iter().enumerate() {
231 if line.is_empty() {
232 continue;
233 }
234
235 let line_width = measure_text_with_fallback(line, &font, &emoji_font, letter_spacing);
236
237 let x = match text.align {
238 TextAlign::Left => area_x,
239 TextAlign::Center => area_x + (area_w - line_width) / 2.0,
240 TextAlign::Right => area_x + area_w - line_width,
241 };
242 let y = y_start + i as f32 * line_height;
243 draw_text_with_fallback(
244 canvas,
245 line,
246 &font,
247 &emoji_font,
248 letter_spacing,
249 x,
250 y,
251 &paint,
252 );
253 }
254
255 Ok(())
256}