research-apollonian.rs
4.8 kB · rust · 178 lines
1use mrlycore::errors::Result;2use mrlyfig::{ink, save, Board, Color, Ramp};3use mrlynum::factor::gcd;4use mrlynum::lattice::totients;56const TOP: i64 = 2048;7const DEEP: usize = 32;8const CIRCLES: usize = 2448;9const RESTING: usize = 323;10const THICK: f64 = 1.7;11const HAIR: f64 = 1.2;12const RULE: f64 = 2.0;13const OVER: f64 = 0.035;1415#[derive(Clone, Copy)]16struct Circle {17 k: i64,18 x: i64,19 y: i64,20}2122type Quad = [Circle; 4];2324fn root() -> Quad {25 [26 Circle { k: 0, x: 0, y: -1 },27 Circle { k: 0, x: 0, y: 1 },28 Circle { k: 2, x: 0, y: 1 },29 Circle { k: 2, x: 2, y: 1 },30 ]31}3233fn descartes(q: &Quad) -> bool {34 let sum: i64 = q.iter().map(|c| c.k).sum();35 let squares: i64 = q.iter().map(|c| c.k * c.k).sum();36 sum * sum == 2 * squares37}3839fn reflect(q: &Quad, i: usize) -> Circle {40 let (mut k, mut x, mut y) = (0i64, 0i64, 0i64);41 for (j, c) in q.iter().enumerate() {42 if j != i {43 k += c.k;44 x += c.x;45 y += c.y;46 }47 }48 Circle {49 k: 2 * k - q[i].k,50 x: 2 * x - q[i].x,51 y: 2 * y - q[i].y,52 }53}5455fn swap(q: &Quad, i: usize) -> Quad {56 let mut out = *q;57 out[i] = reflect(q, i);58 out59}6061fn grow(cap: i64) -> Vec<Circle> {62 let seed = root();63 let mut out = Vec::new();64 let mut stack: Vec<(Quad, usize)> = Vec::new();65 assert!(descartes(&seed));66 for i in 0..2 {67 if reflect(&seed, i).k <= cap {68 stack.push((swap(&seed, i), i));69 }70 }71 while let Some((q, last)) = stack.pop() {72 assert!(descartes(&q));73 out.push(q[last]);74 for j in 0..4 {75 if j == last {76 continue;77 }78 let next = reflect(&q, j);79 if next.k > cap || next.k <= q[j].k {80 continue;81 }82 stack.push((swap(&q, j), j));83 }84 }85 out86}8788fn fraction(c: Circle) -> (i64, i64) {89 let b = ((c.k / 2) as f64).sqrt().round() as i64;90 if b <= 0 {91 return (0, 0);92 }93 (c.x / (2 * b), b)94}9596fn mark(board: &mut Board, at: (f64, f64), r: f64, thick: f64, span: (f64, f64), color: Color) {97 let half = thick / 2.0;98 let reach = r + half + 1.0;99 let x0 = (at.0 - reach).max(span.0 - 1.0).max(0.0).floor() as usize;100 let x1 = (at.0 + reach).min(span.1 + 1.0).max(0.0).ceil() as usize;101 let y0 = (at.1 - reach).max(0.0).floor() as usize;102 let y1 = (at.1 + reach).max(0.0).ceil() as usize;103 for py in y0..y1.min(board.height) {104 for px in x0..x1.min(board.width) {105 let (fx, fy) = (px as f64 + 0.5, py as f64 + 0.5);106 let ring = (((fx - at.0).powi(2) + (fy - at.1).powi(2)).sqrt() - r).abs() - half;107 let cut = (span.0 - fx).max(fx - span.1);108 board.blend(px, py, color, 0.5 - ring.max(cut));109 }110 }111}112113fn main() -> Result<()> {114 let seed = root();115 let grown = grow(TOP);116 let phi = totients(DEEP);117 let nodes = phi[1..=DEEP].iter().sum::<u64>() as usize - 1;118 let resting: Vec<Circle> = grown.iter().copied().filter(|c| c.y == 1).collect();119120 assert_eq!(grown.len(), CIRCLES);121 assert_eq!(resting.len(), RESTING);122 assert_eq!(resting.len(), nodes);123 assert!(grown.iter().all(|c| c.k > 0 && c.x > 0 && c.x < c.k));124 let mut deepest = 1;125 for c in &resting {126 let (a, b) = fraction(*c);127 assert_eq!((c.k, c.x, c.y), (2 * b * b, 2 * a * b, 1));128 assert_eq!(gcd(a as usize, b as usize), 1);129 assert!(0 < a && a < b && b <= DEEP as i64);130 deepest = deepest.max(b);131 }132 assert_eq!(deepest, DEEP as i64);133 assert_eq!(fraction(seed[2]), (0, 1));134 assert_eq!(fraction(seed[3]), (1, 1));135136 let mut board = Board::square();137 let frame = board.frame(0.08);138 let span = (frame.x, frame.x + frame.w);139 let over = frame.w * OVER;140 let place = |c: &Circle| {141 (142 (143 frame.x + frame.w * c.x as f64 / c.k as f64,144 frame.y + frame.h * (1.0 - c.y as f64 / c.k as f64),145 ),146 frame.w / c.k as f64,147 )148 };149 for foot in [frame.y, frame.y + frame.h] {150 board.segment(151 (span.0 - over, foot),152 (span.1 + over, foot),153 RULE,154 ink::dim(),155 );156 }157 let rest = ink::fade(ink::dim(), 0.75);158 for c in grown.iter().filter(|c| c.y != 1) {159 let (at, r) = place(c);160 mark(&mut board, at, r, HAIR, span, rest);161 }162 let ramp = Ramp::tone(ink::blue(), ink::fg());163 let reach = (DEEP as f64).ln();164 for c in resting.iter().chain([seed[2], seed[3]].iter()) {165 let (at, r) = place(c);166 let (_, b) = fraction(*c);167 mark(168 &mut board,169 at,170 r,171 THICK,172 span,173 ramp.at((b as f64).ln() / reach),174 );175 }176 save("research-apollonian", &board)?;177 Ok(())178}