您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a "honk" graphic, and lets you change the volume of the honk in neal.fun/internet-roadtrip
// ==UserScript== // @name Internet Roadtrip - Improved Honking // @description Adds a "honk" graphic, and lets you change the volume of the honk in neal.fun/internet-roadtrip // @namespace me.netux.site/user-scripts/internet-roadtrip/improved-honking // @version 1.0.1 // @author netux (+nameless.sdk) // @license MIT // @match https://neal.fun/internet-roadtrip/ // @icon https://neal.fun/favicons/internet-roadtrip.png // @grant GM.setValue // @grant GM.getValue // @grant GM_addStyle // @run-at document-start // @require https://cdn.jsdelivr.net/npm/[email protected] // ==/UserScript== /* globals IRF */ (async () => { const MOD_NAME = GM.info.script.name.replace('Internet Roadtrip -', '').trim(); const MOD_PREFIX = 'iHONK-'; const cssClass = (... names) => names.map((name) => `${MOD_PREFIX}${name}`).join(' '); const DURATION_MS = 1500; GM_addStyle(` @keyframes ${cssClass('honked')} { 0% { opacity: 0; scale: 0.5; } 10%, 80% { opacity: 1; scale: 1; } 100% { opacity: 0; scale: 1.1; } } .container { .${cssClass('honked')} { position: fixed; top: 50%; left: 50%; translate: -50% -50%; pointer-events: none; opacity: 0; zoom: 0.75; &.${cssClass('show')} { opacity: 1; animation: ${cssClass('honked')} ${DURATION_MS}ms ease-in-out forwards; } } } `); const containerEl = await IRF.dom.container; const containerVDOM = await IRF.vdom.container; const wheelVDOM = await IRF.vdom.wheel; const { Howl } = await IRF.modules.howler; const settings = { honkVolume: containerVDOM.state.honkSound.volume(), longHonkVolume: containerVDOM.state.honkLongSound.volume(), longHonkEffectImageUrl: 'https://cloudy.netux.site/neal_internet_roadtrip/improved-honking/InternetRoadTripHonk%20by%20nameless.sdk.png', debug: { playEverytime: false, } }; for (const key in settings) { settings[key] = await GM.getValue(key, settings[key]); } async function saveSettings() { for (const key in settings) { await GM.setValue(key, settings[key]); } } function updateFromSettings() { wheelVDOM.data.honkSound.volume(settings.honkVolume); containerVDOM.data.honkLongSound.volume(settings.longHonkVolume); } updateFromSettings(); const longHonkEffectImageEl = document.createElement('img'); longHonkEffectImageEl.className = cssClass('honked'); longHonkEffectImageEl.src = settings.longHonkEffectImageUrl; containerEl.append(longHonkEffectImageEl); function playEffectVisual() { longHonkEffectImageEl.classList.add(cssClass('show')); window.setTimeout(() => longHonkEffectImageEl.classList.remove(cssClass('show')), DURATION_MS); } containerVDOM.state.changeStop = new Proxy(containerVDOM.state.changeStop, { apply(ogChangeStop, thisArg, args) { if (!thisArg.isChangingStop) { const chosen = args[1]; if (chosen === -2 || settings.debug.playEverytime) { playEffectVisual(); } } return ogChangeStop.apply(thisArg, args); } }); { const tab = IRF.ui.panel.createTabFor( { ... GM.info, script: { ... GM.info.script, name: MOD_NAME } }, { tabName: MOD_NAME, style: ` .${cssClass('tab-content')} { & *, *::before, *::after { box-sizing: border-box; } & .${cssClass('field-group')} { margin-block: 1rem; gap: 0.25rem; display: flex; align-items: center; justify-content: space-between; & label > small { color: lightgray; display: block; } & .${cssClass('field-group__label-container')}, & .${cssClass('field-group__input-container')} { width: 100%; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; gap: 1ch; } & .${cssClass('field-group__input-container')} { justify-content: end; white-space: nowrap; } } } `, className: cssClass('tab-content') } ); function makeFieldGroup({ id, label }, renderInput) { const fieldGroupEl = document.createElement('div'); fieldGroupEl.className = cssClass('field-group'); const labelContainerEl = document.createElement('div'); labelContainerEl.className = cssClass('field-group__label-container'); fieldGroupEl.append(labelContainerEl); const labelEl = document.createElement('label'); labelEl.textContent = label; labelContainerEl.append(labelEl); const inputContainerEl = document.createElement('div'); inputContainerEl.className = cssClass('field-group__input-container'); fieldGroupEl.append(inputContainerEl); const renderInputOutput = renderInput({ id }); inputContainerEl.append(... (Array.isArray(renderInputOutput) ? renderInputOutput : [renderInputOutput])); return fieldGroupEl; } tab.container.append( makeFieldGroup( { id: `${MOD_PREFIX}honk-volume`, label: 'Honk Volume' }, ({ id }) => { const renderValue = () => `${(settings.honkVolume * 100)}%`; const valueTextEl = document.createElement('span'); valueTextEl.style.width = '5ch'; valueTextEl.textContent = renderValue(); const inputEl = document.createElement('input'); inputEl.type = 'range'; inputEl.className = IRF.ui.panel.styles.slider; inputEl.min = 0; inputEl.max = 1; inputEl.step = 0.05; inputEl.value = settings.honkVolume; inputEl.addEventListener('input', async () => { let numberValue = Number.parseFloat(inputEl.value); if (Number.isNaN(numberValue)) { return; } numberValue = Math.min(Math.max(parseFloat(inputEl.min), numberValue), parseFloat(inputEl.max)); settings.honkVolume = numberValue; await saveSettings(); updateFromSettings(); valueTextEl.textContent = renderValue(); }); return [valueTextEl, inputEl]; } ), makeFieldGroup( { id: `${MOD_PREFIX}long-honk-volume`, label: 'Long Honk Volume' }, ({ id }) => { const renderValue = () => `${(settings.longHonkVolume * 100)}%`; const valueTextEl = document.createElement('span'); valueTextEl.style.width = '5ch'; valueTextEl.textContent = renderValue(); const inputEl = document.createElement('input'); inputEl.type = 'range'; inputEl.className = IRF.ui.panel.styles.slider; inputEl.min = 0; inputEl.max = 1; inputEl.step = 0.05; inputEl.value = settings.longHonkVolume; inputEl.addEventListener('input', async () => { let numberValue = Number.parseFloat(inputEl.value); if (Number.isNaN(numberValue)) { return; } numberValue = Math.min(Math.max(parseFloat(inputEl.min), numberValue), parseFloat(inputEl.max)); settings.longHonkVolume = numberValue; await saveSettings(); updateFromSettings(); valueTextEl.textContent = renderValue(); }); return [valueTextEl, inputEl]; } ), ) } if (typeof unsafeWindow !== undefined) { unsafeWindow.improvedHonking = { playEffect: () => { playEffectVisual(); containerVDOM.state.honkLongSound.play(); } }; } })();