blog/static/demos/code-space-browser.html

569 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Space Browser</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #0a0a0f; font-family: 'Courier New', monospace; }
canvas { display: block; }
#info {
position: fixed; top: 16px; left: 16px; color: #8af; font-size: 13px;
background: rgba(0,0,0,0.7); padding: 10px 14px; border-radius: 8px;
border: 1px solid rgba(100,160,255,0.2); z-index: 10; line-height: 1.6;
}
#info kbd { background: rgba(100,160,255,0.15); padding: 2px 6px; border-radius: 3px; font-size: 11px; }
#tooltip {
position: fixed; display: none; color: #cef; font-size: 12px;
background: rgba(5,5,20,0.9); padding: 8px 12px; border-radius: 6px;
border: 1px solid rgba(100,180,255,0.3); pointer-events: none; z-index: 20;
max-width: 300px; white-space: pre-wrap;
}
#vrButton {
position: fixed; bottom: 20px; right: 20px; z-index: 10;
background: linear-gradient(135deg, #2a6aff, #0af); color: #fff;
border: none; padding: 12px 24px; border-radius: 8px; font-size: 14px;
cursor: pointer; font-weight: bold; display: none;
}
#vrButton:hover { filter: brightness(1.2); }
</style>
</head>
<body>
<div id="info">
<strong style="color:#5bf">⬡ Code Space Browser</strong><br>
<kbd>Scroll</kbd> zoom · <kbd>Drag</kbd> rotate · <kbd>Right-drag</kbd> pan<br>
<kbd>Hover</kbd> file info · <kbd>Click</kbd> inspect
</div>
<div id="tooltip"></div>
<button id="vrButton">Enter VR</button>
<script type="importmap">
{
"imports": {
"three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ── Sample Codebase ──
const codebase = {
name: "my-project", children: [
{ name: "src", children: [
{ name: "main.ts", lines: 245 },
{ name: "app.ts", lines: 380 },
{ name: "config.ts", lines: 65 },
{ name: "components", children: [
{ name: "Header.tsx", lines: 120 },
{ name: "Sidebar.tsx", lines: 210 },
{ name: "Editor.tsx", lines: 580 },
{ name: "FileTree.tsx", lines: 175 },
{ name: "StatusBar.tsx", lines: 88 },
{ name: "Modal.tsx", lines: 145 },
]},
{ name: "utils", children: [
{ name: "parser.ts", lines: 420 },
{ name: "formatter.ts", lines: 190 },
{ name: "helpers.ts", lines: 135 },
{ name: "logger.ts", lines: 78 },
]},
{ name: "services", children: [
{ name: "api.ts", lines: 310 },
{ name: "auth.ts", lines: 225 },
{ name: "storage.ts", lines: 160 },
{ name: "websocket.ts", lines: 290 },
]},
{ name: "types", children: [
{ name: "index.d.ts", lines: 340 },
{ name: "api.d.ts", lines: 180 },
{ name: "components.d.ts", lines: 95 },
]},
]},
{ name: "tests", children: [
{ name: "unit", children: [
{ name: "parser.test.ts", lines: 520 },
{ name: "formatter.test.ts", lines: 280 },
{ name: "auth.test.ts", lines: 190 },
]},
{ name: "e2e", children: [
{ name: "app.e2e.ts", lines: 380 },
{ name: "editor.e2e.ts", lines: 450 },
]},
]},
{ name: "scripts", children: [
{ name: "build.sh", lines: 45 },
{ name: "deploy.sh", lines: 78 },
{ name: "seed.ts", lines: 120 },
]},
{ name: "package.json", lines: 42 },
{ name: "tsconfig.json", lines: 28 },
{ name: "README.md", lines: 156 },
{ name: "Dockerfile", lines: 35 },
]
};
// ── Fake code generator ──
const keywords = ['const','let','function','return','if','else','for','import','export','async','await','class','interface','type','from','new','this','try','catch','throw'];
const types = ['string','number','boolean','void','Promise','Array','Map','Record','any'];
const vars = ['data','result','config','items','ctx','req','res','err','buf','node','key','val','opts','state','props','ref','cb','fn','idx','len'];
function generateFakeCode(lines) {
const out = [];
let indent = 0;
for (let i = 0; i < lines; i++) {
const r = Math.random();
if (r < 0.05 && indent > 0) { indent--; out.push(' '.repeat(indent) + '}'); continue; }
if (r < 0.1) { out.push(''); continue; }
if (r < 0.15) { out.push(' '.repeat(indent) + '// ' + vars[Math.random()*vars.length|0] + ' processing'); continue; }
const pre = ' '.repeat(indent);
if (r < 0.25) {
const kw = Math.random() < 0.5 ? 'function' : 'const';
const v = vars[Math.random()*vars.length|0];
const t = types[Math.random()*types.length|0];
out.push(pre + `${kw} ${v}: ${t} = {`);
indent++; continue;
}
if (r < 0.35) {
out.push(pre + `if (${vars[Math.random()*vars.length|0]}) {`);
indent++; continue;
}
const v1 = vars[Math.random()*vars.length|0];
const v2 = vars[Math.random()*vars.length|0];
out.push(pre + `${keywords[Math.random()*keywords.length|0]} ${v1} = ${v2};`);
}
while (indent > 0) { indent--; out.push(' '.repeat(indent) + '}'); }
return out;
}
// ── Squarified Treemap ──
function squarify(items, rect) {
if (!items.length) return [];
const sorted = [...items].sort((a, b) => b.size - a.size);
const totalSize = sorted.reduce((s, i) => s + i.size, 0);
const results = [];
layoutStrip(sorted, rect, totalSize, results);
return results;
}
function layoutStrip(items, rect, totalArea, results) {
if (items.length === 0) return;
if (items.length === 1) {
results.push({ ...items[0], rect: { ...rect } });
return;
}
const { x, y, w, h } = rect;
const vertical = h <= w;
const side = vertical ? h : w;
let strip = [items[0]];
let stripArea = items[0].size;
let bestRatio = worstRatio(strip, stripArea, side, totalArea);
let i = 1;
for (; i < items.length; i++) {
const newStrip = [...strip, items[i]];
const newStripArea = stripArea + items[i].size;
const newRatio = worstRatio(newStrip, newStripArea, side, totalArea);
if (newRatio > bestRatio) break;
strip = newStrip;
stripArea = newStripArea;
bestRatio = newRatio;
}
const stripFraction = stripArea / totalArea;
let offset = 0;
for (const item of strip) {
const frac = item.size / stripArea;
let r;
if (vertical) {
const sw = w * stripFraction;
r = { x: x, y: y + offset * h, w: sw, h: frac * h };
offset += frac;
} else {
const sh = h * stripFraction;
r = { x: x + offset * w, y: y, w: frac * w, h: sh };
offset += frac;
}
results.push({ ...item, rect: r });
}
const remaining = items.slice(i);
if (remaining.length > 0) {
const remArea = totalArea - stripArea;
let newRect;
if (vertical) {
newRect = { x: x + w * stripFraction, y, w: w * (1 - stripFraction), h };
} else {
newRect = { x, y: y + h * stripFraction, w, h: h * (1 - stripFraction) };
}
layoutStrip(remaining, newRect, remArea, results);
}
}
function worstRatio(strip, stripArea, side, totalArea) {
const stripLen = (stripArea / totalArea) * side;
if (stripLen === 0) return Infinity;
let worst = 0;
for (const item of strip) {
const itemLen = (item.size / stripArea) * side; // incorrect, should use other dim
const area = (item.size / totalArea) * side * side; // approx
// simplified: use fraction-based
const frac = item.size / stripArea;
const w = stripLen;
const h = frac * (side);
const ratio = Math.max(w / h, h / w);
worst = Math.max(worst, ratio);
}
return worst;
}
// ── Color palette ──
const dirColors = [0x1a3a5c, 0x1a4c3c, 0x3c1a4c, 0x4c3c1a, 0x1a2c4c, 0x2c1a1a, 0x1a4c4c, 0x3c2a1a];
const fileColors = {
ts: 0x2255aa, tsx: 0x2266bb, js: 0xccaa22, json: 0x44aa44,
md: 0x8866cc, sh: 0x55aa77, d: 0x336699, Dockerfile: 0x2299cc
};
function getFileColor(name) {
if (name.includes('.d.ts') || name.includes('.d.')) return fileColors.d;
const ext = name.split('.').pop();
return fileColors[ext] || 0x3377aa;
}
// ── Scene setup ──
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x06060f);
scene.fog = new THREE.FogExp2(0x06060f, 0.015);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 18, 25);
camera.lookAt(0, 3, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
// Lights
scene.add(new THREE.AmbientLight(0x334466, 0.6));
const dirLight = new THREE.DirectionalLight(0x88aaff, 0.8);
dirLight.position.set(10, 20, 15);
scene.add(dirLight);
const pointLight = new THREE.PointLight(0x4488ff, 0.5, 50);
pointLight.position.set(0, 10, 0);
scene.add(pointLight);
// Grid helper
const gridHelper = new THREE.GridHelper(40, 40, 0x111133, 0x0a0a22);
gridHelper.position.y = -0.05;
scene.add(gridHelper);
// ── Build treemap layers ──
const LAYER_HEIGHT = 4.0;
const LAYER_SIZE = 16;
const PADDING = 0.15;
const interactables = [];
const allMeshes = [];
function getNodeSize(node) {
if (node.lines) return Math.log2(node.lines + 1);
if (node.children) return node.children.reduce((s, c) => s + getNodeSize(c), 0);
return 1;
}
function buildLayer(node, depth, offsetX, offsetZ, parentW, parentH) {
if (!node.children) return;
const items = node.children.map(c => ({
...c,
size: getNodeSize(c),
}));
const rect = { x: 0, y: 0, w: parentW, h: parentH };
const layout = squarify(items, rect);
const y = depth * LAYER_HEIGHT;
// Platform
const platGeo = new THREE.BoxGeometry(parentW + 0.3, 0.08, parentH + 0.3);
const platMat = new THREE.MeshStandardMaterial({
color: dirColors[depth % dirColors.length],
transparent: true, opacity: 0.35, roughness: 0.8
});
const platform = new THREE.Mesh(platGeo, platMat);
platform.position.set(offsetX + parentW / 2, y - 0.04, offsetZ + parentH / 2);
scene.add(platform);
allMeshes.push(platform);
// Label for this directory
const labelCanvas = document.createElement('canvas');
labelCanvas.width = 512; labelCanvas.height = 64;
const lctx = labelCanvas.getContext('2d');
lctx.fillStyle = '#88bbff';
lctx.font = 'bold 28px Courier New';
lctx.fillText('📁 ' + node.name + '/', 10, 40);
const labelTex = new THREE.CanvasTexture(labelCanvas);
const labelGeo = new THREE.PlaneGeometry(parentW * 0.6, parentW * 0.6 * 64 / 512);
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, depthWrite: false });
const labelMesh = new THREE.Mesh(labelGeo, labelMat);
labelMesh.position.set(offsetX + parentW / 2, y + 0.1, offsetZ - 0.5);
labelMesh.rotation.x = -Math.PI / 2;
scene.add(labelMesh);
allMeshes.push(labelMesh);
for (const item of layout) {
const r = item.rect;
const px = offsetX + r.x + PADDING / 2;
const pz = offsetZ + r.y + PADDING / 2;
const pw = r.w - PADDING;
const ph = r.h - PADDING;
if (pw < 0.1 || ph < 0.1) continue;
if (item.children) {
// Directory → recurse
buildLayer(item, depth + 1, px, pz, pw, ph);
// Small marker on current level
const mGeo = new THREE.BoxGeometry(pw, 0.15, ph);
const mMat = new THREE.MeshStandardMaterial({
color: dirColors[(depth + 1) % dirColors.length],
transparent: true, opacity: 0.5, roughness: 0.6
});
const marker = new THREE.Mesh(mGeo, mMat);
marker.position.set(px + pw / 2, y + 0.08, pz + ph / 2);
scene.add(marker);
allMeshes.push(marker);
// Directory label on marker
const dc = document.createElement('canvas');
dc.width = 256; dc.height = 48;
const dctx = dc.getContext('2d');
dctx.fillStyle = '#66aadd';
dctx.font = '20px Courier New';
dctx.fillText('📂 ' + item.name, 5, 30);
const dtex = new THREE.CanvasTexture(dc);
const dgeo = new THREE.PlaneGeometry(pw * 0.8, pw * 0.8 * 48 / 256);
const dmat = new THREE.MeshBasicMaterial({ map: dtex, transparent: true });
const dlbl = new THREE.Mesh(dgeo, dmat);
dlbl.position.set(px + pw / 2, y + 0.2, pz + ph / 2);
dlbl.rotation.x = -Math.PI / 2;
scene.add(dlbl);
allMeshes.push(dlbl);
} else {
// File → render code on surface
const height = 0.12 + Math.log2(item.lines + 1) * 0.04;
const geo = new THREE.BoxGeometry(pw, height, ph);
const color = getFileColor(item.name);
// Code texture
const canvas = document.createElement('canvas');
const res = 512;
canvas.width = res; canvas.height = res;
const ctx = canvas.getContext('2d');
// Background
ctx.fillStyle = '#0c0c18';
ctx.fillRect(0, 0, res, res);
// File name header
ctx.fillStyle = '#5599dd';
ctx.font = 'bold 14px Courier New';
ctx.fillText(item.name, 8, 16);
ctx.fillStyle = '#334455';
ctx.fillRect(0, 20, res, 1);
// Fake code lines
const codeLines = generateFakeCode(Math.min(item.lines, 40));
const fontSize = 9;
ctx.font = `${fontSize}px Courier New`;
for (let li = 0; li < codeLines.length && li < 50; li++) {
const line = codeLines[li];
const yy = 30 + li * (fontSize + 2);
if (yy > res - 5) break;
// Line number
ctx.fillStyle = '#333355';
ctx.fillText(String(li + 1).padStart(3), 4, yy);
// Syntax coloring
const tokens = line.split(/(\s+)/);
let xOff = 30;
for (const tok of tokens) {
if (keywords.includes(tok.trim())) ctx.fillStyle = '#cc77dd';
else if (types.includes(tok.trim())) ctx.fillStyle = '#44bbaa';
else if (tok.startsWith('//')) ctx.fillStyle = '#446644';
else if (tok === '{' || tok === '}') ctx.fillStyle = '#ccaa44';
else ctx.fillStyle = '#8899aa';
ctx.fillText(tok, xOff, yy);
xOff += ctx.measureText(tok).width;
}
}
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
// Top face gets code texture, sides get solid color
const materials = [
new THREE.MeshStandardMaterial({ color, roughness: 0.7 }),
new THREE.MeshStandardMaterial({ color, roughness: 0.7 }),
new THREE.MeshStandardMaterial({ map: texture, roughness: 0.5 }),
new THREE.MeshStandardMaterial({ color: 0x050510, roughness: 0.9 }),
new THREE.MeshStandardMaterial({ color, roughness: 0.7 }),
new THREE.MeshStandardMaterial({ color, roughness: 0.7 }),
];
const mesh = new THREE.Mesh(geo, materials);
mesh.position.set(px + pw / 2, y + height / 2, pz + ph / 2);
scene.add(mesh);
allMeshes.push(mesh);
// Edge glow
const edgeGeo = new THREE.EdgesGeometry(geo);
const edgeMat = new THREE.LineBasicMaterial({ color: 0x3366aa, transparent: true, opacity: 0.3 });
const edges = new THREE.LineSegments(edgeGeo, edgeMat);
mesh.add(edges);
mesh.userData = {
type: 'file',
name: item.name,
lines: item.lines,
size: item.size,
};
interactables.push(mesh);
// Vertical connection line to platform
if (depth > 0) {
const lineGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(px + pw / 2, y, pz + ph / 2),
new THREE.Vector3(px + pw / 2, y - LAYER_HEIGHT + 0.2, pz + ph / 2),
]);
const lineMat = new THREE.LineBasicMaterial({ color: 0x223355, transparent: true, opacity: 0.2 });
scene.add(new THREE.LineSegments(lineGeo, lineMat));
}
}
}
}
buildLayer(codebase, 0, -LAYER_SIZE / 2, -LAYER_SIZE / 2, LAYER_SIZE, LAYER_SIZE);
// ── Orbit Controls (manual) ──
let isDragging = false, isRightDrag = false;
let prevX = 0, prevY = 0;
let theta = -0.3, phi = 0.8, radius = 30;
let targetX = 0, targetY = 5, targetZ = 0;
function updateCamera() {
camera.position.x = targetX + radius * Math.sin(phi) * Math.sin(theta);
camera.position.y = targetY + radius * Math.cos(phi);
camera.position.z = targetZ + radius * Math.sin(phi) * Math.cos(theta);
camera.lookAt(targetX, targetY, targetZ);
}
updateCamera();
renderer.domElement.addEventListener('mousedown', e => {
if (e.button === 2) isRightDrag = true;
else isDragging = true;
prevX = e.clientX; prevY = e.clientY;
});
renderer.domElement.addEventListener('mousemove', e => {
if (isDragging) {
theta -= (e.clientX - prevX) * 0.005;
phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi - (e.clientY - prevY) * 0.005));
prevX = e.clientX; prevY = e.clientY;
updateCamera();
}
if (isRightDrag) {
const dx = (e.clientX - prevX) * 0.03;
const dy = (e.clientY - prevY) * 0.03;
targetX -= dx * Math.cos(theta);
targetZ += dx * Math.sin(theta);
targetY += dy;
prevX = e.clientX; prevY = e.clientY;
updateCamera();
}
});
window.addEventListener('mouseup', () => { isDragging = false; isRightDrag = false; });
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
renderer.domElement.addEventListener('wheel', e => {
radius = Math.max(5, Math.min(80, radius + e.deltaY * 0.02));
updateCamera();
});
// ── Hover / Click ──
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const tooltip = document.getElementById('tooltip');
let hoveredMesh = null;
renderer.domElement.addEventListener('mousemove', e => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(interactables);
if (hoveredMesh && hoveredMesh !== (hits[0]?.object)) {
// un-hover
if (hoveredMesh.userData.originalY !== undefined) {
hoveredMesh.position.y = hoveredMesh.userData.originalY;
}
hoveredMesh = null;
tooltip.style.display = 'none';
}
if (hits.length > 0) {
const obj = hits[0].object;
if (obj.userData.type === 'file') {
hoveredMesh = obj;
if (obj.userData.originalY === undefined) obj.userData.originalY = obj.position.y;
obj.position.y = obj.userData.originalY + 0.1;
tooltip.style.display = 'block';
tooltip.style.left = (e.clientX + 15) + 'px';
tooltip.style.top = (e.clientY - 10) + 'px';
tooltip.textContent = `📄 ${obj.userData.name}\n ${obj.userData.lines} lines\n size: ${obj.userData.size.toFixed(1)}`;
}
}
});
// ── WebXR ──
if (navigator.xr) {
navigator.xr.isSessionSupported('immersive-vr').then(ok => {
if (ok) {
const btn = document.getElementById('vrButton');
btn.style.display = 'block';
btn.addEventListener('click', () => {
navigator.xr.requestSession('immersive-vr', { optionalFeatures: ['local-floor'] }).then(session => {
renderer.xr.setSession(session);
btn.style.display = 'none';
session.addEventListener('end', () => { btn.style.display = 'block'; });
});
});
}
});
}
// ── Animate ──
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
const t = clock.getElapsedTime();
pointLight.position.x = Math.sin(t * 0.3) * 8;
pointLight.position.z = Math.cos(t * 0.3) * 8;
renderer.render(scene, camera);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>