mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-11 00:23:37 +08:00
fix(glass): align frosted fixed and scroll materials (#617)
This commit is contained in:
48
src/App.vue
48
src/App.vue
@@ -1141,35 +1141,35 @@ onUnmounted(() => {
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
||||
</div>
|
||||
<GlassOpticalLayer
|
||||
v-if="shouldRenderGlassOpticalLayer"
|
||||
:appearance="effectiveGlassSettings.glassAppearance"
|
||||
:deformation-strength="opticalDeformationStrength"
|
||||
:flow-strength="opticalFlowStrength"
|
||||
:quality="opticalQuality === 'high' ? 'high' : 'balanced'"
|
||||
:reflection-strength="opticalReflectionStrength"
|
||||
:transparency-strength="opticalTransparencyStrength"
|
||||
:transmission-strength="opticalTransmissionStrength"
|
||||
:translation-strength="opticalTranslationStrength"
|
||||
:route-key="route.fullPath"
|
||||
:tint-color="globalTheme.current.value.colors.primary"
|
||||
:transition-duration="BACKGROUND_CROSSFADE_DURATION_MS"
|
||||
:transition-started-at="backgroundCrossfadeStartedAt"
|
||||
:wallpaper-url="activeOpticalBackgroundImage"
|
||||
:previous-wallpaper-url="previousOpticalBackgroundImage"
|
||||
:pending-wallpaper-url="pendingOpticalBackgroundImage"
|
||||
:pending-wallpaper-revision="pendingOpticalWallpaperRevision"
|
||||
:activate-wallpaper-revision="activateOpticalWallpaperRevision"
|
||||
@wallpaper-activation-failed="handleOpticalWallpaperActivationFailed"
|
||||
@wallpaper-activated="handleOpticalWallpaperActivated"
|
||||
@wallpaper-preparation-failed="handleOpticalWallpaperPreparationFailed"
|
||||
@wallpaper-prepared="handleOpticalWallpaperPrepared"
|
||||
/>
|
||||
<!-- 页面内容 -->
|
||||
<VApp
|
||||
:class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }"
|
||||
:data-login-visual-profile="isLoginWallpaperRoute ? loginVisualProfile : undefined"
|
||||
>
|
||||
<GlassOpticalLayer
|
||||
v-if="shouldRenderGlassOpticalLayer"
|
||||
:appearance="effectiveGlassSettings.glassAppearance"
|
||||
:deformation-strength="opticalDeformationStrength"
|
||||
:flow-strength="opticalFlowStrength"
|
||||
:quality="opticalQuality === 'high' ? 'high' : 'balanced'"
|
||||
:reflection-strength="opticalReflectionStrength"
|
||||
:transparency-strength="opticalTransparencyStrength"
|
||||
:transmission-strength="opticalTransmissionStrength"
|
||||
:translation-strength="opticalTranslationStrength"
|
||||
:route-key="route.fullPath"
|
||||
:tint-color="globalTheme.current.value.colors.primary"
|
||||
:transition-duration="BACKGROUND_CROSSFADE_DURATION_MS"
|
||||
:transition-started-at="backgroundCrossfadeStartedAt"
|
||||
:wallpaper-url="activeOpticalBackgroundImage"
|
||||
:previous-wallpaper-url="previousOpticalBackgroundImage"
|
||||
:pending-wallpaper-url="pendingOpticalBackgroundImage"
|
||||
:pending-wallpaper-revision="pendingOpticalWallpaperRevision"
|
||||
:activate-wallpaper-revision="activateOpticalWallpaperRevision"
|
||||
@wallpaper-activation-failed="handleOpticalWallpaperActivationFailed"
|
||||
@wallpaper-activated="handleOpticalWallpaperActivated"
|
||||
@wallpaper-preparation-failed="handleOpticalWallpaperPreparationFailed"
|
||||
@wallpaper-prepared="handleOpticalWallpaperPrepared"
|
||||
/>
|
||||
<RouterView />
|
||||
<!-- 全局共享弹窗入口,列表与卡片按需在这里挂载业务弹窗。 -->
|
||||
<SharedDialogHost />
|
||||
|
||||
@@ -14,13 +14,13 @@ describe('shouldUseGlassFixedShellBackplate', () => {
|
||||
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, quality: 'balanced' })).toBe(true)
|
||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, quality: 'high' })).toBe(true)
|
||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, needsStableFixedBackdrop: false })).toBe(false)
|
||||
expect(shouldUseGlassFixedShellBackplate({ ...eligible, appearance: 'clear' })).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, isAuthenticated: 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 () => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||
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)
|
||||
}
|
||||
|
||||
/** 只有需要稳定 fixed 背板的 Chromium 磨砂标准布局使用直接壁纸采样。 */
|
||||
/** Chromium 磨砂 fixed 层使用稳定壁纸背板,避免读取随页面重绘的 document backdrop。 */
|
||||
export function shouldUseGlassFixedShellBackplate(options: GlassFixedShellBackplateEligibility) {
|
||||
return (
|
||||
options.isAuthenticated &&
|
||||
options.needsStableFixedBackdrop &&
|
||||
options.themeName === 'glass' &&
|
||||
options.appearance === 'frosted' &&
|
||||
options.quality === 'css' &&
|
||||
options.hasWallpaper
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1399,6 +1399,8 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
let resumePromise: Promise<void> | null = null
|
||||
let resumeVersion = 0
|
||||
const presentationSpace = options.surfaceSpace ?? 'fixed'
|
||||
const usesDynamicsOnly = () =>
|
||||
presentationSpace === 'scroll' || (presentationSpace === 'fixed' && toValue(options.appearance) === 'frosted')
|
||||
const wallpaperSourceCache = options.wallpaperSourceCache ?? createGlassWallpaperSourceCache()
|
||||
|
||||
/** 滚动期间由原生 backdrop 接管壁纸;稳定态恢复完整纹理折射与流体反馈。 */
|
||||
@@ -3664,7 +3666,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
uBackgroundVisibility: { value: materialResponse.backgroundVisibility },
|
||||
uCoverScale: { value: new three.Vector2(1, 1) },
|
||||
uDeformationStrength: { value: getDeformationStrengthScale() },
|
||||
uDynamicsOnly: { value: presentationSpace === 'scroll' ? 1 : 0 },
|
||||
uDynamicsOnly: { value: usesDynamicsOnly() ? 1 : 0 },
|
||||
uFlowTexture: { value: null },
|
||||
uFlowStrength: { value: getFlowStrengthScale() },
|
||||
uHasFlowTexture: { value: 0 },
|
||||
@@ -3818,6 +3820,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
)
|
||||
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
|
||||
resources.uniforms.uBackgroundVisibility.value = materialResponse.backgroundVisibility
|
||||
resources.uniforms.uDynamicsOnly.value = usesDynamicsOnly() ? 1 : 0
|
||||
resources.uniforms.uFrostDetailLevel.value = materialResponse.frostDetailLevel
|
||||
resources.uniforms.uSurfaceDensity.value = materialResponse.surfaceDensity
|
||||
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 backplate = readFileSync(resolve(cwd(), 'src/components/theme/GlassFixedShellBackplate.vue'), 'utf8')
|
||||
|
||||
@@ -41,6 +41,12 @@ describe('glass overlay material styles', () => {
|
||||
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;/,
|
||||
)
|
||||
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).not.toContain('backdrop-filter')
|
||||
expect(backplate).toContain('var(--glass-fixed-shell-nav-inline-size) 100%')
|
||||
@@ -121,6 +127,9 @@ describe('glass overlay material styles', () => {
|
||||
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;/,
|
||||
)
|
||||
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(
|
||||
/\.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 scss/at-rule-no-unknown */
|
||||
|
||||
@use '@configured-variables' as variables;
|
||||
|
||||
// 材质只覆盖统一表面 token;质量档只替换光学层,不改变业务组件契约。
|
||||
html[data-theme='glass'] {
|
||||
--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'] {
|
||||
.layout-wrapper.layout-fixed-shell-backplate-active .layout-vertical-nav::before,
|
||||
.layout-wrapper.layout-fixed-shell-backplate-active .layout-navbar,
|
||||
@@ -1193,6 +1195,7 @@ html[data-theme='glass'] {
|
||||
|
||||
.glass-optical-layer--fixed {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
}
|
||||
|
||||
// 文档表面与该呈现层共享滚动坐标,canvas backing buffer 仍由 renderer 的质量预算约束。
|
||||
@@ -1256,6 +1259,12 @@ html[data-glass-appearance='frosted']:is(
|
||||
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.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed .layout-navbar {
|
||||
-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-native-surface-backdrop-filter: blur(calc(16px * var(--glass-frost-blur-scale, 1))) saturate(162%)
|
||||
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'] {
|
||||
--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%)
|
||||
brightness(var(--glass-transmission-brightness));
|
||||
--glass-fixed-shell-backplate-filter: var(--glass-native-surface-backdrop-filter);
|
||||
}
|
||||
|
||||
// 异步滚动期间只暂停 GPU 动态层;原生壁纸基底不发生材质切换。
|
||||
|
||||
Reference in New Issue
Block a user