实现流光边框一般是用渐变背景加动画实现,然后使用内部盒子遮挡内部空间,达到边框流光的效果
思路:背景渐变+旋转动画
功能:
- 自定义渐变(是否渐变<不渐变没有流光效果>,渐变颜色,渐变角度,渐变宽度)
- 自定义动画时间
1 基础实现
<template>
<Box> 测试 </Box>
</template>
<script setup lang="ts">
import Box from "./Box.vue";
</script>
<style scoped></style>
<template>
<div class="box">
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.box {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
position: relative;
width: 100%;
height: 100%;
padding: 5px;
border-radius: 10px;
overflow: hidden;
&:before {
content: "";
background-image: linear-gradient(120deg, #5ddcff, #3c67e3 40%, #4e00c2);
position: absolute;
z-index: 0;
padding-left: 130%;
padding-bottom: 130%;
animation: rotate 8s linear infinite;
}
.content {
height: 100%;
width: 100%;
display: flex;
align-items: center;
padding: 24px 20px;
background: #f1d674;
z-index: 2;
border-radius: 6px;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
动图(略)
2 封装组件
2.1 圆形边框
使用mask属性,使得中间部分背景不被遮挡
<template>
<div class="box" :style="{ width: width + 'px', height: height + 'px' }">
<slot></slot>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
width: {
type: Number, //容器宽
default: 100,
},
height: {
type: Number, //容器高
default: 100,
},
colors: {
//颜色数组
type: Array,
default: () => [
{
color: "#64dcfd",
width: 0,
},
{
color: "#406cf1",
width: 100,
},
{
color: "#4501ac",
width: 101,
},
],
},
angle: {
//渐变角度
type: Number,
default: 120,
},
borderWidth: {
//流光边框宽度
type: Number,
default: 10,
},
gradient: {
//是否渐变
type: Boolean,
default: true,
},
duration: {
//动画时间
type: String,
default: "5s",
},
});
const background = computed(() => {
const positions = [];
const colorsCopy = JSON.parse(JSON.stringify(props.colors));
colorsCopy.forEach((s, index) => {
const sum = colorsCopy.slice(0, index).reduce((a, b) => a + b.width, 0);
if (!props.gradient) {
positions.push(sum);
}
positions.push(sum + s.width);
});
return `linear-gradient(
${props.angle}deg, ${colorsCopy
.map((s, index) => {
if (!props.gradient) {
return `${s.color} ${positions[index]}px, ${s.color} ${
positions[2 * index + 1]
}px`;
}
return `${s.color} ${positions[index]}px`;
})
.join(",")})`;
});
const borderLR = computed(() => {
return props.width / 2 - props.borderWidth + "px";
});
const borderLRShink = computed(() => {
return props.width / 2 - props.borderWidth - 1 + "px";
});
</script>
<style scoped lang="scss">
.box {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
&:before {
content: "";
background-image: v-bind(background);
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
animation: rotate v-bind(duration) linear infinite;
mask: radial-gradient(
transparent,
transparent v-bind(borderLRShink),
#000 v-bind(borderLR)
);
-webkit-mask: radial-gradient(
transparent,
transparent v-bind(borderLRShink),
#000 v-bind(borderLR)
);
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
2.2 矩形边框
使用伪元素,自定义中间部分背景
<template>
<div class="box" :style="{ width: width + 'px', height: height + 'px' }">
<slot></slot>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
width: {
type: Number, //容器宽
default: 100,
},
height: {
type: Number, //容器高
default: 100,
},
colors: {
//颜色数组
type: Array,
default: () => [
{
color: "#64dcfd",
width: 0,
},
{
color: "#406cf1",
width: 100,
},
{
color: "#4501ac",
width: 101,
},
],
},
angle: {
//渐变角度
type: Number,
default: 120,
},
borderWidth: {
//左右流光边框宽度
type: [Array, Number],
default: [20, 5],
},
gradient: {
//是否渐变
type: Boolean,
default: true,
},
duration: {
//动画时间
type: String,
default: "5s",
},
innerBackground: {
//内部背景
type: String,
default: "#FFF",
},
});
const background = computed(() => {
const positions = [];
const colorsCopy = JSON.parse(JSON.stringify(props.colors));
colorsCopy.forEach((s, index) => {
const sum = colorsCopy.slice(0, index).reduce((a, b) => a + b.width, 0);
if (!props.gradient) {
positions.push(sum);
}
positions.push(sum + s.width);
});
return `linear-gradient(
${props.angle}deg, ${colorsCopy
.map((s, index) => {
if (!props.gradient) {
return `${s.color} ${positions[index]}px, ${s.color} ${
positions[2 * index + 1]
}px`;
}
return `${s.color} ${positions[index]}px`;
})
.join(",")})`;
});
const innerWidth = computed(() => {
let doubleBorderWidth = 0;
if (Array.isArray(props.borderWidth)) {
if (props.borderWidth.length === 2) {
doubleBorderWidth = props.borderWidth[1] * 2;
} else if (props.borderWidth.length === 1) {
doubleBorderWidth = props.borderWidth[0] * 2;
}
} else {
doubleBorderWidth = props.borderWidth * 2;
}
return props.width - doubleBorderWidth + "px";
});
const innerheight = computed(() => {
let doubleBorderWidth = 0;
if (Array.isArray(props.borderWidth)) {
if (props.borderWidth.length === 2) {
doubleBorderWidth = props.borderWidth[0] * 2;
} else if (props.borderWidth.length === 1) {
doubleBorderWidth = props.borderWidth[0] * 2;
}
} else {
doubleBorderWidth = props.borderWidth * 2;
}
return props.height - doubleBorderWidth + "px";
});
const colorSize = computed(() => {
return (
Math.ceil(
Math.sqrt(props.width * props.width + props.height * props.height)
) + "px"
);
});
</script>
<style scoped lang="scss">
.box {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
&:before {
content: "";
background-image: v-bind(background);
position: absolute;
width: v-bind(colorSize);
height: v-bind(colorSize);
animation: rotate v-bind(duration) linear infinite;
}
&:after {
content: "";
background: v-bind(innerBackground);
position: absolute;
z-index: 1;
width: v-bind(innerWidth);
height: v-bind(innerheight);
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>