Creando efectos WebGL con CurtainsJS

Este artículo se centra en agregar efectos WebGL imagey videoelementos de una página web ya “completada”. Si bien existen algunos recursos útiles sobre este tema (como estos dos), espero ayudar a simplificar este tema resumiendo el proceso en unos pocos pasos:

  • Crea una página web como lo harías normalmente.
  • Renderice piezas a las que desee agregar efectos WebGL con WebGL.
  • Cree (o busque) los efectos WebGL que desea utilizar.
  • Agregue detectores de eventos para conectar su página con los efectos WebGL.

Específicamente, nos centraremos en la conexión entre páginas web normales y WebGL. ¿Qué vamos a hacer? ¿Qué tal un control deslizante de imagen arrastrable con el desplazamiento interactivo del mouse?

No cubriremos la funcionalidad principal del control deslizante ni profundizaremos en los detalles técnicos de los sombreadores WebGL o GLSL. Sin embargo, hay muchos comentarios en el código de demostración y enlaces a recursos externos si desea obtener más información.

Estamos utilizando la última versión de WebGL (WebGL2) y GLSL (GLSL 300) que actualmente no funcionan en Safari ni en Internet Explorer. Entonces, utiliza Firefox o Chrome para ver las demostraciones. Si planea usar algo de lo que cubrimos en producción, debe cargar las versiones GLSL 100 y 300 de los sombreadores y usar la versión GLSL 300 solo si curtains.renderer._isWebGL2es verdadero. Cubro esto en la demostración anterior.

Primero, cree una página web como lo haría normalmente.

Ya sabes, HTML y CSS y todo eso. En este caso, estamos creando un control deslizante de imágenes, pero es sólo a modo de demostración. No vamos a profundizar en cómo hacer un control deslizante (Robin tiene una buena publicación sobre eso). Pero esto es lo que prepararé:

  1. Cada diapositiva es igual al ancho total de la página.
  2. Después de arrastrar una diapositiva, el control deslizante continúa deslizándose en la dirección del arrastre y disminuyendo gradualmente la velocidad con el impulso.
  3. El impulso ajusta el control deslizante a la diapositiva más cercana en el punto final.
  4. Cada diapositiva tiene una animación de salida que se activa cuando comienza el arrastre y una animación de entrada que se activa cuando se detiene el arrastre.
  5. Al pasar el control deslizante, se aplica un efecto de desplazamiento similar a este vídeo.

Soy un gran admirador de la plataforma de animación GreenSock (GSAP). Es especialmente útil para nosotros aquí porque proporciona un complemento para arrastrar, uno que permite el impulso al arrastrar y otro para dividir texto por línea. Si no se siente cómodo creando controles deslizantes con GSAP, le recomendamos que dedique algún tiempo a familiarizarse con el código en la demostración anterior.

Nuevamente, esto es sólo para una demostración, pero al menos quería describir un poco el componente. Estos son los elementos DOM con los que mantendremos sincronizado nuestro WebGL.

A continuación, utilice WebGL para renderizar las piezas que contendrán efectos WebGL.

Ahora necesitamos renderizar nuestras imágenes en WebGL. Para hacer eso necesitamos:

  1. Cargue la imagen como una textura en un sombreador GLSL.
  2. Cree un plano WebGL para la imagen y aplique correctamente la textura de la imagen al plano.
  3. Coloque el plano donde está la versión DOM de la imagen y escale correctamente.

El tercer paso no es particularmente trivial usando WebGL puro porque necesitamos rastrear la posición de los elementos DOM que queremos portar al mundo WebGL mientras mantenemos las partes DOM y WebGL sincronizadas durante el desplazamiento y las interacciones del usuario.

De hecho, existe una biblioteca que nos ayuda a hacer todo esto con facilidad: CurtainsJS. Es la única biblioteca que encontró que crea fácilmente versiones WebGL de imágenes y vídeos DOM y los sincronizan sin muchas otras funciones (pero me encantaría que me demuestren que estoy equivocado en ese punto, así que deja un comentario si conoces otras). que lo hacen bien).

Con Curtains, este es todo el JavaScript que necesitamos agregar:

// Create a new curtains instanceconst curtains = new Curtains({ container: "canvas", autoRender: false });// Use a single rAF for both GSAP and Curtainsfunction renderScene() {  curtains.render();}gsap.ticker.add(renderScene);// Params passed to the curtains instanceconst params = {  vertexShaderID: "slider-planes-vs", // The vertex shader we want to use  fragmentShaderID: "slider-planes-fs", // The fragment shader we want to use   // Include any variables to update the WebGL state here  uniforms: {    // ...  }};// Create a curtains plane for each slideconst planeElements = document.querySelectorAll(".slide");planeElements.forEach((planeEl, i) = {  const plane = curtains.addPlane(planeEl, params);  // const plane = new Plane(curtains, planeEl, params); // v7 version  // If our plane has been successfully created  if(plane) {    // onReady is called once our plane is ready and all its texture have been created    plane.onReady(function() {      // Add a "loaded" class to display the image container      plane.htmlElement.closest(".slide").classList.add("loaded");    });  }});

También necesitamos actualizar nuestra updateProgressfunción para que actualicemos nuestros planos WebGL.

function updateProgress() {  // Update the actual slider  animation.progress(wrapVal(this.x) / wrapWidth);    // Update the WebGL slider planes  planes.forEach(plane = plane.updatePosition());}

También necesitamos agregar un sombreador de fragmentos y vértices muy básico para mostrar la textura que estamos cargando. Podemos hacerlo cargándolos mediante scriptetiquetas, como hago en la demostración, o usando comillas invertidas como muestra en la demostración final.

Nuevamente, este artículo no entrará en muchos detalles sobre los aspectos técnicos de estos sombreadores GLSL. Recomiendo leer The Book of Shaders y el tema WebGL sobre Codrops como puntos de partida.

Si no sabe mucho sobre sombreadores, basta con decir que el sombreador de vértices posiciona los planos y el sombreador de fragmentos procesa los píxeles de la textura. También hay tres prefijos variables que quiero señalar:

  • inLos mensajes de correo electrónico se pasan desde un búfer de datos. En los sombreadores de vértices, provienen de la CPU (nuestro programa). En los sombreadores de fragmentos, provienen del sombreador de vértices.
  • uniformLos mensajes se pasan desde la CPU (nuestro programa).
  • outs son salidas de nuestros sombreadores. En los sombreadores de vértices, se pasan a nuestro sombreador de fragmentos. En los sombreadores de fragmentos, se pasan al búfer de fotogramas (lo que se dibuja en la pantalla).

Una vez que hayamos agregado todo eso a nuestro proyecto, tendremos exactamente lo mismo antes, ¡pero nuestro control deslizante ahora se muestra a través de WebGL! Limpio.

CurtainsJS convierte fácilmente imágenes y vídeos a WebGL. En cuanto a agregar efectos WebGL al texto, existen varios métodos diferentes, pero quizás el más común sea dibujar el texto en a canvasy luego usarlo como textura en el sombreador (por ejemplo, 1, 2). Es posible hacer la mayoría de los demás HTML usando html2canvas (o similar) y usar ese lienzo como textura en el sombreador; sin embargo, esto no es muy eficaz.

Crea (o encuentra) los efectos WebGL para usar

Ahora podemos agregar efectos WebGL ya que tenemos nuestro control deslizante renderizado con WebGL. Analicemos los efectos que se ven en nuestro video de inspiración:

  1. Los colores de la imagen están invertidos.
  2. Hay un radio alrededor de la posición del mouse que muestra el color normal y crea un efecto de ojo de pez.
  3. El radio alrededor del mouse se anima desde 0 cuando se pasa el cursor sobre el control deslizante y vuelve a animarse a 0 cuando ya no está sobre él.
  4. El radio no salta a la posición del mouse, sino que se anima allí con el tiempo.
  5. La imagen completa se traduce según la posición del mouse con respecto al centro de la imagen.

Al crear efectos WebGL, es importante recordar que los sombreadores no tienen un estado de memoria que exista entre fotogramas. Puede hacer algo en función de dónde está el mouse en un momento dado, pero no puede hacer algo en función de dónde ha estado el mouse por sí solo. Es por eso que para ciertos efectos, como animar el radio una vez que el mouse ha ingresado al control deslizante o animar la posición del radio a lo largo del tiempo, debemos usar una variable JavaScript y pasar ese valor a cada cuadro del control deslizante. Hablaremos más sobre ese proceso en la siguiente sección.

Una vez que modifiquemos nuestros sombreadores para invertir el color fuera del radio y crear el efecto de ojo de pez dentro del radio, obtendremos algo como la demostración a continuación. Nuevamente, el objetivo de este artículo es centrarse en la conexión entre los elementos DOM y WebGL, por lo que no entraré en detalles sobre los sombreadores, pero sí les agregué comentarios.

Pero eso no es demasiado interesante todavía porque el radio no reacciona a nuestro mouse. Eso es lo que cubriremos en la siguiente sección.

No he encontrado un repositorio con muchos sombreadores WebGL prediseñados para usar en sitios web normales. Están ShaderToy y VertexShaderArt (¡que tienen algunos sombreadores realmente sorprendentes!), pero ninguno está dirigido al tipo de efectos que se adaptan a la mayoría de los sitios web. Realmente me gustaría ver a alguien crear un repositorio de sombreadores WebGL como recurso para las personas que trabajan en sitios cotidianos. Si conoces alguno, por favor házmelo saber.

Agregue detectores de eventos para conectar su página con los efectos WebGL

¡Ahora podemos agregar interactividad a la parte WebGL! Necesitamos pasar algunas variables (uniformes) a nuestros sombreadores y afectar esas variables cuando el usuario interactúa con nuestros elementos. Esta es la sección donde entraré en más detalles porque es el núcleo de cómo conectamos JavaScript a nuestros sombreadores.

Primero, necesitamos declarar algunos uniformes en nuestros sombreadores. Sólo necesitamos la posición del mouse en nuestro sombreador de vértices:

// The un-transformed mouse positionuniform vec2 uMouse;

Necesitamos declarar el radio y la resolución en nuestro sombreador de fragmentos:

uniform float uRadius; // Radius of pixels to warp/invertuniform vec2 uResolution; // Used in anti-aliasing

Luego agreguemos algunos valores para estos dentro de los parámetros que pasamos a nuestra instancia de Curtains. ¡Ya estábamos haciendo esto por uResolution! Necesitamos especificar namela variable en el sombreador, es typey luego el inicio value:

const params = {  vertexShaderID: "slider-planes-vs", // The vertex shader we want to use  fragmentShaderID: "slider-planes-fs", // The fragment shader we want to use    // The variables that we're going to be animating to update our WebGL state  uniforms: {    // For the cursor effects    mouse: {       name: "uMouse", // The shader variable name      type: "2f",     // The type for the variable - https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html      value: mouse    // The initial value to use    },    radius: {       name: "uRadius",      type: "1f",      value: radius.val    },        // For the antialiasing    resolution: {       name: "uResolution",      type: "2f",       value: [innerWidth, innerHeight]     }  },};

¡Ahora el sombreador uniformsestá conectado a nuestro JavaScript! En este punto, necesitamos crear algunos detectores de eventos y animaciones para afectar los valores que pasamos a los sombreadores. Primero, configuremos la animación para el radio y la función para actualizar el valor que pasamos a nuestro sombreador:

const radius = { val: 0.1 };const radiusAnim = gsap.from(radius, {   val: 0,   duration: 0.3,   paused: true,  onUpdate: updateRadius});function updateRadius() {  planes.forEach((plane, i) = {    plane.uniforms.radius.value = radius.val;  });}

Si reproducimos la animación del radio, nuestro sombreador usará el nuevo valor en cada tick.

También necesitamos actualizar la posición del mouse cuando está sobre nuestro control deslizante tanto para dispositivos de mouse como para pantallas táctiles. Hay mucho código aquí, pero puedes recorrerlo de forma bastante lineal. Tómate tu tiempo y procesa lo que está sucediendo.

const mouse = new Vec2(0, 0);function addMouseListeners() {  if ("ontouchstart" in window) {    wrapper.addEventListener("touchstart", updateMouse, false);    wrapper.addEventListener("touchmove", updateMouse, false);    wrapper.addEventListener("blur", mouseOut, false);  } else {    wrapper.addEventListener("mousemove", updateMouse, false);    wrapper.addEventListener("mouseleave", mouseOut, false);  }}
// Update the stored mouse position along with WebGL "mouse"function updateMouse(e) {  radiusAnim.play();    if (e.changedTouches  e.changedTouches.length) {    e.x = e.changedTouches[0].pageX;    e.y = e.changedTouches[0].pageY;  }  if (e.x === undefined) {    e.x = e.pageX;    e.y = e.pageY;  }    mouse.x = e.x;  mouse.y = e.y;    updateWebGLMouse();}
// Updates the mouse position for all planesfunction updateWebGLMouse(dur) {  // update the planes mouse position uniforms  planes.forEach((plane, i) = {    const webglMousePos = plane.mouseToPlaneCoords(mouse);    updatePlaneMouse(plane, webglMousePos, dur);  });}
// Updates the mouse position for the given planefunction updatePlaneMouse(plane, endPos = new Vec2(0, 0), dur = 0.1) {  gsap.to(plane.uniforms.mouse.value, {    x: endPos.x,    y: endPos.y,    duration: dur,    overwrite: true,  });}
// When the mouse leaves the slider, animate the WebGL "mouse" to the center of sliderfunction mouseOut(e) {  planes.forEach((plane, i) = updatePlaneMouse(plane, new Vec2(0, 0), 1) );    radiusAnim.reverse();}

También deberíamos modificar nuestra updateProgressfunción existente para mantener nuestro mouse WebGL sincronizado.

// Update the slider along with the necessary WebGL variablesfunction updateProgress() {  // Update the actual slider  animation.progress(wrapVal(this.x) / wrapWidth);    // Update the WebGL slider planes  planes.forEach(plane = plane.updatePosition());    // Update the WebGL "mouse"  updateWebGLMouse(0);}

¡Ahora estamos cocinando con fuego! Nuestro control deslizante ahora cumple con todos nuestros requisitos.

Dos beneficios adicionales de usar GSAP para tus animaciones es que proporciona acceso a devoluciones de llamada, como onCompletey GSAP mantiene todo perfectamente sincronizado sin importar la frecuencia de actualización (por ejemplo, esta situación).

¡Tú lo tomas desde aquí!

Esto es, por supuesto, sólo la punta del iceberg en lo que respecta a lo que podemos hacer con el control deslizante ahora que está en WebGL. Por ejemplo, se pueden agregar efectos comunes como turbulencia y desplazamiento a las imágenes en WebGL. El concepto central de un efecto de desplazamiento es mover píxeles según un mapa de luz degradado que utilizamos como fuente de entrada. Podemos usar esta textura (que saqué de esta demostración de desplazamiento de Jesper Landberg; deberías seguirlo) como nuestra fuente y luego conectarla a nuestro sombreador.

Para obtener más información sobre cómo crear texturas como estas, consulte este artículo, este tweet y esta herramienta. No conozco ningún repositorio de imágenes como estos, pero si conoce alguno, hágamelo saber.

Si conectamos la textura de arriba y animamos la potencia y la intensidad del desplazamiento para que varíen con el tiempo y en función de nuestra velocidad de arrastre, entonces crearemos un agradable efecto de desplazamiento semi-aleatorio, pero de apariencia natural:

También vale la pena señalar que Curtains tiene su propia versión React si así es como te gusta hacerlo.

Eso es todo lo que tengo por ahora. Si creas algo usando lo que has aprendido en este artículo, ¡me encantaría verlo! Conéctate conmigo a través de Twitter.

Deja un comentario

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

Subir