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

Создание плавных цветовых градиентов — частая задача в разработке игр, будь то изменение настроения сцены, плавное затухание или создание визуальных эффектов. Phaser предоставляет мощный инструментарий для работы с цветом через класс `Phaser.Display.Color`. Однако способ интерполяции между двумя цветами кардинально влияет на итоговый результат. В этой статье мы наглядно сравним, как работают интерполяции в RGB и HSV пространствах, и почему для создания визуально приятных градиентов часто стоит выбирать HSV.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    create ()
    {
        // Two colors, quite separate in hue.
        const red = new Phaser.Display.Color(255, 96, 96);
        const blue = new Phaser.Display.Color(32, 96, 192);

        const steps = 32;

        for (let i = 0; i < steps; i++)
        {
            // RGB interpolation loses saturation in the middle.
            const interRGB = Phaser.Display.Color.Interpolate.ColorWithColor(red, blue, steps, i);

            // HSV interpolation maintains saturation and value.
            const interHSV = Phaser.Display.Color.Interpolate.ColorWithColor(red, blue, steps, i, true);

            // Positive hue interpolation forces hue to avoid the shorter rotation through purple.
            const interHSVIncrease = Phaser.Display.Color.Interpolate.ColorWithColor(red, blue, steps, i, true, 1);

            const x = (i + 1) * (this.scale.width) / (steps + 2);

            this.add.rectangle(x, 180, 32, 32, interRGB.color);
            this.add.rectangle(x, 360, 32, 32, interHSV.color);
            this.add.rectangle(x, 540, 32, 32, interHSVIncrease.color);

            const style = { fontFamily: 'sans-serif', fontSize: 24, fontColor: 0xffffff };
            this.add.text(64, 220, "RGB interpolation", style);
            this.add.text(64, 400, "HSV nearest interpolation", style);
            this.add.text(64, 580, "HSV positive interpolation", style);
        }
    }

}

const config = {
    width: 1280,
    height: 720,
    type: Phaser.CANVAS,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка: создаем цвета и задаем шаги

Вся логика примера находится в методе create() сцены. Первым делом мы создаем два объекта класса Phaser.Display.Color — красный и синий. Эти цвета достаточно далеки друг от друга по тону (hue), что сделает различия между методами интерполяции более заметными.

Затем мы определяем количество шагов steps, которые хотим получить при переходе от одного цвета к другому. Чем больше шагов, тем плавнее будет градиент.

const red = new Phaser.Display.Color(255, 96, 96);
const blue = new Phaser.Display.Color(32, 96, 192);

const steps = 32;

Интерполяция в пространстве RGB

Самый простой и интуитивно понятный способ — это линейная интерполяция каждого из каналов (Red, Green, Blue) независимо. Эту операцию выполняет статический метод Phaser.Display.Color.Interpolate.ColorWithColor().

При таком подходе мы просто вычисляем промежуточные значения для красного, зеленого и синего каналов. Однако у этого метода есть существенный недостаток: на середине пути между яркими насыщенными цветами часто получается грязный, ненасыщенный оттенок. В нашем примере между красным и синим в середине градиента появится серовато-коричневый цвет.

const interRGB = Phaser.Display.Color.Interpolate.ColorWithColor(red, blue, steps, i);

Интерполяция в пространстве HSV

Пространство HSV (Hue, Saturation, Value — Тон, Насыщенность, Значение) больше соответствует человеческому восприятию цвета. Интерполяция в этом пространстве позволяет сохранить насыщенность и яркость на протяжении всего перехода, что часто выглядит более эстетично.

В методе ColorWithColor() за переход в HSV отвечает четвертый булевый параметр hsv. Установив его в true, мы заставляем метод сначала преобразовать цвета в HSV, провести интерполяцию каждого компонента (H, S, V), а затем преобразовать результат обратно в RGB.

const interHSV = Phaser.Display.Color.Interpolate.ColorWithColor(red, blue, steps, i, true);

Управление направлением изменения тона

У HSV-интерполяции есть нюанс: тон (hue) представляет собой кольцо значений от 0 до 360. Переход от красного (условно, 0°) к синему (240°) можно осуществить двумя путями: через фиолетовый (короткий путь, увеличение значения) или через желтый и зеленый (длинный путь, уменьшение значения).

Пятый параметр метода useShortestPath (по умолчанию true) как раз выбирает кратчайший путь. Но если мы хотим явно задать направление — всегда увеличивать тон, — нужно передать false и указать шестой параметр hsvDirection (1 для увеличения, -1 для уменьшения). Это может быть полезно для создания предсказуемых анимаций.

const interHSVIncrease = Phaser.Display.Color.Interpolate.ColorWithColor(red, blue, steps, i, true, 1);

Визуализация и отрисовка результатов

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

Объект цвета, возвращаемый методом ColorWithColor(), имеет свойство .color, которое содержит числовое значение цвета, готовое для использования в методах отрисовки, таких как this.add.rectangle().

const x = (i + 1) * (this.scale.width) / (steps + 2);

this.add.rectangle(x, 180, 32, 32, interRGB.color);
this.add.rectangle(x, 360, 32, 32, interHSV.color);
this.add.rectangle(x, 540, 32, 32, interHSVIncrease.color);

const style = { fontFamily: 'sans-serif', fontSize: 24, fontColor: 0xffffff };
this.add.text(64, 220, "RGB interpolation", style);
this.add.text(64, 400, "HSV nearest interpolation", style);
this.add.text(64, 580, "HSV positive interpolation", style);

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

Выбор между RGB и HSV интерполяцией — это выбор между простотой и качеством визуального результата. Для плавных переходов фона, анимации освещения или создания гармоничных палитр однозначно побеждает HSV. Для экспериментов попробуйте интерполировать между более чем двумя цветами, создавая сложные градиенты, или анимируйте переход, меняя цвета спрайта во времени, используя полученные значения в setTint.