canvas在前端开发中的实践

1,592 阅读10分钟

1.什么是canvas

Canvas 是 HTML5 中新出的一个元素,开发者可以在上面绘制一系列图形

canvas的画布是一个矩形区域,可以控制其每一像素

canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法

Canvas提供的功能更适合像素处理,动态渲染和大数据量绘制

2.canvas的基本用法

2.1渲染上下文:

<canvas> 元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,其可以用来绘制和处理要展示的内容。


<canvas id="chart1" width="600" height="600"></canvas>
const chart1 = document.getElementById("chart1")
let ctx = chart1.getContext('2d')

2.2 基本绘制

beginPath()新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。

closePath()闭合路径之后图形绘制命令又重新指向到上下文中。

stroke()通过线条来绘制图形轮廓。

fill()通过填充路径的内容区域生成实心的图形。

lineTo(x, y)绘制一条从当前位置到指定x以及y位置的直线

arc(x, y, radius, startAngle, endAngle, anticlockwise)画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成

fillStyle = color设置图形的填充颜色。

strokeStyle = color设置图形轮廓的颜色。

createLinearGradient(x1, y1, x2, y2)createLinearGradient 方法接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)


 // 三角
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(50, 10);
ctx.lineTo(50, 50);
ctx.lineTo(10, 10);
ctx.strokeStyle = "#b18ea6"
ctx.lineWidth = 2
ctx.stroke();
ctx.closePath()
// 矩形
ctx.fillStyle = "#00f"
ctx.fillRect(70, 10, 40 ,40)
// 扇形
ctx.beginPath();
ctx.moveTo(130, 10)
ctx.arc(130, 10, 40, Math.PI / 4, Math.PI / 2, false)
let grd = ctx.createLinearGradient(130, 10, 150, 50);
grd.addColorStop(0, "#f00")
grd.addColorStop(1, "#0ff")
ctx.fillStyle = grd
ctx.fill()


fillText(text, x, y [, maxWidth])在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的.

strokeText(text, x, y [, maxWidth])在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的.


ctx.font = "32px serif";
ctx.textAlign = "center"
ctx.fillText("canvas", 100, 100);
ctx.strokeText("canvas", 100, 130);


2.3 canvas实践

2.3.1 饼图

绘制title,使用绘制文字api


ctx.font = "28px serif";
ctx.textAlign = "center"
ctx.fillText(option.title, 300, 30);

绘制图例,使用绘制文字和矩形的方法


for(..data..) {
    ctx.fillRect (preLegend, 50, 30, 20)
  preLegend += 40   // 偏移量
  ctx.font = "20px serif";
  ctx.fillText(item.name, preLegend, 78);
  preLegend +=  ctx.measureText(item.name).width + 16 // measureText返回传入字符串的渲染尺寸
}


使用上述绘制扇形的方法绘制饼图中每个元素的比例


option.data.map((item, index) => {
    ctx.beginPath()
    ctx.moveTo(300, 300)
    let start = Math.PI * 2 * preArc // preArc角度偏移量
    let end = Math.PI * 2 * (preArc + item.num / total)
    ctx.arc(300, 300, 200, start, end, false)
    ctx.fillStyle = defaultColor[index]
    ctx.fill()
    preArc += item.num / total
})


为每个扇形元素添加名称和数量,连接圆心和对应扇形圆弧中心并延长,绘制元素名称和数量即可


for(..data..) {
  ctx.font = "16px serif";
  let lineEndx = Math.cos(start + (end - start) / 2) * 230 + 300 // 三角函数计算下坐标,每个扇形圆弧中间
  let lineEndy = Math.sin(start + (end - start) / 2) * 230 + 300
  // 中轴右边延线向右,左边延线向左
  let lineEndx2 = lineEndx > 300 ? lineEndx + ctx.measureText(`${item.name}: ${item.num}`).width : lineEndx - ctx.measureText(`${item.name}: ${item.num}`).width
  ctx.moveTo(300, 300)
  ctx.lineTo(lineEndx, lineEndy)
  ctx.lineTo(lineEndx2, lineEndy)
  ctx.strokeStyle = defaultColor[index]
  ctx.stroke()
  ctx.closePath()
  ctx.fillText(`${item.name}: ${item.num}`, lineEndx2, lineEndy)
}


2.3.2 折线柱状图

绘制坐标轴线,两条垂直线段


  ctx.strokeStyle = '#ccc'
  ctx.beginPath()
  ctx.moveTo(50, 100)
  ctx.lineTo(50, 350)
  ctx.lineTo(550, 350)
  ctx.stroke()
  ctx.closePath()

绘制柱状图,计算坐标,渲染对应高度的矩形即可


option.data.map((item, index) => {
  let x = 80 + index * 60
  let y = 350 - item.num/total * 500 * 2
  let w = 30
  let h = item.num/total * 500 * 2
  ctx.fillStyle = defaultColor[index]
  ctx.fillRect(x,y,w,h)
})


绘制折线,使用画线api连接每个矩形上边中线即可,这里使用了渐变效果


if(posList.length > 1) {
  ctx.beginPath()
  ctx.moveTo(posList[index - 1].x + w / 2, posList[index - 1].y)
  ctx.lineTo(x + w / 2, y)
  let grd = ctx.createLinearGradient(posList[index - 1].x + w / 2, posList[index - 1].y, x + w / 2, y);
  grd.addColorStop(0,defaultColor[index - 1])
  grd.addColorStop(1,defaultColor[index])
  ctx.strokeStyle = grd
  ctx.lineWidth = 3
  ctx.stroke()
  ctx.closePath()
}  

绘制坐标轴和元素对应的坐标,以每个元素柱状的高度画虚线至y轴,并绘制数值;元素下方绘制元素名称



ctx.beginPath()
ctx.moveTo(x + w / 2, y)
ctx.lineTo(50, y)
ctx.setLineDash([4]) // 使用虚线
ctx.strokeStyle = defaultColor[index]
ctx.lineWidth = 0.5
ctx.stroke()
ctx.closePath()
ctx.font = "20px serif";
ctx.fillText(item.num, 35, y + 8);
ctx.font = "20px serif";
ctx.fillText(item.name, x + w / 2, 370);


2.3.3 水印

后台系统中很多都有接入水印,这里使用canvas实践一次


rotate 方法,它用于以原点为中心旋转 canvas。

rotate(angle)这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。

首先绘制一个水印模板


let watermarkItem = document.createElement("canvas")
watermarkItem.id = "watermark-item"
watermarkItem.width = 160
watermarkItem.height = 160
document.body.appendChild(watermarkItem)

let ctxitem = watermarkItem.getContext('2d')
ctxitem.font = "28px serif"
ctxitem.textAlign = "center"
ctxitem.save()
ctxitem.rotate(-Math.PI/4)
ctxitem.fillText("canvas", 0, 110)
ctxitem.restore()


以该模板渲染整个浏览器窗口即可

createPattern() 方法在指定的方向内重复指定的元素。

元素可以是图片、视频,或者其他 <canvas> 元素。

被重复的元素可用于绘制/填充矩形、圆形或线条等等。


  let pat = ctx.createPattern(item, "repeat")
  ctx.fillStyle = pat
  ctx.fillRect(0, 0, watermark.width, watermark.height)

接下来隐藏第一个模板,并监听浏览器窗口的变化,窗口变化时重新渲染水印覆盖窗口


window.onload = createWatermark
window.onresize = drawWatermark
function createWatermark() {
  let watermarkItem = document.createElement("canvas")
  let watermark = document.createElement("canvas")
  watermarkItem.id = "watermark-item"
  watermarkItem.width = 160
  watermarkItem.height = 160
  watermarkItem.style.display = "none"
  watermark.id = "watermark"
  watermark.width = window.innerWidth
  watermark.height = window.innerHeight
  document.body.appendChild(watermark)
  document.body.appendChild(watermarkItem)
  drawWatermark()
}

function drawWatermark () {
  let item = document.getElementById("watermark-item")
  let watermark = document.getElementById("watermark")
  watermark.id = "watermark"
  watermark.width = window.innerWidth
  watermark.height = window.innerHeight
  let ctxitem = item.getContext('2d')
  let ctx = watermark.getContext('2d')
  ctxitem.font = "28px serif"
  ctxitem.textAlign = "center"
  ctxitem.save()
  ctxitem.rotate(-Math.PI/4)
  ctxitem.fillText("canvas", 0, 110)
  ctxitem.restore()
  let pat = ctx.createPattern(item, "repeat")
  ctx.fillStyle = pat
  ctx.fillRect(0, 0, watermark.width, watermark.height)
}

3.3.4 树的应用

这里使用canvas绘制一棵树,并具备拖拽、增删改等功能

首先遍历树的节点,绘制树的节点

这里对每层节点都设置不同的高度;对每个节点计算应占有的x轴空间,子节点平分父节点所占空间


递归遍历中 
datalist.map((item, index) => {
  if(p.children) {
    // x1,x2为占用x轴空间坐标
    item.x1 = p.x1 + (p.x2 - p.x1) * index / p.children.length
    item.x2 = p.x1 + (p.x2 - p.x1) * (index + 1) / p.children.length
    item.x = item.x1 + (item.x2 - item.x1) / 2
    item.y = item.level * 100
  } else {
    item.x = 375
    item.y = item.level * 100
    item.x1 = 0
    item.x2 = 800
  }      
  ctx.fillStyle = defaultColor[item.level]
  ctx.fillRect(item.x, item.y, 50, 25)     
}


然后连接子节点与父节点之间,并填写每个节点的信息


ctx.fillStyle = "#000"
ctx.font = "20px serif";
ctx.textAlign = "center"
ctx.fillText(`${item.name}${item.num}`, ix + 25, iy + 20);
ctx.beginPath()
ctx.moveTo(item.x + 25, item.y)
ctx.lineTo(p.x + 25, p.y + 25)
ctx.stroke()
ctx.closePath()


接下来完成拖拽功能,首先绑定事件,并获得mousedown事件点击在canvas画板上的坐标,判断是否点击坐标在树的节点上,点击的是哪个节点

如果点击在节点上,则监听mousemove事件,让节点跟随鼠标移动(通过计算新的节点坐标,并重绘)

监听到mouseup事件,即代表拖拽事件结束,清除掉mouseover事件即可


let move = (e) => {
  ctx.clearRect(0,0,800,1000)
  this.drawTree(this.datalist, {}, ctx, {
    id,
    curX: e.clientX  - chart3.getBoundingClientRect().left,
    curY: e.clientY  - chart3.getBoundingClientRect().top
  })
} 
chart3.addEventListener('mousedown', (e) => {
  // 计算点击在canvas上的坐标
  curX = e.clientX  - chart3.getBoundingClientRect().left
  curY = e.clientY  - chart3.getBoundingClientRect().top
  this.rectList.forEach(item => {
    // 判断点击在矩形上 
    if(curX >= item.x1 && curX <= item.x2 && curY >= item.y1 && curY <= item.y2) {
      id = item.id
      this.current = item
      chart3.addEventListener('mousemove', move, false)
    }
  })
}, false)
chart3.addEventListener('mouseup', (e) => {
  chart3.removeEventListener('mousemove', move)
}, false)

// 重绘节点
if(item.curX) {
  ctx.fillRect(item.curX, item.curY, 50, 25)
} else {
  ctx.fillRect(item.x, item.y, 50, 25)          
}
ctx.fillStyle = "#000"
ctx.font = "20px serif";
ctx.textAlign = "center"
let ix = item.curX ? item.curX : item.x
let iy = item.curY ? item.curY : item.y
let px = p.curX ? p.curX : p.x
let py = p.curY ? p.curY : p.y
ctx.fillText(`${item.name}:${item.num}`, ix + 25, iy + 20);
// 重新连线
ctx.beginPath()
ctx.moveTo(ix + 25, iy)
ctx.lineTo(px + 25, py + 25)


接下来为节点添加增删改的功能,右键点击节点时弹出操作菜单


// 清除默认右键事件
document.oncontextmenu = function(e){
  e.preventDefault();
};

chart3.addEventListener('mousedown', (e) => {
。。。。判断点击在某个节点上
if(e.button ==2){   // 右键点击
  this.outerVisible = true // 弹出菜单
  return
 }
})

这里使用element简单做一个表单


删除操作:

在遍历节点的时候,判断节点的id和需要删除的节点的id一致时,将该节点置空,并重新渲染即可

修改操作:

在遍历节点的时候,判断节点的id和需要修改的节点的id一致时,将该节点的数据设置为新的,并重新渲染

新增节点:

在遍历节点的时候,判断节点的id和需要新增子节点的id一致时,在该节点树children中新增对应的数据,并重新渲染


// 修改节点
edit(datalist) {
  datalist.map((item, index) => {
    if(item.id == this.current.id) {
      item.num = this.num
      item.name = this.name
      const chart3 = document.getElementById("chart3")
      let ctx = chart3.getContext('2d')
      // 重新渲染
      ctx.clearRect(0,0,800,1000)
      this.drawTree(this.datalist, {}, ctx)
      return
    }
    if(item.children) {
      this.edit(item.children)
    }
  })
}



源代码

<template>
  <div class="hello">
    <canvas id="chart1" width="600" height="600"></canvas>
    <canvas id="chart2" width="600" height="600"></canvas>
    <canvas id="chart3" width="800" height="1000"></canvas>
  
    <el-dialog title="操作" :visible.sync="outerVisible">
      <el-button-group>
        <div>id: {{current.id}}</div>
        <el-button type="primary" @click="oprNode(1)">增加节点</el-button>
        <el-button type="primary" @click="deleteNode(datalist, current.id)">删除节点</el-button>
        <el-button type="primary" @click="oprNode(2)">修改节点</el-button>
      </el-button-group>
      <el-dialog
        width="30%"
        title=""
        :visible.sync="innerVisible"
        append-to-body>
        <el-form ref="form" label-width="80px">
          <el-form-item label="id">
            <el-input v-model="id"></el-input>
          </el-form-item>
          <el-form-item label="name">
            <el-input v-model="name"></el-input>
          </el-form-item>
          <el-form-item label="num">
            <el-input v-model="num"></el-input>
          </el-form-item>
          <el-button @click="submit">提交</el-button>
        </el-form>
    </el-dialog>
  </el-dialog>
  </div>
</template>

<script>
// import * as tf from '@tensorflow/tfjs';

export default {
  name: 'Tensor',
  data() {
    return {
      rectList: [],
      outerVisible: false,
      innerVisible: false,
      current: {
        id: ''
      },
      type: '',
      id: '',
      num: '',
      name: '',
      datalist: [{
        name: "A",
        num: 400,
        level: 1,
        id: 1,
        children: [
          {
            name: "B",
            num: 300,
            level: 2,
            id: 2,
            children: [
              {
                name: "D",
                num: 150,
                level: 3,
                id: 3,
                children: [
                  {
                    name: "I",
                    num: 70,
                    level: 4,
                    id: 4,
                  },
                  {
                    name: "J",
                    num: 80,
                    level: 4,
                    id: 5,
                  },
                ]
              },
              {
                name: "E",
                num: 50,
                level: 3,
                id: 6,
              },
              {
                name: "F",
                num: 100,
                level: 3,
                id: 7,
              },
            ]
          },
          {
            name: "C",
            num: 100,
            level: 2,
            id: 8,
            children: [
              {
                name: "G",
                num: 20,
                level: 3,
                id: 9
              },
              {
                name: "H",
                num: 80,
                level: 3,
                id: 10
              },
            ]
          },
        ]
      }]
    }
  },
  mounted() {
    this.init()
    this.initTree()
    document.oncontextmenu = function(e){
      e.preventDefault();
    };
  },
  methods: {
    init() {
      // const model = tf.sequential();
      // model.add(tf.layers.dense({units: 1, inputShape: [1]}));
      // model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});
      // const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]);
      // const ys = tf.tensor2d([1, 3, 5, 7], [4, 1]);
      // model.fit(xs, ys).then(() => {
      //   model.predict(tf.tensor2d([5], [1, 1])).print();
      // });
      let data = [{
          name: 'A',
          num: 80,
        },{
          name: 'B',
          num: 60,
        },{
          name: 'C',
          num: 20,
        },{
          name: 'D',
          num: 40,
        },{
          name: 'E',
          num: 30,
        },{
          name: 'F',
          num: 70,
        },{
          name: 'G',
          num: 60,
        },{
          name: 'H',
          num: 40,
      }]
      const chart1 = document.getElementById("chart1")
      const chart2 = document.getElementById("chart2")
      this.createChart(chart1, {
        title: "canvas饼图:各阶段比例",
        type: "pie",
        data: data
      })
      this.createChart(chart2, {
        title: "canvas折线柱状图",
        type: "bar",
        data: data
      })
    },
    createChart(canvas, option) {
      const defaultColor = ["#ff8080", "#b6ffea", "#fb0091", "#fddede", "#a0855b", "#b18ea6", "#ffc5a1", "#08ffc8"]
      let ctx = canvas.getContext('2d')
      ctx.font = "28px serif";
      ctx.textAlign = "center"
      ctx.fillText(option.title, 300, 30);
     
      let total = option.data.reduce((pre, a) => {
        return pre + a.num
      }, 0)
      let preArc = 0
      let preLegend = 40
      // 饼图
      if(option.type === "pie") {
        ctx.arc(300,300,210,0,Math.PI*2,false)
        ctx.strokeStyle = '#ccc'
        ctx.stroke()
        ctx.closePath()
        option.data.map((item, index) => {
          ctx.beginPath()
          ctx.moveTo(300, 300)
          let start = Math.PI * 2 * preArc
          let end = Math.PI * 2 * (preArc + item.num / total)
          ctx.arc(300, 300, 200, start, end, false)
          ctx.fillStyle = defaultColor[index]
          ctx.fill()
          ctx.font = "16px serif";
          let lineEndx = Math.cos(start + (end - start) / 2) * 230 + 300
          let lineEndy = Math.sin(start + (end - start) / 2) * 230 + 300
          let lineEndx2 = lineEndx > 300 ? lineEndx + ctx.measureText(`${item.name}: ${item.num}`).width : lineEndx - ctx.measureText(`${item.name}: ${item.num}`).width
          ctx.moveTo(300, 300)
          ctx.lineTo(lineEndx, lineEndy)
          ctx.lineTo(lineEndx2, lineEndy)
          ctx.strokeStyle = defaultColor[index]
          ctx.stroke()
          ctx.closePath()
          ctx.fillText(`${item.name}: ${item.num}`, lineEndx2, lineEndy);
            // 图例
          ctx.fillRect (preLegend, 60, 30, 20)
          preLegend += 40
          ctx.font = "20px serif";
          ctx.fillText(item.name, preLegend, 78);
          preLegend +=  ctx.measureText(item.name).width + 16

          preArc += item.num / total
        })
      } else if(option.type === "bar"){
        ctx.strokeStyle = '#ccc'
        ctx.beginPath()
        ctx.moveTo(50, 100)
        ctx.lineTo(50, 350)
        ctx.lineTo(550, 350)
        ctx.stroke()
        ctx.closePath()
        let posList = []
        preArc = 0
        preLegend = 40
        option.data.map((item, index) => {
          let x = 80 + index * 60
          let y = 350 - item.num/total * 500 * 2
          let w = 30
          let h = item.num/total * 500 * 2
          ctx.fillStyle = defaultColor[index]
          ctx.fillRect(x,y,w,h)
          // 坐标轴
          ctx.beginPath()
          ctx.moveTo(x + w / 2, y)
          ctx.lineTo(50, y)
          ctx.setLineDash([4])
          ctx.strokeStyle = defaultColor[index]
          ctx.lineWidth = 0.5
          ctx.stroke()
          ctx.closePath()
          ctx.font = "20px serif";
          ctx.fillText(item.num, 35, y + 8);
          ctx.font = "20px serif";
          ctx.fillText(item.name, x + w / 2, 370);
          // 图例
          ctx.fillRect (preLegend, 60, 30, 20)
          preLegend += 40
          ctx.font = "20px serif";
          ctx.fillText(item.name, preLegend, 78);
          preLegend +=  ctx.measureText(item.name).width + 16
          preArc += item.num / total
          posList.push({x, y})
          // 折线
          if(posList.length > 1) {
            ctx.beginPath()
            ctx.moveTo(posList[index - 1].x + w / 2, posList[index - 1].y)
            ctx.lineTo(x + w / 2, y)
            let grd = ctx.createLinearGradient(posList[index - 1].x + w / 2, posList[index - 1].y, x + w / 2, y);
            grd.addColorStop(0,defaultColor[index - 1])
            grd.addColorStop(1,defaultColor[index])
            ctx.strokeStyle = grd
            ctx.lineWidth = 3
            ctx.setLineDash([])
            ctx.lineCap = "round"
            ctx.lineJoin = "round"
            ctx.stroke()
            ctx.closePath()
          }    
         

        })
      }
    },
    oprNode(type) {
      this.outerVisible = false
      this.innerVisible = true
      this.type = type
    },
    submit() {
      if(this.type == 1) {
        this.add(this.datalist)
      } else {
        this.edit(this.datalist)
      }
    },
    add(datalist) {
      datalist.map((item, index) => {
        if(item.id == this.current.id) {
          if(!item.children) {
            item.children = [{
              num: this.num,
              id: this.id,
              name: this.name,
              level: item.level + 1
            }]
          } else {
            item.children.push({
              num: this.num,
              id: this.id,
              name: this.name,
              level: item.level + 1
            })
          }
         
          const chart3 = document.getElementById("chart3")
          let ctx = chart3.getContext('2d')
          ctx.clearRect(0,0,800,1000)
          this.drawTree(this.datalist, {}, ctx)
          return
        }
        if(item.children) {
          this.add(item.children)
        }
      })
    },
    edit(datalist) {
      datalist.map((item, index) => {
        if(item.id == this.current.id) {
          item.num = this.num
          item.id = this.id
          item.name = this.name
          const chart3 = document.getElementById("chart3")
          let ctx = chart3.getContext('2d')
          ctx.clearRect(0,0,800,1000)
          this.drawTree(this.datalist, {}, ctx)
          return
        }
        if(item.children) {
          this.edit(item.children)
        }
      })

    },
    initTree() {
      const chart3 = document.getElementById("chart3")
      let ctx = chart3.getContext('2d')
      let id = ''
      let curX
      let curY
      let move = (e) => {
        ctx.clearRect(0,0,800,1000)
        this.drawTree(this.datalist, {}, ctx, {
          id,
          curX: e.clientX  - chart3.getBoundingClientRect().left,
          curY: e.clientY  - chart3.getBoundingClientRect().top
        })
      }
      chart3.addEventListener('mousedown', (e) => {
        curX = e.clientX  - chart3.getBoundingClientRect().left
        curY = e.clientY  - chart3.getBoundingClientRect().top
        this.rectList.forEach(item => {
          // 点击在矩形上 
          if(curX >= item.x1 && curX <= item.x2 && curY >= item.y1 && curY <= item.y2) {
            id = item.id
            this.current = item
             if(e.button ==2){
                this.outerVisible = true
                return
             }
            chart3.addEventListener('mousemove', move, false)
          }
        })
      }, false)
      chart3.addEventListener('mouseup', (e) => {
        chart3.removeEventListener('mousemove', move)
      }, false)
    
      this.drawTree(this.datalist, {}, ctx)
    },
    deleteNode(data, id) {
      console.log('data',data)
      this.outerVisible = false
      if(id == 1) return
      let flag = 0
      data.map((item, index) => {
        if(item.children) item.children.map((t, index) => {
          if(t.id == id) {
            flag = 1
            item.children.splice(index ,1)
            const chart3 = document.getElementById("chart3")
            let ctx = chart3.getContext('2d')
            ctx.clearRect(0,0,800,1000)
            this.drawTree(this.datalist, {}, ctx)
            return
          }
        })
        if(item.children && flag == 0) {
          this.deleteNode(item.children, id)
        }
      })
    },
    drawTree(datalist, p, ctx, move) {
      console.log('datalist',datalist)
      const defaultColor = ["#ff8080", "#b6ffea", "#fb0091", "#fddede", "#a0855b", "#b18ea6", "#ffc5a1", "#08ffc8"]
      if(!p.x) this.rectList = []
      datalist.map((item, index) => {
        if(move && move.id == item.id) {
          item.curX = move.curX
          item.curY = move.curY
        }
        if(p.children) {
          item.x1 = p.x1 + (p.x2 - p.x1) * index / p.children.length
          item.x2 = p.x1 + (p.x2 - p.x1) * (index + 1) / p.children.length
          item.x = item.x1 + (item.x2 - item.x1) / 2
          item.y = item.level * 100
        } else {
          item.x = 375
          item.y = item.level * 100
          item.x1 = 0
          item.x2 = 800
        }      
        ctx.fillStyle = defaultColor[item.level]
        if(item.curX) {
          ctx.fillRect(item.curX, item.curY, 50, 25)
        } else {
          ctx.fillRect(item.x, item.y, 50, 25)          
        }
        ctx.fillStyle = "#000"
        ctx.font = "20px serif";
        ctx.textAlign = "center"
        let ix = item.curX ? item.curX : item.x
        let iy = item.curY ? item.curY : item.y
        let px = p.curX ? p.curX : p.x
        let py = p.curY ? p.curY : p.y
        ctx.fillText(`${item.name}:${item.num}`, ix + 25, iy + 20);
        ctx.beginPath()
        ctx.moveTo(ix + 25, iy)
        ctx.lineTo(px + 25, py + 25)
        ctx.stroke()
        ctx.closePath()
     
        if(item.children) {
          this.drawTree(item.children, item, ctx, move)
        }
        let obj = {
          id: item.id,
          x1: item.x,
          x2: item.x + 50,
          y1: item.y,
          y2: item.y + 25
        }
        if(item.curX) {
          obj.x1 = item.curX
          obj.x2 = item.curX + 50
          obj.y1 = item.curY
          obj.y2 = item.curY + 25
        }
        this.rectList.push(obj)
      })
    }

  }

}
</script>

<style scoped>
#chart1 {
  border: 1px solid #eee;
}
#chart2 {
  border: 1px solid #eee;
}
#chart3 {
  border: 1px solid #eee;
}
</style>