文章类型: 原创阅读时长: 1小时
文章地址:
文章在 7/29/2025 修改过
博客头部组件开发
最近写博客头部组件时,遇到了一些问题。
1、使用图片作为头部的话,有些单调
2、使用纯色背景,不太好看
3、使用3D渲染框架 + 粒子背景:技术难度高,性能要求也高
偶然间想到了B站的头部组件
B站的头部组件大概分为春夏秋冬四个季节,每个季节都会去替换不同的图片。B站的头部在鼠标划上去的时候,还会左右移动,交互性也挺舒适的。于是我默默按下了F12。
技术点解析
1、B站的头部大概有 10 多张图片和一个视频组成
2、图片和视频应该是由专业的美术把图给切下来了,几乎每张图片都是把一段内容扣下来,其他地方补齐透明色,这样的好处就是前端拿到直接放上去就好了,不用调位置,只有一两张很小的图需要特殊处理位置。
3、切好的图片按照一定的顺序堆叠在一一起后,每张图片增加一个鼠标方向的位移动画就可以了。
难点
1、由于增加了图片唯一,所以图片的边可能会在屏幕内部,就会有一部分的缝隙,B站我不知道如何解决的,我是采用了缩放,把图片大小 * 1.1 后,图片的边框移动也不会跑到视口内。
2、计算位移,我的做法是当用户鼠标进入图片容器时,记录初始位置,然后根据鼠标移动后和初始位置的距离来计算图片需要移动的位移。
3、鼠标移出后,需要恢复图片的位置,不然下次鼠标进入就会重新计算位置,导致图片位置闪动。这里在图片位置恢复时,加入一个过渡效果,就能慢慢的让图片回去。
代码
文件图片在博客源码的 assets/bilibili 下
<template>
<header ref="headerRef">
<div><img src="../assets//bilibili/banner.webp" /></div>
<div><img src="../assets//bilibili/cloud.webp" /></div>
<div><img src="../assets//bilibili/banner1.webp" /></div>
<div><img src="../assets//bilibili/banner3.webp" /></div>
<div><img src="../assets//bilibili/banner4.webp" /></div>
<div><img src="../assets//bilibili/banner5.webp" /></div>
<div><img src="../assets//bilibili/banner7.webp" /></div>
<div><img src="../assets//bilibili/banner8.webp" /></div>
<div><img class="car" src="../assets//bilibili/car.webp" /></div>
<div><img class="person" src="../assets//bilibili/characterSmall.webp" /></div>
<div><img src="../assets//bilibili/characterBig.webp" /></div>
<div><img src="../assets//bilibili/fence.webp" /></div>
<div><img src="../assets//bilibili/leftBottomGrass.webp" /></div>
<div><img src="../assets//bilibili/leftTopGrass.webp" /></div>
<div><img src="../assets//bilibili/rabbit.webp" /></div>
<div><img src="../assets//bilibili/banner2.webp" /></div>
<div><img src="../assets//bilibili/banner6.webp" /></div>
<div>
<video loop autoplay muted playsinline>
<source src="../assets/bilibili/video.webm" type="video/webm" />
您的浏览器不支持 video 标签。
</video>
</div>
<div>
<h1 class="title leading-tight font-bold mb-6">{{ title }}</h1>
</div>
</header>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
defineProps({
title: {
type: String,
default: '',
},
});
const headerRef = ref<HTMLElement | null>(null);
const startX = ref(0);
const handleMouseEnter = (e: MouseEvent) => {
startX.value = e.clientX; // 计算开始位移
const images = document.querySelectorAll<HTMLElement>('header > div > img');
images.forEach(image => {
// 移除在鼠标离开后添加的恢复图片原始位置的过渡
image.classList.remove('smooth-transition');
});
};
const handleMouseMove = (e: MouseEvent) => {
const images = document.querySelectorAll<HTMLElement>('header > div > img');
const percentage = (e.clientX - startX.value) / window.outerWidth; // 计算位移百分比
let xOffset = percentage,
yOffset = percentage;
images.forEach(image => {
xOffset *= 1.3;
yOffset *= 1.1;
// 设置元素的位移
image.style.setProperty('--xOffset', `${xOffset}px`);
image.style.setProperty('--yOffset', `${yOffset}px`);
image.style.setProperty('--personXoffset', `${-250 + xOffset}px`);
image.style.setProperty('--personYoffset', `${-30 + yOffset}px`);
image.style.setProperty('--carXoffset', `${-100 + xOffset}px`);
image.style.setProperty('--carYoffset', `${20 + yOffset}px`);
});
};
const handleMouseLeave = () => {
const images = document.querySelectorAll<HTMLElement>('header > div > img');
images.forEach(image => {
image.classList.add('smooth-transition');
});
images.forEach(image => {
image.style.setProperty('--xOffset', `${0}px`);
image.style.setProperty('--yOffset', `${0}px`);
image.style.setProperty('--personXoffset', `${-250}px`);
image.style.setProperty('--personYoffset', `${-30}px`);
image.style.setProperty('--carXoffset', `${-100}px`);
image.style.setProperty('--carYoffset', `${20}px`);
});
};
onMounted(() => {
if (headerRef.value) {
headerRef.value.addEventListener('mouseenter', handleMouseEnter);
headerRef.value.addEventListener('mousemove', handleMouseMove);
headerRef.value.addEventListener('mouseleave', handleMouseLeave);
}
});
onBeforeUnmount(() => {
if (headerRef.value) {
headerRef.value.removeEventListener('mouseenter', handleMouseEnter);
headerRef.value.removeEventListener('mousemove', handleMouseMove);
headerRef.value.removeEventListener('mouseleave', handleMouseLeave);
}
});
</script>
<style lang="scss" scoped>
header {
height: 155px;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 40%; // 遮罩高度可调整
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent);
pointer-events: none; // 确保不影响鼠标事件
z-index: 10; // 确保遮罩在最上层
}
}
.title {
color: #fff;
letter-spacing: 3px;
}
header > div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
--xOffset: 0px;
--yOffset: 0px;
--personXoffset: -250px;
--personYoffset: -30px;
--carXoffset: -100px;
--carYoffset: 20px;
}
header > div > img,
header > div > video {
display: block;
object-fit: cover;
height: 100%;
width: 100%;
transform: translate(var(--xOffset), var(--yOffset)) scale(1.1);
}
.smooth-transition {
transition: transform 0.3s ease-out !important;
}
.person {
width: 75px;
height: 60px;
transform: translate(var(--personXoffset), var(--personYoffset)) !important;
}
.car {
transform: translate(var(--carXoffset), var(--carYoffset)) !important;
}
</style>
Tags
Recomends
Comments
登录
暂无评论,快来抢沙发吧!