Estela luminosa del puntero
Cinta suavizada con tono cambiante que se desvanece sola, en canvas; solo dibuja mientras hay movimiento.
Estela de luzMueve el puntero para dibujar
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useRef, type CSSProperties } from "react";
/** Estela luminosa del puntero en canvas: una cinta suavizada con hue cambiante que se desvanece sola. Solo dibuja mientras hay movimiento, y se apaga con movimiento reducido y en táctil. React + TypeScript, sin dependencias. */
export type EfectoEstelaProps = {
hint?: string;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcetl-root { box-sizing: border-box; position: relative; width: 100%; max-width: 640px; height: 320px; overflow: hidden; border-radius: 20px; background: #07060d; color: #e9e6f7; font: 500 15px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; display: grid; place-items: center; touch-action: pan-y; }
.gcetl-root *, .gcetl-root *::before, .gcetl-root *::after { box-sizing: border-box; }
.gcetl-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.gcetl-hint { position: relative; pointer-events: none; text-align: center; color: #8a85a8; }
.gcetl-hint b { display: block; color: #e9e6f7; font: 600 20px/1.2 ui-sans-serif, sans-serif; }`;
type Pt = { x: number; y: number; t: number };
export function EfectoEstela({ hint = "Mueve el puntero para dibujar", className = "", style }: EfectoEstelaProps) {
const root = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const el = root.current;
const cv = canvas.current;
const ctx = cv?.getContext("2d");
if (!el || !cv || !ctx) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let w = 0, h = 0, dpr = 1, raf = 0, hue = 200;
const pts: Pt[] = [];
const LIFE = 900;
const size = () => {
const r = el.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
w = r.width; h = r.height;
cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
};
const frame = (now: number) => {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
while (pts.length && now - pts[0].t > LIFE) pts.shift();
if (pts.length > 1) {
ctx.lineCap = "round";
ctx.lineJoin = "round";
for (let i = 1; i < pts.length; i++) {
const a = pts[i - 1], b = pts[i];
const life = 1 - (now - b.t) / LIFE;
ctx.strokeStyle = `hsla(${(hue + i * 3) % 360}, 90%, 65%, ${life * 0.9})`;
ctx.lineWidth = 2 + life * 14;
ctx.shadowColor = `hsla(${(hue + i * 3) % 360}, 90%, 60%, ${life})`;
ctx.shadowBlur = 16 * life;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.quadraticCurveTo(a.x, a.y, (a.x + b.x) / 2, (a.y + b.y) / 2);
ctx.stroke();
}
}
raf = pts.length && !document.hidden ? requestAnimationFrame(frame) : 0;
};
const move = (e: PointerEvent) => {
if (e.pointerType === "touch") return;
const r = el.getBoundingClientRect();
hue = (hue + 2) % 360;
pts.push({ x: e.clientX - r.left, y: e.clientY - r.top, t: performance.now() });
if (pts.length > 80) pts.shift();
if (!raf) raf = requestAnimationFrame(frame);
};
size();
const ro = new ResizeObserver(size);
ro.observe(el);
el.addEventListener("pointermove", move);
return () => { cancelAnimationFrame(raf); ro.disconnect(); el.removeEventListener("pointermove", move); };
}, []);
return (
<div ref={root} className={`gcetl-root ${className}`} style={style}>
<style>{CSS}</style>
<canvas ref={canvas} className="gcetl-canvas" aria-hidden="true" />
<p className="gcetl-hint">
<b>Estela de luz</b>
{hint}
</p>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/efecto-estelaEjemplo de uso de Estela luminosa del puntero
Después de copiar efecto-estela.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { EfectoEstela } from "./efecto-estela";
export default function Ejemplo() {
return <EfectoEstela hint="Ejemplo" />;
}Propiedades de EfectoEstela
EfectoEstela acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
hint? | string | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 88 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Estela luminosa del puntero por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Reacciona al puntero con eventos pointer (ratón, lápiz y táctil).
- Dibuja en <canvas>, sin imágenes ni librerías gráficas.
- Anima con requestAnimationFrame, sincronizado con el refresco de pantalla.
- Es un componente de cliente de Next.js: incluye la directiva "use client".