fix(glass): stabilize optical rendering and page presentation (#595)

This commit is contained in:
InfinityPacer
2026-07-28 10:41:41 +08:00
committed by GitHub
parent 3daa031a04
commit 8a4ca2482c
22 changed files with 1641 additions and 449 deletions
+4 -4
View File
@@ -66,7 +66,7 @@ describe('glass optics geometry', () => {
expect(getGlassOpticalReflectionStrengthScale(0)).toBe(0)
expect(getGlassOpticalReflectionStrengthScale(50)).toBe(1)
expect(getGlassOpticalReflectionStrengthScale(100)).toBeCloseTo(GLASS_OPTICAL_REFLECTION_MAX_SCALE)
expect(getGlassOpticalTransparency(0)).toBeCloseTo(0.28)
expect(getGlassOpticalTransparency(0)).toBe(0)
expect(getGlassOpticalTransparency(50)).toBeGreaterThan(0.6)
expect(getGlassOpticalTransparency(70)).toBeCloseTo(0.96)
expect(getGlassOpticalTransparency(100)).toBeCloseTo(1.1)
@@ -76,9 +76,9 @@ describe('glass optics geometry', () => {
expect(getGlassOpticalTransmissionStrength(0)).toBe(0)
expect(getGlassOpticalTransmissionStrength(70)).toBe(1)
expect(getGlassOpticalTransmissionStrength(100)).toBe(1.3)
expect(getGlassOpticalCssTransmissionBrightness(0)).toBeCloseTo(0.82)
expect(getGlassOpticalCssTransmissionBrightness(70)).toBeCloseTo(1.28)
expect(getGlassOpticalCssTransmissionBrightness(100)).toBeCloseTo(1.48)
expect(getGlassOpticalCssTransmissionBrightness(0)).toBeCloseTo(0.84)
expect(getGlassOpticalCssTransmissionBrightness(70)).toBeCloseTo(1)
expect(getGlassOpticalCssTransmissionBrightness(100)).toBeCloseTo(1.08)
})
it('keeps presets as concrete six-parameter values', () => {
@@ -0,0 +1,35 @@
import { DEFAULT_GLASS_WALLPAPER_TONE_PROFILE, getGlassWallpaperToneProfile } from '@/utils/glassWallpaperTone'
import { describe, expect, it } from 'vitest'
describe('glass wallpaper tone profile', () => {
it('keeps a representative mid-tone wallpaper near neutral exposure', () => {
const profile = getGlassWallpaperToneProfile([0.18, 0.3, 0.36, 0.4, 0.52, 0.72, 0.82])
expect(profile.medianLuminance).toBeCloseTo(0.4)
expect(profile.highlightLuminance).toBeCloseTo(0.72)
expect(profile.exposure).toBeGreaterThan(0.98)
expect(profile.exposure).toBeLessThan(1.04)
})
it('uses bounded compensation without erasing bright and dark wallpaper character', () => {
const dark = getGlassWallpaperToneProfile([0.01, 0.03, 0.06, 0.1, 0.14, 0.22, 0.32])
const bright = getGlassWallpaperToneProfile([0.42, 0.58, 0.7, 0.78, 0.86, 0.94, 1])
expect(dark.exposure).toBe(1.14)
expect(bright.exposure).toBeGreaterThanOrEqual(0.88)
expect(bright.exposure).toBeLessThan(0.92)
expect(dark.exposure).toBeGreaterThan(bright.exposure)
})
it('uses the highlight percentile to lower exposure for locally overbright wallpapers', () => {
const controlled = getGlassWallpaperToneProfile([0.2, 0.28, 0.34, 0.38, 0.42, 0.5, 0.58])
const highlighted = getGlassWallpaperToneProfile([0.2, 0.28, 0.34, 0.38, 0.42, 0.95, 1])
expect(highlighted.medianLuminance).toBe(controlled.medianLuminance)
expect(highlighted.exposure).toBeLessThan(controlled.exposure)
})
it('falls back to the neutral profile when no valid samples exist', () => {
expect(getGlassWallpaperToneProfile([Number.NaN])).toEqual(DEFAULT_GLASS_WALLPAPER_TONE_PROFILE)
})
})
+5 -5
View File
@@ -290,7 +290,7 @@ export function getGlassOpticalTransparency(value: unknown) {
if (normalized <= GLASS_OPTICAL_REFERENCE_STRENGTH) {
const progress = normalized / GLASS_OPTICAL_REFERENCE_STRENGTH
return 0.28 + 0.68 * progress ** 1.25
return 0.96 * progress ** 0.83
}
const highRangeProgress =
@@ -299,19 +299,19 @@ export function getGlassOpticalTransparency(value: unknown) {
return 0.96 + 0.14 * highRangeProgress ** 1.35
}
/** 标准 CSS 材质使用受控亮度曲线,并在高区间保留有限余量。 */
/** 标准 CSS 材质在壁纸归一化后只做窄幅目标亮度调整,避免重新放大原始明暗差异。 */
export function getGlassOpticalCssTransmissionBrightness(value: unknown) {
const transmission = getGlassOpticalTransmissionStrength(value)
if (transmission <= 1) {
return 0.82 + 0.46 * transmission ** 1.1
return 0.84 + 0.16 * transmission ** 1.05
}
const progress = (transmission - 1) / 0.3
return 1.28 + 0.2 * progress ** 1.2
return 1 + 0.08 * progress ** 1.1
}
/** 实时 renderer 接收透射响应,并在 shader 中按材质和质量执行高亮保护。 */
/** 实时 renderer 以 70 为归一化目标亮度参考,并在 shader 中按材质和质量执行高亮保护。 */
export function getGlassOpticalTransmissionStrength(value: unknown) {
const normalized = normalizeGlassOpticalStrength(value)
if (normalized <= GLASS_OPTICAL_REFERENCE_STRENGTH) {
+132
View File
@@ -0,0 +1,132 @@
export interface GlassWallpaperToneProfile {
/** 进入材质曲线前的有限整体曝光,避免壁纸明暗差异直接放大到操作表面。 */
exposure: number
/** 壁纸采样的高亮分位亮度,用于约束亮场曝光。 */
highlightLuminance: number
/** 壁纸采样的中位亮度,用于确定稳健的整体曝光。 */
medianLuminance: number
}
const ANALYSIS_MAX_EDGE = 64
const PROFILE_CACHE_LIMIT = 32
const PROFILE_LOAD_TIMEOUT_MS = 3000
const EXPOSURE_MIN = 0.88
const EXPOSURE_MAX = 1.14
const MEDIAN_TARGET = 0.38
const HIGHLIGHT_TARGET = 0.82
export const DEFAULT_GLASS_WALLPAPER_TONE_PROFILE: GlassWallpaperToneProfile = {
exposure: 1,
highlightLuminance: HIGHLIGHT_TARGET,
medianLuminance: MEDIAN_TARGET,
}
const profileCache = new Map<string, Promise<GlassWallpaperToneProfile>>()
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value))
}
function readPercentile(sorted: number[], percentile: number) {
if (!sorted.length) return 0
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.round((sorted.length - 1) * percentile)))]
}
/**
* 以中位亮度为主、高亮分位为辅计算有限曝光。
* 指数响应只缩小跨壁纸差异,保留亮场与暗场各自的视觉性格。
*/
export function getGlassWallpaperToneProfile(luminances: number[]): GlassWallpaperToneProfile {
const sorted = luminances
.filter(Number.isFinite)
.map(value => clamp(value, 0, 1))
.sort((a, b) => a - b)
if (!sorted.length) return { ...DEFAULT_GLASS_WALLPAPER_TONE_PROFILE }
const medianLuminance = readPercentile(sorted, 0.5)
const highlightLuminance = readPercentile(sorted, 0.9)
const medianExposure = (MEDIAN_TARGET / Math.max(medianLuminance, 0.06)) ** 0.22
const highlightExposure = (HIGHLIGHT_TARGET / Math.max(highlightLuminance, 0.15)) ** 0.16
const exposure = clamp(medianExposure * 0.75 + highlightExposure * 0.25, EXPOSURE_MIN, EXPOSURE_MAX)
return {
exposure,
highlightLuminance,
medianLuminance,
}
}
/** 从已解码图片生成与 renderer、DOM 背景层共用的稳健亮度 profile。 */
export function analyzeGlassWallpaperTone(image: CanvasImageSource, width: number, height: number) {
if (typeof document === 'undefined' || width <= 0 || height <= 0) {
return { ...DEFAULT_GLASS_WALLPAPER_TONE_PROFILE }
}
try {
const scale = Math.min(1, ANALYSIS_MAX_EDGE / Math.max(width, height))
const canvas = document.createElement('canvas')
canvas.width = Math.max(1, Math.round(width * scale))
canvas.height = Math.max(1, Math.round(height * scale))
const context = canvas.getContext('2d', { willReadFrequently: true })
if (!context) return { ...DEFAULT_GLASS_WALLPAPER_TONE_PROFILE }
context.drawImage(image, 0, 0, canvas.width, canvas.height)
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data
const luminances: number[] = []
for (let offset = 0; offset < pixels.length; offset += 4) {
if (pixels[offset + 3] < 128) continue
luminances.push((pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722) / 255)
}
return getGlassWallpaperToneProfile(luminances)
} catch {
return { ...DEFAULT_GLASS_WALLPAPER_TONE_PROFILE }
}
}
function rememberProfile(url: string, profile: Promise<GlassWallpaperToneProfile>) {
if (profileCache.size >= PROFILE_CACHE_LIMIT) {
const oldestKey = profileCache.keys().next().value
if (oldestKey) profileCache.delete(oldestKey)
}
profileCache.set(url, profile)
return profile
}
/**
* 为 DOM 背景层加载可读像素。跨域来源不支持 CORS 时回落中性 profile
* 避免亮度分析阻断壁纸本身的 CSS 显示能力。
*/
export function loadGlassWallpaperToneProfile(url: string): Promise<GlassWallpaperToneProfile> {
if (!url || typeof Image === 'undefined') return Promise.resolve({ ...DEFAULT_GLASS_WALLPAPER_TONE_PROFILE })
const cached = profileCache.get(url)
if (cached) return cached
const profile = new Promise<GlassWallpaperToneProfile>(resolve => {
const image = new Image()
let settled = false
const finish = (result: GlassWallpaperToneProfile) => {
if (settled) return
settled = true
window.clearTimeout(timeout)
resolve(result)
}
const timeout = window.setTimeout(
() => finish({ ...DEFAULT_GLASS_WALLPAPER_TONE_PROFILE }),
PROFILE_LOAD_TIMEOUT_MS,
)
image.crossOrigin = 'anonymous'
image.decoding = 'async'
image.onload = () =>
finish(analyzeGlassWallpaperTone(image, image.naturalWidth || image.width, image.naturalHeight || image.height))
image.onerror = () => finish({ ...DEFAULT_GLASS_WALLPAPER_TONE_PROFILE })
image.src = url
})
return rememberProfile(url, profile)
}