← каталог
Cursor Trail
Шлейф частиц за курсором на canvas: рождаются вдоль движения, разлетаются с гравитацией и гаснут; hue плавно вращается.
cssmouse-moveintermediate0reduced-motion ✓
Установка
npx shadcn@latest add https://motiva.pages.dev/r/cursor-trail.jsonЧто нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
"use client";
import { useEffect, useRef } from "react";
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
life: number;
hue: number;
}
/**
* Шлейф частиц за курсором на canvas: частицы рождаются вдоль движения, разлетаются и гаснут.
* rAF-луп останавливается, когда частиц нет. Уважает reduced-motion / data-shot (без шлейфа).
*/
export function CursorTrail() {
const wrap = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const box = wrap.current;
const canvas = canvasRef.current;
if (!box || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const frozen = document.documentElement.hasAttribute("data-shot") || reduce;
const dpr = Math.min(2, window.devicePixelRatio);
const resize = () => {
const r = box.getBoundingClientRect();
canvas.width = Math.round(r.width * dpr);
canvas.height = Math.round(r.height * dpr);
};
resize();
window.addEventListener("resize", resize);
const particles: Particle[] = [];
let raf = 0;
let running = false;
let hue = 280;
const tick = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]!;
p.x += p.vx;
p.y += p.vy;
p.vy += 0.05;
p.life -= 0.02;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
ctx.globalAlpha = p.life;
ctx.fillStyle = `oklch(0.78 0.16 ${p.hue})`;
ctx.beginPath();
ctx.arc(p.x * dpr, p.y * dpr, p.life * 4 * dpr, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
if (particles.length) {
raf = requestAnimationFrame(tick);
} else {
running = false; // луп спит, пока курсор не двигается
}
};
const onMove = (e: PointerEvent) => {
if (frozen) return;
const r = box.getBoundingClientRect();
hue = (hue + 3) % 360;
for (let i = 0; i < 3; i++) {
particles.push({
x: e.clientX - r.left,
y: e.clientY - r.top,
vx: (Math.random() - 0.5) * 2.4,
vy: (Math.random() - 0.5) * 2.4 - 0.6,
life: 0.7 + Math.random() * 0.3,
hue,
});
}
if (!running) {
running = true;
raf = requestAnimationFrame(tick);
}
};
box.addEventListener("pointermove", onMove);
return () => {
box.removeEventListener("pointermove", onMove);
window.removeEventListener("resize", resize);
cancelAnimationFrame(raf);
};
}, []);
return (
<div ref={wrap} style={{ position: "absolute", inset: 0, cursor: "crosshair", overflow: "hidden" }}>
<canvas ref={canvasRef} style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }} />
<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 }}>Cursor Trail</div>
<div style={{ marginTop: 6, fontFamily: "ui-monospace, monospace", fontSize: "0.75rem" }}>
двигай курсором — за ним шлейф
</div>
</div>
</div>
</div>
);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0