Scroll suave del documento con CSS (y 12 líneas de javascript) 2.4.16
Nueva entrega y colaboración de la serie Javascript con @Furoya. En esta ocasión dedicada a controlar a voluntad la velocidad de desplazamiento del documento al hacer scroll.
Scroll suave del documento con CSS (y 12 líneas de javascript)
Vamos a comenzar aclarando un punto : CSS aún no tiene cómo leer el desplazamiento de una página, y no existen propiedades ni valores que lo modifiquen.
Hay muchos (demasiados) pedidos en foros, comunidades y comentarios de blogs reclamando efectos CSS para el movimiento generado por una scrollbar, que salvo aisladas excepciones terminan en una sugerencia para caer en javascript, o su variante JQuery.
Uno de los más comunes es el de desplazamiento suave
. Algo que los navegadores ya tienen (yo lo tengo configurado al menos en Chrome y Firefox) pero que no disponen de muchos parámetros de ajuste. Oficialmente, dijimos, es imposible de lograr con estilos; pero hay algunos casos ya publicados que hacen una transición apoyada por javascript en la captura y asignación de valores, no en la animación que sí es trabajo del CSS.
Es un truco, no un método. No se podrá aplicar a todos los documentos en cualquier caso, pero eso no nos va a quitar el sueño al momento de intentar unos ejemplos experimentales aquí en el blog. Porque la idea sigue siendo entender cómo funciona, más que usarlo en una página real.
Cómo emular desplazamientos con un moiré
(consecuente)
Empecemos por uno de estos casos de laboratorio, que en realidad sí puede existir en páginas de producción.
Es una galería de imágenes encolumnadas que se desplazan verticalmente con la scrollbar. La dificultad principal que encontramos al encarar el proyecto es que debemos desenganchar el movimiento de la barra
del arrastre del documento
, para darle después un desplazamiento suave y amortiguado. configurado por nosotros, pero que termine en la misma posición que le daría la scrollbar.
El truco es muy simple.
See the Pen Smooth scroll [1]. (Experimental). by solipsistaCP (@solipsistacp) on CodePen.
Y hablando de simpleza, el escript ya es minimalista.
<script type="text/javascript">
onload = inicia; //EJECUTA AL CARGAR EL DOCUMENTO
onscroll = desplaza; //EJECUTA AL TERMINAR EL DESPLAZAMIENTO
var desplazamiento, contenedor;
function inicia() {
/* REFIERE AL ELEMENTO CONTENEDOR DEL DOCUMENTO */
contenedor = document.querySelector("main");
/* MIDE LA ALTURA DEL CONTENEDOR Y LA PASA AL body */
document.body.style.height = contenedor.offsetHeight + "px"
}
function desplaza() {
/* PASA LA MISMA DISTANCIA DE DESPLAZAMIENTO AL CONTENEDOR COMO top NEGATIVO */
contenedor.style.top = -pageYOffset + "px";
}
</script>
Todo lo demás lo hace el CSS, como corresponde a este blog. Les explico un poco el JS:
- No usé
addEventListener()
para no complicar el código, pero nada impide que en sus pruebas aprovechen para practicar un poco con él. Aquí hay dos eventos que disparan cada uno su función, para cuando se carga el documento y para cuando se lo desplaza. - Hay dos variables globales que llenamos con las funciones siguientes:
- La primera ubica en una variable la referencia al elemento que contiene al documento (las imágenes, los títulos, los enlaces, ...); y luego lo mide en altura para pasarle ese valor al
body
. Esto es porque elmain
tieneposition:fixed
, por lo que no ocupa espacio en el cuerpo del documento y éste no sabe realmente cuánto mide. Con esto logramos quebody
(desplazable verticalmente) ymain
(que está fijo y no se desplaza) de alguna forma puedan emparejarse si los movemos con cualquier método sobre el eje "y" una misma distancia. - Para el caso del
body
, claramente la manera de desplazarlo es con la barra, el teclado, el puntero de dirección sobre la pantalla, el arrastre con el dedo, la ruedita, en fin, los modos usuales. Y por eso la siguiente función usapageYOffset
que captura la cantidad de pixeles desplazados en vertical para luego pasar ese valor (convertido a negativo, claro) comotop
del contenedor de nuestro documento. Si el cuerpo se mueve (p.e.) 500px hacia arriba, la posición de la galería sube esos 500px por obra del JS que reescribe el valortop
en su CSS. - La magia la pone una regla de transición. Al cambiar el valor de
top
la hoja de estilos ya aplica sutransition: top 1500ms cubic-bezier(.5,.33,.3,.9) 200ms
que hace un desplazamiento suave y amortiguado simulando que es el de la scrollbar.
- La primera ubica en una variable la referencia al elemento que contiene al documento (las imágenes, los títulos, los enlaces, ...); y luego lo mide en altura para pasarle ese valor al
No sé si se fijaron en un detalle de la demo anterior. Resulta que el fondo tenía un dibujo de gradientes, que estaba puesto en el contenedor de la galería. Si el fondo hubiese estado en el body
tendríamos un efecto curioso : porque los colores se desplazarían siguiendo la scrollbar, y después se movería el contenido sobre él. Esto puede ser útil en algunos casos, sin importar ya qué tipo de transición usemos.
Antes de pasar a la siguiente versión de desplazamientos, les muestro un ejemplo de fondos desfasados, por si les sugiere alguna idea.
See the Pen Smooth scroll [2]. (Experimental). by solipsistaCP (@solipsistacp) on CodePen.
Cómo emular rotaciones (con un paralaje antecedente)
Si por casualidad no se les había ocurrido combinar animaciones de fondo, seguro que al ver todas las demos presentadas y linkeadas se les apareció la imagen del parallax
.
Y sí, es posible capturar el valor de desplazamiento para aplicarlo a elementos desenganchados de su arrastre, entonces podemos elegir secciones de la página y darles distintas velocidades de animación. En verdad podemos darles distintas animaciones que no necesariamente sean de desplazamiento vertical.
Éste sería un ejemplo más bien simple, muy mejorable, pero que les da una perspectiva cabal de las enormes posibilidades que tiene el escript (para ser experimental, digo).
See the Pen Smooth scroll [3]. (Experimental). by solipsistaCP (@solipsistacp) on CodePen.
Quería agregar un "motorcito con polea" detrás del engranaje, pero hubiese sido ensuciar el código con elementos innecesarios. Esas cosas se las dejo a diseñadores de verdad, que seguro tienen un poco más de creatividad que yo.
Por ahora, demos una vista rápida al javascript.
<script type="text/javascript">
onload = inicia; //EJECUTA AL CARGAR EL DOCUMENTO
onscroll = desplaza; //EJECUTA AL TERMINAR EL DESPLAZAMIENTO
var desplazamiento, contenedor;
function inicia() {
/* REFIERE AL ELEMENTO CONTENEDOR DEL DOCUMENTO */
contenedor = document.querySelector("main");
/* MIDE LA ALTURA DEL CONTENEDOR Y LA PASA AL body */
document.body.style.height = contenedor.offsetHeight + "px";
}
function desplaza() {
/* MIDE EL DESPLAZAMIENTO VERTICAL DEL DOCUMENTO */
desplazamiento = pageYOffset;
/* PASA ESA MISMA DISTANCIA AL CONTENEDOR COMO top NEGATIVO */
contenedor.style.top = -desplazamiento + "px";
/* CALCULA LA ROTACIÓN DEL ENGRANE SEGÚN EL DESPLAZAMIENTO Y LA APLICA */
document.getElementById("engrane").style.transform = "rotate("+ (-desplazamiento*360/754) +"deg)";
}
</script>
Es practicamente igual a los otros, lo único agregado es el giro del engrane, que parece tener un número mágico. En realidad 754
es el diámetro en pixeles de la imagen circular, el cálculo (simple matemática) rota ese borde dentado tantas veces como la distancia que se desplaza la galería. Usando su conversión a grados, claro, porque el giro es un transform:rotate(Gdeg)
.
Para que quede más evidente : si desplazamos 754px el documento, la rueda debe dar un giro completo de -360° porque su diámetro es de 754px. Si desplazamos 1508px, girará -720° (el equivalente a 2 vueltas). La dirección del giro se calcula sola, según la posición de origen.
Un detalle a mencionar es que para el ejemplo las medidas están puestas en pixeles, lo que puede dificultar su adaptabilidad a diferentes pantallas cuando el ancho sea menor a 800px. Es algo que se puede corregir, si alguna vez lo tienen que usar fuera de una demo.
Cómo emular anclajes (con un replicado lamentable)
Vamos a poner un último caso, ya sin muchas explicaciones, porque la mecánica será más o menos la misma. Y si bien los anteriores podrían pasar bajo determinadas circunstancias como algo factible para una página verdadera, éste ya es muy experimental. Es para refregarle en la cara a los que dicen que no se puede
, aunque nos terminen respondiendo con un entonces, no se debe
.
Hasta ahora solamente capturamos valores de tamaño para pasar de una capa a otra. Es una práctica común en cualquier diseño. Pero no se debe abusar del recurso a menos que lo usemos para estos blogs donde siempre se muestran cosas raras.
Supongamos que ya no queremos desplazar el documento con los clásicos escrols, sino que vamos a usar anclas. Un típico menú de navegación para saltar a distintos artículos de la página.
El problema empieza a ser evidente :
- los enlaces van a saltar hasta el elemento que tenga el identificador que esté en su
hiper-referencia
, pero ese salto debe hacerlo en una capa invisible. - Una vez desplazado el documento, entonces se mueve la capa visible hasta la misma posición.
- Para que HTML mueva la capa invisible, debe contener todos los elementos con sus identificadores en la misma posición que en la capa visible.
En principio no alcanza con copiar las medidas, hay que replicar todo el documento y que el navegador mueva el original que no se muestra para que después JS+CSS hagan su animación con la copia que se ve.
See the Pen Smooth scroll [4]. (Experimental). by solipsistaCP (@solipsistacp) on CodePen.
Es verdad, podemos suponer que la copia va a salir de la caché de la máquina y que en realidad no estamos bajando el doble de contenido, pero aún así la idea es impresentable.
Pensemos solamente en que todas las id
's se duplican en el mismo documento, y ya sabemos que no puede existir más de una con el mismo nombre. La distribución de las capas también es crítica, porque el navegador va a interpretar con JS sólo a las primeras así que en el DOM la versión original debe estar antes que la copia. Y ningún otro escript debe hacer referencia a los identificadores, porque también van a buscar al primero de cada uno, que es el que no se ve, y no va a leer o modificar nada para el usuario.
Así y todo, si queremos tomarnos más trabajo, siempre podemos ubicar las coordenadas y tamaños de los elementos que tengan anclajes, y replicar solamente eso en la capa que desplace el menú usando posiciones absolutas, reescribiendo sus valores al vuelo allí y en los botones del menú, para que coincidan y se diferencien de los que estaban en el código fuente, y que ya no van a servir para desplazarse pero aún serían vistos por el CSS y el JS ajenos a nuestro efecto que tenga la página.
Cómo seguir esto (con una sugerencia ajena)
En resumen, lo que quiero decir es que dependiendo de nuestras necesidades y las ganas que le pongamos al producto, hay montones de efectos para aprovechar el arrastre de la escrolbar; pero todos son más o menos iguales en el código. Con entender su mecánica es más que suficiente para empezar a inventar. (Y Edgar Gutiérrez ya estuvo proponiendo uno y dos)
Artículos del autor relacionados
Otros artículos de la serie "Javascript con Furoya" que tienen al scroll y/o sus scrollbars como protagonistas:
- Averiguar tamaños y pasar su valor a Css. Caso de uso: coloreado del scroll
- Parallax y Lazy-Load ¿estoy dentro o fuera del viewport?
- control del scroll. Eventos y efectos al desplazar la página
Créditos y Autoría
Créditos de imagen: La imagen del inicio y el engrane de Internet Archive Book Images; las galerías contienen fotografías de obras que pertenecen a sus respectivos autores.]
Artículo original de Furoya.
La intención del autor con sus colaboraciones no es que los artículos sirvan para hacer un copy&paste de códigos sino que comprendas y aprendas la lógica y el cómo trabaja javaScript.
Y a partir de lo expuesto experimentes tú.
El autor del post y el editor del blog te animamos a que plantees tus dudas o reflexiones y que compartas tus realizaciones en base a lo expuesto en los comentarios. Recuerda que puedes incluir pens (ejemplos en Codepen.io) en ellos.
Primero quiero darles las gracias por estas joyas, que a pesar de la gran cantidad de minas pocas encuentro tan únicas y pulidas.
ResponderEliminarSegundo, no soy muy bueno en javascript (por no decir que ni una pizca), por lo que no sabría como, pero sería interesante ver como se le puede sacar partido a usar unidades relativas como las "viewportheight" o "viewportwidth" (vh o vw) en vez de pixeles, para calcular los desplazamientos.
Saludos.
De nada, a mí me gusta escribir estos artículos. Y en este blog, porque comparto tu opinión sobre el nivel que ofrece. De ahí mis reparos al comenzar a colaborar, porque debía estar a la altura del resto de los artículos y de los colaboradores. Y es difícil.
EliminarEl tema de las unidades relativas es puro cálculo. Javascript trabaja con los valores que maneja el navegador, que son nada más que pixeles. Es la única medida que entiende.
CSS debe hacer la conversión antes de calcular, así que si un hijo tiene el 50% de ancho de su padre, el escript detrás de la hoja de estilos le hace un
[code](50/100) * miElemento.parentElement.offsetWidth[/code]
y ya sabe cuántos pixeles son ese 50% para aplicarlos.
Claro que hay un problema : el redondeo. Y otro más grave : el binario. Lo voy a explicar un poco en la siguiente entrega, pero te imaginarás que el subpixel es nada más para cálculos intermedios; al aplicarlo, la medida más chica es justamente el pixel, y si el ancho a medir tiene 173px no es posible darle a su hijo el 50% como 86.5px . Cada motor de rendering aplica su fórmula de redondeo y allí aparecen los espacios fantasmas, las rayas de colores en un borde, las posiciones desfasadas, ...
Lo que hace javascript es ver la medida tal como lo hace el navegador, pero te permite hacer tu propio redondeo y asegurarte de que se vea igual en cualquier browser. Así que en realidad se puede tomar cualquier medida apuntando a un elemento, y hacer tus propios 'vh', 'vw', '%'; de la misma forma que en el ejemplo del engranaje se podría haber hecho la cuenta del perímetro en el escript, en vez de calcularla "afuera" (diámetro por pi) y poner el valor a mano.
[code]
var perimetro = document.getElementById("engrane").offsetWidth * 3.1416;
document.getElementById("engrane").style.transform = "rotate("+ (-desplazamiento*360/perimetro) +"deg)";
[/code]
Eso lo hubiese ajustado en el caso de existir otro programa que le cambiara su tamaño a la imagen según —por ejemplo— la resolución de pantalla.
g3kdigital me presentó hace un tiempo un JQuery que él usa para hacer desplazamientos suaves en anclas al mismo documento. Creo que la principal ventaja sobre el que usa CSS es que discrimina entre el desplazamiento con anclas y con la barra; por lo demás, sabemos que carga un archivo externo para la animación JS que ya está previamente cargado para el CSS, y eso lo convierte en perdedor.
ResponderEliminarEn la charla que tuvimos en privado salió la posibilidad de pasarlo a vanilla, y me puse a crear una versión que no fuera muy pesada y sí lo bastante comprensible como para publicar aquí.
Veamos la versión que usa JQuery.
[code]
$('nav a').click(function(e){
e.preventDefault(); //evitar el eventos del enlace normal
var strAncla=$(this).attr('href'); //id del ancla
var $distance = - 50; //distancia en pixeles desde el objetivo
var $tiempo = 1000; //tiempo en segundos => 1000 = 1seg
$('body,html').stop(true,true).animate({
scrollTop: $(strAncla).offset().top+$distance
},$tiempo);
});
[/code]
Para empezar, sólo desplaza verticalmente, así que no voy a ocuparme yo tampoco del movimiento horizontal. Una diferencia es que no estoy usando variables para la configuración, los valores están en el mismo escript; y otra es que el motor no trabaja por tiempo sino por distancia, se puede ajustar el largo de los pasos y la demora entre ellos, pero si el enlace está muy separado del destino, va a tardar más tiempo en llegar.
[code]
var Yi, Yf, Yd, pausa;
document.addEventListener("click", function(evento){
var origen = evento.target.getAttribute('href');
if(origen && origen.indexOf("#")==0) {
evento.preventDefault();
var destino = document.querySelector(origen);
Yi = pageYOffset; //alert(Yi)
Yf = destino.offsetTop - 50; //alert(Yf)
Yd = (Yi <= Yf)? 1 : -1; //alert(Yd)
mueve();
}
}, false);
function mueve() {
clearTimeout(pausa);
if(Yd==1) {
scrollBy(0,10);
if(Yf>pageYOffset) pausa = setTimeout(mueve, 20);
}
else if(Yd==-1) {
scrollBy(0,-10);
if(Yf<pageYOffset) pausa = setTimeout(mueve, 20);
}
}
[/code]
El sniper tiene el evento "click", que si se hace sobre un link que comience con "#" lo anula y comienza a trabajar el JS. Ubicando al elemento que tiene ese ID ('destino') y tomando su coordenada hasta el tope ('Yf') junto con el desplazamiento que ya tenga el documento ('Yi') es fácil saber cuánto hay que escrolear para alcanzarlo, y también la dirección ('Yd') para subir o bajar según el caso.
La función 'mueve()' es el motor que estaría en el archivo JQ. Dependiendo de la dirección a moverse, va a escrolear en positivo o negativo de a 10px; y mientras la coordenada "Y" final no alcance la altura del tope del documento (que en 'Yf' tiene una diferencia de 50 para separarlo 50px del borde superior) va a repetir el paso cada 20 milisegundos. Esto le da una velocidad de desplazamiento de 500 pixeles por segundo.
Es un borrador, lo pueden perfeccionar todo lo que quieran, ya que el código se puede reducir aún más.
Y les recuerdo que en el anuario 2017 hay una versión con CSS mejor preparada para páginas industriales.
Ya que estaba, hice una demo para probar el escript (que para quienes no lo probaron antes, les cuento que tenía un bug al llegar a los últimos destinos, cerca del pie, que colgaba la página). Le agregué un par de líneas JS además del HTML y CSS, pero las comentadas son solamente para seguir el funcionamiento al probarlo en una página propia (no todo funciona en Codepen).
Eliminar[pen] data-height="265" data-theme-id="0" data-slug-hash="XzqzOg" data-default-tab="js,result" data-user="solipsistacp" data-embed-version="2" data-pen-title="Desplazamiento suave para anclas." class="codepen"[/pen]
Gracias Furoya por estas ampliaciones y/o mejoras a tus colaboraciones.
EliminarPrometo que en cuanto encuentre un rato esta la paso al corpus del post como ampliación.
Un saludo
¡Pero ni hace falta agregarlo, Kseso! Estas son cosas que se le ocurren al manguero de Edgar, y cuando tengo un minuto libre yo le sigo el juego. Ni siquiera tiene CSS, porque el motor de la animación es javascript. Claro, la ventaja es que ya no es una simulación de desplazamiento: con JS sí se puede hacer un escroleado real sin usar la barra o el teclado, por eso es que el smooth scroll se puede aplicar exclusivamente a las anclas. Ya que estoy, aprovecho a comentar que el bug de la primera versión estaba en que la animación se detiene cuando el destino llega al borde superior, y si ese elemento destino está cerca del pie no va a haber documento debajo para seguir desplazando y que llegue hasta arriba; por lo que el escript sigue funcionando infinitamente ¡y no permite desplazarlo más hacia el top! Si el destino está por el medio, nadie se va a dar cuenta, pero como la idea (de Edgar) era usarlo como el JQ, lo terminé puliendo para que funcionara en cualquier documento.
EliminarY ya sabés que no hay nada que agradecer.
Te dejo un gran abrazo.
Quiero devolver una gentileza, ya que Kseso mencionó este artículo en CSS scroll control. Historia y demo en base a scroll-behavior, yo voy a linkear acá su pormenorizado relato sobre las desventuras del desplazamiento suave en CSS. Que ya está dejando obsoleto el engendro previo y su versión del anuario, porque a la fecha de este comentario solamente los navegadores de Microsoft (y alguno que otro menos usado) ignoran estas reglas.
ResponderEliminarUn saludo.