Visualization of the Levy C curve on Rust

Recently I started to learn such remarkable language as Rust. And for practice I decided to visualise any fractal on it. The choice fell on Levy C curve. Why exactly this task? First, it does not require deep knowledge of mathematics - the formula is pretty simple. Second, this task requires to understand the basics of the language: ownership, borrowing, lifetimes, dependency management etc.

A complete code of the resulting program can be found on github.

Problem statement

We will write a program that will take coordinates of two points and build Levy C curve between them. To diversify out program, the line will be colored with a smooth gradient from one color - in the the beginning, to another color at the end. As a result we will get an image like this:

The task can be divided into three stages:

  • Calculation lines - for each line count of the begin and end coordinates, and its color.
  • Generating the image and drawing on it the lines.
  • Save the image to a file.

Preparing the base

With Cargo create a new project. Create structs with which we will work. Their ony three - Color, Point and Line. From their names, everything is clear. We will working through normal functions and will not add any methods into these structs except constructors.

#[derive(Clone, Copy)]
struct Color {
    r:u32,
    g:u32,
    b:u32,
}
impl Color {
    fn new(r:u32, g:u32, b:u32) -> Color
    {
        Color {r: r, g: g, b:b}
    }
}

#[derive(Clone, Copy)]
struct Point {
    x:i32,
    y:i32,
}
impl Point {
    fn new(x:i32, y:i32) -> Point
    {
        Point {x: x, y: y}
    }
}

struct Line {
    begin_point: Point,
    end_point: Point,
    color: Color,
}

Lines calculation

To build Levy C curve we need to calculate a midpoint for the source points and connect them with her. Thus we get two segments with one common point. Then repeat this for each of them. And so on. It can be repeated any number of times - the more iterations, the more beautiful we get fractal. The number of iterations we move to the separate variable.

Here is a function that calculates coordinates of the midpoint. Take the formula from wikipedia example.

/// Returns point placed between point_1 and point_2.
fn get_middle_point (point_1:&Point, point_2:&Point) -> Point
{
    Point {
        x: (point_1.x + point_2.x) / 2 + (point_2.y - point_1.y) / 2,
        y: (point_1.y + point_2.y) / 2 - (point_2.x - point_1.x) / 2,
    }
}

Also, we need a function to calculate the average color of the line. Not going to bother much and just take an average value for each channel.

/// Returns color placed between color_1 and color_2.
fn get_midle_color(color_1:&Color, color_2:&Color) -> Color
{
    Color {
        r: (color_1.r + color_2.r) / 2,
        g: (color_1.g + color_2.g) / 2,
        b: (color_1.b + color_2.b) / 2,
    }
}

A function which generates list of the lines will be recursive. On entrance it receives coordinates and colors of the source points and the remaining number of iterations. If there is one iteration, it creates and returns a line with the middle color and placed between specified points. If the iteration is not the last, function calculates the midpoint and calls itself recursively for each of the resulting lines with reducing the value of the iterator. The obtained results combines into a single list and returns it.

/// Returns list of lines for drawing.
fn get_lines(point_1:Point, color_1:Color, point_2:Point, color_2:Color, level:i32) -> Vec<Line>
{
    let middle_color: Color = get_midle_color(&color_1, &color_2);

    if level == 1 {
        vec![Line {
            begin_point: point_1,
            end_point: point_2,
            color: middle_color,
        }]
    } else {
        let middle_point = get_middle_point(&point_1, &point_2);
        let mut lines: Vec<Line> = vec![];
        let prev = &mut get_lines(point_1, color_1, middle_point, middle_color, level - 1);
        let next = &mut get_lines(middle_point, middle_color, point_2, color_2, level - 1);
        lines.append(prev);
        lines.append(next);
        lines
    }
}

Image generate

Now we have a list of lines that make the Levy C curve. Create a PNG image and and put lines on it. To work with PNG will use this library. It can be installed in a standard way: add a dependency into Cargo.toml and import a container.

Generate an empty image to that we will put our lines.

let mut img = ImageBuffer::new(width, height);

The library we use has not a function to drawing lines. Then we will write it themselves. We use the Bresenham's line algorithm for this. We port a ready Java function with little modifications. In the end we get this function:

use image::ImageBuffer;

/// Draws line on canvas.
fn draw_line(canvas: &mut ImageBuffer<image::Rgb<u8>, Vec<u8>>, point_1: &Point, point_2: &Point, color: &Color, )
{
    let px = image::Rgb([color.r as u8, color.g as u8, color.b as u8]);

    let mut dx= point_2.x - point_1.x;
    let mut dy = point_2.y - point_1.y;
    let incx = get_sign(dx);
    let incy = get_sign(dy);
    if dx < 0 {
        dx = - dx;
    }
    if dy < 0 {
        dy = -dy;
    }

    let (pdx, pdy, es, el) =
        if dx > dy {
            (incx, 0, dy, dx)
        } else {
            (0, incy, dx, dy)
        };

    let mut x = point_1.x;
    let mut y = point_1.y;
    let mut err = el / 2;

    canvas.put_pixel(x as u32, y as u32, px);

    for _ in 0..el {
        err = err - es;
        if err < 0 {
            err = err + el;
            x = x + incx;
            y = y + incy;
        } else {
            x = x+ pdx;
            y = y + pdy;
        }
        canvas.put_pixel(x as u32, y as u32, px);
    }
}

fn get_sign(value:i32) -> i32
{
    if value > 0 { 1 }
    else if value < 0 { -1 }
    else { 0 }
}

Add create_image function, which creates an empty image and puts the lines to it in the loop.

use image::ImageBuffer;

/// Generates PNG image and saves it.
fn create_image(width:u32, height:u32, lines:&Vec<Line>)
{
    let mut img = ImageBuffer::new(width, height);
    for line in lines {
        draw_line(&mut img, &line.begin_point, &line.end_point, &line.color);
    }
}

Image saving

Our image is ready, it remains only to save it. This is done quite easily with standard language tools. Add image saving to create_image function.

use std::fs::File;
use std::path::Path;
use image::ImageBuffer;

/// Generates PNG image and saves it.
fn create_image(size:u32, lines:&Vec<Line>, file_name: &str)
{
    let mut img = ImageBuffer::new(size,size);
    for line in lines {
        draw_line(&mut img, &line.begin_point, &line.end_point, &line.color);
    }
    let ref mut fout = File::create(&Path::new(file_name)).unwrap();
    let _ = image::ImageRgb8(img).save(fout, image::PNG);
}

Conclusion

Generally, we are all set. We only have to call our code.

fn main() {
    let begin_point = Point::new(300, 500);
    let end_point = Point::new(700, 500);
    let begin_color= Color::new(255, 0, 0);
    let end_color = Color::new(0, 255, 0);
    let iterate_count = 10;
    let lines = get_lines(begin_point, begin_color, end_point, end_color, iterate_count);
    create_image(1000, 700, &lines, "img.png");
}

In the result there will be a file with this image.

You can read this article in Russian here.

Comments

Guest

Interesting. Thank you!

Use Markdown
Thanks for your comment!
It will be published after approval.