<div class="app container-fluid">
  <div id="texture-container">
    
    <div id="canvasHovered"></div>

    <div class="planeHovered">
      
      <img data-sampler="texture0" id="texture0" src="https://i.ibb.co/kczjKzz/napoleon-bonaparte-67784.jpg" crossorigin="anonymous" />
      <img data-sampler="texture1" id="texture1" src="https://i.ibb.co/XxTySDB/personaje-edad-media-juana-arco.jpg" crossorigin="anonymous" />
       <img data-sampler="texture2" id="texture2" src="https://i.ibb.co/LCZwrtK/Alexander-the-Great-detail-painting-Porus-Charles.jpg" crossorigin="anonymous" />
    </div>
     <h1 class="title-page">
HISTORICAL FIGURES</h1>
     <div class="characters">
        <div class="title-text tile-item">
          <span class="character-name">NAPOLEON BONAPARTE</span>
          <span class="date-of-birth">August 15, 1769</span>
          <span class="city-of-birth">Ajaccio, France</span> 
       </div>
        <div class="divider-line tile-item"></div>
        <div class="title-text tile-item">
          <span class="character-name">JOAN OF ARC</span>
          <span class="date-of-birth">January 6, 1412</span>
          <span class="city-of-birth">Domrémy-la-Pucelle, France</span> 
        </div>
        <div class="divider-line tile-item"></div>
        <div class="title-text tile-item">          
          <span class="character-name">ALEXANDER MAGNO</span>
          <span class="date-of-birth">July 356 B.C.</span>
          <span class="city-of-birth">Pela, Greece</span> 
        </div>
        <div class="divider-line tile-item"></div>
     </div>
    
  </div>  
</div>

<script id="vs" type="x-shader/x-vertex">
    #ifdef GL_ES
    precision mediump float;
    #endif
    
    #define TAU 6.28318530718
    #define PI 3.14159265359
    #define S(a,b,n) smoothstep(a,b,n)
    
    uniform float uTime; 
    uniform vec2 uDirection;
    
    attribute vec3 aVertexPosition;
    attribute vec2 aTextureCoord;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    uniform mat4 activeTextureMatrix;
    uniform mat4 texture1Matrix;

    varying vec3 vVertexPosition;
    varying vec2 vActiveTextureCoord;
    varying vec2 vTextureCoord1;
    varying vec2 vDirection;
  
    vec2 deformationCurve(vec2 uv, vec2 direction){
      float x = sin(vActiveTextureCoord.y * PI) * direction.x;
      float y = sin(vActiveTextureCoord.x * PI) * direction.y;
      
      return vec2(x, y);
    } 

    void main() {
      vec3 vertexPosition = aVertexPosition;
      
      vDirection = uDirection;

      vActiveTextureCoord = (activeTextureMatrix * vec4(aTextureCoord, 0., 1.)).xy;

      vertexPosition.xy += deformationCurve(vActiveTextureCoord, uDirection);

      gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);
      vVertexPosition = vertexPosition;
    }
</script>
<script id="fs" type="x-shader/x-fragment">
    #ifdef GL_ES
    precision mediump float;
    #endif

    #define TAU 6.28318530718
    #define PI 3.14159265359
    #define S(a,b,n) smoothstep(a,b,n)
    #define NUM_OCTAVES 5

    uniform float uProgress;
    uniform float uAlpha;
    uniform vec2 uReso;
    uniform vec2 uMouse;
    
    varying vec3 vVertexPosition;
    varying vec2 vActiveTextureCoord;
    varying vec2 vDirection;

    uniform sampler2D activeTexture;

    void main(){
        vec2 uv0 = vActiveTextureCoord;
      
        float r = texture2D(activeTexture, uv0 + vDirection * 0.2).r;
        float g = texture2D(activeTexture, uv0).g;
        float b = texture2D(activeTexture, uv0 - vDirection * 0.2).b;
      
        gl_FragColor = vec4(vec3(r,g,b), 1.0) * uAlpha;
    }
</script>
@import url('https://fonts.googleapis.com/css?family=Playfair+Display:400,400i,700,700i,900,900i&display=swap');

@font-face{
  font-family: ailerons;
  src: url(https://rawcdn.githack.com/AlainBarrios/Fonts/682d01f44606d8c3d85367b3e259bc3d5f64c58a/Ailerons.otf?raw=true)
}

*,
*::before,
*::after {
  box-sizing: border-box;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-kerning: auto;
  -webkit-text-size-adjust: 100%;
}

:root {
  --fontSize: 2rem;
  --flexDirection: column;
}

body {
  /* make the body fits our viewport */
  width: 100%;
  height: 100vh;
  margin: 0;
  overflow: hidden;
  background-color: rgb(242, 233, 218);
  font-family: Montserrat, sans-serif;
}

img {
  max-width: 100%;
  display: block;
}

.container-fluid{
  max-width: 800px;
  margin-left: auto;
  margin-right: auto;
}

#texture-container {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  min-height: 100vh;
}

#canvasHovered {
  /* make the canvas wrapper fits the document */
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

.planeHovered {
  /* define the size of your plane */
  max-width: 250px;
  width: 100%;
  height: 50vh;
  position: absolute;
  top: 0;
  left: 0;
}

.planeHovered img {
  /* hide the img element */
  display: none;
}

.title-page{
  color: rgb(73, 48, 0);
  font-size: calc(var(--fontSize) * 0.7);
  margin-bottom: auto;
  flex-basis: 100%;
  text-align: center;
  position: relative;
  font-family: ailerons;
}

.characters {
  flex-basis: 100%;
  height: 100%;
  display: flex;
  padding-left: 15px;
  padding-right: 15px;
  flex-wrap: wrap;
  font-size: var(--fontSize);
  color: rgb(178, 116, 0);
  align-items: center;
  flex-direction: var(--flexDirection);
  position: relative;
  justify-content: center;
}

.tile-item {
  text-align: center;
  margin-top: 2rem;
  margin-bottom: 2rem;
}

.title-text {
  cursor: pointer;
  display: flex;
  padding-left: 15px;
  padding-right: 15px;
  flex-direction: column;
  flex-grow: 1;
}

.character-name{
  font-family: ailerons;
  mix-blend-mode: difference;
}

.character-name,
.date-of-birth,
.city-of-birth {
  transition: color 0.5s ease-out, opacity 0.5s ease-out;
  pointer-events: none;
}

.date-of-birth,
.city-of-birth {
  display: none;
  padding-top: 10px;
  font-size: 0.85rem;
  color: rgb(73, 48, 0);
}

.title-text:hover .character-name {
  color: rgba(255, 255, 255);
}

.title-text:hover .date-of-birth, .title-text:hover .city-of-birth{
  opacity: 0
}

.divider-line {
  background-color: rgb(73, 48, 0);
  height: 2px;
  display: none;
}

@media screen and (min-width: 600px) {
  :root {
    --fontSize: 2.3rem;
    --flexDirection: row;
  }

  .tile-item {
    width: calc(100% / 3);
  }

  .date-of-birth,
  .city-of-birth {
    display: block;
  }

  .divider-line {
    width: 20%;
    display: block;
  }
}

@media screen and (min-width: 800px) {
  :root {
    --fontSize: 2.5rem;
  }
}
class WEBGL {
  constructor(set) {
    this.canvas = set.canvas;
    this.webGLCurtain = new Curtains("canvasHovered");
    this.planeElement = set.planeElement;
    this.titles = [...document.querySelectorAll(".title-text")];
    this.mouse = {
      x: 0,
      y: 0
    };
    this.boundingEl = {};
    this.lastPosition = { x: 0, y: 0, z: 0 };
    this.strength = 0.25 // fuerza con la que se distorcionara el plano
    this.isOver = false 
    this.startTime = performance.now(); // tiempo inicial
    
    this.params = {
      vertexShader: document.getElementById("vs").textContent, 
      fragmentShader: document.getElementById("fs").textContent, 
      widthSegments: 40,
      heightSegments: 40, 
      uniforms: {
        time: {
          name: "uTime", 
          type: "1f",
          value: 0
        },
        mousepos: {
          name: "uMouse",
          type: "2f",
          value: [0, 0]
        },
        resolution: {
          name: "uReso",
          type: "2f",
          value: [innerWidth, innerHeight]
        },
        progress: {
          name: "uProgress",
          type: "1f",
          value: 0
        },
        alpha: {
          name: "uAlpha",
          type: "1f",
          value: 0
        },
        direction: {
          name: "uDirection",
          type: "2f",
          value: [0, 0]
        }
      }
    };
  }

  initPlane() {
    this.plane = this.webGLCurtain.addPlane(this.planeElement, this.params);

    this.texture = this.plane.createTexture("activeTexture");

    this.texture.setSource(this.plane.images[0]);

    this.boundingEl = this.plane.getBoundingRect(this.planeElement);
    
    if (this.plane) {
      this.plane.onReady(() => {
        this.update();
        this.initEvent();
      });
    }
  }

  lerp(a, b, n) {
    return 
  }

  update() {
    this.plane.onRender(() => {
      this.plane.uniforms.time.value = performance.now(); // valor tiempo actual
      
      // Diferencia entre posicion incial y posicion final
      const distanceX = this.mouse.x - this.lastPosition.x;
      const distanceY = this.mouse.y - this.lastPosition.y;
        
      // Diferencia entre el tiempo inicial y tiempo final
      const time = this.plane.uniforms.time.value - this.startTime;
       
      // Le agregamos la fuerza y le damos animacion
      gsap.to(this.plane.uniforms.direction.value, 1, {
        0: distanceX / time * this.strength,
        1: -(distanceY / time) * this.strength,
        ease: "power4.out"
      });

      // Pasar los valores de las variables iniciales a las variables finales
      this.lastPosition.x = this.mouse.x;
      this.lastPosition.y = this.mouse.y;
      this.startTime = this.plane.uniforms.time.value;

      // Actualiza la posicion del plano
      this.plane.updatePosition();
    });
  }

  movePlane(e) {
    const target = e.target; // Elemento seleccionado
    
    // Mover el plano
    gsap.to(this.mouse, 0.7, {
      x: e.clientX - this.boundingEl.width / 2,
      y: e.clientY - this.boundingEl.height / 2,
      onUpdate: () => {  
        this.planeElement.style.transform = `translate3d(${this.mouse.x}px, ${
          this.mouse.y
        }px, 0)`;
      }
    });

    // Revelar el plano si esta el puntero encima del texto
    if (target.classList.contains("title-text")) {
      const index = this.titles.indexOf(target);

      this.texture.setSource(this.plane.images[index]);

      if (!this.isOver) {
        gsap.to(this.plane.uniforms.alpha, 0.5, {
          value: 1,
          ease: "power4.out"
        });
        
        this.isOver = true
      }
    } else {
      if (this.isOver) {
        gsap.to(this.plane.uniforms.alpha, 0.5, {
          value: 0,
          ease: "power4.out"
        });
        
        this.isOver = false
      }
    }
  }

  initEvent() {
    window.addEventListener("mousemove", e => this.movePlane(e));
    window.addEventListener("resize", () => this.onResize());
  }

  onResize() {
    this.boundingEl = this.plane.getBoundingRect(this.planeElement);
    this.plane.uniforms.resolution.value = [innerWidth, innerHeight];
  }
}

const webgl = new WEBGL({
  canvas: document.getElementById("canvasHovered"),
  planeElement: document.getElementsByClassName("planeHovered")[0]
});

webgl.initPlane();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://www.curtainsjs.com/build/curtains.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.0.4/gsap.min.js