Files
BYW-VoiceHub/app/composables/useBackgroundRenderer.ts
T

265 lines
6.6 KiB
TypeScript

import { computed, onUnmounted, ref, shallowRef } from 'vue'
import type {
AbstractBaseRenderer,
BackgroundRender,
MeshGradientRenderer
} from '@applemusic-like-lyrics/core'
const isClient = typeof window !== 'undefined'
let CoreModule: typeof import('@applemusic-like-lyrics/core') | null = null
const CORS_PROXY_PATH = '/api/proxy/image'
const needsCorsProxy = (url: string): boolean => {
try {
const u = new URL(url)
const h = u.hostname.toLowerCase()
return (
h.endsWith('.y.qq.com') ||
h === 'y.qq.com' ||
h.endsWith('.y.gtimg.cn') ||
h === 'y.gtimg.cn' ||
h.endsWith('.music.126.net') ||
h === 'music.126.net' ||
h.endsWith('.musicapp.migu.cn') ||
h === 'd.musicapp.migu.cn' ||
h.endsWith('.migu.cn')
)
} catch {
return false
}
}
const toProxiedUrl = (url: string): string => {
if (!needsCorsProxy(url)) return url
return `${CORS_PROXY_PATH}?url=${encodeURIComponent(url)}`
}
const ensureCoreModule = async () => {
if (!isClient) return null
if (!CoreModule) {
CoreModule = await import('@applemusic-like-lyrics/core')
await import('@applemusic-like-lyrics/core/style.css')
}
return CoreModule
}
export interface BackgroundConfig {
type: 'gradient' | 'cover'
dynamic: boolean
flowSpeed: number
fps?: number
renderScale?: number
hasLyric?: boolean
}
export const useBackgroundRenderer = () => {
const backgroundRenderer = shallowRef<
BackgroundRender<MeshGradientRenderer> | AbstractBaseRenderer | null
>(null)
const containerElement = ref<HTMLElement | null>(null)
const coverBlurElement = ref<HTMLElement | null>(null)
const isInitialized = ref(false)
const isRendering = ref(false)
const isMotionPaused = ref(false)
const hasRenderError = ref(false)
const currentCoverUrl = ref('')
const loadedCoverUrl = ref('')
const config = ref<BackgroundConfig>({
type: 'gradient',
dynamic: true,
flowSpeed: 2,
fps: 30,
renderScale: 0.5,
hasLyric: true
})
const applyCanvasStyle = (canvas: HTMLElement) => {
canvas.style.position = 'absolute'
canvas.style.inset = '0'
canvas.style.width = '100%'
canvas.style.height = '100%'
canvas.style.display = 'block'
canvas.style.zIndex = '0'
canvas.style.pointerEvents = 'none'
}
const syncRendererMotion = () => {
const renderer = backgroundRenderer.value
if (!renderer) return
if (!isRendering.value) {
renderer.pause()
return
}
renderer.setStaticMode(false)
if (!config.value.dynamic) {
renderer.setFlowSpeed(0)
renderer.pause()
return
}
renderer.setFlowSpeed(isMotionPaused.value ? 0 : config.value.flowSpeed)
renderer.resume()
}
const applyRendererState = async () => {
const renderer = backgroundRenderer.value
if (!renderer) return
renderer.setFPS(config.value.fps ?? 30)
renderer.setRenderScale(config.value.renderScale ?? 0.5)
renderer.setHasLyric(config.value.hasLyric ?? true)
const cover = currentCoverUrl.value
if (cover !== loadedCoverUrl.value) {
try {
await renderer.setAlbum(cover ? toProxiedUrl(cover) : '', false)
if (currentCoverUrl.value === cover) {
loadedCoverUrl.value = cover
hasRenderError.value = false
}
} catch (error) {
if (currentCoverUrl.value === cover) {
hasRenderError.value = true
}
console.error('应用封面到背景失败:', error)
}
}
syncRendererMotion()
}
const initializeBackground = async (container: HTMLElement) => {
if (!container) return
if (isInitialized.value && containerElement.value === container) {
await applyRendererState()
return
}
dispose()
containerElement.value = container
hasRenderError.value = false
try {
const Core = await ensureCoreModule()
if (!Core) return
const renderer = Core.BackgroundRender.new(Core.MeshGradientRenderer)
backgroundRenderer.value = renderer
const canvas = renderer.getElement()
applyCanvasStyle(canvas)
container.appendChild(canvas)
isInitialized.value = true
await applyRendererState()
} catch (error) {
hasRenderError.value = true
console.error('背景渲染器初始化失败:', error)
dispose()
}
}
const updateConfig = async (newConfig: Partial<BackgroundConfig>) => {
config.value = { ...config.value, ...newConfig }
await applyRendererState()
}
const setCoverBackground = async (coverUrl: string) => {
currentCoverUrl.value = coverUrl || ''
if (coverBlurElement.value) {
coverBlurElement.value.style.backgroundImage = coverUrl ? `url(${coverUrl})` : ''
}
await applyRendererState()
}
const setCoverBlurElement = (element: HTMLElement | null) => {
coverBlurElement.value = element
}
const setGradientFromCover = async (coverUrl: string) => {
await setCoverBackground(coverUrl)
}
const startRender = () => {
isRendering.value = true
isMotionPaused.value = false
syncRendererMotion()
}
const stopRender = () => {
const renderer = backgroundRenderer.value
if (!renderer) return
isRendering.value = false
isMotionPaused.value = false
renderer.setFlowSpeed(0)
renderer.pause()
}
const pauseRender = () => {
isMotionPaused.value = true
if (!backgroundRenderer.value) return
syncRendererMotion()
}
const resumeRender = () => {
isMotionPaused.value = false
if (!backgroundRenderer.value) return
syncRendererMotion()
}
function dispose() {
const renderer = backgroundRenderer.value
if (renderer) {
try {
renderer.pause()
renderer.dispose()
} catch (error) {
console.error('清理背景渲染器失败:', error)
}
}
backgroundRenderer.value = null
isInitialized.value = false
isRendering.value = false
isMotionPaused.value = false
containerElement.value = null
coverBlurElement.value = null
currentCoverUrl.value = ''
loadedCoverUrl.value = ''
}
onUnmounted(() => {
dispose()
})
return {
backgroundRenderer: computed(() => backgroundRenderer.value),
isInitialized: computed(() => isInitialized.value),
isRendering: computed(() => isRendering.value),
hasRenderError: computed(() => hasRenderError.value),
config: computed(() => config.value),
initializeBackground,
updateConfig,
setCoverBackground,
setCoverBlurElement,
setGradientFromCover,
startRender,
stopRender,
pauseRender,
resumeRender,
dispose,
applyConfig: applyRendererState
}
}