<main class="wrapper">
  <div class="canvas"></div>
  <section class="slides multi-textures">
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement4.jpg" data-sampler="displacement" />
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/city/amsterdam.jpg" />
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/city/bilbao.jpg" />
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/city/golden-gate-bridge.jpg" />
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/city/valencia.jpg" />
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/city/water.jpg" />
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/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>

<div class="open-modal">
  <button class="js-open-modal" type="button">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
  </button>
  <button class="js-close-modal" type="button">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
  </button>
</div>

<div class="modal">
  <div class="modal__content">
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement4.jpg" data-setting class="active" alt="">
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement1.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement2.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement3.png" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement5.png" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement6.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement7.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement8.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement9.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement10.jpg" data-setting alt="">      
    <img src="https://raw.githubusercontent.com/ivanalbizu/WEBGL-texture-Slideshow/main/src/img/displacement11.jpg" data-setting alt="">      
  </div>
</div>
html,
body {
	min-height: 100%;
}
body {
	margin: 0;
	font-family: sans-serif;
	background: #ffffff;
	overflow: hidden;
}
.slides {
	display: flex;
	flex-wrap: wrap;
}
.wrapper {
	width: 100%;
	height: 100vh;
	overflow: hidden;
	display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  grid-gap: 0;
}
.canvas {
	height: 100vh;
	width: 100vw;
	z-index: 10;
	grid-area: 1 / 1 / 1 / 1;
}
.multi-textures {
	grid-area: 1 / 1 / 1 / 1;
	z-index: 15;
	align-content: center;
	img {
		display: none;
		min-width: 100%;
		min-height: 100%;
		object-fit: cover;
	}
}
.no-curtains {
	.multi-textures {
		overflow: hidden;
		display: flex;
		align-items: center;
		justify-content: center;
	}
	&.image-1 .multi-textures img:nth-child(2) {
		display: block;
	}
	&.image-2 .multi-textures img:nth-child(3) {
		display: block;
	}
	&.image-3 .multi-textures img:nth-child(4) {
		display: block;
	}
	&.image-4 .multi-textures img:nth-child(5) {
		display: block;
	}
	&.image-5 .multi-textures img:nth-child(6) {
		display: block;
	}
}

.nav {
  display: flex;
  justify-content: space-between;
	width: 100%;
}
.btn {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: rgba(0, 0, 0, 0.9);
  color: #fff;
  border: 1px solid rgba(0, 0, 0, 0.2);
  width: 4em;
  height: 4em;
	margin: 0.5em .2em;
	cursor: pointer;
	transition: all .7s ease-in 0s;
	svg,
	svg path {
		pointer-events: none;
	}
}


// Not necessary. Use to show example textures animations
.open-modal {
  position: fixed;
  top: 4px;
  right: 18px;
	background-color: rgba(0, 0, 0, .9);
  z-index: 18;
  button {
		display: flex;
		align-items: center;
		justify-content: center;
		width: 32px;
		height: 32px;
		cursor: pointer;
  }
  .js-close-modal {
    display: none;
  }
}

.modal {
  position: fixed;
  right: 0;
  width: 300px;
  top: 0;
  bottom: 0;
	z-index: 17;
	overflow-y: auto;
	transform: translateX(300px);
	transition: transform .3s ease-in-out;
  img {
		max-width: 100%;
		z-index: 19;
  }

  &__content {
    display: flex;
    flex-wrap: wrap;
    overflow-y: auto;
    background-color: rgba(0, 0, 0, .85);
    padding: 2.5em 1.5em;
    img {
			border: 2px dashed #fff;
      width: 235px;
      height: 155px;
			&:not(:last-of-type) {
				margin-bottom: 2em;
			}
      &.active {
        border-style: solid;
      }
    }
  }
}
.modal-active {
	.open-modal {
		.js-close-modal {
      display: flex;
    }
    .js-open-modal {
			display: none;
    }
  }
  .modal {
		transform: translateX(0px);
		transition: transform .3s ease-in-out;
  }
}
View Compiled
import {Curtains, Plane} from 'https://cdn.jsdelivr.net/npm/curtainsjs@7.1.0/src/index.mjs';
//import fragment from './shaders/fragment.glsl'; //path to local file, instead of creating the variables below
//import vertex from './shaders/vertex.glsl'; //path to local file, instead of creating the variables below

const fragment = `
precision mediump float;

varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
varying vec2 vActiveTextureCoord;
varying vec2 vNextTextureCoord;

// custom uniforms
uniform float uTransitionTimer;

// our textures samplers
// notice how it matches the sampler attributes of the textures we created dynamically
uniform sampler2D activeTexture;
uniform sampler2D nextTexture;
uniform sampler2D displacement;

void main() {
    // our displacement texture
    vec4 displacementTexture = texture2D(displacement, vTextureCoord);

    // slides transitions based on displacement and transition timer
    vec2 firstDisplacementCoords = vActiveTextureCoord + displacementTexture.r * ((cos((uTransitionTimer + 90.0) / (90.0 / 3.141592)) + 1.0) / 1.25);
    vec4 firstDistortedColor = texture2D(activeTexture, vec2(vActiveTextureCoord.x, firstDisplacementCoords.y));

    // same as above but we substract the effect
    vec2 secondDisplacementCoords = vNextTextureCoord - displacementTexture.r * ((cos(uTransitionTimer / (90.0 / 3.141592)) + 1.0) / 1.5);
    vec4 secondDistortedColor = texture2D(nextTexture, vec2(vNextTextureCoord.x, secondDisplacementCoords.y));

    // mix both texture
    vec4 finalColor = mix(firstDistortedColor, secondDistortedColor, 1.0 - ((cos(uTransitionTimer / (90.0 / 3.141592)) + 1.0) / 2.0));

    // handling premultiplied alpha
    finalColor = vec4(finalColor.rgb * finalColor.a, finalColor.a);

    gl_FragColor = finalColor;
}
`;

const vertex = `
precision mediump float;

// default mandatory variables
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

// textures matrices
uniform mat4 activeTextureMatrix;
uniform mat4 nextTextureMatrix;

// varyings : notice we've got 3 texture coords varyings
// displacement texture / visible texture / upcoming texture
varying vec3 vVertexPosition;
varying vec2 vTextureCoord; // displacement
varying vec2 vActiveTextureCoord;
varying vec2 vNextTextureCoord;

// custom uniforms
uniform float uTransitionTimer;


void main() {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

    // varyings
    // use original texture coords for our displacement
    vTextureCoord = aTextureCoord;
    vActiveTextureCoord = (activeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
    vNextTextureCoord = (nextTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;

    vVertexPosition = aVertexPosition;
}
`; 

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: vertex,
			fragmentShader: fragment,
			uniforms: {
				transitionTimer: {
					name: "uTransitionTimer",
					type: "1f",
					value: 0,
				},
			},
		}

		this.init()
	}

	init() {
		this.setupCurtains()
		this.initPlane()
		this.update()
	}

	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());
	}

	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)

			})
	}

	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;
		});
	}

	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);
				}
			})
		})
	}

	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
			}
		}
	}

	error() {
		document.body.classList.add("no-curtains", "image-1");

		this.slidesState.navs.forEach(nav => {
			nav.addEventListener("click", event => {
				const to = event.target.getAttribute('data-goto');
				navigationDirection(to);

				document.body.classList.remove("image-1", "image-2", "image-3", "image-4");
				document.body.classList.add("image-" + this.slidesState.nextTextureIndex);

				this.slidesState.activeTextureIndex = this.slidesState.nextTextureIndex;

			});
		})
	}

	restoreContext() {
		this.curtains.restoreContext();
	}

	removePlanes() {
		this.curtains.dispose();
	}

}

window.addEventListener("load", () => {
	const wrapper = document.querySelector('.wrapper')
	const canvas = wrapper.querySelector('.canvas')
	const planeElement = wrapper.querySelector('.multi-textures')
	const navs = wrapper.querySelectorAll('[data-goto]')
	let slide = new WebglSlides({
		canvas,
		planeElement,
		navs
	})
  
  // Down, not necesary, only for change Displacements Texture
	document.querySelector('.js-open-modal').addEventListener('click', () => {
    document.body.classList.add('modal-active')
  })
  document.querySelector('.js-close-modal').addEventListener('click', () => {
    document.body.classList.remove('modal-active')
	})

	const settings = document.querySelectorAll('[data-setting]');
	settings.forEach(setting => {
		setting.addEventListener('click', event => {
			const target = event.target;
			const path = target.getAttribute('src')
			settings.forEach(setting => setting.classList.remove('active'))
			target.classList.add('active')
			console.log('path :>> ', path);
			document.querySelector('[data-sampler]').src = path
	
			slide.removePlanes()
			slide = new WebglSlides({
				canvas,
				planeElement,
				navs
			})
		})
	})
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.