research-spirograph.rs
6.4 kB · rust · 181 lines
1use mrlycore::errors::Result;2use mrlyfig::{ink, plot, save, Board};3use mrlylab::roulette;4use mrlymath::two;5use mrlynum::spirograph::{6 cover, disc, distinct, nodes as law, pencils, trace, track, Cover, Pencil, Track,7};8use std::collections::HashSet;910const CODE: u128 = 495;11const SIDE: usize = 3;12const LEVEL: usize = 1;13const BASE: usize = 3;14const RING: usize = 7;15const WHEEL: usize = 3;16const REACH: f64 = 0.9;17const SAMPLES: usize = 8000;18const CURVES: usize = 8;19const CROSSINGS: usize = 1288;20const RASTER: usize = 1024;21const TOL: f64 = 4e-4;22const MARGIN: f64 = 0.08;23const GRID: usize = 256;24const SUB: usize = 4;25const WASH: f64 = 0.3;26const THIN: f64 = 1.4;27const HAIR: f64 = 1.1;28const DOT: f64 = 2.6;2930fn main() -> Result<()> {31 let tile = two::create(CODE, SIDE, LEVEL, 0, BASE)?;32 let types = tile.types().bytes().to_vec();33 let path = track("in", RING, WHEEL, 4, 1)?;34 let pens = pencils(&types, tile.width(), tile.height(), "fill", REACH, 0.0, 1)?;35 assert_eq!(distinct(&path, &pens, true), CURVES);36 assert_eq!(law(&path, &pens, true), Some(CROSSINGS as u64));37 assert_eq!(38 roulette::nodes(&path, &pens, SAMPLES, TOL)?.total(),39 CROSSINGS40 );41 let marks = crossings(&path, &pens, SAMPLES)?;42 assert_eq!(marks.len(), CROSSINGS);43 let bounds = disc(&path, &pens)?;44 let shape = cover(&path, &pens, true, 2, RASTER)?;45 let points = trace(&path, &pens, SAMPLES)?;4647 let mut board = Board::square();48 let area = board.frame(MARGIN);49 let (cx, cy) = area.center();50 let scale = area.radius() / RING as f64;51 let at = |x: f64, y: f64| (cx + (x - bounds.x) * scale, cy - (y - bounds.y) * scale);52 let span = 2.0 * bounds.radius * scale;53 wash(&mut board, &shape, (cx - span / 2.0, cy - span / 2.0), span);54 board.ring(cx, cy, RING as f64 * scale, HAIR, ink::line());55 for k in 0..CURVES {56 let curve: Vec<(f64, f64)> = (0..SAMPLES)57 .map(|i| {58 let j = 2 * (k * SAMPLES + i);59 at(f64::from(points[j]), f64::from(points[j + 1]))60 })61 .collect();62 board.polyline(&curve, THIN, ink::blue());63 }64 let dots: Vec<(f64, f64)> = marks.iter().map(|&(x, y)| at(x, y)).collect();65 plot::dots(&mut board, &dots, DOT, ink::orange());66 save("research-spirograph", &board)?;67 Ok(())68}6970// THE SHAPE7172fn wash(board: &mut Board, shape: &Cover, corner: (f64, f64), span: f64) {73 let n = shape.side;74 let cell = span / n as f64;75 let x0 = corner.0.floor().max(0.0) as usize;76 let y0 = corner.1.floor().max(0.0) as usize;77 let x1 = (corner.0 + span).ceil().max(0.0) as usize;78 let y1 = (corner.1 + span).ceil().max(0.0) as usize;79 for py in y0..y1 {80 for px in x0..x1 {81 let mut hit = 0;82 for sy in 0..SUB {83 for sx in 0..SUB {84 let u = (px as f64 + (sx as f64 + 0.5) / SUB as f64 - corner.0) / cell;85 let v = (py as f64 + (sy as f64 + 0.5) / SUB as f64 - corner.1) / cell;86 if u < 0.0 || v < 0.0 {87 continue;88 }89 let (column, row) = (u as usize, v as usize);90 if column < n && row < n && shape.mask[row * n + column] == 3 {91 hit += 1;92 }93 }94 }95 if hit > 0 {96 board.blend(97 px,98 py,99 ink::dim(),100 WASH * f64::from(hit) / (SUB * SUB) as f64,101 );102 }103 }104 }105}106107// THE NODES108109fn crossings(path: &Track, pens: &[Pencil], samples: usize) -> Result<Vec<(f64, f64)>> {110 let points = trace(path, pens, samples)?;111 let steps = samples - 1;112 let at = |k: usize, i: usize| {113 let base = 2 * (k * samples + i);114 (f64::from(points[base]), f64::from(points[base + 1]))115 };116 let (mut low, mut high) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));117 for pair in points.chunks_exact(2) {118 let (x, y) = (f64::from(pair[0]), f64::from(pair[1]));119 low = (low.0.min(x), low.1.min(y));120 high = (high.0.max(x), high.1.max(y));121 }122 let size = (high.0 - low.0, high.1 - low.1);123 let cell = |p: (f64, f64)| {124 let column = ((p.0 - low.0) / size.0 * GRID as f64) as usize;125 let row = ((p.1 - low.1) / size.1 * GRID as f64) as usize;126 (column.min(GRID - 1), row.min(GRID - 1))127 };128 let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); GRID * GRID];129 for k in 0..pens.len() {130 for i in 0..steps {131 let (a, b) = (at(k, i), at(k, i + 1));132 let (x0, y0) = cell((a.0.min(b.0), a.1.min(b.1)));133 let (x1, y1) = cell((a.0.max(b.0), a.1.max(b.1)));134 for x in x0..=x1 {135 for y in y0..=y1 {136 buckets[x * GRID + y].push((k * steps + i) as u32);137 }138 }139 }140 }141 let mut seen: HashSet<(u32, u32)> = HashSet::new();142 let mut out = Vec::new();143 for bucket in &buckets {144 for (u, &left) in bucket.iter().enumerate() {145 for &right in &bucket[u + 1..] {146 let (p, q) = (left as usize, right as usize);147 let ((kp, ip), (kq, iq)) = ((p / steps, p % steps), (q / steps, q % steps));148 let gap = ip.abs_diff(iq);149 if kp == kq && (gap == 1 || gap == steps - 1) {150 continue;151 }152 let (a, b) = (at(kp, ip), at(kp, ip + 1));153 let (c, d) = (at(kq, iq), at(kq, iq + 1));154 if side(a, b, c) * side(a, b, d) < 0155 && side(c, d, a) * side(c, d, b) < 0156 && seen.insert((left.min(right), left.max(right)))157 {158 out.push(meet(a, b, c, d));159 }160 }161 }162 }163 Ok(out)164}165166fn side(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> i32 {167 let turn = (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0);168 match turn.partial_cmp(&0.0) {169 Some(std::cmp::Ordering::Greater) => 1,170 Some(std::cmp::Ordering::Less) => -1,171 _ => 0,172 }173}174175fn meet(a: (f64, f64), b: (f64, f64), c: (f64, f64), d: (f64, f64)) -> (f64, f64) {176 let run = (b.0 - a.0, b.1 - a.1);177 let other = (d.0 - c.0, d.1 - c.1);178 let denominator = run.0 * other.1 - run.1 * other.0;179 let step = ((c.0 - a.0) * other.1 - (c.1 - a.1) * other.0) / denominator;180 (a.0 + step * run.0, a.1 + step * run.1)181}