diff options
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.html | 422 |
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> |
