Введение в PixiJS. Часть 1 — движок для рендеринга графических объектов

JavaScript
31949 просмотров

Когда CSS-анимации уже не справляются, а тысячи независимо движущихся частиц нужны прямо сейчас, на сцену выходит PixiJS — один из самых быстрых 2D-рендереров для браузера. Построенный на WebGL (а в версии 8 ещё и на WebGPU), он умеет отрисовывать десятки тысяч объектов за кадр, почти не нагружая CPU. Если вы делаете интерактивную визуализацию, браузерную игру или хотите добавить на сайт что-то, что заставит пользователя сказать «как это сделано?» — вы попали по адресу.

Обучение PixiJS — 2D-рендерер для браузерных анимаций и игр

Это пошаговое руководство по обучению PixiJS 8 с нуля: от установки до фильтров, с живыми примерами прямо на странице.

Что такое PixiJS и когда его применять

PixiJS — это 2D-рендерер, а не игровой движок. Он не управляет физикой, уровнями или состоянием игры — только рисует. И делает это исключительно хорошо: WebGL-рендеринг, пакетная обработка спрайтов, текстурные атласы, фильтры через шейдеры.

Вот где PixiJS уместен, а где — избыточен:

  • Берите PixiJS, если нужны: частицы, сложные интерактивные анимации, Canvas-игры, визуализация данных в реальном времени, generative art (алгоритмическое искусство — визуальные образы и анимации, созданные кодом, а не нарисованные вручную).
  • Не берите PixiJS для обычных UI-переходов и анимаций — там хватит CSS или Anime.js.
PixiJS 8 работает только там, где браузер поддерживает аппаратное ускорение графики. Большинство современных устройств его поддерживают, но около 1–2% пользователей (старые браузеры, корпоративные машины с заблокированным GPU) увидят ошибку вместо анимации. Если аудитория широкая — добавьте проверку и показывайте статичную картинку как запасной вариант.

Установка PixiJS

CDN — быстрый старт. Добавьте скрипт перед закрывающим </body> . Библиотека доступна через глобальный объект PIXI :

<!-- Вставьте перед закрывающим тегом </body> -->
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8/dist/pixi.min.js"></script>

npm — для проектов со сборщиком

# Установка пакета
npm install pixi.js

С ES-модулями можно импортировать только то, что нужно — это уменьшает финальный бандл:

// Импортируем только нужные классы — лишнее в бандл не попадёт
import { Application, Sprite, Graphics, Assets, Text } from 'pixi.js';

Первое приложение на PixiJS

Основа любого PixiJS-проекта — объект Application . Он создаёт холст, запускает рендерер и игровой цикл. Инициализация асинхронная: сначала создаём экземпляр, затем вызываем app.init() и дожидаемся результата:

const app = new PIXI.Application();

await app.init({
    width:      800,
    height:     600,
    background: 0x1a1a2e,   // цвет фона в hex
    antialias:  true,
    resolution: window.devicePixelRatio || 1,
    autoDensity: true,       // масштабирование под Retina
});

// app.canvas — стандартный HTMLCanvasElement
document.body.appendChild(app.canvas);

После этого на странице появится тёмно-синий прямоугольник 800×600 пикселей — это и есть холст. Пока он пустой, но именно на него мы будем добавлять фигуры, спрайты и анимации в следующих разделах.

Два параметра отвечают за чёткость на Retina-экранах: resolution задаёт соотношение пикселей устройства к CSS-пикселям, а autoDensity: true автоматически масштабирует стили холста — без этой пары на Retina картинка будет размытой.

Если нужно, чтобы холст занимал всё окно браузера и подстраивался при изменении его размера, вместо фиксированных width и height используйте resizeTo: window :

await app.init({
    background:  0x0d0d1a,
    resizeTo:    window,     // холст всегда во весь экран
    autoDensity: true,
    resolution:  window.devicePixelRatio || 1,
});

Добавим на холст несколько фигур — и получим первую живую сцену. Каждая фигура — объект PIXI.Graphics , который добавляется на сцену через app.stage.addChild() . Анимация — игровой цикл app.ticker.add() , который вызывается на каждом кадре:

// Создаём фигуру — круг с центром в (0, 0) и радиусом 30, синего цвета
const circle = new PIXI.Graphics();
circle.circle(0, 0, 30).fill({ color: 0x5e9fd8 });

// Задаём позицию на холсте
circle.x = 100;
circle.y = 80;

// Добавляем на сцену — без этого фигура не появится на экране
app.stage.addChild(circle);

// Скорость движения по горизонтали (пикселей за кадр)
let vx = 0.6;

// Каждый кадр сдвигаем круг вправо
app.ticker.add((time) => {
    circle.x += vx * time.deltaTime;

    // Когда круг уходит за правый край — возвращаем его влево
    if (circle.x > app.screen.width) circle.x = 0;
});

Вот как это выглядит с несколькими фигурами одновременно:

Graphics API — рисуем фигуры

Если вам не нужны картинки — фигуры можно нарисовать прямо кодом. Для этого в PixiJS есть PIXI.Graphics : создаёте объект, описываете форму (круг, прямоугольник, линия) и говорите, какого цвета должна быть заливка или обводка.

Логика простая: сначала описываем форму, затем вызываем .fill() для заливки цветом или .stroke() для обводки. Один объект Graphics может содержать сразу несколько фигур — это удобно и работает быстро:

const gfx = new PIXI.Graphics();

// Круг: центр (200, 150), радиус 60, синяя заливка
gfx.circle(200, 150, 60)
   .fill({ color: 0x5e9fd8 });

// Прямоугольник: позиция (50, 40), размер 120×80, оранжевый с жёлтой обводкой
gfx.rect(50, 40, 120, 80)
   .fill({ color: 0xe8734a })
   .stroke({ width: 3, color: 0xffd060 });

// Треугольник: массив точек [x1, y1, x2, y2, x3, y3, ...]
gfx.poly([300, 50, 360, 150, 240, 150])
   .fill({ color: 0x66e888 });

// Линия: от точки (0, 0) до (400, 300)
gfx.moveTo(0, 0)
   .lineTo(400, 300)
   .stroke({ width: 2, color: 0xffffff, alpha: 0.3 });

// Добавляем все фигуры на сцену одним вызовом
app.stage.addChild(gfx);

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

Ticker — сердце анимации

Чтобы что-то двигалось на экране, нужен цикл, который обновляет позиции объектов снова и снова — в идеале 60 раз в секунду. В PixiJS это делает app.ticker : передайте ему функцию, и она будет вызываться на каждом кадре.

Внутри функции доступен параметр time.deltaTime — он показывает, сколько «условных кадров» прошло с предыдущего вызова. На быстром устройстве при 60 fps это значение равно 1, на медленном при 30 fps — около 2. Умножайте скорость движения на deltaTime , и анимация будет выглядеть одинаково на любом устройстве:

app.ticker.add((time) => {
    // Двигаем спрайт вправо и поворачиваем
    // time.deltaTime выравнивает скорость на разных устройствах
    sprite.x        += 2    * time.deltaTime;
    sprite.rotation += 0.03 * time.deltaTime;
});

// Ограничить максимальный FPS (например, для экономии заряда на мобильных)
app.ticker.maxFPS = 30;

// Поставить анимацию на паузу и возобновить
app.ticker.stop();
app.ticker.start();

Ещё один полезный параметр — time.lastTime : это количество миллисекунд с момента старта приложения. Удобно для плавных колебаний — например, чтобы объект медленно покачивался вверх-вниз: Math.sin(time.lastTime / 1000) .

Спрайты и текстуры

Спрайт — это картинка на сцене. Чтобы показать изображение в PixiJS, нужно сделать два шага: сначала загрузить файл как текстуру, затем создать из неё спрайт и добавить на сцену. Загрузка асинхронная — PixiJS сам следит за кешем, так что одна картинка загружается только один раз:

// Загружаем изображение — результат сохраняется в кеше автоматически
const texture = await PIXI.Assets.load('path/to/image.png');

// Создаём спрайт из загруженной картинки
const sprite = new PIXI.Sprite(texture);

// anchor задаёт «точку крепления» спрайта
// 0.5, 0.5 — центр: спрайт будет вращаться вокруг своей середины
// 0, 0 — верхний левый угол (значение по умолчанию)
sprite.anchor.set(0.5);

// Размещаем по центру холста
sprite.x = app.screen.width  / 2;
sprite.y = app.screen.height / 2;

// Добавляем на сцену
app.stage.addChild(sprite);

Если картинки нет, а фигуру нарисовали кодом через Graphics — её тоже можно превратить в текстуру. Это особенно полезно, когда нужно много одинаковых объектов: все они будут использовать одну текстуру в памяти, а не хранить каждый свою копию:

// Рисуем кружок и превращаем его в текстуру
const shape = new PIXI.Graphics();
shape.circle(0, 0, 20).fill({ color: 0xffffff });
const texture = app.renderer.generateTexture(shape);

// Создаём 1000 спрайтов — все используют одну текстуру в памяти
for (let i = 0; i < 1000; i++) {
    const dot = new PIXI.Sprite(texture);
    dot.anchor.set(0.5);

    // Случайная позиция в пределах холста
    dot.x = Math.random() * app.screen.width;
    dot.y = Math.random() * app.screen.height;

    // tint окрашивает спрайт в нужный цвет без создания новой текстуры
    dot.tint = Math.random() * 0xffffff;

    app.stage.addChild(dot);
}
tint — простой способ перекрасить спрайт, не создавая отдельную текстуру для каждого цвета. Значение 0xffffff означает «без окраски», любое другое — смешивается с исходными пикселями картинки.

Контейнеры и граф сцены

Представьте, что вы собираете корабль из нескольких деталей: корпус, пушка, парус. Чтобы передвинуть корабль — нужно двигать каждую деталь отдельно. Неудобно. Вместо этого можно сложить все детали в один «невидимый ящик» — PIXI.Container — и двигать уже его. Все вложенные объекты будут следовать за контейнером автоматически:

// Создаём контейнер — он сам по себе невидим, это просто «группа»
const ship = new PIXI.Container();

const hull   = new PIXI.Sprite(hullTexture);
const cannon = new PIXI.Sprite(cannonTexture);

// Позиция пушки задаётся относительно корабля, а не экрана
cannon.x = 40;

// Добавляем детали в контейнер
ship.addChild(hull, cannon);

// Добавляем контейнер на сцену
app.stage.addChild(ship);

// Теперь достаточно двигать ship — все детали следуют за ним
app.ticker.add((time) => {
    ship.x        += 1     * time.deltaTime;
    ship.rotation += 0.005 * time.deltaTime; // весь корабль поворачивается вместе
});

Кстати, app.stage — это тоже контейнер, самый верхний. Всё, что вы добавляете на сцену через addChild() , попадает в него. Если объект не добавлен в app.stage (напрямую или через вложенный контейнер) — он не появится на экране. Убрать объект со сцены можно двумя способами: container.removeChild(sprite) — временно скрыть, или sprite.destroy() — удалить полностью и освободить память.

Текст в PixiJS

Чтобы вывести надпись на холст, используйте PIXI.Text . Передайте строку и объект со стилями — размер, цвет, шрифт, тень, обводка. Менять текст можно в любой момент, просто присвоив новое значение свойству .text :

const label = new PIXI.Text({
    text: 'Game Over',
    style: {
        fontSize:   64,
        fill:       0xffffff,         // цвет текста
        fontWeight: 'bold',
        fontFamily: 'Arial, sans-serif',
        stroke:     { color: 0xff0000, width: 4 },  // красная обводка
        dropShadow: {
            color:    0x000000,       // чёрная тень
            blur:     6,
            distance: 4,
            angle:    Math.PI / 4,    // угол 45°
        },
    },
});

// Размещаем по центру холста
label.anchor.set(0.5);
label.x = app.screen.width  / 2;
label.y = app.screen.height / 2;
app.stage.addChild(label);

// Поменять текст в любой момент — просто присвоить новое значение
label.text = 'You Win!';

Если на сцене много текстовых объектов, которые часто меняются — счётчики очков, ярлыки, подписи к объектам — присмотритесь к PIXI.BitmapText . Он работает быстрее, потому что использует готовое изображение шрифта вместо того, чтобы рендерить каждую букву заново. Вот как выглядит PIXI.Text на практике:

Интерактивность

По умолчанию объекты на холсте не реагируют на мышь или касание — они просто рисуются. Чтобы объект начал отвечать на действия пользователя, нужно включить для него события одной строкой: eventMode = "static" . После этого можно вешать обработчики — так же, как на обычные DOM-элементы:

const circle = new PIXI.Graphics();
circle.circle(0, 0, 40).fill({ color: 0x5e9fd8 });
circle.x = 200;
circle.y = 150;

// Включаем обработку событий для этого объекта
circle.eventMode = 'static';

// Меняем курсор при наведении — как у обычной кнопки
circle.cursor = 'pointer';

// Наводим — увеличиваем
circle.on('pointerover', () => { circle.scale.set(1.2); });
// Уводим — возвращаем размер
circle.on('pointerout',  () => { circle.scale.set(1.0); });
// Нажимаем — делаем полупрозрачным
circle.on('pointerdown', () => { circle.alpha = 0.6; });
// Отпускаем — возвращаем непрозрачность
circle.on('pointerup',   () => { circle.alpha = 1.0; });
// Клик (или тап на телефоне)
circle.on('pointertap',  () => { console.log('Нажали!'); });

app.stage.addChild(circle);

Все события начинаются с префикса pointer — это удобно: они одинаково работают и на компьютере (мышь), и на телефоне (касание). Не нужно писать два отдельных обработчика. Попробуйте сами — понажимайте на кружки:

PixiJS определяет, попал ли курсор в объект, по его прямоугольной области. Для круга или сложной фигуры это может работать неточно — курсор будет реагировать на углы прямоугольника, которые визуально пустые. Чтобы задать точную зону клика, используйте hitArea: sprite.hitArea = new PIXI.Rectangle(x, y, w, h).

Фильтры в PixiJS

Фильтры — это визуальные эффекты, которые применяются поверх любого объекта или целой сцены. Размытие, обесцвечивание, яркость, сепия — всё это включается буквально одной строчкой кода. PixiJS берёт изображение уже нарисованного объекта и пропускает его через эффект перед выводом на экран.

// Размытие — чем больше strength, тем сильнее размыто
const blur = new PIXI.BlurFilter({ strength: 8 });

// Цветовой фильтр — меняет яркость, контраст, цвет
const color = new PIXI.ColorMatrixFilter();
color.grayscale(1, false);    // сделать чёрно-белым
// color.brightness(1.5);     // яркость +50%
// color.sepia(false);        // эффект старой фотографии
// color.saturate(0.5, true); // приглушить насыщенность

// Применяем фильтр к спрайту — можно сразу несколько
sprite.filters = [blur, color];

// Или ко всей сцене разом
app.stage.filters = [blur];

// Убрать все фильтры
sprite.filters = null;

Фильтры удобны, но их не стоит использовать без меры: каждый фильтр заставляет браузер проделать дополнительную работу при отрисовке каждого кадра. Старайтесь не вешать больше 2–3 фильтров на один объект, а на объектах, которые скрыты или вышли за край экрана, фильтры лучше отключать совсем — это заметно ускоряет работу на слабых устройствах.

В стандартной поставке фильтров уже немало, но если хочется большего — загляните в пакет @pixi/filters: там собраны ещё два десятка готовых эффектов — свечение, капли дождя, имитация CRT-монитора, хроматическая аберрация и другие.

Итого: что умеет PixiJS

Изучение PixiJS 8 начинается быстро: API продуман, живые примеры работают сразу, документация подробная. Асинхронная инициализация, переработанный Graphics API, WebGPU-рендерер — это не просто косметика, а реальные улучшения, которые ощущаются на практике.

  • Для статики — Graphics с .fill() и .stroke() .
  • Для анимации — app.ticker.add() с учётом time.deltaTime .
  • Для изображений — Assets.load() + Sprite .
  • Для текста — PIXI.Text или BitmapText для высоконагруженных сцен.
  • Для событий — eventMode = "static" и pointer* .

Если хочется анимировать DOM-элементы без WebGL — посмотрите на Anime.js: анимации на JavaScript. А для визуализации данных с графиками и интерактивными диаграммами стоит обратить внимание на D3.js — они с PixiJS прекрасно дополняют друг друга.

Освоили основы? В второй части идём дальше: десятки тысяч частиц через ParticleContainer, рисование прямо в текстуру, режимы смешивания для огня и свечения, покадровая анимация и собственные GLSL-шейдеры.

Вам может быть интересно:
Swiper.js — слайдер для сайта: настройки и примеры
Swiper.js — как подключить и настроить слайдер на сайте
Что такое функция в программировании — типы и примеры на JavaScript и Python
Что такое функция в программировании?
Promise в JavaScript — промисы JS простыми словами для начинающих
Promise в JavaScript простыми словами — полное руководство
Анимация частиц и GLSL-шейдеры в PixiJS 8 — живой пример в браузере
Изучаем PixiJS. Часть 2 — продвинутая анимация и GLSL-шейдеры
Комментарии 0 Разные мнения приветствуются. Здесь можно спорить и обсуждать, но уважительно и без токсичности.
Отмена