mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-15 18:45:36 +08:00
760 lines
30 KiB
TypeScript
760 lines
30 KiB
TypeScript
export const GLASS_OPTICAL_MAX_SURFACES_DESKTOP = 8
|
||
export const GLASS_OPTICAL_MAX_SURFACES_MOBILE = 5
|
||
export const GLASS_OPTICAL_MOTION_MAX_SCALE = 3.2
|
||
export const GLASS_OPTICAL_DEFORMATION_MAX_SCALE = 1.55
|
||
export const GLASS_OPTICAL_FLOW_MAX_SCALE = 1.45
|
||
export const GLASS_OPTICAL_REFLECTION_MAX_SCALE = 1.8
|
||
export const GLASS_OPTICAL_TRANSLATION_MAX_SCALE = 1.7
|
||
export const GLASS_OPTICAL_STRENGTH_DEFAULT = 50
|
||
export const GLASS_OPTICAL_STRENGTH_MAX = 100
|
||
export const GLASS_OPTICAL_STRENGTH_MIN = 0
|
||
export const GLASS_OPTICAL_REFERENCE_STRENGTH = 70
|
||
|
||
export type GlassAppearance = 'clear' | 'frosted' | 'tinted'
|
||
export type GlassOpticalCapability = 'balanced' | 'css' | 'high'
|
||
export type GlassOpticalPreset = 'glide' | 'liquid' | 'natural'
|
||
export type GlassOpticalPresetKey = `${GlassAppearance}:${GlassOpticalCapability}:${GlassOpticalPreset}`
|
||
export type GlassOpticalPresetOverrides = Partial<Record<GlassOpticalPresetKey, GlassOpticalParameters>>
|
||
export type GlassOpticalQuality = 'balanced' | 'high'
|
||
export type GlassCornerRadii = [number, number, number, number]
|
||
|
||
export interface GlassOpticalParameters {
|
||
/** 局部非均匀折射与内容弯曲强度。 */
|
||
deformation: number
|
||
/** 轨迹范围、尾波、惯性与收敛强度。 */
|
||
flow: number
|
||
/** 方向高光、迎光棱镜与背光吸收强度。 */
|
||
reflection: number
|
||
/** 玻璃内部壁纸采样的明暗与暗部展开强度。 */
|
||
transmission: number
|
||
/** 共享壁纸采样在表面内的统一坐标平移强度。 */
|
||
translation: number
|
||
/** 壁纸可见度与材质遮罩强度。 */
|
||
transparency: number
|
||
}
|
||
|
||
export interface GlassMaterialResponse {
|
||
/** 真实壁纸在表面材质后的感知可见程度。 */
|
||
backgroundVisibility: number
|
||
/** 磨砂预滤纹理允许保留的高频细节比例。 */
|
||
frostDetailLevel: number
|
||
/** 标准档 CSS 磨砂半径相对既有 40px 基线的缩放。 */
|
||
frostBlurScale: number
|
||
/** 表面遮罩对真实壁纸的覆盖密度。 */
|
||
surfaceDensity: number
|
||
/** 色调材质的主体染色密度。 */
|
||
tintDensity: number
|
||
}
|
||
|
||
export interface GlassInteractionPoint {
|
||
/** 指针或触点相对视口的横坐标。 */
|
||
x: number
|
||
/** 指针或触点相对视口的纵坐标。 */
|
||
y: number
|
||
}
|
||
|
||
export interface GlassOpticalRect {
|
||
/** 元素的实际高度,元素可部分位于视口外。 */
|
||
height: number
|
||
/** 按左上、右上、右下、左下排列的 CSS 圆角像素值。 */
|
||
radii: GlassCornerRadii
|
||
/** 表面的场景优先级,数值越小越优先。 */
|
||
rank: number
|
||
/** 元素的实际宽度,元素可部分位于视口外。 */
|
||
width: number
|
||
/** 元素左边缘相对视口的位置。 */
|
||
x: number
|
||
/** 元素上边缘相对视口的位置。 */
|
||
y: number
|
||
}
|
||
|
||
export interface GlassOpticalBufferSize {
|
||
height: number
|
||
width: number
|
||
}
|
||
|
||
export interface GlassOpticalPoint {
|
||
/** 归一化或视口坐标系中的横向分量。 */
|
||
x: number
|
||
/** 归一化或视口坐标系中的纵向分量。 */
|
||
y: number
|
||
}
|
||
|
||
export interface GlassOpticalSpringState {
|
||
/** 当前跟随位置。 */
|
||
position: number
|
||
/** 当前每秒位移速度。 */
|
||
velocity: number
|
||
}
|
||
|
||
/** 光学表面在共享 renderer 中采用的动态响应合同。 */
|
||
export type GlassOpticalSurfaceMode = 'dynamic' | 'static-material'
|
||
|
||
export interface GlassOpticalSurfaceCandidate<TKey> {
|
||
/** renderer 生命周期内稳定的表面身份。 */
|
||
key: TKey
|
||
/** 表面使用完整动态光学,或只保留稳定材质能量。 */
|
||
mode?: GlassOpticalSurfaceMode
|
||
/** 表面的当前视口几何。 */
|
||
rect: GlassOpticalRect
|
||
}
|
||
|
||
export interface GlassOpticalSurfaceSlot<TKey> extends GlassOpticalSurfaceCandidate<TKey> {
|
||
/** 槽位在本次交互中的职责。 */
|
||
role: 'active' | 'outgoing' | 'stable'
|
||
}
|
||
|
||
export interface GlassOpticalRenderProfile {
|
||
/** 光学层内部缓冲使用的质量档位。 */
|
||
bufferQuality: GlassOpticalQuality
|
||
/** 是否使用额外壁纸采样保护人物和文字等高梯度内容。 */
|
||
contentProtection: boolean
|
||
/** 磨砂扩散使用的纹理采样数。 */
|
||
diffusionSamples: 5 | 9
|
||
/** 是否使用带时序记忆的液态位移场。 */
|
||
flowField: boolean
|
||
/** 时序位移场能量衰减到一半所需的时间。 */
|
||
flowHalfLife: number
|
||
/** 动态折射在视口像素空间中的软上限。 */
|
||
maxRefractionPixels: number
|
||
/** 输入停止后液态形态收敛到静态所需的时间。 */
|
||
motionDuration: number
|
||
/** 主液态反馈能量衰减到一半所需的时间。 */
|
||
motionHalfLife: number
|
||
/** 高质量缓冲允许使用的设备像素比上限。 */
|
||
pixelRatioCap: number
|
||
/** 新输入立即作用于镜片位置的比例。 */
|
||
pointerImmediateResponse: number
|
||
/** 镜片跟随弹簧的阻尼比。 */
|
||
springDamping: number
|
||
/** 镜片跟随弹簧的角频率,单位弧度每秒。 */
|
||
springFrequency: number
|
||
/** 活动壁纸进入 GPU 前的最长边限制。 */
|
||
textureLimit: number
|
||
textureSource: 'auto' | 'procedural' | 'wallpaper'
|
||
/** 参与液态方向计算的最近输入采样数量。 */
|
||
trailCount: number
|
||
}
|
||
|
||
/** 将用户滑杆输入收敛到 renderer 支持的整数范围,非法存量值回落到默认视觉。 */
|
||
export function normalizeGlassOpticalStrength(value: unknown) {
|
||
if (typeof value !== 'number' || !Number.isFinite(value)) return GLASS_OPTICAL_STRENGTH_DEFAULT
|
||
|
||
return Math.min(GLASS_OPTICAL_STRENGTH_MAX, Math.max(GLASS_OPTICAL_STRENGTH_MIN, Math.round(value)))
|
||
}
|
||
|
||
type GlassOpticalPresetSet = Record<GlassOpticalPreset, GlassOpticalParameters>
|
||
type GlassOpticalCapabilityPresets = {
|
||
balanced: GlassOpticalPresetSet
|
||
css: Pick<GlassOpticalPresetSet, 'natural'>
|
||
high: GlassOpticalPresetSet
|
||
}
|
||
|
||
const GLASS_OPTICAL_PRESET_MATRIX: Record<GlassAppearance, GlassOpticalCapabilityPresets> = {
|
||
clear: {
|
||
css: {
|
||
natural: { deformation: 48, flow: 48, reflection: 42, transmission: 67, translation: 48, transparency: 52 },
|
||
},
|
||
balanced: {
|
||
natural: { deformation: 48, flow: 48, reflection: 42, transmission: 65, translation: 48, transparency: 50 },
|
||
glide: { deformation: 29, flow: 42, reflection: 35, transmission: 70, translation: 70, transparency: 60 },
|
||
liquid: { deformation: 67, flow: 73, reflection: 43, transmission: 61, translation: 54, transparency: 55 },
|
||
},
|
||
high: {
|
||
natural: { deformation: 48, flow: 48, reflection: 38, transmission: 64, translation: 48, transparency: 49 },
|
||
glide: { deformation: 31, flow: 44, reflection: 34, transmission: 67, translation: 71, transparency: 59 },
|
||
liquid: { deformation: 71, flow: 77, reflection: 42, transmission: 60, translation: 55, transparency: 54 },
|
||
},
|
||
},
|
||
tinted: {
|
||
css: {
|
||
natural: { deformation: 48, flow: 48, reflection: 46, transmission: 65, translation: 48, transparency: 37 },
|
||
},
|
||
balanced: {
|
||
natural: { deformation: 50, flow: 48, reflection: 46, transmission: 67, translation: 48, transparency: 34 },
|
||
glide: { deformation: 31, flow: 42, reflection: 41, transmission: 73, translation: 67, transparency: 43 },
|
||
liquid: { deformation: 70, flow: 73, reflection: 47, transmission: 64, translation: 54, transparency: 39 },
|
||
},
|
||
high: {
|
||
natural: { deformation: 50, flow: 48, reflection: 42, transmission: 65, translation: 48, transparency: 32 },
|
||
glide: { deformation: 32, flow: 44, reflection: 38, transmission: 71, translation: 70, transparency: 41 },
|
||
liquid: { deformation: 73, flow: 77, reflection: 46, transmission: 61, translation: 55, transparency: 37 },
|
||
},
|
||
},
|
||
frosted: {
|
||
css: {
|
||
natural: { deformation: 48, flow: 48, reflection: 37, transmission: 60, translation: 48, transparency: 43 },
|
||
},
|
||
balanced: {
|
||
natural: { deformation: 55, flow: 50, reflection: 37, transmission: 62, translation: 46, transparency: 40 },
|
||
glide: { deformation: 36, flow: 42, reflection: 32, transmission: 67, translation: 65, transparency: 55 },
|
||
liquid: { deformation: 74, flow: 73, reflection: 38, transmission: 59, translation: 50, transparency: 47 },
|
||
},
|
||
high: {
|
||
natural: { deformation: 58, flow: 50, reflection: 35, transmission: 60, translation: 46, transparency: 37 },
|
||
glide: { deformation: 38, flow: 44, reflection: 30, transmission: 65, translation: 67, transparency: 53 },
|
||
liquid: { deformation: 79, flow: 77, reflection: 37, transmission: 56, translation: 52, transparency: 44 },
|
||
},
|
||
},
|
||
}
|
||
|
||
/** 返回材质、质量与预置共同确定的六个具体参数,调用方可以安全修改返回值。 */
|
||
export function getGlassOpticalPresetParameters(
|
||
appearance: GlassAppearance,
|
||
quality: GlassOpticalCapability,
|
||
preset: GlassOpticalPreset,
|
||
): GlassOpticalParameters {
|
||
const presets = GLASS_OPTICAL_PRESET_MATRIX[appearance][quality]
|
||
const parameters = 'natural' === preset ? presets.natural : (presets as Partial<GlassOpticalPresetSet>)[preset]
|
||
|
||
return { ...(parameters ?? presets.natural) }
|
||
}
|
||
|
||
/** 标准档只保留自然基线;实时档同时开放滑移与液态方案。 */
|
||
export function getAvailableGlassOpticalPresets(quality: GlassOpticalCapability): GlassOpticalPreset[] {
|
||
return quality === 'css' ? ['natural'] : ['natural', 'glide', 'liquid']
|
||
}
|
||
|
||
/** 生成持久化覆盖使用的稳定组合键;标准档始终归入自然方案。 */
|
||
export function getGlassOpticalPresetKey(
|
||
appearance: GlassAppearance,
|
||
quality: GlassOpticalCapability,
|
||
preset: GlassOpticalPreset,
|
||
): GlassOpticalPresetKey {
|
||
return `${appearance}:${quality}:${quality === 'css' ? 'natural' : preset}`
|
||
}
|
||
|
||
/** 切换组合时优先恢复用户覆盖,没有覆盖才返回预设矩阵副本。 */
|
||
export function getGlassOpticalPresetParametersWithOverrides(
|
||
appearance: GlassAppearance,
|
||
quality: GlassOpticalCapability,
|
||
preset: GlassOpticalPreset,
|
||
overrides: GlassOpticalPresetOverrides,
|
||
) {
|
||
const key = getGlassOpticalPresetKey(appearance, quality, preset)
|
||
|
||
return { ...(overrides[key] ?? getGlassOpticalPresetParameters(appearance, quality, preset)) }
|
||
}
|
||
|
||
const GLASS_RESPONSE_STOPS = [0, 20, 50, 70, 85, 100] as const
|
||
const GLASS_BACKGROUND_VISIBILITY: Record<GlassAppearance, readonly number[]> = {
|
||
clear: [0.18, 0.3, 0.58, 0.77, 0.9, 0.96],
|
||
tinted: [0.08, 0.2, 0.48, 0.7, 0.84, 0.92],
|
||
frosted: [0.04, 0.22, 0.52, 0.72, 0.89, 0.98],
|
||
}
|
||
const GLASS_SURFACE_DENSITY: Record<GlassAppearance, readonly number[]> = {
|
||
clear: [1, 0.88, 0.62, 0.42, 0.26, 0.18],
|
||
tinted: [1, 0.92, 0.72, 0.52, 0.39, 0.3],
|
||
frosted: [1, 0.9, 0.7, 0.52, 0.36, 0.22],
|
||
}
|
||
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[]) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
const upperIndex = GLASS_RESPONSE_STOPS.findIndex(stop => normalized <= stop)
|
||
if (upperIndex <= 0) return anchors[0]
|
||
|
||
const lowerIndex = upperIndex - 1
|
||
const lowerStop = GLASS_RESPONSE_STOPS[lowerIndex]
|
||
const upperStop = GLASS_RESPONSE_STOPS[upperIndex]
|
||
const linearProgress = (normalized - lowerStop) / (upperStop - lowerStop)
|
||
const smoothProgress = linearProgress * linearProgress * (3 - 2 * linearProgress)
|
||
|
||
return anchors[lowerIndex] + (anchors[upperIndex] - anchors[lowerIndex]) * smoothProgress
|
||
}
|
||
|
||
/**
|
||
* 一个通透度输入派生互不混用的材质响应;tone、曝光和透射亮度不在此处计算。
|
||
*/
|
||
export function getGlassMaterialResponse(appearance: GlassAppearance, value: unknown): GlassMaterialResponse {
|
||
const frostDensity = interpolateGlassResponse(
|
||
value,
|
||
appearance === 'frosted' ? GLASS_FROSTED_DENSITY : GLASS_FROST_DENSITY,
|
||
)
|
||
|
||
return {
|
||
backgroundVisibility: interpolateGlassResponse(value, GLASS_BACKGROUND_VISIBILITY[appearance]),
|
||
frostBlurScale: 0.4 + frostDensity * 1.2,
|
||
frostDetailLevel: 1 - frostDensity,
|
||
surfaceDensity: interpolateGlassResponse(value, GLASS_SURFACE_DENSITY[appearance]),
|
||
tintDensity: interpolateGlassResponse(value, GLASS_TINT_DENSITY),
|
||
}
|
||
}
|
||
|
||
/** 标准档磨砂使用独立的 surface/raised 半径锚点,不借用背景亮度制造厚度。 */
|
||
export function getGlassCssFrostBlur(value: unknown) {
|
||
return {
|
||
raised: interpolateGlassResponse(value, [84, 70, 50, 35, 24, 16]),
|
||
surface: interpolateGlassResponse(value, [64, 52, 36, 24, 15, 8]),
|
||
}
|
||
}
|
||
|
||
/** 透明与色调浮层保留独立模糊下限,避免高通透度使临时内容直接暴露在复杂壁纸上。 */
|
||
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
|
||
if (elapsed <= 0) return 0
|
||
|
||
const target = elapsed / duration
|
||
const sample = (time: number, start: number, end: number) => {
|
||
const inverse = 1 - time
|
||
|
||
return 3 * inverse * inverse * time * start + 3 * inverse * time * time * end + time * time * time
|
||
}
|
||
let lower = 0
|
||
let upper = 1
|
||
let parameter = target
|
||
|
||
for (let iteration = 0; iteration < 10; iteration += 1) {
|
||
parameter = (lower + upper) * 0.5
|
||
if (sample(parameter, 0.25, 0.25) < target) lower = parameter
|
||
else upper = parameter
|
||
}
|
||
|
||
return sample(parameter, 0.1, 1)
|
||
}
|
||
|
||
/**
|
||
* 流动强度使用感知曲线:中点保持既有视觉,高区间同时释放更大的形变幅度与空间范围。
|
||
* 最大值仍受质量档和内容保护共同约束,避免高梯度海报出现无界拉伸。
|
||
*/
|
||
export function getGlassOpticalMotionStrengthScale(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
const normalizedRatio = normalized / GLASS_OPTICAL_STRENGTH_DEFAULT
|
||
|
||
return normalizedRatio ** Math.log2(GLASS_OPTICAL_MOTION_MAX_SCALE)
|
||
}
|
||
|
||
/** 采样平移保持中点等于既有即时位移,并在高区间受控增长。 */
|
||
export function getGlassOpticalTranslationStrengthScale(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
const normalizedRatio = normalized / GLASS_OPTICAL_STRENGTH_DEFAULT
|
||
|
||
return normalizedRatio ** Math.log2(GLASS_OPTICAL_TRANSLATION_MAX_SCALE)
|
||
}
|
||
|
||
/** 非均匀形变独立缩放局部折射,最终像素位移仍受质量档软上限限制。 */
|
||
export function getGlassOpticalDeformationStrengthScale(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
const normalizedRatio = normalized / GLASS_OPTICAL_STRENGTH_DEFAULT
|
||
|
||
return normalizedRatio ** Math.log2(GLASS_OPTICAL_DEFORMATION_MAX_SCALE)
|
||
}
|
||
|
||
/** 流动维度只延展轨迹、尾波与惯性,中点保持当前时序手感。 */
|
||
export function getGlassOpticalFlowStrengthScale(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
const normalizedRatio = normalized / GLASS_OPTICAL_STRENGTH_DEFAULT
|
||
|
||
return normalizedRatio ** Math.log2(GLASS_OPTICAL_FLOW_MAX_SCALE)
|
||
}
|
||
|
||
/** 流动强度在完整滑杆区间连续控制轨迹范围,低值也能明显收紧空间足迹。 */
|
||
export function getGlassOpticalMotionExpansion(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
|
||
return (normalized / GLASS_OPTICAL_STRENGTH_MAX) ** 1.4
|
||
}
|
||
|
||
/** 最大几何形变只由质量档约束;流动滑杆改变覆盖与连续性,不继续拉伸背景内容。 */
|
||
export function getGlassOpticalMaxRefractionPixels(basePixels: number, _value: unknown) {
|
||
void _value
|
||
|
||
return basePixels
|
||
}
|
||
|
||
/** 反射强度使用独立感知曲线,只控制光学亮度,不放大背景位移。 */
|
||
export function getGlassOpticalReflectionStrengthScale(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
const normalizedRatio = normalized / GLASS_OPTICAL_STRENGTH_DEFAULT
|
||
|
||
return normalizedRatio ** Math.log2(GLASS_OPTICAL_REFLECTION_MAX_SCALE)
|
||
}
|
||
|
||
/** 通透度独立控制真实背景与可读性遮罩的占比,高区间仍受 shader 可读性上限保护。 */
|
||
export function getGlassOpticalTransparency(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
if (normalized <= GLASS_OPTICAL_REFERENCE_STRENGTH) {
|
||
const progress = normalized / GLASS_OPTICAL_REFERENCE_STRENGTH
|
||
|
||
return 0.96 * progress ** 0.83
|
||
}
|
||
|
||
const highRangeProgress =
|
||
(normalized - GLASS_OPTICAL_REFERENCE_STRENGTH) / (GLASS_OPTICAL_STRENGTH_MAX - GLASS_OPTICAL_REFERENCE_STRENGTH)
|
||
|
||
return 0.96 + 0.14 * highRangeProgress ** 1.35
|
||
}
|
||
|
||
/** 标准 CSS 材质在壁纸归一化后只做窄幅目标亮度调整,避免重新放大原始明暗差异。 */
|
||
export function getGlassOpticalCssTransmissionBrightness(value: unknown) {
|
||
const transmission = getGlassOpticalTransmissionStrength(value)
|
||
if (transmission <= 1) {
|
||
return 0.84 + 0.16 * transmission ** 1.05
|
||
}
|
||
|
||
const progress = (transmission - 1) / 0.3
|
||
|
||
return 1 + 0.08 * progress ** 1.1
|
||
}
|
||
|
||
/** 实时 renderer 以 70 为归一化目标亮度参考,并在 shader 中按材质和质量执行高亮保护。 */
|
||
export function getGlassOpticalTransmissionStrength(value: unknown) {
|
||
const normalized = normalizeGlassOpticalStrength(value)
|
||
if (normalized <= GLASS_OPTICAL_REFERENCE_STRENGTH) {
|
||
return normalized / GLASS_OPTICAL_REFERENCE_STRENGTH
|
||
}
|
||
|
||
const highRangeProgress =
|
||
(normalized - GLASS_OPTICAL_REFERENCE_STRENGTH) / (GLASS_OPTICAL_STRENGTH_MAX - GLASS_OPTICAL_REFERENCE_STRENGTH)
|
||
|
||
return 1 + 0.3 * highRangeProgress ** 1.2
|
||
}
|
||
|
||
/** 质量决定合成缓冲与纹理上限;路由只切换纹理来源,不改变质量档位。 */
|
||
export function getGlassOpticalRenderProfile(
|
||
quality: GlassOpticalQuality,
|
||
routeKey: string,
|
||
): GlassOpticalRenderProfile {
|
||
const highQuality = quality === 'high'
|
||
|
||
return {
|
||
bufferQuality: quality,
|
||
contentProtection: highQuality,
|
||
diffusionSamples: highQuality ? 9 : 5,
|
||
flowField: highQuality,
|
||
flowHalfLife: highQuality ? 130 : 0,
|
||
maxRefractionPixels: highQuality ? 9 : 6,
|
||
motionDuration: highQuality ? 540 : 360,
|
||
motionHalfLife: highQuality ? 125 : 82,
|
||
pixelRatioCap: highQuality ? 1.5 : 1,
|
||
pointerImmediateResponse: highQuality ? 0.58 : 0.7,
|
||
springDamping: highQuality ? 0.78 : 0.9,
|
||
springFrequency: highQuality ? 18 : 24,
|
||
textureLimit: highQuality ? 4096 : 3072,
|
||
textureSource: routeKey.startsWith('/login') ? 'auto' : 'wallpaper',
|
||
trailCount: highQuality ? 4 : 2,
|
||
}
|
||
}
|
||
|
||
/** 将时间常数转换为与刷新率无关的指数衰减。 */
|
||
export function getGlassOpticalDecay(halfLife: number, delta: number) {
|
||
if (halfLife <= 0) return 0
|
||
if (delta <= 0) return 1
|
||
|
||
return 2 ** (-delta / halfLife)
|
||
}
|
||
|
||
/** 输入停止后按时间收敛,尾段平滑归零以恢复事件驱动静止状态。 */
|
||
export function getGlassOpticalMotionEnergy(elapsed: number, duration: number, halfLife: number) {
|
||
if (elapsed >= duration) return 0
|
||
|
||
const safeElapsed = Math.max(0, elapsed)
|
||
const tailDuration = Math.max(1, duration * 0.25)
|
||
const tailProgress = Math.min(1, Math.max(0, (duration - safeElapsed) / tailDuration))
|
||
const tailTaper = tailProgress * tailProgress * (3 - 2 * tailProgress)
|
||
|
||
return getGlassOpticalDecay(halfLife, safeElapsed) * tailTaper
|
||
}
|
||
|
||
/**
|
||
* 解析阻尼弹簧的精确时间步,避免刷新率变化改变跟随手感。
|
||
* 欠阻尼参数只允许一次可见回摆,生命周期结束后由 renderer 归零。
|
||
*/
|
||
export function stepGlassOpticalSpring(
|
||
state: GlassOpticalSpringState,
|
||
target: number,
|
||
deltaMs: number,
|
||
frequency: number,
|
||
damping: number,
|
||
): GlassOpticalSpringState {
|
||
const deltaSeconds = Math.min(0.064, Math.max(0, deltaMs / 1000))
|
||
const safeFrequency = Math.max(0.001, frequency)
|
||
const safeDamping = Math.max(0, damping)
|
||
const offset = state.position - target
|
||
if (deltaSeconds <= 0) return state
|
||
|
||
if (safeDamping >= 1) {
|
||
const decay = Math.exp(-safeFrequency * deltaSeconds)
|
||
const coefficient = state.velocity + safeFrequency * offset
|
||
|
||
return {
|
||
position: target + (offset + coefficient * deltaSeconds) * decay,
|
||
velocity: (state.velocity - safeFrequency * coefficient * deltaSeconds) * decay,
|
||
}
|
||
}
|
||
|
||
const dampedFrequency = safeFrequency * Math.sqrt(1 - safeDamping * safeDamping)
|
||
const decay = Math.exp(-safeDamping * safeFrequency * deltaSeconds)
|
||
const angle = dampedFrequency * deltaSeconds
|
||
const cosine = Math.cos(angle)
|
||
const sine = Math.sin(angle)
|
||
const sineCoefficient = (state.velocity + safeDamping * safeFrequency * offset) / dampedFrequency
|
||
const oscillation = offset * cosine + sineCoefficient * sine
|
||
|
||
return {
|
||
position: target + decay * oscillation,
|
||
velocity:
|
||
decay *
|
||
(-safeDamping * safeFrequency * oscillation -
|
||
offset * dampedFrequency * sine +
|
||
sineCoefficient * dampedFrequency * cosine),
|
||
}
|
||
}
|
||
|
||
/** 单个双相尾波只有一个波峰和一个波谷,不形成连续周期波列。 */
|
||
export function getGlassOpticalWakeSample(position: number) {
|
||
return position * Math.exp(-0.5 * position * position)
|
||
}
|
||
|
||
/** 低速噪声和小角度移动沿用既有方向,显著转向才开始新的尾波。 */
|
||
export function getGlassOpticalWakeDirection(
|
||
current: GlassOpticalPoint,
|
||
next: GlassOpticalPoint,
|
||
speed: number,
|
||
restart: boolean,
|
||
): GlassOpticalPoint {
|
||
const normalize = (point: GlassOpticalPoint) => {
|
||
const length = Math.hypot(point.x, point.y)
|
||
|
||
return length > 0.0001 ? { x: point.x / length, y: point.y / length } : { x: 0, y: -1 }
|
||
}
|
||
const currentDirection = normalize(current)
|
||
const nextDirection = normalize(next)
|
||
if (restart || Math.hypot(current.x, current.y) <= 0.0001) return nextDirection
|
||
if (speed < 0.006) return currentDirection
|
||
|
||
const directionDot = currentDirection.x * nextDirection.x + currentDirection.y * nextDirection.y
|
||
|
||
return directionDot < Math.cos((55 * Math.PI) / 180) ? nextDirection : currentDirection
|
||
}
|
||
|
||
/** 新活动表面立即接管输入,离场表面只做短时单调淡出。 */
|
||
export function getGlassOpticalSurfaceTransitionWeights(elapsed: number, duration: number) {
|
||
const progress = Math.min(1, Math.max(0, elapsed / Math.max(1, duration)))
|
||
const eased = progress * progress * (3 - 2 * progress)
|
||
|
||
return {
|
||
incoming: 1,
|
||
outgoing: 1 - eased,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 复用仍可见的稳定槽位,并为当前与离场表面保留确定位置。
|
||
* 候选顺序只用于补充空位,指针移动不会重排已经占用的稳定槽位。
|
||
*/
|
||
export function reconcileGlassOpticalSurfaceSlots<TKey>(
|
||
previous: GlassOpticalSurfaceSlot<TKey>[],
|
||
candidates: GlassOpticalSurfaceCandidate<TKey>[],
|
||
maxCount: number,
|
||
activeKey?: TKey,
|
||
outgoingKey?: TKey,
|
||
): GlassOpticalSurfaceSlot<TKey>[] {
|
||
if (maxCount <= 0) return []
|
||
|
||
const candidateByKey = new Map(candidates.map(candidate => [candidate.key, candidate]))
|
||
const reserved: GlassOpticalSurfaceSlot<TKey>[] = []
|
||
if (outgoingKey !== undefined && outgoingKey !== activeKey) {
|
||
const outgoing = candidateByKey.get(outgoingKey)
|
||
if (outgoing) reserved.push({ ...outgoing, role: 'outgoing' })
|
||
}
|
||
if (activeKey !== undefined) {
|
||
const active = candidateByKey.get(activeKey)
|
||
if (active) reserved.push({ ...active, role: 'active' })
|
||
}
|
||
|
||
const reservedKeys = new Set(reserved.map(slot => slot.key))
|
||
const stableCount = Math.max(0, maxCount - reserved.length)
|
||
const stable: GlassOpticalSurfaceSlot<TKey>[] = []
|
||
const stableKeys = new Set<TKey>()
|
||
const appendStable = (key: TKey) => {
|
||
if (stable.length >= stableCount || reservedKeys.has(key) || stableKeys.has(key)) return
|
||
|
||
const candidate = candidateByKey.get(key)
|
||
if (!candidate) return
|
||
|
||
stable.push({ ...candidate, role: 'stable' })
|
||
stableKeys.add(key)
|
||
}
|
||
|
||
for (const slot of previous) appendStable(slot.key)
|
||
for (const candidate of candidates) appendStable(candidate.key)
|
||
|
||
return [...stable, ...reserved].slice(0, maxCount)
|
||
}
|
||
|
||
/** 过滤浏览器不会作为纹理读取的协议;跨域读取能力由实际纹理加载结果判定。 */
|
||
export function canUseGlassWallpaperTexture(url: string, documentUrl: string): boolean {
|
||
if (!url || !documentUrl) return false
|
||
|
||
try {
|
||
const source = new URL(url, documentUrl)
|
||
if (source.protocol === 'blob:' || source.protocol === 'data:') return true
|
||
|
||
const document = new URL(documentUrl)
|
||
if (source.protocol !== 'http:' && source.protocol !== 'https:') return false
|
||
if (document.protocol === 'https:' && source.protocol === 'http:') return false
|
||
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/** 按质量档位的像素预算计算内部缓冲,高质量只使用受控的设备像素比增益。 */
|
||
export function getGlassOpticalBufferSize(
|
||
viewportWidth: number,
|
||
viewportHeight: number,
|
||
mobile: boolean,
|
||
quality: GlassOpticalQuality = 'balanced',
|
||
devicePixelRatio = 1,
|
||
): GlassOpticalBufferSize {
|
||
const highQuality = quality === 'high'
|
||
const pixelRatio = highQuality ? Math.min(Math.max(1, devicePixelRatio), 1.5) : 1
|
||
const safeWidth = Math.max(1, viewportWidth) * pixelRatio
|
||
const safeHeight = Math.max(1, viewportHeight) * pixelRatio
|
||
const maxWidth = mobile ? (highQuality ? 1280 : 960) : highQuality ? 1920 : 1440
|
||
const maxHeight = mobile ? (highQuality ? 1440 : 960) : highQuality ? 1200 : 960
|
||
const scale = Math.min(1, maxWidth / safeWidth, maxHeight / safeHeight)
|
||
|
||
return {
|
||
height: Math.max(1, Math.round(safeHeight * scale)),
|
||
width: Math.max(1, Math.round(safeWidth * scale)),
|
||
}
|
||
}
|
||
|
||
/** 文档空间画布独立限制横纵分辨率,避免长页面按纵横比连带降低横向清晰度。 */
|
||
export function getGlassScrollBufferSize(
|
||
presentationWidth: number,
|
||
presentationHeight: number,
|
||
quality: GlassOpticalQuality,
|
||
devicePixelRatio = 1,
|
||
): GlassOpticalBufferSize {
|
||
const highQuality = quality === 'high'
|
||
const pixelRatio = highQuality ? Math.min(Math.max(1, devicePixelRatio), 1.5) : 1
|
||
const maxWidth = highQuality ? 1920 : 1440
|
||
const maxHeight = highQuality ? 4096 : 3072
|
||
|
||
return {
|
||
height: Math.max(1, Math.round(Math.min(presentationHeight * pixelRatio, maxHeight))),
|
||
width: Math.max(1, Math.round(Math.min(presentationWidth * pixelRatio, maxWidth))),
|
||
}
|
||
}
|
||
|
||
/** 计算与 CSS `background-size: cover` 一致的纹理缩放参数。 */
|
||
export function getGlassCoverScale(
|
||
viewportWidth: number,
|
||
viewportHeight: number,
|
||
imageWidth: number,
|
||
imageHeight: number,
|
||
) {
|
||
const viewportAspect = Math.max(1, viewportWidth) / Math.max(1, viewportHeight)
|
||
const imageAspect = Math.max(1, imageWidth) / Math.max(1, imageHeight)
|
||
|
||
return imageAspect > viewportAspect
|
||
? { x: viewportAspect / imageAspect, y: 1 }
|
||
: { x: 1, y: imageAspect / viewportAspect }
|
||
}
|
||
|
||
/** 用可见交集筛选和计费,但保留原始边界给 shader,避免在视口裁剪边生成伪圆角。 */
|
||
export function selectGlassOpticalRects(
|
||
candidates: GlassOpticalRect[],
|
||
viewportWidth: number,
|
||
viewportHeight: number,
|
||
mobile: boolean,
|
||
interactionPoint?: GlassInteractionPoint,
|
||
): GlassOpticalRect[] {
|
||
const maxCount = mobile ? GLASS_OPTICAL_MAX_SURFACES_MOBILE : GLASS_OPTICAL_MAX_SURFACES_DESKTOP
|
||
const visible = candidates
|
||
.map(rect => {
|
||
const left = Math.max(0, rect.x)
|
||
const top = Math.max(0, rect.y)
|
||
const right = Math.min(viewportWidth, rect.x + rect.width)
|
||
const bottom = Math.min(viewportHeight, rect.y + rect.height)
|
||
const visibleHeight = Math.max(0, bottom - top)
|
||
const visibleWidth = Math.max(0, right - left)
|
||
|
||
return {
|
||
rect,
|
||
visibleArea: visibleWidth * visibleHeight,
|
||
visibleHeight,
|
||
visibleWidth,
|
||
}
|
||
})
|
||
.filter(candidate => candidate.visibleWidth >= 24 && candidate.visibleHeight >= 24)
|
||
.sort((left, right) => {
|
||
if (interactionPoint) {
|
||
const leftContainsInteraction =
|
||
interactionPoint.x >= left.rect.x &&
|
||
interactionPoint.x <= left.rect.x + left.rect.width &&
|
||
interactionPoint.y >= left.rect.y &&
|
||
interactionPoint.y <= left.rect.y + left.rect.height
|
||
const rightContainsInteraction =
|
||
interactionPoint.x >= right.rect.x &&
|
||
interactionPoint.x <= right.rect.x + right.rect.width &&
|
||
interactionPoint.y >= right.rect.y &&
|
||
interactionPoint.y <= right.rect.y + right.rect.height
|
||
|
||
if (leftContainsInteraction !== rightContainsInteraction) return leftContainsInteraction ? -1 : 1
|
||
}
|
||
|
||
return left.rect.rank - right.rect.rank || left.visibleArea - right.visibleArea
|
||
})
|
||
|
||
const selected: typeof visible = []
|
||
|
||
for (const candidate of visible) {
|
||
if (selected.length >= maxCount) break
|
||
|
||
const { rect } = candidate
|
||
const nested = selected.some(
|
||
parent =>
|
||
rect.x >= parent.rect.x &&
|
||
rect.y >= parent.rect.y &&
|
||
rect.x + rect.width <= parent.rect.x + parent.rect.width &&
|
||
rect.y + rect.height <= parent.rect.y + parent.rect.height,
|
||
)
|
||
if (nested) continue
|
||
|
||
selected.push(candidate)
|
||
}
|
||
|
||
return selected.map(candidate => candidate.rect)
|
||
}
|
||
|
||
/** 将视口矩形转换为 WebGL 底部原点的归一化参数。 */
|
||
export function normalizeGlassOpticalRect(rect: GlassOpticalRect, viewportWidth: number, viewportHeight: number) {
|
||
const safeWidth = Math.max(1, viewportWidth)
|
||
const safeHeight = Math.max(1, viewportHeight)
|
||
const radii = rect.radii.map(radius => Math.max(0, radius)) as GlassCornerRadii
|
||
const [topLeft, topRight, bottomRight, bottomLeft] = radii
|
||
const fitScale = (side: number, radiusSum: number) => (radiusSum > 0 ? Math.max(0, side) / radiusSum : 1)
|
||
// CSS 会用同一个比例缩小全部圆角,保证任一边的相邻半径之和不超过该边长度。
|
||
const radiusScale = Math.min(
|
||
1,
|
||
fitScale(rect.width, topLeft + topRight),
|
||
fitScale(rect.width, bottomLeft + bottomRight),
|
||
fitScale(rect.height, topLeft + bottomLeft),
|
||
fitScale(rect.height, topRight + bottomRight),
|
||
)
|
||
|
||
return {
|
||
radii: radii.map(radius => radius * radiusScale) as GlassCornerRadii,
|
||
rect: [
|
||
rect.x / safeWidth,
|
||
1 - (rect.y + rect.height) / safeHeight,
|
||
rect.width / safeWidth,
|
||
rect.height / safeHeight,
|
||
] as const,
|
||
}
|
||
}
|