rustmotion_components/
audio_spectrum.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::{Canvas, Color, Paint, PaintStyle, Point, 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::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_bars() -> u32 {
14 16
15}
16fn default_color() -> String {
17 "#38bdf8".to_string()
18}
19fn default_bar_gap() -> f32 {
20 2.0
21}
22fn default_min_height() -> f32 {
23 2.0
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
27#[serde(rename_all = "snake_case")]
28pub enum SpectrumMode {
29 #[default]
30 Bars,
31 Radial,
32}
33
34#[derive(Debug, Serialize, Deserialize, JsonSchema)]
35pub struct AudioSpectrum {
36 #[serde(default)]
38 pub track: Option<String>,
39 #[serde(default = "default_bars")]
41 pub bars: u32,
42 #[serde(default)]
44 pub mode: SpectrumMode,
45 #[serde(default = "default_color")]
47 pub color: String,
48 #[serde(default = "default_bar_gap")]
50 pub bar_gap: f32,
51 #[serde(default = "default_min_height")]
53 pub min_height: f32,
54 #[serde(flatten)]
55 pub timing: TimingConfig,
56 #[serde(default)]
57 pub style: CssStyle,
58 #[serde(default)]
59 pub timeline: Vec<TimelineStep>,
60 #[serde(default)]
61 pub stagger: Option<f32>,
62}
63
64rustmotion_core::impl_traits!(AudioSpectrum {
65 Animatable => animation,
66 Timed => timing,
67 Styled => style,
68});
69
70impl AudioSpectrum {
71 fn get_band_values(&self, time: f64) -> Vec<f32> {
72 let cache = audio_analysis_cache();
73 let analysis = if let Some(ref src) = self.track {
74 cache.get(src).map(|r| r.clone())
75 } else {
76 cache.iter().next().map(|r| r.value().clone())
77 };
78
79 let n = self.bars.max(1) as usize;
80
81 let Some(analysis) = analysis else {
84 return vec![0.0; n];
85 };
86
87 let num_bands = 16usize;
89 (0..n)
90 .map(|i| {
91 let band_f = i as f32 * (num_bands as f32 - 1.0) / (n as f32 - 1.0).max(1.0);
92 let band_lo = band_f as usize;
93 let band_hi = (band_lo + 1).min(num_bands - 1);
94 let frac = band_f - band_lo as f32;
95 let v_lo = analysis.band_at(time, band_lo as u8);
96 let v_hi = analysis.band_at(time, band_hi as u8);
97 v_lo * (1.0 - frac) + v_hi * frac
98 })
99 .collect()
100 }
101}
102
103impl Painter for AudioSpectrum {
104 fn paint_content(
105 &self,
106 canvas: &Canvas,
107 layout: &BoxLayout,
108 _props: &AnimatedProperties,
109 ctx: &PaintCtx,
110 ) {
111 let w = layout.width;
112 let h = layout.height;
113 let n = self.bars.max(1) as usize;
114 let values = self.get_band_values(ctx.scenario_time);
115 let (r, g, b, a) = parse_hex_color(&self.color);
116 let color = Color::from_argb(a, r, g, b);
117
118 match self.mode {
119 SpectrumMode::Bars => {
120 let total_gap = self.bar_gap * (n as f32 - 1.0);
121 let bar_w = ((w - total_gap) / n as f32).max(1.0);
122 let mut paint = Paint::default();
123 paint.set_color(color);
124 paint.set_style(PaintStyle::Fill);
125 paint.set_anti_alias(true);
126
127 for (i, &v) in values.iter().enumerate() {
128 let bar_h = (v * h).max(self.min_height);
129 let x = i as f32 * (bar_w + self.bar_gap);
130 let y = h - bar_h;
131 canvas.draw_rect(Rect::from_xywh(x, y, bar_w, bar_h), &paint);
132 }
133 }
134 SpectrumMode::Radial => {
135 let cx = w / 2.0;
136 let cy = h / 2.0;
137 let max_r = cx.min(cy);
138 let inner_r = max_r * 0.3;
139
140 let mut paint = Paint::default();
141 paint.set_color(color);
142 paint.set_style(PaintStyle::Stroke);
143 paint.set_stroke_width(2.0);
144 paint.set_anti_alias(true);
145
146 for (i, &v) in values.iter().enumerate() {
147 let angle = (i as f32 / n as f32) * 2.0 * std::f32::consts::PI;
148 let bar_len = (v * (max_r - inner_r)).max(self.min_height);
149 let x0 = cx + inner_r * angle.cos();
150 let y0 = cy + inner_r * angle.sin();
151 let x1 = cx + (inner_r + bar_len) * angle.cos();
152 let y1 = cy + (inner_r + bar_len) * angle.sin();
153 canvas.draw_line(Point::new(x0, y0), Point::new(x1, y1), &paint);
154 }
155 }
156 }
157 }
158}