【sgOvalMenu】自定义组件:椭圆形菜单,菜单按钮可以随着椭圆轨迹进行循环运动

简介: 【sgOvalMenu】自定义组件:椭圆形菜单,菜单按钮可以随着椭圆轨迹进行循环运动

image.png

特性

  1. 可以设置椭圆轨迹宽度、高度
  2. 可以设置椭圆轨迹旋转角度,并且自动纠偏菜单文字水平状态
  3. 可以设置运动轨迹坐标移动步长
  4. 可以设置运动轨迹改变频率
  5. 可以设置顺时针还是逆时针旋转
  6. 可以设置移入按钮是否停止循环运动按钮

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>


相关文章
|
小程序
小程序右上角的胶囊颜色怎么修改?
小程序右上角的胶囊颜色怎么修改?
179 0
Qml实用技巧:在可视元素之前半透明覆盖一个可视元素,阻止鼠标透(界面)传(防止点击到被遮挡的按钮)
Qml实用技巧:在可视元素之前半透明覆盖一个可视元素,阻止鼠标透(界面)传(防止点击到被遮挡的按钮)
Qml实用技巧:在可视元素之前半透明覆盖一个可视元素,阻止鼠标透(界面)传(防止点击到被遮挡的按钮)
|
1月前
ThreeJs通过射线获取自己的点击位置坐标
这篇文章详细说明了如何使用Three.js来绘制线条,包括创建线几何体、设置材质以及将线条添加到3D场景中的具体步骤。
92 1
ThreeJs通过射线获取自己的点击位置坐标
|
3月前
|
前端开发 JavaScript
基于Vue3实现鼠标按下某个元素进行拖动,实时改变左侧或右侧元素的宽度,以及点击收起或展开的功能
本文介绍了如何在Vue3项目中实现一个鼠标拖动调整元素宽度的功能,并展示了点击按钮收起或展开侧边栏的效果,提供了完整的实现代码和操作演示。
641 0
基于Vue3实现鼠标按下某个元素进行拖动,实时改变左侧或右侧元素的宽度,以及点击收起或展开的功能
|
6月前
LabVIEW当鼠标悬停在图形曲线上时显示坐标
LabVIEW当鼠标悬停在图形曲线上时显示坐标
86 1
|
6月前
【sgDragMove】自定义组件:自定义拖拽组件,仅支持拖拽、设置吸附屏幕边界距离。
【sgDragMove】自定义组件:自定义拖拽组件,仅支持拖拽、设置吸附屏幕边界距离。
|
6月前
|
测试技术
【sgTileImage】自定义组件:瓦片图拖拽局部加载、实现以鼠标为中心缩放
【sgTileImage】自定义组件:瓦片图拖拽局部加载、实现以鼠标为中心缩放
|
图形学
|
定位技术 C# Windows
C#编程学习(05):使用webbroswer控件显示地图并标注点位坐标
C#编程学习(05):使用webbroswer控件显示地图并标注点位坐标
|
C# 图形学
C#绘制自定义小人
C#绘制自定义小人