Created
January 22, 2026 21:09
-
-
Save clort81/2d6ffd10ceff428ce080b5f3e89b423e to your computer and use it in GitHub Desktop.
Dark Background and Light Text Userscript for Min Browser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Smart HSL Dark Mode | |
| // @namespace http://tampermonkey.net/ | |
| // @version 4.2 | |
| // @description Attempts hue-preserving intensity inversion with reasonable priorities | |
| // @author clort81 + z.AI | |
| // @match *://*/* | |
| // @grant none | |
| // @run-at document-start | |
| // ==/UserScript== | |
| /* --------------------------------------------------------------------------- | |
| UTILITIES | |
| --------------------------------------------------------------------------- */ | |
| function getRelativeLuminance(r, g, b) { | |
| const [rsRGB, gsRGB, bsRGB] = [r, g, b].map(c => { | |
| c = c / 255; | |
| return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); | |
| }); | |
| return 0.2126 * rsRGB + 0.7152 * gsRGB + 0.0722 * bsRGB; | |
| } | |
| function rgbToHsl(r, g, b) { | |
| r /= 255, g /= 255, b /= 255; | |
| const max = Math.max(r, g, b), min = Math.min(r, g, b); | |
| let h, s, l = (max + min) / 2; | |
| if (max === min) { | |
| h = s = 0; | |
| } else { | |
| const d = max - min; | |
| s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | |
| switch (max) { | |
| case r: h = (g - b) / d + (g < b ? 6 : 0); break; | |
| case g: h = (b - r) / d + 2; break; | |
| case b: h = (r - g) / d + 4; break; | |
| } | |
| h /= 6; | |
| } | |
| return [h, s, l]; | |
| } | |
| function hslToRgb(h, s, l) { | |
| 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 { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; | |
| } | |
| function parseColor(colorStr) { | |
| if (colorStr === 'transparent') { | |
| return { r: 0, g: 0, b: 0, a: 0 }; | |
| } | |
| const rgbaMatch = colorStr.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$/); | |
| if (rgbaMatch) { | |
| const alpha = parseFloat(rgbaMatch[4]); | |
| if (alpha === 0) return { r: 0, g: 0, b: 0, a: 0 }; | |
| return { | |
| r: parseInt(rgbaMatch[1], 10), | |
| g: parseInt(rgbaMatch[2], 10), | |
| b: parseInt(rgbaMatch[3], 10), | |
| a: alpha | |
| }; | |
| } | |
| const rgbMatch = colorStr.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); | |
| if (rgbMatch) { | |
| return { | |
| r: parseInt(rgbMatch[1], 10), | |
| g: parseInt(rgbMatch[2], 10), | |
| b: parseInt(rgbMatch[3], 10) | |
| }; | |
| } | |
| const hexMatch = colorStr.match(/^#?([a-f\d]{1,2})([a-f\d]{1,2})([a-f\d]{1,2})$/i); | |
| if (hexMatch) { | |
| const expand = (hex) => hex.length === 1 ? hex + hex : hex; | |
| return { | |
| r: parseInt(expand(hexMatch[1]), 16), | |
| g: parseInt(expand(hexMatch[2]), 16), | |
| b: parseInt(expand(hexMatch[3]), 16) | |
| }; | |
| } | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 1; | |
| canvas.height = 1; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.fillStyle = colorStr; | |
| ctx.fillRect(0, 0, 1, 1); | |
| const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; | |
| return { r, g, b, a: a / 255 }; | |
| } | |
| function isFullyTransparent(parsedColor) { | |
| return parsedColor.a !== undefined && parsedColor.a === 0; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| CORE LOGIC | |
| --------------------------------------------------------------------------- */ | |
| function processElement(element) { | |
| if (!element.tagName) return; | |
| if (['IMG', 'VIDEO', 'CANVAS', 'SVG', 'A'].includes(element.tagName)) return; | |
| const computedStyle = window.getComputedStyle(element); | |
| const bgColorStr = computedStyle.backgroundColor; | |
| const textColorStr = computedStyle.color; | |
| // Handle transparent backgrounds | |
| if (bgColorStr === 'transparent' || | |
| bgColorStr.startsWith('rgba') && /,\s*0+(\.0*)?\)$/.test(bgColorStr)) { | |
| const textRgb = parseColor(textColorStr); | |
| if (textRgb && !isFullyTransparent(textRgb)) { | |
| const textL = getRelativeLuminance(textRgb.r, textRgb.g, textRgb.b); | |
| if (textL < 0.5) { | |
| element.style.setProperty('color', '#e0e0e0', 'important'); | |
| } | |
| } | |
| return; | |
| } | |
| if (['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) { | |
| element.style.setProperty('background-color', '#121212', 'important'); | |
| const textRgb = parseColor(textColorStr); | |
| if (textRgb) { | |
| const textL = getRelativeLuminance(textRgb.r, textRgb.g, textRgb.b); | |
| if (textL < 0.5) { | |
| element.style.setProperty('color', '#e0e0e0', 'important'); | |
| } | |
| } | |
| return; | |
| } | |
| const bgRgb = parseColor(bgColorStr); | |
| if (!bgRgb || isFullyTransparent(bgRgb)) return; | |
| const [h, s, l] = rgbToHsl(bgRgb.r, bgRgb.g, bgRgb.b); | |
| const bgL = getRelativeLuminance(bgRgb.r, bgRgb.g, bgRgb.b); | |
| if (bgL > 0.4) { | |
| const newRgb = hslToRgb(h, s, 0.15); | |
| element.style.setProperty('background-color', `rgb(${newRgb.r}, ${newRgb.g}, ${newRgb.b})`, 'important'); | |
| } | |
| const textRgb = parseColor(textColorStr); | |
| if (textRgb && !isFullyTransparent(textRgb)) { | |
| const textL = getRelativeLuminance(textRgb.r, textRgb.g, textRgb.b); | |
| if (textL < 0.5) { | |
| element.style.setProperty('color', '#e0e0e0', 'important'); | |
| } | |
| } | |
| } | |
| function processPseudoElements() { | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| html { background-color: #121212 !important; } | |
| a { color: #62a0ea !important; } | |
| a:visited { color: #c061cb !important; } | |
| img, video, canvas, svg { | |
| filter: brightness(0.8) contrast(1.1) !important; | |
| -webkit-filter: brightness(0.8) contrast(1.1) !important; | |
| } | |
| iframe { | |
| filter: invert(90%) hue-rotate(180deg) brightness(1.1) !important; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| function processIframes() { | |
| document.querySelectorAll('iframe').forEach(iframe => { | |
| try { | |
| const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; | |
| if (iframeDoc) { | |
| const style = iframeDoc.createElement('style'); | |
| style.textContent = ` | |
| html { background-color: #121212 !important; } | |
| * { background-color: inherit !important; color: inherit !important; border-color: #555 !important; } | |
| `; | |
| iframeDoc.head.appendChild(style); | |
| } | |
| } catch (e) { | |
| iframe.style.filter = 'invert(90%) hue-rotate(180deg) brightness(1.1)'; | |
| } | |
| }); | |
| } | |
| /* --------------------------------------------------------------------------- | |
| INITIALIZATION | |
| --------------------------------------------------------------------------- */ | |
| const darkModeEnabled = localStorage.getItem('smartDarkModeEnabled') === 'true' || | |
| window.matchMedia('(prefers-color-scheme: dark)').matches; | |
| if (darkModeEnabled) { | |
| processPseudoElements(); | |
| const processAll = () => { | |
| document.querySelectorAll('*').forEach(processElement); | |
| processIframes(); | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', processAll); | |
| } else { | |
| processAll(); | |
| } | |
| const observer = new MutationObserver(mutations => { | |
| clearTimeout(observer.timer); | |
| observer.timer = setTimeout(() => { | |
| mutations.forEach(mutation => { | |
| mutation.addedNodes.forEach(node => { | |
| if (node.nodeType === 1) { | |
| processElement(node); | |
| node.querySelectorAll('*').forEach(processElement); | |
| } | |
| }); | |
| }); | |
| processIframes(); | |
| }, 100); | |
| }); | |
| observer.observe(document.documentElement, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| } | |
| document.addEventListener('keydown', e => { | |
| if (e.ctrlKey && e.shiftKey && e.key === 'D') { | |
| e.preventDefault(); | |
| const enabled = localStorage.getItem('smartDarkModeEnabled') === 'true'; | |
| localStorage.setItem('smartDarkModeEnabled', !enabled); | |
| location.reload(); | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment