shadimage
thegridcn
componentagent-avatarsource: ok · hit

Agent Avatar

npx shadcn add "https://thegridcn.com/r/agent-avatar.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Files1 · source cached 10/14/2026, 12:22:16 PM

src/components/thegridcn/agent-avatar.tsx
"use client";

import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";

export type AgentAvatarProps = Omit<
  React.HTMLAttributes<HTMLDivElement>,
  "children"
> & {
  seed: string;
  size?: number;
  animated?: boolean;
  /** Base hue (0-360). Blue ≈ 200, Orange ≈ 30, Cyan ≈ 185 */
  hue?: number;
  /** Show rotating outer ring */
  ring?: boolean;
};

/* ── helpers ─────────────────────────────────────────── */

const hashSeed = (str: string): number => {
  let h = 0;
  for (const c of str) {
    h = ((h << 5) - h + c.charCodeAt(0)) | 0;
  }
  return Math.abs(h);
};

const createRng = (seed: number) => {
  let s = seed;
  return () => {
    s = (s + 0x6d_2b_79_f5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
    return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;
  };
};

/* ── ring layout ─────────────────────────────────────── */

const RINGS = [
  { r: 0.88, segments: 16, width: 0.065 },
  { r: 0.7, segments: 12, width: 0.07 },
  { r: 0.52, segments: 8, width: 0.08 },
  { r: 0.34, segments: 6, width: 0.09 },
];

interface Segment {
  on: boolean;
  phase: number;
  ringIdx: number;
  segIdx: number;
}

const generateSegments = (hash: number): Segment[] => {
  const rng = createRng(hash);
  const segments: Segment[] = [];
  for (let ri = 0; ri < RINGS.length; ri++) {
    const count = RINGS[ri].segments;
    for (let si = 0; si < count; si++) {
      segments.push({
        on: rng() > 0.35,
        phase: rng() * Math.PI * 2,
        ringIdx: ri,
        segIdx: si,
      });
    }
  }
  return segments;
};

/* ── component ───────────────────────────────────────── */

export function AgentAvatar({
  seed,
  size = 64,
  animated = true,
  hue,
  ring = true,
  className,
  ...props
}: AgentAvatarProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const rafRef = useRef<number>(0);

  const hash = hashSeed(seed);
  const baseHue = hue ?? hash % 360;
  const glowColor = `hsl(${baseHue}, 85%, 55%)`;

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      return;
    }

    const dpr = window.devicePixelRatio || 1;
    canvas.width = size * dpr;
    canvas.height = size * dpr;
    ctx.scale(dpr, dpr);

    const segments = generateSegments(hash);
    const cx = size / 2;
    const cy = size / 2;
    const half = size / 2;

    const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
    let shouldAnimate = animated && !motionQuery.matches;

    const draw = (time: number) => {
      ctx.clearRect(0, 0, size, size);
      ctx.save();

      // Circular clip
      ctx.beginPath();
      ctx.arc(cx, cy, half, 0, Math.PI * 2);
      ctx.clip();

      // Dark background
      ctx.fillStyle = "#050508";
      ctx.fillRect(0, 0, size, size);

      // Subtle radial gradient background glow
      const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, half);
      bgGrad.addColorStop(0, `hsla(${baseHue}, 60%, 30%, 0.12)`);
      bgGrad.addColorStop(1, "transparent");
      ctx.fillStyle = bgGrad;
      ctx.fillRect(0, 0, size, size);

      // Global rotation for outermost ring
      const globalRotation = shouldAnimate ? time * 0.0003 : 0;

      // Draw ring segments
      for (const seg of segments) {
        const ringDef = RINGS[seg.ringIdx];
        const count = ringDef.segments;
        const radius = ringDef.r * half;
        const arcWidth = ringDef.width * half;
        const gapAngle = 0.04; // gap between segments in radians
        const segAngle = (Math.PI * 2) / count - gapAngle;

        // Outermost ring rotates
        const rotation = seg.ringIdx === 0 ? globalRotation : 0;
        const startAngle = rotation + seg.segIdx * ((Math.PI * 2) / count);
        const endAngle = startAngle + segAngle;

        if (!seg.on) {
          // Dim inactive segment
          ctx.beginPath();
          ctx.arc(cx, cy, radius, startAngle, endAngle);
          ctx.strokeStyle = `hsla(${baseHue}, 30%, 25%, 0.15)`;
          ctx.lineWidth = arcWidth;
          ctx.stroke();
          continue;
        }

        // Pulsing brightness for active segments
        const pulse = shouldAnimate
          ? 0.6 + 0.4 * Math.sin(time * 0.002 + seg.phase)
          : 0.85;
        const lightness = 45 + pulse * 25;

        // Active segment with glow
        ctx.beginPath();
        ctx.arc(cx, cy, radius, startAngle, endAngle);
        ctx.strokeStyle = `hsla(${baseHue}, 90%, ${lightness}%, ${0.5 + pulse * 0.5})`;
        ctx.lineWidth = arcWidth;
        ctx.shadowColor = `hsla(${baseHue}, 90%, ${lightness}%, 0.6)`;
        ctx.shadowBlur = arcWidth * 1.5;
        ctx.stroke();
        ctx.shadowBlur = 0;
      }

      // Radial spokes connecting rings
      const spokeCount = 8;
      const rng = createRng(hash + 99);
      for (let i = 0; i < spokeCount; i++) {
        if (rng() < 0.4) {
          continue;
        }
        const angle = (i / spokeCount) * Math.PI * 2;
        const innerRing = RINGS.at(-1) ?? RINGS[0];
        const innerR = innerRing.r * half * 0.6;
        const outerR = RINGS[0].r * half * 1.05;
        ctx.beginPath();
        ctx.moveTo(
          cx + Math.cos(angle) * innerR,
          cy + Math.sin(angle) * innerR
        );
        ctx.lineTo(
          cx + Math.cos(angle) * outerR,
          cy + Math.sin(angle) * outerR
        );
        ctx.strokeStyle = `hsla(${baseHue}, 60%, 45%, 0.12)`;
        ctx.lineWidth = 0.5;
        ctx.stroke();
      }

      // Central core glow
      const coreRadius = half * 0.14;
      const corePulse = shouldAnimate
        ? 0.7 + 0.3 * Math.sin(time * 0.003)
        : 0.85;
      const coreGrad = ctx.createRadialGradient(
        cx,
        cy,
        0,
        cx,
        cy,
        coreRadius * 2.5
      );
      coreGrad.addColorStop(
        0,
        `hsla(${baseHue}, 90%, 70%, ${corePulse * 0.5})`
      );
      coreGrad.addColorStop(1, "transparent");
      ctx.fillStyle = coreGrad;
      ctx.beginPath();
      ctx.arc(cx, cy, coreRadius * 2.5, 0, Math.PI * 2);
      ctx.fill();

      // Core dot
      ctx.beginPath();
      ctx.arc(cx, cy, coreRadius, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${baseHue}, 85%, 65%, ${corePulse})`;
      ctx.shadowColor = `hsla(${baseHue}, 90%, 60%, 0.8)`;
      ctx.shadowBlur = coreRadius * 3;
      ctx.fill();
      ctx.shadowBlur = 0;

      ctx.restore();

      if (shouldAnimate) {
        rafRef.current = requestAnimationFrame(draw);
      }
    };

    const handleMotionChange = () => {
      cancelAnimationFrame(rafRef.current);
      shouldAnimate = animated && !motionQuery.matches;
      if (shouldAnimate) {
        rafRef.current = requestAnimationFrame(draw);
      } else {
        draw(0);
      }
    };

    motionQuery.addEventListener("change", handleMotionChange);

    if (shouldAnimate) {
      rafRef.current = requestAnimationFrame(draw);
    } else {
      draw(0);
    }

    return () => {
      cancelAnimationFrame(rafRef.current);
      motionQuery.removeEventListener("change", handleMotionChange);
    };
  }, [size, animated, hash, baseHue]);

  const ringSize = size + 8;

  return (
    <div
      data-slot="tron-agent-avatar"
      className={cn(
        "relative inline-flex items-center justify-center",
        className
      )}
      style={{ height: ring ? ringSize : size, width: ring ? ringSize : size }}
      {...props}
    >
      {ring ? (
        <svg
          aria-hidden="true"
          className="absolute inset-0"
          width={ringSize}
          height={ringSize}
          viewBox={`0 0 ${ringSize} ${ringSize}`}
          style={{
            animation: animated
              ? "agentRingSpin 20s linear infinite"
              : undefined,
          }}
        >
          <circle
            cx={ringSize / 2}
            cy={ringSize / 2}
            r={ringSize / 2 - 2}
            fill="none"
            stroke={glowColor}
            strokeWidth="1"
            strokeDasharray="3 5"
            opacity="0.35"
          />
        </svg>
      ) : null}

      {ring ? (
        <div
          className="absolute rounded-full"
          style={{
            animation: animated
              ? "agentGlowPulse 3s ease-in-out infinite"
              : undefined,
            boxShadow: `0 0 10px ${glowColor}30, 0 0 20px ${glowColor}10`,
            height: size + 2,
            width: size + 2,
          }}
        />
      ) : null}

      <canvas
        aria-label={`Avatar for ${seed}`}
        className="rounded-full"
        ref={canvasRef}
        role="img"
        style={{ height: size, width: size }}
      />

      <style jsx>{`
        @keyframes agentRingSpin {
          0% { transform: rotate(0deg); }
          100% { transform: rotate(360deg); }
        }
        @keyframes agentGlowPulse {
          0%, 100% { opacity: 0.5; }
          50% { opacity: 1; }
        }
      `}</style>
    </div>
  );
}

More from thegridcn

Browse all components in this library →