noyzi

Get started

noyzi turns any seed — email, username, id — into a structured gradient. Deterministic: same seed, same gradient, server and browser. No stored assets.

npm install @noyzi/core @noyzi/react

The packages

  • @noyzi/core — framework-agnostic, zero-dependency engine: seed → GradientSpec → CSS / SVG / canvas / image. Runs anywhere.
  • @noyzi/react — <NoyziGradient /> and <NoyziAnimated /> on top: SVG-first rendering with optional WebGL motion.
  • shadcn — <NoyziAvatar /> and <NoyziImage /> as copy-in components: photo when there is one, gradient when there isn't.
  • img.noyzi.dev — no install: every seed is an image URL in SVG, PNG, JPG, or WebP, for link previews, README banners, and avatars.
  • MCP — gradient backgrounds, covers, and avatars for AI assistants.

How it works

seedHash hashes the seed → a seeded PRNG picks a palette and 1–4 organic color fields (generate) → the spec renders through any output. It's a plain object: generate once, render anywhere.

Example
import { NoyziGradient } from "@noyzi/react";

export function Avatar({ email }: { email: string }) {
	return <NoyziGradient seed={email} className="size-10 rounded-full" />;
}

@noyzi/core

generate()

Signature
function generate(seed: Seed, options?: GenerateOptions): GradientSpec

Seed in, GradientSpec out: the complete requested palette, 1–4 organic structure fields, and an optional vignette. The SVG uses every palette color while the fields preserve deterministic geometry across output formats.

Example
import { generate, seedHash } from "@noyzi/core";

// GradientSpec: { seed, background, palette, fields, vignette }
const spec = generate(seedHash("ada"));
spec.background.hex; // "#1b2a4a"
spec.fields[0].points; // deterministic organic contour

// clean look: no vignette
const flat = generate("ada", {
  palette: ["#f5eee0", "#8fb9be", "#ebdac3"],
  vignette: false,
});

// or a heavier vignette
const moody = generate("ada", { vignette: { strength: 0.3 } });

Try a palette

The first color becomes the background.

ada · 3 colors

Note. palette accepts 2–8 hex colors, with the background first, and overrides colors. Without it, colors clamps to 2–8 (default 4). vignette darkens the edges — strength defaults to 0.08, or disable it with false.

hexToOklch()

Signature
type HexColor = `#${string}`
function hexToOklch(color: HexColor): Oklch

Hex → OKLCH for #rgb and #rrggbb colors. Custom palette colors are converted this way inside generate().

Example
hexToOklch("#5da2e8"); // { l, c, h }

seedHash()

Signature
function seedHash(input: Seed): string

Hashes any string or number into an 8-char lowercase base36 seed. Idempotent — already-hashed input passes through unchanged.

Example
seedHash("[email protected]"); // "f12f1h6x"
seedHash("f12f1h6x"); // "f12f1h6x"

isSeedHash()

Signature
function isSeedHash(value: Seed): boolean

True if the value is already a seedHash result (matches /^[0-9a-z]{8}$/).

Example
isSeedHash("f12f1h6x"); // true
isSeedHash("[email protected]"); // false

isSequentialSeed()

Signature
function isSequentialSeed(seed: Seed): boolean

True for safe integer-like seeds such as 42 or "42". seedHash() preserves these values so sequential ids receive evenly spread palette hues.

Example
isSequentialSeed(42); // true
isSequentialSeed("42"); // true
isSequentialSeed("ada"); // false

paletteFromSeed()

Signature
function paletteFromSeed(seed: Seed, count?: number): ColorStop[]

The deterministic palette family available to a seed. The generator selects a restrained subset for its visible fields. Use it to derive matching UI accents. Count clamps to 2–8 (default 4).

Example
const [background, ...accents] = paletteFromSeed("ada");
background.hex; // "#1b2a4a"
background.oklch; // { l, c, h }

oklchToHex()

Signature
function oklchToHex(color: Oklch): string

OKLCH → #rrggbb, gamut-clamped to sRGB. All palette colors are OKLCH internally.

Example
oklchToHex({ l: 0.7, c: 0.15, h: 240 }); // "#5da2e8"

toCss()

Signature
function toCss(spec: GradientSpec, options?: SvgOptions): CssOutput

Spec → complete CSS background properties containing the exact organic SVG. Pass the artwork dimensions when matching another renderer.

Example
const background = toCss(generate("ada"), { width: 480, height: 320 });

<div style={background} />

toSvg()

Signature
function toSvg(spec: GradientSpec, options?: SvgOptions): string

The reference renderer: an SVG string with one continuous palette surface, warped by deterministic low-frequency noise and softly diffused. CSS, canvas, and browser raster outputs draw from it, and toPixels() reproduces it in plain JavaScript. Default 1000×1000.

Example
const svg = toSvg(generate("ada"), { width: 512, height: 512 });

toSvgDataUri()

Signature
function toSvgDataUri(spec: GradientSpec, options?: SvgOptions): string

toSvg() wrapped in a data:image/svg+xml URI — drop into background-image or <img src>. This is what <NoyziGradient /> uses. SSR-safe.

Example
const uri = toSvgDataUri(generate("ada"));

<div style={{ backgroundImage: `url("${uri}")` }} />

toPixels()

Signature
interface Pixels {
  width: number;
  height: number;
  data: Uint8ClampedArray; // RGBA, fully opaque
}

function toPixels(spec: GradientSpec, options?: SvgOptions): Pixels

Renders the gradient to raw RGBA pixels in plain JavaScript — no browser, canvas, or native code. Use it on servers, workers, and edge functions to make PNG, JPG, or WebP files, then encode with any image encoder. Default 1000×1000.

Example
import sharp from "sharp";

const pixels = toPixels(generate("ada"), { width: 1200, height: 630 });

const jpg = await sharp(pixels.data, {
  raw: { width: pixels.width, height: pixels.height, channels: 4 },
})
  .jpeg({ quality: 92 })
  .toBuffer();

Note. The result matches the browser's SVG rendering closely, including grain, but not byte for byte. For JPG, use a high quality (90+) so the grain survives compression.

toCanvas()

Signature
function toCanvas(
  spec: GradientSpec,
  options?: RasterOptions,
): Promise<HTMLCanvasElement>

Creates a <canvas> and paints the gradient, pixel-identical to the SVG. scale multiplies resolution for high-DPI. Browser-only.

Example
const canvas = await toCanvas(generate("ada"), { width: 500, scale: 2 });

drawToCanvas()

Signature
function drawToCanvas(
  spec: GradientSpec,
  canvas: HTMLCanvasElement | OffscreenCanvas,
  options?: SvgOptions,
): Promise<void>

Paints the exact SVG output onto a canvas you own — for custom resizing and DPR scaling. Browser-only.

Example
const canvas = document.querySelector("canvas");
await drawToCanvas(generate("ada"), canvas, { width: 400, height: 400 });

toAnimatedCanvas()

Signature
const ANIMATION_RANGES = {
  speed: { min: 0, max: 10 },
  strength: { min: 0, max: 3 },
};

interface AnimatedCanvasOptions extends RasterOptions {
  maxPixelRatio?: number;
  speed?: number;
  strength?: number;
}

interface AnimatedCanvas {
  canvas: HTMLCanvasElement;
  render(time: number): void;
  resize(): boolean;
  destroy(): void;
}

function toAnimatedCanvas(
  spec: GradientSpec,
  options?: AnimatedCanvasOptions,
): Promise<AnimatedCanvas | null>

Creates a sized <canvas>, loads the SVG texture, and returns its deterministic WebGL 2 liquid renderer. render(0) is the original generated artwork; pass elapsed seconds to later renders. scale multiplies a created canvas's backing resolution, while maxPixelRatio caps responsive resizing. Returns null when WebGL 2 is unavailable. Browser-only.

Example
const animation = await toAnimatedCanvas(generate("ada"), {
  width: 500,
  height: 500,
  scale: 2,
  speed: 3,
  strength: 3,
});

if (animation) {
  document.body.append(animation.canvas);
  const startedAt = performance.now();
  let frame = 0;
  const animate = (now: number) => {
    animation.render((now - startedAt) / 1000);
    frame = requestAnimationFrame(animate);
  };
  frame = requestAnimationFrame(animate);

  window.addEventListener("pagehide", () => {
    cancelAnimationFrame(frame);
    animation.destroy();
  }, { once: true });
}

Note. The returned controller owns the WebGL resources, while you own the animation frame, resize, visibility, and cleanup lifecycle. speed accepts 0–10 and strength accepts 0–3; invalid values throw a RangeError. Use <NoyziAnimated /> when you want those behaviors managed automatically.

createAnimatedCanvasGroup()

Signature
function createAnimatedCanvasGroup(): AnimatedCanvasGroup | null

interface AnimatedCanvasGroup {
  register(
    spec: GradientSpec,
    canvas: HTMLCanvasElement,
    options?: AnimatedCanvasOptions,
  ): Promise<AnimatedCanvas | null>;
  destroy(): void;
}

Creates one shared WebGL 2 rendering surface for an animated collection. Every registered visible canvas receives frames from that single context, avoiding per-item WebGL context limits. Returns null when WebGL 2 is unavailable. Browser-only.

Example
const group = createAnimatedCanvasGroup();
if (!group) throw new Error("WebGL 2 is unavailable");

const animations = await Promise.all(
  items.map(({ canvas, spec }) =>
    group.register(spec, canvas, { speed: 3, strength: 3 }),
  ),
);

const startedAt = performance.now();
const animate = (now: number) => {
  for (const animation of animations) {
    animation?.render((now - startedAt) / 1000);
  }
  requestAnimationFrame(animate);
};
requestAnimationFrame(animate);

drawToAnimatedCanvas()

Signature
function drawToAnimatedCanvas(
  spec: GradientSpec,
  canvas: HTMLCanvasElement,
  options?: AnimatedCanvasOptions,
): Promise<AnimatedCanvas | null>

Loads the exact SVG surface internally and prepares the same WebGL 2 liquid renderer on a canvas you own. Returns null when WebGL 2 is unavailable. Browser-only.

Example
const canvas = document.querySelector<HTMLCanvasElement>("canvas");
if (!canvas) throw new Error("Canvas not found");

const animation = await drawToAnimatedCanvas(
  generate("ada"),
  canvas,
  { width: 1000, height: 1000, speed: 3, strength: 3 },
);

if (animation) {
  animation.render(0);
}

toBlob()

Signature
function toBlob(
  spec: GradientSpec,
  options?: RasterOptions & EncodeOptions,
): Promise<Blob>

Gradient → image Blob. WebP by default at quality 0.9 (~10x smaller than PNG for gradients); browsers without WebP encoding fall back to PNG — check blob.type. For clipboard, uploads, downloads. Browser-only.

Example
const blob = await toBlob(generate("ada"), { width: 1000 });

// ClipboardItem requires PNG — opt out of WebP:
const png = await toBlob(generate("ada"), { type: "image/png" });
await navigator.clipboard.write([
  new ClipboardItem({ "image/png": png }),
]);

toDataUrl()

Signature
function toDataUrl(
  spec: GradientSpec,
  options?: RasterOptions & EncodeOptions,
): Promise<string>

Gradient → raster data URL. WebP by default at quality 0.9, PNG fallback where unsupported — check the data:image/... prefix. Browser-only.

Example
const url = await toDataUrl(generate("ada"));
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = url.startsWith("data:image/webp")
  ? "noyzi-ada.webp"
  : "noyzi-ada.png";
anchor.click();

@noyzi/react

<NoyziGradient />

Signature
interface NoyziGradientProps extends NoyziBaseProps {
  /** Intrinsic artwork size. Only the aspect ratio affects the
   *  result (the SVG is vector). Defaults to 1000×1000. */
  artwork?: { width?: number; height?: number };
}

interface NoyziBaseProps
  extends Omit<JSX.IntrinsicElements["div"], "children"> {
  seed: Seed;
  options?: GenerateOptions;
}

<div role="img"> with an SVG data-URI background. SSR-safe, zero client JS. Size and shape it with your own CSS — the artwork cover-fills the element. Use artwork to match the aspect ratio of non-square elements.

Example
<NoyziGradient seed="ada" className="size-10 rounded-full" />

<NoyziGradient
  seed="ada"
  artwork={{ width: 1600, height: 400 }}
  className="h-40 w-full rounded-lg"
/>

Select a avatar to copy its exact component.

c 3

v off

c 5

v .12

c 6

v .18

c 4

v off

c 7

v .32

Colors (c)
Palette size, including the background. More colors add more blended regions.
Vignette (v)
Darkens the outer edge. Higher strength creates a moodier frame.

<NoyziAnimated />

Signature
interface NoyziAnimatedProps extends NoyziBaseProps {
  artwork?: { width?: number; height?: number };
  speed?: number;
  strength?: number;
}

Starts as the exact NoyziGradient SVG, then eases into fluid WebGL motion.

  • Initial frame: Deterministic, SSR-safe, and identical to NoyziGradient.
  • Motion: Seed-specific bands drift, split, merge, and orbit.
  • Lifecycle: Pauses offscreen, respects reduced motion, and keeps the SVG fallback when WebGL 2 is unavailable.
Example
<NoyziAnimated
  seed="ada"
  speed={3}
  strength={2.4}
  className="size-20 rounded-full"
/>

<NoyziAnimated
  seed="ada"
  speed={3.8}
  strength={3}
  className="h-48 w-full rounded-2xl"
/>

Select an animated avatar to copy its exact component.

c 3

v off

spd 2.2 · str 1.6

c 5

v .12

spd 2.7 · str 2

c 6

v .18

spd 3.2 · str 2.5

c 4

v off

spd 3.8 · str 2.2

c 7

v .32

spd 2.5 · str 3

Colors (c)
Palette size, including the background. More colors add more blended regions.
Vignette (v)
Darkens the outer edge. Higher strength creates a moodier frame.
Speed (spd)
Scales the seeded liquid current and local field motion.
Strength (str)
Controls how far the liquid ribbons travel and curl.

Note. speed accepts 0–10 and strength accepts 0–3; invalid values throw a RangeError. Each NoyziAnimated owns its WebGL context. For a large list or grid, wrap the collection in NoyziAnimatedGroup.

<NoyziAnimatedGroup />

Signature
interface NoyziAnimatedGroupProps {
  children: ReactNode;
  frameRate?: number;
  maxPixelRatio?: number;
}

Shares one WebGL renderer across a large collection of animated gradients.

  • Best for: Long lists, avatar collections, and dense grids.
  • Shared resources: One visibility-aware WebGL 2 context and one animation scheduler.
  • Offscreen items: Release their renderer resources automatically.
Example
<NoyziAnimatedGroup frameRate={45} maxPixelRatio={1.25}>
  <div className="grid grid-cols-6 gap-4">
    {items.map((item) => (
      <NoyziAnimated
        key={item.id}
        seed={item.id}
        speed={3}
        strength={3}
        className="size-20 rounded-full"
      />
    ))}
  </div>
</NoyziAnimatedGroup>

Note. For one animation or a small handful, use NoyziAnimated directly. Defaults: frameRate 45, maxPixelRatio 1.25.

shadcn

<NoyziAvatar />

Install
npx shadcn@latest add https://noyzi.dev/r/noyzi-avatar.json

An avatar for shadcn/ui projects. It shows the user's photo, and their own gradient while it loads, when there's no photo, or when the link is broken. The code lands in your components folder, so you can change anything.

  • seed: Something stable per user: id or email.
  • src: The photo. Optional; null and undefined are fine.
  • fallback: Shown on the gradient when there's no photo, like initials.
Example
import { NoyziAvatar } from "@/components/noyzi-avatar";

<NoyziAvatar
  seed={user.email}
  src={user.image}
  alt={user.name}
  fallback={user.initials}
  className="size-10"
/>
photo
ALno photo
GHno photo
LTbroken link
nothing

<NoyziImage />

Install
npx shadcn@latest add https://noyzi.dev/r/noyzi-image.json

An image with a gradient placeholder. The gradient shows while the image loads and stays if it fails, so covers and thumbnails never look broken. The seed defaults to src.

  • className: Sizes the wrapper, e.g. aspect-video rounded-lg.
  • imageClassName: Goes on the <img>, e.g. object-top.
Example
import { NoyziImage } from "@/components/noyzi-image";

<NoyziImage
  src={post.cover}
  seed={post.slug}
  alt={post.title}
  className="aspect-video rounded-lg"
/>
Noyzi galleryimage
Missing coverbroken link

img.noyzi.dev

Image URLs

URL
https://img.noyzi.dev/v1/{seed}.svg
https://img.noyzi.dev/v1/{seed}.png
https://img.noyzi.dev/v1/{seed}.jpg
https://img.noyzi.dev/v1/{seed}.webp

Every seed has a public image URL — no install, no key. Use it anywhere an image goes: link previews, README banners, avatars, emails. The seed is the path, so slashes are fine (user/repo), and it gives the same gradient as <NoyziGradient seed="..." />.

  • Forever: A v1 URL always returns the same image, so it's cached for a year at the edge and in browsers.
  • Formats: SVG is the smallest and sharpest. WebP is the best raster for websites. Use JPG for Open Graph images and email, where SVG and WebP aren't always accepted, and PNG when you need lossless.
  • Anywhere: CORS is open, so you can also fetch the images from your own code.
Example
<img src="https://img.noyzi.dev/v1/ada.svg" width="40" height="40" alt="" />

<img src="https://img.noyzi.dev/v1/breeg554/noyzi.svg?w=1280&h=320" alt="" />

Options

Parameters
w         width in px. Default 1000
h         height in px. Default 1000
colors    2-8. Default 4
palette   2-8 hex colors without #, background first
vignette  0-1, or false

The same options as generate(), passed as query parameters. SVG goes up to 4096 px per side, PNG, JPG and WebP up to 2400. Unknown or invalid parameters return a 400 with a readable message.

Example
https://img.noyzi.dev/v1/ada.png?w=512&h=512
https://img.noyzi.dev/v1/ada.svg?colors=6&vignette=false
https://img.noyzi.dev/v1/brand.jpg?palette=0b1020,ff5a5f,ffd166

Note. Use either colors or palette, not both.

Open Graph images

URL
https://img.noyzi.dev/v1/{slug}.jpg?w=1200&h=630

Give every page its own link preview: use the page slug as the seed. Social sites don't show SVG and some skip WebP, so use JPG at 1200×630.

Example
<meta property="og:image" content="https://img.noyzi.dev/v1/my-first-post.jpg?w=1200&h=630" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />

README banners

URL
https://img.noyzi.dev/v1/{user}/{repo}.svg?w=1280&h=320

One line of Markdown gives your repo a banner that's unique to it. GitHub shows SVG images in READMEs.

Example
![](https://img.noyzi.dev/v1/breeg554/noyzi.svg?w=1280&h=320)

Versions

URL
https://img.noyzi.dev/v1/{seed}.svg      never changes
https://img.noyzi.dev/latest/{seed}.svg  redirects to the newest version

If the renderer ever changes how gradients look, it ships as /v2 and /v1 stays the same. Link to /v1 when the image must never change; use /latest to always get the newest look.

MCP

Connect via MCP

Server URL
https://noyzi.dev/mcp

Gradient backgrounds, covers, and avatars for AI assistants. Connect your MCP client to this URL.

generate_gradient

Tool parameters
seed      required string, 1–256 characters
palette   optional 2–8 #rrggbb colors, background first
width     positive integer in px. Default 1000
height    positive integer in px. Default 1000
format    png | webp | jpg | svg. Default png

Returns a gradient image URL from a seed, with optional colors, size, and format.

Example
{
  "name": "generate_gradient",
  "arguments": {
    "seed": "summer-launch",
    "palette": ["#fff4df", "#ff9166", "#eaa0c5"],
    "width": 1600,
    "height": 900,
    "format": "png"
  }
}

Output lab

Compare the same gradient across every renderer. CSS and React wrap the reference SVG, while canvas and raster outputs draw from it. Raster weight varies by seed, dimensions, quality, and browser encoder.

Same seed · 480×320 · sizes measured in your browser

CSS background

toCss()

6.2 KiB
SVG renderer output

SVG

toSvg()

4.0 KiB

React

<NoyziGradient />

6.1 KiB URI

Canvas

drawToCanvas()

600.0 KiB memory

WebP

toBlob({ type: "image/webp" })

measuring…

PNG

toBlob({ type: "image/png" })

measuring…