Files
the-nexus/modules/effects/rune-ring.js
Claude (Opus 4.6) 35dd6c5f17
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
[claude] Phase 4: Effects modules — matrix rain, lightning, beam, runes, gravity, shockwave (#423) (#444)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-24 18:19:26 +00:00

139 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* rune-ring.js — Orbiting Elder Futhark rune sprites
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.portals (count, colors, and online status from portals.json)
*
* Rune sprites orbit the scene in a ring. Count matches the portal count,
* colors come from portal colors, and brightness reflects portal online status.
* A faint torus marks the orbit track.
*/
import * as THREE from 'three';
const RUNE_RING_RADIUS = 7.0;
const RUNE_RING_Y = 1.5;
const RUNE_ORBIT_SPEED = 0.08; // radians per second
const DEFAULT_RUNE_COUNT = 12;
const ELDER_FUTHARK = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','','','ᚹ','ᚺ','ᚾ','','ᛃ'];
const FALLBACK_COLORS = ['#00ffcc', '#ff44ff'];
let _scene = null;
let _state = null;
/** @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number, portalOnline: boolean}>} */
const runeSprites = [];
let _orbitRingMesh = null;
let _builtForPortalCount = -1;
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);
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);
}
function _clearSprites() {
for (const rune of runeSprites) {
_scene.remove(rune.sprite);
if (rune.sprite.material.map) rune.sprite.material.map.dispose();
rune.sprite.material.dispose();
}
runeSprites.length = 0;
}
function _build(portals) {
_clearSprites();
const count = portals ? portals.length : DEFAULT_RUNE_COUNT;
_builtForPortalCount = count;
for (let i = 0; i < count; i++) {
const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length];
const color = portals ? portals[i].color : FALLBACK_COLORS[i % FALLBACK_COLORS.length];
const isOnline = portals ? portals[i].status === 'online' : true;
const texture = _createRuneTexture(glyph, color);
const mat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: isOnline ? 1.0 : 0.15,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const sprite = new THREE.Sprite(mat);
sprite.scale.set(1.3, 1.3, 1);
const baseAngle = (i / 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 / count) * Math.PI * 2, portalOnline: isOnline });
}
}
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.portals)
* @param {object} _theme
*/
export function init(scene, state, _theme) {
_scene = scene;
_state = state;
// Faint orbit track torus
const ringGeo = new THREE.TorusGeometry(RUNE_RING_RADIUS, 0.03, 6, 64);
const ringMat = new THREE.MeshBasicMaterial({ color: 0x224466, transparent: true, opacity: 0.22 });
_orbitRingMesh = new THREE.Mesh(ringGeo, ringMat);
_orbitRingMesh.rotation.x = Math.PI / 2;
_orbitRingMesh.position.y = RUNE_RING_Y;
scene.add(_orbitRingMesh);
// Initial build with defaults — will be rebuilt when portals load
_build(null);
}
export function update(elapsed, _delta) {
// Rebuild rune sprites when portal data changes
const portals = _state?.portals ?? [];
if (portals.length > 0 && portals.length !== _builtForPortalCount) {
_build(portals);
}
// Orbit and 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;
const baseOpacity = rune.portalOnline ? 0.85 : 0.12;
const pulseRange = rune.portalOnline ? 0.15 : 0.03;
rune.sprite.material.opacity = baseOpacity + Math.sin(elapsed * 1.2 + rune.floatPhase) * pulseRange;
}
}
/**
* Force a rebuild from current portal data.
* Called externally after portal health checks update statuses.
*/
export function rebuild() {
const portals = _state?.portals ?? [];
_build(portals.length > 0 ? portals : null);
}