diff options
Diffstat (limited to 'working/clock-display-references')
4 files changed, 901 insertions, 0 deletions
diff --git a/working/clock-display-references/2026-07-30-maeda-cosmos-notes.org b/working/clock-display-references/2026-07-30-maeda-cosmos-notes.org new file mode 100644 index 0000000..04d8629 --- /dev/null +++ b/working/clock-display-references/2026-07-30-maeda-cosmos-notes.org @@ -0,0 +1,11 @@ +#+TITLE: Companion to the Line applet sent a few minutes ago: a copy +#+SOURCE: from website +#+DATE: 2026-07-30 18:48:00 -0500 + +Companion to the Line applet sent a few minutes ago: a copy of Cosmos (C1, 1995), the orbits piece — 'numbers in orbit'. Delivered as 2026-07-30-maeda-cosmos-standalone.html in this inbox. + +Same shape as the Line copy: the <maeda-cosmos> web component plus its model/step/render modules esbuild-bundled inline as an IIFE, so it opens straight from file:// with no server and no network. Verified in Chrome from file:// — canvas 400x300, the month's days riding the rotated ellipse with today red on the sweep hand, seconds odometer live at the right edge, zero console errors. + +Controls: hover reshapes every orbit; press-drag pulls a new loop out of the system; the mark at bottom-left clears them and replays the fly-in credit; click the canvas then ESC toggles strip-move mode. Note this one has no readout event, unlike Line. + +Source of truth stays ~/code/maeda-tribute (src/pieces/cosmos/); regenerate rather than edit. Reference only — no action needed. 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> diff --git a/working/clock-display-references/2026-07-30-maeda-line-notes.org b/working/clock-display-references/2026-07-30-maeda-line-notes.org new file mode 100644 index 0000000..ba50a80 --- /dev/null +++ b/working/clock-display-references/2026-07-30-maeda-line-notes.org @@ -0,0 +1,11 @@ +#+TITLE: Copy of the Line applet (C2, 1997) from the Maeda x Shiseido +#+SOURCE: from website +#+DATE: 2026-07-30 18:38:41 -0500 + +Copy of the Line applet (C2, 1997) from the Maeda x Shiseido tribute — the diagonal timeline piece, 'zoom through time'. Delivered as 2026-07-30-1838-from-website-2026-07-30-maeda-line-standalone.html in this inbox. + +It is a single self-contained file: the <maeda-line> web component and its model/step/render modules are esbuild-bundled inline as an IIFE, so it opens straight from file:// with no server, no build step, and no network. Verified rendering in Chrome from file:// — canvas 460x400, zero console errors, live readout wired. + +Controls: move the pointer along the diagonal to aim, press and hold to zoom continuously into that moment, release to float back out. Full zoom is exactly 60 seconds and the release is symmetric. + +Source of truth stays ~/code/maeda-tribute (src/pieces/line/); this copy is a snapshot, so regenerate rather than edit it if the piece changes. Sent for reference — no action needed unless you want it wired into the panel-widget gallery. diff --git a/working/clock-display-references/2026-07-30-maeda-line-standalone.html b/working/clock-display-references/2026-07-30-maeda-line-standalone.html new file mode 100644 index 0000000..783939c --- /dev/null +++ b/working/clock-display-references/2026-07-30-maeda-line-standalone.html @@ -0,0 +1,457 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Line (C2, 1997) — 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} +.readout{margin-top:.8rem;color:var(--gold);font-size:.82rem;letter-spacing:.06em} +.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 · C2</p> + <h1>Line — zoom through time</h1> + <p class="sub">John Maeda for Shiseido, 1997 (cal1.class) — rebuilt as a web component. Standalone copy, no build step.</p> + + <div class="card"> + <div class="stage"><maeda-line id="piece"></maeda-line></div> + <div class="readout" id="readout">scale — · counter —</div> + <p class="wnote"><b>Controls:</b> move the pointer along the diagonal to aim; press and + hold to zoom continuously into that moment; release and you float back out. Full zoom + is exactly 60 seconds, and the release is symmetric.</p> + <p class="wnote">Time as a single line — <b>diagonal</b>, bottom-left to top-right, the + detail every written description of this applet gets wrong. Maeda's note: it reflects + the relativity and comparability of time intervals. The zoom is the content.</p> + </div> + + <div class="card"> + <p class="eyebrow">Spec</p> + <div class="igrid"> + <span class="ik">original</span><span class="iv">cal1.class, 12,339 bytes, single class, self-contained (Java 1.1)</span> + <span class="ik">dimensions</span><span class="iv">460×400, black ground, Helvetica throughout</span> + <span class="ik">collection</span><span class="iv">SFMOMA 99.554</span> + <span class="ik">zoom plateaus</span><span class="iv">72px per day / hour / minute / second — scn 1 → 72 → 1,728 → 103,680 → 6,220,800 px/day</span> + <span class="ik">easing</span><span class="iv">counter at 20 increments/sec over segments [150, 250, 350, 450]</span> + <span class="ik">labels</span><span class="iv">font = min(72, unit spacing); text under 5px degrades to a line; gray ramps 128→255 as a unit matures; the current unit is always red</span> + <span class="ik">period quirks</span><span class="iv">Mac JVM patch capped zoom at hour level; an unused HAPPY NEW YEAR string ships in the bytecode</span> + <span class="ik">tests</span><span class="iv">33 (Vitest + fast-check)</span> + </div> + </div> +</div> + +<script> +(() => { + // src/lib/calendar.js + function epochDayFromCivil(y, m, d) { + const yy = y - (m <= 2 ? 1 : 0); + const era = Math.floor(yy / 400); + const yoe = yy - era * 400; + const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1; + const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy; + return era * 146097 + doe - 719468; + } + function civilFromEpochDay(ed) { + const z = ed + 719468; + const era = Math.floor(z / 146097); + const doe = z - era * 146097; + const yoe = Math.floor( + (doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365 + ); + const y = yoe + era * 400; + const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100)); + const mp = Math.floor((5 * doy + 2) / 153); + const d = doy - Math.floor((153 * mp + 2) / 5) + 1; + const m = mp + (mp < 10 ? 3 : -9); + return { y: y + (m <= 2 ? 1 : 0), m, d }; + } + + // src/pieces/line/model.js + var DAY = 86400; + var FAITHFUL_1997 = { + stops: [1, 72, 1728, 103680, 6220800], + segments: [150, 250, 350, 450], + incsPerSec: 20 + }; + function totalIncrements(cfg) { + return cfg.segments.reduce((a, b) => a + b, 0); + } + function scaleForCounter(cfg, counter) { + const total = totalIncrements(cfg); + const c = Math.max(0, Math.min(total, counter)); + let acc = 0; + for (let i = 0; i < cfg.segments.length; i++) { + const seg = cfg.segments[i]; + if (c <= acc + seg) { + const f = (c - acc) / seg; + return cfg.stops[i] + f * (cfg.stops[i + 1] - cfg.stops[i]); + } + acc += seg; + } + return cfg.stops[cfg.stops.length - 1]; + } + function advanceCounter(cfg, counter, dtMs, held) { + const d = cfg.incsPerSec * dtMs / 1e3; + const next = held ? counter + d : counter - d; + return Math.max(0, Math.min(totalIncrements(cfg), next)); + } + function pxOfTime(view, t) { + return (t - view.focus) * view.scale / DAY + view.anchorPx; + } + function timeOfPx(view, px) { + return (px - view.anchorPx) * DAY / view.scale + view.focus; + } + var MAX_FONT = 72; + var MIN_TEXT_PX = 5; + function fontSizeFor(spacingPx) { + return Math.min(MAX_FONT, spacingPx); + } + function textVisible(fontSize) { + return fontSize >= MIN_TEXT_PX; + } + function grayFor(fontSize) { + return Math.floor(Math.min(MAX_FONT, fontSize) * 127 / MAX_FONT) + 128; + } + var FIXED_UNITS = { second: 1, minute: 60, hour: 3600, day: DAY }; + function* calendarTicks(unit, t0, t1) { + let { y, m } = civilFromEpochDay(Math.floor(t0 / DAY)); + const stride = unit === "decade" ? 10 : unit === "century" ? 100 : 1; + if (unit === "month") { + for (; ; ) { + const t = epochDayFromCivil(y, m, 1) * DAY; + if (t >= t0) break; + m++; + if (m > 12) m = 1, y++; + } + for (; ; ) { + const t = epochDayFromCivil(y, m, 1) * DAY; + if (t > t1) return; + yield t; + m++; + if (m > 12) m = 1, y++; + } + } else { + let yy = Math.ceil(y / stride) * stride; + if (epochDayFromCivil(yy, 1, 1) * DAY < t0) yy += stride; + while (epochDayFromCivil(yy - stride, 1, 1) * DAY >= t0) yy -= stride; + for (; ; ) { + const t = epochDayFromCivil(yy, 1, 1) * DAY; + if (t > t1) return; + if (t >= t0) yield t; + yy += stride; + } + } + } + function ticksInRange(unit, t0, t1) { + if (unit in FIXED_UNITS) { + const w = FIXED_UNITS[unit]; + const out = []; + for (let t = Math.ceil(t0 / w) * w; t <= t1; t += w) out.push(t); + return out; + } + return [...calendarTicks(unit, t0, t1)]; + } + var MONTHS = [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ]; + function formatMonth(m) { + return MONTHS[m - 1]; + } + function formatDay(m, d) { + return `${m}/${d}`; + } + function formatHour(h) { + const twelve = h % 12 === 0 ? 12 : h % 12; + return { text: ` ${twelve}`, suffix: h >= 12 ? "pm" : "am" }; + } + function formatMinute(mm) { + return `:${String(mm).padStart(2, "0")}`; + } + + // src/pieces/line/step.js + function initState(cfg, range, lineLenPx) { + return { + counter: 0, + scale: scaleForCounter(cfg, 0), + focus: 0, + // epoch seconds under the anchor pixel + anchorPx: 0, + // pixel along the line where focus projects + range, + // {tMin, tMax} or null for the unbounded twist + lineLenPx + }; + } + function step(cfg, s, dtMs, input) { + const cursorPx = Math.max(0, Math.min(s.lineLenPx, input.cursorPx)); + let { focus, anchorPx } = s; + if (input.held || s.counter > 0) { + focus = timeOfPx({ focus: s.focus, scale: s.scale, anchorPx: s.anchorPx }, cursorPx); + anchorPx = cursorPx; + } + const counter = advanceCounter(cfg, s.counter, dtMs, input.held); + const scale = scaleForCounter(cfg, counter); + if (!input.held && counter < cfg.segments[0]) { + const target = cursorPx * DAY; + focus += (target - focus) / (counter + 1); + anchorPx = cursorPx; + } + if (s.range) { + focus = Math.max(s.range.tMin, Math.min(s.range.tMax, focus)); + } + return { ...s, counter, scale, focus, anchorPx }; + } + + // src/pieces/line/render.js + var LEVELS = [ + { unit: "month", secs: 30 * DAY }, + { unit: "day", secs: DAY }, + { unit: "hour", secs: 3600 }, + { unit: "minute", secs: 60 }, + { unit: "second", secs: 1 } + ]; + var FACE = { w: 460, h: 400 }; + function lineOrigin(state) { + const margin = (FACE.h - state.lineLenPx) / 2; + return { mx: FACE.w - FACE.h + margin, my: margin }; + } + function labelFor(unit, t) { + const dayIdx = Math.floor(t / DAY); + const civ = civilFromEpochDay(dayIdx); + const rem = t - dayIdx * DAY; + const hh = Math.floor(rem / 3600); + const mm = Math.floor(rem % 3600 / 60); + const ss = Math.floor(rem % 60); + switch (unit) { + case "month": + return formatMonth(civ.m); + case "day": + return formatDay(civ.m, civ.d); + case "hour": { + const h = formatHour(hh); + return h.text + h.suffix; + } + case "minute": + return formatMinute(mm); + case "second": + return formatMinute(ss); + default: + return ""; + } + } + function sameTick(unit, t, now) { + const w = { second: 1, minute: 60, hour: 3600, day: DAY }[unit]; + if (w) return Math.floor(t / w) === Math.floor(now / w); + const a = civilFromEpochDay(Math.floor(t / DAY)); + const b = civilFromEpochDay(Math.floor(now / DAY)); + return a.y === b.y && a.m === b.m; + } + function draw(ctx, state, nowSec) { + const { mx, my } = lineOrigin(state); + ctx.fillStyle = "#000"; + ctx.fillRect(0, 0, FACE.w, FACE.h); + const view = { focus: state.focus, scale: state.scale, anchorPx: state.anchorPx }; + const tA = Math.max( + state.range ? state.range.tMin : -Infinity, + state.focus - state.anchorPx * DAY / state.scale + ); + const tB = Math.min( + state.range ? state.range.tMax : Infinity, + state.focus + (state.lineLenPx - state.anchorPx) * DAY / state.scale + ); + ctx.strokeStyle = "#666"; + ctx.beginPath(); + ctx.moveTo(mx, FACE.h - my); + ctx.lineTo(mx + state.lineLenPx, FACE.h - my - state.lineLenPx); + ctx.stroke(); + const colWidth = {}; + for (let i = LEVELS.length - 1; i >= 0; i--) { + const { unit, secs } = LEVELS[i]; + const spacing = secs * state.scale / DAY; + if (spacing < 2 && unit !== "month") { + colWidth[unit] = 0; + continue; + } + const size = Math.floor(fontSizeFor(spacing)); + colWidth[unit] = textVisible(size) ? size * 2.2 : 0; + } + for (let i = LEVELS.length - 1; i >= 0; i--) { + const { unit, secs } = LEVELS[i]; + const spacing = secs * state.scale / DAY; + if (spacing < 2 && unit !== "month") continue; + const size = unit === "month" ? Math.floor(Math.min(72, 11 + state.scale)) : Math.floor(fontSizeFor(spacing)); + const gray = unit === "month" ? 255 : grayFor(size); + let offset = 0; + for (let j = LEVELS.length - 1; j > i; j--) offset += colWidth[LEVELS[j].unit]; + ctx.font = `${Math.max(size, 1)}px Helvetica, Arial, sans-serif`; + for (const t of ticksInRange(unit, tA, tB)) { + const n = pxOfTime(view, t); + if (n < 0 || n > state.lineLenPx) continue; + const x = mx + n; + const y = FACE.h - my - n; + const isNow = sameTick(unit, t, nowSec); + ctx.strokeStyle = ctx.fillStyle = isNow ? "#f00" : `rgb(${gray},${gray},${gray})`; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + 3, y); + ctx.stroke(); + const text = labelFor(unit, t); + if (textVisible(size)) { + const w = ctx.measureText(text).width; + ctx.fillText(text, x - w - offset, y); + } else { + ctx.beginPath(); + ctx.moveTo(x - 4 - offset, y); + ctx.lineTo(x - offset, y); + ctx.stroke(); + } + } + } + ctx.fillStyle = "#ccc"; + ctx.font = "15px Helvetica, Arial, sans-serif"; + const label = `${Math.round(state.scale)}X${state.counter >= 1200 ? " (MAX)" : ""}`; + ctx.fillText(label, 3, 16); + } + + // 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/line/index.js + var MaedaLine = 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:460px;cursor:crosshair;touch-action:none"; + this.appendChild(canvas); + const ctx = canvas.getContext("2d"); + const frozen = this.getAttribute("frozen-now"); + const nowSec = () => frozen ? Number(frozen) : Date.now() / 1e3; + const { y } = (() => { + const d = new Date(nowSec() * 1e3); + return { y: d.getUTCFullYear() }; + })(); + const t0 = epochDayFromCivil(y, 1, 1) * DAY; + const t1 = epochDayFromCivil(y + 1, 1, 1) * DAY; + const lineLen = Math.round((t1 - t0) / DAY); + const cfg = FAITHFUL_1997; + let state = initState(cfg, { tMin: 0, tMax: t1 - t0 }, lineLen); + state.focus = nowSec() - t0; + state.anchorPx = state.focus / DAY; + const input = { cursorPx: state.anchorPx, held: false }; + const toLinePx = (ev) => { + const r = canvas.getBoundingClientRect(); + const sx = FACE.w / r.width; + const x = (ev.clientX - r.left) * sx; + const yy = (ev.clientY - r.top) * (FACE.h / r.height); + const margin = (FACE.h - lineLen) / 2; + const mx = FACE.w - FACE.h + margin; + return (x - mx + (FACE.h - margin - yy)) / 2; + }; + canvas.addEventListener("pointermove", (ev) => { + input.cursorPx = toLinePx(ev); + }); + canvas.addEventListener("pointerdown", (ev) => { + canvas.setPointerCapture(ev.pointerId); + input.cursorPx = toLinePx(ev); + input.held = true; + }); + const release = () => { + input.held = false; + }; + canvas.addEventListener("pointerup", release); + canvas.addEventListener("pointercancel", release); + this._stop = startLoop({ + stepFn: (dtMs) => { + state = step(cfg, state, dtMs, input); + }, + renderFn: () => { + draw(ctx, state, nowSec() - t0); + this.dispatchEvent(new CustomEvent("readout", { + detail: { scale: state.scale, counter: state.counter } + })); + } + }); + } + disconnectedCallback() { + if (this._stop) this._stop(); + } + }; + customElements.define("maeda-line", MaedaLine); +})(); + +</script> +<script> +document.getElementById("piece").addEventListener("readout", (e) => { + const { scale, counter } = e.detail; + document.getElementById("readout").textContent = + "scale " + Number(scale).toFixed(3) + " · counter " + Math.round(counter); +}); +</script> +</body> +</html> |
