О чем этот пример

В этой статье мы разберём пример, который демонстрирует мощь графического API Phaser. Используя объект `Graphics`, мы нарисуем и анимируем логотип фреймворка, состоящий из нескольких цветных слоёв. Этот подход полезен для создания динамического UI, нестандартных визуальных эффектов и отрисовки геометрических фигур напрямую на канвасе, без использования растровых изображений. Вы научитесь управлять контекстом рисования: трансформациями, стилями линий и сложными путями (paths).

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    r = 0;
    graphics;

    create ()
    {
        this.graphics = this.add.graphics();
    }

    update ()
    {
        this.r += 0.02;

        this.graphics.clear();

        //  From back to front :)

        this.drawLogo(0x650a05, -380, -100, 0.76);
        this.drawLogo(0xa00d05, -380, -100, 0.78);
        this.drawLogo(0xcd1106, -380, -100, 0.80);
        this.drawLogo(0xf53719, -380, -100, 0.82);
        this.drawLogo(0xf25520, -380, -100, 0.84);
        this.drawLogo(0xf26f21, -380, -100, 0.86);
        this.drawLogo(0xf49214, -380, -100, 0.88);
        this.drawLogo(0xf6a90a, -380, -100, 0.90);
        this.drawLogo(0xfad400, -380, -100, 0.92);
        this.drawLogo(0xfef700, -380, -100, 0.94);
        this.drawLogo(0xffff45, -380, -100, 0.96);
        this.drawLogo(0xffffc4, -380, -100, 0.98);
        this.drawLogo(0xffffff, -380, -100, 1.00);
    }

    drawLogo (color, x, y, scale)
    {
        const thickness = 2;
        const alpha = 1;

        this.graphics.lineStyle(thickness, color, alpha);

        const w = 100;
        const h = 200;
        const h2 = 100;
        const top = y + 0;
        const mid = y + 100;
        const bot = y + 200;
        const s = 20;

        this.graphics.save();
        this.graphics.translateCanvas(400, 300);
        this.graphics.scaleCanvas(scale, scale);
        this.graphics.rotateCanvas(this.r);

        this.graphics.beginPath();

        //  P

        this.graphics.moveTo(x, top);
        this.graphics.lineTo(x + w, top);
        this.graphics.lineTo(x + w, mid);
        this.graphics.lineTo(x, mid);
        this.graphics.lineTo(x, bot);

        //  H

        x += w + s;

        this.graphics.moveTo(x, top);
        this.graphics.lineTo(x, bot);
        this.graphics.moveTo(x, mid);
        this.graphics.lineTo(x + w, mid);
        this.graphics.moveTo(x + w, top);
        this.graphics.lineTo(x + w, bot);

        //  A

        x += w + s;

        this.graphics.moveTo(x, bot);
        this.graphics.lineTo(x + (w * 0.75), top);
        this.graphics.lineTo(x + (w * 0.75) + (w * 0.75), bot);

        //  S

        x += ((w * 0.75) * 2) + s;

        this.graphics.moveTo(x + w, top);
        this.graphics.lineTo(x, top);
        this.graphics.lineTo(x, mid);
        this.graphics.lineTo(x + w, mid);
        this.graphics.lineTo(x + w, bot);
        this.graphics.lineTo(x, bot);

        //  E

        x += w + s;

        this.graphics.moveTo(x + w, top);
        this.graphics.lineTo(x, top);
        this.graphics.lineTo(x, bot);
        this.graphics.lineTo(x + w, bot);
        this.graphics.moveTo(x, mid);
        this.graphics.lineTo(x + w, mid);

        //  R

        x += w + s;

        this.graphics.moveTo(x, top);
        this.graphics.lineTo(x + w, top);
        this.graphics.lineTo(x + w, mid);
        this.graphics.lineTo(x, mid);
        this.graphics.lineTo(x, bot);
        this.graphics.moveTo(x, mid);
        this.graphics.lineTo(x + w, bot);

        this.graphics.strokePath();

        this.graphics.restore();
    }
}

const config = {
    width: 800,
    height: 600,
    type: Phaser.AUTO,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и объекта Graphics

Вся работа начинается в классе сцены. Мы создаём экземпляр графического объекта и объявляем переменную для угла вращения.

class Example extends Phaser.Scene
{
    r = 0;
    graphics;

    create ()
    {
        this.graphics = this.add.graphics();
    }

Метод create вызывается один раз при инициализации сцены. Здесь мы создаём объект this.graphics через this.add.graphics(). Этот объект — наш холст для векторного рисования. Переменная `r` будет хранить текущий угол поворота логотипа и инициализируется нулём.

Цикл обновления и очистка холста

Анимация происходит в методе update, который вызывается на каждом кадре. Перед отрисовкой нового кадра мы очищаем старое изображение и увеличиваем угол вращения.

update ()
{
    this.r += 0.02;
    this.graphics.clear();
    //  From back to front :)
    this.drawLogo(0x650a05, -380, -100, 0.76);
    this.drawLogo(0xa00d05, -380, -100, 0.78);
    // ... остальные вызовы drawLogo
    this.drawLogo(0xffffff, -380, -100, 1.00);
}
Ключевые моменты:
1. `this.r += 0.02;` — инкремент угла вращения, что обеспечивает плавную анимацию.
2. `this.graphics.clear();` — полностью очищает графический объект от всех ранее нарисованных линий и фигур. Без этого вызова кадры накладывались бы друг на друга.
3. Многократный вызов `this.drawLogo()` с разными цветами и масштабами создаёт эффект «толстого» объёмного логотипа. Обратите внимание на комментарий «From back to front»: сначала рисуются дальние (меньшие) слои, затем ближние, что создаёт правильное наложение цветов.

Основная логика рисования заключена в методе drawLogo. Он настраивает стиль линии, применяет трансформации к контексту холста и рисует путь (path), составляющий слово "PHASER".

drawLogo (color, x, y, scale)
{
    const thickness = 2;
    const alpha = 1;
    this.graphics.lineStyle(thickness, color, alpha);
    // ... объявление переменных w, h, top, mid, bot, s
    this.graphics.save();
    this.graphics.translateCanvas(400, 300);
    this.graphics.scaleCanvas(scale, scale);
    this.graphics.rotateCanvas(this.r);
    // ... команды рисования пути (moveTo, lineTo)
    this.graphics.strokePath();
    this.graphics.restore();
}
Разберём по порядку:
- `this.graphics.lineStyle(thickness, color, alpha);` — задаёт стиль для последующих линий: толщину, цвет (в hex) и прозрачность.
- `this.graphics.save();` — сохраняет текущее состояние контекста рисования (все трансформации). Это критически важно.
- Далее применяются трансформации:
  - `translateCanvas(400, 300)` — смещает точку отсчёта (0,0) в центр экрана (при размере игры 800x600).
  - `scaleCanvas(scale, scale)` — масштабирует весь последующий рисунок. Параметр `scale` разный для каждого цветного слоя.
  - `rotateCanvas(this.r)` — вращает холст на текущий угол `r`.
- После этого выполняется последовательность `moveTo` и `lineTo`, которая описывает контур букв.
- `this.graphics.strokePath();` — фактически выполняет отрисовку накопленного пути заданным стилем линии.
- `this.graphics.restore();` — восстанавливает состояние контекста, которое было на момент `save()`. Это сбрасывает все трансформации (смещение, масштаб, поворот) для следующего вызова `drawLogo`, чтобы они не накапливались.

Построение контура букв

Рисование каждой буквы — это последовательное соединение линий. Координаты рассчитываются относительно параметров `xиy, переданных в функцию, и базовых размеровw(ширина буквы) иh` (высота).

//  P
this.graphics.moveTo(x, top);
this.graphics.lineTo(x + w, top);
this.graphics.lineTo(x + w, mid);
this.graphics.lineTo(x, mid);
this.graphics.lineTo(x, bot);
//  H
x += w + s;
this.graphics.moveTo(x, top);
this.graphics.lineTo(x, bot);
this.graphics.moveTo(x, mid);
this.graphics.lineTo(x + w, mid);
this.graphics.moveTo(x + w, top);
this.graphics.lineTo(x + w, bot);

Алгоритм: 1. moveTo(x, y) — перемещает «перо» в начальную точку отрезка, не рисуя. 2. lineTo(x, y) — рисует линию из текущей позиции пера в указанные координаты, после чего перо остаётся в новой точке. 3. Для перехода к следующей букве переменная `xувеличивается на ширину буквыwплюс интервалs:x += w + s;`. Таким образом, каждая следующая буква смещается вправо. 4. Обратите внимание, что для буквы H используется несколько команд moveTo, так как её контур состоит из трёх несвязанных отрезков. Метод strokePath() нарисует их все одним стилем.

Конфигурация и запуск игры

За пределами класса сцены находится стандартная конфигурация игры Phaser 3, которая указывает размеры, тип рендерера и запускает нашу сцену.

const config = {
    width: 800,
    height: 600,
    type: Phaser.AUTO,
    parent: 'phaser-example',
    scene: Example
};
const game = new Phaser.Game(config);

Здесь нет ничего специфичного для графики: - width и height задают размер области отрисовки. - type: Phaser.AUTO позволяет Phaser самому выбрать WebGL или Canvas рендерер. - parent — это ID HTML-элемента, в который будет встроен canvas. - scene: Example — регистрирует наш класс как единственную сцену игры. Создание экземпляра Phaser.Game с этой конфигурацией запускает игровой цикл.

Что попробовать дальше

Этот пример — отличная отправная точка для освоения низкоуровневого рисования в Phaser. Вы узнали, как использовать объект Graphics для создания динамических векторных изображений, управлять трансформациями контекста и строить сложные пути. Для экспериментов попробуйте: 1. Изменить цвета или сделать их плавно меняющимися со временем. 2. Анимировать не только вращение, но и масштаб или положение каждого слоя независимо, создавая эффект параллакса. 3. Заменить отрисовку линий на заполненные фигуры, используя beginPath() и fillPath() вместе с fillStyle(). 4. Реализовать интерактивность: например, менять скорость вращения при клике.