use ratatui::{
style::{Color, Style},
text::{Line, Span},
};
type PixBuf = Vec<Vec<Option<Color>>>;
fn new_buf(pw: usize, ph: usize) -> PixBuf {
vec![vec![None; pw]; ph]
}
fn px(buf: &PixBuf, x: usize, y: usize) -> Option<Color> {
buf.get(y).and_then(|r| r.get(x)).copied().flatten()
}
fn lum(c: Option<Color>) -> f32 {
match c {
Some(Color::Rgb(r, g, b)) => 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32,
_ => 0.0,
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum RenderMode { Half, Quarter, Braille }
impl RenderMode {
fn next(self) -> Self {
match self { Self::Half => Self::Quarter, Self::Quarter => Self::Braille, Self::Braille => Self::Half }
}
fn prev(self) -> Self {
match self { Self::Half => Self::Braille, Self::Quarter => Self::Half, Self::Braille => Self::Quarter }
}
fn pw(self, char_w: usize) -> usize {
match self { Self::Half => char_w, Self::Quarter | Self::Braille => char_w * 2 }
}
fn ph(self, char_h: usize) -> usize {
match self { Self::Half | Self::Quarter => char_h * 2, Self::Braille => char_h * 4 }
}
}
fn render_half(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
to_lines(char_w, char_h, |col, row| {
let t = px(buf, col, row * 2);
let b = px(buf, col, row * 2 + 1);
match (t, b) {
(None, None ) => (' ', Style::new()),
(Some(c), None ) => ('▀', Style::new().fg(c)),
(None, Some(c)) => ('▄', Style::new().fg(c)),
(Some(a), Some(b)) if a == b => ('█', Style::new().fg(a)),
(Some(a), Some(b)) => ('▀', Style::new().fg(a).bg(b)),
}
})
}
const Q: [char; 16] = [
' ', '▗', '▖', '▄', '▝', '▐', '▞', '▟',
'▘', '▚', '▌', '▙', '▀', '▜', '▛', '█',
];
fn render_quarter(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
to_lines(char_w, char_h, |col, row| {
let tl = px(buf, col*2, row*2);
let tr = px(buf, col*2+1, row*2);
let bl = px(buf, col*2, row*2+1);
let br = px(buf, col*2+1, row*2+1);
let pixels = [tl, tr, bl, br];
let ls = pixels.map(lum);
let mut s = ls;
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let thresh = (s[1] + s[2]) / 2.0;
let on = ls.map(|l| l > thresh || (l == thresh && l > 0.0));
let idx = (on[0] as usize)<<3 | (on[1] as usize)<<2 | (on[2] as usize)<<1 | on[3] as usize;
let ch = Q[idx];
let (mut fr, mut fg, mut fb, mut fn_) = (0u32, 0, 0, 0);
let (mut br_, mut bg, mut bb, mut bn) = (0u32, 0, 0, 0);
for (i, c) in pixels.iter().enumerate() {
if let Some(Color::Rgb(r, g, b)) = c {
if on[i] { fr += *r as u32; fg += *g as u32; fb += *b as u32; fn_ += 1; }
else { br_ += *r as u32; bg += *g as u32; bb += *b as u32; bn += 1; }
}
}
let style = match (fn_ > 0, bn > 0) {
(true, true ) => Style::new()
.fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8))
.bg(Color::Rgb((br_/bn) as u8, (bg/bn) as u8, (bb/bn) as u8)),
(true, false) => Style::new()
.fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8)),
(false, true ) => Style::new()
.bg(Color::Rgb((br_/bn) as u8, (bg/bn) as u8, (bb/bn) as u8)),
(false, false) => Style::new(),
};
(ch, style)
})
}
fn render_braille(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
const BIT: [[u8; 2]; 4] = [
[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80], ];
to_lines(char_w, char_h, |col, row| {
let mut mask: u8 = 0;
let mut fr = 0u32; let mut fg = 0u32; let mut fb = 0u32; let mut fn_ = 0u32;
let mut dr = 0u32; let mut dg = 0u32; let mut db = 0u32; let mut dn = 0u32;
let mut lums = [0f32; 8];
let mut cols = [None::<Color>; 8];
let mut k = 0;
for dy in 0..4usize {
for dx in 0..2usize {
let c = px(buf, col*2+dx, row*4+dy);
lums[k] = lum(c);
cols[k] = c;
k += 1;
}
}
let mut s = lums;
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let thresh = (s[3] + s[4]) / 2.0;
for dy in 0..4usize {
for dx in 0..2usize {
let i = dy * 2 + dx;
let c = cols[i];
if let Some(Color::Rgb(r, g, b)) = c {
dr += r as u32; dg += g as u32; db += b as u32; dn += 1;
}
if lums[i] > thresh || (lums[i] == thresh && lums[i] > 0.0) {
mask |= BIT[dy][dx];
if let Some(Color::Rgb(r, g, b)) = c {
fr += r as u32; fg += g as u32; fb += b as u32; fn_ += 1;
}
}
}
}
let ch = char::from_u32(0x2800 + mask as u32).unwrap_or(' ');
let style = if fn_ > 0 && dn > 0 {
let fg_c = Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8);
let bg_c = Color::Rgb(
((dr/dn) as u16).saturating_sub(30) as u8,
((dg/dn) as u16).saturating_sub(30) as u8,
((db/dn) as u16).saturating_sub(30) as u8,
);
Style::new().fg(fg_c).bg(bg_c)
} else if fn_ > 0 {
Style::new().fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8))
} else {
Style::new()
};
(ch, style)
})
}
fn to_lines(
char_w: usize,
char_h: usize,
mut cell: impl FnMut(usize, usize) -> (char, Style),
) -> Vec<Line<'static>> {
(0..char_h).map(|row| {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut run = String::new();
let mut cur = Style::new();
for col in 0..char_w {
let (ch, sty) = cell(col, row);
if sty == cur { run.push(ch); }
else {
if !run.is_empty() { spans.push(Span::styled(run.clone(), cur)); run.clear(); }
cur = sty;
run.push(ch);
}
}
if !run.is_empty() { spans.push(Span::styled(run, cur)); }
Line::from(spans)
}).collect()
}
fn set_px(buf: &mut PixBuf, x: isize, y: isize, c: Color) {
if y >= 0 && x >= 0 {
if let Some(row) = buf.get_mut(y as usize) {
if let Some(cell) = row.get_mut(x as usize) {
*cell = Some(c);
}
}
}
}
fn fill_r(buf: &mut PixBuf, x: isize, y: isize, w: usize, h: usize, c: Color) {
for dy in 0..h as isize {
for dx in 0..w as isize {
set_px(buf, x + dx, y + dy, c);
}
}
}
fn hash(mut x: u64) -> u64 {
x ^= x >> 30;
x = x.wrapping_mul(0xbf58476d1ce4e5b9);
x ^= x >> 27;
x = x.wrapping_mul(0x94d049bb133111eb);
x ^ (x >> 31)
}
fn dispatch(mode: RenderMode, buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
match mode {
RenderMode::Half => render_half(buf, char_w, char_h),
RenderMode::Quarter => render_quarter(buf, char_w, char_h),
RenderMode::Braille => render_braille(buf, char_w, char_h),
}
}
fn draw_mountains_and_buildings(buf: &mut PixBuf, pw: usize, ph: usize) {
let ground_y = ph * 88 / 100;
let sky_col = |py: usize| {
let frac = py as f64 / ground_y as f64;
Color::Rgb((8.0 + frac * 6.0) as u8, (12.0 + frac * 8.0) as u8, (22.0 + frac * 12.0) as u8)
};
let m_base = ph * 72 / 100;
for ppx in 0..pw {
let xf = ppx as f64 / pw as f64;
let r1 = ((xf * std::f64::consts::PI * 2.8).sin() * 0.5 + 0.5) * ph as f64 * 0.28;
let r2 = ((xf * std::f64::consts::PI * 1.5 + 1.0).sin() * 0.5 + 0.5) * ph as f64 * 0.14;
let ridge = (r1 + r2) as usize;
let top = m_base.saturating_sub(ridge);
for py in top..m_base.min(ground_y) {
let d = (py - top) as f64 / ridge.max(1) as f64;
buf[py][ppx] = Some(Color::Rgb(
(38.0 + d * 12.0) as u8,
(45.0 + d * 14.0) as u8,
(72.0 + d * 20.0) as u8,
));
}
}
let win = Color::Rgb(210, 175, 85);
let win_dim = Color::Rgb(125, 103, 42);
let beam = Color::Rgb(48, 32, 16); let mortar = Color::Rgb(88, 82, 74); let spire_c = (82u8, 77u8, 70u8); let plant = Color::Rgb(42, 95, 35);
let pot = Color::Rgb(140, 72, 32);
let bldgs: &[(f64, f64, f64, u8)] = &[
(0.00, 0.07, 0.40, 0),
(0.06, 0.13, 0.26, 1),
(0.17, 0.06, 0.55, 2),
(0.22, 0.14, 0.30, 1),
(0.35, 0.07, 0.46, 0),
(0.41, 0.16, 0.23, 1),
(0.57, 0.06, 0.44, 2),
(0.62, 0.11, 0.32, 1),
(0.72, 0.07, 0.48, 0),
(0.78, 0.13, 0.27, 1),
(0.90, 0.10, 0.42, 0),
];
let b_base = ph * 78 / 100;
for &(xf, wf, hf, style) in bldgs {
let bx = (xf * pw as f64) as isize;
let bw = ((wf * pw as f64) as usize).max(3);
let bh = ((hf * ph as f64) as usize).max(4);
let by = b_base.saturating_sub(bh) as isize;
let wall_bottom = ground_y.min(ph);
match style {
0 => {
for py in (by as usize)..wall_bottom {
for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
let v = hash(ppx as u64 * 3 + py as u64 * 7 + 101);
let row = py.wrapping_sub(by as usize);
let is_mortar = row % 3 == 0 || ppx % 4 == (if (row / 3) % 2 == 0 { 0 } else { 2 });
buf[py][ppx] = Some(if is_mortar {
mortar
} else {
Color::Rgb(
158u8.saturating_add((v % 18) as u8),
150u8.saturating_add(((v >> 4) % 14) as u8),
136u8.saturating_add(((v >> 8) % 11) as u8),
)
});
}
}
let merlon_w = (bw / 4).max(1);
let crenel_h = (bh / 5).max(1);
let mut toggle = true;
let mut cx = bx;
while cx < bx + bw as isize {
let seg_end = (cx + merlon_w as isize).min(bx + bw as isize);
if !toggle {
for py in (by - crenel_h as isize)..by {
for ppx in cx..seg_end {
if py >= 0 && ppx >= 0 && (ppx as usize) < pw && (py as usize) < ph {
buf[py as usize][ppx as usize] = Some(sky_col(py as usize));
}
}
}
}
toggle = !toggle;
cx = seg_end;
}
if bw >= 4 && bh >= 6 {
let wx = bx + bw as isize / 2;
let wy = by + bh as isize / 3;
set_px(buf, wx, wy, win);
set_px(buf, wx, wy + 1, win_dim);
}
}
2 => {
for py in (by as usize)..wall_bottom {
for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
let v = hash(ppx as u64 * 5 + py as u64 * 11 + 303);
buf[py][ppx] = Some(Color::Rgb(
spire_c.0.saturating_add((v % 10) as u8),
spire_c.1.saturating_add(((v >> 4) % 8) as u8),
spire_c.2.saturating_add(((v >> 8) % 7) as u8),
));
}
}
let spire_h = (bh * 3 / 4) as isize;
let half_bw = (bw as isize / 2).max(1);
let cx = bx + bw as isize / 2;
for dy in 0..spire_h {
let hw = (half_bw as f64 * (spire_h - dy) as f64 / spire_h as f64 * 0.6) as isize;
for dx in -hw..=hw {
let v = hash((cx + dx).unsigned_abs() as u64 * 7 + (by - spire_h + dy).unsigned_abs() as u64 * 13 + 404);
set_px(buf, cx + dx, by - spire_h + dy, Color::Rgb(
spire_c.0.saturating_add((v % 8) as u8),
spire_c.1.saturating_add(((v >> 4) % 6) as u8),
spire_c.2.saturating_add(((v >> 8) % 5) as u8),
));
}
}
if bw >= 3 && bh >= 8 {
set_px(buf, bx + bw as isize / 2, by + bh as isize * 2 / 5, win);
}
}
_ => {
let bseed = hash(bx.unsigned_abs() as u64 * 31 + 999);
let br_off = (bseed % 30) as u8;
let bg_off = ((bseed >> 4) % 22) as u8;
let bb_off = ((bseed >> 8) % 18) as u8;
for py in (by as usize)..wall_bottom {
for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
let v = hash(ppx as u64 * 5 + py as u64 * 9 + 202);
buf[py][ppx] = Some(Color::Rgb(
(175u8.saturating_add(br_off)).saturating_add((v % 12) as u8),
(165u8.saturating_add(bg_off)).saturating_add(((v >> 4) % 10) as u8),
(148u8.saturating_add(bb_off)).saturating_add(((v >> 8) % 8) as u8),
));
}
}
for &dx in &[0isize, bw as isize / 3, bw as isize * 2 / 3, bw as isize - 1] {
for py in (by as usize)..wall_bottom {
set_px(buf, bx + dx, py as isize, beam);
}
}
for &dy in &[0isize, bh as isize / 3, bh as isize * 2 / 3] {
for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
set_px(buf, ppx as isize, by + dy, beam);
}
}
let roof_h = (bh / 3).max(2) as isize;
let cx_r = bx + bw as isize / 2;
for dy in 0..roof_h {
let hw = (bw as f64 * 0.5 * (roof_h - dy) as f64 / roof_h as f64) as isize;
for dx in -hw..=hw {
let v = hash((cx_r + dx).unsigned_abs() as u64 * 3 + (by - dy).unsigned_abs() as u64 * 7 + 505);
let edge = dx.abs() == hw;
set_px(buf, cx_r + dx, by - dy, if edge {
beam
} else {
Color::Rgb(
62u8.saturating_add((v % 12) as u8),
45u8.saturating_add(((v >> 4) % 9) as u8),
30u8.saturating_add(((v >> 8) % 7) as u8),
)
});
}
}
set_px(buf, cx_r, by - roof_h, beam);
if bw >= 5 && bh >= 5 {
let wy = by + bh as isize / 3 + 1;
let step = (bw / 3).max(2) as isize;
let mut wx = bx + step / 2;
let mut pot_toggle = false;
while wx < bx + bw as isize - 1 {
set_px(buf, wx, wy, win);
set_px(buf, wx, wy + 1, win_dim);
if pot_toggle {
set_px(buf, wx, wy - 1, plant);
set_px(buf, wx, wy + 2, pot);
}
pot_toggle = !pot_toggle;
wx += step;
}
}
}
}
let door_w = (bw / 4).max(2);
let door_h = (bh / 4).max(3);
let door_x = bx + bw as isize / 2 - door_w as isize / 2;
let door_top = ground_y as isize - door_h as isize;
let door_col = Color::Rgb(14, 10, 6);
for py in door_top..ground_y as isize {
let row_from_top = (py - door_top) as usize;
for dx in 0..door_w as isize {
let is_corner = row_from_top == 0 && (dx == 0 || dx == door_w as isize - 1);
if !is_corner {
set_px(buf, door_x + dx, py, door_col);
}
}
}
}
}
fn trunk_half_w(t: f64, pw: usize) -> isize {
((0.11 + t * 0.04) * pw as f64) as isize
}
fn draw_background_trees(buf: &mut PixBuf, pw: usize, ph: usize) {
let ground_y = ph * 85 / 100;
let trunk_col = Color::Rgb(32, 18, 7);
let trees: &[(f64, f64, f64, usize)] = &[
(0.07, 0.52, 0.11, 0),
(0.17, 0.44, 0.09, 1),
(0.27, 0.62, 0.13, 2),
(0.73, 0.48, 0.10, 2),
(0.83, 0.58, 0.12, 1),
(0.93, 0.40, 0.08, 0),
];
let palettes: &[[Color; 3]] = &[
[Color::Rgb(180, 48, 8), Color::Rgb(200, 70, 12), Color::Rgb(155, 35, 5) ],
[Color::Rgb(195, 120, 0), Color::Rgb(215, 148, 8), Color::Rgb(170, 98, 0)],
[Color::Rgb(162, 72, 15), Color::Rgb(185, 92, 20), Color::Rgb(140, 55, 10)],
];
for &(xf, hf, rf, pal) in trees {
let tx = (xf * pw as f64) as isize;
let trunk_h = (hf * ph as f64) as usize;
let top_y = ground_y.saturating_sub(trunk_h) as isize;
let cr = ((rf * pw as f64).max(3.0)) as isize;
let cy = top_y - cr / 2;
let cols = &palettes[pal % palettes.len()];
let tw = (cr / 3).max(2);
for py in top_y..ground_y as isize {
for dx in -tw..=tw {
set_px(buf, tx + dx, py, trunk_col);
}
}
for dy in -cr..=cr {
for dx in -cr * 2..=cr * 2 {
let ex = dx / 2;
if ex * ex + dy * dy <= cr * cr {
let v = hash((tx as u64).wrapping_add(500).wrapping_add(((cy + dy) as u64).wrapping_mul(83)).wrapping_add((dx as u64).wrapping_mul(17)));
if v % 8 > 1 {
let c = cols[(v % 3) as usize];
set_px(buf, tx + dx, cy + dy, c);
}
}
}
}
}
}
fn draw_moss(buf: &mut PixBuf, pw: usize, ph: usize) {
let ground_y = ph * 85 / 100;
let cx = pw as isize / 2;
let moss_dk = Color::Rgb(20, 42, 16);
let moss_mid = Color::Rgb(30, 58, 23);
let moss_lt = Color::Rgb(42, 75, 32);
for py in ground_y..ph {
let t = py as f64 / ph as f64;
let hw = trunk_half_w(t, pw) as f64;
for ppx in 0..pw {
let dist = (ppx as isize - cx).abs() as f64;
let beyond = dist - hw;
if beyond > 0.0 && beyond < pw as f64 * 0.28 {
let proximity = 1.0 - (beyond / (pw as f64 * 0.28)).min(1.0);
let v = hash((ppx as u64 * 7).wrapping_add(py as u64 * 11).wrapping_add(888));
if ((v % 10) as f64) < 2.5 + proximity * 5.5 {
buf[py][ppx] = Some(match v % 3 {
0 => moss_dk,
1 => moss_mid,
_ => moss_lt,
});
}
}
}
}
}
fn draw_maple_tree(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
let cx = pw as isize / 2;
let very_dark = Color::Rgb(25, 13, 5);
let dark = Color::Rgb(42, 24, 9);
let mid = Color::Rgb(58, 34, 14);
let light = Color::Rgb(74, 46, 20);
for py in 0..ph {
let t = py as f64 / ph as f64;
let hw = trunk_half_w(t, pw);
let lj = (hash(py as u64 * 7 + 13) % 3) as isize - 1;
let rj = (hash(py as u64 * 11 + 29) % 3) as isize - 1;
let left = cx - hw + lj;
let right = cx + hw + rj;
for ppx in left..right {
let edge_d = (ppx - left).min(right - ppx);
let stripe = hash((ppx as u64).wrapping_mul(3).wrapping_add(py as u64 / 4 * 7) + 42) % 10;
let c = if edge_d <= 1 { very_dark }
else if stripe == 0 { very_dark }
else if stripe <= 3 { dark }
else if stripe <= 6 { mid }
else { light };
set_px(buf, ppx, py as isize, c);
}
}
for k in 0..3usize {
let ky = (ph * (k + 1) / 4) as isize;
let kx = cx + [-4isize, 5, -3][k];
fill_r(buf, kx - 2, ky - 1, 5, 3, very_dark);
set_px(buf, kx, ky, Color::Rgb(15, 8, 3));
}
let rope_y = (ph * 30 / 100) as isize;
let rope_hw = trunk_half_w(0.30, pw) + 1;
let rope_l = cx - rope_hw;
let rope_r = cx + rope_hw;
let rope_col = Color::Rgb(185, 155, 80);
let rope_dk = Color::Rgb(140, 115, 55);
for ppx in rope_l..rope_r {
let alt = (ppx % 3 == 0) as isize;
set_px(buf, ppx, rope_y + alt, rope_col);
set_px(buf, ppx, rope_y + 2 + alt, rope_dk);
}
let shide_col = Color::Rgb(235, 230, 218);
let shide_dk = Color::Rgb(195, 190, 178);
let shide_h = (ph / 10).max(5);
for i in 0..7usize {
let sx = rope_l + (rope_r - rope_l) * i as isize / 6;
let sway = ((tick as f64 * 0.015 + i as f64 * 1.1).sin() * 1.2) as isize;
let base_x = sx + sway;
for dy in 0..shide_h as isize {
let ox = if (dy * 3 / shide_h as isize) % 2 == 0 { 0isize } else { 1 };
let c = if dy % 2 == 0 { shide_col } else { shide_dk };
set_px(buf, base_x + ox, rope_y + 4 + dy, c);
set_px(buf, base_x + ox + 1, rope_y + 4 + dy, c);
}
}
}
fn draw_boat(buf: &mut PixBuf, pw: usize, ph: usize, t: f64) {
let horizon = ph * 3 / 10;
let bcx = pw / 3;
let w1 = (bcx as f64 * 0.35 + t).sin() * 2.5;
let w2 = (bcx as f64 * 0.18 - t * 0.65).sin() * 1.5;
let water_surf = horizon as f64 + w1 + w2;
let water_y = (water_surf + ph as f64 * 0.15).max(horizon as f64 + ph as f64 * 0.12);
let w1r = ((bcx + 4) as f64 * 0.35 + t).sin() * 2.5;
let w2r = ((bcx + 4) as f64 * 0.18 - t * 0.65).sin() * 1.5;
let slope = ((horizon as f64 + w1r + w2r) - water_surf) / 4.0;
let half_deck = (pw / 12).max(3) as isize;
let hull_h = (ph / 12).max(2) as isize;
let hull_dark = Color::Rgb(55, 35, 16);
let hull_mid = Color::Rgb(78, 52, 24);
let deck_col = Color::Rgb(105, 72, 36);
let mast_col = Color::Rgb(70, 46, 20);
let sail_col = Color::Rgb(218, 210, 192);
let half_keel = half_deck / 2;
for row in 0..hull_h {
let frac = row as f64 / hull_h as f64;
let hw = (half_deck as f64 - frac * (half_deck - half_keel) as f64) as isize;
let tilt_off = (slope * (row as f64 - hull_h as f64 / 2.0)) as isize;
let deck_y = water_y as isize - hull_h + row;
for dx in -hw..=hw {
let c = if row == 0 { deck_col }
else if dx == -hw || dx == hw { hull_dark }
else { hull_mid };
set_px(buf, bcx as isize + dx + tilt_off, deck_y, c);
}
}
let mast_base = water_y as isize - hull_h;
let mast_h = hull_h * 3;
for dy in 0..mast_h { set_px(buf, bcx as isize, mast_base - dy, mast_col); }
let sail_h = mast_h * 2 / 3;
for dy in 0..sail_h {
let sail_w = (half_deck as f64 * (sail_h - dy) as f64 / sail_h as f64) as isize;
for dx in 1..sail_w { set_px(buf, bcx as isize + dx, mast_base - dy, sail_col); }
}
}
fn draw_fireplace(buf: &mut PixBuf, pw: usize, ph: usize) {
let side_w = (pw * 19 / 100).max(4);
let mantel_h = (ph * 11 / 100).max(2);
let hearth_y = ph.saturating_sub((ph * 9 / 100).max(2));
let fire_x0 = side_w;
let fire_x1 = pw.saturating_sub(side_w);
let mortar = Color::Rgb(52, 47, 42);
let brick_h = (ph / 18).max(2);
let brick_w = (side_w / 3).max(3);
for side in 0..2usize {
let xs = if side == 0 { 0 } else { pw.saturating_sub(side_w) };
for py in mantel_h..hearth_y {
for ppx in xs..(xs + side_w).min(pw) {
let row = (py - mantel_h) / brick_h;
let off = if row % 2 == 0 { 0 } else { brick_w / 2 };
let col_b = (ppx - xs + off) % brick_w;
let row_b = (py - mantel_h) % brick_h;
let v = hash(((row * 13 + (ppx - xs + off) / brick_w) as u64).wrapping_add(77));
let c = if row_b == 0 || col_b == 0 {
mortar
} else if row_b == 1 {
Color::Rgb(
90u8.saturating_add((v % 15) as u8),
45u8.saturating_add(((v >> 4) % 10) as u8),
30u8.saturating_add(((v >> 8) % 8) as u8),
)
} else {
Color::Rgb(
135u8.saturating_add((v % 25) as u8),
72u8.saturating_add(((v >> 4) % 18) as u8),
52u8.saturating_add(((v >> 8) % 14) as u8),
)
};
if py < buf.len() && ppx < buf[0].len() { buf[py][ppx] = Some(c); }
}
}
}
for py in 0..mantel_h {
for ppx in 0..pw {
if py >= buf.len() || ppx >= buf[0].len() { continue; }
let grain = hash((ppx as u64 / 2).wrapping_add((py as u64 * 3).wrapping_mul(17)));
let grain_line = (py * pw + ppx / 3) % 7 == 0;
let (r, g, b) = if py == mantel_h - 1 {
(52u8, 31u8, 12u8) } else if grain_line {
(72u8.saturating_add((grain % 10) as u8), 43u8.saturating_add((grain % 8) as u8), 16u8.saturating_add((grain % 5) as u8))
} else {
(88u8.saturating_add((grain % 18) as u8), 54u8.saturating_add((grain % 13) as u8), 22u8.saturating_add((grain % 9) as u8))
};
buf[py][ppx] = Some(Color::Rgb(r, g, b));
}
}
let carpet_x0 = fire_x0.saturating_sub(fire_x0 / 3);
let carpet_x1 = (fire_x1 + (pw - fire_x1) / 3).min(pw);
for py in hearth_y..ph {
for ppx in 0..pw {
if py >= buf.len() || ppx >= buf[0].len() { continue; }
let on_carpet = ppx >= carpet_x0 && ppx < carpet_x1;
if on_carpet {
let cx = ppx - carpet_x0;
let cy = py - hearth_y;
let cw = carpet_x1 - carpet_x0;
let ch = ph - hearth_y;
let border = cx == 0 || cx == cw.saturating_sub(1) || cy == 0 || cy == ch.saturating_sub(1);
let inner_border = cx == 2 || cx == cw.saturating_sub(3) || cy == 2 || cy == ch.saturating_sub(3);
let v = hash(ppx as u64 * 7 + py as u64 * 13 + 8888);
let noise = (v % 12) as u8;
buf[py][ppx] = Some(if border || inner_border {
Color::Rgb(130u8.saturating_add(noise / 2), 40u8.saturating_add(noise / 4), 18u8.saturating_add(noise / 4))
} else {
Color::Rgb(105u8.saturating_add(noise), 28u8.saturating_add(noise / 3), 18u8.saturating_add(noise / 3))
});
} else {
let v = hash(((ppx / 5) as u64).wrapping_add((py * 53) as u64));
buf[py][ppx] = Some(Color::Rgb(
78u8.saturating_add((v % 18) as u8),
76u8.saturating_add(((v >> 4) % 14) as u8),
72u8.saturating_add(((v >> 8) % 11) as u8),
));
}
}
}
let log_h = (ph * 5 / 100).max(2);
let log_y0 = hearth_y.saturating_sub(log_h + 1);
let fire_w = fire_x1 - fire_x0;
let log_hw = fire_w / 2;
let logs: &[(isize, u8, u8, u8)] = &[
(fire_x0 as isize + fire_w as isize / 3, 58, 32, 12),
(fire_x0 as isize + fire_w as isize * 2 / 3, 48, 26, 10),
];
for &(cx, lr, lg, lb) in logs {
for dy in 0..log_h {
let py = log_y0 + dy;
if py >= ph { continue; }
let ry = dy as f64 / log_h as f64;
let half_w = (log_hw as f64 * (1.0 - (ry * 2.0 - 1.0).powi(2)).sqrt()) as isize;
for dx in -half_w..=half_w {
let px = cx + dx;
if px < fire_x0 as isize || px as usize >= fire_x1 { continue; }
let v = hash(px as u64 * 5 + py as u64 * 11 + 3030);
let bark = (v % 3 == 0) as u8 * 12;
let ember = if dy == 0 { 30u8 } else { 0u8 };
buf[py][px as usize] = Some(Color::Rgb(
lr.saturating_add(bark).saturating_add(ember),
lg.saturating_add(bark / 2),
lb.saturating_add(bark / 3),
));
}
}
}
let grate_h = (ph * 10 / 100).max(3);
let grate_y0 = hearth_y.saturating_sub(grate_h);
let bar_w = 1usize;
let gap_w = (fire_x1 - fire_x0) / 10;
let gap_w = gap_w.max(2);
let iron_hi = Color::Rgb(48, 45, 42);
let iron_dk = Color::Rgb(22, 20, 18);
for ppx in fire_x0..fire_x1 {
if grate_y0 < buf.len() { buf[grate_y0][ppx] = Some(iron_hi); }
if hearth_y < buf.len() && hearth_y < ph { buf[hearth_y.min(ph-1)][ppx] = Some(iron_dk); }
}
let mut bx = fire_x0 + gap_w / 2;
while bx + bar_w <= fire_x1 {
for py in grate_y0..hearth_y {
if py >= buf.len() { continue; }
for dx in 0..bar_w {
let px = bx + dx;
if px < buf[0].len() {
buf[py][px] = Some(if dx == 0 { iron_hi } else { iron_dk });
}
}
}
bx += gap_w;
}
}
fn fill_waves(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
let t = tick as f64 * 0.12;
let horizon = ph * 3 / 10;
for py in 0..ph {
for ppx in 0..pw {
buf[py][ppx] = Some(if py < horizon {
let frac = py as f64 / horizon as f64;
Color::Rgb((8.0 + frac*18.0) as u8, (18.0 + frac*38.0) as u8, (55.0 + frac*75.0) as u8)
} else {
let wy = py - horizon;
let wh = ph - horizon;
let wyf = wy as f64;
let whf = wh as f64;
let depth = wyf / whf;
let w1 = (ppx as f64 * 0.35 + t).sin() * 2.5;
let w2 = (ppx as f64 * 0.18 - t * 0.65).sin() * 1.5;
let surf = (wyf - (w1 + w2)).abs();
let w3 = (ppx as f64 * 0.52 + t * 1.2).sin() * 1.8;
let w4 = (ppx as f64 * 0.27 - t * 0.85).sin() * 1.3;
let d1 = whf * 0.11;
let surf2 = (wyf - d1 - (w3 + w4)).abs();
let w5 = (ppx as f64 * 0.66 - t * 1.45).sin() * 1.4;
let w6 = (ppx as f64 * 0.15 + t * 0.42).sin() * 1.9;
let d2 = whf * 0.23;
let surf3 = (wyf - d2 - (w5 + w6 * 0.6)).abs();
let w7 = (ppx as f64 * 0.80 + t * 1.75).sin() * 1.1;
let w8 = (ppx as f64 * 0.38 - t * 0.55).sin() * 1.6;
let d3 = whf * 0.38;
let surf4 = (wyf - d3 - (w7 + w8 * 0.7)).abs();
let chop1 = (ppx as f64 * 0.9 + wyf * 0.4 + t * 2.1).sin();
let chop2 = (ppx as f64 * 0.7 - wyf * 0.35 - t * 1.8).sin();
let chop = (chop1 * chop2).abs();
let refl = (ppx as f64 * 0.13 - t * 0.28).sin()
* (ppx as f64 * 0.08 + t * 0.19).sin();
let base_g = (90.0 - depth * 50.0).max(0.0);
let base_b = (200.0 - depth * 80.0).max(0.0);
if surf < 1.3 {
Color::Rgb(205, 232, 255)
} else if surf < 2.6 {
Color::Rgb(125, 188, 232)
} else if surf2 < 1.0 {
Color::Rgb(80, 158, 218)
} else if surf2 < 2.0 {
Color::Rgb(30, 122, 200)
} else if surf3 < 0.9 {
Color::Rgb(18, 108, 185)
} else if surf3 < 1.8 {
Color::Rgb(8, 95, 172)
} else if surf4 < 0.85 && depth < 0.72 {
Color::Rgb(12, 102, 168)
} else if chop > 0.82 && depth < 0.55 {
Color::Rgb((12.0 + chop * 28.0) as u8, (base_g + chop * 22.0).min(255.0) as u8, (base_b + chop * 14.0).min(255.0) as u8)
} else if refl > 0.68 && depth < 0.42 {
Color::Rgb((18.0 + refl * 45.0) as u8, (base_g + refl * 32.0).min(255.0) as u8, (base_b + refl * 18.0).min(255.0) as u8)
} else {
Color::Rgb(0, base_g as u8, base_b as u8)
}
});
}
}
let mx = pw * 4 / 5;
let my = ph / 8;
let mr = (pw.min(ph) / 10).max(2) as f64;
for py in 0..ph {
for ppx in 0..pw {
let dx = ppx as f64 - mx as f64;
let dy = (py as f64 - my as f64) * 1.6;
if dx*dx + dy*dy < mr*mr { buf[py][ppx] = Some(Color::Rgb(255, 240, 180)); }
}
}
draw_boat(buf, pw, ph, t);
}
fn fill_rain(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
let ground_y = ph * 88 / 100;
for py in 0..ground_y {
let frac = py as f64 / ground_y as f64;
for ppx in 0..pw {
buf[py][ppx] = Some(Color::Rgb((8.0 + frac*6.0) as u8, (12.0 + frac*8.0) as u8, (22.0 + frac*12.0) as u8));
}
}
let cobble_h = (ph / 22).max(2);
let cobble_w = (pw / 14).max(3);
for py in ground_y..ph {
for ppx in 0..pw {
let row = (py - ground_y) / cobble_h;
let offset = if row % 2 == 0 { 0 } else { cobble_w / 2 };
let col_b = (ppx + offset) % cobble_w;
let row_b = (py - ground_y) % cobble_h;
buf[py][ppx] = Some(if row_b == 0 || col_b == 0 {
Color::Rgb(14, 16, 14) } else {
let v = hash(((row * 89 + (ppx + offset) / cobble_w) as u64).wrapping_add(301));
Color::Rgb(
42u8.saturating_add((v % 22) as u8),
46u8.saturating_add(((v >> 4) % 20) as u8),
40u8.saturating_add(((v >> 8) % 16) as u8),
)
});
}
}
draw_mountains_and_buildings(buf, pw, ph);
let speed = 3usize;
let spacing = 12usize;
for ppx in 0..pw {
let col_offset = (hash(ppx as u64 + 7) % spacing as u64) as usize;
let streak = 3 + (hash(ppx as u64 + 99) % 4) as usize;
let base = (tick as usize * speed + col_offset * ph / spacing) % ph;
for s in 0..streak {
let py = (base + s) % ph;
if py < ground_y {
let alpha = 1.0 - s as f64 / streak as f64;
buf[py][ppx] = Some(Color::Rgb((80.0*alpha) as u8, (130.0*alpha) as u8, (255.0*alpha) as u8));
}
}
}
for py in ground_y..ph {
for ppx in 0..pw {
let ripple = (ppx as f64 * 0.4 + tick as f64 * 0.15).sin();
if ripple > 0.7 { buf[py][ppx] = Some(Color::Rgb(28, 52, 75)); }
}
}
}
fn fill_leaves(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
let ground_y = ph * 85 / 100;
for py in 0..ph {
for ppx in 0..pw {
buf[py][ppx] = Some(if py < ground_y {
let f = py as f64 / ground_y as f64;
Color::Rgb((35.0 + f*12.0) as u8, (28.0 + f*10.0) as u8, (20.0 + f*6.0) as u8)
} else {
let f = (py - ground_y) as f64 / (ph - ground_y) as f64;
Color::Rgb((45.0 + f*15.0) as u8, (30.0 + f*10.0) as u8, (15.0 + f*5.0) as u8)
});
}
}
draw_background_trees(buf, pw, ph);
draw_moss(buf, pw, ph);
draw_maple_tree(buf, pw, ph, tick);
let leaf_colors = [
Color::Rgb(210, 65, 10), Color::Rgb(195, 130, 0),
Color::Rgb(170, 75, 20), Color::Rgb(145, 95, 30), Color::Rgb(220, 160, 0),
];
let n_leaves = (pw * ph / 40).max(12).min(80);
for i in 0..n_leaves {
let h1 = hash(i as u64 + 1);
let h2 = hash(i as u64 + 1000);
let x_base = (h1 % pw as u64) as usize;
let y_start = (h2 % ph as u64) as usize;
let speed = 1 + (h1 >> 20) % 2;
let sway_a = 3.0 + (h2 >> 10 & 0xf) as f64 * 0.3;
let sway_f = 0.04 + (h1 >> 15 & 0x7) as f64 * 0.005;
let phase_s = (h2 >> 8 & 0x3f) as f64;
let x = (x_base as f64 + (tick as f64 * sway_f + phase_s).sin() * sway_a) as isize;
let y = ((y_start + tick as usize * speed as usize) % ph) as isize;
let col = leaf_colors[(h1 as usize) % leaf_colors.len()];
for dy in 0..2isize {
for dx in 0..2isize {
set_px(buf, x + dx, y + dy, col);
}
}
}
let n_floor = (pw * (ph - ground_y) / 8).max(6).min(40);
for i in 0..n_floor {
let h = hash(i as u64 * 3 + 7777);
let fx = (h % pw as u64) as isize;
let fy = (ground_y + (h >> 10) as usize % (ph - ground_y)) as isize;
let col = leaf_colors[(h as usize) % leaf_colors.len()];
set_px(buf, fx, fy, col);
if (h >> 20) % 3 != 0 { set_px(buf, fx + 1, fy, col); }
}
}
fn draw_ufo(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
let period = 380u64;
let visible = 100u64;
let phase = tick % period;
if phase >= visible { return; }
let crossing = tick / period;
let progress = phase as f64 / visible as f64;
let cx = if crossing % 2 == 0 {
(progress * (pw as f64 + 24.0)) as isize - 12
} else {
(pw as f64 + 12.0 - progress * (pw as f64 + 24.0)) as isize
};
let base_y = (ph as f64 * (0.30 + (hash(crossing * 31 + 7) % 20) as f64 * 0.01)) as isize;
let bob = ((tick as f64 * 0.18).sin() * 2.5) as isize;
let cy = base_y + bob;
let body_hi = Color::Rgb(168, 170, 182);
let body_dk = Color::Rgb(108, 110, 124);
let dome_hi = Color::Rgb(85, 195, 230);
let dome_dk = Color::Rgb(48, 142, 178);
let rim_col = Color::Rgb(200, 200, 210);
for dx in -5isize..=5 {
let inner = dx.abs() <= 2;
set_px(buf, cx + dx, cy, if inner { body_hi } else { body_dk });
set_px(buf, cx + dx, cy + 1, body_dk);
}
set_px(buf, cx - 5, cy, rim_col);
set_px(buf, cx + 5, cy, rim_col);
for dx in -2isize..=2 { set_px(buf, cx + dx, cy - 1, dome_dk); }
for dx in -1isize..=1 { set_px(buf, cx + dx, cy - 2, dome_hi); }
set_px(buf, cx, cy - 3, dome_dk);
let pulse = (tick as f64 * 0.12).sin() * 0.5 + 0.5;
let v = (140.0 + pulse * 50.0) as u8;
for lx in [-3isize, 0, 3] {
set_px(buf, cx + lx, cy + 2, Color::Rgb(v, v, v));
}
if phase < 35 {
let alpha = 1.0 - phase as f64 / 35.0;
let beam_h = (ph as f64 * 0.18 * alpha) as isize;
for dy in 3..3 + beam_h {
let spread = (dy / 5).min(3);
for dx in -spread..=spread {
let v = hash((cx + dx).unsigned_abs() as u64 * 7 + (cy + dy).unsigned_abs() as u64 * 13 + tick);
if v % 3 != 0 {
set_px(buf, cx + dx, cy + dy, Color::Rgb(
(180.0 * alpha) as u8,
(210.0 * alpha) as u8,
(255.0 * alpha) as u8,
));
}
}
}
}
}
fn fill_stars(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
for py in 0..ph { for ppx in 0..pw { buf[py][ppx] = Some(Color::Rgb(0, 0, 8)); } }
let layers: &[(usize, u8, u8, bool)] = &[
(1, 90, 110, false),
(2, 170, 190, false),
(4, 240, 255, true),
];
let n = (pw * ph / 80).max(8);
for (depth, (speed, dim, bright, trail)) in layers.iter().enumerate() {
for i in 0..n {
let seed = hash(i as u64 * 3 + depth as u64 * 997);
let y = (seed % ph as u64) as usize;
let x0 = (seed >> 10) as usize % pw;
let x = (x0 + pw - (tick as usize * speed / 10) % pw) % pw;
let c = Color::Rgb(*bright, *bright, *bright);
buf[y][x] = Some(c);
if *trail {
for t in 1..4usize {
let tx = (x + t) % pw;
let fade = dim.saturating_sub(t as u8 * 25);
buf[y][tx] = Some(Color::Rgb(fade, fade, fade + 15));
}
}
}
}
for i in 0..(pw * ph / 400).max(2) {
let seed = hash(i as u64 * 7919 + 42);
let bx = (seed % pw as u64) as usize;
let by = (seed >> 10) as usize % ph;
let tw = ((tick as f64 * 0.07 + i as f64).sin() * 0.5 + 0.5) * 255.0;
let v = tw as u8;
let c = Color::Rgb(v, v, (v as u16 + 40).min(255) as u8);
buf[by][bx] = Some(c);
if bx + 1 < pw { buf[by][bx+1] = Some(c); }
if by + 1 < ph { buf[by+1][bx] = Some(c); }
}
draw_ufo(buf, pw, ph, tick);
}
fn fill_fire(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
let side_w = (pw * 19 / 100).max(4);
let mantel_h = (ph * 11 / 100).max(2);
let hearth_y = ph.saturating_sub((ph * 9 / 100).max(2));
let fire_x0 = side_w;
let fire_x1 = pw.saturating_sub(side_w);
let fire_pw = fire_x1.saturating_sub(fire_x0);
let fire_ph = hearth_y.saturating_sub(mantel_h);
for py in 0..ph { for ppx in 0..pw { buf[py][ppx] = Some(Color::Rgb(5, 4, 3)); } }
for py in mantel_h..hearth_y {
for ppx in fire_x0..fire_x1 {
let v = hash(ppx as u64 * 3 + py as u64 * 7 + 999);
buf[py][ppx] = Some(Color::Rgb(
11u8.saturating_add((v % 4) as u8),
9u8.saturating_add(((v >> 4) % 3) as u8),
8u8.saturating_add(((v >> 8) % 3) as u8),
));
}
}
let crackle_t = tick / 4;
let blend_f = (tick % 4) as f64 / 4.0;
for ppx in fire_x0..fire_x1 {
let local_x = ppx - fire_x0;
let cx = (local_x as f64 - fire_pw as f64 / 2.0).abs() / (fire_pw as f64 / 2.0);
let arch = (1.0 - cx * 0.65).max(0.0);
let ha = hash(ppx as u64 * 11 + crackle_t * 37 + 101);
let hb = hash(ppx as u64 * 11 + (crackle_t + 1) * 37 + 101);
let ca = (ha % 100) as f64 / 100.0 * 0.30 - 0.09;
let cb = (hb % 100) as f64 / 100.0 * 0.30 - 0.09;
let hn = hash((ppx as u64 + 1) * 11 + crackle_t * 37 + 101);
let cn = (hn % 100) as f64 / 100.0 * 0.30 - 0.09;
let crackle = (ca * (1.0 - blend_f) + cb * blend_f) * 0.75 + cn * 0.25;
let height = ((arch + crackle).clamp(0.1, 1.0) * 0.58 * fire_ph as f64) as usize;
let top = hearth_y.saturating_sub(height);
for py in top..hearth_y {
let f = (py - top) as f64 / height.max(1) as f64;
buf[py][ppx] = Some(
if f < 0.20 { let v = f/0.20; Color::Rgb(255, (210.0*v+45.0) as u8, (90.0*(1.0-v)) as u8) }
else if f < 0.60 { let v = (f-0.20)/0.40; Color::Rgb(255, (45.0*(1.0-v)) as u8, 0) }
else { Color::Rgb(((1.0-(f-0.60)/0.40)*240.0).max(0.0) as u8, 0, 0) }
);
}
}
let n_sparks = (fire_pw / 2).max(3);
for i in 0..n_sparks {
let hs = hash(i as u64 * 17 + tick / 5 * 31 + 555);
if hs % 4 != 0 { continue; }
let sx = fire_x0 + (hs >> 10) as usize % fire_pw;
let sy = mantel_h + (hs >> 20) as usize % (fire_ph * 2 / 5);
set_px(buf, sx as isize, sy as isize, match (hs >> 5) % 3 {
0 => Color::Rgb(255, 210, 60),
1 => Color::Rgb(255, 150, 25),
_ => Color::Rgb(255, 255, 190),
});
}
let shadow_w = (fire_pw / 6).max(3);
for py in mantel_h..hearth_y {
for dx in 0..shadow_w {
let alpha = (1.0 - dx as f64 / shadow_w as f64) * 0.78;
for &px in &[fire_x0 + dx, fire_x1.saturating_sub(1 + dx)] {
if px < buf[0].len() {
if let Some(Color::Rgb(r, g, b)) = buf[py][px] {
buf[py][px] = Some(Color::Rgb(
(r as f64 * (1.0 - alpha)) as u8,
(g as f64 * (1.0 - alpha)) as u8,
(b as f64 * (1.0 - alpha)) as u8,
));
}
}
}
}
}
draw_fireplace(buf, pw, ph);
}
fn draw_snow_landscape(buf: &mut PixBuf, pw: usize, ph: usize) {
let base_ground = ph * 84 / 100;
let pi = std::f64::consts::PI;
let ground_at = |ppx: usize| -> usize {
let xf = ppx as f64 / pw as f64;
let h1 = ((xf * pi * 2.3).sin() * 0.5 + 0.5) * ph as f64 * 0.035;
let h2 = ((xf * pi * 4.7 + 0.8).sin() * 0.5 + 0.5) * ph as f64 * 0.018;
let noise = (hash(ppx as u64 * 17 + 333) % 4) as f64;
((base_ground as f64) - h1 - h2 - noise).max(0.0) as usize
};
let draw_range = |buf: &mut PixBuf, base: usize, amp1: f64, freq1: f64, phase1: f64,
amp2: f64, freq2: f64, phase2: f64,
rock: (u8,u8,u8), snow_frac: f64| {
for ppx in 0..pw {
let xf = ppx as f64 / pw as f64;
let h1 = ((xf * pi * freq1 + phase1).sin() * 0.5 + 0.5) * ph as f64 * amp1;
let h2 = ((xf * pi * freq2 + phase2).sin() * 0.5 + 0.5) * ph as f64 * amp2;
let ht = (h1 + h2) as usize;
if ht == 0 { continue; }
let base_noise = (hash(ppx as u64 * 13 + base as u64 * 7 + 444) % 7) as usize;
let base_y = (base + base_noise).min(ph);
let top = base_y.saturating_sub(ht);
let snow_ln = top + (ht as f64 * snow_frac) as usize;
let fade_h = ((ht / 5) + 3).min(ht);
let fade_y = base_y.saturating_sub(fade_h);
for py in top..base_y {
if py >= buf.len() || ppx >= buf[0].len() { continue; }
let noise = (hash(ppx as u64 * 5 + py as u64 * 9 + 1234) % 6) as f64 * 0.5;
buf[py][ppx] = Some(if py < snow_ln {
let b = (py - top) as f64 / (snow_ln - top).max(1) as f64;
Color::Rgb((220.0 - b*25.0 + noise) as u8, (232.0 - b*22.0 + noise) as u8, (248.0 - b*14.0) as u8)
} else if py < fade_y {
Color::Rgb(
(rock.0 as f64 + noise) as u8,
(rock.1 as f64 + noise) as u8,
(rock.2 as f64 + noise) as u8,
)
} else {
let f = (py - fade_y) as f64 / fade_h as f64;
let n2 = (hash(ppx as u64 * 3 + py as u64 * 17 + 5678) % 10) as f64;
Color::Rgb(
(rock.0 as f64 + f * (190.0 - rock.0 as f64) + n2) as u8,
(rock.1 as f64 + f * (205.0 - rock.1 as f64) + n2) as u8,
(rock.2 as f64 + f * (222.0 - rock.2 as f64) + n2) as u8,
)
});
}
}
};
draw_range(buf, ph * 76 / 100, 0.14, 2.2, 0.0, 0.06, 4.3, 0.8, (22, 28, 48), 0.52);
draw_range(buf, base_ground, 0.24, 1.5, 0.5, 0.10, 3.1, 1.3, (8, 11, 20), 0.40);
for ppx in 0..pw {
let gy = ground_at(ppx);
for py in gy..ph {
if py >= buf.len() || ppx >= buf[0].len() { continue; }
let depth = (py - gy) as f64 / (ph - gy).max(1) as f64;
let aurora_t = (1.0 - depth) * 16.0;
let v = hash((ppx as u64 * 5).wrapping_add(py as u64 * 13).wrapping_add(9999));
let noise = (v % 6) as f64 * 0.5;
let bright = 182.0 + depth * 22.0;
buf[py][ppx] = Some(Color::Rgb(
(bright + noise) as u8,
(bright + aurora_t + noise) as u8,
(bright + aurora_t * 1.6 + noise + 10.0).min(255.0) as u8,
));
}
}
}
fn fill_aurora(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
let t = tick as f64 * 0.04;
for py in 0..ph {
for ppx in 0..pw {
let frac = py as f64 / ph as f64;
buf[py][ppx] = Some(Color::Rgb((3.0+frac*5.0) as u8, (5.0+frac*8.0) as u8, (15.0+frac*20.0) as u8));
}
}
let aurora_h = ph * 7 / 10;
let band_colors: [(u8,u8,u8); 4] = [(20,200,120),(40,180,220),(100,60,220),(20,220,160)];
for ppx in 0..pw {
let x = ppx as f64 / pw as f64;
for band in 0..4u32 {
let b = band as f64;
let cx = (b * 0.27 + t * (0.3 + b * 0.1)).sin() * 0.5 + 0.5;
let width = 0.12 + (t * 0.07 + b).sin() * 0.05;
let dist = (x - cx).abs();
if dist < width {
let intensity = (1.0 - dist / width).powi(2);
let band_h = (aurora_h as f64 * (0.4 + intensity * 0.5)) as usize;
let bottom_f = aurora_h as f64
+ (b * 1.7 + t * 0.05).sin() * ph as f64 * 0.10
+ (ppx as f64 * 0.03 + t * 0.18 + b * 2.1).sin() * ph as f64 * 0.07
+ (ppx as f64 * 0.09 + t * 0.31 + b * 0.9).sin() * ph as f64 * 0.04
+ (ppx as f64 * 0.23 + t * 0.14 + b * 3.3).sin() * ph as f64 * 0.02;
let bottom = (bottom_f as isize).clamp(0, ph as isize) as usize;
let top = bottom.saturating_sub(band_h);
let (r, g, bl) = band_colors[band as usize % band_colors.len()];
for py in top..bottom.min(ph) {
let depth = if band_h > 0 { (py - top) as f64 / band_h as f64 } else { 0.0 };
let alpha = intensity * (1.0 - depth * 0.7);
let existing = buf[py][ppx];
let (er, eg, eb) = match existing {
Some(Color::Rgb(a, b, c)) => (a as f64, b as f64, c as f64),
_ => (0.0, 0.0, 0.0),
};
buf[py][ppx] = Some(Color::Rgb(
(er + r as f64 * alpha).min(255.0) as u8,
(eg + g as f64 * alpha).min(255.0) as u8,
(eb + bl as f64 * alpha).min(255.0) as u8,
));
}
}
}
}
for i in 0..(pw * ph / 60).max(5) {
let seed = hash(i as u64 + 5555);
let sx = (seed % pw as u64) as usize;
let sy = (seed >> 8) as usize % (ph / 2).max(1);
let tw = ((tick as f64 * 0.1 + i as f64 * 1.3).sin() * 0.4 + 0.6) * 160.0;
let v = tw as u8;
if let Some(Color::Rgb(er, eg, eb)) = buf[sy][sx] {
if (er as u16 + eg as u16 + eb as u16) < 150 {
buf[sy][sx] = Some(Color::Rgb(v, v, v));
}
}
}
draw_snow_landscape(buf, pw, ph);
}
type FillFn = fn(&mut PixBuf, usize, usize, u64);
struct Theme { fill: FillFn, color: Color }
const THEMES: &[Theme] = &[
Theme { fill: fill_waves, color: Color::Rgb(0, 120, 210) },
Theme { fill: fill_rain, color: Color::Rgb(80, 130, 210) },
Theme { fill: fill_leaves, color: Color::Rgb(210, 95, 15) },
Theme { fill: fill_stars, color: Color::Rgb(140, 140, 255) },
Theme { fill: fill_fire, color: Color::Rgb(255, 90, 0) },
Theme { fill: fill_aurora, color: Color::Rgb(20, 210, 150) },
];
pub struct Animation {
theme_idx: usize,
pub render_mode: RenderMode,
tick_count: u64,
}
impl Animation {
pub fn new() -> Self {
Self { theme_idx: 0, render_mode: RenderMode::Half, tick_count: 0 }
}
pub fn tick(&mut self) { self.tick_count += 1; }
pub fn next_theme(&mut self) { self.theme_idx = (self.theme_idx + 1) % THEMES.len(); }
pub fn prev_theme(&mut self) { self.theme_idx = (self.theme_idx + THEMES.len() - 1) % THEMES.len(); }
pub fn next_mode(&mut self) { self.render_mode = self.render_mode.next(); }
pub fn prev_mode(&mut self) { self.render_mode = self.render_mode.prev(); }
pub fn theme_color(&self) -> Color { THEMES[self.theme_idx].color }
pub fn render_lines(&self, _phase: &crate::timer::Phase, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
if char_w == 0 || char_h == 0 { return vec![]; }
let mode = self.render_mode;
let theme = &THEMES[self.theme_idx];
let (pw, ph) = (mode.pw(char_w), mode.ph(char_h));
let mut buf = new_buf(pw, ph);
(theme.fill)(&mut buf, pw, ph, self.tick_count);
dispatch(mode, &buf, char_w, char_h)
}
}