Существует такая категория сайтов, которые мы обычно называем «вау-сайтами». Они не предназначены для непосредственного потребления контента, а скорее производят впечатление на посетителя. Эти сайты обычно уникальны в своем дизайне, содержат экспериментальные решения для взаимодействия с пользователем, ломают стереотипы и изобилуют разными анимациями. Особый интерес представляют различные трюки с SVG и анимации, основанные на большом количестве частиц, о которых и пойдет речь в этой статье.



В целом идея воссоздания различных объектов в виде набора частиц существует уже давно. Но из-за того, что практическое применение этой идеи сильно ограничено, а каждая конкретная реализация завязана на конкретный контент, нет единой структурированной базы знаний, в которой можно было бы быстро ознакомиться со всеми приемами. В результате многие разработчики не понимают, что в основе всего лежат очень простые идеи. Их под силу реализовать каждому, кто более-менее знаком с JS.

Мы постараемся разобрать базовый подход к созданию анимаций из частиц в 2d и посмотрим пару практических примеров, не углубляясь в низкоуровневые оптимизации и особенности конкретных библиотек для работы с canvas. Все дальнейшие рассуждения будут основаны на предположении, что читатель знаком с основами работы с canvas. Если нет – лучше прерваться на этом моменте и ознакомиться. Примеры кода будут немного упрощены для передачи идей, а не конкретных рецептов для копи-пасты, так что будет полезно находиться в контексте.

Небольшое отступление: найдутся люди, которые скажут, что все это кушает ресурсы процессора, новый макбук лагает, и вообще-вообще низзя-нельзя такое делать на сайте. С одной стороны это замечание верно. Современный JS в браузере – не самая хорошая основа для сложных вычислений, особенно если речь идет о среднестатистическом сайте. Но мы же говорим не об обычных сайтах, не так ли?

Мысль номер раз. Размер имеет значение




В большинстве случаев частицы не могут быть очень маленькими. Это вопрос производительности. Мы не сможем на среднем ноутбуке рассчитывать весь экран по одному пикселю и иметь при этом хотя бы 30fps, не говоря уже о 60. Необходимо уменьшать количество частиц. На практике в анимациях обычно используются либо маленькие частицы по 2-3px, если идет работа с небольшим изображением, либо крупные, размером в десятки пикселей и предназначенные для заполнения большой площади экрана.

С другой стороны частицы не могут быть слишком большими. Это уже вопрос адаптивности. Имеет смысл помнить о том, что на телефоне экран меньше и большие частицы просто создадут нечитаемую кашу. Хорошим выбором будет привязка размера частиц к размеру canvas, с которым мы работаем.

Мысль номер два. Нужна готовая схема расположения частиц.




Нам нужна основа. Никакая сложная анимация не сделается в разумное время без основы. Тут есть два варианта. Либо мы берем за основу изображение, либо текст. В качестве текста может быть заголовок, часы, номер ошибки или просто какое-то слово на заборе.

Основу, какой бы она ни была, необходимо перенести на canvas. С текстом все просто: один раз написав его стандартным fillText, мы получим черно-белую схему для расположения частиц. Это как раз то, что нам нужно.

ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = '#000';
ctx.font = `${fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.fillText('Hello', width/2, height/2 + fontSize/4);



Стоит отметить, что предварительная заливка фона белым не будет излишней. Да, в современных браузерах фон и так будет формально белым, но не в Safari. Если кто-то не слышал, то это наш новый ослик, который по-своему работает с прозрачностями, как в CSS градиентах, так и на canvas. Из-за этого могут появляться совсем не те цвета, которые мы ожидаем. Причем поломки происходят в разных вериях браузера по-разному.

Получив цветовую схему расположения частиц мы можем нехитрым перебором создать набор этих самых частиц, каждая из которых будет иметь свои координаты. В дальнейшем мы будем добавлять к ним дополнительные параметры. Здесь и дальше height и width — это размеры canvas, particles — массив частиц, step — изначальное расстояние между частицами.

const data    = ctx.getImageData(0, 0, width, height).data;
const data32  = new Uint32Array(data.buffer);

for (let x = 0; x < width; x += step) {
    for (let y = 0; y < height; y += step) {
        const color = data32[y * width + x];

        if (color != 0xFFFFFFFF) {
            particles.push({ x, y });
        }
     }
 }

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

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

Теперь мы можем очистить canvas и заполнить его частицами, например нарисовав квадраты на местах их расположения:

particles.forEach((particle) => {
    ctx.fillStyle = particle.color;
    ctx.fillRect(
        particle.x,
        particle.y,
        step,
        step
    );
});

Это не даст особенно интересного эффекта, просто наша картинка или текст немного пикселизируется. Или превратится в набор любых других геометрических форм, если мы будем рисовать не квадраты. Но при добавлении движения начинают получаться занятные результаты:


Ок. А какое движение добавить?


В целом у частиц есть следующие основные параметры:

  • Координаты
  • Размер
  • Форма
  • Цвет

В большинстве случаев у нас будет счетчик времени, который мы будем использовать для определения параметров частиц. Иными словами все, что будет изменяться, мы стараемся привязать ко времени. У этого есть интересная особенность — мы можем увеличивать или уменьшать счетчик, тем самым меняя ход времени и возвращая анимацию к истокам.

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


На этом примере видно, что получается, если на маленьком экране сохранять большой размер частиц (сравните с предыдущим примером). Это к вопросу об адаптивности.

Главное здесь — добавить разнообразие, чтобы частицы меняли свои параметры не совсем синхронно. Это можно сделать разными способами, но обычно для каждой частицы делается какой-то множитель для изменения ее параметров, зависящий от порядкового номера в массиве частиц. Часто достаточно умножить аргумент того же синуса на порядковый номер частицы. Такой подход позволяет добиться приемлемого уровня случайностей без особых умственных усилий. С точки зрения математика-криптографа это не будет выглядеть случайной последовательностью, но для анимации вполне хватает — зритель успеет впечатлиться и пойти дальше раньше, чем произойдут заметные синхронизации в движении частиц.

Полезный совет по поводу цвета для начинающих: если вам нужно плавно менять цвет частиц с течением времени — забудьте про RGB и переходите на HSL. Это позволит не заморачиваться и плавно менять только один из параметров для изменения тона. С насыщенностью то же самое. И даже при уходе в черный или белый не нужно будет думать о том, что цветовая гамма немного плывет как на старом телевизоре.

Пески времени…




Еще одна эффектная штука состоит в том, чтобы создать некоторое подобие водопада, или, лучше сказать, оползня. Частицы начинают двигаться в одну сторону со случайным ускорением, увеличиваясь по высоте. Они как бы оставляют за собой след. Подобные приемы хорошо работают с картинками.

Для того, чтобы поместить картинку на canvas мы используем стандартный метод drawImage. В случае необходимости можно расположить изображения используя поведение, схожее со свойством object-fit: cover или contain. В сети есть готовые решения для этого случая.

Но вернемся к самому эффекту. Его несложно сделать из предыдущего примера. Нам нужно добавить частицам новые параметры — размер и скорость.

particles.forEach((particle) => {
    ctx.fillStyle = particle.color;
    ctx.fillRect(
        particle.x,
        particle.y,
        particle.width,
        particle.height
    );

    particle.speed  += timeCounter * Math.abs(Math.sin(5 * particle.x + 7));
    particle.y      += particle.speed;
    particle.height += particle.speed;
});

timeCounter++;

Если вы когда-нибудь писали генератор случайных чисел, то, вероятно уже подумали о том, что комбинация двух простых чисел (5 и 7) напоминает что-то очень знакомое, только уж очень маленькие эти числа. Можно было бы вспомнить теорию, но на практике при создании таких эффектов численные коэффициенты обычно подбираются методом проб и ошибок — заранее сложно сказать каков будет визуальный эффект в результате. А важен именно он.


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

Куда двигаться дальше?


Мы рассмотрели простейшие методы применения частиц в анимациях. Это лишь верхушка айсберга. Лучший способ научиться делать подобные вещи — это начать делать их. На CodePen регулярно появляются интересные демки, с которыми будет полезно знакомиться. Начинающим разработчикам хочется дать совет: уделяйте большее внимание идеям, заложенным в примеры, которые вы изучаете, нежели конкретным реализациям, и не бойтесь экспериментировать.

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


  1. bgnx
    31.01.2018 15:32

    Мы не сможем на среднем ноутбуке рассчитывать весь экран по одному пикселю и иметь при этом хотя бы 30fps, не говоря уже о 60

    Вы ошибаетесь, вы выбрали неправильную технологию — с 2d канвасом оно конечно будет дико тормозить. Но если взять webgl то все будет летать и можно хоть каждый пиксель экрана вычислять на 60 fps в шейдере. А все потому что в случае с 2d-контекстом канваса браузеру нужно отрендерить все пиксели на процессоре а потом передать весь массив пикселей на видеокарту, в то же время с webgl браузеру нужно передать на видеокарту только данные а все вычисления и рисование пикселей происходит на видеокарте.
    Вот пример вычисления каждого пикселя в шейдере, а здесь полноценные частицы с логикой


    1. sfi0zy Автор
      31.01.2018 15:52

      базовый подход к созданию анимаций из частиц в 2d и посмотрим пару практических примеров, не углубляясь в низкоуровневые оптимизации

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


  1. babylon
    02.02.2018 14:39

    Частичные эффекты это отлично, но вот блур фильтр на флеше и в Adobe Animate (СreateJS)
    к сожалению, сделан по разному. Хуже во втором случае, а клиент хочет аутентичного эффекта