用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 这几天准备阿里巴巴的笔试,可以说已经是心力交瘁,自从阿里和蘑菇街的内推被刷掉之后,开始越来越怀疑起自己的能力来,虽然这点打击应该是微不足道的.毕竟校招在刚 ...
随机推荐
- 文件上传漏洞之js验证
0x00 前言 只有前端验证=没有验证 0x01 剔除JS 打开burpsuite,进入Proxy的Options,把Remove all JavaScript选上. 设置浏览器代理直接上传PHP木马 ...
- python flask 入门
1.入门案例.本质上还是一个socket from flask import Flask,request #### app=Flask(__name__) app.debug=True ####配置路 ...
- GeoServer-REST应用:基于Qt网络编程一键同步发布空间数据和样式至GeoServer
@ 目录 简介 配置 步骤 1.引入Qt网络模块 2.创建网络管理.网络响应.网络请求 3.创建工作空间 4.创建数据存储并上传数据 5.上传样式文件 6.图层发布 6.图 ...
- webpack--性能优化之打包构建速度和代码调试优化
前言 本文来总结写webpack 在性能方面常见的优化方案. 正文 本文分别总结开发环境和生产环境中在打包构建速度和代码调试功能方面的优化方案,如下: 1.开发环境性能优化 (1)优化打包构建速度 a ...
- Elasticsearch-CentOS7单机安装测试
排版比较丑,但按照此步骤执行一定会搭建成功. 一.环境描述及准备 1.下载Elasticsearch包 curl -L -O https://artifacts.elastic.co/download ...
- 使用pip安装扩展包
pip可以对python扩展包进行查找.下载.安装.卸载等
- C++ STL vector扩容原理分析
扩容特点: 1)新增元素:vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素: 2)对vector的 ...
- 解决vue单页面应用做微信JSSDK注入权限时出现“invalid signature”(ios端)
--都说微信开发多坑,没想到遇到一个天坑. 在做一个vue项目时,要用到微信JS-SDK,官方文档详见:https://developers.weixin.qq.com/doc/offiaccount ...
- dotnet 委托的实现解析(2)开放委托和封闭委托 (Open Delegates vs. Closed Delegates)
前言 这是个人对委托的理解系列第二篇,部分翻译自 Open Delegates vs. Closed Delegates – SLaks.Blog,好像还没人翻译过,加上部分个人理解.希望能对大家理解 ...
- CentOS Stream 8 安装 Zabbix6.0 -- LNMP环境(nginx-1.20,mariadb-10.6,php-7.4)
镜像下载.域名解析.时间同步请点击阿里云开源镜像站 zabbix6.0 LTS版本出来了,前段时间刚安装了5.4,今天打算在虚拟机上安装6.0测试,安装6.0的要求php版本不低于7.2,mariad ...