1. 播放器歌词效果

This commit is contained in:
李琦
2026-08-02 18:19:12 +08:00
parent 439839727a
commit 8f3918198a
4 changed files with 558 additions and 122 deletions

View File

@@ -27,27 +27,62 @@
</div>
</div>
<!-- 一大五小 -->
<div v-else-if="page.type === 'one-large-five-small'"
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20">
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative" :class="frameClass">
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" @click.stop="previewImage(page.images[0])" />
<!-- 一大五小固定 6 槽占位 + 串行填内容大图 decode 后再挂小图 -->
<div
v-else-if="page.type === 'one-large-five-small'"
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20"
>
<div
class="col-span-2 row-span-2 relative olfs-slot"
:class="{ 'olfs-placeholder': shownCount < 1 }"
>
<div
v-if="shownCount >= 1"
v-reveal="{ variant: 'up', immediate: true }"
class="absolute inset-0"
:class="frameClass"
>
<img
ref="olfsLargeImgRef"
:src="page.images[0]"
class="preview-img olfs-img"
decoding="async"
@click.stop="previewImage(page.images[0])"
/>
</div>
</div>
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative" :class="frameClass">
<img :src="page.images[i]" class="preview-img" @click.stop="previewImage(page.images[i])" />
<div
v-for="i in [1, 2, 3, 4, 5]"
:key="i"
class="relative olfs-slot"
:class="{ 'olfs-placeholder': shownCount <= i }"
>
<div
v-if="shownCount > i"
v-reveal="{ variant: 'up', immediate: true }"
class="absolute inset-0"
:class="frameClass"
>
<img
:src="page.images[i]"
class="preview-img olfs-img"
decoding="async"
@click.stop="previewImage(page.images[i])"
/>
</div>
</div>
</div>
<!-- 九宫格串行挂载 300ms 1 v-reveal不预挂九格观察器 -->
<!-- 九宫格串行挂载 + immediate由父级 activate 触发 -->
<div v-else-if="page.type === 'nine-grid'" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
<div
v-for="(img, imgIdx) in (page.images || [])"
:key="imgIdx"
class="aspect-square relative nine-grid-slot"
:class="{ 'nine-grid-placeholder': imgIdx >= nineShownCount }"
:class="{ 'nine-grid-placeholder': imgIdx >= shownCount }"
>
<div
v-if="imgIdx < nineShownCount && nineGridAnimate"
v-if="imgIdx < shownCount && nineGridAnimate"
v-reveal="getRevealConfig(imgIdx)"
class="absolute inset-0"
:class="frameClass"
@@ -60,7 +95,7 @@
/>
</div>
<div
v-else-if="imgIdx < nineShownCount"
v-else-if="imgIdx < shownCount"
class="absolute inset-0"
:class="frameClass"
>
@@ -75,11 +110,20 @@
</div>
</div>
<!-- 瀑布流 -->
<div v-else-if="page.type === 'masonry'" class="g-masonry mt-6 px-1">
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }"
class="g-masonry-item" :class="frameClass">
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
<!-- 瀑布流串行挂载 + immediate -->
<div v-else-if="page.type === 'masonry'" ref="multiRootRef" class="g-masonry mt-6 px-1">
<div
v-for="(img, imgIdx) in (page.images || [])"
:key="imgIdx"
>
<div
v-if="imgIdx < shownCount"
v-reveal="{ variant: 'up', immediate: true }"
class="g-masonry-item"
:class="frameClass"
>
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" />
</div>
</div>
</div>
@@ -98,11 +142,20 @@
</div>
</div>
<!-- 拍立得拼贴 -->
<div v-else-if="page.type === 'polaroid'" class="g-polaroid mt-6">
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 70 }"
class="g-polaroid-item" :style="{ transform: polaroidTransform(imgIdx) }">
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
<!-- 拍立得串行挂载 + immediate -->
<div v-else-if="page.type === 'polaroid'" ref="multiRootRef" class="g-polaroid mt-6">
<div
v-for="(img, imgIdx) in (page.images || [])"
:key="imgIdx"
>
<div
v-if="imgIdx < shownCount"
v-reveal="{ variant: 'scale', immediate: true }"
class="g-polaroid-item"
:style="{ transform: polaroidTransform(imgIdx) }"
>
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" />
</div>
</div>
</div>
@@ -113,29 +166,66 @@
<script setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { borderClass } from '@/composables/borderStyles'
import { createSequentialMount } from '@/composables/useSequentialMount'
const STEP_MS = 300
const SETTLE_MS = 400
const NINE_STEP_MS = 300
const NINE_SETTLE_MS = 400
const MULTI_STEP_MS = 220
const MULTI_SETTLE_MS = 300
const OLFS_STEP_MS = 300
const OLFS_SETTLE_MS = 320
const OLFS_DECODE_TIMEOUT_MS = 1200
/** 瀑布流/拍立得仍自观测;九宫格/一大五小由父级 activate+停滚驱动 */
const SEQUENTIAL_SELF_TYPES = new Set(['masonry', 'polaroid'])
const HOLD_ACTIVATE_TYPES = new Set(['nine-grid', 'one-large-five-small'])
const props = defineProps({
page: { type: Object, required: true },
/** 九宫格专用:父级在 section 入视口后设为 true 才挂载展示 */
/** 九宫格/一大五小:父级停滚后设为 true 才串行挂载 */
activate: { type: Boolean, default: false },
/** 开启串行 v-reveal关闭:预加载后直接展示 */
/** 九宫格:开启串行 v-reveal关闭则一次展示 */
nineGridAnimate: { type: Boolean, default: false },
})
const emit = defineEmits(['preview-image', 'ready'])
const nineShownCount = ref(0)
const shownCount = ref(0)
const multiRootRef = ref(null)
const olfsLargeImgRef = ref(null)
let readyEmitted = false
let preloadStarted = false
let seqToken = 0
let multiObserver = null
let multiStarted = false
let olfsToken = 0
const seq = createSequentialMount()
const frameClass = computed(() => borderClass(props.page.borderStyle))
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
/** 关闭动画时提前暖缓存 */
const waitImgReady = (img, timeoutMs) => {
if (!img) return Promise.resolve()
const decode = () => {
if (typeof img.decode === 'function') return img.decode().catch(() => {})
if (img.complete) return Promise.resolve()
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true })
img.addEventListener('error', resolve, { once: true })
})
}
return Promise.race([
decode(),
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
])
}
const emitReady = () => {
if (readyEmitted) return
readyEmitted = true
emit('ready')
}
/** 关闭九宫格动画时提前暖缓存 */
const preloadNineGridImages = () => {
if (preloadStarted || props.nineGridAnimate || props.page?.type !== 'nine-grid') return
const urls = (props.page.images || []).filter(Boolean)
@@ -148,69 +238,153 @@ const preloadNineGridImages = () => {
}
}
const emitReady = () => {
if (readyEmitted) return
readyEmitted = true
emit('ready')
const runNineSequential = async () => {
const total = (props.page.images || []).length
const { cancelled } = await seq.run({
total,
stepMs: NINE_STEP_MS,
settleMs: NINE_SETTLE_MS,
setCount: (n) => { shownCount.value = n },
})
if (!cancelled) emitReady()
}
/** 每 300ms 只挂载 1 格并触发单格 v-reveal */
const runSequentialReveal = async () => {
const token = ++seqToken
const total = (props.page.images || []).length
const showNineInstant = async () => {
shownCount.value = (props.page.images || []).length
await nextTick()
emitReady()
}
/** 一大五小:先大图并等 decode再慢挂小图 */
const runOlfsSequential = async () => {
const token = ++olfsToken
const total = Math.min(6, (props.page.images || []).length)
if (!total) {
emitReady()
return
}
nineShownCount.value = 0
shownCount.value = 0
await nextTick()
if (token !== olfsToken) return
for (let i = 0; i < total; i += 1) {
if (token !== seqToken) return
await sleep(STEP_MS)
if (token !== seqToken) return
nineShownCount.value = i + 1
await sleep(OLFS_STEP_MS)
if (token !== olfsToken) return
shownCount.value = 1
await nextTick()
await waitImgReady(olfsLargeImgRef.value, OLFS_DECODE_TIMEOUT_MS)
if (token !== olfsToken) return
for (let n = 2; n <= total; n += 1) {
await sleep(OLFS_STEP_MS)
if (token !== olfsToken) return
shownCount.value = n
await nextTick()
}
if (token !== seqToken) return
await sleep(SETTLE_MS)
if (token !== seqToken) return
if (token !== olfsToken) return
await sleep(OLFS_SETTLE_MS)
emitReady()
}
const showNineGridInstant = async () => {
const runMultiSequential = async () => {
const type = props.page?.type
if (!SEQUENTIAL_SELF_TYPES.has(type) || multiStarted) return
multiStarted = true
const total = (props.page.images || []).length
nineShownCount.value = total
await seq.run({
total,
stepMs: MULTI_STEP_MS,
settleMs: MULTI_SETTLE_MS,
setCount: (n) => { shownCount.value = n },
})
}
const unbindMultiObserver = () => {
multiObserver?.disconnect()
multiObserver = null
}
const bindMultiObserver = async () => {
unbindMultiObserver()
if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) return
await nextTick()
emitReady()
const el = multiRootRef.value
if (!el || typeof IntersectionObserver === 'undefined') {
// 无观察器时直接串行,避免永不出现
if (!multiStarted) runMultiSequential()
return
}
// 露出约 30% 再串行,避免进屏前播完
multiObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting || entry.intersectionRatio < 0.3) continue
multiObserver?.disconnect()
multiObserver = null
runMultiSequential()
break
}
},
{ threshold: [0.3, 0.5, 0.7], rootMargin: '0px' }
)
multiObserver.observe(el)
}
watch(
() => props.activate && props.page?.type === 'nine-grid',
() => props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type),
async (on) => {
if (!on || readyEmitted) return
if (props.nineGridAnimate) await runSequentialReveal()
else await showNineGridInstant()
const type = props.page?.type
if (type === 'nine-grid') {
if (props.nineGridAnimate) await runNineSequential()
else await showNineInstant()
return
}
if (type === 'one-large-five-small') {
await runOlfsSequential()
}
},
{ flush: 'post' }
)
watch(
() => [props.page?.type, props.nineGridAnimate, props.page?.images],
() => [props.page?.type, props.page?.images],
async () => {
seq.cancel()
olfsToken += 1
unbindMultiObserver()
multiStarted = false
shownCount.value = 0
readyEmitted = false
if (props.page?.type === 'nine-grid') {
if (!props.nineGridAnimate) preloadNineGridImages()
return
}
if (props.page?.type === 'one-large-five-small') return
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
await bindMultiObserver()
}
},
{ deep: true, flush: 'post' }
)
watch(
() => props.nineGridAnimate,
() => {
if (!props.nineGridAnimate) preloadNineGridImages()
},
{ deep: true }
}
)
onMounted(() => {
if (!props.nineGridAnimate) preloadNineGridImages()
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
})
onUnmounted(() => {
seqToken += 1
seq.cancel()
olfsToken += 1
unbindMultiObserver()
})
function getRevealConfig(imgIdx) {
@@ -218,17 +392,19 @@ function getRevealConfig(imgIdx) {
const col = imgIdx % 3
if (imgIdx === 4) {
return { variant: 'scale', delay: 0 }
return { variant: 'scale', delay: 0, immediate: true }
}
if (col === 1) {
return {
variant: row === 0 ? 'down' : 'up',
delay: 0,
immediate: true,
}
}
return {
variant: col === 0 ? 'left' : 'right',
delay: 0,
immediate: true,
}
}
@@ -303,6 +479,22 @@ function polaroidTransform(i) {
border: none !important;
box-shadow: none !important;
}
.olfs-slot {
min-width: 0;
min-height: 0;
contain: layout paint;
}
.olfs-placeholder {
visibility: hidden;
pointer-events: none;
background: transparent !important;
}
.olfs-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.overlap-wrap {
width: 100%;