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

Introducción
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 scroll
evento 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 @for
y 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 div
elemento 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 .container
una 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 div
elemento al body
dinámico, al cual le asignaremos el mismo área height
de 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-scroll
elemento 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()}
setupAnimation
Una vez llamada la función, la página será desplazable y todo estará listo para comenzar a escuchar el scroll
evento y ejecutar la animación.
Así que veamos qué haremos cuando scroll
se 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 scroll
que se activa el evento, simplemente se actualiza la variable de destino con la nueva posición y se llama a la startAnimation
funció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 updateAnimation
funció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 .container
elemento 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 intersectionRatio
se 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 setupAnimation
función, además de actualizar las dimensiones en caso de que resize
se 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 height
basada en su vh
valor 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 debounce
función para los eventos scroll
y 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.
Deja una respuesta