fix(glass): preserve content clarity across transparency (#656) (#657)

Co-authored-by: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com>
This commit is contained in:
jxxghp
2026-08-10 12:49:05 +08:00
committed by GitHub
parent bb53a181a9
commit 6a4af66683
6 changed files with 80 additions and 7 deletions

View File

@@ -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', () => {

View File

@@ -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)

View File

@@ -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')

View File

@@ -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));

View File

@@ -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

View File

@@ -250,6 +250,7 @@ const GLASS_SURFACE_DENSITY: Record<GlassAppearance, readonly number[]> = {
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