(function () { 'use strict'; const STORAGE_KEY = 'w0bmFlashFilterSettings_v1'; const DEFAULT_SETTINGS = { yank: 60, // 0..100 master "yank" enabled: false, advanced: false, // if true, use individual params instead of yank mapping enableResolution: true, // per-axis degradation toggles (advanced only) enableFps: true, enablePalette: true, internalWidth: 320, fps: 12, paletteLevels: 4 }; let settings = loadSettings(); let currentConfig = computeConfig(); let chanLUT = buildChannelLUT(currentConfig.paletteLevels); let currentController = null; let hotkeyAttached = false; let ui = null; // { panel, slider, yankValue, info, toggle } const isMobile = /Mobi/i.test(navigator.userAgent); function isItemPage() { const path = window.location.pathname; // Strictly match item pages (e.g., /123, /user/name/123) and exclude grids/specials const isItem = (path.match(/^\/\d+/) || path.split('/').some(s => /^\d+$/.test(s))) && !path.match(/\/p\//); const isForbidden = path === '/upload' || path.startsWith('/admin') || path.startsWith('/mod'); return isItem && !isForbidden; } // ---------- Settings / Config ---------- function loadSettings() { try { const val = localStorage.getItem(STORAGE_KEY); if (val) { const obj = JSON.parse(val); return Object.assign({}, DEFAULT_SETTINGS, obj); } } catch (e) { console.error('Failed to load settings from localStorage', e); } return Object.assign({}, DEFAULT_SETTINGS); } function saveSettings() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } catch (e) { console.error('Failed to save settings to localStorage', e); } } // Yank 0..100 => config // Higher yank = harsher: lower res, lower FPS, fewer colors function computeConfigFromYank(yank) { const t = Math.min(1, Math.max(0, yank / 100)); // Match the ranges used by the advanced sliders: // Resolution: 160..800, FPS: 8..30, Palette levels: 2..12 const widthMax = 800, widthMin = 160; const fpsMax = 30, fpsMin = 8; const levelsMax = 12, levelsMin = 2; // per channel const width = Math.round(widthMax - (widthMax - widthMin) * t); const height = Math.round(width * 9 / 16); const fps = Math.round(fpsMax - (fpsMax - fpsMin) * t); const paletteLevels = Math.round(levelsMax - (levelsMax - levelsMin) * t); return { internalWidth: width, internalHeight: null, // Will be calculated dynamically based on aspect ratio fps, paletteLevels }; } // Update advanced parameters from current yank value function updateAdvancedFromYank() { const cfg = computeConfigFromYank(settings.yank); settings.internalWidth = cfg.internalWidth; settings.fps = cfg.fps; settings.paletteLevels = cfg.paletteLevels; } // Derive an approximate yank from current advanced sliders (0..100) function updateYankFromAdvanced() { const widthMax = 800, widthMin = 160; const fpsMax = 30, fpsMin = 8; const levelsMax = 12, levelsMin = 2; const tWidth = (widthMax - settings.internalWidth) / (widthMax - widthMin); const tFps = (fpsMax - settings.fps) / (fpsMax - fpsMin); const tPalette = (levelsMax - settings.paletteLevels) / (levelsMax - levelsMin); let t = (tWidth + tFps + tPalette) / 3; t = Math.min(1, Math.max(0, t)); settings.yank = Math.round(t * 100); } // Combine yank mapping with optional advanced overrides function computeConfig() { if (settings.advanced) { const w = clamp(settings.internalWidth || DEFAULT_SETTINGS.internalWidth, 160, 800); const fps = clamp(settings.fps || DEFAULT_SETTINGS.fps, 8, 30); const pl = clamp(settings.paletteLevels || DEFAULT_SETTINGS.paletteLevels, 2, 12); settings.internalWidth = w; settings.fps = fps; settings.paletteLevels = pl; return { internalWidth: w, internalHeight: null, // Calculated dynamically fps, paletteLevels: pl }; } const cfg = computeConfigFromYank(settings.yank); // keep advanced values in sync so toggling advanced inherits current feel settings.internalWidth = cfg.internalWidth; settings.fps = cfg.fps; settings.paletteLevels = cfg.paletteLevels; return cfg; } function clamp(v, min, max) { v = Number(v); if (isNaN(v)) return min; return Math.min(max, Math.max(min, v)); } function applySettingsToRuntime() { currentConfig = computeConfig(); chanLUT = buildChannelLUT(currentConfig.paletteLevels); if (currentController && currentController.onConfigChanged) { currentController.onConfigChanged(); } updateUIFromSettings(); } // ---------- Palette / Image processing ---------- function buildChannelLUT(levels) { const lut = new Uint8Array(256); if (levels <= 1) { lut.fill(0); return lut; } const step = 255 / (levels - 1); for (let i = 0; i < 256; i++) { const idx = Math.round(i / step); lut[i] = Math.round(idx * step); } return lut; } function applyPalette(imageData, lut) { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { data[i] = lut[data[i]]; data[i + 1] = lut[data[i + 1]]; data[i + 2] = lut[data[i + 2]]; // alpha unchanged } } // ---------- Video controller ---------- function createCanvasForVideo(video) { // Ensure parent is relative for absolute positioning of canvas const style = window.getComputedStyle(video.parentNode); if (style.position === 'static') { video.parentNode.style.position = 'relative'; } const canvas = document.createElement('canvas'); canvas.style.display = 'block'; canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.left = '0'; canvas.style.zIndex = '1'; // Lower z-index so controls can sit on top canvas.style.pointerEvents = 'none'; // Let clicks pass through to controls canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.backgroundColor = 'transparent'; canvas.style.imageRendering = 'pixelated'; canvas.style.objectFit = 'contain'; video.parentNode.insertBefore(canvas, video.nextSibling); return canvas; } function makeController(video) { const canvas = createCanvasForVideo(video); const ctx = canvas.getContext('2d', { willReadFrequently: true }); let enabled = false; let intervalId = null; function applyConfigToCanvas() { const rect = video.getBoundingClientRect(); const displayWidth = rect.width || video.clientWidth || video.width || 640; const displayHeight = rect.height || video.clientHeight || video.height || 360; let internalWidth = currentConfig.internalWidth; let internalHeight; // Calculate aspect ratio const videoWidth = video.videoWidth || 640; const videoHeight = video.videoHeight || 360; const aspectRatio = videoHeight / videoWidth; // If advanced resolution scaling is disabled, match display size for best quality if (settings.advanced && !settings.enableResolution) { internalWidth = Math.round(displayWidth); internalHeight = Math.round(displayHeight); } else { // Calculate height based on fixed width and aspect ratio internalHeight = Math.round(internalWidth * aspectRatio); } canvas.width = internalWidth; canvas.height = internalHeight; canvas.width = internalWidth; canvas.height = internalHeight; // The canvas fills the container (which is sized by the hidden video) canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.maxWidth = ''; canvas.style.aspectRatio = ''; } applyConfigToCanvas(); function drawFrame() { if (!enabled || video.paused || video.ended) return; try { const w = canvas.width; const h = canvas.height; ctx.drawImage(video, 0, 0, w, h); // If advanced palette reduction is disabled, skip quantization if (!(settings.advanced && !settings.enablePalette)) { const frame = ctx.getImageData(0, 0, w, h); applyPalette(frame, chanLUT); ctx.putImageData(frame, 0, 0); } } catch (e) { // ignore } } function startLoop() { if (intervalId || !enabled) return; const effectiveFps = (settings.advanced && !settings.enableFps) ? 60 : currentConfig.fps; intervalId = setInterval(drawFrame, 1000 / effectiveFps); } function stopLoop() { if (intervalId) { clearInterval(intervalId); intervalId = null; } } function enable() { if (enabled) return; enabled = true; canvas.style.display = 'block'; video.style.visibility = 'hidden'; // Keep layout space! if (!video.paused && !video.ended) startLoop(); } function disable() { if (!enabled) return; enabled = false; canvas.style.display = 'none'; video.style.visibility = ''; stopLoop(); } function toggle() { if (enabled) disable(); else enable(); } function isEnabled() { return enabled; } function onConfigChanged() { const wasEnabled = enabled; stopLoop(); applyConfigToCanvas(); if (wasEnabled) { startLoop(); } } function destroy() { disable(); canvas.remove(); video.removeEventListener('play', startLoop); video.removeEventListener('pause', stopLoop); video.removeEventListener('ended', stopLoop); } video.addEventListener('play', startLoop); video.addEventListener('pause', stopLoop); video.addEventListener('ended', stopLoop); video.addEventListener('loadedmetadata', applyConfigToCanvas); // Recalculate when metadata allows return { enable, disable, toggle, isEnabled, destroy, onConfigChanged }; } function setupVideo(video) { if (!video) return; if (!isItemPage()) { if (ui) ui.wrapper.style.display = 'none'; return; } // Prioritize the main item player (id="my-video" or class "viewer") const isPrimary = video.id === 'my-video' || video.classList.contains('viewer') || video.classList.contains('v0ck_video'); if (video.dataset.flashFilterAttached === '1') { // If already attached, ensure its currentController is restored if it's the primary one if (isPrimary && !currentController) { currentController = video.__flashFilterController; } return; } video.dataset.flashFilterAttached = '1'; const controller = makeController(video); video.__flashFilterController = controller; // If this is the primary video, or we don't have one yet, set as current if (isPrimary || !currentController) { currentController = controller; } // Always sync with global setting on creation if (settings.enabled) { controller.enable(); } else { controller.disable(); } // Small delay to ensure v0ck has injected its controls setTimeout(() => { if (ui) updateUIFromSettings(); }, 50); if (ui && isPrimary) { ui.wrapper.style.display = 'block'; } } function scanExistingVideos() { document.querySelectorAll('video').forEach(setupVideo); } function startObserver() { if (!document.body) return; const observer = new MutationObserver((mutations) => { for (const m of mutations) { m.addedNodes.forEach((node) => { if (node.nodeType !== 1) return; if (node.tagName === 'VIDEO') { setupVideo(node); } else if (node.querySelectorAll) { node.querySelectorAll('video').forEach(setupVideo); } }); m.removedNodes.forEach((node) => { if (node.nodeType !== 1) return; const vids = []; if (node.tagName === 'VIDEO') { vids.push(node); } if (node.querySelectorAll) { node.querySelectorAll('video').forEach(v => vids.push(v)); } vids.forEach((v) => { const c = v.__flashFilterController; if (c) { c.destroy(); if (currentController === c) { currentController = null; } } }); if (ui && document.querySelectorAll('video').length === 0) { ui.wrapper.style.display = 'none'; } }); } }); observer.observe(document.body, { childList: true, subtree: true }); } // ---------- UI Panel ---------- function createOptionsUI() { if (ui || !document.body) return; const wrapper = document.createElement('div'); wrapper.style.position = 'fixed'; wrapper.style.bottom = '10px'; wrapper.style.right = '10px'; wrapper.style.zIndex = '99999'; wrapper.style.fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; wrapper.style.fontSize = '12px'; wrapper.style.color = '#fff'; wrapper.style.display = 'none'; // Initially hidden wrapper.id = 'flash-yank-ui'; const floatingBadge = document.createElement('div'); floatingBadge.id = 'w0bm-floating-swf'; floatingBadge.textContent = 'SWF'; floatingBadge.style.background = 'rgba(0,0,0,0.9)'; floatingBadge.style.padding = '2px 8px'; floatingBadge.style.borderRadius = '3px'; floatingBadge.style.cursor = 'pointer'; floatingBadge.style.fontWeight = 'bold'; floatingBadge.style.letterSpacing = '0.08em'; const panel = document.createElement('div'); panel.style.position = 'fixed'; panel.style.right = '10px'; panel.style.bottom = '22px'; panel.style.background = 'rgba(0,0,0,0.9)'; panel.style.color = '#fff'; panel.style.padding = '6px 10px'; panel.style.fontSize = '12px'; panel.style.borderRadius = '4px'; panel.style.maxWidth = '320px'; panel.style.minWidth = '240px'; panel.style.maxHeight = 'calc(100vh - 40px)'; // Prevent exceeding screen height panel.style.overflowY = 'auto'; // Make scrollable panel.style.boxSizing = 'border-box'; panel.style.boxShadow = '0 0 6px rgba(0,0,0,0.7)'; panel.style.display = 'none'; panel.innerHTML = `