Spaces:
Running
Running
| /** | |
| * WaveSurface — BufferGeometry height-map mesh creation and per-frame vertex updates. | |
| * ColorGradient — Maps amplitude to diverging blue-white-red colour scheme. | |
| * Loaded as a plain script; classes are defined on the global scope. | |
| */ | |
| class ColorGradient { | |
| /** | |
| * Map an amplitude value to an RGB colour. | |
| * Negative → blue, zero → white/light gray, positive → red. | |
| * @param {number} amplitude | |
| * @param {number} maxAmp | |
| * @returns {{ r: number, g: number, b: number }} | |
| */ | |
| getColor(amplitude, maxAmp) { | |
| if (maxAmp === 0) { | |
| return { r: 0.15, g: 0.15, b: 0.2 }; | |
| } | |
| // Normalize to [-1, 1] | |
| var t = amplitude / maxAmp; | |
| if (t < -1) t = -1; | |
| if (t > 1) t = 1; | |
| // 5-stop gradient for vivid amplitude visualization: | |
| // t = -1.0 → deep blue (0.05, 0.05, 0.6) | |
| // t = -0.5 → cyan (0.0, 0.5, 0.8) | |
| // t = 0.0 → dark gray (0.12, 0.12, 0.15) | |
| // t = +0.5 → orange (0.9, 0.5, 0.0) | |
| // t = +1.0 → bright red (1.0, 0.15, 0.1) | |
| var r, g, b; | |
| if (t < -0.5) { | |
| var s = (t + 1.0) / 0.5; // 0 at t=-1, 1 at t=-0.5 | |
| r = 0.05 + s * (0.0 - 0.05); | |
| g = 0.05 + s * (0.5 - 0.05); | |
| b = 0.6 + s * (0.8 - 0.6); | |
| } else if (t < 0) { | |
| var s = (t + 0.5) / 0.5; // 0 at t=-0.5, 1 at t=0 | |
| r = 0.0 + s * (0.12 - 0.0); | |
| g = 0.5 + s * (0.12 - 0.5); | |
| b = 0.8 + s * (0.15 - 0.8); | |
| } else if (t < 0.5) { | |
| var s = t / 0.5; // 0 at t=0, 1 at t=0.5 | |
| r = 0.12 + s * (0.9 - 0.12); | |
| g = 0.12 + s * (0.5 - 0.12); | |
| b = 0.15 + s * (0.0 - 0.15); | |
| } else { | |
| var s = (t - 0.5) / 0.5; // 0 at t=0.5, 1 at t=1 | |
| r = 0.9 + s * (1.0 - 0.9); | |
| g = 0.5 + s * (0.15 - 0.5); | |
| b = 0.0 + s * (0.1 - 0.0); | |
| } | |
| return { r: r, g: g, b: b }; | |
| } | |
| } | |
| class WaveSurface { | |
| constructor() { | |
| this.mesh = null; | |
| this.geometry = null; | |
| this.nx = 0; | |
| this.ny = 0; | |
| } | |
| /** | |
| * Create the PlaneGeometry mesh with nx × ny grid points. | |
| * PlaneGeometry is created in XY plane then rotated so Y is the height axis (XZ plane). | |
| * @param {number} nx - grid width in cells | |
| * @param {number} ny - grid height in cells | |
| * @param {number} dx - spatial step size | |
| * @returns {THREE.Mesh} | |
| */ | |
| init(nx, ny, dx) { | |
| this.nx = nx; | |
| this.ny = ny; | |
| var width = (nx - 1) * dx; | |
| var height = (ny - 1) * dx; | |
| // Create PlaneGeometry with (nx-1) x (ny-1) segments → nx*ny vertices | |
| this.geometry = new THREE.PlaneGeometry(width, height, nx - 1, ny - 1); | |
| // Rotate from XY plane to XZ plane so Y becomes the height axis | |
| this.geometry.rotateX(-Math.PI / 2); | |
| // Add vertex color attribute (Float32Array, 3 components per vertex) | |
| var vertexCount = this.geometry.attributes.position.count; | |
| var colors = new Float32Array(vertexCount * 3); | |
| // Initialize all vertices to white | |
| for (var i = 0; i < vertexCount * 3; i++) { | |
| colors[i] = 1.0; | |
| } | |
| this.geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| // MeshPhongMaterial with vertex colors and smooth shading | |
| var material = new THREE.MeshPhongMaterial({ | |
| vertexColors: true, | |
| flatShading: false, | |
| side: THREE.DoubleSide | |
| }); | |
| this.mesh = new THREE.Mesh(this.geometry, material); | |
| return this.mesh; | |
| } | |
| /** | |
| * Set each vertex y-position to amplitude[index] * heightScale. | |
| * After updating, recompute normals for correct lighting. | |
| * @param {Float32Array} amplitude | |
| * @param {number} heightScale | |
| */ | |
| updateHeights(amplitude, heightScale) { | |
| if (!this.geometry) return; | |
| var position = this.geometry.attributes.position; | |
| var count = position.count; | |
| var len = Math.min(count, amplitude.length); | |
| for (var i = 0; i < len; i++) { | |
| // After rotateX(-PI/2), the original Y axis maps to the new Y axis (height). | |
| // position.setY sets the height component. | |
| position.setY(i, amplitude[i] * heightScale); | |
| } | |
| position.needsUpdate = true; | |
| this.geometry.computeVertexNormals(); | |
| } | |
| /** | |
| * Set vertex colours using the diverging colour gradient. | |
| * @param {Float32Array} amplitude | |
| * @param {ColorGradient} colorGradient | |
| */ | |
| updateColors(amplitude, colorGradient) { | |
| if (!this.geometry) return; | |
| var colorAttr = this.geometry.attributes.color; | |
| var count = colorAttr.count; | |
| // Find max absolute amplitude for normalization | |
| var maxAmp = 0; | |
| var len = Math.min(count, amplitude.length); | |
| for (var i = 0; i < len; i++) { | |
| var absVal = Math.abs(amplitude[i]); | |
| if (absVal > maxAmp) maxAmp = absVal; | |
| } | |
| // Avoid division by zero | |
| if (maxAmp === 0) maxAmp = 1; | |
| for (var i = 0; i < len; i++) { | |
| var color = colorGradient.getColor(amplitude[i], maxAmp); | |
| colorAttr.setXYZ(i, color.r, color.g, color.b); | |
| } | |
| colorAttr.needsUpdate = true; | |
| } | |
| /** | |
| * Set vertex colours for orthogonal polarization mode. | |
| * Slit 1 wave → yellow, Slit 2 wave → green, overlap → blended. | |
| * @param {Float32Array} amp1 - amplitude from slit 1 | |
| * @param {Float32Array} amp2 - amplitude from slit 2 | |
| */ | |
| updateColorsOrthogonal(amp1, amp2) { | |
| if (!this.geometry) return; | |
| var colorAttr = this.geometry.attributes.color; | |
| var count = colorAttr.count; | |
| var len = Math.min(count, amp1.length, amp2.length); | |
| // Find max absolute amplitude across both for normalization | |
| var maxAmp = 0; | |
| for (var i = 0; i < len; i++) { | |
| var a = Math.abs(amp1[i]); | |
| var b = Math.abs(amp2[i]); | |
| if (a > maxAmp) maxAmp = a; | |
| if (b > maxAmp) maxAmp = b; | |
| } | |
| if (maxAmp === 0) maxAmp = 1; | |
| for (var i = 0; i < len; i++) { | |
| var v1 = amp1[i] / maxAmp; // [-1, 1] | |
| var v2 = amp2[i] / maxAmp; | |
| if (v1 < -1) v1 = -1; if (v1 > 1) v1 = 1; | |
| if (v2 < -1) v2 = -1; if (v2 > 1) v2 = 1; | |
| // Slit 1 (warm): negative=purple, zero=dark, positive=yellow/orange | |
| var r1 = 0.12 + Math.max(0, v1) * 0.88; | |
| var g1 = 0.12 + Math.max(0, v1) * 0.68; | |
| var b1 = 0.15 + Math.max(0, -v1) * 0.65; | |
| // Slit 2 (cool): negative=dark blue, zero=dark, positive=cyan/green | |
| var r2 = 0.12 + Math.max(0, -v2) * 0.1; | |
| var g2 = 0.12 + Math.max(0, v2) * 0.78; | |
| var b2 = 0.15 + Math.max(0, v2) * 0.65 + Math.max(0, -v2) * 0.55; | |
| // Blend: additive mix | |
| var r = Math.min(1, r1 * 0.5 + r2 * 0.5 + Math.abs(v1) * 0.2); | |
| var g = Math.min(1, g1 * 0.5 + g2 * 0.5); | |
| var b = Math.min(1, b1 * 0.5 + b2 * 0.5 + Math.abs(v2) * 0.15); | |
| colorAttr.setXYZ(i, r, g, b); | |
| } | |
| colorAttr.needsUpdate = true; | |
| } | |
| } | |
| window.ColorGradient = ColorGradient; | |
| window.WaveSurface = WaveSurface; | |