diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index d4017bd..6cdce17 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -41,10 +41,10 @@ --posts-meta-bg: #000000b8; --x2154: #215483; --bg-canvas-opacity: 1.0; - --bg-canvas-blur: 300px; - --onara-bg-opacity: 0.84; + --bg-canvas-blur: 30px; + --onara-bg-opacity: 0.7; --onara-backdrop-blur: 5px; - --onara-grid-dim-opacity: 0.15; + --onara-grid-dim-opacity: 0.38; } html:not([theme]), html[theme=""] { @@ -3716,6 +3716,50 @@ body.sidebar-right-hidden #sidebar-drag-zone { color: #000; } +.f0ck-tuner-btn-save-default { + background: color-mix(in srgb, var(--accent, #99ff00) 18%, rgba(0, 0, 0, 0.4)); + border: 1px solid color-mix(in srgb, var(--accent, #99ff00) 45%, transparent); + color: var(--accent, #99ff00); + border-radius: 6px; + padding: 5px 8px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; +} + +.f0ck-tuner-btn-save-default:hover { + background: var(--accent, #99ff00); + border-color: var(--accent, #99ff00); + color: #000; + box-shadow: 0 2px 8px color-mix(in srgb, var(--accent, #99ff00) 35%, transparent); +} + +.f0ck-tuner-btn-factory-reset { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.15); + color: #aaa; + border-radius: 6px; + padding: 5px 8px; + font-size: 11px; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; +} + +.f0ck-tuner-btn-factory-reset:hover { + background: rgba(255, 100, 100, 0.18); + border-color: rgba(255, 100, 100, 0.4); + color: #ff7777; +} + .f0ck-tuner-btn-import-preset:hover { background: rgba(255, 255, 255, 0.2); color: #fff; @@ -3738,13 +3782,13 @@ body.sidebar-right-hidden #sidebar-drag-zone { display: inline-flex; align-items: center; justify-content: center; - gap: 5px; + gap: 4px; background: transparent; border: none; color: #8e8e93; - font-size: 11px; + font-size: 10.5px; font-weight: 600; - padding: 5px 8px; + padding: 5px 5px; border-radius: 6px; cursor: pointer; transition: all 0.15s ease; @@ -10314,14 +10358,13 @@ div.favs div.posts { right: 0; width: 100%; height: 100%; - /* blur is applied in canvas 2D context at draw time — no CSS filter needed. - CSS filter: blur() on a full-screen fixed element forces Chromium to run - a GPU blur pass on every compositor frame (incl. during the fade transition), - which is extremely expensive. Firefox uses a cheaper path for this. */ - transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0) scale(var(--bg-canvas-zoom, 1.12)); + transform-origin: center center; z-index: -2; transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1); opacity: var(--bg-canvas-opacity, 1.0); + filter: var(--bg-canvas-filter, none); + -webkit-filter: var(--bg-canvas-filter, none); } button#togglebg { @@ -20624,9 +20667,14 @@ a.lazy-thumb.filtered-upload-ghost:hover::after { } .user-infobox-username[contenteditable="true"] { border-bottom: 1px dashed rgba(255, 255, 255, 0.3); - padding: 1px 4px; + padding: 1px 6px; border-radius: 3px; transition: all 0.2s ease; + min-width: 80px; + max-width: 320px; + white-space: nowrap; + overflow: visible; + cursor: text; } .user-infobox-username[contenteditable="true"]:hover { border-bottom-color: var(--accent); @@ -20636,6 +20684,7 @@ a.lazy-thumb.filtered-upload-ghost:hover::after { outline: none; border-bottom: 2px solid var(--accent); background: rgba(0, 0, 0, 0.3); + min-width: 120px; } .color-picker-badge { @@ -20821,7 +20870,7 @@ body.onara-modal-open .admin-bar.open { z-index: 10050 !important; display: none; flex-direction: column !important; - background: rgba(0, 0, 0, var(--onara-bg-opacity, 0.84)) !important; + background: rgba(0, 0, 0, var(--onara-bg-opacity, 0.7)) !important; backdrop-filter: blur(var(--onara-backdrop-blur, 5px)) saturate(130%) !important; -webkit-backdrop-filter: blur(var(--onara-backdrop-blur, 5px)) saturate(130%) !important; overflow-y: auto !important; diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index f7dbcd6..a75404f 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -3652,6 +3652,272 @@ window.cancelAnimFrame = (function () { }); 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) { @@ -3662,6 +3928,7 @@ window.cancelAnimFrame = (function () { window.cancelAnimFrame(visualizerRafId); visualizerRafId = null; } + stopYoutubeAmbient(); const canvas = document.getElementById('bg'); if (canvas) { canvas._bgFadingOut = false; @@ -3777,21 +4044,56 @@ window.cancelAnimFrame = (function () { const context = canvas.getContext('2d'); const cfgCurrent = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; - const blurSetting = cfgCurrent.bgCanvasBlur !== undefined ? Number(cfgCurrent.bgCanvasBlur) : 15; + const blurSetting = cfgCurrent.bgCanvasBlur !== undefined ? Number(cfgCurrent.bgCanvasBlur) : 30; const blurPx = Math.round(blurSetting * SCALE); - const brightnessVal = cfgCurrent.bgCanvasBrightness !== undefined ? Number(cfgCurrent.bgCanvasBrightness) : 1.25; + const brightnessVal = cfgCurrent.bgCanvasBrightness !== undefined ? Number(cfgCurrent.bgCanvasBrightness) : 0.45; const getCanvasFilter = () => { const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; - const bSetting = cfg.bgCanvasBlur !== undefined ? Number(cfg.bgCanvasBlur) : 15; - const bPx = Math.round(bSetting * SCALE); - const bVal = cfg.bgCanvasBrightness !== undefined ? Number(cfg.bgCanvasBrightness) : 1.25; + const method = cfg.bgBlurMethod !== undefined ? Number(cfg.bgBlurMethod) : 0; + const bSetting = cfg.bgCanvasBlur !== undefined ? Number(cfg.bgCanvasBlur) : 30; + // Method 1 = GPU CSS filter only (no canvas blur, handled by CSS --bg-canvas-filter) + const bPx = method === 1 ? 0 : Math.round(bSetting * SCALE); + const bVal = cfg.bgCanvasBrightness !== undefined ? Number(cfg.bgCanvasBrightness) : 0.45; + const sat = cfg.bgCanvasSaturate !== undefined ? Number(cfg.bgCanvasSaturate) : 1.8; + const con = cfg.bgCanvasContrast !== undefined ? Number(cfg.bgCanvasContrast) : 1.1; 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'; }; + const drawCoverImage = (ctx, source) => { + if (!source) return; + let sw = 0, sh = 0; + if (source.videoWidth && source.videoHeight) { + sw = source.videoWidth; + sh = source.videoHeight; + } else if (source.naturalWidth && source.naturalHeight) { + sw = source.naturalWidth; + sh = source.naturalHeight; + } else if (source.width && source.height) { + sw = source.width; + sh = source.height; + } else if (source.clientWidth && source.clientHeight) { + sw = source.clientWidth; + sh = source.clientHeight; + } + + if (sw > 0 && sh > 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(source, dx, dy, dw, dh); + } else { + ctx.drawImage(source, 0, 0, cw, ch); + } + }; + let crossfadeRafId = null; const crossfadeToSource = (sourceImg, onComplete) => { if (crossfadeRafId) window.cancelAnimFrame(crossfadeRafId); @@ -3799,7 +4101,7 @@ window.cancelAnimFrame = (function () { context.clearRect(0, 0, cw, ch); context.save(); context.filter = getCanvasFilter(); - context.drawImage(sourceImg, 0, 0, cw, ch); + drawCoverImage(context, sourceImg); context.restore(); if (onComplete) onComplete(); return; @@ -3828,7 +4130,7 @@ window.cancelAnimFrame = (function () { context.globalAlpha = ease; context.filter = getCanvasFilter(); try { - context.drawImage(sourceImg, 0, 0, cw, ch); + drawCoverImage(context, sourceImg); } catch (e) {} context.restore(); @@ -3882,33 +4184,41 @@ window.cancelAnimFrame = (function () { } }; - let videoFadeStartTime = performance.now(); + let videoFadeStartTime = prevSnapshot ? performance.now() : 0; const animationLoop = () => { if (!elem || elem.tagName === 'AUDIO' || elem.paused || elem.ended || (!background && !canvas._bgFadingOut)) { bgRafId = null; return; } try { - const elapsed = performance.now() - videoFadeStartTime; - const progress = Math.min(1.0, elapsed / 350); - const ease = progress < 0.5 ? 2 * progress * progress : -1 + (4 - 2 * progress) * progress; - context.clearRect(0, 0, cw, ch); - if (ease < 1.0 && prevSnapshot) { + if (prevSnapshot) { + const elapsed = performance.now() - videoFadeStartTime; + const progress = Math.min(1.0, elapsed / 350); + const ease = progress < 0.5 ? 2 * progress * progress : -1 + (4 - 2 * progress) * progress; + + if (ease < 1.0) { + context.save(); + context.globalAlpha = 1.0 - ease; + context.drawImage(prevSnapshot, 0, 0, cw, ch); + context.restore(); + } + context.save(); - context.globalAlpha = 1.0 - ease; - context.drawImage(prevSnapshot, 0, 0, cw, ch); + context.globalAlpha = ease; + context.filter = getCanvasFilter(); + drawCoverImage(context, elem); context.restore(); - } - context.save(); - context.globalAlpha = ease; - context.filter = getCanvasFilter(); - context.drawImage(elem, 0, 0, cw, ch); - context.restore(); - - if (progress >= 1.0) { - prevSnapshot = null; + if (progress >= 1.0) { + prevSnapshot = null; + } + } else { + // Video already loaded or resumed from pause: render directly at 100% opacity without fading from black + context.save(); + context.filter = getCanvasFilter(); + drawCoverImage(context, elem); + context.restore(); } } catch (e) { bgRafId = null; @@ -3922,13 +4232,14 @@ window.cancelAnimFrame = (function () { if (lastBgElem !== elem || isAlbumVideo) { if (bgRafId) window.cancelAnimFrame(bgRafId); lastBgElem = elem; - videoFadeStartTime = performance.now(); + if (prevSnapshot) videoFadeStartTime = performance.now(); if (elem.tagName === 'VIDEO') { // For album videos (reused element), remove old listeners first const startVideoLoop = () => { if (bgRafId) window.cancelAnimFrame(bgRafId); - videoFadeStartTime = performance.now(); + // Only reset fade start time if a previous snapshot is actively waiting to be crossfaded + if (prevSnapshot) videoFadeStartTime = performance.now(); if (background) animationLoop(); }; if (isAlbumVideo) { @@ -3945,7 +4256,7 @@ window.cancelAnimFrame = (function () { } else if (elem.tagName === 'CANVAS') { // Ruffle canvas: start loop immediately if (bgRafId) window.cancelAnimFrame(bgRafId); - videoFadeStartTime = performance.now(); + if (prevSnapshot) videoFadeStartTime = performance.now(); if (background) animationLoop(); } else if (elem.tagName === 'AUDIO') { elem.addEventListener('play', () => { @@ -4006,43 +4317,67 @@ window.cancelAnimFrame = (function () { } } 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`; + const mount = document.getElementById('onara-item-mount'); + const ytEmbed = (mount && mount.querySelector('#yt-embed')) || document.getElementById('yt-embed'); + if (ytEmbed) { + if (background) { + canvas._bgFadingOut = false; + initYoutubeAmbient(ytEmbed, canvas, true); } else { - canvas.classList.remove('fader-out', 'fast-fade'); - canvas.classList.add('fader-in'); + stopYoutubeAmbient(); + canvas._bgFadingOut = true; + canvas.classList.add('fader-out'); + canvas.classList.remove('fader-in', 'fast-fade'); } } 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); + stopYoutubeAmbient(); + // No drawable element (e.g. generic file) — 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)`; + const sw = thumb.naturalWidth || thumb.width; + const sh = thumb.naturalHeight || thumb.height; + if (sw > 0 && sh > 0) { + const scale = Math.max(cw / sw, ch / sh); + const dw = sw * scale; + const dh = sh * scale; + context.drawImage(thumb, (cw - dw) / 2, (ch - dh) / 2, dw, dh); + } else { + 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'); } - }; - canvas.addEventListener('transitionend', stopOnFadeEnd); + } 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); + } } } }; @@ -4079,15 +4414,16 @@ window.cancelAnimFrame = (function () { useCustomInnerColor: 0, innerBarColor: "#ff0000", innerBarsMode: 4, - innerRadius: 150, - innerPupilRadius: 46, - innerPupilRingOpacity: 0, - innerBarCount: 73, - innerBarWidth: 80, - innerBarHeight: 2.65, - innerBarOpacity: 0.13, + innerRadius: 152, + innerPupilRadius: 42, + innerPupilRingOpacity: 0.99, + innerPupilColor: "#000000", + innerBarCount: 21, + innerBarWidth: 12, + innerBarHeight: 0.85, + innerBarOpacity: 0.36, outerRingOpacity: 0, - innerHighBoost: 0.162, + innerHighBoost: 0.34, innerRadialRotation: 90, innerBarGlow: 0, innerHideNote: 1, @@ -4099,8 +4435,8 @@ window.cancelAnimFrame = (function () { barGlow: 8, smoothing: 0.86, followMouse: 1, - followSpeed: 0.1, - followRadius: 15, + followSpeed: 0.02, + followRadius: 10, tiltEffect: 1, glowAttack: 1, glowDecay: 0.5, @@ -4114,35 +4450,83 @@ window.cancelAnimFrame = (function () { enableBlink: 0, blinkInterval: 12, beatHueSpeed: 0.1, - onaraBgOpacity: 0.35, - onaraBackdropBlur: 0, + onaraBgOpacity: 0.7, + onaraBackdropBlur: 5, bgCanvasOpacity: 1, - bgCanvasBlur: 59, - bgCanvasBrightness: 0.35, - bgCanvasScale: 0.95, - onaraGridDim: 0, + bgBlurMethod: 0, + bgCanvasBlur: 30, + bgCanvasBrightness: 0.45, + bgCanvasSaturate: 1.8, + bgCanvasContrast: 1.1, + bgCanvasZoom: 1.12, + bgCanvasScale: 3, + onaraGridDim: 0.38, feedAudioToBg: 0 }; + const TUNING_CONFIG_VERSION = '2026-09-15_tuner_v6'; + + const getCustomDefaultTuning = () => { + try { + const stored = localStorage.getItem('f0ck_audio_default_custom'); + if (stored) { + const parsed = JSON.parse(stored); + if (parsed && typeof parsed === 'object') return parsed; + } + } catch (e) {} + if (window.f0ckServerAudioTuner && typeof window.f0ckServerAudioTuner === 'object') { + return window.f0ckServerAudioTuner; + } + return null; + }; + + const getEffectiveDefaultTuning = () => { + const custom = getCustomDefaultTuning(); + return Object.assign({}, DEFAULT_AUDIO_TUNING, custom || {}); + }; + + window.DEFAULT_AUDIO_TUNING = DEFAULT_AUDIO_TUNING; + window.getEffectiveDefaultTuning = getEffectiveDefaultTuning; + let savedTuning = null; try { - const raw = localStorage.getItem('f0ck_audio_tuning'); - if (raw) savedTuning = JSON.parse(raw); + const appliedVer = localStorage.getItem('f0ck_audio_tuning_ver'); + if (appliedVer !== TUNING_CONFIG_VERSION) { + const eff = getEffectiveDefaultTuning(); + localStorage.setItem('f0ck_audio_tuning', JSON.stringify(eff)); + localStorage.setItem('f0ck_audio_tuning_ver', TUNING_CONFIG_VERSION); + savedTuning = Object.assign({}, eff); + } else { + const raw = localStorage.getItem('f0ck_audio_tuning'); + if (raw) savedTuning = JSON.parse(raw); + } } catch (e) {} - window.audioVisualizerTuning = Object.assign({}, DEFAULT_AUDIO_TUNING, savedTuning || {}); + window.audioVisualizerTuning = Object.assign({}, getEffectiveDefaultTuning(), savedTuning || {}); const applyBackgroundOpacitySettings = (cfg) => { const c = cfg || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; - const onaraOp = c.onaraBgOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.onaraBgOpacity))) : 0.84; + const onaraOp = c.onaraBgOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.onaraBgOpacity))) : 0.70; const bgOp = c.bgCanvasOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasOpacity))) : 1.00; - const gridThumbOp = c.onaraGridDim !== undefined ? Math.min(1, Math.max(0, Number(c.onaraGridDim))) : 0.15; + const gridThumbOp = c.onaraGridDim !== undefined ? Math.min(1, Math.max(0, Number(c.onaraGridDim))) : 0.38; const onaraBlur = c.onaraBackdropBlur !== undefined ? Math.max(0, Number(c.onaraBackdropBlur)) : 5; + const bgBlur = c.bgCanvasBlur !== undefined ? Math.max(0, Number(c.bgCanvasBlur)) : 30; + const method = c.bgBlurMethod !== undefined ? Number(c.bgBlurMethod) : 0; + const zoom = c.bgCanvasZoom !== undefined ? Math.max(1.0, Number(c.bgCanvasZoom)) : 1.12; document.documentElement.style.setProperty('--onara-bg-opacity', onaraOp.toFixed(3)); document.documentElement.style.setProperty('--bg-canvas-opacity', bgOp.toFixed(3)); document.documentElement.style.setProperty('--onara-grid-dim-opacity', gridThumbOp.toFixed(3)); document.documentElement.style.setProperty('--onara-backdrop-blur', `${onaraBlur}px`); + document.documentElement.style.setProperty('--bg-canvas-blur', `${bgBlur}px`); + document.documentElement.style.setProperty('--bg-canvas-zoom', zoom.toFixed(3)); + + // Method 1 = GPU CSS filter (the classic look); Method 2 = Hybrid (CSS blur + canvas sat/contrast) + if (method === 1 || method === 2) { + document.documentElement.style.setProperty('--bg-canvas-filter', `blur(${bgBlur}px)`); + } else { + document.documentElement.style.setProperty('--bg-canvas-filter', 'none'); + } const bgEl = document.getElementById('bg'); if (bgEl) { @@ -4354,7 +4738,8 @@ window.cancelAnimFrame = (function () { { 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: 'innerPupilRingOpacity', label: 'Center Pupil Fill Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' }, + { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerPupilColor', label: 'Center Pupil Fill Color', type: 'color' }, { 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: '' }, @@ -4373,17 +4758,25 @@ window.cancelAnimFrame = (function () { { 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: '' }, + { section: 'Eye Simulation & Mouse Follow', key: 'tiltEffect', label: '3D Eye Tilt (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' } + ]; - // Background Canvas & Onara Mode Section - { section: 'Background Canvas & Onara Mode', key: 'bgCanvasOpacity', label: 'Background Canvas Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' }, - { section: 'Background Canvas & Onara Mode', key: 'bgCanvasBlur', label: 'Background Canvas Blur Amount', min: 0, max: 300, step: 1, unit: 'px' }, - { section: 'Background Canvas & Onara Mode', key: 'bgCanvasBrightness', label: 'Background Brightness Boost', min: 0.20, max: 3.00, step: 0.05, unit: 'x' }, - { section: 'Background Canvas & Onara Mode', key: 'bgCanvasScale', label: 'Background Visualizer Bar Scale', min: 0.20, max: 4.00, step: 0.05, unit: 'x' }, - { section: 'Background Canvas & Onara Mode', key: 'onaraBgOpacity', label: 'Onara Modal Backdrop Darkness', min: 0.00, max: 1.00, step: 0.01, unit: '' }, - { section: 'Background Canvas & Onara Mode', key: 'onaraBackdropBlur', label: 'Onara Modal Backdrop Blur', min: 0, max: 50, step: 1, unit: 'px' }, - { section: 'Background Canvas & Onara Mode', key: 'onaraGridDim', label: 'Onara Background Grid Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' }, - { section: 'Background Canvas & Onara Mode', key: 'feedAudioToBg', label: 'Feed Visualizer to Background Canvas (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' } + const backgroundSliders = [ + // Video & Ambient Background Canvas Section + { section: 'Video & Ambient Background Canvas', key: 'bgCanvasOpacity', label: 'Background Canvas Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' }, + { section: 'Video & Ambient Background Canvas', key: 'bgBlurMethod', label: 'Blur Engine (0=Canvas 2D, 1=GPU CSS, 2=Hybrid)', min: 0, max: 2, step: 1, unit: '' }, + { section: 'Video & Ambient Background Canvas', key: 'bgCanvasBlur', label: 'Blur Radius', min: 0, max: 300, step: 1, unit: 'px' }, + { section: 'Video & Ambient Background Canvas', key: 'bgCanvasBrightness', label: 'Brightness', min: 0.10, max: 3.00, step: 0.05, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'bgCanvasSaturate', label: 'Color Saturation', min: 0.50, max: 4.00, step: 0.05, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'bgCanvasContrast', label: 'Contrast Depth', min: 0.50, max: 3.00, step: 0.05, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'bgCanvasZoom', label: 'Edge Overscan Zoom', min: 1.00, max: 1.80, step: 0.01, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'bgCanvasScale', label: 'Visualizer Bar Scale', min: 0.20, max: 4.00, step: 0.05, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'feedAudioToBg', label: 'Feed Visualizer to Background Canvas (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' }, + + // Onara Mode Background Section + { section: 'Onara Mode Background', key: 'onaraBgOpacity', label: 'Onara Modal Backdrop Darkness', min: 0.00, max: 1.00, step: 0.01, unit: '' }, + { section: 'Onara Mode Background', key: 'onaraBackdropBlur', label: 'Onara Modal Backdrop Blur', min: 0, max: 50, step: 1, unit: 'px' }, + { section: 'Onara Mode Background', key: 'onaraGridDim', label: 'Onara Background Grid Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' } ]; const danmakuSliders = [ @@ -4466,6 +4859,25 @@ window.cancelAnimFrame = (function () { } }); + let bgRowsHtml = ''; + let currentBgSection = ''; + backgroundSliders.forEach(s => { + if (s.section && s.section !== currentBgSection) { + currentBgSection = s.section; + bgRowsHtml += `