1. 播放器歌词效果
This commit is contained in:
@@ -27,27 +27,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 一大五小 -->
|
<!-- 一大五小:固定 6 槽占位 + 串行填内容(大图 decode 后再挂小图) -->
|
||||||
<div v-else-if="page.type === 'one-large-five-small'"
|
<div
|
||||||
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20">
|
v-else-if="page.type === 'one-large-five-small'"
|
||||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative" :class="frameClass">
|
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20"
|
||||||
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" @click.stop="previewImage(page.images[0])" />
|
>
|
||||||
|
<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>
|
||||||
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative" :class="frameClass">
|
<div
|
||||||
<img :src="page.images[i]" class="preview-img" @click.stop="previewImage(page.images[i])" />
|
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>
|
||||||
</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-else-if="page.type === 'nine-grid'" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
||||||
<div
|
<div
|
||||||
v-for="(img, imgIdx) in (page.images || [])"
|
v-for="(img, imgIdx) in (page.images || [])"
|
||||||
:key="imgIdx"
|
:key="imgIdx"
|
||||||
class="aspect-square relative nine-grid-slot"
|
class="aspect-square relative nine-grid-slot"
|
||||||
:class="{ 'nine-grid-placeholder': imgIdx >= nineShownCount }"
|
:class="{ 'nine-grid-placeholder': imgIdx >= shownCount }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="imgIdx < nineShownCount && nineGridAnimate"
|
v-if="imgIdx < shownCount && nineGridAnimate"
|
||||||
v-reveal="getRevealConfig(imgIdx)"
|
v-reveal="getRevealConfig(imgIdx)"
|
||||||
class="absolute inset-0"
|
class="absolute inset-0"
|
||||||
:class="frameClass"
|
:class="frameClass"
|
||||||
@@ -60,7 +95,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="imgIdx < nineShownCount"
|
v-else-if="imgIdx < shownCount"
|
||||||
class="absolute inset-0"
|
class="absolute inset-0"
|
||||||
:class="frameClass"
|
:class="frameClass"
|
||||||
>
|
>
|
||||||
@@ -75,11 +110,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 瀑布流 -->
|
<!-- 瀑布流:串行挂载 + immediate -->
|
||||||
<div v-else-if="page.type === 'masonry'" class="g-masonry mt-6 px-1">
|
<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" v-reveal="{ variant: 'up', delay: imgIdx * 50 }"
|
<div
|
||||||
class="g-masonry-item" :class="frameClass">
|
v-for="(img, imgIdx) in (page.images || [])"
|
||||||
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
: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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,11 +142,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 拍立得拼贴 -->
|
<!-- 拍立得:串行挂载 + immediate -->
|
||||||
<div v-else-if="page.type === 'polaroid'" class="g-polaroid mt-6">
|
<div v-else-if="page.type === 'polaroid'" ref="multiRootRef" class="g-polaroid mt-6">
|
||||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 70 }"
|
<div
|
||||||
class="g-polaroid-item" :style="{ transform: polaroidTransform(imgIdx) }">
|
v-for="(img, imgIdx) in (page.images || [])"
|
||||||
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
: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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -113,29 +166,66 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
import { borderClass } from '@/composables/borderStyles'
|
import { borderClass } from '@/composables/borderStyles'
|
||||||
|
import { createSequentialMount } from '@/composables/useSequentialMount'
|
||||||
|
|
||||||
const STEP_MS = 300
|
const NINE_STEP_MS = 300
|
||||||
const SETTLE_MS = 400
|
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({
|
const props = defineProps({
|
||||||
page: { type: Object, required: true },
|
page: { type: Object, required: true },
|
||||||
/** 九宫格专用:父级在 section 入视口后设为 true 才挂载展示 */
|
/** 九宫格/一大五小:父级停滚后设为 true 才串行挂载 */
|
||||||
activate: { type: Boolean, default: false },
|
activate: { type: Boolean, default: false },
|
||||||
/** 开启:串行 v-reveal;关闭:预加载后直接展示 */
|
/** 九宫格:开启串行 v-reveal;关闭则一次展示 */
|
||||||
nineGridAnimate: { type: Boolean, default: false },
|
nineGridAnimate: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['preview-image', 'ready'])
|
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 readyEmitted = false
|
||||||
let preloadStarted = 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 frameClass = computed(() => borderClass(props.page.borderStyle))
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
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 = () => {
|
const preloadNineGridImages = () => {
|
||||||
if (preloadStarted || props.nineGridAnimate || props.page?.type !== 'nine-grid') return
|
if (preloadStarted || props.nineGridAnimate || props.page?.type !== 'nine-grid') return
|
||||||
const urls = (props.page.images || []).filter(Boolean)
|
const urls = (props.page.images || []).filter(Boolean)
|
||||||
@@ -148,69 +238,153 @@ const preloadNineGridImages = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emitReady = () => {
|
const runNineSequential = async () => {
|
||||||
if (readyEmitted) return
|
const total = (props.page.images || []).length
|
||||||
readyEmitted = true
|
const { cancelled } = await seq.run({
|
||||||
emit('ready')
|
total,
|
||||||
|
stepMs: NINE_STEP_MS,
|
||||||
|
settleMs: NINE_SETTLE_MS,
|
||||||
|
setCount: (n) => { shownCount.value = n },
|
||||||
|
})
|
||||||
|
if (!cancelled) emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 每 300ms 只挂载 1 格并触发单格 v-reveal */
|
const showNineInstant = async () => {
|
||||||
const runSequentialReveal = async () => {
|
shownCount.value = (props.page.images || []).length
|
||||||
const token = ++seqToken
|
await nextTick()
|
||||||
const total = (props.page.images || []).length
|
emitReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一大五小:先大图并等 decode,再慢挂小图 */
|
||||||
|
const runOlfsSequential = async () => {
|
||||||
|
const token = ++olfsToken
|
||||||
|
const total = Math.min(6, (props.page.images || []).length)
|
||||||
if (!total) {
|
if (!total) {
|
||||||
emitReady()
|
emitReady()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nineShownCount.value = 0
|
shownCount.value = 0
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
if (token !== olfsToken) return
|
||||||
|
|
||||||
for (let i = 0; i < total; i += 1) {
|
await sleep(OLFS_STEP_MS)
|
||||||
if (token !== seqToken) return
|
if (token !== olfsToken) return
|
||||||
await sleep(STEP_MS)
|
shownCount.value = 1
|
||||||
if (token !== seqToken) return
|
await nextTick()
|
||||||
nineShownCount.value = i + 1
|
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()
|
await nextTick()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token !== seqToken) return
|
if (token !== olfsToken) return
|
||||||
await sleep(SETTLE_MS)
|
await sleep(OLFS_SETTLE_MS)
|
||||||
if (token !== seqToken) return
|
|
||||||
emitReady()
|
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
|
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()
|
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(
|
watch(
|
||||||
() => props.activate && props.page?.type === 'nine-grid',
|
() => props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type),
|
||||||
async (on) => {
|
async (on) => {
|
||||||
if (!on || readyEmitted) return
|
if (!on || readyEmitted) return
|
||||||
if (props.nineGridAnimate) await runSequentialReveal()
|
const type = props.page?.type
|
||||||
else await showNineGridInstant()
|
if (type === 'nine-grid') {
|
||||||
|
if (props.nineGridAnimate) await runNineSequential()
|
||||||
|
else await showNineInstant()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === 'one-large-five-small') {
|
||||||
|
await runOlfsSequential()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ flush: 'post' }
|
{ flush: 'post' }
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
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()
|
if (!props.nineGridAnimate) preloadNineGridImages()
|
||||||
},
|
}
|
||||||
{ deep: true }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!props.nineGridAnimate) preloadNineGridImages()
|
if (!props.nineGridAnimate) preloadNineGridImages()
|
||||||
|
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
seqToken += 1
|
seq.cancel()
|
||||||
|
olfsToken += 1
|
||||||
|
unbindMultiObserver()
|
||||||
})
|
})
|
||||||
|
|
||||||
function getRevealConfig(imgIdx) {
|
function getRevealConfig(imgIdx) {
|
||||||
@@ -218,17 +392,19 @@ function getRevealConfig(imgIdx) {
|
|||||||
const col = imgIdx % 3
|
const col = imgIdx % 3
|
||||||
|
|
||||||
if (imgIdx === 4) {
|
if (imgIdx === 4) {
|
||||||
return { variant: 'scale', delay: 0 }
|
return { variant: 'scale', delay: 0, immediate: true }
|
||||||
}
|
}
|
||||||
if (col === 1) {
|
if (col === 1) {
|
||||||
return {
|
return {
|
||||||
variant: row === 0 ? 'down' : 'up',
|
variant: row === 0 ? 'down' : 'up',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
variant: col === 0 ? 'left' : 'right',
|
variant: col === 0 ? 'left' : 'right',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +479,22 @@ function polaroidTransform(i) {
|
|||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: 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 {
|
.overlap-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
45
vite-tailwindcss/src/composables/useSequentialMount.js
Normal file
45
vite-tailwindcss/src/composables/useSequentialMount.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 串行挂载:每隔 stepMs 增加 1 个可见项,避免同时挂大量观察器/解码。
|
||||||
|
*/
|
||||||
|
export function createSequentialMount() {
|
||||||
|
let token = 0
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
token += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ total: number, stepMs?: number, settleMs?: number, setCount: (n: number) => void }} opts
|
||||||
|
* @returns {Promise<{ cancelled: boolean }>}
|
||||||
|
*/
|
||||||
|
const run = async ({ total, stepMs = 220, settleMs = 300, setCount }) => {
|
||||||
|
const t = ++token
|
||||||
|
const n = Math.max(0, Number(total) || 0)
|
||||||
|
setCount(0)
|
||||||
|
await nextTick()
|
||||||
|
if (t !== token) return { cancelled: true }
|
||||||
|
|
||||||
|
if (!n) return { cancelled: false }
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i += 1) {
|
||||||
|
if (t !== token) return { cancelled: true }
|
||||||
|
await sleep(stepMs)
|
||||||
|
if (t !== token) return { cancelled: true }
|
||||||
|
setCount(i + 1)
|
||||||
|
await nextTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t !== token) return { cancelled: true }
|
||||||
|
if (settleMs > 0) {
|
||||||
|
await sleep(settleMs)
|
||||||
|
if (t !== token) return { cancelled: true }
|
||||||
|
}
|
||||||
|
return { cancelled: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { run, cancel }
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
* v-reveal -> 默认向上淡入
|
* v-reveal -> 默认向上淡入
|
||||||
* v-reveal="'left'" -> 从左侧滑入
|
* v-reveal="'left'" -> 从左侧滑入
|
||||||
* v-reveal="{ variant: 'scale', delay: 200 }"
|
* v-reveal="{ variant: 'scale', delay: 200 }"
|
||||||
|
* v-reveal="{ variant: 'up', immediate: true }" -> 不建观察器,下一帧直接入场(串行挂载用)
|
||||||
*
|
*
|
||||||
* 支持的 variant: up | down | left | right | scale | blur
|
* 支持的 variant: up | down | left | right | scale | blur
|
||||||
*/
|
*/
|
||||||
@@ -18,22 +19,41 @@ const VARIANTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseBinding(value) {
|
function parseBinding(value) {
|
||||||
if (!value) return { variant: 'up', delay: 0 }
|
if (!value) return { variant: 'up', delay: 0, immediate: false }
|
||||||
if (typeof value === 'string') return { variant: value, delay: 0 }
|
if (typeof value === 'string') return { variant: value, delay: 0, immediate: false }
|
||||||
return {
|
return {
|
||||||
variant: value.variant || 'up',
|
variant: value.variant || 'up',
|
||||||
delay: value.delay || 0,
|
delay: value.delay || 0,
|
||||||
|
immediate: !!value.immediate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reveal = {
|
const reveal = {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
const { variant, delay } = parseBinding(binding.value)
|
// false:跳过入场(同模板不挂观察器)
|
||||||
|
if (binding.value === false) return
|
||||||
|
|
||||||
|
const { variant, delay, immediate } = parseBinding(binding.value)
|
||||||
el.classList.add('reveal')
|
el.classList.add('reveal')
|
||||||
el.classList.add(VARIANTS[variant] || 'reveal-up')
|
el.classList.add(VARIANTS[variant] || 'reveal-up')
|
||||||
if (delay) el.style.transitionDelay = `${delay}ms`
|
if (delay) el.style.transitionDelay = `${delay}ms`
|
||||||
|
|
||||||
// 已处于视口(如首屏)时立即显示,避免永远不可见
|
// 串行挂载场景:父级已保证在视口内,免 IntersectionObserver
|
||||||
|
if (immediate) {
|
||||||
|
const kick = () => el.classList.add('reveal--in')
|
||||||
|
if (typeof requestAnimationFrame === 'function') {
|
||||||
|
el.__revealRaf = requestAnimationFrame(() => {
|
||||||
|
el.__revealRaf = requestAnimationFrame(() => {
|
||||||
|
el.__revealRaf = 0
|
||||||
|
kick()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
el.__revealTimer = setTimeout(kick, 16)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
@@ -51,6 +71,14 @@ const reveal = {
|
|||||||
el.__revealObserver.disconnect()
|
el.__revealObserver.disconnect()
|
||||||
delete el.__revealObserver
|
delete el.__revealObserver
|
||||||
}
|
}
|
||||||
|
if (el.__revealRaf) {
|
||||||
|
cancelAnimationFrame(el.__revealRaf)
|
||||||
|
el.__revealRaf = 0
|
||||||
|
}
|
||||||
|
if (el.__revealTimer) {
|
||||||
|
clearTimeout(el.__revealTimer)
|
||||||
|
el.__revealTimer = 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,16 @@
|
|||||||
:active="effectsActive"
|
:active="effectsActive"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 顶部滚动进度条 -->
|
<!-- 顶部滚动进度条(transform 驱动,避免 width/left 强制重排) -->
|
||||||
<div class="fixed top-0 left-0 h-[2px] bg-[#a88c6b] z-[55] transition-[width] duration-150" :style="{ width: progress * 100 + '%' }"></div>
|
<div
|
||||||
|
class="scroll-progress"
|
||||||
|
aria-hidden="true"
|
||||||
|
:style="{ '--scroll-p': String(Math.min(1, Math.max(0, progress))) }"
|
||||||
|
>
|
||||||
|
<div class="scroll-progress-track" />
|
||||||
|
<div class="scroll-progress-fill" />
|
||||||
|
<span class="scroll-progress-xi calligraphy">囍</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 右上角:自动滚动 -->
|
<!-- 右上角:自动滚动 -->
|
||||||
<div class="fixed top-6 right-5 z-50 flex flex-col items-end gap-4">
|
<div class="fixed top-6 right-5 z-50 flex flex-col items-end gap-4">
|
||||||
@@ -110,11 +118,15 @@
|
|||||||
title="打开播放器"
|
title="打开播放器"
|
||||||
@click="openPlayerPanel"
|
@click="openPlayerPanel"
|
||||||
>
|
>
|
||||||
|
<span class="vinyl-rim" aria-hidden="true" />
|
||||||
<span class="vinyl-grooves" aria-hidden="true" />
|
<span class="vinyl-grooves" aria-hidden="true" />
|
||||||
|
<span class="vinyl-sheen" aria-hidden="true" />
|
||||||
|
<span class="vinyl-label-ring" aria-hidden="true" />
|
||||||
<span
|
<span
|
||||||
class="vinyl-label"
|
class="vinyl-label"
|
||||||
:style="audio.currentCover.value ? { backgroundImage: `url(${audio.currentCover.value})` } : undefined"
|
:style="audio.currentCover.value ? { backgroundImage: `url(${audio.currentCover.value})` } : undefined"
|
||||||
/>
|
/>
|
||||||
|
<span class="vinyl-spindle" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -298,17 +310,17 @@
|
|||||||
class="page-section p-6 relative"
|
class="page-section p-6 relative"
|
||||||
:class="data.scrollMode === 'snap' ? 'snap-start' : ''"
|
:class="data.scrollMode === 'snap' ? 'snap-start' : ''"
|
||||||
:style="pageHeightStyle(page)"
|
:style="pageHeightStyle(page)"
|
||||||
:data-nine-grid="page.type === 'nine-grid' ? String(pIdx) : undefined">
|
:data-gallery-hold="isHoldGalleryType(page.type) ? String(pIdx) : undefined">
|
||||||
<div class="absolute inset-0 bg-white/40 -z-10"></div>
|
<div class="absolute inset-0 bg-white/40 -z-10"></div>
|
||||||
<div class="absolute top-0 left-0 w-24 h-24 border-l-[0.5px] border-t-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
|
<div class="absolute top-0 left-0 w-24 h-24 border-l-[0.5px] border-t-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
|
||||||
<div class="absolute bottom-0 right-0 w-24 h-24 border-r-[0.5px] border-b-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
|
<div class="absolute bottom-0 right-0 w-24 h-24 border-r-[0.5px] border-b-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
|
||||||
<div class="w-full h-full flex flex-col justify-center py-6 relative z-10">
|
<div class="w-full h-full flex flex-col justify-center py-6 relative z-10">
|
||||||
<PhotoGallery
|
<PhotoGallery
|
||||||
:page="page"
|
:page="page"
|
||||||
:activate="page.type === 'nine-grid' ? !!nineGridActivate[pIdx] : false"
|
:activate="isHoldGalleryType(page.type) ? !!galleryActivate[pIdx] : false"
|
||||||
:nine-grid-animate="!!data.nineGridAnimate"
|
:nine-grid-animate="!!data.nineGridAnimate"
|
||||||
@preview-image="openImagePreview"
|
@preview-image="openImagePreview"
|
||||||
@ready="onNineGridReady(pIdx)"
|
@ready="onGalleryReady(pIdx)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -732,8 +744,11 @@ const likeBusy = ref(false)
|
|||||||
let likePulseTimer = null
|
let likePulseTimer = null
|
||||||
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
|
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
|
||||||
const albumPreviewImages = ref([])
|
const albumPreviewImages = ref([])
|
||||||
const nineGridActivate = reactive({})
|
/** 九宫格 / 一大五小:进视口中线后停滚再串行加载 */
|
||||||
const nineGridHold = ref(false)
|
const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
||||||
|
const isHoldGalleryType = (type) => HOLD_GALLERY_TYPES.has(type)
|
||||||
|
const galleryActivate = reactive({})
|
||||||
|
const galleryHold = ref(false)
|
||||||
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
||||||
const blessingForm = reactive({ name: '', content: '', color: DEFAULT_DANMAKU_COLOR })
|
const blessingForm = reactive({ name: '', content: '', color: DEFAULT_DANMAKU_COLOR })
|
||||||
const danmakuColorOptions = DANMAKU_COLORS
|
const danmakuColorOptions = DANMAKU_COLORS
|
||||||
@@ -773,13 +788,13 @@ const scheduleBlessingAutoClose = () => {
|
|||||||
|
|
||||||
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null
|
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null
|
||||||
let freeScrollLastTs = 0
|
let freeScrollLastTs = 0
|
||||||
let nineGridObserver = null
|
let galleryObserver = null
|
||||||
let nineGridRevealTimer = null
|
let galleryRevealTimer = null
|
||||||
const nineGridDone = new Set()
|
const galleryDone = new Set()
|
||||||
const BOTTOM_RETURN_DELAY = 30000
|
const BOTTOM_RETURN_DELAY = 30000
|
||||||
const FREE_SCROLL_BASE_STEP = 1.2
|
const FREE_SCROLL_BASE_STEP = 1.2
|
||||||
/** ready 后极短缓冲再恢复滚动(串行等待已在 PhotoGallery 内完成) */
|
/** ready 后极短缓冲再恢复滚动(串行等待已在 PhotoGallery 内完成) */
|
||||||
const NINE_GRID_RESUME_DELAY_MS = 200
|
const GALLERY_RESUME_DELAY_MS = 200
|
||||||
|
|
||||||
const freeInterval = computed(() => {
|
const freeInterval = computed(() => {
|
||||||
const v = Number(data.value.freeScrollInterval)
|
const v = Number(data.value.freeScrollInterval)
|
||||||
@@ -915,12 +930,25 @@ const effectsActive = computed(() =>
|
|||||||
|
|
||||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||||
const pageScrollTop = () => window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
|
const pageScrollTop = () => window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||||
const pageScrollMax = () => Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
/** 缓存文档高度,避免每次 scroll 读 scrollHeight 触发强制重排 */
|
||||||
|
let cachedScrollMax = 0
|
||||||
|
let scrollRaf = 0
|
||||||
|
const refreshScrollMax = () => {
|
||||||
|
cachedScrollMax = Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
||||||
|
return cachedScrollMax
|
||||||
|
}
|
||||||
|
const pageScrollMax = () => (cachedScrollMax > 0 ? cachedScrollMax : refreshScrollMax())
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const max = pageScrollMax()
|
if (scrollRaf) return
|
||||||
progress.value = max > 0 ? pageScrollTop() / max : 0
|
scrollRaf = requestAnimationFrame(() => {
|
||||||
if (isAtBottom()) scheduleBottomReturn()
|
scrollRaf = 0
|
||||||
else clearBottomReturn()
|
const max = pageScrollMax()
|
||||||
|
const top = pageScrollTop()
|
||||||
|
progress.value = max > 0 ? top / max : 0
|
||||||
|
if (max - top <= 8) scheduleBottomReturn()
|
||||||
|
else clearBottomReturn()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUserInteraction = () => {
|
const handleUserInteraction = () => {
|
||||||
@@ -932,7 +960,10 @@ const handleUserInteraction = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sectionEls = () => Array.from(scrollContainerRef.value?.querySelectorAll('.page-section') || [])
|
const sectionEls = () => Array.from(scrollContainerRef.value?.querySelectorAll('.page-section') || [])
|
||||||
const isAtBottom = () => pageScrollMax() - pageScrollTop() <= 8
|
const isAtBottom = () => {
|
||||||
|
const max = pageScrollMax()
|
||||||
|
return max - pageScrollTop() <= 8
|
||||||
|
}
|
||||||
|
|
||||||
const clearBottomReturn = () => {
|
const clearBottomReturn = () => {
|
||||||
clearTimeout(bottomReturnTimer)
|
clearTimeout(bottomReturnTimer)
|
||||||
@@ -987,45 +1018,51 @@ const canAutoAdvance = () =>
|
|||||||
&& !previewVisible.value
|
&& !previewVisible.value
|
||||||
&& !showMapOptions.value
|
&& !showMapOptions.value
|
||||||
&& !showOpenInBrowser.value
|
&& !showOpenInBrowser.value
|
||||||
&& !nineGridHold.value
|
&& !galleryHold.value
|
||||||
|
|
||||||
const beginNineGridGate = (pIdx) => {
|
const beginGalleryGate = (pIdx) => {
|
||||||
if (nineGridDone.has(pIdx)) return
|
if (galleryDone.has(pIdx)) return
|
||||||
nineGridDone.add(pIdx)
|
galleryDone.add(pIdx)
|
||||||
nineGridHold.value = true
|
galleryHold.value = true
|
||||||
nineGridActivate[pIdx] = true
|
galleryActivate[pIdx] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onNineGridReady = (pIdx) => {
|
const onGalleryReady = (pIdx) => {
|
||||||
if (!nineGridDone.has(pIdx)) return
|
if (!galleryDone.has(pIdx)) return
|
||||||
clearTimeout(nineGridRevealTimer)
|
refreshScrollMax()
|
||||||
nineGridRevealTimer = setTimeout(() => {
|
clearTimeout(galleryRevealTimer)
|
||||||
nineGridHold.value = false
|
galleryRevealTimer = setTimeout(() => {
|
||||||
nineGridRevealTimer = null
|
galleryHold.value = false
|
||||||
}, NINE_GRID_RESUME_DELAY_MS)
|
galleryRevealTimer = null
|
||||||
|
}, GALLERY_RESUME_DELAY_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindNineGridObserver = () => {
|
const bindGalleryHoldObserver = () => {
|
||||||
nineGridObserver?.disconnect()
|
galleryObserver?.disconnect()
|
||||||
nineGridObserver = null
|
galleryObserver = null
|
||||||
if (typeof IntersectionObserver === 'undefined') return
|
if (typeof IntersectionObserver === 'undefined') return
|
||||||
const nodes = scrollContainerRef.value?.querySelectorAll('[data-nine-grid]')
|
const nodes = scrollContainerRef.value?.querySelectorAll('[data-gallery-hold]')
|
||||||
if (!nodes?.length) return
|
if (!nodes?.length) return
|
||||||
// 进入窗口即 activate;期间 nineGridHold 暂停自动滚
|
// 区块抵达「自下而上 75%」线后再停滚+activate(= 距视口顶 25%)
|
||||||
nineGridObserver = new IntersectionObserver(
|
galleryObserver = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
|
const vh = window.innerHeight || 1
|
||||||
|
const lineY = vh * (1 - 0.75) // 从下往上 75%
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isIntersecting) continue
|
if (!entry.isIntersecting) continue
|
||||||
const raw = entry.target.getAttribute('data-nine-grid')
|
const rect = entry.boundingClientRect
|
||||||
|
// 区块跨越该水平线
|
||||||
|
if (rect.top > lineY || rect.bottom < lineY) continue
|
||||||
|
const raw = entry.target.getAttribute('data-gallery-hold')
|
||||||
if (raw == null) continue
|
if (raw == null) continue
|
||||||
const pIdx = Number(raw)
|
const pIdx = Number(raw)
|
||||||
if (!Number.isFinite(pIdx) || nineGridDone.has(pIdx)) continue
|
if (!Number.isFinite(pIdx) || galleryDone.has(pIdx)) continue
|
||||||
beginNineGridGate(pIdx)
|
beginGalleryGate(pIdx)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: [0, 0.08, 0.2], rootMargin: '120px 0px' }
|
{ threshold: [0, 0.1, 0.25, 0.5, 0.75, 1], rootMargin: '0px' }
|
||||||
)
|
)
|
||||||
nodes.forEach((el) => nineGridObserver.observe(el))
|
nodes.forEach((el) => galleryObserver.observe(el))
|
||||||
}
|
}
|
||||||
|
|
||||||
const startAutoScroll = () => {
|
const startAutoScroll = () => {
|
||||||
@@ -1045,11 +1082,12 @@ const startAutoScroll = () => {
|
|||||||
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
|
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
|
||||||
freeScrollLastTs = now
|
freeScrollLastTs = now
|
||||||
if (canAutoAdvance()) {
|
if (canAutoAdvance()) {
|
||||||
if (isAtBottom()) {
|
const top = pageScrollTop()
|
||||||
|
const max = pageScrollMax()
|
||||||
|
if (max - top <= 8) {
|
||||||
scheduleBottomReturn()
|
scheduleBottomReturn()
|
||||||
} else {
|
} else {
|
||||||
window.scrollTo(0, pageScrollTop() + FREE_SCROLL_BASE_STEP * (dt / freeInterval.value))
|
window.scrollTo(0, top + FREE_SCROLL_BASE_STEP * (dt / freeInterval.value))
|
||||||
if (isAtBottom()) scheduleBottomReturn()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rafId = requestAnimationFrame(tickFreeScroll)
|
rafId = requestAnimationFrame(tickFreeScroll)
|
||||||
@@ -1288,18 +1326,23 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
bindNineGridObserver()
|
bindGalleryHoldObserver()
|
||||||
|
refreshScrollMax()
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
window.addEventListener('resize', refreshScrollMax, { passive: true })
|
||||||
handleScroll()
|
handleScroll()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.documentElement.classList.remove('page-scroll-snap')
|
document.documentElement.classList.remove('page-scroll-snap')
|
||||||
window.removeEventListener('scroll', handleScroll)
|
window.removeEventListener('scroll', handleScroll)
|
||||||
nineGridObserver?.disconnect()
|
window.removeEventListener('resize', refreshScrollMax)
|
||||||
nineGridObserver = null
|
galleryObserver?.disconnect()
|
||||||
clearTimeout(nineGridRevealTimer)
|
galleryObserver = null
|
||||||
nineGridRevealTimer = null
|
clearTimeout(galleryRevealTimer)
|
||||||
|
galleryRevealTimer = null
|
||||||
|
cancelAnimationFrame(scrollRaf)
|
||||||
|
scrollRaf = 0
|
||||||
cancelAnimationFrame(rafId)
|
cancelAnimationFrame(rafId)
|
||||||
cancelAnimationFrame(animId)
|
cancelAnimationFrame(animId)
|
||||||
clearInterval(snapTimer)
|
clearInterval(snapTimer)
|
||||||
@@ -1316,7 +1359,8 @@ watch(
|
|||||||
() => data.value.photoPages,
|
() => data.value.photoPages,
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
bindNineGridObserver()
|
bindGalleryHoldObserver()
|
||||||
|
refreshScrollMax()
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
@@ -1324,7 +1368,7 @@ watch(
|
|||||||
() => data.value.nineGridAnimate,
|
() => data.value.nineGridAnimate,
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
bindNineGridObserver()
|
bindGalleryHoldObserver()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
watch(freeScrollSignature, restartAutoScroll)
|
watch(freeScrollSignature, restartAutoScroll)
|
||||||
@@ -1486,29 +1530,105 @@ watch(
|
|||||||
.vinyl-wrap {
|
.vinyl-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 52px;
|
width: 54px;
|
||||||
height: 52px;
|
height: 54px;
|
||||||
}
|
}
|
||||||
.vinyl-disc {
|
.vinyl-disc {
|
||||||
width: 52px;
|
width: 54px;
|
||||||
height: 52px;
|
height: 54px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(60, 45, 30, 0.35);
|
border: none;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at center, rgba(40, 30, 22, 0.2) 0 14%, transparent 15%),
|
radial-gradient(circle at 50% 50%, #1a1410 0 6%, transparent 6.2%),
|
||||||
repeating-radial-gradient(circle at center, #2a2118 0 1px, #3a2f24 1px 3px);
|
radial-gradient(circle at 32% 28%, rgba(255, 240, 220, 0.14) 0 10%, transparent 28%),
|
||||||
box-shadow: 0 6px 16px rgba(60, 45, 30, 0.22);
|
repeating-radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
#1e1813 0 0.6px,
|
||||||
|
#2c241c 0.6px 1.4px,
|
||||||
|
#241c16 1.4px 2.2px,
|
||||||
|
#332a21 2.2px 3px
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(90, 70, 50, 0.45),
|
||||||
|
0 0 0 2px rgba(30, 22, 16, 0.55),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
|
||||||
|
0 7px 18px rgba(40, 28, 18, 0.32);
|
||||||
}
|
}
|
||||||
.vinyl-disc.spinning { animation: vinyl-spin 4.8s linear infinite; }
|
.vinyl-disc.spinning { animation: vinyl-spin 4.8s linear infinite; }
|
||||||
.vinyl-grooves { position: absolute; inset: 0; opacity: 0.35; pointer-events: none; }
|
.vinyl-rim {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1.5px rgba(180, 150, 110, 0.22),
|
||||||
|
inset 0 0 10px rgba(0, 0, 0, 0.35);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.vinyl-grooves {
|
||||||
|
position: absolute;
|
||||||
|
inset: 8%;
|
||||||
|
border-radius: 999px;
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
repeating-radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
transparent 0 1.5px,
|
||||||
|
rgba(255, 245, 230, 0.045) 1.5px 1.9px,
|
||||||
|
transparent 1.9px 3.4px,
|
||||||
|
rgba(0, 0, 0, 0.18) 3.4px 3.8px
|
||||||
|
);
|
||||||
|
mask-image: radial-gradient(circle at center, transparent 0 26%, #000 28% 92%, transparent 94%);
|
||||||
|
-webkit-mask-image: radial-gradient(circle at center, transparent 0 26%, #000 28% 92%, transparent 94%);
|
||||||
|
}
|
||||||
|
.vinyl-sheen {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
125deg,
|
||||||
|
rgba(255, 255, 255, 0.22) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 28%,
|
||||||
|
rgba(255, 255, 255, 0) 58%,
|
||||||
|
rgba(255, 255, 255, 0.08) 100%
|
||||||
|
);
|
||||||
|
mix-blend-mode: soft-light;
|
||||||
|
}
|
||||||
|
.vinyl-label-ring {
|
||||||
|
position: absolute;
|
||||||
|
inset: 25%;
|
||||||
|
border-radius: 999px;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(212, 180, 140, 0.35),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
.vinyl-label {
|
.vinyl-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 28%;
|
inset: 28%;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #c4a484 center/cover no-repeat;
|
background:
|
||||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.25);
|
radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.2), transparent 42%),
|
||||||
|
#c4a484 center/cover no-repeat;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.28),
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.vinyl-spindle {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
margin: -2.5px 0 0 -2.5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: radial-gradient(circle at 35% 30%, #f0e0c8, #8a704f 70%, #5c4a35);
|
||||||
|
box-shadow: 0 0 0 1px rgba(40, 28, 18, 0.35);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.vinyl-play-fab {
|
.vinyl-play-fab {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1527,6 +1647,57 @@ watch(
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.vinyl-play-fab .fa-play { margin-left: 1px; }
|
.vinyl-play-fab .fa-play { margin-left: 1px; }
|
||||||
|
|
||||||
|
.scroll-progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 10px;
|
||||||
|
z-index: 55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.scroll-progress-track {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(168, 140, 107, 0.08),
|
||||||
|
rgba(168, 140, 107, 0.18),
|
||||||
|
rgba(168, 140, 107, 0.08)
|
||||||
|
);
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
.scroll-progress-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #d4b48a 0%, #a88c6b 55%, #8a704f 100%);
|
||||||
|
box-shadow: 0 0 8px rgba(168, 140, 107, 0.45);
|
||||||
|
transform: scaleX(var(--scroll-p, 0));
|
||||||
|
transform-origin: left center;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.scroll-progress-xi {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #a88c6b;
|
||||||
|
text-shadow:
|
||||||
|
0 0 6px rgba(253, 251, 247, 0.95),
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.8);
|
||||||
|
transform: translate3d(calc(var(--scroll-p, 0) * (100vw - 1.1em) + 0.55em - 50%), 0, 0);
|
||||||
|
will-change: transform;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
.player-panel {
|
.player-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user