DesignPass.dev

backgrounds

Free

SilkBackground

Full-screen WebGL veil background — a per-pixel neural network (CPPN) morphs an organic dark pattern, with hue shift, pan/zoom, morph, warp, and scanline controls. Zero dependencies.

// preview

// settings

Hue shift (deg)
Intensity
Speed
Warp amount
Scanline intensity
Scanline frequency
Offset X (pan)
Offset Y (pan)
Zoom
Morph A (network input)
Morph B (network input)
Morph C (network input)

The morph sliders nudge the three free inputs of the shader's pattern network — the same values the animation slowly oscillates. Each combination of A/B/C is a different "location" in the network's pattern space.

// source

TSJS
TailwindCSS
/*!
 * SilkBackground — a DesignPass.dev component by Ernest Liu
 * Docs & live playground: https://designpass.dev/components/silk-background
 * MIT licensed — keep this notice in copies and adaptations.
 */
"use client";

import React, { useEffect, useRef } from "react";

export interface SilkBackgroundProps {
  /** Rotates the color palette in degrees, relative to the tuned base hue. */
  hueShift?: number;
  /** Overall brightness (0 = black, 1 = default, 2 = bright). */
  intensity?: number;
  /** Animation speed multiplier. Changing it live won't jump the animation. */
  speed?: number;
  /** Sinusoidal ripple of the pattern coordinates (0 = off, 1 = strong). */
  warpAmount?: number;
  /** Pans the pattern horizontally from the base framing (positive = right). */
  offsetX?: number;
  /** Pans the pattern vertically from the base framing (positive = down). */
  offsetY?: number;
  /** Magnification relative to the base framing (1 = default, >1 zooms in). */
  zoom?: number;
  /** Morphs the pattern by nudging the first CPPN input, relative to the base. */
  morphA?: number;
  /** Nudges the second free input of the CPPN. */
  morphB?: number;
  /** Nudges the third free input of the CPPN. */
  morphC?: number;
  /** CRT scanline darkening (0-1). */
  scanlineIntensity?: number;
  /** Scanline density over screen pixels (try 30-90). */
  scanlineFrequency?: number;
  /** Render-resolution multiplier (lower = cheaper, blurrier). */
  resolutionScale?: number;
  className?: string;
}

const VERTEX_SHADER = `
attribute vec2 aPosition;
void main() {
  gl_Position = vec4(aPosition, 0.0, 1.0);
}
`;

/*
 * How the pattern is made:
 *
 * The organic, ink-like pattern comes from a CPPN (Compositional
 * Pattern-Producing Network) — a tiny 8-layer neural network evaluated
 * per pixel, right in the fragment shader. `cppn_fn` takes the pixel
 * coordinate plus three slowly-oscillating time inputs and pushes them
 * through fixed 4x4 weight matrices (`mat4(...) * buf[n]`) with sigmoid
 * activations between layers. The final layer emits RGB. The weights are
 * frozen from training, which is why they look like magic numbers — the
 * "pattern" IS the network. Changing any weight morphs the veil.
 *
 * Around that core:
 * - uWarp     — sinusoidal domain warp of the input coordinates, makes
 *               the field ripple instead of just drifting.
 * - uHueShift — rotates chroma in YIQ space (keeps luma stable, so
 *               shifting hue never changes perceived brightness).
 * - uIntensity — scalar brightness on the final color.
 * - uScan / uScanFreq — CRT scanlines: sin over gl_FragCoord.y.
 */
const FRAGMENT_SHADER = `
precision highp float;

uniform vec2 uResolution;
uniform float uTime;
uniform float uHueShift;
uniform float uIntensity;
uniform float uWarp;
uniform float uScan;
uniform float uScanFreq;
uniform vec2 uOffset;
uniform float uZoom;
uniform float uMorphA;
uniform float uMorphB;
uniform float uMorphC;

vec4 buf[8];

// Hue rotation in YIQ space: Y (luma) is untouched, I/Q (chroma) rotate.
mat3 rgb2yiq = mat3(0.299, 0.587, 0.114, 0.596, -0.274, -0.322, 0.211, -0.523, 0.312);
mat3 yiq2rgb = mat3(1.0, 0.956, 0.621, 1.0, -0.272, -0.647, 1.0, -1.106, 1.703);

vec3 hueShiftRGB(vec3 col, float deg) {
  vec3 yiq = rgb2yiq * col;
  float rad = radians(deg);
  float cosh = cos(rad), sinh = sin(rad);
  vec3 yiqShift = vec3(yiq.x, yiq.y * cosh - yiq.z * sinh, yiq.y * sinh + yiq.z * cosh);
  return clamp(yiq2rgb * yiqShift, 0.0, 1.0);
}

vec4 sigmoid(vec4 x) { return 1.0 / (1.0 + exp(-x)); }

// The CPPN. buf[6]/buf[7] hold the inputs (coordinate, time oscillators,
// distance from center); each block below is one dense layer.
vec4 cppn_fn(vec2 coordinate, float in0, float in1, float in2) {
  buf[6] = vec4(coordinate.x, coordinate.y, 0.3948333106474662 + in0, 0.36 + in1);
  buf[7] = vec4(0.14 + in2, sqrt(coordinate.x * coordinate.x + coordinate.y * coordinate.y), 0.0, 0.0);
  buf[0] = mat4(vec4(6.5404263, -3.6126034, 0.7590882, -1.13613), vec4(2.4582713, 3.1660357, 1.2219609, 0.06276096), vec4(-5.478085, -6.159632, 1.8701609, -4.7742867), vec4(6.039214, -5.542865, -0.90925294, 3.251348)) * buf[6] + mat4(vec4(0.8473259, -5.722911, 3.975766, 1.6522468), vec4(-0.24321538, 0.5839259, -1.7661959, -5.350116), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0)) * buf[7] + vec4(0.21808943, 1.1243913, -1.7969975, 5.0294676);
  buf[1] = mat4(vec4(-3.3522482, -6.0612736, 0.55641043, -4.4719114), vec4(0.8631464, 1.7432913, 5.643898, 1.6106541), vec4(2.4941394, -3.5012043, 1.7184316, 6.357333), vec4(3.310376, 8.209261, 1.1355612, -1.165539)) * buf[6] + mat4(vec4(5.24046, -13.034365, 0.009859298, 15.870829), vec4(2.987511, 3.129433, -0.89023495, -1.6822904), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0)) * buf[7] + vec4(-5.9457836, -6.573602, -0.8812491, 1.5436668);
  buf[0] = sigmoid(buf[0]); buf[1] = sigmoid(buf[1]);
  buf[2] = mat4(vec4(-15.219568, 8.095543, -2.429353, -1.9381982), vec4(-5.951362, 4.3115187, 2.6393783, 1.274315), vec4(-7.3145227, 6.7297835, 5.2473326, 5.9411426), vec4(5.0796127, 8.979051, -1.7278991, -1.158976)) * buf[6] + mat4(vec4(-11.967154, -11.608155, 6.1486754, 11.237008), vec4(2.124141, -6.263192, -1.7050359, -0.7021966), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0)) * buf[7] + vec4(-4.17164, -3.2281182, -4.576417, -3.6401186);
  buf[3] = mat4(vec4(3.1832156, -13.738922, 1.879223, 3.233465), vec4(0.64300746, 12.768129, 1.9141049, 0.50990224), vec4(-0.049295485, 4.4807224, 1.4733979, 1.801449), vec4(5.0039253, 13.000481, 3.3991797, -4.5561905)) * buf[6] + mat4(vec4(-0.1285731, 7.720628, -3.1425676, 4.742367), vec4(0.6393625, 3.714393, -0.8108378, -0.39174938), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0)) * buf[7] + vec4(-1.1811101, -21.621881, 0.7851888, 1.2329718);
  buf[2] = sigmoid(buf[2]); buf[3] = sigmoid(buf[3]);
  buf[4] = mat4(vec4(5.214916, -7.183024, 2.7228765, 2.6592617), vec4(-5.601878, -25.3591, 4.067988, 0.4602802), vec4(-10.57759, 24.286327, 21.102104, 37.546658), vec4(4.3024497, -1.9625226, 2.3458803, -1.372816)) * buf[0] + mat4(vec4(-17.6526, -10.507558, 2.2587414, 12.462782), vec4(6.265566, -502.75443, -12.642513, 0.9112289), vec4(-10.983244, 20.741234, -9.701768, -0.7635988), vec4(5.383626, 1.4819539, -4.1911616, -4.8444734)) * buf[1] + mat4(vec4(12.785233, -16.345072, -0.39901125, 1.7955981), vec4(-30.48365, -1.8345358, 1.4542528, -1.1118771), vec4(19.872723, -7.337935, -42.941723, -98.52709), vec4(8.337645, -2.7312303, -2.2927687, -36.142323)) * buf[2] + mat4(vec4(-16.298317, 3.5471997, -0.44300047, -9.444417), vec4(57.5077, -35.609753, 16.163465, -4.1534753), vec4(-0.07470326, -3.8656476, -7.0901804, 3.1523974), vec4(-12.559385, -7.077619, 1.490437, -0.8211543)) * buf[3] + vec4(-7.67914, 15.927437, 1.3207729, -1.6686112);
  buf[5] = mat4(vec4(-1.4109162, -0.372762, -3.770383, -21.367174), vec4(-6.2103205, -9.35908, 0.92529047, 8.82561), vec4(11.460242, -22.348068, 13.625772, -18.693201), vec4(-0.3429052, -3.9905605, -2.4626114, -0.45033523)) * buf[0] + mat4(vec4(7.3481627, -4.3661838, -6.3037653, -3.868115), vec4(1.5462853, 6.5488915, 1.9701879, -0.58291394), vec4(6.5858274, -2.2180402, 3.7127688, -1.3730392), vec4(-5.7973905, 10.134961, -2.3395722, -5.965605)) * buf[1] + mat4(vec4(-2.5132585, -6.6685553, -1.4029363, -0.16285264), vec4(-0.37908727, 0.53738135, 4.389061, -1.3024765), vec4(-0.70647055, 2.0111287, -5.1659346, -3.728635), vec4(-13.562562, 10.487719, -0.9173751, -2.6487076)) * buf[2] + mat4(vec4(-8.645013, 6.5546675, -6.3944063, -5.5933375), vec4(-0.57783127, -1.077275, 36.91025, 5.736769), vec4(14.283112, 3.7146652, 7.1452246, -4.5958776), vec4(2.7192075, 3.6021907, -4.366337, -2.3653464)) * buf[3] + vec4(-5.9000807, -4.329569, 1.2427121, 8.59503);
  buf[4] = sigmoid(buf[4]); buf[5] = sigmoid(buf[5]);
  buf[6] = mat4(vec4(-1.61102, 0.7970257, 1.4675229, 0.20917463), vec4(-28.793737, -7.1390953, 1.5025433, 4.656581), vec4(-10.94861, 39.66238, 0.74318546, -10.095605), vec4(-0.7229728, -1.5483948, 0.7301322, 2.1687684)) * buf[0] + mat4(vec4(3.2547753, 21.489103, -1.0194173, -3.3100595), vec4(-3.7316632, -3.3792162, -7.223193, -0.23685838), vec4(13.1804495, 0.7916005, 5.338587, 5.687114), vec4(-4.167605, -17.798311, -6.815736, -1.6451967)) * buf[1] + mat4(vec4(0.604885, -7.800309, -7.213122, -2.741014), vec4(-3.522382, -0.12359311, -0.5258442, 0.43852118), vec4(9.6752825, -22.853785, 2.062431, 0.099892326), vec4(-4.3196306, -17.730087, 2.5184598, 5.30267)) * buf[2] + mat4(vec4(-6.545563, -15.790176, -6.0438633, -5.415399), vec4(-43.591583, 28.551912, -16.00161, 18.84728), vec4(4.212382, 8.394307, 3.0958717, 8.657522), vec4(-5.0237565, -4.450633, -4.4768, -5.5010443)) * buf[3] + mat4(vec4(1.6985557, -67.05806, 6.897715, 1.9004834), vec4(1.8680354, 2.3915145, 2.5231109, 4.081538), vec4(11.158006, 1.7294737, 2.0738268, 7.386411), vec4(-4.256034, -306.24686, 8.258898, -17.132736)) * buf[4] + mat4(vec4(1.6889864, -4.5852966, 3.8534803, -6.3482175), vec4(1.3543309, -1.2640043, 9.932754, 2.9079645), vec4(-5.2770967, 0.07150358, -0.13962056, 3.3269649), vec4(28.34703, -4.918278, 6.1044083, 4.085355)) * buf[5] + vec4(6.6818056, 12.522166, -3.7075126, -4.104386);
  buf[7] = mat4(vec4(-8.265602, -4.7027016, 5.098234, 0.7509808), vec4(8.6507845, -17.15949, 16.51939, -8.884479), vec4(-4.036479, -2.3946867, -2.6055532, -1.9866527), vec4(-2.2167742, -1.8135649, -5.9759874, 4.8846445)) * buf[0] + mat4(vec4(6.7790847, 3.5076547, -2.8191125, -2.7028968), vec4(-5.743024, -0.27844876, 1.4958696, -5.0517144), vec4(13.122226, 15.735168, -2.9397483, -4.101023), vec4(-14.375265, -5.030483, -6.2599335, 2.9848232)) * buf[1] + mat4(vec4(4.0950394, -0.94011575, -5.674733, 4.755022), vec4(4.3809423, 4.8310084, 1.7425908, -3.437416), vec4(2.117492, 0.16342592, -104.56341, 16.949184), vec4(-5.22543, -2.994248, 3.8350096, -1.9364246)) * buf[2] + mat4(vec4(-5.900337, 1.7946124, -13.604192, -3.8060522), vec4(6.6583457, 31.911177, 25.164474, 91.81147), vec4(11.840538, 4.1503043, -0.7314397, 6.768467), vec4(-6.3967767, 4.034772, 6.1714606, -0.32874924)) * buf[3] + mat4(vec4(3.4992442, -196.91893, -8.923708, 2.8142626), vec4(3.4806502, -3.1846354, 5.1725626, 5.1804223), vec4(-2.4009497, 15.585794, 1.2863957, 2.0252278), vec4(-71.25271, -62.441242, -8.138444, 0.50670296)) * buf[4] + mat4(vec4(-12.291733, -11.176166, -7.3474145, 4.390294), vec4(10.805477, 5.6337385, -0.9385842, -4.7348723), vec4(-12.869276, -7.039391, 5.3029537, 7.5436664), vec4(1.4593618, 8.91898, 3.5101583, 5.840625)) * buf[5] + vec4(2.2415268, -6.705987, -0.98861027, -2.117676);
  buf[6] = sigmoid(buf[6]); buf[7] = sigmoid(buf[7]);
  buf[0] = mat4(vec4(1.6794263, 1.3817469, 2.9625452, 0.0), vec4(-1.8834411, -1.4806935, -3.5924516, 0.0), vec4(-1.3279216, -1.0918057, -2.3124623, 0.0), vec4(0.2662234, 0.23235129, 0.44178495, 0.0)) * buf[0] + mat4(vec4(-0.6299101, -0.5945583, -0.9125601, 0.0), vec4(0.17828953, 0.18300213, 0.18182953, 0.0), vec4(-2.96544, -2.5819945, -4.9001055, 0.0), vec4(1.4195864, 1.1868085, 2.5176322, 0.0)) * buf[1] + mat4(vec4(-1.2584374, -1.0552157, -2.1688404, 0.0), vec4(-0.7200217, -0.52666044, -1.438251, 0.0), vec4(0.15345335, 0.15196142, 0.272854, 0.0), vec4(0.945728, 0.8861938, 1.2766753, 0.0)) * buf[2] + mat4(vec4(-2.4218085, -1.968602, -4.35166, 0.0), vec4(-22.683098, -18.0544, -41.954372, 0.0), vec4(0.63792, 0.5470648, 1.1078634, 0.0), vec4(-1.5489894, -1.3075932, -2.6444845, 0.0)) * buf[3] + mat4(vec4(-0.49252132, -0.39877754, -0.91366625, 0.0), vec4(0.95609266, 0.7923952, 1.640221, 0.0), vec4(0.30616966, 0.15693925, 0.8639857, 0.0), vec4(1.1825981, 0.94504964, 2.176963, 0.0)) * buf[4] + mat4(vec4(0.35446745, 0.3293795, 0.59547555, 0.0), vec4(-0.58784515, -0.48177817, -1.0614829, 0.0), vec4(2.5271258, 1.9991658, 4.6846647, 0.0), vec4(0.13042648, 0.08864098, 0.30187556, 0.0)) * buf[5] + mat4(vec4(-1.7718065, -1.4033192, -3.3355875, 0.0), vec4(3.1664357, 2.638297, 5.378702, 0.0), vec4(-3.1724713, -2.6107926, -5.549295, 0.0), vec4(-2.851368, -2.249092, -5.3013067, 0.0)) * buf[6] + mat4(vec4(1.5203838, 1.2212278, 2.8404984, 0.0), vec4(1.5210563, 1.2651345, 2.683903, 0.0), vec4(2.9789467, 2.4364579, 5.2347264, 0.0), vec4(2.2270417, 1.8825914, 3.8028636, 0.0)) * buf[7] + vec4(-1.5468478, -3.6171484, 0.24762098, 0.0);
  buf[0] = sigmoid(buf[0]);
  return vec4(buf[0].x, buf[0].y, buf[0].z, 1.0);
}

void main() {
  // Map pixel coords to [-1, 1], y-down to match the original veil.
  vec2 uv = gl_FragCoord.xy / uResolution.xy * 2.0 - 1.0;
  uv.y *= -1.0;

  // Pan/zoom the window into pattern space. Positive offsets drag the
  // pattern right/down; zoom > 1 magnifies.
  uv = (uv - uOffset) / uZoom;

  // Sinusoidal domain warp — ripples the coordinate space before the CPPN.
  uv += uWarp * vec2(sin(uv.y * 6.283 + uTime * 0.5), cos(uv.x * 6.283 + uTime * 0.5)) * 0.05;

  // Three slow oscillators animate the network's extra inputs — this is
  // what makes the pattern morph over time rather than just translate.
  vec4 col = cppn_fn(
    uv,
    0.1 * sin(0.3 * uTime) + uMorphA,
    0.1 * sin(0.69 * uTime) + uMorphB,
    0.1 * sin(0.44 * uTime) + uMorphC
  );

  col.rgb = hueShiftRGB(col.rgb, uHueShift);
  col.rgb *= uIntensity;

  // CRT scanlines.
  float scanline = sin(gl_FragCoord.y * uScanFreq) * 0.5 + 0.5;
  col.rgb *= 1.0 - scanline * scanline * uScan;

  gl_FragColor = vec4(clamp(col.rgb, 0.0, 1.0), 1.0);
}
`;

// Tuned baseline for the veil. All pattern props are expressed relative to
// this base: rendering with default props produces the house look, and e.g.
// hueShift={10} means "10 degrees away from the base hue". Retune the veil
// by editing these numbers.
const BASE_HUE_SHIFT = -5;
const BASE_OFFSET_X = -0.05;
const BASE_OFFSET_Y = 0.75;
const BASE_ZOOM = 0.95;
const BASE_MORPH_A = 0.18;
const BASE_MORPH_B = -0.1;
const BASE_MORPH_C = 0.1;

function compileShader(gl: WebGLRenderingContext, type: number, source: string) {
  const shader = gl.createShader(type);
  if (!shader) return null;
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  return shader;
}

export default function SilkBackground({
  hueShift = 0,
  intensity = 1,
  speed = 0.4,
  warpAmount = 0.7,
  offsetX = 0,
  offsetY = 0,
  zoom = 1,
  morphA = 0,
  morphB = 0,
  morphC = 0,
  scanlineIntensity = 0.3,
  scanlineFrequency = 45,
  resolutionScale = 1,
  className = "",
}: SilkBackgroundProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  // Uniform values live in a ref so prop changes update live without
  // tearing down the WebGL context and restarting the animation.
  const settings = useRef({
    hueShift,
    intensity,
    speed,
    warpAmount,
    offsetX,
    offsetY,
    zoom,
    morphA,
    morphB,
    morphC,
    scanlineIntensity,
    scanlineFrequency,
    resolutionScale,
  });
  settings.current = {
    hueShift,
    intensity,
    speed,
    warpAmount,
    offsetX,
    offsetY,
    zoom,
    morphA,
    morphB,
    morphC,
    scanlineIntensity,
    scanlineFrequency,
    resolutionScale,
  };

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    let gl: WebGLRenderingContext | null = null;
    let uniforms: Record<string, WebGLUniformLocation | null> = {};
    let frame = 0;
    let running = false;
    let visible = true;
    let pageVisible = !document.hidden;
    // Accumulated shader time — advancing it by dt * speed each frame
    // lets the speed slider change without jumping the animation.
    let shaderTime = 0;
    let lastNow = performance.now();

    const init = () => {
      gl = canvas.getContext("webgl", {
        antialias: false,
        depth: false,
        stencil: false,
        powerPreference: "low-power",
      });
      if (!gl) return false;

      const vs = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
      const fs = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
      const program = gl.createProgram();
      if (!vs || !fs || !program) return false;
      gl.attachShader(program, vs);
      gl.attachShader(program, fs);
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return false;
      gl.useProgram(program);

      // One triangle that covers the whole clip space — cheaper than a quad.
      const buffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
      const aPosition = gl.getAttribLocation(program, "aPosition");
      gl.enableVertexAttribArray(aPosition);
      gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);

      uniforms = {};
      for (const name of [
        "uResolution",
        "uTime",
        "uHueShift",
        "uIntensity",
        "uWarp",
        "uOffset",
        "uZoom",
        "uMorphA",
        "uMorphB",
        "uMorphC",
        "uScan",
        "uScanFreq",
      ]) {
        uniforms[name] = gl.getUniformLocation(program, name);
      }
      return true;
    };

    const resize = () => {
      if (!gl) return;
      const dpr = Math.min(window.devicePixelRatio || 1, 2) * settings.current.resolutionScale;
      const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
      const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
      if (canvas.width !== width || canvas.height !== height) {
        canvas.width = width;
        canvas.height = height;
        gl.viewport(0, 0, width, height);
      }
    };

    const draw = () => {
      if (!gl) return;
      const s = settings.current;
      resize();
      gl.uniform2f(uniforms.uResolution, canvas.width, canvas.height);
      gl.uniform1f(uniforms.uTime, shaderTime);
      gl.uniform1f(uniforms.uHueShift, s.hueShift + BASE_HUE_SHIFT);
      gl.uniform1f(uniforms.uIntensity, s.intensity);
      gl.uniform1f(uniforms.uWarp, s.warpAmount);
      gl.uniform2f(uniforms.uOffset, s.offsetX + BASE_OFFSET_X, s.offsetY + BASE_OFFSET_Y);
      gl.uniform1f(uniforms.uZoom, Math.max(s.zoom * BASE_ZOOM, 0.01));
      gl.uniform1f(uniforms.uMorphA, s.morphA + BASE_MORPH_A);
      gl.uniform1f(uniforms.uMorphB, s.morphB + BASE_MORPH_B);
      gl.uniform1f(uniforms.uMorphC, s.morphC + BASE_MORPH_C);
      gl.uniform1f(uniforms.uScan, s.scanlineIntensity);
      gl.uniform1f(uniforms.uScanFreq, s.scanlineFrequency);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    };

    const loop = () => {
      const now = performance.now();
      shaderTime += ((now - lastNow) / 1000) * settings.current.speed;
      lastNow = now;
      draw();
      frame = requestAnimationFrame(loop);
    };

    const updateRunning = () => {
      const shouldRun = visible && pageVisible && !reducedMotion;
      if (shouldRun && !running) {
        running = true;
        lastNow = performance.now();
        frame = requestAnimationFrame(loop);
      } else if (!shouldRun && running) {
        running = false;
        cancelAnimationFrame(frame);
      }
    };

    if (!init()) return;
    resize();
    draw(); // always paint at least one frame (covers reduced motion)

    const resizeObserver = new ResizeObserver(() => {
      resize();
      if (!running) draw();
    });
    resizeObserver.observe(canvas);

    const intersectionObserver = new IntersectionObserver(([entry]) => {
      visible = entry.isIntersecting;
      updateRunning();
    });
    intersectionObserver.observe(canvas);

    const onVisibility = () => {
      pageVisible = !document.hidden;
      updateRunning();
    };
    document.addEventListener("visibilitychange", onVisibility);

    const onContextLost = (e: Event) => {
      e.preventDefault();
      running = false;
      cancelAnimationFrame(frame);
    };
    const onContextRestored = () => {
      if (init()) {
        resize();
        draw();
        updateRunning();
      }
    };
    canvas.addEventListener("webglcontextlost", onContextLost);
    canvas.addEventListener("webglcontextrestored", onContextRestored);

    updateRunning();

    return () => {
      running = false;
      cancelAnimationFrame(frame);
      resizeObserver.disconnect();
      intersectionObserver.disconnect();
      document.removeEventListener("visibilitychange", onVisibility);
      canvas.removeEventListener("webglcontextlost", onContextLost);
      canvas.removeEventListener("webglcontextrestored", onContextRestored);
      // Note: never call WEBGL_lose_context.loseContext() here. A canvas can
      // only ever produce one WebGL context, so if this effect re-runs on the
      // same canvas (route transitions, strict mode), getContext would return
      // the killed context and rendering would stay dead until a full reload.
    };
  }, []);

  return <canvas ref={canvasRef} className={`block size-full ${className}`} />;
}

// install

npx shadcn@latest add "https://designpass.dev/r/SilkBackground-TS-TW.json"

Need the license details? Read the component license.

// updates

Know when new components drop

A short email when something new lands in the library. No noise, unsubscribe anytime.