Vue组件中使用canvas实现蜂巢效果的一些尝试

16 篇文章 0 订阅
7 篇文章 0 订阅

Vue组件中使用canvas实现蜂巢效果的一些尝试


  前段时间,看到D3.js的官方网站的蜂巢效果,感觉不错,不过一直没有时间去实际的实现下,借这次机会,算是填了前面的坑~~,先来看看d3.js的效果图:
在这里插入图片描述
  近期看到一篇在Vue组件中使用canvas实现蜂巢效果的文章,抱着试一试的想法,对文章中的实现进行了复现。虽然实现上和d3存在区别,但是条条大路通罗马,基本上实现了效果:

蜂巢效果
实现过程:

  • 使用canvas,绘制效果。两个canvas元素,一个用于绘制图标,一个用于绘制蜂巢,并设置交互事件
  • canvas相关的属性设置,背景渐变等
  • 使用的是Vue的组件方式,内部的图片使用了前段时间用到的一些svg格式的图片
  • 将代码中存在的问题进行一一解决,解决运行报错,添加了data属性,以及实现逻辑进行了修改
  • tips:感谢原文章作者的热心回复

存在缺陷

  • 蜂巢的每个六边形的位置、大小等还不能通过简单的配置即可达到预期的效果,往往需要实际进行微调
  • 改变画布的容器大小后,蜂巢的大小并未相应的改变,实际的观感不佳
  • 针对边缘的空白图片,目前使用了透明的图片来占位,感觉实现方式不佳(希望对空白的蜂巢,可以不设置icon或者设置为空,既可渲染,但并未实现),但限于时间关系,对此并未深究。算是遗憾吧!!!

  下面是完整的组件代码,有想要查看效果的,直接复制到vue项目中,替换到图片地址既可(代码较low,勿喷~~~):

<template>
  <div class="canvas_box" id="canvas_box">
    <div class="content_box" id="content_box">
      <div
        style="position: relative;margin-left: 50%;margin-right: 50%;"
        :width="cWidth"
        :height="cHeight"
        :style="'left:' +  (- cWidth / 2 ) + 'px'"
      >
        <canvas ref="mycanvas" :width="cWidth" :height="cHeight"></canvas>
        <canvas
          ref="canvas"
          :width="cWidth"
          :height="cHeight"
          :style="isBtn ? 'cursor: pointer' : 'cursor: default'"
          style="position: absolute;left: 0;top: 0;z-index: 999"
          @mousedown="canvasDown($event)"
          @mouseup="canvasUp($event)"
          @mousemove="canvasMove($event)"
        ></canvas>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'canvasHive',
  data () {
    return {
      cWidth: null,
      cHeight: null,
      isBtn: false,
      buttons: [],
      isMoveDown: null,
      mycanvas: null,
      canvas: null,
      cIsLoad: false,
      ctx: null,
      hctx: null,
      hiveRadius: 0
    }
  },
  mounted () {
    this.cWidth = document.getElementById('content_box').clientWidth
    this.cHeight = document.getElementById('content_box').clientHeight
    this.hiveRadius = Math.floor(this.cWidth / 10 - this.cHeight / 10)
    console.log(this.hiveRadius)
    let that = this
    that.drawBtnInit(this.hiveRadius)

    setTimeout(function () {
      that.cIsLoad = true
    }, 1000)
    // 添加对窗口大小的监听,同时需要调用绘制函数
    window.addEventListener('resize', function () {
      that.cWidth = document.getElementById('content_box').clientWidth
      that.cHeight = document.getElementById('content_box').clientHeight
      that.ctx.fillRect(0, 0, that.cWidth, that.cHeight)
      that.hctx.fillRect(0, 0, that.cWidth, that.cHeight)
      that.hiveRadius = Math.floor(that.cWidth / 10 - that.cHeight / 10)
      that.drawBtnInit(that.hiveRadius)
    })
  },
  methods: {
    // 绘制按钮函数
    drawBtnInit () {
      const that = this
      that.mycanvas = that.$refs.mycanvas // 指定canvas
      that.ctx = that.mycanvas.getContext('2d') // 设置2D渲染区域
      that.canvas = that.$refs.canvas // 指定canvas
      that.hctx = that.canvas.getContext('2d') // 设置2D渲染区域

      // 修改数据,简化代码
      let btnsArray = [
        [
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
        ],
        [
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 0, y: 20 }, isRealBtn: false },
          // { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/qqcutTect.png'), offset: { x: 0, y: 20 } },
          { text: 'xx', tag: {}, path: 'Index', icon: require('../../assets/svg-for-middleware/Elasticsearch.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'Integrated', icon: require('../../assets/svg-for-middleware/Kafka.svg'), offset: { x: 50, y: 20 }, isRealBtn: true }, // 图标原图不是居中,补偿xy
          { text: 'xx', tag: {}, path: 'KeyManagement', icon: require('../../assets/svg-for-middleware/MySQL (1).svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'Security', icon: require('../../assets/svg-for-middleware/Spark 2.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'Contradiction', icon: require('../../assets/svg-for-middleware/Zookeeper (1).svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/logstash.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/neo4j.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 0, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 0, y: 20 }, isRealBtn: false },
        ],
        [
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: 'xx', tag: {}, path: 'myProtectionLineX', icon: require('../../assets/svg-for-middleware/spring.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'School', icon: require('../../assets/svg-for-middleware/nginx.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'OA', icon: require('../../assets/svg-for-middleware/redis (1).svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/spring-logo.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/表格.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: 'xx', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/MySQL.svg'), offset: { x: 50, y: 20 }, isRealBtn: true },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
        ],
        [
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
          { text: '', tag: {}, path: 'x', icon: require('../../assets/svg-for-middleware/透明图片.png'), offset: { x: 50, y: 20 }, isRealBtn: false },
        ]
      ]
      let baseX
      let baseY
      for (let i = 0, iLen = btnsArray.length; i < iLen; i++) {
        for (let j = 0, jLen = btnsArray[i].length; j < jLen; j++) {
          if (i % 2 === 0) {
            baseX = -50 + j * 193
          } else {
            baseX = 50 + j * 193
          }
          baseY = 50 + i * 170
          that.btnAdd(baseX, baseY, btnsArray[i][j].text, btnsArray[i][j].tag, btnsArray[i][j].path, btnsArray[i][j].icon, btnsArray[i][j].offset, btnsArray[i][j].isRealBtn)
        }
      }


    },
    drawImage (x, y, w, h, icon, onSuccess) {
      let _this = this
      // 创建新的图片对象
      let img = new Image()
      // 指定图片的URL

      // img.src = imgSrc
      img.src = icon // 
      // 浏览器加载图片完毕后再绘制图片
      // _this.ctx.drawImage(img, x, y, w, h)
      img.onload = function () {
        // 以Canvas画布上的坐标( x, y)为起始点,绘制图像

        if (onSuccess) {
          onSuccess()
        }
      }
    },
    drawIcon (x, y, maxX, maxH, icon, offset) {
      // console.log(offset)
      let _this = this
      // 创建新的图片对象
      let img = new Image()
      // 指定图片的URL
      img.src = icon
      // 浏览器加载图片完毕后再绘制图片
      img.onload = function () {
        // 以Canvas画布上的坐标( x, y)为起始点,绘制图像
        let w = img.width
        let h = img.height
        if (maxX > img.width) {
          x = x + (maxX - img.width) / 2
        } else {
          w = maxX
        }
        if (maxH > img.height) {
          y = y + (maxH - img.height) / 2
        } else {
          h = maxH
        }
        debugger
        _this.ctx.drawImage(img, x + offset.x, y + offset.y, w, h)

      }
    },
    drawText (x, y, text) {
      let _this = this
      _this.ctx.font = '18px bold 黑体'
      // 设置颜色
      _this.ctx.fillStyle = 'green'
      // 设置水平对齐方式
      _this.ctx.textAlign = 'center'
      // 设置垂直对齐方式
      _this.ctx.textBaseline = 'middle'
      // 绘制文字(参数:要写的字,x坐标,y坐标)

      _this.ctx.fillText(text, x, y)
    },
    drawlowLevelPath (x, y, n, r) {
      let i, ang
      ang = Math.PI * 2 / n // 旋转的角度
      this.ctx.save() // 保存状态
      // this.hctx.fillStyle = 'cyan' // 填充红色,半透明 阴影效果
      // this.ctx.strokeStyle = 'green' // 填充边框色
      // this.hctx.fillStyle = 'rgba(255,255,255,0.1)' // 填充红色,半透明 阴影效果
      this.ctx.fillStyle = 'transparent' // 填充红色,半透明 阴影效果
      this.ctx.strokeStyle = 'rgba(28, 182, 255, .3)' // 填充边框色
      this.ctx.lineWidth = 6 // 设置线宽
      this.ctx.translate(x, y) // 原点移到x,y处,即要画的多边形中心
      this.ctx.moveTo(0, -r) // 据中心r距离处画点
      this.ctx.beginPath()
      for (i = 0; i < n; i++) {
        this.ctx.rotate(ang) // 旋转
        this.ctx.lineTo(0, -r) // 据中心r距离处连线
      }
      this.ctx.closePath()
      this.ctx.stroke()
      this.ctx.fill()
      this.ctx.restore() // 返回原始状态

    },
    drawLinearGradient () {
      let grd = this.ctx.createLinearGradient(0, 0, this.cWidth, 0);
      grd.addColorStop(0, "white");
      grd.addColorStop(0.5, "#88ded2");
      grd.addColorStop(1, "white");

      // 填充渐变
      this.ctx.fillStyle = grd;
      this.ctx.fillRect(0, 0, this.cWidth, this.cHeight);
    },
    drawPath (x, y, n, r) {
      let i, ang
      ang = Math.PI * 2 / n // 旋转的角度
      this.hctx.save() // 保存状态
      this.hctx.fillStyle = 'rgba(255,255,255,0.3)' // 填充红色,半透明 阴影效果
      this.hctx.strokeStyle = 'rgba(23, 97, 105, .8)' // 填充边框色
      this.hctx.lineWidth = 6 // 设置线宽
      this.hctx.translate(x, y) // 原点移到x,y处,即要画的多边形中心
      this.hctx.moveTo(0, -r) // 据中心r距离处画点
      this.hctx.beginPath()
      for (i = 0; i < n; i++) {
        this.hctx.rotate(ang) // 旋转
        this.hctx.lineTo(0, -r) // 据中心r距离处连线
      }
      this.hctx.closePath()
      this.hctx.stroke()
      this.hctx.fill()
      this.hctx.restore() // 返回原始状态
    },
    getBtnObj (e) {
      let offsetX = e.offsetX
      let offsetY = e.offsetY
      if (e.type === 'touchstart' || e.type === 'touchend') {
        // touch 没有 offsetX Y
        let tX = e.changedTouches[0].clientX
        let tY = e.changedTouches[0].clientY
        let rect = this.canvas.getBoundingClientRect()
        offsetX = tX - rect.x
        offsetY = tY - rect.y
      }
      if (e.type !== 'mousemove') {
        console.log('=====修正offsetX=', offsetX)
        console.log('=====修正offsetY=', offsetY)
      }
      let obj = null
      for (let i = 0; i < this.buttons.length; i++) {
        // 判断在哪个个button
        let find = true
        for (let j = 0; j < this.buttons[i].points.length; j++) {
          // 如果不是最后一个,和下一个点组合,如果是最后一个,和第一个组合
          let res = false
          if (j === this.buttons[i].points.length - 1) {
            res = this.judgeIntersect(offsetX, offsetY, this.buttons[i].center.x, this.buttons[i].center.y, this.buttons[i].points[j].x, this.buttons[i].points[j].y, this.buttons[i].points[0].x, this.buttons[i].points[0].y)
          } else {
            res = this.judgeIntersect(offsetX, offsetY, this.buttons[i].center.x, this.buttons[i].center.y, this.buttons[i].points[j].x, this.buttons[i].points[j].y, this.buttons[i].points[j + 1].x, this.buttons[i].points[j + 1].y)
          }
          if (res) {
            find = false
            break
          }
        }
        if (find) {
          // console.log('================' + i)
          obj = this.buttons[i]
          break
        }
      }
      return obj
    },
    // 判断两条线是否相交
    judgeIntersect (x1, y1, x2, y2, x3, y3, x4, y4) {
      if (!(Math.min(x1, x2) <= Math.max(x3, x4) && Math.min(y3, y4) <= Math.max(y1, y2) && Math.min(x3, x4) <= Math.max(x1, x2) && Math.min(y1, y2) <= Math.max(y3, y4))) {
        return false
      }
      let u, v, w, z
      u = (x3 - x1) * (y2 - y1) - (x2 - x1) * (y3 - y1)
      v = (x4 - x1) * (y2 - y1) - (x2 - x1) * (y4 - y1)
      w = (x1 - x3) * (y4 - y3) - (x4 - x3) * (y1 - y3)
      z = (x2 - x3) * (y4 - y3) - (x4 - x3) * (y2 - y3)
      return (u * v <= 0.00000001 && w * z <= 0.00000001)
    },
    btnAdd (baseX, baseY, text, tag, path, icon, offset, isRealBtn) {
      let _this = this
      this.drawImage(baseX, baseY, 193, 221, icon, () => {
        _this.drawLinearGradient()
        setTimeout(() => {

          _this.drawIcon(baseX, baseY, 80, 80, icon, offset)
          _this.drawText(baseX + 90.5, baseY + 135, text)
          _this.drawlowLevelPath(baseX + 95, baseY + 75, 6, 102)
        }, 500)

      })
      this.buttons.push({
        text: text,
        tag: tag,
        path: path,
        basePoint: { x: baseX, y: baseY },
        center: { x: baseX + 96.5, y: baseY + 110.5 },
        points: [{ x: baseX + 1, y: baseY + 56 }, { x: baseX + 96.5, y: baseY + 1 }, { x: baseX + 193, y: baseY + 96.5 }, { x: baseX + 193, y: baseY + 162 }, { x: baseX + 96.5, y: baseY + 221 }, { x: baseX + 1, y: baseY + 162 }],
        isRealBtn // 增加一个属性,由于标记是否是真实的按钮
      })
    },
    // 鼠标移动时绘制
    canvasMove (e) {
      if (this.cIsLoad) {
        let btn = this.getBtnObj(e)
        if (btn) {
          this.isBtn = true
          this.hctx.clearRect(0, 0, this.cWidth, this.cHeight)
          // if (!btn.isRealBtn) { return }
          this.drawPath(btn.center.x, btn.center.y - 36, 6, 102) // 画一个六边形
        } else {
          this.isBtn = false
          this.hctx.clearRect(0, 0, this.cWidth, this.cHeight)
        }
      }
    },
    // 鼠标抬起
    canvasUp (e) {
      // console.log('=======canvasUp=======', e)
      let btn = this.getBtnObj(e)
      if (btn) {
        this.menuClick(btn.text, btn.path)
      }
      // console.log('=======canvasUp=======', btn)
      this.isMoveDown = false
    },
    menuClick (text, path) {
      console.log(text)
    },
    canvasDown (e) {
      console.log(e)
    }
  }

}
</script>

<style  scoped>
.canvas_box {
  background-color: #f5f5f5;
}
.content_box {
  margin: 10px auto;
  height: 97%;
  box-shadow: 2px 2px 5px 5px #999;
  width: 98%;
  background-color: #ffffff;
  overflow: hidden;
}
</style>

  至此就基本上实现了效果。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值