Change element position to relative while scrolling

I created a wrapper with an animation similar to the one on Apple's Airpods Pro page. It features a video that plays gradually as I scroll, with the text smoothly scrolling over it within a specific area (text-display).

So far, this part is working well. Now, my goal is to transition the position of the video-effect-wrapper from fixed to relative once the user reaches the end of the video and the animation completes. This way, the website will continue to scroll its content normally after the video animation.

JSFIDDLE CODE + DEMO

This is what I have attempted:

//If video-animation ended: Make position of video-wrapper relative to continue scrolling
if ($(window).scrollTop() >= $("#video-effect-wrapper").height()) {
    $(video).css("position", "relative");
    $("#video-effect-wrapper .text").css("display", "none");
}

While this somewhat works, it lacks smoothness and does not account for reverse scrolling on the webpage.

Challenges I faced in addressing this issue:

  • The need for natural and smooth scrolling and transitioning from fixed to relative positions
  • The structure of the wrapper with non-fixed .text elements overlapping the fixed video element, complicating the solution

Answer №1

Upon analyzing the Airpods Pro page through reverse engineering, it was observed that the animation utilizes a canvas instead of a video. The process includes:

  • Loading approximately 1500 images via HTTP2, which serve as frames for the animation
  • Constructing an array of images in the form of HTMLImageElement
  • Detecting each scroll DOM event and requesting an animation frame corresponding to the closest image using requestAnimationFrame
  • In the callback function for the animation frame requests, displaying the image with ctx.drawImage (ctx representing the 2d context of the canvas element)

The utilization of the requestAnimationFrame function aids in achieving a smoother effect by deferring and synchronizing frames according to the target screen's "frames per second" rate.

To delve further into the proper display of a frame during a scroll event, refer to: https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event

In relation to the primary issue, a functional solution involves:

  • Creating a placeholder equal in height and width to the video element. This prevents overlap when the video is placed in an absolute position
  • Within the scroll event callback, once the placeholder aligns with the top of the viewport, positioning the video absolutely and setting the correct top value

The concept ensures that the video remains outside the normal flow, only appearing over the placeholder at the appropriate moment while scrolling downwards.

Below is the provided JavaScript code:

//JavaScript code snippet
//Code here...

Here is the HTML implementation:

<div id="video-effect-wrapper">
    <!-- Video and text elements -->
</div>
<div id="video-placeholder">
    <!-- Placeholder for video -->
</div>
<div id="other-parts-of-website">
    <!-- Other content of website -->
</div>

To test the functionality, visit: https://jsfiddle.net/crkj1m0v/3/

Answer №2

To ensure the video remains in place as you scroll back up, it's important to designate where the transition from fixed to relative occurs.

//Locate video element
let video = $("#video-effect-wrapper video").get(0);
video.pause();

let videoLocked = true;
let lockPoint = -1;
const vidHeight = 408;

//Set up video effect wrapper
$(document).ready(function() {

  const videoHeight = $("#video-effect-wrapper").height();

  //If .first text-element is set, position it at the bottom of the
  //text-display
  if ($("#video-effect-wrapper .text.first").length) {
    //Get text-display position properties
    let textDisplay = $("#video-effect-wrapper #text-display");
    let textDisplayPosition = textDisplay.offset().top;
    let textDisplayHeight = textDisplay.height();
    let textDisplayBottom = textDisplayPosition + textDisplayHeight;

    //Get .text.first positions
    let firstText = $("#video-effect-wrapper .text.first");
    let firstTextHeight = firstText.height();
    let startPositionOfFirstText = textDisplayBottom - firstTextHeight + 50;

    //Set start position of .text.first
    firstText.css("margin-top", startPositionOfFirstText);
  }


  //Code to trigger video-effect when user scrolls
  $(document).scroll(function() {

    //Calculate amount of pixels scrolled inside the video-effect-wrapper
    let n = $(window).scrollTop() - $("#video-effect-wrapper").offset().top + vidHeight;
    n = n < 0 ? 0 : n;
    
    // If .text.first is set, calculate one less text-box
    let x = $("#video-effect-wrapper .text.first").length == 0 ? 0 : 1;

    //Calculate percentage of scrolling progress
    let percentage = n / ($(".text").eq(1).outerHeight(true) * ($("#video-effect-wrapper .text").length - x)) * 100;

    //Get video duration
    let duration = video.duration;

    //Calculate which second in the video to skip to
    let skipTo = duration / 100 * percentage;

    //Skip to specified second
    video.currentTime = skipTo;

    //Ensure only text-elements visible within text-display
    let textDisplay = $("#video-effect-wrapper #text-display");
    let textDisplayHeight = textDisplay.height();
    let textDisplayTop = textDisplay.offset().top;
    let textDisplayBottom = textDisplayTop + textDisplayHeight;
    $("#video-effect-wrapper .text").each(function(i) {
      let text = $(this);

      if (text.offset().top < textDisplayBottom && text.offset().top > textDisplayTop) {
        let textProgressPoint = textDisplayTop + (textDisplayHeight / 2);
        let textScrollProgressInPx = Math.abs(text.offset().top - textProgressPoint - textDisplayHeight / 2);
        textScrollProgressInPx = textScrollProgressInPx <= 0 ? 0 : textScrollProgressInPx;
        let textScrollProgressInPerc = textScrollProgressInPx / (textDisplayHeight / 2) * 100;

        if (text.hasClass("first"))
          textScrollProgressInPerc = 100;

        text.css("opacity", textScrollProgressInPerc / 100);
      } else {
        text.css("transition", "0.5s ease");
        text.css("opacity", "0");
      }
    });

    //End video-animation: Adjust video-wrapper position for further scrolling
    if (videoLocked) {
      if ($(window).scrollTop() >= videoHeight) {
        $('video').css("position", "relative");
        videoLocked = false;
        lockPoint = $(window).scrollTop() - 10;
      }
    } else if ($(window).scrollTop() < lockPoint) {
      $('video').css("position", "fixed");
      videoLocked = true;
    }

  });
});
body {
  margin: 0;
  padding: 0;
  background-color: green;
}

#video-effect-wrapper {
  height: auto;
  width: 100%;
}

#video-effect-wrapper video {
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  z-index: -2;
  object-fit: cover;
}

#video-effect-wrapper::after {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: block;
  background: #000000;
  background: linear-gradient(to top, #434343, #000000);
  opacity: 0.4;
  z-index: -1;
}

#video-effect-wrapper .text {
  color: #FFFFFF;
  font-weight: bold;
  font-size: 3em;
  width: 100%;
  margin-top: 50vh;
  font-family: Arial, sans-serif;
  text-align: center;
  opacity: 0;
}

#video-effect-wrapper .text.first {
  margin-top: 50vh;
  opacity: 1;
}

#video-effect-wrapper .text:last-child {
  margin-bottom: 50vh;
}

#video-effect-wrapper #text-display {
  display: block;
  width: 100%;
  height: 225px;
  position: fixed;
  top: 50%;
  transform: translate(0, -50%);
  z-index: -1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="video-effect-wrapper">
  <video muted autoplay>
            <source src="https://ndvibes.com/test/video/video.mp4" type="video/mp4" id="video">
          </video>

  <div id="text-display"></div>
  <div class="text first">
    Scroll down to test this little demo
  </div>
  <div class="text">
    Still a lot to improve
  </div>
  <div class="text">
    So please help me
  </div>
  <div class="text">
    Thanks! :D
  </div>
</div>

<div id="other-parts-of-website">
  <p>
    Normal scroll behaviour wanted.
  </p>
  <p>
    Normal scroll behaviour wanted.
  </p>
  <p>
    Normal scroll behaviour wanted.
  </p>
  <p>
    Normal scroll behaviour wanted.
  </p>
  <p>
    Normal scroll behaviour wanted.
  </p>
  <p>
    Normal scroll behaviour wanted.
  </p>
</div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Creating JavaScript Powered Pie Charts

I am seeking a lightweight JavaScript pie chart option to replace PlotKit, as the library is too large for my low bandwidth. Ideally, I am looking for a compact and efficient solution in either JavaScript or jQuery. ...

Grammarly is seamlessly inserting padding into my body element

After setting up my website on GitHub, I ran into an issue where the Grammarly Chrome extension was automatically adding a padding-top attribute to the body tag. <body data-new-gr-c-s-check-loaded="14.1028.0" data-gr-ext-installed="" ...

NextJS displays outcomes as per query parameters obtained from an external API

I have set up my NextJS app to connect to a rest API. The API returns results based on different filters in the query string, such as: /jobs /jobs?filter=completed /jobs?filter=upcoming /jobs?filter=cancelled On my NextJS page, I have a few buttons that I ...

The jQuery mouseout functionality seems to be malfunctioning and not responding as expected

I am currently developing a slider that displays details when the mouse hovers over the logo, and hides the details when the mouse moves away from the parent div. jQuery('.st_inner img').mouseover(function() { jQuery(this).parent().siblings( ...

Adding JSON data to a MySQL column in JSON format with the help of NodeJS

I currently have a MySQL table set up with the following specifications: CREATE TABLE `WorkOrders` ( `workorder_id` int(11) NOT NULL AUTO_INCREMENT, `intertype_id` int(11) NOT NULL, `equipment_id` int(11) NOT NULL, `reason_id` int(11) NOT NULL ...

restrict the maximum character count in regex

The string can consist of a single number or multiple numbers separated by "-", but the total character count must not exceed 6. Examples of valid strings 5 55-33 4444-1 1-4444 666666 Examples of invalid strings -3 6666- 5555-6666 My initial regex / ...

The retrieval of data using PHP, MYSQL, and AJAX is unsuccessful

A dropdown menu contains the names of months, represented by numbers from 1 to 12: When a month is selected, I want to retrieve data from a database to display relevant tournaments for that specific month. The SQL code has been tested and confirmed to be ...

Different syntax issues in Firefox and Internet Explorer are causing errors

While this code successfully runs on Firefox, it encounters errors when used on IE. document.getElementById('zip_container').style.borderLeft = '1px solid #D9D9D9;'; In this code snippet, zip_container refers to a div element. If any ...

Prevent the ajax script from running if the previous call has not yet completed successfully

My situation involves a button that triggers an ajax function. Sometimes the server response is slow, causing users to click the button multiple times in frustration. The main ajax function in question is structured as follows: $.ajax({ type: "POS ...

Error encountered while parsing JSON data from LocalStorage

Storing an object in localStorage can sometimes lead to unexpected errors. Take this example: function onExit(){ localStorage.setItem("my_object","'" + JSON.stringify(object) + "'"); } When retrieving this data from localStorage, it may loo ...

Troubleshooting problems with CSS menus and submenus, along with addressing browser pixel issues specific to Web

Find my custom css menu here: Custom CSS Menu On hovering over "about us", the "our clergy" sub-menu appears automatically. I need it to show only when hovering over "our clergy". This screenshot is from Firefox, but webkit browsers like Chrome show a sl ...

Update dataTable with new data fetched through an ajax call from a separate function defined in a different

I need to refresh the data in my datatable after using a function to delete an item. The function for deleting is stored in a separate file from the datatable. Here is the delete function code: function removeFunction(table,id) { var txt= $('.ti ...

Updating the content of a window without the need to refresh the page using JavaScript

Is there a way to navigate back to the previous window in chat_user without refreshing the entire page when the back button is clicked? Below is the code I have tried: <a href="" onclick="window.history.go(-1); return false;">back</a> ...

Animate the background of a table cell (td) in React whenever a prop is updated

I have a dynamic table that receives data from props. I would like the background animation of each cell (td) to change every time it receives new props. I've tried creating an animation for the background to indicate when a cell is updated, but it on ...

Where can content-tag and main-tag be found in vue-virtual-scroller?

I've been trying to wrap my head around the vue virtual scroller. I couldn't help but notice in the demo that it utilizes a few HTML attributes... <virtual-scroller v-if="scopedSlots" class="scroller" :item-height="itemHeight" :items="items" ...

Retrieve JSON information from a URL via AJAX if applicable

Is there a way to retrieve the JSON data from the following URL and incorporate it into my webpage in some capacity? (Disclaimer: I do not own this website, I am simply utilizing the data for a basic free app.) Although entering the URL directly into the ...

What are the best methods for visually comparing two HTML documents?

In the process of developing a Windows Forms application, my goal is to compare two HTML documents. The initial document is sourced externally and contains structural errors. To address this, an algorithm is utilized to optimize the HTML text, resulting in ...

Using Node.js to return JSON data containing base64 encoded images

In my database, I store all images as base64 with additional data (creation date, likes, owner, etc). I would like to create a /pictures GET endpoint that returns a JSON object containing the image data, for example: Image Data [{ "creation": 1479567 ...

Implementing a callback function in Vue js 2 for handling dispatched actions within a component

Is there a method to determine if the action dispatched from a component has completed without utilizing state management? Currently, I have an action called createAddress. In my component, there is a modal where users input their address. Once the user en ...

What is the best way to prevent the Android keypad popup using jQuery?

I recently created a webpage with a custom jQuery-based 7keypad. When I open the page, the text field automatically gets focused and the Android keypad pops up. Does anyone know how to disable this popup of the Android keypad using jQuery? The webpage in ...