From 665467336ce84f282855a08e8fa70ee11c11855d Mon Sep 17 00:00:00 2001 From: alwaysmesmerizingyou Date: Fri, 4 Sep 2026 17:54:11 -0700 Subject: [PATCH] updated glass --- src/scripts/animations/glass-surface.js | 569 +++++++++++++++++++----- src/scripts/main.js | 17 +- src/styles/components/buttons.css | 7 +- src/styles/components/glass-surface.css | 49 +- src/styles/components/pills.css | 3 +- src/styles/layout/nav.css | 3 +- 6 files changed, 506 insertions(+), 142 deletions(-) diff --git a/src/scripts/animations/glass-surface.js b/src/scripts/animations/glass-surface.js index 17c71b2..646d893 100644 --- a/src/scripts/animations/glass-surface.js +++ b/src/scripts/animations/glass-surface.js @@ -1,121 +1,474 @@ -let globalSvgDefs = null; +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); -function ensureGlobalDefs() { - if (!globalSvgDefs) { - globalSvgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - globalSvgDefs.setAttribute('id', 'glass-surface-defs'); - globalSvgDefs.setAttribute('aria-hidden', 'true'); - globalSvgDefs.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;z-index:-9999;opacity:0;'; - document.body.appendChild(globalSvgDefs); + 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) { + console.warn('Failed to read glass settings:', 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(); + } } - return globalSvgDefs; } -export function initGlassSurface(selector, options = {}) { - const elements = typeof selector === 'string' ? document.querySelectorAll(selector) : [selector]; - const defsContainer = ensureGlobalDefs(); +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; +} - elements.forEach((el, index) => { - if (!el || el.dataset.glassInitialized) return; - el.dataset.glassInitialized = 'true'; +function destroyGlassSurface(el) { + if (!el || !el._glassSurfaceInstance) return; + el._glassSurfaceInstance.destroy(); + el._glassSurfaceInstance = null; + delete el.dataset.hasGlass; +} - const id = `glass-surf-${index}-${Math.random().toString(36).slice(2, 7)}`; - const filterId = `filter-${id}`; - const redGradId = `red-grad-${id}`; - const blueGradId = `blue-grad-${id}`; +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 config = { - borderRadius: options.borderRadius !== undefined ? Number(options.borderRadius) : 50, - borderWidth: options.borderWidth !== undefined ? Number(options.borderWidth) : 0, - brightness: options.brightness !== undefined ? Number(options.brightness) : 100, - opacity: options.opacity !== undefined ? Number(options.opacity) : 0.93, - blur: options.blur !== undefined ? Number(options.blur) : 15, - displace: options.displace !== undefined ? Number(options.displace) : 0.5, - backgroundOpacity: options.backgroundOpacity !== undefined ? Number(options.backgroundOpacity) : 0, - saturation: options.saturation !== undefined ? Number(options.saturation) : 0, - distortionScale: options.distortionScale !== undefined ? Number(options.distortionScale) : -180, - redOffset: options.redOffset !== undefined ? Number(options.redOffset) : -50, - greenOffset: options.greenOffset !== undefined ? Number(options.greenOffset) : -50, - blueOffset: options.blueOffset !== undefined ? Number(options.blueOffset) : 20, - xChannel: options.xChannel || 'R', - yChannel: options.yChannel || 'G', - mixBlendMode: options.mixBlendMode || 'difference' - }; + const eagerSelectors = [ + '.nav-links', + '.discord-copy-bar', + '.platform-pill', + '.hero-cta-light', + '#navIslandMain', + '#navbarSearchToggle', + '.hero-icon-group', + '.details-overlay-actions .action-icon-btn', + '.cta-button-wrapper .cta-button', + '.spotlight-actions .cta-button', + '.edit-profile-pill' + ]; - const filterGroup = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); - filterGroup.innerHTML = ` - - - - - - - - - - - - - `; - defsContainer.appendChild(filterGroup); + document.querySelectorAll(eagerSelectors.join(', ')).forEach(el => applyGlassSurface(el, config)); - const rawContent = el.innerHTML; - el.innerHTML = ''; + 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' }); + } - const backdropLayer = document.createElement('span'); - backdropLayer.className = 'glass-surface__backdrop'; - - const contentLayer = document.createElement('span'); - contentLayer.className = 'glass-surface__content'; - contentLayer.innerHTML = rawContent; - - el.appendChild(backdropLayer); - el.appendChild(contentLayer); - el.classList.add('glass-surface'); - - el.style.borderRadius = `${config.borderRadius}px`; - el.style.setProperty('--glass-radius', `${config.borderRadius}px`); - el.style.setProperty('--glass-saturation', config.saturation.toString()); - - const feImage = defsContainer.querySelector(`#fe-map-${id}`); - - const updateMap = () => { - const rect = el.getBoundingClientRect(); - const width = Math.max(Math.round(rect.width), 40); - const height = Math.max(Math.round(rect.height), 20); - const edgeSize = Math.min(width, height) * (config.borderWidth * 0.5); - - const svgContent = ` - - - - - - - - - - - - - - - - - `; - - if (feImage) { - feImage.setAttribute('href', `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`); + 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 }); + } - el.style.setProperty('--glass-filter-id', `url(#${filterId})`); - el.style.setProperty('--glass-radius', `${config.borderRadius}px`); - el.style.setProperty('--glass-saturation', config.saturation.toString()); - el.style.borderRadius = `${config.borderRadius}px`; - }; - - updateMap(); - const ro = new ResizeObserver(() => requestAnimationFrame(updateMap)); - ro.observe(el); + document.querySelectorAll('.card-play-btn:not([data-has-glass])').forEach(el => { + window._cardPlayBtnGlassObserver.observe(el); }); -} \ No newline at end of file +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => initLiquidGlass()); +} else { + initLiquidGlass(); +} + +export { GlassSurface, applyGlassSurface, destroyGlassSurface, initLiquidGlass }; diff --git a/src/scripts/main.js b/src/scripts/main.js index dccc1be..434c37c 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,5 +1,5 @@ import { observeReveals } from './animations/reveal-observer.js'; -import { initGlassSurface } from './animations/glass-surface.js'; +import { initLiquidGlass } from './animations/glass-surface.js'; import { initHeroSection } from './sections/hero.js'; import { initTestimonialsSection } from './sections/testimonials.js'; import { initPartnersSection } from './sections/partners.js'; @@ -9,20 +9,7 @@ import { initStatsSection } from './sections/stats.js'; function initApp() { observeReveals(); - initGlassSurface('.nav-links, .discord-copy-bar, .platform-pill, .hero-cta-light', { - borderRadius: 50, - borderWidth: 0, - backgroundOpacity: 0, - saturation: 1, - brightness: 0, - opacity: 1, - blur: 15, - displace: 0.5, - distortionScale: -180, - redOffset: -50, - greenOffset: -50, - blueOffset: 20 - }); + initLiquidGlass(); initHeroSection(); initStatsSection(); diff --git a/src/styles/components/buttons.css b/src/styles/components/buttons.css index 8c89f0c..0a8a3e9 100644 --- a/src/styles/components/buttons.css +++ b/src/styles/components/buttons.css @@ -39,7 +39,8 @@ align-items: center; justify-content: center; padding: 0.7rem 1.35rem; - border-radius: var(--radius-pill); + height: 50px !important; + border-radius: var(--radius-pill) !important; font-family: var(--font-body); font-size: 0.8125rem; font-weight: 600; @@ -76,8 +77,8 @@ position: relative; background: transparent; color: var(--color-fg); - border-radius: 50px; - padding: 0.75rem 1.75rem; + border-radius: 50px !important; + padding: 0.75rem 1.75rem !important; font-weight: 500; font-size: 0.875rem; text-decoration: none; diff --git a/src/styles/components/glass-surface.css b/src/styles/components/glass-surface.css index 65a2d5a..27d9440 100644 --- a/src/styles/components/glass-surface.css +++ b/src/styles/components/glass-surface.css @@ -5,40 +5,61 @@ justify-content: center !important; background: transparent !important; border: none !important; - border-radius: var(--glass-radius, 50px) !important; - overflow: visible !important; + border-radius: var(--glass-radius, 20px) !important; + overflow: hidden !important; text-decoration: none !important; box-sizing: border-box !important; width: auto !important; max-width: max-content !important; + transition: opacity 260ms ease-out; + box-shadow: none !important; } -.glass-surface__backdrop { - position: absolute; - inset: 0; +.glass-surface.dark-mode:focus-visible { + outline: 2px solid #0A84FF; + outline-offset: 2px; +} + +.glass-surface.light-mode:focus-visible { + outline: 2px solid #007AFF; + outline-offset: 2px; +} + +.glass-surface__filter { width: 100%; height: 100%; - border-radius: inherit !important; - background: transparent; - backdrop-filter: var(--glass-filter-id, none) saturate(var(--glass-saturation, 0)); - -webkit-backdrop-filter: var(--glass-filter-id, none) saturate(var(--glass-saturation, 0)); pointer-events: none; - z-index: 0; - transition: border-color 0.2s ease, box-shadow 0.2s ease; + position: absolute; + inset: 0; + opacity: 0; + z-index: -10; + box-shadow: none !important; } .glass-surface__content { position: relative; - z-index: 2; - display: inline-flex; + z-index: 10; + display: flex; flex-direction: row; align-items: center; justify-content: center; gap: inherit; - width: auto; + width: 100%; height: 100%; color: inherit; white-space: nowrap; border-radius: inherit; pointer-events: auto; + box-shadow: none !important; + padding: 4px; +} + +.glass-surface .fa-solid, +.glass-surface .fas { + padding-right: 10px !important; +} + +.glass-surface--fallback .fa-solid, +.glass-surface--fallback .fas { + padding-right: 5px; } \ No newline at end of file diff --git a/src/styles/components/pills.css b/src/styles/components/pills.css index 904fd80..6d98243 100644 --- a/src/styles/components/pills.css +++ b/src/styles/components/pills.css @@ -5,7 +5,8 @@ justify-content: space-between; gap: 1rem; padding: 0.65rem 1.25rem; - border-radius: 50px; + height: 50px !important; + border-radius: 50px !important; cursor: pointer; margin-top: 1.5rem; max-width: 90vw; diff --git a/src/styles/layout/nav.css b/src/styles/layout/nav.css index 13b6fcb..e2c5d22 100644 --- a/src/styles/layout/nav.css +++ b/src/styles/layout/nav.css @@ -28,7 +28,8 @@ .nav-links { gap: 1.25rem; padding: 0.75rem 1.5rem; - border-radius: 100px; + border-radius: 100px !important; + height: 50px !important; } .nav-links a {