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

Работа с графикой — основа визуализации в играх. Часто простого рисования фигур недостаточно: нужно оживить их, заставить двигаться, вращаться и менять размер. В этой статье мы разберем, как использовать методы трансформации канваса в Phaser (translateCanvas, scaleCanvas, rotateCanvas) для создания динамических графических объектов. Вы научитесь управлять контекстом рисования, что позволит вам легко анимировать десятки и сотни элементов с минимальным кодом, не прибегая к тяжеловесным спрайтам.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    rectangles = [];
    t = 0;
    graphics;

    create ()
    {
        this.graphics = this.add.graphics({x: 0, y: 0});

        for (let i = 0; i < 100; ++i)
        {
            this.rectangles.push({
                x: Math.random() * 800,
                y: Math.random() * 600,
                w: 50 + Math.random() * 50,
                h: 50 + Math.random() * 50,
                r: Math.random()
            });
        }

    }

    update ()
    {

        this.graphics.clear();

        for (let i = 0; i < this.rectangles.length; ++i)
        {
            const rect = this.rectangles[i];
            this.graphics.save();
            this.graphics.translateCanvas(rect.x, rect.y);
            this.graphics.scaleCanvas(Math.sin(rect.r), Math.sin(rect.r));
            this.graphics.rotateCanvas(rect.r);
            this.graphics.fillStyle(0xFFFF00, 1.0);
            this.graphics.lineStyle(4.0, 0xFF0000, 1.0);
            this.graphics.fillRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h);
            this.graphics.strokeRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h);
            this.graphics.restore();
            rect.r += 0.01;

        }
    }
}

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

const game = new Phaser.Game(config);


Подготовка сцены и данных

В начале кода мы создаем класс сцены, который будет хранить массив данных для наших прямоугольников и объект Graphics. В методе create() инициализируется холст для рисования и заполняется массив rectangles.

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

class Example extends Phaser.Scene
{
    rectangles = [];
    t = 0;
    graphics;

    create ()
    {
        this.graphics = this.add.graphics({x: 0, y: 0});

        for (let i = 0; i < 100; ++i)
        {
            this.rectangles.push({
                x: Math.random() * 800,
                y: Math.random() * 600,
                w: 50 + Math.random() * 50,
                h: 50 + Math.random() * 50,
                r: Math.random()
            });
        }
    }

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

Метод update() вызывается на каждом кадре игры. Первым делом мы очищаем холст от предыдущего кадра с помощью this.graphics.clear(). Это важно, так как мы рисуем заново все элементы каждый раз, чтобы создать иллюзию плавного движения.

Затем начинается цикл по всем прямоугольникам в массиве. Для каждого из них мы будем применять трансформации и рисовать заново.

update ()
    {
        this.graphics.clear();

        for (let i = 0; i < this.rectangles.length; ++i)
        {
            const rect = this.rectangles[i];
            // ... трансформации и рисование ...
            rect.r += 0.01;
        }
    }

Сохранение и восстановление контекста

Перед применением трансформаций к конкретному прямоугольнику мы вызываем this.graphics.save(). Этот метод сохраняет текущее состояние контекста рисования (все текущие трансформации, стили и т.д.) в стек.

После отрисовки прямоугольника мы вызываем this.graphics.restore(). Это восстанавливает контекст до состояния, сохраненного последним вызовом save(). Такой подход гарантирует, что трансформации, примененные к одному прямоугольнику, не повлияют на отрисовку следующих. Это ключевой паттерн при работе с множественными графическими объектами.

this.graphics.save();
            // ... применяем трансформации и рисуем ...
            this.graphics.restore();

Трансформации: перемещение, масштабирование, вращение

Сердце анимации — три метода трансформации контекста канваса.

1.  `this.graphics.translateCanvas(rect.x, rect.y)` — перемещает начало системы координат (точку (0,0)) в заданные координаты `rect.x` и `rect.y`. Все последующие операции рисования будут отсчитываться от этой новой точки.
2.  `this.graphics.scaleCanvas(Math.sin(rect.r), Math.sin(rect.r))` — масштабирует контекст по осям X и Y. В примере используется синус от угла `rect.r`, что создает пульсирующий эффект (масштаб меняется от -1 до 1).
3.  `this.graphics.rotateCanvas(rect.r)` — поворачивает контекст на угол `rect.r` (в радианах).

Важно! Порядок вызова этих методов имеет значение. В данном коде сначала выполняется перемещение, потом масштабирование, затем вращение.

this.graphics.translateCanvas(rect.x, rect.y);
            this.graphics.scaleCanvas(Math.sin(rect.r), Math.sin(rect.r));
            this.graphics.rotateCanvas(rect.r);

Рисование с учетом нового центра

После применения трансформаций мы задаем стили и рисуем сам прямоугольник. Обратите внимание на координаты: fillRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h). Поскольку мы переместили начало координат в центр будущего прямоугольника (translateCanvas(rect.x, rect.y)), чтобы прямоугольник вращался вокруг своего центра, мы начинаем рисовать его не с (0,0), а со смещения в минус половину ширины и высоты. Это классический прием для вращения объекта вокруг его собственного центра, а не вокруг верхнего левого угла.

Методы fillRect и strokeRect рисуют залитый и обведенный прямоугольник соответственно.

this.graphics.fillStyle(0xFFFF00, 1.0);
            this.graphics.lineStyle(4.0, 0xFF0000, 1.0);
            this.graphics.fillRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h);
            this.graphics.strokeRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h);

Запуск конфигурации игры

В конце файла находится стандартная конфигурация игры Phaser. Указывается тип рендерера (Phaser.CANVAS), элемент-родитель на странице, класс нашей сцены (Example), а также ширина и высота игрового поля. Создание экземпляра Phaser.Game с этой конфигурацией запускает весь описанный выше цикл.

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

const game = new Phaser.Game(config);

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

Использование методов save(), restore() и трансформаций канваса (translateCanvas, scaleCanvas, rotateCanvas) открывает мощный и производительный способ анимации графических примитивов в Phaser. Этот подход идеален для создания частиц, фоновых элементов, UI-эффектов или простых игровых объектов, где использование текстурных спрайтов было бы избыточным. **Идеи для экспериментов:** 1. Измените формулу для scaleCanvas, чтобы масштаб зависел от времени или положения прямоугольника. 2. Добавьте в объекты прямоугольника свойство скорости и изменяйте координаты `xиyвupdate()`, чтобы они двигались по экрану. 3. Попробуйте изменить порядок вызова трансформаций (например, вращение до перемещения) и посмотрите, как изменится поведение анимации. 4. Вместо прямоугольников нарисуйте более сложные фигуры с помощью graphics.lineTo и graphics.strokePath.