aboutsummaryrefslogtreecommitdiff
path: root/working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html
diff options
context:
space:
mode:
Diffstat (limited to 'working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html')
-rw-r--r--working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html422
1 files changed, 422 insertions, 0 deletions
diff --git a/working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html b/working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html
new file mode 100644
index 0000000..bfa9fbb
--- /dev/null
+++ b/working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html
@@ -0,0 +1,422 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Cosmos (C1, 1995) — Maeda × Shiseido tribute</title>
+<style>
+:root{
+ --ground:#151311;
+ --panel:#100f0f;
+ --well:#0a0c0d;
+ --raise:#1a1917;
+ --silver:#bfc4d0;
+ --cream:#f3e7c5;
+ --steel:#969385;
+ --dim:#7c838a;
+ --wash:#2c2f32;
+ --gold:#e2a038;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground);font-size:112%}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.wrap{max-width:820px;margin:0 auto}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{font-size:1.5rem;color:var(--cream);font-weight:600;margin:.35rem 0 .2rem}
+.sub{color:var(--dim);font-size:.9rem;margin-bottom:1.6rem}
+.card{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;
+ border-radius:12px;padding:1.2rem;margin-bottom:1.4rem}
+.stage{background:var(--well);border:1px solid #22201d;border-radius:8px;padding:1rem;
+ display:flex;justify-content:center}
+.wnote{color:var(--dim);font-size:.85rem;margin-top:1rem}
+.wnote b{color:var(--steel);font-weight:600}
+.igrid{display:grid;grid-template-columns:11rem 1fr;gap:.35rem .9rem;margin-top:1rem;
+ font-size:.82rem}
+.ik{color:var(--steel)}
+.iv{color:var(--dim)}
+</style>
+</head>
+<body>
+<div class="wrap">
+ <p class="eyebrow">Waste Time Beautifully · C1</p>
+ <h1>Cosmos — numbers in orbit</h1>
+ <p class="sub">John Maeda for Shiseido, 1995 (Cal0.class) — rebuilt as a web component. Standalone copy, no build step.</p>
+
+ <div class="card">
+ <div class="stage"><maeda-cosmos id="piece"></maeda-cosmos></div>
+ <p class="wnote"><b>Controls:</b> hover to reshape every orbit — the axes track 1.5× the
+ cursor's distance from each orbit's center; press and drag to pull a new loop out of the
+ system; the mark at bottom-left clears them and replays the fly-in credit. Click the
+ canvas first, then <b>ESC</b> toggles strip-move mode (the ground goes orange and the
+ clock strip comes free).</p>
+ <p class="wnote">The days of the month ride a rotated ellipse, today in red on the sweep
+ hand, with a seconds odometer scrolling at the right edge. The month as a gravitational
+ system, days as bodies in orbit, the user as a hand that perturbs the heavens. Time is
+ periodic, not linear.</p>
+ </div>
+
+ <div class="card">
+ <p class="eyebrow">Spec</p>
+ <div class="igrid">
+ <span class="ik">original</span><span class="iv">Cal0.class + UniverseCal, orbit, eint, efloatsin (Java 1.1)</span>
+ <span class="ik">dimensions</span><span class="iv">400×300, black ground, Helvetica 10</span>
+ <span class="ik">collection</span><span class="iv">SFMOMA 99.550</span>
+ <span class="ik">revolution</span><span class="iv">7,500 ms per sweep, phase = wall clock mod cycle — every orbit in lockstep forever; orientation wobbles ±10° on a 15,000 ms sine</span>
+ <span class="ik">interaction</span><span class="iv">every mouse move (not just drag) restyles all orbits; press spawns a zero-axis orbit that inflates as the cursor pulls away</span>
+ <span class="ik">period quirks</span><span class="iv">a cursor-driven rotation angle is computed and stored but never drawn — a dead store in the 1995 bytecode; Mac Java Date bug detected and adjusted</span>
+ <span class="ik">credit</span><span class="iv">chars fly in at 800+80i ms from random scatter, hold 1.5 s, vanish; logo click replays</span>
+ <span class="ik">tests</span><span class="iv">38 (Vitest + fast-check)</span>
+ </div>
+ </div>
+</div>
+
+<script>
+(() => {
+ // src/pieces/cosmos/model.js
+ var TAU = Math.PI * 2;
+ var CREDIT = "designed by john maeda";
+ var FAITHFUL_1995 = {
+ faceW: 400,
+ // applet tag WIDTH=400 HEIGHT=300
+ faceH: 300,
+ cycleMs: 7500,
+ // one revolution, wall-clock modulo
+ wobbleAmp: 10 * Math.PI / 180,
+ // efloatsin(0, 10°, 15000)
+ wobblePeriodMs: 15e3,
+ stretch: 1.5,
+ // axes = 1.5× cursor distance from center
+ initialAxes: [300, 100],
+ // the startup orbit at panel center
+ fontSize: 10,
+ // applet param default; Helvetica plain
+ stripWidth: 20,
+ // stringWidth("59")+6 at 10 px, rounded
+ clockReach: 120,
+ // readout + weekday row extent left of the strip
+ logo: { dx: 8, dy: 8, w: 70, h: 16 }
+ // shiseido.gif box, bottom-left
+ };
+ function sweepPhase(cfg, nowMs) {
+ return nowMs % cfg.cycleMs / cfg.cycleMs * TAU;
+ }
+ function wobbleAngle(cfg, elapsedMs) {
+ return cfg.wobbleAmp * Math.sin(elapsedMs * TAU / cfg.wobblePeriodMs);
+ }
+ function orbitPoint(o, theta, alpha) {
+ const ct = Math.cos(theta);
+ const st = Math.sin(theta);
+ const ca = Math.cos(alpha);
+ const sa = Math.sin(alpha);
+ return {
+ x: o.lmaj * ct * ca - o.lmin * st * sa + o.h,
+ y: o.lmaj * ct * sa + o.lmin * st * ca + o.k
+ };
+ }
+ function dayTheta(day, today, ndays, phase) {
+ return phase + (day - today) * TAU / ndays;
+ }
+ function makeOrbit(x, y, lmaj = 0, lmin = 0) {
+ return { h: x + 0.5, k: y + 0.5, lmaj, lmin, wobbleT: 0 };
+ }
+ function stretchOrbit(cfg, o, mx, my) {
+ return { ...o, lmaj: (mx - o.h) * cfg.stretch, lmin: (my - o.k) * cfg.stretch };
+ }
+ function creditDurations(len) {
+ const chars = [];
+ for (let i = 0; i < len; i++) chars.push(800 + 80 * i);
+ return { chars, sentinel: 800 + 80 * len + 1500 };
+ }
+ function easeLinear(t, a, b, dur) {
+ if (t <= 0) return a;
+ if (t >= dur) return b;
+ return a + (b - a) * t / dur;
+ }
+ function creditDone(len, t) {
+ return t >= creditDurations(len).sentinel;
+ }
+ function stripOffset(cfg, sec, msWithin) {
+ const ld = cfg.fontSize + 1;
+ const siddy = 3 + cfg.fontSize;
+ return -ld * sec - msWithin * ld / 1e3 + siddy - ld;
+ }
+ function logoHit(cfg, x, y) {
+ const { dx, dy, w, h } = cfg.logo;
+ return x < dx + w && y > cfg.faceH - h - dy;
+ }
+ function clockHit(cfg, strip, x, y) {
+ const ld = cfg.fontSize + 1;
+ const left = cfg.faceW - strip.sidex - cfg.clockReach;
+ const right = cfg.faceW - strip.sidex + cfg.stripWidth;
+ return x > left && x < right && y < strip.siddy && y > strip.siddy - ld;
+ }
+
+ // src/pieces/cosmos/step.js
+ function scatterStarts(cfg, rng) {
+ const starts = [];
+ for (let i = 0; i < CREDIT.length; i++) {
+ starts.push({
+ x: Math.floor(rng() * cfg.faceW) * (rng() < 0.5 ? 1 : -1) + cfg.faceW / 2,
+ y: Math.floor(rng() * cfg.faceH) * (rng() < 0.5 ? 1 : -1) + cfg.faceH / 2
+ });
+ }
+ return starts;
+ }
+ function initState(cfg, rng = Math.random) {
+ return {
+ orbits: [makeOrbit(cfg.faceW / 2, cfg.faceH / 2, cfg.initialAxes[0], cfg.initialAxes[1])],
+ credit: { t: 0, starts: scatterStarts(cfg, rng) },
+ strip: { sidex: cfg.stripWidth, siddy: 3 + cfg.fontSize },
+ killPending: false,
+ movable: false,
+ draggingStrip: false,
+ prevHeld: false,
+ prevCursor: null,
+ rng
+ };
+ }
+ function toggleMovable(state) {
+ state.movable = !state.movable;
+ return state;
+ }
+ function step(cfg, state, dtMs, input) {
+ const s = { ...state, strip: { ...state.strip }, credit: { ...state.credit } };
+ const { x, y, held } = input;
+ s.credit.t += dtMs;
+ s.orbits = s.orbits.map((o) => ({ ...o, wobbleT: o.wobbleT + dtMs }));
+ if (x == null) {
+ s.prevHeld = held;
+ return s;
+ }
+ if (held && !s.prevHeld) {
+ if (logoHit(cfg, x, y)) {
+ s.killPending = true;
+ s.credit = { t: 0, starts: scatterStarts(cfg, s.rng) };
+ } else if (clockHit(cfg, s.strip, x, y) || s.movable) {
+ s.draggingStrip = true;
+ } else {
+ s.orbits = [...s.orbits, makeOrbit(x, y)];
+ }
+ }
+ if (held && s.draggingStrip && s.prevCursor) {
+ s.strip.sidex -= x - s.prevCursor.x;
+ s.strip.siddy += y - s.prevCursor.y;
+ }
+ s.orbits = s.orbits.map((o) => stretchOrbit(cfg, o, x, y));
+ if (!held && s.prevHeld) {
+ if (s.killPending) {
+ s.orbits = [s.orbits[0]];
+ s.strip = { sidex: cfg.stripWidth, siddy: 3 + cfg.fontSize };
+ }
+ s.killPending = false;
+ s.draggingStrip = false;
+ }
+ s.prevHeld = held;
+ s.prevCursor = { x, y };
+ return s;
+ }
+
+ // src/lib/calendar.js
+ function isLeapYear(y) {
+ return y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
+ }
+ var MDAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+ function daysInMonth(y, m) {
+ return m === 2 && isLeapYear(y) ? 29 : MDAYS[m - 1];
+ }
+
+ // src/pieces/cosmos/render.js
+ var FACE = { w: 400, h: 300 };
+ var MONTHS = ["jan ", "feb ", "mar ", "apr ", "may ", "jun ", "jul ", "aug ", "sep ", "oct ", "nov ", "dec "];
+ var WEEKDAYS = ["S ", "M ", "T ", "W ", "T ", "F ", "S "];
+ function font(cfg) {
+ return `${cfg.fontSize}px Helvetica, Arial, sans-serif`;
+ }
+ function dateParts(nowMs) {
+ const d = new Date(nowMs);
+ return {
+ year: d.getFullYear(),
+ month: d.getMonth() + 1,
+ // 1-12
+ day: d.getDate(),
+ weekday: d.getDay(),
+ hours: d.getHours(),
+ minutes: d.getMinutes(),
+ seconds: d.getSeconds(),
+ msWithin: nowMs % 1e3
+ };
+ }
+ function drawOrbit(ctx, cfg, o, dp, phase, dim) {
+ const ndays = daysInMonth(dp.year, dp.month);
+ const alpha = wobbleAngle(cfg, o.wobbleT);
+ const daystr = `${dp.day} ${dp.year}`;
+ const monthstr = MONTHS[dp.month - 1];
+ for (let d = 1; d <= ndays; d++) {
+ const p = orbitPoint(o, dayTheta(d, dp.day, ndays, phase), alpha);
+ if (d === dp.day) {
+ ctx.fillStyle = "#f00";
+ ctx.fillText(daystr, p.x, p.y);
+ ctx.fillText(monthstr, p.x - ctx.measureText(monthstr).width, p.y);
+ } else {
+ ctx.fillStyle = dim ? "#808080" : "#fff";
+ ctx.fillText(String(d), p.x, p.y);
+ }
+ }
+ }
+ function drawStrip(ctx, cfg, state, dp) {
+ const ld = cfg.fontSize + 1;
+ const { sidex, siddy } = state.strip;
+ const x = FACE.w - sidex;
+ const tileH = ld * 60;
+ const off = stripOffset(cfg, dp.seconds, dp.msWithin) + (siddy - (3 + cfg.fontSize));
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(x, 0, cfg.stripWidth, FACE.h);
+ ctx.clip();
+ ctx.fillStyle = "#404040";
+ ctx.fillRect(x, 0, cfg.stripWidth, FACE.h);
+ ctx.fillStyle = "#808080";
+ for (const tile of [-1, 0, 1]) {
+ for (let i = 0; i < 60; i++) {
+ const y = off + tile * tileH + i * ld + ld;
+ if (y > -ld && y < FACE.h + ld) {
+ ctx.fillText(String(i).padStart(2, "0"), x + 3, y);
+ }
+ }
+ }
+ ctx.restore();
+ ctx.fillStyle = "#f00";
+ ctx.fillText(String(dp.seconds).padStart(2, "0"), x + 3, siddy);
+ const h12 = dp.hours % 12 === 0 ? 12 : dp.hours % 12;
+ const clock = `${h12}${dp.minutes < 10 ? ":0" : ":"}${dp.minutes}:`;
+ const cx = x + 3 - ctx.measureText(clock).width;
+ ctx.fillText(clock, cx, siddy);
+ let wx = cx - ctx.measureText("S M T W T F S ").width;
+ for (let i = 0; i < 7; i++) {
+ ctx.fillStyle = i === dp.weekday ? "#f00" : "#808080";
+ ctx.fillText(WEEKDAYS[i], wx, siddy);
+ wx += ctx.measureText(WEEKDAYS[i]).width;
+ }
+ }
+ function drawCredit(ctx, cfg, state) {
+ const { t, starts } = state.credit;
+ if (creditDone(CREDIT.length, t)) return;
+ const durs = creditDurations(CREDIT.length);
+ let fx = (FACE.w - ctx.measureText(CREDIT).width) / 2 + 1;
+ const fy = FACE.h / 2 + cfg.fontSize / 2;
+ ctx.fillStyle = "#fff";
+ for (let i = 0; i < CREDIT.length; i++) {
+ const ch = CREDIT[i];
+ const x = easeLinear(t, starts[i].x, fx, durs.chars[i]);
+ const y = easeLinear(t, starts[i].y, fy, durs.chars[i]);
+ ctx.fillText(ch, x, y);
+ fx += ctx.measureText(ch).width;
+ }
+ }
+ function drawLogo(ctx, cfg) {
+ const { dx, dy, h } = cfg.logo;
+ ctx.fillStyle = "#808080";
+ ctx.fillText("shiseido", dx, FACE.h - dy - h / 2 + cfg.fontSize / 2);
+ }
+ function draw(ctx, cfg, state, nowMs) {
+ const dp = dateParts(nowMs);
+ ctx.fillStyle = state.movable ? "#e08000" : "#000";
+ ctx.fillRect(0, 0, FACE.w, FACE.h);
+ ctx.font = font(cfg);
+ ctx.textBaseline = "alphabetic";
+ drawStrip(ctx, cfg, state, dp);
+ const phase = sweepPhase(cfg, nowMs);
+ state.orbits.forEach((o, i) => {
+ drawOrbit(ctx, cfg, o, dp, phase, state.killPending && i > 0);
+ });
+ drawLogo(ctx, cfg);
+ drawCredit(ctx, cfg, state);
+ ctx.strokeStyle = "#fff";
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0.5, 0.5, FACE.w - 1, FACE.h - 1);
+ }
+
+ // src/lib/loop.js
+ var STEP_MS = 30;
+ function startLoop({ stepFn, renderFn, stepMs = STEP_MS }) {
+ let acc = 0;
+ let last = null;
+ let raf = null;
+ let running = true;
+ function frame(ts) {
+ if (!running) return;
+ if (last === null) last = ts;
+ acc += Math.min(ts - last, 250);
+ last = ts;
+ while (acc >= stepMs) {
+ stepFn(stepMs);
+ acc -= stepMs;
+ }
+ renderFn();
+ raf = requestAnimationFrame(frame);
+ }
+ raf = requestAnimationFrame(frame);
+ return () => {
+ running = false;
+ if (raf) cancelAnimationFrame(raf);
+ };
+ }
+
+ // src/pieces/cosmos/index.js
+ var MaedaCosmos = class extends HTMLElement {
+ connectedCallback() {
+ const canvas = document.createElement("canvas");
+ canvas.width = FACE.w;
+ canvas.height = FACE.h;
+ canvas.style.cssText = "display:block;width:100%;max-width:400px;cursor:crosshair;touch-action:none";
+ canvas.tabIndex = 0;
+ this.appendChild(canvas);
+ const ctx = canvas.getContext("2d");
+ const frozen = this.getAttribute("frozen-now");
+ const nowMs = () => frozen ? Number(frozen) * 1e3 : Date.now();
+ const cfg = FAITHFUL_1995;
+ let state = initState(cfg);
+ const input = { x: null, y: null, held: false };
+ const toFace = (ev) => {
+ const r = canvas.getBoundingClientRect();
+ return {
+ x: (ev.clientX - r.left) * (FACE.w / r.width),
+ y: (ev.clientY - r.top) * (FACE.h / r.height)
+ };
+ };
+ canvas.addEventListener("pointermove", (ev) => {
+ Object.assign(input, toFace(ev));
+ });
+ canvas.addEventListener("pointerdown", (ev) => {
+ canvas.setPointerCapture(ev.pointerId);
+ canvas.focus();
+ Object.assign(input, toFace(ev));
+ input.held = true;
+ });
+ const release = () => {
+ input.held = false;
+ };
+ canvas.addEventListener("pointerup", release);
+ canvas.addEventListener("pointercancel", release);
+ canvas.addEventListener("keydown", (ev) => {
+ if (ev.key === "Escape") toggleMovable(state);
+ });
+ this._stop = startLoop({
+ stepFn: (dtMs) => {
+ state = step(cfg, state, dtMs, input);
+ },
+ renderFn: () => draw(ctx, cfg, state, nowMs())
+ });
+ }
+ disconnectedCallback() {
+ if (this._stop) this._stop();
+ }
+ };
+ customElements.define("maeda-cosmos", MaedaCosmos);
+})();
+
+</script>
+</body>
+</html>