偶然间点开nitro文档时,发现其页面鼠标移动至卡片上效果很有趣,故尝试实现其效果。
其主要使用CSSbackground: radial-gradient(xxx)和JS监听指定元素的鼠标移动mousemove事件来实现的
CSS radial-gradient
radial-gradient其作用是创建一个图像,该图像由从原点辐射的两种或多种颜色之间的渐进过渡组成,其形状可以是圆形或椭圆形。我们所需是创建一个渐变圆形,其效果如同一个手电筒效果。
.card::before {
background: radial-gradient(
500px circle at var(--x) var(--y),
var(--fl-color),
transparent 40%
);
will-change: background;
}
如上图所示效果,也可进行扩展实现其他有趣的效果
JS监听获取在Cards容器内的位置
- 页面元素结构
-
JS监听Cards容器鼠标移动事件
JS监听Cards容器内容鼠标的移动,并设置每个子Card的x,y轴方向上的位置,用于设置
radial-gradient属性。
// cardListRef 为外部Cards容器,cardListRef为子card元素集合
function handleCardElMouseProperty(e){
if(!cardListRef.value.length) return;
for (const card of cardListRef.value) {
const rect = card.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
card!.style.setProperty("--x", `${x}px`);
card!.style.setProperty("--y", `${y}px`);
// 设置主题色
card!.style.setProperty("--fl-color", props.color);
}
}
以上两步为该效果主要代码,其效果能实现圆形区域跟随鼠标移动,但是还缺少两点:
1、无边框效果 2、文字内容会被遮盖
完善细节
目前只需要解决以上两点即可达成最终效果:
1、无边框效果高亮
上述的圆形手电筒效果也是利用伪元素::before实现的,将每个Card的伪元素进行调整
.card::before{
content: "";
height: calc(100% + 4px);
position: absolute;
width: calc(100% + 4px);
display: block;
inset: -2px;
z-index:-1;
background: radial-gradient(
500px circle at var(--x) var(--y),
var(--fl-color),
transparent 40%
);
will-change: background;
}
经由如上调整,则能够展示完整边框效果,但是由于设置的z-index:-1会导致每个card里面的亮部区域丢失
2、文字内容会被遮盖
如上一步骤调整后,card里面的亮部区域丢失了文字内容就不会被覆盖,但是还是不完善,如果将card的内容区域设置为一个单独的div元素,将其背景颜色与外部设置一致,并且设置起hover上增加背景色的透明度即可达成nitro一致的交互效果。
// card-content 为子card的内容元素
.card-content{
background-color: var(--card-bg-color);
...
}
.card-content:hover{
opacity: 0.9;
}
组件
<!-- CursorShineCards.vue -->
<template>
<div id="cards-container" ref="cardsRef">
<div class="card" v-for="(item, index) in data" :key="index" :ref="e => setCardListRef(e)">
<div class="card-content">
<slot :item="item" :index="index">
<div class="card-info">
<h3 v-if="item.title">{{item.title}}</h3>
<h3 v-if="item.content">{{item.content}}</h3>
</div>
</slot>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, PropType, ref } from "vue";
defineOptions({
name: "CursorShineCards"
});
const props = defineProps({
data: {
type:Array as PropType<Record<string, any>[]>,
default: []
},
color: {
type: String,
default: 'rgba(0,193,106, 1)'
}
})
const cardsRef = ref()
const cardListRef = ref<Array<HTMLElement>>([])
const setCardListRef = (e: HTMLElement) => {
if(e){
cardListRef.value.push(e)
}
}
function initCardMouseEvt(){
if(cardsRef.value){
cardsRef.value.addEventListener('mousemove', handleCardElMouseProperty);
}
}
onMounted(() => {
initCardMouseEvt()
})
onUnmounted(() => {
if(cardsRef.value){
cardsRef.value.removeEventListener('mousemove', handleCardElMouseProperty);
}
})
function handleCardElMouseProperty(e){
if(!cardListRef.value.length) return;
for (const card of cardListRef.value) {
const rect = card.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
card!.style.setProperty("--x", `${x}px`);
card!.style.setProperty("--y", `${y}px`);
card!.style.setProperty("--fl-color", props.color);
}
}
</script>
<style>
:root{
--fl-color: rgba(239,68, 68, 1);
--card-bg-color: rgb(20, 20, 20);
}
</style>
<style lang="scss" scoped>
#cards-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 916px;
width: calc(100% - 20px);
background-color: var(--card-bg-color);
}
#cards-container:hover > .card::after {
opacity: 1;
}
.card {
border-radius: 10px;
cursor: pointer;
display: flex;
min-height: 160px;
height: auto;
flex-direction: column;
position: relative;
width: 300px;
box-sizing: border-box;
isolation: isolate;
box-shadow: 0 0 0 0px rgb(38,38,38),0 0 0 1px rgb(38,38,38), 0 0 #0000;
}
.card:hover::before {
opacity: 1;
}
.card::before{
border-radius: inherit;
content: "";
height: calc(100% + 4px);
position: absolute;
width: calc(100% + 4px);
display: block;
inset: -2px;
z-index:-1;
background: radial-gradient(
500px circle at var(--x) var(--y),
var(--fl-color),
transparent 40%
);
will-change: background;
}
.card-content{
flex: 1 1 0%;
overflow: hidden;
transition-duration: .15s;
background-color: var(--card-bg-color);
opacity: 1;
transition-property: background-opacity;
transition-timing-function: cubic-bezier(.4,0,.2,1);
border-radius: 10px;
align-items: center;
display: flex;
flex-grow: 1;
justify-content: flex-start;
padding: 0px 20px;
}
.card-content:hover{
opacity: 0.9;
}
.card-content.view-border{
position: relative;
z-index: -1;
}
i {
color: rgb(240, 240, 240);
}
</style>