更新页面效果,但是会卡顿

This commit is contained in:
李琦
2026-08-01 12:36:58 +08:00
parent 7964b78225
commit bf8d6f5b0c
7 changed files with 264 additions and 37 deletions

View File

@@ -8,6 +8,8 @@ import { onMounted, onUnmounted, watch, ref } from 'vue'
const props = defineProps({
petalEnabled: { type: Boolean, default: true },
bubbleEnabled: { type: Boolean, default: true },
active: { type: Boolean, default: true },
performanceMode: { type: Boolean, default: false },
})
const canvasRef = ref(null)
@@ -22,6 +24,7 @@ let width = 0
let height = 0
let dpr = 1
let lastTime = 0
let lastPaint = 0
let resizeObserver = null
let reduceMotion = false
@@ -52,14 +55,16 @@ function resetHeart(h, initial = false) {
}
function seedParticles() {
const petalCount = props.performanceMode ? 8 : 18
const heartCount = props.performanceMode ? 5 : 12
petals.length = 0
hearts.length = 0
for (let i = 0; i < 18; i += 1) {
for (let i = 0; i < petalCount; i += 1) {
const p = {}
resetPetal(p, true)
petals.push(p)
}
for (let i = 0; i < 12; i += 1) {
for (let i = 0; i < heartCount; i += 1) {
const h = {}
resetHeart(h, true)
hearts.push(h)
@@ -72,7 +77,7 @@ function resize() {
const rect = canvas.getBoundingClientRect()
width = Math.max(1, rect.width)
height = Math.max(1, rect.height)
dpr = Math.min(window.devicePixelRatio || 1, 2)
dpr = props.performanceMode ? 1 : Math.min(window.devicePixelRatio || 1, 1.5)
canvas.width = Math.round(width * dpr)
canvas.height = Math.round(height * dpr)
ctx = canvas.getContext('2d')
@@ -119,11 +124,24 @@ function drawHeart(h, now) {
function tick(now) {
if (!ctx) return
if (!props.active || reduceMotion || (!props.petalEnabled && !props.bubbleEnabled)) {
ctx.clearRect(0, 0, width, height)
rafId = 0
return
}
const frameInterval = props.performanceMode ? 50 : 16.7
if (lastPaint && now - lastPaint < frameInterval) {
rafId = requestAnimationFrame(tick)
return
}
const delta = Math.min(48, now - (lastTime || now)) / 1000
lastTime = now
lastPaint = now
ctx.clearRect(0, 0, width, height)
if (!reduceMotion && props.petalEnabled) {
if (props.petalEnabled) {
for (const p of petals) {
p.y += p.speed * delta
p.rotation += p.rotationSpeed * delta
@@ -132,7 +150,7 @@ function tick(now) {
}
}
if (!reduceMotion && props.bubbleEnabled) {
if (props.bubbleEnabled) {
for (const h of hearts) {
h.y -= h.speed * delta
if (h.y < -80) resetHeart(h)
@@ -146,6 +164,12 @@ function tick(now) {
function start() {
cancelAnimationFrame(rafId)
lastTime = 0
lastPaint = 0
if (!props.active || reduceMotion || (!props.petalEnabled && !props.bubbleEnabled)) {
ctx?.clearRect(0, 0, width, height)
rafId = 0
return
}
rafId = requestAnimationFrame(tick)
}
@@ -157,7 +181,13 @@ onMounted(() => {
start()
})
watch(() => [props.petalEnabled, props.bubbleEnabled], start)
watch(
() => [props.petalEnabled, props.bubbleEnabled, props.active, props.performanceMode],
() => {
resize()
start()
}
)
onUnmounted(() => {
cancelAnimationFrame(rafId)
@@ -173,5 +203,7 @@ onUnmounted(() => {
width: 100%;
height: 100vh;
pointer-events: none;
contain: strict;
transform: translateZ(0);
}
</style>

View File

@@ -10,18 +10,18 @@
<!-- 单图默认居中卡幅 / 可配置宽度铺满 -->
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
<div :class="singleFrameClass"><img :src="page.img1" /></div>
<div :class="singleFrameClass"><img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" @load="notifyLayoutChange" /></div>
</div>
<!-- 错落双图交错拼贴左上大图 + 右下压角叠放微旋转clamp 封顶不溢出 -->
<div v-else-if="page.type === 'overlap'" class="overlap-wrap relative w-full mt-2">
<div v-reveal="{ variant: 'left', delay: 100 }"
class="absolute left-0 top-0 w-[72%] h-[78%] z-10 -rotate-2" :class="frameClass">
<img :src="page.img1" />
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" @load="notifyLayoutChange" />
</div>
<div v-reveal="{ variant: 'right', delay: 300 }"
class="absolute right-0 bottom-0 w-[56%] h-[62%] z-20 rotate-3" :class="frameClass">
<img :src="page.img2" />
<img :src="page.img2" class="preview-img" @click.stop="previewImage(page.img2)" @load="notifyLayoutChange" />
</div>
</div>
@@ -29,10 +29,10 @@
<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="hover:scale-105 transition-transform duration-1000" />
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" @click.stop="previewImage(page.images[0])" @load="notifyLayoutChange" />
</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]" />
<img :src="page.images[i]" class="preview-img" @click.stop="previewImage(page.images[i])" @load="notifyLayoutChange" />
</div>
</div>
@@ -42,7 +42,7 @@
<div v-for="(img, imgIdx) in page.images" :key="imgIdx"
v-reveal="getRevealConfig(imgIdx)"
class="aspect-square relative" :class="frameClass">
<img :src="img" class="nine-grid-img" />
<img :src="img" class="nine-grid-img preview-img" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
</div>
</div>
@@ -50,7 +50,7 @@
<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" />
<img :src="img" class="preview-img" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
</div>
</div>
@@ -58,7 +58,7 @@
<div v-else-if="page.type === 'carousel'" class="relative mt-6">
<div ref="trackRef" class="g-carousel-track no-scrollbar" @scroll="onScroll">
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
<img :src="img" />
<img :src="img" class="preview-img" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
</div>
</div>
<button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张"></button>
@@ -73,7 +73,7 @@
<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" />
<img :src="img" class="preview-img" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
</div>
</div>
@@ -89,6 +89,7 @@ import { borderClass } from '@/composables/borderStyles'
const props = defineProps({
page: { type: Object, required: true },
})
const emit = defineEmits(['preview-image', 'layout-change'])
// 边框样式:前台按 page.borderStyle 套用全局 gf-* 类
const frameClass = computed(() => borderClass(props.page.borderStyle))
@@ -130,6 +131,14 @@ function onScroll() {
if (i !== index.value) index.value = i
}
function previewImage(src) {
if (src) emit('preview-image', src)
}
function notifyLayoutChange() {
emit('layout-change')
}
/* ---------- 拍立得旋转/位移 ---------- */
const polaroidAngles = [-5, 4, -3, 6, -4, 3, -2, 5]
const polaroidShift = [0, 10, -8, 6, -10, 8, 4, -6]
@@ -163,13 +172,14 @@ function getRevealConfig(imgIdx) {
// 左右列
return {
variant: col === 0 ? 'left' : 'right',
delay: 80 + ((imgIdx +80) * 12) ,
delay: 80 + ((imgIdx +80) * 20) ,
}
}
</script>
<style scoped>
.gallery { width: 100%; font-family: var(--font-sans); }
.preview-img { cursor: zoom-in; }
.gallery-title {
font-family: var(--font-calligraphy);
font-weight: 700;