Trionn был создан как исследование того, насколько далеко можно продвинуть веб-сайт студии, если анимацию, WebGL и системы взаимодействия рассматривать как единое целое. После нескольких месяцев экспериментов и итераций конечный результат объединяет GSAP, Three.js, Lenis и пользовательское взаимодействие с веб-аудио в адаптивный цифровой интерфейс, где каждый раздел управляется собственной тщательно продуманной системой.
Проект развивался через множество концепций, прежде чем достиг своего окончательного направления. Интерактивный герой, повествование с помощью прокрутки, процедурная графика и эффекты в реальном времени не планировались как изолированные функции, а постепенно превратились в связанную анимационную структуру.
Созданный за четыре месяца, сайт получил признание от FWA, GSAP, Orpetron, CSS Design Awards, Web Design Awards, CSS Winner и нескольких международных галерей дизайна. Что еще более важно, этот процесс выявил ряд технических проблем, связанных с производительностью, синхронизацией, рендерингом и дизайном взаимодействия.
В этом тематическом исследовании мы рассмотрим архитектуру Trionn, разберем системы анимации, методы WebGL, стратегии оптимизации и шаблоны кода, используемые для воплощения опыта в жизнь.
Технический обзор
Создание сайта с таким уровнем анимации означало баланс между творческой гибкостью и производительностью. Каждая часть стека несет четкую ответственность: от управления анимацией и взаимодействием с прокруткой до рендеринга сцен WebGL и генерации звука в реальном времени.
Основные технологии, использованные в проекте:
- ГСАП + @gsap/реагировать для временных шкал, переходов страниц и анимации на уровне компонентов.
- Триггер прокрутки для открытий с помощью прокрутки, закрепленных разделов и очищенных последовательностей.
- Разделить текст для многократного использования текстовых анимаций на основе символов, слов и строк.
- Три.js для символа героя, раздела «Услуги» и интерактивной рабочей сетки.
- Ленис для плавной прокрутки, синхронизированной с GSAP.
- API веб-аудио для создания интерактивных звуковых эффектов во время выполнения.
- Next.js и Реагировать в качестве основы приложения.
- CSS попутного ветра для укладки.
- Свипер для каруселей отзывов и наград.
GSAP находится в центре системы анимации. Переходы страниц, последовательности прокрутки, закрепленные разделы и анимация на уровне компонентов — все это построено на временных шкалах GSAP и управляется с помощью useGSAPчто упрощает настройку и очистку при монтировании и отключении компонентов в Next.js.
ScrollTrigger обрабатывает большую часть взаимодействий при прокрутке сайта, от закрепленных разделов повествования до просматриваемой анимации и эффектов раскрытия. Мы также во многом полагаемся на gsap.matchMedia() поэтому макеты для настольных компьютеров и мобильных устройств могут иметь собственную логику анимации вместо использования одних и тех же значений.
Для текстовой анимации мы создали многоразовый BlurTextReveal компонент с использованием SplitText. Он поддерживает анимацию на основе символов, слов и строк, одновременно централизуя обработку замедленного движения, очистку слоев графического процессора и обновление ScrollTrigger вместо решения этих проблем для каждого отдельного заголовка.
Three.js обеспечивает настраиваемый интерфейс WebGL на сайте. Мы решили не использовать React Three Fiber, потому что нам нужен был прямой контроль над общим циклом рендеринга, управлением ресурсами и индивидуально анимированными панелями сетки главного символа.
Лениса везут прямо из gsap.tickerсохраняя синхронизацию прокрутки с помощью ScrollTrigger по всему сайту.
Интерактивные звуковые эффекты, в том числе эффекты наведения героя, взрыва и сварки, генерируются во время выполнения с помощью API веб-аудио вместо использования предварительно записанных аудиофайлов.
Раздел «Герой»
Герой значительно эволюционировал на протяжении проекта. Он объединяет WebGL, GSAP, SplitText и API веб-аудио в единую систему взаимодействия, что делает его одной из наиболее технически сложных частей сайта.
Герой состоит из двух слоев, находящихся в одном состоянии. Фон представляет собой одну сцену Three.js (useTrionnSymbolScene.ts), отвечающий за символ бренда, включая его холостой ход, магнитное наведение, взаимодействие с удержанием для взрыва и эффекты сварочной искры. Все эти взаимодействия способствуют созданию единого explodeAmt значение, которое определяет, насколько далеко разделяются панели символа. Независимо от того, прокручивает ли пользователь, наводит курсор или удерживает кнопку мыши, каждое взаимодействие обновляет одно и то же значение, позволяя переходам между состояниями выглядеть плавными и непрерывными.
Передний план состоит из стандартных элементов DOM — заголовка, вращающегося слова и подсказки по статистике — анимированных с помощью GSAP и SplitText. Использование обычного HTML сохраняет доступность контента, mix-blend-mode: difference гарантирует, что он останется читаемым на холсте WebGL.
Оба слоя синхронизируются через общий transitionReady флаг. Анимация не начинается до тех пор, пока не завершится переход страницы, а некритическая работа откладывается с помощью requestIdleCallback чтобы избежать конкуренции с начальной загрузкой страницы.
Раскрытие заголовка героя
При загрузке страницы главный заголовок («Создано для») анимируется по одному персонажу за раз, используя поэтапный переход от размытия к резкости. Вместо простого появления каждый персонаж постепенно становится в фокусе, создавая более динамичное введение на страницу.
// components/Sections/Home/Banner.tsx — usage
// components/TextAnimation/BlurTextReveal.tsx — the engine behind it
const split = new SplitText(textRef.current, {
type: "chars, words, lines",
smartWrap: true,
});
const targets = split.chars; // animationType === "chars"
gsap.set([textRef.current, targets], {
autoAlpha: 0,
filter: "blur(12px)",
willChange: "filter, opacity", // promote to its own GPU layer only while animating
});
const tl = gsap.timeline({
paused: manual,
});
tl.to(textRef.current, {
autoAlpha: 1,
filter: "blur(0px)",
duration: 0.5,
}, delay)
.to(targets, {
autoAlpha: 1,
filter: "blur(0px)",
duration: 0.8,
stagger: {
each: 0.08,
from: "random",
}, // characters settle out of order
ease: "power2.out",
}, delay);
С использованием filter: blur() наряду с непрозрачностью создается впечатление, что текст становится в фокусе, а не просто исчезает. После завершения анимации will-change удаляется, поэтому текст больше не занимает собственный слой графического процессора. Одинаковый BlurTextReveal Компонент повторно используется по всему сайту для вращающегося слова и подсказки по статистике с различными настройками анимации.
Символ героя: состояние бездействия
В режиме ожидания главный символ непрерывно вращается, в то время как каждая из трех его частей следует легким синусоидальным движениям с независимым сдвигом фазы. Это предотвращает ощущение идеальной синхронизации анимации и придает символу более органичное ощущение движения.
// hooks/useTrionnSymbolScene.ts — per-frame update loop
// Auto-rotate: a constant rotational drift, eased toward the mouse position
if (!st.dragging) {
st.rotY += prefersReducedMotion ? 0.0015 : 0.0042; // base spin speed
st.rotX = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, st.rotX));
group.rotation.x +=
(st.rotX + mouse.y * 0.22 - group.rotation.x) * 0.06; // eased lerp
group.rotation.y +=
(st.rotY + mouse.x * 0.22 - group.rotation.y) * 0.06;
}
// Per-panel ambient drift — each of the 3 arms gets its own phase offset
// so the whole symbol doesn't breathe in lockstep
particles.forEach((p) => {
const phase = p.shapeIdx * (Math.PI * 2 / 3); // 0°, 120°, 240°
const armDriftX =
Math.sin(t * 0.4 + phase) * 0.012 * (1 - explodeAmt);
const armDriftY =
Math.cos(t * 0.35 + phase) * 0.008 * (1 - explodeAmt);
const armDriftZ =
Math.sin(t * 0.3 + phase * 1.5) * 0.006 * (1 - explodeAmt);
// ...position += drift, scaled down to 0 the moment any explode/hover state kicks in
});
Когда prefersReducedMotion включен, скорость вращения снижается, а не отключается полностью. Движение мыши применяется посредством линейной интерполяции (lerp), придавая символу плавный, магнитный вид, а не прямо совпадающий с курсором. Окружающий дрейф также масштабируется (1 - explodeAmt)позволяя ему естественным образом исчезнуть по мере того, как другие состояния взаимодействия вступят во владение.
Символ героя: Магнитное наведение
Когда курсор перемещается над символом, панель под ним ненадолго «заряжается», становясь ярче и отражающей свет, а при первом попадании курсора на эту панель раздается короткий звуковой сигнал. Обнаружение наведения выполняется с помощью raycasting вместо CSS, что позволяет взаимодействовать с фактической трехмерной геометрией символа при его вращении.
// hooks/useTrionnSymbolScene.ts — hover detection via raycasting
const raycaster = new THREE.Raycaster();
// Per frame: only check for hover when the symbol is fully assembled
// (not mid-explode, not scrolled away, not in the intro animation)
if (
st.mouseScreenX !== -9999 &&
st.scrollProgress !p.isEdge)
.map((p) => p.mesh as THREE.Mesh),
false,
);
const nowHit = hits.length > 0 ? hits[0].object : null;
if (nowHit !== st.hoveredMesh) {
if (nowHit) {
const hm = nowHit as THREE.Mesh & {
_flash?: number;
_flashActive?: boolean;
};
hm._flash = 1.0; // triggers the charge-up below
hm._flashActive = true;
audio.playHoverBeep(); // only fires on a new panel, not every frame
}
st.hoveredMesh = nowHit;
}
}
// Elsewhere: decay the flash and ramp the material toward its "charged" look
mesh._flash = (mesh._flash || 0) * 0.92; // exponential decay each frame
const f = mesh._flash;
mat.envMapIntensity = 3.0 + f * 1.6; // brighter reflections
mat.clearcoatRoughness = Math.max(0.01, 0.05 - f * 0.035);
mat.transmission = 0.35 + f * 0.32; // more "glassy"
Raycasting против геометрии символа гарантирует, что эффект наведения повторяет его фактическую форму и вращение. Яркость каждой панели затухает независимо, используя простое экспоненциальное затухание, что позволяет избежать накладных расходов на создание отдельной анимации GSAP для каждой сетки.
Линии героя: Эффект сварочной искры
Три направляющие линии анимируются наружу от символа при загрузке страницы. Как только анимация завершена, наведение курсора на любую из линий вызывает короткую вспышку сварочных искр, которые направляются к одной или двум оставшимся линиям, подкрепляя подсказку героя: «Осмелитесь ⚡ прикоснуться к линиям».
// hooks/useTrionnSymbolScene.ts
// Sparks are only enabled once the guide lines have finished drawing
const baseLinesReadyForSpark =
inS1 &&
undrawAmt s.prog >= 0.995);
if (baseLinesReadyForSpark) {
// Hit-test the mouse against all 3 line paths (14px tolerance)
const allLinePts = [ptsL, ptsR, ptsB];
let hitResult: { x: number; y: number } | null = null;
let hitLineIdx = -1;
for (let li = 0; li 0) {
const wp = unproj2(hitResult.x, hitResult.y); // screen → world space
// Pick 1–2 other lines as targets
const otherIdxs = [0, 1, 2].filter((i) => i !== hitLineIdx);
const count = Math.random() > 0.5 ? 1 : 2;
const targetIdxs = otherIdxs
.sort(() => Math.random() - 0.5)
.slice(0, count);
const nearWpts = targetIdxs.map((li) => {
// Find the closest point on the target line to the hit position
const pts = allLinePts[li];
let bestPt: LinePt | null = null;
let bestD = Infinity;
for (const pt of pts) {
const dd =
(pt.x - hitResult!.x) ** 2 +
(pt.y - hitResult!.y) ** 2;
if (dd
Проверка «готовности» гарантирует, что эффект сварки будет включен только после завершения рисования всех трех направляющих линий. sparkWasAway Флаг запускает новую вспышку только тогда, когда курсор входит в строку, предотвращая непрерывный поток искр при наведении. Каждая очередь немного различается, со случайным количеством болтов и случайно выбранными целевыми линиями, поэтому взаимодействие никогда не происходит одинаково дважды.
Генерация болтов также ограничена по скорости с помощью weldCooldownпри котором каждый болт находится на расстоянии 0,04–0,10 секунды независимо от частоты кадров. Эффект свечения создается путем наслаивания THREE.Line геометрии, обеспечивая желаемый вид без затрат на постобработку блума.
Сами направляющие линии визуализируются в закадровом 2D. который затем используется в качестве текстуры в сцене Three.js. Это позволяет эффекту сварки выполнять облегченное 2D-тестирование на попадание по координатам пространства холста вместо рейкастинга по 3D-геометрии. Поскольку эффект ведет себя скорее как система частиц, чем как анимация пользовательского интерфейса, его время определяется простыми счетчиками, а не временными шкалами GSAP. Звук искры, синтезированный с помощью API веб-аудио, воспроизводится один раз за пакет, а не один раз за выстрел, чтобы предотвратить наложение звука.
Символ героя: Удержание на случай взрыва
Нажатие и удержание символа героя запускает многоэтапное взаимодействие. Соседние элементы интерфейса, включая навигацию и заголовки, начинают вибрировать по мере накопления заряда. Примерно через полсекунды символ распадается на отдельные панели, каждая из которых следует своей траектории и вращению, в то время как происходит взрыв и…