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

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

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

Живой запуск

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

Исходный код


const T = Math.tan;
const C = Math.cos;
const S = Math.sin;

class Example extends Phaser.Scene
{
    frame = 0;
    time = 0;
    x;
    c;

    create ()
    {
        const canvasTexture = this.textures.createCanvas('dwitter', 1920, 1080);

        this.c = canvasTexture.getSourceImage();
        this.x = this.c.getContext('2d');

        this.add.image(0, 0, 'dwitter').setOrigin(0).setScale(0.5);
    }

    update ()
    {
        this.time = this.frame / 60;

        if (this.time * 60 | this.frame - 1 === 0)
        {
            this.time += 0.000001;
        }

        this.frame++;

        this.u(this.time);
    }

    R (r, g, b, a)
    {
        a = a === undefined ? 1 : a;

        return `rgba(${r|0},${g|0},${b|0},${a})`;
    }

    u (t)
    {
        let c = this.c;
        let x = this.x;
        let i;
        let q;

        c.width |= i = 300;
        x.lineWidth = 0.1;
        while (--i) { q = 19 + S(t / 6) / 28 * i,x.arc(S(q / 3) * i + q * 60,(C(q * S(t / 2)) + 4) * i / 2 + 200,(C(q) * 60 + 200) * S(i / 96),0,7); }
        x.stroke();
    }
}

const config = {
    width: 960,
    height: 540,
    type: Phaser.CANVAS,
    parent: 'phaser-example',
    backgroundColor: '#ffffff',
    scene: Example
};

const game = new Phaser.Game(config);

Создание динамического холста

Ключевой объект для работы — CanvasTexture. Это текстура, источником данных для которой служит HTML Canvas элемент. Его можно создать и модифицировать в реальном времени.

В методе create() создается текстура с именем 'dwitter' и разрешением 1920x1080. Затем мы получаем ссылки на сам элемент <canvas> и его 2D-контекст для рисования. Эта текстура добавляется на сцену как обычное изображение, масштабированное вдвое.

const canvasTexture = this.textures.createCanvas('dwitter', 1920, 1080);

this.c = canvasTexture.getSourceImage();
this.x = this.c.getContext('2d');

this.add.image(0, 0, 'dwitter').setOrigin(0).setScale(0.5);

Игровой цикл и время

Анимация управляется в методе update(), который вызывается каждый кадр. Для плавности анимации, привязанной ко времени, а не к частоте кадров, используется переменная this.time, вычисляемая из номера кадра.

Условный блок if — это технический трюк для предотвращения деления на ноль или других артефактов в самом первом кадре, гарантируя, что this.time никогда не будет абсолютным нулем при использовании в математических функциях.

update ()
{
    this.time = this.frame / 60;

    if (this.time * 60 | this.frame - 1 === 0)
    {
        this.time += 0.000001;
    }

    this.frame++;

    this.u(this.time);
}

Вспомогательная функция для цвета

Метод R() — это удобная утилита для формирования CSS-строки цвета (rgba). Он принимает компоненты красного, зеленого, синего и опционально альфа-канал. Побитовый оператор | 0 используется для быстрого приведения чисел к целочисленному виду.

R (r, g, b, a)
{
    a = a === undefined ? 1 : a;
    return `rgba(${r|0},${g|0},${b|0},${a})`;
}

Сердце визуализации: функция u(t)

Вся магия происходит в функции u(t), где `t— текущее время. Каждый кадр холст очищается (строкаc.width |= i = 300;делает это, присваивая ширину), задается толщина линии, и в цикле рисуются дуги (arc`).

Параметры для каждой дуги (координаты x, y и радиус) вычисляются с помощью комбинации тригонометрических функций (`S— синус,C— косинус) от времениtи переменной циклаi. Это создает сложную интерференционную картину, где каждый элемент цикла вносит свой вклад в итоговый завихряющийся узор. После определения всех путей вызываетсяstroke()` для их отрисовки.

u (t)
{
    let c = this.c;
    let x = this.x;
    let i;
    let q;

    c.width |= i = 300;
    x.lineWidth = 0.1;
    while (--i) { q = 19 + S(t / 6) / 28 * i,x.arc(S(q / 3) * i + q * 60,(C(q * S(t / 2)) + 4) * i / 2 + 200,(C(q) * 60 + 200) * S(i / 96),0,7); }
    x.stroke();
}

Конфигурация игры

Конфиг игры стандартный. Важный момент — type: Phaser.CANVAS. Хотя CanvasTexture будет работать и в WebGL-режиме, использование рендерера Canvas может упростить отладку и обеспечить полную совместимость при прямой работе с 2D-контекстом.

const config = {
    width: 960,
    height: 540,
    type: Phaser.CANVAS,
    parent: 'phaser-example',
    backgroundColor: '#ffffff',
    scene: Example
};

const game = new Phaser.Game(config);

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

CanvasTexture в Phaser — это мост между игровым движком и полной свободой программируемой графики Canvas API. Вы можете создавать не только абстрактные эффекты, как в примере, но и динамические карты, уникальные частицы, шейдероподобные фильтры для 2D или кастомные элементы UI. Для экспериментов попробуйте: изменить математические формулы в u(t), используя this.R() для цветных линий; рисовать не arc, а rect или fillText; привязать параметры анимации к вводу игрока (например, скорости персонажа); или использовать несколько слоев CanvasTexture для композиции сложных сцен.