Optimize fullscreen rendering

This commit is contained in:
lucast committed 2026-10-06 02:33:07 +02:00
1 parent b696da1854
commit b190d1671c
5 files changed
+91 -63

No files matched your search

+2 -2
View File
@@ -81,8 +81,8 @@ nuxtApp.hook("page:finish", () => {
top: 0; top: 0;
left: 0; left: 0;
width: 200vw; width: 100vw;
height: 200vh; height: 100vh;
z-index: 150; z-index: 150;
cursor: none; cursor: none;
@@ -10,14 +10,15 @@ import { shapeBeatPulse } from "~/utils/beat"
const props = defineProps<{ src: string; active: boolean }>() const props = defineProps<{ src: string; active: boolean }>()
const ANIMATION_SPEED = 0.3 const ANIMATION_SPEED = 0.3
const FRAME_RATE = 12
const FRAME_INTERVAL = 1000 / FRAME_RATE
const { beatEnergy } = usePlayer() const { beatEnergy } = usePlayer()
const cpuCanvas = ref<HTMLCanvasElement | null>(null) const cpuCanvas = ref<HTMLCanvasElement | null>(null)
const webGpuCanvas = ref<HTMLCanvasElement | null>(null) const webGpuCanvas = ref<HTMLCanvasElement | null>(null)
const palette = ref<AlbumPalette | null>(null) const palette = ref<AlbumPalette | null>(null)
const backend = ref<"cpu" | "webgpu">("cpu") const backend = ref<"cpu" | "webgpu">("cpu")
let frame = 0 let frameTimer: number | null = null
let startedAt = 0 let startedAt = 0
let lastFrameAt = 0
let random: [number, number] = [0, 0] let random: [number, number] = [0, 0]
let angleJitter = 0 let angleJitter = 0
let flowParams: [number, number, number, number] = [5, 25, 0.75, -0.08] let flowParams: [number, number, number, number] = [5, 25, 0.75, -0.08]
@@ -111,17 +112,17 @@ function makeFrame(now: number, canvas: HTMLCanvasElement): BackgroundFrame {
} }
} }
function updateCanvasScale(canvas: HTMLCanvasElement, beat: number) {
const transform = `scale(${1.08 + beat * 0.003})`
if (canvas.style.transform !== transform) canvas.style.transform = transform
}
function draw(now: number) { function draw(now: number) {
if (!props.active || disposed) return if (!props.active || disposed) return
if (now - lastFrameAt < 1000 / 24) {
frame = requestAnimationFrame(draw)
return
}
lastFrameAt = now
const canvas = backend.value === "webgpu" ? webGpuCanvas.value : cpuCanvas.value const canvas = backend.value === "webgpu" ? webGpuCanvas.value : cpuCanvas.value
if (canvas && activeRenderer) { if (canvas && activeRenderer) {
const renderFrame = makeFrame(now, canvas) const renderFrame = makeFrame(now, canvas)
canvas.style.transform = `scale(${1.08 + renderFrame.beat * 0.003})` updateCanvasScale(canvas, renderFrame.beat)
try { try {
activeRenderer.render(renderFrame) activeRenderer.render(renderFrame)
} catch (error) { } catch (error) {
@@ -133,10 +134,8 @@ function draw(now: number) {
webGpuRenderer?.dispose() webGpuRenderer?.dispose()
webGpuRenderer = null webGpuRenderer = null
useCpuFallback() useCpuFallback()
cpuCanvas.value?.style.setProperty( if (cpuCanvas.value)
"transform", updateCanvasScale(cpuCanvas.value, renderFrame.beat)
`scale(${1.08 + renderFrame.beat * 0.003})`,
)
try { try {
cpuRenderer?.render(renderFrame) cpuRenderer?.render(renderFrame)
} catch (fallbackError) { } catch (fallbackError) {
@@ -147,15 +146,18 @@ function draw(now: number) {
} }
} }
} }
frame = requestAnimationFrame(draw) frameTimer = window.setTimeout(() => {
frameTimer = null
draw(performance.now())
}, FRAME_INTERVAL)
} }
function startAnimation() { function startAnimation() {
cancelAnimationFrame(frame) if (frameTimer !== null) window.clearTimeout(frameTimer)
frameTimer = null
startedAt = 0 startedAt = 0
lastFrameAt = 0
randomizeMotion() randomizeMotion()
frame = requestAnimationFrame(draw) draw(performance.now())
} }
watch(() => props.src, loadArtwork) watch(() => props.src, loadArtwork)
@@ -163,7 +165,10 @@ watch(
() => props.active, () => props.active,
(active) => { (active) => {
if (active) startAnimation() if (active) startAnimation()
else cancelAnimationFrame(frame) else if (frameTimer !== null) {
window.clearTimeout(frameTimer)
frameTimer = null
}
}, },
) )
@@ -175,7 +180,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
disposed = true disposed = true
cancelAnimationFrame(frame) if (frameTimer !== null) window.clearTimeout(frameTimer)
webGpuRenderer?.dispose() webGpuRenderer?.dispose()
cpuRenderer?.dispose() cpuRenderer?.dispose()
}) })
@@ -200,8 +205,8 @@ onBeforeUnmount(() => {
.background-wash { .background-wash {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 50%; width: 100%;
height: 50%; height: 100%;
filter: blur(6px) saturate(1.08); filter: blur(6px) saturate(1.08);
transform: scale(1.08); transform: scale(1.08);
visibility: hidden; visibility: hidden;
@@ -43,7 +43,9 @@ interface GpuAdapter {
} }
interface GpuApi { interface GpuApi {
requestAdapter(): Promise<GpuAdapter | null> requestAdapter(options?: {
powerPreference?: "low-power" | "high-performance"
}): Promise<GpuAdapter | null>
getPreferredCanvasFormat(): string getPreferredCanvasFormat(): string
} }
@@ -70,7 +72,9 @@ export async function createWebGpuRenderer(
console.info("[background renderer] WebGPU API unavailable; using CPU") console.info("[background renderer] WebGPU API unavailable; using CPU")
return null return null
} }
const adapter = await gpu.requestAdapter() const adapter = await gpu.requestAdapter({
powerPreference: "high-performance",
})
if (!adapter) { if (!adapter) {
console.info("[background renderer] no WebGPU adapter; using CPU") console.info("[background renderer] no WebGPU adapter; using CPU")
return null return null
@@ -114,6 +118,11 @@ export async function createWebGpuRenderer(
const activeDevice = device const activeDevice = device
const activeContext = context const activeContext = context
const activeBuffer = uniformBuffer const activeBuffer = uniformBuffer
const uniforms = new Float32Array(20)
let lastPalette: BackgroundFrame["palette"] | null = null
let dominant: [number, number, number] = [0, 0, 0]
let accent: [number, number, number] = [0, 0, 0]
let maxPaletteLightness = 0
let disposed = false let disposed = false
void device.lost.then(() => { void device.lost.then(() => {
if (!disposed) onDeviceLost() if (!disposed) onDeviceLost()
@@ -125,27 +134,25 @@ export async function createWebGpuRenderer(
canvas.width = frame.width canvas.width = frame.width
canvas.height = frame.height canvas.height = frame.height
} }
const dominant = toOkLab(frame.palette.dominant) if (lastPalette !== frame.palette) {
const accent = toOkLab(frame.palette.accent) dominant = toOkLab(frame.palette.dominant)
const uniforms = new Float32Array([ accent = toOkLab(frame.palette.accent)
frame.width, maxPaletteLightness = Math.max(dominant[0], accent[0])
frame.height, lastPalette = frame.palette
frame.time, }
frame.beat, uniforms[0] = frame.width
frame.random[0], uniforms[1] = frame.height
frame.random[1], uniforms[2] = frame.time
frame.angleJitter, uniforms[3] = frame.beat
0, uniforms[4] = frame.random[0]
...frame.flowParams, uniforms[5] = frame.random[1]
dominant[0], uniforms[6] = frame.angleJitter
dominant[1], uniforms[7] = 0
dominant[2], uniforms.set(frame.flowParams, 8)
Math.max(dominant[0], accent[0]), uniforms.set(dominant, 12)
accent[0], uniforms[15] = maxPaletteLightness
accent[1], uniforms.set(accent, 16)
accent[2], uniforms[19] = 0
0,
])
activeDevice.queue.writeBuffer(activeBuffer, 0, uniforms) activeDevice.queue.writeBuffer(activeBuffer, 0, uniforms)
const encoder = activeDevice.createCommandEncoder() const encoder = activeDevice.createCommandEncoder()
const pass = encoder.beginRenderPass({ const pass = encoder.beginRenderPass({
@@ -51,16 +51,30 @@ const height = ref<number>()
const top = ref(0) const top = ref(0)
const syncedHeight = ref<number>() const syncedHeight = ref<number>()
const layoutVersion = ref(0) const layoutVersion = ref(0)
const lyricOffsets = ref<number[]>([])
const topFadeExtension = 75 const topFadeExtension = 75
const updateLyricsLayout = () => { const updateLyricsLayout = () => {
const trackDisplay = document.querySelector(".track-display") const trackDisplay = document.querySelector(".track-display")
if (!trackDisplay) return if (trackDisplay) {
height.value = trackDisplay.clientHeight height.value = trackDisplay.clientHeight
top.value = trackDisplay.getBoundingClientRect().top top.value = trackDisplay.getBoundingClientRect().top
syncedHeight.value = window.innerHeight - top.value - 100 syncedHeight.value = window.innerHeight - top.value - 100
}
const lines = containerRef.value?.children
if (!lines) {
lyricOffsets.value = []
return
}
let accumulatedHeight = 0
lyricOffsets.value = Array.from(lines, (line) => {
const offset = accumulatedHeight
accumulatedHeight += line.getBoundingClientRect().height
return offset
})
} }
watch( watch(
@@ -87,18 +101,20 @@ const outerRef = useTemplateRef("outer-ref")
const transformY = computed(() => { const transformY = computed(() => {
layoutVersion.value layoutVersion.value
const lines = containerRef.value?.children const offset = lyricOffsets.value[scrollActiveIndex.value]
if (scrollActiveIndex.value < 0 || !lines?.length) return 0 return offset === undefined ? 0 : -offset
let accumulatedHeight = 0
for (let i = 0; i < scrollActiveIndex.value; i++) {
const line = lines[i] as HTMLElement | undefined
if (line) accumulatedHeight += line.getBoundingClientRect().height
}
return -accumulatedHeight
}) })
watch(
parsedLyrics,
async () => {
await nextTick()
updateLyricsLayout()
layoutVersion.value++
},
{ flush: "post" },
)
const manualOffset = ref(0) const manualOffset = ref(0)
const isScrolling = ref(false) const isScrolling = ref(false)
+6 -6
View File
@@ -21,7 +21,7 @@ export default defineNuxtPlugin((nuxtApp) => {
} }
const VOLUME_STORAGE_KEY = "music-v2-player-volume" const VOLUME_STORAGE_KEY = "music-v2-player-volume"
const DEFAULT_VOLUME = 0.5 const DEFAULT_VOLUME = 0.5
const DEBUG_BEAT_ANALYSIS = true const DEBUG_BEAT_ANALYSIS = false
const sound = ref<Howl | null>(null) const sound = ref<Howl | null>(null)
const queue = ref<Track[]>([]) const queue = ref<Track[]>([])
@@ -90,6 +90,7 @@ export default defineNuxtPlugin((nuxtApp) => {
}) })
const duration = ref(0) const duration = ref(0)
let lastPositionUpdate = 0 let lastPositionUpdate = 0
let trackingTimer: number | null = null
let mediaSessionSongId: string | null = null let mediaSessionSongId: string | null = null
const stopBeatAnalysis = (reason = "stopped") => { const stopBeatAnalysis = (reason = "stopped") => {
@@ -233,8 +234,6 @@ export default defineNuxtPlugin((nuxtApp) => {
} }
} }
const aniFrame = ref<number | null>(null)
// Helpers // Helpers
const updateMediaSession = () => { const updateMediaSession = () => {
if (!import.meta.client) return if (!import.meta.client) return
@@ -334,6 +333,7 @@ export default defineNuxtPlugin((nuxtApp) => {
} }
const startTracking = () => { const startTracking = () => {
if (trackingTimer !== null) window.clearTimeout(trackingTimer)
const update = () => { const update = () => {
tick.value++ tick.value++
const now = Date.now() const now = Date.now()
@@ -341,15 +341,15 @@ export default defineNuxtPlugin((nuxtApp) => {
lastPositionUpdate = now lastPositionUpdate = now
updateMediaSession() updateMediaSession()
} }
aniFrame.value = requestAnimationFrame(update) trackingTimer = window.setTimeout(update, 100)
} }
update() update()
} }
const stopTracking = () => { const stopTracking = () => {
if (aniFrame.value) cancelAnimationFrame(aniFrame.value) if (trackingTimer !== null) window.clearTimeout(trackingTimer)
aniFrame.value = null trackingTimer = null
} }
const createHowl = async (song: Track) => { const createHowl = async (song: Track) => {