Internet Roadtrip - Look Out The Window v1

Allows you rotate your view 90 degrees and zoom in on neal.fun/internet-roadtrip

اعتبارا من 14-05-2025. شاهد أحدث إصدار.

// ==UserScript==
// @name         Internet Roadtrip - Look Out The Window v1
// @description  Allows you rotate your view 90 degrees and zoom in on neal.fun/internet-roadtrip
// @namespace    me.netux.site/user-scripts/internet-roadtrip/look-out-the-window-v1
// @version      1.8
// @author       Netux
// @license      MIT
// @match        https://neal.fun/internet-roadtrip/
// @icon         https://neal.fun/favicons/internet-roadtrip.png
// @grant        none
// @run-at       document-end
// ==/UserScript==

(() => {
  const STORAGE_KEY = "internet-roadtrip/mod/look-out-the-window";

  const containerEl = document.querySelector('.container');

  function findObjectsDeep(obj, compareFn) {
    const objectsSeenSoFar = [];

    function* find(obj, pathSoFar = '') {
      if (typeof obj !== 'object' || obj == null) {
        return;
      }

      if (objectsSeenSoFar.includes(obj)) {
        return;
      }
      objectsSeenSoFar.push(obj);

      for (const key in obj) {
        const keyPath = (pathSoFar.length > 0 ? `${pathSoFar}.` : '') + key;

        const payload = { key, value: obj[key], parent: obj, path: keyPath };
        if (compareFn(payload)) {
          yield {
            ... payload,
            replace: (newValue) => {
              obj[key] = newValue;
            }
          };
        }

        for (const result of find(obj[key], keyPath)) {
          yield result;
        }
      }
    }

    return find(obj);
  }
  window.DEBUG__findObjectsDeep = (... args) => Array.from(findObjectsDeep(... args));

  function findFirstObjectDeep(obj, compareFn) {
    return findObjectsDeep(obj, compareFn).next().value;
  }

  const Direction = Object.freeze({
    AHEAD: 0,
    RIGHT: 1,
    BACK: 2,
    LEFT: 3
  });

  const state = {
    lookingDirection: Direction.AHEAD,
    zoom: 1,
    dom: {}
  };
  if (STORAGE_KEY in localStorage) {
    Object.assign(
      state,
      JSON.parse(localStorage.getItem(STORAGE_KEY))
    );
  }

  function start(vue) {
    state.vue = vue;

    patch(vue);
    setupDom();
  }

  function setupDom() {
    injectStylesheet();

    const containerEl = document.querySelector('.container');
    state.dom.containerEl = containerEl;

    state.dom.panoIframeEls = Array.from(containerEl.querySelectorAll('[id^="pano"]'));
    state.dom.wheelEl = containerEl.querySelector('.wheel');
    state.dom.optionsContainerEl = containerEl.querySelector('.options');

    state.dom.windowEl = document.createElement('div');
    state.dom.windowEl.className = 'window';
    containerEl.insertBefore(state.dom.windowEl, containerEl.querySelector('iframe').nextSibling);

    function lookRight() {
      state.lookingDirection = (state.lookingDirection + 1) % 4;
      updateLookAt();
      storeSettings();
    }

    function lookLeft() {
      state.lookingDirection = state.lookingDirection - 1;
      if (state.lookingDirection < 0) {
        state.lookingDirection = 3;
      }
      updateLookAt();
      storeSettings();
    }

    function chevronImage(rotation) {
      const imgEl = document.createElement('img');
      imgEl.src = '/sell-sell-sell/arrow.svg'; // yoink
      imgEl.style.width = `10px`;
      imgEl.style.aspectRatio = `1`;
      imgEl.style.filter = `invert(1)`;
      imgEl.style.rotate = `${rotation}deg`;
      return imgEl;
    }

    state.dom.lookLeftButtonEl = document.createElement('button');
    state.dom.lookLeftButtonEl.className = 'look-left-btn';
    state.dom.lookLeftButtonEl.appendChild(chevronImage(90));
    state.dom.lookLeftButtonEl.addEventListener('click', lookLeft);
    containerEl.appendChild(state.dom.lookLeftButtonEl);

    state.dom.lookRightButtonEl = document.createElement('button');
    state.dom.lookRightButtonEl.className = 'look-right-btn';
    state.dom.lookRightButtonEl.appendChild(chevronImage(-90));
    state.dom.lookRightButtonEl.addEventListener('click', lookRight);
    containerEl.appendChild(state.dom.lookRightButtonEl);

    window.addEventListener("keydown", (event) => {
      switch (event.key) {
        case "ArrowLeft": {
          lookLeft();
          break;
        }
        case "ArrowRight": {
          lookRight();
          break;
        }
      }
    });

    window.addEventListener("mousewheel", (event) => {
      if (event.target !== document.documentElement) { // pointing at nothing but the backdrop
        return;
      }

      const scrollingForward = event.deltaY < 0;

      state.zoom = Math.min(Math.max(1, state.zoom * (scrollingForward ? 1.1 : 0.9)), 5);
      updateZoom();
      storeSettings();
    })

    updateLookAt();
    updateZoom();
  }

  function injectStylesheet() {
    const styleEl = document.createElement('style');
    styleEl.innerText = `
    .container {
      & .look-right-btn, & .look-left-btn {
        position: fixed;
        top: 50%;
        transform: translateY(-50%);
        padding-block: 1.5rem;
        border: none;
        background-color: whitesmoke;
        cursor: pointer;
      }

      & .look-right-btn {
        right: 0;
        padding-inline: 0.35rem 0.125rem;
        border-radius: 15px 0 0 15px;
      }

      & .look-left-btn {
        left: 0;
        padding-inline: 0.125rem 0.25rem;
        border-radius: 0 15px 15px 0;
      }

      &:not([data-looking-direction="0"]) :is(.wheel, .options) {
        display: none;
      }

      & .window {
        position: fixed;
        width: 100%;
        background-image: url("https://cloudy.netux.site/neal_internet_roadtrip/side window.png");
        background-size: cover;
        height: 100%;
        background-position: center;
        pointer-events: none;

        &.window--flip {
          rotate: y 180deg;
        }

        &.window--back {
          transform-origin: center 20%;
          background-image: url("https://cloudy.netux.site/neal_internet_roadtrip/back window.png");
        }
      }

      & [id^="pano"], & window {
        transition: scale 100ms linear;
      }
    }
    `;
    document.head.appendChild(styleEl);
  }

  function patch(vue) {
    const currentHeadingFind = findFirstObjectDeep(vue, ({ key }) => key === 'currentHeading');
    const getCurrentHeading = () => currentHeadingFind.parent.currentHeading;

    const currFrameFind = findFirstObjectDeep(vue, ({ key }) => key === 'currFrame');
    const getCurrFrame = () => currFrameFind.parent.currFrame;

    function replaceHeadingInPanoUrl(urlStr, headingOverride) {
      if (!urlStr) {
        return urlStr;
      }

      headingOverride ??= getCurrentHeading();

      const url = new URL(urlStr);
      url.searchParams.set('heading', (headingOverride + state.lookingDirection * 90) % 360);
      return url.toString();
    }

    for (const getPanoUrlFind of findObjectsDeep(vue, ({ key, value }) => key === 'getPanoUrl' && typeof value === 'function')) {
      const ogGetPanoUrl = getPanoUrlFind.value;
      getPanoUrlFind.replace(function() {
        const urlStr = ogGetPanoUrl.apply(this, arguments);
        return replaceHeadingInPanoUrl(urlStr, this.currentHeading);
      });
    }

    state.markPanoUrlDirty = () => {
      const panoEl = document.querySelector(`#pano${getCurrFrame()}`);
      panoEl.src = replaceHeadingInPanoUrl(panoEl.src);
    };
  }

  function updateLookAt() {
    state.dom.containerEl.dataset.lookingDirection = state.lookingDirection;

    const isLookingAhead = state.lookingDirection === Direction.AHEAD;

    state.dom.windowEl.style.display = isLookingAhead ? 'none' : '';
    if (!isLookingAhead) {
      state.dom.windowEl.classList.toggle('window--flip', state.lookingDirection === Direction.LEFT);
      state.dom.windowEl.classList.toggle('window--back', state.lookingDirection === Direction.BACK);
    }

    state.markPanoUrlDirty();
  }

  function updateZoom() {
    for (const panoIframeEl of state.dom.panoIframeEls) {
      panoIframeEl.style.scale = (state.zoom * 0.4 + 0.6 /* parallax */).toString();
    }
    state.dom.windowEl.style.scale = state.zoom.toString();
  }

  function storeSettings() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({
      lookingDirection: state.lookingDirection,
      zoom: state.zoom
    }));
  }

  const waitForVueInterval = setInterval(() => {
    const vue = containerEl.__vue__;
    if (!vue) {
      return;
    }

    window.DEBUG__vue = vue;

    clearInterval(waitForVueInterval);
    start(vue);
  }, 100);
})();
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

一年攒够 12 元

云驰互联

云驰互联