Implementación de una animación basada en desplazamiento con JavaScript

Introducción

Índice
  1. Introducción
  • Prerrequisitos
  • Estructura HTML
  • Aplicación de estilos CSS
  • Implementación de animaciones con JavaScript
    1. Funciones y variables útiles
    2. Implementación del comportamiento de desplazamiento personalizado
    3. Animar imágenes mientras se desplaza
    4. Animación inicial
  • Solución del problema del salto en dispositivos móviles
  • Conclusiones
    1. Créditos
  • Hay un tipo de animación que no ha parado de aumentar su presencia en las webs más modernas y originales: las animaciones basadas en el scrollevento de JavaScript. Esta tendencia literalmente explotó cuando aparecieron los efectos parallax, y desde entonces su uso se ha vuelto más frecuente.

    Pero lo cierto es que hay que tener mucho cuidado al implementar una animación basada en scroll, ya que puede afectar seriamente el rendimiento del sitio web, especialmente en dispositivos móviles.

    Por eso te invitamos a continuar leyendo el tutorial, donde implementaremos desde cero y con vanilla JavaScript, esta hermosa animación basada en scroll, manteniendo además un buen desempeño incluso en dispositivos móviles:

    ¡Empecemos!

    Prerrequisitos

    Este tutorial utiliza Sass para recorrer 10 imágenes @fory hacer referencia a los selectores principales con Parent Selector ( ). Es posible lograr el mismo resultado con CSS, pero requerirá trabajo adicional.

    Estructura HTML

    Utilizaremos una estructura HTML simple, donde cada imagen del diseño será en realidad un divelemento en el código HTML, y las imágenes estarán definidas y posicionadas con CSS, lo que facilitará esta tarea:

    !-- The `.container` element will contain all the images --!-- It will be used also to perform the custom scroll behavior --div  !-- Each following `div` correspond to one image --  !-- The images will be set using CSS backgrounds --  div/div  div/div  div/div  div/div  div/div  div/div  div/div  div/div  div/div  div/div/div

    Ahora veamos los estilos CSS necesarios para lograr el diseño deseado.

    Aplicación de estilos CSS

    Primero, comencemos por crear el diseño.

    En esta ocasión utilizaremos un CSS Grid, aprovechando que esta tecnología ya está soportada en todos los navegadores modernos.

    // The container for all images.container {  // 2 columns grid  display: grid;  grid-template-columns: 1fr 1fr;  grid-gap: 0 10%;  justify-items: end; // This will align all items (images) to the right  // Fixed positioned, so it won't be affected by default scroll  // It will be moved using `transform`, to achieve a custom scroll behavior  position: fixed;  top: 0;  left: 0;  width: 100%;}

    Es importante destacar que, además de la cuadrícula CSS, le estamos dando .containeruna posición al elemento fixed. Esto hará que este elemento no se vea afectado por el comportamiento de desplazamiento predeterminado, lo que permitirá realizar transformaciones personalizadas con JavaScript.

    Ahora veamos cómo definir los estilos asociados a las imágenes. Consulta los comentarios para obtener una breve explicación de cada parte:

    // Styles for image elements// Mainly positioning and background styles.image {  position: relative;  width: 300px;  height: 100vh;  background-repeat: no-repeat;  background-position: center;  // This will align all even images to the left  // For getting centered positioned images, respect to the viewport  :nth-child(2n) {    justify-self: start;  }  // Set each `background-image` using a SCSS `for` loop  @for $i from 1 through 10 {    :nth-child(#{$i}) {      background-image: url('../img/image#{$i}.jpg');    }  }}

    Ahora hagamos algunos ajustes para pantallas pequeñas ya que allí deberíamos tener una columna en lugar de dos.

    // Adjusting layout for small screens@media screen and (max-width: 760px) {  .container {    // 1 column grid    grid-template-columns: 1fr;    // Fix image centering    justify-items: center;  }  // Fix image centering  .image:nth-child(2n) {    justify-self: center;  }}

    Y de esta manera ya tenemos nuestro diseño casi listo, solo nos falta agregarle el fondo al cuerpo, lo cual no explicaremos para no extender el tutorial con detalles triviales.

    Tenga en cuenta también que, por ahora, no podrá desplazarse, ya que le hemos dado una posición fija al elemento contenedor. A continuación, resolveremos este problema y daremos vida a nuestro diseño.

    Implementación de animaciones con JavaScript

    Ahora vamos a ver cómo implementar, desde cero y usando JavaScript vanilla, un movimiento de scroll personalizado, más suave y adecuado a las animaciones planteadas. Todo esto lo conseguiremos sin intentar reimplementar todo el trabajo asociado al scroll que hace el navegador web. En su lugar, mantendremos la funcionalidad nativa del scroll, al mismo tiempo que tendremos un comportamiento de scroll personalizado. Suena bien, ¿verdad? ¡Veamos cómo hacerlo!

    Funciones y variables útiles

    Primero veamos algunas funciones útiles que usaremos. Apóyese en los comentarios para comprender mejor:

    // Easing function used for `translateX` animation// From: https://gist.github.com/gre/1650294function easeOutQuad (t) {  return t * (2 - t)}// Returns a random number (integer) between `min` and `max`function random (min, max) {  return Math.floor(Math.random() * (max - min + 1)) + min}// Returns a random number as well, but it could be negative alsofunction randomPositiveOrNegative (min, max) {  return random(min, max) * (Math.random()  0.5 ? 1 : -1)}// Set CSS `tranform` property for an elementfunction setTransform (el, transform) {  el.style.transform = transform  el.style.WebkitTransform = transform}

    Y estas son las variables que estaremos utilizando también, descritas brevemente para entender mucho mejor el código que presentaremos a continuación:

    // Current scroll positionvar current = 0// Target scroll positionvar target = 0// Ease or speed for moving from `current` to `target`var ease = 0.075// Utility variables for `requestAnimationFrame`var rafId = undefinedvar rafActive = false// Container elementvar container = document.querySelector('.container')// Array with `.image` elementsvar images = Array.prototype.slice.call(document.querySelectorAll('.image'))// Variables for storing dimmensionsvar windowWidth, containerHeight, imageHeight// Variables for specifying transform parameters (max limits)var rotateXMaxList = []var rotateYMaxList = []var translateXMax = -200// Populating the `rotateXMaxList` and `rotateYMaxList` with random valuesimages.forEach(function () {  rotateXMaxList.push(randomPositiveOrNegative(20, 40))  rotateYMaxList.push(randomPositiveOrNegative(20, 60))})

    Con todo esto listo, veamos cómo implementar nuestro comportamiento de desplazamiento personalizado.

    Implementación del comportamiento de desplazamiento personalizado

    Para hacer que nuestra página web sea desplazable, añadiremos un nuevo divelemento al bodydinámico, al cual le asignaremos el mismo área heightde nuestro elemento contenedor, de tal manera que el área desplazable sea la misma.

    // The `fakeScroll` is an element to make the page scrollable// Here we are creating it and appending it to the `body`var fakeScroll = document.createElement('div')fakeScroll.className = 'fake-scroll'document.body.appendChild(fakeScroll)// In the `setupAnimation` function (below) we will set the `height` properly

    También necesitamos un poco de estilos CSS para que nuestro .fake-scrollelemento haga que la página sea desplazable, sin interferir con el diseño y los demás elementos:

    // The styles for a `div` element (inserted with JavaScript)// Used to make the page scrollable// Will be setted a proper `height` value using JavaScript.fake-scroll {  position: absolute;  top: 0;  width: 1px;}

    Ahora veamos la función encargada de calcular todas las dimensiones necesarias y preparar el terreno para las animaciones:

    // Geeting dimmensions and setting up all for animationfunction setupAnimation () {  // Updating dimmensions  windowWidth = window.innerWidth  containerHeight = container.getBoundingClientRect().height  imageHeight = containerHeight / (windowWidth  760 ? images.length / 2 : images.length)  // Set `height` for the fake scroll element  fakeScroll.style.height = containerHeight + 'px'  // Start the animation, if it is not running already  startAnimation()}

    setupAnimationUna vez llamada la función, la página será desplazable y todo estará listo para comenzar a escuchar el scrollevento y ejecutar la animación.

    Así que veamos qué haremos cuando scrollse active el evento:

    // Update scroll `target`, and start the animation if it is not running alreadyfunction updateScroll () {  target = window.scrollY || window.pageYOffset  startAnimation()}// Listen for `scroll` event to update `target` scroll positionwindow.addEventListener('scroll', updateScroll)

    Cada vez scrollque se activa el evento, simplemente se actualiza la variable de destino con la nueva posición y se llama a la startAnimationfunción, que no hace nada más que iniciar la animación si aún no está activa. Aquí está el código:

    // Start the animation, if it is not running alreadyfunction startAnimation () {  if (!rafActive) {    rafActive = true    rafId = requestAnimationFrame(updateAnimation)  }}

    Ahora veamos el comportamiento interno de la updateAnimationfunción, que es la que realmente realiza todos los cálculos y transformaciones en cada cuadro, para lograr la animación deseada. Por favor siga los comentarios para una mejor comprensión del código:

    // Do calculations and apply CSS `transform`s accordinglyfunction updateAnimation () {  // Difference between `target` and `current` scroll position  var diff = target - current  // `delta` is the value for adding to the `current` scroll position  // If `diff  0.1`, make `delta = 0`, so the animation would not be endless  var delta = Math.abs(diff)  0.1 ? 0 : diff * ease  if (delta) { // If `delta !== 0`    // Update `current` scroll position    current += delta    // Round value for better performance    current = parseFloat(current.toFixed(2))    // Call `update` again, using `requestAnimationFrame`    rafId = requestAnimationFrame(updateAnimation)  } else { // If `delta === 0`    // Update `current`, and finish the animation loop    current = target    rafActive = false    cancelAnimationFrame(rafId)  }  // Update images (explained below)  updateAnimationImages()  // Set the CSS `transform` corresponding to the custom scroll effect  setTransform(container, 'translateY('+ -current +'px)')}

    ¡Y nuestro comportamiento de desplazamiento personalizado está listo!

    Después de llamar a la función setupAnimation, podrá desplazarse como lo haría normalmente y el .containerelemento se moverá en correspondencia, pero con un efecto muy suave y agradable.

    Luego sólo nos queda animar las imágenes en correspondencia con la posición en la que se encuentran respecto al visor. ¡Veamos cómo hacerlo!

    Animar imágenes mientras se desplaza

    Para animar las imágenes utilizaremos la posición actual del falso scroll ( current), y calcularemos la relación intersectionRatio(similar al valor de la IntersectionObserver API) entre cada imagen y el viewport. Después, sólo tendremos que aplicar las transformaciones que queramos en función de esa relación, y obtendremos la animación deseada.

    La idea es mostrar la imagen sin ninguna transformación cuando está en el centro de la pantalla ( intersectionRatio = 1), y aumentar las transformaciones a medida que la imagen se mueve hacia los extremos de la pantalla ( intersectionRatio = 0).

    Preste mucha atención al código que se muestra a continuación, especialmente a la parte donde intersectionRatiose calcula el valor de cada imagen. Este valor es fundamental para luego aplicar las transformaciones CSS adecuadas. Siga los comentarios para una mejor comprensión:

    // Calculate the CSS `transform` values for each `image`, given the `current` scroll positionfunction updateAnimationImages () {  // This value is the `ratio` between `current` scroll position and images `height`  var ratio = current / imageHeight  // Some variables for using in the loop  var intersectionRatioIndex, intersectionRatioValue, intersectionRatio  var rotateX, rotateXMax, rotateY, rotateYMax, translateX  // For each `image` element, make calculations and set CSS `transform` accordingly  images.forEach(function (image, index) {    // Calculating the `intersectionRatio`, similar to the value provided by    // the IntersectionObserver API    intersectionRatioIndex = windowWidth  760 ? parseInt(index / 2) : index    intersectionRatioValue = ratio - intersectionRatioIndex    intersectionRatio = Math.max(0, 1 - Math.abs(intersectionRatioValue))    // Calculate the `rotateX` value for the current `image`    rotateXMax = rotateXMaxList[index]    rotateX = rotateXMax - (rotateXMax * intersectionRatio)    rotateX = rotateX.toFixed(2)    // Calculate the `rotateY` value for the current `image`    rotateYMax = rotateYMaxList[index]    rotateY = rotateYMax - (rotateYMax * intersectionRatio)    rotateY = rotateY.toFixed(2)    // Calculate the `translateX` value for the current `image`    if (windowWidth  760) {      translateX = translateXMax - (translateXMax * easeOutQuad(intersectionRatio))      translateX = translateX.toFixed(2)    } else {      translateX = 0    }    // Invert `rotateX` and `rotateY` values in case the image is below the center of the viewport    // Also update `translateX` value, to achieve an alternating effect    if (intersectionRatioValue  0) {      rotateX = -rotateX      rotateY = -rotateY      translateX = index % 2 ? -translateX : 0    } else {      translateX = index % 2 ? 0 : translateX    }    // Set the CSS `transform`, using calculated values    setTransform(image, 'perspective(500px) translateX('+ translateX +'px) rotateX('+ rotateX +'deg) rotateY('+ rotateY +'deg)')  })}

    Animación inicial

    Y ya casi estamos listos para disfrutar de nuestra animación. Solo nos falta hacer la llamada inicial a la setupAnimationfunción, además de actualizar las dimensiones en caso de que resizese dispare el evento:

    // Listen for `resize` event to recalculate dimensionswindow.addEventListener('resize', setupAnimation)// Initial setupsetupAnimation()

    ¡Desplázate! ...

    Solución del problema del salto en dispositivos móviles

    Hasta ahora todo debería funcionar perfectamente en el escritorio, pero la historia es muy diferente si probamos la animación en dispositivos móviles.

    El problema se produce cuando la barra de direcciones (y la barra de navegación en el pie de página de algunos navegadores) se oculta al desplazarse hacia abajo y se vuelve a mostrar al desplazarse hacia arriba. Esto no es un error, sino una característica. El problema aparece cuando utilizamos la unidad CSS vh, ya que en este proceso se recalcula dicha unidad, dando como resultado un salto no deseado en nuestra animación.

    La solución alternativa que hemos implementado es utilizar la pequeña biblioteca vh-fix, que define, para cada elemento con la clase .vh-fix, una estática heightbasada en su vhvalor y la ventana gráfica height.

    De esta manera ya no deberíamos tener saltos indeseados.

    Conclusiones

    Y hemos terminado de implementar esta hermosa animación basada en desplazamiento.

    Puedes consultar la demostración en vivo, jugar con el código en Codepen o consultar el código completo en el repositorio en Github.

    Tenga en cuenta que el objetivo de este tutorial es esencialmente académico y debe servir de inspiración. Para utilizar esta demo en producción se deben tener en cuenta otros aspectos como la accesibilidad, el soporte de navegadores, el uso de alguna debouncefunción para los eventos scrolly resize, etc.

    Sin más preámbulos, ¡esperamos que lo hayáis disfrutado y os haya resultado útil!

    Créditos

    • La demo se inspiró en esta página web: Jorik.
    • Todas las imágenes son de Unsplash.
    • El fondo SVG utilizado fue personalizado en SVGBackgrounds.com.
    SUSCRÍBETE A NUESTRO BOLETÍN 
    No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

    Subir

    Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion