|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Exploratory Atlas | 3D Knowledge Navigator</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script> |
|
|
<style> |
|
|
body { |
|
|
margin: 0; |
|
|
overflow: hidden; |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
} |
|
|
#container { |
|
|
position: absolute; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
} |
|
|
#ui { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
pointer-events: none; |
|
|
} |
|
|
#ui > * { |
|
|
pointer-events: auto; |
|
|
} |
|
|
.panel { |
|
|
background: rgba(10, 20, 30, 0.85); |
|
|
backdrop-filter: blur(10px); |
|
|
border: 1px solid rgba(100, 180, 255, 0.2); |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); |
|
|
color: white; |
|
|
} |
|
|
.holographic { |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
} |
|
|
.holographic::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
top: -50%; |
|
|
left: -50%; |
|
|
width: 200%; |
|
|
height: 200%; |
|
|
background: linear-gradient( |
|
|
to bottom right, |
|
|
rgba(100, 200, 255, 0.1), |
|
|
rgba(100, 200, 255, 0.05), |
|
|
rgba(100, 200, 255, 0.1) |
|
|
); |
|
|
transform: rotate(30deg); |
|
|
z-index: -1; |
|
|
} |
|
|
.glow { |
|
|
text-shadow: 0 0 10px rgba(100, 200, 255, 0.7); |
|
|
} |
|
|
.node-info { |
|
|
transition: all 0.3s ease; |
|
|
max-height: 0; |
|
|
opacity: 0; |
|
|
overflow: hidden; |
|
|
} |
|
|
.node-info.active { |
|
|
max-height: 500px; |
|
|
opacity: 1; |
|
|
} |
|
|
.connection-line { |
|
|
stroke-dasharray: 1000; |
|
|
stroke-dashoffset: 1000; |
|
|
animation: dash 5s linear forwards; |
|
|
} |
|
|
@keyframes dash { |
|
|
to { |
|
|
stroke-dashoffset: 0; |
|
|
} |
|
|
} |
|
|
.pulse { |
|
|
animation: pulse 2s infinite; |
|
|
} |
|
|
@keyframes pulse { |
|
|
0%, 100% { opacity: 0.8; } |
|
|
50% { opacity: 1; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="container"></div> |
|
|
|
|
|
<div id="ui"> |
|
|
|
|
|
<div class="absolute top-4 left-4 right-4 flex justify-between items-start"> |
|
|
<div class="panel p-4 w-1/3"> |
|
|
<h1 class="text-2xl font-bold glow">EXPLORATORY ATLAS</h1> |
|
|
<p class="text-blue-300">3D Knowledge Navigation System</p> |
|
|
</div> |
|
|
|
|
|
<div class="panel p-4 flex space-x-4"> |
|
|
<div> |
|
|
<label class="block text-blue-300 text-sm mb-1">FROM</label> |
|
|
<input type="text" id="topicX" value="Artificial Intelligence" |
|
|
class="bg-transparent border-b border-blue-500 focus:outline-none text-white"> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-blue-300 text-sm mb-1">TO</label> |
|
|
<input type="text" id="topicY" value="Climate Change" |
|
|
class="bg-transparent border-b border-blue-500 focus:outline-none text-white"> |
|
|
</div> |
|
|
<button id="generateBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-1 rounded-lg flex items-center"> |
|
|
<i class="fas fa-satellite-dish mr-2"></i> Generate |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="panel absolute bottom-4 left-4 p-4 w-80"> |
|
|
<h2 class="text-lg font-semibold mb-3 flex items-center"> |
|
|
<i class="fas fa-cogs mr-2 text-blue-400"></i> Navigation Controls |
|
|
</h2> |
|
|
|
|
|
<div class="grid grid-cols-2 gap-3 mb-4"> |
|
|
<button class="bg-blue-800 hover:bg-blue-700 p-2 rounded-lg flex flex-col items-center"> |
|
|
<i class="fas fa-flag mb-1"></i> |
|
|
<span class="text-xs">Place Marker</span> |
|
|
</button> |
|
|
<button class="bg-purple-800 hover:bg-purple-700 p-2 rounded-lg flex flex-col items-center"> |
|
|
<i class="fas fa-digging mb-1"></i> |
|
|
<span class="text-xs">Excavate</span> |
|
|
</button> |
|
|
<button class="bg-green-800 hover:bg-green-700 p-2 rounded-lg flex flex-col items-center"> |
|
|
<i class="fas fa-expand mb-1"></i> |
|
|
<span class="text-xs">Focus Node</span> |
|
|
</button> |
|
|
<button class="bg-yellow-800 hover:bg-yellow-700 p-2 rounded-lg flex flex-col items-center"> |
|
|
<i class="fas fa-route mb-1"></i> |
|
|
<span class="text-xs">Trace Path</span> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="mb-3"> |
|
|
<h3 class="text-sm font-semibold mb-2 flex items-center"> |
|
|
<i class="fas fa-map-marked-alt mr-1 text-blue-400"></i> Legend |
|
|
</h3> |
|
|
<div class="grid grid-cols-2 gap-2 text-xs"> |
|
|
<div class="flex items-center"> |
|
|
<div class="w-3 h-3 bg-green-500 rounded-full mr-2"></div> |
|
|
<span>Core Concepts</span> |
|
|
</div> |
|
|
<div class="flex items-center"> |
|
|
<div class="w-3 h-3 bg-red-500 rounded-full mr-2"></div> |
|
|
<span>Contradictions</span> |
|
|
</div> |
|
|
<div class="flex items-center"> |
|
|
<div class="w-3 h-3 bg-blue-500 rounded-full mr-2"></div> |
|
|
<span>Relationships</span> |
|
|
</div> |
|
|
<div class="flex items-center"> |
|
|
<div class="w-3 h-3 bg-gray-400 rounded-full mr-2 pulse"></div> |
|
|
<span>Uncertainty</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="panel absolute top-1/2 right-4 transform -translate-y-1/2 w-80 max-h-[70vh] overflow-y-auto"> |
|
|
<div class="p-4"> |
|
|
<h2 id="nodeTitle" class="text-xl font-bold mb-2">Knowledge Node</h2> |
|
|
<div id="nodeType" class="text-xs px-2 py-1 bg-blue-900 rounded-full inline-block mb-3">Concept</div> |
|
|
<div id="nodeContent" class="text-sm"> |
|
|
<p>Select a node in the visualization to view detailed information.</p> |
|
|
</div> |
|
|
|
|
|
<div id="nodeConnections" class="mt-4"> |
|
|
<h3 class="text-sm font-semibold mb-2">Connected Concepts</h3> |
|
|
<div class="space-y-2"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mt-6 pt-4 border-t border-gray-700"> |
|
|
<button class="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm mr-2"> |
|
|
<i class="fas fa-bookmark mr-1"></i> Save |
|
|
</button> |
|
|
<button class="bg-green-600 hover:bg-green-700 px-3 py-1 rounded text-sm"> |
|
|
<i class="fas fa-share-alt mr-1"></i> Share |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="panel absolute bottom-4 right-4 w-1/3"> |
|
|
<div class="p-4"> |
|
|
<h2 class="text-lg font-semibold mb-3 flex items-center"> |
|
|
<i class="fas fa-history mr-2 text-blue-400"></i> Exploration Path |
|
|
</h2> |
|
|
<div class="flex overflow-x-auto space-x-2 py-2"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
let scene, camera, renderer, controls; |
|
|
let knowledgeNodes = []; |
|
|
let connections = []; |
|
|
let selectedNode = null; |
|
|
|
|
|
initThreeJS(); |
|
|
createDemoKnowledgeGraph(); |
|
|
animate(); |
|
|
|
|
|
function initThreeJS() { |
|
|
|
|
|
scene = new THREE.Scene(); |
|
|
scene.background = new THREE.Color(0x020a13); |
|
|
scene.fog = new THREE.FogExp2(0x020a13, 0.002); |
|
|
|
|
|
|
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
|
|
camera.position.z = 50; |
|
|
camera.position.y = 30; |
|
|
|
|
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
|
renderer.setPixelRatio(window.devicePixelRatio); |
|
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
|
renderer.shadowMap.enabled = true; |
|
|
document.getElementById('container').appendChild(renderer.domElement); |
|
|
|
|
|
|
|
|
controls = new THREE.OrbitControls(camera, renderer.domElement); |
|
|
controls.enableDamping = true; |
|
|
controls.dampingFactor = 0.05; |
|
|
controls.minDistance = 20; |
|
|
controls.maxDistance = 200; |
|
|
|
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040); |
|
|
scene.add(ambientLight); |
|
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); |
|
|
directionalLight.position.set(1, 1, 1); |
|
|
directionalLight.castShadow = true; |
|
|
scene.add(directionalLight); |
|
|
|
|
|
const hemisphereLight = new THREE.HemisphereLight(0x00aaff, 0xffaa00, 0.3); |
|
|
scene.add(hemisphereLight); |
|
|
|
|
|
|
|
|
const starsGeometry = new THREE.BufferGeometry(); |
|
|
const starVertices = []; |
|
|
for (let i = 0; i < 10000; i++) { |
|
|
const x = (Math.random() - 0.5) * 2000; |
|
|
const y = (Math.random() - 0.5) * 2000; |
|
|
const z = (Math.random() - 0.5) * 2000; |
|
|
starVertices.push(x, y, z); |
|
|
} |
|
|
starsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3)); |
|
|
const starsMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.1 }); |
|
|
const starField = new THREE.Points(starsGeometry, starsMaterial); |
|
|
scene.add(starField); |
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => { |
|
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
|
camera.updateProjectionMatrix(); |
|
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
|
}); |
|
|
} |
|
|
|
|
|
function createDemoKnowledgeGraph() { |
|
|
|
|
|
knowledgeNodes.forEach(node => scene.remove(node.mesh)); |
|
|
connections.forEach(conn => scene.remove(conn.line)); |
|
|
knowledgeNodes = []; |
|
|
connections = []; |
|
|
|
|
|
|
|
|
createNode("AI & Climate", 0, 0, 0, "core", 5); |
|
|
|
|
|
|
|
|
const node1 = createNode("Ethical AI", -20, 5, 15, "core", 4); |
|
|
const node2 = createNode("Carbon Footprint", 15, 8, -10, "core", 4); |
|
|
const node3 = createNode("Data Centers", 5, -10, -15, "relationship", 3.5); |
|
|
const node4 = createNode("AI Solutions", -10, -15, 20, "relationship", 3.5); |
|
|
const node5 = createNode("Policy Impact", 25, -5, 5, "contradiction", 3); |
|
|
const node6 = createNode("Future Predictions", -5, 25, -5, "uncertainty", 3); |
|
|
|
|
|
|
|
|
createConnection(knowledgeNodes[0], node1, "strong"); |
|
|
createConnection(knowledgeNodes[0], node2, "strong"); |
|
|
createConnection(knowledgeNodes[0], node3, "medium"); |
|
|
createConnection(knowledgeNodes[0], node4, "medium"); |
|
|
createConnection(node1, node4, "weak"); |
|
|
createConnection(node2, node3, "strong"); |
|
|
createConnection(node2, node5, "weak"); |
|
|
createConnection(node3, node6, "weak"); |
|
|
createConnection(node4, node6, "medium"); |
|
|
|
|
|
|
|
|
addTechElements(); |
|
|
} |
|
|
|
|
|
function createNode(title, x, y, z, type, size) { |
|
|
let geometry, material; |
|
|
const color = getColorForType(type); |
|
|
|
|
|
if (type === "core") { |
|
|
geometry = new THREE.IcosahedronGeometry(size, 1); |
|
|
material = new THREE.MeshPhongMaterial({ |
|
|
color: color, |
|
|
emissive: color, |
|
|
emissiveIntensity: 0.2, |
|
|
shininess: 100, |
|
|
wireframe: false |
|
|
}); |
|
|
} else if (type === "relationship") { |
|
|
geometry = new THREE.TorusGeometry(size * 0.7, size * 0.3, 16, 32); |
|
|
material = new THREE.MeshPhongMaterial({ |
|
|
color: color, |
|
|
emissive: color, |
|
|
emissiveIntensity: 0.1, |
|
|
shininess: 50, |
|
|
wireframe: false |
|
|
}); |
|
|
} else if (type === "contradiction") { |
|
|
geometry = new THREE.OctahedronGeometry(size, 0); |
|
|
material = new THREE.MeshPhongMaterial({ |
|
|
color: color, |
|
|
emissive: color, |
|
|
emissiveIntensity: 0.3, |
|
|
shininess: 30, |
|
|
wireframe: false |
|
|
}); |
|
|
} else { |
|
|
geometry = new THREE.SphereGeometry(size, 32, 32); |
|
|
material = new THREE.MeshPhongMaterial({ |
|
|
color: color, |
|
|
emissive: color, |
|
|
emissiveIntensity: 0.05, |
|
|
transparent: true, |
|
|
opacity: 0.7, |
|
|
wireframe: true |
|
|
}); |
|
|
} |
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, material); |
|
|
mesh.position.set(x, y, z); |
|
|
mesh.castShadow = true; |
|
|
mesh.receiveShadow = true; |
|
|
|
|
|
|
|
|
if (type === "uncertainty") { |
|
|
mesh.userData.pulseSpeed = 0.02 + Math.random() * 0.03; |
|
|
mesh.userData.originalScale = size; |
|
|
} |
|
|
|
|
|
|
|
|
mesh.userData = { title, type, description: generateDescription(title, type) }; |
|
|
mesh.userData.connections = []; |
|
|
|
|
|
scene.add(mesh); |
|
|
|
|
|
|
|
|
const label = document.createElement('div'); |
|
|
label.className = `absolute text-white text-xs font-medium bg-black bg-opacity-50 px-2 py-1 rounded pointer-events-none`; |
|
|
label.textContent = title; |
|
|
document.getElementById('ui').appendChild(label); |
|
|
|
|
|
mesh.userData.label = label; |
|
|
|
|
|
knowledgeNodes.push({ mesh, label, title, type }); |
|
|
return { mesh, label, title, type }; |
|
|
} |
|
|
|
|
|
function createConnection(node1, node2, strength) { |
|
|
const start = node1.mesh.position; |
|
|
const end = node2.mesh.position; |
|
|
|
|
|
|
|
|
const curve = new THREE.CatmullRomCurve3([ |
|
|
new THREE.Vector3(start.x, start.y, start.z), |
|
|
new THREE.Vector3( |
|
|
(start.x + end.x) / 2 + (Math.random() - 0.5) * 10, |
|
|
(start.y + end.y) / 2 + (Math.random() - 0.5) * 10, |
|
|
(start.z + end.z) / 2 + (Math.random() - 0.5) * 10 |
|
|
), |
|
|
new THREE.Vector3(end.x, end.y, end.z) |
|
|
]); |
|
|
|
|
|
const points = curve.getPoints(50); |
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points); |
|
|
|
|
|
let color, linewidth; |
|
|
if (strength === "strong") { |
|
|
color = 0x00ff00; |
|
|
linewidth = 2; |
|
|
} else if (strength === "medium") { |
|
|
color = 0x0088ff; |
|
|
linewidth = 1.5; |
|
|
} else { |
|
|
color = 0x888888; |
|
|
linewidth = 1; |
|
|
} |
|
|
|
|
|
const material = new THREE.LineBasicMaterial({ |
|
|
color: color, |
|
|
linewidth: linewidth, |
|
|
transparent: true, |
|
|
opacity: 0.7 |
|
|
}); |
|
|
|
|
|
const line = new THREE.Line(geometry, material); |
|
|
scene.add(line); |
|
|
|
|
|
|
|
|
const connection = { |
|
|
node1, node2, strength, line, |
|
|
description: `${node1.title} ↔ ${node2.title} (${strength})` |
|
|
}; |
|
|
|
|
|
connections.push(connection); |
|
|
node1.mesh.userData.connections.push(connection); |
|
|
node2.mesh.userData.connections.push(connection); |
|
|
|
|
|
return connection; |
|
|
} |
|
|
|
|
|
function addTechElements() { |
|
|
|
|
|
const techGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.2, 6); |
|
|
const techMaterial = new THREE.MeshPhongMaterial({ |
|
|
color: 0x00aaff, |
|
|
emissive: 0x0044ff, |
|
|
emissiveIntensity: 0.5, |
|
|
transparent: true, |
|
|
opacity: 0.8 |
|
|
}); |
|
|
|
|
|
for (let i = 0; i < 20; i++) { |
|
|
const tech = new THREE.Mesh(techGeometry, techMaterial); |
|
|
tech.position.set( |
|
|
(Math.random() - 0.5) * 100, |
|
|
(Math.random() - 0.5) * 100, |
|
|
(Math.random() - 0.5) * 100 |
|
|
); |
|
|
tech.rotation.x = Math.random() * Math.PI; |
|
|
tech.rotation.y = Math.random() * Math.PI; |
|
|
tech.userData = { speed: 0.01 + Math.random() * 0.02 }; |
|
|
scene.add(tech); |
|
|
} |
|
|
} |
|
|
|
|
|
function getColorForType(type) { |
|
|
switch(type) { |
|
|
case "core": return 0x00aa00; |
|
|
case "relationship": return 0x0066ff; |
|
|
case "contradiction": return 0xff3300; |
|
|
case "uncertainty": return 0x888888; |
|
|
default: return 0xffffff; |
|
|
} |
|
|
} |
|
|
|
|
|
function generateDescription(title, type) { |
|
|
const descriptors = { |
|
|
core: ["Fundamental concept", "Key principle", "Central idea"], |
|
|
relationship: ["Connected to", "Influences", "Related to"], |
|
|
contradiction: ["Contrasts with", "Opposes", "Conflicts with"], |
|
|
uncertainty: ["Emerging concept", "Debated topic", "Unclear relationship"] |
|
|
}; |
|
|
|
|
|
const aspects = { |
|
|
"AI & Climate": "the intersection of artificial intelligence and climate science", |
|
|
"Ethical AI": "ethical considerations in AI development", |
|
|
"Carbon Footprint": "environmental impact of technology", |
|
|
"Data Centers": "energy consumption of computing infrastructure", |
|
|
"AI Solutions": "how AI can help address climate change", |
|
|
"Policy Impact": "how policies affect AI and climate initiatives", |
|
|
"Future Predictions": "speculative outcomes of AI on climate" |
|
|
}; |
|
|
|
|
|
const descType = descriptors[type][Math.floor(Math.random() * descriptors[type].length)]; |
|
|
const aspect = aspects[title] || "this concept"; |
|
|
|
|
|
return `${descType} about ${aspect}. This ${type} node represents important knowledge in the network.`; |
|
|
} |
|
|
|
|
|
function updateNodeInfo(node) { |
|
|
document.getElementById('nodeTitle').textContent = node.title; |
|
|
document.getElementById('nodeType').textContent = node.type.charAt(0).toUpperCase() + node.type.slice(1); |
|
|
document.getElementById('nodeType').className = `text-xs px-2 py-1 rounded-full inline-block mb-3 ${ |
|
|
node.type === 'core' ? 'bg-green-900' : |
|
|
node.type === 'relationship' ? 'bg-blue-900' : |
|
|
node.type === 'contradiction' ? 'bg-red-900' : 'bg-gray-700' |
|
|
}`; |
|
|
|
|
|
document.getElementById('nodeContent').innerHTML = ` |
|
|
<p class="mb-3">${node.mesh.userData.description}</p> |
|
|
<div class="bg-gray-800 p-3 rounded-lg text-xs"> |
|
|
<p class="font-semibold mb-1">Key Attributes:</p> |
|
|
<ul class="list-disc pl-4"> |
|
|
<li>${node.type === 'core' ? 'Well-established knowledge' : |
|
|
node.type === 'relationship' ? 'Demonstrated connection' : |
|
|
node.type === 'contradiction' ? 'Opposing evidence exists' : 'Emerging/uncertain evidence'}</li> |
|
|
<li>Connected to ${node.mesh.userData.connections.length} other nodes</li> |
|
|
<li>${node.type === 'uncertainty' ? 'Requires further research' : 'Substantial evidence available'}</li> |
|
|
</ul> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const connectionsContainer = document.getElementById('nodeConnections'); |
|
|
connectionsContainer.innerHTML = ` |
|
|
<h3 class="text-sm font-semibold mb-2">Connected Concepts</h3> |
|
|
<div class="space-y-2"> |
|
|
${node.mesh.userData.connections.map(conn => { |
|
|
const otherNode = conn.node1 === node ? conn.node2 : conn.node1; |
|
|
return ` |
|
|
<div class="flex items-center p-2 bg-gray-800 rounded-lg cursor-pointer hover:bg-gray-700" |
|
|
onclick="focusNode('${otherNode.title}')"> |
|
|
<div class="w-3 h-3 rounded-full mr-2 ${ |
|
|
otherNode.type === 'core' ? 'bg-green-500' : |
|
|
otherNode.type === 'relationship' ? 'bg-blue-500' : |
|
|
otherNode.type === 'contradiction' ? 'bg-red-500' : 'bg-gray-400' |
|
|
}"></div> |
|
|
<span class="text-sm">${otherNode.title}</span> |
|
|
<span class="ml-auto text-xs opacity-70">${conn.strength}</span> |
|
|
</div> |
|
|
`; |
|
|
}).join('')} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function focusNode(title) { |
|
|
const node = knowledgeNodes.find(n => n.title === title); |
|
|
if (node) { |
|
|
|
|
|
const targetPosition = new THREE.Vector3(); |
|
|
node.mesh.getWorldPosition(targetPosition); |
|
|
|
|
|
|
|
|
const direction = new THREE.Vector3().subVectors(camera.position, targetPosition).normalize(); |
|
|
const focusPosition = new THREE.Vector3().copy(targetPosition).add(direction.multiplyScalar(15)); |
|
|
|
|
|
|
|
|
focusPosition.y += 5; |
|
|
|
|
|
|
|
|
animateCameraToPosition(focusPosition, targetPosition); |
|
|
|
|
|
|
|
|
highlightNode(node); |
|
|
} |
|
|
} |
|
|
|
|
|
function animateCameraToPosition(newPosition, lookAt) { |
|
|
const startPosition = camera.position.clone(); |
|
|
const startQuaternion = camera.quaternion.clone(); |
|
|
|
|
|
camera.lookAt(lookAt); |
|
|
const endQuaternion = camera.quaternion.clone(); |
|
|
camera.quaternion.copy(startQuaternion); |
|
|
|
|
|
let progress = 0; |
|
|
const duration = 1000; |
|
|
const startTime = Date.now(); |
|
|
|
|
|
function updateCamera() { |
|
|
progress = (Date.now() - startTime) / duration; |
|
|
if (progress >= 1) { |
|
|
camera.position.copy(newPosition); |
|
|
camera.quaternion.copy(endQuaternion); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
progress = progress < 0.5 ? |
|
|
2 * progress * progress : |
|
|
1 - Math.pow(-2 * progress + 2, 2) / 2; |
|
|
|
|
|
camera.position.lerpVectors(startPosition, newPosition, progress); |
|
|
|
|
|
|
|
|
THREE.Quaternion.slerp(startQuaternion, endQuaternion, camera.quaternion, progress); |
|
|
|
|
|
requestAnimationFrame(updateCamera); |
|
|
} |
|
|
|
|
|
updateCamera(); |
|
|
} |
|
|
|
|
|
function highlightNode(node) { |
|
|
|
|
|
knowledgeNodes.forEach(n => { |
|
|
n.mesh.material.emissiveIntensity = n.type === 'uncertainty' ? 0.05 : 0.2; |
|
|
n.mesh.scale.set(1, 1, 1); |
|
|
}); |
|
|
|
|
|
|
|
|
node.mesh.material.emissiveIntensity = 0.8; |
|
|
node.mesh.scale.set(1.2, 1.2, 1.2); |
|
|
|
|
|
|
|
|
updateNodeInfo(node); |
|
|
selectedNode = node; |
|
|
|
|
|
|
|
|
addToExplorationPath(node); |
|
|
} |
|
|
|
|
|
function addToExplorationPath(node) { |
|
|
const pathContainer = document.querySelector('#ui > .panel.absolute.bottom-4.right-4 .flex'); |
|
|
|
|
|
const pathItem = document.createElement('div'); |
|
|
pathItem.className = `flex-shrink-0 px-3 py-1 rounded-full text-sm flex items-center ${ |
|
|
node.type === 'core' ? 'bg-green-900 text-green-300' : |
|
|
node.type === 'relationship' ? 'bg-blue-900 text-blue-300' : |
|
|
node.type === 'contradiction' ? 'bg-red-900 text-red-300' : 'bg-gray-800 text-gray-300' |
|
|
}`; |
|
|
|
|
|
pathItem.innerHTML = ` |
|
|
<i class="fas ${ |
|
|
node.type === 'core' ? 'fa-certificate' : |
|
|
node.type === 'relationship' ? 'fa-link' : |
|
|
node.type === 'contradiction' ? 'fa-exclamation-triangle' : 'fa-question-circle' |
|
|
} mr-1"></i> ${node.title} |
|
|
`; |
|
|
|
|
|
pathContainer.appendChild(pathItem); |
|
|
pathContainer.scrollLeft = pathContainer.scrollWidth; |
|
|
} |
|
|
|
|
|
|
|
|
function animate() { |
|
|
requestAnimationFrame(animate); |
|
|
|
|
|
|
|
|
controls.update(); |
|
|
|
|
|
|
|
|
knowledgeNodes.forEach(node => { |
|
|
const vector = node.mesh.position.clone().project(camera); |
|
|
const x = (vector.x * 0.5 + 0.5) * window.innerWidth; |
|
|
const y = (-(vector.y * 0.5) + 0.5) * window.innerHeight; |
|
|
|
|
|
node.label.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; |
|
|
node.label.style.zIndex = vector.z > 1 ? -1 : Math.round((1 - vector.z) * 10000); |
|
|
}); |
|
|
|
|
|
|
|
|
knowledgeNodes.filter(n => n.type === 'uncertainty').forEach(node => { |
|
|
const scale = node.mesh.userData.originalScale * (1 + Math.sin(Date.now() * node.mesh.userData.pulseSpeed) * 0.1); |
|
|
node.mesh.scale.set(scale, scale, scale); |
|
|
}); |
|
|
|
|
|
|
|
|
scene.children.filter(obj => obj.userData?.speed).forEach(obj => { |
|
|
obj.rotation.x += obj.userData.speed * 0.5; |
|
|
obj.rotation.y += obj.userData.speed; |
|
|
obj.position.y += Math.sin(Date.now() * 0.001 + obj.position.x) * 0.02; |
|
|
}); |
|
|
|
|
|
renderer.render(scene, camera); |
|
|
} |
|
|
|
|
|
|
|
|
const raycaster = new THREE.Raycaster(); |
|
|
const mouse = new THREE.Vector2(); |
|
|
|
|
|
function onMouseClick(event) { |
|
|
|
|
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1; |
|
|
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; |
|
|
|
|
|
|
|
|
raycaster.setFromCamera(mouse, camera); |
|
|
|
|
|
|
|
|
const intersects = raycaster.intersectObjects( |
|
|
knowledgeNodes.map(n => n.mesh) |
|
|
); |
|
|
|
|
|
if (intersects.length > 0) { |
|
|
const clickedNode = knowledgeNodes.find(n => n.mesh === intersects[0].object); |
|
|
if (clickedNode) { |
|
|
highlightNode(clickedNode); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
window.addEventListener('click', onMouseClick, false); |
|
|
|
|
|
|
|
|
document.getElementById('generateBtn').addEventListener('click', function() { |
|
|
const topicX = document.getElementById('topicX').value || 'Topic X'; |
|
|
const topicY = document.getElementById('topicY').value || 'Topic Y'; |
|
|
|
|
|
|
|
|
document.querySelector('header h1').textContent = `EXPLORATORY ATLAS: ${topicX} ↔ ${topicY}`; |
|
|
|
|
|
|
|
|
|
|
|
createDemoKnowledgeGraph(); |
|
|
|
|
|
|
|
|
const notification = document.createElement('div'); |
|
|
notification.className = 'fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg transform translate-x-0 transition-transform'; |
|
|
notification.innerHTML = `<i class="fas fa-satellite-dish mr-2"></i> Generating knowledge map for ${topicX} and ${topicY}`; |
|
|
document.body.appendChild(notification); |
|
|
|
|
|
setTimeout(() => { |
|
|
notification.style.transform = 'translateX(200%)'; |
|
|
setTimeout(() => notification.remove(), 300); |
|
|
}, 3000); |
|
|
}); |
|
|
|
|
|
|
|
|
window.focusNode = focusNode; |
|
|
</script> |
|
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LukasBe/3d-knowledge-navigation-system" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |