backgrounds
FreeSilkBackground
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.