Ha sido mi primera toma de contacto con WebGL, algo que siempre quise mirarme ya que por ahí veía animaciones y transiciones bastante chulas. Aún sigo toqueteando, es un mundo. Mi idea era hacerlo sin ningún framework tipo ThreeJS o PixiJS, pero a la vez que veía que era muy trabajoso y requería de mucho esfuerzo, descubrí una librería muy simple y fácil de usar: https://www.curtainsjs.com/. Recomiendo ojearla, trastearla y empezar a usar los ejemplos que tienen creada en la Doc.
Si decides clonar mi repo, que tengo en el footer, te recomiendo que lo arranques con https://parceljs.org/
See the Pen Animación de slideshow usando WebGL con librería curtainsJS by Iván Albizu (@ivan_albizu) on CodePen.
Recursos interesantes, al menos para un novato como yo en WebGL
- Si no tienes idea de que es WebGL, como yo, esta presentación fue las primeras que ví: https://www.youtube.com/watch?v=jkOnDTAFmD4
 - Tutorial paso a paso para trabajar con WebGL 2: https://webgl2fundamentals.org/
 - De un crack, Yuri Artyukh
- Canal de youtube: https://www.youtube.com/user/flintyara
 - Su Github: https://github.com/akella
 
 - Ejemplos muy PRO: http://acko.net/
 - Ejemplos en Codrops: https://tympanus.net/codrops/tag/webgl/
 
Propiedades de clase
En el constructor de la clase WebglSlides definimos atributos necesarios
class WebglSlides {
	constructor(set) {
		this.canvas = set.canvas
		this.planeElement = set.planeElement
		this.multiTexturesPlane = null
		this.slidesState = {
			activeTextureIndex: 1,
			nextTextureIndex: null,
			maxTextures: set.planeElement.querySelectorAll("img").length - 1, // -1 to displacement
			navs: set.navs,
			isChanging: false,
			transitionTimer: 0,
		}
		this.params = {
			vertexShader: document.getElementById("vs")?.textContent || vertex,
			fragmentShader: document.getElementById("fs")?.textContent || fragment,
			uniforms: {
				transitionTimer: {
					name: "uTransitionTimer",
					type: "1f",
					value: 0,
				},
			},
		}
		this.init()
	}
}
La instancia de la clase nos dara el contexto WebGL y añade el canvas a nuestro envolvente. Recibirá como parámetro un objeto con los elementos del DOM:
<div class=“canvas”>: envolvente donde se creará el elementocanvascon curtainsJS<section class=“slides multi-textures”>: donde colocaremos las imágenes del slideshow y que serán usadas con curtainsJS para crear las texturas<button class=“btn” data-goto=“prev o next” type=“button”>: botones para controlar la paginación de los slides
Los tres primeros atributos son las referencias al canvas, planos y texturas.
Con this.slidesState =  definimos la textura activa, la paginación, el estado de la transición y controlar los tiempos en la transformacion de los shaders.
Con this.params =  definimos los shaders. Los ficheros que contienen los shaders son importados con ES6, también se pueden incluir como etiquetas script. Para poder acceder y realizar las modificaciones sobre los vertex lo haremos accediendo con this.params.uniforms.transitionTimer.name = uTransitionTimer
Finalmente llamamos al método init() para iniciar el slideshow. Este método hace la llamada a otros tres métodos.
Iniciar curtains
setupCurtains() {
	this.curtains = new Curtains({
		container: this.canvas,
		watchScroll: false,
		pixelRatio: Math.min(1.5, window.devicePixelRatio)
	})
	this.curtains.onError(() => this.error());
	this.curtains.onContextLost(() => this.restoreContext());
}
Instanciamos Curtains con tres parámetros
El primera parámetro, container será la referencia el elemento del DOM
El segundo parámetro, watchScroll lo fijamos a false ya que no necesitamos hacer escuchas de scroll para el slideshow
El tercero, pixelRatio optimiza la animación al dispositivo del usuario
Iniciar planos y texturas
initPlane() {
  this.multiTexturesPlane = new Plane(this.curtains, this.planeElement, this.params)
  this.multiTexturesPlane
    .onLoading(texture => {
      texture.setMinFilter(this.curtains.gl.LINEAR_MIPMAP_NEAREST)
    })
    .onReady(() => {
      const activeTexture = this.multiTexturesPlane.createTexture({
        sampler: "activeTexture",
        fromTexture: this.multiTexturesPlane.textures[this.slidesState.activeTextureIndex]
      })
      const nextTexture = this.multiTexturesPlane.createTexture({
        sampler: "nextTexture",
        fromTexture: this.multiTexturesPlane.textures[this.slidesState.nextTextureIndex]
      })
      this.initEvent(activeTexture, nextTexture)
    })
}
Se guarda la instancia del Plano en multiTexturesPlane. Al instanciarlo, le pasamos tres parámetros: instancia de curtains, las imágenes y los shaders
Usamos dos métodos de curtainjs para los Planos
En la carga le especificamos LINEAR_MIPMAP_NEAREST para que el “rellenado” de pixel sea más perfecta
Cuando ha cargado, creamos dos texturas, una será la textura actual y la otra será la siguiente. Al crear las dos texturas, en la key sampler apuntamos a sus nombres en los fragments y vertex shaders activeTexture y nextTexture
Actualizar texturas
update() {
	this.multiTexturesPlane.onRender(() => {
		if (this.slidesState.isChanging) {
			this.slidesState.transitionTimer += (90 - this.slidesState.transitionTimer) * 0.04;
			if (this.slidesState.transitionTimer >= 88.5 && this.slidesState.transitionTimer !== 90) {
				this.slidesState.transitionTimer = 90;
			}
		}
		this.multiTexturesPlane.uniforms.transitionTimer.value = this.slidesState.transitionTimer;
	});
}
Una vez que ya tenemos las texturas, llamamos al método onRender() para hacer modificaciones en función del tiempo sobre la textura activa
Registrar eventos click
initEvent(activeTexture, nextTexture) {
  this.slidesState.navs.forEach(nav => {
    nav.addEventListener('click', event => {
      if (!this.slidesState.isChanging) {
        this.curtains.enableDrawing()
        this.slidesState.isChanging = true;
        const to = event.target.getAttribute('data-goto');
        this.navigationDirection(to);
        nextTexture.setSource(this.multiTexturesPlane.images[this.slidesState.nextTextureIndex]);
        setTimeout(() => {
          this.curtains.disableDrawing();
          this.slidesState.isChanging = false;
          this.slidesState.activeTextureIndex = this.slidesState.nextTextureIndex;
          activeTexture.setSource(this.multiTexturesPlane.images[this.slidesState.activeTextureIndex]);
          this.slidesState.transitionTimer = 0;
        }, 1700);
      }
    })
  })
}
Se registra evento a los botones con atributos data-goto y se detecta el valor next o prev para decidir si será la textura prevía o siguiente. Esta lógica es realizada con el método navigationDirection(to)
Al animación se inicia si detectamos que actualmente no se está produciendo la animación if (!this.slidesState.isChanging). Cambiamos la textura siguiente mediente nextTexture.setSource(this.multiTexturesPlane.images[this.slidesState.nextTextureIndex]) y dentro de timeOut actualizamos la textura siguiente activeTexture.setSource(this.multiTexturesPlane.images[this.slidesState.activeTextureIndex]). Finalmente, reseteamos los tiempos this.slidesState.transitionTimer = 0 que fueron modificados en otro método
Detectar la textura a cargar
navigationDirection(to) {
  if (to == 'next') {
    if (this.slidesState.activeTextureIndex < this.slidesState.maxTextures) {
      this.slidesState.nextTextureIndex = this.slidesState.activeTextureIndex + 1
    } else {
      this.slidesState.nextTextureIndex = 1
    }
  } else {
    if (this.slidesState.activeTextureIndex <= 1) {
      this.slidesState.nextTextureIndex = this.slidesState.maxTextures
    } else {
      this.slidesState.nextTextureIndex = this.slidesState.activeTextureIndex - 1
    }
  }
}
Este método sólo es para detectar la Textura que se debe cargar, y actualizar los índices. Este método es llamda en dos ocasiones, para la actualización de textura o para cargar una alternativa de slider caso de que fallase en algún momento curtainsjs
Html del WebGL slideshow
<main class="wrapper">
  <div class="canvas"></div>
  <section class="slides multi-textures">
    <img src="./src/img/displacement4.jpg" data-sampler="displacement">
    <img src="./src/img/city/amsterdam.jpg">
    <img src="./src/img/city/bilbao.jpg">
    <img src="./src/img/city/golden-gate-bridge.jpg">
    <img src="./src/img/city/valencia.jpg">
    <img src="./src/img/city/water.jpg">
    <img src="./src/img/city/peine.jpg">
    <nav class="nav">
      <button class="btn" data-goto="prev" type="button">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H6M12 5l-7 7 7 7"/></svg>
      </button>
      <button class="btn" data-goto="next" type="button">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h13M12 5l7 7-7 7"/></svg>
      </button>
    </nav>
  </section>
</main>
La estructura del html si es importante. La envolvente wrapper es el contenedor de todo, incluso podremos usar más de un slideshow creando más instancias.
En <div class=“canvas”> es donde la librería curtainsJS va a crear el elemento canvas
Dentro de <section class=“slides multi-textures”> colocaremos todas las imágenes. En este caso, usamos una imagen displacement para las transiciones, la primera imagen debe contener el atributo data-sampler=“displacement” para que curtains pueda interpretarlo
Código CSS del WebGL slideshow
Me ahorro la explicación, no tiene nada especial. Si se quiere ver, en el GitHub está todo. ;)