The look of a link preview, as SVG: the dither field, the pink band, the
window with its black title bar, and type set a pixel at a time in an
8 by 8 bitmap face. It is lmjtfy's card (packages/card, drawn there
straight into a four-colour PNG) as shapes a site can put things inside,
so a site's card looks like the page it unfurls.
Nothing here reads a file or a font: a card is the same bytes everywhere. A site rasterises what it composes (resvg, for whiskers) and owns what goes inside the window.
Every measure is lmjtfy's, on a 600 by 315 sheet, times a whole unit:
Chrome::new(2) is the 1200 by 630 a large card wants.
14use std::fmt::Write;
A rectangle, in picture pixels.
A 4 by 4 ordered dither: how dark a field has to be before each cell of the pattern takes ink.
33const BAYER: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
The sheet's geometry at a whole-number scale.
unit of 0 has nothing to draw.
51 pub fn unit(&self) -> u32 { 52 self.unit 53 } 54 55 pub fn width(&self) -> u32 { 56 Self::BASE_WIDTH * self.unit 57 } 58 59 pub fn height(&self) -> u32 { 60 Self::BASE_HEIGHT * self.unit 61 } 62 63 fn margin(&self) -> u32 { 64 20 * self.unit 65 } 66 67 fn bar(&self) -> u32 { 68 28 * self.unit 69 }
The window: paper with a one-pixel ink frame, over the dither.
Where a card's own content goes: inside the window, under its title bar, with lmjtfy's padding.
The dot field, the pink band and the site's name on it, as a whole SVG
document. It is the part of a card that never changes, so a site
rasterises it once and lays the result under every frame (background_image).
91 pub fn background_svg(&self, site: &str) -> String { 92 let (w, h, u) = (self.width(), self.height(), self.unit); 93 let mut out = format!( 94 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\" shape-rendering=\"crispEdges\">\ 95 <rect width=\"{w}\" height=\"{h}\" fill=\"{PAPER}\"/>" 96 ); 97 out.push_str(&dither(0, 0, w, h - self.band(), 2 * u, GREY)); 98 let _ = write!(out, "<rect y=\"{}\" width=\"{w}\" height=\"{}\" fill=\"{PINK}\"/>", h - self.band(), self.band()); 99 out.push_str(&dither(0, h - self.band(), w, self.band(), 2 * u, INK)); 100 let site = site.to_uppercase(); 101 let scale = 2 * u; 102 let at = (w - width(scale, &site)) / 2; 103 let _ = write!(out, "<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"{PINK}\"/>", at - 8 * u, h - self.band() + 8 * u, width(scale, &site) + 16 * u, 24 * u); 104 out.push_str(&text(at, h - self.band() + 12 * u, scale, INK, &site)); 105 out.push_str("</svg>"); 106 out 107 }
The background laid as a picture, from a file the site rasterised beside the card.
The window: paper, its frame, and a black title bar with title in it.
115 pub fn window_svg(&self, title: &str) -> String { 116 let (Rect { x, y, w, h }, u) = (self.window(), self.unit); 117 let mut out = format!("<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" fill=\"{PAPER}\"/>"); 118 let _ = write!(out, "<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{}\" fill=\"{INK}\"/>", self.bar()); 119 let _ = write!(out, "<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"none\" stroke=\"{INK}\" stroke-width=\"{u}\"/>", x as f32 + u as f32 / 2.0, y as f32 + u as f32 / 2.0, w - u, h - u); 120 out.push_str(&text(x + 10 * u, y + 6 * u, 2 * u, PAPER, title)); 121 out 122 } 123}
The dot field: dense at the top left and bottom right corners, thinning to nothing across the
middle, in cell-pixel dots, as lmjtfy's card. One path, so a large field stays a small file.
127pub fn dither(x0: u32, y0: u32, w: u32, h: u32, cell: u32, colour: &str) -> String { 128 let mut d = String::new(); 129 for y in (y0..y0 + h).step_by(cell as usize) { 130 for x in (x0..x0 + w).step_by(cell as usize) { 131 let across = f64::from(x - x0) / f64::from(w); 132 let down = f64::from(y - y0) / f64::from(h); 133 let from_corner = (across + down).min(2.0 - across - down); 134 let dark = (1.0 - from_corner * 1.6).max(0.0) * 16.0; 135 if f64::from(BAYER[((y / cell) % 4) as usize][((x / cell) % 4) as usize]) < dark { 136 let _ = write!(d, "M{x} {y}h{cell}v{cell}h-{cell}z"); 137 } 138 } 139 } 140 format!("<path fill=\"{colour}\" d=\"{d}\"/>") 141}
Pixel text: scale output pixels to a font pixel, from the top left at (x, y). Runs of lit
pixels in a row are one rectangle. A character the face lacks is a question mark.
149pub fn text(x: u32, y: u32, scale: u32, colour: &str, words: &str) -> String { 150 let mut out = String::new(); 151 for (i, ch) in words.chars().enumerate() { 152 for (row, bits) in glyph(ch).iter().enumerate() { 153 // The least significant bit is the leftmost pixel. 154 let mut col = 0; 155 while col < 8 { 156 if bits >> col & 1 == 1 { 157 let start = col; 158 while col < 8 && bits >> col & 1 == 1 { 159 col += 1; 160 } 161 let _ = write!( 162 out, 163 "<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{scale}\" fill=\"{colour}\"/>", 164 x + (i as u32 * 8 + start) * scale, 165 y + row as u32 * scale, 166 (col - start) * scale, 167 ); 168 } else { 169 col += 1; 170 } 171 } 172 } 173 } 174 out 175}
How wide words is at scale.
182#[cfg(test)] 183mod tests { 184 use super::*; 185 186 #[test] 187 fn a_glyph_is_drawn_from_its_bitmap() { 188 assert!(text(0, 0, 1, "#fff", "I").contains("<rect")); 189 assert_eq!(text(0, 0, 1, "#fff", " "), ""); 190 // A character outside the face draws the question mark rather than nothing. 191 assert_eq!(text(0, 0, 2, "#fff", "\u{1f408}"), text(0, 0, 2, "#fff", "?")); 192 } 193 194 #[test] 195 fn the_chrome_is_lmjtfys_sheet_times_the_unit() { 196 let chrome = Chrome::new(2); 197 assert_eq!((chrome.width(), chrome.height()), (1200, 630)); 198 let window = chrome.window(); 199 assert_eq!(window, Rect { x: 40, y: 40, w: 1120, h: 470 }); 200 let content = chrome.content(); 201 assert!(content.x > window.x && content.y > window.y + 56); 202 assert!(content.x + content.w < window.x + window.w); 203 assert!(content.y + content.h <= window.y + window.h); 204 // The band sits under the window and the window never reaches it. 205 assert!(window.y + window.h < chrome.height() - chrome.band()); 206 } 207 208 #[test] 209 #[should_panic(expected = "at least 600 by 315")] 210 fn there_is_no_card_of_no_size() { 211 Chrome::new(0); 212 } 213 214 #[test] 215 fn the_background_is_one_svg_the_size_of_the_card_with_the_site_on_the_band() { 216 let svg = Chrome::new(2).background_svg("Let Me Jev That For You"); 217 assert!(svg.starts_with("<svg") && svg.ends_with("</svg>")); 218 assert!(svg.contains("width=\"1200\" height=\"630\"")); 219 assert_eq!(svg.matches("<path ").count(), 2, "one dot field above the band and one on it"); 220 let name = "LET ME JEV THAT FOR YOU"; 221 let at = (1200 - width(4, name)) / 2; 222 assert!(svg.contains(&text(at, 630 - 80 + 24, 4, INK, name)), "the site's name is set in ink, centred, in capitals"); 223 assert!(!svg.contains("NaN")); 224 } 225 226 #[test] 227 fn the_dither_is_densest_at_the_corners_and_empty_across_the_middle() { 228 let field = dither(0, 0, 400, 400, 4, INK); 229 let corner = field.matches('M').filter(|_| true).count(); 230 assert!(corner > 0); 231 // Along the anti-diagonal the field is as far from both corners as it gets: no dot there. 232 let d = field.split("d=\"").nth(1).unwrap(); 233 for at in (160..240).step_by(4) { 234 let y = 400 - at; 235 assert!(!d.contains(&format!("M{at} {y}h")), "a dot at ({at}, {y})"); 236 } 237 // Dots are `cell` pixels square. 238 assert!(d.contains("h4v4h-4z")); 239 } 240 241 #[test] 242 fn the_window_has_its_title_in_a_bar() { 243 let svg = Chrome::new(2).window_svg("Jev"); 244 assert!(svg.contains(&format!("fill=\"{INK}\""))); 245 assert!(svg.contains(&text(40 + 20, 40 + 12, 4, PAPER, "Jev"))); 246 } 247}