Internet Roadtrip - Look Out The Window v1

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

目前為 2025-05-17 提交的版本,檢視 最新版本

// ==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.9
// @author       Netux
// @license      MIT
// @match        https://neal.fun/internet-roadtrip/
// @icon         https://neal.fun/favicons/internet-roadtrip.png
// @grant        none
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/npm/[email protected]
// ==/UserScript==

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

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

  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 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';
    state.dom.panoIframeEls.at(-1).insertAdjacentElement('afterend', state.dom.windowEl);

    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("wheel", (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)), 20);
      updateZoom();
      storeSettings();
    })

    updateLookAt();
    updateZoom();
  }

  function injectStylesheet() {
    const styleEl = document.createElement('style');
    styleEl.innerText = `
    .container {
      & .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([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: opacity 300ms linear, scale 100ms linear;
      }
    }
    `;
    document.head.appendChild(styleEl);
  }

  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.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 updateLookAt(animate = true) {
    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.transitionPano(animate);
  }

  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
    }));
  }

  state.vue = await IRF.vdom.container;

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

QingJ © 2025

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