更新页面效果
This commit is contained in:
177
vite-tailwindcss/src/components/CanvasEffects.vue
Normal file
177
vite-tailwindcss/src/components/CanvasEffects.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<canvas ref="canvasRef" class="canvas-effects" aria-hidden="true"></canvas>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, watch, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
petalEnabled: { type: Boolean, default: true },
|
||||
bubbleEnabled: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const canvasRef = ref(null)
|
||||
const petals = []
|
||||
const hearts = []
|
||||
const petalColors = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6']
|
||||
const heartColors = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2']
|
||||
|
||||
let ctx = null
|
||||
let rafId = 0
|
||||
let width = 0
|
||||
let height = 0
|
||||
let dpr = 1
|
||||
let lastTime = 0
|
||||
let resizeObserver = null
|
||||
let reduceMotion = false
|
||||
|
||||
const rand = (min, max) => min + Math.random() * (max - min)
|
||||
|
||||
function resetPetal(p, initial = false) {
|
||||
p.x = rand(0, width)
|
||||
p.y = initial ? rand(-height, height) : rand(-80, -20)
|
||||
p.size = rand(8, 22)
|
||||
p.speed = rand(18, 42)
|
||||
p.sway = rand(12, 34)
|
||||
p.phase = rand(0, Math.PI * 2)
|
||||
p.rotation = rand(0, Math.PI * 2)
|
||||
p.rotationSpeed = rand(-0.9, 0.9)
|
||||
p.opacity = rand(0.28, 0.7)
|
||||
p.color = petalColors[Math.floor(rand(0, petalColors.length))]
|
||||
}
|
||||
|
||||
function resetHeart(h, initial = false) {
|
||||
h.x = rand(0, width)
|
||||
h.y = initial ? rand(0, height) : height + rand(20, 90)
|
||||
h.size = rand(10, 22)
|
||||
h.speed = rand(12, 32)
|
||||
h.sway = rand(8, 26)
|
||||
h.phase = rand(0, Math.PI * 2)
|
||||
h.opacity = rand(0.1, 0.32)
|
||||
h.color = heartColors[Math.floor(rand(0, heartColors.length))]
|
||||
}
|
||||
|
||||
function seedParticles() {
|
||||
petals.length = 0
|
||||
hearts.length = 0
|
||||
for (let i = 0; i < 18; i += 1) {
|
||||
const p = {}
|
||||
resetPetal(p, true)
|
||||
petals.push(p)
|
||||
}
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
const h = {}
|
||||
resetHeart(h, true)
|
||||
hearts.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
width = Math.max(1, rect.width)
|
||||
height = Math.max(1, rect.height)
|
||||
dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||||
canvas.width = Math.round(width * dpr)
|
||||
canvas.height = Math.round(height * dpr)
|
||||
ctx = canvas.getContext('2d')
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
seedParticles()
|
||||
}
|
||||
|
||||
function drawPetal(p, now) {
|
||||
const x = p.x + Math.sin(now * 0.0014 + p.phase) * p.sway
|
||||
ctx.save()
|
||||
ctx.translate(x, p.y)
|
||||
ctx.rotate(p.rotation)
|
||||
ctx.globalAlpha = p.opacity
|
||||
ctx.fillStyle = p.color
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, -p.size * 0.56)
|
||||
ctx.bezierCurveTo(-p.size * 0.58, -p.size * 0.18, -p.size * 0.36, p.size * 0.48, 0, p.size * 0.58)
|
||||
ctx.bezierCurveTo(p.size * 0.36, p.size * 0.48, p.size * 0.58, -p.size * 0.18, 0, -p.size * 0.56)
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.42)'
|
||||
ctx.lineWidth = 0.8
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, -p.size * 0.38)
|
||||
ctx.lineTo(0, p.size * 0.42)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
function drawHeart(h, now) {
|
||||
const x = h.x + Math.sin(now * 0.001 + h.phase) * h.sway
|
||||
const s = h.size / 18
|
||||
ctx.save()
|
||||
ctx.translate(x, h.y)
|
||||
ctx.scale(s, s)
|
||||
ctx.globalAlpha = h.opacity
|
||||
ctx.fillStyle = h.color
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, 7)
|
||||
ctx.bezierCurveTo(-18, -6, -10, -22, 0, -12)
|
||||
ctx.bezierCurveTo(10, -22, 18, -6, 0, 7)
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
function tick(now) {
|
||||
if (!ctx) return
|
||||
const delta = Math.min(48, now - (lastTime || now)) / 1000
|
||||
lastTime = now
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
if (!reduceMotion && props.petalEnabled) {
|
||||
for (const p of petals) {
|
||||
p.y += p.speed * delta
|
||||
p.rotation += p.rotationSpeed * delta
|
||||
if (p.y > height + 80) resetPetal(p)
|
||||
drawPetal(p, now)
|
||||
}
|
||||
}
|
||||
|
||||
if (!reduceMotion && props.bubbleEnabled) {
|
||||
for (const h of hearts) {
|
||||
h.y -= h.speed * delta
|
||||
if (h.y < -80) resetHeart(h)
|
||||
drawHeart(h, now)
|
||||
}
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
function start() {
|
||||
cancelAnimationFrame(rafId)
|
||||
lastTime = 0
|
||||
rafId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches || false
|
||||
resize()
|
||||
resizeObserver = new ResizeObserver(resize)
|
||||
resizeObserver.observe(canvasRef.value)
|
||||
start()
|
||||
})
|
||||
|
||||
watch(() => [props.petalEnabled, props.bubbleEnabled], start)
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rafId)
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.canvas-effects {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="gallery">
|
||||
<!-- 标题区 -->
|
||||
<div v-reveal="'up'" class="text-center mb-5 px-4">
|
||||
<span class="text-xl text-[#a88c6b] tracking-[0.3em] font-light block mb-2">{{ page.title }}</span>
|
||||
<span class="text-[9px] text-[#8A8680] uppercase tracking-[0.4em] block mb-4">{{ page.subtitle }}</span>
|
||||
<span class="text-[12px] text-[#6b6863] font-light leading-relaxed mx-auto max-w-[280px] tracking-widest block whitespace-pre-wrap">{{ page.desc }}</span>
|
||||
<span class="gallery-title text-xl text-[#a88c6b] tracking-[0.3em] font-light block mb-2">{{ page.title }}</span>
|
||||
<span class="gallery-subtitle text-[9px] text-[#8A8680] uppercase tracking-[0.4em] block mb-4">{{ page.subtitle }}</span>
|
||||
<span class="gallery-desc text-[12px] text-[#6b6863] font-light leading-relaxed mx-auto max-w-[280px] tracking-widest block whitespace-pre-wrap">{{ page.desc }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 单图(默认居中卡幅 / 可配置宽度铺满) -->
|
||||
@@ -139,7 +139,10 @@ function polaroidTransform(i) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gallery { width: 100%; }
|
||||
.gallery { width: 100%; font-family: var(--font-sans); }
|
||||
.gallery-title { font-family: var(--font-serif); }
|
||||
.gallery-subtitle { font-family: var(--font-display); }
|
||||
.gallery-desc { font-family: var(--font-kai); }
|
||||
|
||||
/* 错落双图:高度随视口缩放并封顶,保证标题+图片总高始终落在页高预算内 */
|
||||
.overlap-wrap { height: clamp(320px, 54vh, 460px); }
|
||||
|
||||
Reference in New Issue
Block a user