Sidebar audio visualizer with 5 reactive styles and settings toggle
This commit is contained in:
parent
a682f814f7
commit
40521fa499
3 changed files with 427 additions and 1 deletions
|
|
@ -235,6 +235,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar Audio Visualizer (right edge, outside sidebar to avoid overflow clip) -->
|
||||||
|
<div class="sidebar-visualizer" id="sidebar-visualizer"></div>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- Dashboard Page -->
|
<!-- Dashboard Page -->
|
||||||
|
|
@ -3906,6 +3909,18 @@
|
||||||
<label>Custom Color:</label>
|
<label>Custom Color:</label>
|
||||||
<input type="color" id="accent-custom-color" value="#1db954">
|
<input type="color" id="accent-custom-color" value="#1db954">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Sidebar Visualizer:</label>
|
||||||
|
<select id="sidebar-visualizer-type" class="form-select">
|
||||||
|
<option value="bars">Bars</option>
|
||||||
|
<option value="wave">Wave</option>
|
||||||
|
<option value="spectrum">Spectrum</option>
|
||||||
|
<option value="mirror">Mirror</option>
|
||||||
|
<option value="equalizer">Equalizer</option>
|
||||||
|
<option value="none">None</option>
|
||||||
|
</select>
|
||||||
|
<small style="color: #888;">Audio-reactive visualizer on the sidebar edge when music is playing</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Database Settings -->
|
<!-- Database Settings -->
|
||||||
|
|
|
||||||
|
|
@ -1799,6 +1799,7 @@ function clearTrack() {
|
||||||
renderNpQueue();
|
renderNpQueue();
|
||||||
updateNpPrevNextButtons();
|
updateNpPrevNextButtons();
|
||||||
updateMediaSessionPlaybackState();
|
updateMediaSessionPlaybackState();
|
||||||
|
stopSidebarVisualizer();
|
||||||
if (npModalOpen) closeNowPlayingModal();
|
if (npModalOpen) closeNowPlayingModal();
|
||||||
|
|
||||||
console.log('🧹 Track cleared and media player reset');
|
console.log('🧹 Track cleared and media player reset');
|
||||||
|
|
@ -1814,6 +1815,14 @@ function setPlayingState(playing) {
|
||||||
if (pauseIcon) pauseIcon.style.display = playing ? '' : 'none';
|
if (pauseIcon) pauseIcon.style.display = playing ? '' : 'none';
|
||||||
updateNpPlayButton();
|
updateNpPlayButton();
|
||||||
updateMediaSessionPlaybackState();
|
updateMediaSessionPlaybackState();
|
||||||
|
|
||||||
|
// Sidebar audio visualizer
|
||||||
|
if (playing) {
|
||||||
|
npInitVisualizer();
|
||||||
|
startSidebarVisualizer();
|
||||||
|
} else {
|
||||||
|
stopSidebarVisualizer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePlayPause() {
|
async function handlePlayPause() {
|
||||||
|
|
@ -3517,6 +3526,312 @@ function npStopVisualizerLoop() {
|
||||||
bars.forEach(bar => { bar.style.transform = 'scaleY(0.125)'; });
|
bars.forEach(bar => { bar.style.transform = 'scaleY(0.125)'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// SIDEBAR AUDIO VISUALIZER
|
||||||
|
// ===============================
|
||||||
|
|
||||||
|
let sidebarVizAnimFrame = null;
|
||||||
|
let sidebarVisualizerType = 'bars'; // bars | wave | spectrum | mirror | equalizer | none
|
||||||
|
const SIDEBAR_VIZ_BAR_COUNT = 32;
|
||||||
|
|
||||||
|
let _sidebarVizBuiltType = null;
|
||||||
|
|
||||||
|
function buildSidebarVizElements(type) {
|
||||||
|
const container = document.getElementById('sidebar-visualizer');
|
||||||
|
if (!container) return;
|
||||||
|
if (_sidebarVizBuiltType === type && container.children.length > 0) return;
|
||||||
|
_sidebarVizBuiltType = type;
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.className = 'sidebar-visualizer';
|
||||||
|
|
||||||
|
if (type === 'bars') {
|
||||||
|
container.classList.add('viz-bars');
|
||||||
|
for (let i = 0; i < SIDEBAR_VIZ_BAR_COUNT; i++) {
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'sidebar-viz-bar';
|
||||||
|
container.appendChild(bar);
|
||||||
|
}
|
||||||
|
} else if (type === 'wave' || type === 'spectrum') {
|
||||||
|
container.classList.add('viz-canvas');
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.className = 'sidebar-viz-canvas';
|
||||||
|
canvas.width = 10;
|
||||||
|
canvas.height = 600;
|
||||||
|
container.appendChild(canvas);
|
||||||
|
} else if (type === 'mirror') {
|
||||||
|
container.classList.add('viz-mirror');
|
||||||
|
for (let i = 0; i < SIDEBAR_VIZ_BAR_COUNT; i++) {
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'sidebar-viz-mirror-bar';
|
||||||
|
container.appendChild(bar);
|
||||||
|
}
|
||||||
|
} else if (type === 'equalizer') {
|
||||||
|
container.classList.add('viz-equalizer');
|
||||||
|
for (let i = 0; i < SIDEBAR_VIZ_BAR_COUNT; i++) {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'sidebar-viz-eq-wrap';
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'sidebar-viz-eq-bar';
|
||||||
|
const peak = document.createElement('div');
|
||||||
|
peak.className = 'sidebar-viz-eq-peak';
|
||||||
|
wrap.appendChild(bar);
|
||||||
|
wrap.appendChild(peak);
|
||||||
|
container.appendChild(wrap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSidebarVisualizer() {
|
||||||
|
const type = sidebarVisualizerType;
|
||||||
|
if (type === 'none') return;
|
||||||
|
|
||||||
|
const container = document.getElementById('sidebar-visualizer');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
buildSidebarVizElements(type);
|
||||||
|
container.classList.add('active');
|
||||||
|
|
||||||
|
if (sidebarVizAnimFrame) return;
|
||||||
|
if (!npAnalyser) return;
|
||||||
|
|
||||||
|
const bufferLength = npAnalyser.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
|
const hueStart = 200, hueRange = 160;
|
||||||
|
|
||||||
|
// Helper: average frequency bins for a given segment index
|
||||||
|
function getBinValue(i, count) {
|
||||||
|
const binsPerSeg = Math.max(1, Math.floor((bufferLength - 1) / count));
|
||||||
|
let sum = 0;
|
||||||
|
const start = i * binsPerSeg + 1;
|
||||||
|
for (let b = 0; b < binsPerSeg; b++) sum += dataArray[Math.min(start + b, bufferLength - 1)];
|
||||||
|
return (sum / binsPerSeg) / 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bars ──
|
||||||
|
if (type === 'bars') {
|
||||||
|
const bars = container.querySelectorAll('.sidebar-viz-bar');
|
||||||
|
if (bars.length === 0) return;
|
||||||
|
function drawBars() {
|
||||||
|
sidebarVizAnimFrame = requestAnimationFrame(drawBars);
|
||||||
|
npAnalyser.getByteFrequencyData(dataArray);
|
||||||
|
for (let i = 0; i < bars.length; i++) {
|
||||||
|
const value = getBinValue(i, bars.length);
|
||||||
|
const scale = Math.max(0.08, value);
|
||||||
|
const hue = (hueStart + (i / bars.length) * hueRange + value * 30) % 360;
|
||||||
|
bars[i].style.transform = `scaleX(${scale})`;
|
||||||
|
bars[i].style.backgroundColor = `hsla(${hue}, 80%, ${50 + value * 15}%, ${0.5 + value * 0.5})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawBars();
|
||||||
|
|
||||||
|
// ── Wave ──
|
||||||
|
} else if (type === 'wave') {
|
||||||
|
const canvas = container.querySelector('.sidebar-viz-canvas');
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
let hueOffset = 0;
|
||||||
|
function drawWave() {
|
||||||
|
sidebarVizAnimFrame = requestAnimationFrame(drawWave);
|
||||||
|
const ch = container.clientHeight;
|
||||||
|
if (ch > 0 && canvas.height !== ch) canvas.height = ch;
|
||||||
|
npAnalyser.getByteFrequencyData(dataArray);
|
||||||
|
const w = canvas.width, h = canvas.height;
|
||||||
|
if (h === 0) return;
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
let totalEnergy = 0;
|
||||||
|
for (let i = 1; i < bufferLength; i++) totalEnergy += dataArray[i];
|
||||||
|
const avgEnergy = totalEnergy / (bufferLength - 1) / 255;
|
||||||
|
hueOffset = (hueOffset + 0.5) % 360;
|
||||||
|
|
||||||
|
const segments = 64;
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const y = (i / segments) * h;
|
||||||
|
const binIdx = Math.min(Math.floor((i / segments) * (bufferLength - 1)) + 1, bufferLength - 1);
|
||||||
|
const value = dataArray[binIdx] / 255;
|
||||||
|
const x = (w / 2) + Math.sin((i / segments) * Math.PI * 4 + Date.now() * 0.003) * value * (w - 2) * 0.4;
|
||||||
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
const grad = ctx.createLinearGradient(0, 0, 0, h);
|
||||||
|
grad.addColorStop(0, `hsla(${hueOffset + 200}, 80%, 60%, ${0.3 + avgEnergy * 0.7})`);
|
||||||
|
grad.addColorStop(0.5, `hsla(${hueOffset + 280}, 80%, 55%, ${0.3 + avgEnergy * 0.7})`);
|
||||||
|
grad.addColorStop(1, `hsla(${hueOffset + 360}, 80%, 60%, ${0.3 + avgEnergy * 0.7})`);
|
||||||
|
ctx.strokeStyle = grad;
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.globalAlpha = 0.15 + avgEnergy * 0.2;
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
drawWave();
|
||||||
|
|
||||||
|
// ── Spectrum (mountain/terrain fill) ──
|
||||||
|
} else if (type === 'spectrum') {
|
||||||
|
const canvas = container.querySelector('.sidebar-viz-canvas');
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
let hueOffset = 0;
|
||||||
|
// Smoothed values for fluid motion
|
||||||
|
const smoothed = new Float32Array(64);
|
||||||
|
|
||||||
|
function drawSpectrum() {
|
||||||
|
sidebarVizAnimFrame = requestAnimationFrame(drawSpectrum);
|
||||||
|
const ch = container.clientHeight;
|
||||||
|
if (ch > 0 && canvas.height !== ch) canvas.height = ch;
|
||||||
|
npAnalyser.getByteFrequencyData(dataArray);
|
||||||
|
const w = canvas.width, h = canvas.height;
|
||||||
|
if (h === 0) return;
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
hueOffset = (hueOffset + 0.3) % 360;
|
||||||
|
const segments = smoothed.length;
|
||||||
|
|
||||||
|
// Smooth the frequency data
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const binIdx = Math.min(Math.floor((i / segments) * (bufferLength - 1)) + 1, bufferLength - 1);
|
||||||
|
const target = dataArray[binIdx] / 255;
|
||||||
|
smoothed[i] += (target - smoothed[i]) * 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw filled mountain shape from left edge
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, 0);
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const y = (i / segments) * h;
|
||||||
|
const value = i < segments ? smoothed[i] : smoothed[segments - 1];
|
||||||
|
const x = value * w * 0.95;
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.lineTo(0, h);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
// Gradient fill
|
||||||
|
const fillGrad = ctx.createLinearGradient(0, 0, 0, h);
|
||||||
|
fillGrad.addColorStop(0, `hsla(${hueOffset + 200}, 85%, 55%, 0.7)`);
|
||||||
|
fillGrad.addColorStop(0.25, `hsla(${hueOffset + 240}, 80%, 50%, 0.6)`);
|
||||||
|
fillGrad.addColorStop(0.5, `hsla(${hueOffset + 290}, 85%, 50%, 0.65)`);
|
||||||
|
fillGrad.addColorStop(0.75, `hsla(${hueOffset + 330}, 80%, 50%, 0.6)`);
|
||||||
|
fillGrad.addColorStop(1, `hsla(${hueOffset + 360}, 85%, 55%, 0.7)`);
|
||||||
|
ctx.fillStyle = fillGrad;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Bright edge line
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, 0);
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const y = (i / segments) * h;
|
||||||
|
const value = i < segments ? smoothed[i] : smoothed[segments - 1];
|
||||||
|
ctx.lineTo(value * w * 0.95, y);
|
||||||
|
}
|
||||||
|
const lineGrad = ctx.createLinearGradient(0, 0, 0, h);
|
||||||
|
lineGrad.addColorStop(0, `hsla(${hueOffset + 200}, 90%, 70%, 0.9)`);
|
||||||
|
lineGrad.addColorStop(0.5, `hsla(${hueOffset + 290}, 90%, 65%, 0.9)`);
|
||||||
|
lineGrad.addColorStop(1, `hsla(${hueOffset + 360}, 90%, 70%, 0.9)`);
|
||||||
|
ctx.strokeStyle = lineGrad;
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Outer glow
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.globalAlpha = 0.2;
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
drawSpectrum();
|
||||||
|
|
||||||
|
// ── Mirror (bars from center outward) ──
|
||||||
|
} else if (type === 'mirror') {
|
||||||
|
const bars = container.querySelectorAll('.sidebar-viz-mirror-bar');
|
||||||
|
if (bars.length === 0) return;
|
||||||
|
function drawMirror() {
|
||||||
|
sidebarVizAnimFrame = requestAnimationFrame(drawMirror);
|
||||||
|
npAnalyser.getByteFrequencyData(dataArray);
|
||||||
|
const half = Math.floor(bars.length / 2);
|
||||||
|
for (let i = 0; i < half; i++) {
|
||||||
|
const value = getBinValue(i, half);
|
||||||
|
const scale = Math.max(0.06, value);
|
||||||
|
const hue = (hueStart + (i / half) * hueRange + value * 30) % 360;
|
||||||
|
const color = `hsla(${hue}, 80%, ${50 + value * 15}%, ${0.5 + value * 0.5})`;
|
||||||
|
// Top half — mirror index from center
|
||||||
|
const topIdx = half - 1 - i;
|
||||||
|
const bottomIdx = half + i;
|
||||||
|
bars[topIdx].style.transform = `scaleX(${scale})`;
|
||||||
|
bars[topIdx].style.backgroundColor = color;
|
||||||
|
if (bottomIdx < bars.length) {
|
||||||
|
bars[bottomIdx].style.transform = `scaleX(${scale})`;
|
||||||
|
bars[bottomIdx].style.backgroundColor = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawMirror();
|
||||||
|
|
||||||
|
// ── Equalizer (bars with falling peak indicators) ──
|
||||||
|
} else if (type === 'equalizer') {
|
||||||
|
const wraps = container.querySelectorAll('.sidebar-viz-eq-wrap');
|
||||||
|
if (wraps.length === 0) return;
|
||||||
|
const peaks = new Float32Array(wraps.length);
|
||||||
|
const peakVelocity = new Float32Array(wraps.length);
|
||||||
|
|
||||||
|
function drawEqualizer() {
|
||||||
|
sidebarVizAnimFrame = requestAnimationFrame(drawEqualizer);
|
||||||
|
npAnalyser.getByteFrequencyData(dataArray);
|
||||||
|
for (let i = 0; i < wraps.length; i++) {
|
||||||
|
const value = getBinValue(i, wraps.length);
|
||||||
|
const scale = Math.max(0.06, value);
|
||||||
|
const hue = (hueStart + (i / wraps.length) * hueRange + value * 30) % 360;
|
||||||
|
const barEl = wraps[i].querySelector('.sidebar-viz-eq-bar');
|
||||||
|
const peakEl = wraps[i].querySelector('.sidebar-viz-eq-peak');
|
||||||
|
|
||||||
|
barEl.style.transform = `scaleX(${scale})`;
|
||||||
|
barEl.style.backgroundColor = `hsla(${hue}, 80%, ${50 + value * 15}%, ${0.5 + value * 0.5})`;
|
||||||
|
|
||||||
|
// Peak hold with gravity
|
||||||
|
if (value > peaks[i]) {
|
||||||
|
peaks[i] = value;
|
||||||
|
peakVelocity[i] = 0;
|
||||||
|
} else {
|
||||||
|
peakVelocity[i] += 0.002; // gravity
|
||||||
|
peaks[i] = Math.max(0, peaks[i] - peakVelocity[i]);
|
||||||
|
}
|
||||||
|
const peakPos = Math.max(0.06, peaks[i]);
|
||||||
|
peakEl.style.left = `${peakPos * 100}%`;
|
||||||
|
peakEl.style.backgroundColor = `hsla(${hue}, 90%, 75%, ${0.6 + peaks[i] * 0.4})`;
|
||||||
|
peakEl.style.boxShadow = `0 0 4px hsla(${hue}, 90%, 70%, ${peaks[i] * 0.5})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawEqualizer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSidebarVisualizer() {
|
||||||
|
if (sidebarVizAnimFrame) {
|
||||||
|
cancelAnimationFrame(sidebarVizAnimFrame);
|
||||||
|
sidebarVizAnimFrame = null;
|
||||||
|
}
|
||||||
|
const container = document.getElementById('sidebar-visualizer');
|
||||||
|
if (container) {
|
||||||
|
container.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for visualizer type changes in settings — use isPlaying (not wasRunning)
|
||||||
|
// so switching from 'none' to a real type while music plays starts the visualizer
|
||||||
|
document.addEventListener('change', (e) => {
|
||||||
|
if (e.target.id === 'sidebar-visualizer-type') {
|
||||||
|
const newType = e.target.value;
|
||||||
|
stopSidebarVisualizer();
|
||||||
|
_sidebarVizBuiltType = null; // force rebuild for new type
|
||||||
|
sidebarVisualizerType = newType;
|
||||||
|
if (isPlaying && newType !== 'none') {
|
||||||
|
npInitVisualizer();
|
||||||
|
startSidebarVisualizer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// RADIO MODE
|
// RADIO MODE
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
@ -3995,6 +4310,12 @@ async function loadSettingsData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sidebar visualizer type
|
||||||
|
const vizType = settings.ui_appearance?.sidebar_visualizer || 'bars';
|
||||||
|
const vizSelect = document.getElementById('sidebar-visualizer-type');
|
||||||
|
if (vizSelect) vizSelect.value = vizType;
|
||||||
|
sidebarVisualizerType = vizType;
|
||||||
|
|
||||||
// Populate Logging information (read-only)
|
// Populate Logging information (read-only)
|
||||||
document.getElementById('log-level-display').textContent = settings.logging?.level || 'INFO';
|
document.getElementById('log-level-display').textContent = settings.logging?.level || 'INFO';
|
||||||
document.getElementById('log-path-display').textContent = settings.logging?.path || 'logs/app.log';
|
document.getElementById('log-path-display').textContent = settings.logging?.path || 'logs/app.log';
|
||||||
|
|
@ -4685,7 +5006,8 @@ async function saveSettings(quiet = false) {
|
||||||
},
|
},
|
||||||
ui_appearance: {
|
ui_appearance: {
|
||||||
accent_preset: document.getElementById('accent-preset')?.value || '#1db954',
|
accent_preset: document.getElementById('accent-preset')?.value || '#1db954',
|
||||||
accent_color: document.getElementById('accent-custom-color')?.value || '#1db954'
|
accent_color: document.getElementById('accent-custom-color')?.value || '#1db954',
|
||||||
|
sidebar_visualizer: document.getElementById('sidebar-visualizer-type')?.value || 'bars'
|
||||||
},
|
},
|
||||||
youtube: {
|
youtube: {
|
||||||
cookies_browser: document.getElementById('youtube-cookies-browser').value,
|
cookies_browser: document.getElementById('youtube-cookies-browser').value,
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,95 @@ body {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar Audio Visualizer — fixed strip on right edge of sidebar */
|
||||||
|
.sidebar-visualizer {
|
||||||
|
position: fixed;
|
||||||
|
left: 240px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 10px;
|
||||||
|
z-index: 100;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.sidebar-visualizer.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type: Bars */
|
||||||
|
.sidebar-visualizer.viz-bars {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
.sidebar-viz-bar {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
transform: scaleX(0.1);
|
||||||
|
transform-origin: left center;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type: Wave & Spectrum (canvas) */
|
||||||
|
.sidebar-visualizer.viz-canvas {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.sidebar-viz-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type: Mirror (bars from center) */
|
||||||
|
.sidebar-visualizer.viz-mirror {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
.sidebar-viz-mirror-bar {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
transform: scaleX(0.06);
|
||||||
|
transform-origin: left center;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type: Equalizer (bars with peak hold) */
|
||||||
|
.sidebar-visualizer.viz-equalizer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
.sidebar-viz-eq-wrap {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.sidebar-viz-eq-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transform: scaleX(0.06);
|
||||||
|
transform-origin: left center;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.sidebar-viz-eq-peak {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 6%;
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 1px;
|
||||||
|
will-change: left, background-color, box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-visualizer { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Sidebar scrollbar webkit styling */
|
/* Sidebar scrollbar webkit styling */
|
||||||
.sidebar::-webkit-scrollbar {
|
.sidebar::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue