fix(glass): port material accents to v3 (#660)

This commit is contained in:
InfinityPacer
2026-08-10 15:01:08 +08:00
committed by GitHub
parent 6a4af66683
commit 1a303b4323
22 changed files with 742 additions and 86 deletions
+66 -4
View File
@@ -1,11 +1,73 @@
import { vi } from 'vitest'
vi.mock('colorthief', () => ({
default: class ColorThief {},
const mocks = vi.hoisted(() => ({
getColor: vi.fn(),
}))
import { preloadCorsImage } from '@/@core/utils/image'
import { afterEach, describe, expect, it } from 'vitest'
vi.mock('colorthief', () => ({
default: class ColorThief {
getColor(image: HTMLImageElement, quality?: number) {
return mocks.getColor(image, quality)
}
},
}))
import { extractDominantColor, getDominantColor, preloadCorsImage } from '@/@core/utils/image'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
function createImage(cacheKey: string) {
const image = document.createElement('img')
Object.defineProperty(image, 'currentSrc', { configurable: true, value: `https://image.example/${cacheKey}.png` })
return image
}
describe('dominant color extraction', () => {
beforeEach(() => {
mocks.getColor.mockReset()
})
it('shares a pending extraction and reuses only the successful result', async () => {
const image = createImage('shared-success')
mocks.getColor.mockReturnValue([18, 52, 86])
await expect(Promise.all([extractDominantColor(image), extractDominantColor(image)])).resolves.toEqual([
'#123456',
'#123456',
])
await expect(extractDominantColor(image)).resolves.toBe('#123456')
expect(mocks.getColor).toHaveBeenCalledOnce()
})
it('does not cache failures or let one caller fallback pollute another', async () => {
const image = createImage('retry-after-failure')
mocks.getColor.mockImplementation(() => {
throw new Error('tainted canvas')
})
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
await expect(getDominantColor(image, { fallback: '#111111' })).resolves.toBe('#111111')
await expect(getDominantColor(image, { fallback: '#222222' })).resolves.toBe('#222222')
expect(mocks.getColor).toHaveBeenCalledTimes(2)
})
it('keeps the existing default fallback contract for callers such as QuickAccess', async () => {
await expect(getDominantColor(null)).resolves.toBe('#28A9E1')
expect(mocks.getColor).not.toHaveBeenCalled()
})
it('retains the bounded FIFO success cache', async () => {
mocks.getColor.mockReturnValue([1, 2, 3])
for (let index = 0; index <= 100; index += 1) await extractDominantColor(createImage(`fifo-${index}`))
expect(mocks.getColor).toHaveBeenCalledTimes(101)
await extractDominantColor(createImage('fifo-50'))
expect(mocks.getColor).toHaveBeenCalledTimes(101)
await extractDominantColor(createImage('fifo-0'))
expect(mocks.getColor).toHaveBeenCalledTimes(102)
})
})
describe('preloadCorsImage', () => {
afterEach(() => {
+41 -22
View File
@@ -4,7 +4,8 @@ export { preloadCorsImage } from './corsImage'
const DEFAULT_DOMINANT_COLOR = '#28A9E1'
const DOMINANT_COLOR_CACHE_LIMIT = 100
const colorThief = new ColorThief()
const dominantColorCache = new Map<string, Promise<string>>()
const dominantColorCache = new Map<string, string>()
const pendingDominantColorRequests = new Map<string, Promise<string | undefined>>()
interface DominantColorOptions {
fallback?: string
@@ -29,42 +30,60 @@ function getImageCacheKey(image: HTMLImageElement) {
return image.currentSrc || image.src || ''
}
function rememberDominantColor(key: string, colorPromise: Promise<string>) {
if (!key) return colorPromise
function rememberDominantColor(key: string, color: string) {
if (!key) return
if (dominantColorCache.size >= DOMINANT_COLOR_CACHE_LIMIT) {
const firstKey = dominantColorCache.keys().next().value
if (firstKey) dominantColorCache.delete(firstKey)
}
dominantColorCache.set(key, colorPromise)
dominantColorCache.set(key, color)
}
/** 提取真实主色;失败不写入成功缓存,允许后续请求重试。 */
export async function extractDominantColor(
image: HTMLImageElement | undefined | null,
options: Pick<DominantColorOptions, 'quality'> = {},
): Promise<string | undefined> {
if (!image) return undefined
const cacheKey = getImageCacheKey(image)
const cachedColor = cacheKey ? dominantColorCache.get(cacheKey) : undefined
if (cachedColor) return cachedColor
const pendingRequest = cacheKey ? pendingDominantColorRequests.get(cacheKey) : undefined
if (pendingRequest) return pendingRequest
const colorPromise = Promise.resolve()
.then(() => {
const dominantColor = colorThief.getColor(image, options.quality ?? 20)
const color = rgbStringToHex(dominantColor)
rememberDominantColor(cacheKey, color)
return color
})
.catch(error => {
console.warn('Failed to extract dominant color:', error)
return undefined
})
.finally(() => {
if (cacheKey) pendingDominantColorRequests.delete(cacheKey)
})
if (cacheKey) pendingDominantColorRequests.set(cacheKey, colorPromise)
return colorPromise
}
// 提取主要颜色
/** 提取主色并在失败时解析调用方 fallback,保持既有调用合同。 */
export async function getDominantColor(
image: HTMLImageElement | undefined | null,
options: DominantColorOptions = {},
): Promise<string> {
const fallback = options.fallback ?? DEFAULT_DOMINANT_COLOR
if (!image) return fallback
const cacheKey = getImageCacheKey(image)
const cachedColor = cacheKey ? dominantColorCache.get(cacheKey) : undefined
if (cachedColor) return cachedColor
const colorPromise = Promise.resolve()
.then(() => {
const dominantColor = colorThief.getColor(image, options.quality ?? 20)
return rgbStringToHex(dominantColor)
})
.catch(error => {
console.warn('Failed to extract dominant color:', error)
return fallback
})
return rememberDominantColor(cacheKey, colorPromise)
return (await extractDominantColor(image, options)) ?? fallback
}
// 预加载图片