Разбиение на символы

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

В контексте Motion One работа с текстом всегда опирается на DOM-структуру: строка преобразуется в набор элементов, чаще всего <span>, после чего каждый элемент анимируется отдельно через animate, timeline или stagger.

Ключевая особенность подхода:

  • текст перестаёт быть единым узлом
  • каждый символ получает независимые стили и анимационные параметры
  • управление становится декларативным через массив элементов

Базовая реализация разбиения строки

Основной шаг — преобразование строки в массив символов и оборачивание каждого символа в DOM-элемент.

<div class="text">Motion One</div>
const el = document.querySelector(".text");

const chars = el.textContent.split("");

el.innerHTML = chars
  .map(char => `<span class="char">${char}</span>`)
  .join("");

Результат:

  • исходная строка заменяется набором <span>
  • каждый символ становится независимой единицей

Особенность:

  • пробелы необходимо сохранять как неразрывные элементы (&nbsp; или \u00A0)
  • переносы строк обрабатываются отдельно

Подготовка структуры для анимации

После разбиения важно зафиксировать базовые стили:

.text {
  display: inline-block;
}

.char {
  display: inline-block;
  will-change: transform, opacity;
}

Ключевые моменты:

  • inline-block необходим для трансформаций каждого символа
  • will-change уменьшает стоимость первой анимации
  • отсутствие блочного поведения сохраняет естественную линию текста

Интеграция с Motion One

После подготовки DOM каждый символ становится объектом анимации.

Простейший сценарий появления:

import { animate } from "motion";

const chars = document.querySelectorAll(".char");

chars.forEach((char, i) => {
  animate(
    char,
    {
      opacity: [0, 1],
      transform: ["translateY(20px)", "translateY(0px)"]
    },
    {
      delay: i * 0.03,
      duration: 0.4
    }
  );
});

Поведение:

  • каждый символ стартует с задержкой
  • создаётся каскадный эффект
  • анимация независима для каждого узла

Использование stagger вместо ручных задержек

В Motion One существует более компактный способ управления последовательностью через stagger.

import { animate, stagger } from "motion";

animate(
  ".char",
  {
    opacity: [0, 1],
    transform: ["translateY(20px)", "translateY(0px)"]
  },
  {
    delay: stagger(0.03),
    duration: 0.5
  }
);

Особенности:

  • исключается ручной forEach
  • библиотека сама рассчитывает последовательность
  • можно использовать функции для сложной логики задержек

Варианты разбиения текста

Разбиение на символы с сохранением пробелов

const splitText = (text) =>
  text.split("").map(char => char === " " ? "\u00A0" : char);

Разбиение с учетом слов и символов

Иногда требуется комбинированная структура:

const words = text.split(" ");

el.innerHTML = words
  .map(word => {
    const chars = word.split("");
    return `<span class="word">
      ${chars.map(c => `<span class="char">${c}</span>`).join("")}
    </span>`;
  })
  .join(" ");

Это позволяет:

  • анимировать слова как группы
  • а затем символы внутри группы

Анимация появления с контролем физики движения

Разбиение на символы позволяет применять разные траектории движения.

import { animate, stagger } from "motion";

animate(
  ".char",
  {
    opacity: [0, 1],
    transform: [
      "translateY(30px) scale(0.8)",
      "translateY(0px) scale(1)"
    ]
  },
  {
    delay: stagger(0.02, { start: 0.1 }),
    duration: 0.6,
    easing: "ease-out"
  }
);

Здесь:

  • scale добавляет ощущение “сборки” текста
  • stagger.start задаёт задержку всей группы
  • easing влияет на плотность движения

Синхронизация символов через timeline

Таймлайны позволяют управлять всей последовательностью как единым сценарием.

import { timeline } from "motion";

timeline([
  [".char", { opacity: [0, 1] }, { duration: 0.3, delay: stagger(0.02) }],
  [".char", { y: [20, 0] }, { duration: 0.4 }, "-0.2"]
]);

Поведение:

  • первая стадия — появление
  • вторая — смещение
  • перекрытие фаз через отрицательный offset

Преимущества:

  • единый контроль над сценой
  • возможность пересекать анимации
  • упрощение сложных эффектов текста

Управление скоростью по позиции символа

Каждый символ может иметь динамическую задержку:

animate(
  ".char",
  (element, index) => ({
    opacity: [0, 1],
    transform: ["translateY(10px)", "translateY(0px)"]
  }),
  {
    delay: (i) => i * 0.025,
    duration: 0.4
  }
);

Дополнительно возможно:

  • ускорение середины строки
  • замедление концов
  • синусоидальные задержки

Сложные эффекты: волна символов

Разбиение на символы позволяет строить нелинейные анимации:

animate(
  ".char",
  (element, i) => ({
    transform: [
      `translateY(${Math.sin(i) * 10}px)`,
      "translateY(0px)"
    ]
  }),
  {
    delay: stagger(0.02),
    duration: 0.5
  }
);

Такие эффекты используются для:

  • имитации вибрации текста
  • реакций на движение курсора
  • динамических заголовков

Оптимизация рендеринга

При работе с большим количеством символов важно учитывать нагрузку:

Минимизация DOM-операций

  • разбиение выполняется один раз
  • повторное создание элементов избегается

Использование GPU-трансформаций

  • transform и opacity остаются единственными анимируемыми свойствами
  • исключаются layout-triggering свойства

Батчинг анимаций

animate(
  ".char",
  { opacity: [0, 1] },
  { delay: stagger(0.01), duration: 0.3 }
);

Малые задержки уменьшают визуальный разрыв и снижают нагрузку на планировщик кадров.


Практические паттерны разбиения

Паттерн «заголовок-волна»

animate(
  ".char",
  {
    transform: ["translateY(40px)", "translateY(0px)"]
  },
  {
    delay: stagger(0.04, { from: "center" }),
    duration: 0.5
  }
);

Паттерн «печатающийся текст»

animate(
  ".char",
  { opacity: [0, 1] },
  { delay: stagger(0.06), duration: 0.1 }
);

Паттерн «разлёт и сборка»

animate(
  ".char",
  {
    transform: [
      "translate(0px, 0px)",
      "translate(50px, -30px)"
    ]
  },
  {
    delay: stagger(0.02),
    duration: 0.6,
    direction: "alternate"
  }
);

Синхронизация с внешними событиями

Разбиение символов часто связывается с событиями интерфейса:

button.addEventListener("mouseenter", () => {
  animate(".char", {
    transform: ["translateY(0px)", "translateY(-10px)"]
  }, {
    delay: stagger(0.02),
    duration: 0.3
  });
});

Такой подход позволяет:

  • оживлять заголовки при наведении
  • создавать интерактивную типографику
  • синхронизировать текст с UX-событиями