Motiva
Мои коллекции
← каталог

Letter Morph

Буквы летят по выбранному маршруту сквозь кадр и на ходу переливаются одна в другую: через кадр слева направо, из глубины прямо на зрителя или дугой из верхнего угла. Рождаются и уходят они ЗА кромкой кадра, поэтому видно полёт, а не появление. Буква здесь не текст и не геометрия шрифта, а контур: силуэт обводится по кромке маски и приводится к одинаковому у всех букв числу точек — после этого морф становится честной интерполяцией точка-в-точку, а не кроссфейдом. Обводка рисуется металлической лентой с отражением процедурного студийного окружения; проёмы букв живут вторым кольцом.

threemouse-moveautomorphadvanced~165 КБreduced-motion ✓
↗
🎛 Настройки — крути и копируй

Установка

npx shadcn@latest add https://motiva.pages.dev/r/letter-morph.json

зависимости: three@^0.185.1 · @types/three@^0.185.0

Что нужно в проекте до установки
  1. Файл components.json в корне

    Из него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:

    "aliases": { "components": "@/components", "lib": "@/lib" }

    Нет файла — установка не начнётся вовсе.

  2. Алиас @ объявлен дважды

    В корневом tsconfig.json и в конфиге сборщика. В Vite корневой tsconfig часто содержит только references — этого мало.

    "paths": { "@/*": ["./src/*"] }        // tsconfig.json
    resolve.alias = { "@": "/src" }        // vite.config.ts

    Без paths CLI молча создаёт папку с именем @.

  3. Токены подключены один раз на приложение

    Они приезжают вместе с записью — достаточно импортировать их в точке входа:

    import "@/styles/motiva-tokens.css";

    Без них блок потеряет цвета, ритм и тему.

  4. Своя типографика не спорит с блоком

    Наследуемые свойства протекают внутрь: text-align: center на обёртке отцентрует содержимое секции. То же касается font-familyи line-height.

  5. Проект без TypeScript? всё равно нужен tsconfig

    CLI грузит tsconfig.json безусловно, а при "tsx": false идёт за jsconfig.json и падает так же. Минимум — файл с pathsи "tsx": true в components.json.

    Приехавший рядом .tsx можно не трогать — вам нужен .vanilla.js.

Экспорт

↓ Скачать .zip

Код

"use client";

import { useEffect, useRef } from "react";
import { createLetterMorph, type LetterMorph as Engine, type LetterMorphOptions } from "@/lib/letter-morph-engine";

export type LetterMorphProps = Omit<LetterMorphOptions, "animate"> & {
  /** Подложка под холст: CSS `background` или `false`, если её задаёт секция. */
  backdrop?: string | false;
};

// Подложка намеренно НЕ чёрная: светлая металлическая лента на почти чёрном фоне
// даёт максимальный перепад яркости на кромке, и любое движение начинает рябить
// (мера мерцания, RECIPES 4ц). Приподнятый фон снижает перепад, не убирая металл.
const STUDIO =
  "radial-gradient(120% 110% at 50% 8%, oklch(0.34 0.035 268) 0%, oklch(0.22 0.025 264) 48%, oklch(0.15 0.02 260) 100%)";

/**
 * Letter Morph — буквы как контуры: силуэт каждой обводится по пикселям и приводится
 * к одинаковому числу точек, поэтому буква морфится в букву точка-в-точку, пока летит
 * по кривой сквозь кадр металлической обводкой.
 *
 * Сцена — `createLetterMorph` из соседнего файла; компонент держит холст, прокидывает
 * крутилки через `set()` и разбирает всё при размонтировании.
 * `prefers-reduced-motion` и `data-shot` рисуют один стоячий кадр посреди пролёта.
 */
export function LetterMorph({ backdrop = STUDIO, ...options }: LetterMorphProps = {}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const engine = useRef<Engine | null>(null);
  const first = useRef(options);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const animate = !(
      document.documentElement.hasAttribute("data-shot") ||
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    );
    engine.current = createLetterMorph(canvas, { ...first.current, animate });
    return () => {
      engine.current?.destroy();
      engine.current = null;
    };
  }, []);

  const { text, slots, speed, weight, twist, sway, tint, tilt, font } = options;
  useEffect(() => {
    engine.current?.set({ text, slots, speed, weight, twist, sway, tint, tilt, font });
  }, [text, slots, speed, weight, twist, sway, tint, tilt, font]);

  return (
    <div
      style={{
        position: "absolute",
        inset: 0,
        overflow: "hidden",
        ...(backdrop === false ? null : { background: backdrop }),
      }}
    >
      <canvas ref={canvasRef} aria-hidden="true" style={{ display: "block", width: "100%", height: "100%" }} />
    </div>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — разбор Codrops «Turning Names Into Digital Architecture with Three.js» (09.09.2026): буква сводится к фиксированному числу точек контура и морфится вдоль кривой. У нас своя обводка (обход кромки маски по пикселям с пересэмплированием по длине пути), свои кольца проёмов и свой материал; код не заимствованv1.0.0