mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
fix(glass): align frosted fixed and scroll materials (#617)
This commit is contained in:
+5
-5
@@ -1141,6 +1141,11 @@ onUnmounted(() => {
|
|||||||
<!-- 全局磨砂层 -->
|
<!-- 全局磨砂层 -->
|
||||||
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 页面内容 -->
|
||||||
|
<VApp
|
||||||
|
:class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }"
|
||||||
|
:data-login-visual-profile="isLoginWallpaperRoute ? loginVisualProfile : undefined"
|
||||||
|
>
|
||||||
<GlassOpticalLayer
|
<GlassOpticalLayer
|
||||||
v-if="shouldRenderGlassOpticalLayer"
|
v-if="shouldRenderGlassOpticalLayer"
|
||||||
:appearance="effectiveGlassSettings.glassAppearance"
|
:appearance="effectiveGlassSettings.glassAppearance"
|
||||||
@@ -1165,11 +1170,6 @@ onUnmounted(() => {
|
|||||||
@wallpaper-preparation-failed="handleOpticalWallpaperPreparationFailed"
|
@wallpaper-preparation-failed="handleOpticalWallpaperPreparationFailed"
|
||||||
@wallpaper-prepared="handleOpticalWallpaperPrepared"
|
@wallpaper-prepared="handleOpticalWallpaperPrepared"
|
||||||
/>
|
/>
|
||||||
<!-- 页面内容 -->
|
|
||||||
<VApp
|
|
||||||
:class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }"
|
|
||||||
:data-login-visual-profile="isLoginWallpaperRoute ? loginVisualProfile : undefined"
|
|
||||||
>
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<!-- 全局共享弹窗入口,列表与卡片按需在这里挂载业务弹窗。 -->
|
<!-- 全局共享弹窗入口,列表与卡片按需在这里挂载业务弹窗。 -->
|
||||||
<SharedDialogHost />
|
<SharedDialogHost />
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ describe('shouldUseGlassFixedShellBackplate', () => {
|
|||||||
themeName: 'glass',
|
themeName: 'glass',
|
||||||
}
|
}
|
||||||
|
|
||||||
it('enables the direct wallpaper backplate only for affected authenticated Chromium rendering', () => {
|
it('enables the direct wallpaper backplate for every frosted Chromium quality', () => {
|
||||||
expect(shouldUseGlassFixedShellBackplate(eligible)).toBe(true)
|
expect(shouldUseGlassFixedShellBackplate(eligible)).toBe(true)
|
||||||
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, quality: 'balanced' })).toBe(true)
|
||||||
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, quality: 'high' })).toBe(true)
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, needsStableFixedBackdrop: false })).toBe(false)
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, needsStableFixedBackdrop: false })).toBe(false)
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, appearance: 'clear' })).toBe(false)
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, appearance: 'clear' })).toBe(false)
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, appearance: 'tinted' })).toBe(false)
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, appearance: 'tinted' })).toBe(false)
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, quality: 'balanced' })).toBe(false)
|
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, quality: 'high' })).toBe(false)
|
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, themeName: 'transparent' })).toBe(false)
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, themeName: 'transparent' })).toBe(false)
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, isAuthenticated: false })).toBe(false)
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, isAuthenticated: false })).toBe(false)
|
||||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, hasWallpaper: false })).toBe(false)
|
expect(shouldUseGlassFixedShellBackplate({ ...eligible, hasWallpaper: false })).toBe(false)
|
||||||
|
|||||||
@@ -456,6 +456,47 @@ describe('glass optical surface discovery', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps frosted fixed surfaces on dynamics-only GPU composition', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const appearance = ref<'clear' | 'frosted' | 'tinted'>('clear')
|
||||||
|
appendOpticalSurface('layout-vertical-nav', { height: 800, width: 260, x: 0, y: 0 })
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance,
|
||||||
|
canvas: ref(canvas),
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
surfaceSpace: 'fixed',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
const scene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||||
|
children: Array<{
|
||||||
|
material: {
|
||||||
|
uniforms: {
|
||||||
|
uDynamicsOnly: { value: number }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
const dynamicsOnly = scene.children[0].material.uniforms.uDynamicsOnly
|
||||||
|
expect(dynamicsOnly.value).toBe(0)
|
||||||
|
|
||||||
|
appearance.value = 'frosted'
|
||||||
|
await vi.waitFor(() => expect(dynamicsOnly.value).toBe(1))
|
||||||
|
|
||||||
|
appearance.value = 'tinted'
|
||||||
|
await vi.waitFor(() => expect(dynamicsOnly.value).toBe(0))
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
it('re-samples scroll surfaces and material weight on a shared page motion revision', async () => {
|
it('re-samples scroll surfaces and material weight on a shared page motion revision', async () => {
|
||||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
||||||
|
|||||||
@@ -56,14 +56,13 @@ export function isChromiumFixedShellBackplateBrowser(browserIdentity: GlassFixed
|
|||||||
return /\b(?:Chrome|Chromium)\/\d+/u.test(browserIdentity.userAgent)
|
return /\b(?:Chrome|Chromium)\/\d+/u.test(browserIdentity.userAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 只有需要稳定 fixed 背板的 Chromium 磨砂标准布局使用直接壁纸采样。 */
|
/** Chromium 磨砂 fixed 层使用稳定壁纸背板,避免读取随页面重绘的 document backdrop。 */
|
||||||
export function shouldUseGlassFixedShellBackplate(options: GlassFixedShellBackplateEligibility) {
|
export function shouldUseGlassFixedShellBackplate(options: GlassFixedShellBackplateEligibility) {
|
||||||
return (
|
return (
|
||||||
options.isAuthenticated &&
|
options.isAuthenticated &&
|
||||||
options.needsStableFixedBackdrop &&
|
options.needsStableFixedBackdrop &&
|
||||||
options.themeName === 'glass' &&
|
options.themeName === 'glass' &&
|
||||||
options.appearance === 'frosted' &&
|
options.appearance === 'frosted' &&
|
||||||
options.quality === 'css' &&
|
|
||||||
options.hasWallpaper
|
options.hasWallpaper
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1399,6 +1399,8 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
let resumePromise: Promise<void> | null = null
|
let resumePromise: Promise<void> | null = null
|
||||||
let resumeVersion = 0
|
let resumeVersion = 0
|
||||||
const presentationSpace = options.surfaceSpace ?? 'fixed'
|
const presentationSpace = options.surfaceSpace ?? 'fixed'
|
||||||
|
const usesDynamicsOnly = () =>
|
||||||
|
presentationSpace === 'scroll' || (presentationSpace === 'fixed' && toValue(options.appearance) === 'frosted')
|
||||||
const wallpaperSourceCache = options.wallpaperSourceCache ?? createGlassWallpaperSourceCache()
|
const wallpaperSourceCache = options.wallpaperSourceCache ?? createGlassWallpaperSourceCache()
|
||||||
|
|
||||||
/** 滚动期间由原生 backdrop 接管壁纸;稳定态恢复完整纹理折射与流体反馈。 */
|
/** 滚动期间由原生 backdrop 接管壁纸;稳定态恢复完整纹理折射与流体反馈。 */
|
||||||
@@ -3664,7 +3666,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
uBackgroundVisibility: { value: materialResponse.backgroundVisibility },
|
uBackgroundVisibility: { value: materialResponse.backgroundVisibility },
|
||||||
uCoverScale: { value: new three.Vector2(1, 1) },
|
uCoverScale: { value: new three.Vector2(1, 1) },
|
||||||
uDeformationStrength: { value: getDeformationStrengthScale() },
|
uDeformationStrength: { value: getDeformationStrengthScale() },
|
||||||
uDynamicsOnly: { value: presentationSpace === 'scroll' ? 1 : 0 },
|
uDynamicsOnly: { value: usesDynamicsOnly() ? 1 : 0 },
|
||||||
uFlowTexture: { value: null },
|
uFlowTexture: { value: null },
|
||||||
uFlowStrength: { value: getFlowStrengthScale() },
|
uFlowStrength: { value: getFlowStrengthScale() },
|
||||||
uHasFlowTexture: { value: 0 },
|
uHasFlowTexture: { value: 0 },
|
||||||
@@ -3818,6 +3820,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
)
|
)
|
||||||
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
|
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
|
||||||
resources.uniforms.uBackgroundVisibility.value = materialResponse.backgroundVisibility
|
resources.uniforms.uBackgroundVisibility.value = materialResponse.backgroundVisibility
|
||||||
|
resources.uniforms.uDynamicsOnly.value = usesDynamicsOnly() ? 1 : 0
|
||||||
resources.uniforms.uFrostDetailLevel.value = materialResponse.frostDetailLevel
|
resources.uniforms.uFrostDetailLevel.value = materialResponse.frostDetailLevel
|
||||||
resources.uniforms.uSurfaceDensity.value = materialResponse.surfaceDensity
|
resources.uniforms.uSurfaceDensity.value = materialResponse.surfaceDensity
|
||||||
resources.uniforms.uTintDensity.value = materialResponse.tintDensity
|
resources.uniforms.uTintDensity.value = materialResponse.tintDensity
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ describe('glass overlay material styles', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps frosted CSS fixed shells on the stable wallpaper backplate', () => {
|
it('keeps Chromium frosted fixed shells on the stable wallpaper backplate', () => {
|
||||||
const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8')
|
const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8')
|
||||||
const backplate = readFileSync(resolve(cwd(), 'src/components/theme/GlassFixedShellBackplate.vue'), 'utf8')
|
const backplate = readFileSync(resolve(cwd(), 'src/components/theme/GlassFixedShellBackplate.vue'), 'utf8')
|
||||||
|
|
||||||
@@ -41,6 +41,12 @@ describe('glass overlay material styles', () => {
|
|||||||
expect(styles).toMatch(
|
expect(styles).toMatch(
|
||||||
/&\[data-glass-appearance='frosted'\]\[data-glass-quality='css'\][\s\S]*?\.layout-wrapper\.layout-fixed-shell-backplate-active \.layout-vertical-nav::before,[\s\S]*?\.layout-wrapper\.layout-fixed-shell-backplate-active \.layout-navbar,[\s\S]*?backdrop-filter:\s*none\s*!important;/,
|
/&\[data-glass-appearance='frosted'\]\[data-glass-quality='css'\][\s\S]*?\.layout-wrapper\.layout-fixed-shell-backplate-active \.layout-vertical-nav::before,[\s\S]*?\.layout-wrapper\.layout-fixed-shell-backplate-active \.layout-navbar,[\s\S]*?backdrop-filter:\s*none\s*!important;/,
|
||||||
)
|
)
|
||||||
|
expect(styles).toMatch(
|
||||||
|
/\[data-glass-appearance='frosted'\]\[data-glass-quality='balanced'\]\s*\{[\s\S]*?--glass-fixed-shell-backplate-filter:\s*var\(--glass-native-surface-backdrop-filter\);/,
|
||||||
|
)
|
||||||
|
expect(styles).toMatch(
|
||||||
|
/\[data-glass-appearance='frosted'\]\[data-glass-quality='high'\]\s*\{[\s\S]*?--glass-fixed-shell-backplate-filter:\s*var\(--glass-native-surface-backdrop-filter\);/,
|
||||||
|
)
|
||||||
expect(backplate).toContain('filter: var(--glass-fixed-shell-backplate-filter)')
|
expect(backplate).toContain('filter: var(--glass-fixed-shell-backplate-filter)')
|
||||||
expect(backplate).not.toContain('backdrop-filter')
|
expect(backplate).not.toContain('backdrop-filter')
|
||||||
expect(backplate).toContain('var(--glass-fixed-shell-nav-inline-size) 100%')
|
expect(backplate).toContain('var(--glass-fixed-shell-nav-inline-size) 100%')
|
||||||
@@ -121,6 +127,9 @@ describe('glass overlay material styles', () => {
|
|||||||
expect(styles).toMatch(
|
expect(styles).toMatch(
|
||||||
/\[data-glass-renderer-state='ready'\][\s\S]*?\.app-hover-lift-card:not\(\.media-card--image-loaded\)[\s\S]*?backdrop-filter:\s*var\(--glass-native-surface-backdrop-filter\)\s*!important;/,
|
/\[data-glass-renderer-state='ready'\][\s\S]*?\.app-hover-lift-card:not\(\.media-card--image-loaded\)[\s\S]*?backdrop-filter:\s*var\(--glass-native-surface-backdrop-filter\)\s*!important;/,
|
||||||
)
|
)
|
||||||
|
expect(styles).toMatch(
|
||||||
|
/\.layout-wrapper:not\(\.layout-fixed-shell-backplate-active\) \.layout-vertical-nav::before,[\s\S]*?backdrop-filter:\s*var\(--glass-native-surface-backdrop-filter\)\s*!important;/,
|
||||||
|
)
|
||||||
expect(styles).toMatch(
|
expect(styles).toMatch(
|
||||||
/\.settings-section-card\.app-grouped-list\s*\{[\s\S]*?backdrop-filter:\s*var\(--glass-native-surface-backdrop-filter\)\s*!important;/,
|
/\.settings-section-card\.app-grouped-list\s*\{[\s\S]*?backdrop-filter:\s*var\(--glass-native-surface-backdrop-filter\)\s*!important;/,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/* stylelint-disable no-descending-specificity */
|
/* stylelint-disable no-descending-specificity */
|
||||||
/* stylelint-disable scss/at-rule-no-unknown */
|
/* stylelint-disable scss/at-rule-no-unknown */
|
||||||
|
|
||||||
|
@use '@configured-variables' as variables;
|
||||||
|
|
||||||
// 材质只覆盖统一表面 token;质量档只替换光学层,不改变业务组件契约。
|
// 材质只覆盖统一表面 token;质量档只替换光学层,不改变业务组件契约。
|
||||||
html[data-theme='glass'] {
|
html[data-theme='glass'] {
|
||||||
--glass-surface: rgba(11, 19, 34, calc(0.02 + var(--glass-surface-density, 0.62) * 0.3));
|
--glass-surface: rgba(11, 19, 34, calc(0.02 + var(--glass-surface-density, 0.62) * 0.3));
|
||||||
@@ -462,7 +464,7 @@ html[data-theme='glass'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chromium 磨砂标准直接渲染稳定壁纸背板,固定功能层不得读取随页面重绘的 document backdrop。
|
// Chromium 磨砂 fixed 层直接渲染稳定壁纸背板,固定功能层不得读取随页面重绘的 document backdrop。
|
||||||
&[data-glass-appearance='frosted'][data-glass-quality='css'] body[data-theme='glass'] {
|
&[data-glass-appearance='frosted'][data-glass-quality='css'] body[data-theme='glass'] {
|
||||||
.layout-wrapper.layout-fixed-shell-backplate-active .layout-vertical-nav::before,
|
.layout-wrapper.layout-fixed-shell-backplate-active .layout-vertical-nav::before,
|
||||||
.layout-wrapper.layout-fixed-shell-backplate-active .layout-navbar,
|
.layout-wrapper.layout-fixed-shell-backplate-active .layout-navbar,
|
||||||
@@ -1193,6 +1195,7 @@ html[data-theme='glass'] {
|
|||||||
|
|
||||||
.glass-optical-layer--fixed {
|
.glass-optical-layer--fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文档表面与该呈现层共享滚动坐标,canvas backing buffer 仍由 renderer 的质量预算约束。
|
// 文档表面与该呈现层共享滚动坐标,canvas backing buffer 仍由 renderer 的质量预算约束。
|
||||||
@@ -1256,6 +1259,12 @@ html[data-glass-appearance='frosted']:is(
|
|||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout-wrapper:not(.layout-fixed-shell-backplate-active) .layout-vertical-nav::before,
|
||||||
|
.layout-wrapper:not(.layout-fixed-shell-backplate-active) .layout-navbar {
|
||||||
|
-webkit-backdrop-filter: var(--glass-native-surface-backdrop-filter) !important;
|
||||||
|
backdrop-filter: var(--glass-native-surface-backdrop-filter) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-wrapper.window-scrolled.layout-navbar-fixed .layout-navbar,
|
.layout-wrapper.window-scrolled.layout-navbar-fixed .layout-navbar,
|
||||||
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed .layout-navbar {
|
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed .layout-navbar {
|
||||||
-webkit-backdrop-filter: var(--glass-navbar-scrolled-backdrop-filter) !important;
|
-webkit-backdrop-filter: var(--glass-navbar-scrolled-backdrop-filter) !important;
|
||||||
@@ -1280,12 +1289,14 @@ html[data-glass-appearance='frosted'][data-glass-quality='balanced'] {
|
|||||||
--glass-dashboard-backdrop-filter: var(--glass-native-surface-backdrop-filter);
|
--glass-dashboard-backdrop-filter: var(--glass-native-surface-backdrop-filter);
|
||||||
--glass-native-surface-backdrop-filter: blur(calc(16px * var(--glass-frost-blur-scale, 1))) saturate(162%)
|
--glass-native-surface-backdrop-filter: blur(calc(16px * var(--glass-frost-blur-scale, 1))) saturate(162%)
|
||||||
brightness(var(--glass-transmission-brightness));
|
brightness(var(--glass-transmission-brightness));
|
||||||
|
--glass-fixed-shell-backplate-filter: var(--glass-native-surface-backdrop-filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-glass-appearance='frosted'][data-glass-quality='high'] {
|
html[data-glass-appearance='frosted'][data-glass-quality='high'] {
|
||||||
--glass-dashboard-backdrop-filter: var(--glass-native-surface-backdrop-filter);
|
--glass-dashboard-backdrop-filter: var(--glass-native-surface-backdrop-filter);
|
||||||
--glass-native-surface-backdrop-filter: blur(calc(10px * var(--glass-frost-blur-scale, 1))) saturate(154%)
|
--glass-native-surface-backdrop-filter: blur(calc(10px * var(--glass-frost-blur-scale, 1))) saturate(154%)
|
||||||
brightness(var(--glass-transmission-brightness));
|
brightness(var(--glass-transmission-brightness));
|
||||||
|
--glass-fixed-shell-backplate-filter: var(--glass-native-surface-backdrop-filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 异步滚动期间只暂停 GPU 动态层;原生壁纸基底不发生材质切换。
|
// 异步滚动期间只暂停 GPU 动态层;原生壁纸基底不发生材质切换。
|
||||||
|
|||||||
Reference in New Issue
Block a user