b768d02987
- static/tianxuan/globe_embed.js: Three.js globe scene (initGlobe function) - static/tianxuan/timeline.js: LLM workflow timeline (buildTimeline, renderTimeline, escapeHtml, prettyJson, toggleStep) - static/tianxuan/markdown.js: Markdown-to-HTML converter (renderMarkdown) - static/tianxuan/cluster.css: Shared cluster page styles (pills, cards, feats, detail panel) Updated templates: - auto.html: use timeline.js, remove 140 lines of inline JS - cluster_overview.html: use cluster.css, keep layout-specific styles - cluster_detail.html: use cluster.css, keep entity-card styles
130 lines
5.6 KiB
JavaScript
130 lines
5.6 KiB
JavaScript
/**
|
|
* Three.js 3D globe rendering for TLS flow analysis.
|
|
* Extracted from templates/tianxuan/globe_embed.html
|
|
*
|
|
* Usage: initGlobe('globeContainer', geoFlows, options)
|
|
*/
|
|
function initGlobe(containerId, geoFlows, options) {
|
|
options = options || {};
|
|
var W = window.innerWidth, H = window.innerHeight;
|
|
|
|
var scene = new THREE.Scene();
|
|
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 1000);
|
|
camera.position.set(0, 2, 12);
|
|
|
|
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setSize(W, H);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
document.getElementById(containerId).appendChild(renderer.domElement);
|
|
|
|
// Earth
|
|
var earthGeo = new THREE.SphereGeometry(5, 64, 64);
|
|
var earthMat = new THREE.MeshPhongMaterial({ color: 0x1a3a5c, emissive: 0x0a1a2a, specular: new THREE.Color(0x333333), shininess: 5 });
|
|
var earth = new THREE.Mesh(earthGeo, earthMat);
|
|
scene.add(earth);
|
|
|
|
// Glow
|
|
var glow = new THREE.Mesh(new THREE.SphereGeometry(5.1, 64, 64), new THREE.MeshBasicMaterial({ color: 0x224488, transparent: true, opacity: 0.1 }));
|
|
scene.add(glow);
|
|
|
|
// Lights
|
|
scene.add(new THREE.AmbientLight(0x222244));
|
|
var dl = new THREE.DirectionalLight(0xffffff, 1);
|
|
dl.position.set(5, 10, 7);
|
|
scene.add(dl);
|
|
scene.add(new THREE.DirectionalLight(0x4488ff, 0.3));
|
|
|
|
// Stars
|
|
var starGeo = new THREE.BufferGeometry();
|
|
var starPos = new Float32Array(1000 * 3);
|
|
for (var i = 0; i < 1000 * 3; i++) starPos[i] = (Math.random() - 0.5) * 200;
|
|
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
|
|
scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.15 })));
|
|
|
|
// Lat/lon grid
|
|
var gridMat = new THREE.LineBasicMaterial({ color: 0x6699cc, transparent: true, opacity: 0.1 });
|
|
for (var lat = -80; lat <= 80; lat += 20) {
|
|
var pts = [];
|
|
for (var lon = 0; lon <= 360; lon += 5) {
|
|
var phi = (90 - lat) * Math.PI / 180;
|
|
var theta = (lon + 180) * Math.PI / 180;
|
|
pts.push(new THREE.Vector3(-5.02 * Math.sin(phi) * Math.cos(theta), 5.02 * Math.cos(phi), 5.02 * Math.sin(phi) * Math.sin(theta)));
|
|
}
|
|
scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridMat));
|
|
}
|
|
for (var lon = 0; lon < 360; lon += 20) {
|
|
var pts = [];
|
|
for (var lat = -90; lat <= 90; lat += 5) {
|
|
var phi = (90 - lat) * Math.PI / 180;
|
|
var theta = (lon + 180) * Math.PI / 180;
|
|
pts.push(new THREE.Vector3(-5.02 * Math.sin(phi) * Math.cos(theta), 5.02 * Math.cos(phi), 5.02 * Math.sin(phi) * Math.sin(theta)));
|
|
}
|
|
scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridMat));
|
|
}
|
|
|
|
// Flow arcs
|
|
var tlsColors = { 'TLSv1.3': 0x4cc9f0, 'TLSv1.2': 0x43aa8b, 'TLSv1.1': 0xf8961e, 'TLSv1.0': 0xf94144 };
|
|
var flowGroup = new THREE.Group();
|
|
scene.add(flowGroup);
|
|
|
|
function latLonToVec3(lat, lon, r) {
|
|
if (lat == null || lon == null) return null;
|
|
var phi = (90 - lat) * Math.PI / 180;
|
|
var theta = (lon + 180) * Math.PI / 180;
|
|
return new THREE.Vector3(-r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta));
|
|
}
|
|
|
|
var arcMeshes = [];
|
|
var flowData = Array.isArray(geoFlows) ? geoFlows : (typeof geoFlows === 'string' ? JSON.parse(geoFlows) : []);
|
|
flowData.forEach(function (flow) {
|
|
var from = latLonToVec3(flow.slat, flow.slon, 5);
|
|
var to = latLonToVec3(flow.dlat, flow.dlon, 5);
|
|
if (!from || !to) return;
|
|
var mid = new THREE.Vector3().addVectors(from, to).multiplyScalar(0.5).normalize().multiplyScalar(7);
|
|
var curve = new THREE.QuadraticBezierCurve3(from, mid, to);
|
|
var color = tlsColors[flow.tls] || 0x888888;
|
|
var mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.6 });
|
|
var mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, 20, 0.02, 4, false), mat);
|
|
mesh.userData = flow;
|
|
flowGroup.add(mesh);
|
|
arcMeshes.push(mesh);
|
|
});
|
|
|
|
// Mouse rotation
|
|
var isDragging = false, prevMouse = { x: 0, y: 0 };
|
|
renderer.domElement.addEventListener('mousedown', function (e) { isDragging = true; prevMouse = { x: e.clientX, y: e.clientY }; });
|
|
window.addEventListener('mouseup', function () { isDragging = false; });
|
|
window.addEventListener('mousemove', function (e) {
|
|
if (!isDragging) return;
|
|
var dx = e.clientX - prevMouse.x, dy = e.clientY - prevMouse.y;
|
|
earth.rotation.y += dx * 0.005;
|
|
earth.rotation.z += dy * 0.005;
|
|
flowGroup.rotation.y = earth.rotation.y;
|
|
flowGroup.rotation.z = earth.rotation.z;
|
|
prevMouse = { x: e.clientX, y: e.clientY };
|
|
});
|
|
|
|
// Listen for cluster filter from parent
|
|
window.addEventListener('message', function (e) {
|
|
if (e.data && e.data.type === 'globe_filter') {
|
|
var label = e.data.cluster_label;
|
|
arcMeshes.forEach(function (mesh) {
|
|
if (label === null) {
|
|
mesh.material.opacity = 0.6;
|
|
mesh.material.color.setHex(tlsColors[mesh.userData.tls] || 0x888888);
|
|
} else {
|
|
mesh.material.opacity = 0.06;
|
|
mesh.material.color.setHex(0x666666);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Auto-rotate
|
|
(function animate() {
|
|
requestAnimationFrame(animate);
|
|
if (!isDragging) { earth.rotation.y += 0.003; flowGroup.rotation.y = earth.rotation.y; }
|
|
renderer.render(scene, camera);
|
|
})();
|
|
}
|