¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Cabecera animada con GreenSock

¿Os acordáis de GreenSock? ¿No? Bueno... ¡No pasa nada! Puedes echarle un vistazo a nuestra Introducción a GreenSock para descubrir lo que es o simplemente para refrescar tu memoria.

Hoy vamos a ver cómo podemos realizar una vistosa cabecera animada. Esta cabecera la hice para un proyecto que ya no está online y siempre me quedé con las ganas de hacer un tutorial sobre cómo lograr ese efecto.

Veamos un GIF del efecto en acción para ir abriendo apetito:

Antes de ponernos manos a la obra, me gustaría aclarar algunas cosas:

  • Esto es un concepto. La versión final debería tener bastante más código, en concreto para adaptarlo a móvil. En la web en la que estuvo esto, se optó por acortar el header directamente para el móvil. No obstante, se cambiaban las distancias en función del ancho de la pantalla, etc. Por simplificarlo, he quitado todo este código.
  • El código JavaScript que podréis ver aquí, usa toda la sintaxis de ES6 que hemos estado viendo hasta ahora. En concreto el uso de let y las clases. Recuerda que siempre puedes dar un repaso a la Introducción a ES6.
  • El CSS usa flexbox, sobre el cual no he hablado nada de nada en la web pero podéis encontrar un maravilloso artículo de Juan Díaz-Bustamante en castellano: FlexBox: La caja flexible con CSS 3.
  • El código que no sea imprescindible para explicar el efecto, será obviado, esto afecta mayoritariamente al HTML y al CSS, ¡pero no te preocupes que lo puedes ver todo en Codepen!

¡Vamos al lío!

El marcado HTML

<div class="funcion13-logo">  
  <img src="logo.png" alt="Funcion 13 Logo" class="logo">
</div>  
<header class="cabecera">  
  <div class="cabecera__fondo--nitido"></div>
  <div class="cabecera__capa"></div>
  <div class="cabecera__contenido">
    <h1>Tutoriales, código y CSS</h1>
    <p>Descubre lo último sobre ES6, Angular2, etc</p>
    <a href="https://twitter.com/funcion13" class="boton" target="_blank">Síguenos en Twitter</a>
  </div>
  <nav class="nav">
    <div class="nav__columna nav__pull"></div>
    <ul class="nav__columna nav__enlaces">
      <li><a href="">Inicio</a></li>
      <li><a href="">JavaScript</a></li>
      <li><a href="">CSS</a></li>
    </ul>
    <ul class="nav__columna nav__social">
      <li>
        <a href="https://twitter.com/funcion13" target="_blank">
          <i class="fa fa-twitter"></i>
        </a>
      </li>
      <li>
        <a href="https://www.facebook.com/funciontrece/" target="_blank">
          <i class="fa fa-facebook"></i>
        </a>
      </li>
      <li>
        <a href="https://www.funcion13.com/rss/" target="_blank">
          <i class="fa fa-rss"></i>
        </a>
      </li>
    </ul>
  </nav>
</header>  
<div class="relleno"></div>  

El efecto que queremos lograr es tener una gran cabecera que, al hacer scroll, se acople al scroll en un determinado momento, mostrando únicamente la navegación y el logo. Hay varios elementos importantes.

  • El logo funcion13-logo debe permanecer fixed todo el rato para dar la sensación de que es lo que arrastra la cabecera luego.
  • La cabecera en sí, permanece con una posición absoluta, de manera que luego al pasar a fixed no de ningún salto.
  • El relleno ocupa el espacio que ocupe toda la cabecera ya que es absolute (y luego fixed) por lo que, de no usar esta capa, el contenido de la página, quedaría por detrás de la cabecera.

Hasta aquí creo que todo bien, pero vamos a entrar en la cabecera a ver qué nos encontramos.

  • cabecera__fondo--nitido debe tener el mismo ancho y alto que toda la cabecera y contiene la versión sin emborronar de la cabecera. La idea es que vaya perdiendo opacidad para que parezca que se está emborronando.
  • cabecera__capa no es más que una capa que oscurece todo el fondo para hacer que las letras sean legibles. Se puede quitar si aplicamos esta capa con Photoshop o similares.
  • cabecera__contenido tendrá una posición fixed de manera que al hacer scroll, se vaya quedando por detrás de la navegación. Para mejorar el efecto aun más, el botón se irá desvaneciendo antes que el resto del contenido, de manera que logremos un efecto escalonado.

El resto del marcado no es realmente importante ya que la navegación no tiene demasiado misterio.

El estilo CSS

Ahora que ya hemos explicado el marcado y cómo debe quedar, vamos a poner el CSS simplemente.

$tamano-cabecera: 375px;
$tamano-encogida: 225px;
$tamano-contenido: $tamano-cabecera - 220px;
$color-fondo: rgba(0, 0, 0, .5);
$altura-boton: 45px;
$altura-nav: 50px;

//Estilos del tutorial
.cabecera {
  background-image: url(https://www.funcion13.com/content/images/2015/01/blurred-bg.jpg);
  display: inline-block;
  height: $tamano-cabecera;
  left: 0;
  padding: 0 20px;
  position: absolute;
  top: 0;
  width: 100%;
  z-index: 5;

  &--encogida {
    position: fixed;
    top: -$tamano-encogida;
    z-index: 12;
  }

  h1 {
    font-size: 45px;
    letter-spacing: 0.2em;
    margin: 20px 0;
    text-transform: uppercase;
  }

  &__contenido {
    align-items: center;
    display: flex;
    flex-direction: column;
    height: $tamano-contenido;
    left: 0;
    margin-top: 78px;
    padding: 0 20px;
    position: fixed;
    width: 100%;
    z-index: 2;
  }

  &,
  &__fondo--nitido {
    background-position: bottom center;
    background-repeat: no-repeat;
    background-size: cover;
  }

  &__fondo--nitido {
    background-image: url(https://www.funcion13.com/content/images/2015/01/default-bg.jpg);
    height: $tamano-cabecera;
    left: 0;
    opacity: 1;
    position: absolute;
    top: 0;
    width: 100%;
  }

  &__capa {
    background-color: $color-fondo;
    bottom: 0;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
    z-index: 1;
  }
}

.funcion13-logo {
  height: 50px;
  left: 20px;
  margin: 30px auto 0;
  position: fixed;
  right: 20px;
  text-align: center;
  z-index: 15;
}

.logo {
  height: 42px;
  width: 42px;
}

.relleno {
  height: $tamano-cabecera;
  position: relative;
  top: 0;
  width: 100%;
  z-index: -2;
}

Nota: ¿No te suena de nada este tipo de sintaxis? ¡Es SCSS! Echa un vistazo a la introducción a Sass y a SCSS para descubrir cómo puedes usar esta nueva sintaxis.

El código JavaScript

Vamos a ir poco a poco con el código JavaScript para que no se nos atragante. Primero voy a mostrar una sencilla clase que creé para evitar repetición de código en este caso, ya que hay que obtener varios elementos por clase CSS y, para no andar escribiendo document.getElementsByClassName(CLASE)[0], creamos una especie de jQuery:

  class $ {
    static obtenerElementoPorClase (clase) {
      return document.getElementsByClassName(clase)[0]
    }
  }

Como ves, es solo una función estática que se encarga de obtener el primer elemento de una clase (ya que getElementsByClassName devuelve una matriz de elementos aunque solo haya uno).

Ahora veamos la clase Cabecera. ¿Preparados? ¿Listos?

class Cabecera {  
    constructor() {
      this.el = $.obtenerElementoPorClase('cabecera');
      this.elementos = {
        contenido: $.obtenerElementoPorClase('cabecera__contenido'),
        boton: $.obtenerElementoPorClase('boton'),
        fondo: $.obtenerElementoPorClase('cabecera__fondo--nitido')
      };
      this.encogida = false;
      this.tweens = {
        contenido: {
          tween: null,
          px: 200
        },
        boton: {
          tween: null,
          px: 100
        },
        fondo: {
          tween: null,
          px: 250
        }
      };
      this.umbralCabecera = 225;
      this.CLASES = {
        CABECERA_ENCOGIDA: 'cabecera--encogida'
      };
      this.enScroll = this.enScroll_.bind(this);

      window.addEventListener('scroll', this.enScroll);

      this.crearTweens();
      this.enScroll();
    }

    crearTweens() {
      let configTween = {
        paused: true,
        css: {
          opacity: 0
        },
        ease: Cubic.easeInOut
      };

      this.tweens.contenido.tween =
          TweenLite.to(this.elementos.contenido, 1, configTween);
      this.tweens.boton.tween =
          TweenLite.to(this.elementos.boton, 1, configTween);
      this.tweens.fondo.tween =
          TweenLite.to(this.elementos.fondo, 1, configTween);
    }

    enScroll_() {
      var posicionY = window.pageYOffset;

      if (!this.encogida) {
        if (posicionY >= this.umbralCabecera) {
          this.encoger();
        }
      } else {
        if (posicionY <= this.umbralCabecera) {
          this.agrandar();
        }
      }

      this.actualizarTweens(posicionY);
    }

    actualizarTweens(posicion) {
      for (let nombreTween in this.tweens) {
        if (this.tweens.hasOwnProperty(nombreTween)) {
          let objetoTween = this.tweens[nombreTween];
          let progresoTween = (1 / objetoTween.px) * posicion;

          if (progresoTween > 1) {
            objetoTween.tween.progress(1);
          } else if (progresoTween < 0) {
            objetoTween.tween.progress(0);
          } else {
            objetoTween.tween.progress(progresoTween);
          }
        }
      }
    }

    agrandar() {
      this.el.classList.remove(this.CLASES.CABECERA_ENCOGIDA);
      this.encogida = false;
    }

    encoger() {
      this.el.classList.add(this.CLASES.CABECERA_ENCOGIDA);
      this.encogida = true;
    }
  }

¡Uff! Parece más de lo que es, ¡créeme! Vamos una a una:

En el constructor, guardamos referencias a los elementos que vamos a necesitar, así como del elemento principal. Además, tenemos una variable que nos chiva si la cabecera está o no encogida. Igualmente, creamos un diccionario de Tweens en el que luego irá el propio objeto creado por TweenLite pero guardamos la distancia de scroll en la que queremos que el elemento quede totalmente oculto. El umbralCabecera es la distancia a la cual la Cabecera se achicará. Terminando, creamos un diccionario con las clases que vayamos a utilizar, así como una versión bind de la función interna enScroll_, de manera que cuando sea llamada con el evento scroll, tenga el contexto de la instancia de la clase, y no de window. Finalmente llamamos a crearTweens y a enScroll para asegurarnos de que hayamos aterrizado como hayamos aterrizado, se quede en un buen estado.

La función crearTween se encarga de crear todos los objetos TweenLite, usando los elementos que guardamos en el constructor, 1 como duración y la configuración que es igual para todos, simplemente queremos que pierda la opacidad.

La función enScroll_ es la que será llamada cada vez que el evento scroll sea disparado en el objeto window. Únicamente nos encargamos de hacernos con la distancia que hemos recorrido y, en función de si estamos en un umbral o no, llamaremos a encoger o agrandar y, por último, actualizarTweens para que vayamos logrando el efecto conforme vayamos pasando por la página.

actualizarTweens se encarga de actualizar el progreso de cada Tween, simplemente iterando por cada uno, y calculando el porcentaje que se ha recorrido (que se almacena en la propiedad progresoTween). Para evitar errores, no se debe establecer un progreso por encima de 1 ni por debajo de 0, en cualquier otro caso, los progresos serán correctos.

agrandar y encoger son funciones muy sencillas que lo único que hacen es cambiar el valor de la variable encogida y añadir o quitar una clase, que es la que se encarga de que la cabecera se comporte de una u otra manera.

¿Qué te ha parecido? ¿Fue para tanto? Veamos el resultado:

Ve la demo Cabecera animada con Greensock por Antonio Laguna (@Belelros) en CodePen.

O ve directamente a CodePen

Como ves, con poco código podemos lograr un vistoso efecto para añadir algo distintivo a nuestra página y con GreenSock, animar elementos basándonos en scrolls, es muy sencillo.

¡Espero que te haya gustado!

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!