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

При создании инструментов для рисования в играх или интерактивных приложениях часто возникает проблема: движение курсора мыши происходит дискретно. Это приводит к прерывистым, пунктирным линиям. В этой статье мы разберем пример из Phaser, который решает эту задачу, используя Render Texture и интерполяцию позиции указателя. Вы научитесь создавать плавные линии, которые идеально повторяют траекторию движения мыши, даже если она была очень быстрой.

Версия 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('brush', 'assets/sprites/brush2.png');
    }

    create ()
    {
        const rt = this.add.renderTexture(400, 300, 800, 600);

        const brush = this.textures.getFrame('brush');

        const hsv = Phaser.Display.Color.HSVColorWheel();
        let i = 0;

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

            if (pointer.isDown)
            {
                const points = pointer.getInterpolatedPosition(30);

                points.forEach(p =>
                {

                    rt.draw(brush, p.x - 16, p.y - 16, 1, hsv[i].color);

                });

                rt.render();

                i++;

                if (i === 360)
                {
                    i = 0;
                }
            }

        }, this);

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

            rt.draw(brush, pointer.x - 16, pointer.y - 16, 1, hsv[i].color);

            rt.render();

            i++;

            if (i === 360)
            {
                i = 0;
            }

        }, this);

    }
}

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

const game = new Phaser.Game(config);

Инициализация сцены и загрузка ресурсов

Вся логика примера находится в классе сцены. На этапе preload загружается одно изображение — текстура кисти.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('brush', 'assets/sprites/brush2.png');
}

Здесь используется метод setBaseURL для указания базового пути к ресурсам и load.image для загрузки спрайта кисти.

Создание холста и подготовка данных

В методе create мы подготавливаем все необходимое для рисования. Ключевой объект — RenderTexture, который выступает в роли нашего динамического холста.

create ()
{
    const rt = this.add.renderTexture(400, 300, 800, 600);
    const brush = this.textures.getFrame('brush');
    const hsv = Phaser.Display.Color.HSVColorWheel();
    let i = 0;
    // ... обработчики событий
}

* rt — это RenderTexture размером 800x600, размещенный в центре (координаты 400, 300). * brush — это кадр (Frame) загруженной текстуры, который мы будем многократно отрисовывать. * hsv — массив из 360 цветов, представляющий полный цветовой круг HSV. Это позволяет легко менять цвет кисти при каждом новом штрихе. * `i— индекс для перебора цветов из массиваhsv`.

Магия плавных линий: getInterpolatedPosition

Сердце примера — обработчик события движения указателя (pointermove). Вместо того чтобы рисовать кисть только в текущей позиции курсора, мы получаем массив интерполированных позиций.

this.input.on('pointermove', pointer =>
{
    if (pointer.isDown)
    {
        const points = pointer.getInterpolatedPosition(30);
        points.forEach(p =>
        {
            rt.draw(brush, p.x - 16, p.y - 16, 1, hsv[i].color);
        });
        rt.render();
        i++;
        if (i === 360) { i = 0; }
    }
}, this);

Метод pointer.getInterpolatedPosition(30) возвращает массив из 30 точек, равномерно распределенных между предыдущей и текущей позицией курсора. Это сглаживает линию, заполняя пропуски, которые возникают при быстром движении. Каждая точка массива points используется для отрисовки кисти на RenderTexture с помощью метода rt.draw. Смещение на -16 пикселя по осям X и Y центрует кисть относительно курсора (предполагается, что размер кисти 32x32). После отрисовки всех точек вызывается rt.render() для обновления текстуры. Индекс цвета `i` увеличивается после каждого события движения.

Обработка начала рисования

Для корректного начала линии нужен отдельный обработчик события нажатия кнопки мыши (pointerdown).

this.input.on('pointerdown', pointer =>
{
    rt.draw(brush, pointer.x - 16, pointer.y - 16, 1, hsv[i].color);
    rt.render();
    i++;
    if (i === 360) { i = 0; }
}, this);

Он ставит первую точку кисти в месте клика. Без этого обработчика линия начиналась бы только с первого события pointermove, что создавало бы небольшую задержку и пропуск в начале рисования.

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

Использование RenderTexture в паре с getInterpolatedPosition — мощный и эффективный способ реализовать плавное рисование в Phaser. Этот подход можно адаптировать для создания кистей разного размера, формы или прозрачности, а также для реализации ластика. Для экспериментов попробуйте изменить параметр в getInterpolatedPosition (например, на 10 или 50), чтобы увидеть, как меняется плотность точек и плавность линии, или замените цветовой круг на палитру с градиентом.