Files
tianxuan/static/tianxuan/globe_embed.js
T
PM-pinou b768d02987 refactor: extract inline JS/CSS from templates into static files
- 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
2026-07-24 13:26:07 +08:00

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);
})();
}