Last active
July 12, 2021 02:46
-
-
Save andyHa/485f474a63568e610869ceb966ba9b6a to your computer and use it in GitHub Desktop.
Some weekend fun: Simple Rust, complex maths and having fun in the terminal..
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! # Some weekend fun: Simple Rust, complex maths and having fun in the terminal.. | |
//! | |
//! Let's start with a quick thought experiment: If you take a number, e.g. 4 and square it, | |
//! (16) and square it again (256) you quickly realize the numbers run towards infinity. This | |
//! is true for all numbers, right? Well, not quite. Everything between -1..1 will converge | |
//! towards 0 rather than infinity. | |
//! | |
//! So for real numbers this is quite boring. However, if we step it up to complex numbers, we | |
//! have a 2D plane and could render a nice chart... | |
//! | |
//! In case you wonder what complex numbers are - well they're not complex at all. Jump to wikipedia | |
//! for a deep dive our trust the following code to be implemented correctly :) | |
/// A complex number is a two dimensional number which has a real and an imaginary part. | |
#[derive(Copy, Clone)] | |
struct Complex { | |
/// Represents the real part of the number. This is the "normal" part of the number and behaves | |
/// like normal real numbers. | |
pub r: f64, | |
/// Represents the imaginary part of the number. This is where the fun begins. The imaginary | |
/// part is always multiplied with "i". What is "i" you ask? Well easy, it is the result of | |
/// `sqrt(-1)`. But...that...doesn't exist you scream? Yes, hence the name imaginary. We just | |
/// pretend the would be a solution. This isn't actually not that difficult, we just drag along | |
/// the i values if there is one AND if we encounter `i * i` we replace it with `-1`. | |
pub i: f64, | |
} | |
impl Complex { | |
/// Provides a simple way of creating a complex number with a known imaginary and real part. | |
fn new(r: f64, i: f64) -> Self { | |
Complex { r, i } | |
} | |
/// Multiplies the number by itself. | |
/// | |
/// This is actually basic math: `(a + b) * (a + b) = a^2 + 2ab + b^2`. So, if the know that | |
/// "b" is always multiplied with "i", we actually get: `a^2 + 2ab*i + b^2 * i^2` which is | |
/// `a^2 + 2ab*i + b^2 * -1`. To obtain the result, we group everything without an "i" into | |
/// the real part of the result and every thing else into the imaginary: | |
/// * real: `a^2 + b^2 * -1` | |
/// * imaginary: `2ab*i` | |
fn square(&self) -> Self { | |
Complex { | |
r: self.r * self.r + self.i * self.i * -1.0, | |
i: 2.0 * self.r * self.i, | |
} | |
} | |
/// Computes the magnitude of the number. | |
/// | |
/// This is essentially the distance from the center (`0 + 0i`) and computed as any other | |
/// hypotenuse in a right angled triangle: `c = sqrt(a^2 + b^2)` | |
fn magnitude(&self) -> f64 { | |
(self.r * self.r + self.i * self.i).sqrt() | |
} | |
/// Adds the given complex number onto self. | |
/// | |
/// This is done like any other vector addition, by simply summing up the components... | |
fn add(&self, c: Complex) -> Self { | |
Complex { | |
r: self.r + c.r, | |
i: self.i + c.i, | |
} | |
} | |
} | |
/// Now that we got the complex math out of the way, let's draw into out console. | |
/// | |
/// We therefore have to define our console size and the destination viewport and compute a simple | |
/// translation function. | |
/// | |
/// Therefore this takes the console coordinates (in characters) and returns the target coordiantes | |
/// as Complex. | |
fn transform(x: i32, y: i32) -> Complex { | |
Complex::new( | |
x as f64 * LENGTH / SCREEN.0 as f64 + OFFSET, | |
y as f64 * LENGTH / SCREEN.1 as f64 + OFFSET, | |
) | |
} | |
/// Defines out console size. This is a VERY conservative guess. You can easily go ub by a number | |
/// of 10 for a modern screen if you maximize the terminal window... | |
/// | |
/// This is a tuple representing characters (cols) and rows. | |
const SCREEN: (i32, i32) = (80, 24); | |
// This is most probably a way better guess... | |
// const SCREEN: (i32, i32) = (320, 80); | |
/// Defines the offset of our viewport. | |
/// | |
/// Therefore the first character which is at (0,0) will be translated to (-2 - 2i) in the complex | |
/// plane. | |
const OFFSET: f64 = -2.0; | |
/// Determines the size of our viewport. | |
/// | |
/// Therefore the last character (80, 24) will be translated to (2 + 2i) in the complex plane. | |
const LENGTH: f64 = 4.0; | |
/// Now this simply iterates over all "printable" positions, converts the coordinates into the | |
/// complex plane and runs the given iterator. If the iterator converges towards 0, we draw a | |
/// "*" otherwise we output a bank (aren't monospaced font a gift from heaven :) )... | |
fn scan_complex_plane(iterator: impl Fn(Complex) -> bool) { | |
for y in 1..SCREEN.1 { | |
for x in 1..SCREEN.0 { | |
match iterator(transform(x, y)) { | |
true => print!("*"), | |
false => print!(" "), | |
} | |
} | |
println!(); | |
} | |
} | |
fn main() { | |
// Run this first to get a graphic representation of what we're doing... | |
scan_complex_plane(iterate1); | |
// Run this to have your magic moment... | |
// scan_complex_plane(iterate2); | |
} | |
/// So this iterator simply does what the introduction described. | |
/// | |
/// We take a given complex number and keep on squaring it. If after 255 iteration we're still | |
/// within our viewport, we return true, otherwise we return false. | |
/// | |
/// As you might probably have guessed, this simply renders a circle with the radius of 1.... | |
fn iterate1(coordinate: Complex) -> bool { | |
let mut z = coordinate; | |
for _ in 1..=255 { | |
z = z.square(); | |
if z.magnitude() > 2.0 { | |
return false; | |
} | |
} | |
true | |
} | |
/// Now comes the magic part: | |
/// | |
/// We change the iteration function to z = z + c. Where z starts at 0,0 or (0 + 0i) and c is | |
/// the given complex coordinate we're probing. If this beast stays within our viewport for | |
/// 255 iterations, we again return true and, as before, false otherwise. | |
/// | |
/// Now run this using the main method above and be amazed of the object in your terminal. | |
/// Spoiler - no, this isn't even a 2D dimensional object at all. Given its extremely simple | |
/// iteration formula in my eyes this is rightfully called the thumbprint of God (forgive the | |
/// blasphemy here). You can learn a whole lot more in this wonderful video: | |
/// https://www.youtube.com/watch?v=FFftmWSzgmk (not mine, not affiliated, still excellent) | |
fn iterate2(coordinate: Complex) -> bool { | |
let mut z = Complex::new(0.0, 0.0); | |
let mut iters = 255; | |
for _ in 1..=255 { | |
z = z.square().add(coordinate); | |
iters -= 1; | |
if z.magnitude() > 2.0 { | |
return false; | |
} | |
} | |
true | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Wow this was a pleasant read! Thank you