use super::effects::nearest_indexed;
use super::rave::hsv;
use super::*;
use crossterm::event::KeyCode;
use ratatui::buffer::{Buffer, CellDiffOption};
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use std::f32::consts::TAU;
use unicode_width::UnicodeWidthStr;
const CODE: [KeyCode; 10] = [
KeyCode::Right,
KeyCode::Right,
KeyCode::Left,
KeyCode::Left,
KeyCode::Up,
KeyCode::Down,
KeyCode::Up,
KeyCode::Down,
KeyCode::Char('h'),
KeyCode::Char('i'),
];
const FOLD: f32 = 40.0;
const FLOATERS: u32 = 24;
const SHAPES: [&str; 8] = ["✿", "❀", "◌", "∞", "☮", "✺", "◯", "☁"];
#[derive(Debug, Default)]
pub struct High {
heard: usize,
since: Option<Instant>,
}
impl High {
pub fn hear(&mut self, code: KeyCode) -> rave::Heard {
if code != CODE[self.heard] {
self.heard = match code {
KeyCode::Right if self.heard == 2 => 2,
KeyCode::Right => 1,
_ => 0,
};
return rave::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()),
};
rave::Heard::Toggle
}
n if n > 8 => rave::Heard::Swallow,
_ => rave::Heard::Pass,
}
}
pub fn elapsed(&self) -> Option<Duration> {
self.since.map(|since| since.elapsed())
}
}
impl App {
pub(super) fn hear_high(&mut self, key: crossterm::event::KeyEvent) -> bool {
let code = match key.modifiers.is_empty() {
true => key.code,
false => KeyCode::Null,
};
match self.high.hear(code) {
rave::Heard::Pass => false,
rave::Heard::Swallow => true,
rave::Heard::Toggle => {
match (self.high.since, theme::no_color()) {
(Some(_), true) => {
self.high = High::default();
self.set_status("No colour, no trip");
}
(Some(_), false) => self
.set_status("☮ High mode. Stare at it. The same code again to come down"),
(None, _) => self.set_status("Back down. Drink some water"),
}
true
}
}
}
pub fn tripping(&self) -> bool {
self.high.since.is_some()
}
}
pub fn paint(buf: &mut Buffer, footer: Rect, t: Duration, truecolor: bool, label: bool) {
let secs = t.as_secs_f32();
let fade = (secs / 3.0).min(1.0);
let area = buf.area;
let cx = f32::from(area.x) + f32::from(area.width) / 2.0;
let cy = f32::from(area.y) + f32::from(area.height) / 2.0;
let fold = (((secs / FOLD) * TAU - TAU / 4.0).sin() * 1.4).clamp(0.0, 1.0);
let floor = match truecolor {
true => 0.12,
false => 0.33,
};
let mut palette: std::collections::HashMap<(u16, u8), Color> = std::collections::HashMap::new();
let mut ground = |hue: f32, value: f32| {
let hue_step = (hue.rem_euclid(360.0) / 5.0) as u16;
let value_step = (value.clamp(0.0, 1.0) * 40.0) as u8;
*palette.entry((hue_step, value_step)).or_insert_with(|| {
let rgb = hsv(
f32::from(hue_step) * 5.0,
0.65,
f32::from(value_step) / 40.0,
);
match truecolor {
true => Color::Rgb(rgb.0, rgb.1, rgb.2),
false => Color::Indexed(nearest_indexed(rgb)),
}
})
};
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 dx = (f32::from(x) - cx) / 2.0;
let dy = f32::from(y) - cy;
let (hue, glow) = pattern(dx, dy, secs, fold);
let value = fade * (floor + 0.2 * glow);
cell.bg = ground(hue, value);
}
}
floaters(buf, secs, truecolor);
if label {
sign(buf, footer.intersection(area), secs, truecolor);
}
}
fn pattern(dx: f32, dy: f32, secs: f32, fold: f32) -> (f32, f32) {
let r = dx.hypot(dy);
let plasma = (dx * 0.11 + secs * 0.31).sin()
+ (dy * 0.19 - secs * 0.23).sin()
+ ((dx + dy) * 0.07 + secs * 0.17).sin()
+ (r * 0.16 - secs * 0.41).sin();
let plasma_hue = plasma * 45.0 + secs * 6.0;
let plasma_glow = (plasma / 4.0 + 1.0) / 2.0;
let angle = dy.atan2(dx);
let arms = (angle * 3.0 + r * 0.35 - secs * 0.6).sin();
let spiral_hue = angle.to_degrees() + r * 4.0 - secs * 12.0;
let spiral_glow = (arms + 1.0) / 2.0;
let delta = (spiral_hue - plasma_hue + 180.0).rem_euclid(360.0) - 180.0;
let hue = plasma_hue + delta * fold;
let glow = plasma_glow + (spiral_glow - plasma_glow) * fold;
(hue, glow)
}
fn floaters(buf: &mut Buffer, secs: f32, truecolor: bool) {
let area = buf.area;
if area.width < 3 || area.height == 0 {
return;
}
let blank = |buf: &Buffer, x: u16, y: u16| {
buf.cell((x, y))
.is_some_and(|c| c.symbol() == " " && c.diff_option == CellDiffOption::None)
};
let (w, h) = (f32::from(area.width), f32::from(area.height));
for k in 0..FLOATERS {
let seed = scatter(k);
let unit = |shift: u32| ((seed >> shift) & 0xFF) as f32 / 255.0;
let speed = 0.2 + 0.3 * unit(0);
let rise = (unit(8) * h - secs * speed).rem_euclid(h);
let sway = (secs * (0.2 + 0.3 * unit(16)) + unit(24) * TAU).sin() * 3.0;
let x = area.x + ((unit(4) * w + sway).rem_euclid(w)) as u16;
let y = area.y + rise as u16;
let (Some(left), Some(right)) = (x.checked_sub(1), x.checked_add(1)) else {
continue;
};
if !(blank(buf, left, y) && blank(buf, x, y) && blank(buf, right, y)) {
continue;
}
let shape = SHAPES[(seed % SHAPES.len() as u32) as usize];
let hue = unit(12) * 360.0 + secs * 10.0;
let rgb = hsv(hue, 0.35, 0.95);
let fg = match truecolor {
true => Color::Rgb(rgb.0, rgb.1, rgb.2),
false => Color::Indexed(nearest_indexed(rgb)),
};
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_symbol(shape);
cell.fg = fg;
}
}
}
fn scatter(k: u32) -> u32 {
let mut h = k.wrapping_add(1).wrapping_mul(0x9E37_79B1);
h ^= h >> 15;
h = h.wrapping_mul(0x85EB_CA77);
h ^= h >> 13;
h
}
fn sign(buf: &mut Buffer, footer: Rect, secs: f32, truecolor: bool) {
let label = " ☮ →→←←↑↓↑↓hi to come down ☮ ";
let width = label.width() as u16;
if footer.height == 0 || footer.width <= width {
return;
}
let rgb = hsv(secs * 12.0, 0.45, 0.95);
let bg = match truecolor {
true => Color::Rgb(rgb.0, rgb.1, rgb.2),
false => Color::Indexed(nearest_indexed(rgb)),
};
let style = Style::default()
.fg(Color::Indexed(16))
.bg(bg)
.add_modifier(Modifier::BOLD);
let x = footer.x + (footer.width - width) / 2;
buf.set_string(x, footer.y, label, style);
}
#[cfg(test)]
mod tests {
use super::*;
fn type_code(high: &mut High) -> Vec<rave::Heard> {
CODE.iter().map(|&key| high.hear(key)).collect()
}
#[test]
fn the_code_starts_the_trip_and_ends_it() {
let mut high = High::default();
let heard = type_code(&mut high);
assert!(
heard[..8].iter().all(|h| *h == rave::Heard::Pass),
"{heard:?}"
);
assert_eq!(heard[8], rave::Heard::Swallow);
assert_eq!(heard[9], rave::Heard::Toggle);
assert!(high.elapsed().is_some());
type_code(&mut high);
assert!(high.elapsed().is_none());
}
#[test]
fn a_third_right_is_forgiven() {
let mut high = High::default();
for _ in 0..5 {
high.hear(KeyCode::Right);
}
assert_eq!(high.heard, 2);
}
#[test]
fn all_three_codes_are_heard_side_by_side() {
let mut app = crate::ui::tests::test_app();
for code in CODE.into_iter().chain(rave::CODE).chain(drunk::CODE) {
app.on_key(crate::ui::tests::key(code));
}
let colour = !theme::no_color();
assert_eq!(app.tripping(), colour);
assert_eq!(app.raving(), colour);
assert!(app.reeling());
}
#[test]
fn the_ground_melts_and_the_text_stays() {
let area = Rect::new(0, 0, 60, 12);
let footer = Rect::new(0, 11, 60, 1);
for second in 0..90 {
let mut buf = Buffer::empty(area);
for y in 0..11 {
buf.set_string(0, y, "$12.34 spent", Style::default());
}
paint(&mut buf, footer, Duration::from_secs(second), false, true);
for y in 0..11 {
let text: String = (0..13).map(|x| buf[(x, y)].symbol().to_string()).collect();
assert_eq!(text, "$12.34 spent ", "second {second}, row {y}");
}
for cell in buf.content() {
assert!(
!matches!(cell.fg, Color::Rgb(..)) && !matches!(cell.bg, Color::Rgb(..)),
"{cell:?}"
);
}
}
}
#[test]
fn the_spiral_folds_in_and_out() {
let fold = |secs: f32| (((secs / FOLD) * TAU - TAU / 4.0).sin() * 1.4).clamp(0.0, 1.0);
assert_eq!(fold(0.0), 0.0);
assert_eq!(fold(FOLD / 2.0), 1.0);
assert_eq!(fold(FOLD), 0.0);
}
}