P5.js был создан командой Processing в 2014 году. Это JavaScript библиотека, с особым акцентом на изобразительное искусство. Цель P5.js совпадает с изначальной целью Processing — это снизить порог входа в программирование для художников, дизайнеров, учителей. С P5.js можно легко создавать бесконечно большое количество вариантов отображения информации: от веб-формата, статичных картинок для печати, до анимированных и интерактивных схем.


Processing был создан в 2001 году с целью обучить программированию непрограммистов, но с тех пор он стал основным языком для десятков тысяч художников и дизайнеров. P5.js делает рендеринг файлов PDE с исходным кодом Processing. При использовании этой библиотеки программист может вообще не знать JavaScript. Детально об отличиях Processing и p5 можно почитать в этой статье.


Для рендеринга используется canvas, а также p5.js поддерживает работу со звуком и видео. P5.js включает в себя совокупность обьектов, функций, констант с помощью которых можно создавать произвольные формы. Весь ряд возможностей p5.js можно найти в справочнике на официальном сайте.


P5 setup() выполняется один раз, когда проект загружается, так что это идеальное место, для инициализации. Подключить библиотеку можно с сервера или CDN:


<script src="../p5.min.js"></script>
<script src="//cdn.jsdelivr.net/p5.js/0.0.0/p5.min.js"></script>

Давайте создадим наш первый проект на p5.js. Наша цель состоит в том, чтобы создать инструмент для рисования, который преобразует простое изображение в область анимированных звезд. Для начала, мы определим несколько глобальных переменных и напишем setup(). Каждая отдельная графическая работа в p5.js называется «скетч», а каждый скетч состоит как минимум из двух функций — setup и draw.


Загрузить файлы вы можете здесь.


var hintImage, skyImage, stars = [];
function setup() {... }

Внутри нашей функции setup(), мы создадим холст и скроем курсор мыши чтоб иметь возможность рисовать от себя. P5 добавляет контур вокруг формы по умолчанию, в нашем случае, нам нужно отключить строки.


createCanvas(800,500);
noCursor();
noStroke();

image

Далее, мы загрузим два изображения. Одно будет служить в качестве фона — в нашем случае, сцена ночного неба. Другим будет «hint» изображение. Идея заключается в том, чтобы положить большинство звезд на черные пиксели нашего hint изображения, чтобы воссоздать дизайн фоновой сцены. Эти изображения достаточно легко создать с помощью p5 инструментов для рисования, но для краткости мы будем использовать статические изображения.


hintImage = loadImage("//bit.ly/hintImage");
skyImage = loadImage("//bit.ly/skyImage");

image

Еще одна ключевая функция это draw(). Она вызывается в непрерывном цикле, что полезно для анимации.


function draw() {... }

В функции Draw, наша первая задача состоит в том, чтобы заполнить canvas фоновым изображением. P5 не очистит холст между вызовами draw() автоматически, так что мы должны делать это каждый кадр, ибо в конечном итоге получим странные эффекты накопления. Чтобы поместить загруженное изображение на холст, используйте функцию image() и дайте ей координаты х и у для определения местоположения.


image(skyImage, 0, 0);

Далее, мы захватываем текущее местоположение мыши и сохраняем его в качестве p5.Vector используя createVector(). Этот объект включает в себя удобные функции, для работы с точками в пространстве, но мы будем в основном его использовать в качестве контейнера.


var position = createVector(mouseX, mouseY);

Используя недавно сохраненную позицию мыши, мы можем нарисовать наш курсор. Мы установим цвет курсора с помощью fill(), передавая значения RGB и будем использовать ellipse(), дабы нарисовать круг на месте мыши.


fill(255, 192, 0);
ellipse(position.x, position.y, 8, 8);

С помощью mouseIsPressed, мы сделаем так чтобы во время зажатия мыши рисовались новые звезды. Во время зажатия мыши, нам нужно вычислить хорошее место для следующей звезды. Мы сделаем это с помощью специальной функции под названием findPixel(), которую мы определим позже.


Как только у нас появится сигнал, мы создадим новый экземпляр объекта Star и прижмем его к концу нашего звездного массива. А если общее количество звезд превысит 2000, мы начнем отбрасывать самые старые из них.


if (mouseIsPressed) {
  var target = findPixel();
  var star = new Star(position, target);
  stars.push(star);
  if (stars.length > 2000) stars.shift();
}

В конце, мы пройдемся через наш массив звезд и вызовем update() и draw() на каждом из них. Мы рассмотрим эти методы позже.


for (var i = 0; i < stars.length; i++) {
  stars[i].update();
  stars[i].draw();
}

Теперь setup() и draw() на месте, а мы будем работать над вспомогательными функциями и объектами. Во-первых, мы определим функцию, которая находит место для каждой новой звезды. Все, что нам нужно сделать, это проверить рандомные пиксели в нашем изображении, используя get(), чтобы увидеть, черные они или белые.


На самом деле мы должны смотреть только на значение красного, так как в обоих случаях RGB значения совпадает.


function findPixel() {
  var x, y;
  for (var i = 0; i < 15; i++) {
    x = floor(random(hintImage.width));
    y = floor(random(hintImage.height));
    if (red(hintImage.get(x,y)) < 255) break;
  }
  return createVector(x,y);
}

Теперь нам нужен многоразовый контейнер, который хранит информацию о каждой звезде, а также будет обновлять и рисования их. В JavaScript нет классического способа для создания классов в традиционном смысле этого слова, но мы можем получить тот же результат, определив функцию и настроив ее для собственных нужд.


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


function Star(position, target) {
  this.position = position;
  this.target = target;
  this.diameter = random(1, 5);
}

Далее мы добавим метод update() для функции Star, который будет использовать p5.Vector lerp(), чтобы вычислить новое положение между текущим и целевым позициям звезды. В этом случае, мы движемся на 4% оставшегося расстояния на каждой итерации.


Star.prototype.update = function() {
    this.position = p5.Vector.lerp(
    this.position,
    this.target,
    0.04
  );
};

И, наконец, метод draw(), который рисует звезду на холсте. Еще раз, мы используем fill() и ellipse(), хотя на этот раз мы вызываем fill() со значением alpha для прозрачности.


Чтобы дать звездам огонька, значение alpha определяется с помощью функции noise(). Это возвращает значение шума Перлина для указанных координат, то есть вы получаете гладкую, последовательность случайных чисел. В третий параметр, мы вводим значение на основе времени, с помощью него шум будет анимироваться в течение некоторого времени.


Star.prototype.draw = function() {
  var alpha = noise(
  this.target.x,
  this.target.y,
  millis()/1000.0
);
fill(255, alpha * 255);
  ellipse(
    this.position.x, this.position.y,
    this.diameter, this.diameter
  );
};

Вот что у нас в итоге получилось:


image

На этом работа над нашим первым скетчем закончена.


Еще один хороший пример работы с p5.js можно посмотреть в этом видео.


Справочный материал:


Поделиться с друзьями
-->

Комментарии (12)


  1. VDemot
    01.09.2016 13:18

    Интересует несколько вопросов.
    1. В цикле отрисовки все время происходит перерисовка всего холста или же только измененных его частей?
    2. Поддерживается ли многослойность?


    1. k1lly
      01.09.2016 18:25
      +1

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


      1. VDemot
        01.09.2016 19:43

        Это трудно назвать многослойностью. Перерисовываться то все слои целиком тогда будут. Слои должны состоять из отдельных canvas элементов, наложенных друг на друга.


        1. Sirion
          02.09.2016 08:55

          На первый взгляд звучит как костыль. Такое где-нибудь в серьёзных либах вообще используется?


          1. VDemot
            02.09.2016 09:11

            Я думаю что это не костыль, а пока единственное возможное решение многослойности. За счёт чего и достигается производительность.
            Простой пример, статичный фон 800х600 и анимированный спрайт 50х50. Зачем нам ~16 раз в секунду перерисовывать весь холст 800х600 только из-за анимации маленького спрайта? Вот и падение производительности.


            1. Sirion
              02.09.2016 09:23

              Как бы этот выигрыш в производительности не оказался съеден накладными расходами. Без бенчмарков я как-то не уверен.


              1. VDemot
                02.09.2016 09:29
                +1

                Ну а что тут измерять? Я думаю по примеру просто очевидно, 800х600 перерисовывать или 50х50.
                Пример очень простой, но дает понять суть.


                1. Sirion
                  02.09.2016 09:32
                  -1

                  Я сварщик не настоящий и в низкоуровневых аспектах графики разбираюсь плохо. Но что-то мне подсказывает, что браузер при любом изменении всё равно перерисовывает канвас целиком.


              1. Zerstoren
                02.09.2016 13:00
                +1

                По скорости несколько слоев выгодней, но оперативки жрет больше.

                Личные наблюдения.


          1. VDemot
            02.09.2016 09:12

            На счет использования этого метода. Знаю пока только про LibCanvas. Там как раз все так и происходит


            1. Keyten
              04.09.2016 16:18

              В Graphics2D тоже так можно.
              https://habrahabr.ru/post/243457/


  1. megaultraseo
    01.09.2016 19:30

    Что с производительностью? Официальный сайт чуть не повесил Chrome.