Files
MoShiro/web/display/src/backgroundTint.ts
T

92 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
}
/**
* 暗色下玻璃的颜色,和盖在背景图上的底色,都从当前背景图(选了哪组照片、哪个主题)自己的平均色算出来:
* 色相照搬图里的,饱和度保留大半,亮度跟着图本身的亮度走但压得很暗。写进 <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, 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()
}
}, [])
}