Статьи

Рисование с обработкой и Ruby

processing2-логотип

Обработка — это среда / язык программирования, призванный сделать наглядные интерактивные приложения чрезвычайно простыми в написании. Он может быть использован для всего: от обучения детей программированию до визуализации научных данных.

К счастью, все это больше не скрывается внутри Обработки; Вы можете использовать его из Ruby! В этой статье мы рассмотрим гем ruby-processing и примеры Ruby / Processing в действии.

Настройка

Вам понадобятся копии jRuby и Processing, чтобы ruby-processing работала правильно.

Инструкции по настройке рабочих параметров различаются в зависимости от того, какой менеджер среды Ruby вы используете (например, rbenv, rvm), вашей операционной системы и версии Java.

Прежде всего, вы хотите получить копию jRuby. Если вы используете RVM:

 rvm install jruby rvm use jruby 

Если вы на rbenv,

 rbenv install jruby-*version number* 

Далее вам нужна копия Processing, которая довольно проста, поскольку у них есть хорошая страница для скачивания .

Мы можем получить гем рубиновой обработки:

 gem install ruby-processing 

Прежде чем мы сможем начать работу, нам нужно сообщить ruby-processing, где находится наша копия Processing. В ~/.rp5rc конфигурации ~/.rp5rc YAML установите переменную PROCESSING_ROOT . К счастью, есть файл Processing, который устанавливает эту переменную для вас. Если вы работаете в Mac OS X, это должно выглядеть примерно так:

 PROCESSING_ROOT: "/Applications/Processing.app/Contents/Java" 

Достаточно конфигурации. Давайте углубимся в код.

Шаги малыша

Вот вершина некоторого кода Ruby / Processing:

 def setup size 200, 200 background 0 smooth end def draw fill 255, 102, 18 ellipse 56, 46, 55, 55 end 

Первое, что вы можете заметить, это то, что в этом фрагменте нет исполняемого кода! Итак, если бы мы запустили его со стандартным интерпретатором ruby , не было бы никакого вывода. Вместо этого мы должны запустить его с помощью специальной команды rp5 :

 rp5 run first.rb 

Запустите его, немного подождите, пока JVM не запустится, и вы увидите цветной круг, смотрящий на вас. Код невероятно прост. Все программы Ruby / Processing имеют метод setup который вызывается только один раз, когда настраивается «сцена». Затем у нас есть метод draw который вызывается повторно и должен обновлять экран. В нашей setup мы устанавливаем размер экрана, цвет фона ( 0 означает черный). Затем при draw установите цвет заливки, используя значения RGB, и нарисуйте круг с координатами (56, 46).

Обработка имеет фантастические ссылки , почти все из которых можно сразу передать (то есть дословно) в Ruby / Processing. Тем не менее, важно отметить, что Processing использует camelCase для имен методов, тогда как в эквиваленте в Ruby используются подчеркивания.

Теперь давайте перейдем к чему-то более сложному.

Частицы

Давайте создадим простой, интерактивный дисплей частиц. Когда вы щелкаете, частицы должны двигаться в направлении мыши, а затем рассеиваться, когда вы отпускаете кнопку мыши.

 class Particle attr_accessor :to_x, :to_y, 😡, :y, :velocity def initialize(x, y, width, height) @x = x @y = y @velocity = 2 @to_x = rand(width) @to_y = rand(height) @alpha = rand(255) end def draw stroke 255, 255, 255 fill 150, 150, 150, 200 ellipse @x, @y, 8, 8 end def move mag = Math.sqrt(@x ** 2 + @y ** 2) @x = @x + @velocity * (@to_x - @x)/mag @y = @y + @velocity * (@to_y - @y)/mag end end def setup @bgcolor = "#2f2f2f" size displayWidth, displayHeight background color(@bgcolor) smooth @particles = [] 1000.times do @particles << Particle.new(rand(width), rand(height), width, height) end end def mouse_pressed @particles.each do |particle| particle.velocity = 6 particle.to_x = mouse_x particle.to_y = mouse_y end end def mouse_released @particles.each do |particle| particle.to_x = rand(width) particle.to_y = rand(height) particle.velocity = 10 end end def draw background color(@bgcolor) @particles.each do |particle| particle.draw particle.move end end 

Вау, это похоже на тонну кода! Однако, когда он разбит на куски размером с укус, получается довольно просто. Давайте посмотрим на класс Particle :

 class Particle attr_accessor :to_x, :to_y, 😡, :y, :velocity def initialize(x, y, width, height) @x = x @y = y @velocity = 2 @to_x = rand(width) @to_y = rand(height) @alpha = rand(255) end def draw stroke 255, 255, 255 fill 150, 150, 150, 200 ellipse @x, @y, 8, 8 end def move mag = Math.sqrt(@x ** 2 + @y ** 2) @x = @x + @velocity * (@to_x - @x)/mag @y = @y + @velocity * (@to_y - @y)/mag end end 

Метод initialize более или менее шаблонный. Волшебство происходит при draw , когда мы используем методы Ruby / Processing: stroke , fill , ellipse чтобы нарисовать частицу в виде круга в заданной точке. Каждая частица имеет to_x и to_y , которые сообщают ей, куда она должна идти. В методе move мы просто используем формулу расстояния, чтобы переместить частицу в правильном направлении.

 def setup size displayWidth, displayHeight background color(@bgcolor) smooth @particles = [] 1000.times do @particles << Particle.new(rand(width), rand(height), width, height) end end 

Установите размер displayWidth и displayHeight , что позволяет нам создавать полу-полноэкранные функции на большинстве платформ. Затем установите цвет background вместе с color (мне нравятся эти интуитивно понятные названия). Завершите настройку, создав список из 1000 частиц для рисования.

 def draw background color(@bgcolor) @particles.each do |particle| particle.draw particle.move end end 

Мы используем довольно распространенную идею обработки: в начале большинства кадров вы будете очищать все, обращаясь к background . Затем нарисуйте каждую частицу на экране, а затем вызов
move , сдвинув его ближе к значениям to_x и to_y . Но где эти значения на самом деле установлены?

 def mouse_pressed @particles.each do |particle| particle.velocity = 6 particle.to_x = mouse_x particle.to_y = mouse_y end end def mouse_released @particles.each do |particle| particle.to_x = rand(width) particle.to_y = rand(height) particle.velocity = 10 end end 

Обработка делает обработку событий действительно простой. Здесь мы используем mouse_pressed и mouse_released которые вызываются при нажатии и отпускании кнопки мыши, соответственно. Когда мышь нажата, установите значения «to» для каждой частицы, чтобы они указывали на местоположение мыши (вызывая их перемещение в направлении мыши). Затем, отпустив мышь, рандомизируйте значения «до» частицы, заставляя их распространяться.

Эффект, который это создает, на самом деле довольно крутой и приятный для глаз. Это немного просто, но это дает нам строительные блоки, которые мы можем использовать для создания более классных вещей.

Редактор графиков

Когда мы думаем о графиках, мы обычно думаем о двух осях и некоторых данных, нанесенных на их плоскости. Оказывается, что графы — это математические объекты, которые состоят из узлов и ребер . По сути, это точки, соединенные линиями. Как правило, углы между линиями и длинами линий не имеют значения. Вместо этого понятие «связность» является более важным. Эти графики имеют все виды интересных свойств. Давайте посмотрим, сможем ли мы создать инструмент с Ruby / Processing для создания графиков (строго говоря, неориентированных графиков), как этот:

Image of a Graph

Нам понадобится:

  • Способ создания узлов
  • Способ создания краев
  • Способ перемещаться по узлам, следя за тем, чтобы края оставались соединенными

Давайте Point класс Point (который представляет узел):

 class Point attr_accessor 😡, :y, :segment_color, :to_points, :fill_color def initialize(x, y) @x = x @y = y @segment_color = color(100, 100, 100, 100) @radius = 30 @fill_color = color(0) #points to draw segments to @to_points = [] end def draw fill @fill_color ellipse @x, @y, @radius, @radius end def segment_to(other_point) stroke @segment_color line @x, @y, other_point.x, other_point.y end def in_thresh(x, y) thresh = @radius return (abs(@x - x) < thresh and abs(@y - y) < thresh) end end 

Мы добавили два важных метода по сравнению с классом Particle мы создали ранее. segment_to создает концепцию линий между краями. Метод Обработка line рисует линии, предоставляя координаты начальной и конечной точек в качестве аргументов. Наконец, метод in_thresh позволяет нам определить, находится ли пара координат «достаточно близко» к нашей точке; он будет использоваться для проверки совпадения кликов с узлом.

Настройка сцены довольно проста:

 def setup size displayWidth, displayHeight stroke_weight 3 background(255) smooth @points = [] 7.times do @points << Point.new(rand(width), rand(height)) end end 

Во-первых, настройте стандартный size и background вместе с вызовом stroke_weight который сообщает Processing, какова толщина линий. Кроме того, инициализируйте @points и добавьте семь случайных узлов, только для ударов.

 def draw background(255) @points.each do |point| point.draw point.to_points.each do |tp| point.segment_to tp end end end 

Метод ничьей так же прост. Просто нарисуйте данные точки вместе с соответствующими ребрами. Но как эти края попадают туда в первую очередь?

 def mouse_clicked clicked_points = @points.select do |p| p.in_thresh mouse_x, mouse_y end if clicked_points.length == 0 @points << Point.new(mouse_x, mouse_y) elsif @from_node @from_node.to_points << clicked_points[0] @from_node.fill_color = color(0) @from_node = nil else @from_node = clicked_points[0] @from_node.fill_color = color(255, 0, 0) end end 

Способ добавления узлов должен быть простым: просто щелкните где-нибудь. Чтобы добавить ребро, щелкните по одному узлу, затем по другому, и между ними будет нарисовано ребро.

Из-за того, как мы используем клики, в событии mouse_clicked есть некоторые вовлеченные вещи. Сначала настройте clicked_points как массив точек, которые находятся в пределах диапазона щелчка. Затем, если щелчок не имеет ничего общего с существующими точками, создайте новый узел. Впоследствии используйте переменную @from_node чтобы определить, является ли это первым или вторым щелчком при создании ребра. Если это первый, выделите узел цветом, чтобы показать пользователю, что он собирается создать ребро. Если это второй узел, добавьте ребро.

Обратите внимание, что внутри mouse_clicked нет никакого кода для mouse_clicked . Все эти вещи обрабатываются внутри класса Point и метода draw . Как правило, ваш код рисования должен оставаться отделенным от реальной логики (немного похоже на идею разделения побочных эффектов в функциональных языках).

Что если мы хотим переместить узлы вокруг?

 def mouse_pressed @points.each do |p| if p.in_thresh(mouse_x, mouse_y) @node = p end end end def mouse_dragged if @node @node.x = mouse_x @node.y = mouse_y end end 

Используйте события mouse_pressed и mouse_dragged . Первый вызывается при нажатии кнопки мыши, а второй вызывается только при перетаскивании мыши. В первом случае установите @node чтобы пользователь «нажимал» мышью. Затем, в mouse_dragged , если пользователь нажал на точку, переместите точку туда, где находится мышь. Обратите внимание, что mouse_dragged вызывается многократно, пока пользователь перетаскивает, поэтому узел будет двигаться вместе с мышью на время перетаскивания.

Давайте посмотрим код во всей красе:

 class Point attr_accessor 😡, :y, :segment_color, :to_points, :fill_color def initialize(x, y) @x = x @y = y @segment_color = color(100, 100, 100, 100) @radius = 30 @fill_color = color(0) #points to draw segments to @to_points = [] end def draw fill @fill_color ellipse @x, @y, @radius, @radius end def segment_to(other_point) stroke @segment_color line @x, @y, other_point.x, other_point.y end def in_thresh(x, y) thresh = @radius return (abs(@x - x) < thresh and abs(@y - y) < thresh) end end def setup size displayWidth, displayHeight-160 stroke_weight 3 background(255) smooth @points = [] 7.times do @points << Point.new(rand(width), rand(height)) end end def mouse_pressed @points.each do |p| if p.in_thresh(mouse_x, mouse_y) @node = p end end end def mouse_dragged if @node @node.x = mouse_x @node.y = mouse_y end end def mouse_clicked clicked_points = @points.select do |p| p.in_thresh mouse_x, mouse_y end if clicked_points.length == 0 @points << Point.new(mouse_x, mouse_y) elsif @from_node @from_node.to_points << clicked_points[0] @from_node.fill_color = color(0) @from_node = nil else @from_node = clicked_points[0] @from_node.fill_color = color(255, 0, 0) end end def key_pressed if key == ' ' @points = [] end end def draw background(255) @points.each do |point| point.draw point.to_points.each do |tp| point.segment_to tp end end end 

Немного поиграв с ним, он кажется удивительно плавным и дружелюбным. Хотя мы могли бы добавить множество функций (например, сохранение в файл, счетчик количества ребер и т. Д.), Это хорошая основа. Обратите внимание, что мы использовали набор довольно простых вызовов, таких как line , ellipse , background и т. Д. Обработка предоставляет нам эту простоту благодаря использованию конечного автомата внутри. Вызовы, которые вы делаете, могут повлиять на результаты последующих вызовов из-за того, что Обработка удерживает состояние. Это позволяет очень простой код, но отладка может стать трудной, потому что вы должны мысленно отслеживать это состояние, пока вы работаете со своим кодом.

Завершение

Надеюсь, вам понравился тур по обработке через линзы Ruby. Мы рассмотрели только часть API обработки , но продемонстрировали его мощь в создании визуальных эффектов с помощью Ruby.