use super::effects::nearest_indexed;
use super::*;
use crossterm::event::KeyCode;
use ratatui::buffer::{Buffer, CellDiffOption};
use ratatui::layout::Rect;
use ratatui::style::Color;
const CODE: [KeyCode; 10] = [
KeyCode::Up,
KeyCode::Up,
KeyCode::Down,
KeyCode::Down,
KeyCode::Left,
KeyCode::Right,
KeyCode::Left,
KeyCode::Right,
KeyCode::Char('b'),
KeyCode::Char('a'),
];
const BPM: f32 = 128.0;
#[derive(Debug, Default)]
pub struct Rave {
heard: usize,
since: Option<Instant>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Heard {
Pass,
Swallow,
Toggle,
}
impl Rave {
pub fn hear(&mut self, code: KeyCode) -> Heard {
if code != CODE[self.heard] {
self.heard = match code {
KeyCode::Up if self.heard == 2 => 2,
KeyCode::Up => 1,
_ => 0,
};
return Heard::Pass;
}
self.heard += 1;
match self.heard {
n if n == CODE.len() => {
self.heard = 0;
self.since = match self.since {
Some(_) => None,
None => Some(Instant::now()),
};
Heard::Toggle
}
n if n > 8 => Heard::Swallow,
_ => Heard::Pass,
}
}
pub fn elapsed(&self) -> Option<Duration> {
self.since.map(|since| since.elapsed())
}
}
impl App {
pub(super) fn hear_rave(&mut self, key: crossterm::event::KeyEvent) -> bool {
let code = match key.modifiers.is_empty() {
true => key.code,
false => KeyCode::Null,
};
match self.rave.hear(code) {
Heard::Pass => false,
Heard::Swallow => true,
Heard::Toggle => {
match (self.rave.elapsed(), theme::no_color()) {
(Some(_), true) => {
self.rave = Rave::default();
self.set_status("No colour, no party");
}
(Some(_), false) => {
self.set_status("♫ Rave mode. The same code again to go home")
}
(None, _) => self.set_status("Lights up. Back to work"),
}
true
}
}
}
pub fn raving(&self) -> bool {
self.rave.since.is_some()
}
}
pub fn paint(buf: &mut Buffer, footer: Rect, t: Duration, truecolor: bool) {
let secs = t.as_secs_f32();
let beats = secs * BPM / 60.0;
let beat = beats.floor();
let thump = (-(beats - beat) * 5.0).exp();
let ground_hue = (beat * 72.0) % 360.0;
let ground = snap(hsv(ground_hue, 0.9, 0.08 + 0.24 * thump), truecolor);
let area = buf.area;
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
let Some(cell) = buf.cell_mut((x, y)) else {
continue;
};
if cell.diff_option == CellDiffOption::Skip {
continue;
}
let hue = (f32::from(x) * 6.0 + f32::from(y) * 12.0 - secs * 180.0).rem_euclid(360.0);
cell.fg = snap(hsv(hue, 0.85, 1.0), truecolor);
cell.bg = ground;
}
}
equaliser(buf, footer.intersection(area), secs, thump, truecolor);
}
fn equaliser(buf: &mut Buffer, footer: Rect, secs: f32, thump: f32, truecolor: bool) {
const BARS: [&str; 8] = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
let y = footer.y;
for x in footer.left()..footer.right() {
let fx = f32::from(x);
let level = 0.5
+ 0.2 * (fx * 0.9 + secs * 7.0).sin()
+ 0.15 * (fx * 0.37 - secs * 3.1).sin()
+ 0.15 * (fx * 1.7 + secs * 11.3).sin();
let level = (level * (0.55 + 0.45 * thump)).clamp(0.0, 0.999);
let hue = (fx * 8.0 + secs * 90.0).rem_euclid(360.0);
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_diff_option(CellDiffOption::None);
cell.set_symbol(BARS[(level * BARS.len() as f32) as usize]);
cell.fg = snap(hsv(hue, 1.0, 1.0), truecolor);
cell.bg = Color::Indexed(16);
}
}
let label = " ♫ ↑↑↓↓←→←→ba to go home ♫ ";
let width = label.chars().count() as u16;
if footer.width > width {
let x = footer.x + (footer.width - width) / 2;
let style = ratatui::style::Style::default()
.fg(Color::Indexed(16))
.bg(snap(
hsv((secs * 90.0).rem_euclid(360.0), 0.7, 1.0),
truecolor,
))
.add_modifier(ratatui::style::Modifier::BOLD);
buf.set_string(x, y, label, style);
}
}
fn snap(rgb: (u8, u8, u8), truecolor: bool) -> Color {
match truecolor {
true => Color::Rgb(rgb.0, rgb.1, rgb.2),
false => Color::Indexed(nearest_indexed(rgb)),
}
}
fn hsv(hue: f32, saturation: f32, value: f32) -> (u8, u8, u8) {
let c = value * saturation;
let h = hue.rem_euclid(360.0) / 60.0;
let x = c * (1.0 - (h % 2.0 - 1.0).abs());
let (r, g, b) = match h as u8 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let m = value - c;
let byte = |v: f32| ((v + m) * 255.0).round().clamp(0.0, 255.0) as u8;
(byte(r), byte(g), byte(b))
}
#[cfg(test)]
mod tests {
use super::*;
fn type_code(rave: &mut Rave) -> Vec<Heard> {
CODE.iter().map(|&key| rave.hear(key)).collect()
}
#[test]
fn the_code_starts_the_party_and_ends_it() {
let mut rave = Rave::default();
let heard = type_code(&mut rave);
assert!(heard[..8].iter().all(|h| *h == Heard::Pass), "{heard:?}");
assert_eq!(heard[8], Heard::Swallow);
assert_eq!(heard[9], Heard::Toggle);
assert!(rave.elapsed().is_some());
type_code(&mut rave);
assert!(rave.elapsed().is_none());
}
#[test]
fn a_wrong_key_starts_over_but_a_third_up_does_not() {
let mut rave = Rave::default();
rave.hear(KeyCode::Up);
rave.hear(KeyCode::Up);
rave.hear(KeyCode::Down);
assert_eq!(rave.hear(KeyCode::Char('x')), Heard::Pass);
assert_eq!(rave.heard, 0);
assert_eq!(rave.hear(KeyCode::Char('b')), Heard::Pass);
for _ in 0..5 {
rave.hear(KeyCode::Up);
}
assert_eq!(rave.heard, 2);
let rest: Vec<_> = CODE[2..].iter().map(|&key| rave.hear(key)).collect();
assert_eq!(rest.last(), Some(&Heard::Toggle));
}
#[test]
fn the_code_is_heard_away_from_the_table() {
let mut app = crate::ui::tests::test_app();
app.mode = Mode::Help;
for code in CODE {
app.on_key(crate::ui::tests::key(code));
}
assert!(app.raving());
}
#[test]
fn hsv_lands_on_the_primaries() {
assert_eq!(hsv(0.0, 1.0, 1.0), (255, 0, 0));
assert_eq!(hsv(120.0, 1.0, 1.0), (0, 255, 0));
assert_eq!(hsv(240.0, 1.0, 1.0), (0, 0, 255));
assert_eq!(hsv(360.0, 1.0, 1.0), (255, 0, 0));
assert_eq!(hsv(42.0, 0.0, 0.0), (0, 0, 0));
}
#[test]
fn the_party_repaints_colours_and_keeps_the_text() {
let area = Rect::new(0, 0, 40, 3);
let mut buf = Buffer::empty(area);
buf.set_string(0, 0, "$12.34 spent", ratatui::style::Style::default());
let footer = Rect::new(0, 2, 40, 1);
paint(&mut buf, footer, Duration::from_millis(1234), false);
let text: String = (0..12).map(|x| buf[(x, 0)].symbol().to_string()).collect();
assert_eq!(text, "$12.34 spent");
for cell in buf.content() {
assert!(
!matches!(cell.fg, Color::Rgb(..)) && !matches!(cell.bg, Color::Rgb(..)),
"{cell:?}"
);
}
assert!(buf.content()[80..].iter().any(|c| c.symbol() != " "));
}
}