【sgOvalMenu】Custom component: elliptical menu, the menu button can move circularly along with the elliptical track

characteristic:

  1. You can set the width and height of the ellipse track 
  2. The rotation angle of the ellipse track can be set, and the horizontal state of the menu text can be automatically corrected
  3. You can set the moving step of the motion trajectory coordinates
  4. You can set the movement track to change the frequency
  5. Can be set to rotate clockwise or counterclockwise
  6. You can set whether the move-in button stops the circular motion button

sgOvalMenu source code 

<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>

application

<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>

Guess you like

Origin blog.csdn.net/qq_37860634/article/details/132517527