О чем этот пример
Создание сложных визуальных эффектов без текстуры — мощный навык для разработчика игр. В этой статье мы разберем официальный пример из Phaser, где логотип движка превращается в гипнотическую трехмерную спираль, используя только векторную графику и математику. Вы научитесь управлять `Graphics` объектом, применять трансформации к канвасу и создавать плавные анимации через синусоидальные функции, что пригодится для визуализаций, спецэффектов или стилизованных меню.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
let s;
let r;
let logos;
let alpha;
let colors;
let thickness;
let go = false;
let temp = { t: 0 };
class Example extends Phaser.Scene
{
graphics;
create ()
{
this.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);
}
// colors = [ 0x650a05, 0xa00d05, 0xcd1106, 0xf53719, 0xf25520, 0xf26f21, 0xf49214, 0xf6a90a, 0xfad400, 0xfef700, 0xffff45, 0xffffc4, 0xffffff ];
r = 0;
s = [];
logos = 13;
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: [temp],
t: 1,
duration: 100,
repeat: -1,
onRepeat: () => {
Phaser.Utils.Array.RotateRight(colors);
},
})
}
});
}
update ()
{
this.graphics.clear();
r += 0.01;
let scale = 0.9 - (logos * 0.01);
for (let i = 0; i < logos; i++)
{
// 3D slant :)
// drawLogo(colors[i], -380 + (i * 2), -100 + (i * 2), scale, s[i]);
// drawLogo(colors[i], -380, -100, scale, s[i]);
// drawLogo(colors[i], -380, -100 + ((i * 2) * Math.sin(r * 2)), scale, s[i]);
this.drawLogo(colors[i], -380 + ((i * 2) * Math.sin(r * 2)), -100 + ((i * 2) * Math.cos(r * 2)), scale, s[i]);
if (go)
{
s[i] = Math.sin(r / 2);
}
// s[i] = Math.sin(r * i) / 16;
// s[i] = Math.sin(i) * r / 8;
// s[i] = r + (i * 0.01);
// s[i] += (Math.sin(r) * Math.sin(i)) / 128;
// s[i] += (Math.sin(13 - i) / 1024);
// s[i] += (0.002 * (0.25 * (i + 1) + 0.75 * (13 - i)));
scale += 0.01;
}
}
drawLogo (color, x, y, scale, rot)
{
// var thickness = 10;
// var alpha = 1;
this.graphics.lineStyle(thickness, color, 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;
this.graphics.save();
this.graphics.translateCanvas(400, 300);
this.graphics.scaleCanvas(scale, scale);
this.graphics.rotateCanvas(rot);
this.graphics.beginPath();
// P
this.graphics.moveTo(x, top);
this.graphics.lineTo(x + w, top);
this.graphics.lineTo(x + w, mid);
this.graphics.lineTo(x, mid);
this.graphics.lineTo(x, bot);
// H
x += w + s;
this.graphics.moveTo(x, top);
this.graphics.lineTo(x, bot);
this.graphics.moveTo(x, mid);
this.graphics.lineTo(x + w, mid);
this.graphics.moveTo(x + w, top);
this.graphics.lineTo(x + w, bot);
// A
x += w + s;
this.graphics.moveTo(x, bot);
this.graphics.lineTo(x + (w * 0.75), top);
this.graphics.lineTo(x + (w * 0.75) + (w * 0.75), bot);
// S
x += ((w * 0.75) * 2) + s;
this.graphics.moveTo(x + w, top);
this.graphics.lineTo(x, top);
this.graphics.lineTo(x, mid);
this.graphics.lineTo(x + w, mid);
this.graphics.lineTo(x + w, bot);
this.graphics.lineTo(x, bot);
// E
x += w + s;
this.graphics.moveTo(x + w, top);
this.graphics.lineTo(x, top);
this.graphics.lineTo(x, bot);
this.graphics.lineTo(x + w, bot);
this.graphics.moveTo(x, mid);
this.graphics.lineTo(x + w, mid);
// R
x += w + s;
this.graphics.moveTo(x, top);
this.graphics.lineTo(x + w, top);
this.graphics.lineTo(x + w, mid);
this.graphics.lineTo(x, mid);
this.graphics.lineTo(x, bot);
this.graphics.moveTo(x, mid);
this.graphics.lineTo(x + w, bot);
this.graphics.strokePath();
this.graphics.restore();
}
}
const config = {
width: 800,
height: 600,
type: Phaser.AUTO,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка: создание Graphics и палитры цветов
Вся визуализация в этом примере строится на одном объекте Graphics. Это низкоуровневый инструмент Phaser для рисования линий, фигур и заполнений.
Сначала мы получаем полный цветовой круг HSV (Hue, Saturation, Value) — это массив из 360 цветовых объектов. Для экономии ресурсов мы берем каждый четвертый цвет, создавая более компактную палитру colors. Инициализируются глобальные переменные для радиуса анимации `r, массива поворотовs` для каждого слоя логотипа и других параметров.
this.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);
}
Анимация через update: математика движения
В методе update() происходит вся магия. На каждом кадре мы очищаем предыдущий рисунок, увеличиваем глобальный счётчик `rи в цикле рисуем 13 слоёв логотипа (logos). Ключевой момент — вычисление позиции для каждого слоя с помощьюMath.sinиMath.cosотr`. Это создает круговое движение слоев в 2D-плоскости, имитирующее 3D-спираль.
Переменная scale плавно увеличивается для каждого последующего слоя, создавая перспективу. Флаг go, активируемый через событие таймера, запускает анимацию вращения каждого логотипа вокруг своей оси, записывая в s[i] значение синуса.
r += 0.01;
let scale = 0.9 - (logos * 0.01);
for (let i = 0; i < logos; i++) {
this.drawLogo(colors[i], -380 + ((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 — сердце примера. Он рисует буквы "PHASER" векторными линиями. Важно не то, как выводятся конкретные линии (это просто координаты), а как применяются трансформации.
Перед рисованием мы сохраняем состояние канваса с помощью graphics.save(). Затем применяем три ключевые трансформации: перенос (translateCanvas) в центр экрана, масштабирование (scaleCanvas) и поворот (rotateCanvas). После отрисовки контура линиями и вызова strokePath() состояние канваса восстанавливается через graphics.restore(). Это позволяет изолировать трансформации для каждого слоя.
this.graphics.save();
this.graphics.translateCanvas(400, 300);
this.graphics.scaleCanvas(scale, scale);
this.graphics.rotateCanvas(rot);
// ... рисование линий ...
this.graphics.strokePath();
this.graphics.restore();
Таймеры и твины: управление временем
Пример использует два временных события. Первое, через 5 секунд после старта, устанавливает флаг go = true, запуская анимацию вращения логотипов. Второе событие, через 14 секунд, создает бесконечный твин, который циклически сдвигает массив цветов colors вправо. Это приводит к плавной смене цветовой палитры у всей спирали.
Обратите внимание на использование временного объекта temp как цели для твина — это распространённый трюк, когда нужно выполнять действие на каждом повторении (onRepeat).
this.time.addEvent({
delay: 5000,
callback: () => {
r = 0;
go = true;
}
});
this.tweens.add({
targets: [temp],
t: 1,
duration: 100,
repeat: -1,
onRepeat: () => {
Phaser.Utils.Array.RotateRight(colors);
},
})
Что попробовать дальше
Вы разобрали, как создать сложную геометрическую анимацию, используя лишь Graphics, базовую тригонометрию и систему трансформаций Phaser. Этот подход не зависит от текстур и разрешения, что делает его очень производительным. Для экспериментов попробуйте: изменить форму логотипа на свою, заменить синусоидальные функции на другие (например, шум Перлина), управлять толщиной линии thickness или альфой alpha в реальном времени, добавить интерактивность — менять направление вращения по клику мыши.
