Fix mobile clear glass surfaces

This commit is contained in:
jxxghp
2026-07-23 08:12:39 +08:00
parent 91ce2c8aee
commit 032bda61f6
3 changed files with 64 additions and 17 deletions

View File

@@ -1,4 +1,5 @@
import {
collectGlassOpticalRects,
containsGlassOpticalSurface,
setGlassRendererState,
useGlassOpticalRenderer,
@@ -35,11 +36,33 @@ vi.mock('three', async importOriginal => {
}
})
/** 提供 renderer 单元测试所需的最小 ResizeObserver 实现。 */
class ResizeObserverMock {
/** 断开观察时无需执行额外逻辑。 */
disconnect() {}
/** 测试不需要追踪具体被观察元素。 */
observe() {}
}
/** 创建带稳定视口边界的光学表面元素。 */
function appendOpticalSurface(className: string, bounds: Pick<DOMRect, 'height' | 'width' | 'x' | 'y'>) {
const surface = document.createElement('section')
surface.className = className
surface.getBoundingClientRect = () =>
({
...bounds,
bottom: bounds.y + bounds.height,
left: bounds.x,
right: bounds.x + bounds.width,
top: bounds.y,
toJSON: () => ({}),
}) as DOMRect
document.body.append(surface)
return surface
}
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
vi.stubGlobal('WebGLRenderingContext', class {})
@@ -49,6 +72,7 @@ beforeEach(() => {
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
document.body.replaceChildren()
delete document.documentElement.dataset.glassRendererState
})
@@ -89,6 +113,32 @@ describe('glass optical surface discovery', () => {
expect(document.documentElement.dataset.glassRendererState).toBe('fallback')
})
it('excludes the navbar from mobile clear optical surfaces', () => {
appendOpticalSurface('layout-navbar', { height: 64, width: 390, x: 0, y: 0 })
appendOpticalSurface('dashboard-grid-item-content', { height: 200, width: 350, x: 20, y: 120 })
const rects = collectGlassOpticalRects(390, 800, 'clear')
expect(rects).toHaveLength(1)
expect(rects[0]).toMatchObject({ height: 200, width: 350, x: 20, y: 120 })
})
it('keeps the navbar in mobile frosted optical surfaces', () => {
appendOpticalSurface('layout-navbar', { height: 64, width: 390, x: 0, y: 0 })
expect(collectGlassOpticalRects(390, 800, 'frosted')).toEqual([
expect.objectContaining({ height: 64, width: 390, x: 0, y: 0 }),
])
})
it('keeps the navbar in desktop clear optical surfaces', () => {
appendOpticalSurface('layout-navbar', { height: 64, width: 1000, x: 0, y: 0 })
expect(collectGlassOpticalRects(1000, 800, 'clear')).toEqual([
expect.objectContaining({ height: 64, width: 1000, x: 0, y: 0 }),
])
})
it('recovers after consecutive WebGL context loss cycles', async () => {
const canvas = document.createElement('canvas')
const scope = effectScope()

View File

@@ -231,11 +231,18 @@ function isVisibleSurface(element: HTMLElement, bounds: DOMRect) {
}
/** 将活动界面中的高价值材质面集中转换为 renderer 矩形预算。 */
export function collectGlassOpticalRects(viewportWidth: number, viewportHeight: number) {
export function collectGlassOpticalRects(
viewportWidth: number,
viewportHeight: number,
appearance: ThemeCustomizerGlassAppearance,
) {
const candidates: GlassOpticalRect[] = []
const seen = new Set<HTMLElement>()
for (const { rank, selector } of SURFACE_SELECTORS) {
// 移动端透明顶栏只使用稳定的 CSS 表面,避免滚动重扫时再次叠加壁纸折射。
if (viewportWidth <= 600 && appearance === 'clear' && selector === '.layout-navbar') continue
for (const element of document.querySelectorAll<HTMLElement>(selector)) {
if (seen.has(element)) continue
seen.add(element)
@@ -315,7 +322,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
const rects = collectGlassOpticalRects(viewportWidth, viewportHeight)
const rects = collectGlassOpticalRects(viewportWidth, viewportHeight, toValue(options.appearance))
currentRects = rects
const normalized = rects.map(rect => normalizeGlassOpticalRect(rect, viewportWidth, viewportHeight))
const uniformRects = resources.uniforms.uRects.value
@@ -742,7 +749,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
if (!resources) return
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
scheduleFrame()
scheduleSurfaceUpdate()
},
)

View File

@@ -41,8 +41,6 @@ html[data-theme='glass'] {
--glass-control-backdrop-filter: none;
--glass-control-prominent-backdrop-filter: none;
--glass-navbar-backdrop-filter: var(--glass-raised-backdrop-filter);
--glass-clear-overlay-surface: rgba(11, 19, 34, 72%);
--glass-clear-mobile-nav-surface: rgba(11, 19, 34, 68%);
--glass-control-icon-color: rgba(242, 245, 250, 70%);
--glass-control-placeholder-color: rgba(242, 245, 250, 54%);
--glass-control-shortcut-background: rgba(255, 255, 255, 8%);
@@ -490,7 +488,8 @@ html[data-theme='glass'] {
.v-overlay__content {
border-radius: var(--app-overlay-radius);
> :where(.v-card, .v-sheet, .v-list) {
> :where(.v-card, .v-sheet, .v-list),
> form > :where(.v-card, .v-sheet) {
-webkit-backdrop-filter: var(--glass-raised-backdrop-filter) !important;
backdrop-filter: var(--glass-raised-backdrop-filter) !important;
background-color: var(--glass-surface-raised) !important;
@@ -499,7 +498,7 @@ html[data-theme='glass'] {
}
}
// 透明弹层沿用普通玻璃的色相,只提高 alpha不引入独立底色
// 透明弹层与顶栏共用同一静态表面,不额外增加模糊或深色覆盖
&[data-glass-appearance='clear'] {
.v-overlay__content > :where(.v-card, .v-sheet, .v-list),
.v-overlay__content > form > :where(.v-card, .v-sheet),
@@ -507,7 +506,7 @@ html[data-theme='glass'] {
.agent-assistant-panel {
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
background-color: var(--glass-clear-overlay-surface) !important;
background-color: var(--glass-surface-raised) !important;
}
}
@@ -810,15 +809,6 @@ html[data-theme='glass'] {
}
@media (width <= 960px) {
&[data-glass-appearance='clear'] {
// 底部主导航和旁侧动态按钮共用 footer-nav-card提高 alpha 后仍保持透明材质色相。
.footer-nav-card.v-card {
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
background-color: var(--glass-clear-mobile-nav-surface) !important;
}
}
// 全屏快捷弹窗释放外层玻璃背景,让定时服务卡片直接采样全局壁纸。
&[data-glass-appearance='frosted'] .scheduler-shortcut-dialog-card {
-webkit-backdrop-filter: none !important;