Unified 3D FX

Unified 3D/image filter toggle with media control panel (button-based controls).

  1. // ==UserScript==
  2. // @name Unified 3D FX
  3. // @namespace http://your.namespace.here/
  4. // @version 4.2
  5. // @description Unified 3D/image filter toggle with media control panel (button-based controls).
  6. // @match *://*/*
  7. // @grant none
  8. // @run-at document-end
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. const hostKey = `filters_${location.hostname}`;
  15. const globalKey = `filters_global`;
  16.  
  17. let settings = {
  18. is3D: false,
  19. scaleImages: false,
  20. brightness: 0,
  21. contrast: 0,
  22. saturation: 0,
  23. clarityBoost: false,
  24. lazyEnhance: true,
  25. showImages: true,
  26. showVideos: true,
  27. include: '',
  28. exclude: ''
  29. };
  30.  
  31. const saved = JSON.parse(localStorage.getItem(hostKey) || localStorage.getItem(globalKey));
  32. if (saved) Object.assign(settings, saved);
  33.  
  34. const controlToggleButton = document.createElement('button');
  35. controlToggleButton.textContent = '✨';
  36. controlToggleButton.className = 'control-toggle-btn';
  37. controlToggleButton.style.cssText = `
  38. position: fixed; bottom: 10px; right: 10px;
  39. background-color: #333; color: white;
  40. border: none; padding: 6px 8px;
  41. font-size: 14px; z-index: 2147483647;
  42. border-radius: 5px; cursor: pointer;
  43. `;
  44. document.body.appendChild(controlToggleButton);
  45.  
  46. const controlPanel = document.createElement('div');
  47. controlPanel.className = 'control-panel';
  48. controlPanel.style.cssText = `
  49. position: fixed; bottom: 38px; right: 10px;
  50. background: rgba(0,0,0,0.95); color: white;
  51. padding: 8px; width: 110px;
  52. font-family: Arial, sans-serif;
  53. display: none; z-index: 2147483646;
  54. border-radius: 10px; max-height: 90vh; overflow-y: auto;
  55. font-size: 11px;
  56. `;
  57. document.body.appendChild(controlPanel);
  58.  
  59. controlToggleButton.addEventListener('click', () => {
  60. controlPanel.style.display = controlPanel.style.display === 'none' ? 'block' : 'none';
  61. });
  62.  
  63. function addToggle(label, key, colorOn, callback) {
  64. const btn = document.createElement('button');
  65. btn.textContent = settings[key] ? `❌ ${label}` : `✔️ ${label}`;
  66. btn.style.cssText = `
  67. width: 100%; background-color: ${settings[key] ? colorOn : '#444'};
  68. color: white; border: none; padding: 5px;
  69. border-radius: 5px; cursor: pointer; margin-bottom: 6px;
  70. font-size: 11px;
  71. `;
  72. btn.addEventListener('click', () => {
  73. settings[key] = !settings[key];
  74. btn.textContent = settings[key] ? `❌ ${label}` : `✔️ ${label}`;
  75. btn.style.backgroundColor = settings[key] ? colorOn : '#444';
  76. if (callback) callback();
  77. reObserve();
  78. });
  79. controlPanel.appendChild(btn);
  80. }
  81.  
  82. function addFilterControl(label, key, min, max, step) {
  83. const wrapper = document.createElement('div');
  84. wrapper.style.cssText = `margin-bottom: 6px;`;
  85.  
  86. const lbl = document.createElement('div');
  87. lbl.textContent = `${label}: ${settings[key].toFixed(1)}`;
  88. lbl.style.marginBottom = '2px';
  89. wrapper.appendChild(lbl);
  90.  
  91. const row = document.createElement('div');
  92. row.style.display = 'flex';
  93. row.style.justifyContent = 'space-between';
  94.  
  95. const btnDec = document.createElement('button');
  96. btnDec.textContent = '−';
  97. btnDec.style.cssText = baseBtnStyle;
  98. btnDec.onclick = () => {
  99. settings[key] = Math.max(min, settings[key] - step);
  100. lbl.textContent = `${label}: ${settings[key].toFixed(1)}`;
  101. reObserve();
  102. };
  103.  
  104. const btnInc = document.createElement('button');
  105. btnInc.textContent = '+';
  106. btnInc.style.cssText = baseBtnStyle;
  107. btnInc.onclick = () => {
  108. settings[key] = Math.min(max, settings[key] + step);
  109. lbl.textContent = `${label}: ${settings[key].toFixed(1)}`;
  110. reObserve();
  111. };
  112.  
  113. row.appendChild(btnDec);
  114. row.appendChild(btnInc);
  115. wrapper.appendChild(row);
  116. controlPanel.appendChild(wrapper);
  117. }
  118.  
  119. const baseBtnStyle = `
  120. width: 45%; padding: 3px 0;
  121. font-size: 12px; background: #555;
  122. border: none; color: white; border-radius: 4px;
  123. cursor: pointer;
  124. `;
  125.  
  126. // Toggles
  127. addToggle('3D FX', 'is3D', '#d9534f');
  128. addToggle('Upscale', 'scaleImages', '#5bc0de');
  129. addToggle('Clarity', 'clarityBoost', '#1abc9c');
  130. addToggle('Lazy Mode', 'lazyEnhance', '#5cb85c');
  131. addToggle('Show Images', 'showImages', '#337ab7');
  132. addToggle('Show Videos', 'showVideos', '#8e44ad');
  133.  
  134. // Filters (Button-based)
  135. addFilterControl('Brightness', 'brightness', -1, 1, 0.1);
  136. addFilterControl('Contrast', 'contrast', -1, 1, 0.1);
  137. addFilterControl('Saturation', 'saturation', -1, 2, 0.1);
  138.  
  139. // Save & Reset
  140. const scopeSection = document.createElement('div');
  141. scopeSection.style.marginTop = '8px';
  142. scopeSection.innerHTML = `
  143. <button id="saveDomain" style="width:100%; margin-bottom:4px;">💾 Save Site</button>
  144. <button id="saveGlobal" style="width:100%; margin-bottom:4px;">🌐 Save All</button>
  145. <button id="resetAll" style="width:100%; background-color:#c9302c;">⛔ Reset</button>
  146. `;
  147. controlPanel.appendChild(scopeSection);
  148.  
  149. document.getElementById('saveDomain').onclick = () => {
  150. localStorage.setItem(hostKey, JSON.stringify(settings));
  151. alert('Saved for site.');
  152. };
  153. document.getElementById('saveGlobal').onclick = () => {
  154. localStorage.setItem(globalKey, JSON.stringify(settings));
  155. alert('Saved globally.');
  156. };
  157. document.getElementById('resetAll').onclick = () => {
  158. localStorage.removeItem(hostKey);
  159. localStorage.removeItem(globalKey);
  160. Object.assign(settings, {
  161. is3D: false, scaleImages: false, clarityBoost: false,
  162. brightness: 0, contrast: 0, saturation: 0,
  163. lazyEnhance: true, showImages: true, showVideos: true,
  164. include: '', exclude: ''
  165. });
  166. reObserve();
  167. alert('Reset done. Reload to clear display.');
  168. };
  169.  
  170. function applyMediaEffects(el) {
  171. const tag = el.tagName.toLowerCase();
  172. if ((tag === 'img' && !settings.showImages) || (tag === 'video' && !settings.showVideos)) return;
  173.  
  174. const src = el.currentSrc || el.src || el.poster || '';
  175. if (settings.include && !src.includes(settings.include)) return;
  176. if (settings.exclude && src.includes(settings.exclude)) return;
  177.  
  178. let filters = [
  179. `brightness(${Math.max(0, 1 + settings.brightness)})`,
  180. `contrast(${Math.max(0, 1 + settings.contrast)})`,
  181. `saturate(${Math.max(0, 1 + settings.saturation)})`
  182. ];
  183.  
  184. if (settings.clarityBoost) {
  185. filters.push('contrast(1.0)', 'brightness(1.03)', 'drop-shadow(0 0 4px white)');
  186. }
  187.  
  188. el.style.filter = filters.join(' ');
  189.  
  190. let transform = '';
  191. if (settings.scaleImages) {
  192. transform += ' scale(1.30)';
  193. el.style.maxWidth = 'none';
  194. el.style.maxHeight = 'none';
  195. el.style.objectFit = 'contain';
  196. }
  197. if (settings.is3D) {
  198. transform += 'perspective(1400px) translateZ(50px) rotateX(25deg) rotateY(25deg)';
  199. el.style.transformStyle = 'preserve-3d';
  200. el.style.backfaceVisibility = 'hidden';
  201. }
  202.  
  203. el.style.transform = transform.trim();
  204. }
  205.  
  206. function enhanceAll() {
  207. document.querySelectorAll('img, video').forEach(applyMediaEffects);
  208. }
  209.  
  210. function reObserve() {
  211. observer.disconnect();
  212. enhanceAll();
  213. observeNewMedia();
  214. }
  215.  
  216. function observeNewMedia() {
  217. observer.observe(document.body, { childList: true, subtree: true });
  218. }
  219.  
  220. const observer = new MutationObserver(mutations => {
  221. mutations.forEach(mutation => {
  222. mutation.addedNodes.forEach(node => {
  223. if (!(node instanceof HTMLElement)) return;
  224. const media = node.matches?.('img, video') ? [node] : [...node.querySelectorAll?.('img, video') || []];
  225. media.forEach(el => {
  226. if (settings.lazyEnhance) {
  227. requestIdleCallback(() => applyMediaEffects(el));
  228. } else {
  229. applyMediaEffects(el);
  230. }
  231. });
  232. });
  233. });
  234. });
  235.  
  236. enhanceAll();
  237. observeNewMedia();
  238. })();