Some weekend fun: Simple Rust, complex maths and having fun in the terminal..
//! # 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
This comment has been minimized.
Wow this was a pleasant read! Thank you