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.

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. Every other static output is derived from it. 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}")` }} />

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.

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()

2.1 KiB
SVG renderer output

SVG

toSvg()

1.3 KiB

React

<NoyziGradient />

2.0 KiB URI

Canvas

drawToCanvas()

600.0 KiB memory

WebP

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

measuring…

PNG

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

measuring…