<!
doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>2.5D Spherical Graph — Demo</title>
<style>
html,body{height:100%;margin:0;background:#0b1220;color:#ddd;font-
family:Inter,system-ui,Arial}
#container{width:100%;height:100%;position:relative;overflow:hidden}
#ui {
position:absolute;right:12px;top:12px;background:rgba(10,12,20,0.45);
color:#fff;padding:10px;border-radius:8px;font-size:13px;backdrop-filter:
blur(6px);
}
#legend {position:absolute;left:12px;bottom:12px;background:rgba(10,12,20,0.45);
padding:8px;border-radius:8px;}
.label {
padding:4px 7px;background:rgba(0,0,0,0.6);border-radius:4px;font-weight:600;
font-size:12px;color:#fff;pointer-events:none;white-space:nowrap;
}
[Link]{font-size:12px;color:#9fd; text-decoration:none}
</style>
</head>
<body>
<div id="container"></div>
<div id="ui">
<div><strong>2.5D Spherical Graph</strong></div>
<div style="margin-top:6px">
<label style="display:block">Node scale <input id="nodeScale" type="range"
min="0.2" max="3" step="0.1" value="1"></label>
<label>Auto rotate <input id="autoRotate" type="checkbox" checked></label>
</div>
<div style="margin-top:6px"><small><a class="small" href="#"
id="toggleLabels">Toggle labels</a></small></div>
</div>
<div id="legend">
<div style="font-size:12px;margin-bottom:6px">Legend</div>
<div style="display:flex;gap:8px;align-items:center">
<div style="width:12px;height:12px;border-radius:50%;background:#ff8c42"></
div><div style="font-size:12px">High value</div>
</div>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<div style="width:12px;height:12px;border-radius:50%;background:#4499ff"></
div><div style="font-size:12px">Low value</div>
</div>
</div>
<!-- [Link] from CDN -->
<script src="[Link]
<script
src="[Link]
script>
<script>
/*
2.5D Spherical Graph — Interactive demo
Data format: array of nodes:
{ id, name, lat (deg), lon (deg), value (0..1), group (optional) }
*/
const sampleData = [
{id:1, name:"Core", lat: 10, lon: -10, value: 0.95, group: "A"},
{id:2, name:"Node B", lat: 40, lon: 60, value: 0.7, group: "A"},
{id:3, name:"Node C", lat: -20, lon: 120, value: 0.4, group: "B"},
{id:4, name:"Node D", lat: -45, lon: -140, value: 0.25, group: "B"},
{id:5, name:"Node E", lat: 60, lon: -60, value: 0.5, group: "C"}
];
const connections = [
[1,2],[1,3],[2,5],[3,4],[4,5]
];
const container = [Link]('container');
const scene = new [Link]();
[Link] = new THREE.FogExp2(0x071019, 0.06);
const camera = new [Link](50, innerWidth/innerHeight, 0.1, 1000);
[Link](0, 0, 8);
const renderer = new [Link]({antialias:true, alpha:true});
[Link](innerWidth, innerHeight);
[Link]([Link]([Link], 2));
[Link]([Link]);
// controls
const controls = new [Link](camera, [Link]);
[Link] = true;
[Link] = false;
[Link] = 4;
[Link] = 30;
// lighting
const hemi = new [Link](0xbfe8ff, 0x202030, 0.9);
[Link](hemi);
const spot = new [Link](0xffffff, 0.9);
[Link](5,5,5);
[Link](spot);
// globe base (subtle glassy)
const globeRadius = 3;
const globeGeo = new [Link](globeRadius, 64, 40);
const globeMat = new [Link]({
color: 0x0f2a46,
metalness: 0.1,
roughness: 0.2,
transmission: 0.5,
transparent: true,
opacity: 0.9,
clearcoat: 0.6,
clearcoatRoughness: 0.1
});
const globeMesh = new [Link](globeGeo, globeMat);
[Link] = 0;
[Link](globeMesh);
// faint grid lines (latitude/longitude)
const gridMat = new [Link]({color:0x2b4b6b, transparent:true,
opacity:0.35});
const gridLines = new [Link]();
for(let lat=-80; lat<=80; lat+=20){
const radius = globeRadius * [Link]([Link](lat));
const y = globeRadius * [Link]([Link](lat));
const circle = new [Link](radius, 128);
[Link] = [Link]; // compatibility
const geom = new [Link]();
const pts = [];
for(let i=0;i<=128;i++){
const a = i/128 * [Link] * 2;
[Link](new THREE.Vector3([Link](a)*radius, y, [Link](a)*radius));
}
[Link](pts);
const line = new [Link](geom, gridMat);
[Link](line);
}
[Link](gridLines);
// node group
const nodeGroup = new [Link]();
[Link](nodeGroup);
// helper: lat/lon -> 3D point on sphere
function latLonToVector3(lat, lon, radius){
const phi = [Link](90 - lat);
const theta = [Link](lon + 180);
const x = radius * [Link](phi) * [Link](theta);
const y = radius * [Link](phi);
const z = radius * [Link](phi) * [Link](theta);
return new THREE.Vector3(x,y,z);
}
// color scale: maps value 0..1 into blue->orange
function valueToColor(v){
const c1 = new [Link](0x4499ff); // low
const c2 = new [Link](0xff8c42); // high
return [Link](c2, [Link](v,0,1));
}
// create node sprite label
function makeLabel(text){
const canvas = [Link]('canvas');
const ctx = [Link]('2d');
[Link] = '600 36px sans-serif';
const padding = 12;
const w = [Link]([Link](text).width) + padding*2;
const h = 48;
[Link] = w;
[Link] = h;
// background
[Link] = 'rgba(0,0,0,0.6)';
[Link](0,0,w,h);
// text
[Link] = '#fff';
[Link] = '600 26px sans-serif';
[Link] = 'middle';
[Link](text, padding, h/2 + 2);
const tex = new [Link](canvas);
[Link] = [Link];
const sprite = new [Link](new [Link]({map: tex,
transparent:true}));
[Link]((w/64), (h/64), 1);
return sprite;
}
// add nodes
const nodeData = [];
const labelSprites = [];
for(const d of sampleData){
const pos = latLonToVector3([Link], [Link], globeRadius + 0.06 +
([Link]*0.12)); // slightly above surface
const color = valueToColor([Link]);
// sphere
const sphereGeo = new [Link](0.05 + [Link]*0.14, 16, 12);
const sphereMat = new [Link]({
color: color,
emissive: [Link]().multiplyScalar(0.12),
metalness: 0.4,
roughness: 0.3
});
const nodeMesh = new [Link](sphereGeo, sphereMat);
[Link](pos);
[Link] = {...d, basePos: [Link]()};
[Link](nodeMesh);
[Link](nodeMesh);
// glow (a transparent, slightly larger sprite)
const spriteMat = new [Link]({
map: new [Link]().load(createGlowCanvas([Link]())),
blending: [Link],
opacity: 0.65,
depthWrite: false
});
const glow = new [Link](spriteMat);
[Link](0.6 + [Link]*0.6, 0.6 + [Link]*0.6, 1);
[Link](pos);
[Link](glow);
// label
const sprite = makeLabel([Link] + ' • ' + [Link]([Link]*100) + '%');
[Link]([Link]().multiplyScalar(1.06));
[Link](sprite);
[Link](sprite);
}
// create connectors with smooth curve
const connGroup = new [Link]();
for(const pair of connections){
const a = [Link](n=>[Link]===pair[0]);
const b = [Link](n=>[Link]===pair[1]);
if(!a || !b) continue;
const mid = [Link]().add([Link]).multiplyScalar(0.5);
// push midpoint slightly outwards to create an arc
const direction = [Link]().normalize();
const arcHeight = 0.6 + ([Link] + [Link]) * 0.5;
[Link]([Link](arcHeight));
const curve = new THREE.CatmullRomCurve3([[Link](), mid,
[Link]()]);
const pts = [Link](60);
const geom = new [Link]().setFromPoints(pts);
const mat = new [Link]({color:0xffffff, linewidth:1,
transparent:true, opacity:0.45});
const line = new [Link](geom, mat);
[Link](line);
}
[Link](connGroup);
// small subtle rim outline (2.5D layering effect)
const rim = new [Link](
new [Link](globeRadius + 0.02, 64, 32),
new [Link]({color:0xffffff, transparent:true, opacity:0.035})
);
[Link](rim);
// helpers
function createGlowCanvas(colorStyle){
const size = 128;
const c = [Link]('canvas');
[Link]=[Link]=size;
const ctx = [Link]('2d');
const g = [Link](size/2,size/2,0,size/2,size/2,size/2);
[Link](0, colorStyle);
[Link](0.2, colorStyle);
[Link](0.7, 'rgba(0,0,0,0.05)');
[Link](1, 'rgba(0,0,0,0)');
[Link] = g;
[Link](0,0,size,size);
return [Link]();
}
// interaction: raycast hover for tiny pop
const raycaster = new [Link]();
const pointer = new THREE.Vector2();
let hovered = null;
function onPointerMove(e){
const rect = [Link]();
pointer.x = (([Link] - [Link]) / [Link]) * 2 - 1;
pointer.y = -(([Link] - [Link]) / [Link]) * 2 + 1;
}
[Link]('pointermove', onPointerMove);
[Link]('click', (e)=>{
if(hovered){
alert('Clicked: ' + [Link] + '\\nvalue: ' +
[Link]);
}
});
// UI bindings
const nodeScaleEl = [Link]('nodeScale');
const autoRotateEl = [Link]('autoRotate');
const toggleLabels = [Link]('toggleLabels');
let labelsOn = true;
[Link]('click', (ev)=>{[Link](); labelsOn = !
labelsOn; [Link](s=>[Link]=labelsOn);});
[Link]('resize', ()=>{
[Link] = innerWidth/innerHeight;
[Link]();
[Link](innerWidth, innerHeight);
});
// animation
const clock = new [Link]();
function animate(){
requestAnimationFrame(animate);
const t = [Link]();
// gentle globe breathing / parallax (2.5D feel)
[Link].y += ([Link] ? 0.004 : 0) ;
[Link].y = [Link].y * 0.98;
[Link]([Link]);
// node subtle bob & face labels to camera
const scaleFactor = parseFloat([Link]);
[Link]((n, i)=>{
// bobbing
const offset = 0.06 * [Link](t*1.2 + i);
const target =
[Link]().normalize().multiplyScalar(globeRadius + 0.06 +
[Link]*0.12 + offset);
[Link](target, 0.08);
// scale by UI
[Link](scaleFactor);
// label facing camera
labelSprites[i].[Link]([Link]().multiplyScalar(1.06));
labelSprites[i].lookAt([Link]);
});
// hover detection
[Link](pointer, camera);
const intersects = [Link](nodeData, false);
if([Link]){
const obj = intersects[0].object;
if(hovered !== obj){
if(hovered) [Link](0.2);
hovered = obj;
[Link] = [Link]().add(new
[Link](0x333333));
}
} else {
if(hovered) {
// restore
[Link](0.6);
hovered = null;
}
}
[Link]();
[Link](scene, camera);
}
animate();
</script>
</body>
</html>