// ==UserScript==
// @name geoDK's Random Zoom Mode
// @namespace https://geodk.dev/
// @version 1.0.0
// @description Adds random zooms and screen blackouts in Geoguessr for challenge play. Special thanks to Chhote for the idea!!
// @author Dorukhan Bozkurt (geoDK)
// @match https://www.geoguessr.com/*
// @icon https://www.svgrepo.com/show/40039/eye.svg
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let zoomTime = parseFloat(localStorage.getItem('zoomTime')) || 1.0;
let zoomNumber = parseInt(localStorage.getItem('zoomNumber')) || 2;
let zoomEnabled = localStorage.getItem('zoomEnabled') === 'enabled';
const blinkTime = 500; // ms
const minZoom = 3.0;
const maxZoom = 5.0;
let wasBackdropThereOrLoading = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getPanoramaEl() {
return document.querySelector("[data-qa=panorama]");
}
function applySimpleRandomZoom(pano) {
if (!pano) return;
pano.style.transition = 'none';
const zoomAmount = minZoom + Math.random() * (maxZoom - minZoom);
const randPercent = () => `${Math.random() * 100}%`;
pano.style.transformOrigin = `${randPercent()} ${randPercent()}`;
pano.style.transform = `scale(${zoomAmount})`;
}
function resetZoom(pano) {
if (!pano) return;
pano.style.transform = 'scale(1)';
pano.style.transformOrigin = 'center center';
}
function hidePanorama() {
const pano = getPanoramaEl();
if (pano) pano.style.filter = 'brightness(0%)';
}
function showPanorama() {
const pano = getPanoramaEl();
if (pano) pano.style.filter = 'brightness(100%)';
}
function isBackdropThereOrLoading() {
return document.querySelector('[class*=overlay_backdrop__]') ||
document.querySelector('[class*=round-starting_wrapper__]') ||
document.querySelector('[class*=fullscreen-spinner_root__]') ||
document.querySelector('[class*=game-starting_container__]');
}
async function handleRoundStart() {
if (!zoomEnabled) return;
const pano = getPanoramaEl();
if (!pano) return;
resetZoom(pano);
showPanorama();
for (let i = 0; i < zoomNumber; i++) {
applySimpleRandomZoom(pano);
await sleep(zoomTime * 1000);
if (i < zoomNumber - 1) {
hidePanorama();
await sleep(blinkTime);
showPanorama();
resetZoom(pano);
}
}
hidePanorama();
resetZoom(pano);
}
const observer = new MutationObserver(() => {
addSettingsUI();
if (isBackdropThereOrLoading()) {
wasBackdropThereOrLoading = true;
} else if (wasBackdropThereOrLoading) {
wasBackdropThereOrLoading = false;
handleRoundStart();
}
});
observer.observe(document.body, { childList: true, subtree: true });
function addSettingsUI() {
const container = document.querySelector('[class*=map-block_mapStatsContainer__]');
if (!container || document.getElementById('zoomModeSettings')) return;
const html = `
<div id="zoomModeSettings" style="
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.05);
padding: 12px;
border-radius: 12px;
z-index: 999;
backdrop-filter: blur(4px);
color: white;
width: 220px;
font-family: inherit;
box-shadow: 0 0 8px rgba(0,0,0,0.2);
">
<h3 style="margin-bottom: 10px; font-size: 16px; text-align: center;">🎥 geoDK's Random Zoom Mode</h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<label style="margin-right: 10px;">Enabled</label>
<input type="checkbox" id="zoomEnabled">
</div>
<div style="margin-bottom: 10px;">
<label style="display: block; margin-bottom: 4px;">Zoom Time (s)</label>
<input type="number" id="zoomTime" value="${zoomTime}" min="0.1" step="0.1" style="width: 100%;">
</div>
<div>
<label style="display: block; margin-bottom: 4px;">Zoom Number</label>
<input type="number" id="zoomNumber" value="${zoomNumber}" min="1" step="1" style="width: 100%;">
</div>
</div>`;
container.insertAdjacentHTML('afterend', html);
document.getElementById('zoomEnabled').checked = zoomEnabled;
document.getElementById('zoomEnabled').addEventListener('change', e => {
zoomEnabled = e.target.checked;
localStorage.setItem('zoomEnabled', zoomEnabled ? 'enabled' : 'disabled');
});
document.getElementById('zoomTime').addEventListener('change', e => {
zoomTime = parseFloat(e.target.value);
localStorage.setItem('zoomTime', zoomTime);
});
document.getElementById('zoomNumber').addEventListener('change', e => {
zoomNumber = parseInt(e.target.value);
localStorage.setItem('zoomNumber', zoomNumber);
});
}
})();
/*
* ----------------------------------------------------------------------------
* MIT License
* ----------------------------------------------------------------------------
*
* Copyright (c) 2025 Dorukhan Bozkurt (geoDK)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* ----------------------------------------------------------------------------
* TL;DR (just for human understanding, not legally binding):
* - ✅ You can use, copy, change, and share this script freely
* - 🧠 You must keep the credit to geoDK (Dorukhan Bozkurt)
* - 🚫 No warranty — if something breaks, it's not my responsibility
* ----------------------------------------------------------------------------
*/