您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.15.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/npm/[email protected] // ==/UserScript== (async () => { const LEGACY_LOCAL_STORAGE_KEY = "internet-roadtrip/mod/look-out-the-window"; const DEFAULT_FRONT_OVERLAY_IMAGE = { imageSrc: null }; const DEFAULT_SIDE_OVERLAY_IMAGE = { imageSrc: `https://cloudy.netux.site/neal_internet_roadtrip/side%20window.png`, transformOrigin: { x: "50%", y: "40%" } }; const DEFAULT_BACK_OVERLAY_IMAGE = { imageSrc: `https://cloudy.netux.site/neal_internet_roadtrip/back%20window.png`, transformOrigin: { x: "50%", y: "20%" } }; const Direction = Object.freeze({ FRONT: 0, RIGHT: 1, BACK: 2, LEFT: 3 }); const state = { settings: { lookingDirection: Direction.FRONT, zoom: 1, showVehicleUi: true, alwaysShowGameUi: false, frontOverlay: DEFAULT_FRONT_OVERLAY_IMAGE, sideOverlay: DEFAULT_SIDE_OVERLAY_IMAGE, backOverlay: DEFAULT_BACK_OVERLAY_IMAGE }, 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(); preloadOverlayImages(); const containerEl = document.querySelector('.container'); state.dom.containerEl = containerEl; state.dom.panoIframeEls = Array.from(containerEl.querySelectorAll('.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(); updateOverlays(); 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-look-out-the-window-direction="${Direction.FRONT}"]) :is(.freshener-container, .wheel-container, .options) { display: none; } & .window { position: fixed; width: 100%; background-size: cover; height: 100%; background-position: center; pointer-events: none; display: none; } &[data-look-out-the-window-direction="${Direction.FRONT}"] .window { transform-origin: var(--look-out-the-window-front-overlay-transform-origin); background-image: var(--look-out-the-window-front-overlay-image-src); } &[data-look-out-the-window-direction="${Direction.LEFT}"] .window, &[data-look-out-the-window-direction="${Direction.RIGHT}"] .window { transform-origin: var(--look-out-the-window-side-overlay-transform-origin); background-image: var(--look-out-the-window-side-overlay-image-src); } &[data-look-out-the-window-direction="${Direction.RIGHT}"] .window { rotate: y 180deg; } &[data-look-out-the-window-direction="${Direction.BACK}"] .window { transform-origin: var(--look-out-the-window-back-overlay-transform-origin); background-image: var(--look-out-the-window-back-overlay-image-src); } &.look-out-the-window-show-vehicle-ui .window { display: initial; } & .pano, & window { transition: opacity 300ms linear, scale 100ms linear; } } `; document.head.appendChild(styleEl); } function preloadOverlayImages() { const configuredOverlayImagesSources = [state.settings.frontOverlay, state.settings.sideOverlay, state.settings.backOverlay] .map((overlay) => overlay?.imageSrc) .filter((imageSrc) => !!imageSrc); for (const imageSrc of configuredOverlayImagesSources) { 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) { const calculateOverridenHeadingAngle = (baseHeading) => (baseHeading + state.settings.lookingDirection * 90) % 360; function replaceHeadingInPanoUrl(urlStr, vanillaHeadingOverride = null) { if (!urlStr) { return urlStr; } const url = new URL(urlStr); if (vanillaHeadingOverride != null || url.searchParams.has('heading')) { const currentHeading = vanillaHeadingOverride ?? parseFloat(url.searchParams.get('heading')); if (!isNaN(currentHeading)) { url.searchParams.set('heading', calculateOverridenHeadingAngle(currentHeading)); } } return url.toString(); } vue.state.getPanoUrl = new Proxy(vue.methods.getPanoUrl, { apply(ogGetPanoUrl, thisArg, args) { const urlStr = ogGetPanoUrl.apply(thisArg, args); return replaceHeadingInPanoUrl(urlStr); } }); const panoEls = Object.keys(vue.$refs).filter((name) => name.startsWith('pano')).map((key) => vue.$refs[key]); let isVanillaTransitioning = false; { /** * For reference, this is what the vanilla code more-or-less does: * * ```js * function changeStop(..., newPano, newHeading, ...) { * // ... * this.currFrame = this.currFrame === 0 ? 1 : 0; * this.currentPano = newPano; * // ... * setTimeout(() => { * this.switchFrameOrder(); * this.currentHeading = newHeading; * // ... * }, someDelay)); * } * ``` * * Note the heading is set with a delay, after switchFrameOrder is called. */ vue.state.changeStop = new Proxy(vue.methods.changeStop, { apply(ogChangeStop, thisArg, args) { isVanillaTransitioning = true; return ogChangeStop.apply(thisArg, args); } }); function isCurrentFrameFacingTheCorrectDirection() { const currPanoSrc = panoEls[vue.state.currFrame]?.src; const currPanoUrl = currPanoSrc && new URL(currPanoSrc); if (!currPanoUrl) { return false; } const urlHeading = parseFloat(currPanoUrl.searchParams.get('heading')); if (isNaN(urlHeading)) { return false; } const correctHeading = calculateOverridenHeadingAngle(state.vue.data.currentHeading); return Math.abs(urlHeading - correctHeading) < 1e-3; } vue.state.switchFrameOrder = new Proxy(vue.methods.switchFrameOrder, { apply(ogSwitchFrameOrder, thisArg, args) { isVanillaTransitioning = false; requestIdleCallback(() => { // run after currentHeading is updated (see reference method implementation above) if (!isCurrentFrameFacingTheCorrectDirection()) { attemptManualPanoTransition(/* animate: */ true); } }); return ogSwitchFrameOrder.apply(thisArg, args); } }); } let modTransitionTimeout = null; function attemptManualPanoTransition(animate = true) { const now = Date.now(); const currFrame = vue.state.currFrame; const nextFrame = (currFrame + 1) % panoEls.length; const activePanoEl = panoEls[currFrame]; const attemptManualPanoTransitionEl = panoEls[nextFrame]; if (!activePanoEl.src) { // The vanilla code hasn't set a src on the current pano iframe yet, meaning this ran too soon. // We'll let the vanilla code do the transition for us. clearTimeout(modTransitionTimeout); return; } if (isVanillaTransitioning) { // The page will do the transition for us clearTimeout(modTransitionTimeout); return; } const newPanoUrl = replaceHeadingInPanoUrl(activePanoEl.src, state.vue.data.currentHeading); if (animate) { if (modTransitionTimeout == null) { state.vue.state.currFrame = nextFrame; attemptManualPanoTransitionEl.src = newPanoUrl; } else { clearTimeout(modTransitionTimeout); activePanoEl.src = newPanoUrl; } modTransitionTimeout = setTimeout(() => { modTransitionTimeout = null; state.vue.methods.switchFrameOrder(); }, 500); } else { activePanoEl.src = newPanoUrl; } }; state.attemptManualPanoTransition = attemptManualPanoTransition; } 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 updateOverlays() { const setCssVariable = (element, name, value) => value ? element.style.setProperty(`--${name}`, value) : element.style.removeProperty(`--${name}`); for (const overlayName of ['front', 'side', 'back']) { const overlay = state.settings[`${overlayName}Overlay`]; const cssVariable = (name) => `look-out-the-window-${overlayName}-overlay-${name}`; setCssVariable( state.dom.windowEl, cssVariable('image-src'), `url("${overlay.imageSrc}")` ); setCssVariable( state.dom.windowEl, cssVariable('transform-origin'), overlay.transformOrigin ? `${overlay.transformOrigin.x} ${overlay.transformOrigin.y}` : null ); } } function updateLookAt(animate = true) { document.body.dataset.lookOutTheWindowDirection = state.settings.lookingDirection; state.attemptManualPanoTransition(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(); saveSettings(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址