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

DNA Helix

Вертикальная двойная спираль из скруглённых треугольных плиток: две нити вьются вокруг оси, между ними идут перемычки-ступени, вся конструкция медленно поворачивается. Плитки текут вверх по нити, снизу собираются из россыпи, а в середине кадра по каждой проходит шумный скан с горящей кромкой, и печатающийся код на грани проявляется в кадр видео. Тот же приём, что у `shard-ribbon` (россыпь → сборка → скан → медиа), но путь плитки — винтовая линия, и отсюда вся разница в математике. Во-первых, параметр нити — ДЛИНА ДУГИ, а не высота: плитки стоят вдоль нити через полширины, и если мерить этот шаг по высоте, то при подъёме в 60° между ними остаётся вдвое больше дуги, чем сама плитка, — нить рассыпается на отдельные пластины. Во-вторых, грань ленты смотрит РАДИАЛЬНО наружу: иначе плитки идут к зрителю рёбрами и ни кода, ни видео не видно. Ступени темнее ободка плиток втрое и подхватывают цвет скана — на полной яркости восемь длинных прутков перебивают сами нити.

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

Установка

npx shadcn@latest add https://motiva.pages.dev/r/dna-helix.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 { createDnaHelix, type DnaHelix as Engine, type DnaHelixOptions } from "@/lib/dna-helix-engine";

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

const NIGHT =
  "radial-gradient(120% 100% at 50% 40%, oklch(0.21 0.02 265) 0%, oklch(0.13 0.015 262) 52%, oklch(0.08 0.01 258) 100%)";

/**
 * DNA Helix — вертикальная двойная спираль из треугольных плиток: нити вьются
 * вокруг оси, между ними перемычки, плитки текут вверх, собираются из россыпи
 * и в середине кадра переключаются со кода на видео горящим сканом.
 *
 * Сцена — `createDnaHelix` из соседнего файла; компонент держит холст, прокидывает
 * крутилки через `set()` и разбирает всё при размонтировании.
 * `prefers-reduced-motion` и `data-shot` рисуют один стоячий кадр.
 */
export function DnaHelix({ backdrop = NIGHT, ...options }: DnaHelixProps = {}) {
  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 = createDnaHelix(canvas, { ...first.current, animate });
    return () => {
      engine.current?.destroy();
      engine.current = null;
    };
  }, []);

  const { rows, tile, speed, radius, pitch, strands, rungs, spin, scatter, formSpan, glow, tilt } = options;
  useEffect(() => {
    engine.current?.set({ rows, tile, speed, radius, pitch, strands, rungs, spin, scatter, formSpan, glow, tilt });
  }, [rows, tile, speed, radius, pitch, strands, rungs, spin, scatter, formSpan, glow, tilt]);

  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); заказ владельца 15.09.2026 — «сделаем ещё один на основе shard-ribbon, только вертикальную ДНК-спираль с таким же принципом»v1.0.0