Time for Aliens

Reconstructing the idea of time from an alien's POV using p5.js

Year:
2025
Tools:
ChatGPT, P5.js

what even is this project...

This clock begins with a simple question: what if time was not measured by Earth’s rotation, but by the life of a star?

i had to reimagine time from the perspective of an alien. light work.

For aliens, a “day” is not sunrise to sunset. Instead, I defined one full stellar lifecycle — from Protostar → Main Sequence → Red Giant → Planetary Nebula → White Dwarf — as the fundamental unit of time.

One stellar cycle = one alien day.

Color = time.
The passing of time is expressed not by numbers or ticks, but by the evolving color of a star as it changes state.

Subdivisions. I divided this alien day into 12 equal segments (not hours in the human sense), which act like checkpoints within the larger lifecycle. These subdivisions are visualized by outer markers that inherit the star’s color, each one slightly offset, so that the phases “chase” around the ring.

Discrete & continuous. The clock blends smooth, continuous transitions (colors morphing, noisy stellar contours shifting) with discrete cues (the magenta planetary nebula flash as a “tick,” and the marker cycle as alien “hours”).

Logic. Just as humans use astronomical cycles (Earth’s spin, Moon’s orbit) as time scaffolding, this alien system uses astrophysical cycles (stellar evolution). It is cyclical, yet unimaginably large in scale.

01) Reference Model

Timescales (order-of-magnitude; used for ratios, not direct time):
Protostar ~ 5 Myr
Main Sequence ~ 10 Gyr
Red Giant ~ 1 Gyr
Planetary Nebula ~ 30 kyr
White Dwarf cooling ~ 1 Tyr

I keep true ratios internally, then apply a visual floor so tiny phases aren’t invisible:

const SUN_SPANS_YEARS = {
  protostar: 5e6,
  mainSeq: 1.0e10,
  redGiant: 1.0e9,
  planetaryNebula: 3.0e4,
  whiteDwarf: 1.0e12
};

02) Time Logic

CYCLE_MS = screen-time for one alien day (120 s).
Normalize astrophysical spans ⇒ trueWeights.
Apply perceptual floor ⇒ visWeights.
tCycle ∈ [0..1) = normalized progress of the alien day.

const CYCLE_MS = 120000; // one alien day on screen
const tCycle = (millis() % CYCLE_MS) / CYCLE_MS;

function phaseAt(u01, weights) {
  let cum = [0];
  for (let i = 0; i < weights.length; i++) cum[i + 1] = cum[i] + weights[i];
  let idx = 0;
  for (let i = 0; i < weights.length; i++) {
    if (u01 >= cum[i] && u01 < cum[i + 1]) { idx = i; break; }
  }
  const localT = (u01 - cum[idx]) / (cum[idx + 1] - cum[idx]);
  return { idx, localT, start: cum[idx], end: cum[idx + 1] };
}

03) Color as time

Each phase has a key color.
Time blends from current phase color → next phase color with a smoothstep.

function lifecycleColor(info) {
  const a = phaseDefs[info.idx].col;
  const b = phaseDefs[(info.idx + 1) % phaseDefs.length].col;
  const t = smoothstep(info.localT);
  return lerpColor(a, b, t);
}

function smoothstep(t) { return t * t * (3 - 2 * t); }

Visual Language — Versions → Final

Center disc changes color through lifecycle.
Outer ring has 12 markers (loop + trig + transforms).Each marker runs the same cycle with a
phase offset → visible “chase.”

for (let i = 0; i < ALIEN_HOURS; i++) {
  hourRing.stars.push({
    baseAng: TWO_PI * i / ALIEN_HOURS,
    size: map(i, 0, ALIEN_HOURS - 1, 10, 14),
    phaseOffset: i / ALIEN_HOURS
  });
}

v2

Top center: “It’s [n] o’clock” (white).
Bottom left:
current phase.
Bottom right: countdown to next “hour.”

function currentAlienHour(t) {
  let h = floor(t * ALIEN_HOURS) + 1;
  return (h > ALIEN_HOURS) ? 1 : h;
}

function msToNextAlienHour(t) {
  const msPer = CYCLE_MS / ALIEN_HOURS;
  const f = (t * ALIEN_HOURS) % 1;
  return (1 - f) * msPer;
}

v3

PN flash (magenta ripple) as the brief tick.
Perlin-morphed core (contour-only) to show internal “stellar weather.”
Noisy ring (single wavy outline) to frame the clock without adding extra time semantics.

function drawPNFlash(tCycle) {
  const pnIndex = phaseDefs.findIndex(p => p.key === 'Planetary nebula');
  const info = phaseAt(tCycle, visWeights);
  if (info.idx === pnIndex) {
    const k = pow(1 - abs(0.5 - info.localT) * 2, 2.5);
    const s = map(k, 0, 1, 0, 120);
    push();
    noFill();
    stroke(315, 65, 100, map(k, 0, 1, 0, 70));
    strokeWeight(2);
    circle(0, 0, s + 140);
    circle(0, 0, s + 220);
    pop();
  }
}

Final sketch!