2024-11-29 09:50:37 +08:00
|
|
|
<head>
|
|
|
|
<style> body { margin: 0; } </style>
|
|
|
|
|
|
|
|
<script src="3d-force-graph.min.js"></script>
|
|
|
|
<!--<script src="../../dist/3d-force-graph.js"></script>-->
|
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
|
|
|
<div id="3d-graph"></div>
|
|
|
|
<div style="position: absolute; top: 5px; right: 5px;">
|
|
|
|
<button id="rotationToggle" style="margin: 8px; height: 25px; width: 150px;">
|
|
|
|
暂停旋转
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<script type="importmap">{ "imports": { "three": "//unpkg.com/three/build/three.module.js" }}</script>
|
|
|
|
<script type="module">
|
|
|
|
import SpriteText from "//unpkg.com/three-spritetext/dist/three-spritetext.mjs";
|
|
|
|
|
2024-12-16 08:49:05 +08:00
|
|
|
const distance = 2000;
|
2024-11-29 09:50:37 +08:00
|
|
|
let isRotationActive = true;
|
|
|
|
|
|
|
|
// Random tree
|
2024-12-16 08:49:05 +08:00
|
|
|
const N = 1200;
|
2024-11-29 09:50:37 +08:00
|
|
|
const gData = {
|
2024-12-16 08:49:05 +08:00
|
|
|
nodes: [...Array(N).keys()].map(i => ({ id: i,group:Math.floor(i/150) })),
|
2024-11-29 09:50:37 +08:00
|
|
|
links: [...Array(N).keys()]
|
|
|
|
.filter(id => id)
|
|
|
|
.map(id => ({
|
|
|
|
source: id,
|
|
|
|
target: Math.round(Math.random() * (id-1))
|
|
|
|
}))
|
|
|
|
};
|
|
|
|
const Graph = ForceGraph3D()
|
|
|
|
(document.getElementById('3d-graph'))
|
|
|
|
.graphData(gData)
|
|
|
|
// .jsonUrl('./miserables.json')
|
|
|
|
.nodeAutoColorBy('group')
|
|
|
|
.nodeLabel('id')
|
|
|
|
.linkDirectionalParticles(4)
|
2024-12-16 08:49:05 +08:00
|
|
|
.linkDirectionalParticleWidth(4)
|
2024-11-29 09:50:37 +08:00
|
|
|
.linkDirectionalParticleSpeed(d => 4 * 0.001)// d.value
|
2024-12-16 08:49:05 +08:00
|
|
|
.linkWidth(4)
|
|
|
|
.nodeRelSize(6)
|
2024-11-29 09:50:37 +08:00
|
|
|
// .nodeThreeObject(node => {// 借助三方库 实现文字几何的展示
|
|
|
|
// const sprite = new SpriteText(node.id);
|
|
|
|
// sprite.material.depthWrite = false; // make sprite background transparent
|
|
|
|
// sprite.color = node.color;// node.color
|
|
|
|
// sprite.textHeight = 8;
|
|
|
|
// return sprite;
|
|
|
|
// })
|
|
|
|
// .linkDirectionalArrowLength(3)// 让边带上箭头
|
|
|
|
// .linkDirectionalArrowRelPos(1)// 设置箭头位置
|
|
|
|
.onNodeClick(node => {
|
|
|
|
// Aim at node from outside it
|
2024-12-16 08:49:05 +08:00
|
|
|
const distance = 1800;
|
2024-11-29 09:50:37 +08:00
|
|
|
const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
|
|
|
|
|
|
|
|
const newPos = node.x || node.y || node.z
|
|
|
|
? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
|
|
|
|
: { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)
|
|
|
|
|
|
|
|
Graph.cameraPosition(
|
|
|
|
newPos, // new position
|
|
|
|
node, // lookAt ({ x, y, z })
|
|
|
|
3000 // ms transition duration
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.cameraPosition({ z: distance })
|
|
|
|
|
|
|
|
// Spread nodes a little wider
|
|
|
|
// Graph.d3Force('charge').strength(-120);
|
|
|
|
|
|
|
|
// camera orbit
|
|
|
|
let angle = 0;
|
|
|
|
let time = setInterval(() => {
|
|
|
|
if (isRotationActive) {
|
|
|
|
Graph.cameraPosition({
|
|
|
|
x: distance * Math.sin(angle),
|
|
|
|
y: 0,
|
|
|
|
z: distance * Math.cos(angle),
|
|
|
|
});
|
2024-12-16 08:49:05 +08:00
|
|
|
angle += Math.PI / 500;
|
2024-11-29 09:50:37 +08:00
|
|
|
}
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
document.getElementById('rotationToggle').addEventListener('click', event => {
|
|
|
|
if(isRotationActive){
|
|
|
|
isRotationActive = !isRotationActive
|
|
|
|
}else{
|
|
|
|
isRotationActive = !isRotationActive
|
|
|
|
}
|
|
|
|
event.target.innerHTML = `${(isRotationActive ? '暂停' : '重置')} 旋转`;
|
|
|
|
});
|
|
|
|
|
|
|
|
</script>
|
|
|
|
</body>
|