From 39e0eecb9e2de7703c249c6d6f258fa70557c91f Mon Sep 17 00:00:00 2001 From: "Claude (Opus 4.6)" Date: Tue, 24 Mar 2026 04:42:29 +0000 Subject: [PATCH] [claude] Ring of floating runes around Nexus center platform (#110) (#240) --- app.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/app.js b/app.js index adb18c0..b396094 100644 --- a/app.js +++ b/app.js @@ -575,6 +575,87 @@ async function loadSovereigntyStatus() { loadSovereigntyStatus(); +// === RUNE RING === +// 12 Elder Futhark rune sprites in a slow-orbiting ring around the center platform. + +const RUNE_COUNT = 12; +const RUNE_RING_RADIUS = 7.0; +const RUNE_RING_Y = 1.5; // base height above platform +const RUNE_ORBIT_SPEED = 0.08; // radians per second + +const ELDER_FUTHARK = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚲ','ᚷ','ᚹ','ᚺ','ᚾ','ᛁ','ᛃ']; +const RUNE_GLOW_COLORS = ['#00ffcc', '#ff44ff']; // alternating cyan / magenta + +/** + * Creates a canvas texture for a single glowing rune glyph. + * @param {string} glyph + * @param {string} color + * @returns {THREE.CanvasTexture} + */ +function createRuneTexture(glyph, color) { + const W = 128, H = 128; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, W, H); + + // Outer glow + ctx.shadowColor = color; + ctx.shadowBlur = 28; + + ctx.font = 'bold 78px serif'; + ctx.fillStyle = color; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(glyph, W / 2, H / 2); + + return new THREE.CanvasTexture(canvas); +} + +// Faint torus marking the orbit height +const runeOrbitRingGeo = new THREE.TorusGeometry(RUNE_RING_RADIUS, 0.03, 6, 64); +const runeOrbitRingMat = new THREE.MeshBasicMaterial({ + color: 0x224466, + transparent: true, + opacity: 0.22, +}); +const runeOrbitRingMesh = new THREE.Mesh(runeOrbitRingGeo, runeOrbitRingMat); +runeOrbitRingMesh.rotation.x = Math.PI / 2; +runeOrbitRingMesh.position.y = RUNE_RING_Y; +scene.add(runeOrbitRingMesh); + +/** + * @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number}>} + */ +const runeSprites = []; + +for (let i = 0; i < RUNE_COUNT; i++) { + const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length]; + const color = RUNE_GLOW_COLORS[i % RUNE_GLOW_COLORS.length]; + const texture = createRuneTexture(glyph, color); + + const runeMat = new THREE.SpriteMaterial({ + map: texture, + transparent: true, + opacity: 0.85, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + const sprite = new THREE.Sprite(runeMat); + sprite.scale.set(1.3, 1.3, 1); + + const baseAngle = (i / RUNE_COUNT) * Math.PI * 2; + sprite.position.set( + Math.cos(baseAngle) * RUNE_RING_RADIUS, + RUNE_RING_Y, + Math.sin(baseAngle) * RUNE_RING_RADIUS + ); + scene.add(sprite); + runeSprites.push({ sprite, baseAngle, floatPhase: (i / RUNE_COUNT) * Math.PI * 2 }); +} + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -680,6 +761,15 @@ function animate() { } } + // Animate rune ring — orbit and vertical float + for (const rune of runeSprites) { + const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED; + rune.sprite.position.x = Math.cos(angle) * RUNE_RING_RADIUS; + rune.sprite.position.z = Math.sin(angle) * RUNE_RING_RADIUS; + rune.sprite.position.y = RUNE_RING_Y + Math.sin(elapsed * 0.7 + rune.floatPhase) * 0.4; + rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2; + } + composer.render(); }