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

Обрезка изображений (crop) — мощный инструмент в арсенале игрового разработчика. Она позволяет не загружать десятки отдельных текстур, а работать с атласами, вырезая из них нужные области. Однако в Phaser 3 есть нюанс: координаты обрезки рассчитываются относительно внутренней системы отсчёта спрайта, а не глобальных координат сцены. Эта статья на реальном примере из репозитория Phaser показывает, как корректно работать с методом `setCrop`, избегая распространённой ошибки в расчётах. Вы научитесь динамически менять область обрезки изображения в реальном времени (например, при движении курсора) и правильно отрисовывать визуальную рамку вокруг этой области, что полезно для создания интерактивных интерфейсов, мини-карт или эффектов "взгляда через прицел".

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('atlas', 'assets/atlas/megaset-2.png', 'assets/atlas/megaset-2.json');
    }

    create ()
    {
        this.add.image(400, 300, 'atlas', 'hello').setAlpha(0.3);

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

        this.bob = this.add.image(400, 300, 'atlas', 'hello');

        const cropWidth = 200;
        const cropHeight = 100;

        this.bob.setCrop(20, 20, 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.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d88',
    scene: Example
};

const game = new Phaser.Game(config);

Суть проблемы: система координат для cropping

Основная сложность при работе с setCrop() заключается в точке отсчёта. Метод ожидает координаты не относительно левого верхнего угла сцены или даже самого спрайта на сцене, а относительно внутренней текстуры (фрейма) этого спрайта.

Визуализируем: ваш спрайт bob находится на сцене в позиции (400, 300). Но его внутреннее изображение (фрейм hello из атласа) имеет свою собственную систему координат, где точка (0,0) — это левый верхний угол этого фрейма. Метод setCrop(x, y, width, height) работает именно в этой системе.

Поэтому просто передать в него координаты курсора (pointer.x, pointer.y) — ошибка. Нужно перевести глобальные координаты курсора в локальные координаты фрейма спрайта.

// Так делать НЕЛЬЗЯ. Это вызовет непредсказуемую обрезку.
this.bob.setCrop(pointer.x, pointer.y, cropWidth, cropHeight);

Решение: корректный расчёт offset

В предоставленном примере используется верный подход. В момент создания (create) вычисляется и сохраняется точка offset — это глобальные координаты левого верхнего угла *текстурной области* спрайта. Именно от этой точки нужно вести расчёт.

Ключевые строки кода:

// Получаем мировые координаты верхнего левого угла спрайта
this.offset = this.bob.getTopLeft();

// В обработчике движения курсора пересчитываем координаты
this.bob.setCrop(
    (pointer.x - this.offset.x) - cropWidth / 2,
    (pointer.y - this.offset.y) - cropHeight / 2,
    cropWidth,
    cropHeight
);

Алгоритм: 1. pointer.x - this.offset.x: переводим глобальную координату X курсора в локальную для фрейма. 2. - cropWidth / 2: смещаем точку обрезки так, чтобы курсор оказался в центре вырезаемого прямоугольника. Это делает интерфейс более интуитивным. 3. Аналогично для координаты Y.

Таким образом, область обрезки будет следовать за курсором, оставаясь корректно привязанной к изображению спрайта.

Визуализация области crop

Чтобы видеть, какая именно область изображения сейчас обрезана, в методе 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
);
Как это работает:
- `this.bob._crop` — внутренний объект, содержащий текущие параметры обрезки (`x`, `y`, `width`, `height`). Эти значения — локальные для фрейма.
- Чтобы нарисовать рамку на сцене в нужном месте, мы берём мировую точку отсчёта (`this.offset`) и прибавляем к ней локальные координаты обрезки.
- `this.graphics.clear()` вызывается каждый кадр, чтобы старая рамка стиралась перед отрисовкой новой.

**Важно:** Использование внутреннего свойства _crop (с подчёркиванием) указывает на то, что оно не является частью публичного API и может измениться в будущих версиях. В продакшн-коде для получения этих данных стоит использовать публичные геттеры, если они доступны.

Практическое применение и вариации

Понимание механики cropping открывает несколько практических путей для использования:

1. **Интерактивные элементы:** Создание подсветки или "увеличения" части спрайта при наведении. 2. **Анимация появления:** Можно анимировать ширину или высоту обрезки (cropWidth/cropHeight), чтобы изображение "проявлялось" по горизонтали или вертикали. 3. **Маска для диалогового окна:** Использовать обрезку для создания эффекта портрета говорящего персонажа в диалоговом окне, динамически меняя область обрезки в зависимости от эмоции.

Пример анимации появления спрайта сверху вниз:

// В create()
this.bob.setCrop(0, 0, this.bob.width, 0);

// В update() - увеличиваем высоту обрезки до полной
if (this.bob._crop.height < this.bob.height) {
    this.bob.setCrop(0, 0, this.bob.width, this.bob._crop.height + 2);
}

Главное правило — всегда помнить о переводе координат из глобальной системы сцены в локальную систему фрейма спрайта при вызове setCrop().

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

Динамический cropping в Phaser 3 — это не просто обрезка картинки, а инструмент для создания интерактивности и анимации непосредственно в рантайме. Ключ к успеху — правильный расчёт offset через getTopLeft(). Для экспериментов попробуйте заставить область обрезки следовать не за курсором, а за другим движущимся игровым объектом, или создайте эффект "дыхания", циклически меняя размеры обрезанной области. Также стоит изучить связку setCrop с setOrigin, чтобы менять точку отсчёта для обрезки.