grain

SVG-based film grain noise and radial gradient effects

Maintained Updated 8mo ago

grain builds full-screen gradient-and-film-grain backgrounds entirely out of CSS and SVG — no <canvas>, no raster images. Pick a palette, set how many gradients to scatter and how strong the grain is, regenerate for a fresh random arrangement, and export the result to PNG. It is a small Preact and TypeScript static site, prerendered with preact-iso and deployed to Vercel.

The background is one CSS property

The entire visible texture lives in a single element's background shorthand. A useBackground hook returns two pieces: an array of CSS radial-gradient() layers and one SVG noise image encoded as a data: URI. The component joins them into one comma-separated background value — grain on top, gradients beneath — and the browser's compositor does the rest. There is no drawing loop and no canvas; changing a control just rebuilds the string.

The grain

The noise layer is an inline SVG running a single feTurbulence filter — fractal noise across a couple of octaves, with stitchTiles="stitch" so the pattern tiles without visible seams — painted over a full-bleed rectangle. Every regenerate picks a new random seed, so the grain reshuffles each time. The finished SVG is base64-encoded with btoa into a data:image/svg+xml URI and handed straight to CSS, and the noise slider drives the opacity of the group wrapping the rectangle.

The trick that keeps it cheap: the SVG viewBox is sized to 40% of the viewport rather than its full pixels, then stretched to fill. The grain reads as slightly coarser in exchange for a filter region less than a fifth the area.

The generated noise SVG
xml
<svg viewBox="0 0 768 432" xmlns="http://www.w3.org/2000/svg">
  <filter id="noiseFilter">
    <feTurbulence type="fractalNoise" baseFrequency="2.1"
                  numOctaves="2" seed="482910" stitchTiles="stitch"/>
  </filter>
  <g opacity="0.9">
    <rect width="100%" height="100%" filter="url(#noiseFilter)"/>
  </g>
</svg>

Placing the gradients

Each gradient is a radial-gradient(farthest-corner at X% Y%, color, transparent), its color drawn at random from the active palette. The origins aren't scattered freely — every one is pinned to an edge of the viewport, so color bleeds inward from the borders and leaves the center comparatively open. That placement comes from a small helper that treats the perimeter of a 100×100 box as a single number line of length 400 and maps one random integer onto it, walking the top edge, then the right, the bottom, and the left. One integer in, one edge point out.

How the perimeter walk works
typescript
const getEdgePoint = (n, width, height) => {
  const full = 2 * width + 2 * height;
  if (n > full) n %= full;
  if (n < 0) n = full - n;

  if (n < width) return [n, 0];                 // top edge
  if (n < width + height) return [width, n - width];          // right
  if (n < 2 * width + height)
    return [n - (2 * width + height), height];  // bottom
  return [0, height - (n - (2 * width + height))];            // left
};

Controls and export

Eight palettes ship built in — Classic, Sunset, Ocean, Forest, Aurora, Fire, Cosmic, and Monochrome — alongside two sliders: gradient count (2 to 8) and noise intensity (0 to 150%). Regenerate and export are also bound to R and E. Export leans on modern-screenshot's domToPng to rasterize the live background element into a PNG download — it screenshots the actual composited DOM rather than re-deriving the effect, so what you save is exactly what's on screen.

§05

Gallery

Built with
Experiment Image Generation Preact TypeScript Visualization Web App
On this page