102 lines
3.5 KiB
Vue
102 lines
3.5 KiB
Vue
<script setup>
|
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
|
|
const canvasRef = ref(null);
|
|
let resizeHandler;
|
|
let particleRaf;
|
|
|
|
onMounted(() => {
|
|
const canvas = canvasRef.value;
|
|
if (!canvas) return;
|
|
|
|
const context = canvas.getContext('2d');
|
|
const stars = [];
|
|
const drift = [];
|
|
let width = 0;
|
|
let height = 0;
|
|
let dpr = 1;
|
|
|
|
resizeHandler = () => {
|
|
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
width = canvas.width = window.innerWidth * dpr;
|
|
height = canvas.height = window.innerHeight * dpr;
|
|
canvas.style.width = `${window.innerWidth}px`;
|
|
canvas.style.height = `${window.innerHeight}px`;
|
|
stars.length = 0;
|
|
drift.length = 0;
|
|
|
|
const density = Math.min(260, Math.floor((window.innerWidth * window.innerHeight) / 9000));
|
|
for (let i = 0; i < density; i += 1) {
|
|
stars.push({
|
|
x: Math.random() * width,
|
|
y: Math.random() * height,
|
|
r: Math.random() * 1.3 * dpr + 0.3 * dpr,
|
|
baseAlpha: Math.random() * 0.6 + 0.2,
|
|
twinkleSpeed: Math.random() * 0.003 + 0.0005,
|
|
twinklePhase: Math.random() * Math.PI * 2,
|
|
hue: Math.random() < 0.2 ? 'moon' : 'white',
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < 18; i += 1) {
|
|
drift.push({
|
|
x: Math.random() * width,
|
|
y: Math.random() * height,
|
|
vx: (Math.random() - 0.5) * 0.15,
|
|
vy: (Math.random() - 0.5) * 0.1 - 0.02,
|
|
r: Math.random() * 2 * dpr + 0.5 * dpr,
|
|
alpha: Math.random() * 0.3 + 0.05,
|
|
});
|
|
}
|
|
};
|
|
|
|
const draw = (time) => {
|
|
context.clearRect(0, 0, width, height);
|
|
|
|
for (const star of stars) {
|
|
const alpha = star.baseAlpha * (0.5 + 0.5 * Math.sin(time * star.twinkleSpeed + star.twinklePhase));
|
|
context.beginPath();
|
|
context.arc(star.x, star.y, star.r, 0, Math.PI * 2);
|
|
context.fillStyle = star.hue === 'moon' ? `rgba(200, 230, 255, ${alpha})` : `rgba(244, 236, 207, ${alpha})`;
|
|
context.fill();
|
|
}
|
|
|
|
for (const particle of drift) {
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
if (particle.x < 0) particle.x = width;
|
|
if (particle.x > width) particle.x = 0;
|
|
if (particle.y < 0) particle.y = height;
|
|
if (particle.y > height) particle.y = 0;
|
|
|
|
const gradient = context.createRadialGradient(particle.x, particle.y, 0, particle.x, particle.y, particle.r * 6);
|
|
gradient.addColorStop(0, `rgba(176, 136, 238, ${particle.alpha})`);
|
|
gradient.addColorStop(1, 'rgba(176, 136, 238, 0)');
|
|
context.fillStyle = gradient;
|
|
context.beginPath();
|
|
context.arc(particle.x, particle.y, particle.r * 6, 0, Math.PI * 2);
|
|
context.fill();
|
|
}
|
|
|
|
particleRaf = window.requestAnimationFrame(draw);
|
|
};
|
|
|
|
resizeHandler();
|
|
draw(0);
|
|
window.addEventListener('resize', resizeHandler);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', resizeHandler);
|
|
if (particleRaf) window.cancelAnimationFrame(particleRaf);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="bg">
|
|
<div class="bg-moon" aria-hidden="true"></div>
|
|
<div class="bg-fog" aria-hidden="true"></div>
|
|
</div>
|
|
<canvas id="particles" ref="canvasRef" aria-hidden="true"></canvas>
|
|
</template>
|