${subSlug} (${currentIndex + 1}/${albumData.length}):`;
}
let subTags = Array.isArray(sub.tags) ? sub.tags : [];
// If this sub-item has no rating tag, inherit the parent album's rating
const RATING_NORMS = ['sfw', 'nsfw', 'nsfl'];
const subHasRating = subTags.some(t => RATING_NORMS.includes(t.normalized));
if (!subHasRating) {
const parentRating = getParentRatingTags();
if (parentRating.length > 0) subTags = [...parentRating, ...subTags];
}
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 transformStandaloneItemToOnara = async () => {
const main = document.getElementById('main');
if (!main) return;
// Find the item content inside #main
const itemContainer = main.querySelector('.item-layout-container') || main.querySelector('.container') || main.firstElementChild;
if (!itemContainer) return;
// Ensure Onara modal exists in DOM
const modal = getOrCreateOnaraModal();
const mount = document.getElementById('onara-item-mount');
if (mount) {
mount.innerHTML = '';
mount.appendChild(itemContainer);
}
// Open Onara modal immediately
openOnaraModal();
const returnUrl = getOnaraBaseUrl() || '/';
window._onaraReturnUrl = returnUrl;
window._onaraReturnTitle = window.f0ckDomain || 'f0ck';
window._onaraCurrentGridUrl = returnUrl;
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;
try {
const resp = await fetch(returnUrl, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (resp.ok) {
const html = await resp.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const gridContent = doc.querySelector('#main') || doc.body;
if (gridContent) {
main.className = '';
main.innerHTML = gridContent.innerHTML;
if (typeof window.initLazyLoading === 'function') {
window.initLazyLoading();
}
if (typeof window.initThumbnailHover === 'function') {
window.initThumbnailHover();
}
}
}
} catch (err) {
console.warn('[ONARA] Failed to load background grid for direct item:', err);
}
if (activeKey) {
updateOnaraActiveItem(activeKey, window.location.href);
if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey);
}
}
};
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 {
// Check if this is a direct item visit rendered as standalone item view (e.g. before cookie reached server)
const isDirectStandaloneItem = !document.getElementById('posts') &&
(document.querySelector('.item-container, .media-object, #main.item-view') !== null);
if (isDirectStandaloneItem) {
transformStandaloneItemToOnara();
return;
}
window._onaraCurrentGridUrl = window.location.pathname + window.location.search;
}
};
// Initial Load
document.addEventListener('DOMContentLoaded', () => {
setupMedia();
initOnaraInitialState();
schedulePrefetch(800);
});
initOnaraInitialState();
// Dynamic YouTube ambient color timeline manager
let ytAmbientState = {
rafId: null,
videoId: null,
colors: null,
duration: 0,
currentTime: 0,
isPlaying: false,
lastTimeUpdate: 0,
currentColor: [0, 0, 0],
targetColor: [0, 0, 0],
cleanup: null
};
const stopYoutubeAmbient = () => {
if (ytAmbientState.rafId) {
window.cancelAnimFrame(ytAmbientState.rafId);
ytAmbientState.rafId = null;
}
if (ytAmbientState.cleanup) {
ytAmbientState.cleanup();
ytAmbientState.cleanup = null;
}
ytAmbientState.videoId = null;
ytAmbientState.colors = null;
ytAmbientState.isPlaying = false;
};
const initYoutubeAmbient = (ytEmbed, canvas, backgroundEnabled) => {
stopYoutubeAmbient();
if (!ytEmbed || !canvas || !backgroundEnabled) return;
const videoId = ytEmbed.dataset.ytId || (ytEmbed.src && (ytEmbed.src.match(/embed\/([a-zA-Z0-9_-]+)/) || [])[1]);
if (!videoId) return;
ytAmbientState.videoId = videoId;
const context = canvas.getContext('2d');
const _SCALE = 0.5;
const updateCanvasSize = () => {
const cw = Math.max(1, (canvas.clientWidth * _SCALE) | 0);
const ch = Math.max(1, (canvas.clientHeight * _SCALE) | 0);
if (canvas.width !== cw || canvas.height !== ch) {
canvas.width = cw;
canvas.height = ch;
}
return { cw: canvas.width, ch: canvas.height };
};
const { cw, ch } = updateCanvasSize();
const blurPx = Math.round(80 * _SCALE) || 1;
// 1. Draw static thumbnail immediately for instant background (with robust fallback)
const itemId = window.getCurrentItemId();
const tryDrawThumb = (url, fallbackUrl) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
try {
context.filter = `blur(${blurPx}px) brightness(1.2)`;
const sw = img.naturalWidth || img.width;
const sh = img.naturalHeight || img.height;
if (sw > 0 && sh > 0) {
const scale = Math.max(canvas.width / sw, canvas.height / sh);
const dw = sw * scale;
const dh = sh * scale;
context.drawImage(img, (canvas.width - dw) / 2, (canvas.height - dh) / 2, dw, dh);
} else {
context.drawImage(img, 0, 0, canvas.width, canvas.height);
}
context.filter = 'none';
} catch (e) {}
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
};
img.onerror = () => {
if (fallbackUrl && url !== fallbackUrl) {
tryDrawThumb(fallbackUrl, null);
} else {
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
}
};
img.src = url;
};
const localThumb = itemId ? `/t/${itemId}.webp` : null;
const remoteThumb = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
tryDrawThumb(localThumb || remoteThumb, remoteThumb);
// 2. Fetch storyboard ambient colors
fetch(`/api/v2/ambient/yt/${videoId}`)
.then(res => res.ok ? res.json() : null)
.then(data => {
if (!data || !Array.isArray(data.colors) || !data.colors.length || ytAmbientState.videoId !== videoId) return;
ytAmbientState.colors = data.colors;
ytAmbientState.duration = Number(data.duration) || 0;
if (ytAmbientState.colors.length > 0) {
ytAmbientState.currentColor = [...ytAmbientState.colors[0]];
ytAmbientState.targetColor = [...ytAmbientState.colors[0]];
// Immediately paint the first frame color as ambient glow!
renderAmbientFrame();
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
}
if (ytAmbientState.isPlaying) {
startAmbientLoop();
}
})
.catch(() => {});
// 3. Render function for ambient lighting
const renderAmbientFrame = () => {
if (!ytAmbientState.colors || !ytAmbientState.colors.length || !ytAmbientState.duration) return;
const { cw: curW, ch: curH } = updateCanvasSize();
let t = ytAmbientState.currentTime;
if (ytAmbientState.isPlaying && ytAmbientState.lastTimeUpdate > 0) {
const dt = (performance.now() - ytAmbientState.lastTimeUpdate) / 1000;
t = Math.min(ytAmbientState.duration, t + dt);
}
const norm = Math.max(0, Math.min(1, t / ytAmbientState.duration));
const pos = norm * (ytAmbientState.colors.length - 1);
const idx = Math.floor(pos);
const next = Math.min(ytAmbientState.colors.length - 1, idx + 1);
const frac = pos - idx;
const c1 = ytAmbientState.colors[idx];
const c2 = ytAmbientState.colors[next];
ytAmbientState.targetColor = [
c1[0] + (c2[0] - c1[0]) * frac,
c1[1] + (c2[1] - c1[1]) * frac,
c1[2] + (c2[2] - c1[2]) * frac
];
// Smooth lerp
ytAmbientState.currentColor[0] += (ytAmbientState.targetColor[0] - ytAmbientState.currentColor[0]) * 0.15;
ytAmbientState.currentColor[1] += (ytAmbientState.targetColor[1] - ytAmbientState.currentColor[1]) * 0.15;
ytAmbientState.currentColor[2] += (ytAmbientState.targetColor[2] - ytAmbientState.currentColor[2]) * 0.15;
const r = Math.min(255, Math.max(0, Math.round(ytAmbientState.currentColor[0])));
const g = Math.min(255, Math.max(0, Math.round(ytAmbientState.currentColor[1])));
const b = Math.min(255, Math.max(0, Math.round(ytAmbientState.currentColor[2])));
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Render radial ambient wash over canvas
ctx.save();
const cx = curW / 2;
const cy = curH / 2;
const maxR = Math.max(curW, curH) * 0.85;
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxR);
grad.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.95)`);
grad.addColorStop(0.4, `rgba(${Math.round(r * 0.75)}, ${Math.round(g * 0.75)}, ${Math.round(b * 0.75)}, 0.7)`);
grad.addColorStop(0.8, `rgba(${Math.round(r * 0.3)}, ${Math.round(g * 0.3)}, ${Math.round(b * 0.3)}, 0.4)`);
grad.addColorStop(1, 'rgba(5, 5, 10, 0.95)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, curW, curH);
ctx.restore();
};
const startAmbientLoop = () => {
if (ytAmbientState.rafId) return;
const loop = () => {
if (!ytAmbientState.isPlaying || ytAmbientState.videoId !== videoId) {
ytAmbientState.rafId = null;
return;
}
renderAmbientFrame();
ytAmbientState.rafId = window.requestAnimFrame(loop);
};
ytAmbientState.rafId = window.requestAnimFrame(loop);
};
// 4. Connect to YouTube player via official API and postMessage
const sendPostMsg = (msg) => {
if (!ytEmbed.contentWindow) return;
try {
ytEmbed.contentWindow.postMessage(typeof msg === 'string' ? msg : JSON.stringify(msg), '*');
} catch (e) {}
};
const registerListeners = () => {
// YouTube postMessage handshake
sendPostMsg({ event: 'listening', id: ytEmbed.id || 1 });
sendPostMsg({ event: 'command', func: 'addEventListener', args: ['onStateChange'] });
};
ytEmbed.addEventListener('load', registerListeners);
registerListeners();
setTimeout(registerListeners, 500);
setTimeout(registerListeners, 1500);
// Also attach YouTube IFrame API if available
const tryAttachYTPlayer = () => {
if (window.YT && window.YT.Player) {
try {
new window.YT.Player(ytEmbed, {
events: {
onStateChange: (event) => {
if (event.data === 1) { // PLAYING
ytAmbientState.isPlaying = true;
startAmbientLoop();
} else if (event.data === 2 || event.data === 0) {
ytAmbientState.isPlaying = false;
}
}
}
});
} catch (e) {}
}
};
if (window.YT && window.YT.Player) {
tryAttachYTPlayer();
} else if (!document.querySelector('script[src*="youtube.com/iframe_api"]')) {
const tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
tag.onload = () => setTimeout(tryAttachYTPlayer, 200);
document.head.appendChild(tag);
} else {
setTimeout(tryAttachYTPlayer, 1000);
}
const onMessage = (event) => {
if (typeof event.origin !== 'string' || !event.origin.includes('youtube.com')) return;
let data;
try {
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
} catch (e) {
return;
}
if (!data) return;
let state = undefined;
if (data.event === 'onStateChange') {
state = data.info;
} else if (data.event === 'infoDelivery' && data.info && data.info.playerState !== undefined) {
state = data.info.playerState;
}
if (state === 1) { // PLAYING
ytAmbientState.isPlaying = true;
startAmbientLoop();
} else if (state === 2 || state === 0) { // PAUSED or ENDED
ytAmbientState.isPlaying = false;
}
if (data.event === 'infoDelivery' && data.info && typeof data.info.currentTime === 'number') {
ytAmbientState.currentTime = data.info.currentTime;
ytAmbientState.lastTimeUpdate = performance.now();
if (!ytAmbientState.isPlaying) {
renderAmbientFrame();
}
}
};
window.addEventListener('message', onMessage);
ytAmbientState.cleanup = () => {
window.removeEventListener('message', onMessage);
ytEmbed.removeEventListener('load', registerListeners);
};
};
// Destroy / stop background canvas instance and animation loops
window.destroyBackgroundInstance = () => {
if (bgRafId) {
window.cancelAnimFrame(bgRafId);
bgRafId = null;
}
if (visualizerRafId) {
window.cancelAnimFrame(visualizerRafId);
visualizerRafId = null;
}
stopYoutubeAmbient();
const canvas = document.getElementById('bg');
if (canvas) {
canvas._bgFadingOut = false;
canvas.classList.remove('fader-in');
canvas.classList.add('fader-out');
canvas.style.removeProperty('opacity');
canvas.style.removeProperty('transition');
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
};
let mediaAmbientSampleCanvas = null;
let mediaAmbientSampleCtx = null;
const sampleMediaSpatialColors = (source) => {
if (!source) return null;
const w = source.videoWidth || source.naturalWidth || source.width || 0;
const h = source.videoHeight || source.naturalHeight || source.height || 0;
if (w <= 0 || h <= 0) return null;
if (!mediaAmbientSampleCanvas) {
mediaAmbientSampleCanvas = document.createElement('canvas');
mediaAmbientSampleCanvas.width = 4;
mediaAmbientSampleCanvas.height = 4;
mediaAmbientSampleCtx = mediaAmbientSampleCanvas.getContext('2d', { willReadFrequently: true });
}
if (!mediaAmbientSampleCtx) return null;
try {
mediaAmbientSampleCtx.clearRect(0, 0, 4, 4);
mediaAmbientSampleCtx.drawImage(source, 0, 0, 4, 4);
const data = mediaAmbientSampleCtx.getImageData(0, 0, 4, 4).data;
// Sample top half (rows 0, 1) and bottom half (rows 2, 3)
let rTop = 0, gTop = 0, bTop = 0, countTop = 0;
let rBot = 0, gBot = 0, bBot = 0, countBot = 0;
for (let y = 0; y < 4; y++) {
for (let x = 0; x < 4; x++) {
const idx = (y * 4 + x) * 4;
const a = data[idx + 3];
if (a > 20) {
if (y < 2) {
rTop += data[idx];
gTop += data[idx + 1];
bTop += data[idx + 2];
countTop++;
} else {
rBot += data[idx];
gBot += data[idx + 1];
bBot += data[idx + 2];
countBot++;
}
}
}
}
const bot = countBot > 0
? [Math.round(rBot / countBot), Math.round(gBot / countBot), Math.round(bBot / countBot)]
: (countTop > 0 ? [Math.round(rTop / countTop), Math.round(gTop / countTop), Math.round(bTop / countTop)] : [25, 25, 35]);
const top = countTop > 0
? [Math.round(rTop / countTop), Math.round(gTop / countTop), Math.round(bTop / countTop)]
: (countBot > 0 ? [...bot] : [25, 25, 35]);
return { top, bottom: bot };
} catch (e) {
return null;
}
};
const parseCssColorToRgb = (str) => {
if (!str) return [153, 255, 0];
if (str.startsWith('#')) {
let hex = str.slice(1);
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
const num = parseInt(hex, 16);
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
}
const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (m) return [Number(m[1]), Number(m[2]), Number(m[3])];
const hm = str.match(/hsl\(\s*([\d.]+)\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*\)/);
if (hm) {
const h = Number(hm[1]) / 360, s = Number(hm[2]) / 100, l = Number(hm[3]) / 100;
let r, g, b;
if (s === 0) { r = g = b = l; } else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1; if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
return [153, 255, 0];
};
const boostColorSaturation = (rgb, boost = 1.35) => {
if (!rgb || !Array.isArray(rgb) || rgb.length < 3) return [30, 30, 40];
const [r, g, b] = rgb;
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
return [
Math.min(255, Math.max(0, Math.round(lum + (r - lum) * boost))),
Math.min(255, Math.max(0, Math.round(lum + (g - lum) * boost))),
Math.min(255, Math.max(0, Math.round(lum + (b - lum) * boost)))
];
};
const mediaAmbientState = {
top: [25, 25, 35],
targetTop: [25, 25, 35],
bottom: [25, 25, 35],
targetBottom: [25, 25, 35],
hasSampled: false
};
const setAmbientTargetColors = (colors) => {
if (!colors) return;
if (colors.top) mediaAmbientState.targetTop = [...colors.top];
if (colors.bottom) mediaAmbientState.targetBottom = [...colors.bottom];
};
const updateAmbientLerp = (smoothingFactor) => {
const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
const factor = smoothingFactor !== undefined
? smoothingFactor
: (cfg.bgAmbientSmoothness !== undefined ? Number(cfg.bgAmbientSmoothness) : 0.05);
const s = mediaAmbientState;
for (let i = 0; i < 3; i++) {
s.top[i] += (s.targetTop[i] - s.top[i]) * factor;
s.bottom[i] += (s.targetBottom[i] - s.bottom[i]) * factor;
}
return {
top: s.top.map(v => Math.min(255, Math.max(0, Math.round(v)))),
bottom: s.bottom.map(v => Math.min(255, Math.max(0, Math.round(v))))
};
};
const renderAmbientLightingWash = (ctx, targetW, targetH, colors, alpha = 1.0, reactiveOpts = null) => {
if (!ctx || !colors) return;
const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
const satBoost = cfg.bgAmbientSaturation !== undefined ? Number(cfg.bgAmbientSaturation) : 1.35;
let intensity = cfg.bgAmbientIntensity !== undefined ? Number(cfg.bgAmbientIntensity) : 1.0;
let spread = cfg.bgAmbientSpread !== undefined ? Number(cfg.bgAmbientSpread) : 0.85;
if (reactiveOpts) {
if (reactiveOpts.intensityMult !== undefined) intensity *= reactiveOpts.intensityMult;
if (reactiveOpts.spreadMult !== undefined) spread *= reactiveOpts.spreadMult;
}
const rawTop = colors.top || colors;
const rawBot = colors.bottom || colors.top || colors;
const top = boostColorSaturation(rawTop, satBoost);
const bot = boostColorSaturation(rawBot, satBoost);
ctx.save();
if (alpha < 1.0) ctx.globalAlpha = alpha;
const cx = targetW / 2;
const cyTop = targetH * 0.28;
const cyBot = targetH * 0.72;
const maxR = Math.max(targetW, targetH) * spread;
// 1. Dark background base
ctx.fillStyle = '#06060c';
ctx.fillRect(0, 0, targetW, targetH);
const a0 = Math.min(1.0, Math.max(0.0, 0.90 * intensity)).toFixed(3);
const a1 = Math.min(1.0, Math.max(0.0, 0.55 * intensity)).toFixed(3);
const a2 = Math.min(1.0, Math.max(0.0, 0.18 * intensity)).toFixed(3);
// 2. Top ambient lobe
const [rt, gt, bt] = top;
const gradTop = ctx.createRadialGradient(cx, cyTop, 0, cx, cyTop, maxR);
gradTop.addColorStop(0, `rgba(${rt}, ${gt}, ${bt}, ${a0})`);
gradTop.addColorStop(0.35, `rgba(${Math.round(rt * 0.75)}, ${Math.round(gt * 0.75)}, ${Math.round(bt * 0.75)}, ${a1})`);
gradTop.addColorStop(0.70, `rgba(${Math.round(rt * 0.35)}, ${Math.round(gt * 0.35)}, ${Math.round(bt * 0.35)}, ${a2})`);
gradTop.addColorStop(1.0, 'rgba(6, 6, 12, 0.0)');
ctx.fillStyle = gradTop;
ctx.fillRect(0, 0, targetW, targetH);
// 3. Bottom ambient lobe
const [rb, gb, bb] = bot;
const gradBot = ctx.createRadialGradient(cx, cyBot, 0, cx, cyBot, maxR);
gradBot.addColorStop(0, `rgba(${rb}, ${gb}, ${bb}, ${a0})`);
gradBot.addColorStop(0.35, `rgba(${Math.round(rb * 0.75)}, ${Math.round(gb * 0.75)}, ${Math.round(bb * 0.75)}, ${a1})`);
gradBot.addColorStop(0.70, `rgba(${Math.round(rb * 0.35)}, ${Math.round(gb * 0.35)}, ${Math.round(bb * 0.35)}, ${a2})`);
gradBot.addColorStop(1.0, 'rgba(6, 6, 12, 0.0)');
ctx.fillStyle = gradBot;
ctx.fillRect(0, 0, targetW, targetH);
// 4. Reactive Center Beat Bloom (Dynamic Audio Pulse)
if (reactiveOpts && reactiveOpts.pulse > 0.03) {
const p = Math.min(1.0, reactiveOpts.pulse);
const coreR = maxR * (0.30 + p * 0.45);
const rc = reactiveOpts.coreColor ? reactiveOpts.coreColor[0] : Math.round((rt + rb) / 2);
const gc = reactiveOpts.coreColor ? reactiveOpts.coreColor[1] : Math.round((gt + gb) / 2);
const bc = reactiveOpts.coreColor ? reactiveOpts.coreColor[2] : Math.round((bt + bb) / 2);
const pAlpha = Math.min(1.0, Math.max(0.0, p * 0.75 * intensity)).toFixed(3);
const pAlphaHalf = (pAlpha * 0.45).toFixed(3);
const gradCore = ctx.createRadialGradient(cx, targetH * 0.50, 0, cx, targetH * 0.50, coreR);
gradCore.addColorStop(0, `rgba(${rc}, ${gc}, ${bc}, ${pAlpha})`);
gradCore.addColorStop(0.45, `rgba(${rc}, ${gc}, ${bc}, ${pAlphaHalf})`);
gradCore.addColorStop(1.0, 'rgba(6, 6, 12, 0.0)');
ctx.fillStyle = gradCore;
ctx.fillRect(0, 0, targetW, targetH);
}
ctx.restore();
};
let ambientTransitionRaf = null;
const transitionAmbientToColors = (colors, onComplete) => {
setAmbientTargetColors(colors);
if (ambientTransitionRaf) window.cancelAnimFrame(ambientTransitionRaf);
const canvas = document.getElementById('bg');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let frames = 0;
const maxFrames = 30; // ~500ms smooth cinematic color melt
const step = () => {
const current = updateAmbientLerp(0.10);
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
renderAmbientLightingWash(ctx, w, h, current, 1.0);
if (typeof window.applyBgColorOverlay === 'function') {
window.applyBgColorOverlay(ctx, w, h);
}
frames++;
const distTop = Math.abs(mediaAmbientState.top[0] - mediaAmbientState.targetTop[0]) +
Math.abs(mediaAmbientState.top[1] - mediaAmbientState.targetTop[1]) +
Math.abs(mediaAmbientState.top[2] - mediaAmbientState.targetTop[2]);
const distBot = Math.abs(mediaAmbientState.bottom[0] - mediaAmbientState.targetBottom[0]) +
Math.abs(mediaAmbientState.bottom[1] - mediaAmbientState.targetBottom[1]) +
Math.abs(mediaAmbientState.bottom[2] - mediaAmbientState.targetBottom[2]);
if (frames < maxFrames && (distTop > 2 || distBot > 2)) {
ambientTransitionRaf = window.requestAnimFrame(step);
} else {
ambientTransitionRaf = null;
if (onComplete) onComplete();
}
};
ambientTransitionRaf = window.requestAnimFrame(step);
};
// ── Background canvas filter string — stateless, module-level ──────────────
// Method 0 = Canvas 2D blur (applied via ctx.filter before drawImage)
// Method 1 = GPU CSS only (canvas drawn sharp; CSS --bg-canvas-filter blurs the element)
// Method 2 = Hybrid (CSS blur + canvas sat/contrast, no canvas-level blur)
const _computeBgCanvasFilter = (scale) => {
const cfg = (typeof window.getEffectiveBackgroundTuning === 'function'
? window.getEffectiveBackgroundTuning() : null)
|| window.audioVisualizerTuning
|| DEFAULT_AUDIO_TUNING;
const method = Number(cfg.bgBlurMethod ?? 0);
const bSetting = Number(cfg.bgCanvasBlur ?? 31);
const bPx = (method === 1 || method === 2) ? 0 : Math.round(bSetting * (scale ?? 0.5));
const bVal = Number(cfg.bgCanvasBrightness ?? 0.6);
const sat = Number(cfg.bgCanvasSaturate ?? 1.85);
const con = Number(cfg.bgCanvasContrast ?? 1.15);
const parts = [];
if (bPx > 0) parts.push(`blur(${bPx}px)`);
if (bVal !== 1) parts.push(`brightness(${bVal})`);
if (sat !== 1) parts.push(`saturate(${sat})`);
if (con !== 1) parts.push(`contrast(${con})`);
return parts.length > 0 ? parts.join(' ') : 'none';
};
// Paint background canvas immediately from thumbnail without network or animation delay
window.paintImmediateBgThumb = (thumbOrElem) => {
if (typeof background !== 'undefined' && !background) return;
const canvas = document.getElementById('bg');
if (!canvas) return;
let img = null;
if (thumbOrElem instanceof HTMLImageElement) {
img = thumbOrElem;
} else if (thumbOrElem && typeof thumbOrElem.querySelector === 'function') {
img = thumbOrElem.querySelector('img');
}
if (!img) {
const activeEl = document.querySelector('.posts > a.thumb.onara-active img, .posts > a.thumb:focus img');
if (activeEl) img = activeEl;
}
if (!img || !img.complete || (img.naturalWidth === 0 && img.width === 0)) return;
const SCALE = 0.5;
const cw = Math.max(1, (canvas.clientWidth * SCALE) | 0);
const ch = Math.max(1, (canvas.clientHeight * SCALE) | 0);
if (canvas.width !== cw || canvas.height !== ch) {
canvas.width = cw;
canvas.height = ch;
}
canvas._bgFadingOut = false;
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
canvas.style.transition = 'none';
// opacity driven by --bg-canvas-opacity CSS var (set by applyBackgroundOpacitySettings) + .fader-in
canvas.style.removeProperty('opacity');
const ctx = canvas.getContext('2d');
if (!ctx) return;
const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning(true) : null) || (typeof window.audioVisualizerTuning !== 'undefined' && window.audioVisualizerTuning) || (typeof DEFAULT_AUDIO_TUNING !== 'undefined' ? DEFAULT_AUDIO_TUNING : {});
const method = cfg.bgBlurMethod !== undefined ? Number(cfg.bgBlurMethod) : 0;
const bSetting = cfg.bgCanvasBlur !== undefined ? Number(cfg.bgCanvasBlur) : 31;
const bPx = (method === 1 || method === 2) ? 0 : Math.round(bSetting * SCALE);
const bVal = cfg.bgCanvasBrightness !== undefined ? Number(cfg.bgCanvasBrightness) : 0.6;
const sat = cfg.bgCanvasSaturate !== undefined ? Number(cfg.bgCanvasSaturate) : 1.85;
const con = cfg.bgCanvasContrast !== undefined ? Number(cfg.bgCanvasContrast) : 1.15;
const parts = [];
if (bPx > 0) parts.push(`blur(${bPx}px)`);
if (bVal !== 1) parts.push(`brightness(${bVal})`);
if (sat !== 1) parts.push(`saturate(${sat})`);
if (con !== 1) parts.push(`contrast(${con})`);
const filterStr = parts.length > 0 ? parts.join(' ') : 'none';
const useAmbient = Number(cfg.bgAmbientLighting) === 1;
if (useAmbient) {
ctx.clearRect(0, 0, cw, ch);
const sampled = sampleMediaSpatialColors(img) || { top: [30, 30, 45], bottom: [30, 30, 45] };
setAmbientTargetColors(sampled);
mediaAmbientState.top = [...mediaAmbientState.targetTop];
mediaAmbientState.bottom = [...mediaAmbientState.targetBottom];
renderAmbientLightingWash(ctx, cw, ch, sampled, 1.0);
} else {
ctx.clearRect(0, 0, cw, ch);
ctx.save();
ctx.filter = filterStr;
let sw = img.naturalWidth || img.width;
let sh = img.naturalHeight || img.height;
if (sw > 0 && sw > 0) {
const scale = Math.max(cw / sw, ch / sh);
const dw = sw * scale;
const dh = sh * scale;
const dx = (cw - dw) / 2;
const dy = (ch - dh) / 2;
ctx.drawImage(img, dx, dy, dw, dh);
} else {
ctx.drawImage(img, 0, 0, cw, ch);
}
ctx.restore();
}
// Color overlay tint if configured
const colOp = cfg.bgCanvasColorOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.bgCanvasColorOpacity))) : 0;
if (colOp > 0.001) {
const col = cfg.bgCanvasColor || '#000000';
ctx.save();
ctx.fillStyle = colOp >= 0.999 ? col : `color-mix(in srgb, ${col} ${Math.round(colOp * 100)}%, transparent)`;
ctx.fillRect(0, 0, cw, ch);
ctx.restore();
}
};
// Export init function for dynamic calls
window.initBackground = () => {
// Media selection priority
let elem = null;
if (document.body.classList.contains('onara-modal-open')) {
const mount = document.getElementById('onara-item-mount');
if (mount) {
// For albums: prioritize visible video/audio over the always-present