|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | +<meta charset="utf-8" /> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 6 | +<title>Teleology → Recursion → Sonata</title> |
| 7 | +<style> |
| 8 | + :root{ --bg:#111; --text:#eee; --muted:#bbb; --ink:#9ad1ff; } |
| 9 | + *{ box-sizing:border-box } |
| 10 | + html,body{ height:100% } |
| 11 | + body{ |
| 12 | + margin:0; background:var(--bg); color:var(--text); |
| 13 | + font:16px/1.5 "Courier New", Courier, monospace; |
| 14 | + display:flex; flex-direction:column; align-items:center; justify-content:center; |
| 15 | + padding:1rem; gap:1rem; |
| 16 | + } |
| 17 | + h1{ |
| 18 | + font-size:clamp(1rem,2.6vw,1.4rem); letter-spacing:.1em; margin:0; text-align:center; color:#ddd; |
| 19 | + } |
| 20 | + a{ color:#ddd; text-decoration:none; transition:color .25s ease } |
| 21 | + a:hover{ color:var(--ink); text-decoration:underline } |
| 22 | + canvas{ width:min(80vmin,900px); height:auto; aspect-ratio:4/1.6; display:block } |
| 23 | + footer{ color:#777; font-size:clamp(.7rem,1.5vw,.85rem) } |
| 24 | +</style> |
| 25 | +</head> |
| 26 | +<body> |
| 27 | + <h1> |
| 28 | + <a href="https://claude.ai/public/artifacts/4ec0a53e-03b8-4462-ab92-ac9c9b65e162" target="_blank" rel="noopener">Teleology</a> → |
| 29 | + <a href="https://abikesa.github.io/buddhism/" target="_blank" rel="noopener">Recursion</a> → |
| 30 | + <a href="https://ukb-pyro.github.io/inf/" target="_blank" rel="noopener">Sonata</a> → |
| 31 | + </h1> |
| 32 | + |
| 33 | + <canvas id="stage" aria-label="Smooth morph: line → circle → infinity → line"></canvas> |
| 34 | + |
| 35 | + <footer>© 2025 Ukubona LLC</footer> |
| 36 | + |
| 37 | +<script> |
| 38 | +(function(){ |
| 39 | + const canvas = document.getElementById('stage'); |
| 40 | + const ctx = canvas.getContext('2d'); |
| 41 | + |
| 42 | + // --- sizing with HiDPI crispness --- |
| 43 | + function fit(){ |
| 44 | + const cssW = canvas.getBoundingClientRect().width; |
| 45 | + const cssH = cssW / (4/1.6); |
| 46 | + const dpr = Math.max(1, window.devicePixelRatio || 1); |
| 47 | + canvas.width = Math.floor(cssW * dpr); |
| 48 | + canvas.height = Math.floor(cssH * dpr); |
| 49 | + ctx.setTransform(dpr,0,0,dpr,0,0); // draw in CSS pixels |
| 50 | + } |
| 51 | + |
| 52 | + // parametric shapes sampled at uniform t in [0,1) |
| 53 | + const N = 900; // many points for imperceptible transitions |
| 54 | + const T = 8_000; // ms per morph leg (long & smooth) |
| 55 | + const CYCLE = T * 3; // line->circle, circle->∞, ∞->line |
| 56 | + const TWO_PI = Math.PI * 2; |
| 57 | + |
| 58 | + // ease: very gentle S-curve (cosine) |
| 59 | + const ease = t => 0.5 - 0.5 * Math.cos(Math.PI * t); |
| 60 | + |
| 61 | + // base sampler returning arrays of [x,y] |
| 62 | + function sample(fn){ |
| 63 | + const pts = new Array(N); |
| 64 | + for(let i=0;i<N;i++){ |
| 65 | + const t = i / N; |
| 66 | + pts[i] = fn(t); |
| 67 | + } |
| 68 | + return pts; |
| 69 | + } |
| 70 | + |
| 71 | + // shapes centered at (0,0) with radius a, later scaled to canvas |
| 72 | + const line = sample(t => [ (t*2-1), 0 ]); // horizontal segment [-1,1]×{0} |
| 73 | + const circle = sample(t => [ Math.cos(TWO_PI*t), Math.sin(TWO_PI*t) ]); // unit circle |
| 74 | + // Lemniscate of Gerono (sideways ∞), vertically softened to better match circle |
| 75 | + const lemni = sample(t => { |
| 76 | + const th = TWO_PI * t; |
| 77 | + const x = Math.cos(th); |
| 78 | + const y = 0.60 * Math.sin(th) * Math.cos(th); // 0.5*sin(2θ) scaled |
| 79 | + return [x, y]; |
| 80 | + }); |
| 81 | + |
| 82 | + // interpolate between two shapes, pointwise |
| 83 | + function blend(a, b, m){ |
| 84 | + const out = new Array(N); |
| 85 | + for(let i=0;i<N;i++){ |
| 86 | + const x = a[i][0] + (b[i][0] - a[i][0]) * m; |
| 87 | + const y = a[i][1] + (b[i][1] - a[i][1]) * m; |
| 88 | + out[i] = [x, y]; |
| 89 | + } |
| 90 | + return out; |
| 91 | + } |
| 92 | + |
| 93 | + // subtle "breathing" on radius to avoid mechanical feel |
| 94 | + function radius(now, base){ |
| 95 | + return base * (1 + 0.02 * Math.sin(now/3000) + 0.01 * Math.cos(now/7000)); |
| 96 | + } |
| 97 | + |
| 98 | + function draw(now){ |
| 99 | + fit(); |
| 100 | + const w = canvas.width / (window.devicePixelRatio||1); |
| 101 | + const h = canvas.height / (window.devicePixelRatio||1); |
| 102 | + const cx = w/2, cy = h/2; |
| 103 | + const scale = Math.min(w, h) * 0.38; |
| 104 | + |
| 105 | + // which leg of the cycle? |
| 106 | + const t = (now % CYCLE); |
| 107 | + const leg = Math.floor(t / T); // 0,1,2 |
| 108 | + const local = (t % T) / T; // 0..1 within leg |
| 109 | + const m = ease(local); // eased morph amount |
| 110 | + |
| 111 | + // choose endpoints |
| 112 | + let A, B; |
| 113 | + if(leg === 0){ A = line; B = circle; } |
| 114 | + else if(leg === 1){ A = circle; B = lemni; } |
| 115 | + else { A = lemni; B = line; } |
| 116 | + |
| 117 | + // blend and render |
| 118 | + const pts = blend(A, B, m); |
| 119 | + const r = radius(now, scale); |
| 120 | + |
| 121 | + ctx.clearRect(0,0,w,h); |
| 122 | + ctx.save(); |
| 123 | + ctx.translate(cx, cy); |
| 124 | + |
| 125 | + ctx.beginPath(); |
| 126 | + for(let i=0;i<N;i++){ |
| 127 | + const x = pts[i][0] * r; |
| 128 | + const y = pts[i][1] * r; |
| 129 | + if(i===0) ctx.moveTo(x, y); |
| 130 | + else ctx.lineTo(x, y); |
| 131 | + } |
| 132 | + // for circle/lemni, gently close; for the line it’s effectively a straight segment |
| 133 | + ctx.closePath(); |
| 134 | + |
| 135 | + ctx.lineWidth = Math.max(2, Math.min(6, w*0.006)); |
| 136 | + ctx.lineJoin = "round"; |
| 137 | + ctx.lineCap = "round"; |
| 138 | + ctx.strokeStyle = "#9ad1ff"; |
| 139 | + ctx.shadowColor = "rgba(154,209,255,.25)"; |
| 140 | + ctx.shadowBlur = 8; |
| 141 | + ctx.stroke(); |
| 142 | + |
| 143 | + ctx.restore(); |
| 144 | + requestAnimationFrame(draw); |
| 145 | + } |
| 146 | + |
| 147 | + fit(); |
| 148 | + requestAnimationFrame(draw); |
| 149 | + window.addEventListener('resize', fit, {passive:true}); |
| 150 | +})(); |
| 151 | +</script> |
| 152 | +</body> |
| 153 | +</html> |
0 commit comments