← alle entries

noise-gradient

FC-0150Achtergrondennoise-backgroundprimitivev1.0.0

Levend verloop: drie lagen simplex-ruis mengen vier kleuren over een bodemkleur, met een vignet naar de randen, een gloed in het midden en fijne filmkorrel. WebGL, nul dependencies; kleuren zijn CSS-kleuren inclusief tokens (default --muted/--primary/--info/--background). Stopt buiten beeld en onder reduced motion.

zelfde component — andere tokens
WebGL

Een verloop dat leeft

Simplex-ruis mengt --muted, --primary, --info en --background; vignet, gloed en korrel erbovenop. Wissel van skin en herlaad de demo: het palet volgt de tokens.

primitives/backgrounds/noise-gradient.tsx
/**
 * NoiseGradient — een levend verloop: drie lagen simplex-ruis mengen vier
 * kleuren over een bodemkleur, met een vignet naar de randen, een gloed in
 * het midden en fijne filmkorrel. WebGL, nul dependencies. Kinderen renderen
 * bovenop het vlak.
 *
 * Geoogst uit 21st.dev Velaris (amanshakya307): vaste hexkleuren
 * (#86efac, #4ade80, #059669, #000) → `colors`/`bg` als willekeurige CSS-
 * kleuren INCLUSIEF var(--token) (default: --background, --muted, --primary,
 * --info, --background), geparsed via een 2D-canvas zoals shader-background;
 * shaders ongewijzigd; render-lus stopt buiten beeld en onder reduced motion
 * (dan één stilstaand frame); `height` → CSS-lengte-prop.
 */

"use client";

import { useEffect, useRef } from "react";
import type * as React from "react";

export interface NoiseGradientProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Bodemkleur; elke CSS-kleur incl. var(--token). Default var(--background). */
  bg?: string;
  /** Vier mengkleuren; elke CSS-kleur incl. var(--token). */
  colors?: readonly string[];
  /** Tempo van de ruis. Default 2. */
  speed?: number;
  /** Filmkorrel 0–1. Default 0.3. */
  grain?: number;
  /** Hoogte als CSS-lengte. Default 100vh. */
  height?: string;
}

const VERTEX = `attribute vec2 position; varying vec2 vUv; void main(){ vUv = position * 0.5 + 0.5; gl_Position = vec4(position, 0.0, 1.0); }`;
const FRAGMENT = `precision highp float; varying vec2 vUv; uniform vec2 u_resolution; uniform float u_time; uniform float u_grain; uniform vec3 u_colors[4]; uniform vec3 u_bg;
vec3 permute(vec3 x){ return mod(((x*34.0)+1.0)*x, 289.0); }
float snoise(vec2 v){ const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); vec2 i = floor(v + dot(v, C.yy)); vec2 x0 = v - i + dot(i, C.xx); vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); vec4 x12 = x0.xyxy + C.xxzz; x12.xy -= i1; i = mod(i, 289.0); vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); m = m*m; m = m*m; vec3 x = 2.0 * fract(p * C.www) - 1.0; vec3 h = abs(x) - 0.5; vec3 ox = floor(x + 0.5); vec3 a0 = x - ox; m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h); vec3 g; g.x = a0.x * x0.x + h.x * x0.y; g.yz = a0.yz * x12.xz + h.yz * x12.yw; return 130.0 * dot(m, g); }
void main(){ vec2 uv = vUv; float ratio = u_resolution.x / u_resolution.y; vec2 p = uv - 0.5; p.x *= ratio; float t = u_time * 0.1;
float n1 = snoise(p * 0.4 + vec2(t * 0.2, -t * 0.3)); float n2 = snoise(p * 0.55 + vec2(-t * 0.15, t * 0.25) + n1 * 0.25); float n3 = snoise(p * 0.75 + vec2(t * 0.1, -t * 0.2) + n2 * 0.2);
vec3 col = u_bg; float dist = length(p) * 1.5; float vignette = 1.0 - smoothstep(0.3, 1.2, dist);
col = mix(col, u_colors[0], smoothstep(-0.2, 0.5, n1) * 0.85); col = mix(col, u_colors[1], smoothstep(-0.1, 0.6, n2) * 0.7); col = mix(col, u_colors[2], smoothstep(-0.3, 0.4, n3) * 0.6); col = mix(col, u_colors[3], smoothstep(0.0, 0.7, n1 * n2) * 0.5);
float glow = smoothstep(0.8, 0.0, dist) * 0.3; col += u_colors[1] * glow; col = mix(col * 0.2, col, vignette);
float grain = fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453 + u_time); col += (grain - 0.5) * u_grain * 0.1; gl_FragColor = vec4(col, 1.0); }`;

const STANDAARD = ["var(--muted)", "var(--primary)", "var(--info)", "var(--background)"];

/** Elke CSS-kleur naar rgb 0–1, met het element als context voor var(--token); een 2D-canvas parset. */
function naarRgb(kleur: string, context: Element): [number, number, number] {
  const doek = document.createElement("canvas");
  doek.width = doek.height = 1;
  const ctx = doek.getContext("2d", { willReadFrequently: true });
  if (!ctx) return [0, 0, 0];
  let css = kleur.trim();
  const m = css.match(/^var\(\s*(--[a-zA-Z0-9-]+)\s*\)$/);
  if (m) css = getComputedStyle(context).getPropertyValue(m[1]!).trim();
  ctx.fillStyle = css || "black";
  ctx.fillRect(0, 0, 1, 1);
  const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
  return [r! / 255, g! / 255, b! / 255];
}

export function NoiseGradient({ bg = "var(--background)", colors = STANDAARD, speed = 2, grain = 0.3, height = "100vh", className, style, children, ...rest }: NoiseGradientProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const kleurSleutel = colors.join("|");

  useEffect(() => {
    const canvas = canvasRef.current;
    const container = containerRef.current;
    if (!canvas || !container) return;
    const gl = canvas.getContext("webgl");
    if (!gl) return;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)");

    const shader = (type: number, src: string) => { const s = gl.createShader(type)!; gl.shaderSource(s, src); gl.compileShader(s); return s; };
    const program = gl.createProgram()!;
    gl.attachShader(program, shader(gl.VERTEX_SHADER, VERTEX));
    gl.attachShader(program, shader(gl.FRAGMENT_SHADER, FRAGMENT));
    gl.linkProgram(program);
    gl.useProgram(program);
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
    const pos = gl.getAttribLocation(program, "position");
    gl.enableVertexAttribArray(pos);
    gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
    const locs = { res: gl.getUniformLocation(program, "u_resolution"), time: gl.getUniformLocation(program, "u_time"), grain: gl.getUniformLocation(program, "u_grain"), colors: gl.getUniformLocation(program, "u_colors"), bg: gl.getUniformLocation(program, "u_bg") };

    // Kleuren één keer oplossen (tokens van het project → rgb) en als uniform zetten.
    const vier = [...colors.slice(0, 4)];
    while (vier.length < 4) vier.push(vier[vier.length - 1] ?? "var(--background)");
    const flat = new Float32Array(vier.flatMap((k) => naarRgb(k, container)));
    const bodem = naarRgb(bg, container);
    gl.uniform3fv(locs.colors, flat);
    gl.uniform3f(locs.bg, bodem[0], bodem[1], bodem[2]);
    gl.uniform1f(locs.grain, grain);

    const resize = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = Math.max(1, container.clientWidth * dpr);
      canvas.height = Math.max(1, container.clientHeight * dpr);
      gl.viewport(0, 0, canvas.width, canvas.height);
    };
    const ro = new ResizeObserver(resize);
    ro.observe(container);

    let raf = 0, zichtbaar = true;
    const frame = (t: number) => {
      gl.uniform2f(locs.res, canvas.width, canvas.height);
      gl.uniform1f(locs.time, t * 0.001 * speed);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    };
    const render = (t: number) => {
      raf = 0;
      frame(t);
      if (zichtbaar && !reduce.matches && !document.hidden) raf = requestAnimationFrame(render);
    };
    const wek = () => { if (!raf) raf = requestAnimationFrame(render); };
    const io = new IntersectionObserver(([e]) => { zichtbaar = e?.isIntersecting ?? true; if (zichtbaar) wek(); }, { threshold: 0 });
    io.observe(container);
    document.addEventListener("visibilitychange", wek);
    reduce.addEventListener("change", wek);
    resize();
    wek();
    return () => {
      ro.disconnect(); io.disconnect();
      document.removeEventListener("visibilitychange", wek);
      reduce.removeEventListener("change", wek);
      cancelAnimationFrame(raf);
    };
  }, [bg, kleurSleutel, speed, grain]); // eslint-disable-line react-hooks/exhaustive-deps -- colors via kleurSleutel

  return (
    <div ref={containerRef} {...rest} className={["fp-noise", className].filter(Boolean).join(" ")} style={{ ...style, height }}>
      <canvas ref={canvasRef} className="fp-noise__doek" aria-hidden="true" />
      <div className="fp-noise__inhoud">{children}</div>
    </div>
  );
}
primitives/backgrounds/backgrounds.css
/**
 * Achtergronden II — importeren in de app-root (registry-site: app/globals.css).
 * Token-contract 2. Canvas-vlakken die inhoud dragen: het vlak zelf is het
 * raster, de kinderen liggen erbovenop.
 */

/* ── sonar-grid ─────────────────────────────────────────────────────────── */
.fp-sonar { position: relative; isolation: isolate; overflow: hidden; width: 100%; }
.fp-sonar--interactief { cursor: crosshair; }
/* De stippenkleur is currentColor van het doek: standaard de merkkleur, via `color` te overschrijven. */
.fp-sonar__doek { position: absolute; inset: 0; z-index: -1; width: 100%; height: 100%; color: var(--primary); pointer-events: none; }

/* ── noise-gradient ─────────────────────────────────────────────────────── */
.fp-noise { position: relative; width: 100%; overflow: hidden; }
.fp-noise__doek { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.fp-noise__inhoud { position: relative; z-index: 1; width: 100%; height: 100%; }