上班摸鱼时间使用vue3实现哔哩哔哩滚动视差banner
效果:
20240911
代码解释
模板部分
<template>
<div>
<h1>Home Page</h1>
<p>Go to <router-link to="/about">About</router-link></p>
<div class="animated-banner animated-element" ref="animatedBanner">
<!-- Multiple layers of images and one video -->
<div class="layer">
<img src="../assets/1.webp" data-height="187" data-width="2000" height="187" width="2000" />
</div>
<!-- ...more layers... -->
<div class="layer">
<video loop muted src="../assets/1.webm" autoplay width="180" height="100" data-height="100" data-width="180"></video>
</div>
<!-- ...more layers... -->
</div>
</div>
</template>
<div class="layer">
:动画每一层的包装器。这包括多个<img>
和一个<video>
标签,其中的图片可以从bilibili
使用f12
即可拿到如:
脚本部分
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, reactive } from "vue";
const animatedBanner = ref<HTMLDivElement | null>(null);
const bannerLeft = ref<number>(0);
const bannerWidth = ref<number>(0);
let initMouseLeft = ref<number>(0);
const styleMap = reactive<any>({});
animatedBanner
:对元素的引用.animated-banner
。
bannerLeft
:横幅的初始水平偏移(用于计算鼠标移动)。
bannerWidth
:横幅的宽度。
initMouseLeft
:鼠标进入Banner
区域时的初始鼠标X
位置。
styleMap
:将每个层映射到其样式和初始状态的反应对象。
生命周期钩子
onMounted(() => {
styleMap.value = {
0: { /* Styles for layer 1 */ },
// More layers...
23: { /* Styles for layer 24 */ },
};
if (animatedBanner?.value) {
bannerLeft.value = animatedBanner.value.offsetLeft;
bannerWidth.value = animatedBanner.value.offsetWidth;
} else {
bannerLeft.value = 0;
bannerWidth.value = 0;
}
init();
if (animatedBanner.value) {
animatedBanner.value.addEventListener("mouseenter", handleMouseEnter);
animatedBanner.value.addEventListener("mouseleave", handleMouseLeave);
}
});
onUnmounted(() => {
if (animatedBanner.value) {
animatedBanner.value.removeEventListener("mouseenter", handleMouseEnter);
animatedBanner.value.removeEventListener("mouseleave", handleMouseLeave);
}
});
onMounted
:安装后初始化组件。
styleMap.value
:设置各层的样式和初始配置。
bannerLeft和bannerWidth
:用横幅的当前位置和宽度进行初始化。
init()
:将初始样式应用至各个图层。添加鼠标进入和离开事件的事件监听器。
onUnmounted
:当组件被销毁时清理事件监听器。
方法
init()
:根据 将初始样式应用于每个图层styleMap
。
const init = () => {
Object.keys(styleMap.value).forEach((item) => {
const current = styleMap.value[item];
const initStyle = current.initialStyle;
current.element.style = `height: ${initStyle.height}; width: ${initStyle.width}; transform: translate(${initStyle.translateX}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale}); opacity: ${initStyle.opacity}; object-fit: ${initStyle.objectFit};`;
});
};
handleMouseLeave(event)
:停止事件传播并调用clearListener()
重置样式。
const handleMouseLeave = (event: any) => {
event.stopPropagation();
clearListener();
};
handleMouseEnter(event)
:停止事件传播,设置初始鼠标位置,并开始监听鼠标移动。
const handleMouseEnter = (event: any) => {
event.stopPropagation();
initMouseLeft.value = event.pageX - bannerLeft.value;
startListener();
};
calcutedPosition(mouseLeft, scale)
:根据鼠标位置和比例计算位置偏移。
const calcutedPosition = (mouseLeft: number, scale: number) => {
return (-(mouseLeft - initMouseLeft.value) * scale) / bannerWidth.value;
};
clearListener()
:鼠标离开时重置各图层的样式。动画显示样式变化并恢复到初始状态。
const clearListener = () => {
const mouseLeft = event.pageX - bannerLeft.value;
Object.keys(styleMap.value).forEach((item) => {
const current = styleMap.value[item];
const style = current.style;
const initStyle = current.initialStyle;
const element = current.element;
if (current.style) {
const offset = calcutedPosition(mouseLeft, style.scale);
let startValue = offset;
let endValue = 0;
let duration = 500;
let interval = 10;
let steps = duration / interval;
let stepValue = (startValue - endValue) / steps;
let currentValue = startValue;
let timer = setInterval(() => {
currentValue -= stepValue;
let styleResult = `height: ${initStyle.height}; width: ${initStyle.width}; opacity: ${initStyle.opacity}; object-fit: ${initStyle.objectFit};`;
if (style.direction === "y") {
styleResult += `transform: translate(${initStyle.translateX}px, ${initStyle.translateY - currentValue}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
} else {
styleResult += `transform: translate(${initStyle.translateX - currentValue}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
}
if (Math.abs(currentValue - endValue) < Math.abs(stepValue)) {
clearInterval(timer);
styleResult = `transform: translate(${initStyle.translateX}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
}
element.style = styleResult;
}, interval);
}
});
};
startListener()
:mousemove
为横幅添加事件监听器,根据鼠标移动来更新各图层的样式。
const startListener = () => {
animatedBanner.value?.addEventListener(
"mousemove",
function (event: { stopPropagation: () => void; pageX: number }) {
event.stopPropagation();
const mouseLeft = event.pageX - bannerLeft.value;
Object.keys(styleMap.value).forEach((item) => {
const current = styleMap.value[item];
if (current.style) {
const initStyle = current.initialStyle;
const style = current.style;
const element = current.element;
const offset = calcutedPosition(mouseLeft, style.scale);
let styleResult = `height: ${initStyle.height}; width: ${initStyle.width}; opacity: ${initStyle.opacity}; object-fit: ${initStyle.objectFit};`;
if (style.direction === "y") {
styleResult += `transform: translate(${initStyle.translateX}px, ${initStyle.translateY - offset}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
} else {
styleResult += `transform: translate(${initStyle.translateX - offset}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
}
element.style = styleResult;
}
});
}
);
};
进行图片样式重叠
<style scope>
body {
margin: 0;
padding: 0;
position: relative;
}
.animated-banner {
position: absolute;
top: 150px;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
min-width: 1000px;
min-height: 155px;
height: 9.375vw;
}
.animated-banner > .layer {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.animated-element {
transition: transform 2s ease, opacity 0.5s ease;
}
</style>
全部代码
<template>
<div>
<h1>Home Page</h1>
<p>Go to <router-link to="/about">About</router-link></p>
<div class="animated-banner animated-element" ref="animatedBanner">
<div class="layer">
<img
src="../assets/1.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/2.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/3.webp"
data-height="187"
data-width="2000"
height="224"
width="2400"
/>
</div>
<div class="layer">
<img
src="../assets/4.webp"
data-height="187"
data-width="2000"
height="205"
width="2200"
/>
</div>
<div class="layer">
<img
src="../assets/5.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/6.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/7.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/8.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/9.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/10.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/11.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/12.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/13.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/14.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/15.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/16.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/17.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/18.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/19.webp"
data-height="187"
data-width="2000"
height="168"
width="1800"
/>
</div>
<div class="layer">
<img
src="../assets/20.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<img
src="../assets/21.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
<div class="layer">
<video
loop
muted
src="../assets/1.webm"
autoplay
width="180"
height="100"
data-height="100"
data-width="180"
></video>
</div>
<div class="layer">
<img
src="../assets/22.webp"
data-height="187"
data-width="2000"
height="205"
width="2200"
/>
</div>
<div class="layer">
<img
src="../assets/23.webp"
data-height="187"
data-width="2000"
height="187"
width="2000"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, reactive } from "vue";
const animatedBanner = ref<HTMLDivElement | null>(null);
const bannerLeft = ref<number>(0);
const bannerWidth = ref<number>(0);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let initMouseLeft = ref<number>(0);
const styleMap = reactive<any>({});
onMounted(() => {
styleMap.value = {
0: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: null,
element: animatedBanner.value
?.querySelector(".layer:nth-child(1)")
?.querySelector("img"),
},
1: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "y",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(2)")
?.querySelector("img"),
},
2: {
initialStyle: {
height: "224.4px",
width: "2400px",
translateX: 300,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: null,
element: animatedBanner.value
?.querySelector(".layer:nth-child(3)")
?.querySelector("img"),
},
3: {
initialStyle: {
height: "205.7px",
width: "2200px",
translateX: 330,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 50,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(4)")
?.querySelector("img"),
},
4: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: null,
element: animatedBanner.value
?.querySelector(".layer:nth-child(5)")
?.querySelector("img"),
},
5: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(6)")
?.querySelector("img"),
},
6: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(7)")
?.querySelector("img"),
},
7: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 2,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(8)")
?.querySelector("img"),
},
8: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(9)")
?.querySelector("img"),
},
9: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: null,
element: animatedBanner.value
?.querySelector(".layer:nth-child(10)")
?.querySelector("img"),
},
10: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 50,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(11)")
?.querySelector("img"),
},
11: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(12)")
?.querySelector("img"),
},
12: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 30,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(13)")
?.querySelector("img"),
},
13: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 30,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(14)")
?.querySelector("img"),
},
14: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: null,
element: animatedBanner.value
?.querySelector(".layer:nth-child(15)")
?.querySelector("img"),
},
15: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 20,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(16)")
?.querySelector("img"),
},
16: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(17)")
?.querySelector("img"),
},
17: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: -100,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 20,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(18)")
?.querySelector("img"),
},
18: {
initialStyle: {
height: "168.3px",
width: "1800px",
translateX: -90,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 400,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(19)")
?.querySelector("img"),
},
19: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(20)")
?.querySelector("img"),
},
20: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 10,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(21)")
?.querySelector("img"),
},
21: {
initialStyle: {
height: "100px",
width: "180px",
translateX: -245,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
objectFit: "cover",
},
style: null,
element: animatedBanner.value
?.querySelector(".layer:nth-child(22)")
?.querySelector("video"),
},
22: {
initialStyle: {
height: "205.7px",
width: "2200px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 200,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(23)")
?.querySelector("img"),
},
23: {
initialStyle: {
height: "187px",
width: "2000px",
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
},
style: {
direction: "x",
scale: 200,
},
element: animatedBanner.value
?.querySelector(".layer:nth-child(24)")
?.querySelector("img"),
},
};
if (animatedBanner?.value) {
bannerLeft.value = animatedBanner.value.offsetLeft;
} else {
// 设置一个默认值,例如 0
bannerLeft.value = 0;
}
if (animatedBanner?.value) {
bannerWidth.value = animatedBanner?.value?.offsetWidth;
} else {
// 设置一个默认值,例如 0
bannerWidth.value = 0;
}
init();
if (animatedBanner.value) {
animatedBanner.value.addEventListener("mouseenter", handleMouseEnter);
animatedBanner.value.addEventListener("mouseleave", handleMouseLeave);
}
});
onUnmounted(() => {
if (animatedBanner.value) {
animatedBanner.value.removeEventListener("mouseenter", handleMouseEnter);
animatedBanner.value.removeEventListener("mouseleave", handleMouseLeave);
}
});
const init = () => {
Object.keys(styleMap.value).forEach((item) => {
const current = styleMap.value[item];
const initStyle = current.initialStyle;
current.element.style = `height: ${initStyle.height}; width: ${initStyle.width}; transform: translate(${initStyle.translateX}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale}); opacity: ${initStyle.opacity}; object-fit: ${initStyle.objectFit}`;
});
};
const handleMouseLeave = (event: any) => {
event.stopPropagation();
// 还原
clearListener();
};
const handleMouseEnter = (event: any) => {
event.stopPropagation();
// 计算初始鼠标 x 位置
initMouseLeft.value = event.pageX - bannerLeft.value;
// 开始监听偏移量
startListener();
};
const calcutedPosition = (mouseLeft: number, scale: number) => {
return (-(mouseLeft - initMouseLeft.value) * scale) / bannerWidth.value;
};
const clearListener = () => {
const mouseLeft = event.pageX - bannerLeft.value;
// // 确保样式清除或复位
// if (animatedBanner.value) {
// animatedBanner.value.style.transform = `translate(0, 0) rotate(0deg) scale(1)`;
// animatedBanner.value.style.opacity = '1';
// }
Object.keys(styleMap.value).forEach((item) => {
const current = styleMap.value[item];
const style = current.style;
const initStyle = current.initialStyle;
const element = current.element;
// if (current.element) {
// current.element.style.transform = `translate(${initStyle.translateX}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale})`;
// current.element.style.opacity = initStyle.opacity;
// }
if (current.style) {
// 计算偏移
const offset = calcutedPosition(mouseLeft, style.scale);
let startValue = offset;
let endValue = 0;
let duration = 500; // 总时间,单位为毫秒
let interval = 10; // 每次更新的间隔时间,单位为毫秒
let steps = duration / interval; // 总步数
let stepValue = (startValue - endValue) / steps; // 每一步的值变化量
let currentValue = startValue;
let timer = setInterval(() => {
currentValue -= stepValue;
let styleResult = `height: ${initStyle.height}; width: ${initStyle.width}; opacity: ${initStyle.opacity}; object-fit: ${initStyle.objectFit};`;
if (style.direction === "y") {
styleResult += `transform: translate(${initStyle.translateX}px, ${
initStyle.translateY - currentValue
}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
} else {
styleResult += `transform: translate(${
initStyle.translateX - currentValue
}px, ${initStyle.translateY}px) rotate(${
initStyle.rotate
}deg) scale(${initStyle.scale});`;
}
if (Math.abs(currentValue - endValue) < Math.abs(stepValue)) {
clearInterval(timer);
styleResult = `transform: translate(${initStyle.translateX}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
}
element.style = styleResult;
}, interval);
}
});
};
const startListener = () => {
animatedBanner.value?.addEventListener(
"mousemove",
function (event: { stopPropagation: () => void; pageX: number }) {
event.stopPropagation();
const mouseLeft = event.pageX - bannerLeft.value;
Object.keys(styleMap.value).forEach((item) => {
const current = styleMap.value[item];
if (current.style) {
const initStyle = current.initialStyle;
const style = current.style;
const element = current.element;
// 计算偏移
const offset = calcutedPosition(mouseLeft, style.scale);
let styleResult = `height: ${initStyle.height}; width: ${initStyle.width}; opacity: ${initStyle.opacity}; object-fit: ${initStyle.objectFit};`;
if (style.direction === "y") {
styleResult += `transform: translate(${initStyle.translateX}px, ${
initStyle.translateY - offset
}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
} else {
styleResult += `transform: translate(${
initStyle.translateX - offset
}px, ${initStyle.translateY}px) rotate(${
initStyle.rotate
}deg) scale(${initStyle.scale});`;
}
element.style = styleResult;
}
});
}
);
};
</script>
<style scope>
body {
margin: 0;
padding: 0;
position: relative;
}
.animated-banner {
position: absolute;
top: 150px;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
min-width: 1000px;
min-height: 155px;
height: 9.375vw;
}
.animated-banner > .layer {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.animated-element {
transition: transform 2s ease, opacity 0.5s ease;
}
</style>