Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MCMC_WALKER.exe // NEURAL SAMPLER v2.077</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap'); | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #0a0a0f; | |
| color: #eee; | |
| font-family: 'Share Tech Mono', monospace; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| position: relative; | |
| } | |
| /* Scanline overlay */ | |
| body::before { | |
| content: ""; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| rgba(0, 0, 0, 0.15), | |
| rgba(0, 0, 0, 0.15) 1px, | |
| transparent 1px, | |
| transparent 2px | |
| ); | |
| pointer-events: none; | |
| z-index: 1000; | |
| } | |
| /* Grid background */ | |
| body::after { | |
| content: ""; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: | |
| linear-gradient(rgba(255, 0, 128, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(255, 0, 128, 0.03) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| pointer-events: none; | |
| z-index: -1; | |
| } | |
| h1 { | |
| margin: 0; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 900; | |
| font-size: 1.5rem; | |
| letter-spacing: 4px; | |
| text-transform: uppercase; | |
| background: linear-gradient(90deg, #ff0080, #00ffff, #ff0080); | |
| background-size: 200% auto; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| animation: gradient-shift 3s linear infinite; | |
| text-shadow: 0 0 30px rgba(255, 0, 128, 0.5); | |
| } | |
| @keyframes gradient-shift { | |
| 0% { background-position: 0% center; } | |
| 100% { background-position: 200% center; } | |
| } | |
| .subtitle { | |
| color: #00ffff; | |
| font-size: 0.75rem; | |
| margin-bottom: 10px; | |
| letter-spacing: 3px; | |
| opacity: 0.7; | |
| } | |
| /* Info Banner */ | |
| .info-banner { | |
| width: 100%; | |
| max-width: 1000px; | |
| background: linear-gradient(90deg, rgba(0, 255, 255, 0.1), rgba(255, 0, 128, 0.1)); | |
| border: 1px solid rgba(0, 255, 255, 0.3); | |
| padding: 12px 20px; | |
| margin-bottom: 15px; | |
| font-size: 0.8rem; | |
| color: #aaa; | |
| line-height: 1.5; | |
| position: relative; | |
| } | |
| .info-banner::before { | |
| content: "◈ PROTOCOL"; | |
| position: absolute; | |
| top: -8px; | |
| left: 15px; | |
| background: #0a0a0f; | |
| padding: 0 8px; | |
| font-size: 0.6rem; | |
| color: #ff0080; | |
| letter-spacing: 2px; | |
| } | |
| .info-banner strong { | |
| color: #00ffff; | |
| } | |
| .info-banner em { | |
| color: #ff0080; | |
| font-style: normal; | |
| } | |
| .container { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| width: 100%; | |
| max-width: 1000px; | |
| flex-grow: 1; | |
| max-height: 450px; | |
| } | |
| .panel { | |
| background: linear-gradient(135deg, rgba(10, 10, 20, 0.95), rgba(20, 10, 30, 0.95)); | |
| border: 1px solid #ff0080; | |
| border-radius: 0; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| box-shadow: | |
| 0 0 20px rgba(255, 0, 128, 0.3), | |
| inset 0 0 60px rgba(255, 0, 128, 0.05); | |
| clip-path: polygon(0 10px, 10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%); | |
| } | |
| .panel::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent, #ff0080, #00ffff, #ff0080, transparent); | |
| animation: scan-line 2s linear infinite; | |
| } | |
| @keyframes scan-line { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| .panel-header { | |
| padding: 8px 15px; | |
| background: linear-gradient(90deg, rgba(255, 0, 128, 0.2), rgba(0, 255, 255, 0.1)); | |
| border-bottom: 1px solid rgba(255, 0, 128, 0.5); | |
| font-size: 0.75rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .panel-header span:first-child { | |
| color: #ff0080; | |
| text-shadow: 0 0 10px rgba(255, 0, 128, 0.8); | |
| } | |
| /* Panel description overlay */ | |
| .panel-desc { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: linear-gradient(transparent, rgba(0, 0, 0, 0.95)); | |
| padding: 30px 12px 10px 12px; | |
| font-size: 0.65rem; | |
| color: #888; | |
| pointer-events: none; | |
| z-index: 5; | |
| line-height: 1.4; | |
| } | |
| .panel-desc .highlight { | |
| color: #00ffff; | |
| } | |
| .panel-desc .action { | |
| color: #ff0080; | |
| } | |
| canvas { | |
| flex-grow: 1; | |
| width: 100%; | |
| height: 100%; | |
| cursor: crosshair; | |
| touch-action: none; | |
| background: #050508; | |
| } | |
| /* Controls */ | |
| .controls { | |
| margin-top: 15px; | |
| display: flex; | |
| gap: 15px; | |
| background: linear-gradient(135deg, rgba(20, 10, 30, 0.9), rgba(10, 20, 30, 0.9)); | |
| padding: 15px 25px; | |
| border: 1px solid rgba(0, 255, 255, 0.3); | |
| align-items: center; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| position: relative; | |
| clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%, 0 10px); | |
| } | |
| .controls::before { | |
| content: "// CONTROL_MATRIX"; | |
| position: absolute; | |
| top: -10px; | |
| left: 20px; | |
| background: #0a0a0f; | |
| padding: 0 10px; | |
| font-size: 0.65rem; | |
| color: #00ffff; | |
| letter-spacing: 2px; | |
| } | |
| button { | |
| background: linear-gradient(135deg, rgba(255, 0, 128, 0.2), rgba(255, 0, 128, 0.1)); | |
| color: #ff0080; | |
| border: 1px solid #ff0080; | |
| padding: 10px 20px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 0.75rem; | |
| transition: all 0.3s; | |
| font-family: 'Orbitron', sans-serif; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| position: relative; | |
| overflow: hidden; | |
| clip-path: polygon(5px 0, 100% 0, 100% calc(100% - 5px), calc(100% - 5px) 100%, 0 100%, 0 5px); | |
| } | |
| button::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 0, 128, 0.4), transparent); | |
| transition: left 0.5s; | |
| } | |
| button:hover { | |
| background: linear-gradient(135deg, rgba(255, 0, 128, 0.4), rgba(255, 0, 128, 0.2)); | |
| box-shadow: 0 0 20px rgba(255, 0, 128, 0.5), inset 0 0 20px rgba(255, 0, 128, 0.1); | |
| text-shadow: 0 0 10px #ff0080; | |
| } | |
| button:hover::before { | |
| left: 100%; | |
| } | |
| button.primary { | |
| background: linear-gradient(135deg, rgba(0, 255, 255, 0.3), rgba(0, 255, 255, 0.1)); | |
| color: #00ffff; | |
| border-color: #00ffff; | |
| } | |
| button.primary:hover { | |
| background: linear-gradient(135deg, rgba(0, 255, 255, 0.5), rgba(0, 255, 255, 0.2)); | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.5), inset 0 0 20px rgba(0, 255, 255, 0.1); | |
| text-shadow: 0 0 10px #00ffff; | |
| } | |
| button.preset { | |
| padding: 6px 12px; | |
| font-size: 0.6rem; | |
| background: linear-gradient(135deg, rgba(128, 0, 255, 0.2), rgba(128, 0, 255, 0.1)); | |
| color: #a855f7; | |
| border-color: #a855f7; | |
| } | |
| button.preset:hover { | |
| background: linear-gradient(135deg, rgba(128, 0, 255, 0.4), rgba(128, 0, 255, 0.2)); | |
| box-shadow: 0 0 15px rgba(128, 0, 255, 0.5); | |
| } | |
| .stat-val { | |
| color: #00ffff; | |
| font-family: 'Share Tech Mono', monospace; | |
| text-shadow: 0 0 10px rgba(0, 255, 255, 0.8); | |
| } | |
| /* Stats Display */ | |
| #stats-output { | |
| margin-top: 15px; | |
| width: 100%; | |
| max-width: 1000px; | |
| background: linear-gradient(135deg, rgba(10, 10, 20, 0.95), rgba(20, 10, 30, 0.95)); | |
| border: 1px solid #00ffff; | |
| padding: 12px 20px; | |
| font-size: 0.8rem; | |
| color: #ddd; | |
| min-height: 50px; | |
| display: flex; | |
| align-items: center; | |
| gap: 25px; | |
| position: relative; | |
| clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px)); | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.2); | |
| flex-wrap: wrap; | |
| } | |
| #stats-output::before { | |
| content: ""; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| height: 2px; | |
| background: linear-gradient(90deg, #00ffff, #ff0080, #00ffff); | |
| animation: pulse-line 1.5s ease-in-out infinite; | |
| } | |
| @keyframes pulse-line { | |
| 0%, 100% { opacity: 0.5; } | |
| 50% { opacity: 1; } | |
| } | |
| .stats-label { | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: bold; | |
| color: #ff0080; | |
| margin-right: 10px; | |
| font-size: 0.65rem; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| white-space: nowrap; | |
| text-shadow: 0 0 10px rgba(255, 0, 128, 0.8); | |
| } | |
| .stat-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .stat-item span:first-child { | |
| color: #666; | |
| font-size: 0.7rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .stat-item .stat-val { | |
| min-width: 50px; | |
| } | |
| .stat-item.good .stat-val { color: #22c55e; text-shadow: 0 0 10px rgba(34, 197, 94, 0.8); } | |
| .stat-item.warn .stat-val { color: #eab308; text-shadow: 0 0 10px rgba(234, 179, 8, 0.8); } | |
| .stat-item.bad .stat-val { color: #ef4444; text-shadow: 0 0 10px rgba(239, 68, 68, 0.8); } | |
| /* Sliders */ | |
| .slider-group { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 0.6rem; | |
| color: #888; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .slider-group .slider-label { | |
| display: flex; | |
| justify-content: space-between; | |
| width: 100%; | |
| color: #666; | |
| } | |
| .slider-group .slider-value { | |
| color: #00ffff; | |
| font-size: 0.7rem; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| width: 80px; | |
| height: 4px; | |
| background: linear-gradient(90deg, #ff0080, #00ffff); | |
| border-radius: 0; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 12px; | |
| height: 20px; | |
| background: #00ffff; | |
| cursor: pointer; | |
| box-shadow: 0 0 10px #00ffff; | |
| clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 12px; | |
| height: 20px; | |
| background: #00ffff; | |
| cursor: pointer; | |
| border: none; | |
| box-shadow: 0 0 10px #00ffff; | |
| } | |
| .divider { | |
| width: 1px; | |
| height: 30px; | |
| background: linear-gradient(180deg, transparent, #ff0080, transparent); | |
| margin: 0 5px; | |
| } | |
| /* Glitch effect for title on hover */ | |
| h1:hover { | |
| animation: glitch 0.3s infinite; | |
| } | |
| @keyframes glitch { | |
| 0% { transform: translate(0); } | |
| 20% { transform: translate(-2px, 2px); } | |
| 40% { transform: translate(-2px, -2px); } | |
| 60% { transform: translate(2px, 2px); } | |
| 80% { transform: translate(2px, -2px); } | |
| 100% { transform: translate(0); } | |
| } | |
| /* Corner decorations */ | |
| .corner-decor { | |
| position: fixed; | |
| width: 80px; | |
| height: 80px; | |
| pointer-events: none; | |
| opacity: 0.3; | |
| } | |
| .corner-decor.top-left { | |
| top: 10px; | |
| left: 10px; | |
| border-top: 2px solid #ff0080; | |
| border-left: 2px solid #ff0080; | |
| } | |
| .corner-decor.top-right { | |
| top: 10px; | |
| right: 10px; | |
| border-top: 2px solid #00ffff; | |
| border-right: 2px solid #00ffff; | |
| } | |
| .corner-decor.bottom-left { | |
| bottom: 10px; | |
| left: 10px; | |
| border-bottom: 2px solid #00ffff; | |
| border-left: 2px solid #00ffff; | |
| } | |
| .corner-decor.bottom-right { | |
| bottom: 10px; | |
| right: 10px; | |
| border-bottom: 2px solid #ff0080; | |
| border-right: 2px solid #ff0080; | |
| } | |
| /* Tooltip styles */ | |
| .tooltip { | |
| position: relative; | |
| cursor: help; | |
| border-bottom: 1px dotted #666; | |
| } | |
| .tooltip:hover::after { | |
| content: attr(data-tip); | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0, 0, 0, 0.95); | |
| border: 1px solid #00ffff; | |
| padding: 8px 12px; | |
| font-size: 0.7rem; | |
| white-space: nowrap; | |
| z-index: 100; | |
| color: #ddd; | |
| } | |
| @media (max-width: 700px) { | |
| .container { grid-template-columns: 1fr; max-height: none; } | |
| .panel { min-height: 300px; } | |
| #stats-output { flex-wrap: wrap; gap: 15px; } | |
| .controls { gap: 10px; } | |
| .info-banner { font-size: 0.7rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="corner-decor top-left"></div> | |
| <div class="corner-decor top-right"></div> | |
| <div class="corner-decor bottom-left"></div> | |
| <div class="corner-decor bottom-right"></div> | |
| <h1>MCMC_WALKER.exe</h1> | |
| <p class="subtitle">// NEURAL PROBABILITY SAMPLER v2.077</p> | |
| <!-- Info Banner --> | |
| <div class="info-banner"> | |
| <strong>Draw a probability landscape</strong> on the left — <em>brightness = probability</em>. | |
| The walker on the right uses <strong>Metropolis-Hastings</strong> to sample from your distribution. | |
| It visits bright regions more often, reconstructing your drawing through random exploration! | |
| </div> | |
| <div class="container"> | |
| <!-- Input Canvas --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span>◈ Target_Distribution</span> | |
| <div style="display:flex; gap:5px;"> | |
| <button class="preset" onclick="loadPreset('hex')">Hex</button> | |
| <button class="preset" onclick="loadPreset('gaussian')">Gauss</button> | |
| <button class="preset" onclick="loadPreset('rings')">Rings</button> | |
| <button onclick="clearCanvas()" style="padding:4px 10px; font-size:0.6rem;">Purge</button> | |
| </div> | |
| </div> | |
| <canvas id="canvas-target"></canvas> | |
| <div class="panel-desc"> | |
| <span class="action">DRAW</span> to define probability density. | |
| <span class="highlight">Brighter = Higher probability</span>. | |
| The walker will spend more time in bright regions. | |
| </div> | |
| </div> | |
| <!-- Output Simulation --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span>◈ MCMC_Reconstruction</span> | |
| <span class="stat-val" id="step-count">CYCLES: 0</span> | |
| </div> | |
| <canvas id="canvas-sim"></canvas> | |
| <div class="panel-desc"> | |
| <span class="highlight">LIVE SAMPLING</span> — Walker proposes random jumps, | |
| accepts moves to brighter areas, sometimes rejects moves to darker regions. | |
| Trail reveals the underlying distribution! | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats Display --> | |
| <div id="stats-output"> | |
| <span class="stats-label">◈ Telemetry</span> | |
| <div class="stat-item" id="stat-accept"> | |
| <span class="tooltip" data-tip="Ideal: 20-50%. Too high = slow exploration. Too low = stuck.">Accept:</span> | |
| <span class="stat-val" id="accept-rate">0%</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="tooltip" data-tip="Current (X, Y) coordinates of the walker">Position:</span> | |
| <span class="stat-val" id="walker-pos">[0, 0]</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="tooltip" data-tip="Brightness at current position (0-255). Higher = more probable">Density:</span> | |
| <span class="stat-val" id="current-density">0</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="tooltip" data-tip="Estimated time to explore full distribution">Coverage:</span> | |
| <span class="stat-val" id="coverage">0%</span> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="primary" onclick="resetWalker()">↻ Reset</button> | |
| <button onclick="togglePause()"><span id="pause-btn">⏸ Halt</span></button> | |
| <div class="divider"></div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Brush</span> | |
| <span class="slider-value" id="brush-val">20</span> | |
| </div> | |
| <input type="range" id="brush-size" min="5" max="50" value="20"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span class="tooltip" data-tip="Steps per frame — higher = faster sampling">Speed</span> | |
| <span class="slider-value" id="speed-val">50</span> | |
| </div> | |
| <input type="range" id="speed" min="10" max="200" value="50"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span class="tooltip" data-tip="Proposal distance — affects acceptance rate">Jump</span> | |
| <span class="slider-value" id="jump-val">10</span> | |
| </div> | |
| <input type="range" id="jump-size" min="2" max="30" value="10"> | |
| </div> | |
| </div> | |
| <script> | |
| // --- Setup --- | |
| const cvsTarget = document.getElementById('canvas-target'); | |
| const ctxTarget = cvsTarget.getContext('2d', { willReadFrequently: true }); | |
| const cvsSim = document.getElementById('canvas-sim'); | |
| const ctxSim = cvsSim.getContext('2d'); | |
| let width, height; | |
| let isDrawing = false; | |
| let brushSize = 20; | |
| let stepSize = 50; | |
| let jumpSize = 10; | |
| let pixelData = null; | |
| let isPaused = false; | |
| // Walker State | |
| let walker = { x: 0, y: 0 }; | |
| let totalSteps = 0; | |
| let acceptedSteps = 0; | |
| // Coverage tracking | |
| let visitedCells = new Set(); | |
| let totalTargetCells = 0; | |
| // Trail history for glow effect | |
| let trailHistory = []; | |
| const maxTrailHistory = 500; | |
| // --- Canvas Handling --- | |
| function resize() { | |
| const rect = cvsTarget.parentElement.getBoundingClientRect(); | |
| width = rect.width; | |
| height = rect.height - 70; // Account for header and description | |
| cvsTarget.width = width; | |
| cvsTarget.height = height; | |
| cvsSim.width = width; | |
| cvsSim.height = height; | |
| ctxTarget.fillStyle = '#050508'; | |
| ctxTarget.fillRect(0, 0, width, height); | |
| ctxSim.fillStyle = '#050508'; | |
| ctxSim.fillRect(0, 0, width, height); | |
| loadPreset('hex'); | |
| } | |
| function loadPreset(type) { | |
| ctxTarget.fillStyle = '#050508'; | |
| ctxTarget.fillRect(0, 0, width, height); | |
| const cx = width / 2; | |
| const cy = height / 2; | |
| ctxTarget.shadowBlur = 20; | |
| if (type === 'hex') { | |
| // Hexagon with inner triangle | |
| const r = height / 3; | |
| ctxTarget.strokeStyle = '#ff0080'; | |
| ctxTarget.shadowColor = '#ff0080'; | |
| ctxTarget.lineWidth = 15; | |
| ctxTarget.lineCap = 'square'; | |
| ctxTarget.beginPath(); | |
| for (let i = 0; i < 6; i++) { | |
| const angle = (i * Math.PI / 3) - Math.PI / 2; | |
| const x = cx + r * Math.cos(angle); | |
| const y = cy + r * Math.sin(angle); | |
| if (i === 0) ctxTarget.moveTo(x, y); | |
| else ctxTarget.lineTo(x, y); | |
| } | |
| ctxTarget.closePath(); | |
| ctxTarget.stroke(); | |
| // Inner triangle | |
| ctxTarget.strokeStyle = '#00ffff'; | |
| ctxTarget.shadowColor = '#00ffff'; | |
| ctxTarget.lineWidth = 10; | |
| ctxTarget.beginPath(); | |
| for (let i = 0; i < 3; i++) { | |
| const angle = (i * 2 * Math.PI / 3) - Math.PI / 2; | |
| const x = cx + (r * 0.5) * Math.cos(angle); | |
| const y = cy + (r * 0.5) * Math.sin(angle); | |
| if (i === 0) ctxTarget.moveTo(x, y); | |
| else ctxTarget.lineTo(x, y); | |
| } | |
| ctxTarget.closePath(); | |
| ctxTarget.stroke(); | |
| // Center dot | |
| ctxTarget.fillStyle = '#ffffff'; | |
| ctxTarget.shadowColor = '#ffffff'; | |
| ctxTarget.beginPath(); | |
| ctxTarget.arc(cx, cy, 8, 0, Math.PI * 2); | |
| ctxTarget.fill(); | |
| } else if (type === 'gaussian') { | |
| // Bivariate Gaussian blob | |
| const imgData = ctxTarget.createImageData(width, height); | |
| const sigma = height / 4; | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const dx = x - cx; | |
| const dy = y - cy; | |
| const val = Math.exp(-(dx*dx + dy*dy) / (2 * sigma * sigma)); | |
| const intensity = Math.floor(val * 255); | |
| const idx = (y * width + x) * 4; | |
| imgData.data[idx] = intensity; | |
| imgData.data[idx + 1] = Math.floor(intensity * 0.5); | |
| imgData.data[idx + 2] = intensity; | |
| imgData.data[idx + 3] = 255; | |
| } | |
| } | |
| ctxTarget.putImageData(imgData, 0, 0); | |
| } else if (type === 'rings') { | |
| // Concentric rings - multimodal | |
| ctxTarget.lineWidth = 12; | |
| const colors = ['#ff0080', '#00ffff', '#a855f7', '#22c55e']; | |
| const radii = [height * 0.4, height * 0.28, height * 0.16]; | |
| radii.forEach((r, i) => { | |
| ctxTarget.strokeStyle = colors[i % colors.length]; | |
| ctxTarget.shadowColor = colors[i % colors.length]; | |
| ctxTarget.beginPath(); | |
| ctxTarget.arc(cx, cy, r, 0, Math.PI * 2); | |
| ctxTarget.stroke(); | |
| }); | |
| // Center | |
| ctxTarget.fillStyle = '#ffffff'; | |
| ctxTarget.shadowColor = '#ffffff'; | |
| ctxTarget.beginPath(); | |
| ctxTarget.arc(cx, cy, 6, 0, Math.PI * 2); | |
| ctxTarget.fill(); | |
| } | |
| ctxTarget.shadowBlur = 0; | |
| updatePixelCache(); | |
| resetWalker(); | |
| } | |
| function clearCanvas() { | |
| ctxTarget.fillStyle = '#050508'; | |
| ctxTarget.fillRect(0, 0, width, height); | |
| ctxSim.fillStyle = '#050508'; | |
| ctxSim.fillRect(0, 0, width, height); | |
| trailHistory = []; | |
| visitedCells = new Set(); | |
| updatePixelCache(); | |
| resetWalker(); | |
| } | |
| // --- Interaction --- | |
| function startDraw(e) { | |
| isDrawing = true; | |
| draw(e); | |
| } | |
| function endDraw() { | |
| if (isDrawing) { | |
| isDrawing = false; | |
| updatePixelCache(); | |
| } | |
| } | |
| function draw(e) { | |
| if (!isDrawing) return; | |
| const rect = cvsTarget.getBoundingClientRect(); | |
| const clientX = e.touches ? e.touches[0].clientX : e.clientX; | |
| const clientY = e.touches ? e.touches[0].clientY : e.clientY; | |
| const x = clientX - rect.left; | |
| const y = clientY - rect.top; | |
| // Neon glow brush | |
| const gradient = ctxTarget.createRadialGradient(x, y, 0, x, y, brushSize); | |
| gradient.addColorStop(0, '#ffffff'); | |
| gradient.addColorStop(0.3, '#ff0080'); | |
| gradient.addColorStop(0.7, '#ff008080'); | |
| gradient.addColorStop(1, 'transparent'); | |
| ctxTarget.fillStyle = gradient; | |
| ctxTarget.beginPath(); | |
| ctxTarget.arc(x, y, brushSize, 0, Math.PI * 2); | |
| ctxTarget.fill(); | |
| } | |
| function togglePause() { | |
| isPaused = !isPaused; | |
| document.getElementById('pause-btn').textContent = isPaused ? '▶ Run' : '⏸ Halt'; | |
| } | |
| // Event Listeners | |
| window.addEventListener('resize', resize); | |
| cvsTarget.addEventListener('mousedown', startDraw); | |
| window.addEventListener('mouseup', endDraw); | |
| cvsTarget.addEventListener('mousemove', draw); | |
| cvsTarget.addEventListener('touchstart', (e) => { e.preventDefault(); startDraw(e); }, { passive: false }); | |
| window.addEventListener('touchend', endDraw); | |
| cvsTarget.addEventListener('touchmove', (e) => { e.preventDefault(); draw(e); }, { passive: false }); | |
| document.getElementById('brush-size').addEventListener('input', e => { | |
| brushSize = parseInt(e.target.value); | |
| document.getElementById('brush-val').textContent = brushSize; | |
| }); | |
| document.getElementById('speed').addEventListener('input', e => { | |
| stepSize = parseInt(e.target.value); | |
| document.getElementById('speed-val').textContent = stepSize; | |
| }); | |
| document.getElementById('jump-size').addEventListener('input', e => { | |
| jumpSize = parseInt(e.target.value); | |
| document.getElementById('jump-val').textContent = jumpSize; | |
| }); | |
| // --- MCMC Logic --- | |
| function updatePixelCache() { | |
| const imgData = ctxTarget.getImageData(0, 0, width, height); | |
| pixelData = new Uint8Array(width * height); | |
| totalTargetCells = 0; | |
| for (let i = 0; i < pixelData.length; i++) { | |
| // Use max of RGB for brightness | |
| const r = imgData.data[i * 4]; | |
| const g = imgData.data[i * 4 + 1]; | |
| const b = imgData.data[i * 4 + 2]; | |
| const val = Math.max(r, g, b); | |
| pixelData[i] = Math.max(1, val); | |
| if (val > 20) totalTargetCells++; | |
| } | |
| } | |
| function getDensity(x, y) { | |
| if (x < 0 || x >= width || y < 0 || y >= height) return 0; | |
| const idx = Math.floor(y) * width + Math.floor(x); | |
| return pixelData[idx]; | |
| } | |
| function resetWalker() { | |
| // Start in center | |
| walker.x = width / 2; | |
| walker.y = height / 2; | |
| totalSteps = 0; | |
| acceptedSteps = 0; | |
| trailHistory = []; | |
| visitedCells = new Set(); | |
| ctxSim.fillStyle = '#050508'; | |
| ctxSim.fillRect(0, 0, width, height); | |
| } | |
| function metropolisStep() { | |
| // Propose a random jump | |
| const propX = walker.x + (Math.random() - 0.5) * jumpSize * 2; | |
| const propY = walker.y + (Math.random() - 0.5) * jumpSize * 2; | |
| const pCurrent = getDensity(walker.x, walker.y); | |
| const pProp = getDensity(propX, propY); | |
| // Metropolis-Hastings acceptance ratio | |
| const ratio = pProp / pCurrent; | |
| // Accept if ratio >= 1 OR with probability = ratio | |
| if (ratio >= 1 || Math.random() < ratio) { | |
| walker.x = propX; | |
| walker.y = propY; | |
| return true; | |
| } | |
| return false; | |
| } | |
| // --- Animation --- | |
| function loop() { | |
| if (pixelData && !isPaused) { | |
| // Subtle fade effect for trail | |
| ctxSim.fillStyle = 'rgba(5, 5, 8, 0.03)'; | |
| ctxSim.fillRect(0, 0, width, height); | |
| let accepted = 0; | |
| for (let i = 0; i < stepSize; i++) { | |
| if (metropolisStep()) accepted++; | |
| const dx = Math.min(width - 1, Math.max(0, walker.x)); | |
| const dy = Math.min(height - 1, Math.max(0, walker.y)); | |
| // Track coverage | |
| const cellKey = `${Math.floor(dx/5)},${Math.floor(dy/5)}`; | |
| if (getDensity(dx, dy) > 20) { | |
| visitedCells.add(cellKey); | |
| } | |
| // Add to trail history | |
| trailHistory.push({ x: dx, y: dy }); | |
| if (trailHistory.length > maxTrailHistory) { | |
| trailHistory.shift(); | |
| } | |
| totalSteps++; | |
| } | |
| acceptedSteps += accepted; | |
| // Draw trail with color gradient based on age | |
| for (let i = 0; i < trailHistory.length; i++) { | |
| const point = trailHistory[i]; | |
| const alpha = (i / trailHistory.length) * 0.8; | |
| const hue = (totalSteps * 0.01 + i * 0.5) % 360; | |
| // Outer glow | |
| ctxSim.fillStyle = `hsla(${hue}, 100%, 50%, ${alpha * 0.3})`; | |
| ctxSim.beginPath(); | |
| ctxSim.arc(point.x, point.y, 4, 0, Math.PI * 2); | |
| ctxSim.fill(); | |
| // Core | |
| ctxSim.fillStyle = `hsla(${hue}, 100%, 70%, ${alpha})`; | |
| ctxSim.beginPath(); | |
| ctxSim.arc(point.x, point.y, 2, 0, Math.PI * 2); | |
| ctxSim.fill(); | |
| } | |
| // Draw current walker with bright glow | |
| const wx = Math.min(width - 1, Math.max(0, walker.x)); | |
| const wy = Math.min(height - 1, Math.max(0, walker.y)); | |
| ctxSim.shadowColor = '#00ffff'; | |
| ctxSim.shadowBlur = 15; | |
| ctxSim.fillStyle = '#ffffff'; | |
| ctxSim.beginPath(); | |
| ctxSim.arc(wx, wy, 4, 0, Math.PI * 2); | |
| ctxSim.fill(); | |
| ctxSim.shadowBlur = 0; | |
| // Update stats | |
| document.getElementById('step-count').innerText = `CYCLES: ${totalSteps.toLocaleString()}`; | |
| const acceptRate = totalSteps > 0 ? (acceptedSteps / totalSteps) * 100 : 0; | |
| document.getElementById('accept-rate').innerText = `${acceptRate.toFixed(1)}%`; | |
| // Color-code acceptance rate | |
| const statAccept = document.getElementById('stat-accept'); | |
| statAccept.className = 'stat-item'; | |
| if (acceptRate >= 20 && acceptRate <= 50) { | |
| statAccept.classList.add('good'); | |
| } else if (acceptRate > 50 && acceptRate <= 70) { | |
| statAccept.classList.add('warn'); | |
| } else { | |
| statAccept.classList.add('bad'); | |
| } | |
| document.getElementById('walker-pos').innerText = | |
| `[${Math.round(walker.x)}, ${Math.round(walker.y)}]`; | |
| document.getElementById('current-density').innerText = | |
| getDensity(walker.x, walker.y); | |
| // Coverage estimate | |
| const coverage = totalTargetCells > 0 | |
| ? Math.min(100, (visitedCells.size / (totalTargetCells / 25)) * 100) | |
| : 0; | |
| document.getElementById('coverage').innerText = `${coverage.toFixed(0)}%`; | |
| } | |
| requestAnimationFrame(loop); | |
| } | |
| // Initialize | |
| setTimeout(() => { | |
| resize(); | |
| loop(); | |
| }, 100); | |
| </script> | |
| </body> | |
| </html> |