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

Image Trail

Шлейф изображений за курсором: каждые N пикселей пути всплывает картинка со случайным наклоном и тут же тает (пул тайлов).

gsapmouse-moverevealintermediate~23 КБreduced-motion ✓

Установка

npx shadcn@latest add https://motiva.pages.dev/r/image-trail.json

зависимости: gsap@^3.15.0

Что нужно в проекте до установки
  1. components.json в корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.
  2. Алиас @ объявлен дважды: "paths": {"@/*": ["./src/*"]} в корневом tsconfig.json (в Vite он часто содержит только references — тогда CLI создаёт папку@) и resolve.alias в конфиге сборщика.
  3. Токены подключены один раз на приложение:import "@/styles/motiva-tokens.css" — они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему.
  4. Своя типографика не должна спорить: если у вас на обёрткеtext-align: center, он протечёт внутрь блоков.
  5. Проект без TypeScript? Всё равно нужен минимальныйtsconfig.json с pathsи "tsx": true в components.json: CLI грузит tsconfig безусловно, а с "tsx": false идёт за jsconfig.json и падает так же. Приехавший рядом.tsx можно не трогать — вам нужен.vanilla.js.

Экспорт

↓ Скачать .zip

Код

"use client";

import { useRef, type PointerEvent } from "react";
import gsap from "gsap";

const ARTS = [280, 230, 170, 60, 330, 20].map(
  (hue) => `linear-gradient(150deg, oklch(0.6 0.16 ${hue} / 0.9) 0%, oklch(0.2 0.04 ${hue}) 75%)`,
);
const SPAWN_DIST = 90;
const POOL = 10;

/**
 * Шлейф изображений за курсором: каждые N пикселей пути в точке курсора всплывает «картинка»
 * (пул переиспользуемых тайлов), тут же уменьшаясь и исчезая. Уважает reduced-motion / data-shot.
 */
export function ImageTrail() {
  const root = useRef<HTMLDivElement>(null);
  const last = useRef({ x: -1e4, y: -1e4 });
  const idx = useRef(0);

  const frozen = () =>
    document.documentElement.hasAttribute("data-shot") ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const onMove = (e: PointerEvent<HTMLDivElement>) => {
    const el = root.current;
    if (!el || frozen()) return;
    const r = el.getBoundingClientRect();
    const x = e.clientX - r.left;
    const y = e.clientY - r.top;
    if (Math.hypot(x - last.current.x, y - last.current.y) < SPAWN_DIST) return;
    last.current = { x, y };

    const tiles = el.querySelectorAll<HTMLElement>(".it-tile");
    const tile = tiles[idx.current % POOL];
    if (!tile) return;
    idx.current += 1;
    tile.style.background = ARTS[idx.current % ARTS.length]!;
    tile.style.zIndex = String(idx.current);
    gsap
      .timeline()
      .fromTo(
        tile,
        { x, y, xPercent: -50, yPercent: -50, scale: 0.3, opacity: 0, rotation: gsap.utils.random(-14, 14) },
        { scale: 1, opacity: 1, duration: 0.3, ease: "power2.out" },
      )
      .to(tile, { scale: 0.2, opacity: 0, duration: 0.6, ease: "power2.in" }, "+=0.12");
  };

  return (
    <div ref={root} onPointerMove={onMove} style={{ position: "absolute", inset: 0, overflow: "hidden", cursor: "crosshair" }}>
      {Array.from({ length: POOL }, (_, i) => (
        <div
          key={i}
          className="it-tile"
          aria-hidden
          style={{
            position: "absolute",
            left: 0,
            top: 0,
            width: 130,
            height: 170,
            borderRadius: 12,
            border: "1px solid oklch(0.34 0.02 270)",
            opacity: 0,
            pointerEvents: "none",
            willChange: "transform, opacity",
          }}
        />
      ))}
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "grid",
          placeItems: "center",
          pointerEvents: "none",
          color: "oklch(0.92 0.01 265)",
        }}
      >
        <div style={{ textAlign: "center", opacity: 0.6 }}>
          <div style={{ fontSize: "1.5rem", fontWeight: 700 }}>Image Trail</div>
          <div style={{ marginTop: 6, fontFamily: "ui-monospace, monospace", fontSize: "0.75rem" }}>
            веди курсором — за ним шлейф картинок
          </div>
        </div>
      </div>
    </div>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — Codrops image trailv1.0.0