Files
nl-blogs/blog-wot-uniapp/src/components/c-end/AmbientBackground.vue
李琦 4e245f789b 1. 小程序端
2. 视频优化2
2026-07-30 14:14:28 +08:00

285 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* C 端氛围层:暗色星空 + 偶发流星;浅色金色光斑漂移。
* 粒子数约为 PC StarBackground 的 1/3挂在 AppPageShell 底层。
* 使用旧版 canvas 上下文以保证各端兼容不可点击pointer-events: none
*/
import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, watch } from 'vue'
import { onHide, onShow } from '@dcloudio/uni-app'
import { useTheme } from '@/composables/useTheme'
const { isDark } = useTheme()
const canvasId = `ambient-${Math.random().toString(36).slice(2, 9)}`
interface Star {
x: number
y: number
r: number
a: number
out: boolean
sp: number
}
interface Meteor {
x: number
y: number
len: number
sp: number
a: number
}
interface Orb {
bx: number
by: number
r: number
ampX: number
ampY: number
phX: number
phY: number
sp: number
color: string
}
interface Dust {
x: number
y: number
r: number
a: number
out: boolean
sp: number
base: number
}
let ctx: UniApp.CanvasContext | null = null
let width = 0
let height = 0
let running = false
let timer: ReturnType<typeof setTimeout> | null = null
let time = 0
const stars: Star[] = []
const meteors: Meteor[] = []
const orbs: Orb[] = []
const dust: Dust[] = []
const clearPools = () => {
stars.length = 0
meteors.length = 0
orbs.length = 0
dust.length = 0
}
const initDark = () => {
clearPools()
for (let i = 0; i < 55; i++) {
stars.push({
x: Math.random() * width,
y: Math.random() * height,
r: Math.random() * 1.4 + 0.3,
a: Math.random(),
out: Math.random() > 0.5,
sp: Math.random() * 0.02 + 0.005,
})
}
}
const initLight = () => {
clearPools()
const palette = [
'rgba(212,179,131,0.18)',
'rgba(196,150,90,0.14)',
'rgba(232,200,150,0.16)',
]
const places = [
{ bx: 0.18, by: 0.22 },
{ bx: 0.8, by: 0.38 },
{ bx: 0.42, by: 0.78 },
]
places.forEach((p, i) => {
orbs.push({
bx: p.bx * width,
by: p.by * height,
r: Math.min(width, height) * (0.22 + Math.random() * 0.1),
ampX: 28 + Math.random() * 40,
ampY: 22 + Math.random() * 36,
phX: Math.random() * Math.PI * 2,
phY: Math.random() * Math.PI * 2,
sp: 0.0003 + Math.random() * 0.0002,
color: palette[i % palette.length],
})
})
for (let i = 0; i < 28; i++) {
const base = Math.random() * 0.3 + 0.12
dust.push({
x: Math.random() * width,
y: Math.random() * height,
r: Math.random() * 1.6 + 0.4,
a: base,
out: Math.random() > 0.5,
sp: Math.random() * 0.008 + 0.003,
base,
})
}
}
const initTheme = () => {
if (isDark.value) initDark()
else initLight()
}
const draw = () => {
if (!ctx || !running) return
time += 1
ctx.clearRect(0, 0, width, height)
if (isDark.value) {
stars.forEach((s) => {
if (s.out) {
s.a -= s.sp
if (s.a <= 0.1) s.out = false
}
else {
s.a += s.sp
if (s.a >= 1) s.out = true
}
ctx!.beginPath()
ctx!.arc(s.x, s.y, s.r, 0, Math.PI * 2)
ctx!.setFillStyle(`rgba(255,255,255,${Math.max(0.08, s.a)})`)
ctx!.fill()
})
if (Math.random() < 0.008) {
meteors.push({
x: Math.random() * width,
y: 0,
len: Math.random() * 60 + 24,
sp: Math.random() * 8 + 5,
a: 1,
})
}
for (let i = meteors.length - 1; i >= 0; i--) {
const m = meteors[i]
const ex = m.x - m.len * 0.7
const ey = m.y - m.len * 0.7
ctx.beginPath()
ctx.moveTo(m.x, m.y)
ctx.lineTo(ex, ey)
ctx.setStrokeStyle(`rgba(255,255,255,${m.a})`)
ctx.setLineWidth(1.2)
ctx.stroke()
m.x += m.sp * 0.7
m.y += m.sp * 0.7
m.a -= 0.012
if (m.a <= 0 || m.y > height) meteors.splice(i, 1)
}
}
else {
orbs.forEach((o) => {
const x = o.bx + Math.sin(time * o.sp + o.phX) * o.ampX
const y = o.by + Math.cos(time * o.sp + o.phY) * o.ampY
const g = ctx!.createCircularGradient(x, y, o.r)
g.addColorStop(0, o.color)
g.addColorStop(1, 'rgba(212,179,131,0)')
ctx!.beginPath()
ctx!.arc(x, y, o.r, 0, Math.PI * 2)
ctx!.setFillStyle(g)
ctx!.fill()
})
dust.forEach((d) => {
if (d.out) {
d.a -= d.sp
if (d.a <= 0.05) d.out = false
}
else {
d.a += d.sp
if (d.a >= d.base) d.out = true
}
ctx!.beginPath()
ctx!.arc(d.x, d.y, d.r, 0, Math.PI * 2)
ctx!.setFillStyle(`rgba(196,150,90,${d.a})`)
ctx!.fill()
})
}
ctx.draw()
timer = setTimeout(() => {
if (running) draw()
}, 48)
}
const stop = () => {
running = false
if (timer) {
clearTimeout(timer)
timer = null
}
}
const start = () => {
stop()
running = true
draw()
}
const setup = () => {
const inst = getCurrentInstance()
uni.getSystemInfo({
success: (info) => {
width = info.windowWidth
height = info.windowHeight
nextTick(() => {
ctx = uni.createCanvasContext(canvasId, inst?.proxy as never)
initTheme()
start()
})
},
})
}
onMounted(setup)
onShow(() => {
if (ctx) start()
})
onHide(stop)
onBeforeUnmount(stop)
watch(isDark, () => {
initTheme()
})
</script>
<template>
<view class="ambient" aria-hidden="true">
<canvas
:canvas-id="canvasId"
class="ambient-canvas"
:style="{ width: '100%', height: '100%' }"
disable-scroll
/>
<view class="noise" :class="isDark ? 'noise--dark' : 'noise--light'" />
</view>
</template>
<style scoped lang="scss">
.ambient {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.ambient-canvas {
width: 100%;
height: 100%;
display: block;
}
.noise {
position: absolute;
inset: 0;
pointer-events: none;
/* 轻量噪点:用半透明叠层近似胶片感,避免额外图片体积 */
background-image: radial-gradient(rgba(127, 127, 127, 0.35) 0.5px, transparent 0.5px);
background-size: 4px 4px;
}
.noise--light { opacity: 0.045; mix-blend-mode: multiply; }
.noise--dark { opacity: 0.06; mix-blend-mode: soft-light; }
</style>