О чем этот пример
Создание сложных визуальных эффектов с помощью векторной графики — мощный инструмент в арсенале геймдева. Эта статья разбирает пример динамической анимации логотипа Phaser, где несколько копий рисуются через Graphics API, плавно смещаются и меняют свои свойства. Вы научитесь управлять трансформациями canvas, создавать цветовые палитры и синхронизировать анимации с помощью твинов и таймеров — навыки, полезные для создания меню, заставок или абстрактных фоновых эффектов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
let graphics;
let s;
let r;
let colors;
let go;
let props;
let logos;
class Example extends Phaser.Scene
{
create ()
{
graphics = this.add.graphics();
const hsv = Phaser.Display.Color.HSVColorWheel();
colors = [];
for (let i = 0; i < hsv.length; i += 4)
{
colors.push(hsv[i].color);
}
r = 0;
s = [];
go = false;
logos = 13;
props = {
a: 0,
thickness: 10,
alpha: 1
};
for (let i = 0; i < logos; i++)
{
s.push(0);
}
this.time.addEvent({
delay: 5000,
callback: () => {
r = 0;
go = true;
}
});
this.time.addEvent({
delay: 14000,
callback: () => {
this.tweens.add({
targets: [props],
a: 1,
duration: 500,
repeat: -1,
onRepeat: () => {
Phaser.Utils.Array.RotateRight(colors);
},
})
}
});
this.time.addEvent({
delay: 30000,
callback: () => {
this.tweens.add({
targets: [props],
alpha: 0.1,
duration: 3000,
ease: Phaser.Math.Easing.Sine.InOut,
yoyo: true,
repeat: -1,
repeatDelay: 4
});
}
});
this.time.addEvent({
delay: 22000,
callback: () => {
this.tweens.add({
targets: [props],
thickness: 2,
duration: 6000,
ease: Phaser.Math.Easing.Sine.InOut,
yoyo: true,
repeat: -1,
repeatDelay: 16
});
}
});
}
update ()
{
graphics.clear();
r += 0.015;
let scale = 0.9 - (logos * 0.01);
for (let i = 0; i < logos; i++)
{
this.drawLogo(colors[i], -400 + ((i * 2) * Math.sin(r * 2)), -100 + ((i * 2) * Math.cos(r * 2)), scale, s[i]);
if (go)
{
s[i] = Math.sin(r / 2);
}
scale += 0.01;
}
}
drawLogo (color, x, y, scale, rot)
{
graphics.lineStyle(Math.round(props.thickness), color, props.alpha);
const w = 100;
const h = 200;
const h2 = 100;
const top = y + 0;
const mid = y + 100;
const bot = y + 200;
const s = 30;
graphics.save();
graphics.translateCanvas(400, 300);
graphics.scaleCanvas(scale, scale);
graphics.rotateCanvas(rot);
graphics.beginPath();
// P
graphics.moveTo(x, top);
graphics.lineTo(x + w, top);
graphics.lineTo(x + w, mid);
graphics.lineTo(x, mid);
graphics.lineTo(x, bot);
// H
x += w + s;
graphics.moveTo(x, top);
graphics.lineTo(x, bot);
graphics.moveTo(x, mid);
graphics.lineTo(x + w, mid);
graphics.moveTo(x + w, top);
graphics.lineTo(x + w, bot);
// A
x += w + s;
graphics.moveTo(x, bot);
graphics.lineTo(x + (w * 0.75), top);
graphics.lineTo(x + (w * 0.75) + (w * 0.75), bot);
// S
x += ((w * 0.75) * 2) + s;
graphics.moveTo(x + w, top);
graphics.lineTo(x, top);
graphics.lineTo(x, mid);
graphics.lineTo(x + w, mid);
graphics.lineTo(x + w, bot);
graphics.lineTo(x, bot);
// E
x += w + s;
graphics.moveTo(x + w, top);
graphics.lineTo(x, top);
graphics.lineTo(x, bot);
graphics.lineTo(x + w, bot);
graphics.moveTo(x, mid);
graphics.lineTo(x + w, mid);
// R
x += w + s;
graphics.moveTo(x, top);
graphics.lineTo(x + w, top);
graphics.lineTo(x + w, mid);
graphics.lineTo(x, mid);
graphics.lineTo(x, bot);
graphics.moveTo(x, mid);
graphics.lineTo(x + w, bot);
graphics.strokePath();
graphics.restore();
}
}
const config = {
width: 800,
height: 600,
type: Phaser.AUTO,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и ресурсов
В методе create() инициализируются основные переменные и создаются таймеры для запуска анимаций. Ключевой элемент — graphics, объект Phaser.GameObjects.Graphics, который используется для векторного рисования.
Сначала создаётся цветовая палитра на основе HSV-цветового круга. Мы берём каждый четвёртый цвет, чтобы получить разнообразную, но не слишком частую выборку.
const hsv = Phaser.Display.Color.HSVColorWheel();
colors = [];
for (let i = 0; i < hsv.length; i += 4)
{
colors.push(hsv[i].color);
}
Массив `sхранит значения вращения для каждого из 13 логотипов. Флагgoконтролирует, когда начать анимировать это вращение. Объектpropsцентрализованно хранит общие для всех логотипов свойства:alpha(прозрачность),thickness(толщину линии) и вспомогательное значениеa` для циклической смены цветов.
Управление временем: Таймеры и твины
Эффект построен на последовательном запуске анимаций через this.time.addEvent. Это позволяет создавать сложные сценарии без ручного управления счётчиками в update.
Через 5 секунд после старта включается волнообразное движение логотипов.
this.time.addEvent({
delay: 5000,
callback: () => {
r = 0;
go = true;
}
});
Через 14 секунд запускается твин, который циклически меняет значение props.a от 0 до 1. На каждом повторении (onRepeat) цвета в палитре сдвигаются вправо, создавая эффект бегущей волны цвета.
onRepeat: () => {
Phaser.Utils.Array.RotateRight(colors);
},
Последующие таймеры (через 22 и 30 секунд) запускают твины для анимации толщины линии и её прозрачности (alpha). Использование yoyo: true и repeat: -1 делает эти анимации бесконечно повторяющимися и возвращающимися.
Цикл отрисовки и расчёт позиций
В методе update() происходит очистка холста и перерасчёт позиций для каждого кадра. Переменная `r` выступает в роли общего счётчика времени для анимации.
graphics.clear();
r += 0.015;
В цикле для каждого из 13 логотипов вычисляется его позиция и масштаб. Позиция (x, y) рассчитывается с помощью Math.sin и Math.cos от значения `r, что создаёт круговое или волнообразное смещение. Масштаб (scale`) каждого следующего логотипа немного увеличивается.
this.drawLogo(colors[i], -400 + ((i * 2) * Math.sin(r * 2)), -100 + ((i * 2) * Math.cos(r * 2)), scale, s[i]);
Если флаг go установлен в true, значение вращения s[i] для каждого логотипа привязывается к синусу от r/2, создавая плавные качающиеся движения.
Рисование логотипа: трансформации Canvas
Метод drawLogo — сердце примера. Он рисует логотип "PHASER" по точкам, используя текущий стиль линии из props. Важно: все трансформации (смещение, масштаб, поворот) применяются через методы Graphics API, которые являются обёртками над native Canvas API.
Перед рисованием состояние canvas сохраняется, чтобы трансформации не влияли на другие объекты.
graphics.save();
graphics.translateCanvas(400, 300);
graphics.scaleCanvas(scale, scale);
graphics.rotateCanvas(rot);
После save() выполняются три ключевые трансформации:
1. translateCanvas(400, 300) — смещает начало координат в центр экрана.
2. scaleCanvas(scale, scale) — применяет расчитанный масштаб.
3. rotateCanvas(rot) — поворачивает весь последующий рисунок.
Затем, используя moveTo и lineTo, отрисовываются буквы. Координаты рассчитываются относительно переданных `xиy, которые уже анимированы вupdate(). После завершения путь обводится (strokePath()), и оригинальное состояние canvas восстанавливается (restore()`).
Что попробовать дальше
Пример демонстрирует, как, комбинируя простые инструменты Phaser — Graphics API, твины и таймеры — можно создавать сложные и визуально привлекательные динамические композиции. Для экспериментов попробуйте: изменить алгоритм движения логотипов с кругового на, например, эллиптический; анимировать не все свойства сразу, а в случайном порядке; заменить рисование линий на заполненные пути (fillPath); или использовать текстуру для обводки вместо сплошного цвета.
