Spaces:
Running
Running
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced DEX AMM Visualizer | AnyCoder</title> | |
| <!-- External Libraries --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --card-bg: #1e293b; | |
| --accent-primary: #6366f1; | |
| --accent-curve: #f59e0b; | |
| --accent-fixed: #10b981; | |
| --text-primary: #f8fafc; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-primary); | |
| overflow-x: hidden; | |
| } | |
| /* Glassmorphism */ | |
| .glass-card { | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| border-radius: 16px; | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); | |
| transition: all 0.3s ease; | |
| } | |
| /* Inputs */ | |
| .custom-input { | |
| background: rgba(15, 23, 42, 0.6); | |
| border: 1px solid rgba(148, 163, 184, 0.2); | |
| color: var(--text-primary); | |
| transition: all 0.3s ease; | |
| } | |
| .custom-input:focus { | |
| outline: none; | |
| border-color: var(--accent-primary); | |
| box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); | |
| } | |
| /* Tab System */ | |
| .tab-btn { | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .tab-btn.active { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-bottom: 2px solid; | |
| } | |
| .tab-btn.active[data-model="cp"] { border-color: var(--accent-primary); color: #818cf8; } | |
| .tab-btn.active[data-model="ss"] { border-color: var(--accent-curve); color: #fbbf24; } | |
| .tab-btn.active[data-model="cs"] { border-color: var(--accent-fixed); color: #34d399; } | |
| /* Range Slider */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 18px; | |
| width: 18px; | |
| border-radius: 50%; | |
| background: #fff; | |
| cursor: pointer; | |
| margin-top: -7px; | |
| box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: #334155; | |
| border-radius: 2px; | |
| } | |
| .font-mono { font-family: 'JetBrains Mono', monospace; } | |
| /* Math Formula Rendering */ | |
| .katex-display { | |
| font-size: 0.9em; | |
| overflow-x: auto; | |
| padding: 0.5rem; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="w-full py-4 px-6 border-b border-slate-800 flex justify-between items-center bg-slate-900/80 backdrop-blur-md sticky top-0 z-50"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center text-white font-bold text-xl shadow-lg shadow-blue-500/20"> | |
| <i class="fa-solid fa-layer-group"></i> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-bold tracking-tight leading-none">Multi-Model <span class="text-blue-400">AMM</span></h1> | |
| <p class="text-xs text-slate-500 font-mono">DEX Algorithm Simulator</p> | |
| </div> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-sm font-medium text-slate-400 hover:text-white transition-colors flex items-center gap-2 border border-slate-700 px-3 py-1.5 rounded-full hover:bg-slate-800"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square text-xs"></i> | |
| </a> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-grow container mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-12 gap-6"> | |
| <!-- Left Column: Controls --> | |
| <div class="lg:col-span-4 flex flex-col gap-6"> | |
| <!-- Model Selection Tabs --> | |
| <div class="glass-card p-2 flex justify-between gap-1"> | |
| <button class="tab-btn active flex-1 py-2 px-3 rounded-lg text-sm font-semibold text-slate-400 hover:text-white" onclick="switchModel('cp')" data-model="cp"> | |
| <i class="fa-solid fa-shuffle mr-1"></i> Uniswap | |
| </button> | |
| <button class="tab-btn flex-1 py-2 px-3 rounded-lg text-sm font-semibold text-slate-400 hover:text-white" onclick="switchModel('ss')" data-model="ss"> | |
| <i class="fa-solid fa-bezier-curve mr-1"></i> Curve | |
| </button> | |
| <button class="tab-btn flex-1 py-2 px-3 rounded-lg text-sm font-semibold text-slate-400 hover:text-white" onclick="switchModel('cs')" data-model="cs"> | |
| <i class="fa-solid fa-equals mr-1"></i> Fixed | |
| </button> | |
| </div> | |
| <!-- Pool Configuration --> | |
| <section class="glass-card p-6 relative overflow-hidden"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-lg font-semibold flex items-center gap-2"> | |
| <i class="fa-solid fa-coins text-blue-400"></i> Liquidity Pool | |
| </h2> | |
| <button onclick="resetPool()" class="text-xs text-slate-500 hover:text-white underline">Reset 1:1</button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="flex justify-between text-xs uppercase font-bold text-slate-500 mb-1"> | |
| <span>Reserve X (Token A)</span> | |
| <span class="text-slate-600">Input Token</span> | |
| </label> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-xl w-8 text-center">🍏</span> | |
| <input type="number" id="reserveA" value="1000" class="custom-input w-full p-3 rounded-lg font-mono text-lg" oninput="updateSimulation()"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="flex justify-between text-xs uppercase font-bold text-slate-500 mb-1"> | |
| <span>Reserve Y (Token B)</span> | |
| <span class="text-slate-600">Output Token</span> | |
| </label> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-xl w-8 text-center">🫐</span> | |
| <input type="number" id="reserveB" value="1000" class="custom-input w-full p-3 rounded-lg font-mono text-lg" oninput="updateSimulation()"> | |
| </div> | |
| </div> | |
| <!-- Curve Specific Input: Amp Factor --> | |
| <div id="ampContainer" class="hidden pt-2 border-t border-slate-700 mt-2"> | |
| <div class="flex justify-between mb-1"> | |
| <label class="text-xs uppercase font-bold text-amber-500">Amplification (A)</label> | |
| <span id="ampValue" class="text-xs font-mono text-white">100</span> | |
| </div> | |
| <input type="range" id="ampSlider" min="1" max="1000" value="100" class="w-full" oninput="updateSimulation()"> | |
| <p class="text-[10px] text-slate-500 mt-1">Higher A = Flatter curve (closer to fixed price).</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Swap Interface --> | |
| <section class="glass-card p-6 border-t-4 border-t-blue-500 transition-colors" id="swapCard"> | |
| <h2 class="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <i class="fa-solid fa-right-left"></i> Swap Simulator | |
| </h2> | |
| <div class="relative"> | |
| <!-- Input --> | |
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700 focus-within:border-blue-500/50 transition-colors"> | |
| <div class="flex justify-between mb-2"> | |
| <label class="text-xs text-slate-400">Pay (In)</label> | |
| <span class="text-xs text-slate-500 font-mono">Token A</span> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <input type="number" id="swapInput" value="0" class="bg-transparent text-2xl font-bold w-full outline-none text-white placeholder-slate-600 font-mono" placeholder="0.0"> | |
| </div> | |
| </div> | |
| <!-- Arrow --> | |
| <div class="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 z-10"> | |
| <div class="bg-slate-800 border border-slate-700 p-1.5 rounded-full shadow-xl text-slate-400"> | |
| <i class="fa-solid fa-arrow-down text-sm"></i> | |
| </div> | |
| </div> | |
| <!-- Output --> | |
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700 mt-2"> | |
| <div class="flex justify-between mb-2"> | |
| <label class="text-xs text-slate-400">Receive (Out)</label> | |
| <span class="text-xs text-slate-500 font-mono">Token B</span> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <input type="text" id="swapOutput" readonly class="bg-transparent text-2xl font-bold w-full outline-none text-emerald-400 font-mono" value="0.00"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <input type="range" id="swapSlider" min="0" max="1000" value="0" class="w-full accent-blue-500" oninput="syncSlider()"> | |
| </div> | |
| </section> | |
| <!-- Stats Mini --> | |
| <div class="glass-card p-4 flex justify-between items-center"> | |
| <div> | |
| <div class="text-xs text-slate-500">Slippage / Impact</div> | |
| <div id="priceImpact" class="font-bold text-emerald-400 font-mono">0.00%</div> | |
| </div> | |
| <div class="text-right"> | |
| <div class="text-xs text-slate-500">Execution Price</div> | |
| <div id="execPrice" class="font-bold text-white font-mono">0.00</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Column: Visualization --> | |
| <div class="lg:col-span-8 flex flex-col gap-6"> | |
| <!-- Chart --> | |
| <section class="glass-card p-6 h-[450px] flex flex-col relative"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <div> | |
| <h2 class="text-lg font-semibold" id="chartTitle">Constant Product Curve</h2> | |
| <p class="text-xs text-slate-400" id="chartSubtitle">x * y = k</p> | |
| </div> | |
| <div class="flex gap-2 text-xs"> | |
| <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-indigo-500"></span> Curve</span> | |
| <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-emerald-500"></span> Current</span> | |
| <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-pink-500"></span> New</span> | |
| </div> | |
| </div> | |
| <div class="flex-grow relative w-full h-full"> | |
| <canvas id="ammChart"></canvas> | |
| </div> | |
| </section> | |
| <!-- Formula Explanation --> | |
| <section class="glass-card p-6"> | |
| <h3 class="text-sm font-bold text-slate-400 uppercase mb-4 tracking-wider flex items-center gap-2"> | |
| <i class="fa-solid fa-square-root-variable"></i> Mathematical Model | |
| </h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- Formula Display --> | |
| <div class="bg-slate-900/50 p-4 rounded-lg border border-slate-700/50 flex flex-col justify-center items-center text-center min-h-[100px]"> | |
| <div id="formulaDisplay" class="font-mono text-lg text-blue-300"> | |
| x · y = k | |
| </div> | |
| <div id="formulaDesc" class="text-xs text-slate-500 mt-2"> | |
| Uniswap V2 Model. Ensures liquidity is never depleted. | |
| </div> | |
| </div> | |
| <!-- Dynamic Variables --> | |
| <div class="space-y-2 text-sm font-mono"> | |
| <div class="flex justify-between p-2 bg-slate-800/30 rounded hover:bg-slate-800/50 transition"> | |
| <span class="text-slate-400">Invariant (K/D)</span> | |
| <span id="mathK" class="text-indigo-300">1,000,000</span> | |
| </div> | |
| <div class="flex justify-between p-2 bg-slate-800/30 rounded hover:bg-slate-800/50 transition"> | |
| <span class="text-slate-400">Current Spot Price</span> | |
| <span id="spotPrice" class="text-emerald-400">1.0000</span> | |
| </div> | |
| <div class="flex justify-between p-2 bg-slate-800/30 rounded hover:bg-slate-800/50 transition"> | |
| <span class="text-slate-400">Effective Output (dy)</span> | |
| <span id="mathDy" class="text-white">0.00</span> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </main> | |
| <script> | |
| // --- State Management --- | |
| const state = { | |
| model: 'cp', // 'cp' (Constant Product), 'ss' (StableSwap), 'cs' (Constant Sum) | |
| resA: 1000, | |
| resB: 1000, | |
| amp: 100, // For Curve | |
| input: 0 | |
| }; | |
| let chartInstance = null; | |
| // --- DOM Elements --- | |
| const els = { | |
| resA: document.getElementById('reserveA'), | |
| resB: document.getElementById('reserveB'), | |
| inp: document.getElementById('swapInput'), | |
| out: document.getElementById('swapOutput'), | |
| slider: document.getElementById('swapSlider'), | |
| ampContainer: document.getElementById('ampContainer'), | |
| ampSlider: document.getElementById('ampSlider'), | |
| ampValue: document.getElementById('ampValue'), | |
| chartTitle: document.getElementById('chartTitle'), | |
| chartSubtitle: document.getElementById('chartSubtitle'), | |
| formulaDisplay: document.getElementById('formulaDisplay'), | |
| formulaDesc: document.getElementById('formulaDesc'), | |
| mathK: document.getElementById('mathK'), | |
| spotPrice: document.getElementById('spotPrice'), | |
| mathDy: document.getElementById('mathDy'), | |
| impact: document.getElementById('priceImpact'), | |
| execPrice: document.getElementById('execPrice'), | |
| tabs: document.querySelectorAll('.tab-btn'), | |
| swapCard: document.getElementById('swapCard') | |
| }; | |
| // --- Initialization --- | |
| window.onload = () => { | |
| initChart(); | |
| updateSimulation(); | |
| els.inp.addEventListener('input', () => { | |
| els.slider.value = Math.min(els.inp.value, 1000); | |
| updateSimulation(); | |
| }); | |
| }; | |
| // --- Core Logic --- | |
| function switchModel(model) { | |
| state.model = model; | |
| // Update Tabs UI | |
| els.tabs.forEach(t => { | |
| if(t.dataset.model === model) t.classList.add('active'); | |
| else t.classList.remove('active'); | |
| }); | |
| // Update Controls UI | |
| if (model === 'ss') { | |
| els.ampContainer.classList.remove('hidden'); | |
| els.chartTitle.innerText = "Stableswap Curve"; | |
| els.chartSubtitle.innerText = "An^n * sum(x) + D = ADn^n + D^(n+1) / (n^n * prod(x))"; | |
| els.formulaDisplay.innerHTML = "An^n \\sum x_i + D = ADn^n + \\frac{D^{n+1}}{n^n \\prod x_i}"; | |
| els.formulaDesc.innerText = "Curve Finance Model. Low slippage for like-assets (e.g., USDC/DAI)."; | |
| els.swapCard.style.borderColor = "#f59e0b"; | |
| } else if (model === 'cs') { | |
| els.ampContainer.classList.add('hidden'); | |
| els.chartTitle.innerText = "Constant Sum"; | |
| els.chartSubtitle.innerText = "x + y = k"; | |
| els.formulaDisplay.innerText = "x + y = k"; | |
| els.formulaDesc.innerText = "Fixed Exchange Rate. Zero slippage until empty."; | |
| els.swapCard.style.borderColor = "#10b981"; | |
| } else { | |
| els.ampContainer.classList.add('hidden'); | |
| els.chartTitle.innerText = "Constant Product"; | |
| els.chartSubtitle.innerText = "x * y = k"; | |
| els.formulaDisplay.innerText = "x · y = k"; | |
| els.formulaDesc.innerText = "Uniswap V2 Model. Infinite liquidity range."; | |
| els.swapCard.style.borderColor = "#6366f1"; | |
| } | |
| updateSimulation(); | |
| } | |
| function resetPool() { | |
| els.resA.value = 1000; | |
| els.resB.value = 1000; | |
| updateSimulation(); | |
| } | |
| function syncSlider() { | |
| els.inp.value = els.slider.value; | |
| updateSimulation(); | |
| } | |
| function updateSimulation() { | |
| // Update State | |
| state.resA = parseFloat(els.resA.value) || 1; | |
| state.resB = parseFloat(els.resB.value) || 1; | |
| state.input = parseFloat(els.inp.value) || 0; | |
| state.amp = parseInt(els.ampSlider.value); | |
| els.ampValue.innerText = state.amp; | |
| let output = 0; | |
| let newY = 0; | |
| let invariant = 0; | |
| let spotP = 0; | |
| // --- MATH LOGIC --- | |
| if (state.model === 'cp') { | |
| // x * y = k | |
| const k = state.resA * state.resB; | |
| invariant = k; | |
| const newX = state.resA + state.input; | |
| newY = k / newX; | |
| output = state.resB - newY; | |
| spotP = state.resB / state.resA; | |
| } else if (state.model === 'cs') { | |
| // x + y = k | |
| const k = state.resA + state.resB; | |
| invariant = k; | |
| const newX = state.resA + state.input; | |
| // If newX > k, pool is empty | |
| if (newX >= k) { | |
| output = state.resB; // Can only take what's left | |
| newY = 0; | |
| } else { | |
| newY = k - newX; | |
| output = state.resB - newY; | |
| } | |
| spotP = 1.0; | |
| } else if (state.model === 'ss') { | |
| // Stableswap Math (Simplified for 2 coins) | |
| // D is invariant | |
| const D = getD(state.resA, state.resB, state.amp); | |
| invariant = D; | |
| const newX = state.resA + state.input; | |
| newY = getY(newX, D, state.amp); | |
| output = state.resB - newY; | |
| // Approx spot price (derivative at current point) | |
| // For display, simple ratio is close enough for small dx, but let's use small delta | |
| const smallDy = state.resB - getY(state.resA + 0.1, D, state.amp); | |
| spotP = smallDy / 0.1; | |
| } | |
| // --- UI Updates --- | |
| els.out.value = formatNum(output, 4); | |
| els.mathDy.innerText = formatNum(output, 2); | |
| els.mathK.innerText = formatNum(invariant, 0); | |
| els.spotPrice.innerText = formatNum(spotP, 4); | |
| // Execution Price & Impact | |
| const execP = state.input > 0 ? output / state.input : 0; | |
| els.execPrice.innerText = state.input > 0 ? formatNum(execP, 4) : "0.00"; | |
| let impact = 0; | |
| if (state.input > 0 && spotP > 0) { | |
| impact = ((spotP - execP) / spotP) * 100; | |
| } | |
| els.impact.innerText = `${formatNum(impact, 2)}%`; | |
| // Color coding impact | |
| if(impact < 0.1) els.impact.className = "font-bold text-emerald-400 font-mono"; | |
| else if(impact < 1.0) els.impact.className = "font-bold text-blue-400 font-mono"; | |
| else if(impact < 5.0) els.impact.className = "font-bold text-yellow-400 font-mono"; | |
| else els.impact.className = "font-bold text-red-500 font-mono"; | |
| // Update Chart | |
| updateChart(state.resA, state.resB, state.resA + state.input, newY, invariant); | |
| } | |
| // --- Curve (Stableswap) Math Helpers --- | |
| // Based on Curve Whitepaper for n=2 | |
| function getD(xp, yp, A) { | |
| // Newton's method to find D | |
| const S = xp + yp; | |
| if (S === 0) return 0; | |
| let D = S; | |
| const Ann = A * 4; // n=2, so Ann = A * 2^2 = 4A | |
| for (let i = 0; i < 15; i++) { | |
| let D_P = D * D * D / (4 * xp * yp); // D^(n+1) / (n^n * prod(x)) -> D^3 / (4xy) | |
| let prevD = D; | |
| // Formula: D = (Ann * S + D_P * n) * D / ((Ann - 1) * D + (n + 1) * D_P) | |
| // n=2 | |
| let num = (Ann * S + 2 * D_P) * D; | |
| let den = (Ann - 1) * D + 3 * D_P; | |
| D = num / den; | |
| if (Math.abs(D - prevD) <= 1) break; | |
| } | |
| return D; | |
| } | |
| function getY(x, D, A) { | |
| // Calculate Y given X and D | |
| const Ann = A * 4; | |
| const c = (D * D * D) / (4 * x * Ann); // c = D^(n+1) / (n * prod(x_others) * Ann * n^n) | |
| const b = x + (D / Ann) - D; // b = sum(x_others) + D/Ann - D | |
| // Solve y^2 + b*y = c | |
| // y = ( -b + sqrt(b^2 + 4c) ) / 2 (Standard quadratic formula, since y must be positive) | |
| // Or iterative (Curve uses iterative for gas efficiency, we can use quadratic here for JS speed for n=2) | |
| // y^2 + (x + D/4A - D)y - D^3/(16Ax) = 0 | |
| // Let's trust the iterative method to match Curve logic exactly | |
| let y = D; | |
| for(let i=0; i < 15; i++){ | |
| // y = (y^2 + c) / (2y + b) | |
| let prevY = y; | |
| y = (y*y + c) / (2*y + b); | |
| if(Math.abs(y - prevY) <= 1) break; | |
| } | |
| return y; | |
| } | |
| // --- Charting --- | |
| function initChart() { | |
| const ctx = document.getElementById('ammChart').getContext('2d'); | |
| Chart.defaults.color = '#64748b'; | |
| Chart.defaults.font.family = "'Inter', sans-serif"; | |
| chartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| datasets: [ | |
| { | |
| label: 'Curve', | |
| data: [], | |
| borderColor: '#6366f1', // Will change dynamically | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| tension: 0.4 | |
| }, | |
| { | |
| label: 'Current', | |
| data: [], | |
| backgroundColor: '#10b981', | |
| borderColor: '#fff', | |
| borderWidth: 2, | |
| pointRadius: 6 | |
| }, | |
| { | |
| label: 'New', | |
| data: [], | |
| backgroundColor: '#ec4899', | |
| borderColor: '#fff', | |
| borderWidth: 2, | |
| pointRadius: 6 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| backgroundColor: 'rgba(15, 23, 42, 0.9)', | |
| titleColor: '#fff', | |
| bodyFont: { family: 'JetBrains Mono' } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| title: { display: true, text: 'Reserve A (Input)', color: '#94a3b8' }, | |
| grid: { color: 'rgba(255, 255, 255, 0.05)' } | |
| }, | |
| y: { | |
| title: { display: true, text: 'Reserve B (Output)', color: '#94a3b8' }, | |
| grid: { color: 'rgba(255, 255, 255, 0.05)' } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function updateChart(currX, currY, newX, newY, invariant) { | |
| const dataPoints = []; | |
| const steps = 100; | |
| // Calculate range to draw | |
| const centerX = currX; | |
| const range = Math.max(currX, newX) * 1.5; | |
| const startX = Math.max(1, centerX - range/2); | |
| const endX = centerX + range/2; | |
| // Update Line Color based on model | |
| if(state.model === 'cp') chartInstance.data.datasets[0].borderColor = '#6366f1'; | |
| if(state.model === 'ss') chartInstance.data.datasets[0].borderColor = '#f59e0b'; | |
| if(state.model === 'cs') chartInstance.data.datasets[0].borderColor = '#10b981'; | |
| for (let i = 0; i <= steps; i++) { | |
| const xVal = startX + (endX - startX) * (i / steps); | |
| let yVal = 0; | |
| if (state.model === 'cp') { | |
| yVal = invariant / xVal; | |
| } else if (state.model === 'cs') { | |
| yVal = invariant - xVal; | |
| } else if (state.model === 'ss') { | |
| yVal = getY(xVal, invariant, state.amp); | |
| } | |
| if (yVal >= 0) { | |
| dataPoints.push({ x: xVal, y: yVal }); | |
| } | |
| } | |
| chartInstance.data.datasets[0].data = dataPoints; | |
| chartInstance.data.datasets[1].data = [{ x: currX, y: currY }]; | |
| if (Math.abs(newX - currX) > 0.01) { | |
| chartInstance.data.datasets[2].data = [{ x: newX, y: newY }]; | |
| } else { | |
| chartInstance.data.datasets[2].data = []; | |
| } | |
| chartInstance.update('none'); | |
| } | |
| function formatNum(num, decimals = 2) { | |
| return parseFloat(num).toLocaleString('en-US', { | |
| minimumFractionDigits: decimals, | |
| maximumFractionDigits: decimals | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |