使用三次贝塞尔曲线绘制弧形菜单

简介: 使用三次贝塞尔曲线绘制弧形菜单

最近项目需要做一个弧形菜单,发现都是根据旋转角度做的,但是如果我们需要的弧线很扁的话弧度就做不来了,所以我根据搜到的这两篇文章将三次贝塞尔曲线整合进去,实现弧形菜单。

借鉴的文章:

弧形菜单效果制作(vue3) 2021-12-24_anywo01的博客-CSDN博客_弧形菜单

html5网页特效-弧形菜单_编程世界-云的博客-CSDN博客

下面是我做的效果:

2b0d4c32179a4f609c5818ce8b8f5646.png


原理我的都已经在注释里解释了,这里我只是加了鼠标滚轮事件,如果想要加上下按钮可以直接调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)
  }
}


相关文章
|
6月前
|
Python
绘制矩形
【5月更文挑战第11天】绘制矩形。
44 1
|
5月前
Qt绘图(线条、椭圆、矩形、图片滚动)
Qt绘图(线条、椭圆、矩形、图片滚动)
309 3
|
4月前
|
前端开发
canvas系列教程02——圆、弧线、圆角矩形、曲线(气泡、心形、N叶草)、扇形
canvas系列教程02——圆、弧线、圆角矩形、曲线(气泡、心形、N叶草)、扇形
39 0
|
C# 图形学
C#之深入理解GDI+绘制圆弧及圆角矩形等比缩放的绘制
GDI+中对于圆弧的绘制,是以给定的长方形(Rectangle`结构)为边界绘制的椭圆的一部分形成的圆弧。绘制的圆弧的中心为长方形内切椭圆的圆心(如果是正方形,则正方形的...
603 0
C#之深入理解GDI+绘制圆弧及圆角矩形等比缩放的绘制
C#编程-132:DrawRectangle绘制矩形
C#编程-132:DrawRectangle绘制矩形
187 0
C#编程-132:DrawRectangle绘制矩形
C#编程-133:绘制椭圆、弧、扇形
C#编程-133:绘制椭圆、弧、扇形
199 0
C#编程-133:绘制椭圆、弧、扇形
|
前端开发 API
Canvas绘制圆点线段
Canvas绘制圆点线段
Canvas绘制圆点线段