Buckets:
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Partial Reprogramming — The Sweet Spot</title> | |
| <style> | |
| /* ---------- typography ---------- */ | |
| @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600&family=Inter+Tight:wght@300;400;500;600;700&display=swap'); | |
| :root { | |
| --bg-0: #050a14; | |
| --bg-1: #0a1428; | |
| --bg-2: #0f1d3a; | |
| --ink: #f8fafc; | |
| --ink-dim: #94a3b8; | |
| --ink-faint: #475569; | |
| --teal: #0ea5a4; | |
| --teal-glow: #14d6d3; | |
| --teal-deep: #0f766e; | |
| --amber: #f59e0b; | |
| --amber-glow: #fcd34d; | |
| --red: #ef4444; | |
| --red-deep: #b91c1c; | |
| --grid: rgba(20, 214, 211, 0.06); | |
| --hairline: rgba(148, 163, 184, 0.18); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { height: 100%; } | |
| body { | |
| font-family: 'Inter Tight', sans-serif; | |
| background: var(--bg-0); | |
| color: var(--ink); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| overflow-y: auto; | |
| /* atmospheric vignette */ | |
| background-image: | |
| radial-gradient(ellipse at 50% 30%, rgba(14, 165, 164, 0.08) 0%, transparent 60%), | |
| radial-gradient(ellipse at 80% 80%, rgba(245, 158, 11, 0.04) 0%, transparent 50%), | |
| linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%); | |
| background-attachment: fixed; | |
| } | |
| /* ---------- grid backdrop ---------- */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(var(--grid) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--grid) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; | |
| z-index: 0; | |
| mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%); | |
| -webkit-mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%); | |
| } | |
| /* ---------- container ---------- */ | |
| .stage { | |
| position: relative; | |
| z-index: 1; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 36px 48px 60px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| } | |
| /* ---------- header ---------- */ | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-end; | |
| gap: 32px; | |
| border-bottom: 1px solid var(--hairline); | |
| padding-bottom: 20px; | |
| } | |
| .header-left .eyebrow { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 500; | |
| letter-spacing: 0.28em; | |
| color: var(--amber); | |
| text-transform: uppercase; | |
| margin-bottom: 8px; | |
| } | |
| .header-left h1 { | |
| font-family: 'Fraunces', serif; | |
| font-size: 38px; | |
| font-weight: 600; | |
| line-height: 1.05; | |
| letter-spacing: -0.01em; | |
| } | |
| .header-left h1 em { | |
| font-style: italic; | |
| color: var(--teal-glow); | |
| font-weight: 400; | |
| } | |
| .header-left .sub { | |
| margin-top: 10px; | |
| font-size: 14px; | |
| color: var(--ink-dim); | |
| max-width: 60ch; | |
| line-height: 1.5; | |
| } | |
| .header-right { | |
| text-align: right; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--ink-faint); | |
| letter-spacing: 0.15em; | |
| text-transform: uppercase; | |
| } | |
| .header-right .meta-row { | |
| margin-bottom: 4px; | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 12px; | |
| } | |
| .header-right .meta-label { color: var(--ink-faint); } | |
| .header-right .meta-value { color: var(--ink-dim); } | |
| /* ---------- main grid ---------- */ | |
| .main { | |
| display: grid; | |
| grid-template-columns: 1fr 320px; | |
| gap: 28px; | |
| align-items: start; | |
| } | |
| /* ---------- viewer panel ---------- */ | |
| .viewer { | |
| background: linear-gradient(180deg, rgba(15, 29, 58, 0.6) 0%, rgba(10, 20, 40, 0.4) 100%); | |
| border: 1px solid var(--hairline); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| position: relative; | |
| backdrop-filter: blur(2px); | |
| } | |
| .viewer::before { | |
| /* corner ticks */ | |
| content: ''; | |
| position: absolute; | |
| inset: 12px; | |
| pointer-events: none; | |
| background-image: | |
| linear-gradient(to right, var(--teal) 0, var(--teal) 12px, transparent 12px, transparent calc(100% - 12px), var(--teal) calc(100% - 12px)), | |
| linear-gradient(to bottom, var(--teal) 0, var(--teal) 12px, transparent 12px, transparent calc(100% - 12px), var(--teal) calc(100% - 12px)); | |
| background-size: 100% 1px, 1px 100%; | |
| background-position: 0 0, 0 0; | |
| background-repeat: no-repeat; | |
| opacity: 0.5; | |
| } | |
| .viewer-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 14px 20px; | |
| border-bottom: 1px solid var(--hairline); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| letter-spacing: 0.2em; | |
| text-transform: uppercase; | |
| color: var(--ink-faint); | |
| } | |
| .viewer-header .live-dot { | |
| display: inline-block; | |
| width: 8px; height: 8px; | |
| background: var(--teal-glow); | |
| border-radius: 50%; | |
| box-shadow: 0 0 8px var(--teal-glow); | |
| animation: pulse 1.6s ease-in-out infinite; | |
| margin-right: 8px; | |
| vertical-align: middle; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| } | |
| .viewer-header .specimen-id { | |
| color: var(--teal-glow); | |
| } | |
| .viewer-body { | |
| height: 480px; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .cell-svg { | |
| width: 100%; height: 100%; | |
| } | |
| /* annotations overlay */ | |
| .annotation { | |
| position: absolute; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--ink-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 0.15em; | |
| pointer-events: none; | |
| opacity: 0.7; | |
| } | |
| .annotation.tl { top: 24px; left: 24px; } | |
| .annotation.tr { top: 24px; right: 24px; text-align: right; } | |
| .annotation.bl { bottom: 24px; left: 24px; } | |
| .annotation.br { bottom: 24px; right: 24px; text-align: right; } | |
| /* ---------- side readout panel ---------- */ | |
| .readout { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .readout-card { | |
| background: linear-gradient(180deg, rgba(15, 29, 58, 0.6) 0%, rgba(10, 20, 40, 0.4) 100%); | |
| border: 1px solid var(--hairline); | |
| border-radius: 4px; | |
| padding: 18px 20px; | |
| position: relative; | |
| } | |
| .readout-card .label { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 9px; | |
| color: var(--ink-faint); | |
| text-transform: uppercase; | |
| letter-spacing: 0.2em; | |
| margin-bottom: 10px; | |
| } | |
| .readout-card .value { | |
| font-family: 'Fraunces', serif; | |
| font-size: 32px; | |
| font-weight: 600; | |
| line-height: 1; | |
| margin-bottom: 6px; | |
| transition: color 0.4s; | |
| } | |
| .readout-card .value .unit { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| color: var(--ink-dim); | |
| font-weight: 400; | |
| margin-left: 4px; | |
| } | |
| .readout-card .desc { | |
| font-size: 11.5px; | |
| color: var(--ink-dim); | |
| line-height: 1.5; | |
| } | |
| /* gauge bar */ | |
| .gauge-track { | |
| position: relative; | |
| height: 4px; | |
| background: rgba(148, 163, 184, 0.15); | |
| border-radius: 2px; | |
| margin: 12px 0 8px; | |
| overflow: hidden; | |
| } | |
| .gauge-fill { | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(90deg, var(--teal) 0%, var(--teal-glow) 100%); | |
| border-radius: 2px; | |
| transform-origin: left; | |
| transition: transform 0.4s ease, background 0.4s ease; | |
| } | |
| .danger .value { color: var(--red); } | |
| .danger .gauge-fill { background: linear-gradient(90deg, var(--red-deep) 0%, var(--red) 100%); } | |
| .warn .value { color: var(--amber); } | |
| .warn .gauge-fill { background: linear-gradient(90deg, var(--amber) 0%, var(--amber-glow) 100%); } | |
| .ok .value { color: var(--teal-glow); } | |
| /* ---------- slider ---------- */ | |
| .slider-section { | |
| margin-top: 8px; | |
| background: linear-gradient(180deg, rgba(15, 29, 58, 0.6) 0%, rgba(10, 20, 40, 0.4) 100%); | |
| border: 1px solid var(--hairline); | |
| border-radius: 4px; | |
| padding: 24px 28px 28px; | |
| position: relative; | |
| } | |
| .slider-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| margin-bottom: 20px; | |
| } | |
| .slider-title { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--ink-faint); | |
| text-transform: uppercase; | |
| letter-spacing: 0.2em; | |
| } | |
| .slider-percent { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| color: var(--teal-glow); | |
| letter-spacing: 0.05em; | |
| } | |
| /* zone bar */ | |
| .zone-bar { | |
| position: relative; | |
| height: 14px; | |
| margin-bottom: 6px; | |
| border-radius: 7px; | |
| overflow: hidden; | |
| background: linear-gradient( | |
| 90deg, | |
| var(--red-deep) 0%, | |
| var(--red) 18%, | |
| #7a3a18 30%, | |
| var(--teal-deep) 38%, | |
| var(--teal) 52%, | |
| var(--teal-deep) 65%, | |
| #7a4a18 73%, | |
| var(--amber) 85%, | |
| var(--red) 100% | |
| ); | |
| box-shadow: inset 0 1px 2px rgba(0,0,0,0.5); | |
| } | |
| .zone-bar::before { | |
| /* sweet-spot highlight band */ | |
| content: ''; | |
| position: absolute; | |
| top: 0; bottom: 0; | |
| left: 38%; width: 27%; | |
| border-left: 1px dashed rgba(255,255,255,0.3); | |
| border-right: 1px dashed rgba(255,255,255,0.3); | |
| pointer-events: none; | |
| } | |
| /* zone labels */ | |
| .zone-labels { | |
| position: relative; | |
| height: 32px; | |
| margin-bottom: 18px; | |
| } | |
| .zone-label { | |
| position: absolute; | |
| transform: translateX(-50%); | |
| text-align: center; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 9.5px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.15em; | |
| line-height: 1.4; | |
| } | |
| .zone-label .name { color: var(--ink-dim); } | |
| .zone-label .pct { color: var(--ink-faint); font-size: 8.5px; margin-top: 2px; } | |
| .zone-label.aged .name { color: var(--red); } | |
| .zone-label.sweet .name { color: var(--teal-glow); } | |
| .zone-label.stem .name { color: var(--amber); } | |
| .zone-label.cancer .name { color: var(--red); } | |
| /* slider input */ | |
| .slider-input { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 100%; | |
| height: 38px; | |
| background: transparent; | |
| cursor: ew-resize; | |
| margin: 0; | |
| padding: 0; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| .slider-input:focus { outline: none; } | |
| .slider-input::-webkit-slider-runnable-track { | |
| height: 38px; | |
| background: transparent; | |
| } | |
| .slider-input::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| height: 38px; | |
| width: 14px; | |
| background: var(--ink); | |
| border: 2px solid var(--bg-0); | |
| border-radius: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--teal-glow), | |
| 0 0 18px var(--teal-glow), | |
| 0 4px 12px rgba(0,0,0,0.6); | |
| cursor: ew-resize; | |
| transition: box-shadow 0.2s; | |
| } | |
| .slider-input::-moz-range-track { | |
| height: 38px; | |
| background: transparent; | |
| } | |
| .slider-input::-moz-range-thumb { | |
| height: 38px; | |
| width: 14px; | |
| background: var(--ink); | |
| border: 2px solid var(--bg-0); | |
| border-radius: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--teal-glow), | |
| 0 0 18px var(--teal-glow), | |
| 0 4px 12px rgba(0,0,0,0.6); | |
| cursor: ew-resize; | |
| } | |
| /* tick marks under slider */ | |
| .tick-row { | |
| margin-top: 8px; | |
| height: 8px; | |
| position: relative; | |
| border-top: 1px solid var(--hairline); | |
| } | |
| .tick { | |
| position: absolute; | |
| width: 1px; | |
| height: 6px; | |
| background: var(--ink-faint); | |
| transform: translateX(-50%); | |
| top: 0; | |
| } | |
| .tick.major { height: 8px; background: var(--ink-dim); } | |
| /* ---------- cell-type selector ---------- */ | |
| .cell-type-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .cell-type-section .label { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--ink-faint); | |
| text-transform: uppercase; | |
| letter-spacing: 0.2em; | |
| } | |
| .cell-type-buttons { | |
| display: flex; | |
| gap: 0; | |
| border: 1px solid var(--hairline); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .cell-type-btn { | |
| background: transparent; | |
| border: none; | |
| padding: 9px 18px; | |
| color: var(--ink-dim); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.12em; | |
| cursor: pointer; | |
| border-right: 1px solid var(--hairline); | |
| transition: all 0.2s; | |
| } | |
| .cell-type-btn:last-child { border-right: none; } | |
| .cell-type-btn:hover { background: rgba(14, 165, 164, 0.08); color: var(--ink); } | |
| .cell-type-btn.active { | |
| background: var(--teal); | |
| color: var(--bg-0); | |
| font-weight: 600; | |
| } | |
| /* ---------- status banner ---------- */ | |
| .status-banner { | |
| margin-top: 16px; | |
| padding: 14px 18px; | |
| border-radius: 4px; | |
| border: 1px solid; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| transition: all 0.4s; | |
| } | |
| .status-banner .status-icon { | |
| flex-shrink: 0; | |
| width: 22px; height: 22px; | |
| border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| font-weight: 700; | |
| } | |
| .status-banner.aged { | |
| background: rgba(239, 68, 68, 0.08); | |
| border-color: rgba(239, 68, 68, 0.3); | |
| color: var(--ink); | |
| } | |
| .status-banner.aged .status-icon { background: var(--red); color: var(--ink); } | |
| .status-banner.sweet { | |
| background: rgba(14, 165, 164, 0.10); | |
| border-color: rgba(20, 214, 211, 0.4); | |
| color: var(--ink); | |
| animation: sweet-pulse 2.4s ease-in-out infinite; | |
| } | |
| @keyframes sweet-pulse { | |
| 0%, 100% { box-shadow: 0 0 0 rgba(20, 214, 211, 0); } | |
| 50% { box-shadow: 0 0 24px rgba(20, 214, 211, 0.25); } | |
| } | |
| .status-banner.sweet .status-icon { background: var(--teal-glow); color: var(--bg-0); } | |
| .status-banner.warn { | |
| background: rgba(245, 158, 11, 0.08); | |
| border-color: rgba(245, 158, 11, 0.3); | |
| color: var(--ink); | |
| } | |
| .status-banner.warn .status-icon { background: var(--amber); color: var(--bg-0); } | |
| .status-banner.danger { | |
| background: rgba(185, 28, 28, 0.12); | |
| border-color: rgba(239, 68, 68, 0.5); | |
| color: var(--ink); | |
| } | |
| .status-banner.danger .status-icon { background: var(--red); color: var(--ink); } | |
| .status-banner .title { | |
| font-weight: 600; | |
| margin-right: 6px; | |
| } | |
| /* ---------- footer ---------- */ | |
| .footer { | |
| margin-top: 8px; | |
| text-align: center; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--ink-faint); | |
| letter-spacing: 0.15em; | |
| text-transform: uppercase; | |
| } | |
| .footer .em { color: var(--teal-glow); } | |
| /* responsive */ | |
| @media (max-width: 980px) { | |
| .stage { padding: 24px 20px 40px; } | |
| .header { flex-direction: column; align-items: flex-start; } | |
| .header-right { text-align: left; } | |
| .main { grid-template-columns: 1fr; } | |
| .header-left h1 { font-size: 28px; } | |
| .viewer-body { height: 380px; } | |
| .zone-label { font-size: 8px; } | |
| .cell-type-section { flex-direction: column; align-items: flex-start; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="stage"> | |
| <!-- ===== HEADER ===== --> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <div class="eyebrow">Live Cellular Reprogramming Simulator</div> | |
| <h1>Finding the <em>sweet spot</em></h1> | |
| <p class="sub"> | |
| Drag the reprogramming slider. Watch the cell move from aged through partial rejuvenation toward pluripotency. The challenge isn't going further — it's stopping at the right place. | |
| </p> | |
| </div> | |
| <div class="header-right"> | |
| <div class="meta-row"><span class="meta-label">Mode</span><span class="meta-value" id="modeLabel">OSK · Partial Reprogramming</span></div> | |
| <div class="meta-row"><span class="meta-label">Reference</span><span class="meta-value">Lu et al. 2020</span></div> | |
| <div class="meta-row"><span class="meta-label">Spec ID</span><span class="meta-value" id="specId">RPG-0001</span></div> | |
| </div> | |
| </header> | |
| <!-- ===== MAIN GRID ===== --> | |
| <div class="main"> | |
| <!-- VIEWER --> | |
| <div class="viewer"> | |
| <div class="viewer-header"> | |
| <div><span class="live-dot"></span>SPECIMEN VIEWER</div> | |
| <div>Cell type: <span class="specimen-id" id="specimenType">RETINAL GANGLION</span></div> | |
| </div> | |
| <div class="viewer-body"> | |
| <span class="annotation tl" id="annoTL">EPI-DRIFT · HIGH</span> | |
| <span class="annotation tr" id="annoTR">METHYLATION · AGED</span> | |
| <span class="annotation bl" id="annoBL">IDENTITY · INTACT</span> | |
| <span class="annotation br" id="annoBR">RISK · LOW</span> | |
| <svg class="cell-svg" id="cellSvg" viewBox="0 0 800 480" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"> | |
| <defs> | |
| <!-- Gradients --> | |
| <radialGradient id="membraneGrad" cx="50%" cy="50%" r="50%"> | |
| <stop offset="0%" stop-color="#0ea5a4" stop-opacity="0.15"/> | |
| <stop offset="80%" stop-color="#0f766e" stop-opacity="0.4"/> | |
| <stop offset="100%" stop-color="#0a1428" stop-opacity="0"/> | |
| </radialGradient> | |
| <radialGradient id="nucleusGrad" cx="50%" cy="50%" r="50%"> | |
| <stop offset="0%" stop-color="#14d6d3" stop-opacity="0.9"/> | |
| <stop offset="60%" stop-color="#0ea5a4" stop-opacity="0.7"/> | |
| <stop offset="100%" stop-color="#0f766e" stop-opacity="0.4"/> | |
| </radialGradient> | |
| <radialGradient id="stemGrad" cx="50%" cy="50%" r="50%"> | |
| <stop offset="0%" stop-color="#fcd34d" stop-opacity="1"/> | |
| <stop offset="50%" stop-color="#f59e0b" stop-opacity="0.85"/> | |
| <stop offset="100%" stop-color="#b45309" stop-opacity="0.4"/> | |
| </radialGradient> | |
| <radialGradient id="dangerGrad" cx="50%" cy="50%" r="50%"> | |
| <stop offset="0%" stop-color="#fca5a5" stop-opacity="1"/> | |
| <stop offset="50%" stop-color="#ef4444" stop-opacity="0.85"/> | |
| <stop offset="100%" stop-color="#7f1d1d" stop-opacity="0.4"/> | |
| </radialGradient> | |
| <!-- soft glow filter --> | |
| <filter id="glow" x="-50%" y="-50%" width="200%" height="200%"> | |
| <feGaussianBlur stdDeviation="6" result="blur"/> | |
| <feMerge> | |
| <feMergeNode in="blur"/> | |
| <feMergeNode in="SourceGraphic"/> | |
| </feMerge> | |
| </filter> | |
| <filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%"> | |
| <feGaussianBlur stdDeviation="3" result="blur"/> | |
| <feMerge> | |
| <feMergeNode in="blur"/> | |
| <feMergeNode in="SourceGraphic"/> | |
| </feMerge> | |
| </filter> | |
| </defs> | |
| <!-- Tissue-specific context behind the cell --> | |
| <g id="tissueContext" opacity="1"></g> | |
| <!-- Outer glow halo (changes color with state) --> | |
| <circle id="halo" cx="400" cy="240" r="190" | |
| fill="url(#membraneGrad)" opacity="0.6"/> | |
| <!-- Cell membrane --> | |
| <path id="membrane" d="" | |
| fill="rgba(14, 165, 164, 0.055)" | |
| stroke="#0ea5a4" | |
| stroke-width="2" | |
| stroke-linejoin="round" | |
| opacity="0.78"/> | |
| <!-- ============================================ | |
| Cell-shape extensions (specific to cell type) | |
| ============================================ --> | |
| <g id="cellShape" opacity="1"> | |
| <!-- Filled in by JS based on cell type --> | |
| </g> | |
| <!-- Mitochondria (organelles) - get healthier as we rejuvenate --> | |
| <g id="organelles"> | |
| <!-- Filled in by JS --> | |
| </g> | |
| <!-- Epigenetic damage / "scratches" - fade as we rejuvenate --> | |
| <g id="damage"> | |
| <!-- Filled in by JS --> | |
| </g> | |
| <!-- Nucleus --> | |
| <g id="nucleusGroup"> | |
| <circle id="nucleus" cx="400" cy="240" r="50" | |
| fill="url(#nucleusGrad)" | |
| filter="url(#glow)" | |
| opacity="0.9"/> | |
| </g> | |
| <!-- DNA / chromatin inside nucleus --> | |
| <g id="chromatin"></g> | |
| <!-- Yamanaka factor sparkles --> | |
| <g id="factors"></g> | |
| <!-- Warning corona (only shown near pluripotency) --> | |
| <circle id="warningCorona" cx="400" cy="240" r="200" | |
| fill="none" | |
| stroke="#ef4444" | |
| stroke-width="1" | |
| stroke-dasharray="4 6" | |
| opacity="0"/> | |
| </svg> | |
| </div> | |
| </div> | |
| <!-- READOUT PANEL --> | |
| <aside class="readout"> | |
| <div class="readout-card" id="cardAge"> | |
| <div class="label">Biological age</div> | |
| <div class="value"><span id="ageValue">72</span><span class="unit">years</span></div> | |
| <div class="gauge-track"><div class="gauge-fill" id="ageGauge"></div></div> | |
| <div class="desc">Estimated from epigenetic methylation patterns (Horvath-style clock).</div> | |
| </div> | |
| <div class="readout-card" id="cardIdentity"> | |
| <div class="label">Cell identity preserved</div> | |
| <div class="value"><span id="identityValue">100</span><span class="unit">%</span></div> | |
| <div class="gauge-track"><div class="gauge-fill" id="identityGauge"></div></div> | |
| <div class="desc">How much the cell still functions as itself — retinal ganglion, cortical neuron, dermal fibroblast.</div> | |
| </div> | |
| <div class="readout-card" id="cardRisk"> | |
| <div class="label">Tumorigenic risk</div> | |
| <div class="value"><span id="riskValue">Low</span></div> | |
| <div class="gauge-track"><div class="gauge-fill" id="riskGauge"></div></div> | |
| <div class="desc">Probability of uncontrolled proliferation. Climbs steeply as cells approach pluripotency.</div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- ===== SLIDER SECTION ===== --> | |
| <div class="slider-section"> | |
| <div class="cell-type-section"> | |
| <div class="label">Cell type</div> | |
| <div class="cell-type-buttons" role="tablist"> | |
| <button class="cell-type-btn active" data-type="retinal">Retinal Ganglion</button> | |
| <button class="cell-type-btn" data-type="neuron">Neuron</button> | |
| <button class="cell-type-btn" data-type="skin">Skin Fibroblast</button> | |
| </div> | |
| </div> | |
| <div class="slider-header"> | |
| <div class="slider-title">Reprogramming Intensity → OSK Exposure Duration</div> | |
| <div class="slider-percent"><span id="sliderPct">0</span>%</div> | |
| </div> | |
| <div class="zone-bar"></div> | |
| <input type="range" class="slider-input" id="slider" min="0" max="100" value="0" step="0.5"/> | |
| <div class="tick-row"> | |
| <span class="tick major" style="left: 0%"></span> | |
| <span class="tick" style="left: 10%"></span> | |
| <span class="tick" style="left: 20%"></span> | |
| <span class="tick major" style="left: 38%"></span> | |
| <span class="tick" style="left: 50%"></span> | |
| <span class="tick major" style="left: 65%"></span> | |
| <span class="tick" style="left: 75%"></span> | |
| <span class="tick" style="left: 85%"></span> | |
| <span class="tick major" style="left: 100%"></span> | |
| </div> | |
| <div class="zone-labels"> | |
| <div class="zone-label aged" style="left: 8%"><div class="name">Aged Cell</div><div class="pct">0–18%</div></div> | |
| <div class="zone-label sweet" style="left: 51.5%"><div class="name">Sweet Spot</div><div class="pct">38–65%</div></div> | |
| <div class="zone-label stem" style="left: 80%"><div class="name">Pluripotent</div><div class="pct">75–90%</div></div> | |
| <div class="zone-label cancer" style="left: 96%"><div class="name">Tumor Risk</div><div class="pct">90–100%</div></div> | |
| </div> | |
| <div class="status-banner aged" id="statusBanner"> | |
| <div class="status-icon" id="statusIcon">!</div> | |
| <div><span class="title" id="statusTitle">AGED CELL</span><span id="statusDesc">Drifted methylation, reduced function. The starting point for therapy.</span></div> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| <span class="em">PARTIAL REPROGRAMMING</span> · OSK CASCADES EPIGENETIC RESET WITHOUT ERASING CELL IDENTITY · BASED ON LU ET AL., NATURE 2020 | |
| </div> | |
| </div> | |
| <script> | |
| // ============================================================ | |
| // STATE | |
| // ============================================================ | |
| const state = { | |
| intensity: 0, // 0 - 100 | |
| cellType: 'retinal', // retinal | neuron | skin | |
| }; | |
| // ============================================================ | |
| // LERP & UTILITIES | |
| // ============================================================ | |
| const lerp = (a, b, t) => a + (b - a) * t; | |
| const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); | |
| const smoothstep = (e0, e1, x) => { | |
| const t = clamp((x - e0) / (e1 - e0), 0, 1); | |
| return t * t * (3 - 2 * t); | |
| }; | |
| // ============================================================ | |
| // DERIVED METRICS — what the audience sees | |
| // ============================================================ | |
| function computeMetrics(intensity) { | |
| const t = intensity / 100; | |
| // BIOLOGICAL AGE: starts at 72 years (aged). Drops sharply through sweet spot, | |
| // bottoms out near pluripotency at "0 years" (embryonic-equivalent). | |
| // Mathematically: age decreases monotonically. | |
| const startAge = 72; | |
| const minAge = 0; | |
| const ageRaw = startAge * Math.pow(1 - t, 1.6); | |
| const age = Math.round(ageRaw); | |
| // CELL IDENTITY PRESERVED: 100% at 0, stays high through sweet spot, | |
| // collapses sharply as we approach full reprogramming. | |
| // Use a smoothstep that holds high then drops between 65% and 90%. | |
| const identityHold = 1 - smoothstep(0.55, 0.92, t); | |
| const identity = Math.round(identityHold * 100); | |
| // TUMORIGENIC RISK: stays low through aged & sweet spot, ramps up sharply | |
| // as we cross into pluripotency, peaks at 100. | |
| // Use smoothstep starting at 0.6, fully at 1.0. | |
| const riskRaw = smoothstep(0.55, 1.0, t); | |
| const risk = Math.round(riskRaw * 100); | |
| let riskLabel; | |
| if (risk < 15) riskLabel = "Low"; | |
| else if (risk < 40) riskLabel = "Moderate"; | |
| else if (risk < 70) riskLabel = "Elevated"; | |
| else if (risk < 90) riskLabel = "High"; | |
| else riskLabel = "Critical"; | |
| // ZONE classification - drives banner + colors | |
| let zone; | |
| if (t < 0.18) zone = 'aged'; | |
| else if (t < 0.38) zone = 'transitioning'; | |
| else if (t <= 0.65) zone = 'sweet'; | |
| else if (t <= 0.78) zone = 'overshoot'; | |
| else if (t <= 0.92) zone = 'pluripotent'; | |
| else zone = 'cancer'; | |
| return { age, identity, risk, riskLabel, zone, t }; | |
| } | |
| // ============================================================ | |
| // STATUS BANNER CONTENT | |
| // ============================================================ | |
| const ZONE_BANNERS = { | |
| aged: { | |
| cls: 'aged', icon: '!', title: 'AGED CELL', | |
| desc: 'Drifted methylation, dysfunctional organelles. Functional but declining.' | |
| }, | |
| transitioning: { | |
| cls: 'aged', icon: '~', title: 'PARTIAL RESET BEGINNING', | |
| desc: 'OSK is acting on the epigenome. Methylation is being rewritten. Hold steady — you\'re approaching the sweet spot.' | |
| }, | |
| sweet: { | |
| cls: 'sweet', icon: '✓', title: 'SWEET SPOT', | |
| desc: 'Younger epigenetic state achieved while cell identity preserved. This is the therapeutic target. ER-100 lives here.' | |
| }, | |
| overshoot: { | |
| cls: 'warn', icon: '!', title: 'IDENTITY ERODING', | |
| desc: 'Cell is starting to forget what it is. Function is degrading. You\'ve gone too far.' | |
| }, | |
| pluripotent: { | |
| cls: 'warn', icon: '!', title: 'APPROACHING PLURIPOTENCY', | |
| desc: 'Cell is becoming stem-cell-like. In a body, this would mean uncontrolled growth.' | |
| }, | |
| cancer: { | |
| cls: 'danger', icon: '✕', title: 'TUMORIGENIC', | |
| desc: 'Full reprogramming achieved. In a living organism, this cell would form a teratoma.' | |
| }, | |
| }; | |
| // ============================================================ | |
| // CELL-TYPE SHAPES — drawn into <g id="cellShape"> | |
| // These are appendages that distinguish cell types. | |
| // They fade out as the cell approaches pluripotency. | |
| // ============================================================ | |
| const CELL_SHAPES = { | |
| retinal: { | |
| label: 'RETINAL GANGLION', | |
| specId: 'RGC-0412', | |
| body: 'M 286 236 C 286 168 339 123 405 126 C 476 129 528 180 525 244 C 522 311 468 354 398 350 C 329 346 286 303 286 236 Z', | |
| context: ` | |
| <g opacity="0.38"> | |
| <path d="M 70 86 C 190 48 315 48 444 84 C 558 115 668 109 760 76" fill="none" stroke="#14d6d3" stroke-width="1" opacity="0.35"/> | |
| <path d="M 64 402 C 185 356 310 358 433 393 C 560 430 660 422 758 388" fill="none" stroke="#14d6d3" stroke-width="1" opacity="0.22"/> | |
| <path d="M 70 128 C 205 97 320 104 438 132 C 555 160 655 150 746 122" fill="none" stroke="#64748b" stroke-width="0.8" opacity="0.22"/> | |
| <path d="M 62 352 C 178 326 303 322 432 350 C 555 376 657 372 748 345" fill="none" stroke="#64748b" stroke-width="0.8" opacity="0.18"/> | |
| </g> | |
| `, | |
| svg: ` | |
| <path d="M 522 236 C 594 226 650 218 717 194" stroke="#0ea5a4" stroke-width="5.5" fill="none" stroke-linecap="round"/> | |
| <path d="M 522 246 C 604 250 658 265 727 292" stroke="#0f766e" stroke-width="3.4" fill="none" stroke-linecap="round" opacity="0.85"/> | |
| <path d="M 535 238 C 620 240 676 240 752 232" stroke="#14d6d3" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.7"/> | |
| <circle cx="717" cy="194" r="6" fill="#14d6d3" filter="url(#softGlow)"/> | |
| <circle cx="727" cy="292" r="4.5" fill="#14d6d3" opacity="0.85"/> | |
| <circle cx="752" cy="232" r="3.5" fill="#fcd34d" opacity="0.8"/> | |
| <path d="M 292 220 C 245 192 207 164 151 134" stroke="#0ea5a4" stroke-width="3" fill="none" stroke-linecap="round"/> | |
| <path d="M 291 235 C 235 230 188 228 126 238" stroke="#0ea5a4" stroke-width="2.7" fill="none" stroke-linecap="round"/> | |
| <path d="M 299 258 C 251 291 213 316 162 351" stroke="#0ea5a4" stroke-width="2.5" fill="none" stroke-linecap="round"/> | |
| <path d="M 314 196 C 282 159 267 132 245 91" stroke="#14d6d3" stroke-width="1.8" fill="none" stroke-linecap="round" opacity="0.8"/> | |
| <path d="M 318 286 C 290 331 276 358 252 398" stroke="#14d6d3" stroke-width="1.8" fill="none" stroke-linecap="round" opacity="0.75"/> | |
| <circle cx="151" cy="134" r="4" fill="#14d6d3"/> | |
| <circle cx="126" cy="238" r="3.5" fill="#14d6d3"/> | |
| <circle cx="162" cy="351" r="3.5" fill="#14d6d3"/> | |
| ` | |
| }, | |
| neuron: { | |
| label: 'CORTICAL NEURON', | |
| specId: 'CTX-2209', | |
| body: 'M 285 246 C 275 179 326 124 397 125 C 471 126 530 176 529 241 C 528 309 468 359 394 354 C 326 349 294 307 285 246 Z', | |
| context: ` | |
| <g opacity="0.3"> | |
| <path d="M 72 92 C 176 124 252 122 350 95 C 466 62 572 69 734 120" fill="none" stroke="#64748b" stroke-width="1" opacity="0.28"/> | |
| <path d="M 90 385 C 170 332 262 316 365 342 C 486 372 588 348 724 299" fill="none" stroke="#64748b" stroke-width="1" opacity="0.24"/> | |
| <circle cx="144" cy="112" r="2.2" fill="#14d6d3" opacity="0.45"/> | |
| <circle cx="650" cy="102" r="2.2" fill="#14d6d3" opacity="0.35"/> | |
| <circle cx="684" cy="330" r="2.2" fill="#14d6d3" opacity="0.4"/> | |
| <circle cx="196" cy="344" r="2.2" fill="#14d6d3" opacity="0.32"/> | |
| </g> | |
| `, | |
| svg: ` | |
| <path d="M 524 242 C 596 243 660 245 739 242" stroke="#0ea5a4" stroke-width="4.6" fill="none" stroke-linecap="round"/> | |
| <path d="M 739 242 L 764 226 M 739 242 L 766 242 M 739 242 L 764 259" stroke="#14d6d3" stroke-width="2.2" fill="none" stroke-linecap="round"/> | |
| <path d="M 291 218 C 245 177 203 139 133 105 C 109 93 88 94 73 106" stroke="#0ea5a4" stroke-width="3" fill="none" stroke-linecap="round"/> | |
| <path d="M 286 236 C 225 218 167 202 96 217" stroke="#0ea5a4" stroke-width="2.8" fill="none" stroke-linecap="round"/> | |
| <path d="M 297 261 C 242 303 194 337 119 366" stroke="#0ea5a4" stroke-width="2.7" fill="none" stroke-linecap="round"/> | |
| <path d="M 317 190 C 288 145 276 111 263 64" stroke="#14d6d3" stroke-width="1.8" fill="none" stroke-linecap="round" opacity="0.8"/> | |
| <path d="M 326 288 C 286 337 267 373 247 424" stroke="#14d6d3" stroke-width="1.8" fill="none" stroke-linecap="round" opacity="0.8"/> | |
| <path d="M 215 159 C 181 151 154 153 122 166 M 203 308 C 163 312 135 325 105 350 M 198 217 C 170 195 146 187 113 186" stroke="#0ea5a4" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.72"/> | |
| <circle cx="73" cy="106" r="3.5" fill="#14d6d3"/> | |
| <circle cx="96" cy="217" r="3.5" fill="#14d6d3"/> | |
| <circle cx="119" cy="366" r="3.5" fill="#14d6d3"/> | |
| <circle cx="764" cy="226" r="2.8" fill="#fcd34d"/> | |
| <circle cx="766" cy="242" r="2.8" fill="#fcd34d"/> | |
| <circle cx="764" cy="259" r="2.8" fill="#fcd34d"/> | |
| ` | |
| }, | |
| skin: { | |
| label: 'DERMAL FIBROBLAST', | |
| specId: 'FB-7781', | |
| body: 'M 124 238 C 200 197 294 164 396 166 C 504 168 604 196 678 238 C 606 282 505 313 395 313 C 291 313 194 282 124 238 Z', | |
| context: ` | |
| <g opacity="0.36"> | |
| <path d="M 42 118 C 151 82 258 92 357 126 C 455 160 561 165 758 112" fill="none" stroke="#fcd34d" stroke-width="1.4" opacity="0.24"/> | |
| <path d="M 50 165 C 164 136 248 145 354 180 C 456 215 566 214 748 168" fill="none" stroke="#14d6d3" stroke-width="1" opacity="0.18"/> | |
| <path d="M 42 334 C 155 300 258 309 360 342 C 466 376 576 377 758 323" fill="none" stroke="#fcd34d" stroke-width="1.4" opacity="0.25"/> | |
| <path d="M 66 382 C 176 355 278 360 377 390 C 486 423 598 420 736 382" fill="none" stroke="#14d6d3" stroke-width="1" opacity="0.18"/> | |
| <path d="M 82 88 L 738 397 M 70 401 L 735 83" stroke="#64748b" stroke-width="0.65" opacity="0.16"/> | |
| </g> | |
| `, | |
| svg: ` | |
| <path d="M 125 238 C 86 223 58 205 36 183" stroke="#0ea5a4" stroke-width="2.6" fill="none" stroke-linecap="round"/> | |
| <path d="M 125 238 C 82 253 55 272 31 298" stroke="#0ea5a4" stroke-width="2.6" fill="none" stroke-linecap="round"/> | |
| <path d="M 675 238 C 716 220 743 202 771 176" stroke="#0ea5a4" stroke-width="2.6" fill="none" stroke-linecap="round"/> | |
| <path d="M 675 238 C 716 255 744 276 770 303" stroke="#0ea5a4" stroke-width="2.6" fill="none" stroke-linecap="round"/> | |
| <path d="M 205 222 C 276 197 331 190 405 191 C 489 192 546 207 606 230" stroke="#14d6d3" stroke-width="1.3" fill="none" opacity="0.42"/> | |
| <path d="M 205 257 C 280 283 341 290 419 286 C 499 282 555 266 606 245" stroke="#14d6d3" stroke-width="1.3" fill="none" opacity="0.38"/> | |
| <ellipse cx="400" cy="240" rx="118" ry="42" fill="none" stroke="#0f766e" stroke-width="1.2" opacity="0.35"/> | |
| ` | |
| } | |
| }; | |
| function stemLikeBodyPath(t) { | |
| const r = lerp(158, 122, smoothstep(0.6, 0.95, t)); | |
| return ` | |
| M ${400 - r} 240 | |
| C ${400 - r} ${240 - r * 0.58}, ${400 - r * 0.58} ${240 - r}, 400 ${240 - r} | |
| C ${400 + r * 0.58} ${240 - r}, ${400 + r} ${240 - r * 0.58}, ${400 + r} 240 | |
| C ${400 + r} ${240 + r * 0.58}, ${400 + r * 0.58} ${240 + r}, 400 ${240 + r} | |
| C ${400 - r * 0.58} ${240 + r}, ${400 - r} ${240 + r * 0.58}, ${400 - r} 240 Z | |
| `; | |
| } | |
| function membranePathFor(type, t) { | |
| return t < 0.72 ? CELL_SHAPES[type].body : stemLikeBodyPath(t); | |
| } | |
| function renderTissueContext(type, intensity) { | |
| const t = intensity / 100; | |
| const identityOpacity = 1 - smoothstep(0.55, 0.92, t); | |
| const stemFogOpacity = smoothstep(0.68, 0.95, t) * 0.55; | |
| return ` | |
| <g opacity="${identityOpacity.toFixed(3)}">${CELL_SHAPES[type].context}</g> | |
| <g opacity="${stemFogOpacity.toFixed(3)}"> | |
| <circle cx="358" cy="197" r="18" fill="#f59e0b" opacity="0.22" filter="url(#softGlow)"/> | |
| <circle cx="448" cy="194" r="22" fill="#fcd34d" opacity="0.18" filter="url(#softGlow)"/> | |
| <circle cx="487" cy="281" r="16" fill="#f59e0b" opacity="0.16" filter="url(#softGlow)"/> | |
| <circle cx="314" cy="282" r="16" fill="#fcd34d" opacity="0.14" filter="url(#softGlow)"/> | |
| </g> | |
| `; | |
| } | |
| // ============================================================ | |
| // ORGANELLES — fade in / out and "heal" with rejuvenation | |
| // ============================================================ | |
| function renderOrganelles(intensity) { | |
| const t = intensity / 100; | |
| // Fade out as cell becomes stem-cell-like (homogenous interior) | |
| const orgOpacity = 1 - smoothstep(0.55, 0.85, t); | |
| // Health: aged cells have shriveled, dim mitochondria; healthy cells have plump, bright ones | |
| const health = clamp(t * 1.8, 0, 1) * (1 - smoothstep(0.6, 0.85, t)); | |
| const mitoSize = lerp(4, 8, health); | |
| const mitoBright = lerp(0.3, 0.95, health); | |
| // Mitochondria scattered in cytoplasm | |
| const mitos = [ | |
| { x: 320, y: 200 }, { x: 470, y: 195 }, { x: 340, y: 290 }, | |
| { x: 460, y: 295 }, { x: 305, y: 245 }, { x: 495, y: 245 }, | |
| { x: 380, y: 180 }, { x: 420, y: 305 }, | |
| ]; | |
| return mitos.map((m, i) => { | |
| const angle = (i % 2 ? -18 : 22); | |
| return ` | |
| <g transform="rotate(${angle} ${m.x} ${m.y})" opacity="${orgOpacity * mitoBright}"> | |
| <ellipse cx="${m.x}" cy="${m.y}" rx="${mitoSize * 1.35}" ry="${mitoSize * 0.72}" | |
| fill="#fcd34d" filter="url(#softGlow)"/> | |
| <path d="M ${m.x - mitoSize * 0.72} ${m.y} C ${m.x - mitoSize * 0.28} ${m.y - mitoSize * 0.38}, ${m.x + mitoSize * 0.22} ${m.y + mitoSize * 0.38}, ${m.x + mitoSize * 0.72} ${m.y}" | |
| stroke="#7c2d12" stroke-width="0.8" fill="none" opacity="${health * 0.6}"/> | |
| </g>`; | |
| }).join(''); | |
| } | |
| // ============================================================ | |
| // EPIGENETIC DAMAGE - "scratches" | |
| // Heavy at low intensity (aged), fades through sweet spot | |
| // ============================================================ | |
| function renderDamage(intensity) { | |
| const t = intensity / 100; | |
| // Damage starts high, fades as we rejuvenate | |
| const damageOpacity = (1 - smoothstep(0.0, 0.5, t)) * 0.6; | |
| if (damageOpacity < 0.02) return ''; | |
| // Use deterministic "noise" — small lines & dots scattered throughout cytoplasm | |
| const seeds = [ | |
| { x: 295, y: 175, r: 18, a: 35 }, { x: 478, y: 178, r: 14, a: -28 }, | |
| { x: 320, y: 320, r: 16, a: 50 }, { x: 470, y: 325, r: 12, a: -45 }, | |
| { x: 270, y: 250, r: 20, a: 90 }, { x: 528, y: 255, r: 18, a: -90 }, | |
| { x: 350, y: 175, r: 10, a: 15 }, { x: 440, y: 320, r: 14, a: -20 }, | |
| { x: 285, y: 215, r: 9, a: 60 }, { x: 510, y: 285, r: 11, a: -65 }, | |
| ]; | |
| const dots = [ | |
| { x: 305, y: 200 }, { x: 488, y: 215 }, { x: 348, y: 305 }, | |
| { x: 440, y: 310 }, { x: 285, y: 280 }, { x: 510, y: 215 }, | |
| { x: 370, y: 165 }, { x: 425, y: 320 }, | |
| ]; | |
| let svg = ''; | |
| seeds.forEach(s => { | |
| const x2 = s.x + s.r * Math.cos(s.a * Math.PI / 180); | |
| const y2 = s.y + s.r * Math.sin(s.a * Math.PI / 180); | |
| svg += `<line x1="${s.x}" y1="${s.y}" x2="${x2}" y2="${y2}" | |
| stroke="#ef4444" stroke-width="1.5" opacity="${damageOpacity}"/>`; | |
| }); | |
| dots.forEach(d => { | |
| svg += `<circle cx="${d.x}" cy="${d.y}" r="1.5" fill="#fca5a5" opacity="${damageOpacity * 0.8}"/>`; | |
| }); | |
| return svg; | |
| } | |
| // ============================================================ | |
| // CHROMATIN inside nucleus | |
| // ============================================================ | |
| function renderChromatin(intensity) { | |
| const t = intensity / 100; | |
| // At low t: messy, scattered (drift). At high t (stem): smooth, glowing | |
| const messiness = (1 - smoothstep(0.3, 0.9, t)); | |
| const opacity = lerp(0.4, 0.7, messiness); | |
| const cx = 400, cy = 240; | |
| let svg = ''; | |
| // Concentric arcs representing chromatin loops | |
| for (let i = 0; i < 5; i++) { | |
| const r = 18 + i * 6; | |
| const offsetA = messiness * (Math.random() - 0.5) * 0; // deterministic for now, no jitter | |
| svg += `<circle cx="${cx}" cy="${cy}" r="${r}" | |
| fill="none" stroke="#14d6d3" | |
| stroke-width="0.7" opacity="${opacity * (1 - i * 0.15)}"/>`; | |
| } | |
| return svg; | |
| } | |
| // ============================================================ | |
| // YAMANAKA FACTOR SPARKLES — visible during reprogramming | |
| // ============================================================ | |
| function renderFactors(intensity) { | |
| const t = intensity / 100; | |
| // Peaks during active reprogramming (sweet spot region) | |
| const factorVisibility = Math.sin(t * Math.PI) * 0.9; | |
| if (factorVisibility < 0.05) return ''; | |
| const positions = [ | |
| { x: 260, y: 130 }, { x: 540, y: 130 }, { x: 260, y: 350 }, { x: 540, y: 350 }, | |
| { x: 200, y: 240 }, { x: 600, y: 240 }, { x: 400, y: 100 }, { x: 400, y: 380 }, | |
| { x: 320, y: 145 }, { x: 480, y: 335 }, | |
| ]; | |
| return positions.map((p, i) => { | |
| const seed = i * 0.35; | |
| const phase = (t + seed) % 1; | |
| const localOpacity = factorVisibility * (0.5 + 0.5 * Math.sin(phase * Math.PI * 2)); | |
| return `<g opacity="${localOpacity}"> | |
| <circle cx="${p.x}" cy="${p.y}" r="2" fill="#fcd34d" filter="url(#softGlow)"/> | |
| <circle cx="${p.x}" cy="${p.y}" r="5" fill="none" stroke="#fcd34d" stroke-width="0.5" opacity="0.6"/> | |
| </g>`; | |
| }).join(''); | |
| } | |
| // ============================================================ | |
| // RENDER LOOP | |
| // ============================================================ | |
| function render() { | |
| const m = computeMetrics(state.intensity); | |
| const t = m.t; | |
| // ---- cell shape (cell type) — fades out as we approach pluripotency | |
| const cellShape = document.getElementById('cellShape'); | |
| const shapeOpacity = 1 - smoothstep(0.55, 0.92, t); | |
| cellShape.innerHTML = CELL_SHAPES[state.cellType].svg; | |
| cellShape.setAttribute('opacity', shapeOpacity.toFixed(3)); | |
| document.getElementById('specimenType').textContent = CELL_SHAPES[state.cellType].label; | |
| document.getElementById('specId').textContent = CELL_SHAPES[state.cellType].specId; | |
| document.getElementById('tissueContext').innerHTML = renderTissueContext(state.cellType, state.intensity); | |
| // ---- membrane: specimen-specific morphology collapses into a round stem-like body | |
| const membrane = document.getElementById('membrane'); | |
| membrane.setAttribute('d', membranePathFor(state.cellType, t)); | |
| membrane.setAttribute('stroke', t > 0.92 ? '#ef4444' : (t > 0.7 ? '#f59e0b' : '#0ea5a4')); | |
| membrane.setAttribute('fill', t > 0.92 ? 'rgba(239, 68, 68, 0.09)' : (t > 0.7 ? 'rgba(245, 158, 11, 0.10)' : 'rgba(14, 165, 164, 0.055)')); | |
| // ---- nucleus: grows as cell approaches pluripotency, color shifts | |
| const nucleusR = lerp(50, 95, smoothstep(0.6, 0.95, t)); | |
| const nucleus = document.getElementById('nucleus'); | |
| nucleus.setAttribute('r', nucleusR); | |
| // Gradient transition: teal nucleus at low/mid intensity, amber stem at high, red at extreme | |
| let nucleusFill = 'url(#nucleusGrad)'; | |
| if (t > 0.92) nucleusFill = 'url(#dangerGrad)'; | |
| else if (t > 0.7) nucleusFill = 'url(#stemGrad)'; | |
| nucleus.setAttribute('fill', nucleusFill); | |
| // ---- halo: glows brighter through sweet spot, turns amber/red toward end | |
| const halo = document.getElementById('halo'); | |
| let haloOpacity = 0.4 + 0.5 * Math.sin(t * Math.PI); | |
| let haloFill = 'url(#membraneGrad)'; | |
| if (t > 0.92) haloFill = 'url(#dangerGrad)'; | |
| else if (t > 0.7) haloFill = 'url(#stemGrad)'; | |
| halo.setAttribute('opacity', haloOpacity.toFixed(2)); | |
| halo.setAttribute('fill', haloFill); | |
| // ---- organelles | |
| document.getElementById('organelles').innerHTML = renderOrganelles(state.intensity); | |
| // ---- damage | |
| document.getElementById('damage').innerHTML = renderDamage(state.intensity); | |
| // ---- chromatin | |
| document.getElementById('chromatin').innerHTML = renderChromatin(state.intensity); | |
| // ---- yamanaka factors | |
| document.getElementById('factors').innerHTML = renderFactors(state.intensity); | |
| // ---- warning corona | |
| const wc = document.getElementById('warningCorona'); | |
| const warnOpacity = smoothstep(0.78, 0.95, t); | |
| wc.setAttribute('opacity', warnOpacity.toFixed(2)); | |
| wc.setAttribute('r', 175 + warnOpacity * 25); | |
| // ---- side readouts | |
| document.getElementById('ageValue').textContent = m.age; | |
| document.getElementById('identityValue').textContent = m.identity; | |
| document.getElementById('riskValue').textContent = m.riskLabel; | |
| // gauges | |
| const ageFraction = (72 - m.age) / 72; // how much progress in age reduction | |
| setGauge('ageGauge', ageFraction, m.age < 5 ? 'danger' : (ageFraction > 0.4 && ageFraction < 0.85 ? 'ok' : 'warn'), 'cardAge'); | |
| setGauge('identityGauge', m.identity / 100, m.identity > 70 ? 'ok' : (m.identity > 30 ? 'warn' : 'danger'), 'cardIdentity'); | |
| setGauge('riskGauge', m.risk / 100, m.risk < 15 ? 'ok' : (m.risk < 60 ? 'warn' : 'danger'), 'cardRisk'); | |
| // ---- slider % | |
| document.getElementById('sliderPct').textContent = state.intensity.toFixed(0); | |
| // ---- status banner | |
| const banner = document.getElementById('statusBanner'); | |
| const cfg = ZONE_BANNERS[m.zone]; | |
| banner.className = 'status-banner ' + cfg.cls; | |
| document.getElementById('statusIcon').textContent = cfg.icon; | |
| document.getElementById('statusTitle').textContent = cfg.title; | |
| document.getElementById('statusDesc').textContent = cfg.desc; | |
| // ---- annotations on viewer | |
| const annoTL = document.getElementById('annoTL'); | |
| const annoTR = document.getElementById('annoTR'); | |
| const annoBL = document.getElementById('annoBL'); | |
| const annoBR = document.getElementById('annoBR'); | |
| annoTL.textContent = `EPI-DRIFT · ${t < 0.2 ? 'HIGH' : t < 0.5 ? 'RESETTING' : t < 0.7 ? 'YOUTHFUL' : 'ERASED'}`; | |
| annoTR.textContent = `METHYLATION · ${t < 0.2 ? 'AGED' : t < 0.65 ? 'YOUTHFUL' : t < 0.9 ? 'EMBRYONIC' : 'BLANK'}`; | |
| annoBL.textContent = `IDENTITY · ${m.identity > 80 ? 'INTACT' : m.identity > 40 ? 'WAVERING' : 'LOST'}`; | |
| annoBR.textContent = `RISK · ${m.riskLabel.toUpperCase()}`; | |
| } | |
| function setGauge(gaugeId, fraction, klass, cardId) { | |
| const el = document.getElementById(gaugeId); | |
| el.style.transform = `scaleX(${clamp(fraction, 0, 1)})`; | |
| const card = document.getElementById(cardId); | |
| card.classList.remove('ok', 'warn', 'danger'); | |
| card.classList.add(klass); | |
| } | |
| // ============================================================ | |
| // EVENT WIRING | |
| // ============================================================ | |
| const slider = document.getElementById('slider'); | |
| slider.addEventListener('input', (e) => { | |
| state.intensity = parseFloat(e.target.value); | |
| render(); | |
| }); | |
| document.querySelectorAll('.cell-type-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.cell-type-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.cellType = btn.dataset.type; | |
| render(); | |
| }); | |
| }); | |
| function applyHashPreset() { | |
| const raw = window.location.hash.replace('#', '').trim(); | |
| if (!raw) return; | |
| const [type, value] = raw.split(':'); | |
| if (CELL_SHAPES[type]) { | |
| state.cellType = type; | |
| document.querySelectorAll('.cell-type-btn').forEach(b => { | |
| b.classList.toggle('active', b.dataset.type === type); | |
| }); | |
| } | |
| const parsed = Number(value); | |
| if (Number.isFinite(parsed)) { | |
| state.intensity = clamp(parsed, 0, 100); | |
| slider.value = state.intensity; | |
| } | |
| } | |
| // Initial render | |
| applyHashPreset(); | |
| render(); | |
| </script> | |
| </body> | |
| </html> | |
Xet Storage Details
- Size:
- 47.8 kB
- Xet hash:
- e6c6fd623b0742a3d60dbe4a2b7caab280d11cc2075a76789e3ee1c65881c9ea
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.