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