Skip to content

Instantly share code, notes, and snippets.

@sdan
Last active December 6, 2025 06:10
Show Gist options
  • Select an option

  • Save sdan/830e3400fff1fc2351b71879c594c4db to your computer and use it in GitHub Desktop.

Select an option

Save sdan/830e3400fff1fc2351b71879c594c4db to your computer and use it in GitHub Desktop.
Gerstner Waves
import React, { useState, useEffect } from 'react';
export default function Tsunami() {
const [t, setT] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setT(prev => prev + 0.045);
}, 45);
return () => clearInterval(interval);
}, []);
const W = 90, H = 32;
const PW = W * 2, PH = H * 4;
const zBuf = new Float32Array(PW * PH);
const lum = new Float32Array(PW * PH);
// Rotation
const B = t * 0.6;
const cA = Math.cos(0.4 + Math.sin(t * 0.2) * 0.2);
const sA = Math.sin(0.4 + Math.sin(t * 0.2) * 0.2);
const cB = Math.cos(B);
const sB = Math.sin(B);
// Sample wave surface
for (let u = -3.5; u <= 3.5; u += 0.05) {
for (let v = -3.5; v <= 3.5; v += 0.05) {
const wavePos = u + t * 0.5;
const tsunami = Math.exp(-Math.pow(wavePos - 0.5, 2) * 0.8) * 2.8;
const curlFactor = Math.max(0, tsunami - 1.4);
const curl = Math.sin(wavePos * 2.5 + 0.5) * curlFactor * 0.7;
const trail = Math.sin(u * 1.5 + t) * 0.25 * Math.exp(-Math.pow(u - 2.5, 2) * 0.2);
const ripple = Math.sin(v * 2.5 + t * 1.5) * 0.12 * (1 - tsunami * 0.2);
const texture = Math.sin(u * 5 + v * 3 + t * 2) * 0.05;
const y = tsunami + curl + trail + ripple + texture;
let x = u;
let z = v;
let y1 = y * cA - z * sA;
let z1 = y * sA + z * cA;
let x2 = x * cB + z1 * sB;
let z2 = -x * sB + z1 * cB;
let y2 = y1;
const camDist = 7;
const depth = z2 + camDist;
if (depth <= 0.1) continue;
const fov = 75;
const scale = fov / depth;
const px = Math.floor(PW / 2 + x2 * scale * 4);
const py = Math.floor(PH / 2 - y2 * scale * 4);
if (px >= 0 && px < PW && py >= 0 && py < PH) {
const idx = py * PW + px;
const invZ = 1 / depth;
if (invZ > zBuf[idx]) {
zBuf[idx] = invZ;
const eps = 0.05;
const wavePos2 = (u + eps) + t * 0.5;
const tsunami2 = Math.exp(-Math.pow(wavePos2 - 0.5, 2) * 0.8) * 2.8;
const trail2 = Math.sin((u + eps) * 1.5 + t) * 0.25;
const hu = tsunami2 + trail2;
const ripple2 = Math.sin((v + eps) * 2.5 + t * 1.5) * 0.12;
const hv = tsunami + ripple2;
const nx = -(hu - y) / eps;
const ny = 1;
const nz = -(hv - y) / eps;
const nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
const L = (nx * 0.3 + ny * 1 + nz * -0.4) / nl;
const foam = tsunami > 2.2 ? 0.25 : 0;
lum[idx] = Math.max(0, Math.min(1, L * 0.6 + foam + 0.1));
}
}
}
}
// Spray
for (let i = 0; i < 80; i++) {
const pu = 0.5 - t * 0.5 + (Math.sin(i * 0.7) * 0.5);
const pv = (Math.sin(i * 1.3) * 3);
const wavePos = pu + t * 0.5;
const tsunami = Math.exp(-Math.pow(wavePos - 0.5, 2) * 0.8) * 2.8;
if (tsunami > 2.3) {
const sy = tsunami + 0.3 + Math.sin(t * 12 + i) * 0.3;
const sx = pu + Math.sin(i * 2.1) * 0.2;
const sz = pv;
let y1 = sy * cA - sz * sA;
let z1 = sy * sA + sz * cA;
let x2 = sx * cB + z1 * sB;
let z2 = -sx * sB + z1 * cB;
let y2 = y1;
const depth = z2 + 7;
if (depth > 0.1) {
const scale = 75 / depth;
const px = Math.floor(PW / 2 + x2 * scale * 4);
const py = Math.floor(PH / 2 - y2 * scale * 4);
if (px >= 0 && px < PW && py >= 0 && py < PH) {
if (Math.sin(t * 10 + i * 0.7) > 0) {
const idx = py * PW + px;
lum[idx] = 1;
zBuf[idx] = 1;
}
}
}
}
}
// Braille render
const patterns = [
0x2800, 0x2801, 0x2821, 0x2825,
0x282d, 0x282f, 0x286f, 0x28ef, 0x28ff
];
let output = '';
for (let cy = 0; cy < H; cy++) {
for (let cx = 0; cx < W; cx++) {
let total = 0, count = 0;
for (let dy = 0; dy < 4; dy++) {
for (let dx = 0; dx < 2; dx++) {
const idx = (cy * 4 + dy) * PW + (cx * 2 + dx);
if (zBuf[idx] > 0) {
total += lum[idx];
count++;
}
}
}
if (count > 0) {
const level = Math.min(8, Math.floor((total / count) * 9));
output += String.fromCharCode(patterns[level]);
} else {
output += String.fromCharCode(0x2800);
}
}
output += '\n';
}
return (
<div style={{
minHeight: '100vh',
background: '#000',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif'
}}>
<div style={{ marginBottom: '32px', textAlign: 'center' }}>
<h1 style={{
color: '#f5f5f7',
fontSize: '48px',
fontWeight: 600,
margin: 0,
letterSpacing: '-0.02em'
}}>
Gerstner Waves
</h1>
<p style={{
color: '#86868b',
fontSize: '21px',
fontWeight: 400,
marginTop: '8px',
letterSpacing: '-0.01em'
}}>
Real-time fluid dynamics rendered in Unicode
</p>
</div>
<pre style={{
fontFamily: 'SF Mono, Menlo, monospace',
fontSize: '9px',
lineHeight: '9px',
letterSpacing: 0,
color: '#f5f5f7',
opacity: 0.9
}}>
{output}
</pre>
<p style={{
color: '#86868b',
fontSize: '14px',
marginTop: '32px',
fontWeight: 400
}}>
3D wave simulation with z-buffering, surface normals, and dynamic lighting
</p>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment