Yếu tố từ cố định đến tương đối trên cuộn


9

Tôi đã tạo một trình bao bọc trong đó tôi có hiệu ứng hoạt hình giống như Apple trên trang Airpods Pro của họ . Về cơ bản nó là một video, khi tôi cuộn video phát từng chút một. Vị trí của video được cố định để văn bản cuộn qua nó một cách độc đáo. Tuy nhiên, văn bản chỉ hiển thị khi nằm giữa phần bù của một bộ phận cụ thể (hiển thị văn bản).

Phần đó hoạt động tốt. Bây giờ tôi muốn rằng khi người dùng đã cuộn đến cuối video và do đó hoạt ảnh kết thúc, trình bao bọc hiệu ứng video sẽ đi từ vị trí cố định đến vị trí tương đối. Vì vậy, trang web đó sẽ cuộn nội dung của nó bình thường sau khi hoạt hình video .

MÃ SỐ MÃ + DEMO

Đây là một ví dụ về những gì tôi đã thử:

        //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");
        }

Đây là loại công việc ... Nhưng là tất cả mọi thứ ngoại trừ trơn tru. Và nó cũng cần có thể cuộn ngược trang web về phía sau.

Những vấn đề tôi gặp phải khi cố gắng khắc phục vấn đề này:

  • Việc cuộn và chuyển từ nhu cầu cố định sang tương đối để cảm thấy tự nhiên và trơn tru
  • Bản thân trình bao bọc không cố định và chứa các phần tử .text, video được cố định để các phần tử .text có thể đi qua phần tử video (tạo hiệu ứng). Các phần tử .text này gây ra sự cố khi cố gắng tìm giải pháp

Câu trả lời:


3

Khi thực hiện một số kỹ thuật đảo ngược trên trang Airpods Pro , chúng tôi nhận thấy rằng hình ảnh động không sử dụng a video, nhưng a canvas. Việc thực hiện như sau:

  • Tải trước khoảng 1500 hình ảnh qua HTTP2, thực sự là các khung hình của hình ảnh động
  • Tạo một mảng hình ảnh ở dạng HTMLImageElement
  • Phản ứng với mọi scrollsự kiện DOM và yêu cầu khung hình động tương ứng với hình ảnh gần nhất, vớirequestAnimationFrame
  • Trong khung hình động yêu cầu gọi lại, hiển thị hình ảnh bằng cách sử dụng ctx.drawImage( ctx2dbối cảnh của canvasphần tử)

Các requestAnimationFramechức năng này sẽ giúp bạn đạt được một hiệu ứng mượt mà như các khung sẽ được hoãn lại và đồng bộ với "khung hình mỗi giây" tỷ lệ của màn hình mục tiêu.

Để biết thêm thông tin về cách hiển thị đúng khung trên sự kiện cuộn, bạn có thể đọc phần này: https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event

Điều đó đang được nói, liên quan đến vấn đề chính của bạn, tôi có một giải pháp làm việc bao gồm:

  • Tạo một trình giữ chỗ, có cùng chiều cao và chiều rộng so với videophần tử. Mục đích của nó là để tránh video chồng lấp phần còn lại của HTML khi được đặt thành absolutevị trí
  • Trong cuộc scrollgọi lại sự kiện, khi trình giữ chỗ đạt đến đỉnh của chế độ xem, đặt vị trí của video thành absolutetopgiá trị đúng

Ý tưởng là video luôn nằm ngoài luồng và diễn ra trên trình giữ chỗ vào đúng thời điểm khi cuộn xuống phía dưới.

Đây là JavaScript:

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

let topOffset;

$(window).resize(onResize);

function computeVideoSizeAndPosition() {
    const { width, height } = video.getBoundingClientRect();
    const videoPlaceholder = $("#video-placeholder");
    videoPlaceholder.css("width", width);
    videoPlaceholder.css("height", height);
    topOffset = videoPlaceholder.position().top;
}

function updateVideoPosition() {
    if ($(window).scrollTop() >= topOffset) {
        $(video).css("position", "absolute");
        $(video).css("left", "0px");
        $(video).css("top", topOffset);
    } else {
        $(video).css("position", "fixed");
        $(video).css("left", "0px");
        $(video).css("top", "0px");
    }
}

function onResize() {
    computeVideoSizeAndPosition();
    updateVideoPosition();
}

onResize();

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

    //If .first text-element is set, place it in bottom of
    //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 launch video-effect when user scrolls
$(document).scroll(function () {

    //Calculate amount of pixels there is scrolled in the video-effect-wrapper
    let n = $(window).scrollTop() - $("#video-effect-wrapper").offset().top + 408;
    n = n < 0 ? 0 : n;

    //If .text.first is set, we need to calculate one less text-box
    let x = $("#video-effect-wrapper .text.first").length == 0 ? 0 : 1;

    //Calculate how many percent of the video-effect-wrapper is currenlty scrolled
    let percentage = n / ($(".text").eq(1).outerHeight(true) * ($("#video-effect-wrapper .text").length - x)) * 100;
    //console.log(percentage);
    //console.log(percentage);

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

    //Calculate to which second in video we need to go
    let skipTo = duration / 100 * percentage;

    //console.log(skipTo);

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

    //Only allow text-elements to be visible inside 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;

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

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

    updateVideoPosition();

});

Đây là HTML:

<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 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="video-placeholder">

</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>

Bạn có thể thử tại đây: https://jsfiddle.net/crkj1m0v/3/


2
Mặc dù đây là nền tảng thú vị và hữu ích về cách triển khai loại hoạt hình này, nhưng nó dường như không liên quan đặc biệt đến câu hỏi của @ oniel, cụ thể là làm thế nào để tiếp tục cuộn trang sau khi hoạt ảnh kết thúc. Như O'Niel lưu ý, liên kết giữa cuộn và phát lại đã hoạt động.
Jeremy Caney

1
Cảm ơn thông tin chi tiết về cách Apple đã thực hiện nó, và một lời cảm ơn thậm chí còn lớn hơn cho giải pháp tốt đẹp và trơn tru!
O'Niel

1

Nếu bạn muốn video để khóa lại ở vị trí như bạn di chuyển trở lại, bạn sẽ cần phải đánh dấu nơi bạn chuyển từ fixedtới relative.

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

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

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

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

  //If .first text-element is set, place it in bottom of
  //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 launch video-effect when user scrolls
  $(document).scroll(function() {

    //Calculate amount of pixels there is scrolled in the video-effect-wrapper
    let n = $(window).scrollTop() - $("#video-effect-wrapper").offset().top + vidHeight;
    n = n < 0 ? 0 : n;
    // console.log('n: ' + n);

    //If .text.first is set, we need to calculate one less text-box
    let x = $("#video-effect-wrapper .text.first").length == 0 ? 0 : 1;

    //Calculate how many percent of the video-effect-wrapper is currenlty scrolled
    let percentage = n / ($(".text").eq(1).outerHeight(true) * ($("#video-effect-wrapper .text").length - x)) * 100;
    //console.log(percentage);

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

    //Calculate to which second in video we need to go
    let skipTo = duration / 100 * percentage;

    //console.log(skipTo);

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

    //Only allow text-elements to be visible inside 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;

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

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


    //If video-animation ended: Make position of video-wrapper relative to continue scrolling
    if (videoLocked) {
      if ($(window).scrollTop() >= videoHeight) {
        $('video').css("position", "relative");
        videoLocked = false;
        lockPoint = $(window).scrollTop() - 10;
        // I gave it an extra 10px to avoid flickering between locked and unlocked.
      }
    } 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;
  /*
                background-color: blue;
                */
}

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

#video-effect-wrapper .text:last-child {
  /*margin-bottom: 100vh;*/
  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;
  /*
                background-color: red;
                */
}
<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>


Xin chào cảm ơn câu trả lời của bạn. Giải pháp này đã gần nhưng tôi vẫn gặp phải một vấn đề lớn với mã của bạn: Ngay sau khi video kết thúc, vị trí của thành phần video đã được sửa, nhưng vẫn có các đoạn trắng hoạt hình trên nền màu xanh lá cây. Div # other-part-of-website phải là nội dung đầu tiên người dùng nhìn thấy sau video hoạt hình.
O'Niel
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.