From 84eb99afd2fd21b34391d64bc7c57393a078772e Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:41:12 -0400 Subject: [PATCH] feat: ring of 12 floating Elder Futhark runes around Nexus platform - 12 rune billboard sprites (Elder Futhark glyphs) in a slow-orbiting ring at radius 7.0, y=1.5, around the center platform - Alternating cyan (#00ffcc) / magenta (#ff44ff) additive-blend glow rendered via canvas + shadowBlur onto CanvasTexture sprites - Runes orbit at 0.08 rad/s with per-rune vertical float phase offsets - Faint orbit-indicator torus marks the ring height - Opacity pulses gently on each rune using individual phase offsets Fixes #110 Co-Authored-By: Claude Sonnet 4.6 --- 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(); } -- 2.43.0