drparse / GeoFilter

// ==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()})()`);

})();