| 1 | use super::*; |
| 2 | use crate::tui::ambient_life::{frame_ocean_ramp, occupied_text_bounds}; |
| 3 | use crate::tui::ocean; |
| 4 | use ratatui::{style::Modifier, text::Line}; |
| 5 | |
| 6 | pub(super) struct LegacyColumn(pub(super) OceanColumn); |
| 7 | impl LegacyColumn { |
| 8 | fn color_at_y(&self, y: u16) -> Color { |
| 9 | self.0.color_at_y(y) |
| 10 | } |
| 11 | pub fn paint_matching(self, area: Rect, buf: &mut Buffer, background: Color) { |
| 12 | let area = area.intersection(buf.area); |
| 13 | for y in area.top()..area.bottom() { |
| 14 | let row_bg = self.color_at_y(y); |
| 15 | for x in area.left()..area.right() { |
| 16 | let cell = &mut buf[(x, y)]; |
| 17 | if cell.bg == background && !cell.modifier.contains(Modifier::REVERSED) { |
| 18 | cell.set_bg(row_bg); |
| 19 | } |
| 20 | } |
| 21 | } |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | pub(super) struct LegacyWater { |
| 26 | pub(super) ocean_column: Option<OceanColumn>, |
| 27 | pub(super) lines: Vec<Line<'static>>, |
| 28 | pub(super) background: Color, |
| 29 | pub(super) ocean_elapsed_ms: u128, |
| 30 | } |
| 31 | impl LegacyWater { |
| 32 | pub(super) fn paint(&self, area: Rect, buf: &mut Buffer) { |
| 33 | if let Some(column) = self.ocean_column { |
| 34 | // Cache per-row ocean colors; invalidate only on phase/size/breath. |
| 35 | let phase_tag = column.phase_tag(); |
| 36 | let fingerprint = column.ramp_fingerprint(); |
| 37 | let ramp = crate::tui::ambient_life::frame_ocean_ramp( |
| 38 | &column, |
| 39 | area.height, |
| 40 | area.y, |
| 41 | self.ocean_elapsed_ms, |
| 42 | phase_tag, |
| 43 | fingerprint, |
| 44 | ); |
| 45 | for local_y in 0..area.height { |
| 46 | let protected = self |
| 47 | .lines |
| 48 | .get(usize::from(local_y)) |
| 49 | .and_then(occupied_text_bounds); |
| 50 | let row_bg = ramp |
| 51 | .get(usize::from(local_y)) |
| 52 | .copied() |
| 53 | .unwrap_or_else(|| column.color_at_y(area.y.saturating_add(local_y))); |
| 54 | for local_x in 0..area.width { |
| 55 | let is_protected = protected.is_some_and(|(start, end)| { |
| 56 | usize::from(local_x) >= start && usize::from(local_x) < end |
| 57 | }); |
| 58 | let cell = &mut buf[(area.x + local_x, area.y + local_y)]; |
| 59 | // Plain transcript text participates in the water column; |
| 60 | // explicit semantic surfaces (selection, code, warnings) |
| 61 | // retain their own background. |
| 62 | if !is_protected || cell.bg == self.background { |
| 63 | cell.set_bg(row_bg); |
| 64 | } |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | pub fn apply_caustic_shimmer( |
| 72 | area: Rect, |
| 73 | buf: &mut Buffer, |
| 74 | column: &OceanColumn, |
| 75 | elapsed_ms: u128, |
| 76 | animated: bool, |
| 77 | lines: &[Line<'static>], |
| 78 | ) { |
| 79 | if !animated || area.width < AMBIENT_MIN_WIDTH || area.height < AMBIENT_MIN_HEIGHT { |
| 80 | return; |
| 81 | } |
| 82 | // Sparse sampling: every 3rd column on every other row near the surface. |
| 83 | // |
| 84 | // The light stops where the composition starts. Sunlight raking across |
| 85 | // the rows a wordmark is sitting in is the same failure as a fish |
| 86 | // swimming through them, just quieter, and it costs nothing to measure: |
| 87 | // the surface band is clipped to the first row that carries text. |
| 88 | let ceiling = (0..area.height) |
| 89 | .find(|row| { |
| 90 | lines |
| 91 | .get(usize::from(*row)) |
| 92 | .and_then(occupied_text_bounds) |
| 93 | .is_some() |
| 94 | }) |
| 95 | .unwrap_or(area.height); |
| 96 | let band = (area.height / 3).max(2).min(ceiling); |
| 97 | for local_y in 0..band { |
| 98 | let protected = lines |
| 99 | .get(usize::from(local_y)) |
| 100 | .and_then(occupied_text_bounds); |
| 101 | let ramp = frame_ocean_ramp( |
| 102 | column, |
| 103 | area.height, |
| 104 | area.y, |
| 105 | elapsed_ms, |
| 106 | column.phase_tag(), |
| 107 | column.ramp_fingerprint(), |
| 108 | ); |
| 109 | let row_bg = ramp |
| 110 | .get(usize::from(local_y)) |
| 111 | .copied() |
| 112 | .unwrap_or_else(|| column.color_at_y(area.y.saturating_add(local_y))); |
| 113 | for local_x in (0..area.width).step_by(3) { |
| 114 | if protected.is_some_and(|(start, end)| { |
| 115 | usize::from(local_x) >= start && usize::from(local_x) < end |
| 116 | }) { |
| 117 | continue; |
| 118 | } |
| 119 | let cell = &mut buf[(area.x + local_x, area.y + local_y)]; |
| 120 | // Soften toward ambient ink without replacing semantic glyphs. |
| 121 | if cell.symbol() == " " || cell.symbol().is_empty() { |
| 122 | // Sunlight dissolves with depth instead of stopping: full |
| 123 | // amplitude at the surface easing to zero at the band's |
| 124 | // floor. The former hard cutoff at `band` drew a visible |
| 125 | // horizontal line across tall windows. |
| 126 | let depth_fade = 1.0 - f32::from(local_y) / f32::from(band.max(1)); |
| 127 | let shimmer = ocean::scale_color( |
| 128 | row_bg, |
| 129 | caustic_brightness(elapsed_ms, local_x, local_y, depth_fade * depth_fade), |
| 130 | ); |
| 131 | cell.set_bg(shimmer); |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | /// Continuous travelling caustic. The former `(elapsed / 80) % 12` mask |
| 138 | /// toggled cells fully on/off at 12.5 Hz; truecolor made that quantization look |
| 139 | /// like dropped frames. A narrow cosine crest preserves the same sparse light |
| 140 | /// band while cross-fading every sampled cell between frames. |
| 141 | fn caustic_brightness(elapsed_ms: u128, local_x: u16, local_y: u16, depth_fade: f32) -> f32 { |
| 142 | const CYCLE_MS: f64 = 960.0; |
| 143 | const SPATIAL_SLOTS: f64 = 4.0; |
| 144 | let time = (elapsed_ms % CYCLE_MS as u128) as f64 / CYCLE_MS; |
| 145 | // The sampled grid advances by three terminal columns. Four grid phases |
| 146 | // therefore preserve the old 12-column repeat instead of stretching the |
| 147 | // caustic topology while changing only its temporal interpolation. |
| 148 | let slot = (u32::from(local_x / 3) + u32::from(local_y)) % 4; |
| 149 | let phase = (time + f64::from(slot) / SPATIAL_SLOTS) * std::f64::consts::TAU; |
| 150 | let crest = ((phase.cos() + 1.0) * 0.5).powi(8); |
| 151 | 1.0 + 0.08 * (crest as f32) * depth_fade.clamp(0.0, 1.0) |
| 152 | } |
| 153 | |
| 154 | // End of exact legacy caustic/brightness source fragment. |
| 155 |