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

Работа с текстом в играх часто не ограничивается простым выводом строк. Чтобы создать эффект подсветки, постепенного появления или интерактивного раскрытия текста, необходимо управлять видимой областью. Метод `setCrop()` для текстовых объектов в Phaser 3 позволяет именно это — определять прямоугольную область отображения. Эта статья на практическом примере покажет, как реализовать динамическое кадрирование текста, которое следует за курсором мыши, открывая путь к созданию визуально интересных интерфейсов и титров.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    offset;
    graphics;
    bob;

    create ()
    {
        const dull = this.add.text(400, 300, 'Phaser 3\nText Crop\nHell Yeah!', { fontFamily: 'Arial Black', fontSize: 74, color: '#c51b7d', align: 'center' }).setStroke('#de77ae', 16);
        dull.setAlpha(0.15).setOrigin(0.5);

        // dull.setFlipX(true);
        // dull.setFlipY(true);

        this.bob = this.add.text(400, 300, 'Phaser 3\nText Crop\nHell Yeah!', { fontFamily: 'Arial Black', fontSize: 74, color: '#c51b7d', align: 'center' }).setStroke('#de77ae', 16).setOrigin(0.5);

        // bob.setFlipX(true);
        // bob.setFlipY(true);

        this.graphics = this.add.graphics();

        const cropWidth = 200;
        const cropHeight = 50;

        this.bob.setCrop(0, 0, cropWidth, cropHeight);

        this.offset = this.bob.getTopLeft();

        this.input.on('pointermove', pointer =>
        {

            this.bob.setCrop(
                (pointer.x - this.offset.x) - cropWidth / 2,
                (pointer.y - this.offset.y) - cropHeight / 2,
                cropWidth,
                cropHeight
            );
        });
    }

    update ()
    {
        this.graphics.clear();
        this.graphics.lineStyle(1, 0x00ff00);
        this.graphics.strokeRect(this.offset.x + this.bob._crop.x, this.offset.y + this.bob._crop.y, this.bob._crop.width, this.bob._crop.height);
    }
}

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

const game = new Phaser.Game(config);

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

В методе create() сцены мы создаём два идентичных текстовых объекта. Первый, с низкой прозрачностью, служит фоновым шаблоном, чтобы пользователь видел полный текст. Второй — это основной объект, к которому будет применено кадрирование.

Обратите внимание на использование setOrigin(0.5) для центрирования текста относительно его координат. Это важно для последующих расчётов позиции кадрирования.

const dull = this.add.text(400, 300, 'Phaser 3\nText Crop\nHell Yeah!', { fontFamily: 'Arial Black', fontSize: 74, color: '#c51b7d', align: 'center' }).setStroke('#de77ae', 16);
dull.setAlpha(0.15).setOrigin(0.5);

this.bob = this.add.text(400, 300, 'Phaser 3\nText Crop\nHell Yeah!', { fontFamily: 'Arial Black', fontSize: 74, color: '#c51b7d', align: 'center' }).setStroke('#de77ae', 16).setOrigin(0.5);

Инициализация кадрирования и графики

Перед началом интерактивной части мы задаём начальные размеры окна кадрирования (кропа) и применяем их к текстовому объекту bob с помощью setCrop(x, y, width, height). Параметры `xиy` задают смещение области от левого верхнего угла исходного текста.

Также создаётся объект Graphics для визуальной отрисовки границ текущей области кадрирования в методе update. Важный шаг — сохранение опорной точки текста через getTopLeft() в свойство offset. Так как мы центрировали текст, его реальные координаты левого верхнего угла отличаются от позиции (400, 300). Этот offset нужен для корректного перевода координат курсора в систему координат текста.

const cropWidth = 200;
const cropHeight = 50;

this.bob.setCrop(0, 0, cropWidth, cropHeight);

this.offset = this.bob.getTopLeft();

this.graphics = this.add.graphics();

Обработка движения курсора

Суть интерактивности заключается в обработчике события pointermove. При каждом движении мыши мы вычисляем новые координаты `xиy` для области кадрирования.

Формула (pointer.x - this.offset.x) - cropWidth / 2 переводит глобальные координаты курсора в локальные координаты текста (вычитая offset.x) и затем центрирует окно кадрирования относительно курсора (вычитая половину ширины окна). Аналогично рассчитывается координата Y. В результате окно кадрирования следует за указателем мыши, сохраняя свои постоянные размеры.

this.input.on('pointermove', pointer => {
    this.bob.setCrop(
        (pointer.x - this.offset.x) - cropWidth / 2,
        (pointer.y - this.offset.y) - cropHeight / 2,
        cropWidth,
        cropHeight
    );
});

Визуализация области кадрирования

Чтобы сделать механизм наглядным, в методе update() каждый кадр рисуется зелёная рамка, точно соответствующая текущей области кадрирования. Сначала графический контекст очищается от предыдущего кадра.

Координаты для прямоугольника рассчитываются сложением смещения текста (offset) и текущих значений кадрирования, хранящихся во внутреннем свойстве _crop объекта текста. Это свойство обновляется при каждом вызове setCrop().

this.graphics.clear();
this.graphics.lineStyle(1, 0x00ff00);
this.graphics.strokeRect(this.offset.x + this.bob._crop.x, this.offset.y + this.bob._crop.y, this.bob._crop.width, this.bob._crop.height);

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

Метод setCrop() предоставляет прямой контроль над видимой частью текста. На основе этого примера можно экспериментировать: создать эффект постепенного набора текста, анимируя ширину кропа, реализовать "линзу" или подсветку для текстовых подсказок, либо сделать интерактивную загадку, где игрок должен "стереть" маску, водя по ней курсором. Попробуйте изменить логику в обработчике pointermove, чтобы область кадрирования не следовала, а, например, прыгала к ближайшей букве.