Графическая библиотека dlib – это замечательный и интересный инструмент как для работы, так и для проведения разного рода математических экспериментов. Однако, несмотря на свое богатство, dlib достаточно скромная и минималистическая библиотека, и очень часто встроенных примитивов не хватает под некоторые задачи отрисовки, а иногда бывает и так, что хочется иметь простой и удобный интерфейс для уже привычных, ставших рутинными, действий.
Мне очень часто приходится строить линии, соответствующие разным интересным математическим функциям, и довольно быстро я понял, что такого рода рисование содержит в себе повторяющиеся для разных случаев элементы кода, а это значит, что процедура отрисовки геометрических образов на основе функциональных зависимостей весьма хорошо поддается обобщению. Кроме того, вдохновленный примером Haskell’а, я решил сделать процедуры отрисовки в функциональном стиле, что позволило создать очень гибкие функции.
Итак, процедура рисования графика некоторой функции:
import std.algorithm; import std.range; import std.traits; auto proceduralDraw(alias func, R)(R r, ref SuperImage simg, Color4f pointColor = Color4f(0.0f, 0.0f, 0.0f)) if ((isInputRange!(Unqual!R))) { auto xs = map!(a => cast(int) a)(r); auto ys = map!(a => cast(int) (func(a)))(r); each!(a => simg[a[0], a[1]] = pointColor)(zip(xs, ys)); }
Лаконично и изящно!
Шаблон proceduralDraw
принимает некоторую функцию (которую проще всего задать через анонимную функцию) и диапазон ввода, который является диапазоном иксов, по которым рассчитывается значение функции. Помимо аргументов времени компиляции, шаблон принимает и аргументы времени исполнения, обязательными из которых являются сам диапазон ввода и массив, в котором хранится изображение, на котором мы рисуем. Ограничение сигнатуры isInputRange!(Unqual!R)
проверяет является ли поступивший на вход шаблона диапазон диапазоном ввода и гарантирует корректную работу с поступившими в обработку данными.
Для испытания proceduralDraw
построим несколько графиков, задействовав необязательный параметр шаблона, отвечающий за цвет отображаемых точек:
import std.algorithm; import std.math; import std.range; import std.traits; import dlib.image; void main() { SuperImage simg = image(500, 500); for (int i = 0; i < simg.width; i++) { for (int j = 0; j < simg.height; j++) { simg[i, j] = Color4f(1.0f, 1.0f, 1.0f); } } auto ix = iota(0.0, 500.0, 0.001); auto f = function(float x) { return x * PI / 180.0; }; proceduralDraw!(a => 250 + 20 * sin(a / 20.0))(ix, simg, Color4f(0.0f, 0.0f, 0.9f)); proceduralDraw!(a => 250 + 200 * cos(a / 20.0))(ix, simg, Color4f(0.9f, 0.0f, 0.0f)); simg.savePNG("sample.png"); } auto proceduralDraw(alias func, R)(R r, ref SuperImage simg, Color4f pointColor = Color4f(0.0f, 0.0f, 0.0f)) if ((isInputRange!(Unqual!R))) { auto xs = map!(a => cast(int) a)(r); auto ys = map!(a => cast(int) (func(a)))(r); each!(a => simg[a[0], a[1]] = pointColor)(zip(xs, ys)); }
Для генерации множества x был использован алгоритм (в терминологии D, алгоритм – это некоторая функция, которая принимает диапазон и возвращает некоторое значение или же новый диапазон) iota
, который позволяет получить диапазон чисел с заданными границами и заданным шагом.
Чтобы графики синуса и косинуса были более качественными, вместо исходных функций sin и cos в анонимной функции использованы их измененные аналоги, учитывающие масштаб по обеим осям и смещающие начало координат: координаты нового центра – (250.0, 250.0), одна единица длины составляет 200 пикселей изображения и коэффициент растяжения по оси OX равен 20.
Хотя шаблон и универсален, но имеются некоторые проблемы, которые я пока еще (к общему сожалению) я не смог решить: ограничение сигнатуры, на мой взгляд, не сильно строгое (в частности, нет проверки типа элементов диапазона, аналогичной isIntegral, что в принципе может привести к попытке использования нечислового диапазона), также отсутствует автоматическое масштабирование по обеим осям (вся ответственность за корректное отображение графика ложится на программиста). Возможно, кто-то из читателей, поможет решить существующие проблемы (или подскажет несколько интересных идей), я же надеюсь на то, что рассказанное в этой небольшой статье окажется для вас полезным и даст импульс для претворения собственных идей по процедурной графике.
Эта статья была написана мной специально для журнала FPS и была в нем опубликована (стр. 37), я же без лишних слов ее дословно процитирую.
Особая благодарность Тимуру Гафарову и журналу FPS за возможность публикации статей 🙂 Кроме того, хочу сказать отдельное спасибо Тимуру за его прекрасные статьи в журнале FPS, именно благодаря им я понял всю мощь шаблонов в D и наконец-то стал их применять сам.