Last active
November 21, 2025 16:42
-
-
Save Jacbo1/e750aa7d09cf63ed9e79464bab86d405 to your computer and use it in GitHub Desktop.
Twitter Background
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 Twitter Background | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2024-03-23 | |
| // @description Give Twitter background images | |
| // @author You | |
| // @match *://*.twitter.com/* | |
| // @match *://*.x.com/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com | |
| // @grant none | |
| // ==/UserScript== | |
| // https://gist.github.com/Jacbo1/e750aa7d09cf63ed9e79464bab86d405 | |
| (function() { | |
| 'use strict'; | |
| // ##################### SETTINGS ##################### | |
| const repeats = 1 | |
| const fadeInterval = 60 * 1000 | |
| const fadeDuration = 1 * 1000 | |
| const localStorageKey = "adnfhufsjmidsigfsjmdafiu twitter bg img" | |
| const localStorageTimeKey = "adnfhufsjmidsigfsjmdafiu twitter bg img time" | |
| const localStorageIndexKey = "adnfhufsjmidsigfsjmdafiu twitter bg img index" | |
| const localStorageIndexActiveKey = "adnfhufsjmidsigfsjmdafiu twitter bg img active" | |
| const localStorageIndexInactiveKey = "adnfhufsjmidsigfsjmdafiu twitter bg img inactive" | |
| // Add images here. Only images from Twitter seem to load | |
| const images = [ | |
| 'https://pbs.twimg.com/media/GDmNHmvXIAIT0cp?format=jpg&name=orig', // https://fxtwitter.com/godonnellart/status/1745581527457579392/photo/1 | |
| { | |
| // https://fxtwitter.com/kamillustrator/status/1950068655435235331/photo/4 | |
| url: 'https://pbs.twimg.com/media/GxAJG3vbUAA11Cb?format=jpg&name=orig', | |
| }, | |
| /*{ | |
| // <tweet URL> | |
| url: '<twitter image url>', | |
| fit: true, // Fit instead of fill | |
| bgcolor: '#F6A450', | |
| pos: '0% 50%' // left, vertically centered | |
| },*/ | |
| ] | |
| // #################################################### | |
| // #################################################### | |
| const style = document.createElement('style'); | |
| const scrollFraction = 0.5 | |
| const scrollMin = 50 - 50 * scrollFraction | |
| const scrollMax = 50 + 50 * scrollFraction | |
| const scrollDuration = (fadeInterval + fadeDuration) / 1000 + 1 | |
| style.innerHTML = ` | |
| @keyframes backgroundScrollUp { | |
| 0% { | |
| background-position-y: ${scrollMax}%; | |
| } | |
| 100% { | |
| background-position-y: ${scrollMin}%; | |
| } | |
| } | |
| @keyframes backgroundScrollDown { | |
| 0% { | |
| background-position-y: ${scrollMin}%; | |
| } | |
| 100% { | |
| background-position-y: ${scrollMax}%; | |
| } | |
| } | |
| @keyframes backgroundScrollLeft { | |
| 0% { | |
| background-position-x: ${scrollMax}%; | |
| } | |
| 100% { | |
| background-position-x: ${scrollMin}%; | |
| } | |
| } | |
| @keyframes backgroundScrollRight { | |
| 0% { | |
| background-position-x: ${scrollMin}%; | |
| } | |
| 100% { | |
| background-position-x: ${scrollMax}%; | |
| } | |
| } | |
| .background-scroll-up { | |
| animation: backgroundScrollUp ${scrollDuration}s linear forwards; | |
| } | |
| .background-scroll-down { | |
| animation: backgroundScrollDown ${scrollDuration}s linear forwards; | |
| } | |
| .background-scroll-left { | |
| animation: backgroundScrollLeft ${scrollDuration}s linear forwards; | |
| } | |
| .background-scroll-right { | |
| animation: backgroundScrollRight ${scrollDuration}s linear forwards; | |
| } | |
| ` | |
| document.head.appendChild(style); | |
| function removeScrollClasses(div) { | |
| div.classList.remove('background-scroll-up') | |
| div.classList.remove('background-scroll-down') | |
| div.classList.remove('background-scroll-left') | |
| div.classList.remove('background-scroll-right') | |
| } | |
| function addRandomScrollClass(div, imageUrl) { | |
| removeScrollClasses(div) | |
| const img = new Image() | |
| img.src = imageUrl | |
| img.onload = function() { | |
| const xratio = img.naturalWidth / window.innerWidth | |
| const yratio = img.naturalHeight / window.innerHeight | |
| if (xratio > yratio) { | |
| // Scroll width | |
| if (Math.random() > 0.5) div.classList.add('background-scroll-left') | |
| else div.classList.add('background-scroll-right') | |
| } else { | |
| // Scroll height | |
| if (Math.random() > 0.5) div.classList.add('background-scroll-up') | |
| else div.classList.add('background-scroll-down') | |
| } | |
| } | |
| } | |
| // #################################################### | |
| // ##################### INIT ##################### | |
| for (let i = 0; i < images.length; i++) { | |
| const img = images[i]; | |
| const regex = /^(https:\/\/pbs\.twimg\.com\/media\/[a-zA-Z0-9-_]+\?format=\w+&name=)(?!orig).+$/; | |
| if (typeof img === 'string') { | |
| const match = regex.exec(img); | |
| if (match) { | |
| images[i] = match[1] + 'orig'; | |
| } | |
| } else if (img.url) { | |
| const match = regex.exec(img.url); | |
| if (match) { | |
| img.url = match[1] + 'orig'; | |
| } | |
| } | |
| } | |
| let indexes = [] | |
| let newImage = Math.floor(Math.random() * indexes.length) | |
| let curImage = newImage | |
| let fadeTime = Date.now() | |
| let docVisible = document.visibilityState === 'visible' | |
| let docFocused = document.hasFocus() | |
| let shouldRun = docVisible && docFocused | |
| const backgroundDivs = [ | |
| document.createElement('div'), | |
| document.createElement('div') | |
| ] | |
| for (let i = 0; i < 2; i++) { | |
| const div = backgroundDivs[i]; | |
| // Set styles for the div | |
| div.style.position = 'fixed'; | |
| div.style.top = '0'; | |
| div.style.left = '0'; | |
| div.style.width = '100%'; | |
| div.style.height = '100%'; | |
| div.style.zIndex = -9999 + i; | |
| div.style.backgroundSize = 'cover'; | |
| div.style.backgroundPosition = 'center'; | |
| div.style.backgroundRepeat = 'no-repeat' | |
| // Append the div to the body | |
| document.body.appendChild(div); | |
| } | |
| let backDiv = backgroundDivs[0] | |
| let frontDiv = backgroundDivs[1] | |
| // ################################################ | |
| // ##################### IMAGE CHANGING ##################### | |
| function applyImage(div, index) { | |
| const image = images[index] | |
| let url | |
| if (typeof image === 'string') { | |
| // Simple URL | |
| url = image | |
| div.style.backgroundSize = 'cover'; | |
| div.style.backgroundPosition = 'center'; | |
| } else { | |
| url = image.url | |
| if (image.bgcolor) div.style.backgroundColor = image.bgcolor | |
| div.style.backgroundSize = image.fit ? 'contain' : 'cover' | |
| div.style.backgroundPosition = image.pos ?? 'center' | |
| } | |
| div.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("${url}")`; | |
| if (div === frontDiv && !image.pos) addRandomScrollClass(div, url) | |
| } | |
| function refreshIndexes() { | |
| for (let i = 0; i < images.length; i++) { | |
| for (let j = 0; j < repeats; j++) { | |
| indexes.push(i) | |
| } | |
| } | |
| } | |
| function getIndex(remove = true) { | |
| if (indexes.length === 0) { | |
| refreshIndexes() | |
| } | |
| const i = Math.floor(Math.random() * indexes.length) | |
| const index = indexes[i] | |
| if (remove) { | |
| indexes.splice(i, 1) | |
| } | |
| if (indexes.length === 0) { | |
| refreshIndexes() | |
| } | |
| return index | |
| } | |
| let firstSet = true | |
| function setImageTo(newIndex) { | |
| console.log(`New image: ${getImageUrl(images[newIndex])}`); | |
| curImage = newImage | |
| newImage = newIndex | |
| if (firstSet) { | |
| firstSet = false | |
| curImage = newImage | |
| } | |
| backDiv.style.transition = ''; | |
| frontDiv.style.transition = ''; | |
| applyImage(backDiv, curImage) | |
| applyImage(frontDiv, newImage) | |
| } | |
| let divSwitch = true | |
| function fadeImageTo(newIndex) { | |
| console.log(`New image: ${getImageUrl(images[newIndex])}`); | |
| curImage = newImage | |
| newImage = newIndex | |
| if (firstSet) { | |
| firstSet = false | |
| curImage = newImage | |
| } | |
| divSwitch = !divSwitch | |
| backDiv; | |
| frontDiv; | |
| if (divSwitch) { | |
| backDiv = backgroundDivs[0] | |
| frontDiv = backgroundDivs[1] | |
| } else { | |
| backDiv = backgroundDivs[1] | |
| frontDiv = backgroundDivs[0] | |
| } | |
| backDiv.style.zIndex = '-9999'; | |
| frontDiv.style.zIndex = '-9998'; | |
| backDiv.style.transition = ''; | |
| frontDiv.style.transition = ''; | |
| backDiv.style.opacity = 1 | |
| frontDiv.style.opacity = 0 | |
| backDiv.style.transition = `opacity ${fadeDuration / 1000}s`; | |
| frontDiv.style.transition = `opacity ${fadeDuration / 1000}s`; | |
| backDiv.style.opacity = 0 | |
| frontDiv.style.opacity = 1 | |
| applyImage(backDiv, curImage) | |
| applyImage(frontDiv, newImage) | |
| fadeTime = Date.now() | |
| saveTime() | |
| } | |
| // ########################################################## | |
| // ##################### SAVING AND LOADING ##################### | |
| function getImageUrl(arrayItem) { | |
| if (typeof arrayItem === 'string') return arrayItem | |
| return arrayItem.url | |
| } | |
| function saveTime() { | |
| localStorage.setItem(localStorageTimeKey, JSON.stringify(fadeTime)) | |
| } | |
| function loadTime() { | |
| try { | |
| let time = localStorage.getItem(localStorageTimeKey) | |
| if (!time) return false; | |
| time = JSON.parse(time) | |
| const now = Date.now() | |
| if (now - time < fadeDuration + fadeInterval) { | |
| fadeTime = time | |
| return true | |
| } | |
| } catch {} | |
| return false | |
| } | |
| function saveActiveTime() { | |
| localStorage.setItem(localStorageIndexActiveKey, JSON.stringify(Date.now())) | |
| } | |
| function loadActiveTime() { | |
| const data = localStorage.getItem(localStorageIndexActiveKey) | |
| if (!data) return Date.now() | |
| return JSON.parse(data) | |
| } | |
| function saveInactiveTime() { | |
| localStorage.setItem(localStorageIndexInactiveKey, JSON.stringify(Date.now())) | |
| } | |
| function loadInactiveTime() { | |
| const data = localStorage.getItem(localStorageIndexInactiveKey) | |
| if (!data) return Date.now() | |
| return JSON.parse(data) | |
| } | |
| function saveIndexes() { | |
| const imageUrls = [] | |
| for (const image of images) { | |
| imageUrls.push(getImageUrl(image)) | |
| } | |
| localStorage.setItem(localStorageKey, JSON.stringify({ | |
| images: imageUrls, | |
| indexes: indexes, | |
| curIndex: newImage | |
| })) | |
| } | |
| function loadIndexes() { | |
| try { | |
| const obj = JSON.parse(localStorage.getItem(localStorageKey)) | |
| const objIndexes = obj.indexes | |
| const objImages = obj.images | |
| const imageMap = new Map() | |
| for (let i = 0; i < images.length; i++) { | |
| imageMap.set(getImageUrl(images[i]), i) | |
| } | |
| for (let i = objIndexes.length - 1; i >= 0; i--) { | |
| const newIndex = imageMap.get(objImages[objIndexes[i]]) | |
| if (newIndex) { | |
| objIndexes[i] = newIndex | |
| } else { | |
| objIndexes.splice(i, 1) | |
| } | |
| } | |
| let curIndex = imageMap.get(objImages[obj.curIndex]) | |
| const objImageSet = new Set(objImages) | |
| for (let i = 0; i < images.length; i++) { | |
| if (!objImageSet.has(getImageUrl(images[i]))) { | |
| for (let j = 0; j < repeats; j++) { | |
| objImageSet.add(i) | |
| } | |
| } | |
| } | |
| indexes = objIndexes | |
| if (curIndex && curIndex !== newImage) { | |
| setImageTo(curIndex) | |
| } | |
| saveIndexes() | |
| } catch {} | |
| } | |
| newImage = getIndex(false) | |
| curImage = newImage | |
| loadIndexes() | |
| loadTime() | |
| fadeImageTo(newImage) | |
| saveIndexes() | |
| saveTime() | |
| let lastTime = Date.now() | |
| setInterval(() => { | |
| // Handle regaining focus | |
| // This is janky | |
| const activeTime = loadActiveTime() | |
| const now = Date.now() | |
| if (shouldRun && activeTime > lastTime) { | |
| // Became active | |
| const timeElapsed = Math.max(0, activeTime - loadInactiveTime()) | |
| if (timeElapsed < 100) { | |
| // Not inactive | |
| loadTime() | |
| } else { | |
| // Went inactive | |
| fadeTime += timeElapsed | |
| } | |
| } | |
| if (shouldRun) { | |
| const now = Date.now() | |
| if (now - fadeTime >= fadeInterval + fadeDuration) { | |
| fadeImageTo(getIndex()) | |
| saveIndexes() | |
| } | |
| } | |
| // wasRunning = shouldRun | |
| lastTime = Date.now() | |
| }, 1000) | |
| // ############################################################## | |
| // ##################### VISIBILITY AND FOCUS ##################### | |
| let focusChangeTime = Date.now() | |
| async function onFocus() { | |
| focusChangeTime = Date.now() | |
| saveActiveTime() | |
| loadIndexes() | |
| frontDiv.style.animationPlayState = 'running'; | |
| backDiv.style.animationPlayState = 'running'; | |
| } | |
| function onLostFocus() { | |
| focusChangeTime = Date.now() | |
| saveInactiveTime() | |
| frontDiv.style.animationPlayState = 'paused'; | |
| backDiv.style.animationPlayState = 'paused'; | |
| } | |
| // Tab closing | |
| window.addEventListener('beforeunload', function(event) { | |
| shouldRun = false | |
| onLostFocus() | |
| }) | |
| // Event listener for when the document gains focus | |
| window.addEventListener('focus', function(event) { | |
| docFocused = true | |
| if (!shouldRun && docVisible) { | |
| shouldRun = true | |
| onFocus() | |
| } | |
| }); | |
| // Event listener for when the document loses focus | |
| window.addEventListener('blur', function(event) { | |
| docFocused = false | |
| if (shouldRun) { | |
| shouldRun = false | |
| onLostFocus() | |
| } | |
| }); | |
| document.addEventListener('visibilitychange', function() { | |
| docVisible = document.visibilityState === 'visible' | |
| const focused = docVisible && docFocused | |
| if (shouldRun !== focused) { | |
| shouldRun = focused | |
| if (shouldRun) onFocus() | |
| else onLostFocus() | |
| } | |
| }); | |
| // ################################################################ | |
| // ##################### UI MANIPULATION ##################### | |
| const findInterval = 200 | |
| const findTimeout = 4000 | |
| const findMaxIterations = findTimeout / findInterval | |
| const findQueue = [] | |
| function findIteration(findObj) { | |
| return new Promise(resolve => { | |
| if (findObj.url !== window.location.href) { | |
| resolve(true) | |
| return | |
| } | |
| let el; | |
| try { el = findObj.selector() } catch {} | |
| if (el) { | |
| findObj.callback(el) | |
| setTimeout(() => { | |
| el = undefined | |
| try { el = findObj.selector() } catch {} | |
| if (el) { | |
| findObj.callback(el) | |
| resolve(true) | |
| } else resolve(++findObj.iteration >= findMaxIterations) | |
| }); | |
| } else resolve(++findObj.iteration >= findMaxIterations) | |
| }) | |
| } | |
| async function findTimer() { | |
| while (findQueue.length !== 0) { | |
| const promises = [] | |
| for (const findObj of findQueue) { | |
| promises.push(findIteration(findObj)) | |
| } | |
| for (let i = promises.length - 1; i >= 0; i--) { | |
| if (await promises[i]) { | |
| // Remove find object from queue | |
| findQueue.splice(i, 1) | |
| } | |
| } | |
| if (findQueue.length !== 0) { | |
| await new Promise(res => setTimeout(res, findInterval)) | |
| } | |
| } | |
| } | |
| let findIntervalId = 0 | |
| async function find(selector, callback) { | |
| const obj = { | |
| selector: selector, | |
| callback: callback, | |
| iteration: 0, | |
| url: window.location.href | |
| } | |
| if (await findIteration(obj)) return | |
| findQueue.push(obj) | |
| if (findQueue.length === 1) findTimer(); | |
| } | |
| function hideSideBarItems() { | |
| function hide(el) { | |
| el.style.display = 'none' | |
| } | |
| // Can't remove these or the page breaks | |
| find(() => document.querySelector('[aria-label="Lists"]'), hide) | |
| find(() => document.querySelector('[aria-label="Jobs"]'), hide) | |
| find(() => document.querySelector('[aria-label="Verified Orgs"]'), hide) | |
| // find(() => document.querySelector('[aria-label="Communities"]'), hide) | |
| find(() => document.querySelector('[aria-label="Grok"]'), hide) | |
| find(() => document.querySelector('[aria-label="Premium"]'), hide) | |
| find(() => document.querySelector('[aria-label="Business"]'), hide) | |
| } | |
| function findAll() { | |
| const color = 'rgba(0,0,0,0.5)'; | |
| function makeInvisible(el) { | |
| el.style.backgroundColor = 'rgba(0,0,0,0)' | |
| } | |
| hideSideBarItems() | |
| find(() => document.querySelector('[aria-label="Search and explore"]'), el => el.remove()) | |
| find(() => document.querySelector('[aria-label="Footer"]').parentElement, el => el.remove()) | |
| find(() => document.querySelector('[aria-label="Offer extended!"]').parentElement, el => el.remove()) | |
| find(() => document.querySelector('[aria-label="Timeline: Trending now"]').parentElement.parentElement.parentElement, el => el.remove()) | |
| find(() => document.querySelector('[aria-label="Relevant people"]').parentElement, el => el.remove()) | |
| find(() => document.querySelector('[aria-label="Who to follow"]').parentElement.parentElement, el => el.remove()) | |
| find(() => document.querySelector('[aria-label="Subscribe to Premium"]').parentElement.parentElement, el => el.remove()) | |
| find(() => document.querySelector('[aria-label="Search"]').parentElement.parentElement.parentElement.parentElement, makeInvisible) | |
| find(() => document.querySelector('[aria-label="Search"]').children[0].children[0], el => el.style.backgroundColor = color) | |
| find(() => document.querySelector('[aria-label="Timeline: Trending now"]').parentElement.parentElement.parentElement, el => el.style.backgroundColor = color) | |
| find(() => document.querySelector('[data-testid="primaryColumn"]'), el => el.style.backgroundColor = color) | |
| find(() => document.querySelector('[data-at-shortcutkeys]'), makeInvisible) | |
| find(() => document.querySelector('[role="progressbar"]').parentElement.children[1], makeInvisible) | |
| find(() => document.querySelector('[aria-label="Home timelkmeline"]').children[0].children[0].children[0].children[0].children[0].children[0], makeInvisible) | |
| find(() => document.querySelector('[role="progressbar"]').parentElement.parentElement, makeInvisible) | |
| setTimeout(() => find(() => document.querySelector('[role="progressbar"]').parentElement.parentElement, makeInvisible), 250) | |
| find(() => document.querySelector('[role="progressbar"]').parentElement.children[1].children[0].children[0].children[0].children[0].children[1].children[1], makeInvisible) | |
| find(() => document.querySelector('[role="progressbar"]').parentElement.children[1].children[0].children[0].children[0].children[0].children[1].children[1].children[1].children[0].children[0], makeInvisible) | |
| find(() => document.querySelector('[role="progressbar"]').parentElement.children[1].children[0].children[0].children[0].children[0].children[1].children[0].children[0].children[0].children[0].children[0].children[0].children[0].children[0].children[0], makeInvisible) | |
| find(() => { | |
| const arr = document.getElementsByTagName('span') | |
| for (const el of arr) { | |
| if (el.innerHTML === 'Most relevant') return el.parentElement.parentElement | |
| } | |
| }, makeInvisible) | |
| let found = false | |
| for (let i = 0; i <= 1000; i += 100) { | |
| setTimeout(() => { | |
| if (!found) { | |
| const el = document.querySelector('[role="progressbar"]')?.parentElement?.parentElement | |
| if (el) { | |
| makeInvisible(el); | |
| found = true; | |
| } else if (i === 1000) { | |
| find(() => document.querySelector('[role="progressbar"]').parentElement.parentElement, makeInvisible) | |
| } | |
| } | |
| }, i) | |
| } | |
| } | |
| window.addEventListener('resize', hideSideBarItems) | |
| findAll() | |
| let url = ''; | |
| setInterval(() => { | |
| // Can't find an event that works | |
| // popstate, load, navigate don't work | |
| let curUrl = window.location.href | |
| if (curUrl !== url) { | |
| url = curUrl | |
| findAll() | |
| } | |
| }, 500) | |
| // Remove the thing that shows when people have tweeted | |
| setInterval(() => document.querySelector('[aria-label="New posts are available. Push the period key to go to the them."]')?.parentElement?.parentElement?.parentElement?.remove(), 1000) | |
| setInterval(() => { | |
| const el = document.querySelector('[data-testid="toolBar"]') | |
| if (el) el.style.backgroundColor = 'rgba(0,0,0,0)' | |
| }, 500) | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment