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