Last active
December 6, 2025 06:10
-
-
Save sdan/830e3400fff1fc2351b71879c594c4db to your computer and use it in GitHub Desktop.
Gerstner Waves
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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