|
// ==UserScript== |
|
// @name Internet Roadtrip - Look Out The Window |
|
// @namespace me.netux.site/user-scripts/internet-roadtrip/look-out-the-window-v1 |
|
// @version 1.8 |
|
// @author Netux |
|
// @match https://neal.fun/internet-roadtrip/ |
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=neal.fun |
|
// @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); |
|
})(); |