基于Vue開(kāi)發(fā)一個(gè)很火的卡片動(dòng)畫(huà)效果
扣子這個(gè) AI 平臺(tái),可以看到它首頁(yè)的卡片效果還是很酷炫的;大致包含兩個(gè)效果,光的跟隨效果還有卡片傾斜像 3D 的效果

github 在你沒(méi)有登錄的時(shí)候,首頁(yè)也有這樣一個(gè)卡片效果

我們也來(lái)實(shí)現(xiàn)一下,寫(xiě)一個(gè)這樣的效果

先準(zhǔn)備三個(gè)盒子
這里用的react組件和tailwind來(lái)寫(xiě)樣式,盒子不管你怎么寫(xiě),但是得有 relative 定位,因?yàn)楣庑枰枚ㄎ粊?lái)跟隨
export default function Home() {
return (
<main className="min-h-screen p-24 flex justify-center items-center bg-black gap-5">
<div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F]"></div>
<div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F]"></div>
<div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F]"></div>
</main>
)
}

實(shí)現(xiàn)光源跟隨效果
1、需要監(jiān)聽(tīng)盒子上的 mouseMove 事件和 mouseLeave 事件,進(jìn)入的時(shí)候顯示光源并計(jì)算隨鼠標(biāo)滾動(dòng)的位置
2、需要注意光源不能擋住元素上面的位置,所以要設(shè)置低一點(diǎn)的層級(jí)
3、光源的模糊效果可以用filter:blur(100px)實(shí)現(xiàn)
'use client'
import { useRef, useState } from 'react'
export default function Home() {
const cardRef = (useRef < HTMLDivElement) | (null > null) //卡片
const lightRef = (useRef < HTMLDivElement) | (null > null) //光源
const [isShowLight, setIsShowLight] = useState(false) //是否顯示光源
// 光源隨鼠標(biāo)移動(dòng)
const [pos, setPos] = useState({
left: '0px',
top: '0px'
})
return (
<main className="h-screen p-24 flex justify-center items-center bg-black gap-5">
<div
className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative overflow-hidden"
onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
if (cardRef.current) {
// 進(jìn)入盒子顯示光源
setIsShowLight(true)
// 父元素相對(duì)于頁(yè)面窗口
const { x, y } = cardRef.current.getBoundingClientRect()
// 鼠標(biāo)在頁(yè)面位置
const { clientX, clientY } = e
//光源隨鼠標(biāo)移動(dòng)
setPos({
left: clientX - x - 100 + 'px', // 100為光源寬度的1/2
top: clientY - y - 100 + 'px' // 100為光源高度的1/2
})
}
}}
onMouseLeave={() => {
// 離開(kāi)盒子隱藏光源
setIsShowLight(false)
}}
ref={cardRef}
>
<div
className={`${
isShowLight ? '' : 'hidden'
} absolute h-[200px] w-[200px] rounded-full bg-[#ff4132] blur-[150px] filter`}
ref={lightRef}
style={pos}
></div>
</div>
<div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative"></div>
<div className="w-[384px] h-[384px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative"></div>
</main>
)
}

實(shí)現(xiàn) 3D 卡片視差效果
1、主要是通過(guò)transform:'perspective(1000px) rotateX(10deg) rotateY(10deg) scale3d(1, 1, 1)'這個(gè)屬性實(shí)現(xiàn)
perspective(1000px): 這個(gè)函數(shù)定義了元素的透視效果。它接受一個(gè)參數(shù),表示視點(diǎn)(觀察者)與屏幕之間的距離。在這個(gè)例子中,透視距離被設(shè)置為 1000 像素,使得元素在進(jìn)行 3D 變換時(shí)產(chǎn)生透視效果。也就是偏移幅度。
rotateX(10deg): 這個(gè)函數(shù)定義了元素繞其 X 軸旋轉(zhuǎn)的角度。它接受一個(gè)參數(shù),表示旋轉(zhuǎn)的角度。在這個(gè)例子中,元素繞 X 軸順時(shí)針旋轉(zhuǎn)了 10 度。
rotateY(10deg): 這個(gè)函數(shù)定義了元素繞其 Y 軸旋轉(zhuǎn)的角度。它接受一個(gè)參數(shù),表示旋轉(zhuǎn)的角度。在這個(gè)例子中,元素繞 Y 軸順時(shí)針旋轉(zhuǎn)了 10 度。
scale3d(1, 1, 1): 這個(gè)函數(shù)定義了元素在三個(gè)軸上的縮放比例。它接受三個(gè)參數(shù),分別表示 X 軸、Y 軸和 Z 軸上的縮放比例。在這個(gè)例子中,元素在三個(gè)軸上的縮放比例都為 1,表示不進(jìn)行縮放。
import { useRef, useState } from 'react'
export default function Home() {
const cardRef = (useRef < HTMLDivElement) | (null > null) //卡片
const lightRef = (useRef < HTMLDivElement) | (null > null) //光源
const [isShowLight, setIsShowLight] = useState(false) //是否顯示光源
// 光源隨鼠標(biāo)移動(dòng)
const [pos, setPos] = useState({
left: '0px',
top: '0px'
})
return (
<main className="h-screen p-24 flex justify-center items-center bg-black gap-5">
<div
className="w-[400px] h-[400px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative overflow-hidden"
onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
if (cardRef.current) {
setIsShowLight(true) // 進(jìn)入盒子顯示光源
const { x, y } = cardRef.current.getBoundingClientRect() // 父元素相對(duì)于頁(yè)面窗口
const { clientX, clientY } = e // 鼠標(biāo)在頁(yè)面位置
const offsetX = clientX - x // 計(jì)算鼠標(biāo)在盒子內(nèi)的水平偏移量
const offsetY = clientY - y // 計(jì)算鼠標(biāo)在盒子內(nèi)的垂直偏移量
setPos({
left: offsetX - 100 + 'px', // 100為光源寬度的1/2
top: offsetY - 100 + 'px' // 100為光源高度的1/2
})
// 新增
const maxXRotation = 10 // 最大繞 X 軸旋轉(zhuǎn)角度
const maxYRotation = 10 // 最大繞 Y 軸旋轉(zhuǎn)角度
const rangeX = 400 / 2 // X 軸旋轉(zhuǎn)范圍
const rangeY = 400 / 2 // Y 軸旋轉(zhuǎn)范圍
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation // 根據(jù)鼠標(biāo)在 Y 軸上的位置計(jì)算繞 X 軸的旋轉(zhuǎn)角度
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation // 根據(jù)鼠標(biāo)在 X 軸上的位置計(jì)算繞 Y 軸的旋轉(zhuǎn)角度
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)` //設(shè)置3D透視
}
}}
onMouseLeave={() => {
// 離開(kāi)盒子隱藏光源
setIsShowLight(false)
}}
ref={cardRef}
style={{
willChange: 'transform',
transform:
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
}}
>
<div
className={`${
isShowLight ? '' : 'hidden'
} absolute h-[200px] w-[200px] rounded-full bg-[#ff4132] blur-[150px] filter`}
ref={lightRef}
style={pos}
></div>
</div>
<div className="w-[400px] h-[400px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative"></div>
<div className="w-[400px] h-[400px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative"></div>
</main>
)
}
封裝成好用的 Hook
聰明的你肯定看到了只實(shí)現(xiàn)了一個(gè)盒子,如果頁(yè)面很多盒子的時(shí)候怎么辦,所以還是要封裝個(gè)Hook來(lái)邏輯復(fù)用
然后統(tǒng)一的光源有點(diǎn)丑,所以設(shè)置個(gè)不同的光源
'use client'
import { useRef, useState, useEffect } from 'react'
const useCardAnimation = () => {
const cardRef = (useRef < HTMLDivElement) | (null > null) // 卡片
const lightRef = (useRef < HTMLDivElement) | (null > null) // 光源
const [isShowLight, setIsShowLight] = useState(false) // 是否顯示光源
const [pos, setPos] = useState({ left: '0px', top: '0px' }) // 光源位置
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (cardRef.current) {
setIsShowLight(true) // 進(jìn)入盒子顯示光源
const { x, y } = cardRef.current.getBoundingClientRect() // 父元素相對(duì)于頁(yè)面窗口
const { clientX, clientY } = e // 鼠標(biāo)在頁(yè)面位置
const offsetX = clientX - x // 計(jì)算鼠標(biāo)在盒子內(nèi)的水平偏移量
const offsetY = clientY - y // 計(jì)算鼠標(biāo)在盒子內(nèi)的垂直偏移量
setPos({
left: offsetX - 100 + 'px', // 100為光源寬度的1/2
top: offsetY - 100 + 'px' // 100為光源高度的1/2
})
const maxXRotation = 5 // 最大繞 X 軸旋轉(zhuǎn)角度
const maxYRotation = 5 // 最大繞 Y 軸旋轉(zhuǎn)角度
const rangeX = 400 / 2 // X 軸旋轉(zhuǎn)范圍
const rangeY = 400 / 2 // Y 軸旋轉(zhuǎn)范圍
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation // 根據(jù)鼠標(biāo)在 Y 軸上的位置計(jì)算繞 X 軸的旋轉(zhuǎn)角度
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation // 根據(jù)鼠標(biāo)在 X 軸上的位置計(jì)算繞 Y 軸的旋轉(zhuǎn)角度
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)` // 設(shè)置3D透視
}
}
const handleMouseLeave = () => {
setIsShowLight(false) // 離開(kāi)盒子隱藏光源
if (cardRef.current) {
cardRef.current.style.transform = `perspective(1000px) rotateX(0deg) rotateY(0deg)` // 設(shè)置3D透視
}
}
cardRef.current?.addEventListener('mousemove', handleMouseMove)
cardRef.current?.addEventListener('mouseleave', handleMouseLeave)
return () => {
cardRef.current?.removeEventListener('mousemove', handleMouseMove)
cardRef.current?.removeEventListener('mouseleave', handleMouseLeave)
}
}, [])
return { cardRef, lightRef, isShowLight, pos }
}
export default function Home() {
const {
cardRef: cardRef1,
lightRef: lightRef1,
isShowLight: isShowLight1,
pos: pos1
} = useCardAnimation()
const {
cardRef: cardRef2,
lightRef: lightRef2,
isShowLight: isShowLight2,
pos: pos2
} = useCardAnimation()
const {
cardRef: cardRef3,
lightRef: lightRef3,
isShowLight: isShowLight3,
pos: pos3
} = useCardAnimation()
return (
<main className="h-screen p-24 flex justify-center items-center bg-black gap-5">
<div
className="w-[400px] h-[400px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative overflow-hidden"
ref={cardRef1}
style={{
willChange: 'transform',
transform:
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
}}
>
<div
className={`${
isShowLight1 ? '' : 'hidden'
} absolute h-[200px] w-[200px] rounded-full bg-[#ff4132] blur-[150px] filter`}
ref={lightRef1}
style={pos1}
></div>
</div>
<div
className="w-[400px] h-[400px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative overflow-hidden"
ref={cardRef2}
style={{
willChange: 'transform',
transform:
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
}}
>
<div
className={`${
isShowLight2 ? '' : 'hidden'
} absolute h-[200px] w-[200px] rounded-full bg-[#f9b613] blur-[150px] filter`}
ref={lightRef2}
style={pos2}
></div>
</div>
<div
className="w-[400px] h-[400px] flex-center flex-col rounded-lg border border-[rgba(255,255,255,0.1)] bg-[#1C1C1F] relative overflow-hidden"
ref={cardRef3}
style={{
willChange: 'transform',
transform:
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
}}
>
<div
className={`${
isShowLight3 ? '' : 'hidden'
} absolute h-[200px] w-[200px] rounded-full bg-[#3191f7] blur-[150px] filter`}
ref={lightRef3}
style={pos3}
></div>
</div>
</main>
)
}

以上就是基于Vue開(kāi)發(fā)一個(gè)很火的卡片動(dòng)畫(huà)效果的詳細(xì)內(nèi)容,更多關(guān)于Vue卡片動(dòng)畫(huà)效果的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
elementUI組件中el-date-picker限制時(shí)間范圍精確到小時(shí)的方法
現(xiàn)在需要做一個(gè)時(shí)間選擇器,可以根據(jù)小時(shí)(同時(shí)選天和小時(shí))和天?和月,節(jié)假日等類型控制日歷的選擇樣式,下面這篇文章主要給大家介紹了關(guān)于elementUI組件中el-date-picker限制時(shí)間范圍精確到小時(shí)的相關(guān)資料,需要的朋友可以參考下2023-04-04
一文詳解Vue響應(yīng)式數(shù)據(jù)的原理
在vue2的響應(yīng)式中,存在著添加屬性、刪除屬性、以及通過(guò)下標(biāo)修改數(shù)組,但頁(yè)面不會(huì)自動(dòng)更新的問(wèn)題,而這些問(wèn)題在vue3中都得以解決,本文就給大家詳細(xì)的介紹一下Vue響應(yīng)式數(shù)據(jù)原理,感興趣的小伙伴跟著小編一起來(lái)看看吧2023-08-08
vue3+vite2實(shí)現(xiàn)動(dòng)態(tài)綁定圖片的優(yōu)雅解決方案
這篇文章主要為大家詳細(xì)介紹了vue3+vite2實(shí)現(xiàn)動(dòng)態(tài)綁定圖片的優(yōu)雅解決方案,文中的示例代碼簡(jiǎn)潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-08-08
ElementUI之表格toggleRowSelection選中踩坑記錄
這篇文章主要介紹了ElementUI之表格toggleRowSelection選中踩坑記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03

