ASCII Art
(() => {
"use strict";
const cv = document.getElementById("c");
const ctx = cv.getContext("2d");
const TAU = Math.PI * 2;
// deterministic rng (variation 5167 / retry 15692)
let seed = 5167 ^ 15692;
const rnd = () => {
seed ^= seed << 13; seed >>>= 0;
seed ^= seed >>> 17;
seed ^= seed << 5; seed >>>= 0;
return seed / 4294967296;
};
// power-law sample x = min * u^(-1/alpha), clamped
const plaw = (mn, alpha) => {
let u; do { u = rnd(); } while (u < 1e-6);
const v = mn * Math.pow(u, -1 / alpha);
return v > 1.4 ? 1.4 : v;
};
// three character densities: heavy / medium / sparse
const RAMPS = [
" .`:.-=+*x%#@",
" . ..::--=+=",
" . . : -"
];
let W, H, cols, rows, cw, ch, fontPx;
let acc, accZ, accR;
// ---- latitude rings, clustered toward both poles (power-law) ----
const rings = [];
{
const N = 26;
for (let i = 1; i < N; i++) {
const half = i < N / 2;
let u = half ? (2 * i / N) : (2 * (N - i) / N);
u = Math.max(u, 0.02);
let lat = Math.PI / 2 * Math.pow(u, 0.58);
if (!half) lat = Math.PI - lat;
const r = rnd();
rings.push({
lat,
w: plaw(0.16, 1.9),
ramp: r < 0.20 ? 0 : (r < 0.58 ? 1 : 2),
k: 2 + Math.floor(rnd() * 4),
sp: 0.5 + rnd() * 1.3,
ph: rnd() * TAU
});
}
}
// ---- meridians, power-law clustered in longitude ----
const mers = [];
{
const M = 9;
for (let j = 1; j < M; j++) {
mers.push({
phi: TAU * Math.pow(j / M, 1.35),
w: plaw(0.14, 2.2),
ramp: rnd() < 0.30 ? 0 : (rnd() < 0.5 ? 1 : 2),
k: 3 + Math.floor(rnd() * 4),
sp: 0.7 + rnd() * 1.1,
ph: rnd() * TAU
});
}
}
// ---- three heavy great circles ----
const gcs = [];
for (let g = 0; g < 3; g++) {
gcs.push({
ta: rnd() * Math.PI, pa: rnd() * TAU,
w: 0.8 + 0.25 * rnd(),
ramp: 0, k: 5, sp: 1.3, ph: rnd() * TAU
});
}
// ---- marine snow, power-law fall rates ----
const SNOW_N = 120;
const snow = [];
function buildSnow() {
snow.length = 0;
for (let i = 0; i < SNOW_N; i++) {
const s = plaw(0.05, 2.4);
const r = rnd();
snow.push({
x: rnd() * cols, y: rnd() * rows,
v: 0.06 + s * 0.55,
ph: rnd() * TAU, b: 0.3 + rnd() * 1.5,
c: r < 0.60 ? "\u00b7" : (r < 0.86 ? "." : "*"),
hue: 162 + rnd() * 36
});
}
}
function resize() {
const d = window.devicePixelRatio || 1;
W = cv.width = Math.floor(innerWidth * d);
H = cv.height = Math.floor(innerHeight * d);
cv.style.width = innerWidth + "px";
cv.style.height = innerHeight + "px";
fontPx = Math.max(11, Math.min(22, innerWidth / 90)) * d;
ctx.font = fontPx + 'px "Courier New",Courier,monospace';
ctx.textAlign = "center";
ctx.textBaseline = "middle";
cw = ctx.measureText("M").width;
ch = fontPx * 1.06;
cols = Math.max(20, Math.floor(W / cw));
rows = Math.max(14, Math.floor(H / ch));
Rpx = Math.min(W, H) * 0.335;
acc = new Float32Array(cols * rows);
accZ = new Float32Array(cols * rows);
accR = new Float32Array(cols * rows);
buildSnow();
ctx.fillStyle = "#01060b";
ctx.fillRect(0, 0, W, H);
}
let Rpx = 0;
addEventListener("resize", resize);
resize();
function splat(px, py, pz, inten, ri) {
const c = Math.floor(px / cw), r = Math.floor(py / ch);
if (c < 0 || c >= cols || r < 0 || r >= rows) return;
const idx = r * cols + c;
acc[idx] += inten;
accZ[idx] += inten * pz;
accR[idx] += inten * ri;
}
let last = performance.now();
function frame(now) {
const dt = Math.min(0.05, (now - last) / 1000); last = now;
const t = now / 1000;
acc.fill(0); accZ.fill(0); accR.fill(0);
const breathe = 1 + 0.028 * Math.sin(t * 0.47);
const yaw = t * 0.33;
const tilt = 0.42 + 0.13 * Math.sin(t * 0.17);
const roll = t * 0.09;
const cyy = Math.cos(yaw), syy = Math.sin(yaw);
const ctt = Math.cos(tilt), stt = Math.sin(tilt);
const crr = Math.cos(roll), srr = Math.sin(roll);
const RR = Rpx * breathe;
const P = (x, y, z, w, ri, shim) => {
const x1 = x * cyy + z * syy;
const z1 = -x * syy + z * cyy;
const y2 = y * ctt - z1 * stt;
const z2 = y * stt + z1 * ctt;
const x3 = x1 * crr - y2 * srr;
const y3 = x1 * srr + y2 * crr;
const pers = 3.2 / (3.2 - z2);
const depth = 0.22 + 0.78 * (z2 + 1) * 0.5;
splat(W / 2 + x3 * RR * pers, H / 2 + y3 * RR * pers, z2, w * shim * depth, ri);
};
for (const g of rings) {
const cl = Math.cos(g.lat), sl = Math.sin(g.lat);
const N = 150;
for (let s = 0; s < N; s++) {
const th = s / N * TAU;
const shim = 0.55 + 0.45 * Math.sin(g.k * th + g.ph + t * g.sp);
P(cl * Math.cos(th), sl, cl * Math.sin(th), g.w, g.ramp, shim);
}
}
for (const m of mers) {
const N = 200;
const cp = Math.cos(m.phi), sp = Math.sin(m.phi);
for (let s = 0; s < N; s++) {
const lat = s / N * Math.PI;
const r2 = Math.sin(lat);
const shim = 0.55 + 0.45 * Math.sin(m.k * lat + m.ph + t * m.sp);
P(r2 * cp, Math.cos(lat), r2 * sp, m.w, m.ramp, shim);
}
}
for (const g of gcs) {
const cta = Math.cos(g.ta), sta = Math.sin(g.ta);
const cpa = Math.cos(g.pa), spa = Math.sin(g.pa);
const N = 220;
for (let s = 0; s < N; s++) {
const th = s / N * TAU;
const x0 = Math.cos(th), z0 = Math.sin(th);
const y1 = -z0 * sta, z1 = z0 * cta;
const shim = 0.55 + 0.45 * Math.sin(g.k * th + g.ph + t * g.sp);
P(x0 * cpa - y1 * spa, x0 * spa + y1 * cpa, z1, g.w, g.ramp, shim);
}
}
// the lure: bright sparks orbiting the surface, trailing
for (let q = 0; q < 3; q++) {
const la = Math.PI / 2 + 0.55 * Math.sin(t * 0.19);
const lo = t * 0.42 - q * 0.085;
const sa = Math.sin(la);
const x = sa * Math.cos(lo), y = Math.cos(la), z = sa * Math.sin(lo);
const x1 = x * cyy + z * syy;
const z1 = -x * syy + z * cyy;
const y2 = y * ctt - z1 * stt;
const z2 = y * stt + z1 * ctt;
const x3 = x1 * crr - y2 * srr;
const y3 = x1 * srr + y2 * crr;
const pers = 3.2 / (3.2 - z2);
const px = W / 2 + x3 * RR * pers;
const py = H / 2 + y3 * RR * pers;
const it = 1.6 / (1 + q * 2.1);
splat(px, py, z2, it, 0);
if (q === 0) {
splat(px + cw, py, z2, 0.5, 0);
splat(px - cw, py, z2, 0.5, 0);
splat(px, py + ch, z2, 0.5, 0);
splat(px, py - ch, z2, 0.5, 0);
}
}
// trail wash * motion smear reveals form
ctx.fillStyle = "rgba(1,6,11,0.30)";
ctx.fillRect(0, 0, W, H);
// marine snow drifting behind
for (const f of snow) {
f.y += f.v * dt;
f.x += Math.sin(t * 0.25 + f.ph) * 0.05 * dt;
if (f.y > rows + 1) { f.y = -1; f.x = Math.random() * cols; }
const blink = Math.pow(0.5 + 0.5 * Math.sin(t * f.b + f.ph), 2);
if (blink < 0.06) continue;
ctx.fillStyle = "hsla(" + f.hue.toFixed(0) + ",80%," + (16 + 38 * blink).toFixed(0) + "%,0.8)";
ctx.fillText(f.c, (Math.floor(f.x) + 0.5) * cw, (Math.floor(f.y) + 0.5) * ch);
}
// sphere cells * char density from ramp, color from depth + glow
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const idx = r * cols + c;
const a = acc[idx];
if (a < 0.035) continue;
const n = 1 - Math.exp(-a * 1.5);
const ri = Math.max(0, Math.min(2, Math.round(accR[idx] / a)));
const ramp = RAMPS[ri];
const ch0 = ramp[Math.min(ramp.length - 1, Math.floor(n * ramp.length))];
if (ch0 === " ") continue;
const z = accZ[idx] / a;
const hue = 198 - 40 * n - 8 * z;
const sat = 78 + 14 * n;
let li = 16 + 62 * Math.pow(n, 1.25);
if (li > 86) li = 86;
ctx.fillStyle = "hsl(" + hue.toFixed(1) + "," + sat.toFixed(0) + "%," + li.toFixed(0) + "%)";
ctx.fillText(ch0, (c + 0.5) * cw, (r + 0.5) * ch);
}
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();