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/
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 elementocanvas
con 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. ;)