UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

562 lines (527 loc) 19.5 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useRef, useState, useEffect } from 'react'; import { cn } from '../../lib/utilsComprehensive.js'; import { LIQUID_GLASS } from '../../tokens/glass.js'; // WebGL shader sources for Liquid Glass effects const VERTEX_SHADER = ` attribute vec2 position; attribute vec2 texCoord; varying vec2 vTexCoord; varying vec2 vPosition; uniform mat4 uProjection; uniform mat4 uModelView; uniform float uTime; uniform vec2 uResolution; void main() { vTexCoord = texCoord; vPosition = position; gl_Position = uProjection * uModelView * vec4(position, 0.0, 1.0); } `; const FRAGMENT_SHADER = ` precision highp float; varying vec2 vTexCoord; varying vec2 vPosition; uniform sampler2D uBackdropTexture; uniform sampler2D uNormalMap; uniform vec2 uResolution; uniform float uTime; uniform float uIOR; uniform float uThickness; uniform float uSheen; uniform vec2 uTilt; uniform bool uEnableRefraction; uniform bool uEnableReflection; uniform bool uEnableParallax; // IOR-based refraction calculation vec2 refract2D(vec2 incident, vec2 normal, float ior) { float cosI = dot(incident, normal); float sinT2 = (1.0 - cosI * cosI) / (ior * ior); if (sinT2 > 1.0) { // Total internal reflection return reflect(incident, normal); } float cosT = sqrt(1.0 - sinT2); return incident / ior - normal * (cosT - cosI / ior); } // Generate procedural normal map for glass thickness vec3 generateNormal(vec2 uv, float thickness, float time) { vec2 offset = vec2(0.001) * thickness; // Create subtle surface variation float height1 = sin(uv.x * 20.0 + time * 0.5) * cos(uv.y * 15.0 + time * 0.3) * 0.1; float height2 = sin(uv.x * 35.0 - time * 0.7) * cos(uv.y * 25.0 - time * 0.4) * 0.05; float heightL = sin((uv.x - offset.x) * 20.0 + time * 0.5) * cos(uv.y * 15.0 + time * 0.3) * 0.1; float heightR = sin((uv.x + offset.x) * 20.0 + time * 0.5) * cos(uv.y * 15.0 + time * 0.3) * 0.1; float heightT = sin(uv.x * 20.0 + time * 0.5) * cos((uv.y + offset.y) * 15.0 + time * 0.3) * 0.1; float heightB = sin(uv.x * 20.0 + time * 0.5) * cos((uv.y - offset.y) * 15.0 + time * 0.3) * 0.1; vec3 normal; normal.x = (heightL - heightR) / (2.0 * offset.x); normal.y = (heightB - heightT) / (2.0 * offset.y); normal.z = 1.0; return normalize(normal); } // Fresnel reflectance calculation float fresnel(vec3 incident, vec3 normal, float ior) { float cosI = abs(dot(incident, normal)); float sinI = sqrt(1.0 - cosI * cosI); float sinT = sinI / ior; if (sinT >= 1.0) { return 1.0; // Total internal reflection } float cosT = sqrt(1.0 - sinT * sinT); float rs = (ior * cosI - cosT) / (ior * cosI + cosT); float rp = (ior * cosT - cosI) / (ior * cosT + cosI); return (rs * rs + rp * rp) * 0.5; } void main() { vec2 uv = vTexCoord; vec2 screenUV = gl_FragCoord.xy / uResolution; // Generate surface normal based on thickness vec3 normal = generateNormal(uv, uThickness * 0.1, uTime); // Apply device tilt to normal normal.xy += uTilt * 0.3; normal = normalize(normal); vec4 color = vec4(0.0); if (uEnableRefraction) { // Calculate refracted sampling coordinates vec2 incident = normalize(vec2(0.0, -1.0)); vec2 refracted = refract2D(incident, normal.xy, 1.0 / uIOR); // Sample backdrop with refraction offset vec2 refractedUV = screenUV + refracted * uThickness * 0.01; refractedUV = clamp(refractedUV, 0.0, 1.0); vec4 refractedColor = texture2D(uBackdropTexture, refractedUV); color += refractedColor * 0.7; } else { // Fallback: direct backdrop sampling color += texture2D(uBackdropTexture, screenUV) * 0.7; } if (uEnableReflection) { // Calculate reflection vec3 viewDir = normalize(vec3(screenUV - 0.5, -1.0)); vec3 reflected = reflect(viewDir, normal); // Sample environment for reflection (simplified) vec2 reflectionUV = screenUV + reflected.xy * 0.05; reflectionUV = clamp(reflectionUV, 0.0, 1.0); vec4 reflectionColor = texture2D(uBackdropTexture, reflectionUV); // Apply Fresnel for realistic reflection strength float fresnelStrength = fresnel(viewDir, normal, uIOR); color += reflectionColor * fresnelStrength * 0.3; } // Edge sheen effect if (uSheen > 0.0) { float edgeDistance = min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y)); float sheenStrength = 1.0 - smoothstep(0.0, 0.1, edgeDistance); vec3 sheenColor = vec3(1.0, 1.0, 1.0); color.rgb += sheenColor * sheenStrength * uSheen * 0.2; } // Glass tint overlay vec4 glassTint = vec4(1.0, 1.0, 1.0, 0.95); color = mix(color, glassTint, 0.15); // Apply thickness-based opacity color.a = 0.85 + uThickness * 0.02; gl_FragColor = color; } `; // WebGL capability detection and context management class LiquidGlassGPUDriver { constructor() { this.gl = null; this.program = null; this.canvas = null; this.uniforms = {}; this.attributes = {}; this.isInitialized = false; this.animationFrameId = null; this.backdropTexture = null; } /** * Initialize WebGL context and shaders */ async initialize(canvas) { try { this.canvas = canvas; this.gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!this.gl) { console.warn('LiquidGlassGPU: WebGL not supported'); return false; } // Check for required extensions const requiredExtensions = ['OES_texture_float', 'WEBGL_color_buffer_float']; for (const ext of requiredExtensions) { if (!this.gl.getExtension(ext)) { console.warn(`LiquidGlassGPU: Required extension ${ext} not available`); } } // Create shader program this.program = this.createShaderProgram(VERTEX_SHADER, FRAGMENT_SHADER); if (!this.program) { return false; } // Get uniform and attribute locations this.setupUniformsAndAttributes(); // Set up geometry this.setupGeometry(); this.isInitialized = true; return true; } catch (error) { console.error('LiquidGlassGPU: Initialization failed', error); return false; } } /** * Render liquid glass effect with given parameters */ render(params) { if (!this.gl || !this.program || !this.isInitialized) { return; } const gl = this.gl; // Clear and set up viewport gl.clear(gl.COLOR_BUFFER_BIT); gl.viewport(0, 0, this.canvas.width, this.canvas.height); // Use shader program gl.useProgram(this.program); // Update uniforms this.updateUniforms(params); // Draw quad gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } /** * Update backdrop texture from DOM element */ async updateBackdrop(element) { if (!this.gl || !this.isInitialized) return; try { // Capture element as image data const imageData = await this.captureElementAsImageData(element); if (!imageData) return; // Update texture if (!this.backdropTexture) { this.backdropTexture = this.gl.createTexture(); } this.gl.bindTexture(this.gl.TEXTURE_2D, this.backdropTexture); this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, imageData); // Set texture parameters this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); } catch (error) { console.warn('LiquidGlassGPU: Failed to update backdrop', error); } } /** * Clean up WebGL resources */ dispose() { if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } if (this.gl) { if (this.program) { this.gl.deleteProgram(this.program); } if (this.backdropTexture) { this.gl.deleteTexture(this.backdropTexture); } } this.isInitialized = false; } /** * Check if GPU acceleration is available */ static isSupported() { try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); return !!gl; } catch { return false; } } // Private helper methods createShaderProgram(vertexSource, fragmentSource) { if (!this.gl) return null; const vertexShader = this.compileShader(vertexSource, this.gl.VERTEX_SHADER); const fragmentShader = this.compileShader(fragmentSource, this.gl.FRAGMENT_SHADER); if (!vertexShader || !fragmentShader) { return null; } const program = this.gl.createProgram(); if (!program) return null; this.gl.attachShader(program, vertexShader); this.gl.attachShader(program, fragmentShader); this.gl.linkProgram(program); if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { console.error('LiquidGlassGPU: Shader program linking failed:', this.gl.getProgramInfoLog(program)); this.gl.deleteProgram(program); return null; } return program; } compileShader(source, type) { if (!this.gl) return null; const shader = this.gl.createShader(type); if (!shader) return null; this.gl.shaderSource(shader, source); this.gl.compileShader(shader); if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { console.error('LiquidGlassGPU: Shader compilation failed:', this.gl.getShaderInfoLog(shader)); this.gl.deleteShader(shader); return null; } return shader; } setupUniformsAndAttributes() { if (!this.gl || !this.program) return; // Get uniform locations const uniformNames = ['uBackdropTexture', 'uNormalMap', 'uResolution', 'uTime', 'uIOR', 'uThickness', 'uSheen', 'uTilt', 'uEnableRefraction', 'uEnableReflection', 'uEnableParallax', 'uProjection', 'uModelView']; for (const name of uniformNames) { this.uniforms[name] = this.gl.getUniformLocation(this.program, name); } // Get attribute locations this.attributes.position = this.gl.getAttribLocation(this.program, 'position'); this.attributes.texCoord = this.gl.getAttribLocation(this.program, 'texCoord'); } setupGeometry() { if (!this.gl) return; // Create quad geometry const positions = new Float32Array([-1, -1, // bottom-left 1, -1, // bottom-right -1, 1, // top-left 1, 1 // top-right ]); const texCoords = new Float32Array([0, 0, // bottom-left 1, 0, // bottom-right 0, 1, // top-left 1, 1 // top-right ]); // Create and bind vertex buffer const positionBuffer = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW); this.gl.enableVertexAttribArray(this.attributes.position); this.gl.vertexAttribPointer(this.attributes.position, 2, this.gl.FLOAT, false, 0, 0); // Create and bind texture coordinate buffer const texCoordBuffer = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, texCoordBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, texCoords, this.gl.STATIC_DRAW); this.gl.enableVertexAttribArray(this.attributes.texCoord); this.gl.vertexAttribPointer(this.attributes.texCoord, 2, this.gl.FLOAT, false, 0, 0); } updateUniforms(params) { if (!this.gl || !this.canvas) return; // Update scalar uniforms if (this.uniforms.uIOR) { this.gl.uniform1f(this.uniforms.uIOR, params.ior); } if (this.uniforms.uThickness) { this.gl.uniform1f(this.uniforms.uThickness, params.thickness); } if (this.uniforms.uSheen) { this.gl.uniform1f(this.uniforms.uSheen, params.sheen); } if (this.uniforms.uTime) { this.gl.uniform1f(this.uniforms.uTime, params.time); } // Update vector uniforms if (this.uniforms.uResolution) { this.gl.uniform2f(this.uniforms.uResolution, this.canvas.width, this.canvas.height); } if (this.uniforms.uTilt) { this.gl.uniform2f(this.uniforms.uTilt, params.tilt.x, params.tilt.y); } // Update boolean uniforms if (this.uniforms.uEnableRefraction) { this.gl.uniform1i(this.uniforms.uEnableRefraction, params.enableRefraction ? 1 : 0); } if (this.uniforms.uEnableReflection) { this.gl.uniform1i(this.uniforms.uEnableReflection, params.enableReflection ? 1 : 0); } if (this.uniforms.uEnableParallax) { this.gl.uniform1i(this.uniforms.uEnableParallax, params.enableParallax ? 1 : 0); } // Bind backdrop texture if (this.uniforms.uBackdropTexture && this.backdropTexture) { this.gl.activeTexture(this.gl.TEXTURE0); this.gl.bindTexture(this.gl.TEXTURE_2D, this.backdropTexture); this.gl.uniform1i(this.uniforms.uBackdropTexture, 0); } } async captureElementAsImageData(element) { try { // In a real implementation, this would use various techniques: // - html2canvas for DOM elements // - OffscreenCanvas for better performance // - canvas.transferControlToOffscreen for worker threads // For now, create a simple placeholder const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return null; canvas.width = element.clientWidth || 256; canvas.height = element.clientHeight || 256; // Create gradient placeholder that simulates backdrop const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); gradient.addColorStop(0, '#4f46e5'); gradient.addColorStop(0.5, '#7c3aed'); gradient.addColorStop(1, '#ec4899'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); return ctx.getImageData(0, 0, canvas.width, canvas.height); } catch (error) { console.warn('LiquidGlassGPU: Failed to capture element', error); return null; } } } const LiquidGlassGPURenderer = ({ material = 'liquid', variant = 'regular', ior = LIQUID_GLASS.material.ior.liquid, thickness = LIQUID_GLASS.material.thickness.medium, sheen = LIQUID_GLASS.material.sheen.subtle, enableRefraction = true, enableReflection = true, enableParallax = false, adaptToMotion = true, className, children, onFallback }) => { const canvasRef = useRef(null); const containerRef = useRef(null); const gpuRef = useRef(null); const [isGPUSupported, setIsGPUSupported] = useState(false); const [deviceTilt, setDeviceTilt] = useState({ x: 0, y: 0 }); const startTimeRef = useRef(Date.now()); // Initialize GPU renderer useEffect(() => { const initializeGPU = async () => { if (!canvasRef.current) return; const isSupported = LiquidGlassGPUDriver.isSupported(); setIsGPUSupported(isSupported); if (!isSupported) { onFallback?.(); return; } gpuRef.current = new LiquidGlassGPUDriver(); const success = await gpuRef.current.initialize(canvasRef.current); if (!success) { setIsGPUSupported(false); onFallback?.(); } }; initializeGPU(); return () => { gpuRef.current?.dispose(); }; }, [onFallback]); // Device motion tracking useEffect(() => { if (!adaptToMotion || !isGPUSupported) return; const handleOrientation = event => { if (event.beta !== null && event.gamma !== null) { const sensitivity = LIQUID_GLASS.motionFluency.tilt.sensitivity; setDeviceTilt({ x: Math.max(-1, Math.min(1, event.gamma * sensitivity)), y: Math.max(-1, Math.min(1, event.beta * sensitivity)) }); } }; if (typeof DeviceOrientationEvent !== 'undefined') { window.addEventListener('deviceorientation', handleOrientation); return () => window.removeEventListener('deviceorientation', handleOrientation); } }, [adaptToMotion, isGPUSupported]); // Animation loop useEffect(() => { if (!isGPUSupported || !gpuRef.current) return; let animationId; const animate = () => { if (gpuRef.current && canvasRef.current) { const time = (Date.now() - startTimeRef.current) / 1000; gpuRef.current.render({ ior, thickness, sheen, tilt: deviceTilt, enableRefraction, enableReflection, enableParallax, time }); // Update backdrop periodically if (containerRef.current && Math.floor(time * 10) % 5 === 0) { gpuRef.current.updateBackdrop(containerRef.current); } } animationId = requestAnimationFrame(animate); }; animate(); return () => { if (animationId) { cancelAnimationFrame(animationId); } }; }, [isGPUSupported, ior, thickness, sheen, deviceTilt, enableRefraction, enableReflection, enableParallax]); // Resize canvas to match container useEffect(() => { if (!canvasRef.current || !containerRef.current) return; const resizeCanvas = () => { const canvas = canvasRef.current; const container = containerRef.current; const rect = container.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; canvas.style.width = `${rect.width}px`; canvas.style.height = `${rect.height}px`; }; resizeCanvas(); window.addEventListener('resize', resizeCanvas); return () => window.removeEventListener('resize', resizeCanvas); }, []); if (!isGPUSupported) { // Fallback to CSS-based rendering return jsx("div", { "data-glass-component": true, ref: containerRef, className: cn('glass-surface-primary glass-blur-backdrop glass-relative glass-overflow-hidden', className), style: { // Use createGlassStyle() instead, // Use createGlassStyle() instead, background: `linear-gradient(135deg, rgba(255,255,255,${0.15 + sheen * 0.05}) 0%, rgba(255,255,255,${0.05 + sheen * 0.02}) 100%)`, border: `1px solid rgba(255,255,255,${0.3 + sheen * 0.1})` }, children: children }); } return jsxs("div", { ref: containerRef, className: cn('glass-surface-primary glass-relative glass-overflow-hidden', className), children: [jsx("canvas", { ref: canvasRef, className: cn('glass-absolute glass-inset-0 glass-w-full glass-h-full glass-pointer-events-none'), style: { zIndex: 1 } }), jsx("div", { className: cn('glass-relative glass-z-10'), children: children })] }); }; const LiquidGlassGPU = LiquidGlassGPURenderer; export { LiquidGlassGPU, LiquidGlassGPUDriver, LiquidGlassGPURenderer, LiquidGlassGPURenderer as default }; //# sourceMappingURL=LiquidGlassGPU.js.map