Skip to content

Instantly share code, notes, and snippets.

@netux
Last active May 14, 2025 22:10
Show Gist options
  • Save netux/54bf1c9cf7def79b4b6fc1c66bbe61cd to your computer and use it in GitHub Desktop.
Save netux/54bf1c9cf7def79b4b6fc1c66bbe61cd to your computer and use it in GitHub Desktop.
Internet Roadtrip - Vote History

Vote History for Internet Roadtrip

How to install

  1. Install a browser extension to install userscripts (GreaseMonkey, TamperMonkey, ViolentMonkey, etc.)

    ❗ If you are on a Chromium-based browser (like Google Chrome), you may need to enable Developer Mode. Without this, this userscript may not work! Other browsers, like Firefox, don't have this problem.

    Follow this guide to do learn how to enable Developer Mode.

  2. Click on the Raw below (not the one above this)

  3. Install userscript

  4. Reload Internet Roadtrip tab

// ==UserScript==
// @name Internet Roadtrip - Vote History
// @namespace me.netux.site/user-scripts/internet-roadtrip/vote-history
// @match https://neal.fun/internet-roadtrip/*
// @grant none
// @version 1.5.1
// @author Netux
// @icon https://www.google.com/s2/favicons?sz=64&domain=neal.fun
// ==/UserScript==
(() => {
const MAX_ENTRIES = 8;
const containerEl = document.querySelector('.container');
const state = {
dom: {}
}
function start(vue) {
state.vue = vue;
setupDom();
patch(vue);
/*
for (let i = 0; i < 5; i++) {
const vote = -2;
addVote(vote, 0, { [vote]: { heading: 180 } }, { [vote]: 100, [9999]: 50 });
}
*/
}
function setupDom() {
injectStylesheet();
state.dom.voteHistoryContainerEl = document.createElement('ul');
state.dom.voteHistoryContainerEl.className = 'vote-history';
containerEl.appendChild(state.dom.voteHistoryContainerEl);
}
function injectStylesheet() {
const styleEl = document.createElement('style');
styleEl.textContent = `
.vote-history {
position: fixed;
right: 10px;
top: 350px;
margin: 0;
padding: 0;
list-style: none;
color: white;
font-family: "Roboto", sans-serif;
font-size: 0.8rem;
text-align: right;
user-select: none;
& .vote-history-entry {
margin: 0.5rem 0;
text-shadow: 1px 1px 2px black;
& .vote-history-entry__icon-container {
position: relative;
height: 12px;
aspect-ratio: 1;
margin-right: 0.5rem;
vertical-align: middle;
display: inline-block;
& > img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
& .vote-history-entry__icon-shadow {
opacity: .5;
scale: 1.5;
filter: blur(5px);
}
& .vote-history-entry__icon {
filter: invert(1);
z-index: 1;
}
}
& .vote-history-entry__time {
margin-left: 1ch;
font-size: 80%;
color: lightgrey;
}
${new Array(MAX_ENTRIES).fill(null).map((_, i) => `
&:nth-child(${i + 1}) {
opacity: ${1.2 - Math.pow(1 - ((MAX_ENTRIES - i) / MAX_ENTRIES), 2)};
}
`).join('\n\n')}
}
}
`;
document.head.appendChild(styleEl);
}
function patch(vue) {
const ogChangeStop = vue.changeStop;
vue.changeStop = function (_, currentChosen) {
if (!this.isChangingStop) {
try {
addVote(currentChosen, this.currentHeading, this.currentOptions, this.voteCounts);
} catch (error) {
console.error('Could not add vote:', { currentChosen, currentHeading: this.currentHeading, currentOptions: this.currentOptions, voteCounts: this.voteCounts }, error);
}
}
return ogChangeStop.apply(this, arguments);
}
}
function addVote(vote, heading, options, voteCounts) {
function angleDifference(a, b) {
var t = (b - a) % 360;
if (t > 180) t -= 360;
if (t < -180) t += 360;
return t;
}
function getRotation(vote) {
return 0.8 * angleDifference(heading, options[vote].heading);
}
const newEntryEl = document.createElement('li');
newEntryEl.className = 'vote-history-entry';
let entryActionText = '?';
let entryIconSrc = null;
let entryIconRotation = 0;
switch (vote) {
case -2: {
entryActionText = "HONK!";
entryIconSrc = '/internet-roadtrip/honk.svg'
break;
}
case -1: {
entryActionText = "Seek Radio";
entryIconSrc = '/internet-roadtrip/skip.svg'
break;
}
default: {
entryIconSrc = '/internet-roadtrip/chevron-black.svg';
if (options[vote]) {
entryActionText = options[vote].description;
entryIconRotation = getRotation(vote);
} else {
entryActionText = 'Turn Around';
entryIconRotation = (-heading) % 360;
}
break;
}
}
const voteIconEl = document.createElement('div');
voteIconEl.className = 'vote-history-entry__icon-container';
if (entryIconRotation !== 0) {
voteIconEl.style.rotate = `${entryIconRotation}deg`;
}
newEntryEl.appendChild(voteIconEl);
const voteIconShadowImageEl = document.createElement('img');
voteIconShadowImageEl.className = 'vote-history-entry__icon-shadow';
voteIconShadowImageEl.src = entryIconSrc;
voteIconEl.appendChild(voteIconShadowImageEl);
const voteIconImageEl = document.createElement('img');
voteIconImageEl.className = 'vote-history-entry__icon';
voteIconImageEl.src = entryIconSrc;
voteIconEl.appendChild(voteIconImageEl);
const voteCount = voteCounts[vote];
const entryVotesText = voteCount != null
? `${voteCount} vote${voteCount === 1 ? '' : 's'}, ${Math.round(voteCount / Object.values(voteCounts).reduce((acc, votes) => acc + votes, 0) * 100)}%`
: 'no votes';
const entryTextNode = document.createTextNode(`${entryActionText} (${entryVotesText})`);
newEntryEl.appendChild(entryTextNode);
const entryTimeEl = document.createElement('span');
entryTimeEl.className = 'vote-history-entry__time';
entryTimeEl.innerText = new Date().toLocaleTimeString();
newEntryEl.appendChild(entryTimeEl);
while (state.dom.voteHistoryContainerEl.childElementCount >= MAX_ENTRIES) {
state.dom.voteHistoryContainerEl.lastElementChild.remove();
}
if (state.dom.voteHistoryContainerEl.childElementCount > 0) {
state.dom.voteHistoryContainerEl.insertBefore(newEntryEl, state.dom.voteHistoryContainerEl.firstChild);
} else {
state.dom.voteHistoryContainerEl.appendChild(newEntryEl);
}
}
{
const waitForVueInterval = setInterval(() => {
const vue = containerEl.__vue__;
if (!vue?.changeStop) {
return;
}
clearInterval(waitForVueInterval);
start(vue);
}, 100);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment