Last active
November 4, 2024 04:58
-
-
Save emvaized/2cc80eed27bce3ea66bbd2657b28f693 to your computer and use it in GitHub Desktop.
Userscript to scroll page on double tap (for phones). 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Scroll page on double tap (mobile) | |
// @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. | |
// @description:ru Этот скрипт разработан для мобильных браузеров, и прокручивает страницу при двойном нажатии. Верхняя половина экрана прокручивает вверх, а нижняя половина — вниз. Двойное нажатие и движение пальцем включает режим быстрой прокрутки. Когда страница уже прокручивается, одиночный тап прокрутит её дальше. | |
// @version 1.0.2 | |
// @author emvaized | |
// @license MIT | |
// @namespace scroll_page_on_double_tap | |
// @match *://*/* | |
// @grant none | |
// @run-at document-start | |
// ==/UserScript== | |
/* *** Changelog | |
1.0.2 | |
- switched to screenY instead of clientY to determine screen halves | |
- reordered config variables for better relevance | |
1.0.1 | |
- implemented fling (kinetic) scrolling | |
- make preventing first tap toggleable, and disable by default | |
- implemented fast scrolling on double tap + move (enabled by default) | |
1.0.0 | |
Initial release | |
*** */ | |
(function() { | |
'use strict'; | |
// Configs | |
const fastScrollingEnabled = true; // To enter fast scrolling mode, double tap and move finger on a second tap | |
const fastScrollingFriction = 0.93; // Multiplier for dy touch movement during fast scrolling | |
const scrollVelocity = 13.5; // Initial speed of the scroll (higher values will make it faster) | |
const scrollDecay = 0.98; // The rate at which the scroll slows down (values closer to 1 will make the scroll slower to decelerate) | |
const scrollInterval = 8; // Time in milliseconds between each scroll step (16 ms gives approximately 60 frames per second) | |
const doubleTapTimeout = 200; // Timeout for second tap in milliseconds | |
const maxTapMovement = 10; // Maximum touch movement (in pixels) to qualify as a tap | |
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 | |
// Service variables | |
let lastTapUpTime = 0; // To track timing between taps | |
let firstTapEvent = null; // To store the first tap event details | |
let startX = 0; // Start position for touch | |
let startY = 0; | |
let isFastScrolling = false; // Manual scroll mode on double tap + move | |
let lastMoveY; // Last Y position for manual scrolling | |
let doubleTapDownTimeout; | |
let lastTapDownTime = 0; // To track timing between taps | |
let preventFlingScrolling = false; // Use to stop fling scroll when user manually scrolled page | |
let isFlingScrolling = false; // To track if page is currently in kinetic scrolling state | |
document.addEventListener('touchstart', function(event) { | |
// Only handle single-finger touches | |
if (event.touches.length === 1) { | |
startX = event.touches[0].clientX; | |
startY = event.touches[0].clientY; | |
// Reset variables from last taps | |
lastMoveY = null; | |
preventFlingScrolling = false; | |
// Detect double tap for manual scrolling | |
const currentTapDownTime = new Date().getTime(); | |
const tapDownInterval = currentTapDownTime - lastTapDownTime; | |
if (fastScrollingEnabled && tapDownInterval < doubleTapTimeout && tapDownInterval > 0) { | |
// Double-tap detected within timeout | |
cancelEvent(event); | |
// Enable manual scrolling mode | |
doubleTapDownTimeout = setTimeout(() => { | |
isFastScrolling = true; | |
}, doubleTapTimeout); | |
} else { | |
// First tap: store the event and set timeout for single-tap action | |
lastTapDownTime = currentTapDownTime; | |
isFastScrolling = false; | |
if (preventFirstTap) cancelEvent(event); | |
} | |
} | |
}, { passive: fastScrollingEnabled || preventFirstTap ? false : true}, true); | |
if (fastScrollingEnabled) | |
window.addEventListener('touchmove', function(event){ | |
if ((isFastScrolling || isFlingScrolling) && event.touches.length === 1) { | |
const currentMoveY = event.touches[0].clientY; | |
if(!lastMoveY) lastMoveY = currentMoveY; | |
const scrollDelta = lastMoveY - currentMoveY; | |
const scrollDeltaAbs = Math.abs(scrollDelta); | |
if(isFastScrolling){ | |
// Prevent default action (scrolling page) | |
cancelEvent(event); | |
lastMoveY = currentMoveY; | |
if(scrollDeltaAbs > 0.3) | |
flingScroll( | |
window, | |
(fastScrollingFriction * scrollDelta) * 2, | |
scrollDecay, scrollInterval | |
); | |
} else if(isFlingScrolling && scrollDeltaAbs > 0.3) { | |
isFlingScrolling = false; | |
preventFlingScrolling = true; | |
} | |
} | |
}, { passive: false }, true) | |
document.addEventListener('touchend', function(event) { | |
// Only proceed if it’s a single-finger touch event | |
if (event.changedTouches.length > 1) return; | |
const endX = event.changedTouches[0].clientX; | |
const endY = event.changedTouches[0].clientY; | |
const deltaX = Math.abs(endX - startX); | |
const deltaY = Math.abs(endY - startY); | |
// If movement exceeds maxTapMovement, consider it a scroll/swipe and ignore | |
if (deltaX > maxTapMovement || deltaY > maxTapMovement) return; | |
const currentTime = new Date().getTime(); | |
const tapInterval = currentTime - lastTapUpTime; | |
// Disable manual scrolling mode | |
isFastScrolling = false; | |
clearTimeout(doubleTapDownTimeout); | |
// Scroll up if tapped in top half of the screen, scroll down if tapped in bottom half | |
const scrollDown = event.changedTouches[0].screenY > (window.screen.height / 2); | |
if (tapInterval < doubleTapTimeout && tapInterval > 0) { | |
// Double-tap detected within timeout | |
// Prevent default action (including link navigation) | |
cancelEvent(event); | |
// Execute custom double-tap action: scroll down | |
scrollPage(scrollDown); | |
// Reset stored event and timing | |
lastTapUpTime = 0; | |
firstTapEvent = null; | |
} else { | |
// First tap | |
// If kinetic scrolling in proccess, scroll again | |
if(isFlingScrolling){ | |
cancelEvent(event); | |
scrollPage(scrollDown); | |
return; | |
} | |
// Store the event and set timeout for single-tap action | |
firstTapEvent = event; | |
lastTapUpTime = currentTime; | |
setTimeout(() => { | |
// If no second tap, execute single-tap action | |
if (preventFirstTap && firstTapEvent && !isFlingScrolling && !isFastScrolling) { | |
if(isFlingScrolling) return; | |
// Prevent default action | |
event.preventDefault(); | |
event.stopPropagation(); | |
// Create and dispatch a manual click event | |
const singleClickEvent = new MouseEvent('click', { | |
bubbles: true, | |
cancelable: true, | |
button: 0, | |
detail: 1, | |
view: window, | |
}); | |
firstTapEvent.target.dispatchEvent(singleClickEvent); | |
} | |
firstTapEvent = null; | |
}, doubleTapTimeout); | |
} | |
}, { passive: false }, true); | |
// Prevent regular scrolling when manual scroll is in progress | |
if (fastScrollingEnabled){ | |
document.addEventListener('wheel', function(e) { | |
if(isFastScrolling) cancelEvent(event); | |
}, { passive: false }); | |
document.addEventListener('scroll', function(e) { | |
if(isFastScrolling) cancelEvent(event); | |
}, { passive: false }); | |
} | |
// Scroll page up or down | |
function scrollPage(scrollDown){ | |
flingScroll( | |
document.scrollingElement, | |
(scrollDown ? 1 : -1) * | |
scrollVelocity / window.visualViewport.scale, | |
scrollDecay, | |
scrollInterval | |
) | |
} | |
// Prevent default event action | |
function cancelEvent(event){ | |
event.preventDefault(); | |
event.stopPropagation(); | |
// event.stopImmediatePropagation(); | |
} | |
// For fling scroll (kinetic scrolling) | |
function flingScroll(element, velocity = 13.5, decay = 0.98, interval = 8) { | |
let currentVelocity = velocity; // initial velocity of the fling scroll | |
function scrollStep() { | |
isFlingScrolling = true; | |
if(preventFlingScrolling && !isFastScrolling) { | |
isFlingScrolling = false; | |
return; | |
} | |
// Scroll the element by the current velocity | |
element.scrollBy({ | |
left: 0, | |
top: currentVelocity, | |
behavior: "instant" | |
}); | |
// Reduce the velocity to simulate natural slowing down | |
currentVelocity *= decay; | |
// Stop scrolling if the velocity is very low | |
if (Math.abs(currentVelocity) > 1) { | |
setTimeout(scrollStep, interval); | |
} else { | |
isFlingScrolling = false; | |
} | |
} | |
// Start the fling scroll | |
scrollStep(); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment