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

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

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

Живой запуск

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

Исходный код


class DemoB extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'DemoB', active: true });
    }

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

    create ()
    {
        const source = this.textures.get('sonic').source[0].image;
        const canvas = this.textures.createCanvas('pad', 38, 42).source[0].image;
        const ctx = canvas.getContext('2d');

        ctx.drawImage(source, 0, 0);

        const imageData = ctx.getImageData(0, 0, 38, 42);

        let x = 0;
        let y = 0;
        const color = new Phaser.Display.Color();

        for (var i = 0; i < imageData.data.length; i += 4)
        {
            var r = imageData.data[i];
            var g = imageData.data[i + 1];
            var b = imageData.data[i + 2];
            var a = imageData.data[i + 3];

            if (a > 0)
            {
                var dx = 100 + x * 16;
                var dy = 0 + y * 16;

                var image = this.add.image(Phaser.Math.Between(0, 800), Phaser.Math.Between(0, 600), 'pixel').setScale(0);

                color.setTo(r, g, b, a);

                image.setTint(color.color);

                this.tweens.add({
                    targets: image,
                    duration: 2000,
                    x: dx,
                    y: dy,
                    scaleX: 1,
                    scaleY: 1,
                    angle: 360,
                    delay: i / 1500,
                    yoyo: true,
                    repeat: -1,
                    repeatDelay: 6

                });
            }

            x++;

            if (x === 38)
            {
                x = 0;
                y++;
            }
        }

        const cam = this.cameras.main;
        cam.setBackgroundColor('#2d2d2d');

        cam.x = 800;
        cam.y = 0;
    }
}

Подготовка Canvas и чтение данных пикселей

В основе эффекта лежит работа с Canvas API через возможности Phaser. Цель — получить массив данных о цвете каждого пикселя исходного изображения.

В методе create() мы сначала получаем доступ к исходному изображению 'sonic' и создаем на его основе новый холст (canvas) такого же размера.

const source = this.textures.get('sonic').source[0].image;
const canvas = this.textures.createCanvas('pad', 38, 42).source[0].image;
const ctx = canvas.getContext('2d');

Затем на этот холст рисуется исходная картинка. Это необходимо, чтобы впоследствии считать данные пикселей. Метод getImageData возвращает объект ImageData, содержащий одномерный массив data.

ctx.drawImage(source, 0, 0);
const imageData = ctx.getImageData(0, 0, 38, 42);

Массив imageData.data хранит информацию о каналах RGBA (Red, Green, Blue, Alpha) для каждого пикселя подряд. То есть для одного пикселя отводится 4 элемента массива.

Итерация по пикселям и создание спрайтов

Далее в цикле мы проходим по массиву данных, обрабатывая пиксели блоками по 4 элемента.

for (var i = 0; i < imageData.data.length; i += 4)
{
    var r = imageData.data[i];
    var g = imageData.data[i + 1];
    var b = imageData.data[i + 2];
    var a = imageData.data[i + 3];
    // ...
}

Ключевое условие — if (a > 0). Оно проверяет альфа-канал (прозрачность) пикселя. Таким образом, мы создаем спрайты только для непрозрачных пикселей, экономя ресурсы и не загромождая сцену невидимыми объектами.

Для каждого непрозрачного пикселя создается спрайт image из текстуры 'pixel' в случайной позиции на экране и с нулевым масштабом. Его конечная позиция (dx, dy) рассчитывается так, чтобы в итоге он встал на свое место в исходной картинке, образуя мозаику.

var image = this.add.image(Phaser.Math.Between(0, 800), Phaser.Math.Between(0, 600), 'pixel').setScale(0);

Цвет пикселя применяется к спрайту через твин. Класс Phaser.Display.Color используется для удобной работы с цветом, а метод setTint окрашивает спрайт.

color.setTo(r, g, b, a);
image.setTint(color.color);

Анимация с помощью Tween Manager

Сердце эффекта — плавная анимация, создаваемая системой твинов Phaser. Для каждого спрайта-пикселя создается отдельный твин с набором свойств.

this.tweens.add({
    targets: image,
    duration: 2000,
    x: dx,
    y: dy,
    scaleX: 1,
    scaleY: 1,
    angle: 360,
    delay: i / 1500,
    yoyo: true,
    repeat: -1,
    repeatDelay: 6
});

Разберем ключевые параметры: * targets: объект, который будет анимироваться. * x, y: конечные координаты, где пиксель должен занять свое место в сетке. * scaleX, scaleY: анимация от 0 до 1 создает эффект «вырастания» пикселя. * angle: 360: пиксель совершит полный оборот за время анимации. * delay: i / 1500: задержка старта твина, рассчитанная на основе индекса в массиве. Это создает волнообразный, последовательный запуск анимации для всех пикселей, а не единовременный. * yoyo: true: после завершения анимация проиграется в обратном порядке (пиксель «свернется» и вернется в случайную точку). * repeat: -1: бесконечное повторение всей последовательности (вперед-назад). * repeatDelay: пауза между циклами повторения.

Настройка камеры и итоговая сцена

После создания всех анимированных пикселей код настраивает основную камеру сцены.

const cam = this.cameras.main;
cam.setBackgroundColor('#2d2d2d');
cam.x = 800;
cam.y = 0;

Эти строки выполняют две задачи: 1. setBackgroundColor устанавливает темно-серый фон для всей зоны просмотра камеры, что улучшает визуальный контраст и выделяет цветные пиксели. 2. Смещение камеры по `xиy(cam.x = 800`) фактически сдвигает весь viewport. В данном конкретном примере это, скорее всего, сделано для демонстрации в составе более крупного проекта с несколькими камерами, чтобы расположить эту сцену в определенной области экрана. В изолированной сцене это сместит видимую область, и часть пикселей может оказаться за ее границами.

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

Разобранный пример — отличная основа для создания собственных эффектов. Для экспериментов попробуйте изменить текстуру пикселя на круг или звезду, задать конечные позиции не сеткой, а по окружности или случайным образом. Можно управлять не только позицией и масштабом, но и alpha-каналом, создавая эффект постепенного появления. Заменив yoyo и repeat на однократное выполнение, вы получите красивый переход для загрузки уровня, где картинка собирается из разлетающихся частиц.