Download templates/gradient-descent-three.html from deedrop1140/machinelearningalgorithms: direct link, hf CLI and curl.
- Browser
- Download file 32.6 kB
-
https://huggingface.co/spaces/deedrop1140/machinelearningalgorithms/resolve/main/templates/gradient-descent-three.html
- Command line
-
hf download hf://spaces/deedrop1140/machinelearningalgorithms/templates/gradient-descent-three.html
-
curl -L -o gradient-descent-three.html https://huggingface.co/spaces/deedrop1140/machinelearningalgorithms/resolve/main/templates/gradient-descent-three.html
32.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Gradient Descent Simulator</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --background: #05080a; | |
| --primary: #00ffff; | |
| --secondary: #a855f7; | |
| --accent: #ec4899; | |
| --surface: #0a1014; | |
| --border: #1e293b; | |
| } | |
| body { | |
| background-color: var(--background); | |
| color: #f8fafc; | |
| font-family: 'Space Grotesk', sans-serif; | |
| margin: 0; | |
| overflow-x: hidden; | |
| } | |
| .gradient-text { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .glass { | |
| background: rgba(10, 16, 20, 0.8); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(30, 41, 59, 0.5); | |
| } | |
| .glow-cyan { | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.3); | |
| } | |
| .canvas-container { | |
| width: 100%; | |
| height: 350px; /* Default for mobile */ | |
| position: relative; | |
| background: #000; | |
| border-radius: 1rem; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| } | |
| /* Desktop override */ | |
| @media (min-width: 768px) { | |
| .canvas-container { | |
| height: 500px; | |
| } | |
| } | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 6px; | |
| cursor: pointer; | |
| background: #1e293b; | |
| border-radius: 3px; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| height: 18px; | |
| width: 18px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| cursor: pointer; | |
| -webkit-appearance: none; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px rgba(0, 255, 255, 0.5); | |
| } | |
| .hidden { display: none; } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| .animate-float { animation: float 3s ease-in-out infinite; } | |
| </style> | |
| </head> | |
| <body class="p-4 md:p-8"> | |
| <div class="max-w-7xl mx-auto grid lg:grid-cols-[1fr_350px] gap-8"> | |
| <!-- Left Side: Header & Viewport --> | |
| <div class="space-y-6"> | |
| <header class="relative flex flex-col md:block"> | |
| <h1 class="text-4xl md:text-5xl font-bold gradient-text text-center md:text-left">Gradient Descent</h1> | |
| <!-- Centered Button (Responsive Position) --> | |
| <div class="order-last md:order-none mt-4 md:mt-0 md:absolute md:left-1/2 md:-translate-x-1/2 md:top-1 flex items-center justify-center"> | |
| <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio> | |
| <a href="/gradient-descent" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider"> | |
| Back to Core | |
| </a> | |
| </div> | |
| <p class="text-slate-400 text-sm max-w-md mt-2 text-center md:text-left mx-auto md:mx-0"> | |
| Optimize parameters by descending the loss landscape. Now with <b>Adaptive Learning Rate</b> logic! | |
| </p> | |
| </header> | |
| <div class="canvas-container" id="container"> | |
| <div id="three-canvas"></div> | |
| <!-- Viewport Overlays --> | |
| <div class="absolute top-4 left-4 pointer-events-none"> | |
| <div class="glass px-3 py-2 rounded-lg"> | |
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Status</div> | |
| <div id="status-text" class="text-xs font-mono text-cyan-400 mt-1 uppercase">Ready</div> | |
| </div> | |
| </div> | |
| <div class="absolute top-4 right-4 pointer-events-none text-right"> | |
| <div class="glass px-3 py-2 rounded-lg"> | |
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Position</div> | |
| <div id="pos-display" class="text-xs font-mono text-cyan-400 mt-1">X: 3.00 Y: 3.00</div> | |
| </div> | |
| </div> | |
| <div class="absolute bottom-4 left-4 glass px-3 py-1 rounded-full text-[10px] text-slate-400 font-bold border border-slate-800"> | |
| GOAL AT <span id="goal-coords" class="text-green-400">0, 0</span> | |
| </div> | |
| </div> | |
| <!-- Educational Info --> | |
| <div class="glass rounded-xl overflow-hidden"> | |
| <button onclick="toggleInfo()" class="w-full flex items-center justify-between p-4 hover:bg-slate-800/30 transition-colors"> | |
| <div class="flex items-center gap-2"> | |
| <svg class="w-5 h-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg> | |
| <span class="font-bold">Optimizer Secrets</span> | |
| </div> | |
| <svg id="info-arrow" class="w-5 h-5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> | |
| </button> | |
| <div id="info-content" class="p-4 pt-0 text-sm text-slate-400 leading-relaxed grid md:grid-cols-2 gap-6 border-t border-slate-800 hidden"> | |
| <div class="space-y-2 pt-4"> | |
| <p><strong class="text-white">Momentum (β):</strong> Acts like a physical ball with weight. It keeps moving in the same direction, helping it "roll" through flat valleys and over small local pits.</p> | |
| </div> | |
| <div class="space-y-2 pt-4"> | |
| <p><strong class="text-white">The Challenge:</strong> Rosenbrock and Rastrigin are "non-convex" or have "vanishing gradients." Vanilla GD is often too weak to reach the center without help!</p> | |
| </div> | |
| <!-- Pro Tip Note --> | |
| <div class="col-span-full pt-4 border-t border-slate-800/50 mt-2"> | |
| <p class="text-yellow-400 font-medium"> | |
| <span class="mr-2">💡</span> <b>Pro Tip:</b> If the ball gets stuck or vibrates wildly, you must <b>adjust the learning rate</b> or <b>momentum</b>. Or turn on <b>Adaptive Rate</b> to let the algorithm handle it! | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Side: Controls --> | |
| <aside class="space-y-6"> | |
| <div class="glass rounded-xl p-6 space-y-6"> | |
| <!-- Learning Rate Slider --> | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">Learning Rate (α)</label> | |
| <span id="lr-value" class="text-cyan-400 font-mono bg-cyan-400/10 px-2 py-0.5 rounded border border-cyan-400/20 text-sm">0.050</span> | |
| </div> | |
| <input type="range" id="lr-slider" min="0.001" max="0.5" step="0.001" value="0.05"> | |
| </div> | |
| <!-- Adaptive Toggle --> | |
| <div onclick="toggleAdaptive()" class="flex items-center justify-between p-3 rounded-lg bg-slate-800/40 border border-slate-700/50 cursor-pointer hover:bg-slate-800 transition-colors"> | |
| <div class="flex flex-col"> | |
| <span class="text-xs font-bold text-slate-300">Adaptive Rate</span> | |
| <span class="text-[10px] text-slate-500">Auto-tune α during descent</span> | |
| </div> | |
| <div id="adaptive-toggle-bg" class="w-10 h-5 rounded-full bg-slate-700 relative transition-colors"> | |
| <div id="adaptive-toggle-dot" class="absolute left-1 top-1 w-3 h-3 rounded-full bg-white transition-all"></div> | |
| </div> | |
| </div> | |
| <!-- Momentum Slider --> | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">Momentum (β)</label> | |
| <span id="mom-value" class="text-purple-400 font-mono bg-purple-400/10 px-2 py-0.5 rounded border border-purple-400/20 text-sm">0.10</span> | |
| </div> | |
| <input type="range" id="mom-slider" min="0" max="0.99" step="0.01" value="0.1"> | |
| </div> | |
| <!-- Playback Buttons --> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <button id="btn-toggle" class="flex items-center justify-center gap-2 py-3 rounded-lg font-bold transition-all bg-cyan-400 text-black glow-cyan hover:brightness-110"> | |
| <span id="toggle-icon">▶</span> <span id="toggle-text">Start</span> | |
| </button> | |
| <button id="btn-step" class="flex items-center justify-center gap-2 py-3 rounded-lg border border-slate-700 hover:bg-slate-800 transition-all font-bold"> | |
| Step ➜ | |
| </button> | |
| </div> | |
| <button id="btn-reset" class="w-full flex items-center justify-center gap-2 py-2 text-slate-400 hover:text-white text-sm transition-colors font-medium"> | |
| Reset Position ↺ | |
| </button> | |
| <!-- Stats --> | |
| <div class="grid grid-cols-2 gap-4 pt-4 border-t border-slate-800"> | |
| <div class="text-center"> | |
| <div id="steps-val" class="text-2xl font-mono font-bold text-cyan-400">0</div> | |
| <div class="text-[10px] text-slate-500 uppercase tracking-tighter">Steps</div> | |
| </div> | |
| <div class="text-center"> | |
| <div id="loss-val" class="text-2xl font-mono font-bold text-pink-500">9.00</div> | |
| <div class="text-[10px] text-slate-500 uppercase tracking-tighter">Loss</div> | |
| </div> | |
| </div> | |
| <!-- Progress --> | |
| <div class="p-4 bg-slate-900/50 rounded-lg border border-slate-800"> | |
| <div class="flex justify-between text-xs mb-2"> | |
| <span class="text-slate-500">Distance to Target</span> | |
| <span id="dist-val" class="font-mono text-slate-300">4.242</span> | |
| </div> | |
| <div class="h-1.5 bg-slate-800 rounded-full overflow-hidden"> | |
| <div id="progress-bar" class="h-full bg-cyan-400 transition-all duration-300" style="width: 10%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Level Selector --> | |
| <div class="glass rounded-xl p-4 space-y-3"> | |
| <h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest px-2">Function Landscape</h3> | |
| <div id="level-list" class="space-y-1"> | |
| <!-- Levels injected here --> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Victory Modal --> | |
| <div id="victory-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm hidden"> | |
| <div class="glass relative max-w-sm w-full rounded-2xl p-8 text-center animate-float"> | |
| <!-- Close Button (Cross) --> | |
| <button onclick="closeModal()" class="absolute top-4 right-4 text-slate-500 hover:text-white transition-colors"> | |
| <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> | |
| </button> | |
| <div class="w-16 h-16 bg-cyan-400/20 rounded-full flex items-center justify-center mx-auto mb-4"> | |
| <span class="text-3xl text-cyan-400">🏆</span> | |
| </div> | |
| <h2 class="text-2xl font-bold gradient-text mb-1">Convergence!</h2> | |
| <p id="modal-level-name" class="text-slate-400 text-sm mb-6">Level Complete</p> | |
| <div class="bg-slate-900/80 rounded-xl p-4 mb-6"> | |
| <div id="modal-steps" class="text-3xl font-mono text-cyan-400 font-bold">0</div> | |
| <div class="text-xs text-slate-500 uppercase tracking-widest">Total Steps</div> | |
| </div> | |
| <div class="flex gap-3"> | |
| <button onclick="closeModal()" class="flex-1 py-2 rounded-lg border border-slate-700 hover:bg-slate-800 transition-colors text-sm font-semibold">Retry</button> | |
| <button onclick="nextLevel()" class="flex-1 py-2 rounded-lg bg-cyan-400 text-black font-bold text-sm">Next Level</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- LEVEL DEFINITIONS --- | |
| // Added 'presets' to automatically tune parameters for each landscape | |
| const LEVELS = [ | |
| { | |
| id: 1, | |
| name: 'The Bowl', | |
| difficulty: 'easy', | |
| loss: (x, y) => x*x + y*y, | |
| min: {x: 0, y: 0}, | |
| winRadius: 0.15, | |
| presets: { lr: 0.05, mom: 0.1 } // Slow & Steady | |
| }, | |
| { | |
| id: 2, | |
| name: 'The Ellipse', | |
| difficulty: 'easy', | |
| loss: (x, y) => x*x + 10*y*y, | |
| min: {x: 0, y: 0}, | |
| winRadius: 0.2, | |
| presets: { lr: 0.05, mom: 0.1 } | |
| }, | |
| { | |
| id: 3, | |
| name: 'Rosenbrock Valley', | |
| difficulty: 'medium', | |
| loss: (x, y) => Math.pow(1-x, 2) + 100*Math.pow(y-x*x, 2), | |
| min: {x: 1, y: 1}, | |
| winRadius: 0.25, | |
| presets: { lr: 0.002, mom: 0.85 } // Needs momentum to traverse the valley | |
| }, | |
| { | |
| id: 4, | |
| name: 'Beale Function', | |
| difficulty: 'medium', | |
| loss: (x, y) => Math.pow(1.5-x+x*y, 2) + Math.pow(2.25-x+x*y*y, 2) + Math.pow(2.625-x+x*y*y*y, 2), | |
| min: {x: 3, y: 0.5}, | |
| winRadius: 0.25, | |
| presets: { lr: 0.01, mom: 0.5 } | |
| }, | |
| { | |
| id: 5, | |
| name: 'Rastrigin', | |
| difficulty: 'hard', | |
| loss: (x, y) => 20 + x*x - 10*Math.cos(2*Math.PI*x) + y*y - 10*Math.cos(2*Math.PI*y), | |
| min: {x: 0, y: 0}, | |
| winRadius: 0.3, | |
| presets: { lr: 0.005, mom: 0.95 } // HIGH MOMENTUM to escape local minima | |
| } | |
| ]; | |
| // Constants for 3D Visuals | |
| const SURFACE_OFFSET = -0.5; | |
| const Z_SCALE = 0.8; | |
| const BALL_FLOAT = 0.1; | |
| // --- APP STATE --- | |
| let currentLevel = LEVELS[0]; | |
| let pos = { x: 3, y: 3 }; | |
| let velocity = { x: 0, y: 0 }; | |
| let path = []; | |
| let learningRate = 0.05; | |
| let momentum = 0.1; // Default starting momentum | |
| let isAdaptive = false; | |
| let isRunning = false; | |
| let steps = 0; | |
| let loopId = null; | |
| let prevLoss = Infinity; | |
| // --- THREE.JS SETUP --- | |
| const container = document.getElementById('container'); | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x05080a); | |
| const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000); | |
| camera.position.set(8, 8, 8); | |
| camera.lookAt(0, 0, 0); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| document.getElementById('three-canvas').appendChild(renderer.domElement); | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); | |
| scene.add(ambientLight); | |
| const pointLight = new THREE.PointLight(0x00ffff, 1); | |
| pointLight.position.set(10, 10, 10); | |
| scene.add(pointLight); | |
| function calculateVizHeight(x, y) { | |
| let z = currentLevel.loss(x, y); | |
| z = Math.min(z, 50); | |
| return (Math.log(z + 1) * Z_SCALE) + SURFACE_OFFSET; | |
| } | |
| // Surface | |
| let surfaceMesh; | |
| function updateSurface() { | |
| if (surfaceMesh) scene.remove(surfaceMesh); | |
| const size = 64; | |
| const geo = new THREE.PlaneBufferGeometry(10, 10, size, size); | |
| const posAttr = geo.attributes.position; | |
| const colorAttr = new THREE.BufferAttribute(new Float32Array(posAttr.count * 3), 3); | |
| let minVal = Infinity, maxVal = -Infinity; | |
| const vals = []; | |
| for (let i = 0; i < posAttr.count; i++) { | |
| const x = posAttr.getX(i); | |
| const y = posAttr.getY(i); | |
| let z = currentLevel.loss(x, y); | |
| z = Math.min(z, 50); | |
| z = Math.log(z + 1) * Z_SCALE; | |
| vals.push(z); | |
| minVal = Math.min(minVal, z); | |
| maxVal = Math.max(maxVal, z); | |
| } | |
| for (let i = 0; i < posAttr.count; i++) { | |
| const z = vals[i]; | |
| posAttr.setZ(i, z); | |
| const t = (z - minVal) / (maxVal - minVal || 1); | |
| colorAttr.setXYZ(i, 0.1 + t * 0.8, 0.8 - t * 0.6, 0.8 + t * 0.2); | |
| } | |
| geo.setAttribute('color', colorAttr); | |
| geo.computeVertexNormals(); | |
| const mat = new THREE.MeshStandardMaterial({ vertexColors: true, side: THREE.DoubleSide, transparent: true, opacity: 0.8, roughness: 0.5 }); | |
| surfaceMesh = new THREE.Mesh(geo, mat); | |
| surfaceMesh.rotation.x = -Math.PI / 2; | |
| surfaceMesh.position.y = SURFACE_OFFSET; | |
| scene.add(surfaceMesh); | |
| } | |
| const ball = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.18, 24, 24), | |
| new THREE.MeshStandardMaterial({ color: 0xff66cc, emissive: 0xff0088, emissiveIntensity: 0.5 }) | |
| ); | |
| scene.add(ball); | |
| let pathLine; | |
| function updatePath() { | |
| if (pathLine) scene.remove(pathLine); | |
| if (path.length < 2) return; | |
| const points = path.map(p => new THREE.Vector3(p.x, calculateVizHeight(p.x, p.y) + 0.05, p.y)); | |
| const geo = new THREE.BufferGeometry().setFromPoints(points); | |
| const mat = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.8 }); | |
| pathLine = new THREE.Line(geo, mat); | |
| scene.add(pathLine); | |
| } | |
| const goalMarker = new THREE.Group(); | |
| const torus = new THREE.Mesh(new THREE.TorusGeometry(0.3, 0.02, 16, 32), new THREE.MeshBasicMaterial({ color: 0x00ff66, transparent: true, opacity: 0.5 })); | |
| torus.rotation.x = Math.PI/2; | |
| const star = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), new THREE.MeshBasicMaterial({ color: 0x00ff66 })); | |
| goalMarker.add(torus); | |
| goalMarker.add(star); | |
| scene.add(goalMarker); | |
| function updateGoalMarker() { | |
| const gx = currentLevel.min.x; | |
| const gy = currentLevel.min.y; | |
| const gz = calculateVizHeight(gx, gy); | |
| goalMarker.position.set(gx, gz + 0.05, gy); | |
| document.getElementById('goal-coords').innerText = `[${gx}, ${gy}]`; | |
| } | |
| scene.add(new THREE.GridHelper(10, 20, 0x1a3a4a, 0x0a1a2a)); | |
| // --- OPTIMIZER LOGIC --- | |
| function calculateGradient(x, y) { | |
| const h = 0.0001; | |
| const dx = (currentLevel.loss(x + h, y) - currentLevel.loss(x - h, y)) / (2 * h); | |
| const dy = (currentLevel.loss(x, y + h) - currentLevel.loss(x, y - h)) / (2 * h); | |
| const magnitude = Math.sqrt(dx*dx + dy*dy); | |
| const limit = 50; | |
| if (magnitude > limit) { | |
| return { x: (dx / magnitude) * limit, y: (dy / magnitude) * limit, magnitude: limit }; | |
| } | |
| return { x: dx, y: dy, magnitude }; | |
| } | |
| function step() { | |
| const currentLoss = currentLevel.loss(pos.x, pos.y); | |
| const grad = calculateGradient(pos.x, pos.y); | |
| // --- ADAPTIVE LOGIC --- | |
| if (isAdaptive) { | |
| if (currentLoss > prevLoss * 1.05) { | |
| learningRate *= 0.5; | |
| } | |
| else if (Math.abs(currentLoss - prevLoss) < 0.00001 && grad.magnitude < 0.01) { | |
| learningRate *= 1.2; | |
| } | |
| learningRate = Math.max(0.001, Math.min(0.5, learningRate)); | |
| updateLRElement(); | |
| } | |
| // Momentum Logic | |
| velocity.x = momentum * velocity.x - learningRate * grad.x; | |
| velocity.y = momentum * velocity.y - learningRate * grad.y; | |
| pos.x = Math.max(-5, Math.min(5, pos.x + velocity.x)); | |
| pos.y = Math.max(-5, Math.min(5, pos.y + velocity.y)); | |
| path.push({...pos}); | |
| steps++; | |
| prevLoss = currentLoss; | |
| updateUI(); | |
| const dist = Math.sqrt(Math.pow(pos.x - currentLevel.min.x, 2) + Math.pow(pos.y - currentLevel.min.y, 2)); | |
| if (dist < currentLevel.winRadius) { | |
| stopDescent(); | |
| showVictory(); | |
| } else if (steps > 3000) { | |
| stopDescent(); | |
| } | |
| } | |
| function updateLRElement() { | |
| document.getElementById('lr-slider').value = learningRate; | |
| document.getElementById('lr-value').innerText = learningRate.toFixed(3); | |
| } | |
| function updateUI() { | |
| document.getElementById('pos-display').innerText = `X: ${pos.x.toFixed(2)} Y: ${pos.y.toFixed(2)}`; | |
| document.getElementById('steps-val').innerText = steps; | |
| const lossVal = currentLevel.loss(pos.x, pos.y); | |
| document.getElementById('loss-val').innerText = lossVal.toFixed(2); | |
| const dist = Math.sqrt(Math.pow(pos.x - currentLevel.min.x, 2) + Math.pow(pos.y - currentLevel.min.y, 2)); | |
| document.getElementById('dist-val').innerText = dist.toFixed(3); | |
| document.getElementById('progress-bar').style.width = `${Math.max(5, Math.min(100, (1 - dist / 7) * 100))}%`; | |
| const ballZ = calculateVizHeight(pos.x, pos.y); | |
| ball.position.set(pos.x, ballZ + BALL_FLOAT, pos.y); | |
| updatePath(); | |
| } | |
| function toggleAdaptive() { | |
| isAdaptive = !isAdaptive; | |
| const bg = document.getElementById('adaptive-toggle-bg'); | |
| const dot = document.getElementById('adaptive-toggle-dot'); | |
| if (isAdaptive) { | |
| bg.classList.replace('bg-slate-700', 'bg-cyan-400'); | |
| dot.style.left = '22px'; | |
| } else { | |
| bg.classList.replace('bg-cyan-400', 'bg-slate-700'); | |
| dot.style.left = '4px'; | |
| } | |
| } | |
| function startDescent() { | |
| isRunning = true; | |
| document.getElementById('toggle-text').innerText = 'Pause'; | |
| document.getElementById('status-text').innerText = 'Descending...'; | |
| document.getElementById('status-text').className = 'text-xs font-mono text-yellow-400 mt-1 uppercase'; | |
| prevLoss = currentLevel.loss(pos.x, pos.y); | |
| loopId = setInterval(step, 50); | |
| } | |
| function stopDescent() { | |
| isRunning = false; | |
| document.getElementById('toggle-text').innerText = 'Start'; | |
| document.getElementById('status-text').innerText = 'Paused'; | |
| document.getElementById('status-text').className = 'text-xs font-mono text-cyan-400 mt-1 uppercase'; | |
| clearInterval(loopId); | |
| } | |
| function showVictory() { | |
| document.getElementById('modal-level-name').innerText = currentLevel.name; | |
| document.getElementById('modal-steps').innerText = steps; | |
| document.getElementById('victory-modal').classList.remove('hidden'); | |
| } | |
| function reset() { | |
| stopDescent(); | |
| if(currentLevel.id === 3) pos = { x: -3, y: -3 }; | |
| else if(currentLevel.id === 4) pos = { x: 1, y: 1 }; | |
| else pos = { x: (Math.random() - 0.5) * 8, y: (Math.random() - 0.5) * 8 }; | |
| velocity = { x: 0, y: 0 }; | |
| path = [{...pos}]; | |
| steps = 0; | |
| updateUI(); | |
| } | |
| // --- EVENT HANDLERS --- | |
| document.getElementById('btn-toggle').onclick = () => isRunning ? stopDescent() : startDescent(); | |
| document.getElementById('btn-step').onclick = step; | |
| document.getElementById('btn-reset').onclick = reset; | |
| document.getElementById('lr-slider').oninput = (e) => { | |
| learningRate = parseFloat(e.target.value); | |
| document.getElementById('lr-value').innerText = learningRate.toFixed(3); | |
| }; | |
| document.getElementById('mom-slider').oninput = (e) => { | |
| momentum = parseFloat(e.target.value); | |
| document.getElementById('mom-value').innerText = momentum.toFixed(2); | |
| }; | |
| function toggleInfo() { | |
| const content = document.getElementById('info-content'); | |
| content.classList.toggle('hidden'); | |
| } | |
| function closeModal() { | |
| document.getElementById('victory-modal').classList.add('hidden'); | |
| reset(); | |
| } | |
| function nextLevel() { | |
| const idx = LEVELS.findIndex(l => l.id === currentLevel.id); | |
| const next = LEVELS[(idx + 1) % LEVELS.length]; | |
| selectLevel(next.id); | |
| document.getElementById('victory-modal').classList.add('hidden'); | |
| } | |
| function selectLevel(id) { | |
| currentLevel = LEVELS.find(l => l.id === id); | |
| // Apply Presets if they exist | |
| if (currentLevel.presets) { | |
| learningRate = currentLevel.presets.lr; | |
| momentum = currentLevel.presets.mom; | |
| // Update UI Controls | |
| document.getElementById('lr-slider').value = learningRate; | |
| document.getElementById('lr-value').innerText = learningRate.toFixed(3); | |
| document.getElementById('mom-slider').value = momentum; | |
| document.getElementById('mom-value').innerText = momentum.toFixed(2); | |
| } | |
| renderLevels(); | |
| updateSurface(); | |
| updateGoalMarker(); | |
| reset(); | |
| } | |
| function renderLevels() { | |
| const list = document.getElementById('level-list'); | |
| list.innerHTML = LEVELS.map(l => ` | |
| <button onclick="selectLevel(${l.id})" class="w-full text-left p-3 rounded-lg transition-all border ${currentLevel.id === l.id ? 'bg-cyan-400/10 border-cyan-400/50' : 'border-transparent hover:bg-slate-800'}"> | |
| <div class="flex justify-between items-center mb-1"> | |
| <span class="font-bold text-sm ${currentLevel.id === l.id ? 'text-cyan-400' : ''}">${l.name}</span> | |
| <span class="text-[9px] px-1.5 py-0.5 rounded border ${l.difficulty === 'easy' ? 'border-green-500/30 text-green-400' : l.difficulty === 'medium' ? 'border-yellow-500/30 text-yellow-400' : 'border-red-500/30 text-red-400'} uppercase">${l.difficulty}</span> | |
| </div> | |
| </button> | |
| `).join(''); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (isRunning) { | |
| const s = 1 + Math.sin(Date.now() * 0.01) * 0.15; | |
| ball.scale.setScalar(s); | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| window.addEventListener('resize', () => { | |
| camera.aspect = container.clientWidth / container.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| }); | |
| renderLevels(); | |
| updateSurface(); | |
| updateGoalMarker(); | |
| updateUI(); | |
| animate(); | |
| // --- MOUSE & TOUCH CONTROLS --- | |
| let isMouseDown = false; | |
| let prevMouse = { x: 0, y: 0 }; | |
| // Mouse Events | |
| container.addEventListener('mousedown', (e) => { | |
| isMouseDown = true; | |
| prevMouse = { x: e.clientX, y: e.clientY }; | |
| }); | |
| window.addEventListener('mouseup', () => isMouseDown = false); | |
| window.addEventListener('mousemove', (e) => handleCameraRotate(e.clientX, e.clientY)); | |
| // Touch Events (Mobile) | |
| container.addEventListener('touchstart', (e) => { | |
| if(e.touches.length === 1) { | |
| isMouseDown = true; | |
| prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY }; | |
| } | |
| }, { passive: false }); | |
| window.addEventListener('touchend', () => isMouseDown = false); | |
| window.addEventListener('touchmove', (e) => { | |
| if(e.touches.length === 1 && isMouseDown) { | |
| handleCameraRotate(e.touches[0].clientX, e.touches[0].clientY); | |
| } | |
| }, { passive: false }); | |
| // Common Rotation Logic | |
| function handleCameraRotate(clientX, clientY) { | |
| if (!isMouseDown) return; | |
| const dx = (clientX - prevMouse.x) * 0.005; | |
| const dy = (clientY - prevMouse.y) * 0.005; | |
| const radius = camera.position.length(); | |
| let phi = Math.atan2(camera.position.x, camera.position.z); | |
| let theta = Math.acos(camera.position.y / radius); | |
| phi -= dx; | |
| theta = Math.max(0.1, Math.min(Math.PI / 2.1, theta - dy)); | |
| camera.position.x = radius * Math.sin(theta) * Math.sin(phi); | |
| camera.position.y = radius * Math.cos(theta); | |
| camera.position.z = radius * Math.sin(theta) * Math.cos(phi); | |
| camera.lookAt(0, 0, 0); | |
| prevMouse = { x: clientX, y: clientY }; | |
| } | |
| </script> | |
| </body> | |
| </html> |