(() => { const vertexShaderSource = `#version 300 es in vec2 a_position; out vec2 v_position; void main() { v_position = a_position; gl_Position = vec4(a_position, 0.0, 1.0); } `; const fragmentShaderSource = `#version 300 es precision highp float; uniform vec2 u_viewportSize; uniform vec3 u_color1; uniform vec3 u_color2; uniform vec3 u_color3; uniform vec3 u_color4; uniform float u_colorSize; uniform float u_colorSpacing; uniform float u_colorRotation; uniform float u_colorSpread; uniform vec2 u_colorOffset; uniform float u_displacement; uniform float u_zoom; uniform float u_spacing; uniform float u_seed; uniform vec2 u_transformPosition; uniform float u_time; in vec2 v_position; out vec4 outColor; vec4 permute(vec4 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); } vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } float snoise(vec3 v) { const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); vec3 i = floor(v + dot(v, C.yyy)); vec3 x0 = v - i + dot(i, C.xxx); vec3 g = step(x0.yzx, x0.xyz); vec3 l = 1.0 - g; vec3 i1 = min(g.xyz, l.zxy); vec3 i2 = max(g.xyz, l.zxy); vec3 x1 = x0 - i1 + C.xxx; vec3 x2 = x0 - i2 + C.yyy; vec3 x3 = x0 - D.yyy; i = mod(i, 289.0); vec4 p = permute(permute(permute( i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0)); float n_ = 0.142857142857; vec3 ns = n_ * D.wyz - D.xzx; vec4 j = p - 49.0 * floor(p * ns.z * ns.z); vec4 x_ = floor(j * ns.z); vec4 y_ = floor(j - 7.0 * x_); vec4 x = x_ * ns.x + ns.yyyy; vec4 y = y_ * ns.x + ns.yyyy; vec4 h = 1.0 - abs(x) - abs(y); vec4 b0 = vec4(x.xy, y.xy); vec4 b1 = vec4(x.zw, y.zw); vec4 s0 = floor(b0) * 2.0 + 1.0; vec4 s1 = floor(b1) * 2.0 + 1.0; vec4 sh = -step(h, vec4(0.0)); vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; vec3 p0 = vec3(a0.xy, h.x); vec3 p1 = vec3(a0.zw, h.y); vec3 p2 = vec3(a1.xy, h.z); vec3 p3 = vec3(a1.zw, h.w); vec4 norm = taylorInvSqrt(vec4( dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3) )); p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; vec4 m = max(0.6 - vec4( dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3) ), 0.0); m = m * m; return 42.0 * dot(m * m, vec4( dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3) )); } vec3 noiseDerivatives(vec3 p) { float e = 0.035; float n = snoise(p); float dx = snoise(p + vec3(e, 0.0, 0.0)) - n; float dy = snoise(p + vec3(0.0, e, 0.0)) - n; float dz = snoise(p + vec3(0.0, 0.0, e)) - n; return vec3(dx, dy, dz) / e; } float grain(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } mat2 rotate2d(float angle) { float s = sin(angle); float c = cos(angle); return mat2(c, -s, s, c); } void main() { vec2 position = v_position; position.x *= min(1.0, u_viewportSize.x / u_viewportSize.y); position.y *= min(1.0, u_viewportSize.y / u_viewportSize.x); position /= u_zoom; position += u_transformPosition; vec2 noisePosition = position * 0.5 + 0.5; vec3 displacementNoise = noiseDerivatives(vec3(noisePosition, u_seed + u_time * 0.015)); position += displacementNoise.xz * u_displacement * 0.12; vec2 offsetPosition = position; offsetPosition -= u_colorOffset; offsetPosition = mod(offsetPosition - u_spacing, vec2(u_spacing * 2.0)) - u_spacing; offsetPosition = rotate2d(offsetPosition.x * 0.04 - u_colorRotation) * offsetPosition; offsetPosition /= vec2(max(u_colorSize, 0.001)); offsetPosition *= vec2(1.0 / max(u_colorSpread, 0.001), 1.0); vec3 color = vec3(0.0); color = mix(u_color1, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, u_colorSpacing * 1.5)))); color = mix(u_color2, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, u_colorSpacing * 0.5)))); color = mix(u_color3, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, -u_colorSpacing * 0.5)))); color = mix(u_color4, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, -u_colorSpacing * 1.5)))); float textureNoise = grain(v_position * u_viewportSize * 0.5 + u_time * 12.0); float vignette = smoothstep(1.1, 0.16, length(v_position * vec2(0.82, 1.0))); color += (textureNoise - 0.5) * 0.055; color *= 0.74 + vignette * 0.42; color = clamp(color, 0.0, 1.0); outColor = vec4(color, 1.0); } `; const defaultOptions = { color1: "#16254b", color2: "#23418a", color3: "#aadfd9", color4: "#e64f0f", colorSize: 0.75, colorSpacing: 0.52, colorRotation: -0.381592653589793, colorSpread: 4.52, colorOffset: [-0.7741174697875977, -0.20644775390624992], displacement: 5, seed: 0.18, position: [-0.2816110610961914, -0.43914794921875], zoom: 0.72, spacing: 4.27 }; function compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const message = gl.getShaderInfoLog(shader); gl.deleteShader(shader); throw new Error(message || "Shader compile failed"); } return shader; } function createProgram(gl) { const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { const message = gl.getProgramInfoLog(program); gl.deleteProgram(program); throw new Error(message || "Program link failed"); } return program; } function parseHexColor(value) { const hex = String(value || "").trim().replace("#", ""); const normalized = hex.length === 3 ? hex.split("").map((char) => char + char).join("") : hex.padEnd(6, "0").slice(0, 6); const int = Number.parseInt(normalized, 16); if (!Number.isFinite(int)) return [0, 0, 0]; return [ ((int >> 16) & 255) / 255, ((int >> 8) & 255) / 255, (int & 255) / 255 ]; } function parseNumber(value, fallback) { const number = Number.parseFloat(value); return Number.isFinite(number) ? number : fallback; } function parseVector(value, fallback) { if (!value) return fallback; const parts = String(value).split(",").map((part) => Number.parseFloat(part.trim())); return parts.length >= 2 && parts.every(Number.isFinite) ? [parts[0], parts[1]] : fallback; } class FluidGradient extends HTMLElement { static get observedAttributes() { return [ "color1", "color2", "color3", "color4", "colorsize", "colorspacing", "colorrotation", "colorspread", "coloroffset", "displacement", "seed", "position", "zoom", "spacing" ]; } constructor() { super(); this.canvas = document.createElement("canvas"); this.shadow = this.attachShadow({ mode: "open" }); this.shadow.innerHTML = ` `; this.shadow.append(this.canvas); this.options = structuredClone(defaultOptions); this.pointer = { x: 0.5, y: 0.5, targetX: 0.5, targetY: 0.5, displacement: defaultOptions.displacement, seed: defaultOptions.seed }; this.bounds = { width: 1, height: 1, dpr: 1 }; this.frame = 0; this.start = performance.now(); this.resizeObserver = new ResizeObserver(() => this.resize()); this.handlePointerMove = this.handlePointerMove.bind(this); this.render = this.render.bind(this); } connectedCallback() { this.gl = this.canvas.getContext("webgl2", { alpha: false, antialias: true, depth: false, preserveDrawingBuffer: false }); if (!this.gl) { this.style.background = "radial-gradient(circle at 30% 15%, #aadfd9, transparent 28%), radial-gradient(circle at 24% 78%, #e64f0f, transparent 38%), radial-gradient(circle at 70% 60%, #23418a, transparent 42%), #05090a"; return; } this.program = createProgram(this.gl); this.createGeometry(); this.collectUniforms(); this.readAttributes(); this.resizeObserver.observe(this); this.addEventListener("pointermove", this.handlePointerMove, { passive: true }); this.resize(); this.play(); } disconnectedCallback() { cancelAnimationFrame(this.frame); this.resizeObserver.disconnect(); this.removeEventListener("pointermove", this.handlePointerMove); } attributeChangedCallback() { this.readAttributes(); } createGeometry() { const gl = this.gl; const positions = new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1 ]); this.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); const positionLocation = gl.getAttribLocation(this.program, "a_position"); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); } collectUniforms() { const gl = this.gl; this.uniforms = { viewportSize: gl.getUniformLocation(this.program, "u_viewportSize"), color1: gl.getUniformLocation(this.program, "u_color1"), color2: gl.getUniformLocation(this.program, "u_color2"), color3: gl.getUniformLocation(this.program, "u_color3"), color4: gl.getUniformLocation(this.program, "u_color4"), colorSize: gl.getUniformLocation(this.program, "u_colorSize"), colorSpacing: gl.getUniformLocation(this.program, "u_colorSpacing"), colorRotation: gl.getUniformLocation(this.program, "u_colorRotation"), colorSpread: gl.getUniformLocation(this.program, "u_colorSpread"), colorOffset: gl.getUniformLocation(this.program, "u_colorOffset"), displacement: gl.getUniformLocation(this.program, "u_displacement"), seed: gl.getUniformLocation(this.program, "u_seed"), transformPosition: gl.getUniformLocation(this.program, "u_transformPosition"), zoom: gl.getUniformLocation(this.program, "u_zoom"), spacing: gl.getUniformLocation(this.program, "u_spacing"), time: gl.getUniformLocation(this.program, "u_time") }; } readAttributes() { this.options.color1 = parseHexColor(this.getAttribute("color1") || defaultOptions.color1); this.options.color2 = parseHexColor(this.getAttribute("color2") || defaultOptions.color2); this.options.color3 = parseHexColor(this.getAttribute("color3") || defaultOptions.color3); this.options.color4 = parseHexColor(this.getAttribute("color4") || defaultOptions.color4); this.options.colorSize = parseNumber(this.getAttribute("colorsize"), defaultOptions.colorSize); this.options.colorSpacing = parseNumber(this.getAttribute("colorspacing"), defaultOptions.colorSpacing); this.options.colorRotation = parseNumber(this.getAttribute("colorrotation"), defaultOptions.colorRotation); this.options.colorSpread = parseNumber(this.getAttribute("colorspread"), defaultOptions.colorSpread); this.options.colorOffset = parseVector(this.getAttribute("coloroffset"), defaultOptions.colorOffset); this.options.displacement = parseNumber(this.getAttribute("displacement"), defaultOptions.displacement); this.options.seed = parseNumber(this.getAttribute("seed"), defaultOptions.seed); this.options.position = parseVector(this.getAttribute("position"), defaultOptions.position); this.options.zoom = parseNumber(this.getAttribute("zoom"), defaultOptions.zoom); this.options.spacing = parseNumber(this.getAttribute("spacing"), defaultOptions.spacing); } resize() { if (!this.gl) return; const rect = this.getBoundingClientRect(); const dpr = Math.min(window.devicePixelRatio || 1, 2); const width = Math.max(1, Math.floor(rect.width * dpr)); const height = Math.max(1, Math.floor(rect.height * dpr)); if (this.canvas.width !== width || this.canvas.height !== height) { this.canvas.width = width; this.canvas.height = height; this.bounds = { width, height, dpr }; this.gl.viewport(0, 0, width, height); } } handlePointerMove(event) { const rect = this.getBoundingClientRect(); const x = rect.width > 0 ? (event.clientX - rect.left) / rect.width : 0.5; const y = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5; this.pointer.targetX = Math.min(1, Math.max(0, x)); this.pointer.targetY = Math.min(1, Math.max(0, y)); } play() { if (this.frame) return; this.frame = requestAnimationFrame(this.render); } render(now) { const gl = this.gl; const pointer = this.pointer; const options = this.options; pointer.x += (pointer.targetX - pointer.x) * 0.1; pointer.y += (pointer.targetY - pointer.y) * 0.1; pointer.displacement += ((pointer.x * 5) - pointer.displacement) * 0.1; pointer.seed += (((pointer.y * 2) - 1) - pointer.seed) * 0.1; gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(this.program); gl.uniform2f(this.uniforms.viewportSize, this.bounds.width, this.bounds.height); gl.uniform3fv(this.uniforms.color1, options.color1); gl.uniform3fv(this.uniforms.color2, options.color2); gl.uniform3fv(this.uniforms.color3, options.color3); gl.uniform3fv(this.uniforms.color4, options.color4); gl.uniform1f(this.uniforms.colorSize, options.colorSize); gl.uniform1f(this.uniforms.colorSpacing, options.colorSpacing); gl.uniform1f(this.uniforms.colorRotation, options.colorRotation); gl.uniform1f(this.uniforms.colorSpread, options.colorSpread); gl.uniform2fv(this.uniforms.colorOffset, options.colorOffset); gl.uniform1f(this.uniforms.displacement, pointer.displacement); gl.uniform1f(this.uniforms.seed, pointer.seed); gl.uniform2fv(this.uniforms.transformPosition, options.position); gl.uniform1f(this.uniforms.zoom, options.zoom); gl.uniform1f(this.uniforms.spacing, options.spacing); gl.uniform1f(this.uniforms.time, (now - this.start) / 1000); gl.drawArrays(gl.TRIANGLES, 0, 6); this.frame = requestAnimationFrame(this.render); } } customElements.define("fluid-gradient", FluidGradient); })();