用Canvas画一棵二叉树
笔墨伺候
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
用Canvas画一棵二叉树的更多相关文章
- 樱花的季节,教大家用canvas画出飞舞的樱花树
又到了樱花的季节,教大家使用canvas画出飞舞的樱花树效果. 废话少说,先看效果. 演示效果地址:http://suohb.com/work/tree4.htm 查看演示效果 第一步,我们先画出一棵 ...
- js画一棵树
用纯js画一棵树.思路: 1.一棵树的图片,作为页面背景: 2.通过html5中的canvas画布进行遮罩: 3.定时每隔10ms,从下往上清除1px的遮罩: <!DOCTYPE html> ...
- 使用javascript和canvas画月半弯
使用javascript和canvas画月半弯,月半弯好浪漫!浏览器须支持html5 查看效果:http://keleyi.com/a/bjad/8xqdm0r2.htm 以下是代码: <!do ...
- canvas 画圈 demo
html代码: <canvas id="clickCanvas2" width="180" height="180" data-to ...
- 踩个猴尾不容易啊 Canvas画个猴子
踩个猴尾不容易啊 Canvas画个猴子 <!DOCTYPE html> <html> <head> <meta charset="UTF-8&qu ...
- canvas画随机闪烁的星星
canvas画一颗星星: 规则的星星有内切圆和外切圆,每两个点之间的角度是固定的,因此可得到星星的每个点的坐标,画出星星. function drawStars(x,y,radius1,radius2 ...
- canvas画时钟
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http ...
- Canvas画椭圆的方法
虽然标题是画椭圆,但是我们先来说说Canvas中的圆 相信大家对于Canvas画圆都不陌生 oGC.arc(400, 300, 100, 0, 2*Math.PI, false); 如上所示,直接 ...
- 深夜,用canvas画一个时钟
深夜,用canvas画一个时钟 查看demo 这几天准备阿里巴巴的笔试,可以说已经是心力交瘁,自从阿里和蘑菇街的内推被刷掉之后,开始越来越怀疑起自己的能力来,虽然这点打击应该是微不足道的.毕竟校招在刚 ...
随机推荐
- 函数式组件中实现Antd打开Modal后其Input框自动聚焦(focus)到文字的最后
目前React使用函数式组件已经成为趋势, 如何把React函数式组件用好, 提高性能, 从而实现业务需求也成为了一种能力的体现......咳咳咳, 进入正题: 现实场景需求 我想实现这一个需求, 父 ...
- [转载]MATLAB中神经网络的函数
1.设计函数 solvein 设计线性网络: solverb 设计径向基网络: solverbe 设计精确的径向 ...
- 【数据库】SQL 语句大全
数据操作 SELECT --从数据库表中检索数据行和列 INSERT --向数据库表添加新数据行 DELETE --从数据库表中删除数据行 UPDATE --更新数据库表中的数据 数据定义 CREAT ...
- vue3-组件中使用setup函数获取vuex中的数据的新方式
传统方式 setup() { const store = useStore() //传统方式 const aName = computed(() => store.state.name) ret ...
- Golang 语言特性
1. 函数 与c 语言不同,go 语言中,函数的参数和返回值都由栈来存储. 传值:函数调用时会复制参数,被调用方和调用方持有两份不相关的两份数据 传引用:函数调用时会传递参数指针,被调用方和调用方持有 ...
- vim编写C/C++程序过程
vim编写C.C++程序过程(以hello world为例): vim hello.c/hello.cpp 或者vi hello.c/hello.cpp创建hello.c/hello.cpp文件并进入 ...
- c++-投骰子
#include<iostream>#include<cstdlib>//产生随机数的函数using namespace std;enum GameStatus{WIN,LOS ...
- Mybatis结果集ResultMap映射
基本使用: 解决属性名和数据库字段名不一致的问题 <resultMap id="user" type="com.guan.bean.UserBean"&g ...
- Django orm Q查询补充
Q的简单用法 from django.db.models import Q q = Q() q.children.append(("username", "lyj&quo ...
- my file server1
环境配置 先导入虚拟机 设置网路为NAT,扫描iP netdiscover -r 192.168.164.0/24 靶机ip:192.168.164.183 kali:192.168.164.137 ...