Визуализация кривой Леви на Rust

Недавно я начал знакомиться с таким замечательным языком, как Rust. И в качестве пробы пера решил визуализировать на нем какой-нибудь фрактал. Выбор пал на кривую Леви. Почему именно такая задача? Во-первых, она не требует глубоких знаний математики - формула довольно проста. Во-вторых, эта задача требует разобраться с основами языка: владением, заимствованием, временем жизни, работой с зависимостями и т.п.

Полный код получившейся программы можно найти на github.

Постановка задачи

Мы напишем программу, которая будет принимать координаты двух точек и строить между ними кривую Леви. Чтобы хоть как-то разнообразить нашу поделку, линия будет цветной с плавным градиентом от одного цвета - в начале, к другому цвету - в конце. В результате мы будем получать изображения подобные этому:

Пример генерируемого изображения

Задачу можно разбить на три этапа:

  1. Расчет линий - для каждой линии рассчитываем координаты начала и конца, а так же ее цвет.
  2. Создание изображения и нанесение на него линий.
  3. Сохранение изображения в файл.

Подготовка каркаса

При помощи Cargo создаем новый проект. О том, как это сделать, я писал {{post_3_здесь}}. Создаем структуры, с которыми будем работать. Их всего три - Color, Point и Line. Из их названий все понятно. Работать мы будем через обычные функции и не станем добавлять в эти структуры какие-либо методы, кроме конструкторов.

#[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,
}

Расчет линий

Для того чтобы построить кривую Леви необходимо найти среднюю точку для исходных точек и соединить их с ней. Таким образом мы получим два отрезка, имеющих одну общую точку. Затем повторить эту операцию для каждого из них. И так далее. Повторять можно сколько угодно раз - чем больше итераций, тем красивей получиться фрактал. Количество итераций мы вынесем в отдельную переменную.

Вот функция, которая рассчитывает координаты средней точки. Формулу возьмем из примера в википедии.

/// 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,
    }
}

Так же нам нужна функция для расчета среднего цвета. Не будем сильно заморачиваться и просто возьмем среднее значение для каждого канала.

/// 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,
    }
}

Функция, которая непосредственно занимается формированием списка линий будет рекурсивной. На вход она получает координаты и цвета исходных точек и оставшееся количество итераций. Если осталась одна итерация, она создает и возвращает линию со средним цветом и находящуюся между указанными точками. Если итерация еще не последняя, функция находит среднюю точку и рекурсивно запускает себя для каждой из получившихся линий при этом уменьшая значение итерации. Полученные результаты объединяет в один список и возвращает его.

/// 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
    }
}

Создание изображения

Теперь у нас есть список линий, которые и составляют кривую Леви. Создадим PNG изображение и нанесем на него наши линии. Для работы с PNG будем использовать вот эту библиотеку. Устанавливается она стандартным способом: добавляем зависимость в Cargo.toml и импортируем контейнер.

Создаем пустое изображение, на которое потом будем наносить наши линии.

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

Используемая нами библиотека не имеет готовой функции для рисования отрезков, поэтому мы напишем ее сами. Для этого воспользуемся алгоритмом Брезенхэма. Чтобы долго не морочиться, мы портируем уже готовую функцию с Java, немного изменив ее под свои нужды. В итоге получим вот такую функцию:

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 }
}

Добавим функцию create_image, которая создает пустое изображение и в цикле наносит на него линии.

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);
    }
}

Сохранение изображения

Наше изображение готово, осталось только сохранить его. Делается это достаточно просто стандартными средствами языка. Добавим сохранение фала в функцию create_image.

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);
}

Заключение

В общем, у нас все готово. Осталось только вызвать наш код.

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");
}

В результате рядом появится файл с вот таким изображением.

Пример генерируемого изображения

Вы можете прочитать эту статью на английском здесь.

Ключевые слова:

Коментарии

Используйте Markdown

Thank you for comment!
Ваше сообщение будет доступно после проверки.