use std::time::SystemTime;
use pixtuoid_core::layout::WALKING_Y_OFF;
use pixtuoid_core::sprite::{Rgb, RgbBuffer};
use super::epoch_ms;
use super::palette::blend_rgb;
use crate::tui::layout::Point;
use crate::tui::theme::Theme;
pub(super) fn paint_screen_glow(
buf: &mut RgbBuffer,
desk_x: u16,
desk_y: u16,
now: SystemTime,
tint: Rgb,
theme: &Theme,
) {
let frame_lit = theme.effects.monitor_frame_lit;
let glow = tint;
let white = Rgb {
r: 255,
g: 255,
b: 255,
};
let glow_bright = blend_rgb(tint, white, 0.4);
let scanline = blend_rgb(tint, white, 0.7);
let put = |buf: &mut RgbBuffer, dx: u16, dy: u16, c: Rgb| {
let px = desk_x + dx;
let py = desk_y + dy;
if px < buf.width && py < buf.height {
buf.put(px, py, c);
}
};
for dx in 3..=10 {
put(buf, dx, 0, frame_lit);
}
for dx in 4..=9 {
put(buf, dx, 1, glow_bright);
put(buf, dx, 2, glow);
}
for dx in 4..=9 {
put(buf, dx, 3, frame_lit);
}
let elapsed_ms = epoch_ms(now);
let phase = (elapsed_ms / 120) as u16 + desk_x;
let scan_col = 4 + (phase % 6);
put(buf, scan_col, 1, scanline);
put(buf, scan_col, 2, scanline);
}
pub(super) fn paint_sleep_z(
buf: &mut RgbBuffer,
head_anchor: Point,
now: SystemTime,
seed: u64,
theme: &Theme,
) {
let z_color = theme.effects.sleep_z;
const RISE_MS: u64 = 2000;
const REST_MS: u64 = 400;
const CYCLE_MS: u64 = RISE_MS + REST_MS;
const MAX_RISE: u16 = 4;
const FADE_IN_MS: f32 = 150.0;
const PEAK_ALPHA: f32 = 0.9;
let phase_ms = epoch_ms(now).wrapping_add(seed % CYCLE_MS) % CYCLE_MS;
if phase_ms >= RISE_MS {
return;
}
let t = phase_ms as f32 / RISE_MS as f32;
let fade_in = (phase_ms as f32 / FADE_IN_MS).min(1.0);
let alpha = PEAK_ALPHA * fade_in * (1.0 - t);
if alpha < 0.06 {
return;
}
let rise = (t * MAX_RISE as f32) as u16;
let z_x = head_anchor.x + 5;
let z_y = head_anchor.y.saturating_sub(rise + 3);
const GLYPH: &[(u16, u16)] = &[(0, 0), (1, 0), (1, 1), (0, 2), (1, 2)];
for (dx, dy) in GLYPH {
let px = z_x + dx;
let py = z_y + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(px, py, blend_rgb(cur, z_color, alpha));
}
}
}
pub(super) fn paint_coffee_steam(buf: &mut RgbBuffer, base: Point, now: SystemTime, theme: &Theme) {
let steam = theme.effects.coffee_steam;
let elapsed_ms = epoch_ms(now);
for offset in 0..3u64 {
let phase = (elapsed_ms + offset * 600) % 1800;
let rise = (phase / 140) as u16;
let alpha = 1.0 - phase as f32 / 1800.0;
if alpha < 0.15 {
continue;
}
let wiggle = if (phase / 200).is_multiple_of(2) {
0
} else {
1
};
let px = base.x + wiggle;
let py = base.y.saturating_sub(rise + 2);
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(px, py, blend_rgb(cur, steam, alpha * 0.55));
}
}
}
pub(super) fn paint_walking_dust(
buf: &mut RgbBuffer,
walker_anchor: Point,
frame_idx: usize,
theme: &Theme,
) {
let dust = theme.effects.walking_dust;
let foot_y = walker_anchor.y + WALKING_Y_OFF;
let foot_x = walker_anchor.x + if frame_idx == 0 { 6 } else { 1 };
if foot_x < buf.width && foot_y < buf.height {
let cur = buf.get(foot_x, foot_y);
buf.put(foot_x, foot_y, blend_rgb(cur, dust, 0.45));
}
}
pub(super) fn paint_pet_hearts(buf: &mut RgbBuffer, cat_pos: Point, elapsed_ms: u64) {
const STAGGER_MS: u64 = 150;
const HEART_LIFE_MS: u64 = 1550;
let heart_color = Rgb {
r: 255,
g: 100,
b: 100,
};
for i in 0..4u64 {
let stagger = i * STAGGER_MS;
if elapsed_ms < stagger {
continue;
}
let local_ms = elapsed_ms - stagger;
if local_ms >= HEART_LIFE_MS {
continue;
}
let t = local_ms as f32 / HEART_LIFE_MS as f32;
let rise = (t * 6.0) as u16;
let alpha = 1.0 - t;
if alpha < 0.05 {
continue;
}
let dx: i16 = (i as i16) * 2 - 3;
let hx = (cat_pos.x as i32 + dx as i32).max(0) as u16;
let hy = cat_pos.y.saturating_sub(4 + rise);
for dy in 0..2u16 {
for ddx in 0..2u16 {
let px = hx + ddx;
let py = hy + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(px, py, blend_rgb(cur, heart_color, alpha * 0.8));
}
}
}
}
}
pub(super) fn paint_waiting_bubble(buf: &mut RgbBuffer, anchor: Point, theme: &Theme) {
let fg = theme.effects.waiting_bubble;
const GLYPH: &[&[u8]] = &[b".YYY.", b"...Y.", b"..Y..", b"..Y.."];
let bx = anchor.x + 1;
let by = anchor.y.saturating_sub(5) & !1u16;
for (dy, row) in GLYPH.iter().enumerate() {
for (dx, byte) in row.iter().enumerate() {
if *byte != b'Y' {
continue;
}
let px = bx + dx as u16;
let py = by + dy as u16;
if px < buf.width && py < buf.height {
buf.put(px, py, fg);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn theme() -> &'static Theme {
crate::tui::theme::theme_by_name("normal").expect("normal theme")
}
fn render(head: Point, phase_ms: u64) -> RgbBuffer {
let mut buf = RgbBuffer::filled(64, 64, Rgb { r: 0, g: 0, b: 0 });
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(phase_ms);
paint_sleep_z(&mut buf, head, now, 0, theme());
buf
}
fn lum(c: Rgb) -> u32 {
c.r as u32 + c.g as u32 + c.b as u32
}
fn top_lit(buf: &RgbBuffer, head: Point, bg: Rgb) -> Option<(u16, Rgb)> {
let zx = head.x + 5;
(0..head.y).find_map(|y| {
let p = buf.get(zx, y);
(p != bg).then_some((y, p))
})
}
#[test]
fn sleep_z_dims_as_it_rises_then_rests() {
let head = Point { x: 20, y: 30 };
let bg = Rgb { r: 0, g: 0, b: 0 };
let zx = head.x + 5;
let low = render(head, 200);
let low_px = low.get(zx, head.y - 3);
assert!(lum(low_px) > 0, "z near the head is visible");
let high = render(head, 1600);
let (top_y, top_px) = top_lit(&high, head, bg).expect("risen z still visible");
assert!(top_y < head.y - 3, "z rose above its spawn row");
assert!(
lum(top_px) < lum(low_px),
"a higher z must be dimmer than one at the head"
);
let resting = render(head, 2300);
for y in 0..resting.height {
for x in 0..resting.width {
assert_eq!(resting.get(x, y), bg, "no z during the rest gap");
}
}
}
}