- // ==UserScript==
- // @name HTML canvas fps limiter
- // @description Fps limiter for browser games or some 2D/3D animations
- // @author Konf
- // @namespace https://greasyfork.org/users/424058
- // @icon https://img.icons8.com/external-neu-royyan-wijaya/32/external-animation-neu-solid-neu-royyan-wijaya.png
- // @icon64 https://img.icons8.com/external-neu-royyan-wijaya/64/external-animation-neu-solid-neu-royyan-wijaya.png
- // @version 1.0.0
- // @match *://*/*
- // @compatible Chrome
- // @compatible Opera
- // @compatible Firefox
- // @run-at document-start
- // @grant unsafeWindow
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // ==/UserScript==
-
- /*
- * Implementation is kinda rough, but it seems working, so I don't care anymore
- *
- * A huge part is inspired (stolen) from:
- * https://chriscourses.com/blog/standardize-your-javascript-games-framerate-for-different-monitors
- *
- * msPrevMap is needed to provide individual rate limiting in cases
- * where requestAnimationFrame is used by more than one function loop.
- * Using a variable instead of a map in such cases makes so the only one
- * random loop will be working, and the others will not be working at all.
- * But if some loop is using anonymous functions, the map mode can't limit it,
- * so I've decided to make a switcher: the map mode or the single variable mode.
- * Default is the map mode (mode 1)
- */
-
- /* jshint esversion: 8 */
-
- (function() {
- function DataStore(uuid, defaultStorage = {}) {
- if (typeof uuid !== 'string' && typeof uuid !== 'number') {
- throw new Error('Expected uuid when creating DataStore');
- }
-
- let cachedStorage = defaultStorage;
-
- try {
- cachedStorage = JSON.parse(GM_getValue(uuid));
- } catch (err) {
- GM_setValue(uuid, JSON.stringify(defaultStorage));
- }
-
- const getter = (obj, prop) => cachedStorage[prop];
-
- const setter = (obj, prop, val) => {
- cachedStorage[prop] = val;
-
- GM_setValue(uuid, JSON.stringify(cachedStorage));
-
- return val;
- }
-
- return new Proxy({}, { get: getter, set: setter });
- }
-
- const MODE = {
- map: 1,
- variable: 2,
- };
-
- const DEFAULT_FPS_CAP = 5;
- const DEFAULT_MODE = MODE.map;
-
- const s = DataStore('storage', {
- fpsCap: DEFAULT_FPS_CAP,
- isFirstRun: true,
- mode: DEFAULT_MODE,
- });
-
- const oldRequestAnimationFrame = window.requestAnimationFrame;
- const msPrevMap = new Map();
- const menuCommandsIds = [];
- let msPerFrame = 1000 / s.fpsCap;
- let msPrev = 0;
-
- unsafeWindow.requestAnimationFrame = function newRequestAnimationFrame(cb, el) {
- return oldRequestAnimationFrame((now) => {
- const msPassed = now - ((s.mode === MODE.map ? msPrevMap.get(cb) : msPrev) || 0);
-
- if (msPassed < msPerFrame) return newRequestAnimationFrame(cb, el);
-
- if (s.mode === MODE.variable) {
- msPrev = now - (msPassed % msPerFrame); // subtract excess time
- } else {
- msPrevMap.set(cb, now - (msPassed % msPerFrame));
- }
-
- return cb(now);
- }, el);
- }
-
- // mode 1 garbage collector. 50 is random number
- setInterval(() => (msPrevMap.size > 50) && msPrevMap.clear(), 1000);
-
- function changeFpsCapWithUser() {
- const userInput = prompt(
- `Current fps cap: ${s.fpsCap}. ` +
- 'What should be the new one? Leave empty or cancel to not to change'
- );
-
- if (userInput !== null && userInput !== '') {
- let userInputNum = Number(userInput);
-
- if (isNaN(userInputNum)) {
- messageUser('bad input', 'Seems like the input is not number');
- } else if (userInputNum > 9999) {
- s.fpsCap = 9999;
- messageUser(
- 'bad input',
- 'Seems like the input is way too big number. Decreasing it to 9999',
- );
- } else if (userInputNum < 0) {
- messageUser(
- 'bad input',
- "The input number can't be negative",
- );
- } else {
- s.fpsCap = userInputNum;
- }
-
- msPerFrame = 1000 / s.fpsCap;
-
- // can't be applied in iframes
- messageUser(
- `the fps cap was set to ${s.fpsCap}`,
- "For some places the fps cap change can't be applied without a reload, " +
- "and if you can't tell worked it out or not, better to refresh the page",
- );
-
- unregisterMenuCommands();
- registerMenuCommands();
- }
- }
-
- function messageUser(title, text) {
- alert(`Fps limiter: ${title}.\n\n${text}`);
- }
-
- function registerMenuCommands() {
- menuCommandsIds.push(GM_registerMenuCommand(
- `Cap fps (${s.fpsCap} now)`, () => changeFpsCapWithUser(), 'c'
- ));
-
- menuCommandsIds.push(GM_registerMenuCommand(
- `Switch mode to ${s.mode === MODE.map ? MODE.variable : MODE.map}`, () => {
- s.mode = s.mode === MODE.map ? MODE.variable : MODE.map;
-
- // can't be applied in iframes
- messageUser(
- `the mode was set to ${s.mode}`,
- "For some places the mode change can't be applied without a reload, " +
- "and if you can't tell worked it out or not, better to refresh the page. " +
- "You can find description of the modes at the script download page",
- );
-
- unregisterMenuCommands();
- registerMenuCommands();
- }, 'm'
- ));
- }
-
- function unregisterMenuCommands() {
- for (const id of menuCommandsIds) {
- GM_unregisterMenuCommand(id);
- }
-
- menuCommandsIds.length = 0;
- }
-
- registerMenuCommands();
-
- if (s.isFirstRun) {
- messageUser(
- 'it seems like your first run of this script',
- 'You need to refresh the page on which this script should work. ' +
- `What fps cap do you prefer? Default is ${DEFAULT_FPS_CAP} as a demonstration. ` +
- 'You can always quickly change it from your script manager icon ↗'
- );
-
- changeFpsCapWithUser();
- s.isFirstRun = false;
- }
- })();