最近项目需要做一个弧形菜单,发现都是根据旋转角度做的,但是如果我们需要的弧线很扁的话弧度就做不来了,所以我根据搜到的这两篇文章将三次贝塞尔曲线整合进去,实现弧形菜单。
借鉴的文章:
弧形菜单效果制作(vue3) 2021-12-24_anywo01的博客-CSDN博客_弧形菜单
html5网页特效-弧形菜单_编程世界-云的博客-CSDN博客
下面是我做的效果:
原理我的都已经在注释里解释了,这里我只是加了鼠标滚轮事件,如果想要加上下按钮可以直接调animatio方法。
<template>
<div id="menu" style="position: relative; " class="menu">
<svg height="700" width="300">
<path
:d="`
M ${p1} C ${cp1} ${cp2} ${p2}`
"
stroke="rgba(132, 196, 233)"
stroke-width="10"
stroke-linecap="round"
stroke-opacity="0.3"
fill="none"
/>
<foreignObject width="100%" height="100%">
<div id="allmenu">
<div
v-for="(item, index) in menulist.slice(0,9)"
:key="index"
class="menuitem"
@click="changeindex(item.title,item.path)"
>
<img v-if="activeindex!==item.title" src="../../assets/menu/测导航默认.png" class="itembody">
<img v-else src="../../assets/menu/测导航点击.png" class="itembody">
<span class="itemname" :class="[activeindex===item.title?'fontcolor':'']">{{ item.title }}</span>
<i class="itemicon" :class="item.icon" />
</div>
</div>
<!-- top与bottom都是用来做上下菜单蒙版的 -->
<!-- <div class="top" />
<div class="bottom" /> -->
</foreignObject>
</svg>
</div>
</template>
<script>
import { _throttle } from '@/commonUtil/throttle'
export default {
components: {},
props: {
// 组件宽高
width: {
type: Number,
default: 300
},
height: {
type: Number,
default: 700
},
towMenuname: {
type: String,
default: '温度'
},
// 组件菜单栏
menulist: {
type: Array,
default() {
return [
// 菜单项
{ name: '菜单一' },
{ name: '菜单二' },
{ name: '菜单三' },
{ name: '菜单四' },
{ name: '菜单五' },
{ name: '菜单六' },
{ name: '菜单七' },
{ name: '菜单八' },
{ name: '菜单九' },
{ name: '菜单十' }
]
}
}
},
data() {
return {
// 菜单每个选项的t,t用来求三层次贝塞尔曲线对应的点,缓慢的增加或减少t可以实现动画效果
menuitemt: [],
// 菜单选中状态
activeindex: '气温',
// 计数器,根据每次动画帧数缓慢增加减少t实现动画
counter: 0,
bottom: 4,
top: 4
}
},
computed: {
// 起始点
p1: function name() {
return [0, 0]
},
p2: function name() {
return [0, this.height]
},
// 控制点
cp1: function name() {
return [70, 110]
},
cp2: function name() {
return [70, this.height - 110]
}
},
watch: {
menulist: {
handler(newName, oldName) {
this.init()
}
},
towMenuname: {
handler(newName, oldName) {
this.activeindex = newName
this.top = 0
this.bottom = this.menulist.length - 8
console.log(this.bottom)
}
}
},
created() {},
async mounted() {
// 添加滚轮事件
var menu = document.getElementById('menu')
// 为menu绑定一个鼠标滚动的事件
menu.onmousewheel = (event) => {
// 判断鼠标滚轮滚动的方向
event = event || window.event
// alert(event.wheelDelta);向上120,向下-120
if (event.wheelDelta > 0 || event.detail < 0) {
// 火狐event.detail 向上滚动-3 向下滚动+3
// 向上滚,改变menulist数组
this.up()
} else {
// 向下滚,改变menulist数组
this.down()
}
return false
}
this.init()
},
methods: {
// 初始化
init() {
this.$nextTick(function() {
this.elem = document.getElementsByClassName('menuitem')
let i
// 每个菜单项的在贝塞尔曲线的间隔
const menuinterval = 1 / 10
for (i = 0; i < 9; i++) {
// 这里写死了t,t取值在0-1之间,这里不算0的话t有9个,显示的菜单也有九个(菜单小于9个有多少显示多少)
this.menuitemt[i] = (i + 1) * menuinterval
// 操作dom移动到合适的位置
this.elem[i].style.left =
24 +
this.threeOrderBezier(
this.menuitemt[i],
this.p1,
this.cp1,
this.cp2,
this.p2
)[0] +
'px'
this.elem[i].style.top =
-30 +
this.threeOrderBezier(
this.menuitemt[i],
this.p1,
this.cp1,
this.cp2,
this.p2
)[1] +
'px'
}
})
},
// 获取三次贝塞尔曲线的位置
threeOrderBezier(t, p1, cp1, cp2, p2) {
// 参数分别为t,起始点,两个控制点和终点
var [x1, y1] = p1
var [cx1, cy1] = cp1
var [cx2, cy2] = cp2
var [x2, y2] = p2
var x =
x1 * (1 - t) * (1 - t) * (1 - t) +
3 * cx1 * t * (1 - t) * (1 - t) +
3 * cx2 * t * t * (1 - t) +
x2 * t * t * t
var y =
y1 * (1 - t) * (1 - t) * (1 - t) +
3 * cy1 * t * (1 - t) * (1 - t) +
3 * cy2 * t * t * (1 - t) +
y2 * t * t * t
return [x, y]
},
// 改变menu数组使用防抖节流
down: _throttle(function(val) {
if (this.menulist.length > 8 && this.top > 0) {
this.animation({}, 0)
console.log('bbb')
this.bottom = this.bottom + 1
this.top = this.top - 1
}
}),
up: _throttle(function(val) {
if (this.menulist.length > 8 && this.bottom > 0) {
this.animation({}, 1)
console.log('aaaa')
this.bottom = this.bottom - 1
this.top = this.top + 1
}
}),
// up() {
// this.animation({}, 1)
// },
// 改变选中
changeindex(name, route) {
this.activeindex = name
this.$router.push(route)
},
// 每次鼠标滚轮触发该方法
animation(args, flag) {
function animate(draw, duration, callback) {
var start = 0
requestAnimationFrame(function animate(time) {
start = start + 1
var timePassed = start
if (timePassed > 10) {
timePassed = duration
}
draw(timePassed)
if (timePassed < 10) {
requestAnimationFrame(animate)
} else callback()
})
}
animate(
(timePassed) => {
// 调用需要执行的方法
this.counter = this.counter + 1
var i
// 根据帧数将每个间隔再分成26份
const menuinterval = 1 / 10 / 10
var elem = document.getElementsByClassName('menuitem')
if (flag) {
for (i = 0; i < 9; i++) {
elem[i].style.left =
24 +
this.threeOrderBezier(
this.menuitemt[i] - menuinterval * this.counter,
this.p1,
this.cp1,
this.cp2,
this.p2
)[0] +
'px'
elem[i].style.top =
-30 +
this.threeOrderBezier(
this.menuitemt[i] - menuinterval * this.counter,
this.p1,
this.cp1,
this.cp2,
this.p2
)[1] +
'px'
}
} else {
for (i = 0; i < 9; i++) {
elem[i].style.left =
24 +
this.threeOrderBezier(
this.menuitemt[i] + menuinterval * this.counter,
this.p1,
this.cp1,
this.cp2,
this.p2
)[0] +
'px'
elem[i].style.top =
-30 +
this.threeOrderBezier(
this.menuitemt[i] + menuinterval * this.counter,
this.p1,
this.cp1,
this.cp2,
this.p2
)[1] +
'px'
}
}
// console.log(elem[1].style.left, elem[1].style.top)
},
400,
function changeItems() {
if (flag) {
this.menulist.push(this.menulist.shift())
} else {
var last = this.menulist.pop() // 取数组最后一项
this.menulist.unshift(last) // 插入数组第一位
}
this.init()
this.counter = 0
}.bind(this)
)
}
}
}
</script>
<style lang="scss" scoped>
.menu {
width: 300px;
height: 700px;
background: transparent;
position: relative;
.menuitem {
position: absolute;
.itembody {
position: relative;
// opacity: 1;
}
.itemname {
z-index: 1000;
display: block;
position: absolute;
left: 40px;
width: 100px;
text-align: center;
bottom: 27px;
font-size: 13px;
font-family: Microsoft YaHei;
font-weight: bold;
color: #42a8ff;
}
.itemicon {
z-index: 10000;
width: 30px;
height: 30px;
border-radius: 50%;
// background-color: rgb(136, 15, 15);
position: absolute;
left: 3px;
top: 16px;
display: flex;
align-items: center;
justify-content: center;
color: chocolate;
}
.fontcolor {
color: #00f0ff;
transition: color 0.5s;
font-size: 14px;
transition: font-size 0.5s;
}
}
.menuitem:nth-child(1),
.menuitem:nth-child(9) {
// opacity: 0.4;
transition: opacity 0.5s;
}
.top {
pointer-events: none;
position: absolute;
top: 0;
// background-color: rgb(144, 61, 61);
width: 70%;
height: 60px;
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
}
.bottom {
pointer-events: none;
position: absolute;
bottom: 0;
background-image: linear-gradient(
to top,
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
width: 70%;
height: 70px;
}
}
</style>
在鼠标滚轮事件中我加了防抖节流
// 节流
export function _throttle(fn, interval = 1000) {
let last
let timer
return function() {
const th = this
const args = arguments
const now = +new Date()
if (last && now - last < interval) {
clearTimeout(timer)
timer = setTimeout(function() {
last = now
fn(th, args)
}, interval)
} else {
last = now
fn.apply(th, args)
}
}
}
// 防抖
export function _debounce(fn, delay = 500) {
let timer
return function() {
const th = this
const args = arguments
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function() {
timer = null
fn.apply(th, args)
}, delay)
}
}