use std::time::SystemTime;
use pixtuoid_core::sprite::{Rgb, RgbBuffer};
use crate::tui::pixel_painter::epoch_ms;
use crate::tui::pixel_painter::palette::blend_rgb;
use crate::tui::theme::Theme;
#[derive(Clone, Copy)]
pub(in crate::tui::pixel_painter) struct Ellipse {
pub cx: u16,
pub cy: u16,
pub half_w: u16,
pub half_h: u16,
}
fn paint_ellipse_blend(buf: &mut RgbBuffer, e: Ellipse, strength: f32, color: Rgb) {
if e.half_w == 0 || e.half_h == 0 || strength <= 0.0 {
return;
}
let min_x = e.cx.saturating_sub(e.half_w);
let max_x = (e.cx + e.half_w).min(buf.width);
let min_y = e.cy.saturating_sub(e.half_h);
let max_y = (e.cy + e.half_h).min(buf.height);
for y in min_y..max_y {
for x in min_x..max_x {
let nx = (x as f32 - e.cx as f32) / e.half_w as f32;
let ny = (y as f32 - e.cy as f32) / e.half_h as f32;
let r2 = nx * nx + ny * ny;
if r2 > 1.0 {
continue;
}
let t = (1.0 - r2) * strength;
let cur = buf.get(x, y);
buf.put(x, y, blend_rgb(cur, color, t));
}
}
}
pub(in crate::tui::pixel_painter) fn paint_ceiling_pool(
buf: &mut RgbBuffer,
ellipse: Ellipse,
strength: f32,
theme: &Theme,
) {
paint_ellipse_blend(buf, ellipse, strength, theme.lighting.ceiling_pool);
}
pub(in crate::tui::pixel_painter) fn paint_floor_lamp_halo(
buf: &mut RgbBuffer,
cx: u16,
cy: u16,
strength: f32,
theme: &Theme,
) {
let warm = theme.lighting.floor_lamp_halo;
const RADIUS: u16 = 11;
if strength <= 0.0 {
return;
}
let min_x = cx.saturating_sub(RADIUS);
let max_x = (cx + RADIUS).min(buf.width);
let min_y = cy.saturating_sub(RADIUS);
let max_y = (cy + RADIUS).min(buf.height);
let r2max = (RADIUS as f32) * (RADIUS as f32);
for y in min_y..max_y {
for x in min_x..max_x {
let dx = x as f32 - cx as f32;
let dy = y as f32 - cy as f32;
let r2 = dx * dx + dy * dy;
if r2 > r2max {
continue;
}
let t = (1.0 - (r2 / r2max).sqrt()) * strength;
let cur = buf.get(x, y);
buf.put(x, y, blend_rgb(cur, warm, t));
}
}
}
pub(in crate::tui::pixel_painter) fn paint_neon_panel(
buf: &mut RgbBuffer,
x: u16,
y: u16,
w: u16,
h: u16,
now: SystemTime,
theme: &Theme,
) {
let elapsed_ms = epoch_ms(now);
let pulse = 0.7 + 0.3 * ((elapsed_ms as f32 / 1200.0).sin() * 0.5 + 0.5);
let panel_bg = theme.office.neon_panel_bg;
let base = theme.office.neon_frame_base;
let clamp = |v: f32| v.clamp(0.0, 255.0) as u8;
let frame_color = Rgb {
r: clamp(base.r as f32 + 25.0 * pulse),
g: clamp(base.g as f32 + 50.0 * pulse),
b: clamp(base.b as f32 + 50.0 * pulse),
};
for dy in 0..h {
for dx in 0..w {
let px = x + dx;
let py = y + dy;
if px >= buf.width || py >= buf.height {
continue;
}
let on_border = dx == 0 || dx == w - 1 || dy == 0 || dy == h - 1;
if on_border {
buf.put(px, py, frame_color);
} else {
buf.put(px, py, panel_bg);
}
}
}
}
pub(in crate::tui::pixel_painter) fn paint_clock(
buf: &mut RgbBuffer,
x: u16,
y: u16,
now: SystemTime,
theme: &Theme,
) {
let rim = theme.office.clock_rim;
let face = theme.office.clock_face;
let hand_color = theme.office.clock_hand;
let hand_min = hand_color;
let rows: &[&[u8]] = &[
b"..RRR..", b".RFFFR.", b"RFFFFFR", b"RFFFFFR", b"RFFFFFR", b".RFFFR.", b"..RRR..",
];
for (dy, row) in rows.iter().enumerate() {
for (dx, ch) in row.iter().enumerate() {
let c = match ch {
b'R' => rim,
b'F' => face,
_ => continue,
};
let px = x + dx as u16;
let py = y + dy as u16;
if px < buf.width && py < buf.height {
buf.put(px, py, c);
}
}
}
let unix_now = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let local = chrono::DateTime::<chrono::Local>::from(std::time::UNIX_EPOCH + unix_now);
use chrono::Timelike;
let hour = local.hour() % 12;
let minute = local.minute();
let hour_turns = (hour as f32 + minute as f32 / 60.0) / 12.0;
let min_turns = minute as f32 / 60.0;
let put = |buf: &mut RgbBuffer, ox: i32, oy: i32, color: Rgb| {
let px = x as i32 + 3 + ox;
let py = y as i32 + 3 + oy;
if px >= 0 && py >= 0 && (px as u16) < buf.width && (py as u16) < buf.height {
buf.put(px as u16, py as u16, color);
}
};
put(buf, 0, 0, hand_color);
let (hdx, hdy) = octant_offset(hour_turns);
put(buf, hdx, hdy, hand_color);
let (mdx, mdy) = octant_offset(min_turns);
let max_step = if mdx != 0 && mdy != 0 { 1 } else { 2 };
for step in 1..=max_step {
put(buf, mdx * step, mdy * step, hand_min);
}
}
fn octant_offset(turn: f32) -> (i32, i32) {
let oct = ((turn * 8.0).round() as i32).rem_euclid(8);
match oct {
0 => (0, -1),
1 => (1, -1),
2 => (1, 0),
3 => (1, 1),
4 => (0, 1),
5 => (-1, 1),
6 => (-1, 0),
7 => (-1, -1),
_ => (0, 0),
}
}
pub(in crate::tui::pixel_painter) fn paint_corridor_runner(
buf: &mut RgbBuffer,
rect: crate::tui::layout::Bounds,
theme: &Theme,
) {
let runner_base = theme.office.runner_base;
let runner_stripe = theme.office.runner_stripe;
let runner_edge = theme.office.runner_edge;
let max_x = (rect.x + rect.width).min(buf.width);
let max_y = (rect.y + rect.height).min(buf.height);
for y in rect.y..max_y {
for x in rect.x..max_x {
let is_edge = y == rect.y || y + 1 == max_y;
let is_inner_edge = y == rect.y + 1 || y + 2 == max_y;
let dy = (y - rect.y) as i32;
let dx = (x - rect.x) as i32;
let diamond = ((dx + dy) % 6 == 0) || ((dx - dy).rem_euclid(6) == 0);
let color = if is_edge {
runner_edge
} else if is_inner_edge || diamond {
runner_stripe
} else {
runner_base
};
buf.put(x, y, color);
}
}
}
pub(in crate::tui::pixel_painter) fn paint_shadow(
buf: &mut RgbBuffer,
ellipse: Ellipse,
strength: f32,
theme: &Theme,
) {
paint_ellipse_blend(buf, ellipse, strength, theme.office.shadow);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ellipse_blend_degenerate_is_a_noop() {
let theme = &crate::tui::theme::NORMAL;
let fill = Rgb {
r: 30,
g: 30,
b: 30,
};
let mut buf = RgbBuffer::filled(20, 20, fill);
paint_ellipse_blend(
&mut buf,
Ellipse {
cx: 10,
cy: 10,
half_w: 0,
half_h: 5,
},
0.8,
theme.lighting.ceiling_pool,
);
for y in 0..buf.height {
for x in 0..buf.width {
assert_eq!(buf.get(x, y), fill, "half_w==0 must paint nothing");
}
}
let mut buf = RgbBuffer::filled(20, 20, fill);
paint_ellipse_blend(
&mut buf,
Ellipse {
cx: 10,
cy: 10,
half_w: 5,
half_h: 5,
},
0.0,
theme.lighting.ceiling_pool,
);
for y in 0..buf.height {
for x in 0..buf.width {
assert_eq!(buf.get(x, y), fill, "strength<=0 must paint nothing");
}
}
}
#[test]
fn ellipse_blend_paints_when_valid() {
let theme = &crate::tui::theme::NORMAL;
let fill = Rgb {
r: 30,
g: 30,
b: 30,
};
let mut buf = RgbBuffer::filled(20, 20, fill);
paint_ellipse_blend(
&mut buf,
Ellipse {
cx: 10,
cy: 10,
half_w: 5,
half_h: 5,
},
0.9,
theme.lighting.ceiling_pool,
);
assert_ne!(buf.get(10, 10), fill, "the ellipse centre must be tinted");
}
#[test]
fn neon_panel_off_edge_does_not_panic() {
let theme = &crate::tui::theme::NORMAL;
let now = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(5);
let mut buf = RgbBuffer::filled(10, 10, Rgb { r: 0, g: 0, b: 0 });
paint_neon_panel(&mut buf, 8, 8, 6, 5, now, theme);
assert_ne!(
buf.get(8, 8),
Rgb { r: 0, g: 0, b: 0 },
"in-bounds frame paints"
);
}
}