Skip to content

Instantly share code, notes, and snippets.

@andyHa
Last active July 12, 2021 02:46
Show Gist options
  • Save andyHa/485f474a63568e610869ceb966ba9b6a to your computer and use it in GitHub Desktop.
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..
//! # 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
}
@nickx720
Copy link

Wow this was a pleasant read! Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment