笔墨伺候

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

树的样子

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, ...)
}
}

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

父节点与子结点在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

所以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地址

用Canvas画一棵二叉树的更多相关文章

  1. 樱花的季节,教大家用canvas画出飞舞的樱花树

    又到了樱花的季节,教大家使用canvas画出飞舞的樱花树效果. 废话少说,先看效果. 演示效果地址:http://suohb.com/work/tree4.htm 查看演示效果 第一步,我们先画出一棵 ...

  2. js画一棵树

    用纯js画一棵树.思路: 1.一棵树的图片,作为页面背景: 2.通过html5中的canvas画布进行遮罩: 3.定时每隔10ms,从下往上清除1px的遮罩: <!DOCTYPE html> ...

  3. 使用javascript和canvas画月半弯

    使用javascript和canvas画月半弯,月半弯好浪漫!浏览器须支持html5 查看效果:http://keleyi.com/a/bjad/8xqdm0r2.htm 以下是代码: <!do ...

  4. canvas 画圈 demo

    html代码: <canvas id="clickCanvas2"  width="180" height="180" data-to ...

  5. 踩个猴尾不容易啊 Canvas画个猴子

    踩个猴尾不容易啊  Canvas画个猴子 <!DOCTYPE html> <html> <head> <meta charset="UTF-8&qu ...

  6. canvas画随机闪烁的星星

    canvas画一颗星星: 规则的星星有内切圆和外切圆,每两个点之间的角度是固定的,因此可得到星星的每个点的坐标,画出星星. function drawStars(x,y,radius1,radius2 ...

  7. canvas画时钟

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http ...

  8. Canvas画椭圆的方法

    虽然标题是画椭圆,但是我们先来说说Canvas中的圆 相信大家对于Canvas画圆都不陌生   oGC.arc(400, 300, 100, 0, 2*Math.PI, false); 如上所示,直接 ...

  9. 深夜,用canvas画一个时钟

    深夜,用canvas画一个时钟 查看demo 这几天准备阿里巴巴的笔试,可以说已经是心力交瘁,自从阿里和蘑菇街的内推被刷掉之后,开始越来越怀疑起自己的能力来,虽然这点打击应该是微不足道的.毕竟校招在刚 ...

随机推荐

  1. 数据分析logistic回归与时间序列

    logistics回归 1.影响关系研究是所有研究中最为常见的. 2.当y是定量数据时,线性回归可以用来分析影响关系. 3.如果现在想对某件事情发生的概率进行预估,比如一件衣服的是否有人想购买? 这里 ...

  2. 利用while循环写的简单小游戏猜数字

    猜数字的大小游戏 C:\Users\Administrator>python Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 23 2018, 23:31:1 ...

  3. C# Event (1) —— 我想搞个事件

    本文地址:https://www.cnblogs.com/oberon-zjt0806/p/15975299.html 本文最初来自于博客园 本文遵循CC BY-NC-SA 4.0协议,转载请注明出处 ...

  4. xor加密的python实现

    #md5加密 import hashlib hash_md5 = hashlib.md5() x=input("Please input your text:") print( & ...

  5. JS 实现排序算法

    冒泡排序 比较相邻的元素.如果第一个比第二个大,就交换他们两个. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对.在这一点,最后的元素应该会是最大的数. 针对所有的元素重复以上的步骤,除了 ...

  6. CentOS8时间同步

    CentOS8中默认已经不再支持ntpd软件包,同时也无法通过官方软件仓库安装, CentOS8上使用Chrony配置NTP服务器,用于同步时间. 它有两个程序,chrony和chronyd, chr ...

  7. 【系列】关于NJUPT电赛自控方向第一次积分赛的总结

    本人是NJUPT电子科学与技术专业大一摸鱼狗一枚.本博客旨在总结与分享个人准备电赛所学知识,同时也是为了防止遗忘,锻炼写文章的能力.目前电赛方向为自控方向.主要研究方向为单片机.图像处理.自动控制相关 ...

  8. 女朋友问我深浅copy到底是什么?

    深浅拷贝 列表存放值的时候,是先申请一块内存空间,存放索引和内存地址的对应关系,其实通俗的来说列表内不存真正的值,是一种间接存放的对应关系: 列表内存不可变类型的数据 就比如说,如果现在将L列表索引0 ...

  9. mtu的原理和作用

    MTU: 最大传输单元,是指一种通信协议的某一层上面所能通过的最大数据包大小,最大传输单元这个参数通常与通信接口有关. 因为协议数据单元的包头和包尾的长度是固定的,MTU越大,则一个协议数据单元的承载 ...

  10. sqlserver下载地址及密匙

    SqlServer 2017 下载地址及密钥 下载地址: ed2k://|file|cn_sql_server_2017_developer_x64_dvd_11296175.iso|17697771 ...