动态词云效果——组件封装

👻:团队在开发大屏项目时,常常会遇到动态词云的效果,如下图所示:

"每条词条在容器范围内随机移动,当鼠标放在单独词条上时,停止移动"

为了方便之后开发,现在记录一下该组件的封装 (Vue3项目)😎

WordCloudRandom.vue文件:

ini 复制代码
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
​
interface AnimateElement extends HTMLElement {
  animateInfo: {
    scale?: number
    x?: number
    y?: number
    z?: number
  }
  prevAnimateInfo: {
    scale?: number
    x?: number
    y?: number
    z?: number
  }
}
​
const container = ref<HTMLElement>()
let randomAnimate: RandomAnimate
​
class RandomAnimate {
  container: HTMLElement | null = null
  items: AnimateElement[] = []
  minScale = 0.9
  scaleInterval = 10000 // 缩放间隔
  xInterval = 40000 // 横向位移速度
  yInterval = 40000 // 纵向位移速度
  animateInterval = 16 // 动画间隔 16ms
  index = 1
  animationFrame: number | undefined
  restartTimer: number | undefined
  mutationObserver: MutationObserver | undefined
​
  constructor(container: HTMLElement) {
    this.container = container
    this.init()
  }
​
  init() {
    this.container!.style.position = 'relative'
    this.createMutation()
  }
​
  createMutation() {
    this.mutationObserver = new MutationObserver(function (mutationRecords) {
      mutationRecords.forEach((record) => {
        const { addedNodes, removedNodes } = record
        if (addedNodes.length) {
          randomAnimate.add(
            Array.from(addedNodes).filter(
              (node) => node.nodeType === 1
            ) as AnimateElement[]
          )
        }
        if (removedNodes.length) {
          randomAnimate.remove(
            Array.from(removedNodes).filter(
              (node) => node.nodeType === 1
            ) as AnimateElement[]
          )
        }
      })
    })
​
    this.mutationObserver.observe(container.value!, {
      childList: true,
      attributes: false
    })
  }
​
  add(items: AnimateElement | AnimateElement[]) {
    items = Array.isArray(items) ? items : [items]
    items.forEach((item: AnimateElement) => {
      item.style.position = 'absolute'
      item.style.left = '0px'
      item.style.top = '0px'
      item.animateInfo = {}
      item.prevAnimateInfo = {} // 上一状态,用来判断移动方向与缩放状态
      this.setScale(item)
      this.setPosition(item)
      this.setHover(item)
      this.render(item)
    })
    this.items.push(...items)
  }
​
  remove(item: AnimateElement | AnimateElement[]) {
    if (Array.isArray(item)) {
      item.forEach((i) => {
        this.remove(i)
      })
    } else {
      // item.parentElement!.removeChild(item)
      const index = this.items.indexOf(item)
      if (index > -1) {
        this.items.splice(index, 1)
      }
    }
  }
​
  setScale(item: AnimateElement) {
    const maxZ = this.items.length * 100
    const minScale = this.minScale
    const { animateInfo, prevAnimateInfo } = item
    if (!animateInfo.scale) {
      animateInfo.scale = Math.random() * (1 - minScale) + minScale
    } else {
      const scaleInterval = this.scaleInterval
      const scaleStep = ((1 - minScale) / scaleInterval) * this.animateInterval
      const _animate = JSON.parse(JSON.stringify(animateInfo))
​
      let direction
      if (prevAnimateInfo.scale) {
        direction =
          animateInfo.scale > prevAnimateInfo.scale ||
          animateInfo.scale === minScale
            ? 1
            : -1
      } else {
        direction = Math.random() > 0.5 ? 1 : -1 // 随机开始放大或缩小
      }
​
      if (direction > 0) {
        // 放大
        animateInfo.scale += scaleStep
        if (animateInfo.scale > 1) {
          animateInfo.scale = 1
        }
      } else {
        // 缩小
        animateInfo.scale -= scaleStep
        if (animateInfo.scale < minScale) {
          animateInfo.scale = minScale
        }
      }
      item.prevAnimateInfo.scale = _animate.scale
    }
    animateInfo.z = ~~(((animateInfo.scale - minScale) / (1 - minScale)) * maxZ)
  }
​
  setPosition(item: AnimateElement) {
    const maxX = this.container!.clientWidth - item.clientWidth
    const maxY = this.container!.clientHeight - item.clientHeight
    const { animateInfo, prevAnimateInfo } = item
    const _animate = JSON.parse(JSON.stringify(animateInfo))
​
    if (animateInfo.x === undefined) {
      animateInfo.x = Math.random() * maxX
      animateInfo.y = Math.random() * maxY
    } else {
      let { xInterval, yInterval, animateInterval } = this
      // xInterval = xInterval - Math.random() * 15000
      // yInterval = yInterval - Math.random() * 15000
      const xStep = (maxX / xInterval) * animateInterval
      const yStep = (maxY / yInterval) * animateInterval
​
      let directionX, directionY
​
      if (prevAnimateInfo.x === undefined) {
        directionX = Math.random() > 0.5 ? 1 : -1 // 随机开始向左或向右
        directionY = Math.random() > 0.5 ? 1 : -1 // 随机开始向上或向下
      } else {
        directionX =
          animateInfo.x > prevAnimateInfo.x || animateInfo.x === 0 ? 1 : -1
        directionY =
          animateInfo.y! > prevAnimateInfo.y! || animateInfo.y === 0 ? 1 : -1
      }
​
      if (directionX > 0) {
        // 向右
        animateInfo.x += xStep
        if (animateInfo.x > maxX) {
          animateInfo.x = maxX
        }
      } else {
        // 向左
        animateInfo.x -= xStep
        if (animateInfo.x < 0) {
          animateInfo.x = 0
        }
      }
​
      if (directionY > 0) {
        // 向下
        animateInfo.y! += yStep
        if (animateInfo.y! > maxY) {
          animateInfo.y = maxY
        }
      } else {
        // 向上
        animateInfo.y! -= yStep
        if (animateInfo.y! < 0) {
          animateInfo.y = 0
        }
      }
​
      item.prevAnimateInfo.x = _animate.x
      item.prevAnimateInfo.y = _animate.y
    }
  }
​
  setHover(item: AnimateElement) {
    item.addEventListener('mouseenter', () => {
      this.stop()
      clearTimeout(this.restartTimer)
      item.style.zIndex = this.items.length * 10000 + ''
      item.style.transform = item.style.transform.replace(
        /scale((\d+.?\d*))/,
        'scale(1)'
      )
    })
    item.addEventListener('mouseleave', () => {
      item.style.zIndex = item.animateInfo.z + ''
      item.style.transform = item.style.transform.replace(
        /scale((\d+.?\d*))/,
        `scale(${item.animateInfo.scale})`
      )
      this.restartTimer = setTimeout(() => {
        this.start()
      }, 300)
    })
  }
​
  render(item: AnimateElement) {
    const { x = 10, y = 10, z, scale } = item.animateInfo
    item.style.zIndex = z + ''
    item.style.transform = `translate(${x}px, ${y}px) scale(${scale})`
  }
​
  stop() {
    this.animationFrame && window.cancelAnimationFrame(this.animationFrame)
  }
​
  start() {
    this.items.forEach((item: AnimateElement) => {
      this.setScale(item)
      this.setPosition(item)
      this.render(item)
    })
    this.animationFrame = window.requestAnimationFrame(() => {
      this.start()
    })
  }
​
  destroy() {
    this.stop()
    this.items = []
    this.container = null
    this.mutationObserver && this.mutationObserver.disconnect()
  }
}
​
function init() {
  randomAnimate = new RandomAnimate(container.value!)
  const items = Array.from(container.value!.children) as AnimateElement[]
  randomAnimate.add(items)
  randomAnimate.start()
}
​
onMounted(() => {
  init()
})
​
onUnmounted(() => {
  randomAnimate.destroy()
})
</script>
<template>
  <div class="word-cloud-container" ref="container">
    <slot></slot>
  </div>
</template>
<style lang="less" scoped>
.word-cloud-container {
  width: 100%;
  height: 100%;
  position: relative;
}
</style>
​

使用:

xml 复制代码
<script setup lang="ts">
import WordCloudRandom from '@/components/WordCloudRandom/index.vue'
const list = ref(['xxx1','xxx2','xxx3'])
</script>
<template>
  <div class="container">
    <WordCloudRandom>
      <div class="card" v-for="item in list" :key="item">
        <div class="content">{{ item }}</div>
      </div>
    </WordCloudRandom>
  </div>
</template>
<style lang="less" scoped>
.word-cloud-random {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: transparent;
  position: relative;
}
.container {
  width: 660px;
  height: 440px;
  // border: 1px solid #124664;
  // border-radius: 4px;
  // margin: 0px auto;
  position: absolute;
  // left: 50%;
  top: 50%;
  transform: translate(0, -45%);
}
​
.card {
  width: 213px;
  height: auto;
  box-shadow: 0px 2px 4px 0px rgba(0, 78, 106, 0.8);
  border-radius: 7px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  cursor: pointer;
  background: #073351;
  text-align: center;
  padding: 6px;
  transition: all 0.3s;
  // white-space: nowrap;
  // overflow: hidden;
  // text-overflow: ellipsis;
}
​
.card:hover {
  border: 1px solid #2be5eb;
  background: #06436d;
  box-shadow: 0px 2px 5px 0px rgba(0, 78, 106, 0.8);
}
.content {
  font-size: 22px;
  font-family: PingFangSC-Semibold, PingFang SC;
  font-weight: 600;
  color: #2be5eb;
  line-height: 48px;
}
</style>
​

以上,文章开头图片当中的效果就可以实现了;当然有了以上的基础,我们还可以升级样式,如:

实现如下:

xml 复制代码
<template>
  <div class="hexagon">
    <WordCloudRandom class="wrapper">
      <div class="item" v-for="item in indiArr" :key="item.title">
        <div class="shape" :class="[item.type, item.color]">
          <div class="six-wrapper" v-if="item.type === 'six'">
            <div class="main1 tool"></div>
            <div class="main2 tool"></div>
            <div class="main3 tool"></div>
            <div class="main4 tool"></div>
            <div class="main5 tool"></div>
            <div class="main6 tool"></div>
          </div>
        </div>
        <div class="title">{{ item.title }}</div>
      </div>
    </WordCloudRandom>
  </div>
</template>
​
<script setup lang="tsx">
import {
  onBeforeMount,
  onBeforeUnmount,
  onMounted,
  watch,
  watchEffect
} from 'vue'
import WordCloudRandom from '@/components/WordCloudRandom/index.vue'
​
const indiArr = ref([
    {
        title: '清运车数据',
        type: 'four',
        color: 'color2'
    },
    {
        title: '集治点数据',
        type: 'four',
        color: 'color2'
    },
    {
        title: '投放点数据',
        type: 'four',
        color: 'color3'
    },
    {
        title: '巡查/问题既记录',
        type: 'four',
        color: 'color3'
    },
    {
        title: '网格员信息',
        type: 'four',
        color: 'color4'
    },
​
    {
        title: '垃圾箱满溢算法',
        type: 'six',
        color: 'color1'
    },
    {
        title: '垃圾桶不规范算法',
        type: 'six',
        color: 'color1'
    },
    {
        title: '打包垃圾检测算法',
        type: 'six',
        color: 'color1'
    },
​
    {
        title: '推送服务',
        type: 'three',
        color: 'color6'
    },
    {
        title: '算力资源',
        type: 'zero',
        color: 'color5'
    }
])
</script>
​
<style lang="less" scoped>
.hexagon {
  width: 1340px;
  height: 1440px;
  background-image: url(@/assets/images/page1/center-grid.png);  //背景图
  background-repeat: no-repeat;
  background-size: 1340px 1440px;
  padding: 300px 50px;
  transition: 0.8s;
​
  .wrapper {
    position: relative;
    width: 100%;
    height: 100%;
​
    .item {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 208px;
      height: 263px;
      background-image: url(@/assets/images/page1/bg-hexagon.svg); //背景图
      background-repeat: no-repeat;
      background-size: 100% 100%;
​
      .shape {
        position: absolute; // 父元素要加上position: relative;
        top: 70px;
        left: 50%;
        transform: translate(-50%, -50%);
        margin-bottom: 10px;
        // 圆形
        &.zero {
          width: 33px;
          height: 33px;
          border-radius: 50%;
          // background-color: red;
​
          &.color1 {
            background-color: #d64c3b;
          }
          &.color2 {
            background-color: #ffd943;
          }
          &.color3 {
            background-color: #ffa328;
          }
          &.color4 {
            background-color: #46c2ff;
          }
          &.color5 {
            background-color: #8f0eb8;
          }
          &.color6 {
            background-color: #232cff;
          }
        }
        // 三角形
        &.three {
          top: 75px;
          left: 33%;
          width: 0;
          height: 0;
          // border: 40px solid red;
          // border-color: red transparent transparent transparent;
          transform-origin: 50% 0 0;
          transform: scaleX(0.6) rotateX(180deg);
​
          &.color1 {
            border: 40px solid #d64c3b;
            border-color: #d64c3b transparent transparent transparent;
          }
          &.color2 {
            border: 40px solid #ffd943;
            border-color: #ffd943 transparent transparent transparent;
          }
          &.color3 {
            border: 40px solid #ffa328;
            border-color: #ffa328 transparent transparent transparent;
          }
          &.color4 {
            border: 40px solid #46c2ff;
            border-color: #46c2ff transparent transparent transparent;
          }
          &.color5 {
            border: 40px solid #8f0eb8;
            border-color: #8f0eb8 transparent transparent transparent;
          }
          &.color6 {
            border: 40px solid #232cff;
            border-color: #232cff transparent transparent transparent;
          }
        }
        // 正方形
        &.four {
          width: 33px;
          height: 33px;
          // background-color: red;
          &.color1 {
            background-color: #d64c3b;
          }
          &.color2 {
            background-color: #ffd943;
          }
          &.color3 {
            background-color: #ffa328;
          }
          &.color4 {
            background-color: #46c2ff;
          }
          &.color5 {
            background-color: #8f0eb8;
          }
          &.color6 {
            background-color: #232cff;
          }
        }
        // 六边形
        &.six {
          left: 46%;
          .six-wrapper {
            transform: scale(0.3);
            .main2 {
              transform: rotate(60deg);
            }
            .main3 {
              transform: rotate(120deg);
            }
            .main4 {
              transform: rotate(180deg);
            }
            .main5 {
              transform: rotate(240deg);
            }
            .main6 {
              transform: rotate(300deg);
            }
            .tool {
              width: 0px;
              height: 0px;
              border-right: calc(60px / 1.732) solid transparent;
              border-left: calc(60px / 1.732) solid transparent;
              // border-bottom: 60px solid red;
              transform-origin: top;
              position: absolute;
              top: 0;
              left: 0;
            }
          }
          &.color1 {
            .tool {
              border-bottom: 60px solid #d64c3b;
            }
          }
          &.color2 {
            .tool {
              border-bottom: 60px solid #ffd943;
            }
          }
          &.color3 {
            .tool {
              border-bottom: 60px solid #ffa328;
            }
          }
          &.color4 {
            .tool {
              border-bottom: 60px solid #46c2ff;
            }
          }
          &.color5 {
            .tool {
              border-bottom: 60px solid #8f0eb8;
            }
          }
          &.color6 {
            .tool {
              border-bottom: 60px solid #232cff;
            }
          }
        }
      }
​
      .title {
        position: absolute;
        top: 40%;
        // left: 0%;
        width: 100%;
        padding: 0 30px;
        font-size: 32px;
        font-family: PingFangSC-Semibold, PingFang SC;
        font-weight: 600;
        color: #ffffff;
        line-height: 45px;
        text-shadow: 0px 0px 10px rgba(30, 198, 255, 0.8);
        text-align: center;
      }
    }
  }
}
</style>
​

大功告成🎉🎉🎉

相关推荐
黑客老陈38 分钟前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安43 分钟前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
编程百晓君1 小时前
一文解释清楚OpenHarmony面向全场景的分布式操作系统
vue.js
暴富的Tdy1 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se1 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235611 小时前
web 渗透学习指南——初学者防入狱篇
前端
℘团子এ1 小时前
js和html中,将Excel文件渲染在页面上
javascript·html·excel
z千鑫1 小时前
【前端】入门指南:Vue中使用Node.js进行数据库CRUD操作的详细步骤
前端·vue.js·node.js