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();
Далее, мы загрузим два изображения. Одно будет служить в качестве фона — в нашем случае, сцена ночного неба. Другим будет «hint» изображение. Идея заключается в том, чтобы положить большинство звезд на черные пиксели нашего hint изображения, чтобы воссоздать дизайн фоновой сцены. Эти изображения достаточно легко создать с помощью p5 инструментов для рисования, но для краткости мы будем использовать статические изображения.
hintImage = loadImage("//bit.ly/hintImage");
skyImage = loadImage("//bit.ly/skyImage");
Еще одна ключевая функция это 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
);
};
Вот что у нас в итоге получилось:
На этом работа над нашим первым скетчем закончена.
Еще один хороший пример работы с p5.js можно посмотреть в этом видео.
Справочный материал:
VDemot
Интересует несколько вопросов.
1. В цикле отрисовки все время происходит перерисовка всего холста или же только измененных его частей?
2. Поддерживается ли многослойность?
k1lly
Перерисовка происходит для всей «сцены», так что многие применяют трюки для борьбы с падением производительности.
Многослойность поддерживается так: что сначала в коде — то и рисуется ниже, то есть порядок определяет z-индекс элементов.
VDemot
Это трудно назвать многослойностью. Перерисовываться то все слои целиком тогда будут. Слои должны состоять из отдельных canvas элементов, наложенных друг на друга.
Sirion
На первый взгляд звучит как костыль. Такое где-нибудь в серьёзных либах вообще используется?
VDemot
Я думаю что это не костыль, а пока единственное возможное решение многослойности. За счёт чего и достигается производительность.
Простой пример, статичный фон 800х600 и анимированный спрайт 50х50. Зачем нам ~16 раз в секунду перерисовывать весь холст 800х600 только из-за анимации маленького спрайта? Вот и падение производительности.
Sirion
Как бы этот выигрыш в производительности не оказался съеден накладными расходами. Без бенчмарков я как-то не уверен.
VDemot
Ну а что тут измерять? Я думаю по примеру просто очевидно, 800х600 перерисовывать или 50х50.
Пример очень простой, но дает понять суть.
Sirion
Я сварщик не настоящий и в низкоуровневых аспектах графики разбираюсь плохо. Но что-то мне подсказывает, что браузер при любом изменении всё равно перерисовывает канвас целиком.
Zerstoren
По скорости несколько слоев выгодней, но оперативки жрет больше.
Личные наблюдения.
VDemot
На счет использования этого метода. Знаю пока только про LibCanvas. Там как раз все так и происходит
Keyten
В Graphics2D тоже так можно.
https://habrahabr.ru/post/243457/