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>