From 6a4af6668331db935d8a2bc845d130d584e8fa74 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 10 Aug 2026 12:49:05 +0800 Subject: [PATCH] fix(glass): preserve content clarity across transparency (#656) (#657) Co-authored-by: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> --- .../__tests__/useThemeCustomizer.spec.ts | 21 ++++++++++++++ src/composables/useThemeCustomizer.ts | 3 ++ .../__tests__/glassOverlayMaterial.spec.ts | 16 +++++++++- src/styles/themes/glass.scss | 12 ++++---- src/utils/__tests__/glassOptics.spec.ts | 29 +++++++++++++++++++ src/utils/glassOptics.ts | 6 ++++ 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/composables/__tests__/useThemeCustomizer.spec.ts b/src/composables/__tests__/useThemeCustomizer.spec.ts index 62deac9e..dc1df586 100644 --- a/src/composables/__tests__/useThemeCustomizer.spec.ts +++ b/src/composables/__tests__/useThemeCustomizer.spec.ts @@ -210,6 +210,27 @@ describe('useThemeCustomizer glass settings', () => { expect(Number(document.body.style.getPropertyValue('--glass-background-visibility'))).toBeCloseTo(0.48) expect(Number(document.documentElement.style.getPropertyValue('--glass-surface-density'))).toBeCloseTo(0.72) expect(Number(document.body.style.getPropertyValue('--glass-tint-density'))).toBeCloseTo(0.65) + expect(document.documentElement.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('6.7px') + expect(document.body.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('6.7px') + }) + + it('keeps overlay clarity synchronized across preview, cancel, and commit', () => { + persistPartialThemeCustomizerSettings({ glassTransparencyStrength: 50 }) + expect(document.documentElement.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('6.7px') + + previewGlassSettings({ glassTransparencyStrength: 100 }) + expect(document.documentElement.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('5px') + expect(document.body.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('5px') + + cancelGlassPreview() + expect(document.documentElement.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('6.7px') + expect(document.body.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('6.7px') + + previewGlassSettings({ glassTransparencyStrength: 20 }) + commitGlassPreview() + expect(readThemeCustomizerSettings().glassTransparencyStrength).toBe(20) + expect(document.documentElement.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('7.8px') + expect(document.body.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('7.8px') }) it('previews glass settings without persisting them', () => { diff --git a/src/composables/useThemeCustomizer.ts b/src/composables/useThemeCustomizer.ts index bd728439..0ffb2b6c 100644 --- a/src/composables/useThemeCustomizer.ts +++ b/src/composables/useThemeCustomizer.ts @@ -8,6 +8,7 @@ import { GLASS_OPTICAL_STRENGTH_MAX, getGlassCssFrostBlur, getGlassMaterialResponse, + getGlassOverlayClarityBlur, getGlassOpticalCssTransmissionBrightness, getGlassOpticalPresetKey, getGlassOpticalPresetParameters, @@ -480,6 +481,7 @@ export function applyThemeCustomizerRootSettings( const materialResponse = getGlassMaterialResponse(settings.glassAppearance, settings.glassTransparencyStrength) const frostBlur = getGlassCssFrostBlur(settings.glassTransparencyStrength) + const overlayClarityBlur = getGlassOverlayClarityBlur(settings.glassTransparencyStrength) const applyGlassResponse = (element: HTMLElement) => { element.style.setProperty('--glass-background-visibility', String(materialResponse.backgroundVisibility)) element.style.setProperty('--glass-frost-blur-scale', String(materialResponse.frostBlurScale)) @@ -488,6 +490,7 @@ export function applyThemeCustomizerRootSettings( element.style.setProperty('--glass-tint-density', String(materialResponse.tintDensity)) element.style.setProperty('--glass-blur-surface', `${frostBlur.surface}px`) element.style.setProperty('--glass-blur-raised', `${frostBlur.raised}px`) + element.style.setProperty('--glass-overlay-clarity-blur', `${overlayClarityBlur}px`) } document.documentElement.setAttribute('data-glass-appearance', settings.glassAppearance) diff --git a/src/styles/__tests__/glassOverlayMaterial.spec.ts b/src/styles/__tests__/glassOverlayMaterial.spec.ts index 97d3c0cf..9d799856 100644 --- a/src/styles/__tests__/glassOverlayMaterial.spec.ts +++ b/src/styles/__tests__/glassOverlayMaterial.spec.ts @@ -8,7 +8,7 @@ describe('glass overlay material styles', () => { const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') expect(styles).toContain('calc(0.1 + var(--glass-surface-density, 0.62) * 0.22)') - expect(styles.match(/--glass-overlay-blur:\s*8px/g)).toHaveLength(2) + expect(styles.match(/--glass-overlay-blur:\s*var\(--glass-overlay-clarity-blur, 6px\)/g)).toHaveLength(2) expect(styles).toContain('--glass-overlay-saturate: 115%') expect(styles).toContain('--glass-overlay-saturate: 120%') expect(styles).toContain('--glass-overlay-blur: min(var(--glass-blur-raised), 36px)') @@ -21,6 +21,20 @@ describe('glass overlay material styles', () => { expect(styles).not.toContain('background: rgba(3, 7, 18, 62%)') }) + it('protects ordinary clear and tinted content without changing raised or frosted materials', () => { + const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') + + expect(styles).toContain('rgba(11, 19, 34, calc(0.12 + var(--glass-surface-density, 0.62) * 0.2))') + expect(styles).toContain('rgba(11, 19, 34, calc(0.13 + var(--glass-surface-density, 0.62) * 0.23))') + expect(styles).toContain('rgba(11, 19, 34, calc(0.12 + var(--glass-surface-density, 0.72) * 0.2)) 88%') + expect(styles).toContain('rgba(11, 19, 34, calc(0.13 + var(--glass-surface-density, 0.72) * 0.23)) 89%') + expect(styles).toContain('rgba(11, 19, 34, calc(0.07 + var(--glass-surface-density, 0.62) * 0.36))') + expect(styles).toContain('rgba(11, 19, 34, calc(0.07 + var(--glass-surface-density, 0.72) * 0.36)) 84%') + expect(styles).toContain('rgba(255, 255, 255, calc(0.035 + var(--glass-surface-density, 0.86) * 0.075))') + expect(styles).toContain('rgba(255, 255, 255, calc(0.03 + var(--glass-surface-density, 0.86) * 0.07))') + expect(styles).toContain('rgba(255, 255, 255, calc(0.045 + var(--glass-surface-density, 0.86) * 0.09))') + }) + it('renders colored chips as shadowless glass without flattening their variants', () => { const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') diff --git a/src/styles/themes/glass.scss b/src/styles/themes/glass.scss index aa8e58b6..1951fa27 100644 --- a/src/styles/themes/glass.scss +++ b/src/styles/themes/glass.scss @@ -5,8 +5,8 @@ // 材质只覆盖统一表面 token;质量档只替换光学层,不改变业务组件契约。 html[data-theme='glass'] { - --glass-surface: rgba(11, 19, 34, calc(0.02 + var(--glass-surface-density, 0.62) * 0.3)); - --glass-surface-soft: rgba(11, 19, 34, calc(0.04 + var(--glass-surface-density, 0.62) * 0.32)); + --glass-surface: rgba(11, 19, 34, calc(0.12 + var(--glass-surface-density, 0.62) * 0.2)); + --glass-surface-soft: rgba(11, 19, 34, calc(0.13 + var(--glass-surface-density, 0.62) * 0.23)); --glass-surface-raised: rgba(11, 19, 34, calc(0.07 + var(--glass-surface-density, 0.62) * 0.36)); --glass-control: rgba(11, 19, 34, 52%); --glass-control-prominent: rgba(255, 255, 255, 7%); @@ -54,7 +54,7 @@ html[data-theme='glass'] { --glass-navbar-backdrop-filter: var(--glass-raised-backdrop-filter); --glass-navbar-scrolled-backdrop-filter: blur(3px) saturate(115%); --glass-overlay-surface: rgba(11, 19, 34, calc(0.1 + var(--glass-surface-density, 0.62) * 0.22)); - --glass-overlay-blur: 8px; + --glass-overlay-blur: var(--glass-overlay-clarity-blur, 6px); --glass-overlay-saturate: 115%; --glass-overlay-scrim: rgba(3, 7, 18, 30%); --glass-overlay-backdrop-filter: blur(var(--glass-overlay-blur)) saturate(var(--glass-overlay-saturate)); @@ -130,12 +130,12 @@ html[data-theme='glass'] { &[data-glass-appearance='tinted'] { --glass-surface: color-mix( in srgb, - rgba(11, 19, 34, calc(0.02 + var(--glass-surface-density, 0.72) * 0.3)) 88%, + rgba(11, 19, 34, calc(0.12 + var(--glass-surface-density, 0.72) * 0.2)) 88%, rgba(var(--v-theme-primary), calc(var(--glass-tint-density, 0.65) * 0.28)) ); --glass-surface-soft: color-mix( in srgb, - rgba(11, 19, 34, calc(0.04 + var(--glass-surface-density, 0.72) * 0.32)) 89%, + rgba(11, 19, 34, calc(0.13 + var(--glass-surface-density, 0.72) * 0.23)) 89%, rgba(var(--v-theme-primary), calc(var(--glass-tint-density, 0.65) * 0.26)) ); --glass-surface-raised: color-mix( @@ -187,7 +187,7 @@ html[data-theme='glass'] { rgba(11, 19, 34, calc(0.09 + var(--glass-surface-density, 0.72) * 0.2)) 88%, rgba(var(--v-theme-primary), calc(var(--glass-tint-density, 0.65) * 0.22)) ); - --glass-overlay-blur: 8px; + --glass-overlay-blur: var(--glass-overlay-clarity-blur, 6px); --glass-overlay-saturate: 120%; --glass-overlay-scrim: rgba(3, 7, 18, 32%); --glass-chip-backdrop-filter: blur(10px) saturate(165%) brightness(var(--glass-transmission-brightness)); diff --git a/src/utils/__tests__/glassOptics.spec.ts b/src/utils/__tests__/glassOptics.spec.ts index 53425760..2b9ec7df 100644 --- a/src/utils/__tests__/glassOptics.spec.ts +++ b/src/utils/__tests__/glassOptics.spec.ts @@ -6,6 +6,7 @@ import { getGlassCssFrostBlur, getGlassCoverScale, getGlassMaterialResponse, + getGlassOverlayClarityBlur, getGlassOpticalCssTransmissionBrightness, getGlassOpticalDecay, getGlassOpticalBufferSize, @@ -269,6 +270,34 @@ describe('glass optics geometry', () => { ).toBe(true) }) + it('derives a continuous overlay clarity floor from transparency anchors', () => { + const anchors = [ + [0, 8.9], + [20, 7.8], + [50, 6.7], + [70, 5.8], + [85, 5.4], + [100, 5], + ] as const + + for (const [transparency, blur] of anchors) { + expect(getGlassOverlayClarityBlur(transparency)).toBe(blur) + } + + expect(getGlassOverlayClarityBlur(-1)).toBe(8.9) + expect(getGlassOverlayClarityBlur(101)).toBe(5) + expect(getGlassOverlayClarityBlur(Number.NaN)).toBe(6.7) + + const samples = Array.from({ length: 101 }, (_, value) => getGlassOverlayClarityBlur(value)) + expect(samples.every((sample, index) => index === 0 || sample <= samples[index - 1])).toBe(true) + + for (const anchor of [20, 50, 70, 85]) { + const blur = getGlassOverlayClarityBlur(anchor) + expect(Math.abs(getGlassOverlayClarityBlur(anchor - 1) - blur)).toBeLessThan(0.02) + expect(Math.abs(getGlassOverlayClarityBlur(anchor + 1) - blur)).toBeLessThan(0.03) + } + }) + it('returns preset copies so previews cannot mutate the shared matrix', () => { const first = getGlassOpticalPresetParameters('tinted', 'high', 'glide') first.translation = 0 diff --git a/src/utils/glassOptics.ts b/src/utils/glassOptics.ts index 2b383716..ffe17f8e 100644 --- a/src/utils/glassOptics.ts +++ b/src/utils/glassOptics.ts @@ -250,6 +250,7 @@ const GLASS_SURFACE_DENSITY: Record = { const GLASS_TINT_DENSITY = [1, 0.9, 0.65, 0.48, 0.36, 0.28] as const const GLASS_FROST_DENSITY = [1, 0.9, 0.7, 0.34, 0.12, 0.04] as const const GLASS_FROSTED_DENSITY = [1, 0.82, 0.55, 0.28, 0.1, 0.025] as const +const GLASS_OVERLAY_CLARITY_BLUR = [8.9, 7.8, 6.7, 5.8, 5.4, 5] as const /** 在相邻业务锚点之间使用零斜率边界插值,避免滑杆经过锚点时出现视觉折线。 */ function interpolateGlassResponse(value: unknown, anchors: readonly number[]) { @@ -292,6 +293,11 @@ export function getGlassCssFrostBlur(value: unknown) { } } +/** 透明与色调浮层保留独立模糊下限,避免高通透度使临时内容直接暴露在复杂壁纸上。 */ +export function getGlassOverlayClarityBlur(value: unknown) { + return interpolateGlassResponse(value, GLASS_OVERLAY_CLARITY_BLUR) +} + /** 计算与 CSS `ease` 相同的交叉淡化进度,使 DOM 壁纸与 shader 双纹理保持同一时钟。 */ export function getGlassWallpaperTransitionProgress(elapsed: number, duration: number) { if (duration <= 0 || elapsed >= duration) return 1