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);
})();