feat: 3D hovering transparent VPS server rack schematic (#249)
Some checks failed
CI / validate (pull_request) Failing after 13s
CI / auto-merge (pull_request) Has been skipped

Adds a procedural holographic server rack schematic to the Nexus scene:
- Transparent enclosure box with glowing cyan wireframe edges
- 12 stacked 1U server slab units with slot edge highlights
- Per-slot LED status indicators (green/blue/amber) with async blink phases
- Vertical mounting rails on front face
- Floating label sprite showing "VPS SERVER RACK / 143.198.27.163"
- Interior point glow light
- Animation: gentle hover float, subtle Y-axis sway, pulsing edge opacity,
  blinking LEDs, and breathing interior glow

Fixes #249

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-24 00:45:33 -04:00
parent 39e0eecb9e
commit d22fb1f4f7

151
app.js
View File

@@ -656,6 +656,148 @@ for (let i = 0; i < RUNE_COUNT; i++) {
runeSprites.push({ sprite, baseAngle, floatPhase: (i / RUNE_COUNT) * Math.PI * 2 });
}
// === VPS SERVER RACK SCHEMATIC ===
// Hovering transparent holographic schematic of the physical VPS hosting the Nexus.
// Positioned to the right-front of the platform; gently floats and sways.
const SERVER_RACK_COLOR = 0x00ccff; // cyan-blue schematic tint
const SERVER_RACK_DIM = 0x001a2e; // dark translucent face fill
const serverRackGroup = new THREE.Group();
serverRackGroup.position.set(7.2, 2.2, 3.5);
const RACK_W = 0.6;
const RACK_H = 2.4;
const RACK_D = 0.5;
// Translucent enclosure faces
const enclosureMat = new THREE.MeshBasicMaterial({
color: SERVER_RACK_DIM,
transparent: true,
opacity: 0.1,
side: THREE.DoubleSide,
depthWrite: false,
});
const enclosureGeo = new THREE.BoxGeometry(RACK_W, RACK_H, RACK_D);
serverRackGroup.add(new THREE.Mesh(enclosureGeo, enclosureMat));
// Glowing wireframe outline of enclosure
const enclosureEdgeMat = new THREE.LineBasicMaterial({
color: SERVER_RACK_COLOR,
transparent: true,
opacity: 0.85,
});
serverRackGroup.add(new THREE.LineSegments(new THREE.EdgesGeometry(enclosureGeo), enclosureEdgeMat));
// Server slabs — stacked 1U units inside the rack
const RACK_UNIT_H = 0.12;
const RACK_UNIT_GAP = 0.03;
const RACK_UNIT_W = RACK_W * 0.92;
const RACK_UNIT_D = RACK_D * 0.85;
const RACK_UNIT_STEP = RACK_UNIT_H + RACK_UNIT_GAP;
const RACK_UNIT_COUNT = 12;
const rackUnitGeo = new THREE.BoxGeometry(RACK_UNIT_W, RACK_UNIT_H, RACK_UNIT_D);
const rackUnitEdgeGeo = new THREE.EdgesGeometry(rackUnitGeo);
const rackUnitFaceMat = new THREE.MeshBasicMaterial({
color: 0x000d1a,
transparent: true,
opacity: 0.14,
side: THREE.DoubleSide,
depthWrite: false,
});
// LED status colors per slot
const RACK_LED_COLORS = [
0x00ff88, 0x00ff88, 0x00ff88, 0x4488ff,
0x00ff88, 0x00ff88, 0xffcc00, 0x00ff88,
0x4488ff, 0x00ff88, 0x00ff88, 0x00ff88,
];
/** @type {Array<{mat: THREE.MeshBasicMaterial, phase: number}>} */
const rackLeds = [];
const rackUnitEdgeMats = [];
const rackStartY = -(RACK_H / 2) + RACK_UNIT_H / 2 + 0.06;
for (let i = 0; i < RACK_UNIT_COUNT; i++) {
const y = rackStartY + i * RACK_UNIT_STEP;
// Translucent slab
const slab = new THREE.Mesh(rackUnitGeo, rackUnitFaceMat.clone());
slab.position.y = y;
serverRackGroup.add(slab);
// Glowing slot edges
const edgeMat = new THREE.LineBasicMaterial({
color: SERVER_RACK_COLOR,
transparent: true,
opacity: 0.3,
});
const edgeLines = new THREE.LineSegments(rackUnitEdgeGeo, edgeMat);
edgeLines.position.y = y;
serverRackGroup.add(edgeLines);
rackUnitEdgeMats.push(edgeMat);
// LED indicator on front face
const ledColor = RACK_LED_COLORS[i % RACK_LED_COLORS.length];
const ledGeo = new THREE.SphereGeometry(0.018, 6, 6);
const ledMat = new THREE.MeshBasicMaterial({ color: ledColor, transparent: true, opacity: 0.9 });
const led = new THREE.Mesh(ledGeo, ledMat);
led.position.set(RACK_UNIT_W / 2 - 0.07, y, RACK_UNIT_D / 2 + 0.002);
serverRackGroup.add(led);
rackLeds.push({ mat: ledMat, phase: (i / RACK_UNIT_COUNT) * Math.PI * 2 });
}
// Rack mounting rails — two thin vertical bars on front face
for (const xOffset of [-RACK_W / 2 + 0.04, RACK_W / 2 - 0.04]) {
const railGeo = new THREE.BoxGeometry(0.025, RACK_H * 0.96, 0.025);
const railMat = new THREE.MeshBasicMaterial({
color: SERVER_RACK_COLOR,
transparent: true,
opacity: 0.45,
});
const rail = new THREE.Mesh(railGeo, railMat);
rail.position.set(xOffset, 0, RACK_D / 2);
serverRackGroup.add(rail);
}
// Floating label sprite above the rack
function createRackLabelTexture() {
const W = 256, H = 64;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = '#00ccff';
ctx.textAlign = 'center';
ctx.fillText('VPS SERVER RACK', W / 2, 26);
ctx.font = '10px "Courier New", monospace';
ctx.fillStyle = '#336688';
ctx.fillText('143.198.27.163', W / 2, 46);
return new THREE.CanvasTexture(canvas);
}
const rackLabelMat = new THREE.SpriteMaterial({
map: createRackLabelTexture(),
transparent: true,
opacity: 0.85,
depthWrite: false,
});
const rackLabelSprite = new THREE.Sprite(rackLabelMat);
rackLabelSprite.scale.set(2.4, 0.6, 1);
rackLabelSprite.position.set(0, RACK_H / 2 + 0.45, 0);
serverRackGroup.add(rackLabelSprite);
// Interior glow
const rackGlowLight = new THREE.PointLight(SERVER_RACK_COLOR, 0.35, 3.5);
serverRackGroup.add(rackGlowLight);
scene.add(serverRackGroup);
const SERVER_RACK_BASE_Y = serverRackGroup.position.y;
// === ANIMATION LOOP ===
const clock = new THREE.Clock();
@@ -770,6 +912,15 @@ function animate() {
rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2;
}
// Animate VPS server rack — hover float, subtle sway, LED blink
serverRackGroup.position.y = SERVER_RACK_BASE_Y + Math.sin(elapsed * 0.55 + 1.1) * 0.18;
serverRackGroup.rotation.y = Math.sin(elapsed * 0.13) * 0.07;
enclosureEdgeMat.opacity = 0.65 + Math.sin(elapsed * 1.0) * 0.2;
for (const { mat, phase } of rackLeds) {
mat.opacity = 0.55 + Math.sin(elapsed * 3.0 + phase) * 0.4;
}
rackGlowLight.intensity = 0.25 + Math.sin(elapsed * 1.5) * 0.12;
composer.render();
}