File size: 6,911 Bytes
a44e2de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/**

 * 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;