aether-garden / assets /bonds_graph.html
kavyabhand's picture
Deploy Aether Garden application
7f93bfb verified
Raw
History Blame Contribute Delete
8.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bonds of the Realm</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%; overflow: hidden;
background: radial-gradient(ellipse at 50% 38%, #1a1410 0%, #0a0806 72%);
font-family: Georgia, 'Times New Roman', serif;
}
#canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; }
#canvas:active { cursor: grabbing; }
#loading {
position: fixed; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; color: #e8c873; z-index: 10;
background: #0a0806; transition: opacity 0.6s;
}
#loading.hidden { opacity: 0; pointer-events: none; }
.ring {
width: 42px; height: 42px; border: 2px solid rgba(232,200,115,0.2);
border-top-color: #e8c873; border-radius: 50%;
animation: spin 1s linear infinite; margin-bottom: 0.8rem;
}
@keyframes spin { to { transform: rotate(360deg); } }
#hint {
position: fixed; bottom: 0.65rem; left: 50%; transform: translateX(-50%);
color: rgba(232, 216, 176, 0.55); font-size: 0.72rem; letter-spacing: 0.08em;
pointer-events: none; z-index: 5;
}
#vignette {
position: fixed; inset: 0; pointer-events: none; z-index: 4;
background: radial-gradient(ellipse at 50% 44%, transparent 42%, rgba(4,3,2,0.55) 100%);
}
</style>
</head>
<body>
<div id="loading"><div class="ring"></div><p>Weaving the web of bonds…</p></div>
<canvas id="canvas"></canvas>
<div id="vignette"></div>
<div id="hint">Drag to orbit · scroll to zoom</div>
<script type="importmap">
{ "imports": { "three": "./vendor/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from './vendor/OrbitControls.js';
const canvas = document.getElementById('canvas');
const loading = document.getElementById('loading');
function decodeScene() {
const raw = location.hash.slice(1);
if (!raw.startsWith('data=')) return null;
try {
return JSON.parse(atob(decodeURIComponent(raw.slice(5))));
} catch (_) {
return null;
}
}
const sceneData = decodeScene() || { nodes: [], edges: [] };
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0806, 0.028);
const camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(0, 8, 28);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.35;
controls.minDistance = 10;
controls.maxDistance = 55;
controls.target.set(0, 0, 0);
scene.add(new THREE.AmbientLight(0xc8a86a, 0.45));
const key = new THREE.DirectionalLight(0xfff0d0, 0.9);
key.position.set(12, 18, 10);
scene.add(key);
const rim = new THREE.DirectionalLight(0x9070b8, 0.35);
rim.position.set(-10, 4, -12);
scene.add(rim);
const nodeGroup = new THREE.Group();
const edgeGroup = new THREE.Group();
const labelSprites = [];
scene.add(edgeGroup);
scene.add(nodeGroup);
const nodeMeshes = new Map();
function hexToColor(hex) {
return new THREE.Color(hex || '#d4a84b');
}
function makeLabel(text) {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
const fontSize = 22;
ctx.font = `600 ${fontSize}px Georgia, serif`;
const w = Math.min(280, ctx.measureText(text).width + 24);
c.width = w;
c.height = fontSize + 14;
ctx.font = `600 ${fontSize}px Georgia, serif`;
ctx.fillStyle = 'rgba(10,8,6,0.55)';
ctx.fillRect(0, 0, c.width, c.height);
ctx.fillStyle = '#f8efd9';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, c.width / 2, c.height / 2);
const tex = new THREE.CanvasTexture(c);
tex.needsUpdate = true;
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
const sprite = new THREE.Sprite(mat);
const scale = 0.018 * c.width;
sprite.scale.set(scale, scale * (c.height / c.width), 1);
return sprite;
}
for (const node of sceneData.nodes) {
const color = hexToColor(node.color);
const radius = 0.55 + (node.degree || 1) * 0.08;
const geo = new THREE.SphereGeometry(radius, 24, 24);
const mat = new THREE.MeshStandardMaterial({
color,
emissive: color,
emissiveIntensity: 0.35,
metalness: 0.25,
roughness: 0.42,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(node.x || 0, node.y || 0, node.z || 0);
nodeGroup.add(mesh);
nodeMeshes.set(node.id, mesh);
const glow = new THREE.Mesh(
new THREE.SphereGeometry(radius * 1.55, 16, 16),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.12 })
);
glow.position.copy(mesh.position);
nodeGroup.add(glow);
if (node.showLabel) {
const label = makeLabel(node.name || '');
label.position.copy(mesh.position);
label.position.y += radius + 0.9;
nodeGroup.add(label);
labelSprites.push(label);
}
}
for (const edge of sceneData.edges) {
const a = nodeMeshes.get(edge.a);
const b = nodeMeshes.get(edge.b);
if (!a || !b) continue;
const color = hexToColor(edge.color);
const strength = edge.strength || 1;
const curve = new THREE.QuadraticBezierCurve3(
a.position.clone(),
new THREE.Vector3(
(a.position.x + b.position.x) / 2 + (Math.random() - 0.5) * 2,
(a.position.y + b.position.y) / 2 + 1.2 + strength * 0.15,
(a.position.z + b.position.z) / 2 + (Math.random() - 0.5) * 2
),
b.position.clone()
);
const points = curve.getPoints(24);
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({
color,
transparent: true,
opacity: 0.35 + strength * 0.12,
linewidth: 1,
});
edgeGroup.add(new THREE.Line(geo, mat));
}
// Floating dust motes
const dustGeo = new THREE.BufferGeometry();
const dustCount = 120;
const dustPos = new Float32Array(dustCount * 3);
for (let i = 0; i < dustCount; i++) {
dustPos[i * 3] = (Math.random() - 0.5) * 40;
dustPos[i * 3 + 1] = (Math.random() - 0.5) * 24;
dustPos[i * 3 + 2] = (Math.random() - 0.5) * 40;
}
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3));
const dust = new THREE.Points(
dustGeo,
new THREE.PointsMaterial({ color: 0xe8c873, size: 0.08, transparent: true, opacity: 0.35 })
);
scene.add(dust);
function resize() {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
}
window.addEventListener('resize', resize);
let t = 0;
function animate() {
requestAnimationFrame(animate);
t += 0.01;
nodeGroup.children.forEach((child, i) => {
if (child.isMesh && child.geometry?.type === 'SphereGeometry') {
child.position.y += Math.sin(t + i) * 0.0008;
}
});
dust.rotation.y += 0.0006;
controls.update();
renderer.render(scene, camera);
}
setTimeout(() => {
loading.classList.add('hidden');
animate();
}, 350);
</script>
</body>
</html>