|
| 1 | +document.addEventListener('DOMContentLoaded', () => { |
| 2 | + // --- Theme Toggle Logic --- |
| 3 | + const themeBtn = document.getElementById('theme-toggle'); |
| 4 | + const sunIcon = document.getElementById('icon-sun'); |
| 5 | + const moonIcon = document.getElementById('icon-moon'); |
| 6 | + let encodeChart = null; |
| 7 | + |
| 8 | + function toggleTheme() { |
| 9 | + document.body.classList.toggle('light-mode'); |
| 10 | + const isLight = document.body.classList.contains('light-mode'); |
| 11 | + |
| 12 | + // Icon visibility swap |
| 13 | + if(isLight) { |
| 14 | + sunIcon.classList.remove('hidden'); |
| 15 | + moonIcon.classList.add('hidden'); |
| 16 | + } else { |
| 17 | + sunIcon.classList.add('hidden'); |
| 18 | + moonIcon.classList.remove('hidden'); |
| 19 | + } |
| 20 | + |
| 21 | + // Update Chart Colors |
| 22 | + if(encodeChart) { |
| 23 | + Chart.defaults.color = isLight ? '#475569' : '#94a3b8'; |
| 24 | + Chart.defaults.borderColor = isLight ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)'; |
| 25 | + encodeChart.update(); |
| 26 | + } |
| 27 | + } |
| 28 | + |
| 29 | + if(themeBtn) { |
| 30 | + themeBtn.addEventListener('click', toggleTheme); |
| 31 | + } |
| 32 | + |
| 33 | + // --- Navigation Logic --- |
| 34 | + const navBtns = document.querySelectorAll('.nav-btn'); |
| 35 | + const sections = document.querySelectorAll('.content-section'); |
| 36 | + |
| 37 | + navBtns.forEach(btn => { |
| 38 | + btn.addEventListener('click', () => { |
| 39 | + navBtns.forEach(b => { |
| 40 | + b.classList.remove('tab-active'); |
| 41 | + b.classList.add('text-slate-400'); |
| 42 | + }); |
| 43 | + btn.classList.add('tab-active'); |
| 44 | + btn.classList.remove('text-slate-400'); |
| 45 | + |
| 46 | + sections.forEach(sec => sec.classList.add('hidden-section')); |
| 47 | + const target = document.getElementById(btn.dataset.target); |
| 48 | + if(target) target.classList.remove('hidden-section'); |
| 49 | + }); |
| 50 | + }); |
| 51 | + |
| 52 | + // --- Chart.js Initialization --- |
| 53 | + Chart.defaults.color = '#94a3b8'; |
| 54 | + Chart.defaults.font.family = "'JetBrains Mono', monospace"; |
| 55 | + |
| 56 | + const ctx = document.getElementById('encodeChart').getContext('2d'); |
| 57 | + encodeChart = new Chart(ctx, { |
| 58 | + type: 'bar', |
| 59 | + data: { |
| 60 | + labels: ['Serial', 'Parallel'], |
| 61 | + datasets: [ |
| 62 | + { label: 'Chunk 1', data: [240, 60], backgroundColor: '#06b6d4', barThickness: 40 }, |
| 63 | + { label: 'Chunk 2', data: [0, 60], backgroundColor: '#8b5cf6', barThickness: 40 }, |
| 64 | + { label: 'Stitch Overhead', data: [0, 5], backgroundColor: '#d946ef', barThickness: 40 } |
| 65 | + ] |
| 66 | + }, |
| 67 | + options: { |
| 68 | + indexAxis: 'y', |
| 69 | + responsive: true, |
| 70 | + maintainAspectRatio: false, |
| 71 | + plugins: { |
| 72 | + legend: { position: 'bottom' }, |
| 73 | + tooltip: { callbacks: { label: (c) => c.dataset.label } } |
| 74 | + }, |
| 75 | + scales: { |
| 76 | + x: { stacked: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { display: false } }, |
| 77 | + y: { stacked: true, grid: { display: false } } |
| 78 | + } |
| 79 | + } |
| 80 | + }); |
| 81 | + |
| 82 | + // --- BPP Calculator Logic --- |
| 83 | + const MIN_BPP = 0.04; |
| 84 | + const MB_TO_BITS = 8 * 1024 * 1024; |
| 85 | + |
| 86 | + function calculateBPP() { |
| 87 | + const targetMB = parseFloat(document.getElementById('calc-mb').value) || 100; |
| 88 | + const duration = parseFloat(document.getElementById('calc-dur').value) || 120; |
| 89 | + const srcW = parseInt(document.getElementById('calc-w').value) || 1920; |
| 90 | + const srcH = parseInt(document.getElementById('calc-h').value) || 1080; |
| 91 | + const srcFps = parseFloat(document.getElementById('calc-fps').value) || 60; |
| 92 | + |
| 93 | + const targetBits = targetMB * MB_TO_BITS; |
| 94 | + const aspectRatio = srcW / srcH; |
| 95 | + |
| 96 | + // Filter Options |
| 97 | + let heightOptions = [2160, 1440, 1080, 720]; |
| 98 | + let fpsOptions = [120.0, 90.0, 60.0]; |
| 99 | + |
| 100 | + let validHeights = heightOptions.filter(h => h <= srcH); |
| 101 | + if (!validHeights.includes(srcH)) validHeights.unshift(srcH); |
| 102 | + |
| 103 | + let validFps = fpsOptions.filter(f => f <= srcFps); |
| 104 | + if (!validFps.includes(srcFps)) validFps.unshift(srcFps); |
| 105 | + |
| 106 | + let candidates = []; |
| 107 | + validHeights.forEach(h => { |
| 108 | + const w = Math.round(h * aspectRatio); |
| 109 | + validFps.forEach(f => { |
| 110 | + const pixelsPerSec = w * h * f; |
| 111 | + const bpp = targetBits / (duration * pixelsPerSec); |
| 112 | + candidates.push({ h, w, f, bpp, pps: pixelsPerSec, fpsPriority: f >= 60 }); |
| 113 | + }); |
| 114 | + }); |
| 115 | + |
| 116 | + // Sort logic: Top candidates > MIN_BPP, then FPS >= 60, then throughput |
| 117 | + let safeOnes = candidates.filter(c => c.bpp >= MIN_BPP); |
| 118 | + safeOnes.sort((a, b) => { |
| 119 | + if (a.fpsPriority !== b.fpsPriority) return a.fpsPriority ? -1 : 1; |
| 120 | + return b.pps - a.pps; |
| 121 | + }); |
| 122 | + |
| 123 | + const best = safeOnes.length > 0 ? safeOnes[0] : candidates[candidates.length - 1]; // Fallback to smallest |
| 124 | + |
| 125 | + // Render Results |
| 126 | + const tbody = document.getElementById('calc-results'); |
| 127 | + if(tbody) { |
| 128 | + tbody.innerHTML = ''; |
| 129 | + candidates.forEach(c => { |
| 130 | + const isBest = c === best; |
| 131 | + const isSafe = c.bpp >= MIN_BPP; |
| 132 | + |
| 133 | + const tr = document.createElement('tr'); |
| 134 | + if (isBest) tr.className = 'bg-cyan-900/30 border-l-2 border-cyan-400'; |
| 135 | + tr.innerHTML = ` |
| 136 | + <td class="px-4 py-3 ${isBest ? 'text-cyan-400 font-bold' : ''}">${c.w}x${c.h}</td> |
| 137 | + <td class="px-4 py-3">${c.f}</td> |
| 138 | + <td class="px-4 py-3 ${isSafe ? 'text-emerald-400' : 'text-rose-400'}">${c.bpp.toFixed(4)}</td> |
| 139 | + <td class="px-4 py-3 uppercase text-[10px]">${isSafe ? '<span class="text-emerald-400">Pass</span>' : '<span class="text-rose-400">Fail</span>'}</td> |
| 140 | + `; |
| 141 | + tbody.appendChild(tr); |
| 142 | + }); |
| 143 | + } |
| 144 | + |
| 145 | + const conc = document.getElementById('calc-conclusion'); |
| 146 | + if(conc) { |
| 147 | + conc.classList.remove('hidden', 'border-emerald-500', 'bg-emerald-900/20', 'border-amber-500', 'bg-amber-900/20'); |
| 148 | + if (best.bpp >= MIN_BPP) { |
| 149 | + conc.classList.add('border-emerald-500', 'bg-emerald-900/20'); |
| 150 | + conc.innerHTML = `<strong>Quality Pass:</strong> Script will use <strong>${best.w}x${best.h} @ ${best.f}fps</strong>. Clarity threshold maintained.`; |
| 151 | + } else { |
| 152 | + conc.classList.add('border-amber-500', 'bg-amber-900/20'); |
| 153 | + conc.innerHTML = `<strong>Downscale Required:</strong> Target size is too restrictive. Scaling to <strong>${best.w}x${best.h} @ ${best.f}fps</strong> to avoid pixelation.`; |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + const debounce = (f, w) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => f(...a), w); }; }; |
| 159 | + const debouncedCalc = debounce(calculateBPP, 500); |
| 160 | + ['calc-mb', 'calc-dur', 'calc-w', 'calc-h', 'calc-fps'].forEach(id => { |
| 161 | + const el = document.getElementById(id); |
| 162 | + if(el) el.addEventListener('input', debouncedCalc); |
| 163 | + }); |
| 164 | + calculateBPP(); |
| 165 | + |
| 166 | + // --- Hardware Matrix Logic --- |
| 167 | + const osData = { |
| 168 | + win: { |
| 169 | + title: "Windows Execution Priority", icon: "fa-windows", color: "text-blue-400", |
| 170 | + chain: [ |
| 171 | + { name: "Nvidia NVENC", desc: "H.265/H.264 via dedicated hardware. Supports parallel 2-pass.", icon: "fa-microchip" }, |
| 172 | + { name: "AMD AMF", desc: "H.265/H.264 via AMD's Media Framework.", icon: "fa-microchip" }, |
| 173 | + { name: "Intel Quick Sync (QSV)", desc: "H.265/H.264 via Intel's integrated GPU.", icon: "fa-microchip" }, |
| 174 | + { name: "CPU Fallback (libx265/libx264)", desc: "Software encoding. Most compatible, but slowest.", icon: "fa-server" } |
| 175 | + ], |
| 176 | + note: "The script probes for encoders for the selected codec (H.265/HEVC or H.264). If dedicated GPU encoding fails or is unavailable, it defaults to CPU." |
| 177 | + }, |
| 178 | + lin: { |
| 179 | + title: "Linux Execution Priority", icon: "fa-linux", color: "text-yellow-400", |
| 180 | + chain: [ |
| 181 | + { name: "Nvidia NVENC", desc: "H.265/H.264 via dedicated hardware. Supports parallel 2-pass.", icon: "fa-microchip" }, |
| 182 | + { name: "VA-API", desc: "Unified API for AMD and Intel hardware acceleration.", icon: "fa-layer-group" }, |
| 183 | + { name: "CPU Fallback (libx265/libx264)", desc: "Software encoding. Most compatible, but slowest.", icon: "fa-server" } |
| 184 | + ], |
| 185 | + note: "VA-API is a versatile API that covers both AMD and Intel hardware on Linux. NVENC is preferred if an Nvidia GPU is present." |
| 186 | + }, |
| 187 | + mac: { |
| 188 | + title: "macOS Execution Priority", icon: "fa-apple", color: "text-slate-200", |
| 189 | + chain: [ |
| 190 | + { name: "Apple VideoToolbox", desc: "Native API for Apple Silicon, AMD, and Intel hardware.", icon: "fa-apple" }, |
| 191 | + { name: "CPU Fallback (libx265/libx264)", desc: "Software encoding. Most compatible, but slowest.", icon: "fa-server" } |
| 192 | + ], |
| 193 | + note: "VideoToolbox is the native macOS framework that abstracts hardware (Apple Silicon, Intel, AMD). Nvidia encoding is not supported on modern macOS." |
| 194 | + } |
| 195 | + }; |
| 196 | + |
| 197 | + function renderChain(k) { |
| 198 | + const d = osData[k]; |
| 199 | + const osTitle = document.getElementById('os-title'); |
| 200 | + const osNote = document.getElementById('os-note'); |
| 201 | + const priorityChain = document.getElementById('priority-chain'); |
| 202 | + |
| 203 | + if(osTitle && osNote && priorityChain) { |
| 204 | + osTitle.innerHTML = `<i class="fa-brands ${d.icon} ${d.color} mr-3"></i>${d.title}`; |
| 205 | + osNote.textContent = d.note; |
| 206 | + priorityChain.innerHTML = ''; |
| 207 | + d.chain.forEach((e, i) => { |
| 208 | + const el = document.createElement('div'); |
| 209 | + el.className = `flex items-center p-4 bg-slate-800/50 rounded-lg border border-slate-700 hover:border-slate-500 transition-all ${i===0?'ring-1 ring-cyan-500/30':''}`; |
| 210 | + el.innerHTML = `<div class="w-8 h-8 rounded-full ${i===0?'bg-cyan-500 text-slate-900':'bg-slate-700 text-white'} flex items-center justify-center font-bold text-sm mr-4">${i+1}</div><div class="flex-grow"><div class="font-bold text-white font-mono text-sm">${e.name}</div><div class="text-xs text-slate-400">${e.desc}</div></div><i class="fa-solid ${e.icon} text-slate-600 text-xl ml-4"></i>`; |
| 211 | + priorityChain.appendChild(el); |
| 212 | + }); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + document.querySelectorAll('.os-btn').forEach(b => b.addEventListener('click', e => { |
| 217 | + document.querySelectorAll('.os-btn').forEach(x => { x.classList.remove('os-btn-active'); x.classList.add('bg-slate-800', 'border-slate-600'); }); |
| 218 | + const btn = e.target.closest('button'); |
| 219 | + if(btn) { |
| 220 | + btn.classList.remove('bg-slate-800', 'border-slate-600'); |
| 221 | + btn.classList.add('os-btn-active'); |
| 222 | + renderChain(btn.id.replace('btn-', '')); |
| 223 | + } |
| 224 | + })); |
| 225 | + |
| 226 | + renderChain('win'); |
| 227 | +}); |
0 commit comments