From f400f63321a15cb7b4eac6a3240973133988c852 Mon Sep 17 00:00:00 2001
From: Quinta0 <0pietroquintavalle0@gmail.com>
Date: Tue, 15 Sep 2026 17:58:27 +0200
Subject: [PATCH] 3js
---
package-lock.json | 67 ++++++++++++++-
package.json | 4 +-
src/App.tsx | 115 +++----------------------
src/ThreeViewport.tsx | 68 +++++++++++++++
src/index.css | 15 ++++
src/scenes.ts | 195 ++++++++++++++++++++++++++++++++++++++++++
6 files changed, 360 insertions(+), 104 deletions(-)
create mode 100644 src/ThreeViewport.tsx
create mode 100644 src/scenes.ts
diff --git a/package-lock.json b/package-lock.json
index 157f878..8306f32 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,16 +9,25 @@
"version": "0.1.0",
"dependencies": {
"react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react-dom": "^18.3.1",
+ "three": "^0.186.0"
},
"devDependencies": {
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
+ "@types/three": "^0.186.0",
"@vitejs/plugin-react": "^6.0.5",
"typescript": "^5.6.2",
"vite": "^8.2.1"
}
},
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/@oxc-project/types": {
"version": "0.143.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz",
@@ -292,6 +301,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -320,6 +336,35 @@
"@types/react": "^18.0.0"
}
},
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.186.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.186.0.tgz",
+ "integrity": "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.3",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@vitejs/plugin-react": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
@@ -381,6 +426,13 @@
}
}
},
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -687,6 +739,13 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/meshoptimizer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
@@ -832,6 +891,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/three": {
+ "version": "0.186.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.186.0.tgz",
+ "integrity": "sha512-cr/fIM2ddMSVbYVgkfD4jLJv7Fh/8ZTjvo+7gQeSVGUZHxpx9FDwoL5iC7hUz/LiRA8wMbqfnb90xKfm1/HHkQ==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
diff --git a/package.json b/package.json
index ef80880..a1055a1 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,13 @@
},
"dependencies": {
"react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react-dom": "^18.3.1",
+ "three": "^0.186.0"
},
"devDependencies": {
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
+ "@types/three": "^0.186.0",
"@vitejs/plugin-react": "^6.0.5",
"typescript": "^5.6.2",
"vite": "^8.2.1"
diff --git a/src/App.tsx b/src/App.tsx
index 94b438f..535ef29 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,6 @@
import type { ReactNode } from 'react'
+import ThreeViewport from './ThreeViewport'
+import { buildRackScene, buildClusterScene, buildNasScene } from './scenes'
const EMAIL = '0pietroquintavalle0@gmail.com'
const GITHUB = 'https://github.com/Quinta0'
@@ -15,6 +17,7 @@ type CaseStudy = {
footerL: string
footerR: string
diagram: ReactNode
+ interactive?: boolean
}
const cases: CaseStudy[] = [
@@ -25,50 +28,11 @@ const cases: CaseStudy[] = [
tags: ['19" EIA-310 Cabinet', 'ZFS RAID 10', 'Structured Cabling'],
tint: '#EFECE6',
headerL: 'RACK_CHASSIS // EIA-310',
- headerR: 'ZFS RAID 10 POOL',
+ headerR: '• DRAG TO ROTATE',
footerL: 'STRUCTURED HARNESS',
footerR: 'SUB-30M RTO SYNC',
- diagram: (
-
- ),
+ diagram: ,
+ interactive: true,
},
{
label: '02 / Startup Infrastructure • DEC Energy',
@@ -77,34 +41,11 @@ const cases: CaseStudy[] = [
tags: ['Bare-Metal K8s', 'LLM Inference', 'Thermal Profiling'],
tint: '#EAE8E1',
headerL: 'DUAL_NODE // TOPOLOGY',
- headerR: 'BARE-METAL FABRIC',
+ headerR: '• DRAG TO ROTATE',
footerL: 'ON-PREMISES INGESTION',
footerR: 'USI STARTUP',
- diagram: (
-
- ),
+ diagram: ,
+ interactive: true,
},
{
label: '03 / Protocol Reverse Engineering',
@@ -151,41 +92,11 @@ const cases: CaseStudy[] = [
tags: ['Zero-Trust WAN', 'Ansible IaC', 'Proxmox / UNRAID'],
tint: '#EDEAE3',
headerL: 'TOPOLOGY MAP',
- headerR: 'ZERO EXPOSED WAN',
+ headerR: '• HOVER BAYS',
footerL: 'ANSIBLE PROVISIONED',
footerR: 'DAILY PRODUCTION',
- diagram: (
-
- ),
+ diagram: ,
+ interactive: true,
},
]
@@ -350,7 +261,7 @@ export default function App() {
{c.headerL}
- {c.headerR}
+ {c.headerR}
{c.diagram}
diff --git a/src/ThreeViewport.tsx b/src/ThreeViewport.tsx
new file mode 100644
index 0000000..261f80b
--- /dev/null
+++ b/src/ThreeViewport.tsx
@@ -0,0 +1,68 @@
+import { useEffect, useRef } from 'react'
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+
+export type ViewportHandle = {
+ update?: () => void
+ dispose?: () => void
+}
+
+export type BuildFn = (
+ scene: THREE.Scene,
+ camera: THREE.PerspectiveCamera,
+ controls: OrbitControls,
+ renderer: THREE.WebGLRenderer,
+ container: HTMLDivElement,
+) => ViewportHandle | void
+
+export default function ThreeViewport({ build, className }: { build: BuildFn; className?: string }) {
+ const containerRef = useRef
(null)
+
+ useEffect(() => {
+ const container = containerRef.current
+ if (!container) return
+
+ const scene = new THREE.Scene()
+ const camera = new THREE.PerspectiveCamera(38, container.clientWidth / container.clientHeight, 0.1, 100)
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
+ renderer.setSize(container.clientWidth, container.clientHeight)
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
+ container.appendChild(renderer.domElement)
+
+ const controls = new OrbitControls(camera, renderer.domElement)
+ controls.enableDamping = true
+ controls.dampingFactor = 0.05
+ controls.enableZoom = false
+ controls.autoRotate = true
+ controls.autoRotateSpeed = 1
+
+ const handle = build(scene, camera, controls, renderer, container) ?? undefined
+
+ let frameId = 0
+ const animate = () => {
+ frameId = requestAnimationFrame(animate)
+ controls.update()
+ handle?.update?.()
+ renderer.render(scene, camera)
+ }
+ animate()
+
+ const onResize = () => {
+ camera.aspect = container.clientWidth / container.clientHeight
+ camera.updateProjectionMatrix()
+ renderer.setSize(container.clientWidth, container.clientHeight)
+ }
+ window.addEventListener('resize', onResize)
+
+ return () => {
+ cancelAnimationFrame(frameId)
+ window.removeEventListener('resize', onResize)
+ handle?.dispose?.()
+ controls.dispose()
+ renderer.dispose()
+ container.removeChild(renderer.domElement)
+ }
+ }, [build])
+
+ return
+}
diff --git a/src/index.css b/src/index.css
index cd77304..97e189a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -109,6 +109,20 @@ body{
.diagram-footer{padding-top:12px;border-top:1px solid rgba(230,228,221,.4);font-size:10px}
.diagram-svg-wrap{width:100%;overflow-x:auto;padding:8px 0}
.case-svg{width:100%;min-width:320px;height:auto}
+.diagram-interactive{color:var(--accent);font-weight:600}
+
+.case-canvas{
+ position:relative;width:100%;height:288px;border-radius:6px;overflow:hidden;
+ cursor:grab;touch-action:pan-y;
+}
+.case-canvas:active{cursor:grabbing}
+.case-canvas canvas{display:block;touch-action:pan-y}
+.nas-hud{
+ position:absolute;z-index:5;pointer-events:none;
+ background:var(--ink);color:var(--canvas);
+ font-family:var(--f-mono);font-size:10px;
+ padding:4px 8px;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.2);
+}
.led-pulse{animation:ledBlink 2.4s ease-in-out infinite}
@keyframes ledBlink{0%,100%{opacity:.3}50%{opacity:1}}
@@ -197,5 +211,6 @@ body{
.case-desc{font-size:12px}
.case-diagram{padding:16px;border-radius:12px}
.diagram-card{padding:16px}
+ .case-canvas{height:256px}
.site-footer{padding:48px var(--pad);flex-direction:column;align-items:flex-start}
}
diff --git a/src/scenes.ts b/src/scenes.ts
new file mode 100644
index 0000000..d5e8883
--- /dev/null
+++ b/src/scenes.ts
@@ -0,0 +1,195 @@
+import * as THREE from 'three'
+import type { BuildFn } from './ThreeViewport'
+
+const wireMat = new THREE.LineBasicMaterial({ color: 0x121212 })
+const chassisMat = new THREE.MeshBasicMaterial({ color: 0xf7f6f2, transparent: true, opacity: 0.75 })
+const darkPartMat = new THREE.MeshBasicMaterial({ color: 0x1c1b19 })
+const swissRedMat = new THREE.MeshBasicMaterial({ color: 0xc1121f })
+const copperMat = new THREE.MeshBasicMaterial({ color: 0xb9814f })
+const tealDriveMat = new THREE.MeshBasicMaterial({ color: 0x1f8f86, transparent: true, opacity: 0.75 })
+
+export const buildRackScene: BuildFn = (scene, camera) => {
+ camera.position.set(4, 2.5, 4.5)
+ const rack = new THREE.Group()
+ rack.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.6, 2.8, 1.4)), wireMat))
+
+ for (let i = 0; i < 4; i++) {
+ const server = new THREE.Mesh(new THREE.BoxGeometry(1.45, 0.38, 1.25), chassisMat)
+ server.position.set(0, -0.9 + i * 0.55, 0)
+ server.add(new THREE.LineSegments(new THREE.EdgesGeometry(server.geometry), wireMat))
+
+ for (let d = 0; d < 4; d++) {
+ const drive = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.22, 0.05), darkPartMat)
+ drive.position.set(-0.45 + d * 0.3, 0, 0.63)
+ server.add(drive)
+ }
+
+ const led = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.04, 0.04), swissRedMat)
+ led.position.set(0.6, 0.1, 0.64)
+ server.add(led)
+ rack.add(server)
+ }
+
+ scene.add(rack)
+}
+
+export const buildClusterScene: BuildFn = (scene, camera) => {
+ camera.position.set(3.8, 3.5, 3.8)
+ const cluster = new THREE.Group()
+
+ const makeBoard = (x: number) => {
+ const board = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.08, 1.8), chassisMat)
+ board.position.set(x, 0, 0)
+ board.add(new THREE.LineSegments(new THREE.EdgesGeometry(board.geometry), wireMat))
+ const cpu = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.12, 0.45), darkPartMat)
+ cpu.position.set(0, 0.06, -0.2)
+ board.add(cpu)
+ return board
+ }
+
+ const b1 = makeBoard(-0.9)
+ const gpu = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.4, 1.2), darkPartMat)
+ gpu.position.set(0.5, 0.2, 0.1)
+ b1.add(gpu)
+ cluster.add(b1)
+
+ const b2 = makeBoard(0.9)
+ const hba = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.35, 1), darkPartMat)
+ hba.position.set(-0.5, 0.18, 0.1)
+ b2.add(hba)
+ cluster.add(b2)
+
+ const bridge = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.04, 0.06), swissRedMat)
+ bridge.position.set(0, 0.08, -0.2)
+ cluster.add(bridge)
+
+ scene.add(cluster)
+}
+
+function makeGrilleTexture() {
+ const c = document.createElement('canvas')
+ c.width = c.height = 128
+ const ctx = c.getContext('2d')!
+ ctx.fillStyle = '#1a1a1a'
+ ctx.fillRect(0, 0, 128, 128)
+ ctx.fillStyle = '#3a3a3a'
+ for (let x = 5; x < 128; x += 10) {
+ for (let y = 5; y < 128; y += 10) ctx.fillRect(x - 1.5, y - 1.5, 3, 3)
+ }
+ const tex = new THREE.CanvasTexture(c)
+ tex.wrapS = tex.wrapT = THREE.RepeatWrapping
+ tex.repeat.set(2, 2)
+ return tex
+}
+
+export const buildNasScene: BuildFn = (scene, camera, controls, renderer, container) => {
+ camera.position.set(3.6, 2.6, 3.8)
+
+ const nas = new THREE.Group()
+ const W = 2.3, H = 2.4, D = 2.6
+ const halfH = H / 2
+ const stripY = 0.25
+ const bayTopY = stripY - 0.15
+ const bayHeight = bayTopY - (-halfH + 0.25)
+
+ nas.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(W, H, D)), wireMat))
+
+ const grilleTex = makeGrilleTexture()
+ const topMat = new THREE.MeshBasicMaterial({ map: grilleTex, transparent: true, opacity: 0.85 })
+ const top = new THREE.Mesh(new THREE.PlaneGeometry(W - 0.12, D - 0.12), topMat)
+ top.rotation.x = -Math.PI / 2
+ top.position.y = halfH - 0.05
+ nas.add(top)
+
+ const strip = new THREE.Group()
+ strip.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(W, 0.26, 0.08)), new THREE.LineBasicMaterial({ color: 0xb9814f })))
+ const stripFace = new THREE.Mesh(new THREE.PlaneGeometry(W - 0.04, 0.22), copperMat)
+ stripFace.position.z = 0.045
+ strip.add(stripFace)
+ strip.position.set(0, stripY, D / 2 + 0.02)
+ nas.add(strip)
+
+ const bayWidth = 0.17, gap = 0.02
+ const startX = -(9 * bayWidth + 8 * gap) / 2 + bayWidth / 2
+ const hitMeshes: THREE.Mesh[] = []
+ const leds: { mat: THREE.MeshBasicMaterial; phase: number }[] = []
+
+ for (let i = 0; i < 9; i++) {
+ const x = startX + i * (bayWidth + gap)
+ const bay = new THREE.Group()
+ bay.position.set(x, bayTopY - bayHeight / 2, D / 2)
+ bay.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(bayWidth, bayHeight, 0.18)), wireMat))
+
+ const drive = new THREE.Mesh(new THREE.BoxGeometry(bayWidth - 0.03, bayHeight - 0.12, 0.32), tealDriveMat)
+ drive.position.z = -0.16
+ bay.add(drive)
+
+ const hitMesh = new THREE.Mesh(new THREE.BoxGeometry(bayWidth, bayHeight, 0.5), new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }))
+ hitMesh.position.z = -0.1
+ hitMesh.userData.label = `BAY ${String(i + 1).padStart(2, '0')} // WD RED 10TB (ONLINE)`
+ bay.add(hitMesh)
+ hitMeshes.push(hitMesh)
+
+ const ledMat = new THREE.MeshBasicMaterial({ color: 0xc1121f, transparent: true, opacity: 0.5 })
+ const led = new THREE.Mesh(new THREE.CircleGeometry(0.018, 8), ledMat)
+ led.position.set(0, bayHeight / 2 - 0.08, 0.1)
+ bay.add(led)
+ leds.push({ mat: ledMat, phase: i * 0.5 })
+
+ nas.add(bay)
+ }
+
+ scene.add(nas)
+
+ const hud = document.createElement('div')
+ hud.className = 'nas-hud'
+ hud.hidden = true
+ container.appendChild(hud)
+
+ const raycaster = new THREE.Raycaster()
+ const pointer = new THREE.Vector2()
+
+ const onPointerMove = (e: PointerEvent) => {
+ const rect = renderer.domElement.getBoundingClientRect()
+ pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
+ pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
+
+ raycaster.setFromCamera(pointer, camera)
+ const hit = raycaster.intersectObjects(hitMeshes)[0]
+
+ if (hit) {
+ hud.textContent = hit.object.userData.label
+ hud.style.left = `${Math.min(e.clientX - rect.left + 12, rect.width - 180)}px`
+ hud.style.top = `${Math.max(e.clientY - rect.top - 28, 10)}px`
+ hud.hidden = false
+ controls.autoRotate = false
+ } else {
+ hud.hidden = true
+ controls.autoRotate = true
+ }
+ }
+
+ const onPointerLeave = () => {
+ hud.hidden = true
+ controls.autoRotate = true
+ }
+
+ renderer.domElement.addEventListener('pointermove', onPointerMove)
+ renderer.domElement.addEventListener('pointerleave', onPointerLeave)
+
+ const clock = new THREE.Clock()
+
+ return {
+ update: () => {
+ const t = clock.getElapsedTime()
+ for (const l of leds) l.mat.opacity = 0.3 + Math.abs(Math.sin(t * 1.6 + l.phase)) * 0.6
+ },
+ dispose: () => {
+ renderer.domElement.removeEventListener('pointermove', onPointerMove)
+ renderer.domElement.removeEventListener('pointerleave', onPointerLeave)
+ container.removeChild(hud)
+ grilleTex.dispose()
+ topMat.dispose()
+ },
+ }
+}