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

В игровых интерфейсах, меню или терминалах часто нужно показать больше текста, чем помещается на экране. Вместо стандартных HTML-элементов можно создать собственный скроллируемый блок, используя возможности Phaser. Эта статья покажет, как с помощью маски и обработки событий указателя реализовать перетаскивание текста для его прокрутки, что даёт полный контроль над визуальным стилем и поведением элемента.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('tv', 'assets/pics/tvzor-lazur.png');
    }

    create ()
    {
        const content = [
            'The sky above the port was the color of television, tuned to a dead channel.',
            '`It\'s not like I\'m using,\' Case heard someone say, as he shouldered his way ',
            'through the crowd around the door of the Chat. `It\'s like my body\'s developed',
            'this massive drug deficiency.\' It was a Sprawl voice and a Sprawl joke.',
            'The Chatsubo was a bar for professional expatriates; you could drink there for',
            'a week and never hear two words in Japanese.',
            '',
            'Ratz was tending bar, his prosthetic arm jerking monotonously as he filled a tray',
            'of glasses with draft Kirin. He saw Case and smiled, his teeth a webwork of',
            'East European steel and brown decay. Case found a place at the bar, between the',
            'unlikely tan on one of Lonny Zone\'s whores and the crisp naval uniform of a tall',
            'African whose cheekbones were ridged with precise rows of tribal scars. `Wage was',
            'in here early, with two joeboys,\' Ratz said, shoving a draft across the bar with',
            'his good hand. `Maybe some business with you, Case?\'',
            '',
            'Case shrugged. The girl to his right giggled and nudged him.',
            'The bartender\'s smile widened. His ugliness was the stuff of legend. In an age of',
            'affordable beauty, there was something heraldic about his lack of it. The antique',
            'arm whined as he reached for another mug.',
            '',
            '',
            'From Neuromancer by William Gibson'
        ];

        this.add.image(0, 0, 'tv').setOrigin(0);

        const graphics = this.make.graphics();

        // graphics.fillStyle(0xffffff);
        graphics.fillRect(152, 133, 320, 250);

        const mask = new Phaser.Display.Masks.GeometryMask(this, graphics);

        const text = this.add.text(160, 280, content, { fontFamily: 'Arial', color: '#00ff00', wordWrap: { width: 310 } }).setOrigin(0);

        text.setMask(mask);

        //  The rectangle they can 'drag' within
        const zone = this.add.zone(152, 130, 320, 256).setOrigin(0).setInteractive();

        zone.on('pointermove', pointer =>
        {

            if (pointer.isDown)
            {
                text.y += (pointer.velocity.y / 10);

                text.y = Phaser.Math.Clamp(text.y, -400, 300);
            }

        });
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и создание графики для маски

В методе preload загружается фоновое изображение TV. Основная работа происходит в create.

Первым делом создаётся графический объект (Graphics), который будет служить основой для маски. Мы используем его для рисования прямоугольника. Важно: координаты и размеры прямоугольника задаются в мировых координатах и определяют видимую область для нашего текста.

const graphics = this.make.graphics();
graphics.fillRect(152, 133, 320, 250);

Затем на основе этой графики создаётся геометрическая маска (GeometryMask). Маска скрывает всё, что находится за пределами нарисованного прямоугольника.

Добавление текста и применение маски

Текст создаётся с помощью this.add.text. В качестве содержимого передаётся массив строк content. В стилях задаётся шрифт, цвет и важное свойство wordWrap, которое автоматически переносит строки, чтобы текст не выходил за заданную ширину.

const text = this.add.text(160, 280, content, { fontFamily: 'Arial', color: '#00ff00', wordWrap: { width: 310 } }).setOrigin(0);

После создания текстового объекта к нему применяется созданная ранее маска с помощью метода setMask. Теперь текст будет виден только внутри прямоугольной области, заданной в объекте graphics.

text.setMask(mask);

Создание интерактивной зоны для перетаскивания

Чтобы реализовать прокрутку текста, нужно определить область, в которой будет работать перетаскивание. Для этого создаётся интерактивная зона (Zone) с помощью this.add.zone. Её размеры и положение должны соответствовать или быть близкими к видимой области маски.

const zone = this.add.zone(152, 130, 320, 256).setOrigin(0).setInteractive();

Зона слушает событие pointermove. Однако перемещение текста происходит только если кнопка мыши или касание удерживается (проверка pointer.isDown). Это классический паттерн для drag-and-drop.

Логика прокрутки на основе скорости указателя

В обработчике события pointermove находится ядро механики скроллинга. Положение текста по оси Y изменяется на величину, пропорциональную вертикальной скорости указателя (pointer.velocity.y). Деление на 10 — это коэффициент чувствительности, который можно регулировать.

text.y += (pointer.velocity.y / 10);

Сразу после изменения позиции применяется ограничение с помощью Phaser.Math.Clamp. Это не даёт тексту уехать за пределы разумного, оставляя его в заданном вертикальном диапазоне. Параметры -400 и 300 подобраны эмпирически для данного примера.

text.y = Phaser.Math.Clamp(text.y, -400, 300);

Использование скорости указателя, а не просто разницы координат, даёт более естественную инерционную прокрутку.

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

Мы создали кастомный скроллящийся текстовый блок, используя связку маски, текстового объекта и интерактивной зоны. Это мощная и гибкая основа. Для экспериментов попробуйте: изменить форму маски на круглую, добавить инерцию после отпускавания кнопки мыши, реализовать скролл колёсиком мыши через событие wheel, или привязать положение текста к ползунку (Slider).