Copy <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BubbleTech 3D Graph Mockup</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/utils/BufferGeometryUtils.js"></script>
<style>
body {
margin: 0;
font-family: "Inter", sans-serif;
background: radial-gradient(circle at top, rgba(108, 43, 217, 0.2), transparent 45%), #050509;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.container {
width: min(1200px, 100%);
background: linear-gradient(160deg, rgba(10, 10, 20, 0.95), rgba(5, 5, 12, 0.92));
border-radius: 28px;
box-shadow: 0 40px 80px rgba(0, 0, 0, 0.5);
padding: 32px;
text-align: center;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 0 0 10px rgba(108, 43, 217, 0.5);
}
p {
color: rgba(255, 255, 255, 0.7);
margin-bottom: 20px;
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.controls button {
padding: 10px 20px;
border: 1px solid rgba(108, 43, 217, 0.5);
background: rgba(108, 43, 217, 0.2);
color: #fff;
border-radius: 999px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.controls button:hover {
transform: translateY(-1px);
box-shadow: 0 0 20px rgba(108, 43, 217, 0.5);
}
#viewport {
width: 100%;
height: 600px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
background: #000;
}
.legend {
display: flex;
gap: 20px;
justify-content: center;
margin-top: 20px;
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
flex-wrap: wrap;
}
.legend span {
display: flex;
align-items: center;
gap: 8px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
@media (max-width: 768px) {
.container {
padding: 24px;
}
#viewport {
height: 400px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>BubbleTech 3D Graph Mockup</h1>
<p>Interactive demonstration of wallet and protocol relationships in a stylized 3D environment.</p>
<div class="controls">
<button id="expand">Expand Jupiter Cluster</button>
<button id="focus">Center on Jupiter</button>
<button id="reset">Reset Layout</button>
</div>
<div id="viewport"></div>
<div class="legend">
<span><span class="dot" style="background: #6c2bd9;"></span>Primary wallet</span>
<span><span class="dot" style="background: #00e5ff;"></span>Wallet node</span>
<span><span class="dot" style="background: #fc71ff;"></span>Jupiter DEX hub</span>
<span><span class="dot" style="background: #ffd166;"></span>Token route</span>
<span>Drag to reposition • Click to select • Edge thickness signals volume</span>
</div>
</div>
<script>
// Initialize Three.js scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(document.getElementById('viewport').offsetWidth, 600);
renderer.setClearColor(0x000000, 1);
document.getElementById('viewport').appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(10, 10, 10);
scene.add(pointLight);
// Camera controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 5;
controls.maxDistance = 50;
camera.position.set(0, 0, 20);
// Node and edge data
const baseNodes = [
{ id: 'primary', label: 'Primary Wallet', type: 'primary', layer: 0, x: 0, y: 0, z: 0 },
{ id: 'wallet-1', label: 'Counterparty Wallet A', type: 'wallet', layer: 1, x: -10, y: 5, z: 0 },
{ id: 'wallet-2', label: 'Counterparty Wallet B', type: 'wallet', layer: 1, x: -8, y: -5, z: 2 },
{ id: 'jupiter', label: 'Jupiter DEX Router', type: 'dex', layer: 1, x: 10, y: 0, z: 0 },
{ id: 'token-1', label: 'USDC Route', type: 'token', layer: 2, x: 12, y: 6, z: 3 },
{ id: 'token-2', label: 'SOL Route', type: 'token', layer: 2, x: 14, y: -4, z: -2 },
{ id: 'token-3', label: 'BONK Route', type: 'token', layer: 2, x: 10, y: 8, z: 5 },
{ id: 'token-4', label: 'Raydium LP', type: 'token', layer: 2, x: 8, y: -6, z: 4 }
];
const baseEdges = [
{ from: 'primary', to: 'wallet-1', weight: 8 },
{ from: 'primary', to: 'wallet-2', weight: 5 },
{ from: 'primary', to: 'jupiter', weight: 9 },
{ from: 'jupiter', to: 'token-1', weight: 7 },
{ from: 'jupiter', to: 'token-2', weight: 6 },
{ from: 'jupiter', to: 'token-3', weight: 4 },
{ from: 'jupiter', to: 'token-4', weight: 3 }
];
let nodes = [];
let edges = [];
let nodeMeshes = [];
let edgeLines = [];
let arrows = [];
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let selectedNode = null;
let dragControls = null;
// Colors
const colors = {
primary: new THREE.Color(0x6c2bd9),
wallet: new THREE.Color(0x00e5ff),
dex: new THREE.Color(0xfc71ff),
token: new THREE.Color(0xffd166),
edge: new THREE.Color(0xffffff),
highlight: new THREE.Color(0xfc71ff)
};
function initGraph() {
nodes = [...baseNodes];
edges = [...baseEdges];
createNodes();
createEdges();
createArrows();
}
function createNodes() {
nodeMeshes.forEach(mesh => scene.remove(mesh));
nodeMeshes = [];
nodes.forEach(node => {
const geometry = new THREE.SphereGeometry(node.type === 'primary' ? 1 : 0.6, 16, 16);
const material = new THREE.MeshPhongMaterial({
color: colors[node.type],
emissive: colors[node.type].clone().multiplyScalar(0.1)
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(node.x, node.y, node.z);
mesh.userData = node;
scene.add(mesh);
nodeMeshes.push(mesh);
});
}
function createEdges() {
edgeLines.forEach(line => scene.remove(line));
edgeLines = [];
edges.forEach(edge => {
const fromNode = nodes.find(n => n.id === edge.from);
const toNode = nodes.find(n => n.id === edge.to);
if (!fromNode || !toNode) return;
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(fromNode.x, fromNode.y, fromNode.z),
new THREE.Vector3(toNode.x, toNode.y, toNode.z)
]);
const material = new THREE.LineBasicMaterial({
color: selectedNode && (selectedNode.id === edge.from || selectedNode.id === edge.to) ? colors.highlight : colors.edge,
linewidth: 2 + edge.weight * 0.5
});
const line = new THREE.Line(geometry, material);
scene.add(line);
edgeLines.push(line);
});
}
function createArrows() {
arrows.forEach(arrow => scene.remove(arrow));
arrows = [];
edges.forEach(edge => {
const fromNode = nodes.find(n => n.id === edge.from);
const toNode = nodes.find(n => n.id === edge.to);
if (!fromNode || !toNode) return;
const direction = new THREE.Vector3(toNode.x - fromNode.x, toNode.y - fromNode.y, toNode.z - fromNode.z).normalize();
const arrowPos = new THREE.Vector3(
fromNode.x + direction.x * (fromNode.type === 'primary' ? 1.5 : 1),
fromNode.y + direction.y * (fromNode.type === 'primary' ? 1.5 : 1),
fromNode.z + direction.z * (fromNode.type === 'primary' ? 1.5 : 1)
);
const geometry = new THREE.ConeGeometry(0.2, 0.5, 8);
const material = new THREE.MeshPhongMaterial({ color: colors.edge });
const arrow = new THREE.Mesh(geometry, material);
arrow.position.copy(arrowPos);
arrow.lookAt(toNode.x, toNode.y, toNode.z);
arrow.rotateX(Math.PI / 2);
scene.add(arrow);
arrows.push(arrow);
});
}
function onMouseMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
selectedNode = intersects[0].object.userData;
console.log('Hovering:', selectedNode.label);
} else {
selectedNode = null;
}
updateHighlights();
}
function onMouseClick(event) {
if (selectedNode) {
console.log('Selected:', selectedNode.label);
// Additional selection logic here
}
}
function updateHighlights() {
nodeMeshes.forEach(mesh => {
if (mesh.userData === selectedNode) {
mesh.material.emissive = colors.highlight.clone().multiplyScalar(0.3);
} else {
mesh.material.emissive = colors[mesh.userData.type].clone().multiplyScalar(0.1);
}
});
createEdges(); // Refresh edges to show highlights
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
// Event listeners
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('click', onMouseClick);
// Controls
document.getElementById('expand').addEventListener('click', () => {
const jupiter = nodes.find(n => n.id === 'jupiter');
if (jupiter) {
for (let i = 0; i < 4; i++) {
const id = `extra-${i}`;
const angle = (Math.PI / 2) + (i * Math.PI / 3);
const distance = 15;
nodes.push({
id,
label: `Extended Route ${i + 1}`,
type: 'token',
layer: 3,
x: jupiter.x + Math.cos(angle) * distance,
y: jupiter.y + Math.sin(angle) * distance,
z: jupiter.z + Math.random() * 5
});
edges.push({ from: 'jupiter', to: id, weight: 2 + Math.random() * 3 });
}
createNodes();
createEdges();
createArrows();
}
});
document.getElementById('focus').addEventListener('click', () => {
const jupiter = nodes.find(n => n.id === 'jupiter');
if (jupiter) {
camera.position.set(jupiter.x + 5, jupiter.y + 5, jupiter.z + 10);
controls.target.set(jupiter.x, jupiter.y, jupiter.z);
controls.update();
}
});
document.getElementById('reset').addEventListener('click', () => {
initGraph();
});
// Resize handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(document.getElementById('viewport').offsetWidth, 600);
});
// Initialize
initGraph();
animate();
</script>
</body>
</html>