ribbit

the manual

Documentation

Install deterministic avatars for React or Svelte, then use the framework-agnostic Canvas, SVG, PNG and WebM engine when you need more control.

Ribbit turns a string or number into the same generative mark every time. Use the component for an avatar, or use the core to paint Canvas, generate SVG and export media without storing images or making network requests.

Installation

$ bun add ribbit-canvas

Ribbit has no runtime dependencies. react >= 18 and svelte >= 5 are optional peer dependencies for their respective adapters.

Components

For an avatar, import the adapter for your UI framework and pass a stable user identifier.

React

import { RibbitAvatar } from "ribbit-canvas/react";

export function Avatar({ user }) {
  return <RibbitAvatar seed={user.id} size={40} />;
}

Svelte

The default export has the same avatar defaults.

<script>
  import RibbitAvatar from "ribbit-canvas/svelte";
</script>

<RibbitAvatar seed={user.id} size={40} />

Core renderer

Use the core directly when the mark belongs in a canvas composition rather than a component.

import { render } from "ribbit-canvas";

render(canvas, "null-frog", { size: 256, pattern: "dither" });

Props

RibbitAvatar is the small, high-level API for both adapters.

Prop Type Default Effect
seed string | number required Stable identity for the mark
size number 32 Displayed square size in CSS pixels
pattern "bars" | "dither" | "glyph" | "maze" | "pulse" | "wave" "dither" Rendering language
radius number | string "9999px" Circle, rounded square or square presentation
palette { background, ramp } moss Overrides the color system
animated boolean false Evolves the field when visible
reactive boolean false Cells light up around the cursor
className, style React only Applied to the clipping wrapper
class Svelte only Applied to the canvas

The component first paints a static frame. When animated is enabled, it pauses offscreen and respects prefers-reduced-motion.

reactive makes the cells near the pointer climb the tonal ramp. The glow is folded into the field before it is quantized, so the mark lights up rather than wearing an overlay, and the geometry of the seed never moves. It is ignored on touch, under prefers-reduced-motion, and for wave, which has no cell grid. The two props compose: animated reactive gives a field that both evolves and answers the cursor.

<RibbitAvatar seed="null-frog" size={96} reactive />

Under the hood this is renderReactive, which takes a pointer in canvas coordinates. Reach for it directly when you drive your own canvas:

import { renderReactive } from "ribbit-canvas";

canvas.addEventListener("pointermove", (event) => {
  const box = canvas.getBoundingClientRect();
  renderReactive(ctx, "null-frog", {
    size: 256,
    pointer: { x: event.clientX - box.left, y: event.clientY - box.top },
  });
});

With no pointer it is render, so static exports are untouched and the mark at rest is exactly the one ribbit generates. glowRadius (default 0.42 of the shortest side) and glowBoost (default 0.6) tune reach and lift.

Sizes & shapes

radius controls only how an adapter is displayed. It defaults to a circle; pass a number for a rounded square or 0 for a square.

<RibbitAvatar seed="studio" size={84} />
<RibbitAvatar seed="studio" size={84} radius={18} />
<RibbitAvatar seed="studio" size={84} radius={0} />

For generated files, use the core option shape: "circle". It makes PNG and SVG corners truly transparent, rather than merely clipping them in CSS.

const avatar = await toBlob("ada", {
  preset: "avatar",
  size: 512,
  shape: "circle",
});

Patterns

Pattern Character Best suited to
dither Square Bayer cells over a continuous field Avatars, pixel art, compact marks
glyph The field sampled as monospace symbols Technical identities, editorial graphics
pulse Dots that swell and brighten with the field Soft avatars, halftone textures, hover surfaces
maze Seeded diagonals in the 10 PRINT tradition Retro identities, texture fills, terminal aesthetics
bars Vertical bars rising with the field Audio-adjacent brands, dashboards, data-flavored marks
wave Five to nine layered contours Covers, backgrounds, animated surfaces

Engine helpers

Use the core when a component is not the right output.

Helper Output Environment
render(target, seed, options?) Existing Canvas or 2D context Browser or Offscreen Canvas
toSVG(seed, options?) Standalone SVG string Browser or server
toDataURL(seed, options?) PNG data URL Browser DOM
toBlob(seed, options?) PNG Blob Browser DOM
toWebM(seed, options?) Animated WebM Blob Browser with MediaRecorder

The renderer accepts an HTMLCanvasElement, an OffscreenCanvas, or either 2D context. Passing a canvas sets its backing dimensions; passing a context preserves your transform.

import { render } from "ribbit-canvas";

const width = 320;
const height = 180;
const dpr = Math.min(window.devicePixelRatio, 2);

canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;

const ctx = canvas.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
render(ctx, "brook", { width, height, pattern: "wave" });

width and height override size. On a non-square canvas, cells and field frequencies stay proportional instead of stretching.

Exports

All export helpers share seed, pattern, shape, palette and t.

import { toBlob, toSVG } from "ribbit-canvas";

const avatar = await toBlob("ada", {
  preset: "avatar",
  size: 512,
  pattern: "glyph",
  shape: "circle",
});

const socialCover = toSVG("release-1.4", {
  preset: "og",
  pattern: "wave",
});

preset: "avatar" is square. preset: "og" always produces a native 1200×630 cover; size, width and height do not change that preset.

Try avatar and OG formats in the playground.

Animation

t: 0 is a static frame. Advance time in seconds to animate the same visual identity.

render(ctx, "dragonfly", {
  size: 256,
  pattern: "dither",
  t: performance.now() / 1000,
});

Use toWebM when an animation must leave the browser as a file.

import { PALETTES, toWebM } from "ribbit-canvas";

const video = await toWebM("dragonfly", {
  preset: "og",
  pattern: "wave",
  palette: PALETTES.tide,
  duration: 5,
  fps: 30,
});

Recording happens in real time. Ribbit tries VP9, then VP8, then generic WebM; the helper rejects when the browser cannot encode any supported format.

Palettes

Every renderer accepts a palette with a background and a background-to-foreground ramp. Ribbit ships four families — moss, tide, ember and mono — each with a light-mode variant: mossLight, tideLight, emberLight and monoLight.

import { PALETTES, toSVG } from "ribbit-canvas";

const cover = toSVG("launch", {
  preset: "og",
  pattern: "wave",
  palette: PALETTES.ember,
});

Pick a variant at runtime to follow the reader’s color scheme:

const dark = matchMedia("(prefers-color-scheme: dark)").matches;
render(canvas, "launch", { palette: PALETTES[dark ? "tide" : "tideLight"] });

Transparent backgrounds

Set background: null to skip the backdrop. The canvas keeps whatever is behind it, toSVG omits its backing rect, and toBlob/toWebM keep the alpha channel (pass matte to flatten onto a color instead).

render(canvas, "launch", {
  palette: { ...PALETTES.tide, background: null },
});

Only tone 0 is left unpainted, so how much shows through depends on the pattern: glyph and maze stay mostly transparent, pulse and bars around half, wave around 15%, and dither covers nearly the whole surface.

Custom ramps need at least two colors, so a palette override always carries a ramp. Changing a palette never changes a seed’s geometry, so the same seed and pattern retain their composition.

API reference

Option Type Default Effect
size number 200 Fallback for both dimensions
width, height number size Logical dimensions
pattern pattern name "dither" Bars, dither, glyph, maze, pulse or wave renderer
shape "rectangle" | "circle" "rectangle" Transparent circular crop for core output
palette palette moss Background and tonal ramp; background: null is transparent
t number 0 Time in seconds

Lower-level drawDither, drawGlyph and drawWave paint square contexts directly. seedFromString hashes strings to unsigned 32-bit seeds, and toSeed accepts either a string or number.

License

Ribbit is MIT licensed and free to use in personal and commercial projects.