${subSlug} (${currentIndex + 1}/${albumData.length}):`;
}
const subTags = Array.isArray(sub.tags) ? sub.tags : [];
if (typeof window.renderTags === 'function') {
window.renderTags(subTags);
}
};
const updateInfoModal = () => {
const modal = document.getElementById('info-modal');
if (!modal) return;
const sub = albumData[currentIndex];
if (!sub) return;
const subHeading = modal.querySelector('#info-modal-subheading');
if (subHeading) {
const parentId = subHeading.getAttribute('data-item-id') || container.getAttribute('data-album-id') || '';
const parentSlug = subHeading.getAttribute('data-item-slug') || '';
const subSlug = sub.slug || sub.subf0ck_id || sub.id;
const total = albumData.length;
const idx = currentIndex + 1;
subHeading.innerHTML = `Post ID: ${parentId}${parentSlug ? ` (${parentSlug})` : ''} • Subf0ck: ${subSlug} (${idx}/${total})`;
}
const specsTitle = modal.querySelector('#info-specs-header-title');
if (specsTitle) {
specsTitle.textContent = `Technical Specifications (Subf0ck ${currentIndex + 1}/${albumData.length})`;
}
const fileSizeEl = modal.querySelector('#info-file-size');
if (fileSizeEl) {
let displaySize = sub.size || '';
if (displaySize === 'NaN B' || displaySize === 'NaN') displaySize = '';
if (displaySize && !isNaN(displaySize)) {
const num = Number(displaySize);
if (num > 0) {
const i = Math.min(4, Math.max(0, ~~(Math.log(num) / Math.log(1024))));
displaySize = (num / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
}
fileSizeEl.textContent = displaySize;
}
const dimsCard = modal.querySelector('#info-card-dimensions');
const dimsEl = modal.querySelector('#info-file-dimensions');
if (dimsCard && dimsEl) {
if (sub.width && sub.height) {
dimsEl.textContent = `${sub.width} × ${sub.height} px`;
dimsCard.style.display = '';
} else {
dimsCard.style.display = 'none';
}
}
const mimeEl = modal.querySelector('#info-file-mime');
if (mimeEl) {
mimeEl.textContent = sub.mime || '';
}
const directLink = modal.querySelector('#info-file-direct-link');
if (directLink) {
directLink.href = sub.dest || sub.src || '#';
}
const sourceCard = modal.querySelector('#info-card-source');
if (sourceCard) {
sourceCard.style.display = 'none';
}
const hashCard = modal.querySelector('#info-card-hash');
const hashEl = modal.querySelector('#info-file-hash');
const copyHashBtn = modal.querySelector('#info-copy-hash-btn');
if (hashCard) {
const cleanHash = sub.checksum ? String(sub.checksum).split('_bypass_')[0] : '';
if (cleanHash) {
if (hashEl) hashEl.textContent = cleanHash;
if (copyHashBtn) copyHashBtn.setAttribute('data-hash', cleanHash);
hashCard.style.display = '';
} else {
hashCard.style.display = 'none';
}
}
};
if (imgEl) {
imgEl.addEventListener('load', () => {
const sub = albumData[currentIndex];
if (sub && (!sub.width || !sub.height) && imgEl.naturalWidth) {
sub.width = imgEl.naturalWidth;
sub.height = imgEl.naturalHeight;
updateInfoModal();
}
});
}
if (videoEl) {
videoEl.addEventListener('loadedmetadata', () => {
const sub = albumData[currentIndex];
if (sub && (!sub.width || !sub.height) && videoEl.videoWidth) {
sub.width = videoEl.videoWidth;
sub.height = videoEl.videoHeight;
updateInfoModal();
}
});
}
// Ensure proper initial media display (especially if initial item is video or subf0ck hash was requested)
showImage(currentIndex, 'none', !!initialHash);
if (prevBtn) {
prevBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showImage(currentIndex - 1, 'prev');
});
}
if (nextBtn) {
nextBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showImage(currentIndex + 1, 'next');
});
}
let hasDraggedStrip = false;
if (stripEl && !stripEl._dragInitialized) {
stripEl._dragInitialized = true;
let isDown = false;
let startX = 0;
let startScrollLeft = 0;
let momentumID = null;
let velX = 0;
let lastX = 0;
let lastTime = 0;
stripEl.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isDown = true;
hasDraggedStrip = false;
startX = e.pageX;
startScrollLeft = stripEl.scrollLeft;
lastX = e.pageX;
lastTime = performance.now();
velX = 0;
cancelAnimationFrame(momentumID);
});
window.addEventListener('mousemove', (e) => {
if (!isDown) return;
const now = performance.now();
const walk = e.pageX - startX;
if (Math.abs(walk) > 6) {
hasDraggedStrip = true;
stripEl.classList.add('is-dragging');
e.preventDefault();
stripEl.scrollLeft = startScrollLeft - walk;
const dt = Math.max(1, now - lastTime);
velX = (e.pageX - lastX) / dt;
lastX = e.pageX;
lastTime = now;
}
});
window.addEventListener('mouseup', () => {
if (!isDown) return;
isDown = false;
stripEl.classList.remove('is-dragging');
showStripPeek(6000);
if (hasDraggedStrip && Math.abs(velX) > 0.15) {
let currentVel = velX * 18;
const glide = () => {
if (Math.abs(currentVel) > 0.5) {
stripEl.scrollLeft -= currentVel;
currentVel *= 0.93;
momentumID = requestAnimationFrame(glide);
}
};
momentumID = requestAnimationFrame(glide);
}
setTimeout(() => {
hasDraggedStrip = false;
}, 100);
});
// Touch drag / swipe support for mobile
let touchStartX = 0;
let touchStartScrollLeft = 0;
let isTouchingStrip = false;
stripEl.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
e.stopPropagation();
if (stripTimeout) clearTimeout(stripTimeout);
container.classList.add('strip-peek');
stripEl.classList.add('strip-peek');
isTouchingStrip = true;
hasDraggedStrip = false;
touchStartX = e.touches[0].clientX;
touchStartScrollLeft = stripEl.scrollLeft;
lastX = e.touches[0].clientX;
lastTime = performance.now();
velX = 0;
cancelAnimationFrame(momentumID);
}
}, { passive: true });
stripEl.addEventListener('touchmove', (e) => {
if (!isTouchingStrip || e.touches.length !== 1) return;
e.stopPropagation();
const currentX = e.touches[0].clientX;
const walk = currentX - touchStartX;
if (Math.abs(walk) > 3) {
hasDraggedStrip = true;
stripEl.classList.add('is-dragging');
stripEl.scrollLeft = touchStartScrollLeft - walk;
const now = performance.now();
const dt = Math.max(1, now - lastTime);
velX = (currentX - lastX) / dt;
lastX = currentX;
lastTime = now;
}
}, { passive: true });
stripEl.addEventListener('touchend', (e) => {
if (!isTouchingStrip) return;
e.stopPropagation();
isTouchingStrip = false;
stripEl.classList.remove('is-dragging');
showStripPeek(6000);
if (hasDraggedStrip) {
if (Math.abs(velX) > 0.05) {
let currentVel = velX * 24;
const glide = () => {
if (Math.abs(currentVel) > 0.5) {
stripEl.scrollLeft -= currentVel;
currentVel *= 0.93;
momentumID = requestAnimationFrame(glide);
}
};
momentumID = requestAnimationFrame(glide);
}
setTimeout(() => {
hasDraggedStrip = false;
}, 150);
}
}, { passive: true });
stripEl.addEventListener('touchcancel', (e) => {
e.stopPropagation();
isTouchingStrip = false;
stripEl.classList.remove('is-dragging');
showStripPeek(6000);
setTimeout(() => {
hasDraggedStrip = false;
}, 100);
}, { passive: true });
stripEl.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
stripEl.scrollLeft += e.deltaY;
}
}, { passive: false });
}
thumbItems.forEach((btn) => {
btn.addEventListener('click', (e) => {
if (hasDraggedStrip) {
e.preventDefault();
e.stopPropagation();
return;
}
e.preventDefault();
e.stopPropagation();
const targetIdx = parseInt(btn.getAttribute('data-index'), 10);
if (!isNaN(targetIdx)) {
showImage(targetIdx);
}
});
});
// Touch tap on container reveals thumbnail strip briefly on touch devices
container.addEventListener('click', (e) => {
if (!e.target.closest('.v0ck_settings_menu, .v0ck_hud, .album-btn')) {
showStripPeek(3000);
}
});
// Handle touch swipe for mobile gallery navigation
let touchStartX = null;
let touchStartY = null;
let touchIgnored = false;
container.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
touchIgnored = !!e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn');
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
container.addEventListener('touchend', (e) => {
if (touchStartX === null || touchStartY === null || touchIgnored) {
touchStartX = null;
touchStartY = null;
touchIgnored = false;
return;
}
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const diffX = touchEndX - touchStartX;
const diffY = touchEndY - touchStartY;
// Minimum swipe distance of 50px and predominantly horizontal
if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY) * 1.5) {
if (diffX < 0) {
showImage(currentIndex + 1, 'next');
} else {
showImage(currentIndex - 1, 'prev');
}
}
touchStartX = null;
touchStartY = null;
touchIgnored = false;
}, { passive: true });
// Handle hash change if user navigates back/forward to another subf0ck
window.addEventListener('hashchange', () => {
const newHash = getHashSubf0ckId();
if (!newHash) return;
const foundIdx = albumData.findIndex((item) =>
String(item.slug || '') === newHash ||
String(item.subf0ck_id || '') === newHash ||
String(item.id) === newHash ||
String(item.order_index + 1) === newHash
);
if (foundIdx !== -1 && foundIdx !== currentIndex) {
showImage(foundIdx, 'none', false);
}
});
window._currentActiveAlbumGallery = {
prev: () => showImage(currentIndex - 1, 'prev'),
next: () => showImage(currentIndex + 1, 'next'),
showBySlug: (subKey) => {
if (!subKey || !Array.isArray(albumData)) return false;
const foundIdx = albumData.findIndex((item) =>
String(item.slug || '') === String(subKey) ||
String(item.subf0ck_id || '') === String(subKey) ||
String(item.id) === String(subKey) ||
String(item.order_index + 1) === String(subKey)
);
if (foundIdx !== -1) {
showImage(foundIdx, 'none', true);
return true;
}
return false;
},
updateInfoModal: updateInfoModal,
updateAlbumTags: updateAlbumTags,
getCurrentSubf0ck: () => albumData[currentIndex],
isHovered: false
};
window.albumGallery = window._currentActiveAlbumGallery;
container.addEventListener('mouseenter', () => {
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = true;
});
container.addEventListener('mouseleave', () => {
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = false;
});
};
const syncLocationSubf0ck = () => {
const layout = document.querySelector('.item-layout-container');
const reqSub = layout ? (layout.getAttribute('data-requested-subf0ck') || '') : '';
const hash = (window.location.hash || '').replace(/^#/, '').trim();
const targetSub = hash || reqSub;
if (targetSub) {
if (!hash && reqSub) {
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + reqSub);
}
const locEl = document.querySelector('.location');
if (locEl && !locEl.textContent.includes('#')) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + targetSub;
}
}
};
const setupMedia = () => {
window._currentActiveAlbumGallery = null;
window.albumGallery = null;
const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video");
if (elem) {
video = new v0ck(elem);
} else {
video = null;
}
initAlbumGallery();
syncLocationSubf0ck();
};
document.addEventListener('f0ck:contentLoaded', () => {
initAlbumGallery();
syncLocationSubf0ck();
});
const initOnaraInitialState = () => {
if (!isOnaraActive()) return;
promoteOnaraModals();
if (document.body.classList.contains('onara-modal-open')) {
const modal = openOnaraModal();
if (!window._onaraReturnUrl) {
window._onaraReturnUrl = getOnaraBaseUrl();
window._onaraReturnTitle = window.f0ckDomain || 'f0ck';
}
window._onaraCurrentGridUrl = window._onaraReturnUrl;
const pathSegments = window.location.pathname.split('/');
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
const activeKey = keySegments.length ? keySegments[keySegments.length - 1] : null;
if (activeKey) {
updateOnaraActiveItem(activeKey, window.location.href);
if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey);
}
}
} else {
window._onaraCurrentGridUrl = window.location.pathname + window.location.search;
}
};
// Initial Load
document.addEventListener('DOMContentLoaded', () => {
setupMedia();
initOnaraInitialState();
schedulePrefetch(800);
});
initOnaraInitialState();
// Export init function for dynamic calls
window.initBackground = () => {
// Media selection priority
let elem = document.querySelector("#my-video");
if (!elem) {
const rp = document.querySelector('ruffle-player');
if (rp) {
elem = rp.shadowRoot ? rp.shadowRoot.querySelector('canvas') : null;
if (!elem) {
// If we have a player but no canvas yet, it's likely still initializing.
// Re-init background in a moment.
setTimeout(window.initBackground, 200);
return;
}
}
}
if (elem && elem.tagName === 'AUDIO') {
elem = document.querySelector("#f0ck-audio-cover") || elem;
}
if (!elem || (elem.tagName === 'AUDIO')) {
elem = document.querySelector("#f0ck-image") || elem;
}
const canvas = document.getElementById('bg');
if (elem) {
if (canvas) {
// Restore visual state on re-init
if (background) {
canvas._bgFadingOut = false;
// For images: defer fader-in until drawOnce draws the thumbnail.
// For video/audio: fader-in immediately.
if (elem.tagName !== 'IMG') {
canvas.classList.add('fader-in');
canvas.classList.remove('fader-out', 'fast-fade');
}
} else {
// Don't clear the canvas here — let the existing content fade out.
canvas._bgFadingOut = true;
canvas.classList.add('fader-out');
canvas.classList.remove('fader-in', 'fast-fade');
const stopOnFadeEnd = (ev) => {
if (ev.propertyName === 'opacity') {
canvas._bgFadingOut = false;
canvas.removeEventListener('transitionend', stopOnFadeEnd);
}
};
canvas.addEventListener('transitionend', stopOnFadeEnd);
return; // nothing more to do — let CSS do the fade
}
// Only reset canvas dimensions when turning ON (avoids clearing pixels mid-fade-out).
const context = canvas.getContext('2d');
// Draw at 1/4 resolution — the canvas is stretched to full-screen by CSS,
// so a smaller internal resolution is imperceptible for a blurred background.
// This reduces blur computation from O(W*H) to O(W/4 * H/4) = 1/16th the pixels.
const SCALE = 0.25;
const cw = canvas.width = Math.max(1, (canvas.clientWidth * SCALE) | 0);
const ch = canvas.height = Math.max(1, (canvas.clientHeight * SCALE) | 0);
// Blur radius scaled proportionally to the downsampled canvas size
const blurPx = Math.round(100 * SCALE) || 1;
const drawOnce = () => {
if (!background || !context) return;
// Always use the thumbnail first for instant backdrop — thumbnails are tiny,
// often browser-cached from grid view, and give us a frame-0 equivalent for GIFs too.
// Extract item ID from URL for thumbnail path.
const itemId = window.getCurrentItemId();
const showCanvas = () => {
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
};
const isDrawable = elem && elem.tagName === 'IMG';
if (itemId) {
// Step 1: draw thumbnail immediately for instant background
const thumb = new Image();
thumb.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(thumb, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
// Step 2: upgrade with full image when it's ready (skip for AUDIO elements)
if (isDrawable) {
if (elem.complete) {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
} else {
elem.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
};
}
}
};
thumb.onerror = () => {
// Thumbnail failed — fall back to waiting for the main image (skip for AUDIO)
if (isDrawable) {
if (elem.complete) {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
} else {
elem.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
};
}
}
// For audio-only items with no thumbnail, canvas stays blank (nothing to draw)
};
let newSrc = `/t/${itemId}.webp`;
if (window.applyThumbCacheBust) newSrc = window.applyThumbCacheBust(newSrc);
thumb.src = newSrc;
} else if (isDrawable) {
// No item ID — fall back to waiting for the main image
if (elem.complete) {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
} else {
elem.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
};
}
}
};
const animationLoop = () => {
if (!elem || elem.tagName === 'AUDIO' || elem.paused || elem.ended || (!background && !canvas._bgFadingOut)) {
bgRafId = null;
return;
}
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {
bgRafId = null;
return;
}
bgRafId = window.requestAnimFrame(animationLoop);
};
// Singleton: Ensure only one listener and one loop per element
if (lastBgElem !== elem) {
if (bgRafId) window.cancelAnimFrame(bgRafId);
lastBgElem = elem;
if (elem.tagName === 'VIDEO') {
elem.addEventListener('play', () => {
if (bgRafId) window.cancelAnimFrame(bgRafId);
if (background) animationLoop();
});
} else if (elem.tagName === 'CANVAS') {
// Ruffle canvas: start loop immediately
if (bgRafId) window.cancelAnimFrame(bgRafId);
if (background) animationLoop();
}
}
if (elem.tagName === 'VIDEO') {
if (!elem.paused && background) {
if (bgRafId) window.cancelAnimFrame(bgRafId);
animationLoop();
}
} else if (elem.tagName === 'CANVAS') {
if (background) {
if (bgRafId) window.cancelAnimFrame(bgRafId);
animationLoop();
}
} else if (elem.tagName === 'IMG' || elem.tagName === 'AUDIO') {
// IMG: draw from thumbnail. AUDIO: draw thumbnail from URL (no drawable elem, just background).
drawOnce();
}
}
} else if (canvas) {
// No drawable element (e.g. YouTube iframe) — still handle canvas fade toggle
if (background) {
canvas._bgFadingOut = false;
// Draw the item thumbnail if we have an item ID in the URL
const itemId = window.getCurrentItemId();
if (itemId) {
const context = canvas.getContext('2d');
const _SCALE = 0.25;
const cw = canvas.width = Math.max(1, (canvas.clientWidth * _SCALE) | 0);
const ch = canvas.height = Math.max(1, (canvas.clientHeight * _SCALE) | 0);
const blurPx = Math.round(100 * _SCALE) || 1;
const thumb = new Image();
thumb.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(thumb, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
};
thumb.src = `/t/${itemId}.webp`;
} else {
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
}
} else {
canvas._bgFadingOut = true;
canvas.classList.add('fader-out');
canvas.classList.remove('fader-in', 'fast-fade');
const stopOnFadeEnd = (ev) => {
if (ev.propertyName === 'opacity') {
canvas._bgFadingOut = false;
canvas.removeEventListener('transitionend', stopOnFadeEnd);
}
};
canvas.addEventListener('transitionend', stopOnFadeEnd);
}
}
};
// Audio Visualizer Reactivity Tuner
const DEFAULT_AUDIO_TUNING = {
useCustomColor: 0,
visualizerColor: "#241f31",
enableBeatHue: 0,
beatHueThreshold: 0.3,
beatHueStep: 45,
beatHueSmooth: 0.65,
beatHueIdleDrift: 0.2,
beatHueCooldown: 150,
coverSize: 110,
glowIntensity: 500,
coverGlowBase: 128,
glowBrightness: 2.05,
glowSensitivity: 1,
glowDynamism: 0.85,
glowSmoothness: 0.95,
solidCover: 1,
coverColor: "#000000",
coverOpacity: 0,
bassGain: 0.5,
bassPower: 0.3,
scaleBounce: 0.85,
bounceBoost: 1,
attackSpeed: 1,
releaseSpeed: 0.6,
enableBars: 0,
enableInnerBars: 1,
useCustomInnerColor: 0,
innerBarColor: "#ff0000",
innerBarsMode: 4,
innerRadius: 150,
innerPupilRadius: 46,
innerPupilRingOpacity: 0,
innerBarCount: 73,
innerBarWidth: 80,
innerBarHeight: 2.65,
innerBarOpacity: 0.13,
outerRingOpacity: 0,
innerHighBoost: 0.162,
innerRadialRotation: 90,
innerBarGlow: 0,
innerHideNote: 1,
barHeight: 0.9,
barWidth: 34,
barGap: 9,
barRadius: 12,
barOpacity: 0.1,
barGlow: 0,
smoothing: 0.86,
followMouse: 1,
followSpeed: 0.1,
followRadius: 15,
tiltEffect: 1,
glowAttack: 1,
glowDecay: 0.5,
enableBgGradient: 1,
bgGradientStyle: 1,
bgGradientOpacity: 0.4,
bgGradientReactivity: 1.2,
bgGradientSpread: 75,
useCustomBgColor: 0,
bgGradientColor: "#3a1c71",
enableBlink: 0,
blinkInterval: 12,
beatHueSpeed: 0.1
};
let savedTuning = null;
try {
const raw = localStorage.getItem('f0ck_audio_tuning');
if (raw) savedTuning = JSON.parse(raw);
} catch (e) {}
window.audioVisualizerTuning = Object.assign({}, DEFAULT_AUDIO_TUNING, savedTuning || {});
// Eye Cursor Follower & 3D Tracking
const mousePos = { x: null, y: null, active: false };
let isEyeActive = false;
let eyeInactivityTimer = null;
const isCursorNearV0ck = (e) => {
const margin = 10;
const players = document.querySelectorAll('.v0ck, .sidebar-media-placeholder.audio');
for (const p of players) {
const r = p.getBoundingClientRect();
if (r.width > 0 && r.height > 0 &&
e.clientX >= r.left - margin && e.clientX <= r.right + margin &&
e.clientY >= r.top - margin && e.clientY <= r.bottom + margin) {
return true;
}
}
return false;
};
const handlePointerMove = (e) => {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
const nearPlayer = isCursorNearV0ck(e);
if (nearPlayer) {
isEyeActive = true;
mousePos.active = true;
clearTimeout(eyeInactivityTimer);
eyeInactivityTimer = setTimeout(() => {
isEyeActive = false;
mousePos.active = false;
}, 2500);
} else {
// More than 10px outside of player: stop following and return to center
isEyeActive = false;
mousePos.active = false;
clearTimeout(eyeInactivityTimer);
}
};
window.addEventListener('pointermove', handlePointerMove, { passive: true });
window.addEventListener('pointerdown', (e) => {
if (isCursorNearV0ck(e)) handlePointerMove(e);
}, { passive: true });
document.addEventListener('mouseleave', () => {
isEyeActive = false;
mousePos.active = false;
clearTimeout(eyeInactivityTimer);
});
window.addEventListener('pointerup', () => {
if (window.matchMedia && window.matchMedia('(hover: none)').matches) {
isEyeActive = false;
mousePos.active = false;
clearTimeout(eyeInactivityTimer);
}
}, { passive: true });
const updateEyeTracking = () => {
const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
const isEnabled = cfg.followMouse !== undefined ? cfg.followMouse === 1 : true;
const speed = cfg.followSpeed !== undefined ? cfg.followSpeed : 0.04;
const allowTilt = cfg.tiltEffect !== undefined ? cfg.tiltEffect === 1 : true;
const circles = document.querySelectorAll('.sidebar-media-placeholder.audio .audio-cover-circle');
circles.forEach(circle => {
const ph = circle.closest('.sidebar-media-placeholder.audio');
if (!ph) return;
const rect = ph.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const v0ckPlayer = ph.closest('.v0ck') || ph;
const pRect = v0ckPlayer.getBoundingClientRect();
const margin = 10;
const isNearThisPlayer = (
mousePos.x !== null && mousePos.y !== null &&
mousePos.x >= pRect.left - margin && mousePos.x <= pRect.right + margin &&
mousePos.y >= pRect.top - margin && mousePos.y <= pRect.bottom + margin
);
let isCursorHiddenOnPlayer = false;
if (v0ckPlayer && !v0ckPlayer.classList.contains('v0ck_hover')) {
if (mousePos.active && mousePos.x >= pRect.left && mousePos.x <= pRect.right &&
mousePos.y >= pRect.top && mousePos.y <= pRect.bottom) {
isCursorHiddenOnPlayer = true;
}
}
let targetX = 0;
let targetY = 0;
let tiltX = 0;
let tiltY = 0;
if (isEnabled && isEyeActive && mousePos.active && isNearThisPlayer && !isCursorHiddenOnPlayer) {
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = mousePos.x - centerX;
const dy = mousePos.y - centerY;
const maxRadius = cfg.followRadius !== undefined ? cfg.followRadius : 20;
const dist = Math.hypot(dx, dy);
if (dist > 0) {
const clampedDist = Math.min(dist, maxRadius);
targetX = (dx / dist) * clampedDist;
targetY = (dy / dist) * clampedDist;
}
if (allowTilt) {
tiltX = -(targetY / (maxRadius || 1)) * 20;
tiltY = (targetX / (maxRadius || 1)) * 20;
}
}
circle._eyeX = circle._eyeX || 0;
circle._eyeY = circle._eyeY || 0;
circle._eyeTiltX = circle._eyeTiltX || 0;
circle._eyeTiltY = circle._eyeTiltY || 0;
circle._eyeX += (targetX - circle._eyeX) * speed;
circle._eyeY += (targetY - circle._eyeY) * speed;
circle._eyeTiltX += (tiltX - circle._eyeTiltX) * speed;
circle._eyeTiltY += (tiltY - circle._eyeTiltY) * speed;
// If audio visualizer is NOT actively driving the transform this frame, update it here
if (!circle._visualizerDriving) {
const curX = circle._eyeX.toFixed(1);
const curY = circle._eyeY.toFixed(1);
const curTiltX = circle._eyeTiltX.toFixed(1);
const curTiltY = circle._eyeTiltY.toFixed(1);
circle.style.transform = `translate(calc(-50% + ${curX}px), calc(-50% + ${curY}px)) perspective(600px) rotateX(${curTiltX}deg) rotateY(${curTiltY}deg) scale(1)`;
}
});
requestAnimationFrame(updateEyeTracking);
};
requestAnimationFrame(updateEyeTracking);
const initAudioTunerUI = () => {
if (document.getElementById('f0ck-audio-tuner-panel')) return;
const sidebarContainer = document.getElementById('sidebar-tuner-container');
const panel = document.createElement('div');
panel.id = 'f0ck-audio-tuner-panel';
panel.className = sidebarContainer ? 'f0ck-audio-tuner-panel in-sidebar' : 'f0ck-audio-tuner-panel hidden';
const sliders = [
// Visualizer Color Section
{ section: 'Visualizer Color & Theme', key: 'useCustomColor', label: 'Use Custom Color (1=Custom, 0=Site Accent)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Visualizer Color & Theme', key: 'visualizerColor', label: 'Custom Visualizer Color', type: 'color' },
{ section: 'Visualizer Color & Theme', key: 'enableBeatHue', label: 'Beat-Reactive Hue Color (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Visualizer Color & Theme', key: 'beatHueThreshold', label: 'Beat Trigger Threshold', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Visualizer Color & Theme', key: 'beatHueStep', label: 'Color Shift Angle per Beat', min: 1, max: 180, step: 1, unit: '°' },
{ section: 'Visualizer Color & Theme', key: 'beatHueSmooth', label: 'Color Morph Smoothness (0=Snap, 1=Glide)', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Visualizer Color & Theme', key: 'beatHueIdleDrift', label: 'Idle Ambient Hue Drift', min: 0.00, max: 2.00, step: 0.01, unit: 'x' },
{ section: 'Visualizer Color & Theme', key: 'beatHueCooldown', label: 'Min Time Between Jumps', min: 50, max: 800, step: 25, unit: 'ms' },
// Cover Art & Glow Section
{ section: 'Cover Art & Glow Reactivity', key: 'coverSize', label: 'Cover Art Size (Diameter)', min: 40, max: 600, step: 5, unit: 'px' },
{ section: 'Cover Art & Glow Reactivity', key: 'solidCover', label: 'Solid Color Cover (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'coverColor', label: 'Solid Cover Color', type: 'color' },
{ section: 'Cover Art & Glow Reactivity', key: 'coverOpacity', label: 'Cover / Disc Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowIntensity', label: 'Cover Art Glow Size (Dynamic)', min: 0, max: 500, step: 5, unit: 'px' },
{ section: 'Cover Art & Glow Reactivity', key: 'coverGlowBase', label: 'Cover Art Base Glow (Ambient)', min: 0, max: 150, step: 2, unit: 'px' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowBrightness', label: 'Cover Art Glow Brightness', min: 0.0, max: 3.0, step: 0.05, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowSensitivity', label: 'Glow Beat Reactivity / Sensitivity', min: 0.10, max: 5.00, step: 0.05, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowDynamism', label: 'Glow Dynamism (Dynamic Punch)', min: 0.00, max: 3.00, step: 0.05, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowSmoothness', label: 'Glow Smoothness (Liquid Glow)', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'scaleBounce', label: 'Disc Bounce (Scale)', min: 0.00, max: 5.00, step: 0.05, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'bounceBoost', label: 'Bounce Reactivity Boost', min: 1.0, max: 100.0, step: 0.5, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'bassGain', label: 'Bass Gain (Multiplier)', min: 0.5, max: 6.0, step: 0.1, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'bassPower', label: 'Sensitivity Curve (Power)', min: 0.10, max: 2.00, step: 0.01, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'attackSpeed', label: 'Attack Speed (Snap)', min: 0.05, max: 1.00, step: 0.01, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'releaseSpeed', label: 'Release Speed (Decay)', min: 0.01, max: 1.00, step: 0.01, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowAttack', label: 'Glow Attack Speed (Swell)', min: 0.01, max: 1.00, step: 0.01, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowDecay', label: 'Glow Decay Speed (Dissipate)', min: 0.01, max: 1.00, step: 0.01, unit: '' },
// Reactive Background Gradient Section
{ section: 'Reactive Background Gradient', key: 'enableBgGradient', label: 'Background Gradient (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Reactive Background Gradient', key: 'bgGradientStyle', label: 'Gradient Style (1=Radial Aura, 2=Dual Fog, 3=Conic Vortex, 4=Horizon Flare)', min: 1, max: 4, step: 1, unit: '' },
{ section: 'Reactive Background Gradient', key: 'bgGradientOpacity', label: 'Gradient Base Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Reactive Background Gradient', key: 'bgGradientReactivity', label: 'Beat Reactivity (Pulse Strength)', min: 0.00, max: 3.00, step: 0.05, unit: 'x' },
{ section: 'Reactive Background Gradient', key: 'bgGradientSpread', label: 'Gradient Radius / Spread', min: 10, max: 200, step: 2, unit: '%' },
{ section: 'Reactive Background Gradient', key: 'useCustomBgColor', label: 'Custom Gradient Color (1=On, 0=Accent)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Reactive Background Gradient', key: 'bgGradientColor', label: 'Custom Gradient Color', type: 'color' },
// Inner Eye Visualizer Section
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'enableInnerBars', label: 'Inner Visualizer (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarsMode', label: 'Inner Style (1=Center, 2=Outward Iris, 3=Arc, 4=Inverted Iris, 5=Inverted Rev, 6=Dual Stargate, 7=360° Radar)', min: 1, max: 7, step: 1, unit: '' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerRadius', label: 'Inner Visualizer Size (Outer Radius)', min: 30, max: 800, step: 2, unit: 'px' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerPupilRadius', label: 'Center Pupil Gap (Inner Radius)', min: 0, max: 600, step: 2, unit: 'px' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'useCustomInnerColor', label: 'Solid Custom Inner Color (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarColor', label: 'Solid Inner Bar Color', type: 'color' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarCount', label: 'Spectrum Density (Note Count)', min: 2, max: 512, step: 1, unit: '' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerHighBoost', label: 'Melody & Treble Sensitivity', min: 0.000, max: 2.000, step: 0.001, unit: 'x' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarHeight', label: 'Inner Bar Height / Scale', min: 0.05, max: 100.00, step: 0.05, unit: 'x' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarWidth', label: 'Inner Bar Width', min: 1, max: 250, step: 1, unit: 'px' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarOpacity', label: 'Inner Bar Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'outerRingOpacity', label: 'Outer Ring Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerPupilRingOpacity', label: 'Center Pupil Gap Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerRadialRotation', label: 'Radial Iris Rotation Angle', min: 0, max: 360, step: 5, unit: '°' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarGlow', label: 'Inner Bar Glow', min: 0, max: 30, step: 1, unit: 'px' },
{ section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerHideNote', label: 'Hide Music Note Icon (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
// Visualizer Bars Section
{ section: 'Bottom Visualizer Bars', key: 'enableBars', label: 'Show Visualizer Bars (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Bottom Visualizer Bars', key: 'barHeight', label: 'Visualizer Bar Height', min: 0.05, max: 2.50, step: 0.05, unit: 'x' },
{ section: 'Bottom Visualizer Bars', key: 'barWidth', label: 'Bar Width', min: 1, max: 35, step: 1, unit: 'px' },
{ section: 'Bottom Visualizer Bars', key: 'barGap', label: 'Bar Spacing (Gap)', min: 0, max: 15, step: 1, unit: 'px' },
{ section: 'Bottom Visualizer Bars', key: 'barRadius', label: 'Bar Top Rounding', min: 0, max: 12, step: 1, unit: 'px' },
{ section: 'Bottom Visualizer Bars', key: 'barOpacity', label: 'Bar Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ section: 'Bottom Visualizer Bars', key: 'barGlow', label: 'Bar Glow Aura', min: 0, max: 30, step: 1, unit: 'px' },
{ section: 'Bottom Visualizer Bars', key: 'smoothing', label: 'Visualizer Bar Smoothing', min: 0.00, max: 1.00, step: 0.01, unit: '' },
// Mouse Follow & Tilt Simulation Section
{ section: 'Eye Simulation & Mouse Follow', key: 'followMouse', label: 'Follow Mouse Cursor (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
{ section: 'Eye Simulation & Mouse Follow', key: 'followSpeed', label: 'Eye Follow Speed', min: 0.01, max: 0.40, step: 0.01, unit: '' },
{ section: 'Eye Simulation & Mouse Follow', key: 'followRadius', label: 'Eye Follow Range (Radius)', min: 5, max: 100, step: 1, unit: 'px' },
{ section: 'Eye Simulation & Mouse Follow', key: 'tiltEffect', label: '3D Eye Tilt (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' }
];
const updateCoverArtSolidMode = () => {
const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
const isSolid = Number(cfg.solidCover) === 1;
const color = cfg.coverColor || '#000000';
const coverOp = cfg.coverOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.coverOpacity))) : 1.0;
const bgCol = coverOp <= 0.001 ? 'transparent' : (coverOp >= 0.999 ? color : `color-mix(in srgb, ${color} ${Math.round(coverOp * 100)}%, transparent)`);
document.querySelectorAll('.audio-cover-circle').forEach(c => {
if (isSolid) {
if (c.style.backgroundImage && c.style.backgroundImage !== 'none' && !c._origBgImage) {
c._origBgImage = c.style.backgroundImage;
}
c.style.backgroundImage = 'none';
c.style.backgroundColor = bgCol;
} else {
if (c._origBgImage) {
c.style.backgroundImage = c._origBgImage;
c.style.backgroundColor = bgCol;
} else {
c.style.backgroundImage = 'none';
c.style.backgroundColor = bgCol;
}
}
});
};
let rowsHtml = '';
let currentSection = '';
sliders.forEach(s => {
if (s.section && s.section !== currentSection) {
currentSection = s.section;
rowsHtml += `