| 1 | //! Bounded, off-thread presentation. The world never sees interpolation or pixels. |
| 2 | use super::live::{Pose, Scene, View}; |
| 3 | use base64::Engine; |
| 4 | use std::io::{self, Write}; |
| 5 | |
| 6 | pub fn image_id() -> u32 { |
| 7 | 0x4357_0000 ^ std::process::id() |
| 8 | } |
| 9 | pub fn clear(output: &mut impl Write) -> io::Result<()> { |
| 10 | write!(output, "\x1b_Ga=d,d=I,i={},q=2;\x1b\\", image_id()) |
| 11 | } |
| 12 | |
| 13 | #[derive(Default)] |
| 14 | pub struct Renderer { |
| 15 | dimensions: (usize, usize), |
| 16 | background: Vec<u8>, |
| 17 | appearance: super::appearance::Appearance, |
| 18 | } |
| 19 | impl Renderer { |
| 20 | pub fn set_appearance(&mut self, appearance: &super::appearance::Appearance) { |
| 21 | if self.appearance != *appearance { |
| 22 | self.dimensions = (0, 0); |
| 23 | self.appearance = appearance.clone(); |
| 24 | } |
| 25 | } |
| 26 | pub fn render( |
| 27 | &mut self, |
| 28 | pose: &Pose, |
| 29 | previous: Option<&Scene>, |
| 30 | mix: f64, |
| 31 | view: &View, |
| 32 | time: f64, |
| 33 | ) -> io::Result<(Vec<u8>, Option<Vec<u8>>)> { |
| 34 | let cols = usize::from(view.width.clamp(1, 512)); |
| 35 | let rows = usize::from(view.height.clamp(1, 256)); |
| 36 | let cw = if view.cell_width.is_finite() && view.cell_width >= 1.0 { |
| 37 | view.cell_width.min(64.0) |
| 38 | } else { |
| 39 | 8.0 |
| 40 | }; |
| 41 | let ch = if view.cell_height.is_finite() && view.cell_height >= 1.0 { |
| 42 | view.cell_height.min(128.0) |
| 43 | } else { |
| 44 | 16.0 |
| 45 | }; |
| 46 | let width = cols as f64 * cw; |
| 47 | let height = rows as f64 * ch; |
| 48 | let resolution = (960.0 / width).min(560.0 / height).min(1.0); |
| 49 | let w = (width * resolution).round().clamp(1.0, 960.0) as usize; |
| 50 | let h = (height * resolution).round().clamp(1.0, 560.0) as usize; |
| 51 | if self.dimensions != (w, h) { |
| 52 | self.dimensions = (w, h); |
| 53 | self.background = vec![0; w * h * 3]; |
| 54 | for y in 0..h { |
| 55 | for x in 0..w { |
| 56 | let light = (1.0 |
| 57 | - (((x as f64 / w as f64 - 0.5) * 1.1).powi(2) |
| 58 | + (y as f64 / h as f64 * 0.7).powi(2)) |
| 59 | .sqrt()) |
| 60 | .clamp(0.0, 1.0); |
| 61 | let at = (y * w + x) * 3; |
| 62 | for k in 0..3 { |
| 63 | self.background[at + k] = (f64::from(self.appearance.background[k]) |
| 64 | * (1.0 - light) |
| 65 | + f64::from(self.appearance.background_top[k]) * light) |
| 66 | as u8; |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | let mut rgb = if view.pixels { |
| 72 | self.background.clone() |
| 73 | } else { |
| 74 | Vec::new() |
| 75 | }; |
| 76 | let mut cells = vec![0u8; cols * rows]; |
| 77 | let val = |name: &str, default: f64| pose.state[name].as_f64().unwrap_or(default); |
| 78 | let attention = val("attention", 0.0); |
| 79 | let flip = val("flip", 1.0); |
| 80 | let scale = (w as f64 * 0.52).min(h as f64 * 0.85) * (1.0 + attention * 0.07); |
| 81 | let ox = w as f64 * (0.5 + val("roamX", 0.0) * 0.30); |
| 82 | let oy = h as f64 * (0.47 + val("roamY", 0.0) * 0.30 + attention * 0.05); |
| 83 | let colour = [pose.style.r, pose.style.g, pose.style.b]; |
| 84 | if view.pixels && self.appearance.environment { |
| 85 | for x in 16..w.saturating_sub(16) { |
| 86 | blend(&mut rgb, w, h, x as i32, 16, [45.0, 70.0, 81.0], 0.5); |
| 87 | } |
| 88 | for band in 0..6 { |
| 89 | for x in 0..w { |
| 90 | let y = h as f64 * 0.85 |
| 91 | + ((x as f64 / 110.0) + band as f64 + time * 0.18).sin() * 6.0 |
| 92 | + band as f64 * 5.0; |
| 93 | blend( |
| 94 | &mut rgb, |
| 95 | w, |
| 96 | h, |
| 97 | x as i32, |
| 98 | y as i32, |
| 99 | [96.0, 163.0, 185.0], |
| 100 | 0.025, |
| 101 | ); |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | let radius = (w.min(h) as f64 * 0.00285).clamp(0.68, 1.55) * self.appearance.dot_scale; |
| 106 | for (i, q) in pose.points.iter().enumerate() { |
| 107 | let p = previous.and_then(|s| s.points.get(i)); |
| 108 | let xx = p.map_or(q[0], |p| p[0] + (q[0] - p[0]) * mix); |
| 109 | let yy = p.map_or(q[1], |p| p[1] + (q[1] - p[1]) * mix); |
| 110 | let x = ox + xx * scale * flip; |
| 111 | let y = oy + yy * scale; |
| 112 | let cx = (x / w as f64 * (cols * 2) as f64).floor() as i32; |
| 113 | let cy = (y / h as f64 * (rows * 4) as f64).floor() as i32; |
| 114 | if cx >= 0 && cy >= 0 && cx < (cols * 2) as i32 && cy < (rows * 4) as i32 { |
| 115 | let bit = [[0, 3], [1, 4], [2, 5], [6, 7]][cy as usize % 4][cx as usize % 2]; |
| 116 | cells[(cy as usize / 4) * cols + cx as usize / 2] |= 1 << bit; |
| 117 | } |
| 118 | if !view.pixels { |
| 119 | continue; |
| 120 | } |
| 121 | let depth = 0.65 + 0.35 * ((i * 37 % 101) as f64 / 100.0); |
| 122 | let alpha = (pose.style.alpha * depth).clamp(0.0, 1.0); |
| 123 | let r = radius * depth; |
| 124 | let glow = if !pose.style.hollow && i % 5 == 0 { |
| 125 | 1.0 + 4.0 * self.appearance.glow |
| 126 | } else { |
| 127 | 1.0 |
| 128 | }; |
| 129 | let reach = (r * glow + 1.0).ceil() as i32; |
| 130 | for dy in -reach..=reach { |
| 131 | for dx in -reach..=reach { |
| 132 | let px = x.floor() as i32 + dx; |
| 133 | let py = y.floor() as i32 + dy; |
| 134 | let dist = |
| 135 | ((px as f64 + 0.5 - x).powi(2) + (py as f64 + 0.5 - y).powi(2)).sqrt(); |
| 136 | let coverage = if pose.style.hollow { |
| 137 | (0.7 - (dist - r).abs()).clamp(0.0, 1.0) |
| 138 | } else { |
| 139 | (r + 0.5 - dist).clamp(0.0, 1.0) |
| 140 | }; |
| 141 | let a = alpha * coverage |
| 142 | + if glow > 1.0 { |
| 143 | (1.0 - dist / (r * 3.0)).max(0.0) * 0.035 |
| 144 | } else { |
| 145 | 0.0 |
| 146 | }; |
| 147 | if a > 0.0 { |
| 148 | blend(&mut rgb, w, h, px, py, colour, a) |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | let image = if view.pixels { |
| 154 | let mut compressor = |
| 155 | flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast()); |
| 156 | compressor.write_all(&rgb)?; |
| 157 | let data = base64::engine::general_purpose::STANDARD.encode(compressor.finish()?); |
| 158 | let mut protocol = Vec::with_capacity(data.len() + data.len() / 4096 * 32 + 160); |
| 159 | for (index, chunk) in data.as_bytes().chunks(4096).enumerate() { |
| 160 | let more = u8::from((index + 1) * 4096 < data.len()); |
| 161 | if index == 0 { |
| 162 | write!( |
| 163 | protocol, |
| 164 | "\x1b_Ga=T,t=d,f=24,o=z,s={w},v={h},i={},p=1,c={cols},r={rows},C=1,q=2,m={more};", |
| 165 | image_id() |
| 166 | )?; |
| 167 | } else { |
| 168 | write!(protocol, "\x1b_Gm={more},q=2;")?; |
| 169 | } |
| 170 | protocol.extend_from_slice(chunk); |
| 171 | protocol.extend_from_slice(b"\x1b\\"); |
| 172 | } |
| 173 | Some(protocol) |
| 174 | } else { |
| 175 | None |
| 176 | }; |
| 177 | Ok((cells, image)) |
| 178 | } |
| 179 | } |
| 180 | fn blend(rgb: &mut [u8], w: usize, h: usize, x: i32, y: i32, c: [f64; 3], a: f64) { |
| 181 | if x < 0 || y < 0 || x >= w as i32 || y >= h as i32 { |
| 182 | return; |
| 183 | } |
| 184 | let offset = (y as usize * w + x as usize) * 3; |
| 185 | let a = a.clamp(0.0, 1.0); |
| 186 | for k in 0..3 { |
| 187 | rgb[offset + k] = (rgb[offset + k] as f64 * (1.0 - a) + c[k] * a).clamp(0.0, 255.0) as u8; |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | #[cfg(test)] |
| 192 | mod tests { |
| 193 | use super::super::live::Style; |
| 194 | use super::*; |
| 195 | #[test] |
| 196 | fn pixel_transport_is_bounded_chunked_rgb_and_braille_remains_available() { |
| 197 | let pose = Pose { |
| 198 | points: vec![[0.0, 0.0]; 980], |
| 199 | style: Style { |
| 200 | r: 120., |
| 201 | g: 210., |
| 202 | b: 230., |
| 203 | alpha: 0.7, |
| 204 | hollow: false, |
| 205 | channel: "reasoning".into(), |
| 206 | arch: "gyre".into(), |
| 207 | }, |
| 208 | state: serde_json::json!({}), |
| 209 | }; |
| 210 | let mut renderer = Renderer::default(); |
| 211 | let view = View { |
| 212 | width: 120, |
| 213 | height: 35, |
| 214 | pixels: true, |
| 215 | ..View::default() |
| 216 | }; |
| 217 | let (cells, image) = renderer.render(&pose, None, 1.0, &view, 0.0).unwrap(); |
| 218 | assert_eq!(cells.len(), 4200); |
| 219 | assert!(cells.iter().any(|b| *b != 0)); |
| 220 | let image = String::from_utf8(image.unwrap()).unwrap(); |
| 221 | assert!(image.contains("a=T,t=d,f=24,o=z,s=960,v=560")); |
| 222 | let mut encoded = String::new(); |
| 223 | for chunk in image.split("\x1b\\").filter(|s| !s.is_empty()) { |
| 224 | let (_, data) = chunk.split_once(';').unwrap(); |
| 225 | assert!(data.len() <= 4096); |
| 226 | encoded.push_str(data); |
| 227 | } |
| 228 | let compressed = base64::engine::general_purpose::STANDARD |
| 229 | .decode(encoded) |
| 230 | .unwrap(); |
| 231 | let mut decoder = flate2::read::ZlibDecoder::new(compressed.as_slice()); |
| 232 | let mut rgb = Vec::new(); |
| 233 | std::io::Read::read_to_end(&mut decoder, &mut rgb).unwrap(); |
| 234 | assert_eq!(rgb.len(), 960 * 560 * 3); |
| 235 | let (fallback, image) = renderer |
| 236 | .render( |
| 237 | &pose, |
| 238 | None, |
| 239 | 1.0, |
| 240 | &View { |
| 241 | pixels: false, |
| 242 | ..view |
| 243 | }, |
| 244 | 0.0, |
| 245 | ) |
| 246 | .unwrap(); |
| 247 | assert_eq!(fallback, cells); |
| 248 | assert!(image.is_none()); |
| 249 | } |
| 250 | } |
| 251 |