164 lines
4.5 KiB
JavaScript
164 lines
4.5 KiB
JavaScript
/**
|
|
* Module A — "The Subcarrier Manifold"
|
|
* 3D scrolling surface: 64 subcarriers x 60 time slots
|
|
*/
|
|
import * as THREE from 'three';
|
|
|
|
const MANIFOLD_VERTEX = `
|
|
attribute float aHeight;
|
|
attribute float aAge; // 0 = newest, 1 = oldest
|
|
varying float vHeight;
|
|
varying float vAge;
|
|
void main() {
|
|
vec3 pos = position;
|
|
pos.y += aHeight * 2.0;
|
|
vHeight = aHeight;
|
|
vAge = aAge;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
|
}
|
|
`;
|
|
|
|
const MANIFOLD_FRAGMENT = `
|
|
uniform float uTime;
|
|
varying float vHeight;
|
|
varying float vAge;
|
|
void main() {
|
|
// Color map: low=deep blue, mid=cyan, high=amber
|
|
vec3 lo = vec3(0.02, 0.06, 0.2);
|
|
vec3 mid = vec3(0.0, 0.83, 1.0);
|
|
vec3 hi = vec3(1.0, 0.53, 0.0);
|
|
|
|
float h = clamp(vHeight, 0.0, 1.0);
|
|
vec3 col = h < 0.5
|
|
? mix(lo, mid, h * 2.0)
|
|
: mix(mid, hi, (h - 0.5) * 2.0);
|
|
|
|
// Fade older rows
|
|
float alpha = 0.3 + 0.7 * (1.0 - vAge);
|
|
gl_FragColor = vec4(col, alpha);
|
|
}
|
|
`;
|
|
|
|
const SUBS = 64;
|
|
const TIME_SLOTS = 60;
|
|
|
|
export class SubcarrierManifold {
|
|
constructor(scene, panelGroup) {
|
|
this.group = new THREE.Group();
|
|
if (panelGroup) panelGroup.add(this.group);
|
|
else scene.add(this.group);
|
|
|
|
this._history = []; // ring buffer of Float32Array[64]
|
|
for (let i = 0; i < TIME_SLOTS; i++) {
|
|
this._history.push(new Float32Array(SUBS));
|
|
}
|
|
this._head = 0;
|
|
|
|
// Build surface geometry
|
|
const geo = new THREE.PlaneGeometry(8, 5, SUBS - 1, TIME_SLOTS - 1);
|
|
const vertCount = SUBS * TIME_SLOTS;
|
|
|
|
this._heights = new Float32Array(vertCount);
|
|
this._ages = new Float32Array(vertCount);
|
|
for (let t = 0; t < TIME_SLOTS; t++) {
|
|
for (let s = 0; s < SUBS; s++) {
|
|
this._ages[t * SUBS + s] = t / TIME_SLOTS;
|
|
}
|
|
}
|
|
|
|
geo.setAttribute('aHeight', new THREE.BufferAttribute(this._heights, 1));
|
|
geo.setAttribute('aAge', new THREE.BufferAttribute(this._ages, 1));
|
|
|
|
// Solid surface
|
|
const mat = new THREE.ShaderMaterial({
|
|
vertexShader: MANIFOLD_VERTEX,
|
|
fragmentShader: MANIFOLD_FRAGMENT,
|
|
uniforms: { uTime: { value: 0 } },
|
|
transparent: true,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: false,
|
|
blending: THREE.AdditiveBlending,
|
|
});
|
|
this._mesh = new THREE.Mesh(geo, mat);
|
|
this._mesh.rotation.x = -Math.PI * 0.35;
|
|
this.group.add(this._mesh);
|
|
|
|
// Wireframe overlay
|
|
const wireGeo = geo.clone();
|
|
wireGeo.setAttribute('aHeight', new THREE.BufferAttribute(this._heights, 1));
|
|
wireGeo.setAttribute('aAge', new THREE.BufferAttribute(this._ages, 1));
|
|
const wireMat = new THREE.ShaderMaterial({
|
|
vertexShader: MANIFOLD_VERTEX,
|
|
fragmentShader: `
|
|
varying float vHeight;
|
|
varying float vAge;
|
|
void main() {
|
|
float alpha = 0.15 * (1.0 - vAge);
|
|
gl_FragColor = vec4(0.0, 0.83, 1.0, alpha);
|
|
}
|
|
`,
|
|
uniforms: { uTime: { value: 0 } },
|
|
transparent: true,
|
|
wireframe: true,
|
|
depthWrite: false,
|
|
blending: THREE.AdditiveBlending,
|
|
});
|
|
this._wire = new THREE.Mesh(wireGeo, wireMat);
|
|
this._wire.rotation.x = -Math.PI * 0.35;
|
|
this.group.add(this._wire);
|
|
|
|
this._frameAccum = 0;
|
|
this._pushInterval = 1 / 15; // push ~15 rows/sec
|
|
}
|
|
|
|
update(dt, elapsed, data) {
|
|
this._mesh.material.uniforms.uTime.value = elapsed;
|
|
|
|
// Push new amplitude data at regular intervals
|
|
this._frameAccum += dt;
|
|
if (this._frameAccum >= this._pushInterval && data) {
|
|
this._frameAccum = 0;
|
|
|
|
const amp = data.nodes?.[0]?.amplitude;
|
|
const row = new Float32Array(SUBS);
|
|
if (amp && amp.length > 0) {
|
|
for (let i = 0; i < SUBS; i++) {
|
|
row[i] = amp[i % amp.length] || 0;
|
|
}
|
|
}
|
|
|
|
this._history[this._head] = row;
|
|
this._head = (this._head + 1) % TIME_SLOTS;
|
|
|
|
this._rebuildHeights();
|
|
}
|
|
}
|
|
|
|
_rebuildHeights() {
|
|
for (let t = 0; t < TIME_SLOTS; t++) {
|
|
const histIdx = (this._head + t) % TIME_SLOTS;
|
|
const row = this._history[histIdx];
|
|
for (let s = 0; s < SUBS; s++) {
|
|
const idx = t * SUBS + s;
|
|
this._heights[idx] = row[s];
|
|
this._ages[idx] = t / TIME_SLOTS;
|
|
}
|
|
}
|
|
|
|
const geo = this._mesh.geometry;
|
|
geo.attributes.aHeight.needsUpdate = true;
|
|
geo.attributes.aAge.needsUpdate = true;
|
|
|
|
const wGeo = this._wire.geometry;
|
|
wGeo.attributes.aHeight.needsUpdate = true;
|
|
wGeo.attributes.aAge.needsUpdate = true;
|
|
}
|
|
|
|
dispose() {
|
|
this._mesh.geometry.dispose();
|
|
this._mesh.material.dispose();
|
|
this._wire.geometry.dispose();
|
|
this._wire.material.dispose();
|
|
}
|
|
}
|