Goodgrowth: последовательности загрузки, вращающиеся диски и искусство портфолио | Кодропс

Goodgrowth: последовательности загрузки, вращающиеся диски и искусство портфолио | Кодропс



Создание личного портфолио всегда самое увлекательное занятие, но может быть и самым сложным. Когда направление и творческий подход в конечном итоге зависят от вас, иногда может быть еще труднее решить, каким путем идти. Разрабатывая свой сайт, я хотел приступить к его созданию с относительно ясным внешним видом и ощущениями, а также установить добровольный график, чтобы он действительно был завершен, а не отправлен в мусорное ведро или невыполненную работу.

Эпоха видеоигр конца 90-х — начала 2000-х всегда была одной из моих любимых. Я хорошо помню экраны запуска и загрузки PlayStation и Dreamcast, и мне хотелось продолжить это, чтобы передать свою визуальную эстетику. Назовите это ностальгией, назовите это старостью или просто воспоминаниями о более простых временах. В конечном счете, я действительно хотел создать ощущение включения консоли и раскрытия опыта.

Все начинается с эскиза

Одним из основных принципов, которыми я руководствовался при разработке своего сайта, было выход за пределы моей зоны комфорта. Имея опыт работы с HTML/CSS/JS, я многое выхожу за рамки своего стандартного набора навыков, и именно тогда интеграция Claude Code в мой рабочий процесс действительно помогла мне воплотить в жизнь то, что я задумал.

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

Технический стек

  • Three.js: используется для основных 3D-элементов сайта.
  • GSAP – обработана почти вся анимация.
  • Ленис — плавная прокрутка: включена
  • Vite – для всей сборки
  • API веб-аудио – гарантия того, что звук звучит правдоподобно
  • Никаких фреймворков – ванильные модули JS и ES.
  • Cinema4D – ресурсы компакт-дисков и дискет смоделированы, а затем преобразованы в GLB для WebGL.

Загрузите это

Наличие сайта, который чувствовал себя целостным от предварительной загрузки до перехода к проектам, было еще одним фундаментальным принципом, к которому я вернулся. Начиная с экрана «Вставьте диск», это служило тому, чтобы звук был выдвинут на передний план, а не просто второстепенным. Вращающийся диск обрабатывался с помощью простого RotateY в CSS, чтобы не создавать ненужный код и сделать его максимально простым. При входе начинается предзагрузчик. Я хотел, чтобы номера загрузки были максимально привязаны к логотипу, поэтому я заменил О на 0.

Больше всего времени я потратил на миниатюры, которые расходятся веером, а затем возвращаются обратно перед раскрытием. Я продолжал пытаться представить это как спиральный путь, но это всегда выглядело механически. Что в конечном итоге сработало, так это осознание того, что спираль — это вовсе не путь, а просто увеличение угла и уменьшение радиуса. Каждая миниатюра хранит свой угол и радиус отдельно, бегущая строка преобразует их в x/y каждый кадр, а «спираль» — это то, что выпадает, когда вы одновременно ускоряете общий угол и сжимаете радиусы.

// angle and radius are tweened independently — no path involved
const placePreviews = () => {
  for (let i = 0; i  {
  out.to(prevState[i], { r: 0, duration: 0.55, ease: 'power2.in' }, 0.09 * i);
});

Отклонение 0,09 — вот что придает хвосту — замыкающие миниатюры все еще вращаются по орбите, в то время как лидеры уже исчезли в центре.

Оставаться глобальным

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

Следующей задачей стала полоса проекта, обнимающая карусель. Я не хотел, чтобы это была просто прямая горизонтальная полоса, а хотел, чтобы она шла от верхнего левого угла к правому нижнему. Это заняло немало итераций и в конечном итоге привело к тому, что полоса проекта была представлена ​​в виде единого атласа текстуры, причем кривая применялась как сдвиг по широте, а не вращение, поэтому полоса оставалась точно на сфере.

Одна из самых крутых деталей – невидимка. Магнитный фиксатор фиксирует кольцо на кратных плитках, но меридианы вращаются в своем собственном соотношении, поэтому каждая поза покоя приземлялась на немного другую фазу меридиана, земной шар никогда не выглядел одинаково дважды. Это исправило изменение коэффициента встречного вращения, чтобы один шаг плитки продвигал меридианы ровно на один шаг меридиана. Константа изменилась с 0,6 до 0,571, что трудно увидеть во время движения, но теперь каждая поза покоя воспроизводит эталонную компоновку.

const TILE_STEP     = (Math.PI * 2) / projects.length;
const MERIDIAN_STEP =  Math.PI / MERIDIAN_COUNT;

// snapped so one detent = exactly one meridian step
const GLOBE_SPIN =
  (Math.max(1, Math.round(0.6 * TILE_STEP / MERIDIAN_STEP)) * MERIDIAN_STEP) / TILE_STEP;

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

const dir = netDX > 0 ? -1 : 1;
// snap from where the gesture STARTED, not from the half-rotated current value
targetRot = (Math.round(touchStartRot / step) + dir) * step;

Чувствую себя живым

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

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

vec3 prev = texture2D(uPrev, vUv).rgb * 0.94;   // the decay IS the trail
prev += vec3(uVel * s, s * length(uVel));       // velocity, not position

// red and blue sample at opposite offsets — that's the chromatic split
float cr = texture2D(uMap, uv + flow * 0.05).r;
float cb = texture2D(uMap, uv - flow * 0.05).b;

Дизеринг, дизеринг, дизеринг

Лоу-файная полигональная эстетика игровой эпохи Y2K настолько знакова, что, чтобы отыграть ее, я применил сильно размытый подход к своим шейдерам по всему сайту. От глобуса до фона облаков интеграция сглаживания Байера помогла мне отобразить это и сохранить оригинальное видение. Это двухстрочная функция, повторно используемая повсюду, и она работает в пространстве экрана, а не в UV-пространстве, поэтому узор остается привязанным к дисплею, как артефакт печати, а не плавает вместе с геометрией.

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

float sweep = dot(normalize(vDiscNormalV), normalize(-vViewPosition));

// the sweep term is what makes the cross PIVOT rather than rotate with the disc
float axis   = ang - sweep * 1.6;
float bowtie = pow(abs(cos(axis)), 6.0);   // two-lobe cross through the centre

// spectral fringe splits green↔magenta along the band's dark edges
float edge   = sin(axis * 2.0);
vec3  fringe = hsv2rgb(vec3(fract(0.30 + edge * 0.22), 0.85, 1.0));

Еще одним преимуществом Three.js является возможность аккуратного применения текстур к GLB, что означало, что мне не приходилось полагаться на какое-либо текстурирование в Cinema4D. Поскольку каждой сетке при загрузке назначается свой собственный материал, все карты PBR, написанные экспортером, лежали мертвым грузом. Удаление их плюс сварка + квантование увеличили размер GLB основного компакт-диска с 9,7 МБ до 182 КБ без каких-либо визуальных различий.

От одного к другому

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

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

// each half clips a different horizontal slice, but the -top offset means
// every title replica sits at the same absolute screen position
inner.style.top = `${-top}dvh`;

// collapsing the halves to zero width IS the reveal — centre bands lead
pwHalves.forEach((pair, i) => {
  tl.to(pair, { width: 0, duration: 0.6, ease: 'power3.inOut' },
    1.8 + Math.abs(i - (pwHalves.length - 1) / 2) * 0.07);
});

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

Это оказалось немного сложнее, чем первоначально ожидалось, а также вызвало некоторые проблемы, связанные с поведением прокрутки: если пользователь прокручивал слишком много сразу после заполнения, он отправлялся на половину страницы вниз. Решение заключалось в том, чтобы закрепить значок вверху и запретить любое взаимодействие до завершения сборки.

const IDLE_MS  = 220;    // wheel considered "stopped" after this quiet gap
const MAX_WAIT = 4000;   // safety cap so it can never hang

const settle = () => {
  const now = performance.now();
  if (now - _swallowLastInput 

Звучит чудесно

Помимо визуального оформления, одним из самых запоминающихся компонентов стартового экрана всегда был звук. Вы сразу же поняли, в какой системе находитесь, и что волнение нарастало. Это был очень важный аспект, который я хотел исправить, поэтому я связался со своим другом Лейном Фудзитой, чтобы он разработал его, поскольку мы с ним оба любим игры и потратили бесчисленное количество часов на то, чтобы разобраться с контроллерами. Мало того, что я был полностью потрясен тем, что он прислал, Лейн прислал подробный документ со спецификациями, в котором было указано время для звука, а также когда и когда ничего не должно быть слышно.

При включении звука я столкнулся с огромной проблемой с производительностью, на выяснение которой потребовалось некоторое время. Первоначальные выводы заключались в том, что когда звук был отключен, все работало плавно, но когда он был включен, он создавал массу заиканий и замедлений, в частности, на экране загрузки. Проблема заключалась в том, что HTMLAudioElement.play() во время подсчета запускал около 100 вызовов из кадра анимации. Безголовый хром не смог его воспроизвести, поэтому мне пришлось использовать веб-инспектор Safari на устройстве, чтобы посмотреть, что происходит.

Исправление заключалось в том, чтобы полностью прекратить вызов play() для каждого тика и передать всю последовательность часам веб-аудио за один проход до начала отсчета. Веб-аудио выполняется вне основного потока, поэтому, как только оно запланировано, подсчет ничего не стоит.

export function scheduleLoaderTicks(durationSec, count = 100) {
  const t0 = audioCtx.currentTime + 0.02;
  const MIN_GAP = 1 / 18;            // rate ceiling, per the sound spec
  const maxTicks = Math.min(count, Math.floor(durationSec / MIN_GAP));
  let prev = -Infinity;

  for (let i = 0; i 

Кроме того, в iOS было несколько интересных особенностей.



Источник

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх