Internet Roadtrip - Look Out The Window v1

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

// ==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.13.1
// @author       Netux
// @license      MIT
// @match        https://neal.fun/internet-roadtrip/
// @icon         https://neal.fun/favicons/internet-roadtrip.png
// @grant        GM.setValues
// @grant        GM.getValues
// @grant        GM.registerMenuCommand
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/[email protected]
// @require      https://cdn.jsdelivr.net/npm/[email protected]
// ==/UserScript==

(async () => {
  const LEGACY_LOCAL_STORAGE_KEY = "internet-roadtrip/mod/look-out-the-window";
  const SIDE_WINDOW_IMAGE_SRC = `https://cloudy.netux.site/neal_internet_roadtrip/side%20window.png`;
  const BACK_WINDOW_IMAGE_SRC = `https://cloudy.netux.site/neal_internet_roadtrip/back%20window.png`;

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

  const state = {
    settings: {
      lookingDirection: Direction.AHEAD,
      zoom: 1,
      showVehicleUi: true,
      alwaysShowGameUi: false
    },
    dom: {}
  };

  {
    // migrate locals storage data form versions <=1.12.0
    if (LEGACY_LOCAL_STORAGE_KEY in localStorage) {
      const localStorageSettings = JSON.parse(localStorage.getItem(LEGACY_LOCAL_STORAGE_KEY));
      await GM.setValues(localStorageSettings);
      localStorage.removeItem(LEGACY_LOCAL_STORAGE_KEY);
    }
  }

  {
    const storedSettings = await GM.getValues(Object.keys(state.settings))
    Object.assign(
      state.settings,
      storedSettings
    );
  }

  function setupDom() {
    injectStylesheet();
    preloadImages();

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

    state.dom.panoIframeEls = Array.from(containerEl.querySelectorAll('[id^="pano"]'));

    state.dom.windowEl = document.createElement('div');
    state.dom.windowEl.className = 'window';
    state.dom.panoIframeEls.at(-1).insertAdjacentElement('afterend', state.dom.windowEl);

    async function lookRight() {
      state.settings.lookingDirection = (state.settings.lookingDirection + 1) % 4;
      updateLookAt();
      await saveSettings();
    }

    async function lookLeft() {
      state.settings.lookingDirection = state.settings.lookingDirection - 1;
      if (state.settings.lookingDirection < 0) {
        state.settings.lookingDirection = 3;
      }
      updateLookAt();
      await saveSettings();
    }

    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", async (event) => {
      if (event.target !== document.body) {
        return;
      }

      switch (event.key) {
        case "ArrowLeft": {
          await lookLeft();
          break;
        }
        case "ArrowRight": {
          await lookRight();
          break;
        }
      }
    });

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

      const scrollingForward = event.deltaY < 0;

      state.settings.zoom = Math.min(Math.max(1, state.settings.zoom * (scrollingForward ? 1.1 : 0.9)), 20);
      updateZoom();
      await saveSettings();
    })

    updateUiFromSettings();
    updateLookAt();
    updateZoom();
  }

  function injectStylesheet() {
    const styleEl = document.createElement('style');
    styleEl.innerText = `
    body {
      & .look-right-btn, & .look-left-btn {
        position: fixed;
        bottom: 200px;
        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(.look-out-the-window-always-show-game-ui):not([data-looking-direction="0"]) :is(.freshener-container, .wheel-container, .options) {
        display: none;
      }

      &.look-out-the-window-show-vehicle-ui .window {
        position: fixed;
        width: 100%;
        background-image: url("${SIDE_WINDOW_IMAGE_SRC}");
        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("${BACK_WINDOW_IMAGE_SRC}");
        }
      }

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

  function preloadImages() {
    for (const imageSrc of [SIDE_WINDOW_IMAGE_SRC, BACK_WINDOW_IMAGE_SRC]) {
      const image = new Image();
      image.onload = () => {
        console.debug(`Successfully preloaded Look Out the Window image at "${imageSrc}"`);
      };
      image.onerror = (event) => {
        console.error(`Failed to preload Look Out the Window image at "${imageSrc}"`, event);
      };
      image.src = imageSrc;
    }
  }

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

      headingOverride ??= vue.state.currentHeading;

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

    vue.state.getPanoUrl = new Proxy(vue.methods.getPanoUrl, {
      apply(ogGetPanoUrl, thisArg, args) {
        const urlStr = ogGetPanoUrl.apply(thisArg, args);
        return replaceHeadingInPanoUrl(urlStr, this.currentHeading);
      }
    });

    const panoEls = Object.keys(vue.$refs).filter((name) => name.startsWith('pano')).map((key) => vue.$refs[key]);

    state.transitionPano = (animate = true) => {
      const currFrame = vue.state.currFrame;
      const nextFrame = (currFrame + 1) % panoEls.length;

      const activePanoEl = panoEls[currFrame];

      if (animate) {
        const transitionPanoEl = panoEls[nextFrame];
        vue.state.currFrame = nextFrame;

        transitionPanoEl.src = replaceHeadingInPanoUrl(activePanoEl.src);

        setTimeout(() => {
          vue.methods.switchFrameOrder();
        }, 500);
      } else {
        activePanoEl.src = replaceHeadingInPanoUrl(activePanoEl.src);
      }
    };
  }

  function updateUiFromSettings() {
    document.body.classList.toggle('look-out-the-window-show-vehicle-ui', state.settings.showVehicleUi);
    document.body.classList.toggle('look-out-the-window-always-show-game-ui', state.settings.alwaysShowGameUi);
  }

  function updateLookAt(animate = true) {
    document.body.dataset.lookingDirection = state.settings.lookingDirection;

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

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

    state.transitionPano(animate);
  }

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

  async function saveSettings() {
    await GM.setValues(state.settings);
  }

  GM.registerMenuCommand('Toggle Vehicle UI', async () => {
    state.settings.showVehicleUi = !state.settings.showVehicleUi;
    updateUiFromSettings();
    await saveSettings();
  }, { id: 'look-out-the-window-toggle-vehicle-ui' });
  GM.registerMenuCommand('Toggle Always show Game UI', async () => {
    state.settings.alwaysShowGameUi = !state.settings.alwaysShowGameUi;
    updateUiFromSettings();
    await saveSettings();
  }, { id: 'look-out-the-window-toggle-always-show-game-ui' });

  state.vue = await IRF.vdom.container;

  patch(state.vue);
  setupDom();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址