当前位置: 首页 > news >正文

用Canvas画一棵二叉树

笔墨伺候

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
// 然后便可以挥毫泼墨了

树的样子

clipboard.png

const root = {
      value: 'A',
      label: '100',
      left: {
        value: 'B',
        label: '70',
        left: {
          value: 'D',
          label: '40',
          left: {
            value: 'H',
            label: '20',
            left: null,
            right: null
          },
          right: {
            value: 'I',
            label: '20',
            left: null,
            right: null
          }
        },
        right: {
          value: 'E',
          label: '30',
          left: null,
          right: null
        }
      },
      right: {
        value: 'C',
        label: '30',
        left: {
          value: 'F',
          label: '15',
          left: null,
          right: null
        },
        right: {
          value: 'G',
          label: '15',
          left: null,
          right: null
        }
      }
    }

构思构思

这样一幅大作,无非就是由黑色的正方形+线段构成
这正方形怎么画

function drawRect(text, x, y, unit) {
  ctx.fillRect(x, y, unit, unit)
  // fillRect(x, y, width, height) 
  // x与y指定了在canvas画布上所绘制的矩形的左上角(相对于原点)的坐标
  // width和height设置矩形的尺寸。
  ctx.font = "14px serif"
  ctx.fillText(text, x + unit, y + unit) // 再给每个正方形加个名字
}

这直线怎么画

function drawLine(x1, y1, x2, y2) {
  ctx.moveTo(x1, y1)
  ctx.lineTo(x2, y2)
  ctx.stroke()
}

这关系怎么画

// 前序遍历二叉树
function preOrderTraverse(root, x, y){
  drawRect(root.value, x, y)
  if(root.left){
    drawLine(x, y, ...)
    preOrderTraverse(root.left, ...)
  }
  if(root.right){
    drawLine(x, y, ...)
    preOrderTraverse(root.right, ...)
  }
}

现在遇到个小问题,如何确定节点的子节的位置?

clipboard.png

父节点与子结点在y轴上的距离固定,为正方形长度unit的两倍;父节点与子结点在x轴上的距离满足n2=(n1+2)*2-2,其中设父节点与子结点在x轴上最短的距离n0=1,即unit,而父节点与子结点在x轴上最长的距离取决于该树的层数。
如何得到树的深度?

function getDeepOfTree(root) {
  if (!root) {
    return 0
  }
  let left = getDeepOfTree(root.left)
  let right = getDeepOfTree(root.right)
  return (left > right) ? left + 1 : right + 1
}

这样父节点与子结点在x轴上最长的距离

let distance = 1
const deep = getDeepOfTree(root)
for (let i = 2; i < deep; i++) {
  distance = (distance + 2) * 2 - 2
}
// distance*unit 即为父节点与子结点在x轴上最长的距离

unit为正方形的长度,如何确定,假设canvas的宽度为1000,由深度deep可知,树的最大宽度为Math.pow(2, deep - 1),最底层的正方形占据4个unit

clipboard.png

所以unit是如此计算,const unit = 1000 / (Math.pow(2, deep - 1) * 4 + 8)+8是个备用空间。

代码

<html>

<body>
  <canvas id="canvas" width="1000"></canvas>
  <script>
    const root = {
      value: 'A',
      label: '100',
      left: {
        value: 'B',
        label: '70',
        left: {
          value: 'D',
          label: '40',
          left: {
            value: 'H',
            label: '20',
            left: null,
            right: null
          },
          right: {
            value: 'I',
            label: '20',
            left: null,
            right: null
          }
        },
        right: {
          value: 'E',
          label: '30',
          left: null,
          right: null
        }
      },
      right: {
        value: 'C',
        label: '30',
        left: {
          value: 'F',
          label: '15',
          left: null,
          right: null
        },
        right: {
          value: 'G',
          label: '15',
          left: null,
          right: null
        }
      }
    }
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')

    const deep = getDeepOfTree(root)
    let distance = 1
    for (let i = 2; i < deep; i++) {
      distance = (distance + 2) * 2 - 2
    }
    const unit = 1000 / (Math.pow(2, deep - 1) * 4 + 8)
    canvas.setAttribute('height', deep * unit * 4)

    const rootX = (1000 - unit) / 2
    const rootY = unit
    preOrderTraverse(root, rootX, rootY, distance)
    
    // 得到该树的高度
    function getDeepOfTree(root) {
      if (!root) {
        return 0
      }
      let left = getDeepOfTree(root.left)
      let right = getDeepOfTree(root.right)
      return (left > right) ? left + 1 : right + 1
    }

    function preOrderTraverse(root, x, y, distance) {
      drawRect(root.value, x, y) // 绘制节点
      if (root.left) {
        drawLeftLine(x, y + unit, distance)
        preOrderTraverse(root.left, x - (distance + 1) * unit, y + 3 * unit, distance / 2 - 1)
      }
      if (root.right) {
        drawRightLine(x + unit, y + unit, distance)
        preOrderTraverse(root.right, x + (distance + 1) * unit, y + 3 * unit, distance / 2 - 1)
      }
    }

    function drawRect(text, x, y) {
      ctx.fillRect(x, y, unit, unit)
      ctx.font = "14px serif"
      ctx.fillText(text, x + unit, y + unit)
    }

    function drawLeftLine (x, y, distance) {
      ctx.moveTo(x, y)
      ctx.lineTo(x - distance * unit, y + 2 * unit)
      ctx.stroke()
    }

    function drawRightLine (x, y, distance) {
      ctx.moveTo(x, y)
      ctx.lineTo(x + distance * unit, y + 2 * unit)
      ctx.stroke()
    }
  </script>
</body>

</html>

来点互动

实现移动至节点出现tooltip

首先要有tooltip

<div id="tooltip" style="position:absolute;"></div>
...
const tooltip = document.getElementById('tooltip')

由于canvas是一个整体元素,所以只能给canvas绑定事件,根据鼠标的坐标,判断是否落在某个正方形区域内
这里有个关健个函数

ctx.rect(0, 0, 100, 100)
ctx.isPointInPath(x, y)
// 判断x,y是否落在刚刚由path绘制出的区域内

所以在绘制正方形时还要将其path记下来

let pathArr = []
function preOrderTraverse(root, x, y, distance) {
  pathArr.push({
    x,
    y,
    value: root.value,
    label: root.label
  })
  // 记录正方形左上角的位置,就可以重绘路径
  drawRect(root.value, x, y) // 绘制节点
  if (root.left) {
    drawLeftLine(x, y + unit, distance)
    preOrderTraverse(root.left, x - (distance + 1) * unit, y + 3 * unit, distance / 2 - 1)
  }
  if (root.right) {
    drawRightLine(x + unit, y + unit, distance)
    preOrderTraverse(root.right, x + (distance + 1) * unit, y + 3 * unit, distance / 2 - 1)
  }
}

绑定事件

// 模拟鼠标hover效果
canvas.addEventListener('mousemove', (e) => {
  let i = 0
  while (i < pathArr.length) {
    ctx.beginPath()
    ctx.rect(pathArr[i].x, pathArr[i].y, unit, unit)
    if (ctx.isPointInPath(e.offsetX, e.offsetY)) {
      canvas.style.cursor = 'pointer'
      tooltip.innerHTML = `<span style="font-size:14px;">${pathArr[i].label}</span>`
      tooltip.style.top = `${pathArr[i].y + unit + 4}px`
      tooltip.style.left = `${pathArr[i].x + unit}px`
      break
    } else {
      i++
    }
  }
  if (i === pathArr.length) {
    canvas.style.cursor = 'default'
    tooltip.innerHTML = ``
  }
})

线上demo

JSBin地址

相关文章:

  • webpack+react环境搭建与hello world
  • iOS 实现UINavigation全屏滑动返回(一)
  • c++那些事儿4 0 多态
  • 1.Node.js
  • 使用Doxygen生成C#帮助文档
  • [喵咪大数据]Hadoop集群模式
  • The working copy is locked due to previous error
  • iOS推送Tips
  • mysql5.7二进制编译包的安装
  • 笨办法31做出决定
  • SQL PRIMARY KEY 约束
  • 串口工具
  • python计算器
  • 怎样实现高质量发展(展望2018)
  • 阮一峰JS基础读后感
  • [ JavaScript ] 数据结构与算法 —— 链表
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • C++类中的特殊成员函数
  • Java 9 被无情抛弃,Java 8 直接升级到 Java 10!!
  • Leetcode 27 Remove Element
  • magento 货币换算
  • MYSQL如何对数据进行自动化升级--以如果某数据表存在并且某字段不存在时则执行更新操作为例...
  • Python - 闭包Closure
  • React-Native - 收藏集 - 掘金
  • select2 取值 遍历 设置默认值
  • 前端代码风格自动化系列(二)之Commitlint
  • 容器服务kubernetes弹性伸缩高级用法
  • 微信小程序:实现悬浮返回和分享按钮
  • CMake 入门1/5:基于阿里云 ECS搭建体验环境
  • FaaS 的简单实践
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • #android不同版本废弃api,新api。
  • $().each和$.each的区别
  • ( )的作用是将计算机中的信息传送给用户,计算机应用基础 吉大15春学期《计算机应用基础》在线作业二及答案...
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (6)设计一个TimeMap
  • (C语言)求出1,2,5三个数不同个数组合为100的组合个数
  • (初研) Sentence-embedding fine-tune notebook
  • (附源码)SSM环卫人员管理平台 计算机毕设36412
  • (附源码)ssm失物招领系统 毕业设计 182317
  • (免费领源码)python+django+mysql线上兼职平台系统83320-计算机毕业设计项目选题推荐
  • (强烈推荐)移动端音视频从零到上手(上)
  • (四)【Jmeter】 JMeter的界面布局与组件概述
  • (已解决)报错:Could not load the Qt platform plugin “xcb“
  • (转)ABI是什么
  • **python多态
  • .NET Core 版本不支持的问题
  • .NET/C# 将一个命令行参数字符串转换为命令行参数数组 args
  • .NET高级面试指南专题十一【 设计模式介绍,为什么要用设计模式】
  • .NET开发不可不知、不可不用的辅助类(一)
  • .NET学习教程二——.net基础定义+VS常用设置
  • /deep/和 >>>以及 ::v-deep 三者的区别
  • /run/containerd/containerd.sock connect: connection refused
  • []利用定点式具实现:文件读取,完成不同进制之间的