import { useEffect } from 'react' import { getAppearance, subscribeAppearance } from './appearance' import { backgroundUrl } from './backgrounds' /** 取样的缩略图大小:背景本来就是预先模糊过的,缩得很小就够算平均色。 */ 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] } /** * 暗色下玻璃的颜色,和盖在背景图上的底色,都从当前背景图(选了哪组照片、哪个主题)自己的平均色算出来: * 色相照搬图里的,饱和度保留大半,亮度跟着图本身的亮度走但压得很暗。写进 的 --bg-tint(玻璃)和 --bg-veil-tint(底色), * tokens.css 里暗色的 --glass-tint / --glass-veil 用它们;切换主题时重新取。亮色不用这个。 */ export function useBackgroundTint() { useEffect(() => { const root = document.documentElement const images = new Map() const apply = (url: string) => { const image = images.get(url) const average = image && averageColor(image) if (!average) return const [h, s, l] = toHsl(...average) // 玻璃:色相照搬,饱和度保留大半(上限 45%),亮度跟着图本身的亮度走(图越亮,玻璃越亮,10%–22%); // 底色(盖在背景图上的遮罩)同理,但更暗(6%–14%)。所以换一张照片,暗色玻璃的颜色和深浅都会跟着变 const clamp = (value: number, low: number, high: number) => Math.max(low, Math.min(high, value)) const glass = clamp(9 + l * 26, 10, 22) const veil = clamp(5 + l * 14, 6, 14) root.style.setProperty('--bg-tint', `hsl(${h.toFixed(0)} ${clamp(s * 90, 0, 45).toFixed(0)}% ${glass.toFixed(1)}%)`) root.style.setProperty('--bg-veil-tint', `hsl(${h.toFixed(0)} ${clamp(s * 75, 0, 38).toFixed(0)}% ${veil.toFixed(1)}%)`) } const update = () => { // 取样用预先模糊的那个版本:和不模糊的原图平均色一样,图小、取得快 const url = backgroundUrl(getAppearance().photo, root.dataset.theme === 'dark', true, false) 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'] }) const unsubscribe = subscribeAppearance(update) return () => { observer.disconnect() unsubscribe() } }, []) }