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

Работа с цветом — мощный инструмент для создания визуальных эффектов в играх. Прямое манипулирование пикселями текстуры позволяет динамически менять её внешний вид без загрузки дополнительных ассетов. В этой статье мы разберем, как в Phaser 3 можно программно сдвигать цветовой тон (hue) текстуры, используя Canvas API и встроенные функции конвертации цветовых моделей. Этот метод полезен для создания эффектов заклинаний, смены времени суток, индикации состояния персонажа или генерации вариаций врагов из одной базовой текстуры.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    originalTexture;
    newTexture;
    context;

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

    create ()
    {
        this.originalTexture = this.textures.get('dude').getSourceImage();

        this.newTexture = this.textures.createCanvas('dudeNew', this.originalTexture.width, this.originalTexture.height);

        this.context = this.newTexture.getSourceImage().getContext('2d');

        this.context.drawImage(this.originalTexture, 0, 0);

        this.add.image(100, 100, 'dude');
        this.add.image(200, 100, 'dudeNew');

        this.time.addEvent({ delay: 500, callback: () => this.hueShift(), loop: true });
    }

    hueShift ()
    {
        const pixels = this.context.getImageData(0, 0, this.originalTexture.width, this.originalTexture.height);

        for (let i = 0; i < pixels.data.length / 4; i++)
        {
            this.processPixel(pixels.data, i * 4, 0.1);
        }

        this.context.putImageData(pixels, 0, 0);

        this.newTexture.refresh();
    }

    processPixel (data, index, deltahue)
    {
        const r = data[index];
        const g = data[index + 1];
        const b = data[index + 2];

        const hsv = Phaser.Display.Color.RGBToHSV(r, g, b);

        const h = hsv.h + deltahue;

        const rgb = Phaser.Display.Color.HSVToRGB(h, hsv.s, hsv.v);

        data[index] = rgb.r;
        data[index + 1] = rgb.g;
        data[index + 2] = rgb.b;
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и загрузка текстур

Вся работа происходит в классе сцены. На этапе preload загружается исходное изображение. В методе create мы получаем доступ к загруженной текстуре и создаем новую, пустую текстуру на основе Canvas, которая будет служить целевой для наших манипуляций.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('dude', 'assets/sprites/phaser-dude.png');
}
create ()
{
    // Получаем исходное HTMLImageElement из текстуры 'dude'
    this.originalTexture = this.textures.get('dude').getSourceImage();

    // Создаем новую текстуру типа Canvas с тем же размером
    this.newTexture = this.textures.createCanvas('dudeNew', this.originalTexture.width, this.originalTexture.height);

    // Получаем 2D-контекст canvas новой текстуры для рисования
    this.context = this.newTexture.getSourceImage().getContext('2d');
}

Копирование и периодическое обновление

После получения контекста мы рисуем на нём исходное изображение. Затем добавляем на сцену два спрайта: один с оригинальной текстурой, другой — с нашей новой, пока ещё неизменённой. Чтобы увидеть анимацию, мы настраиваем таймер, который будет регулярно вызывать функцию сдвига оттенка.

create ()
{
    // ... предыдущий код

    // Копируем пиксели оригинальной текстуры на canvas новой текстуры
    this.context.drawImage(this.originalTexture, 0, 0);

    // Добавляем оба спрайта на сцену для сравнения
    this.add.image(100, 100, 'dude');
    this.add.image(200, 100, 'dudeNew');

    // Запускаем таймер, который каждые 500 мс вызывает hueShift()
    this.time.addEvent({ delay: 500, callback: () => this.hueShift(), loop: true });
}

Манипуляция пикселями: сдвиг в HSV

Ключевой метод hueShift работает с данными пикселей. Мы получаем массив ImageData из контекста, проходим по каждому пикселю и изменяем его цвет, после чего записываем измененные данные обратно и обновляем текстуру.

hueShift ()
{
    // Получаем объект ImageData, содержащий массив data (RGBA)
    const pixels = this.context.getImageData(0, 0, this.originalTexture.width, this.originalTexture.height);

    // Проходим по каждому пикселю (4 элемента массива на пиксель: R, G, B, A)
    for (let i = 0; i < pixels.data.length / 4; i++)
    {
        this.processPixel(pixels.data, i * 4, 0.1);
    }

    // Возвращаем измененные пиксели в контекст
    this.context.putImageData(pixels, 0, 0);

    // Сообщаем текстуре Phaser, что её источник (canvas) изменился
    this.newTexture.refresh();
}

Преобразование цвета: от RGB к HSV и обратно

Сам сдвиг цвета проще выполнять в модели HSV (Hue, Saturation, Value), где тон (Hue) — это угол на цветовом круге. Phaser предоставляет встроенные утилиты для конвертации. Функция processPixel извлекает компоненты RGB, преобразует их в HSV, изменяет компоненту Hue, а затем конвертирует обратно в RGB.

processPixel (data, index, deltahue)
{
    // Извлекаем компоненты цвета из массива данных
    const r = data[index];
    const g = data[index + 1];
    const b = data[index + 2];

    // Конвертируем RGB в HSV. Возвращается объект {h, s, v}
    const hsv = Phaser.Display.Color.RGBToHSV(r, g, b);

    // Изменяем тон. Значение h нормализовано (0-1).
    const h = hsv.h + deltahue;

    // Конвертируем модифицированный HSV обратно в RGB
    const rgb = Phaser.Display.Color.HSVToRGB(h, hsv.s, hsv.v);

    // Записываем новые значения компонент обратно в массив данных
    data[index] = rgb.r;
    data[index + 1] = rgb.g;
    data[index + 2] = rgb.b;
    // Компонента Alpha (data[index + 3]) остаётся без изменений
}

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

Вы научились динамически изменять цветовой тон текстуры в Phaser 3, используя низкоуровневый Canvas API и встроенные цветовые утилиты. Этот подход открывает дорогу для множества экспериментов. Попробуйте менять не только тон, но и насыщенность (hsv.s) или значение (hsv.v) для создания эффектов затемнения или выцветания. Можно привязать сдвиг не к таймеру, а к игровым событиям (получение урона, усиление). Также стоит изучить работу с Phaser.Display.Color для других преобразований, например, в градации серого или для применения предустановленных цветовых фильтров.