特性:
- 可以设置椭圆轨迹宽度、高度
- 可以设置椭圆轨迹旋转角度,并且自动纠偏菜单文字水平状态
- 可以设置运动轨迹坐标移动步长
- 可以设置运动轨迹改变频率
- 可以设置顺时针还是逆时针旋转
- 可以设置移入按钮是否停止循环运动按钮
sgOvalMenu源码
<template> <div :class="$options.name" :border-animate="borderAnimate" :style="style"> <div class="ovalMenuBtn" v-for="(a, i) in menubtns" :key="i" :style="a.style" @click="$emit(`click`, a);" @mouseover="mouseover(a)" @mouseout="mouseout(a)"> <slot :data="a"></slot> </div> </div> </template> <script> export default { name: 'sgOvalMenu', data() { return { style: {}, coordinates: [], step_: 0,//按钮在椭圆轨道上面移动的步长 time_: 0,//按钮坐标变化的时间间隔 rotate_: 0,//椭圆旋转角度 oval_step: 1,//椭圆动画步长 interval1: null, interval2: null, menubtns: [], isHoverBtn: false, borderAnimate: true, } }, props: [ "width",//椭圆的长直径 "height",//椭圆的短直径 "rotate",//椭圆旋转角度 "step",//按钮在椭圆轨道上面移动的步长 "time",//按钮坐标变化的时间间隔 "clockwise",//顺时针运动(boolean) "hoverButtonPause",//移入按钮暂停运动(boolean) "data",//椭圆上面的按钮数据 ], watch: { width: { handler(d) { this.style.width = `${d || 800}px`; }, deep: true, immediate: true, }, height: { handler(d) { this.style.height = `${d || 400}px`; }, deep: true, immediate: true, }, rotate_: { handler(d) { this.style.rotate = `${d || 0}deg`; this.setProperty(); }, deep: true, immediate: true, }, rotate: { handler(d) { this.rotate_ = d; }, deep: true, immediate: true, }, step: { handler(d) { this.step_ = d || 2; }, deep: true, immediate: true, }, time: { handler(d) { this.time_ = d || 200; }, deep: true, immediate: true, }, data: { handler(d) { if (d) { this.menubtns = JSON.parse(JSON.stringify(d)); this.getCoordinates(d => { this.coordinates = d; this.initAnimate(); }); } }, deep: true, immediate: true, }, isHoverBtn(newValue, oldValue) { if (this.hoverButtonPause || this.hoverButtonPause === '') { newValue ? this.clearIntervalAll() : this.initAnimate(); this.borderAnimate = !newValue; } }, }, destroyed() { this.clearIntervalAll(); }, mounted() { this.setProperty(); }, methods: { mouseover(d) { this.isHoverBtn = true; this.$emit(`mouseover`, d); }, mouseout(d) { this.isHoverBtn = false; this.$emit(`mouseout`, d); }, setProperty() { this.$el && this.$el.style.setProperty("--rotate", `${-1 * parseFloat(this.style.rotate || 0)}deg`); //js往css传递局部参数 }, clearIntervalAll(d) { clearInterval(this.interval1); clearInterval(this.interval2); }, initAnimate(d) { this.initAnimateBtn(); this.initAnmiateOval() }, // 按钮旋转动画 initAnimateBtn() { clearInterval(this.interval1); this.interval1 = setInterval(() => { this.setStyles(); }, this.time_); this.setStyles(); }, // 椭圆旋转动画 initAnmiateOval(d) { clearInterval(this.interval2); this.interval2 = setInterval(() => { this.rotate_ = this.rotate_ + this.oval_step; this.rotate_ > this.rotate && (this.oval_step = -1); this.rotate_ < -1 * this.rotate && (this.oval_step = 1); }, 382); }, setStyles() { let coordinateStep = this.coordinates.length / this.menubtns.length; let arr = this.coordinates, N = this.step_; if (this.clockwise || this.clockwise === '') { //前面N个元素挪到最后 arr.splice(arr.length - 1, 0, ...arr.splice(0, N)); } else { //最后N个元素挪到前面 arr.splice(0, 0, ...arr.splice(arr.length - N)); } this.coordinates = arr; this.menubtns.forEach((v, i) => { let coordinate = this.coordinates[i * coordinateStep]; this.$set(v, "style", { left: `${coordinate.x}px`, top: `${coordinate.y}px`, }); }); }, getCoordinates(cb) { let a = parseFloat(this.style.width) / 2; let b = parseFloat(this.style.height) / 2; this.getCPoint(a, b, 1, a, b, cb); }, // a 长半径, b 短半径, p 节点的间隔 , cx, cy 圆心, getCPoint(a, b, p = 1, cx = 0, cy = 0, cb) { const data = [] for (let index = 0; index < 360; index = index + p) { let x = a * Math.cos(Math.PI * 2 * index / 360) let y = b * Math.sin(Math.PI * 2 * index / 360) data.push({ x: x + cx, y: y + cy }) } cb && cb(data); }, } }; </script> <style lang="scss" scoped> $rotate: var(--rotate); .sgOvalMenu { border: 2px dashed transparent; border-color: #409EFF66 #409EFFAA #409EFF #409EFF; border-radius: 100%; width: 100%; height: 100%; transform-origin: center; transition: .382s linear; .ovalMenuBtn { transition: .382s linear; user-select: none; white-space: nowrap; position: absolute; width: max-content; height: max-content; left: 0; top: 0; transform: translate(-50%, -50%) rotate($rotate); transform-origin: center; pointer-events: auto; color: white; cursor: pointer; &:hover { z-index: 1; font-weight: bold; color: #409EFF; text-shadow: 0px 0px 5px #409EFF; filter: brightness(1.1); } } /*边框虚线滚动动画特效*/ &[border-animate] { background: linear-gradient(90deg, #409EFF 60%, transparent 60%) repeat-x left top/10px 1px, linear-gradient(0deg, #409EFF 60%, transparent 60%) repeat-y right top/1px 10px, linear-gradient(90deg, #409EFF 60%, transparent 60%) repeat-x right bottom/10px 1px, linear-gradient(0deg, #409EFF 60%, transparent 60%) repeat-y left bottom/1px 10px; animation: border-animate .382s infinite linear; @keyframes border-animate { 0% { background-position: left top, right top, right bottom, left bottom; } 100% { background-position: left 10px top, right top 10px, right 10px bottom, left bottom 10px; } } } } </style>
应用
<template> <div :class="$options.name"> <!-- 椭圆菜单 --> <sgOvalMenu :data="ovalMenus" @click="clickOvalMenu" :width="700" :height="200" :rotate="30" clockwise hoverButtonPause> <template v-slot="{ data }"> <div class="btn"> {{ data.label }} </div> </template> </sgOvalMenu> </div> </template> <script> import sgOvalMenu from "./sgOvalMenu"; export default { name: 'sgBody', components: { sgOvalMenu }, data() { return { ovalMenus: [ { value: '1', label: '显示文本1', }, { value: '2', label: '显示文本2', }, { value: '3', label: '显示文本3', }, { value: '4', label: '显示文本4', }, { value: '5', label: '显示文本5', }, ], } }, methods: { clickOvalMenu(d) { // console.log(`获取点击信息:`, JSON.stringify(d, null, 2)); }, } }; </script> <style lang="scss" scoped> .sgBody { display: flex; justify-content: center; align-items: center; background-color: black; .btn { box-sizing: border-box; padding: 10px 20px; border-radius: 88px; box-sizing: border-box; border: 1px solid #409EFF; box-shadow: 0 10px 30px #409EFFAA, 0 10px 30px #409EFF99 inset; color: #409EFF; &:hover { box-shadow: 0 10px 30px #409EFFAA, 0 10px 30px #409EFF99 inset; background-color: #409EFF; color: black; filter: brightness(1.3); } } } </style>