Elemento de fijo a relativo en desplazamiento


9

He creado un contenedor en el que he animado el mismo efecto que Apple en su página de Airpods Pro . Básicamente es un video, cuando me desplazo el video se reproduce poco a poco. La posición del video es fija, por lo que el texto se desplaza muy bien sobre él. Sin embargo, el texto solo es visible cuando se encuentra entre el desplazamiento de una división específica (visualización de texto).

Esa parte funciona bien. Ahora quiero que cuando el usuario se haya desplazado hasta el final del video y, por lo tanto, la animación haya terminado, el envoltorio de efectos de video pase de una posición fija a una posición relativa. Entonces, ese sitio web desplazará su contenido normalmente después de la animación de video .

CÓDIGO JSFIDDLE + DEMO

Este es un ejemplo de lo que ya probé:

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

Este tipo de trabajo ... Pero es todo menos suave. Y también debe ser posible invertir el desplazamiento de la página web hacia atrás.

Problemas que encontré al intentar solucionar este problema:

  • El desplazamiento y la transición de lo fijo a lo relativo necesita sentirse natural y suave
  • El contenedor en sí no está arreglado y contiene elementos .text, el video está arreglado para que los elementos .text puedan pasar sobre el elemento video (creando el efecto). Estos elementos .text causan problemas al tratar de encontrar una solución

Respuestas:


3

Cuando hacemos ingeniería inversa en la página de Airpods Pro , notamos que la animación no usa a video, sino a canvas. La implementación es la siguiente:

  • Precargue aproximadamente 1500 imágenes sobre HTTP2, en realidad los cuadros de la animación
  • Crear una matriz de imágenes en forma de HTMLImageElement
  • Reaccione a cada scrollevento DOM y solicite un cuadro de animación correspondiente a la imagen más cercana, conrequestAnimationFrame
  • En el marco de animación, solicita la devolución de llamada, muestra la imagen usando ctx.drawImage( ctxsiendo el 2dcontexto del canvaselemento)

La requestAnimationFramefunción debería ayudarlo a lograr un efecto más suave ya que los fotogramas se diferirán y sincronizarán con la velocidad de "fotogramas por segundo" de la pantalla de destino.

Para obtener más información sobre cómo mostrar correctamente un marco en un evento de desplazamiento, puede leer esto: https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event

Dicho esto, con respecto a su problema principal, tengo una solución de trabajo que consiste en:

  • Crear un marcador de posición, de la misma altura y anchura que el videoelemento. Su propósito es evitar que el video se superponga con el resto del HTML cuando se establece en la absoluteposición
  • En la scrolldevolución de llamada del evento, cuando el marcador de posición llegue a la parte superior de la ventana gráfica, establezca la posición del video absolutey el topvalor correcto

La idea es que el video siempre permanezca fuera del flujo y tenga lugar sobre el marcador de posición en el momento correcto al desplazarse hacia abajo.

Aquí está el 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();

});

Aquí está el 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>

Puedes probar aquí: https://jsfiddle.net/crkj1m0v/3/


2
Si bien este es un trasfondo interesante y útil sobre cómo implementar este tipo de animación, no parece especialmente relevante para la pregunta de @oniel, que es específica sobre cómo reanudar el desplazamiento de la página una vez que finaliza la animación. Como señala O'Niel, la asociación entre el desplazamiento y la reproducción ya está funcionando.
Jeremy Caney

1
¡Gracias por la información sobre cómo Apple lo ha hecho, y gracias aún más por la solución agradable y fluida!
O'Niel

1

Si desea que el vídeo a la espalda de bloqueo en su lugar mientras se desplaza una copia de seguridad, tendrá que marcar el lugar donde se cambia de fixeda 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>


Hola gracias por tu respuesta Esta solución se acerca, pero aún encuentro un gran problema con su código: tan pronto como termina el video, la posición del elemento de video se arregla, pero todavía hay párrafos blancos que animan en el fondo verde. El div # other-parts-of-website debe ser el primer contenido que el usuario ve después de la animación de video.
O'Niel
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.