1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct HexPattern {
13 pub startdir: String,
15 #[serde(default)]
17 pub signature: String,
18}
19
20type V = (f32, f32);
21
22const DIRS: [(i32, i32); 6] = [(1, -1), (1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1)];
24
25fn dir_index(d: &str) -> Option<usize> {
26 Some(match d.trim().to_ascii_uppercase().as_str() {
27 "NE" | "NORTH_EAST" => 0,
28 "E" | "EAST" => 1,
29 "SE" | "SOUTH_EAST" => 2,
30 "SW" | "SOUTH_WEST" => 3,
31 "W" | "WEST" => 4,
32 "NW" | "NORTH_WEST" => 5,
33 _ => return None,
34 })
35}
36
37impl HexPattern {
38 pub fn positions(&self) -> Vec<(i32, i32)> {
40 let Some(mut dir) = dir_index(&self.startdir) else { return Vec::new() };
41 let mut at = (0, 0);
42 let mut out = vec![at];
43 for c in self.signature.chars() {
44 let turn = match c {
45 'w' => 0,
46 'e' => 1,
47 'd' => 2,
48 's' => 3,
49 'a' => 4,
50 'q' => 5,
51 _ => continue,
52 };
53 at = (at.0 + DIRS[dir].0, at.1 + DIRS[dir].1);
54 out.push(at);
55 dir = (dir + turn) % 6;
56 }
57 out.push((at.0 + DIRS[dir].0, at.1 + DIRS[dir].1));
58 out
59 }
60}
61
62fn to_px((q, r): (i32, i32)) -> V {
64 let s3 = 3f32.sqrt();
65 (s3 * q as f32 + s3 / 2.0 * r as f32, 1.5 * r as f32)
66}
67
68fn dup_indices(pos: &[(i32, i32)]) -> Vec<bool> {
70 let mut first = std::collections::HashMap::new();
71 let mut dup = vec![false; pos.len()];
72 for (i, p) in pos.iter().enumerate() {
73 match first.get(p) {
74 Some(&j) => {
75 dup[i] = true;
76 dup[j] = true;
77 }
78 None => {
79 first.insert(*p, i);
80 }
81 }
82 }
83 dup
84}
85
86struct Lcg(i64);
90
91impl Lcg {
92 fn new(seed: i64) -> Lcg {
93 Lcg((seed ^ 0x5DEECE66D) & ((1 << 48) - 1))
94 }
95 fn next(&mut self, bits: u32) -> i32 {
96 self.0 = (self.0.wrapping_mul(0x5DEECE66D).wrapping_add(11)) & ((1 << 48) - 1);
97 (self.0 >> (48 - bits)) as i32
98 }
99 fn next_int(&mut self, bound: i32) -> i32 {
100 if bound & (bound - 1) == 0 {
101 return ((bound as i64 * self.next(31) as i64) >> 31) as i32;
102 }
103 loop {
104 let i = self.next(31);
105 let j = i % bound;
106 if i.wrapping_sub(j).wrapping_add(bound - 1) >= 0 {
107 return j;
108 }
109 }
110 }
111 fn next_double(&mut self) {
112 self.next(26);
113 self.next(27);
114 }
115}
116
117struct Simplex([i32; 256]);
119
120const GRADIENT: [[i32; 3]; 16] = [
121 [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
122 [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1], [1, 1, 0], [0, -1, 1], [-1, 1, 0], [0, -1, -1],
123];
124
125impl Simplex {
126 fn new(rng: &mut Lcg) -> Simplex {
127 for _ in 0..3 {
129 rng.next_double();
130 }
131 let mut p = [0i32; 256];
132 for (i, v) in p.iter_mut().enumerate() {
133 *v = i as i32;
134 }
135 for l in 0..256 {
136 let j = rng.next_int(256 - l as i32) as usize;
137 p.swap(l, j + l);
138 }
139 Simplex(p)
140 }
141 fn p(&self, i: i32) -> i32 {
142 self.0[(i & 255) as usize]
143 }
144 fn corner(g: i32, x: f64, y: f64, z: f64) -> f64 {
145 let d = 0.6 - x * x - y * y - z * z;
146 if d < 0.0 {
147 return 0.0;
148 }
149 let d = d * d;
150 let g = GRADIENT[g as usize];
151 d * d * (g[0] as f64 * x + g[1] as f64 * y + g[2] as f64 * z)
152 }
153 fn value(&self, x: f64, y: f64, z: f64) -> f64 {
154 let s = (x + y + z) * 0.333_333_333_333_333_3;
155 let (i, j, k) = ((x + s).floor() as i32, (y + s).floor() as i32, (z + s).floor() as i32);
156 let t = (i + j + k) as f64 * 0.166_666_666_666_666_66;
157 let (x0, y0, z0) = (x - (i as f64 - t), y - (j as f64 - t), z - (k as f64 - t));
158 let (i1, j1, k1, i2, j2, k2) = if x0 >= y0 {
159 if y0 >= z0 {
160 (1, 0, 0, 1, 1, 0)
161 } else if x0 >= z0 {
162 (1, 0, 0, 1, 0, 1)
163 } else {
164 (0, 0, 1, 1, 0, 1)
165 }
166 } else if y0 < z0 {
167 (0, 0, 1, 0, 1, 1)
168 } else if x0 < z0 {
169 (0, 1, 0, 0, 1, 1)
170 } else {
171 (0, 1, 0, 1, 1, 0)
172 };
173 let g6 = 0.166_666_666_666_666_66;
174 let g3 = 0.333_333_333_333_333_3;
175 let (x1, y1, z1) = (x0 - i1 as f64 + g6, y0 - j1 as f64 + g6, z0 - k1 as f64 + g6);
176 let (x2, y2, z2) = (x0 - i2 as f64 + g3, y0 - j2 as f64 + g3, z0 - k2 as f64 + g3);
177 let (x3, y3, z3) = (x0 - 0.5, y0 - 0.5, z0 - 0.5);
178 let (ii, jj, kk) = (i & 255, j & 255, k & 255);
179 let g0 = self.p(ii + self.p(jj + self.p(kk))) % 12;
180 let g1 = self.p(ii + i1 + self.p(jj + j1 + self.p(kk + k1))) % 12;
181 let g2 = self.p(ii + i2 + self.p(jj + j2 + self.p(kk + k2))) % 12;
182 let g3i = self.p(ii + 1 + self.p(jj + 1 + self.p(kk + 1))) % 12;
183 32.0 * (Self::corner(g0, x0, y0, z0)
184 + Self::corner(g1, x1, y1, z1)
185 + Self::corner(g2, x2, y2, z2)
186 + Self::corner(g3i, x3, y3, z3))
187 }
188}
189
190fn noise() -> &'static Simplex {
191 static N: std::sync::OnceLock<Simplex> = std::sync::OnceLock::new();
192 N.get_or_init(|| Simplex::new(&mut Lcg::new(9001)))
193}
194
195fn get_noise(x: f64, y: f64, z: f64) -> f64 {
197 noise().value(x * 0.6, y * 0.6, z * 0.6) / 2.0
198}
199
200fn mth_sin(f: f32) -> f32 {
202 let i = ((f * 10430.378) as i32) & 0xFFFF;
203 (i as f64 * std::f64::consts::PI * 2.0 / 65536.0).sin() as f32
204}
205fn mth_cos(f: f32) -> f32 {
206 let i = ((f * 10430.378 + 16384.0) as i32) & 0xFFFF;
207 (i as f64 * std::f64::consts::PI * 2.0 / 65536.0).sin() as f32
208}
209
210fn make_zappy(points: &[V], dups: &[bool], readability: f32, last_segment: f32, seed: f64) -> Vec<V> {
212 const HOPS: usize = 10;
213 const VARIANCE: f32 = 0.5;
214 const FLOW_IRREGULAR: f64 = 0.2;
215 let zappify = |pts: &[V], truncate_last: bool| -> Vec<V> {
216 let scale_variance = |t: f64| 1f64.min(8.0 * (0.5 - (0.5 - t).abs()));
217 let mut out = vec![pts[0]];
218 for i in 0..pts.len().saturating_sub(1) {
219 let (src, target) = (pts[i], pts[i + 1]);
220 let delta = (target.0 - src.0, target.1 - src.1);
221 let hop_dist = ((target.0 - src.0).powi(2) + (target.1 - src.1).powi(2)).sqrt() / HOPS as f32;
222 let max_variance = hop_dist * VARIANCE;
223 let max_j = if truncate_last && i == pts.len() - 2 {
224 (last_segment * HOPS as f32).round() as usize
225 } else {
226 HOPS
227 };
228 for j in 1..=max_j {
229 let progress = j as f64 / (HOPS + 1) as f64;
230 let pos = (src.0 + delta.0 * progress as f32, src.1 + delta.1 * progress as f32);
231 let minor = get_noise(i as f64, j as f64, 0.0) * FLOW_IRREGULAR;
232 let theta = (3.0 * get_noise(i as f64 + progress + minor, 1337.0, seed) * std::f64::consts::TAU) as f32;
233 let r = (get_noise(i as f64 + progress, 69420.0, seed) * max_variance as f64 * scale_variance(progress)) as f32;
234 out.push((pos.0 + r * mth_cos(theta), pos.1 + r * mth_sin(theta)));
235 if j == HOPS {
236 out.push(target);
237 }
238 }
239 }
240 out
241 };
242 let mut out = Vec::new();
243 let mut chain: Vec<V> = Vec::new();
244 for i in 0..points.len().saturating_sub(1) {
245 let (head, tail) = (points[i], points[i + 1]);
246 let tangent = ((tail.0 - head.0) * readability, (tail.1 - head.1) * readability);
247 chain.push(if i != 0 && dups[i] { (head.0 + tangent.0, head.1 + tangent.1) } else { head });
248 if i == points.len() - 2 {
249 chain.push(tail);
250 out.extend(zappify(&chain, true));
251 } else if dups[i + 1] {
252 chain.push((tail.0 - tangent.0, tail.1 - tangent.1));
253 out.extend(zappify(&chain, false));
254 chain.clear();
255 }
256 }
257 out
258}
259
260const OUTER: &str = "#d2c8c8";
262const INNER: &str = "#554d54";
263const START_DOT: &str = "#5b7bd7";
264const STROKE: f64 = 4.0;
265
266pub fn svg(patterns: &[HexPattern], w: f32, h: f32) -> String {
269 let (w, h) = (w as f64, h as f64);
270 let mut s = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">"#);
271 let n = patterns.len().max(1);
272 let cols = (n as f64).sqrt().ceil() as usize;
273 let rows = n.div_ceil(cols);
274 let (cell_w, cell_h) = (w / cols as f64, h / rows as f64);
275 for (k, pat) in patterns.iter().enumerate() {
276 let pos = pat.positions();
277 if pos.len() < 2 {
278 continue;
279 }
280 let dots: Vec<V> = pos.iter().map(|&p| to_px(p)).collect();
281 let zappy = make_zappy(&dots, &dup_indices(&pos), 0.2, 0.8, 0.0);
282 let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
284 for &(x, y) in &zappy {
285 x0 = x0.min(x as f64);
286 x1 = x1.max(x as f64);
287 y0 = y0.min(y as f64);
288 y1 = y1.max(y as f64);
289 }
290 let base = 16.0 / 1.5;
291 let (bw, bh) = ((x1 - x0) * base, (y1 - y0) * base);
292 let mut scale = 1f64;
293 scale = scale.min((cell_h - 4.0 - STROKE) / bh);
294 scale = scale.min((cell_w - 4.0 - STROKE) / bw);
295 let fin = base * scale;
296 let (iw, ih) = (bw * scale + 4.0 + STROKE, bh * scale + 4.0 + STROKE);
297 let off_x = (iw - bw * scale) / 2.0 + (cell_w - iw).max(0.0) / 2.0;
298 let off_y = (ih - bh * scale) / 2.0 + (cell_h - ih).max(0.0) / 2.0;
299 let (cx, cy) = (cell_w * (k % cols) as f64, cell_h * (k / cols) as f64);
300 let at = |&(x, y): &V| (cx + (x as f64 - x0) * fin + off_x, cy + (y as f64 - y0) * fin + off_y);
301 let line: Vec<String> = zappy.iter().map(|p| {
302 let (x, y) = at(p);
303 format!("{x:.2},{y:.2}")
304 }).collect();
305 let line = line.join(" ");
306 for (color, width) in [(OUTER, STROKE), (INNER, STROKE * 2.0 / 5.0)] {
307 s += &format!(
308 r#"<polyline points="{line}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linecap="round" stroke-linejoin="round"/>"#
309 );
310 }
311 let spot = |(x, y): (f64, f64), r: f64, fill: &str, opacity: f64| {
313 let pts: Vec<String> = (0..6)
314 .map(|i| {
315 let t = i as f64 / 6.0 * std::f64::consts::TAU;
316 format!("{:.2},{:.2}", x + t.cos() * r, y + t.sin() * r)
317 })
318 .collect();
319 format!(r#"<polygon points="{}" fill="{fill}" fill-opacity="{opacity}"/>"#, pts.join(" "))
320 };
321 s += &spot(at(&dots[0]), 0.8 * STROKE * 2.0 / 5.0, START_DOT, 1.0);
322 for d in &dots[1..] {
323 s += &spot(at(d), 0.4 * STROKE * 2.0 / 5.0, OUTER, 0.5);
324 }
325 }
326 s + "</svg>"
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn p(dir: &str, sig: &str) -> HexPattern {
334 HexPattern { startdir: dir.into(), signature: sig.into() }
335 }
336
337 #[test]
338 fn positions_follow_the_signature() {
339 assert_eq!(p("EAST", "w").positions(), [(0, 0), (1, 0), (2, 0)]);
341 assert_eq!(p("E", "s").positions(), [(0, 0), (1, 0), (0, 0)]);
343 assert_eq!(p("NORTH_EAST", "qaq").positions().len(), 5);
345 assert!(p("UP", "w").positions().is_empty(), "an unknown direction draws nothing");
346 }
347
348 #[test]
349 fn random_and_noise_match_minecraft() {
350 assert_eq!(Lcg::new(0).next(32), -1155484576);
352 let a = get_noise(0.5, 1337.0, 0.0);
354 assert!(a.abs() <= 0.5 && (a - get_noise(0.5001, 1337.0, 0.0)).abs() < 1e-3);
355 assert_eq!(mth_sin(0.0), 0.0);
356 assert!((mth_cos(0.0) - 1.0).abs() < 1e-6);
357 }
358
359 #[test]
360 fn zappy_lines_stop_short_and_stay_close() {
361 let pos = p("E", "w").positions();
362 let dots: Vec<V> = pos.iter().map(|&q| to_px(q)).collect();
363 let z = make_zappy(&dots, &dup_indices(&pos), 0.2, 0.8, 0.0);
364 assert_eq!(z.len(), 1 + 10 + 1 + 8);
366 assert!(z.iter().all(|&(x, y)| x >= -0.5 && x <= 3.7 && y.abs() < 0.5));
367 }
368
369 #[test]
370 fn svg_has_a_line_per_pattern() {
371 let s = svg(&[p("NE", "qaq"), p("E", "qaq")], 116.0, 64.0);
372 assert_eq!(s.matches("<polyline").count(), 4, "outline and line each");
373 assert!(s.contains(START_DOT));
374 assert!(s.starts_with("<svg") && s.ends_with("</svg>"));
375 }
376}