diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 7e8f122..180807a 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -41,11 +41,12 @@ --badge-tag: #090909; --posts-meta-bg: #000000b8; --x2154: #215483; - --bg-canvas-opacity: 1.0; - --bg-canvas-blur: 30px; - --bg-canvas-color: transparent; - --bg-canvas-color-opacity: 0.0; - --onara-bg-opacity: 0.7; + --bg-canvas-opacity: 0.81; + --bg-canvas-blur: 151px; + --bg-canvas-zoom: 1.38; + --bg-canvas-color: rgba(0, 0, 0, 0.09); + --bg-canvas-color-opacity: 0.09; + --onara-bg-opacity: 0.4; --onara-backdrop-blur: 5px; --onara-grid-dim-opacity: 0.38; } @@ -10391,11 +10392,11 @@ div.favs div.posts { bottom: 0; width: calc(100vw - var(--sidebar-w, 300px)); height: 100%; - transform: translate3d(0, 0, 0) scale(var(--bg-canvas-zoom, 1.12)); + transform: translate3d(0, 0, 0) scale(var(--bg-canvas-zoom, 1.38)); transform-origin: center center; z-index: -2; transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s ease-in-out; - opacity: var(--bg-canvas-opacity, 1.0); + opacity: var(--bg-canvas-opacity, 0.81); filter: var(--bg-canvas-filter, none); -webkit-filter: var(--bg-canvas-filter, none); background-color: var(--bg-canvas-color, transparent); @@ -20947,7 +20948,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.7)) !important; + background: rgba(0, 0, 0, var(--onara-bg-opacity, 0.4)) !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; @@ -21148,11 +21149,36 @@ body.layout-modern #onara-modal .item-layout-container .item-main-content { margin-right: auto !important; } -#onara-modal .content:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom), -#onara-modal ._204863:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom), -#onara-modal .item_title:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom), -#onara-modal .item-title:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom) { - animation: contentSlideIn 0.55s cubic-bezier(0.16, 1, 0.3, 1) both !important; +#onara-modal .content.slide-next, +#onara-modal .content.slide-prev, +#onara-modal ._204863.slide-next, +#onara-modal ._204863.slide-prev, +#onara-modal .item_title.slide-next, +#onara-modal .item_title.slide-prev, +#onara-modal .item-title.slide-next, +#onara-modal .item-title.slide-prev, +#onara-modal .content:not(.slide-out-left):not(.slide-out-right):not(.fade-out-zoom), +#onara-modal ._204863:not(.slide-out-left):not(.slide-out-right):not(.fade-out-zoom), +#onara-modal .item_title:not(.slide-out-left):not(.slide-out-right):not(.fade-out-zoom), +#onara-modal .item-title:not(.slide-out-left):not(.slide-out-right):not(.fade-out-zoom) { + animation: contentFadeInZoom 0.32s cubic-bezier(0.16, 1, 0.3, 1) both !important; + will-change: transform, opacity; +} + +#onara-modal .content.slide-out-left, +#onara-modal .content.slide-out-right, +#onara-modal ._204863.slide-out-left, +#onara-modal ._204863.slide-out-right, +#onara-modal .item_title.slide-out-left, +#onara-modal .item_title.slide-out-right, +#onara-modal .item-title.slide-out-left, +#onara-modal .item-title.slide-out-right, +#onara-modal .content.fade-out-zoom, +#onara-modal ._204863.fade-out-zoom, +#onara-modal .item_title.fade-out-zoom, +#onara-modal .item-title.fade-out-zoom { + animation: contentFadeOutZoom 0.18s cubic-bezier(0.3, 0, 0.8, 1) forwards !important; + pointer-events: none; will-change: transform, opacity; } diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 11e1c89..da0c3f9 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -1682,7 +1682,11 @@ window.cancelAnimFrame = (function () { modal._closeTimeout = null; } modal._isClosing = false; + const wasAlreadyOpen = document.body.classList.contains('onara-modal-open'); document.body.classList.add('onara-modal-open'); + if (typeof window.applyBackgroundOpacitySettings === 'function') { + window.applyBackgroundOpacitySettings(null, true); + } if (document.querySelector('.index-container') && window.scrollY !== 0) { window.scrollTo(0, 0); } @@ -1698,9 +1702,9 @@ window.cancelAnimFrame = (function () { canvas.classList.remove('fader-out', 'fast-fade'); canvas.classList.add('fader-in'); canvas.style.transition = 'none'; - canvas.style.opacity = 'var(--bg-canvas-opacity, 1.0)'; + canvas.style.opacity = 'var(--bg-canvas-opacity, 0.95)'; } - if (typeof window.paintImmediateBgThumb === 'function') { + if (!wasAlreadyOpen && typeof window.paintImmediateBgThumb === 'function') { window.paintImmediateBgThumb(); } @@ -1978,6 +1982,9 @@ window.cancelAnimFrame = (function () { modal.classList.remove('onara-fade-in'); modal.classList.add('onara-fade-out'); document.body.classList.remove('onara-modal-open'); + if (typeof window.applyBackgroundOpacitySettings === 'function') { + window.applyBackgroundOpacitySettings(null, false); + } // Destroy background instance when Onara modal closes so it doesn't linger on the index grid if (window.destroyBackgroundInstance) { @@ -4028,6 +4035,199 @@ window.cancelAnimFrame = (function () { } }; + 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 top = countTop > 0 + ? [Math.round(rTop / countTop), Math.round(gTop / countTop), Math.round(bTop / countTop)] + : [25, 25, 35]; + const bot = countBot > 0 + ? [Math.round(rBot / countBot), Math.round(gBot / countBot), Math.round(bBot / countBot)] + : [25, 25, 35]; + + return { top, bottom: bot }; + } catch (e) { + return null; + } + }; + + 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) => { + 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; + const intensity = cfg.bgAmbientIntensity !== undefined ? Number(cfg.bgAmbientIntensity) : 1.0; + const spread = cfg.bgAmbientSpread !== undefined ? Number(cfg.bgAmbientSpread) : 0.85; + + 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); + + 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); + }; + // Paint background canvas immediately from thumbnail without network or animation delay window.paintImmediateBgThumb = (thumbOrElem) => { if (typeof background !== 'undefined' && !background) return; @@ -4058,18 +4258,18 @@ window.cancelAnimFrame = (function () { canvas.classList.remove('fader-out', 'fast-fade'); canvas.classList.add('fader-in'); canvas.style.transition = 'none'; - canvas.style.opacity = 'var(--bg-canvas-opacity, 1.0)'; + canvas.style.opacity = document.body.classList.contains('onara-modal-open') ? 'var(--bg-canvas-opacity, 0.95)' : 'var(--bg-canvas-opacity, 0.81)'; const ctx = canvas.getContext('2d'); if (!ctx) return; - const cfg = (typeof window.audioVisualizerTuning !== 'undefined' && window.audioVisualizerTuning) || (typeof DEFAULT_AUDIO_TUNING !== 'undefined' ? DEFAULT_AUDIO_TUNING : {}); + 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) : 30; - 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 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})`); @@ -4077,23 +4277,33 @@ window.cancelAnimFrame = (function () { if (con !== 1) parts.push(`contrast(${con})`); const filterStr = parts.length > 0 ? parts.join(' ') : 'none'; - 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 && 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(img, dx, dy, dw, dh); + 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.drawImage(img, 0, 0, cw, ch); + 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(); } - ctx.restore(); // Color overlay tint if configured const colOp = cfg.bgCanvasColorOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.bgCanvasColorOpacity))) : 0; @@ -4216,20 +4426,22 @@ window.cancelAnimFrame = (function () { } const context = canvas.getContext('2d'); - const cfgCurrent = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; - const blurSetting = cfgCurrent.bgCanvasBlur !== undefined ? Number(cfgCurrent.bgCanvasBlur) : 30; - const blurPx = Math.round(blurSetting * SCALE); - const brightnessVal = cfgCurrent.bgCanvasBrightness !== undefined ? Number(cfgCurrent.bgCanvasBrightness) : 0.45; + const cfgCurrent = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; + const blurSetting = cfgCurrent.bgCanvasBlur !== undefined ? Number(cfgCurrent.bgCanvasBlur) : 31; + const methodSetting = cfgCurrent.bgBlurMethod !== undefined ? Number(cfgCurrent.bgBlurMethod) : 0; + const blurPx = (methodSetting === 1 || methodSetting === 2) ? 0 : Math.round(blurSetting * SCALE); + const brightnessVal = cfgCurrent.bgCanvasBrightness !== undefined ? Number(cfgCurrent.bgCanvasBrightness) : 0.6; const getCanvasFilter = () => { - const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; + const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; const method = cfg.bgBlurMethod !== undefined ? Number(cfg.bgBlurMethod) : 0; - const bSetting = cfg.bgCanvasBlur !== undefined ? Number(cfg.bgCanvasBlur) : 30; + const bSetting = cfg.bgCanvasBlur !== undefined ? Number(cfg.bgCanvasBlur) : 31; // 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; + // Method 2 = Hybrid (CSS blur + canvas sat/contrast) + 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})`); @@ -4270,7 +4482,7 @@ window.cancelAnimFrame = (function () { }; const applyBgColorOverlay = (ctx, w, h) => { - const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; + const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; const colOp = cfg.bgCanvasColorOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.bgCanvasColorOpacity))) : 0; if (colOp > 0.001) { const col = cfg.bgCanvasColor || '#000000'; @@ -4287,8 +4499,15 @@ window.cancelAnimFrame = (function () { syncCanvasDimensions(); const targetW = canvas.width; const targetH = canvas.height; - const isOnara = document.body.classList.contains('onara-modal-open') || (typeof isOnaraActive === 'function' && isOnaraActive()); - if (!prevSnapshot || isOnara) { + const useAmbient = Number(cfgCurrent.bgAmbientLighting) === 1; + + if (useAmbient) { + const sampled = sampleMediaSpatialColors(sourceImg) || { top: [30, 30, 45], bottom: [30, 30, 45] }; + transitionAmbientToColors(sampled, onComplete); + return; + } + + if (!prevSnapshot) { context.clearRect(0, 0, targetW, targetH); context.save(); context.filter = getCanvasFilter(); @@ -4312,15 +4531,12 @@ window.cancelAnimFrame = (function () { const h = canvas.height; context.clearRect(0, 0, w, h); - // 1. Fade out previous background - if (ease < 1.0 && prevSnapshot) { - context.save(); - context.globalAlpha = 1.0 - ease; + // 1. Base: keep previous background at full opacity underneath (prevents transparent dip) + if (prevSnapshot) { context.drawImage(prevSnapshot, 0, 0, w, h); - context.restore(); } - // 2. Fade in new background with blur and brightness + // 2. Fade in new background smoothly over previous context.save(); context.globalAlpha = ease; context.filter = getCanvasFilter(); @@ -4352,36 +4568,37 @@ window.cancelAnimFrame = (function () { if (itemId) { if (document.body.classList.contains('onara-modal-open')) { const onaraThumbImg = document.querySelector('.posts > a.thumb.onara-active img, .posts > a.thumb:focus img'); + if (isDrawable && elem.complete && elem.naturalWidth > 0) { + crossfadeToSource(elem); + return; + } if (onaraThumbImg && onaraThumbImg.complete && onaraThumbImg.naturalWidth > 0) { - crossfadeToSource(onaraThumbImg, () => { - if (isDrawable && elem.complete && elem.naturalWidth > 0) { - crossfadeToSource(elem); - } - }); + crossfadeToSource(onaraThumbImg); return; } } - const thumb = new Image(); - thumb.onload = () => { - crossfadeToSource(thumb, () => { - if (isDrawable) { - if (elem.complete && elem.naturalWidth > 0) { - crossfadeToSource(elem); - } else { - elem.onload = () => crossfadeToSource(elem); - } - } - }); + let resolved = false; + const onLoaded = (src) => { + if (resolved) return; + resolved = true; + crossfadeToSource(src); }; + + if (isDrawable) { + if (elem.complete && elem.naturalWidth > 0) { + onLoaded(elem); + return; + } + elem.onload = () => onLoaded(elem); + } + + const thumb = new Image(); + thumb.onload = () => onLoaded(thumb); thumb.onerror = () => { - if (isDrawable) { - if (elem.complete && elem.naturalWidth > 0) { - crossfadeToSource(elem); - } else { - elem.onload = () => crossfadeToSource(elem); - } - } + if (isDrawable && elem.complete && elem.naturalWidth > 0) { + onLoaded(elem); + } }; let newSrc = `/t/${itemId}.webp`; if (window.applyThumbCacheBust) newSrc = window.applyThumbCacheBust(newSrc); @@ -4407,38 +4624,66 @@ window.cancelAnimFrame = (function () { const curCw = canvas.width; const curCh = canvas.height; context.clearRect(0, 0, curCw, curCh); - const isOnara = document.body.classList.contains('onara-modal-open') || (typeof isOnaraActive === 'function' && isOnaraActive()); - if (prevSnapshot && !isOnara) { - 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, curCw, curCh); - context.restore(); + const useAmbient = Number(cfgCurrent.bgAmbientLighting) === 1; + if (useAmbient) { + if (!elem._ambientFrameCount) elem._ambientFrameCount = 0; + elem._ambientFrameCount++; + if (elem._ambientFrameCount % 3 === 0 || !mediaAmbientState.hasSampled) { + const sampled = sampleMediaSpatialColors(elem); + if (sampled) { + setAmbientTargetColors(sampled); + mediaAmbientState.hasSampled = true; + } } - context.save(); - context.globalAlpha = ease; - context.filter = getCanvasFilter(); - try { - drawCoverImage(context, elem); - } catch (e) {} - context.restore(); + // Smooth exponential lerp on every single RAF frame (0.05 per frame = buttery smooth) + const current = updateAmbientLerp(0.05); - if (progress >= 1.0) { - prevSnapshot = null; + 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; + + context.drawImage(prevSnapshot, 0, 0, curCw, curCh); + renderAmbientLightingWash(context, curCw, curCh, current, ease); + + if (progress >= 1.0) { + prevSnapshot = null; + } + } else { + renderAmbientLightingWash(context, curCw, curCh, current, 1.0); } } else { - // Video already loaded or resumed from pause (or instant in Onara): render directly at 100% opacity without fading from black - context.save(); - context.filter = getCanvasFilter(); - try { - drawCoverImage(context, elem); - } catch (e) {} - context.restore(); + 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; + + // Base: previous frame drawn underneath at full opacity + context.drawImage(prevSnapshot, 0, 0, curCw, curCh); + + // Video dissolves smoothly on top + context.save(); + context.globalAlpha = ease; + context.filter = getCanvasFilter(); + try { + drawCoverImage(context, elem); + } catch (e) {} + context.restore(); + + 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(); + try { + drawCoverImage(context, elem); + } catch (e) {} + context.restore(); + } } // Color overlay tint if configured @@ -4458,8 +4703,19 @@ window.cancelAnimFrame = (function () { if (prevSnapshot) videoFadeStartTime = performance.now(); if (elem.tagName === 'VIDEO') { - // For album videos (reused element), remove old listeners first + if (background) { + if (!elem.paused) { + if (bgRafId) window.cancelAnimFrame(bgRafId); + animationLoop(); + } + // For album videos that haven't started yet, or Onara videos pending play, drawOnce with the thumbnail + else if (isAlbumVideo || document.body.classList.contains('onara-modal-open')) { + drawOnce(); + } + } + const startVideoLoop = () => { + if (crossfadeRafId) window.cancelAnimFrame(crossfadeRafId); if (bgRafId) window.cancelAnimFrame(bgRafId); // Only reset fade start time if a previous snapshot is actively waiting to be crossfaded if (prevSnapshot) videoFadeStartTime = performance.now(); @@ -4721,23 +4977,81 @@ window.cancelAnimFrame = (function () { enableBlink: 0, blinkInterval: 12, beatHueSpeed: 0.1, - onaraBgOpacity: 0.7, + onaraBgOpacity: 0.4, onaraBackdropBlur: 5, - bgCanvasColor: '#000000', - bgCanvasColorOpacity: 0, - bgCanvasOpacity: 1, + bgCanvasColor: "#000000", + bgCanvasColorOpacity: 0.09, + bgCanvasOpacity: 0.81, bgBlurMethod: 0, - bgCanvasBlur: 30, - bgCanvasBrightness: 0.45, - bgCanvasSaturate: 1.8, - bgCanvasContrast: 1.1, - bgCanvasZoom: 1.12, - bgCanvasScale: 3, + bgCanvasBlur: 151, + bgCanvasBrightness: 0.6, + bgCanvasSaturate: 1.9, + bgCanvasContrast: 1.15, + bgCanvasZoom: 1.38, + bgCanvasScale: 2.15, onaraGridDim: 0.38, - feedAudioToBg: 0 + feedAudioToBg: 1, + bgAmbientLighting: 1, + bgAmbientIntensity: 1.9, + bgAmbientSpread: 0.5, + bgAmbientSaturation: 1.5, + bgAmbientSmoothness: 0.06 }; - const TUNING_CONFIG_VERSION = '2026-09-16_tuner_v7'; + const TUNING_CONFIG_VERSION = '2026-09-16_tuner_v12'; + + const DEFAULT_ONARA_TUNING = { + bgCanvasColor: "#000000", + bgCanvasColorOpacity: 0.8, + bgCanvasOpacity: 0.95, + bgBlurMethod: 2, + bgCanvasBlur: 31, + bgCanvasBrightness: 0.6, + bgCanvasSaturate: 1.85, + bgCanvasContrast: 1.15, + bgCanvasZoom: 1.38, + bgCanvasScale: 2.15, + feedAudioToBg: 1, + onaraBgOpacity: 0, + onaraBackdropBlur: 10, + onaraGridDim: 0, + bgAmbientLighting: 0, + bgAmbientIntensity: 1.0, + bgAmbientSpread: 0.85, + bgAmbientSaturation: 1.35, + bgAmbientSmoothness: 0.05 + }; + + const ONARA_TUNING_CONFIG_VERSION = '2026-09-16_onara_v3'; + let savedOnaraTuning = null; + try { + const appliedOnaraVer = localStorage.getItem('f0ck_onara_tuning_ver'); + if (appliedOnaraVer !== ONARA_TUNING_CONFIG_VERSION) { + localStorage.setItem('f0ck_onara_tuning', JSON.stringify(DEFAULT_ONARA_TUNING)); + localStorage.setItem('f0ck_onara_tuning_ver', ONARA_TUNING_CONFIG_VERSION); + savedOnaraTuning = Object.assign({}, DEFAULT_ONARA_TUNING); + } else { + const raw = localStorage.getItem('f0ck_onara_tuning'); + if (raw) savedOnaraTuning = JSON.parse(raw); + } + } catch (e) {} + + window.DEFAULT_ONARA_TUNING = DEFAULT_ONARA_TUNING; + window.onaraTuning = Object.assign({}, DEFAULT_ONARA_TUNING, savedOnaraTuning || {}); + + const isCurrentlyInOnara = () => { + return document.body.classList.contains('onara-modal-open') || + (typeof isOnaraActive === 'function' && isOnaraActive() && !!document.getElementById('onara-item-mount')); + }; + + const getEffectiveBackgroundTuning = (forceMode) => { + const useOnara = forceMode !== undefined ? (forceMode === 'onara' || forceMode === true) : isCurrentlyInOnara(); + if (useOnara) { + return Object.assign({}, window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING, window.onaraTuning || DEFAULT_ONARA_TUNING); + } + return window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; + }; + window.getEffectiveBackgroundTuning = getEffectiveBackgroundTuning; const getCustomDefaultTuning = () => { try { @@ -4777,17 +5091,18 @@ window.cancelAnimFrame = (function () { 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.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.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; + const applyBackgroundOpacitySettings = (cfg, forceMode) => { + const inOnara = forceMode !== undefined ? (forceMode === 'onara' || forceMode === true) : isCurrentlyInOnara(); + const c = cfg || getEffectiveBackgroundTuning(forceMode); + const onaraOp = c.onaraBgOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.onaraBgOpacity))) : (inOnara ? 0.0 : 0.40); + const bgOp = c.bgCanvasOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasOpacity))) : (inOnara ? 0.95 : 0.81); + const gridThumbOp = c.onaraGridDim !== undefined ? Math.min(1, Math.max(0, Number(c.onaraGridDim))) : (inOnara ? 0.0 : 0.38); + const onaraBlur = c.onaraBackdropBlur !== undefined ? Math.max(0, Number(c.onaraBackdropBlur)) : (inOnara ? 10 : 5); + const bgBlur = c.bgCanvasBlur !== undefined ? Math.max(0, Number(c.bgCanvasBlur)) : (inOnara ? 31 : 151); + const method = c.bgBlurMethod !== undefined ? Number(c.bgBlurMethod) : (inOnara ? 2 : 0); + const zoom = c.bgCanvasZoom !== undefined ? Math.max(1.0, Number(c.bgCanvasZoom)) : 1.38; const bgColor = c.bgCanvasColor || '#000000'; - const bgColOp = c.bgCanvasColorOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasColorOpacity))) : 0.0; + const bgColOp = c.bgCanvasColorOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasColorOpacity))) : (inOnara ? 0.8 : 0.09); const computedBgCol = bgColOp <= 0.001 ? 'transparent' : (bgColOp >= 0.999 ? bgColor : `color-mix(in srgb, ${bgColor} ${Math.round(bgColOp * 100)}%, transparent)`); document.documentElement.style.setProperty('--onara-bg-opacity', onaraOp.toFixed(3)); @@ -4799,8 +5114,12 @@ window.cancelAnimFrame = (function () { document.documentElement.style.setProperty('--bg-canvas-color', computedBgCol); document.documentElement.style.setProperty('--bg-canvas-color-opacity', bgColOp.toFixed(3)); - // Method 1 = GPU CSS filter (the classic look); Method 2 = Hybrid (CSS blur + canvas sat/contrast) - if (method === 1 || method === 2) { + // Ambient lighting mode: when enabled, soft GPU diffusion blur makes it look identical to YouTube's ambient mode + const useAmbient = Number(c.bgAmbientLighting) === 1; + if (useAmbient) { + const ambientBlur = Math.max(30, Math.min(100, Number(c.bgCanvasBlur) || 48)); + document.documentElement.style.setProperty('--bg-canvas-filter', `blur(${ambientBlur}px)`); + } else if (method === 1 || method === 2) { document.documentElement.style.setProperty('--bg-canvas-filter', `blur(${bgBlur}px)`); } else { document.documentElement.style.setProperty('--bg-canvas-filter', 'none'); @@ -4816,7 +5135,7 @@ window.cancelAnimFrame = (function () { } }; window.applyBackgroundOpacitySettings = applyBackgroundOpacitySettings; - applyBackgroundOpacitySettings(window.audioVisualizerTuning); + applyBackgroundOpacitySettings(null); // Eye Cursor Follower & 3D Tracking const mousePos = { x: null, y: null, active: false }; @@ -5095,6 +5414,11 @@ window.cancelAnimFrame = (function () { { section: 'Video & Ambient Background Canvas', key: 'bgCanvasColor', label: 'Background Color', type: 'color' }, { section: 'Video & Ambient Background Canvas', key: 'bgCanvasColorOpacity', label: 'Background Color Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' }, { 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: 'bgAmbientLighting', label: 'Ambient Lighting (1=YouTube Glow, 0=Filter)', min: 0, max: 1, step: 1, unit: '' }, + { section: 'Video & Ambient Background Canvas', key: 'bgAmbientIntensity', label: 'Ambient Glow Brightness', min: 0.10, max: 2.50, step: 0.05, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'bgAmbientSpread', label: 'Ambient Glow Spread / Size', min: 0.30, max: 2.00, step: 0.05, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'bgAmbientSaturation', label: 'Ambient Glow Saturation', min: 0.50, max: 3.50, step: 0.05, unit: 'x' }, + { section: 'Video & Ambient Background Canvas', key: 'bgAmbientSmoothness', label: 'Ambient Video Transition Speed', min: 0.01, max: 0.25, 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' }, @@ -5367,13 +5691,17 @@ window.cancelAnimFrame = (function () {