---
name: generative-art-background
description: Use when you want "Artistic, unique, exotic" - Creates abstract visuals algorithmically in real time.
---

# Generative Art Background

> **Category:** Advanced WebGL / Canvas  -  **Personality:** Artistic, unique, exotic
>
> **Best use:** Creative portfolios, experimental brands
>
> **Ratings:** Professional 3/5 - Casual 3/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 5/5 - Performance cost 5/5

## What it is

A piece of code that *paints itself*. Hundreds of invisible "agents" wander across a 2D canvas, and at every point each agent reads an angle from a **flow field** (a grid of directions derived from layered value-noise) and steps that way, leaving a faint translucent stroke. Thousands of overlapping strokes accumulate into an organic, never-quite-repeating artwork that slowly evolves. Because the whole field is derived from a single numeric **seed**, the same seed always reproduces the same piece — so "art" becomes shareable and parameterised. Perfect as a living hero backdrop for a portfolio or an experimental brand.

## Dependencies / CDN

**None - vanilla JS.** Pure Canvas 2D and a hand-rolled value-noise function — no three.js, no noise library. The demo only loads two display fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,opsz,wght@0,6..96,600;0,6..96,700;1,6..96,600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
```

## HTML

A framed stage holding the canvas, a decorative vignette, and the control dock:

```html
<div class="ga-stage" id="gaStage">
  <canvas class="ga-canvas" id="gaCanvas" role="img"
          aria-label="Generative flow-field artwork, regenerated from a numeric seed"></canvas>
  <div class="ga-frame" aria-hidden="true"></div>
  <div class="ga-ui">
    <div class="ga-top"> ... brand + live "Edition" readout ... </div>
    <div class="ga-dock ga-plate">
      <div class="ga-group"><span class="ga-label">Palette</span><div class="ga-swatches" id="gaSw"></div></div>
      <div class="ga-group"><span class="ga-label">Seed</span>
        <input class="ga-seed" id="gaSeed" type="text" inputmode="numeric" value="4821"></div>
      <div class="ga-group"><span class="ga-label">Flow</span>
        <input class="ga-slider" id="gaFlow" type="range" min="0" max="100" value="36"></div>
      <button class="ga-regen" id="gaRegen" type="button">&#8635; Regenerate</button>
    </div>
  </div>
</div>
```

## CSS

The effect itself needs no CSS — only the stage framing and the floating UI. The two lines that *make* the look are `position:absolute; inset:0` on the canvas and `overflow:hidden` on a rounded stage:

```css
.ga-stage{position:relative; border-radius:26px; overflow:hidden; min-height:620px;
  background:#08080a; isolation:isolate; color:#ece6da;
  font-family:'DM Mono',ui-monospace,monospace}
.ga-canvas{position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0}

/* decorative: a vignette frame above the art, below the UI */
.ga-frame{position:absolute; inset:0; z-index:1; pointer-events:none; border-radius:26px;
  box-shadow:inset 0 0 0 1px rgba(236,230,218,.07), inset 0 0 150px 24px rgba(0,0,0,.5);
  background:radial-gradient(130% 120% at 50% -10%, transparent 58%, rgba(0,0,0,.42))}

/* UI floats on top; only the controls capture clicks */
.ga-ui{position:absolute; inset:0; z-index:2; display:flex; flex-direction:column;
  justify-content:space-between; padding:24px; pointer-events:none}
.ga-dock{pointer-events:auto; display:flex; flex-wrap:wrap; align-items:center; gap:13px 20px;
  padding:14px 16px; border-radius:16px;
  background:rgba(9,9,12,.46); -webkit-backdrop-filter:blur(9px); backdrop-filter:blur(9px);
  border:1px solid rgba(236,230,218,.16)}
.ga-regen{background:#ece6da; color:#0b0b0d; border:0; border-radius:999px; padding:11px 21px;
  cursor:pointer; transition:transform .18s, box-shadow .25s}
.ga-regen:hover{transform:translateY(-2px); box-shadow:0 14px 32px -12px rgba(236,230,218,.55)}
```

## JavaScript

The whole engine. Seeded noise → flow field → particles → translucent additive strokes. Reduced motion paints **one** finished frame instead of looping.

```js
const stage=document.getElementById('gaStage'), canvas=document.getElementById('gaCanvas');
const ctx=canvas.getContext('2d');
let seed=4821, noiseScale=0.0023, gPhase=0, cssW=0, cssH=0, pal, rng, parts=[], P=0, raf=0, gStart=0;
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const PAL = {bg:[6,12,20], colors:['#3ea6ff','#7fd4ff','#1e63d6','#bfe9ff','#2ec4b6']}; // one of several

// --- seeded math: mulberry32 PRNG + integer-hashed value-noise (deterministic per seed) ---
function mulberry32(a){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;};}
function hash2(ix,iy){let n=Math.imul(ix|0,374761393)+Math.imul(iy|0,668265263)+Math.imul(seed|0,1274126177)|0;
  n=Math.imul(n^n>>>13,1274126177); n^=n>>>16; return (n>>>0)/4294967296;}
function smooth(t){return t*t*t*(t*(t*6-15)+10);}                 // smootherstep
function vnoise(x,y){const x0=Math.floor(x),y0=Math.floor(y),xf=x-x0,yf=y-y0,
  v00=hash2(x0,y0),v10=hash2(x0+1,y0),v01=hash2(x0,y0+1),v11=hash2(x0+1,y0+1),
  u=smooth(xf),w=smooth(yf),a=v00+(v10-v00)*u,b=v01+(v11-v01)*u; return a+(b-a)*w;}
function fbm(x,y){let v=0,amp=.6,f=1; for(let i=0;i<3;i++){v+=amp*vnoise(x*f,y*f); f*=2; amp*=.5;} return v;}
function field(x,y){return fbm(x*noiseScale,y*noiseScale)*Math.PI*3 + gPhase;} // angle (rad)

function respawn(p){p.x=rng()*cssW; p.y=rng()*cssH; p.spd=.7+rng()*1.5; p.w=.6+rng()*1.3;
  p.alpha=.025+rng()*.055; p.life=(50+rng()*200)|0; p.color=PAL.colors[(rng()*PAL.colors.length)|0];}

function fit(){ cssW=stage.clientWidth; cssH=stage.clientHeight;
  const dpr=Math.min(devicePixelRatio||1,2);                      // cap DPR for perf
  canvas.width=Math.round(cssW*dpr); canvas.height=Math.round(cssH*dpr);
  ctx.setTransform(dpr,0,0,dpr,0,0); }                            // draw in CSS px, crisp on HiDPI

function init(){ rng=mulberry32(seed>>>0);
  P=Math.max(380,Math.min(1200,Math.round(cssW*cssH/1600)));      // density scales with area
  parts=Array.from({length:P},()=>{const p={};respawn(p);return p;});
  ctx.globalCompositeOperation='source-over'; ctx.fillStyle=`rgb(${PAL.bg})`; ctx.fillRect(0,0,cssW,cssH); }

function stepOnce(){ ctx.lineCap='round';
  for(const p of parts){ const a=field(p.x,p.y), nx=p.x+Math.cos(a)*p.spd, ny=p.y+Math.sin(a)*p.spd;
    ctx.strokeStyle=p.color; ctx.globalAlpha=p.alpha; ctx.lineWidth=p.w;
    ctx.beginPath(); ctx.moveTo(p.x,p.y); ctx.lineTo(nx,ny); ctx.stroke();
    p.x=nx; p.y=ny;
    if(--p.life<=0 || nx<-12||nx>cssW+12||ny<-12||ny>cssH+12) respawn(p); } }

function loop(){ gPhase=(performance.now()-gStart)*0.00017;        // slow field drift = "evolving"
  ctx.globalCompositeOperation='source-over'; ctx.globalAlpha=1;
  ctx.fillStyle=`rgba(${PAL.bg},0.004)`; ctx.fillRect(0,0,cssW,cssH); // faint veil bounds the build-up
  ctx.globalCompositeOperation='lighter'; stepOnce();               // additive glow
  ctx.globalCompositeOperation='source-over'; raf=requestAnimationFrame(loop); }

function renderStatic(){ gPhase=0; ctx.globalCompositeOperation='lighter';
  for(let i=0;i<280;i++) stepOnce();                               // one finished piece, no loop
  ctx.globalCompositeOperation='source-over'; }

function restart(){ cancelAnimationFrame(raf); init();
  if(reduce) renderStatic(); else { gStart=performance.now(); loop(); } }

fit(); restart();
new ResizeObserver(()=>{ fit(); restart(); }).observe(stage);       // re-fit DPR buffer on resize
// Controls: Regenerate -> seed=random; restart().  Seed input -> seed=value; restart().
// Palette swatch -> swap PAL; restart().  Flow slider -> noiseScale = 0.0010 + t*0.0036.
```

## How it works

- **Flow field.** `field(x,y)` turns a smooth noise value into an **angle**. Walk a particle in that direction and it traces a streamline; do it for hundreds of particles and you reveal the hidden currents of the noise.
- **Seeded, layered value-noise.** `hash2` mixes the integer grid coordinates *and the seed* into a pseudo-random value; `vnoise` bilinearly interpolates the four corners with smootherstep; `fbm` stacks three octaves for richer, more organic curves. Same seed → identical field → reproducible art.
- **Translucent additive strokes.** Each particle paints a short, low-`alpha` segment with `globalCompositeOperation='lighter'`. Overlapping strokes *add* light, so dense currents glow while sparse areas stay dark.
- **Bounded build-up.** Instead of clearing each frame, the loop lays down a near-transparent background veil (`rgba(bg, 0.004)`). New ink keeps accumulating, but the veil caps the brightness at an equilibrium — the piece looks like it is forever *building and evolving* without ever blowing out to white.
- **Gentle evolution.** `gPhase` (driven by `performance.now()`) slowly rotates the whole field, so the currents drift and the artwork is never frozen.

## Customization

- **Curliness:** the `* Math.PI * 3` multiplier in `field()` sets how many turns the angle spans — lower = long lazy meanders, higher = tight curls.
- **Scale (the "Flow" slider):** `noiseScale` (~0.001–0.005) zooms the noise. Small = broad sweeping rivers; large = fine turbulent detail.
- **Density:** the `cssW*cssH/1600` divisor sets particle count; the `0.004` veil alpha sets how dense the equilibrium gets (smaller veil = richer, brighter build-up).
- **Palette:** any array of hex colors over a dark `bg`. Keep the background near-black so `lighter` blending reads as glow.
- **Texture:** raise `p.alpha` for bolder ink, widen `p.w` for a painterly look, or shorten `p.life` for shorter dashes.

## Accessibility & performance

- **Reduced motion:** check `matchMedia('(prefers-reduced-motion: reduce)')`; if set, call `renderStatic()` (a fixed ~280-iteration build) **once** and never start `requestAnimationFrame`. The catalog's global reduced-motion rule also neutralises CSS transitions.
- **DPR-aware & capped:** size the backing store to `clientWidth * devicePixelRatio` (capped at 2) and `setTransform(dpr,…)` so you draw in CSS pixels and stay crisp without quadrupling the pixel cost on 3x phones.
- **Cheap per frame:** one short line per particle (~380–1200 total) at 60fps. No per-pixel work, no library. `ResizeObserver` re-fits the buffer (debounce it in production).
- The canvas is decorative; it carries `role="img"` + `aria-label`, and all real interaction lives in labelled `<button>`/`<input>` controls.

## Gotchas

- **`lighter` + no clear = potential white-out.** Additive blending with full accumulation eventually saturates to white. The fade veil (`rgba(bg, 0.004)`) is what bounds it — verified `whiteFrac = 0` at equilibrium across every palette. If you drop the veil for a "finished print" look, **cap the iteration count** instead (that is exactly what `renderStatic()` does).
- **Set the transform, not width/height with CSS.** Sizing the canvas via CSS only stretches a low-res buffer (blurry). Always set `canvas.width/height` to the device-pixel size and `setTransform` the DPR.
- **Seed must feed the noise, not just the PRNG.** If `hash2` ignores `seed`, "Regenerate" only reshuffles spawn points while the underlying field stays identical — every piece looks the same. The seed has to be mixed into the spatial hash.
- **Reset `globalAlpha` / `globalCompositeOperation`.** Leaving `lighter` or a low `globalAlpha` set will silently corrupt the next thing you draw (e.g. the background fill).
- **`isolation:isolate` on the rounded stage** keeps the `overflow:hidden` clip and any `backdrop-filter` UI from sampling pixels outside the frame.
