Конфетти на завершении оплаты, искры на кнопке успеха, лёгкий снег на новогоднем лендинге, шлейф за курсором: частицы это самая дешёвая трюк интерфейса, потому что за ней стоит всего лишь холст, цикл кадров и пара формул физики школы. Красота в том, что эффект собирается из одинаковых кирпичей, и разобравшись с одним дождём, получаешь за вечер и дым, и взрыв, и снежную шапку. Некрасота в том, что наивная реализация ложит страницу на четвёртой секунде, и разница между лёгким праздником и тормозом лежит в дисциплине кадров, о которой ниже каждый раздел.
Разобраны основы: жизнь одной частицы, характеры эмиттеров, производительность с пулом и бюджетом, чёткость на плотных экранах, взаимодействие с курсором и честный момент переезда на WebGL. Код в статье короткий и рабочий, числа проверяются профилем за минуту.
Что такое система частиц и где она уместна в интерфейсах
Система частиц это множество короткоживущих независимых объектов с одинаковыми правилами поведения: у каждой частицы есть положение, скорость, время жизни и вид, и поведение задаётся не сценарием, а физикой. Ни одна частица не знает о других, и именно независимость даёт эффекту живость: общий рисунок рождается из сотни простых судеб, как шум дождя рождается из капель.
Уместность в интерфейсах измеряется смыслом: частицы работают там, где надо отметить событие и подарить секунду радости, завершение покупки, победа в игре, праздник на странице. Они вредят там, где конкурируют с контентом за внимание: постоянный снег поверх формы утомляет, а искры на каждой кнопке превращают фокус в карнавал. Правило жанра: эффект появляется по случаю и живёт секунды, а не вечно.
Объём уместности измеряется и техникой: холст с сотней частиц невесом, с тысячей требует дисциплины, а на десятках тысяч честнее сразу звать тяжёлую артиллерию. Границы этих чисел зависят от железа и правил отрисовки, и потому каждое утверждение о счёте в статье проверяется профилем на целевых устройствах, а не верится на слово.
Инженерный смысл системы частиц и в повторном использовании: один движок с разными настройками закрывает все эффекты продукта, и настройки выносятся в описания, которые читают даже те, кто не пишет код. Движок это актив, каждый отдельный эффект это строчка конфигурации.
Жизнь частицы рождение движение затухание в одном цикле кадров
Частица рождается со стартовыми значениями: положение, начальная скорость, случайное отклонение для неповторимости, размер, оттенок и время жизни. Дальше каждый кадр повторяется одна и та же тройка операций: физика обновляет положение, время жизни тикает, отрисовка рисует то, что осталось. Упрощённость этой тройки и есть вся система.
const canvas = document.querySelector("#fx");
const ctx = canvas.getContext("2d");
let last = 0;
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
ctx.clearRect(0, 0, canvas.width, canvas.height);
updateParticles(dt);
drawParticles(ctx);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
Физика держится двумя правилами: скорость меняет положение, ускорение меняет скорость, и гравитация это постоянное ускорение вниз. Ветер, сопротивление среды и притяжение к точке это тоже ускорения, добавленные по вкусу эффекта, и вся физика умещается в пару строк сложения на частицу. Движение по времени кадра, а не по кадрам: приращение умножается на интервал, и эффект живёт одинаково на шестидесяти и на ста сорока кадрах в секунду.
Затухание отвечает за красоту финала: частица умирает постепенно, прозрачность и размер уходят к нулю, и исчезновение выглядит таянием, а не обрывом. Кривая затухания выбирается по вкусу, линейная для дыма, ускоренная для искр, и время жизни задаёт общий темп эффекта.
Смерть частицы это выход из списка живых, и единственная сложность тут аккуратность с индексами: удаление из середины массива в цикле требует обратного обхода или обмена с последним. Ошибки этого класса не роняют страницу, они просто дарят вечную жизнь нескольким частицам и медленный зомби-эффект.
Цикл кадров завершается планированием следующего: стандартный запрос следующего кадра у браузера сам подстраивается под экран и экономит батарею, а принудительные таймеры с фиксированным темпом ломают ритм и жгут ресурсы. Пауза вкладки по событию видимости останавливает цикл целиком: невидимый эффект не рисуется и не считается, а возвращение страницы запускает кадр с потолком интервала, чтобы задняя пауза не обрушилась взрывом догоняющих шагов.
Эмиттеры сплошной поток и одиночный взрыв как задавать характер эффекта
Эмиттер это источник частиц, и его характер задаёт характер всего эффекта. Сплошной поток с постоянной скоростью рождения даёт дождь, дым, водопад: равномерность ожидаема, и случайность живёт в отклонениях. Одиночный взрыв рождает пачку мгновенно: конфетти, искры, брызги, и весь эффект это судьба одной пачки без продолжения.
Смешанный характер живёт между краями: поток с всплесками по событию, взрывы с хвостами из редких капель, и характер описывается тремя числами, скорость рождения, размер пачки и профиль по времени. Эмиттеры двигаются: привязанный к курсору источник рождает шлейф, привязанный к кнопке подчёркивает момент.
Направленность задаётся конусом и разбросом: узкий конус даёт струю, широкий туман, полная сфера у взрыва. Скорости наследуют тот же разброс, и пара диапазонов задаёт узнаваемость эффекта сильнее любых текстур: медленный широкий поток это дым, быстрый узкий это струя, мгновенный сферический это взрыв.
Ограничение потока встроено в здоровье системы: эмиттер уважает верхний порог живых частиц, и при упоре в потолок рождения приостанавливаются. Эффект, который честно гасит рождение вместо накопления хвоста, живёт вечно, эффект без тормоза умирает от собственной щедрости на слабом устройстве.
Производительность пул объектов батчинг и бюджет кадра
Первый враг скорости это не количество частиц, а мусор: создание и выбрасывание сотен объектов в кадр грузит сборщика, и паузы сборки читаются как заикание. Лечение известно и называется пулом: объекты частиц создаются заранее и переиспользуются, мёртвая частица возвращается в пул, рождение берёт из пула, и распределения памяти в цикле нет вовсе.
const pool = [];
const alive = [];
function spawn(p) {
const particle = pool.pop() ?? {};
particle.x = p.x;
particle.y = p.y;
particle.vx = p.vx;
particle.life = 1;
alive.push(particle);
}
function retire(i) {
pool.push(alive[i]);
alive.splice(i, 1);
}
Второй враг это дорогие вызовы отрисовки: теневые фильтры, размытия и композиции по одной частице умножаются на сотни. Здоровый стиль рисования скромен: простые формы, предрассчитанные спрайты на вспомогательном холсте, глобальная прозрачность вместо попиксельной, и минимальные переключения состояния контекста. Спрайт из четырёх кругов с мягким краем рисуется быстрее любого размытия на порядок.
Третий враг это нетрезвый бюджет: эффект обязан укладываться в свою долю кадра, и доля проверяется замером, а не ощущением. Практические потолки для двухмерного холста измеряются сотнями активных частиц с простым стилем и тысячей на грани, и потому счёт живых частиц ограничен настройкой, а замер кадра встроен в разработку с первого дня.
Пул и спрайты и потолок это и есть разница между эффектом, который украшает продукт, и эффектом, ради которого продукт ругают в поддержке. Профиль кадра делается до релиза, и числа публикуются рядом с конфигурацией эффекта как паспорт.
Чёткость на любом экране пиксельная плотность и холст вне потока вёрстки
Плотные экраны наказывают наивный холст размытием: холст в физических пикселях меньше, чем экранных точек, и браузер растягивает мягкую кашу. Правило знакомо каждому, кто рисовал на холсте: размер буфера умножается на плотность экрана, масштаб контекста выставляется той же плотностью, и частицы становятся резкими на любом устройстве.
Плотность учитывается с потолком: на очень плотных экранах честная отрисовка в три физических пикселя на экранный стоит дорого, и потолок плотности в двойке-тройке держит баланс чёткости и скорости. Пересчёт на изменение окна делается один раз по событию, а не в каждом кадре, и холст живёт своей жизнью независимо от вёрстки вокруг.
Позиционирование холста это отдельная аккуратность: контейнер с размером и абсолютный холст внутри не ломают поток вёрстки, и эффект не толкает контент. Прозрачный фон холста по умолчанию смешивается со страницей, и фон страницы под эффектом выбирается осознанно, потому что полупрозрачные частицы складываются именно с ним.
Слои эффектов живут на отдельных холстах там, где стили конфликтуют: статичный фон рисуется раз, живой слой обновляется в кадре, и разнесение слоёв экономит кадры без единой жертвы внешнего вида. Два холста дешевле одного холста с двумя стилями на каждую частицу.
Взаимодействие курсор и палец как источник поля для частиц
Живой эффект отвечает на человека, и минимальное взаимодействие это курсор как источник: шлейф рождается за указателем, и чем быстрее движение, тем гуще хвост, потому что рождения распределены по пройденному пути, а не по времени. Палец на сенсорном экране делает то же для мобильного жанра, и события касаний транслируются в координаты холста честно, с учётом плотности.
Второй жанр взаимодействия это поле: указатель становится притягателем или расталкивателем, и каждая частица в радиусе получает ускорение к точке или от неё. Поле превращает ровный дождь в живую среду, которая расступается перед курсором, и одно это правило окупает весь движок вниманием пользователей.
Третий жанр это событие: клик рождает взрыв в точке, попадание по кнопке запускает салют, и события интерфейса питают эмиттеры напрямую. Событийные эффекты тем и хороши, что появляются по случаю и не просят ресурсов в покое, и потому они лучший первый жанр для внедрения частиц в продукт.
Ограничения взаимодействия физиологичны: невидимые поля без визуального отклика читаются как глюк, и потому радиус влияния подсвечивается самим поведением частиц. Отклик на вход обязателен: первое касание и первый клик после простоя не теряются, эмиттер стартует с события, а не со второго события.
Когда частицам тесно на 2D переход на WebGL без слёз
Двумерный холст прощает сотни частиц и прощает тысячу при дисциплине, но настоящие десять тысяч живут в другом мире. Порог переезда измеряется профилем: когда доля эффекта в кадре стабильно превышает разумный бюджет и сокращать стиль уже нечего, списки правил ведут к одному направлению, и направление называется WebGL.
Переезд не переписывает концепцию: система частиц остаётся системой с эмиттерами, физикой и пулом, меняется только исполнитель отрисовки. Массовая отрисовка точек в среде с ускорением железа снимает потолок на порядок, и существующие обвязки движков позволяют переехать без написания шейдеров руками с нуля.
Гибрид это честный компромисс: повседневные эффекты остаются на простом холсте, один тяжёлый сценарий уезжает на ускоренный слой, и продукт не платит сложностью за весь арсенал. Гибриды живут годами в зрелых продуктах, и переезд всего на WebGL остаётся случаем настоящей необходимости, а не модой.
Тест переезда лёгок: один и тот же эффект включается на обоих исполнителях, и сравнение доли кадра на слабом устройстве решает спор числами. Держать оба исполнителя в движке дороже одного, зато каждая конфигурация эффекта получает честную цену в кадрах, и решение остаётся инженерным, а не вероучительным.
Порядок сборки производительной системы частиц свёрнут в короткую лестницу:
- описать эффект тремя числами эмиттера и выбрать характер: поток, взрыв, смесь;
- написать цикл кадров с временем кадра и потолком интервала;
- собрать пул частиц и убрать любые распределения памяти из цикла;
- перевести отрисовку на простые формы или предрассчитанные спрайты;
- выставить плотность экрана с потолком и проверить резкость на устройствах;
- добавить взаимодействие: шлейф, поле, событие, с откликом на первое касание;
- замерить долю кадра и задокументировать потолок счёта живых частиц.
Частицы это редкий случай, когда красота измерима строками кода: пара формул, сотня объектов и дисциплина кадров дают эффект, который запоминается сильнее параграфов текста. Движок, собранный по этой лестнице, весит меньше одной картинки и украшает продукт годами, не прося ничего взамен.