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:
1 parent
f8e8bb8acb
commit
e49990776b
10 files changed
+1003
-7
No files matched your search
@@ -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()
|
||||
}, [])
|
||||
}
|
||||
Reference in new issue
Block a user