笔墨伺候
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 = ``
}
})
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。