Skip to main content

rustmotion_components/
waveform.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::{Canvas, Color, Paint, PaintStyle, 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::audio_analysis::audio_analysis_cache;
9use rustmotion_core::engine::renderer::parse_hex_color;
10use rustmotion_core::schema::TimelineStep;
11use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
12
13fn default_color() -> String {
14    "#38bdf8".to_string()
15}
16fn default_window() -> f32 {
17    2.0
18}
19
20#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
21#[serde(rename_all = "snake_case")]
22pub enum DrawStyle {
23    #[default]
24    Line,
25    Filled,
26}
27
28#[derive(Debug, Serialize, Deserialize, JsonSchema)]
29pub struct Waveform {
30    /// Source audio track (src path). If None, uses the first track in the cache.
31    #[serde(default)]
32    pub track: Option<String>,
33    /// Waveform color as hex string.
34    #[serde(default = "default_color")]
35    pub color: String,
36    /// Visual style: line or filled.
37    #[serde(default)]
38    pub draw_style: DrawStyle,
39    /// Time window in seconds, centered on ctx.scenario_time.
40    #[serde(default = "default_window")]
41    pub window: f32,
42    #[serde(flatten)]
43    pub timing: TimingConfig,
44    #[serde(default)]
45    pub style: CssStyle,
46    #[serde(default)]
47    pub timeline: Vec<TimelineStep>,
48    #[serde(default)]
49    pub stagger: Option<f32>,
50}
51
52rustmotion_core::impl_traits!(Waveform {
53    Animatable => animation,
54    Timed => timing,
55    Styled => style,
56});
57
58impl Painter for Waveform {
59    fn paint_content(
60        &self,
61        canvas: &Canvas,
62        layout: &BoxLayout,
63        _props: &AnimatedProperties,
64        ctx: &PaintCtx,
65    ) {
66        let w = layout.width;
67        let h = layout.height;
68        let (r, g, b, a) = parse_hex_color(&self.color);
69        let color = Color::from_argb(a, r, g, b);
70
71        let cache = audio_analysis_cache();
72        let analysis = if let Some(ref src) = self.track {
73            cache.get(src).map(|r| r.clone())
74        } else {
75            cache.iter().next().map(|r| r.value().clone())
76        };
77
78        let Some(analysis) = analysis else {
79            let mut paint = Paint::default();
80            paint.set_color(color);
81            paint.set_style(PaintStyle::Stroke);
82            paint.set_stroke_width(1.0);
83            paint.set_anti_alias(true);
84            canvas.draw_line(
85                skia_safe::Point::new(0.0, h / 2.0),
86                skia_safe::Point::new(w, h / 2.0),
87                &paint,
88            );
89            return;
90        };
91
92        let half_window = self.window as f64 / 2.0;
93        let t_start = (ctx.scenario_time - half_window).max(0.0);
94        let t_end = ctx.scenario_time + half_window;
95
96        let n_points = w as usize;
97        let mut points: Vec<(f32, f32)> = Vec::with_capacity(n_points);
98        for i in 0..n_points {
99            let t = t_start + (i as f64 / n_points.max(1) as f64) * (t_end - t_start);
100            let amp = analysis.amplitude_at(t);
101            let x = i as f32;
102            let y = h / 2.0 - amp * (h / 2.0 - 2.0);
103            points.push((x, y));
104        }
105
106        let mut paint = Paint::default();
107        paint.set_color(color);
108        paint.set_anti_alias(true);
109
110        match self.draw_style {
111            DrawStyle::Line => {
112                paint.set_style(PaintStyle::Stroke);
113                paint.set_stroke_width(1.5);
114                if points.len() < 2 {
115                    return;
116                }
117                let mut path = PathBuilder::new();
118                path.move_to((points[0].0, points[0].1));
119                for &(x, y) in &points[1..] {
120                    path.line_to((x, y));
121                }
122                canvas.draw_path(&path.detach(), &paint);
123            }
124            DrawStyle::Filled => {
125                let (r2, g2, b2, _) = parse_hex_color(&self.color);
126                let fill_color = Color::from_argb(100, r2, g2, b2);
127                paint.set_style(PaintStyle::Fill);
128                paint.set_color(fill_color);
129                if points.is_empty() {
130                    return;
131                }
132                let mut fill_path = PathBuilder::new();
133                fill_path.move_to((0.0, h / 2.0));
134                for &(x, y) in &points {
135                    fill_path.line_to((x, y));
136                }
137                fill_path.line_to((w, h / 2.0));
138                fill_path.close();
139                canvas.draw_path(&fill_path.detach(), &paint);
140
141                paint.set_style(PaintStyle::Stroke);
142                paint.set_stroke_width(1.5);
143                paint.set_color(color);
144                if points.len() >= 2 {
145                    let mut outline = PathBuilder::new();
146                    outline.move_to((points[0].0, points[0].1));
147                    for &(x, y) in &points[1..] {
148                        outline.line_to((x, y));
149                    }
150                    canvas.draw_path(&outline.detach(), &paint);
151                }
152            }
153        }
154    }
155}