Files
2026-03-06 16:45:02 +08:00

206 lines
6.3 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据之海</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { background:#000; overflow:hidden; }
canvas { display:block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const FONT_SIZE = 13;
const CW = 7.8;
const CH = FONT_SIZE;
let COLS, ROWS;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
COLS = Math.floor(canvas.width / CW);
ROWS = Math.floor(canvas.height / CH);
ctx.font = `${FONT_SIZE}px "Courier New"`;
ctx.textBaseline = 'top';
}
resize();
window.addEventListener('resize', () => { resize(); initWaves(); });
// ── Wave config ───────────────────────────────────────────────────
const WAVE_COUNT = 5;
const SHORE_ROWS = 3;
let waves = [];
function waveSpacing() { return (ROWS - SHORE_ROWS) / WAVE_COUNT; }
function makeWave(y) {
return {
y,
speed: 0.12 + Math.random() * 0.06,
// Three sine components for an organic, non-repeating crest
a1: 1.5 + Math.random() * 1.0, f1: 0.045 + Math.random() * 0.025, p1: Math.random() * Math.PI * 2,
a2: 0.45 + Math.random() * 0.3, f2: 0.10 + Math.random() * 0.05, p2: Math.random() * Math.PI * 2,
a3: 0.18 + Math.random() * 0.12, f3: 0.19 + Math.random() * 0.07, p3: Math.random() * Math.PI * 2,
};
}
function initWaves() {
const sp = waveSpacing();
waves = [];
for (let i = 0; i < WAVE_COUNT + 2; i++) {
waves.push(makeWave(-sp + i * sp));
}
}
initWaves();
function crestOf(w, c) {
return w.y
+ Math.sin(c * w.f1 + w.p1) * w.a1
+ Math.sin(c * w.f2 + w.p2) * w.a2
+ Math.sin(c * w.f3 + w.p3) * w.a3;
}
// ── Multi-frequency noise — breaks up diagonal banding ────────────
// Returns 0..1, no visible periodicity at normal zoom
function bodyNoise(c, r, t) {
return (
Math.sin(c * 1.3 + r * 0.7 + t * 0.11) * 0.35 +
Math.sin(c * 0.37 + r * 1.17 + t * 0.07) * 0.30 +
Math.sin(c * 2.1 + r * 0.31 + t * 0.19) * 0.20 +
Math.sin(c * 0.71 + r * 2.03 + t * 0.05) * 0.15
) * 0.5 + 0.5;
}
// Separate noise for 0/1 character choice — different frequency so
// character grid doesn't line up with density grid
function charNoise(c, r, t) {
return Math.sin(c * 0.97 + r * 1.53 + t * 0.13) > 0 ? '1' : '0';
}
const CREST_CHARS = ['~','≈','~','≈','∿','~','~','≈'];
let T = 0;
let last = 0;
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const sp = waveSpacing();
const shoreY = ROWS - SHORE_ROWS;
// ── Waves ─────────────────────────────────────────────────────
waves.sort((a, b) => a.y - b.y);
for (let r = 0; r < shoreY; r++) {
for (let c = 0; c < COLS; c++) {
// Find which wave owns this cell
let ownWave = null, ownDepth = Infinity;
for (const w of waves) {
const cr = crestOf(w, c);
const depth = r - cr;
if (depth >= -0.5 && depth < ownDepth) {
ownDepth = depth;
ownWave = w;
}
}
if (!ownWave) continue; // above all waves → empty sky
const depth = ownDepth;
const depthRatio = Math.min(1, depth / sp);
const proximity = Math.max(0, Math.min(1, ownWave.y / shoreY));
let bright = 0, ch = '';
if (depth < 1.0) {
// ── CREST ──────────────────────────────────────────────
bright = 0.55 + proximity * 0.45;
const idx = Math.floor((c * 0.35 + T * 0.07) % CREST_CHARS.length);
ch = CREST_CHARS[idx];
} else {
// ── WAVE BODY ──────────────────────────────────────────
// Brightness: high near crest, smooth power-curve fade to 0
bright = Math.pow(1 - depthRatio, 1.5) * (0.18 + proximity * 0.62);
if (bright < 0.03) continue;
// Density mask: multi-frequency noise → no banding
const n = bodyNoise(c, r, T * 0.06);
// Threshold: crest area always dense, fades to ~50% at bottom of body
// Use a smooth S-curve so the transition looks organic
const threshold = depthRatio * depthRatio * 0.65;
if (n < threshold) continue;
ch = charNoise(c, r, T);
}
const v = Math.round(Math.min(bright, 1) * 255);
ctx.fillStyle = `rgb(${v},${v},${v})`;
ctx.fillText(ch, c * CW, r * CH);
}
}
// ── Shore foam ─────────────────────────────────────────────────
for (let r = shoreY; r < ROWS; r++) {
const dInShore = (r - shoreY) / SHORE_ROWS;
for (let c = 0; c < COLS; c++) {
// Foam uses same multi-freq noise so it doesn't look striped either
const n = bodyNoise(c, r, T * 0.2);
const bright = (1 - dInShore) * (0.3 + n * 0.6);
if (bright < 0.06) continue;
const v = Math.round(bright * 255);
ctx.fillStyle = `rgb(${v},${v},${v})`;
const ci = Math.floor((c * 0.4 + T * 0.1) % CREST_CHARS.length);
ctx.fillText(
r === shoreY ? CREST_CHARS[ci] : (n > 0.5 ? '1' : '0'),
c * CW, r * CH
);
}
}
}
function advance() {
const sp = waveSpacing();
for (const w of waves) {
// Slight acceleration as wave nears shore (shoaling)
const proximity = Math.max(0, w.y / (ROWS - SHORE_ROWS));
w.y += w.speed * (1 + proximity * 0.4);
// Slowly shift phases to keep crest shape evolving
w.p1 += 0.008; w.p2 -= 0.005; w.p3 += 0.011;
}
waves = waves.filter(w => w.y < ROWS + sp);
const topY = waves.length ? Math.min(...waves.map(w => w.y)) : 0;
while (waves.length < WAVE_COUNT + 2) {
waves.push(makeWave(topY - sp));
}
}
function loop(ts) {
if (ts - last >= 33) { // 30 fps cap
T++;
advance();
render();
last = ts;
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>