vue球形词云旋转+连线

10 篇文章 5 订阅

vue球形词云旋转的实现,基于这篇文章 https://blog.csdn.net/weixin_43951323/article/details/120903651
如果不需要连线,可到这篇文章中拷贝即可

若需要各词云之间的连线,我增加了一些相关逻辑

  • 固定容器的大小,此处为600*600
  • 使用svg来画线(line),只需要找出每条线段的两个点坐标
  • 优化 this.radius 的计算,保证每个元素的 left 和 top 值都为正(方便线条的点定位),由2修改为2.5或更大
  • 优化定时器为 requestAnimationFrame,减少重排重绘
  • 画线时通过文档碎片,避免频繁的 Dom 操作

效果如下

在这里插入图片描述

以下是完整代码

<template>
  <section class="cloud-bed">
    <div class="cloud-box">
      <span
        v-for="(item, index) in dataList"
        :key="index"
        @click="getDataInfo(item)"
      >
        {{ item.name }}
      </span>
      <!-- svg容器 -->
      <svg width="600" height="600" xmlns="http://www.w3.org/2000/svg" version="1.1"></svg>
    </div>
  </section>
</template>
 
<script>
  export default {
    name: "word-cloud",
    data() {
      return {
        timer: 30, // 球体转动速率
        radius: 0, // 词云球体面积大小
        dtr: Math.PI/180, //鼠标滑过球体转动速度
        active: false, // 默认加载是否开启转动
        lasta: 0, // 上下转动
        lastb: 0.5, // 左右转动
        distr: true,
        tspeed: 0, // 鼠标移动上去时球体转动
        mouseX: 0,
        mouseY: 0,
        tagAttrList: [],
        tagContent: null,
        cloudContent: null,
        sinA: '',
        cosA: '',
        sinB: '',
        cosB: '',
        sinC: '',
        cosC: '',
        dataList: [
          {
            name: '测试1',
            value: '1'
          },
          {
            name: '测试2',
            value: '2'
          },
          {
            name: '测试3',
            value: '3'
          },
          {
            name: '测试4',
            value: '4'
          },
          {
            name: '测试5',
            value: '5'
          },
          {
            name: '测试6',
            value: '6'
          },
          {
            name: '测试7',
            value: '7'
          },
          {
            name: '测试8',
            value: '8'
          },
          {
            name: '测试9',
            value: '9'
          },
          {
            name: '测试10',
            value: '10'
          },
        ],
        // animationId
        animationId: undefined,
        // 如果需要控制requestAnimationFrame的频率,需要自己定义两个变量用于比较
        nowTime: 0,
        lastTime: 0
      }
    },
    mounted () {
      this.$nextTick(() => {
        this.initWordCloud()
      })
    },
    beforeDestroy () {
      // clearInterval(this.timer)
      // 销毁动画
      this.animationDestroy()
    },
    methods:{
      // 获取点击文本信息
      getDataInfo (item) {
        console.log(item, 'item')
      },
      initWordCloud () {
        // 销毁动画(list变化时可再次调用该事件,用于刷新动画)
        this.animationDestroy()
        this.cloudContent = document.querySelector('.cloud-box');
        this.radius = this.cloudContent && this.cloudContent.offsetWidth / 2.5
        this.tagContent = this.cloudContent.getElementsByTagName('span');
        for (let i = 0; i < this.tagContent.length; i++) {
          let tagObj = {};
          tagObj.offsetWidth = this.tagContent[i].offsetWidth;
          tagObj.offsetHeight = this.tagContent[i].offsetHeight;
          this.tagAttrList.push(tagObj);
        }
        this.sineCosine(0, 0, 0);
        this.positionAll();
        this.cloudContent.onmouseover = () => {
          this.active=true;
        };
        this.cloudContent.onmouseout = () => {
          this.active=false;
        };
        this.cloudContent.onmousemove = (ev) => {
          let oEvent = window.event || ev;
          this.mouseX = oEvent.clientX - (this.cloudContent.offsetLeft + this.cloudContent.offsetWidth/2);
          this.mouseY = oEvent.clientY - (this.cloudContent.offsetTop + this.cloudContent.offsetHeight/2);
          this.mouseX/= 5;
          this.mouseY/= 5;
        };
        // setInterval(this.update, this.timer);
        this.animationUpdate()
      },
      // 通过 requestAnimationFrame 执行动画
      animationUpdate () {
        this.nowTime = Date.now()
        if (this.nowTime - this.lastTime > 10) {
          this.lastTime = this.nowTime
          this.update()
        }
        this.animationId = window.requestAnimationFrame(this.animationUpdate)
      },
      // 销毁动画,状态重置
      animationDestroy () {
        this.animationId && window.cancelAnimationFrame(this.animationId)
        this.animationId = null
        // 所有与构建动画相关的变量重置
        this.timer = 40
        this.radius = 50
        this.dtr = Math.PI / 180
        this.active =  false
        this.lasta = 0
        this.lastb = 0.5
        this.distr = true
        this.tspeed = 0
        this.mouseX = 0
        this.mouseY = 0
        this.tagAttrList = []
        this.tagContent = null
        this.cloudContent = null
        this.sinA = ''
        this.cosA = ''
        this.sinB = ''
        this.cosB = ''
        this.sinC = ''
        this.cosC = ''
        this.nowTime = 0
        this.lastTime = 0
      },
      positionAll () {
        let phi = 0;
        let theta = 0;
        let max = this.tagAttrList.length;
        let aTmp = [];
        let oFragment = document.createDocumentFragment();
        //随机排序
        for (let i=0; i < this.tagContent.length; i++) {
          aTmp.push(this.tagContent[i]);
        }
        aTmp.sort(() => {
          return Math.random() < 0.5 ? 1 : -1;
        });
        for (let i = 0; i < aTmp.length; i++) {
          oFragment.appendChild(aTmp[i]);
        }
        this.cloudContent.appendChild(oFragment);
        for(let i = 1; i < max + 1; i++){
          if (this.distr) {
            phi = Math.acos(-1 + (2 * i - 1) / max);
            theta = Math.sqrt(max * Math.PI) * phi;
          } else {
            phi = Math.random() * (Math.PI);
            theta = Math.random() * (2 * Math.PI);
          }
          //坐标变换
          this.tagAttrList[i-1].cx = this.radius * Math.cos(theta) * Math.sin(phi);
          this.tagAttrList[i-1].cy = this.radius * Math.sin(theta) * Math.sin(phi);
          this.tagAttrList[i-1].cz = this.radius * Math.cos(phi);
          this.tagContent[i-1].style.left = this.tagAttrList[i-1].cx + this.cloudContent.offsetWidth / 2 - this.tagAttrList[i-1].offsetWidth / 2 + 'px';
          this.tagContent[i-1].style.top = this.tagAttrList[i-1].cy + this.cloudContent.offsetHeight / 2 - this.tagAttrList[i-1].offsetHeight / 2 + 'px';
        }
      },
      update () {
        let angleBasicA;
        let angleBasicB;
 
        if (this.active) {
          angleBasicA = (-Math.min(Math.max(-this.mouseY, -200 ), 200) / this.radius) * this.tspeed;
          angleBasicB = (Math.min(Math.max(-this.mouseX, -200 ), 200) / this.radius) * this.tspeed;
        } else {
          angleBasicA = this.lasta * 0.98;
          angleBasicB = this.lastb * 0.98;
        }
 
        //默认转动是后是否需要停下
        // lasta=a;
        // lastb=b;
 
        // if(Math.abs(a)<=0.01 && Math.abs(b)<=0.01)
        // {
        // return;
        // }
        this.sineCosine(angleBasicA, angleBasicB, 0);
        for(let j = 0; j < this.tagAttrList.length; j++) {
          let rx1 = this.tagAttrList[j].cx;
          let ry1 = this.tagAttrList[j].cy * this.cosA + this.tagAttrList[j].cz * (-this.sinA);
          let rz1 = this.tagAttrList[j].cy * this.sinA + this.tagAttrList[j].cz * this.cosA;
 
          let rx2 = rx1 * this.cosB + rz1 * this.sinB;
          let ry2 = ry1;
          let rz2 = rx1 * (-this.sinB) + rz1 * this.cosB;
 
          let rx3 = rx2 * this.cosC + ry2 * (-this.sinC);
          let ry3 = rx2 * this.sinC + ry2 * this.cosC;
          let rz3 = rz2;
          this.tagAttrList[j].cx = rx3;
          this.tagAttrList[j].cy = ry3;
          this.tagAttrList[j].cz = rz3;
 
          let per = 350 / (350 + rz3);
 
          this.tagAttrList[j].x = rx3 * per - 2;
          this.tagAttrList[j].y = ry3 * per;
          this.tagAttrList[j].scale = per;
          this.tagAttrList[j].alpha = per;
 
          this.tagAttrList[j].alpha = (this.tagAttrList[j].alpha - 0.6) * (10/6);
        }
        this.doPosition();
        this.depthSort();
        this.setLinePosition()
      },
      doPosition() {
        let len = this.cloudContent.offsetWidth/2;
        let height = this.cloudContent.offsetHeight/2;
        for (let i=0;i < this.tagAttrList.length;i++) {
          this.tagContent[i].style.left = this.tagAttrList[i].cx + len - this.tagAttrList[i].offsetWidth/2 + 'px';
          this.tagContent[i].style.top = this.tagAttrList[i].cy + height - this.tagAttrList[i].offsetHeight/2 + 'px';
          this.tagContent[i].style.fontSize = Math.ceil(12 * this.tagAttrList[i].scale/2) + 8 + 'px';
          this.tagContent[i].style.filter = "alpha(opacity="+100 * this.tagAttrList[i].alpha+")";
          this.tagContent[i].style.opacity = this.tagAttrList[i].alpha;
        }
      },
      depthSort(){
        let aTmp = [];
        for (let i = 0; i < this.tagContent.length; i++) {
          aTmp.push(this.tagContent[i]);
        }
        aTmp.sort((item1, item2) => item2.cz - item1.cz);
        for (let i = 0; i < aTmp.length; i++) {
          aTmp[i].style.zIndex=i;
        }
      },
      // 计算线的位置
      setLinePosition () {
        // 获取svg
        const oSvg = document.getElementsByTagName('svg')[0]
        // 清空svg
        oSvg.innerHTML = ''
        // 创建文档碎片
        let oFrag = document.createDocumentFragment()
        // 属性列表(取坐标准备)
        let attrList = this.tagContent
        // 节点个数
        const len = attrList.length
        // 外部控制循环次数
        for(let i = 0; i < len - 1; i++) {
          // 内部控制每次循环链接的次数
          for (let j = 0; j < len - i - 1; j++) {
            // 创建线条
            let oLine = document.createElementNS("http://www.w3.org/2000/svg", 'line')
            // 第一个点
            oLine.setAttribute('x1', attrList[i].style.left)
            oLine.setAttribute('y1', attrList[i].style.top)
            // 第二个点
            oLine.setAttribute('x2', attrList[len - j - 1].style.left)
            oLine.setAttribute('y2', attrList[len - j - 1].style.top)
            // 设置线条颜色
            oLine.setAttribute('stroke','#bbb')
            // 文档碎片插入线条
            oFrag.appendChild(oLine)
          }
        }
        // 插入svg中
        oSvg.appendChild(oFrag)
      },
      sineCosine (a, b, c) {
        this.sinA = Math.sin(a * this.dtr);
        this.cosA = Math.cos(a * this.dtr);
        this.sinB = Math.sin(b * this.dtr);
        this.cosB = Math.cos(b * this.dtr);
        this.sinC = Math.sin(c * this.dtr);
        this.cosC = Math.cos(c * this.dtr);
      }
    }
  };
</script>
 
 
<style scoped lang="scss">
.cloud-bed {
  width: 600px;
  height: 600px;
  margin: auto;
  .cloud-box{
    position:relative;
    margin:20px auto 0px;
    width: 100%;
    height: 100%;
    background: #00000000;
    span{
      position: absolute;
      padding: 3px 6px;
      top: 0px;
      font-weight: bold;
      text-decoration:none;
      left:0px;
      background-image: linear-gradient(to bottom, red, #fff);
      background-clip: text;
      color: transparent;
    }
  }
}
</style>
欢迎留言,一起探索更多~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>