最近看到了一个浩瀚星空效果图,来自于B站UP主山羊の前端小窝,于是照着效果封装了一个 vue组件。
废话不多说,一起来看看吧!
效果展示
浩瀚星空
源码
如果当做背景,需要把
.g-background
样式中的z-index
属性打开,这个时候鼠标动画不会生效。
App.vue
<script setup lang="ts">
import Background from "@/components/BackgroundView.vue";
</script>
<template>
<Background></Background>
<RouterView />
</template>
<template>
<Background>
<RouterView />
</Background>
</template>
<style scoped></style>
BackgroundView.vue
<script lang="ts" setup>
const options = {
STAR_COLOR: "white", // 星星颜色
STAR_SIZE: 3, // 星星大小
STAR_NUMBER: (window.innerWidth + window.innerHeight) / 8, // 星星数量
STAR_MIN_SCALE: 0.2, // 星星最小缩放比例
OVERFLOW_THRESHOLD: 50, // 溢出阈值
touchInput: false,
scale: 1, // 定义缩放比例 device pixel ratio
width: 0, // 画布宽度
height: 0, // 画布高度
mouseX: 0,
mouseY: 0,
};
let velocity = { x: 0, y: 0, tx: 0, ty: 0, z: 0.0009 }; // 速度对象
type STAR = { x: number; y: number; z: number };
let stars = <STAR[]>[];
let context = <CanvasRenderingContext2D | null>null;
const canvas = useTemplateRef("canvas");
starInit();
onMounted(() => {
context = canvas.value!.getContext("2d");
canvas.value!.width = options.width;
canvas.value!.height = options.height;
animation();
canvas.value!.onmousemove = mouseMove;
canvas.value!.ontouchmove = touchMove;
canvas.value!.ontouchend = mouseLeave;
document.onmouseleave = mouseLeave;
});
// 动画运行
function animation() {
// 清空画布
context!.clearRect(0, 0, options.width, options.height);
starDrawing();
starUpdate();
// 请求下一帧动画
requestAnimationFrame(animation);
}
// 生成星星
function starInit() {
// 获取设备像素比例
options.scale = window.devicePixelRatio || 1;
// 设置画布的宽高
options.width = window.innerWidth * options.scale;
options.height = window.innerHeight * options.scale;
for (let i = 0; i < options.STAR_NUMBER; i++) {
stars.push({
x: Math.random() * options.width,
y: Math.random() * options.height,
z: options.STAR_MIN_SCALE + Math.random() * (1 - options.STAR_MIN_SCALE),
});
}
}
// 绘制星星
function starDrawing() {
for (let star of stars) {
// 设置星星样式
// context!.beginPath();
context!.lineCap = "round";
context!.lineWidth = options.STAR_SIZE * star.z * options.scale;
context!.globalAlpha = 0.3 + 0.7 * Math.random();
context!.strokeStyle = options.STAR_COLOR;
// 绘制星星路径
context!.beginPath();
context!.moveTo(star.x, star.y);
let tailX = velocity.x * 2,
tailY = velocity.y * 2;
// 计算星星的尾巴坐标
if (Math.abs(tailX) < 0.5) tailX = 0.5;
if (Math.abs(tailY) < 0.5) tailY = 0.5;
// 绘制星星的尾巴
context!.lineTo(star.x + tailX, star.y + tailY);
context!.stroke();
}
}
// 更新星星位置和速度
function starUpdate() {
// 移动速度
velocity.tx *= 0.96;
velocity.ty *= 0.96;
velocity.x += (velocity.tx - velocity.x) * 0.8;
velocity.y += (velocity.ty - velocity.y) * 0.8;
for (let star of stars) {
// 根据速度和缩放比例更新星星的位置
star.x += velocity.x * star.z;
star.y += velocity.y * star.z;
// 使星星围绕屏幕中心旋转
star.x += (star.x - options.width / 2) * velocity.z * star.z;
star.y += (star.y - options.height / 2) * velocity.z * star.z;
// 更新星星的缩放比例
star.z += velocity.z;
// 如果星星超出屏幕范围,则重新放置到屏幕上
if (
star.x < -options.OVERFLOW_THRESHOLD ||
star.x > options.width + options.OVERFLOW_THRESHOLD ||
star.y < -options.OVERFLOW_THRESHOLD ||
star.y > options.height + options.OVERFLOW_THRESHOLD
) {
recycleStar(star);
}
}
}
// 回收星星并重新放到新的位置
function recycleStar(star: STAR) {
let direction = "Z", // 初始化方向
vx = Math.abs(velocity.x),
vy = Math.abs(velocity.y);
// 如果速度大于1,则根据速度的大小随机确定方向
if (vx > 1 || vy > 1) {
let axis;
// 如果水平速度大于垂直速度,则根据水平速度的比例随机确定水平或垂直方向
if (vx > vy) axis = Math.random() < vx / (vx + vy) ? "h" : "v";
else axis = Math.random() < vy / (vx + vy) ? "v" : "h";
// 根据方向确定具体的移动方向
if (axis == "h") direction = velocity.x > 0 ? "L" : "R";
else direction = velocity.y > 0 ? "T" : "B";
}
// 随机设置星星的缩放比例
star.z = Math.random() + (1 - Math.random()) * options.STAR_MIN_SCALE;
switch (direction) {
// 星星放置在屏幕中心
case "Z":
star.z = 0.1;
star.x = options.width * Math.random();
star.y = options.height * Math.random();
break;
// 星星放置在屏幕左侧
case "L":
star.x = -options.OVERFLOW_THRESHOLD;
star.y = options.height * Math.random();
break;
// 星星放置在屏幕右侧
case "R":
star.x = options.width + options.OVERFLOW_THRESHOLD;
star.y = options.height * Math.random();
break;
// 星星放置在屏幕顶部
case "T":
star.x = Math.random() * options.width;
star.y = -options.OVERFLOW_THRESHOLD;
break;
// 星星放置在屏幕底部
case "B":
star.x = Math.random() * options.width;
star.y = options.OVERFLOW_THRESHOLD + options.height;
break;
}
}
function mousePoint(x: number, y: number) {
// 如果之前有记录鼠标指针的位置,则计算鼠标指针的移动距离,并更新速度
if (options.mouseX && options.mouseY) {
let ox = x - options.mouseX,
oy = y - options.mouseY;
velocity.tx = velocity.tx + (ox / 8) * options.scale * (options.touchInput ? 1 : -1);
velocity.ty = velocity.ty + (oy / 8) * options.scale * (options.touchInput ? 1 : -1);
}
options.mouseX = x;
options.mouseY = y;
}
function mouseMove(e: MouseEvent) {
options.touchInput = false;
mousePoint(e.clientX, e.clientY);
}
function mouseLeave() {
options.mouseX = 0;
options.mouseY = 0;
}
function touchMove(e: TouchEvent) {
options.touchInput = true;
mousePoint(e.touches[0].clientX, e.touches[0].clientY);
e.preventDefault();
}
</script>
<template>
<div class="g-background">
<canvas ref="canvas" class="g-canvas"></canvas>
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.g-background {
position: fixed;
width: 100%;
height: 100%;
background: linear-gradient(-225deg, #231557 0%, #43107a 30%, #ff1361 100%);
// z-index: -999;
.g-canvas {
position: absolute;
width: 100%;
height: 100%;
z-index: inherit;
}
}
</style>