2

I am trying to debug some code I created for a webpage that auto scrolls to a video on the page once the video starts to come into view of the viewport. The code works but if you scroll too quickly, the code that autoscrolls the video overshoots the video because of the "inertia" of the scroll. I want to make it so that if the page is scrolling from inertia when the code grabs it and starts scrolling to the video, it will cancel out the inertia of the page so that it stops once the video is centered on the screen. Here's a minimal code example that shows the problem I'm having:

/////////////////////////////
// Main scroll event listener
//////////////////////////////

window.addEventListener("scroll", () => {
    const videoScrollSpeed = 1000;                  // Speed of the overall scroll effect in ms
    const videoScrollTriggerPercentage = 25;        // How far the video placeholder is into the viewport before the scrolling effect is triggered
    const video = "video";                          // Get video reference
    
    // Trigger the scrollToElement when the video placeholder is 25% within the viewport and call the inline callback function
    // (This function works correctly)
    scrollTriggeredCallback(video, videoScrollTriggerPercentage, function(trigger, triggerElement){
        // This is the inline callback code that is called once the trigger event takes place
        if (trigger === true) {
            // Scrolls the document to center it on the given element
            // (this is where the issues are)
            scrollToElement(triggerElement, videoScrollSpeed, function(placeholder) {
                // This is the inline callback code that is triggered once the scroll animation finishes
                console.log ("Scroll complete");
            });
        }
    });
});


/////////////////////////////
// Helper Functions
//////////////////////////////

/**
 * Scrolls the given element into the center of the viewport using the given duration and easing curve. Once the animation completes, the optional callback function can be called
 * @param {Obj}    element        The element to scroll into view
 * @param {int}    duration       Time (in ms) for the animation to run
 * @param {func}   callback       (Optional) Callback function to call after scrolling completes
 */
function scrollToElement(element, duration, callback) {
    const elementRect = element.getBoundingClientRect();

    let doc = document.documentElement;
    if(doc.scrollTop === 0){
        var t = doc.scrollTop;
        ++doc.scrollTop;
        doc = (t + 1 === doc.scrollTop-- ? doc : document.body);
    }

    const startPosition = doc.scrollTop;
    const absoluteElementTop = elementRect.top + window.pageYOffset;
    const middle = absoluteElementTop - (window.innerHeight / 2) + (elementRect.height / 2);

    let startTime, previousTimeStamp;
    function stepScroll(timestamp) {
        if (startTime === undefined) {
            startTime = timestamp;
        }

        const elapsed = timestamp - startTime;
    
        if (previousTimeStamp !== timestamp) {
            // Calculate the current scroll position between startPosition and middle of the element to show based on the time elapsed
            doc.scrollTop = incrementOverRange(startTime + elapsed, startTime, startTime + duration, startPosition, middle); 
        }
    
        // Stop the animation after the duration has elapsed
        if (elapsed < duration) {
            previousTimeStamp = timestamp;
            window.requestAnimationFrame(stepScroll);
        } else {
            if (callback) {
                callback(element);
            }
        }
    }

    // Begin the animation
    window.requestAnimationFrame(stepScroll);
}

/**
 * Returns the X and Y offsets of an element due to it being shifted using the CSS Transform translate method
 * @param {*} element   Element reference (CSS selector string or DOMObject)
 * @returns             An array containing the X and Y offset coordinates
 */
function getCSSTranslationOffset(element) {
    element = getDOMObject(element);
    const computedStyle = window.getComputedStyle(element);
    const transformMatrix = new DOMMatrix(computedStyle.transform);
    const translationX = transformMatrix.m41;
    const translationY = transformMatrix.m42;

    return [transformMatrix.m41, transformMatrix.m42]
}

/**
* Returns a value within a custom range based on the input percent scrolled value
* @param {*} percentValue       Value to be transformed from the start/end percent range to the min/max value range
* @param {*} startPercent      Starting percentage value to begin incrementing the value range
* @param {*} endPercent        Ending percentage value to end incrementing the value range
* @param {*} minValue          Starting value of the value range
* @param {*} maxValue          Ending value of the value range
* @returns                     The corresponding value within the value range
*/
function incrementOverRange(percentValue, startPercent, endPercent, minValue, maxValue) {
    // Limit input range to start/end Percent range
    if (percentValue < startPercent)
        percentValue = startPercent;
    else if (percentValue > endPercent)
        percentValue = endPercent;

    // NOTE: Formula borrowed from Arduino map() function
    return ((percentValue - startPercent) * (maxValue - minValue) / (endPercent - startPercent) + minValue);
}

/**
* Triggers a callback function when the specified element scrolls into range. The callback function is called again once the element scrolls outside of the given range.
*
* Example usage:
*
* scrollTriggeredCallback(".triggerElement", 50, function(triggered) {
*     if (triggered === true)
*         // Do something here 
*     else
*         // Do something else here
* });
*
* NOTE: This version of the function uses the viewport to calculate the scroll position of the element within the screen. 
*       Scroll position = 0% when the element's top boundary is at the bottom of the viewport
*       Scroll position = 100% when the element's bottom boundary is at the top of the viewport
*
* @param {*} triggerElement     The element to trigger the callback on
* @param {*} percentage         Percentage value to trigger the animation
* @param {*} callback           Function to call (with single parameter specfying whether animation was triggered or not) once animation trigger state changes
*                               If animation is triggered, input parameter is set to true and false otherwise.
*/
function scrollTriggeredCallback(triggerElement, percentage, callback) {
    // Get references to the HTML element and its client rect object to work with
    triggerElement = document.querySelector(triggerElement);
    var triggerElementRect = triggerElement.getBoundingClientRect();

    // Calculate the scroll position of the element within the viewport
    let scrollPosition = triggerElementRect.top + triggerElementRect.height; // Sticky height >= viewport height
    let scrollHeight = window.innerHeight + triggerElementRect.height;
    let percentScrolled = 100 - (scrollPosition / scrollHeight) * 100;

    // Limit the scroll range to 0-100%
    if (percentScrolled > 100)
        percentScrolled = 100;
    else if (percentScrolled < 0)
        percentScrolled = 0;

    // Add the animation CSS selector to the given element if the percentScrolled value is within the given percentage range, and remove it otherwise
    if (percentScrolled >= percentage) {
        if(!triggerElement.classList.contains("triggered")) {
            triggerElement.classList.add("triggered");
            callback(true, triggerElement);
        }
    }
    else if (percentScrolled <= 0) {
        if (triggerElement.classList.contains("triggered")) {
            triggerElement.classList.remove('triggered');
            callback(false, triggerElement);
        }
    }
}
#content {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
}

span {
    display: block;
    height: 40vh;
    width: 100%;
    background: lightgray;
    margin: 10px 0;
}

.video {
    position: relative;
    width: 100%;
    height: calc(100% / 1.77777);
    display: block;
    cursor: pointer;
}
<html>
    <head>
        <link rel="stylesheet" href="style.css">
        <!--<script defer src="../../Templates/Code Library/Animation Code Library.js"></script>-->
        <script defer src="script.js"></script>
    </head>
    <body>
        <div id="content">
            <span>Text</span>
            <span>Text</span>
            <span>Text</span>
            <span>Text</span>

            <video class="video" controls loading="lazy" style="object-fit: cover">
                <source src="https://www.staging7.midstory.org/wp-content/uploads/2022/01/Video-of-green-foliage.mp4" type="video/mp4">
            </video>
            
            <span>Text</span>
            <span>Text</span>
            <span>Text</span>
            <span>Text</span>
        </div>
    </body>
</html>
Mert Ekinci
  • 352
  • 2
  • 13
Jason O
  • 110
  • 6

1 Answers1

0

One more resilient way to accomplish this would be to utilize the IntersectionObserver API, which gives you a callback when an element is visible in the viewport

For example, you could try something like:

const video = document.querySelector("video");

const observer = new IntersectionObserver(entries => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      // play video
    } else {
      // pause video
    }
  }
});

observer.observe(video);
bren
  • 4,176
  • 3
  • 28
  • 43