| Cỡ chữ:   
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three/build/three.module.js", "three-spritetext": "https://unpkg.com/three-spritetext/dist/three-spritetext.mjs", "3d-force-graph": "https://unpkg.com/3d-force-graph/dist/3d-force-graph.mjs" } } </script> <link rel="stylesheet" href="style.css"> </head> <body> <div id="controls"> <h3>ToT 3D Explorer</h3> <label>THÊM Ý TƯỞNG (NODE)</label> <input type="text" id="nodeName" placeholder="Tên ý tưởng..."> <button onclick="addNewNode()">Thêm Nút</button> <hr> <label>TẠO LIÊN KẾT (LINK)</label> <input type="text" id="sourceNode" placeholder="ID Nguồn (số)"> <input type="text" id="targetNode" placeholder="ID Đích (số)"> <input type="text" id="linkText" placeholder="Nội dung liên kết (VD: Gây ra)"> <button onclick="addNewLink()">Kết nối</button> <hr> <p id="selectedInfo" style="font-size: 0.85em; color: #ffeb3b; min-height: 20px;">Chưa chọn mục nào</p> <button class="delete" onclick="deleteElement()">Xóa phần tử</button> <div class="instructions"> <b>Thao tác:</b><br> • Cuộn chuột: Zoom<br> • Chuột trái: Xoay không gian<br> • Chuột phải: Di chuyển nội dung </div> </div> <div id="3d-graph"></div> <script src="script.js"></script> </body> </html>
body { margin: 0; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #050505; } #controls { position: absolute; top: 15px; left: 15px; background: rgba(0, 0, 0, 0.8); padding: 20px; border-radius: 12px; color: #e0e0e0; backdrop-filter: blur(10px); z-index: 10; border: 1px solid rgba(255,255,255,0.1); width: 250px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); } input, button { margin: 8px 0; display: block; width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #333; background: #1a1a1a; color: white; box-sizing: border-box; } button { cursor: pointer; background: #2196f3; border: none; font-weight: bold; transition: 0.3s; } button:hover { background: #1976d2; } button.delete { background: #d32f2f; margin-top: 15px; } button.delete:hover { background: #b71c1c; } .instructions { font-size: 0.75em; color: #888; margin-top: 15px; line-height: 1.4; } h3 { margin: 0 0 15px 0; color: #2196f3; font-size: 1.2em; text-align: center; border-bottom: 1px solid #333; padding-bottom: 10px; } hr { border: 0; border-top: 1px solid #333; margin: 15px 0; } label { font-size: 0.8em; color: #aaa; }
// Dữ liệu ban đầu let gData = { nodes: [ { id: '1', name: 'Vấn đề chính', color: '#f44336' }, { id: '2', name: 'Giải pháp A', color: '#4caf50' }, { id: '3', name: 'Kết quả mong đợi', color: '#2196f3' } ], links: [ { source: '1', target: '2', label: 'Áp dụng' }, { source: '2', target: '3', label: 'Dẫn đến' } ] }; let selectedObj = null; const Graph = ForceGraph3D() (document.getElementById('3d-graph')) .graphData(gData) .nodeLabel('name') .nodeAutoColorBy('id') // Cấu hình hiển thị nhãn cho Link .linkThreeObjectExtend(true) .linkThreeObject(link => { // Sử dụng SpriteText để nhãn luôn hướng về camera const sprite = new SpriteText(`${link.label || ''}`); sprite.color = '#cccccc'; sprite.textHeight = 1.5; // Kích thước chữ return sprite; }) .linkPositionUpdate((sprite, { start, end }) => { // Định vị nhãn ở giữa sợi dây liên kết const middlePos = Object.assign({}, ...['x', 'y', 'z'].map(c => ({ [c]: start[c] + (end[c] - start[c]) / 2 }))); Object.assign(sprite.position, middlePos); }) .linkDirectionalParticles(3) .linkDirectionalParticleSpeed(0.005) .onNodeClick(node => { selectedObj = { type: 'node', data: node }; document.getElementById('selectedInfo').innerText = `Chọn Nút: ${node.name} (ID: ${node.id})`; }) .onLinkClick(link => { selectedObj = { type: 'link', data: link }; document.getElementById('selectedInfo').innerText = `Chọn Link: ${link.label || 'Không tên'}`; }); function addNewNode() { const name = document.getElementById('nodeName').value || "Ý tưởng mới"; const { nodes, links } = Graph.graphData(); // Tạo ID số tiếp theo const maxId = nodes.length > 0 ? Math.max(...nodes.map(n => parseInt(n.id))) : 0; const id = (maxId + 1).toString(); Graph.graphData({ nodes: [...nodes, { id, name }], links: links }); document.getElementById('nodeName').value = ''; } function addNewLink() { const source = document.getElementById('sourceNode').value; const target = document.getElementById('targetNode').value; const label = document.getElementById('linkText').value || ""; const { nodes, links } = Graph.graphData(); if (nodes.find(n => n.id === source) && nodes.find(n => n.id === target)) { Graph.graphData({ nodes: nodes, links: [...links, { source, target, label }] }); // Xóa input sau khi thêm document.getElementById('sourceNode').value = ''; document.getElementById('targetNode').value = ''; document.getElementById('linkText').value = ''; } else { alert("Lỗi: ID nguồn hoặc đích không tồn tại trên bản đồ!"); } } function deleteElement() { if (!selectedObj) return alert("Vui lòng chọn một nút hoặc liên kết trước!"); let { nodes, links } = Graph.graphData(); if (selectedObj.type === 'node') { nodes = nodes.filter(n => n.id !== selectedObj.data.id); links = links.filter(l => l.source.id !== selectedObj.data.id && l.target.id !== selectedObj.data.id); } else { links = links.filter(l => l !== selectedObj.data); } Graph.graphData({ nodes, links }); selectedObj = null; document.getElementById('selectedInfo').innerText = "Đã xóa phần tử thành công."; }