笔墨伺候

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. MySQL第四讲

    昨日内容回顾 表与表之间建关系(外键) """ 表与表之间最多只有四种关系 一对多 多对多 一对一 没有关系 在确定表与表之间的关系的时候记住一句话 换位思考 " ...

  2. selenium+python安装

    整理了下selenium+python环境搭建,搭建了很多次但每次都还是手忙脚乱,今天用心整理下 selenium 是用于测试 Web 应用程序用户界面 (UI) 的常用框架,并且 Selenium ...

  3. 测试平台系列(91) 编写oss管理页面

    大家好~我是米洛! 我正在从0到1打造一个开源的接口测试平台, 也在编写一套与之对应的教程,希望大家多多支持. 欢迎关注我的公众号米洛的测开日记,获取最新文章教程! 回顾 上一节我们编写好了oss相关 ...

  4. 面试题 正则表达式 验证邮箱 Pattern.matches

    故事背景 今天面试遇到这道题,对正则表达式还是有些懵,面试完回家复盘实现一下.这里使用到了 Pattern 这个类来校验正则表达式. 正则表示式分析: ^([a-z0-9A-Z]+[-|\\.]?)+ ...

  5. MySQL配置主从分离

    主服务器 192.168.176.110 从服务器 192.168.176.120 主数据库操作(ip:192.168.176.110)  配置MySQL主服务器的配置文件 [root@localho ...

  6. C#编程基础之字符串操作

    本文来源于复习基础知识的学习笔记.自用的同时希望也能帮到其他童鞋. 什么是编程语言? 计算机可以执行的指令.这些指令成为源代码或者代码 有什么用? 以人们可读可理解的方式编写指令.人们希望计算机执行指 ...

  7. Circle Linux镜像在阿里云镜像站首发上线

    镜像下载.域名解析.时间同步请点击阿里云开源镜像站 Circle Linux简介 Circle Linux 社区是一个开源.共创的 Linux 社区,将通过完全开放.包容的社区形式与全球开发者共同构建 ...

  8. 6月4日 python学习总结 初次接触jQuery

    1. jQuery是什么?是一个轻量级的,兼容多浏览器的JS库(write less, do more) 1. 是一个工具,简单方便的实现一些DOM操作 2. 不用jQuery完全可以,但是不明智. ...

  9. json.dumps参数之解

    宝藏参数,懂的都懂^-^ 说明:编译后print()打印内容,此内容以字符串紧凑输出,且无顺序,中文不可读..   应用:使用pycharm做接口测试时,print()打印出的接口下行,如下图:   ...

  10. 各种环境下反弹shell

    0x00 NC命令详解 在介绍如何反弹shell之前,先了解相关知识要点. nc全称为netcat,所做的就是在两台电脑之间建立链接,并返回两个数据流 可运行在TCP或者UDP模式,添加参数 -u 则 ...