Created
November 1, 2025 08:29
-
-
Save wpeasy/426374414a0ed7e95ea79e0f62144e3a to your computer and use it in GitHub Desktop.
Basic JavaScript SCRUB
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
| /* | |
| Add class .ab-vp-tracker to elements you want to track | |
| [data-track-side="top|center|bottom|ends"] - part of element to track | |
| [data-track-top-offset="px|%"] pixels or % from the top of VP to track | |
| [data-track-bottom-offset="px|%"] pixels or % from the bottom of VP to track | |
| [data-invert] Scrubs outside of the tracked window from vp top and bottom to offsets. | |
| */ | |
| (() => { | |
| const parseOffset = (offset, vh) => | |
| offset?.endsWith('%') ? (parseFloat(offset) / 100) * vh : parseFloat(offset || 0); | |
| const getTargetPoint = (rect, side, vh) => { | |
| switch (side) { | |
| case 'top': | |
| return rect.top; | |
| case 'center': | |
| return rect.top + rect.height / 2; | |
| case 'bottom': | |
| return rect.bottom; | |
| case 'ends': { | |
| const center = rect.top + rect.height / 2; | |
| return center < vh / 2 ? rect.bottom : rect.top; | |
| } | |
| default: | |
| return rect.top; | |
| } | |
| }; | |
| const calculateScrub = (point, topOffset, bottomOffset, vh, inverted = false) => { | |
| if (inverted) { | |
| if (point >= topOffset && point <= bottomOffset) return 1; | |
| const isAbove = point < topOffset; | |
| const distFromEdge = isAbove | |
| ? topOffset - point | |
| : point - bottomOffset; | |
| const maxDist = isAbove | |
| ? topOffset | |
| : vh - bottomOffset; | |
| return 1 - Math.min(distFromEdge / maxDist, 1); | |
| } else { | |
| if (point >= topOffset && point <= bottomOffset) return 1; | |
| const range = bottomOffset - topOffset; | |
| const distance = point < topOffset | |
| ? topOffset - point | |
| : point - bottomOffset; | |
| return Math.max(0, 1 - distance / range); | |
| } | |
| }; | |
| const updateVisibility = () => { | |
| const vh = window.innerHeight; | |
| const trackers = [...document.querySelectorAll('.ab-vp-tracker')]; | |
| trackers.forEach(el => { | |
| const rect = el.getBoundingClientRect(); | |
| const partiallyVisible = rect.bottom > 0 && rect.top < vh; | |
| const side = el.dataset.trackSide || 'top'; | |
| const topOffset = parseOffset(el.dataset.trackTopOffset, vh); | |
| const bottomOffset = vh - parseOffset(el.dataset.trackBottomOffset, vh); | |
| const point = getTargetPoint(rect, side, vh); | |
| el.style.setProperty('--ab-scroll-position', point.toFixed(2)); | |
| const isInverted = el.hasAttribute('data-invert'); | |
| const scrub = calculateScrub(point, topOffset, bottomOffset, vh, isInverted); | |
| // Apply data-outside-* in both modes | |
| if (point < topOffset) { | |
| el.setAttribute('data-outside-top', ''); | |
| el.removeAttribute('data-outside-bottom'); | |
| } else if (point > bottomOffset) { | |
| el.setAttribute('data-outside-bottom', ''); | |
| el.removeAttribute('data-outside-top'); | |
| } else { | |
| el.removeAttribute('data-outside-top'); | |
| el.removeAttribute('data-outside-bottom'); | |
| } | |
| const scrubNeg = 1 - scrub; | |
| el.style.setProperty('--ab-scrub', scrub.toFixed(4)); | |
| el.style.setProperty('--ab-scrub-negative', scrubNeg.toFixed(4)); | |
| const children = el.children; | |
| for (let i = 0; i < children.length; i++) { | |
| const child = children[i]; | |
| child.style.setProperty('--ab-scroll-position', point.toFixed(2)); | |
| child.style.setProperty('--ab-scrub', scrub.toFixed(4)); | |
| child.style.setProperty('--ab-scrub-negative', scrubNeg.toFixed(4)); | |
| } | |
| if (partiallyVisible) { | |
| el.setAttribute('data-in-vp', ''); | |
| } else { | |
| el.removeAttribute('data-in-vp'); | |
| } | |
| const minOffset = Math.min(topOffset, bottomOffset); | |
| const maxOffset = Math.max(topOffset, bottomOffset); | |
| if (point >= minOffset && point <= maxOffset) { | |
| el.setAttribute('data-in-bounds', ''); | |
| } else { | |
| el.removeAttribute('data-in-bounds'); | |
| } | |
| }); | |
| }; | |
| window.addEventListener('scroll', updateVisibility, { passive: true }); | |
| window.addEventListener('resize', updateVisibility); | |
| document.addEventListener('DOMContentLoaded', updateVisibility); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment