rustmotion_components/
waveform.rs1use 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 #[serde(default)]
32 pub track: Option<String>,
33 #[serde(default = "default_color")]
35 pub color: String,
36 #[serde(default)]
38 pub draw_style: DrawStyle,
39 #[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();
81 paint.set_color(color);
82 paint.set_style(PaintStyle::Stroke);
83 paint.set_stroke_width(1.0);
84 paint.set_anti_alias(true);
85 canvas.draw_line(
86 skia_safe::Point::new(0.0, h / 2.0),
87 skia_safe::Point::new(w, h / 2.0),
88 &paint,
89 );
90 return;
91 };
92
93 let half_window = self.window as f64 / 2.0;
94 let t_start = (ctx.scenario_time - half_window).max(0.0);
95 let t_end = ctx.scenario_time + half_window;
96
97 let n_points = w as usize;
99 let mut points: Vec<(f32, f32)> = Vec::with_capacity(n_points);
100 for i in 0..n_points {
101 let t = t_start + (i as f64 / n_points.max(1) as f64) * (t_end - t_start);
102 let amp = analysis.amplitude_at(t);
103 let x = i as f32;
104 let y = h / 2.0 - amp * (h / 2.0 - 2.0);
105 points.push((x, y));
106 }
107
108 let mut paint = Paint::default();
109 paint.set_color(color);
110 paint.set_anti_alias(true);
111
112 match self.draw_style {
113 DrawStyle::Line => {
114 paint.set_style(PaintStyle::Stroke);
115 paint.set_stroke_width(1.5);
116 if points.len() < 2 {
117 return;
118 }
119 let mut path = PathBuilder::new();
120 path.move_to((points[0].0, points[0].1));
121 for &(x, y) in &points[1..] {
122 path.line_to((x, y));
123 }
124 canvas.draw_path(&path.detach(), &paint);
125 }
126 DrawStyle::Filled => {
127 let (r2, g2, b2, _) = parse_hex_color(&self.color);
129 let fill_color = Color::from_argb(100, r2, g2, b2);
130 paint.set_style(PaintStyle::Fill);
131 paint.set_color(fill_color);
132 if points.is_empty() {
133 return;
134 }
135 let mut fill_path = PathBuilder::new();
136 fill_path.move_to((0.0, h / 2.0));
137 for &(x, y) in &points {
138 fill_path.line_to((x, y));
139 }
140 fill_path.line_to((w, h / 2.0));
141 fill_path.close();
142 canvas.draw_path(&fill_path.detach(), &paint);
143
144 paint.set_style(PaintStyle::Stroke);
146 paint.set_stroke_width(1.5);
147 paint.set_color(color);
148 if points.len() >= 2 {
149 let mut outline = PathBuilder::new();
150 outline.move_to((points[0].0, points[0].1));
151 for &(x, y) in &points[1..] {
152 outline.line_to((x, y));
153 }
154 canvas.draw_path(&outline.detach(), &paint);
155 }
156 }
157 }
158 }
159}