ru · Lab · complete

От Unicode spinner к Rose Curve

Как вопрос о загрузочной анимации Hermes превратился в локальный playground для Unicode-кадров и математического SVG-loader.

  • animation
  • interface
  • react
  • svg
  • experiment

Всё началось с одного loader

Я увидел в Hermes маленькую загрузочную анимацию и захотел понять, как она сделана. Она выглядела слишком аккуратно для обычного CSS-spinner: один светящийся знак плавно менял форму, не отвлекал от интерфейса и при этом хорошо показывал, что процесс ещё идёт.

Первое объяснение оказалось простым. Для статусов инструментов и субагентов Hermes использует Unicode-анимации: готовые последовательности символов, которые сменяют друг друга через небольшой интервал. Но рядом обнаружился другой loader, уже не текстовый. При загрузке страниц Hermes рисует анимированную Rose Curve через SVG и requestAnimationFrame.

Так один вопрос разделился на два разных эксперимента.

Rose Curve Loader Playground с интерактивной настройкой формы, частиц и скорости

Локальный playground: исходный Rose Curve loader, текущие параметры и часть панели управления.

Unicode spinner: анимация из текста

Unicode-spinner не требует SVG, canvas или набора изображений. Пакет unicode-animations хранит последовательности кадров вроде этих:

⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏

Компонент выбирает preset, берёт очередной кадр и меняет его через setInterval. В Hermes есть ещё режим, где из каждого кадра оставляется только первый Unicode glyph. На маленьком размере это выглядит спокойнее и лучше помещается в строку статуса.

Я сделал отдельную витрину, чтобы не угадывать по исходному коду, как разные наборы ощущаются в интерфейсе. В ней можно выбрать preset, изменить скорость, размер, цвет и glow, а затем переключиться между полным кадром и one-glyph mode. Рядом показываются все символы последовательности и готовый React-сниппет.

Технически здесь почти нечего скрывать:

const spin = spinners.breathe
const [frame, setFrame] = useState(0)

useEffect(() => {
  const id = setInterval(() => {
    setFrame(value => (value + 1) % spin.frames.length)
  }, spin.interval)

  return () => clearInterval(id)
}, [spin])

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


Rose Curve оказалась другой системой

Page loader использует параметрическую кривую, которую часто называют розой Родонеи. Точка движется по формуле, а множитель k меняет количество и расположение лепестков:

const t = progress * Math.PI * 2
const a = 9.2 + detailScale * 0.6
const r = a * (0.72 + detailScale * 0.28) * Math.cos(5 * t)

const x = 50 + Math.cos(t) * r * 3.25
const y = 50 + Math.sin(t) * r * 3.25

Сама линия почти прозрачная. Поверх неё движутся 78 SVG-кружков. У каждого меняются координаты, радиус и opacity, поэтому они складываются в светящийся хвост. Вся группа медленно вращается, а detailScale добавляет мягкое «дыхание» формы.

В исходном виде параметры подобраны под один конкретный размер и контекст Hermes. По коду было трудно почувствовать, что именно отвечает за характер движения. Поэтому я вынес loader в самостоятельный React-компонент и сделал почти каждый параметр интерактивным:

  • k и curveScale меняют геометрию;
  • particleCount, trailSpan и strokeScale управляют плотностью и хвостом;
  • durationMs, pulseDurationMs и rotationDurationMs задают три разных темпа;
  • pathSteps, radiusBase и radiusPulse влияют на точность и амплитуду формы.

Playground показывает loader в крупном preview и в компактном размере, близком к реальному интерфейсу. Это оказалось полезно: настройка может красиво выглядеть на площади в двести пикселей и превратиться в нечитаемое пятно внутри кнопки или пустого состояния.

Что получилось проверить

Я добавил четыре стартовых preset: исходный Hermes, более резкий Sharp star, спокойный Soft orbit и плотный Dense bloom. Они используют тот же компонент, но ощущаются как разные анимации.

k сильнее всего меняет узнаваемую форму. Количество частиц само по себе не делает loader лучше: после определённого значения хвост становится тяжёлым, а движение теряет направление. Скорость вращения тоже лучше держать отдельно от скорости частицы. Если связать их одним duration, результат выглядит механическим.

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

Проект собран на React, TypeScript, Vite, Bun и Tailwind CSS. Production build проходит без ошибок. Публичного demo и отдельного репозитория у него пока нет: это локальный инструмент для исследования, а не готовая библиотека компонентов.

Где эксперимент заканчивается

Playground не решает задачу loader-системы целиком. В нём пока нет экспорта SVG или JSON-конфига, галереи других математических кривых и отдельного reduced-motion режима. Rose Curve также обновляет десятки SVG-элементов каждый кадр, поэтому её нельзя бездумно размножать по всему интерфейсу.

Я бы не ставил такую анимацию возле каждой фоновой операции. Для коротких статусов достаточно Unicode spinner. Rose Curve уместнее там, где интерфейс действительно ждёт перехода или загрузки отдельной поверхности и может дать анимации немного пространства.

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