Scroll down/up by aligning images
// ==UserScript==
// @name Next Image/Previous Image with Alignment and Large Image Support
// @author rekt
// @license MIT
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Scroll down/up by aligning images
// @include *
// @grant unsafeWindow
// ==/UserScript==
(function() {
const KEY_W = "w";
const KEY_S = "s";
let positions = [];
function findNonTruncatedPosition(initialPosition, viewportHeight) {
let currentPosition = initialPosition;
let found = false;
while (!found) {
found = true;
for (let i = 0; i < positions.length; i++) {
const [, imgTop, imgBottom] = positions[i];
if (imgTop < currentPosition && imgBottom > currentPosition) {
currentPosition = imgTop;
found = false;
break;
}
}
}
return currentPosition;
}
function scrollToNextImage(currentScroll, viewportHeight) {
const nextScrollPosition = currentScroll + viewportHeight;
const currentBottomEdge = currentScroll + viewportHeight;
// First check for large images that extend beyond viewport
for (let i = 0; i < positions.length; i++) {
if (positions[i][1] <= currentScroll && positions[i][2] > currentBottomEdge) {
// Large image case: find next image after this one
for (let j = i + 1; j < positions.length; j++) {
if (positions[j][1] > positions[i][1]) {
return findNonTruncatedPosition(positions[j][1], viewportHeight);
}
}
// If no next image found, scroll by viewport height
return findNonTruncatedPosition(nextScrollPosition, viewportHeight);
}
}
// Normal case: handle truncated images
for (let i = 0; i < positions.length; i++) {
if (positions[i][2] > currentBottomEdge && positions[i][1] < nextScrollPosition) {
return findNonTruncatedPosition(positions[i][1], viewportHeight);
}
}
return findNonTruncatedPosition(nextScrollPosition, viewportHeight);
}
function scrollToPreviousImage(currentScroll, viewportHeight) {
const previousScrollPosition = currentScroll - viewportHeight;
const newTopCandidate = Math.max(0, previousScrollPosition);
for (let i = positions.length - 1; i >= 0; i--) {
if (positions[i][1] < currentScroll && positions[i][2] > newTopCandidate) {
return findNonTruncatedPosition(positions[i][1], viewportHeight);
}
}
return findNonTruncatedPosition(newTopCandidate, viewportHeight);
}
function scrollShiftUp(currentScroll, viewportHeight) {
const prevViewportTop = Math.max(currentScroll - viewportHeight, 0);
let newScroll = prevViewportTop;
for (let i = positions.length - 1; i >= 0; i--) {
const imgTop = positions[i][1];
if (imgTop >= prevViewportTop && imgTop <= prevViewportTop + 20) {
newScroll = imgTop;
break;
}
if (imgTop < prevViewportTop && imgTop >= prevViewportTop - 100) {
newScroll = imgTop;
break;
}
}
return findNonTruncatedPosition(newScroll, viewportHeight);
}
function getYOffset(node) {
let offset = 0;
while (node) {
offset += node.offsetTop;
node = node.offsetParent;
}
return offset;
}
document.addEventListener("keydown", function(event) {
if (event.ctrlKey || event.altKey) return;
const tagName = event.target.tagName;
const contentEditable = event.target.getAttribute("contenteditable");
if ((tagName && tagName.match(/input|select|textarea/i)) || contentEditable === "true") {
return;
}
positions = [];
const allImages = document.images;
for (let idx = 0; idx < allImages.length; idx++) {
const img = allImages[idx];
if (img.width * img.height < 80 * 80) continue;
const ytop = getYOffset(img);
const ybottom = ytop + img.height;
positions.push([idx, ytop, ybottom]);
}
positions.sort((a, b) => a[1] - b[1]);
const currentScroll = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
const viewportHeight = window.innerHeight;
let newScrollPosition = currentScroll;
const key = event.key;
const lowerKey = key.toLowerCase();
if (lowerKey === KEY_S && !event.shiftKey) {
newScrollPosition = scrollToNextImage(currentScroll, viewportHeight);
} else if (lowerKey === KEY_W && !event.shiftKey) {
newScrollPosition = scrollToPreviousImage(currentScroll, viewportHeight);
} else if (lowerKey === KEY_W && event.shiftKey) {
newScrollPosition = scrollShiftUp(currentScroll, viewportHeight);
} else {
return;
}
if (newScrollPosition !== currentScroll) {
event.preventDefault();
document.body.scrollTop = newScrollPosition;
document.documentElement.scrollTop = newScrollPosition;
}
});
})();