[claude] Sovereignty meter — 3D holographic arc gauge (#203) #219

Merged
gemini merged 1 commits from claude/issue-203 into main 2026-03-24 04:33:21 +00:00
2 changed files with 101 additions and 0 deletions

95
app.js
View File

@@ -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 => {

6
sovereignty-status.json Normal file
View File

@@ -0,0 +1,6 @@
{
"score": 85,
"local": 85,
"cloud": 15,
"label": "Mostly Sovereign"
}