class GlassSurface { constructor(element, config = {}) { this.el = element; this.config = Object.assign({ width: 200, height: 20, borderRadius: 20, borderWidth: 0.07, brightness: 50, opacity: 0, blur: 11, displace: 0, backgroundOpacity: 0, saturation: 1, distortionScale: -180, redOffset: 0, greenOffset: 0, blueOffset: 0, xChannel: 'R', yChannel: 'G', mixBlendMode: 'difference', className: '', style: {} }, config); this.uniqueId = Math.random().toString(36).substring(2, 9).replace(/:/g, '-'); this.filterId = `glass-filter-${this.uniqueId}`; this.redGradId = `red-grad-${this.uniqueId}`; this.blueGradId = `blue-grad-${this.uniqueId}`; this.isMounted = false; this.liquidGlassEnabled = true; this.init(); } supportsSVGFilters() { if (typeof window === 'undefined' || typeof document === 'undefined') return false; const ua = navigator.userAgent.toLowerCase(); if (ua.includes('firefox')) return false; const isIOS = /iphone|ipad|ipod/.test(ua); const isSafari = ua.includes('safari') && !ua.includes('chrome'); const isWebkitOnly = ua.includes('webkit') && !ua.includes('chrome'); if (isIOS || isSafari || isWebkitOnly) return false; const div = document.createElement('div'); div.style.backdropFilter = `url(#${this.filterId})`; const supported = div.style.backdropFilter !== ''; if (!supported) { div.style.cssText = `-webkit-backdrop-filter: url(#${this.filterId})`; return div.style.webkitBackdropFilter !== '' && div.style.webkitBackdropFilter !== undefined; } return supported; } supportsBackdropFilter() { return CSS.supports('backdrop-filter', 'blur(10px)'); } getIsDark() { return window.matchMedia('(prefers-color-scheme: dark)').matches; } init() { this.isMounted = true; this.isDark = this.getIsDark(); this.svgSupported = this.supportsSVGFilters(); this.backdropSupported = this.supportsBackdropFilter(); this.darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); this.darkModeListener = (e) => { this.isDark = e.matches; this.updateStyles(); }; this.darkModeQuery.addEventListener('change', this.darkModeListener); this.checkSettings(); this.el.classList.add('glass-surface'); this.el.style.position = 'relative'; this.el.style.transform = 'translateZ(0)'; this.el.style.backfaceVisibility = 'hidden'; this.el.style.perspective = '1000'; this.el.style.contain = 'layout style paint'; this.applyBaseStyles(); this.createContentWrapper(); if (this.liquidGlassEnabled) { this.applyGlassEffect(); } else { this.applyFallback(); } this.setupResizeObserver(); } checkSettings() { try { const settings = localStorage.getItem('glassSettings'); if (settings) { const parsed = JSON.parse(settings); this.liquidGlassEnabled = parsed.liquidGlassEnabled !== false; } } catch (e) { } } applyBaseStyles() { const width = typeof this.config.width === 'number' ? `${this.config.width}px` : this.config.width; const height = typeof this.config.height === 'number' ? `${this.config.height}px` : this.config.height; this.el.style.width = width || 'auto'; this.el.style.height = height || 'auto'; this.el.style.borderRadius = `${this.config.borderRadius}px`; this.el.style.setProperty('--glass-frost', this.config.backgroundOpacity); this.el.style.setProperty('--glass-saturation', this.config.saturation); Object.assign(this.el.style, this.config.style); if (this.config.className) { this.el.classList.add(...this.config.className.split(' ')); } if (this.isDark) { this.el.classList.add('dark-mode'); } else { this.el.classList.add('light-mode'); } } createContentWrapper() { this.contentWrapper = document.createElement('div'); this.contentWrapper.className = 'glass-surface__content'; this.contentWrapper.style.cssText = ` width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; padding: 4px; border-radius: inherit; position: relative; z-index: 10; `; while (this.el.firstChild) { this.contentWrapper.appendChild(this.el.firstChild); } this.el.appendChild(this.contentWrapper); } applyGlassEffect() { if (this.svgSupported && this.backdropSupported) { this.applySVGFilter(); } else if (this.backdropSupported) { this.applyBackdropFallback(); } else { this.applyBasicFallback(); } } applySVGFilter() { this.el.style.willChange = 'backdrop-filter, transform'; const bgColor = this.isDark ? `hsl(0 0% 0% / ${this.config.backgroundOpacity})` : `hsl(0 0% 100% / ${this.config.backgroundOpacity})`; const shadow = this.isDark ? '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset, 0px 4px 16px rgba(0, 0, 0, 0.1), 0px 8px 24px rgba(0, 0, 0, 0.08)' : '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset, 0px 4px 16px rgba(0, 0, 0, 0.06), 0px 8px 24px rgba(0, 0, 0, 0.04)'; this.el.style.background = bgColor; this.el.style.backdropFilter = `url(#${this.filterId}) saturate(${this.config.saturation})`; this.el.style.webkitBackdropFilter = `url(#${this.filterId}) saturate(${this.config.saturation})`; this.el.style.boxShadow = shadow; this.createSVGFilter(); } createSVGFilter() { this.svgContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.svgContainer.setAttribute("class", "glass-surface__filter"); this.svgContainer.style.cssText = ` width: 100%; height: 100%; position: absolute; inset: 0; opacity: 0; z-index: -10; pointer-events: none; transform: translateZ(0); `; this.svgContainer.innerHTML = ` `; this.el.appendChild(this.svgContainer); this.feImage = this.svgContainer.querySelector('[ref="feImage"]'); this.dispRed = this.svgContainer.querySelector('[ref="dispRed"]'); this.dispGreen = this.svgContainer.querySelector('[ref="dispGreen"]'); this.dispBlue = this.svgContainer.querySelector('[ref="dispBlue"]'); this.gaussianBlur = this.svgContainer.querySelector('[ref="gaussianBlur"]'); this.updateFilterAttributes(); requestAnimationFrame(() => this.updateDisplacementMap()); } updateFilterAttributes() { if (!this.dispRed || !this.dispGreen || !this.dispBlue || !this.gaussianBlur) return; this.dispRed.setAttribute('scale', (this.config.distortionScale + this.config.redOffset).toString()); this.dispRed.setAttribute('xChannelSelector', this.config.xChannel); this.dispRed.setAttribute('yChannelSelector', this.config.yChannel); this.dispGreen.setAttribute('scale', (this.config.distortionScale + this.config.greenOffset).toString()); this.dispGreen.setAttribute('xChannelSelector', this.config.xChannel); this.dispGreen.setAttribute('yChannelSelector', this.config.yChannel); this.dispBlue.setAttribute('scale', (this.config.distortionScale + this.config.blueOffset).toString()); this.dispBlue.setAttribute('xChannelSelector', this.config.xChannel); this.dispBlue.setAttribute('yChannelSelector', this.config.yChannel); this.gaussianBlur.setAttribute('stdDeviation', this.config.displace.toString()); } applyBackdropFallback() { this.el.style.willChange = 'transform'; if (this.isDark) { this.el.style.background = 'rgba(0, 0, 0, 0.3)'; this.el.style.backdropFilter = 'blur(12px) saturate(1.8) brightness(1.1)'; this.el.style.webkitBackdropFilter = 'blur(12px) saturate(1.8) brightness(1.1)'; this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset'; } else { this.el.style.background = 'rgba(255, 255, 255, 0.1)'; this.el.style.backdropFilter = 'blur(12px) saturate(1.8) brightness(1.05)'; this.el.style.webkitBackdropFilter = 'blur(12px) saturate(1.8) brightness(1.05)'; this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset, 0px 4px 16px rgba(0, 0, 0, 0.04)'; } } applyBasicFallback() { this.el.style.willChange = 'transform'; if (this.isDark) { this.el.style.background = 'rgba(0, 0, 0, 0.5)'; this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset'; } else { this.el.style.background = 'rgba(255, 255, 255, 0.2)'; this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset'; } } applyFallback() { if (this.backdropSupported) { this.applyBackdropFallback(); } else { this.applyBasicFallback(); } } updateStyles() { if (!this.isMounted) return; this.el.classList.remove('dark-mode', 'light-mode'); if (this.isDark) { this.el.classList.add('dark-mode'); } else { this.el.classList.add('light-mode'); } if (this.liquidGlassEnabled) { if (this.svgSupported && this.backdropSupported) { const bgColor = this.isDark ? `hsl(0 0% 0% / ${this.config.backgroundOpacity})` : `hsl(0 0% 100% / ${this.config.backgroundOpacity})`; const shadow = this.isDark ? '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset, 0px 4px 16px rgba(0, 0, 0, 0.1), 0px 8px 24px rgba(0, 0, 0, 0.08)' : '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset, 0px 4px 16px rgba(0, 0, 0, 0.06), 0px 8px 24px rgba(0, 0, 0, 0.04)'; this.el.style.background = bgColor; this.el.style.boxShadow = shadow; } else { this.applyFallback(); } } else { this.applyFallback(); } } setupResizeObserver() { if (!this.svgSupported) return; this.resizeObserver = new ResizeObserver(() => { if ('requestIdleCallback' in window) { window.requestIdleCallback(() => this.updateDisplacementMap(), { timeout: 100 }); } else { setTimeout(() => this.updateDisplacementMap(), 0); } }); this.resizeObserver.observe(this.el); } updateDisplacementMap() { if (!this.svgSupported || !this.feImage) return; const rect = this.el.getBoundingClientRect(); const actualWidth = Math.max(Math.round(rect.width) || 400, 10); const actualHeight = Math.max(Math.round(rect.height) || 200, 10); const edgeSize = Math.min(actualWidth, actualHeight) * (this.config.borderWidth * 0.5); const svgContent = ` `; const dataUrl = `data:image/svg+xml,${encodeURIComponent(svgContent)}`; this.feImage.setAttribute('href', dataUrl); } destroy() { this.isMounted = false; if (this.darkModeQuery && this.darkModeListener) { this.darkModeQuery.removeEventListener('change', this.darkModeListener); } if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } if (this.svgContainer && this.svgContainer.parentNode) { this.svgContainer.remove(); } if (this.contentWrapper && this.contentWrapper.parentNode) { while (this.contentWrapper.firstChild) { this.el.appendChild(this.contentWrapper.firstChild); } this.contentWrapper.remove(); } } } function applyGlassSurface(el, config) { if (!el || el.dataset.hasGlass) return null; if (typeof GlassSurface !== 'function') return null; el.dataset.hasGlass = "true"; const instance = new GlassSurface(el, config); el._glassSurfaceInstance = instance; return instance; } function destroyGlassSurface(el) { if (!el || !el._glassSurfaceInstance) return; el._glassSurfaceInstance.destroy(); el._glassSurfaceInstance = null; delete el.dataset.hasGlass; } function initLiquidGlass() { const config = { width: 200, height: 25, borderRadius: 20, borderWidth: 0.07, brightness: 50, opacity: 0.93, blur: 11, displace: 0, backgroundOpacity: 0, saturation: 1, distortionScale: -180, redOffset: 0, greenOffset: 10, blueOffset: 20, xChannel: 'R', yChannel: 'G', mixBlendMode: 'difference' }; const eagerSelectors = [ '.nav-links', '.discord-copy-bar', '.platform-pill', '.hero-cta-unsure', '#navIslandMain', '#navbarSearchToggle', '.hero-icon-group', '.details-overlay-actions .action-icon-btn', '.cta-button-wrapper .cta-button', '.spotlight-actions .cta-button', '.edit-profile-pill' ]; document.querySelectorAll(eagerSelectors.join(', ')).forEach(el => applyGlassSurface(el, config)); if (!window._cardPlayBtnGlassObserver) { window._cardPlayBtnGlassObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { applyGlassSurface(entry.target, config); window._cardPlayBtnGlassObserver.unobserve(entry.target); } }); }, { rootMargin: '200px' }); } if (!window._sharedDomObserver) { window._sharedDomObserver = new MutationObserver((mutations) => { for (const m of mutations) { m.addedNodes.forEach(node => { if (node.nodeType !== 1) return; if (node.matches?.('.card-play-btn') && !node.dataset.hasGlass) { window._cardPlayBtnGlassObserver.observe(node); } node.querySelectorAll?.('.card-play-btn:not([data-has-glass])').forEach(el => { window._cardPlayBtnGlassObserver.observe(el); }); }); } }); window._sharedDomObserver.observe(document.body, { childList: true, subtree: true }); } document.querySelectorAll('.card-play-btn:not([data-has-glass])').forEach(el => { window._cardPlayBtnGlassObserver.observe(el); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => initLiquidGlass()); } else { initLiquidGlass(); } export { GlassSurface, applyGlassSurface, destroyGlassSurface, initLiquidGlass };