Skip to content

Instantly share code, notes, and snippets.

@emvaized
Last active November 4, 2024 04:58
Show Gist options
  • Save emvaized/2cc80eed27bce3ea66bbd2657b28f693 to your computer and use it in GitHub Desktop.
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.
// ==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