diff --git a/app.js b/app.js index 485320f..9be7c99 100644 --- a/app.js +++ b/app.js @@ -340,6 +340,97 @@ window.addEventListener('resize', () => { composer.setSize(window.innerWidth, window.innerHeight); }); +// === SOVEREIGNTY METER === +// Holographic arc gauge floating above the platform; reads from sovereignty-status.json +const sovereigntyGroup = new THREE.Group(); +sovereigntyGroup.position.set(0, 3.8, 0); + +// Background ring — full circle, dark frame +const meterBgGeo = new THREE.TorusGeometry(1.6, 0.1, 8, 64); +const meterBgMat = new THREE.MeshBasicMaterial({ color: 0x0a1828, transparent: true, opacity: 0.5 }); +sovereigntyGroup.add(new THREE.Mesh(meterBgGeo, meterBgMat)); + +let sovereigntyScore = 85; +let sovereigntyLabel = 'Mostly Sovereign'; + +function sovereigntyHexColor(score) { + if (score >= 80) return 0x00ff88; + if (score >= 40) return 0xffcc00; + return 0xff4444; +} + +function buildScoreArcGeo(score) { + return new THREE.TorusGeometry(1.6, 0.1, 8, 64, (score / 100) * Math.PI * 2); +} + +const scoreArcMat = new THREE.MeshBasicMaterial({ + color: sovereigntyHexColor(sovereigntyScore), + transparent: true, + opacity: 0.9, +}); +const scoreArcMesh = new THREE.Mesh(buildScoreArcGeo(sovereigntyScore), scoreArcMat); +scoreArcMesh.rotation.z = Math.PI / 2; // arc starts at 12 o'clock +sovereigntyGroup.add(scoreArcMesh); + +// Glow light at gauge center +const meterLight = new THREE.PointLight(sovereigntyHexColor(sovereigntyScore), 0.7, 6); +sovereigntyGroup.add(meterLight); + +function buildMeterTexture(score, label) { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 128; + const ctx = canvas.getContext('2d'); + const hexStr = score >= 80 ? '#00ff88' : score >= 40 ? '#ffcc00' : '#ff4444'; + ctx.clearRect(0, 0, 256, 128); + ctx.font = 'bold 52px "Courier New", monospace'; + ctx.fillStyle = hexStr; + ctx.textAlign = 'center'; + ctx.fillText(`${score}%`, 128, 58); + ctx.font = '16px "Courier New", monospace'; + ctx.fillStyle = '#8899bb'; + ctx.fillText(label.toUpperCase(), 128, 82); + ctx.font = '11px "Courier New", monospace'; + ctx.fillStyle = '#445566'; + ctx.fillText('SOVEREIGNTY', 128, 104); + return new THREE.CanvasTexture(canvas); +} + +const meterSpriteMat = new THREE.SpriteMaterial({ + map: buildMeterTexture(sovereigntyScore, sovereigntyLabel), + transparent: true, + depthWrite: false, +}); +const meterSprite = new THREE.Sprite(meterSpriteMat); +meterSprite.scale.set(3.2, 1.6, 1); +sovereigntyGroup.add(meterSprite); + +scene.add(sovereigntyGroup); + +async function loadSovereigntyStatus() { + try { + const res = await fetch('./sovereignty-status.json'); + if (!res.ok) throw new Error('not found'); + const data = await res.json(); + const score = Math.max(0, Math.min(100, typeof data.score === 'number' ? data.score : 85)); + const label = typeof data.label === 'string' ? data.label : ''; + sovereigntyScore = score; + sovereigntyLabel = label; + scoreArcMesh.geometry.dispose(); + scoreArcMesh.geometry = buildScoreArcGeo(score); + const col = sovereigntyHexColor(score); + scoreArcMat.color.setHex(col); + meterLight.color.setHex(col); + if (meterSpriteMat.map) meterSpriteMat.map.dispose(); + meterSpriteMat.map = buildMeterTexture(score, label); + meterSpriteMat.needsUpdate = true; + } catch { + // defaults already set above + } +} + +loadSovereigntyStatus(); + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -384,6 +475,10 @@ function animate() { orbitControls.update(); } + // Animate sovereignty meter — gentle hover float and glow pulse + sovereigntyGroup.position.y = 3.8 + Math.sin(elapsed * 0.8) * 0.15; + meterLight.intensity = 0.5 + Math.sin(elapsed * 1.8) * 0.25; + // Animate floating commit banners const FADE_DUR = 1.5; commitBanners.forEach(banner => { diff --git a/sovereignty-status.json b/sovereignty-status.json new file mode 100644 index 0000000..1926bb9 --- /dev/null +++ b/sovereignty-status.json @@ -0,0 +1,6 @@ +{ + "score": 85, + "local": 85, + "cloud": 15, + "label": "Mostly Sovereign" +}