feat(display): falling snow in three depth layers with wind, mouse and bullet time; appearance panel; dark glass tinted from the background [CI SKIP]

This commit is contained in:
justme committed 2026-10-09 03:54:49 +08:00
1 parent f8e8bb8acb
commit e49990776b
10 files changed
+1003 -7

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import { useEffect } from 'react'
import bgLight from './assets/bg.jpg'
import bgDark from './assets/bg-dark.jpg'
/** 取样的缩略图大小:背景本来就是预先模糊过的,缩得很小就够算平均色。 */
const SAMPLE_WIDTH = 32
const SAMPLE_HEIGHT = 18
/** 一张图的平均颜色(0–255 的 r、g、b),按每个像素的亮度加权过轻:太亮的高光(衣服、头发的亮部)不要把整体拉亮。 */
function averageColor(image: HTMLImageElement): [number, number, number] | null {
const canvas = document.createElement('canvas')
canvas.width = SAMPLE_WIDTH
canvas.height = SAMPLE_HEIGHT
const context = canvas.getContext('2d', { willReadFrequently: true })
if (!context) return null
context.drawImage(image, 0, 0, SAMPLE_WIDTH, SAMPLE_HEIGHT)
const { data } = context.getImageData(0, 0, SAMPLE_WIDTH, SAMPLE_HEIGHT)
let r = 0
let g = 0
let b = 0
let weight = 0
for (let i = 0; i < data.length; i += 4) {
const luma = (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114) / 255
const w = 1 - 0.6 * luma
r += data[i] * w
g += data[i + 1] * w
b += data[i + 2] * w
weight += w
}
return weight > 0 ? [r / weight, g / weight, b / weight] : null
}
/** rgb(0–255)转 hsl(h 0–360,s、l 0–1)。 */
function toHsl(r: number, g: number, b: number): [number, number, number] {
const red = r / 255
const green = g / 255
const blue = b / 255
const max = Math.max(red, green, blue)
const min = Math.min(red, green, blue)
const l = (max + min) / 2
if (max === min) return [0, 0, l]
const d = max - min
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
const h = max === red ? (green - blue) / d + (green < blue ? 6 : 0) : max === green ? (blue - red) / d + 2 : (red - green) / d + 4
return [h * 60, s, l]
}
/**
* 暗色下玻璃的颜色,和盖在背景图上的底色,都从当前背景图自己的平均色算出来:
* 色相照搬图里的,饱和度打个折(免得发艳、发蓝),亮度压到很暗。写进 <html> 的 --bg-tint(玻璃)和 --bg-veil-tint(底色),
* tokens.css 里暗色的 --glass-tint / --glass-veil 用它们;切换主题时重新取。亮色不用这个。
*/
export function useBackgroundTint() {
useEffect(() => {
const root = document.documentElement
const images = new Map<string, HTMLImageElement>()
const apply = (url: string) => {
const image = images.get(url)
const average = image && averageColor(image)
if (!average) return
const [h, s] = toHsl(...average)
root.style.setProperty('--bg-tint', `hsl(${h.toFixed(0)} ${(s * 55).toFixed(0)}% 13%)`)
root.style.setProperty('--bg-veil-tint', `hsl(${h.toFixed(0)} ${(s * 45).toFixed(0)}% 8%)`)
}
const update = () => {
const url = root.dataset.theme === 'dark' ? bgDark : bgLight
if (images.has(url)) {
apply(url)
return
}
const image = new Image()
images.set(url, image)
image.onload = () => apply(url)
image.src = url
}
update()
const observer = new MutationObserver(update)
observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] })
return () => observer.disconnect()
}, [])
}