diff --git a/Cargo.toml b/Cargo.toml index 9404026..bb4fd05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ rand = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" tui-textarea = "0.7" +tui-big-text = "0.7" arboard = "3" minimad = "0.13" crossbeam-channel = "0.5" diff --git a/src/main.rs b/src/main.rs index bd19d0d..ef3892e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,7 @@ use settings::Settings; use state::audio::RefreshRate; #[derive(Parser)] -#[command(name = "seq", about = "A step sequencer with Ableton Link support")] +#[command(name = "cagire", about = "A step sequencer with Ableton Link support")] struct Args { /// Directory containing audio samples to load (can be specified multiple times) #[arg(short, long)] diff --git a/src/state/ui.rs b/src/state/ui.rs index b013e48..9881974 100644 --- a/src/state/ui.rs +++ b/src/state/ui.rs @@ -2,7 +2,15 @@ use std::time::{Duration, Instant}; use crate::state::Modal; +pub struct Sparkle { + pub x: u16, + pub y: u16, + pub char_idx: usize, + pub life: u8, +} + pub struct UiState { + pub sparkles: Vec, pub status_message: Option, pub flash_until: Option, pub modal: Modal, @@ -16,6 +24,7 @@ pub struct UiState { impl Default for UiState { fn default() -> Self { Self { + sparkles: Vec::new(), status_message: None, flash_until: None, modal: Modal::None, diff --git a/src/views/render.rs b/src/views/render.rs index 60276fc..04a48ce 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -17,7 +17,7 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq let term = frame.area(); if app.ui.show_title { - title_view::render(frame, term); + title_view::render(frame, term, &mut app.ui); return; } diff --git a/src/views/title_view.rs b/src/views/title_view.rs index 843b3c5..e43f90a 100644 --- a/src/views/title_view.rs +++ b/src/views/title_view.rs @@ -1,51 +1,107 @@ +use rand::Rng; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; -use ratatui::style::{Color, Modifier, Style}; +use ratatui::style::{Color, Style, Stylize}; use ratatui::text::{Line, Span}; use ratatui::widgets::Paragraph; use ratatui::Frame; +use tui_big_text::{BigText, PixelSize}; -pub fn render(frame: &mut Frame, area: Rect) { - let title_style = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD); - let subtitle_style = Style::new().fg(Color::White); - let dim_style = Style::new() - .fg(Color::Rgb(120, 125, 135)) - .add_modifier(Modifier::DIM); - let link_style = Style::new().fg(Color::Rgb(100, 160, 180)); +use crate::state::ui::{Sparkle, UiState}; - let lines = vec![ +const SPARKLE_CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*']; +const SPARKLE_COLORS: &[(u8, u8, u8)] = &[ + (200, 220, 255), + (255, 200, 150), + (150, 255, 200), + (255, 150, 200), + (200, 150, 255), +]; + +pub fn render(frame: &mut Frame, area: Rect, ui: &mut UiState) { + let mut rng = rand::thread_rng(); + + // Spawn new sparkles + for _ in 0..3 { + if rng.gen_bool(0.6) { + let x = rng.gen_range(0..area.width); + let y = rng.gen_range(0..area.height); + ui.sparkles.push(Sparkle { + x, + y, + char_idx: rng.gen_range(0..SPARKLE_CHARS.len()), + life: rng.gen_range(15..40), + }); + } + } + + // Age and remove dead sparkles + ui.sparkles.iter_mut().for_each(|s| s.life = s.life.saturating_sub(1)); + ui.sparkles.retain(|s| s.life > 0); + + // Render sparkles + for sparkle in &ui.sparkles { + let color = SPARKLE_COLORS[sparkle.char_idx % SPARKLE_COLORS.len()]; + let intensity = (sparkle.life as f32 / 30.0).min(1.0); + let r = (color.0 as f32 * intensity) as u8; + let g = (color.1 as f32 * intensity) as u8; + let b = (color.2 as f32 * intensity) as u8; + + let ch = SPARKLE_CHARS[sparkle.char_idx]; + let span = Span::styled(ch.to_string(), Style::new().fg(Color::Rgb(r, g, b))); + let para = Paragraph::new(Line::from(span)); + let sparkle_area = Rect::new(sparkle.x, sparkle.y, 1, 1); + if sparkle_area.x < area.width && sparkle_area.y < area.height { + frame.render_widget(para, sparkle_area); + } + } + + // Main content + let author_style = Style::new().fg(Color::Rgb(180, 140, 200)); + let link_style = Style::new().fg(Color::Rgb(120, 200, 180)); + let license_style = Style::new().fg(Color::Rgb(200, 160, 100)); + + let big_title = BigText::builder() + .pixel_size(PixelSize::Quadrant) + .style(Style::new().cyan().bold()) + .lines(vec!["CAGIRE".into()]) + .centered() + .build(); + + let subtitle_lines = vec![ Line::from(""), + Line::from(Span::styled( + "A Forth Music Sequencer", + Style::new().fg(Color::White), + )), Line::from(""), - Line::from(Span::styled("seq", title_style)), - Line::from(""), - Line::from(Span::styled("A Forth Music Sequencer", subtitle_style)), - Line::from(""), - Line::from(""), - Line::from(Span::styled("by BuboBubo", dim_style)), - Line::from(Span::styled("Raphael Maurice Forment", dim_style)), + Line::from(Span::styled("by BuboBubo", author_style)), Line::from(""), Line::from(Span::styled("https://raphaelforment.fr", link_style)), Line::from(""), - Line::from(""), - Line::from(Span::styled("AGPL-3.0", dim_style)), - Line::from(""), + Line::from(Span::styled("AGPL-3.0", license_style)), Line::from(""), Line::from(""), Line::from(Span::styled( "Press any key to continue", - Style::new().fg(Color::DarkGray), + Style::new().fg(Color::Rgb(140, 160, 170)), )), ]; - let text_height = lines.len() as u16; - let vertical_padding = area.height.saturating_sub(text_height) / 2; + let big_text_height = 4; + let subtitle_height = subtitle_lines.len() as u16; + let total_height = big_text_height + subtitle_height; + let vertical_padding = area.height.saturating_sub(total_height) / 2; - let [_, center_area, _] = Layout::vertical([ + let [_, title_area, subtitle_area, _] = Layout::vertical([ Constraint::Length(vertical_padding), - Constraint::Length(text_height), + Constraint::Length(big_text_height), + Constraint::Length(subtitle_height), Constraint::Fill(1), ]) .areas(area); - let paragraph = Paragraph::new(lines).alignment(Alignment::Center); - frame.render_widget(paragraph, center_area); + frame.render_widget(big_title, title_area); + + let subtitle = Paragraph::new(subtitle_lines).alignment(Alignment::Center); + frame.render_widget(subtitle, subtitle_area); }