NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name GeoFilter // @description Shift-F to set filter // @namespace https://www.geoguessr.com/ // @version 0.8 // @author drparse // @match https://www.geoguessr.com/* // @grant unsafeWindow // @run-at document-start // @updateURL https://openuserjs.org/meta/drparse/GeoFilter.meta.js // @copyright 2020, drparse // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @noframes // ==/UserScript== // TODO: change nocar blur in mauve and sepia modes // TODO: turn off only car // ideas: checkerboard with components // TODO: move nocar to top of stack so that the filters stack (function() { 'use strict'; function injected() { const st = JSON.parse(window.localStorage.getItem('bintuluFilterState') || '{}'); const OPTIONS = { colorR: 0.5, colorG: 0.5, colorB: 0.5, }; // If the script breaks, search devtools for "BINTULU" and replace these lines with the new one const vertexOld = "const float f=3.1415926;varying vec3 a;uniform vec4 b;attribute vec3 c;attribute vec2 d;uniform mat4 e;void main(){vec4 g=vec4(c,1);gl_Position=e*g;a=vec3(d.xy*b.xy+b.zw,1);a*=length(c);}"; const fragOld = "precision highp float;const float h=3.1415926;varying vec3 a;uniform vec4 b;uniform float f;uniform sampler2D g;void main(){vec4 i=vec4(texture2DProj(g,a).rgb,f);gl_FragColor=i;}"; const vertexNew = ` const float f=3.1415926; varying vec3 a; varying vec3 potato; uniform vec4 b; attribute vec3 c; attribute vec2 d; uniform mat4 e; void main(){ vec4 g=vec4(c,1); gl_Position=e*g; a = vec3(d.xy * b.xy + b.zw,1); a *= length(c); potato = vec3(d.xy, 1.0) * length(c); }`; const fragNew = `precision highp float; const float h=3.1415926; const float tau=6.2831853; varying vec3 a; varying vec3 potato; uniform vec4 b; uniform float f; uniform sampler2D g; vec3 RGBToHSL(vec3 color) { vec3 hsl; // init to 0 to avoid warnings ? (and reverse if + remove first part) float fmin = min(min(color.r, color.g), color.b); //Min. value of RGB float fmax = max(max(color.r, color.g), color.b); //Max. value of RGB float delta = fmax - fmin; //Delta RGB value hsl.z = (fmax + fmin) / 2.0; // Luminance if (delta == 0.0) //This is a gray, no chroma... { hsl.x = 0.0; // Hue hsl.y = 0.0; // Saturation } else //Chromatic data... { if (hsl.z < 0.5) hsl.y = delta / (fmax + fmin); // Saturation else hsl.y = delta / (2.0 - fmax - fmin); // Saturation float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta; float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta; float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta; if (color.r == fmax ) hsl.x = deltaB - deltaG; // Hue else if (color.g == fmax) hsl.x = (1.0 / 3.0) + deltaR - deltaB; // Hue else if (color.b == fmax) hsl.x = (2.0 / 3.0) + deltaG - deltaR; // Hue if (hsl.x < 0.0) hsl.x += 1.0; // Hue else if (hsl.x > 1.0) hsl.x -= 1.0; // Hue } return hsl; } float HueToRGB(float f1, float f2, float hue) { if (hue < 0.0) hue += 1.0; else if (hue > 1.0) hue -= 1.0; float res; if ((6.0 * hue) < 1.0) res = f1 + (f2 - f1) * 6.0 * hue; else if ((2.0 * hue) < 1.0) res = f2; else if ((3.0 * hue) < 2.0) res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0; else res = f1; return res; } vec3 HSLToRGB(vec3 hsl) { vec3 rgb; if (hsl.y == 0.0) rgb = vec3(hsl.z); // Luminance else { float f2; if (hsl.z < 0.5) f2 = hsl.z * (1.0 + hsl.y); else f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z); float f1 = 2.0 * hsl.z - f2; rgb.r = HueToRGB(f1, f2, hsl.x + (1.0/3.0)); rgb.g = HueToRGB(f1, f2, hsl.x); rgb.b= HueToRGB(f1, f2, hsl.x - (1.0/3.0)); } return rgb; } vec3 filter_nightTime(vec3 rgb) { vec3 hsl = RGBToHSL(rgb); hsl.z = 1.0 - hsl.z; return HSLToRGB(hsl); } vec3 filter_hueShift(vec3 rgb, float degrees) { vec3 hsl = RGBToHSL(rgb); hsl.x = mod(hsl.x + degrees/360.0, 1.0); return HSLToRGB(hsl); } vec3 filter_satShift(vec3 rgb, float amount) { vec3 hsl = RGBToHSL(rgb); hsl.y = clamp(hsl.y * amount, 0.0, 1.0); return HSLToRGB(hsl); } vec3 filter_sepia(vec3 rgb, float hue, float amount) { vec3 hsl = RGBToHSL(rgb); hsl.x = hue; hsl.y = 0.33;//clamp(hsl.y * amount, 0.0, 1.0); return HSLToRGB(hsl); } vec3 filter_mauve(vec3 rgb) { //vec3 hsl = RGBToHSL(rgb); //hsl.x = 0.76; //hsl.y = 0.33;//clamp(hsl.y * amount, 0.0, 1.0); vec3 mauve = vec3(224.0/255.0, 176.0/255.0, 255.0/255.0); return mix(vec3(1.0), mauve, clamp(distance(rgb, mauve)/(1.74*0.8), 0.0, 1.0)); //HSLToRGB(hsl); } vec3 filter_posterize(vec3 rgb, float amount) { rgb = clamp(floor(rgb * amount + 0.5) / amount, 0.0, 1.0); return rgb; } vec3 filter_gen69(vec3 rgb, float hue, float amount) { vec3 hsl = RGBToHSL(rgb); float prevSat = hsl.y; hsl.y = 1.0; hsl.x = prevSat < 0.1 ? mod(hsl.z * 2.0, 1.0) : hsl.x; hsl.z = clamp(hsl.z, 0.2, 1.0); return HSLToRGB(hsl); } vec3 filter_hueWheel(vec3 rgb, float angle) { vec3 hsl = RGBToHSL(rgb); hsl.x = mod(hsl.x + angle, 1.0); return HSLToRGB(hsl); } bool filter_colorInclude(vec3 rgb, float hueMin, float hueMax, float satMin) { vec3 hsl = RGBToHSL(rgb); float fudge = 0.1; //bool shouldIncludeOld = (hsl.y > 0.2 && hsl.x > 0.33-0.1666*1.5 && hsl.x < 0.33+0.1666); bool shouldInclude = ( hueMin < hsl.x && hsl.x < hueMax && satMin < hsl.y ) || (hueMin-fudge < hsl.x && hsl.x < hueMax+fudge && hsl.z < 0.2); return shouldInclude; } vec3 DEBUG_filter_colorInclude(vec3 rgb, float hueMin, float hueMax, float satMin) { vec3 hsl = RGBToHSL(rgb); float hueMid = (hueMin + hueMax)/2.0; float hueDistance = abs(hsl.x - hueMid); float blackDistance = clamp(hsl.z, 0.0, 0.5) * 2.0; float satDistance = 1.0 - clamp(hsl.y, 0.0, 0.2) * 5.0; return (hueDistance * blackDistance * (satDistance*2.0) < 0.15) ? rgb : vec3(0.5, 0.5, 0.5); bool shouldInclude = hueMin < hsl.x && hsl.x < hueMax && satMin < hsl.y; return ( !(hueMin < hsl.x) ? vec3(0.75, 0.5, 0.5) : !(hsl.x < hueMax) ? vec3(0.5, 0.5, 0.75) : !(satMin < hsl.y) ? vec3(0.5, 0.75, 0.5) : rgb ); } void main(){ vec2 aD = potato.xy / a.z; float thetaD = aD.y; float thresholdD1 = 0.6; float thresholdD2 = 0.7; float x = aD.x; float y = abs(4.0*x - 2.0); float phiD = smoothstep(0.0, 1.0, y > 1.0 ? 2.0 - y : y); vec3 coord = a; `+(st.filterMinecraft?.on ? ` `+(st.filterSemiMinecraft?.on ? ` vec3 rgb0 = texture2DProj(g,a).rgb; vec3 hsl0 = RGBToHSL(rgb0); float q = (hsl0.y < 0.075 ? 1.0 : 5.0 * float(`+(st.filterMinecraft.value||20)+`))/1000.0 * a.z; ` : ` float q = float(`+(st.filterMinecraft.value||20)+`)/1000.0 * a.z; `)+` coord = normalize(floor(a/q)*q); ` : '')+` `+(st.filterWaveDistortion?.on ? ` //coord.x = coord.x + sin(x * tau * 10.0)*0.1; coord = normalize(a); coord.y = coord.y + sin(y * tau * 10.0) * (float(`+(st.filterWaveDistortion.value||10)+`)/1000.0); ` : '')+` vec3 rgb = texture2DProj(g,coord).rgb; bool mask = true; `+(st.filterVegetationOnly?.on ? ` mask = mask && filter_colorInclude(rgb, 0.10, 0.40, 0.1); // 0.333-0.166, 0.333+0.166 ` : '')+` `+(st.filterVegetationOff?.on ? ` mask = mask && !filter_colorInclude(rgb, 0.10, 0.40, 0.1); // 0.333-0.166, 0.333+0.166 ` : '')+` `+(st.filterCheckerboard?.on ? ` float checkerboard = mod(dot(floor(gl_FragCoord.xy / float(`+(st.filterCheckerboard?.value || 400)+`)), vec2(1.0)), 2.0); mask = mask && checkerboard < 1.0; ` : '')+` `+(st.filterNoCar?.on ? ` mask = mask && thetaD <= mix(thresholdD1, thresholdD2, phiD); ` : '')+` rgb = mask ? rgb : vec3(0.5); `+(false && st.filterVegetationOnly?.on ? ` rgb = DEBUG_filter_colorInclude(rgb, 0.10, 0.40, 0.1); // 0.333-0.166, 0.333+0.166 ` : '')+` `+(st.filterNightTime?.on ? ` rgb = filter_nightTime(rgb); ` : '')+` `+(st.filterGen5?.on ? ` rgb = filter_satShift(rgb, 2.0); ` : '')+` `+(st.filterGen69?.on ? ` rgb = filter_gen69(rgb, 0.1, 0.5); ` : '')+` `+(st.filterHueWheel?.on ? ` rgb = filter_hueWheel(rgb, x); ` : '')+` `+(st.filterSepia?.on ? ` rgb = filter_sepia(rgb, 0.1, 0.5); ` : '')+` `+(st.filterMauve?.on ? ` rgb = filter_mauve(rgb); ` : '')+` `+(st.filterHueShift?.on ? ` rgb = filter_hueShift(rgb, float(`+(st.filterHueShift.value||180)+`)); ` : '')+` `+(st.filterPosterize?.on ? ` rgb = filter_posterize(rgb, float(`+(st.filterPosterize?.value || 5)+`)); ` : '')+` `+(st.filterGrayscale?.on ? ` rgb = vec3(dot(rgb, vec3( 0.2125, 0.7154, 0.0721 ) )); ` : '')+` `+(st.filterInvert?.on ? ` rgb = vec3(1.0) - rgb; ` : '')+` `+(st.filterPinhole?.on ? ` const float pinholeFactor = 1.65; rgb = mix(vec3(0.0), rgb, clamp( (2.0*abs(x-0.5) - 1.0/pinholeFactor)*(5.0*pinholeFactor), 0.0, 1.0) ); // ` : '')+` vec4 i = vec4( rgb, f); gl_FragColor=i; }`; console.log('BINTULU NEW FRAG', fragNew); const EXP_GL2 = false; // experimental webgl 2 mode function installShaderSource(ctx) { const g = ctx.shaderSource; function shaderSource() { if (typeof arguments[1] === 'string') { let glsl = arguments[1]; console.log('BINTULU shader', glsl); if (glsl === vertexOld) glsl = vertexNew; else if (glsl === fragOld) glsl = fragNew; return g.call(this, arguments[0], glsl); } return g.apply(this, arguments); } shaderSource.bestcity = 'bintulu'; ctx.shaderSource = shaderSource; } function installGetContext(el) { const g = el.getContext; el.getContext = function() { if (arguments[0] === 'webgl' || arguments[0] === 'webgl2') { if (EXP_GL2) arguments[0] = 'webgl2'; // force WebGL 2 const ctx = g.apply(this, arguments); if (ctx && ctx.shaderSource && ctx.shaderSource.bestcity !== 'bintulu') { installShaderSource(ctx); } return ctx; } return g.apply(this, arguments); }; } const f = document.createElement; document.createElement = function() { if (arguments[0] === 'canvas' || arguments[0] === 'CANVAS') { const el = f.apply(this, arguments); installGetContext(el); return el; } return f.apply(this, arguments); }; function addCompassStyle() { let style = document.createElement('style'); style.id = 'bintulu_nocompass'; style.innerHTML = '.compass { display: none }'; document.head.appendChild(style); } function h(selector, ...args) { let tag = selector.match(/^\w+/); tag = tag ? tag[0] : 'div'; const classes = selector.matchAll(/\.(\w+)/g); const id = selector.match(/#(\w+)/); const $el = document.createElement(tag); $el.classList.add(...([...classes].map(x => x[1]))); if (id) $el.id = id; function addChild(child) { if (typeof child === 'string') { $el.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { $el.appendChild(child); } else if (typeof child === 'object') { return child; } } for (const arg of args) { if (Array.isArray(arg)) { for (const sub of arg) addChild(sub); } else { const residual = addChild(arg); if (residual) { for (const key in residual) { if (key.startsWith('on')) { $el.addEventListener(key.slice(2).toLowerCase(), residual[key]); } else if (key === 'style') { Object.assign($el.style, residual[key]); } else { $el[key] = residual[key]; } } } } } return $el; } function showGUI() { let $el = document.getElementById('bintulu_filter_ui'); if ($el) { $el.style.display = 'block'; return $el; } $el = document.createElement('div'); $el.id = 'bintulu_filter_ui'; Object.assign($el.style, { display: 'block', position: 'fixed', background: 'white', zIndex: '1000', width: 'fit-content', height: 'fit-content', top: '30px', right: '30px', padding: '20px', borderRadius: '10px', boxShadow: '0 10px 40px 0', overflow: 'hidden', }); /*$el.innerHTML = ` <div id=" `;*/ function stateChanged() { // Do something when state changes // Store state in local storage const j = JSON.stringify(st); window.localStorage.setItem('bintuluFilterState', j); } function makeFilterWithOneParam(displayName, key, min, max, defaultV) { return [ h('label.bntFilterHeader', { id: 'bnt_'+key }, [ h('input', { type: 'checkbox', checked: st[key]?.on || false, onChange: (e) => { st[key] = st[key] || { }; st[key].on = e.target.checked; if (e.target.checked) { if (key === 'filterMinecraft' && st.filterWaveDistortion) { st.filterWaveDistortion.on = false; document.querySelector('#bnt_filterWaveDistortion input[type=checkbox]').checked = false; } else if (key === 'filterWaveDistortion' && st.filterMinecraft) { st.filterMinecraft.on = false; document.querySelector('#bnt_filterMinecraft input[type=checkbox]').checked = false; } } stateChanged(); }}), displayName, h('div.bntRangeInput', [ h('input', { type: 'range', min, max, value: st[key]?.value || defaultV, onInput: (e) => { st[key] = st[key] || { }; if (Number.isSafeInteger(Number(e.target.value))) { st[key].on = true; st[key].value = Number(e.target.value); } document.getElementById('BntAmount_'+key).innerText = String(st[key]?.value || defaultV); stateChanged(); }}), h('span.bntAmount', { id: 'BntAmount_'+key }, [String(st[key]?.value || defaultV)]), ]), ]), ]; } function makeFilterWithZeroParams(displayName, key) { return [ h('label.bntFilterHeader', { id: 'bnt_'+key }, [ h('input', { type: 'checkbox', checked: st[key]?.on || false, onChange: (e) => { st[key] = st[key] || { }; st[key].on = e.target.checked; if (key === 'filterGen69' && st.filterGen69) { st.filterHueWheel.on = e.target.checked; document.querySelector('#bnt_filterHueWheel input[type=checkbox]').checked = e.target.checked; } else if (key === 'filterMauve' && st.filterMauve) { st.filterSepia.on = false; document.querySelector('#bnt_filterSepia input[type=checkbox]').checked = false; } else if (key === 'filterSepia' && st.filterSepia) { st.filterMauve.on = false; document.querySelector('#bnt_filterMauve input[type=checkbox]').checked = false; } stateChanged(); }}), displayName, ]), ]; } function spacer() { return h('br'); } $el.appendChild( h('.bntFilterOuter', [ ...makeFilterWithOneParam('Minecraft', 'filterMinecraft', '5', '50', 20), ...makeFilterWithZeroParams('Semi Minecraft', 'filterSemiMinecraft'), ...makeFilterWithOneParam('Cheer up beer', 'filterWaveDistortion', '1', '100', 10), spacer(), ...makeFilterWithOneParam('Hue Shift', 'filterHueShift', '10', '350', 180), ...makeFilterWithZeroParams('Gen 5 camera', 'filterGen5'), ...makeFilterWithZeroParams('Gen \'69 camera', 'filterGen69'), ...makeFilterWithZeroParams('Hue wheel', 'filterHueWheel'), ...makeFilterWithZeroParams('Sepia', 'filterSepia'), ...makeFilterWithZeroParams('Mauve', 'filterMauve'), ...makeFilterWithOneParam('Dog Vision', 'filterPosterize', '1', '10', 5), spacer(), ...makeFilterWithZeroParams('Grayscale', 'filterGrayscale'), ...makeFilterWithZeroParams('Invert', 'filterInvert'), ...makeFilterWithZeroParams('Night mode', 'filterNightTime'), spacer(), ...makeFilterWithZeroParams('Vegan mode', 'filterVegetationOnly'), ...makeFilterWithZeroParams('Keto mode', 'filterVegetationOff'), spacer(), ...makeFilterWithZeroParams('No Car', 'filterNoCar'), ...makeFilterWithZeroParams('Pinhole', 'filterPinhole'), ...makeFilterWithOneParam('Checkerboard (nmpz)', 'filterCheckerboard', '2', '1000', 400), /* Ideas: - Pinhole mode [how?] - Mirror mode [how?] - done - Mauve filter - done - TODO: turn compass on by default - nofix - TODO: fix zoom in minecraft mode - TODO: fix artefacts - done - TODO: NCNC controls */ ]), ); $el.appendChild( h('.bntFooter', [ h('input', { type: 'button', value: 'Reload', onClick: () => { location.reload() } }), h('span.helper', ' shift-F to hide'), ]), ); let ss = document.createElement('style'); ss.id = 'bintulu_filters'; ss.innerHTML = ` .bntFilterOuter { } .bntFilterHeader { display: block; user-select: none; } .bntFilterBody { } .bntRangeInput { display: flex; flex-direction: row; align-items: center; justify-content: center; } .bntRangeInput input { flex: 1; } .bntRangeInput span { min-width: 2em; } #bnt_filterHueWheel { text-indent: 20px; } `; document.head.appendChild(ss); document.body.appendChild($el); return $el; } function hideGUI() { const $el = document.getElementById('bintulu_filter_ui'); $el.style.display = 'none'; } //addCompassStyle(); document.addEventListener('keydown', (evt) => { if (!evt.repeat && evt.code === 'KeyK' && evt.shiftKey && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { let style = document.getElementById('bintulu_nocompass'); if (!style) { addCompassStyle(); } else { style.remove(); } } if (!evt.repeat && evt.code === 'KeyF' && evt.shiftKey && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { let $el = document.getElementById('bintulu_filter_ui'); if (!$el) { showGUI(); } else { if ($el.style.display === 'none') { showGUI(); } else { hideGUI(); } } } }); } unsafeWindow.eval(`(${injected.toString()})()`); })();