← back to Games Agentabrams

games/constellation-clock/ConstellationClock.aa

365 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Constellation Clock</title>
<style>
  html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    overflow: hidden;
    background: #05060f;
  }
  #sky {
    display: block;
    width: 100vw;
    height: 100vh;
  }
  #digital {
    position: fixed;
    right: 22px;
    bottom: 18px;
    font-family: "Courier New", ui-monospace, monospace;
    color: rgba(200, 220, 255, 0.85);
    letter-spacing: 3px;
    font-size: 26px;
    text-shadow: 0 0 8px rgba(120,170,255,0.6), 0 0 18px rgba(80,120,255,0.35);
    user-select: none;
    z-index: 5;
  }
  #digital .date {
    display: block;
    font-size: 12px;
    letter-spacing: 4px;
    text-align: right;
    margin-top: 4px;
    color: rgba(150, 180, 230, 0.6);
  }
  #label {
    position: fixed;
    left: 22px;
    top: 16px;
    font-family: "Courier New", ui-monospace, monospace;
    color: rgba(160, 190, 240, 0.5);
    font-size: 12px;
    letter-spacing: 4px;
    text-transform: uppercase;
    user-select: none;
    z-index: 5;
  }
</style>
</head>
<body>
<canvas id="sky"></canvas>
<div id="label">Celestial Chronometer</div>
<div id="digital"><span id="clock">00:00:00</span><span class="date" id="date"></span></div>

<script>
(function () {
  "use strict";

  const canvas = document.getElementById("sky");
  const ctx = canvas.getContext("2d");
  const clockEl = document.getElementById("clock");
  const dateEl = document.getElementById("date");

  let W = 0, H = 0, CX = 0, CY = 0, DPR = 1;

  function resize() {
    DPR = Math.min(window.devicePixelRatio || 1, 2);
    W = window.innerWidth;
    H = window.innerHeight;
    canvas.width = Math.floor(W * DPR);
    canvas.height = Math.floor(H * DPR);
    canvas.style.width = W + "px";
    canvas.style.height = H + "px";
    ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
    CX = W / 2;
    CY = H / 2;
    buildStars();
  }

  // ---------- Background starfield ----------
  let bgStars = [];
  function buildStars() {
    bgStars = [];
    const count = Math.round((W * H) / 5200);
    for (let i = 0; i < count; i++) {
      bgStars.push({
        x: Math.random() * W,
        y: Math.random() * H,
        r: Math.random() * 1.3 + 0.2,
        base: Math.random() * 0.5 + 0.2,
        amp: Math.random() * 0.5 + 0.1,
        speed: Math.random() * 2 + 0.5,
        phase: Math.random() * Math.PI * 2,
        hue: 210 + Math.random() * 40
      });
    }
  }

  function drawBackground(t) {
    // subtle vertical nebula gradient
    const g = ctx.createRadialGradient(CX, CY, 0, CX, CY, Math.max(W, H) * 0.75);
    g.addColorStop(0, "#0a1230");
    g.addColorStop(0.5, "#070a1c");
    g.addColorStop(1, "#03040c");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);

    for (let i = 0; i < bgStars.length; i++) {
      const s = bgStars[i];
      const tw = s.base + s.amp * Math.sin(t * s.speed + s.phase);
      const a = Math.max(0, Math.min(1, tw));
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fillStyle = "hsla(" + s.hue + ",70%,85%," + a + ")";
      ctx.fill();
    }
  }

  // ---------- Constellation hand definitions ----------
  // Each hand is a chain of stars at given radius fractions along the hand.
  // Points: {r: fraction of handRadius, o: sideways offset fraction, mag: star size}
  const secondShape = [
    { r: 0.00, o: 0.00, mag: 2.0 },
    { r: 0.30, o: 0.05, mag: 1.6 },
    { r: 0.55, o: -0.05, mag: 1.8 },
    { r: 0.78, o: 0.04, mag: 1.5 },
    { r: 0.95, o: 0.00, mag: 2.6 }
  ];
  const minuteShape = [
    { r: 0.00, o: 0.00, mag: 2.4 },
    { r: 0.22, o: 0.07, mag: 2.0 },
    { r: 0.42, o: -0.06, mag: 2.2 },
    { r: 0.60, o: 0.08, mag: 2.0 },
    { r: 0.78, o: -0.04, mag: 2.4 },
    { r: 0.92, o: 0.05, mag: 3.2 }
  ];
  const hourShape = [
    { r: 0.00, o: 0.00, mag: 2.8 },
    { r: 0.25, o: 0.10, mag: 2.4 },
    { r: 0.30, o: -0.10, mag: 2.4 },
    { r: 0.52, o: 0.06, mag: 2.6 },
    { r: 0.70, o: -0.08, mag: 2.6 },
    { r: 0.88, o: 0.00, mag: 3.6 }
  ];

  function drawConstellation(shape, angle, handRadius, color, glow, t, twinkle) {
    // angle: 0 = 12 o'clock (up). rotate coordinate.
    const ca = Math.cos(angle - Math.PI / 2);
    const sa = Math.sin(angle - Math.PI / 2);
    const pts = [];
    for (let i = 0; i < shape.length; i++) {
      const p = shape[i];
      const along = p.r * handRadius;
      const side = p.o * handRadius;
      // local coords: along axis + perpendicular offset
      const lx = along;
      const ly = side;
      const x = CX + lx * ca - ly * sa;
      const y = CY + lx * sa + ly * ca;
      pts.push({ x: x, y: y, mag: p.mag });
    }

    // connecting lines
    ctx.save();
    ctx.lineWidth = 1.2;
    ctx.strokeStyle = color.line;
    ctx.shadowColor = color.glow;
    ctx.shadowBlur = glow;
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
    ctx.stroke();
    ctx.restore();

    // stars
    for (let i = 0; i < pts.length; i++) {
      const p = pts[i];
      const tw = twinkle ? (0.75 + 0.25 * Math.sin(t * 3 + i * 1.7)) : 1;
      const rad = p.mag * tw;
      ctx.save();
      ctx.shadowColor = color.glow;
      ctx.shadowBlur = glow * 1.4;
      const grd = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, rad * 3);
      grd.addColorStop(0, color.core);
      grd.addColorStop(0.4, color.star);
      grd.addColorStop(1, "rgba(0,0,0,0)");
      ctx.fillStyle = grd;
      ctx.beginPath();
      ctx.arc(p.x, p.y, rad * 3, 0, Math.PI * 2);
      ctx.fill();
      // bright core
      ctx.shadowBlur = 0;
      ctx.fillStyle = color.core;
      ctx.beginPath();
      ctx.arc(p.x, p.y, rad * 0.8, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
    return pts;
  }

  // ---------- Hour ticks as faint stars around the dial ----------
  function drawDialStars(t) {
    const R = Math.min(W, H) * 0.44;
    for (let i = 0; i < 12; i++) {
      const a = (i / 12) * Math.PI * 2 - Math.PI / 2;
      const x = CX + Math.cos(a) * R;
      const y = CY + Math.sin(a) * R;
      const tw = 0.5 + 0.5 * Math.sin(t * 1.5 + i);
      const big = (i % 3 === 0);
      ctx.beginPath();
      ctx.arc(x, y, big ? 2.4 : 1.4, 0, Math.PI * 2);
      ctx.fillStyle = "rgba(180,205,255," + (0.35 + 0.35 * tw) + ")";
      ctx.shadowColor = "rgba(140,180,255,0.8)";
      ctx.shadowBlur = big ? 10 : 5;
      ctx.fill();
      ctx.shadowBlur = 0;
    }
    // center star (the pole)
    const cg = ctx.createRadialGradient(CX, CY, 0, CX, CY, 14);
    cg.addColorStop(0, "rgba(255,255,255,0.95)");
    cg.addColorStop(0.4, "rgba(180,210,255,0.6)");
    cg.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = cg;
    ctx.beginPath();
    ctx.arc(CX, CY, 14, 0, Math.PI * 2);
    ctx.fill();
  }

  // ---------- Shooting stars ----------
  let shootings = [];
  let nextShoot = 2 + Math.random() * 4;
  function spawnShoot() {
    const edge = Math.random();
    const startX = Math.random() * W;
    const startY = Math.random() * H * 0.5;
    const angle = Math.PI * 0.15 + Math.random() * Math.PI * 0.2; // downward-right-ish
    const speed = 500 + Math.random() * 400;
    shootings.push({
      x: startX,
      y: startY,
      vx: Math.cos(angle) * speed * (Math.random() < 0.5 ? 1 : -1),
      vy: Math.sin(angle) * speed,
      life: 0,
      maxLife: 0.9 + Math.random() * 0.5,
      len: 80 + Math.random() * 120
    });
  }
  function updateShoot(dt) {
    nextShoot -= dt;
    if (nextShoot <= 0) {
      spawnShoot();
      nextShoot = 4 + Math.random() * 7;
    }
    for (let i = shootings.length - 1; i >= 0; i--) {
      const s = shootings[i];
      s.life += dt;
      s.x += s.vx * dt;
      s.y += s.vy * dt;
      if (s.life > s.maxLife) shootings.splice(i, 1);
    }
  }
  function drawShoot() {
    for (let i = 0; i < shootings.length; i++) {
      const s = shootings[i];
      const prog = s.life / s.maxLife;
      const alpha = Math.sin(prog * Math.PI); // fade in/out
      const mag = Math.hypot(s.vx, s.vy) || 1;
      const ux = s.vx / mag, uy = s.vy / mag;
      const tx = s.x - ux * s.len;
      const ty = s.y - uy * s.len;
      const grd = ctx.createLinearGradient(s.x, s.y, tx, ty);
      grd.addColorStop(0, "rgba(255,255,255," + (0.9 * alpha) + ")");
      grd.addColorStop(1, "rgba(255,255,255,0)");
      ctx.strokeStyle = grd;
      ctx.lineWidth = 2;
      ctx.shadowColor = "rgba(200,220,255,0.9)";
      ctx.shadowBlur = 8;
      ctx.beginPath();
      ctx.moveTo(s.x, s.y);
      ctx.lineTo(tx, ty);
      ctx.stroke();
      ctx.shadowBlur = 0;
      // head
      ctx.beginPath();
      ctx.arc(s.x, s.y, 2, 0, Math.PI * 2);
      ctx.fillStyle = "rgba(255,255,255," + alpha + ")";
      ctx.fill();
    }
  }

  // ---------- Main loop ----------
  const colHour = {
    line: "rgba(255,200,120,0.45)",
    glow: "rgba(255,180,90,0.9)",
    star: "rgba(255,190,110,0.9)",
    core: "rgba(255,240,210,1)"
  };
  const colMinute = {
    line: "rgba(140,200,255,0.5)",
    glow: "rgba(120,180,255,0.9)",
    star: "rgba(150,200,255,0.9)",
    core: "rgba(235,245,255,1)"
  };
  const colSecond = {
    line: "rgba(180,255,220,0.45)",
    glow: "rgba(140,255,200,0.85)",
    star: "rgba(170,255,210,0.9)",
    core: "rgba(240,255,250,1)"
  };

  let lastTime = null;

  function frame(now) {
    if (lastTime === null) lastTime = now;
    let dt = (now - lastTime) / 1000;
    if (dt > 0.1) dt = 0.1;
    lastTime = now;
    const t = now / 1000;

    const d = new Date();
    const ms = d.getMilliseconds();
    const sec = d.getSeconds() + ms / 1000;
    const min = d.getMinutes() + sec / 60;
    const hr = (d.getHours() % 12) + min / 60;

    const secAngle = (sec / 60) * Math.PI * 2;
    const minAngle = (min / 60) * Math.PI * 2;
    const hrAngle = (hr / 12) * Math.PI * 2;

    const base = Math.min(W, H);
    const hourR = base * 0.20;
    const minR = base * 0.33;
    const secR = base * 0.40;

    drawBackground(t);
    drawDialStars(t);

    updateShoot(dt);
    drawShoot();

    drawConstellation(hourShape, hrAngle, hourR, colHour, 16, t, true);
    drawConstellation(minuteShape, minAngle, minR, colMinute, 14, t, true);
    drawConstellation(secondShape, secAngle, secR, colSecond, 12, t, true);

    // digital readout
    const pad = (n) => (n < 10 ? "0" + n : "" + n);
    clockEl.textContent = pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
    dateEl.textContent = d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });

    requestAnimationFrame(frame);
  }

  window.addEventListener("resize", resize);
  resize();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>