Created
June 30, 2025 01:43
-
-
Save airstrike/b2e958164380e0e6d03b2cd26a295a2d to your computer and use it in GitHub Desktop.
iced • now playing: chill tracks only
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use rand::Rng; | |
| use iced::time::{Duration, Instant}; | |
| use iced::widget::canvas::{Frame, Geometry, Path, Program}; | |
| use iced::widget::{button, canvas, center, column, text}; | |
| use iced::{Element, Point, Rectangle, Renderer, Size, Theme}; | |
| use iced::{Fill, Subscription, Task}; | |
| const NUM_BARS: usize = 20; | |
| const MAX_BAR_HEIGHT: f32 = 0.7; | |
| const BAR_SPACING: f32 = 2.0; | |
| fn main() -> iced::Result { | |
| iced::application( | |
| NowPlayingApp::new, | |
| NowPlayingApp::update, | |
| NowPlayingApp::view, | |
| ) | |
| .subscription(NowPlayingApp::subscription) | |
| .title("iced • Now Playing") | |
| .window_size([400.0, 200.0]) | |
| .theme(|_| iced::Theme::GruvboxDark) | |
| .centered() | |
| .run() | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum Message { | |
| TogglePlay, | |
| Tick, | |
| Pulse, | |
| } | |
| #[derive(Debug)] | |
| struct NowPlaying { | |
| is_playing: bool, | |
| bars: [f32; NUM_BARS], | |
| target_bars: [f32; NUM_BARS], | |
| peaks: [f32; NUM_BARS], | |
| peak_hold_timers: [u8; NUM_BARS], | |
| } | |
| impl Default for NowPlaying { | |
| fn default() -> Self { | |
| // Start with all bars and targets at zero (empty) | |
| Self { | |
| is_playing: false, | |
| bars: [0.0; NUM_BARS], | |
| target_bars: [0.0; NUM_BARS], | |
| peaks: [0.0; NUM_BARS], | |
| peak_hold_timers: [0; NUM_BARS], | |
| } | |
| } | |
| } | |
| impl NowPlaying { | |
| fn create_wave(&mut self, center_index: usize, intensity: f32) { | |
| for i in 0..NUM_BARS { | |
| let distance = (i as isize - center_index as isize).abs() as f32; | |
| let wave_intensity = intensity * (-distance * 0.25).exp(); | |
| if wave_intensity > 0.1 { | |
| let random_factor = 0.85 + rand::rng().random_range(0.0..0.35); | |
| self.target_bars[i] = self.target_bars[i].max(wave_intensity * random_factor); | |
| } | |
| } | |
| } | |
| } | |
| struct NowPlayingApp { | |
| state: NowPlaying, | |
| last_pulse_time: Instant, | |
| next_pulse_interval: Duration, | |
| } | |
| impl NowPlayingApp { | |
| fn new() -> (Self, Task<Message>) { | |
| // Create an initial wave using create_wave | |
| let mut state = NowPlaying::default(); | |
| state.create_wave(NUM_BARS / 2, 0.8); | |
| ( | |
| Self { | |
| state, | |
| last_pulse_time: Instant::now(), | |
| next_pulse_interval: Duration::from_millis(200), | |
| }, | |
| Task::none(), | |
| ) | |
| } | |
| fn subscription(&self) -> Subscription<Message> { | |
| iced::time::every(Duration::from_millis(16)).map(|_| Message::Tick) | |
| } | |
| fn update(&mut self, message: Message) -> Task<Message> { | |
| match message { | |
| Message::TogglePlay => { | |
| self.state.is_playing = !self.state.is_playing; | |
| } | |
| Message::Tick => { | |
| // --- Pulse scheduling logic (matches JS) --- | |
| if self.state.is_playing { | |
| let now = Instant::now(); | |
| if now.duration_since(self.last_pulse_time) > self.next_pulse_interval { | |
| self.last_pulse_time = now; | |
| // Randomize next interval between 150-350ms | |
| let next_ms = 150 + (rand::rng().random_range(0.0..200.0)) as u64; | |
| self.next_pulse_interval = Duration::from_millis(next_ms); | |
| // Create 1-3 wave centers | |
| let num_waves = 1 + (rand::rng().random_range(0.0..3.0)) as usize; | |
| for _ in 0..num_waves { | |
| let center = rand::rng().random_range(0..NUM_BARS); | |
| let intensity = 0.6 + rand::rng().random_range(0.0..0.4); | |
| self.state.create_wave(center, intensity); | |
| } | |
| } | |
| } | |
| // Always update bars and peaks (decay/animation) | |
| for i in 0..NUM_BARS { | |
| // Smoothly move bars toward target | |
| let diff = self.state.target_bars[i] - self.state.bars[i]; | |
| self.state.bars[i] += diff * 0.25; | |
| // Peak logic (ghost bar) | |
| if self.state.bars[i] > self.state.peaks[i] { | |
| self.state.peaks[i] = self.state.bars[i]; | |
| self.state.peak_hold_timers[i] = 30; // Hold for ~0.5s at 60fps | |
| } else if self.state.peak_hold_timers[i] > 0 { | |
| self.state.peak_hold_timers[i] -= 1; | |
| } else { | |
| self.state.peaks[i] *= 0.95; | |
| } | |
| // Decay the target | |
| self.state.target_bars[i] *= 0.92; | |
| // Additional decay to bars | |
| self.state.bars[i] *= 0.96; | |
| // Clamp values | |
| self.state.bars[i] = self.state.bars[i].max(0.0).min(1.0); | |
| self.state.target_bars[i] = self.state.target_bars[i].max(0.0).min(1.0); | |
| self.state.peaks[i] = self.state.peaks[i].max(0.0).min(1.0); | |
| } | |
| } | |
| _ => {} | |
| } | |
| Task::none() | |
| } | |
| fn view(&self) -> Element<Message> { | |
| let content = column![ | |
| button( | |
| text( | |
| if self.state.is_playing { | |
| "Pause" | |
| } else { | |
| "Play" | |
| } | |
| .to_uppercase() | |
| ) | |
| .size(12) | |
| ) | |
| .on_press(Message::TogglePlay), | |
| canvas(NowPlayingViewer { | |
| bars: self.state.bars, | |
| peaks: self.state.peaks, | |
| }) | |
| .width(Fill) | |
| .height(Fill) | |
| ] | |
| .spacing(10) | |
| .padding(10); | |
| center(content).into() | |
| } | |
| } | |
| struct NowPlayingViewer { | |
| bars: [f32; NUM_BARS], | |
| peaks: [f32; NUM_BARS], | |
| } | |
| impl<Message> Program<Message> for NowPlayingViewer { | |
| type State = (); | |
| fn draw( | |
| &self, | |
| _state: &Self::State, | |
| renderer: &Renderer, | |
| theme: &Theme, | |
| bounds: Rectangle, | |
| _cursor: iced::mouse::Cursor, | |
| ) -> Vec<Geometry> { | |
| let mut frame = Frame::new(renderer, bounds.size()); | |
| let bar_width = bounds.width / NUM_BARS as f32; | |
| let actual_bar_width = bar_width - BAR_SPACING; | |
| let palette = theme.extended_palette(); | |
| let bar_color = palette.primary.base.color; | |
| let peak_color = palette.primary.strong.color; | |
| for (i, &height) in self.bars.iter().enumerate() { | |
| let scaled_height = height * MAX_BAR_HEIGHT * bounds.height; | |
| let x = i as f32 * bar_width + BAR_SPACING / 2.0; | |
| let y = bounds.height - scaled_height; | |
| // Draw main bar | |
| let bar = Path::rectangle(Point::new(x, y), Size::new(actual_bar_width, scaled_height)); | |
| // Simple solid color for bars | |
| frame.fill(&bar, bar_color); | |
| // Draw peak indicator | |
| if self.peaks[i] > 0.01 { | |
| let peak_y = bounds.height - (self.peaks[i] * MAX_BAR_HEIGHT * bounds.height); | |
| let peak = Path::rectangle( | |
| Point::new(x, peak_y - 2.0), | |
| Size::new(actual_bar_width, 2.0), | |
| ); | |
| frame.fill(&peak, peak_color); | |
| } | |
| } | |
| vec![frame.into_geometry()] | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment