Создание Ridgeline: проектирование 3D-опыта в реальном времени в Webflow | Кодропс

Создание Ridgeline: проектирование 3D-опыта в реальном времени в Webflow | Кодропс



Риджлайн — это место для пеших прогулок и фотографий: кинематографическая прогулка по трем настоящим альпийским походам. Это личный проект и мой способ понять, как я хочу создавать сайты. Все началось с одного вопроса: может ли веб-поток на сайте размещен действительно реальный трехмерный ландшафт, то есть фактические данные о высоте (DEM), представленные в виде сетки геодезических контуров и визуализированные в реальном времени в Three.js, а не цикл видео и не запеченный лист спрайтов, не отказываясь при этом от возможности редактирования Webflow?

Единственное правило, которое я установил для себя, заключалось в одном предложении: «Мне не важно, встроенный это компонент или внешний JS, я хочу его ВИДЕТЬ». Это правило незаметно решило всю архитектуру. Я рассмотрел два способа внедрения 3D в реальном времени в Webflow, прежде чем остановился на одном, и именно в этом решении (компромиссы и все такое) и заключается настоящая история.

Готовый сайт представляет собой три сцены «состояния», каждая из которых представляет собой настоящий поход, нарисованный на своей местности: Рассвет (Tre Cime, буря), Восход (Монблан, синий час переходит в розовый), и Снег (Аннапурна, ночь синяя от падающего снега). Они связаны между собой фотографией, управляемой прокруткой, и окружающим звуком, а также домашней страницей атласа, которая летает по местности с тремя интерактивными превью сцен. Все это настоящая геометрия, и все это создается и поддерживается из кода через Веб-поток MCP оставаясь при этом обычным, редактируемым человеком проектом Webflow.

Поход на рассвете — мой. Я записал подъем над Кортиной в Tre Cime, экспортировал активность из Strava в виде GPS-трека (GPX), очистил обычные всплески GPS и наложил реальный след на истинный склон SRTM. Только нормализованная версия корабля без координат, поэтому местность узнаваема, но точный маршрут не отображается. (Sunrise и Snow используют правдоподобные синтезированные маршруты, поскольку у меня не было записанного трека для них.)

Еще одна вещь, о которой стоит сказать заранее, потому что она определяла, насколько быстро я мог двигаться: весь сайт был написан с помощью Claude (Opus 4.8 и Fable 5) под управлением Webflow MCP, и он был собран примерно за неделю.

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

Стек

  • Three.js + React Three Fiber + drei для 3D-сцен.
  • GSAP (ScrollTrigger) + Ленис для прокрутки и анимации.
  • Блендер (управляется через Blender MCP) для каждой модели, смоделированной или запеченной, затем экспортируется в glTF (сжатие Draco).
  • Облачная вспышка R2 для размещения пакета JS, а также ресурсов GLB и текстур (CDN с загрузчиком с отключенным кешем).
  • веб-поток для структуры, стиля, страниц и CMS: поверхность, редактируемая человеком.
  • Веб-поток MCP (1.3 на момент написания статьи) в качестве уровня сборки, управляемого Клодом: страницы, компоненты, классы, переменные, пользовательский код, публикация.
  • Страва в качестве источника данных для реального похода Dawn, экспортированного в виде трека GPX и наложенного на местность SRTM.

1. Два способа внедрения React в Webflow и почему я выбрал встраивание

У Webflow есть подходящий собственный маршрут для этого: компонент кода (развертываемый через DevLink или общую библиотеку), который находится в дизайнере как первоклассный элемент. Это отлично подходит, когда ваш компонент имеет форму пользовательского интерфейса. У меня было другое животное. Один тяжелый пакет WebGL с собственным этапом сборки, Three.js, конвейером ресурсов Blender и GLB, размещенными на R2, представляет собой множество механизмов, которые можно маршрутизировать через любую систему компонентов. Поэтому я сравнил это с самостоятельным внедрением JS, и для этого проекта встраивание победило. Компромисс — это интересная часть:

  • Автономная сборка. Three.js, сборщик и ресурсы находятся в одном месте, которое я полностью контролирую.
  • Итерация за секунды. Развертывание пакета, жесткое обновление, готово. Никакой повторной публикации для изменения поведения.
  • Портативный. Одна и та же вставка работает на любом сайте или CMS, которая подходит для данного специализированного пакета.

Единственное, чем не является встраивание, — это элементом Designer, который можно перетаскивать. Я решил эту проблему с помощью монтирования на основе атрибутов. Встраивание ищет элементы хоста, которые разместил дизайнер (например, [data-terrain-scene] или [data-terrain-card]) и монтируется в них. Дизайнер остается под контролем где; код управляет что.

// The embed hunts for designer-placed hosts and mounts into them, so the
// Webflow user keeps arranging layout and the 3D fills the slots they define.

document.querySelectorAll("[data-terrain-card]").forEach((host) => {
  const key = host.getAttribute("data-terrain-card"); // "dawn" | "sunrise" | "snow"
  mountScenePreview(host, key);
});

Вынос

  1. Сопоставьте путь интеграции с компонентом. Прежде чем принять решение, убедитесь, что оно соответствует вашему варианту использования.
  2. Автономное внедрение заменяет одно удобство Designer на автономную сборку и мгновенную итерацию.
  3. Монтаж на основе атрибутов обеспечивает чистоту разделения: веб-поток владеет макетом, код — поведением.

2. Создание всего сайта из кода: рабочий процесс Webflow MCP

Это та часть, которая меня больше всего удивила. Весь сайт (страницы, компоненты, классы, CSS, переменные, пользовательский код, SEO, публикация) создается и поддерживается агентом через Веб-поток MCP (Протокол контекста модели). Агент здесь — Клод. И на выходе по-прежнему остается совершенно обычный проект Webflow, который человек может открыть и редактировать.

Основное правило, на котором я остановился:

Разметка и CSS находятся в Webflow Designer как именованные компоненты и классы. Поведение, 3D и анимация реализованы в версии JS на R2. Дизайнер поддерживает структуру; CDN содержит поведение.

Одна вещь, о которой стоит знать заранее: Webflow предоставляет вам два места для пользовательского кода, и они предназначены для разных задач. Существуют зарегистрированные сценарии (API сценариев, внедренный JS), а также собственный код верхнего и нижнего колонтитула (необработанный HTML/CSS). Все, что должно существовать в первая краска принадлежит второму, и я объясню, почему, во флэш-разделе.

Вынос

  1. Агент может запустить настоящую сборку Webflow через MCP и оставить после себя проект, полностью редактируемый человеком.
  2. Придерживайтесь жесткой линии: структура в Webflow, поведение в коде. Это то, что обеспечивает ремонтопригодность обеих половин.

3. Делаем 3D реальным: от Blender до glTF и Three.js

Неоспоримым было то, что геометрия реальная, а не умный шейдер на примитиве. Каждый ландшафт представляет собой настоящую ЦМР (набор данных высот SRTM) для реального массива, смоделированного в Blender, экспортированного в glTF и загруженного в Three.js. Если это «кристаллический монолит», ответ начинается в Blender, а не в JSX.

Трубопровод:

  1. Смоделируйте или запеките в Blender. Я использую параметрический build.py для математически определенных форм и интерактивного моделирования для художественных форм. В любом случае настоящий .blend существует, поэтому геометрию можно повторить позже.
  2. Экспортируйте в glTF с важными флагами:
bpy.ops.export_scene.gltf(
    export_yup=True,        # Blender Z-up to three.js Y-up
    export_apply=True,      # bake modifiers
    export_extras=True,     # object custom props to glTF extras to three.js userData
    # ALWAYS Draco-compress. Static meshes shrink 5-13x (11 MB to ~1 MB)
    export_draco_mesh_compression_enable=True,
    export_draco_mesh_compression_level=6,
    export_draco_position_quantization=14,
)
  1. Загрузить с useGLTFперемещайтесь для поиска именованных объектов, применяйте материалы и фиксируйте состояние каждого экземпляра при загрузке. Не пересчитывайте геометрию во время выполнения.

Blender также использует собственный MCP, ту же агентскую настройку, что и Webflow, поэтому настройки модели могут происходить непосредственно в Blender, когда это необходимо сцене, без необходимости ручного обхода пользовательского интерфейса.

Три GLB ландшафта занимают примерно 540–580 КБ каждый после Драко, что достаточно мало, чтобы загрузка никогда не была узким местом. Все узкие места были в графическом процессоре и основном потоке, о котором речь пойдет дальше в этой статье.

Внешний вид обзорной карты — один шейдер, без текстур.

Эстетика контурной карты — это не текстура. Это фрагментный шейдер, который считывает высоту сетки в мировом пространстве. Хитрость, позволяющая сохранить четкость линий на любом расстоянии от камеры, заключается в следующем. fwidth(): он определяет ширину сглаживания в пространство экрана от скорости изменения индекса полосы, поэтому линии имеют ширину в один пиксель независимо от увеличения или уменьшения масштаба.

// Survey-contour terrain (fragment): banding by elevation, hillshade, snow line.
float scaled = vWorldPos.y * uContourFreq;            // height to band index
float dMinor = abs(fract(scaled) - 0.5) * 2.0;
float aa     = fwidth(scaled) * 2.0;                  // screen-space AA width, crisp at any zoom
float minor  = 1.0 - smoothstep(uContourWidth - aa, uContourWidth + aa, dMinor);

// every Nth line is a bolder "index" contour, the classic survey-map read
float dMajor = abs(fract(scaled / uMajorEvery) - 0.5) * 2.0;
float major  = 1.0 - smoothstep(uContourWidth * uMajorBoost - aa, uContourWidth * uMajorBoost + aa, dMajor);
float line   = max(minor * uMinorDim, major);

// hillshade from the surface normal, elevation tint, snow above the line
float shade  = clamp(dot(normalize(vWorldNormal), normalize(uLightDir)), 0.0, 1.0);
float elev   = clamp((vWorldPos.y - uElevLo) / (uElevHi - uElevLo), 0.0, 1.0);
vec3  ground = mix(uGround, uGroundHi, elev);
ground = mix(ground, mix(uGround * 0.5, ground * 0.92, shade), uHillshade);
ground = mix(ground, uSnowColor, smoothstep(uSnowLineY, uSnowLineY + uSnowSoftness, vWorldPos.y) * uSnowStrength);

vec3 contourCol = mix(uContourLo, uContourHi, elev);
gl_FragColor = vec4(mix(ground, contourCol, line), 1.0);

Каждая сцена представляет собой один и тот же шейдер с разным набором форм (цвета земли и контуров, сила снега, направление света), что позволяет Dawn, Sunrise и Snow чувствовать себя тремя местами, используя одну программу. Одно маленькое прикосновение, которое превосходит его вес: крошечное попиксельное сглаживание ((hash(gl_FragCoord.xy) – 0,5) * 0,0045) уничтожает 8-битные полосы, которые в противном случае проявляются в виде зернистых пятен в почти черных градиентах рассвета.

Вынос

  1. Реальная геометрия читается иначе, чем искусственный примитив. Это стоит трубопровода.
  2. export_yup, export_apply, export_extrasи Драко — это четыре флага, которые вам всегда нужны.
  3. fwidth() обеспечивает независимую от разрешения ширину линий, что является ключом к получению четких процедурных контуров при любом масштабировании.
  4. Один шейдер плюс униформа для каждой сцены лучше трех шейдеров, а дешевое сглаживание превосходит видимые 8-битные полосы.

4. Анимация: прокрутка без повторного рендеринга React

Все движения управляются прокруткой, и основное правило заключается в том, что React никогда не выполняет повторный рендеринг при прокрутке. Прогресс прокрутки попадает в ссылку и считывается внутри цикла рендеринга.

  • Ленис управляет плавной прокруткой и передает ScrollTrigger.
  • GSAP ScrollTrigger владеет закреплением и очисткой.
  • Прогресс переходит в refпрочитайте каждый кадр в useFrame. Нет состояния React на пути прокрутки.
// Lenis + GSAP, wired once. Lenis drives the ticker; ScrollTrigger reads it.

lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add(
gsap.ticker.lagSmoothing(0);

Два шаблона сделали тяжелую работу.

Во-первых, раздел сворачивания кадра. Полноэкранное изображение сворачивается в небольшую пластинку с соотношением сторон 4:5, в то время как окружающие столбцы изображения стекают внутрь. Ловушка: анимация width а высота от полноэкранного до планшетного меняет соотношение сторон в каждом кадре, что заставляет object-fit повторно обрезать каждый кадр, и это повторное кадрирование представляет собой видимый скачок. Исправление заключалось в том, чтобы сделать элемент фиксированным блоком 4:5, размером один раз, чтобы покрыть область просмотра, анимируемым исключительно с помощью transform: scale. Постоянное поле плюс постоянный аспект означает, что браузер вычисляет обрезку один раз и никогда не выполняет повторную обрезку, с нулевым макетом для каждого кадра.

// Uniform scale only. No width/height tween, so no re-crop, no layout thrash.

const coverW = Math.max(W, H * 0.8);
const scale = ip(ip(1.14, 1, parallax), plateW / coverW, collapse);
gsap.set(hero, { xPercent: -50, yPercent: -50, scale });

Потом ловушка, которая стоила мне слишком долго. Анимация смещения CSS, ключевые кадры которой начинаются со смещения (scale(1.04) translate(...)). В тот момент, когда это произошло, элемент привязался к этому смещению. Второй «прыжок», который выдержал все исправления в математике коллапса, потому что это был не коллапс. Урок: любая анимация, которая переключается при средней прокрутке, должна начинаться с состояния покоя (идентичности) элемента, иначе она всплывет.

Вынос

  1. Никогда не перерисовывайте React при прокрутке. Прогресс в ссылке считывается в цикле кадров.
  2. Анимация width и height повторный урожай object-fit каждый кадр. Предпочитайте преобразование: оно композиционное, без макета.
  3. Ключевой кадр, который не начинается с идентификатора, привязывается при его задействовании и скрывается от кода, который вы используете. думать несет ответственность.

5. Швы: прелоадер, первая покраска, аудиогейт

3D никогда не было сложной задачей. Швы, моменты между состояниями съедали большую часть времени. Каждый из них — обучаемая ошибка.

Начните с первой вспышки. При жесткой перезагрузке вы могли уловить долю секунды простого фона, прежде чем появился темный предзагрузчик. Это проблема времени: JS сайта внедряется загрузчиком, поэтому он запускается после браузер уже нарисовал необработанный HTML, а это означает, что JS не может предотвратить собственную предварительную загрузку Flash. Обложка должна присутствовать в кастоме головы…



Источник

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

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

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