Scroll page on double tap (mobile)

This userscript is designed for mobile browsers, and scrolls page on double tap. Top half of the screen scrolls up, and bottom half scrolls down. Double tap and move finger enables fast scrolling mode. When page is already scrolling, single tap will scroll it further.

  1. // ==UserScript==
  2. // @name Scroll page on double tap (mobile)
  3. // @description This userscript is designed for mobile browsers, and scrolls page on double tap. Top half of the screen scrolls up, and bottom half scrolls down. Double tap and move finger enables fast scrolling mode. When page is already scrolling, single tap will scroll it further.
  4. // @description:ru Этот скрипт разработан для мобильных браузеров, и прокручивает страницу при двойном нажатии. Верхняя половина экрана прокручивает вверх, а нижняя половина — вниз. Двойное нажатие и движение пальцем включает режим быстрой прокрутки. Когда страница уже прокручивается, одиночный тап прокрутит её дальше.
  5. // @version 1.0.2
  6. // @author emvaized
  7. // @license MIT
  8. // @namespace scroll_page_on_double_tap
  9. // @match *://*/*
  10. // @grant none
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. /* *** Changelog
  15. 1.0.2
  16. - switched to screenY instead of clientY to determine screen halves
  17. - reordered config variables for better relevance
  18.  
  19. 1.0.1
  20. - implemented fling (kinetic) scrolling
  21. - make preventing first tap toggleable, and disable by default
  22. - implemented fast scrolling on double tap + move (enabled by default)
  23.  
  24. 1.0.0
  25. Initial release
  26. *** */
  27.  
  28. (function() {
  29. 'use strict';
  30.  
  31. // Configs
  32. const fastScrollingEnabled = true; // To enter fast scrolling mode, double tap and move finger on a second tap
  33. const fastScrollingFriction = 0.93; // Multiplier for dy touch movement during fast scrolling
  34. const scrollVelocity = 13.5; // Initial speed of the scroll (higher values will make it faster)
  35. const scrollDecay = 0.98; // The rate at which the scroll slows down (values closer to 1 will make the scroll slower to decelerate)
  36. const scrollInterval = 8; // Time in milliseconds between each scroll step (16 ms gives approximately 60 frames per second)
  37. const doubleTapTimeout = 200; // Timeout for second tap in milliseconds
  38. const maxTapMovement = 10; // Maximum touch movement (in pixels) to qualify as a tap
  39. const preventFirstTap = false; // With this flag script will block the first tap on page and wait for the second tap. Allows to double tap links and images to scroll, but causes issues
  40.  
  41. // Service variables
  42. let lastTapUpTime = 0; // To track timing between taps
  43. let firstTapEvent = null; // To store the first tap event details
  44. let startX = 0; // Start position for touch
  45. let startY = 0;
  46. let isFastScrolling = false; // Manual scroll mode on double tap + move
  47. let lastMoveY; // Last Y position for manual scrolling
  48. let doubleTapDownTimeout;
  49. let lastTapDownTime = 0; // To track timing between taps
  50. let preventFlingScrolling = false; // Use to stop fling scroll when user manually scrolled page
  51. let isFlingScrolling = false; // To track if page is currently in kinetic scrolling state
  52.  
  53. document.addEventListener('touchstart', function(event) {
  54. // Only handle single-finger touches
  55. if (event.touches.length === 1) {
  56. startX = event.touches[0].clientX;
  57. startY = event.touches[0].clientY;
  58. // Reset variables from last taps
  59. lastMoveY = null;
  60. preventFlingScrolling = false;
  61.  
  62. // Detect double tap for manual scrolling
  63. const currentTapDownTime = new Date().getTime();
  64. const tapDownInterval = currentTapDownTime - lastTapDownTime;
  65. if (fastScrollingEnabled && tapDownInterval < doubleTapTimeout && tapDownInterval > 0) {
  66. // Double-tap detected within timeout
  67. cancelEvent(event);
  68.  
  69. // Enable manual scrolling mode
  70. doubleTapDownTimeout = setTimeout(() => {
  71. isFastScrolling = true;
  72. }, doubleTapTimeout);
  73. } else {
  74. // First tap: store the event and set timeout for single-tap action
  75. lastTapDownTime = currentTapDownTime;
  76. isFastScrolling = false;
  77. if (preventFirstTap) cancelEvent(event);
  78. }
  79. }
  80. }, { passive: fastScrollingEnabled || preventFirstTap ? false : true}, true);
  81.  
  82. if (fastScrollingEnabled)
  83. window.addEventListener('touchmove', function(event){
  84. if ((isFastScrolling || isFlingScrolling) && event.touches.length === 1) {
  85. const currentMoveY = event.touches[0].clientY;
  86. if(!lastMoveY) lastMoveY = currentMoveY;
  87. const scrollDelta = lastMoveY - currentMoveY;
  88. const scrollDeltaAbs = Math.abs(scrollDelta);
  89. if(isFastScrolling){
  90. // Prevent default action (scrolling page)
  91. cancelEvent(event);
  92. lastMoveY = currentMoveY;
  93. if(scrollDeltaAbs > 0.3)
  94. flingScroll(
  95. window,
  96. (fastScrollingFriction * scrollDelta) * 2,
  97. scrollDecay, scrollInterval
  98. );
  99.  
  100. } else if(isFlingScrolling && scrollDeltaAbs > 0.3) {
  101. isFlingScrolling = false;
  102. preventFlingScrolling = true;
  103. }
  104. }
  105. }, { passive: false }, true)
  106.  
  107. document.addEventListener('touchend', function(event) {
  108. // Only proceed if it’s a single-finger touch event
  109. if (event.changedTouches.length > 1) return;
  110.  
  111. const endX = event.changedTouches[0].clientX;
  112. const endY = event.changedTouches[0].clientY;
  113. const deltaX = Math.abs(endX - startX);
  114. const deltaY = Math.abs(endY - startY);
  115.  
  116. // If movement exceeds maxTapMovement, consider it a scroll/swipe and ignore
  117. if (deltaX > maxTapMovement || deltaY > maxTapMovement) return;
  118.  
  119. const currentTime = new Date().getTime();
  120. const tapInterval = currentTime - lastTapUpTime;
  121.  
  122. // Disable manual scrolling mode
  123. isFastScrolling = false;
  124. clearTimeout(doubleTapDownTimeout);
  125.  
  126. // Scroll up if tapped in top half of the screen, scroll down if tapped in bottom half
  127. const scrollDown = event.changedTouches[0].screenY > (window.screen.height / 2);
  128.  
  129. if (tapInterval < doubleTapTimeout && tapInterval > 0) {
  130. // Double-tap detected within timeout
  131. // Prevent default action (including link navigation)
  132. cancelEvent(event);
  133. // Execute custom double-tap action: scroll down
  134. scrollPage(scrollDown);
  135.  
  136. // Reset stored event and timing
  137. lastTapUpTime = 0;
  138. firstTapEvent = null;
  139. } else {
  140. // First tap
  141. // If kinetic scrolling in proccess, scroll again
  142. if(isFlingScrolling){
  143. cancelEvent(event);
  144. scrollPage(scrollDown);
  145. return;
  146. }
  147.  
  148. // Store the event and set timeout for single-tap action
  149. firstTapEvent = event;
  150. lastTapUpTime = currentTime;
  151.  
  152. setTimeout(() => {
  153. // If no second tap, execute single-tap action
  154. if (preventFirstTap && firstTapEvent && !isFlingScrolling && !isFastScrolling) {
  155. if(isFlingScrolling) return;
  156.  
  157. // Prevent default action
  158. event.preventDefault();
  159. event.stopPropagation();
  160.  
  161. // Create and dispatch a manual click event
  162. const singleClickEvent = new MouseEvent('click', {
  163. bubbles: true,
  164. cancelable: true,
  165. button: 0,
  166. detail: 1,
  167. view: window,
  168. });
  169. firstTapEvent.target.dispatchEvent(singleClickEvent);
  170. }
  171. firstTapEvent = null;
  172. }, doubleTapTimeout);
  173. }
  174. }, { passive: false }, true);
  175.  
  176. // Prevent regular scrolling when manual scroll is in progress
  177. if (fastScrollingEnabled){
  178. document.addEventListener('wheel', function(e) {
  179. if(isFastScrolling) cancelEvent(event);
  180. }, { passive: false });
  181. document.addEventListener('scroll', function(e) {
  182. if(isFastScrolling) cancelEvent(event);
  183. }, { passive: false });
  184. }
  185. // Scroll page up or down
  186. function scrollPage(scrollDown){
  187. flingScroll(
  188. document.scrollingElement,
  189. (scrollDown ? 1 : -1) *
  190. scrollVelocity / window.visualViewport.scale,
  191. scrollDecay,
  192. scrollInterval
  193. )
  194. }
  195.  
  196. // Prevent default event action
  197. function cancelEvent(event){
  198. event.preventDefault();
  199. event.stopPropagation();
  200. // event.stopImmediatePropagation();
  201. }
  202.  
  203. // For fling scroll (kinetic scrolling)
  204. function flingScroll(element, velocity = 13.5, decay = 0.98, interval = 8) {
  205. let currentVelocity = velocity; // initial velocity of the fling scroll
  206.  
  207. function scrollStep() {
  208. isFlingScrolling = true;
  209. if(preventFlingScrolling && !isFastScrolling) {
  210. isFlingScrolling = false;
  211. return;
  212. }
  213. // Scroll the element by the current velocity
  214. element.scrollBy({
  215. left: 0,
  216. top: currentVelocity,
  217. behavior: "instant"
  218. });
  219.  
  220. // Reduce the velocity to simulate natural slowing down
  221. currentVelocity *= decay;
  222.  
  223. // Stop scrolling if the velocity is very low
  224. if (Math.abs(currentVelocity) > 1) {
  225. setTimeout(scrollStep, interval);
  226. } else {
  227. isFlingScrolling = false;
  228. }
  229. }
  230. // Start the fling scroll
  231. scrollStep();
  232. }
  233. })();