HTML canvas fps limiter

Fps limiter for browser games or some 2D/3D animations

目前为 2023-05-25 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name HTML canvas fps limiter
  3. // @description Fps limiter for browser games or some 2D/3D animations
  4. // @author Konf
  5. // @namespace https://greasyfork.org/users/424058
  6. // @icon https://img.icons8.com/external-neu-royyan-wijaya/32/external-animation-neu-solid-neu-royyan-wijaya.png
  7. // @icon64 https://img.icons8.com/external-neu-royyan-wijaya/64/external-animation-neu-solid-neu-royyan-wijaya.png
  8. // @version 1.0.0
  9. // @match *://*/*
  10. // @compatible Chrome
  11. // @compatible Opera
  12. // @compatible Firefox
  13. // @run-at document-start
  14. // @grant unsafeWindow
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_unregisterMenuCommand
  19. // ==/UserScript==
  20.  
  21. /*
  22. * Implementation is kinda rough, but it seems working, so I don't care anymore
  23. *
  24. * A huge part is inspired (stolen) from:
  25. * https://chriscourses.com/blog/standardize-your-javascript-games-framerate-for-different-monitors
  26. *
  27. * msPrevMap is needed to provide individual rate limiting in cases
  28. * where requestAnimationFrame is used by more than one function loop.
  29. * Using a variable instead of a map in such cases makes so the only one
  30. * random loop will be working, and the others will not be working at all.
  31. * But if some loop is using anonymous functions, the map mode can't limit it,
  32. * so I've decided to make a switcher: the map mode or the single variable mode.
  33. * Default is the map mode (mode 1)
  34. */
  35.  
  36. /* jshint esversion: 8 */
  37.  
  38. (function() {
  39. function DataStore(uuid, defaultStorage = {}) {
  40. if (typeof uuid !== 'string' && typeof uuid !== 'number') {
  41. throw new Error('Expected uuid when creating DataStore');
  42. }
  43.  
  44. let cachedStorage = defaultStorage;
  45.  
  46. try {
  47. cachedStorage = JSON.parse(GM_getValue(uuid));
  48. } catch (err) {
  49. GM_setValue(uuid, JSON.stringify(defaultStorage));
  50. }
  51.  
  52. const getter = (obj, prop) => cachedStorage[prop];
  53.  
  54. const setter = (obj, prop, val) => {
  55. cachedStorage[prop] = val;
  56.  
  57. GM_setValue(uuid, JSON.stringify(cachedStorage));
  58.  
  59. return val;
  60. }
  61.  
  62. return new Proxy({}, { get: getter, set: setter });
  63. }
  64.  
  65. const MODE = {
  66. map: 1,
  67. variable: 2,
  68. };
  69.  
  70. const DEFAULT_FPS_CAP = 5;
  71. const DEFAULT_MODE = MODE.map;
  72.  
  73. const s = DataStore('storage', {
  74. fpsCap: DEFAULT_FPS_CAP,
  75. isFirstRun: true,
  76. mode: DEFAULT_MODE,
  77. });
  78.  
  79. const oldRequestAnimationFrame = window.requestAnimationFrame;
  80. const msPrevMap = new Map();
  81. const menuCommandsIds = [];
  82. let msPerFrame = 1000 / s.fpsCap;
  83. let msPrev = 0;
  84.  
  85. unsafeWindow.requestAnimationFrame = function newRequestAnimationFrame(cb, el) {
  86. return oldRequestAnimationFrame((now) => {
  87. const msPassed = now - ((s.mode === MODE.map ? msPrevMap.get(cb) : msPrev) || 0);
  88.  
  89. if (msPassed < msPerFrame) return newRequestAnimationFrame(cb, el);
  90.  
  91. if (s.mode === MODE.variable) {
  92. msPrev = now - (msPassed % msPerFrame); // subtract excess time
  93. } else {
  94. msPrevMap.set(cb, now - (msPassed % msPerFrame));
  95. }
  96.  
  97. return cb(now);
  98. }, el);
  99. }
  100.  
  101. // mode 1 garbage collector. 50 is random number
  102. setInterval(() => (msPrevMap.size > 50) && msPrevMap.clear(), 1000);
  103.  
  104. function changeFpsCapWithUser() {
  105. const userInput = prompt(
  106. `Current fps cap: ${s.fpsCap}. ` +
  107. 'What should be the new one? Leave empty or cancel to not to change'
  108. );
  109.  
  110. if (userInput !== null && userInput !== '') {
  111. let userInputNum = Number(userInput);
  112.  
  113. if (isNaN(userInputNum)) {
  114. messageUser('bad input', 'Seems like the input is not number');
  115. } else if (userInputNum > 9999) {
  116. s.fpsCap = 9999;
  117. messageUser(
  118. 'bad input',
  119. 'Seems like the input is way too big number. Decreasing it to 9999',
  120. );
  121. } else if (userInputNum < 0) {
  122. messageUser(
  123. 'bad input',
  124. "The input number can't be negative",
  125. );
  126. } else {
  127. s.fpsCap = userInputNum;
  128. }
  129.  
  130. msPerFrame = 1000 / s.fpsCap;
  131.  
  132. // can't be applied in iframes
  133. messageUser(
  134. `the fps cap was set to ${s.fpsCap}`,
  135. "For some places the fps cap change can't be applied without a reload, " +
  136. "and if you can't tell worked it out or not, better to refresh the page",
  137. );
  138.  
  139. unregisterMenuCommands();
  140. registerMenuCommands();
  141. }
  142. }
  143.  
  144. function messageUser(title, text) {
  145. alert(`Fps limiter: ${title}.\n\n${text}`);
  146. }
  147.  
  148. function registerMenuCommands() {
  149. menuCommandsIds.push(GM_registerMenuCommand(
  150. `Cap fps (${s.fpsCap} now)`, () => changeFpsCapWithUser(), 'c'
  151. ));
  152.  
  153. menuCommandsIds.push(GM_registerMenuCommand(
  154. `Switch mode to ${s.mode === MODE.map ? MODE.variable : MODE.map}`, () => {
  155. s.mode = s.mode === MODE.map ? MODE.variable : MODE.map;
  156.  
  157. // can't be applied in iframes
  158. messageUser(
  159. `the mode was set to ${s.mode}`,
  160. "For some places the mode change can't be applied without a reload, " +
  161. "and if you can't tell worked it out or not, better to refresh the page. " +
  162. "You can find description of the modes at the script download page",
  163. );
  164.  
  165. unregisterMenuCommands();
  166. registerMenuCommands();
  167. }, 'm'
  168. ));
  169. }
  170.  
  171. function unregisterMenuCommands() {
  172. for (const id of menuCommandsIds) {
  173. GM_unregisterMenuCommand(id);
  174. }
  175.  
  176. menuCommandsIds.length = 0;
  177. }
  178.  
  179. registerMenuCommands();
  180.  
  181. if (s.isFirstRun) {
  182. messageUser(
  183. 'it seems like your first run of this script',
  184. 'You need to refresh the page on which this script should work. ' +
  185. `What fps cap do you prefer? Default is ${DEFAULT_FPS_CAP} as a demonstration. ` +
  186. 'You can always quickly change it from your script manager icon ↗'
  187. );
  188.  
  189. changeFpsCapWithUser();
  190. s.isFirstRun = false;
  191. }
  192. })();