Prism
← All primitives

SeedEmblem

Generative

A unique animated SVG 'system' generated deterministically from any string. Same seed, same emblem. Server-renderable, no canvas.

Requires the @keyframes ge-spin / ge-pulse and .ge-spin / .ge-pulse rules from globals.css — copy them in, or the emblem renders static.

$npx shadcn@latest add https://prism.icglabs.co/r/seed-emblem.json
View raw manifest →

components/prism/SeedEmblem.tsx
import { rngFrom, hashStr } from "@/lib/seed";

/**
 * SeedEmblem — a deterministic, animated SVG "system" (orbiting rings, nodes,
 * a glowing core) generated entirely from a seed string. Same string ⇒ same
 * emblem, every time. Pure/server-renderable. Ported from Sky's GenEmblem.
 *
 * Use it for unique-but-consistent avatars, project marks, or entity glyphs.
 */
export function SeedEmblem({
  seed,
  hue = "#8b5cff",
  label,
  size = 56,
}: {
  seed: string;
  hue?: string;
  label?: string;
  size?: number;
}) {
  const rng = rngFrom(seed);
  const id = hashStr(seed).toString(36);
  // Round derived values so server- and client-rendered SVG attributes serialize
  // identically (trig last-ULP differences would otherwise trip hydration).
  const r3 = (n: number) => Math.round(n * 1000) / 1000;

  const rings = Array.from({ length: 3 }, (_, i) => {
    const rx = r3(30 + i * 8 + rng() * 6);
    const ry = r3(rx * (0.48 + rng() * 0.36));
    const rot = Math.floor(rng() * 360);
    const dur = r3(20 + i * 9 + rng() * 12);
    return { rx, ry, rot, dur, rev: i % 2 === 1, op: 0.42 - i * 0.1 };
  });

  const nodeCount = 4 + Math.floor(rng() * 4);
  const nodes = Array.from({ length: nodeCount }, () => {
    const a = rng() * Math.PI * 2;
    const r = 22 + rng() * 24;
    return {
      x: r3(60 + Math.cos(a) * r),
      y: r3(60 + Math.sin(a) * r * 0.72),
      rr: r3(1.3 + rng() * 1.8),
    };
  });

  const coreR = r3(7 + rng() * 5);

  return (
    <svg viewBox="0 0 120 120" width={size} height={size} role="img" aria-hidden>
      <defs>
        <radialGradient id={`glow-${id}`}>
          <stop offset="0%" stopColor={hue} stopOpacity="0.95" />
          <stop offset="45%" stopColor={hue} stopOpacity="0.45" />
          <stop offset="100%" stopColor={hue} stopOpacity="0" />
        </radialGradient>
      </defs>
      {nodes.map((n, i) => (
        <line key={`l${i}`} x1="60" y1="60" x2={n.x} y2={n.y} stroke={hue} strokeOpacity="0.12" strokeWidth="0.6" />
      ))}
      {rings.map((r, i) => (
        <ellipse
          key={`r${i}`}
          className="ge-spin"
          style={{
            animationDuration: `${r.dur}s`,
            animationDirection: r.rev ? "reverse" : "normal",
            animationDelay: `${-(r.rot / 360) * r.dur}s`,
          }}
          cx="60"
          cy="60"
          rx={r.rx}
          ry={r.ry}
          fill="none"
          stroke={hue}
          strokeOpacity={r.op}
          strokeWidth="0.8"
        />
      ))}
      {nodes.map((n, i) => (
        <circle key={`n${i}`} cx={n.x} cy={n.y} r={n.rr} fill={hue} fillOpacity="0.85" />
      ))}
      <circle className="ge-pulse" cx="60" cy="60" r={coreR * 1.9} fill={`url(#glow-${id})`} />
      <circle cx="60" cy="60" r={coreR} fill={hue} fillOpacity="0.92" />
      {label ? (
        <text x="60" y="67" textAnchor="middle" fontSize="20" fontWeight="600" fill="#08080c">
          {label}
        </text>
      ) : (
        <circle cx="60" cy="60" r={coreR * 0.42} fill="#ffffff" fillOpacity="0.85" />
      )}
    </svg>
  );
}
lib/seed.ts
// Tiny deterministic PRNG so generative visuals are stable per seed string.
// Ported from Sky — the seed→visual pipeline is one of the most reusable tools.

export function hashStr(s: string): number {
  let h = 1779033703 ^ s.length;
  for (let i = 0; i < s.length; i++) {
    h = Math.imul(h ^ s.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  h ^= h >>> 16;
  return h >>> 0;
}

export function mulberry32(seed: number): () => number {
  let a = seed >>> 0;
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

export function rngFrom(s: string): () => number {
  return mulberry32(hashStr(s));
}
Live demo — read-only. Every section is a real, copyable primitive.