Vue3 實(shí)現(xiàn)最近很火的酷炫功能:卡片懸浮發(fā)光
有趣的動畫效果
前幾天在網(wǎng)上看到了一個(gè)很有趣的動畫效果,如下,光會跟隨鼠標(biāo)在卡片上進(jìn)行移動,并且卡片會有視差的效果。
那么在 Vue3 中應(yīng)該如何去實(shí)現(xiàn)這個(gè)效果呢?
圖片
基本實(shí)現(xiàn)思路
其實(shí)實(shí)現(xiàn)思路很簡單,無非就是分幾步:
- 首先,卡片是相對定位,光是絕對定位
- 監(jiān)聽卡片的鼠標(biāo)移入事件mouseenter,當(dāng)鼠標(biāo)進(jìn)入時(shí)顯示光
- 監(jiān)聽卡片的鼠標(biāo)移動事件mouseover,鼠標(biāo)移動時(shí)修改光的left、top,讓光跟隨鼠標(biāo)移動
- 監(jiān)聽卡片的鼠標(biāo)移出事件mouseleave,鼠標(biāo)移出時(shí),隱藏光
我們先在 Index.vue 中準(zhǔn)備一個(gè)卡片頁面,光的CSS效果可以使用filter: blur() 來實(shí)現(xiàn)。
圖片
可以看到現(xiàn)在的效果是這樣:
圖片
實(shí)現(xiàn)光源跟隨鼠標(biāo)
在實(shí)現(xiàn)之前我們需要注意幾點(diǎn):
1、鼠標(biāo)移入時(shí)需要設(shè)置卡片 overflow: hidden,否則光會溢出,而鼠標(biāo)移出時(shí)記得還原。
2、獲取鼠標(biāo)坐標(biāo)時(shí)需要用clientX/Y而不是pageX/Y,因?yàn)榍罢邥秧撁鏉L動距離也算進(jìn)去,比較嚴(yán)謹(jǐn)。
剛剛說到實(shí)現(xiàn)思路時(shí)我們說到了mouseenter、mousemove、mouseleave,其實(shí)mouseenter、mouseleave 這二者的邏輯比較簡單,重點(diǎn)是 mouseover 這個(gè)監(jiān)聽函數(shù)。
而在 mouseover 這個(gè)函數(shù)中,最重要的邏輯就是:光怎么跟隨鼠標(biāo)移動呢?
或者也可以這么說:怎么計(jì)算光相對于卡片盒子的 left 和 top。
對此我專門畫了一張圖,相信大家一看就懂怎么算了。
圖片
- left = clientX - x - width/2
- height = clientY - y - height/2
知道了怎么計(jì)算,那么邏輯的實(shí)現(xiàn)也很明了了~封裝一個(gè)use-light-card.ts:
圖片
接著在頁面中去使用:
圖片
這樣就能實(shí)現(xiàn)基本的效果啦~
圖片
卡片視差效果
卡片的視差效果需要用到樣式中 transform 樣式,主要是配置四個(gè)東西:
- perspective:定義元素在 3D 變換時(shí)的透視效果
- rotateX:X 軸旋轉(zhuǎn)角度
- rotateY:Y 軸旋轉(zhuǎn)角度
- scale3d:X/Y/Z 軸上的縮放比例
圖片
現(xiàn)在就有了卡片視差的效果啦~
圖片
給所有卡片添加光源
上面只是給一個(gè)卡片增加光源,接下來可以給每一個(gè)卡片都增加光源啦!?。?/p>
圖片
圖片
讓光源變成可配置
上面的代碼,總感覺這個(gè) hooks 耦合度太高不太通用,所以我們可以讓光源變成可配置化,這樣每個(gè)卡片就可以展示不同大小、顏色的光源了~像下面一樣。
圖片
既然是配置化,那我們希望是這么去使用 hooks 的,我們并不需要自己在頁面中去寫光源的dom節(jié)點(diǎn),也不需要自己去寫光源的樣式,而是通過配置傳入 hooks 中。
所以 hooks 內(nèi)部要自己通過操作 DOM 的方式,去添加、刪除光源,可以使用createElement、appendChild、removeChild 去做這些事~。
圖片
完整源碼
<!-- Index.vue -->
<template>
<div class="container">
<!-- 方塊盒子 -->
<div class="item" ref="cardRef1"></div>
<!-- 方塊盒子 -->
<div class="item" ref="cardRef2"></div>
<!-- 方塊盒子 -->
<div class="item" ref="cardRef3"></div>
</div>
</template>
<script setup lang="ts">
import { useLightCard } from './use-light-card';
const { cardRef: cardRef1 } = useLightCard();
const { cardRef: cardRef2 } = useLightCard({
light: {
color: '#ffffff',
width: 100,
},
});
const { cardRef: cardRef3 } = useLightCard({
light: {
color: 'yellow',
},
});
</script>
<style scoped lang="less">
.container {
background: black;
width: 100%;
height: 100%;
padding: 200px;
display: flex;
justify-content: space-between;
.item {
position: relative;
width: 125px;
height: 125px;
background: #1c1c1f;
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
</style>
// use-light-card.ts
import { onMounted, onUnmounted, ref } from 'vue';
interface IOptions {
light?: {
width?: number; // 寬
height?: number; // 高
color?: string; // 顏色
blur?: number; // filter: blur()
};
}
export const useLightCard = (option: IOptions = {}) => {
// 獲取卡片的dom節(jié)點(diǎn)
const cardRef = ref<HTMLDivElement | null>(null);
let cardOverflow = '';
// 光的dom節(jié)點(diǎn)
const lightRef = ref<HTMLDivElement>(document.createElement('div'));
// 設(shè)置光源的樣式
const setLightStyle = () => {
const { width = 60, height = 60, color = '#ff4132', blur = 40 } = option.light ?? {};
const lightDom = lightRef.value;
lightDom.style.position = 'absolute';
lightDom.style.width = `${width}px`;
lightDom.style.height = `${height}px`;
lightDom.style.background = color;
lightDom.style.filter = `blur(${blur}px)`;
};
// 設(shè)置卡片的 overflow 為 hidden
const setCardOverflowHidden = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardOverflow = cardDom.style.overflow;
cardDom.style.overflow = 'hidden';
}
};
// 還原卡片的 overflow
const restoreCardOverflow = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.style.overflow = cardOverflow;
}
};
// 往卡片添加光源
const addLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.appendChild(lightRef.value);
}
};
// 刪除光源
const removeLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.removeChild(lightRef.value);
}
};
// 監(jiān)聽卡片的鼠標(biāo)移入
const onMouseEnter = () => {
// 添加光源
addLight();
setCardOverflowHidden();
};
// use-light-card.ts
// 監(jiān)聽卡片的鼠標(biāo)移動
const onMouseMove = (e: MouseEvent) => {
// 獲取鼠標(biāo)的坐標(biāo)
const { clientX, clientY } = e;
// 讓光跟隨鼠標(biāo)
const cardDom = cardRef.value;
const lightDom = lightRef.value;
if (cardDom) {
// 獲取卡片相對于窗口的x和y坐標(biāo)
const { x, y } = cardDom.getBoundingClientRect();
// 獲取光的寬高
const { width, height } = lightDom.getBoundingClientRect();
lightDom.style.left = `${clientX - x - width / 2}px`;
lightDom.style.top = `${clientY - y - height / 2}px`;
// 設(shè)置動畫效果
const maxXRotation = 10; // X 軸旋轉(zhuǎn)角度
const maxYRotation = 10; // Y 軸旋轉(zhuǎn)角度
const rangeX = 200 / 2; // X 軸旋轉(zhuǎn)的范圍
const rangeY = 200 / 2; // Y 軸旋轉(zhuǎn)的范圍
const rotateX = ((clientX - x - rangeY) / rangeY) * maxXRotation; // 根據(jù)鼠標(biāo)在 Y 軸上的位置計(jì)算繞 X 軸的旋轉(zhuǎn)角度
const rotateY = -1 * ((clientY - y - rangeX) / rangeX) * maxYRotation; // 根據(jù)鼠標(biāo)在 X 軸上的位置計(jì)算繞 Y 軸的旋轉(zhuǎn)角度
cardDom.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; //設(shè)置 3D 透視
}
};
// 監(jiān)聽卡片鼠標(biāo)移出
const onMouseLeave = () => {
// 鼠標(biāo)離開移出光源
removeLight();
restoreCardOverflow();
};
onMounted(() => {
// 設(shè)置光源樣式
setLightStyle();
// 綁定事件
cardRef.value?.addEventListener('mouseenter', onMouseEnter);
cardRef.value?.addEventListener('mousemove', onMouseMove);
cardRef.value?.addEventListener('mouseleave', onMouseLeave);
});
onUnmounted(() => {
// 解綁事件
cardRef.value?.removeEventListener('mouseenter', onMouseEnter);
cardRef.value?.removeEventListener('mousemove', onMouseMove);
cardRef.value?.removeEventListener('mouseleave', onMouseLeave);
});
return {
cardRef,
};
};