Ir al contenido
Evaluar mi proyecto
Componentes · Cursor y efectosDisponible

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.

efecto-estela.tsx88 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/efecto-estela

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
hint?stringOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)