Данный пост является введением в Quil. Quil - это библиотека для создания интерактивной анимации в Clojure. Попросту говоря она позволяет рисовать на экране всё, что душе угодно. Quil предоставляет множество полезных функций для рисования в 2D и 3D. В это посте я покажу, как создавать и запускать эскизы (скетчи). Начнём с чего-нибудь простого, например с тригонометрии... Все её любят: синусы, косинусы, тангенсы, что может быть лучше? Наш первый скетч будет просто рисовать спираль используя функции sin и cos.
project.clj
(defproject quil-intro "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.8.0"]
[quil "2.5.0"]])
И собственно сам код quil_intro.clj:
(ns quil-intro
(:require [quil.core :as q]))
; определяем функцию, которая рисует спираль
(defn draw []
; делаем фон белым
(q/background 255)
; перемещаем начало координат в центр экрана
; по умолчанию оно находится в левом верхнем углоу
(q/with-translation [(/ (q/width) 2) (/ (q/height) 2)]
; параметр t пробегает по значениям от 0 до 100 с шагом 0.01
(doseq [t (range 0 100 0.01)]
; рисуем точку с координатами x=t*sin(t) и y=t*cos(t)
(q/point (* t (q/sin t))
(* t (q/cos t))))))
; запускаем скетч
(q/defsketch trigonometry
:size [300 300]
:draw draw)
Для базового скетча требуется задать draw
функцию, которая будет что-нибудь рисовать. Затем вызвать макрос defsketch
и передать ему draw
. Вот что рисует наш скетч:
Теперь давайте немного порефакторим draw
, чтобы сделать построение графиков функций чуть проще. Для этого мы зададим функцию draw-plot
, которая принимает параметрическую функцию f(t) = (x, y) и границы параметра t на которых нужно построить график. Вот какой получился код:
; задаём f
(defn f [t]
[(* t (q/sin t))
(* t (q/cos t))])
(defn draw-plot [f from to step]
(doseq [two-points (->> (range from to step)
(map f)
(partition 2 1))]
; мы могли бы использовать функцию point для того, чтобы нарисовать точку
; но будет лучше, если мы нарисуем линию, соединяющую соседние точки графика
(apply q/line two-points)))
(defn draw []
(q/background 255)
(q/with-translation [(/ (q/width) 2) (/ (q/height) 2)]
(draw-plot f 0 100 0.01)))
Отлично, теперь можно экспериментировать с функцией f
. И здесь проявляется великолепие Quil и Clojure: перезагрузка на лету.
Перезагрузка на лету
В большинстве языков, после изменения кода нам бы понадобилось закрыть текущий скетч, скомпилировать изменения и запустить скетч заново. В Quil мы можем изменить все функции на лету и увидеть изменения немедленно. Вообще, можно запрограммировать весь скетч, от начала до конца, ни разу его не закрыв, а постепенно наращивая его функционал. Конечно, не всё можно изменить на лету, например, невозможно зарегистрировать обработчики событий мыши и клавиатуры. Но это не мешает изменить существующие, т.е. можно изначально зарегистрировать пустые обработчики, а, потом в процессе творчества, добавить в них логику. Теперь давайте вернёмся обратно к коду и изменим функцию f
:
; можно получить кучу интересных графиков пробуя
; произвольные комбинации тригонометрических функций,
; например f, представленная, ниже рисует цветок
(defn f [t]
(let [r (* 200 (q/sin t) (q/cos t))]
[(* r (q/sin (* t 0.2)))
(* r (q/cos (* t 0.2)))]))
Теперь нужно перегрузить изменённую функцию f
. Для этого используются стандартные для Clojure приёмы:
- Emacs:
C-x C-e
для перегрузкиf
. - LightTable:
Ctrl+Enter
для перегрузкиf
. - REPL: заново определить функцию
f
.
Ниже изображение цветка (и ещё пары других графиков случайных функций):
Анимация
Теперь рассмотрим ещё одну фичу Quil. До этого момента мы рисовали только статичные изображения, которые не изменялись с течением времени. На самом деле функция draw
вызывается периодически с короткими интервалами, что позволяет рисовать движущиеся объекты и настоящую анимацию! Сейчас мы изменим draw
так, чтобы на каждой итерации рисовалась только небольшая часть графика: линия от f(t) до f(t+1). Единственная проблема - то, что на каждой итерации t должно меняться. Для этого мы воспользуемся функцией frame-count
, которая возвращает номер текущей итерации. Этот номер и будет служить числом t. Теперь cобственно реализация:
(defn draw []
(q/with-translation [(/ (q/width) 2) (/ (q/height) 2)]
; заметьте, что мы не используем draw-plot здесь,
; т.к. нам нужно отрисовывать только небольшую часть
; графика на каждой итерации
(let [t (/ (q/frame-count) 10)]
(q/line (f t)
(f (+ t 0.1))))))
; 'setup' - это брат функции 'draw'
; setup инициализирует скетч и вызывается только один раз,
; перед первым вызовом draw
(defn setup []
; draw будет вызываться 60 раз в секунду
(q/frame-rate 60)
; сделаем фон белым только в setup
; если мы будем это вызывать в draw, то на каждой итерации
; скетч будет очищаться
(q/background 255))
(q/defsketch trigonometry
:size [300 300]
:setup setup
:draw draw)
Время для анимации!
До сих пор все наши скетчи были чёрно-белыми. Было бы неплохо добавить побольше цветов. Я не буду разбирать, как это сделать в этом посте - это будет упражнение читателю, или, если вы слишком ленивый - можно посмотреть реализации в репо на GitHub в конце этого поста. Вот что у меня получилось:
На сегодня всё. Пару финальных замечаний: Quil основан на языке Processing, который сам по себе является замечательным языком/программой для создания изображений и анимаций, но Quil улучшает его при помощи перезагрузки на лету (в принципе тоже самое можно сказать и про сам кложур по отношению к программированию в целом). Это очень классно, иметь возможность перегружать части скетча на лету и немедленно видеть эффект. Такая возможность ускоряет скорость разработки и экспериментирования, так что я всем советую поиграться с ним. Несколько полезных ссылок:
- Код из этого поста доступен на GitHub.
- Оффициальный репо Quil.
- Quil API доки.
- Сайт Processing.
Любые коментарии приветствуются