1//! The picture a link unfurls with: the question and Jev's answer, drawn the 2//! way the page looks, as a PNG. No I/O. 3//! 4//! It is drawn by hand, a pixel at a time, in an 8 by 8 bitmap face scaled 5//! up. That is the page's look (dither, window frames, pixel type), and it 6//! needs no font engine: a Worker has ten milliseconds to make this, and a 7//! megabyte or two less to carry. Four colours, two bits a pixel. 8#![forbid(unsafe_code)] 9 10use archive::{Answer, Detail}; 11use font8x8::UnicodeFonts; 12 13pub const WIDTH: usize = 600; 14pub const HEIGHT: usize = 315; 15 16/// The palette, as the page's `--paper`, `--ink`, `--pink` and `--grey`. 17const PALETTE: [u8; 12] = [0xfe, 0xfe, 0xfe, 0x1e, 0x1e, 0x1e, 0xf3, 0x86, 0xa1, 0xde, 0xde, 0xde]; 18const PAPER: u8 = 0; 19const INK: u8 = 1; 20const PINK: u8 = 2; 21const GREY: u8 = 3; 22 23/// What the card is of. 24#[derive(Clone, Copy, Debug, PartialEq)] 25pub enum Card<'a> { 26 /// The site itself. 27 Home, 28 /// A question, and what Jev said: nothing, if it has not been asked. 29 Question { input: &'a str, said: &'a [Answer] }, 30 /// A repository's code: its name (`lmjtfy.git`), the clone command for 31 /// `url` (with submodules when it has some), what it is, and the latest 32 /// commit if it could be read. 33 Code { name: &'a str, url: &'a str, recurse: bool, about: &'a str, branch: &'a str, latest: Option<&'a Commit> }, 34} 35 36/// The latest commit on the code's main branch, as the card and the page 37/// show it. 38#[derive(Clone, Debug, PartialEq)] 39pub struct Commit { 40 /// The full hash. 41 pub sha: String, 42 /// The message's first line. 43 pub subject: String, 44 /// When it was committed, as `YYYY-MM-DD`. 45 pub date: String, 46 /// How many commits main has, if GitHub said. 47 pub count: Option<u64>, 48} 49 50impl Commit { 51 pub fn short(&self) -> &str { 52 self.sha.get(..7).unwrap_or(&self.sha) 53 } 54 55 /// `4d93353 · 2026-10-02 · 140 commits`. 56 pub fn line(&self) -> String { 57 let mut parts = vec![self.short().to_owned(), self.date.clone()]; 58 parts.extend(self.count.map(|count| format!("{count} commits"))); 59 parts.join(" · ") 60 } 61} 62 63struct Canvas { 64 pixels: Vec<u8>, 65} 66 67/// A 4 by 4 ordered dither: how dark a field has to be before each cell of 68/// the pattern takes ink. 69const BAYER: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]]; 70 71impl Canvas { 72 fn new() -> Self { 73 Canvas { pixels: vec![PAPER; WIDTH * HEIGHT] } 74 } 75 76 fn fill(&mut self, x: usize, y: usize, w: usize, h: usize, colour: u8) { 77 for row in y..(y + h).min(HEIGHT) { 78 for column in x..(x + w).min(WIDTH) { 79 self.pixels[row * WIDTH + column] = colour; 80 } 81 } 82 } 83 84 fn frame(&mut self, x: usize, y: usize, w: usize, h: usize, colour: u8) { 85 self.fill(x, y, w, 1, colour); 86 self.fill(x, y + h - 1, w, 1, colour); 87 self.fill(x, y, 1, h, colour); 88 self.fill(x + w - 1, y, 1, h, colour); 89 } 90 91 /// The page's dot field: dense at the top left and bottom right corners, 92 /// thinning to nothing across the middle. Dots are 2 by 2. 93 fn dither(&mut self, x0: usize, y0: usize, w: usize, h: usize, colour: u8) { 94 for y in (y0..y0 + h).step_by(2) { 95 for x in (x0..x0 + w).step_by(2) { 96 let across = (x - x0) as f64 / w as f64; 97 let down = (y - y0) as f64 / h as f64; 98 let from_corner = (across + down).min(2.0 - across - down); 99 let dark = (1.0 - from_corner * 1.6).max(0.0) * 16.0; 100 if f64::from(BAYER[(y / 2) % 4][(x / 2) % 4]) < dark { 101 self.fill(x, y, 2, 2, colour); 102 } 103 } 104 } 105 } 106 107 /// One line of text, each font pixel `scale` across. Returns its width. 108 fn text(&mut self, x: usize, y: usize, text: &str, scale: usize, colour: u8) -> usize { 109 for (index, ch) in text.chars().enumerate() { 110 let glyph = glyph(ch); 111 for (row, bits) in glyph.iter().enumerate() { 112 for column in 0..8 { 113 if bits >> column & 1 == 1 { 114 self.fill(x + (index * 8 + column) * scale, y + row * scale, scale, scale, colour); 115 } 116 } 117 } 118 } 119 width(text, scale) 120 } 121} 122 123/// A character's 8 rows, bit 0 leftmost. One the face lacks is drawn as `?`. 124fn glyph(ch: char) -> [u8; 8] { 125 font8x8::BASIC_FONTS 126 .get(ch) 127 .or_else(|| font8x8::LATIN_FONTS.get(ch)) 128 .or_else(|| font8x8::BASIC_FONTS.get('?')) 129 .unwrap_or([0; 8]) 130} 131 132fn width(text: &str, scale: usize) -> usize { 133 text.chars().count() * 8 * scale 134} 135 136/// `text` cut to `chars`, the cut marked. 137fn cut(text: &str, chars: usize) -> String { 138 if text.chars().count() <= chars { 139 return text.to_owned(); 140 } 141 let kept: String = text.chars().take(chars.saturating_sub(3)).collect(); 142 format!("{}...", kept.trim_end()) 143} 144 145/// `text` broken at spaces into at most `lines` lines of at most `chars`, 146/// the last cut if there is more. A word longer than a line is cut. 147fn wrap(text: &str, chars: usize, lines: usize) -> Vec<String> { 148 let mut wrapped: Vec<String> = Vec::new(); 149 let mut words = text.split_whitespace().peekable(); 150 while let Some(word) = words.next() { 151 let fits = wrapped.last().is_some_and(|line| line.chars().count() + 1 + word.chars().count() <= chars); 152 if fits { 153 let line = wrapped.last_mut().expect("a line was seen to fit"); 154 line.push(' '); 155 line.push_str(word); 156 } else if wrapped.len() < lines { 157 wrapped.push(cut(word, chars)); 158 } else { 159 // No room for another line: the last one says there was more. 160 let last = wrapped.last_mut().expect("lines is at least one"); 161 *last = cut(&format!("{last} {word}"), chars); 162 if words.peek().is_some() && !last.ends_with("...") { 163 *last = cut(&format!("{last} ..."), chars); 164 } 165 break; 166 } 167 } 168 wrapped 169} 170 171/// The largest scale, at most `most`, at which `text` is no wider than `room`. 172fn fit(text: &str, room: usize, most: usize) -> usize { 173 (room / (text.chars().count().max(1) * 8)).clamp(1, most) 174} 175 176const MARGIN: usize = 20; 177const BAR: usize = 28; 178const BAND: usize = 40; 179 180fn draw(card: Card<'_>) -> Canvas { 181 let mut canvas = Canvas::new(); 182 canvas.dither(0, 0, WIDTH, HEIGHT - BAND, GREY); 183 // The pink band along the bottom, with its own darker dots. 184 canvas.fill(0, HEIGHT - BAND, WIDTH, BAND, PINK); 185 canvas.dither(0, HEIGHT - BAND, WIDTH, BAND, INK); 186 let site = "LET ME JEV THAT FOR YOU"; 187 let site_x = (WIDTH - width(site, 2)) / 2; 188 canvas.fill(site_x - 8, HEIGHT - BAND + 8, width(site, 2) + 16, 24, PINK); 189 canvas.text(site_x, HEIGHT - BAND + 12, site, 2, INK); 190 191 // The window. 192 let (x, y) = (MARGIN, MARGIN); 193 let (w, h) = (WIDTH - 2 * MARGIN, HEIGHT - BAND - 2 * MARGIN); 194 canvas.fill(x, y, w, h, PAPER); 195 canvas.frame(x, y, w, h, INK); 196 canvas.fill(x, y, w, BAR, INK); 197 let title = if let Card::Code { name, .. } = card { name } else { "Jev" }; 198 canvas.text(x + 10, y + 6, title, 2, PAPER); 199 let inner = x + 16; 200 let room = w - 32; 201 let top = y + BAR + 14; 202 203 match card { 204 Card::Home => { 205 let scale = 5; 206 for (line, words) in ["Let Me Jev", "That For You"].iter().enumerate() { 207 canvas.text(inner + (room - width(words, scale)) / 2, top + 22 + line * 52, words, scale, INK); 208 } 209 let ask = "Ask it anything. It says yes, no, which or how much."; 210 canvas.text(inner + (room - width(ask, 1)) / 2, top + 140, ask, 1, INK); 211 } 212 Card::Question { input, said } => { 213 // The question, small, up to two lines. 214 let label = canvas.text(inner, top, "Q:", 2, INK) + 12; 215 let lines = wrap(input, (room - label) / 16, 2); 216 for (line, words) in lines.iter().enumerate() { 217 canvas.text(inner + label, top + line * 20, words, 2, INK); 218 } 219 let mut at = top + lines.len().max(1) * 20 + 10; 220 if said.is_empty() { 221 canvas.text(inner, at + 30, "Let me Jev that for you.", 2, INK); 222 return canvas; 223 } 224 // The answer, as large as fits the width, up to 48 high. 225 let headlines: Vec<&str> = said.iter().map(|answer| answer.headline.as_str()).collect(); 226 let headline = cut(&headlines.join(" "), 66); 227 let scale = fit(&headline, room, 6); 228 canvas.text(inner, at, &headline, scale, INK); 229 at += 8 * scale + 8; 230 // Its type and numbers, then its distribution: half of what Jev says. 231 let notes: Vec<String> = said.iter().filter_map(Answer::note).collect(); 232 if !notes.is_empty() { 233 canvas.text(inner, at, &cut(¬es.join(" / "), room / 8), 1, INK); 234 at += 18; 235 } 236 if let [answer] = said { 237 detail(&mut canvas, inner, at, room, &answer.detail); 238 } 239 } 240 Card::Code { url, recurse, about, branch, latest, .. } => { 241 canvas.text(inner, top + 4, "Get the code", 4, INK); 242 // The command, white on black, as a terminal shows it. 243 let (box_y, box_h) = (top + 50, 44); 244 canvas.fill(inner, box_y, room, box_h, INK); 245 canvas.text(inner + 10, box_y + 10, if recurse { "$ git clone --recurse-submodules" } else { "$ git clone" }, 1, PAPER); 246 canvas.text(inner + 10, box_y + 26, &cut(&format!(" {url}"), (room - 20) / 8), 1, PAPER); 247 let mut at = box_y + box_h + 14; 248 if let Some(latest) = latest { 249 canvas.text(inner, at, &cut(&format!("{branch} @ {}", latest.line()), room / 8), 1, INK); 250 canvas.text(inner, at + 14, &cut(&latest.subject, room / 8), 1, INK); 251 at += 34; 252 } 253 canvas.text(inner, at, &cut(about, room / 8), 1, INK); 254 } 255 } 256 canvas 257} 258 259/// A line with a square on it at `p`: the page's slider. 260fn slider(canvas: &mut Canvas, x: usize, y: usize, w: usize, p: f64) { 261 canvas.fill(x, y + 4, w, 1, INK); 262 let thumb = x + ((w - 9) as f64 * p.clamp(0.0, 1.0)).round() as usize; 263 canvas.fill(thumb, y, 9, 9, INK); 264} 265 266/// An answer's numbers, drawn: where a yes-or-no fell between no and yes, 267/// the likeliest options of a pick, every level of a how-much. 268fn detail(canvas: &mut Canvas, x: usize, y: usize, room: usize, detail: &Detail) { 269 match detail { 270 Detail::Unknown => {} 271 Detail::Noul { p_yes: p } | Detail::Chance { p } => { 272 let (low, high) = if matches!(detail, Detail::Noul { .. }) { ("no", "yes") } else { ("0%", "100%") }; 273 canvas.text(x, y, low, 1, INK); 274 canvas.text(x + room - width(high, 1), y, high, 1, INK); 275 slider(canvas, x, y + 12, room, *p); 276 } 277 Detail::Choice { options, .. } => { 278 for (row, (label, p)) in options.iter().take(3).enumerate() { 279 let at = y + row * 22; 280 let percent = format!("{:.0}%", p * 100.0); 281 canvas.text(x, at, &cut(label, (room - 48) / 8), 1, INK); 282 canvas.text(x + room - width(&percent, 1), at, &percent, 1, INK); 283 slider(canvas, x, at + 10, room, *p); 284 } 285 } 286 Detail::Score { score, levels, .. } => { 287 // One column a level, as tall as it is likely beside the 288 // likeliest; the nearest in ink. 289 let most = levels.iter().map(|(_, p)| *p).fold(0.0, f64::max).max(f64::EPSILON); 290 let count = levels.len().max(1); 291 let nearest = (score.round().max(0.0) as usize).min(count - 1); 292 let (gap, tall) = (6, 34); 293 let column = (room - gap * (count - 1)) / count; 294 for (level, (_, p)) in levels.iter().enumerate() { 295 let height = ((tall as f64) * (p / most).clamp(0.0, 1.0)).round() as usize; 296 let left = x + level * (column + gap); 297 canvas.fill(left, y + tall - height.max(1), column, height.max(1), if level == nearest { INK } else { GREY }); 298 canvas.frame(left, y + tall - height.max(1), column, height.max(1), INK); 299 } 300 canvas.fill(x, y + tall, room, 1, INK); 301 if let (Some((lowest, _)), Some((highest, _))) = (levels.first(), levels.last()) { 302 let half = room / 16 - 1; 303 let highest = cut(highest, half); 304 canvas.text(x, y + tall + 5, &cut(lowest, half), 1, INK); 305 canvas.text(x + room - width(&highest, 1), y + tall + 5, &highest, 1, INK); 306 } 307 } 308 } 309} 310 311/// The card as a PNG. 312pub fn png(card: Card<'_>) -> Vec<u8> { 313 let canvas = draw(card); 314 // Two bits a pixel, four pixels a byte, first pixel in the high bits. 315 let packed: Vec<u8> = canvas 316 .pixels 317 .chunks(4) 318 .map(|four| four.iter().enumerate().fold(0, |byte, (index, colour)| byte | colour << (6 - 2 * index))) 319 .collect(); 320 let mut out = Vec::new(); 321 let mut encoder = png::Encoder::new(&mut out, WIDTH as u32, HEIGHT as u32); 322 encoder.set_color(png::ColorType::Indexed); 323 encoder.set_depth(png::BitDepth::Two); 324 encoder.set_palette(PALETTE.to_vec()); 325 encoder.set_compression(png::Compression::Fast); 326 let mut writer = encoder.write_header().expect("the header is valid for these fixed settings"); 327 writer.write_image_data(&packed).expect("the data is the size the header says"); 328 writer.finish().expect("writing to a Vec cannot fail"); 329 out 330} 331 332#[cfg(test)] 333mod tests;