206 lines
6.3 KiB
HTML
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>
|