Skip to main content

globe/
lib.rs

1//! Customizable ASCII globe generator.
2//!
3//! Based on [C++ code by DinoZ1729](https://github.com/DinoZ1729/Earth).
4
5#![allow(dead_code)]
6
7use std::f32::consts::PI;
8use std::fs::File;
9use std::io::Read;
10
11pub type Int = isize;
12pub type Float = f32;
13
14static EARTH_TEXTURE: &str = include_str!("../textures/earth.txt");
15static EARTH_NIGHT_TEXTURE: &str = include_str!("../textures/earth_night.txt");
16
17/// Globe texture.
18pub struct Texture {
19    day: Vec<Vec<char>>,
20    night: Option<Vec<Vec<char>>>,
21    palette: Option<Vec<char>>,
22}
23
24impl Texture {
25    pub fn new(
26        day: Vec<Vec<char>>,
27        night: Option<Vec<Vec<char>>>,
28        palette: Option<Vec<char>>,
29    ) -> Self {
30        Texture {
31            day,
32            night,
33            palette,
34        }
35    }
36    pub fn get_size(&self) -> (usize, usize) {
37        (self.day[0].len() - 1, self.day.len() - 1)
38    }
39}
40
41/// Canvas that will be used to render the globe onto.
42pub struct Canvas {
43    pub matrix: Vec<Vec<char>>,
44    size: (usize, usize),
45    // character size
46    pub char_pix: (usize, usize),
47}
48
49impl Canvas {
50    pub fn new(x: u16, y: u16, cp: Option<(usize, usize)>) -> Self {
51        let x = x as usize;
52        let y = y as usize;
53
54        let matrix = vec![vec![' '; x]; y];
55
56        Self {
57            size: (x, y),
58            matrix,
59            char_pix: cp.unwrap_or((4, 8)),
60        }
61    }
62    pub fn get_size(&self) -> (usize, usize) {
63        self.size
64    }
65    pub fn clear(&mut self) {
66        for i in self.matrix.iter_mut().flatten() {
67            *i = ' ';
68        }
69    }
70    fn draw_point(&mut self, a: usize, b: usize, c: char) {
71        if a >= self.size.0 || b >= self.size.1 {
72            return;
73        }
74        self.matrix[b][a] = c;
75    }
76}
77
78/// Main globe abstraction.
79pub struct Globe {
80    pub camera: Camera,
81    pub radius: Float,
82    pub angle: Float,
83    pub texture: Texture,
84    pub display_night: bool,
85}
86
87impl Globe {
88    pub fn render_on(&self, canvas: &mut Canvas) {
89        // let there be light
90        let light: [Float; 3] = [0., 999999., 0.];
91        // shoot the ray through every pixel
92        let (size_x, size_y) = canvas.get_size();
93        for yi in 0..size_y {
94            let yif = yi as Int;
95            for xi in 0..size_x {
96                let xif = xi as Int;
97                // coordinates of the camera, origin of the ray
98                let o: [Float; 3] = [self.camera.x, self.camera.y, self.camera.z];
99                // u is unit vector, direction of the ray
100                let mut u: [Float; 3] = [
101                    -((xif - (size_x / canvas.char_pix.0 / 2) as Int) as Float + 0.5)
102                        / (size_x / canvas.char_pix.0 / 2) as Float,
103                    ((yif - (size_y / canvas.char_pix.1 / 2) as Int) as Float + 0.5)
104                        / (size_y / canvas.char_pix.1 / 2) as Float,
105                    -1.,
106                ];
107                transform_vector(&mut u, self.camera.matrix);
108                u[0] -= self.camera.x;
109                u[1] -= self.camera.y;
110                u[2] -= self.camera.z;
111                normalize(&mut u);
112                let dot_uo = dot(&u, &o);
113                let discriminant: Float = dot_uo * dot_uo - dot(&o, &o) + self.radius * self.radius;
114
115                // ray doesn't hit the sphere
116                if discriminant < 0. {
117                    continue;
118                }
119
120                let distance: Float = -discriminant.sqrt() - dot_uo;
121
122                // intersection point
123                let inter: [Float; 3] = [
124                    o[0] + distance * u[0],
125                    o[1] + distance * u[1],
126                    o[2] + distance * u[2],
127                ];
128
129                // surface normal
130                let mut n: [Float; 3] = [
131                    o[0] + distance * u[0],
132                    o[1] + distance * u[1],
133                    o[2] + distance * u[2],
134                ];
135                normalize(&mut n);
136
137                // unit vector pointing from intersection to light source
138                let mut l: [Float; 3] = [0.; 3];
139                vector(&mut l, &inter, &light);
140                normalize(&mut l);
141                let luminance: Float = clamp(5. * (dot(&n, &l)) + 0.5, 0., 1.);
142                let mut temp: [Float; 3] = [inter[0], inter[1], inter[2]];
143                rotate_x(&mut temp, -PI * 2. * 0. / 360.);
144
145                // computing coordinates for the sphere
146                let phi: Float = -temp[2] / self.radius / 2. + 0.5;
147                let mut theta: Float = (temp[1] / temp[0]).atan() / PI + 0.5 + self.angle / 2. / PI;
148                // let mut theta: Float = (temp[1] / temp[0]).atan() / PI + self.angle / 2. / PI * 20.;
149                theta -= theta.floor();
150                let (tex_x, tex_y) = self.texture.get_size();
151                let earth_x = (theta * tex_x as Float) as usize;
152                let earth_y = (phi * tex_y as Float) as usize;
153
154                // if night texture and palette are available, draw the night side
155                if self.display_night
156                    && self.texture.night.is_some()
157                    && self.texture.palette.is_some()
158                {
159                    let palette = self.texture.palette.as_ref().unwrap();
160                    let day = find_index(self.texture.day[earth_y][earth_x], palette);
161                    let night = find_index(
162                        self.texture.night.as_ref().unwrap()[earth_y][earth_x],
163                        palette,
164                    );
165
166                    let mut index =
167                        ((1.0 - luminance) * night as Float + luminance * day as Float) as usize;
168                    if index >= palette.len() {
169                        index = 0;
170                    }
171                    canvas.draw_point(xi, yi, palette[index]);
172                }
173                // else just draw the day texture without considering luminance
174                else {
175                    canvas.draw_point(xi, yi, self.texture.day[earth_y][earth_x]);
176                }
177            }
178        }
179    }
180}
181
182/// Globe configuration struct implementing the builder pattern.
183#[derive(Default)]
184pub struct GlobeConfig {
185    camera_cfg: Option<CameraConfig>,
186    radius: Option<Float>,
187    angle: Option<Float>,
188    template: Option<GlobeTemplate>,
189    texture: Option<Texture>,
190    display_night: bool,
191}
192
193impl GlobeConfig {
194    /// Creates an empty `GlobeConfig`.
195    pub fn new() -> Self {
196        Self::default()
197    }
198
199    /// Sets `CameraConfig` to be used by the builder.
200    pub fn with_camera(mut self, config: CameraConfig) -> Self {
201        self.camera_cfg = Some(config);
202        self
203    }
204
205    /// Sets the globe radius.
206    pub fn with_radius(mut self, r: Float) -> Self {
207        self.radius = Some(r);
208        self
209    }
210
211    /// Selects a template to be used by the builder.
212    pub fn use_template(mut self, t: GlobeTemplate) -> Self {
213        self.template = Some(t);
214        self
215    }
216
217    /// Sets the day texture to be displayed on the globe.
218    pub fn with_texture(mut self, texture: &str, palette: Option<Vec<char>>) -> Self {
219        let mut day = Vec::new();
220        let lines = texture.lines();
221        for line in lines {
222            let row: Vec<char> = line.chars().rev().collect();
223            day.push(row);
224        }
225        if let Some(texture) = &mut self.texture {
226            texture.day = day;
227        } else {
228            self.texture = Some(Texture::new(day, None, palette));
229        }
230        self
231    }
232
233    /// Sets the night texture to be displayed on the globe.
234    pub fn with_night_texture(mut self, texture: &str, palette: Option<Vec<char>>) -> Self {
235        let mut night = Vec::new();
236        let lines = texture.lines();
237        for line in lines {
238            let row: Vec<char> = line.chars().rev().collect();
239            night.push(row);
240        }
241
242        if let Some(texture) = &mut self.texture {
243            texture.night = Some(night);
244        } else {
245            self.texture = Some(Texture::new(night.clone(), Some(night), palette));
246        }
247
248        self
249    }
250
251    /// Sets the day texture to be loaded from the given path.
252    pub fn with_texture_at(self, path: &str, palette: Option<Vec<char>>) -> Self {
253        let mut file = File::open(path).unwrap();
254        let mut out_string = String::new();
255        file.read_to_string(&mut out_string).unwrap();
256        self.with_texture(&out_string, palette)
257    }
258
259    /// Sets the night display toggle to the given value.
260    pub fn display_night(mut self, b: bool) -> Self {
261        self.display_night = b;
262        self
263    }
264
265    /// Builds new `Globe` from the collected configuration settings.
266    pub fn build(mut self) -> Globe {
267        if let Some(template) = &self.template {
268            match template {
269                GlobeTemplate::Earth => {
270                    let palette = vec![
271                        ' ', '.', ':', ';', '\'', ',', 'w', 'i', 'o', 'g', 'O', 'L', 'X', 'H', 'W',
272                        'Y', 'V', '@',
273                    ];
274                    self = self
275                        .with_texture(EARTH_TEXTURE, Some(palette.clone()))
276                        .with_night_texture(EARTH_NIGHT_TEXTURE, Some(palette))
277                }
278            }
279        }
280        let texture = self.texture.expect("texture not provided");
281        let camera = self
282            .camera_cfg
283            .unwrap_or_else(CameraConfig::default)
284            .build();
285        Globe {
286            camera,
287            radius: self.radius.unwrap_or(1.),
288            angle: self.angle.unwrap_or(0.),
289            texture,
290            display_night: self.display_night,
291        }
292    }
293}
294
295/// Built-in globe template enumeration.
296pub enum GlobeTemplate {
297    Earth,
298    // Moon,
299    // Mars,
300}
301
302/// Camera configuration struct implementing the builder pattern.
303pub struct CameraConfig {
304    radius: Float,
305    alpha: Float,
306    beta: Float,
307}
308
309impl CameraConfig {
310    /// Creates a new `CameraConfig`.
311    ///
312    /// # Arguments
313    ///
314    /// - `r` is the distance from the camera to the origin.
315    /// - `alfa` is camera's angle along the xy plane.
316    /// - `beta` is camera's angle along z axis.
317    pub fn new(radius: Float, alpha: Float, beta: Float) -> Self {
318        Self {
319            radius,
320            alpha,
321            beta,
322        }
323    }
324
325    /// Creates a new `CameraConfig` using default values.
326    pub fn default() -> Self {
327        Self {
328            radius: 2.,
329            alpha: 0.,
330            beta: 0.,
331        }
332    }
333
334    /// Builds a camera from the collected config information.
335    pub fn build(&self) -> Camera {
336        let mut camera = Camera::default();
337        camera.update(self.radius, self.alpha, self.beta);
338        camera
339    }
340}
341
342#[derive(Default)]
343pub struct Camera {
344    x: Float,
345    y: Float,
346    z: Float,
347    matrix: [Float; 16],
348    inv: [Float; 16],
349}
350
351impl Camera {
352    /// Updates the camera using new data.
353    pub fn update(&mut self, r: Float, alpha: Float, beta: Float) {
354        let sin_a = alpha.sin();
355        let cos_a = alpha.cos();
356        let sin_b = beta.sin();
357        let cos_b = beta.cos();
358
359        let x = r * cos_a * cos_b;
360        let y = r * sin_a * cos_b;
361        let z = r * sin_b;
362
363        let mut matrix = [0.; 16];
364
365        // matrix
366        matrix[3] = 0.;
367        matrix[7] = 0.;
368        matrix[11] = 0.;
369        matrix[15] = 1.;
370        // x
371        matrix[0] = -sin_a;
372        matrix[1] = cos_a;
373        matrix[2] = 0.;
374        // y
375        matrix[4] = cos_a * sin_b;
376        matrix[5] = sin_a * sin_b;
377        matrix[6] = -cos_b;
378        // z
379        matrix[8] = cos_a * cos_b;
380        matrix[9] = sin_a * cos_b;
381        matrix[10] = sin_b;
382
383        matrix[12] = x;
384        matrix[13] = y;
385        matrix[14] = z;
386
387        let mut inv = [0.; 16];
388        invert(&mut inv, matrix);
389
390        self.x = x;
391        self.y = y;
392        self.z = z;
393        self.matrix = matrix;
394        self.inv = inv;
395    }
396}
397
398/// Get index of the given character on the palette.
399fn find_index(target: char, palette: &[char]) -> Int {
400    for (i, &ch) in palette.iter().enumerate() {
401        if target == ch {
402            return i as Int;
403        }
404    }
405    -1
406}
407
408fn transform_vector(vec: &mut [Float; 3], m: [Float; 16]) {
409    let tx: Float = vec[0] * m[0] + vec[1] * m[4] + vec[2] * m[8] + m[12];
410    let ty: Float = vec[0] * m[1] + vec[1] * m[5] + vec[2] * m[9] + m[13];
411    let tz: Float = vec[0] * m[2] + vec[1] * m[6] + vec[2] * m[10] + m[14];
412    vec[0] = tx;
413    vec[1] = ty;
414    vec[2] = tz;
415}
416
417fn invert(inv: &mut [Float; 16], matrix: [Float; 16]) {
418    inv[0] = matrix[5] * matrix[10] * matrix[15]
419        - matrix[5] * matrix[11] * matrix[14]
420        - matrix[9] * matrix[6] * matrix[15]
421        + matrix[9] * matrix[7] * matrix[14]
422        + matrix[13] * matrix[6] * matrix[11]
423        - matrix[13] * matrix[7] * matrix[10];
424
425    inv[4] = -matrix[4] * matrix[10] * matrix[15]
426        + matrix[4] * matrix[11] * matrix[14]
427        + matrix[8] * matrix[6] * matrix[15]
428        - matrix[8] * matrix[7] * matrix[14]
429        - matrix[12] * matrix[6] * matrix[11]
430        + matrix[12] * matrix[7] * matrix[10];
431
432    inv[8] = matrix[4] * matrix[9] * matrix[15]
433        - matrix[4] * matrix[11] * matrix[13]
434        - matrix[8] * matrix[5] * matrix[15]
435        + matrix[8] * matrix[7] * matrix[13]
436        + matrix[12] * matrix[5] * matrix[11]
437        - matrix[12] * matrix[7] * matrix[9];
438
439    inv[12] = -matrix[4] * matrix[9] * matrix[14]
440        + matrix[4] * matrix[10] * matrix[13]
441        + matrix[8] * matrix[5] * matrix[14]
442        - matrix[8] * matrix[6] * matrix[13]
443        - matrix[12] * matrix[5] * matrix[10]
444        + matrix[12] * matrix[6] * matrix[9];
445
446    inv[1] = -matrix[1] * matrix[10] * matrix[15]
447        + matrix[1] * matrix[11] * matrix[14]
448        + matrix[9] * matrix[2] * matrix[15]
449        - matrix[9] * matrix[3] * matrix[14]
450        - matrix[13] * matrix[2] * matrix[11]
451        + matrix[13] * matrix[3] * matrix[10];
452
453    inv[5] = matrix[0] * matrix[10] * matrix[15]
454        - matrix[0] * matrix[11] * matrix[14]
455        - matrix[8] * matrix[2] * matrix[15]
456        + matrix[8] * matrix[3] * matrix[14]
457        + matrix[12] * matrix[2] * matrix[11]
458        - matrix[12] * matrix[3] * matrix[10];
459
460    inv[9] = -matrix[0] * matrix[9] * matrix[15]
461        + matrix[0] * matrix[11] * matrix[13]
462        + matrix[8] * matrix[1] * matrix[15]
463        - matrix[8] * matrix[3] * matrix[13]
464        - matrix[12] * matrix[1] * matrix[11]
465        + matrix[12] * matrix[3] * matrix[9];
466
467    inv[13] = matrix[0] * matrix[9] * matrix[14]
468        - matrix[0] * matrix[10] * matrix[13]
469        - matrix[8] * matrix[1] * matrix[14]
470        + matrix[8] * matrix[2] * matrix[13]
471        + matrix[12] * matrix[1] * matrix[10]
472        - matrix[12] * matrix[2] * matrix[9];
473
474    inv[2] = matrix[1] * matrix[6] * matrix[15]
475        - matrix[1] * matrix[7] * matrix[14]
476        - matrix[5] * matrix[2] * matrix[15]
477        + matrix[5] * matrix[3] * matrix[14]
478        + matrix[13] * matrix[2] * matrix[7]
479        - matrix[13] * matrix[3] * matrix[6];
480
481    inv[6] = -matrix[0] * matrix[6] * matrix[15]
482        + matrix[0] * matrix[7] * matrix[14]
483        + matrix[4] * matrix[2] * matrix[15]
484        - matrix[4] * matrix[3] * matrix[14]
485        - matrix[12] * matrix[2] * matrix[7]
486        + matrix[12] * matrix[3] * matrix[6];
487
488    inv[10] = matrix[0] * matrix[5] * matrix[15]
489        - matrix[0] * matrix[7] * matrix[13]
490        - matrix[4] * matrix[1] * matrix[15]
491        + matrix[4] * matrix[3] * matrix[13]
492        + matrix[12] * matrix[1] * matrix[7]
493        - matrix[12] * matrix[3] * matrix[5];
494
495    inv[14] = -matrix[0] * matrix[5] * matrix[14]
496        + matrix[0] * matrix[6] * matrix[13]
497        + matrix[4] * matrix[1] * matrix[14]
498        - matrix[4] * matrix[2] * matrix[13]
499        - matrix[12] * matrix[1] * matrix[6]
500        + matrix[12] * matrix[2] * matrix[5];
501
502    inv[3] = -matrix[1] * matrix[6] * matrix[11]
503        + matrix[1] * matrix[7] * matrix[10]
504        + matrix[5] * matrix[2] * matrix[11]
505        - matrix[5] * matrix[3] * matrix[10]
506        - matrix[9] * matrix[2] * matrix[7]
507        + matrix[9] * matrix[3] * matrix[6];
508
509    inv[7] = matrix[0] * matrix[6] * matrix[11]
510        - matrix[0] * matrix[7] * matrix[10]
511        - matrix[4] * matrix[2] * matrix[11]
512        + matrix[4] * matrix[3] * matrix[10]
513        + matrix[8] * matrix[2] * matrix[7]
514        - matrix[8] * matrix[3] * matrix[6];
515
516    inv[11] = -matrix[0] * matrix[5] * matrix[11]
517        + matrix[0] * matrix[7] * matrix[9]
518        + matrix[4] * matrix[1] * matrix[11]
519        - matrix[4] * matrix[3] * matrix[9]
520        - matrix[8] * matrix[1] * matrix[7]
521        + matrix[8] * matrix[3] * matrix[5];
522
523    inv[15] = matrix[0] * matrix[5] * matrix[10]
524        - matrix[0] * matrix[6] * matrix[9]
525        - matrix[4] * matrix[1] * matrix[10]
526        + matrix[4] * matrix[2] * matrix[9]
527        + matrix[8] * matrix[1] * matrix[6]
528        - matrix[8] * matrix[2] * matrix[5];
529
530    let mut det: Float =
531        matrix[0] * inv[0] + matrix[1] * inv[4] + matrix[2] * inv[8] + matrix[3] * inv[12];
532
533    det = 1.0 / det;
534
535    for inv_i in inv.iter_mut() {
536        *inv_i *= det;
537    }
538}
539
540fn cross(r: &mut [Float; 3], a: [Float; 3], b: [Float; 3]) {
541    r[0] = a[1] * b[2] - a[2] * b[1];
542    r[1] = a[2] * b[0] - a[0] * b[2];
543    r[2] = a[0] * b[1] - a[1] * b[0];
544}
545
546fn magnitude(r: &[Float; 3]) -> Float {
547    dot(r, r).sqrt()
548}
549
550fn normalize(r: &mut [Float; 3]) {
551    let len: Float = magnitude(r);
552    r[0] /= len;
553    r[1] /= len;
554    r[2] /= len;
555}
556
557fn dot(a: &[Float; 3], b: &[Float; 3]) -> Float {
558    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
559}
560
561fn vector(a: &mut [Float; 3], b: &[Float; 3], c: &[Float; 3]) {
562    a[0] = b[0] - c[0];
563    a[1] = b[1] - c[1];
564    a[2] = b[2] - c[2];
565}
566
567fn transform_vector2(vec: &mut [Float; 3], m: &[Float; 9]) {
568    vec[0] = m[0] * vec[0] + m[1] * vec[1] + m[2] * vec[2];
569    vec[1] = m[3] * vec[0] + m[4] * vec[1] + m[5] * vec[2];
570    vec[2] = m[6] * vec[0] + m[7] * vec[1] + m[8] * vec[2];
571}
572
573fn rotate_x(vec: &mut [Float; 3], theta: Float) {
574    let a = theta.sin();
575    let b = theta.cos();
576    let m: [Float; 9] = [1., 0., 0., 0., b, -a, 0., a, b];
577    transform_vector2(vec, &m);
578}
579
580fn rotate_y(vec: &mut [Float; 3], theta: Float) {
581    let a = theta.sin();
582    let b = theta.cos();
583    let m: [Float; 9] = [b, 0., a, 0., 1., 0., -a, 0., b];
584    transform_vector2(vec, &m);
585}
586
587fn rotate_z(vec: &mut [Float; 3], theta: Float) {
588    let a = theta.sin();
589    let b = theta.cos();
590    let m: [Float; 9] = [b, -a, 0., a, b, 0., 0., 0., 1.];
591    transform_vector2(vec, &m);
592}
593
594fn clamp(mut x: Float, min: Float, max: Float) -> Float {
595    if x < min {
596        x = min;
597    } else if x > max {
598        x = max;
599    }
600    x
601}