老规矩,先上效果图
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
display: block;
width: 100%;
height: 100%;
background: #000;
}
</style>
</head>
<body>
<canvas id="network"></canvas>
</body>
<script>
class TaskQueue {
constructor() {
this.taskList = []
this.hasTaskDone = false
this.status = 'do' // do or stop
this.requestAnimationFrame = null
this.requestAnimationFrameDrawBind = this.requestAnimationFrameDraw.bind(this)
}
addTask(func) {
this.taskList.push(func)
if (this.requestAnimationFrame === null) {
this.addRequestAnimationFrame()
this.do()
}
}
do() {
this.status = 'do'
new Promise(res => {
this.taskList[0] && this.taskList[0]()
this.taskList.shift()
this.hasTaskDone = true
res()
}).then(() => {
if (this.status === 'do' && this.taskList.length) {
this.do()
}
})
}
stop() {
this.status = 'stop'
}
requestAnimationFrameDraw() {
this.stop()
if (this.hasTaskDone && this.reDraw) {
this.hasTaskDone = false
this.reDraw()
}
if (this.taskList.length) {
this.addRequestAnimationFrame()
this.do()
} else {
this.clearRequestAnimationFrame()
}
}
addRequestAnimationFrame() {
this.requestAnimationFrame = window.requestAnimationFrame(this.requestAnimationFrameDrawBind)
}
clearRequestAnimationFrame() {
window.cancelAnimationFrame(this.requestAnimationFrame)
this.requestAnimationFrame = null
}
removeEvent() {
this.stop()
this.clearRequestAnimationFrame()
}
}
class Layout extends TaskQueue {
constructor(opt) {
super(opt)
this.qIndex = opt.layout.qIndex
this.fStableL = opt.layout.fStableL
this.fIndex = opt.layout.fIndex
this.count = opt.layout.count || opt.nodes.length * Math.ceil(opt.nodes.length / 5)
this.countForce = 0
}
doLayout() {
this.countForce++
if (this.countForce >= this.count) {
return
}
// 计算开始
this.forceComputed(this.arc, this.line)
setTimeout(() => {
this.addTask(() => {
this.doLayout();
})
})
}
forceComputed(nodes) {
nodes.forEach(item => {
item.translateX = 0
item.translateY = 0
})
nodes.forEach((curNode, index) => {
// 库仑力计算
for (let i = index + 1; i < nodes.length; i++) {
let otherNode = nodes[i]
if (otherNode) {
this.computedXYByQ(curNode, otherNode)
}
}
// 弹簧力计算
if (curNode.fromArcs?.length) {
curNode.fromArcs.forEach(id => {
let fromNode = nodes.filter(node => {
return node.id === id
})[0]
if (fromNode) {
this.computedXYByK(curNode, fromNode)
}
})
}
// 中心拉力
if (curNode.fromArcs?.length) {
this.computedXYByK(curNode, {
xy: {
x: this.canvas.width / 2,
y: this.canvas.height / 2
}
})
}
})
// let maxTranslate = 1
// nodes.forEach(item => {
// if(item.translateX && Math.abs(item.translateX) > maxTranslate){
// maxTranslate = Math.abs(item.translateX)
// }
// if(item.translateY && Math.abs(item.translateY) > maxTranslate){
// maxTranslate = Math.abs(item.translateY)
// }
// })
// nodes.forEach(item => {
// if(item.translateX){
// item.x += item.translateX / maxTranslate
// }
// if(item.translateY){
// item.y += item.translateY / maxTranslate
// }
// })
nodes.forEach(item => {
if (item.translateX) {
item.xy.x += item.translateX
}
if (item.translateY) {
item.xy.y += item.translateY
}
})
}
computedXYByQ(node1, node2) {
let x1 = node1.xy.x
let y1 = node1.xy.y
let x2 = node2.xy.x
let y2 = node2.xy.y
let xl = x2 - x1
let yl = y2 - y1
let angle = Math.PI
if (!xl) {
if (y2 > y1) {
angle = -Math.PI / 2
} else {
angle = Math.PI / 2
}
} else if (!yl) {
if (x2 > x1) {
angle = 0
} else {
angle = Math.PI
}
} else {
angle = Math.atan(yl / xl)
}
let r = Math.sqrt(Math.pow(xl, 2) + Math.pow(yl, 2))
if (r < 1) {
r = 1
}
// 库仑力 r越大,库仑力越小
let node1Q = (node1.fromNodes?.length || 0) + (node1.toNodes?.length || 0) + 1
let node2Q = (node2.fromNodes?.length || 0) + (node2.toNodes?.length || 0) + 1
let f = this.qIndex * node1Q * node2Q / Math.pow(r, 2)
let fx = f * Math.cos(angle)
let fy = f * Math.sin(angle)
node1.translateX = node1.translateX
node1.translateY = node1.translateY
node2.translateX = node2.translateX
node2.translateY = node2.translateY
// node1.translateX -= fx
// node2.translateX += fx
// node1.translateY -= fy
// node2.translateY += fy
if (x2 > x1) {
if (fx > 0) {
node1.translateX -= fx
node2.translateX += fx
} else {
node1.translateX += fx
node2.translateX -= fx
}
} else {
if (fx > 0) {
node1.translateX += fx
node2.translateX -= fx
} else {
node1.translateX -= fx
node2.translateX += fx
}
}
if (y2 > y1) {
if (fy > 0) {
node1.translateY -= fy
node2.translateY += fy
} else {
node1.translateY += fy
node2.translateY -= fy
}
} else {
if (fy > 0) {
node1.translateY += fy
node2.translateY -= fy
} else {
node1.translateY -= fy
node2.translateY += fy
}
}
}
computedXYByK(node1, node2) {
let x1 = node1.xy.x
let y1 = node1.xy.y
let x2 = node2.xy.x
let y2 = node2.xy.y
let xl = x2 - x1
let yl = y2 - y1
let angle = Math.PI
if (!xl) {
if (y2 > y1) {
angle = -Math.PI / 2
} else {
angle = Math.PI / 2
}
} else if (!yl) {
if (x2 > x1) {
angle = 0
} else {
angle = Math.PI
}
} else {
angle = Math.atan(yl / xl)
}
let r = Math.sqrt(Math.pow(xl, 2) + Math.pow(yl, 2))
if (r > this.fStableL * 2) {
r = this.fStableL * 2
} else if (r < 1) {
r = 1
}
// 弹簧力
let f = this.fIndex * (r - this.fStableL)
let fx = f * Math.cos(angle)
let fy = f * Math.sin(angle)
node1.translateX = node1.translateX
node1.translateY = node1.translateY
node2.translateX = node2.translateX
node2.translateY = node2.translateY
if (f > 0) {
// 拉力
if (x2 > x1) {
if (fx > 0) {
node1.translateX += fx
node2.translateX -= fx
} else {
node1.translateX -= fx
node2.translateX += fx
}
} else {
if (fx > 0) {
node1.translateX -= fx
node2.translateX += fx
} else {
node1.translateX += fx
node2.translateX -= fx
}
}
if (y2 > y1) {
if (fy > 0) {
node1.translateY += fy
node2.translateY -= fy
} else {
node1.translateY -= fy
node2.translateY += fy
}
} else {
if (fy > 0) {
node1.translateY -= fy
node2.translateY += fy
} else {
node1.translateY += fy
node2.translateY -= fy
}
}
} else {
// 弹力
if (x2 > x1) {
if (fx > 0) {
node1.translateX -= fx
node2.translateX += fx
} else {
node1.translateX += fx
node2.translateX -= fx
}
} else {
if (fx > 0) {
node1.translateX += fx
node2.translateX -= fx
} else {
node1.translateX -= fx
node2.translateX += fx
}
}
if (y2 > y1) {
if (fy > 0) {
node1.translateY -= fy
node2.translateY += fy
} else {
node1.translateY += fy
node2.translateY -= fy
}
} else {
if (fy > 0) {
node1.translateY += fy
node2.translateY -= fy
} else {
node1.translateY -= fy
node2.translateY += fy
}
}
}
}
}
class View extends Layout {
constructor(opt) {
super(opt)
this.canvas = opt.canvas
this.dpr = window.devicePixelRatio || 1
this.nodes = opt.nodes
this.paths = opt.paths
this.circleStyle = opt.circleStyle
this.lineStyle = opt.lineStyle
this.line = []
this.arc = []
this.init()
}
init() {
if (!this.canvas) {
return
}
if (this.canvas.width !== Math.floor(this.canvas.offsetWidth * this.dpr) || this.canvas.height !== Math.floor(this.canvas.offsetHeight * this.dpr)) {
this.canvas.width = Math.floor(this.canvas.offsetWidth * this.dpr)
this.canvas.height = Math.floor(this.canvas.offsetHeight * this.dpr)
}
this.ctx = this.canvas.getContext('2d')
this.addData(this.nodes, this.paths)
}
addData(nodes, paths) {
if (nodes && nodes.length) {
this.addArc(nodes)
}
if (paths && paths.length) {
this.addLine(paths)
}
super.countForce = 0
super.doLayout()
}
addArc(nodes) {
// 数据多时可以考虑将初始化随机坐标范围与数据量做等比函数
nodes.forEach(node => {
this.arc.push({
id: node.id,
fromArcs: [],
toArcs: [],
xy: {
x: this.rand(0, this.canvas.width),
y: this.rand(0, this.canvas.height)
}
})
})
}
addLine(paths) {
paths.forEach(path => {
let fromArc = this.arc.filter(node => {
return node.id === path.from
})[0]
let toArc = this.arc.filter(node => {
return node.id === path.to
})[0]
fromArc.toArcs.push(toArc.id)
toArc.fromArcs.push(fromArc.id)
if (fromArc && toArc) {
this.line.push({
id: path.id,
from: path.from,
to: path.to,
fromArc,
toArc
})
}
})
}
reDraw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.draw()
}
draw() {
this.line.forEach(item => {
this.drawLine(item)
})
this.arc.forEach(item => {
this.drawArc(item)
})
}
drawLine(data) {
this.ctx.save()
this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2)
this.ctx.scale(this.scaleC, this.scaleC)
this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2)
this.ctx.beginPath()
this.ctx.lineWidth = this.lineStyle.width
this.ctx.strokeStyle = this.lineStyle.color
this.ctx.moveTo(data.fromArc.xy.x, data.fromArc.xy.y)
this.ctx.lineTo(data.toArc.xy.x, data.toArc.xy.y)
this.ctx.stroke()
this.ctx.closePath()
this.ctx.restore()
}
drawArc(data) {
this.ctx.save()
this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2)
this.ctx.scale(this.scaleC, this.scaleC)
this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2)
this.ctx.beginPath()
this.ctx.fillStyle = this.circleStyle.background
this.ctx.arc(data.xy.x, data.xy.y, this.circleStyle.r, 0, 2 * Math.PI)
this.ctx.fill()
this.ctx.closePath()
this.ctx.restore()
}
rand = (n, m) => {
var c = m - n + 1
return Math.floor(Math.random() * c + n)
}
}
// 测试数据
let data = {
"nodes": [
{
"id": "36"
},
{
"id": "50"
},
{
"id": "20077"
},
{
"id": "1090"
},
{
"id": "1078"
},
{
"id": "10007"
},
{
"id": "20039"
},
{
"id": "1074"
},
{
"id": "20058"
},
{
"id": "1062"
},
{
"id": "10001"
},
{
"id": "20076"
},
{
"id": "1089"
},
{
"id": "20038"
},
{
"id": "1068"
},
{
"id": "20057"
},
{
"id": "1081"
},
{
"id": "20070"
},
{
"id": "1034"
},
{
"id": "1077"
},
{
"id": "10002"
},
{
"id": "10003"
},
{
"id": "20069"
},
{
"id": "1002"
},
{
"id": "47"
},
{
"id": "10010"
},
{
"id": "14"
},
{
"id": "42"
},
{
"id": "94"
},
{
"id": "16"
},
{
"id": "41"
},
{
"id": "64"
},
{
"id": "20002"
},
{
"id": "73"
},
{
"id": "1001"
},
{
"id": "10009"
},
{
"id": "10008"
},
{
"id": "10006"
},
{
"id": "10005"
},
{
"id": "10004"
},
{
"id": "33"
},
{
"id": "10"
},
{
"id": "18"
},
{
"id": "70"
},
{
"id": "98"
},
{
"id": "20"
},
{
"id": "24"
},
{
"id": "20001"
}
],
"paths": [
{
"id": "606",
"from": "50",
"to": "36"
},
{
"id": "346",
"from": "20077",
"to": "1090"
},
{
"id": "343",
"from": "1078",
"to": "10007"
},
{
"id": "382",
"from": "20039",
"to": "1074"
},
{
"id": "419",
"from": "20058",
"to": "1062"
},
{
"id": "344",
"from": "1078",
"to": "10001"
},
{
"id": "356",
"from": "20076",
"to": "1089"
},
{
"id": "439",
"from": "20038",
"to": "1068"
},
{
"id": "417",
"from": "20057",
"to": "1081"
},
{
"id": "358",
"from": "20070",
"to": "1078"
},
{
"id": "438",
"from": "20038",
"to": "1034"
},
{
"id": "248",
"from": "1077",
"to": "10002"
},
{
"id": "249",
"from": "1077",
"to": "10003"
},
{
"id": "364",
"from": "20069",
"to": "1077"
},
{
"id": "4797",
"from": "1002",
"to": "10003"
},
{
"id": "4787",
"from": "1002",
"to": "10002"
},
{
"id": "223",
"from": "1002",
"to": "10003"
},
{
"id": "222",
"from": "1002",
"to": "10002"
},
{
"id": "2659",
"from": "1002",
"to": "47"
},
{
"id": "4777",
"from": "1002",
"to": "10001"
},
{
"id": "4867",
"from": "1002",
"to": "10010"
},
{
"id": "1466",
"from": "14",
"to": "1002"
},
{
"id": "1437",
"from": "42",
"to": "1002"
},
{
"id": "1414",
"from": "94",
"to": "1002"
},
{
"id": "1411",
"from": "16",
"to": "1002"
},
{
"id": "1395",
"from": "16",
"to": "1002"
},
{
"id": "1382",
"from": "41",
"to": "1002"
},
{
"id": "1377",
"from": "64",
"to": "1002"
},
{
"id": "436",
"from": "20002",
"to": "1002"
},
{
"id": "2658",
"from": "73",
"to": "1002"
},
{
"id": "4856",
"from": "1001",
"to": "10009"
},
{
"id": "4846",
"from": "1001",
"to": "10008"
},
{
"id": "4836",
"from": "1001",
"to": "10007"
},
{
"id": "4826",
"from": "1001",
"to": "10006"
},
{
"id": "4816",
"from": "1001",
"to": "10005"
},
{
"id": "4806",
"from": "1001",
"to": "10004"
},
{
"id": "4796",
"from": "1001",
"to": "10003"
},
{
"id": "4786",
"from": "1001",
"to": "10002"
},
{
"id": "4776",
"from": "1001",
"to": "10001"
},
{
"id": "221",
"from": "1001",
"to": "10001"
},
{
"id": "4866",
"from": "1001",
"to": "10010"
},
{
"id": "1469",
"from": "33",
"to": "1001"
},
{
"id": "1459",
"from": "10",
"to": "1001"
},
{
"id": "1448",
"from": "18",
"to": "1001"
},
{
"id": "1406",
"from": "70",
"to": "1001"
},
{
"id": "1396",
"from": "47",
"to": "1001"
},
{
"id": "1369",
"from": "98",
"to": "1001"
},
{
"id": "1365",
"from": "20",
"to": "1001"
},
{
"id": "1363",
"from": "24",
"to": "1001"
},
{
"id": "406",
"from": "20001",
"to": "1001"
}
]
}
// canvas dom
const canvas = document.getElementById('network');
new View({
canvas,
nodes: data.nodes,
paths: data.paths,
circleStyle: {
r: 10,
background: '#FFFFFF'
},
lineStyle: {
width: 1,
color: '#FFFFFF'
},
layout: {
qIndex: 2000, // 库仑力系数,值越大,库仑力越大
fStableL: 80,
fIndex: 0.1, // 拉力系数,数值越大,力越大
}
})
</script>
</html>