This commit is contained in:
2026-01-21 17:20:14 +01:00
parent 0a14651835
commit 2f15bce223
5 changed files with 94 additions and 28 deletions

View File

@@ -23,6 +23,7 @@ rand = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tui-textarea = "0.7" tui-textarea = "0.7"
tui-big-text = "0.7"
arboard = "3" arboard = "3"
minimad = "0.13" minimad = "0.13"
crossbeam-channel = "0.5" crossbeam-channel = "0.5"

View File

@@ -37,7 +37,7 @@ use settings::Settings;
use state::audio::RefreshRate; use state::audio::RefreshRate;
#[derive(Parser)] #[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 { struct Args {
/// Directory containing audio samples to load (can be specified multiple times) /// Directory containing audio samples to load (can be specified multiple times)
#[arg(short, long)] #[arg(short, long)]

View File

@@ -2,7 +2,15 @@ use std::time::{Duration, Instant};
use crate::state::Modal; use crate::state::Modal;
pub struct Sparkle {
pub x: u16,
pub y: u16,
pub char_idx: usize,
pub life: u8,
}
pub struct UiState { pub struct UiState {
pub sparkles: Vec<Sparkle>,
pub status_message: Option<String>, pub status_message: Option<String>,
pub flash_until: Option<Instant>, pub flash_until: Option<Instant>,
pub modal: Modal, pub modal: Modal,
@@ -16,6 +24,7 @@ pub struct UiState {
impl Default for UiState { impl Default for UiState {
fn default() -> Self { fn default() -> Self {
Self { Self {
sparkles: Vec::new(),
status_message: None, status_message: None,
flash_until: None, flash_until: None,
modal: Modal::None, modal: Modal::None,

View File

@@ -17,7 +17,7 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
let term = frame.area(); let term = frame.area();
if app.ui.show_title { if app.ui.show_title {
title_view::render(frame, term); title_view::render(frame, term, &mut app.ui);
return; return;
} }

View File

@@ -1,51 +1,107 @@
use rand::Rng;
use ratatui::layout::{Alignment, Constraint, Layout, Rect}; 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::text::{Line, Span};
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use ratatui::Frame; use ratatui::Frame;
use tui_big_text::{BigText, PixelSize};
pub fn render(frame: &mut Frame, area: Rect) { use crate::state::ui::{Sparkle, UiState};
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));
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(""),
Line::from(Span::styled(
"A Forth Music Sequencer",
Style::new().fg(Color::White),
)),
Line::from(""), Line::from(""),
Line::from(Span::styled("seq", title_style)), Line::from(Span::styled("by BuboBubo", author_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(""), Line::from(""),
Line::from(Span::styled("https://raphaelforment.fr", link_style)), Line::from(Span::styled("https://raphaelforment.fr", link_style)),
Line::from(""), Line::from(""),
Line::from(""), Line::from(Span::styled("AGPL-3.0", license_style)),
Line::from(Span::styled("AGPL-3.0", dim_style)),
Line::from(""),
Line::from(""), Line::from(""),
Line::from(""), Line::from(""),
Line::from(Span::styled( Line::from(Span::styled(
"Press any key to continue", "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 big_text_height = 4;
let vertical_padding = area.height.saturating_sub(text_height) / 2; 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(vertical_padding),
Constraint::Length(text_height), Constraint::Length(big_text_height),
Constraint::Length(subtitle_height),
Constraint::Fill(1), Constraint::Fill(1),
]) ])
.areas(area); .areas(area);
let paragraph = Paragraph::new(lines).alignment(Alignment::Center); frame.render_widget(big_title, title_area);
frame.render_widget(paragraph, center_area);
let subtitle = Paragraph::new(subtitle_lines).alignment(Alignment::Center);
frame.render_widget(subtitle, subtitle_area);
} }