1use std::io::IsTerminal;
13
14use inkling::{
15 art::Art,
16 frame,
17 ordering::{Geodesic, GeodesicReport, Ordering, StartHint},
18};
19
20fn main() {
21 let args: Vec<String> = std::env::args().skip(1).collect();
22 let snapshots = args.iter().any(|a| a == "--snapshots");
23
24 let art = match arg_value(&args, "--art") {
25 Some(path) => match std::fs::read_to_string(&path) {
26 Ok(text) => Art::parse(&text),
27 Err(e) => {
28 eprintln!("inkling: could not read {path}: {e}");
29 std::process::exit(1);
30 }
31 },
32 None => Art::parse(&serpent(64, 13)),
33 };
34
35 let ordering = Geodesic {
36 start: StartHint::TopLeft,
37 };
38 let GeodesicReport {
39 ink_cells,
40 connected_cells,
41 spine_length,
42 } = ordering.diagnose(&art);
43 let ranks = ordering.rank(&art);
44
45 eprintln!(
46 "inkling · {ink_cells} ink cells · {connected_cells} on the spine \
47 ({:.0}% connected) · spine length {spine_length}",
48 100.0 * connected_cells as f32 / ink_cells.max(1) as f32,
49 );
50
51 if snapshots || !std::io::stdout().is_terminal() {
53 for p in [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] {
54 println!("\n── progress {:>3.0}% {}", p * 100.0, "─".repeat(28));
55 print!("{}", frame::to_string(&art, &ranks, p));
56 }
57 return;
58 }
59
60 #[cfg(feature = "terminal")]
61 {
62 use inkling::{
63 easing::Easing,
64 render::{animate, Style},
65 };
66 use std::time::Duration;
67 if let Err(e) = animate(
68 &art,
69 &ranks,
70 Style::default(),
71 Duration::from_millis(3500),
72 Easing::EaseInOutCubic,
73 ) {
74 eprintln!("inkling: render error: {e}");
75 }
76 }
77}
78
79fn arg_value(args: &[String], key: &str) -> Option<String> {
81 let mut it = args.iter();
82 while let Some(a) = it.next() {
83 if a == key {
84 return it.next().cloned();
85 }
86 if let Some(v) = a.strip_prefix(&format!("{key}=")) {
87 return Some(v.to_string());
88 }
89 }
90 None
91}
92
93#[allow(clippy::needless_range_loop)]
100fn serpent(width: usize, height: usize) -> String {
101 use std::f32::consts::TAU;
102
103 let amp = ((height as f32) - 3.0).max(1.0) / 2.0;
104 let mid = (height as f32 - 1.0) / 2.0;
105 let period = (width as f32) / 2.0; let y_at = |x: usize| -> usize {
108 let yf = mid - amp * ((x as f32) / period * TAU).sin();
109 yf.round().clamp(0.0, height as f32 - 1.0) as usize
110 };
111
112 let mut grid = vec![vec![' '; width]; height];
113 let mut prev_y = y_at(0);
114 for x in 0..width {
115 let y = y_at(x);
116 if x > 0 && y.abs_diff(prev_y) > 1 {
118 for row in grid[y.min(prev_y)..=y.max(prev_y)].iter_mut() {
119 if row[x] == ' ' {
120 row[x] = '|';
121 }
122 }
123 }
124 grid[y][x] = match prev_y {
125 _ if x == 0 => '~',
126 py if y < py => '/',
127 py if y > py => '\\',
128 _ => '~',
129 };
130 prev_y = y;
131 }
132
133 for x in 1..width.saturating_sub(1) {
135 let y = y_at(x);
136 if y > 0 && y < y_at(x - 1) && y <= y_at(x + 1) {
137 grid[y - 1][x] = '^';
138 }
139 }
140
141 let (hx, hy) = (width - 1, y_at(width - 1));
143 grid[hy][hx] = '>';
144 if hy > 0 {
145 grid[hy - 1][hx.saturating_sub(1)] = 'o'; }
147
148 grid.into_iter()
149 .map(|row| row.into_iter().collect::<String>())
150 .collect::<Vec<_>>()
151 .join("\n")
152}