← Mike Higgins Home
Custom Code Experiment •

Wave Interference

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wave Interference</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }

  html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #000000;
    color: #ffffff;
    font-family: 'Courier New', Courier, monospace;
  }

  /* ── fullscreen canvas ── */
  canvas {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    display: block;
  }

  /* ── overlay container ── */
  .overlay {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
    padding: 0 0 20px;
    z-index: 10;
  }

  .panel {
    width: 100%;
    max-width: 900px;
    margin: 0 20px;
  }

  /* ── controls ── */
  .controls {
    width: 100%;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 0;
    border: 1px solid #ffffff;
    border-bottom: none;
    background: #000000;
  }

  .ctrl-card {
    background: #000000;
    border-top: 1px solid #ffffff;
    border-right: 1px solid #ffffff;
    padding: 10px 14px;
  }

  .ctrl-card label {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 12px;
    font-weight: 700;
    color: #ffffff;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 7px;
  }
  .ctrl-card label span.val {
    color: #ffffff;
    font-size: 12px;
    font-weight: 700;
    min-width: 36px;
    text-align: right;
  }

  input[type=range] {
    width: 100%;
    -webkit-appearance: none;
    height: 2px;
    background: #ffffff;
    outline: none;
    cursor: pointer;
  }
  input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 12px; height: 12px;
    background: #ffffff;
    border: none;
    cursor: pointer;
  }
  input[type=range]::-moz-range-thumb {
    width: 12px; height: 12px;
    background: #ffffff;
    border: none;
    cursor: pointer;
  }

  /* ── preset buttons ── */
  .presets {
    display: flex;
    gap: 0;
    flex-wrap: wrap;
    justify-content: center;
    width: 100%;
    border: 1px solid #ffffff;
    background: #000000;
  }
  .preset-btn {
    flex: 1;
    background: #000000;
    border: none;
    border-right: 1px solid #ffffff;
    color: #ffffff;
    font-family: 'Courier New', Courier, monospace;
    font-size: 12px;
    font-weight: 700;
    padding: 8px 12px;
    cursor: pointer;
    letter-spacing: 0.4px;
    text-transform: uppercase;
  }
  .preset-btn:last-child { border-right: none; }
  .preset-btn:hover, .preset-btn.active {
    background: #ffffff;
    color: #000000;
  }
</style>
</head>
<body>

<canvas id="c"></canvas>

<div class="overlay">

  <div class="panel">
    <div class="controls">
      <div class="ctrl-card">
        <label>Frequency <span class="val" id="vFreq">3.0</span></label>
        <input type="range" id="freq" min="0.5" max="8" step="0.1" value="3.0">
      </div>
      <div class="ctrl-card">
        <label>Separation <span class="val" id="vSep">0.30</span></label>
        <input type="range" id="sep" min="0.05" max="0.5" step="0.01" value="0.3">
      </div>
      <div class="ctrl-card">
        <label>Phase Δ <span class="val" id="vPhase">0.0</span>π</label>
        <input type="range" id="phase" min="0" max="2" step="0.05" value="0">
      </div>
      <div class="ctrl-card">
        <label>Decay <span class="val" id="vDecay">1.5</span></label>
        <input type="range" id="decay" min="0.1" max="4" step="0.1" value="1.5">
      </div>
      <div class="ctrl-card">
        <label>Speed <span class="val" id="vSpeed">1.0</span>×</label>
        <input type="range" id="speed" min="0.1" max="3" step="0.1" value="1.0">
      </div>
      <div class="ctrl-card">
        <label>Colour Mode <span class="val" id="vMode">Wave</span></label>
        <input type="range" id="mode" min="0" max="2" step="1" value="0">
      </div>
    </div>

    <div class="presets">
      <button class="preset-btn active" onclick="applyPreset('default')">Default</button>
      <button class="preset-btn" onclick="applyPreset('double-slit')">Double Slit</button>
      <button class="preset-btn" onclick="applyPreset('standing')">Standing Wave</button>
      <button class="preset-btn" onclick="applyPreset('beats')">Beat Pattern</button>
      <button class="preset-btn" onclick="applyPreset('chaos')">Chaos</button>
    </div>
  </div>

</div>

<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl');

function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);

// ── Vertex shader ────────────────────────────────────────────────────
const VS = `
attribute vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
`;

// ── Fragment shader ──────────────────────────────────────────────────
const FS = `
precision highp float;

uniform vec2  u_res;
uniform float u_time;
uniform float u_freq;
uniform float u_sep;
uniform float u_phase;
uniform float u_decay;
uniform int   u_mode;

float wave(vec2 uv, vec2 src, float t, float phaseOff) {
  float d = length(uv - src);
  float amp = exp(-d * u_decay);
  return amp * sin(u_freq * 6.2831853 * (d - t) + phaseOff);
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_res;
  // aspect-correct coords centred at 0,0
  float aspect = u_res.x / u_res.y;
  vec2 p = (uv - 0.5) * vec2(aspect, 1.0);

  // Two sources
  vec2 s1 = vec2(-u_sep * aspect * 0.5, 0.0);
  vec2 s2 = vec2( u_sep * aspect * 0.5, 0.0);

  float w1 = wave(p, s1, u_time, 0.0);
  float w2 = wave(p, s2, u_time, u_phase * 3.14159265);
  float total = (w1 + w2) * 0.5; // −1…+1

  vec3 col;

  if (u_mode == 0) {
    // Black & white — constructive=white, destructive=black
    float pos = max(0.0, total);
    float neg = max(0.0, -total);
    float g = 0.5 + pos * 0.5 - neg * 0.5;
    col = vec3(g);
  } else if (u_mode == 1) {
    // High-contrast banded (stripes)
    float t = total * 0.5 + 0.5;
    float bands = floor(t * 8.0) / 8.0;
    col = vec3(bands);
  } else {
    // Dithered dot pattern feel — sharp threshold
    float w1r = wave(p, s1, u_time, 0.0);
    float w2r = wave(p, s2, u_time, u_phase * 3.14159265);
    float total2 = (w1r + w2r) * 0.5;
    float g = step(0.0, total2);
    col = vec3(g);
  }

  gl_FragColor = vec4(col, 1.0);
}
`;

// ── WebGL setup ──────────────────────────────────────────────────────
function mkShader(type, src) {
  const s = gl.createShader(type);
  gl.shaderSource(s, src);
  gl.compileShader(s);
  return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, mkShader(gl.VERTEX_SHADER,   VS));
gl.attachShader(prog, mkShader(gl.FRAGMENT_SHADER, FS));
gl.linkProgram(prog);
gl.useProgram(prog);

const quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
  -1,-1,  1,-1,  -1,1,  -1,1,  1,-1,  1,1
]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

const uRes   = gl.getUniformLocation(prog, 'u_res');
const uTime  = gl.getUniformLocation(prog, 'u_time');
const uFreq  = gl.getUniformLocation(prog, 'u_freq');
const uSep   = gl.getUniformLocation(prog, 'u_sep');
const uPhase = gl.getUniformLocation(prog, 'u_phase');
const uDecay = gl.getUniformLocation(prog, 'u_decay');
const uMode  = gl.getUniformLocation(prog, 'u_mode');

// ── State ────────────────────────────────────────────────────────────
let params = {
  freq: 3.0, sep: 0.30, phase: 0.0, decay: 1.5, speed: 1.0, mode: 0
};

function bind(id, key, disp, decimals=1) {
  const el = document.getElementById(id);
  const vEl = document.getElementById(disp);
  const modeNames = ['Wave','Heat','Phase'];
  el.addEventListener('input', () => {
    params[key] = parseFloat(el.value);
    vEl.textContent = key === 'mode' ? modeNames[params[key]] : params[key].toFixed(decimals);
  });
}
bind('freq',  'freq',  'vFreq',  1);
bind('sep',   'sep',   'vSep',   2);
bind('phase', 'phase', 'vPhase', 1);
bind('decay', 'decay', 'vDecay', 1);
bind('speed', 'speed', 'vSpeed', 1);
bind('mode',  'mode',  'vMode',  0);

// ── Presets ──────────────────────────────────────────────────────────
const presets = {
  'default':     { freq:3.0, sep:0.30, phase:0.0, decay:1.5, speed:1.0, mode:0 },
  'double-slit': { freq:5.0, sep:0.22, phase:0.0, decay:0.8, speed:0.7, mode:0 },
  'standing':    { freq:4.0, sep:0.35, phase:1.0, decay:0.6, speed:1.2, mode:0 },
  'beats':       { freq:2.0, sep:0.45, phase:0.5, decay:1.0, speed:0.5, mode:1 },
  'chaos':       { freq:7.0, sep:0.15, phase:1.5, decay:2.5, speed:2.0, mode:2 },
};

function applyPreset(name) {
  const p = presets[name];
  Object.assign(params, p);
  document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
  event.target.classList.add('active');
  // sync sliders
  const modeNames = ['Wave','Heat','Phase'];
  for (const [k, v] of Object.entries(p)) {
    const sl  = document.getElementById(k);
    const vEl = document.getElementById('v' + k.charAt(0).toUpperCase() + k.slice(1));
    if (sl) sl.value = v;
    if (vEl) vEl.textContent = k === 'mode' ? modeNames[v] : v.toFixed(k==='sep'?2:1);
  }
}

// ── Render loop ──────────────────────────────────────────────────────
let t0 = null;
function render(ts) {
  if (!t0) t0 = ts;
  const elapsed = (ts - t0) / 1000;

  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.uniform2f(uRes,   canvas.width, canvas.height);
  gl.uniform1f(uTime,  elapsed * params.speed);
  gl.uniform1f(uFreq,  params.freq);
  gl.uniform1f(uSep,   params.sep);
  gl.uniform1f(uPhase, params.phase);
  gl.uniform1f(uDecay, params.decay);
  gl.uniform1i(uMode,  params.mode);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>