Bilder und Videos mit curtains.js animieren

Avatar of Martin Laxenaire
Martin Laxenaire am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Wenn Sie sich die neuesten preisgekrönten Websites ansehen, bemerken Sie vielleicht viele ausgefallene Bildverzerrungsanimationen oder nette 3D-Effekte. Die meisten davon werden mit WebGL erstellt, einer API, die GPU-beschleunigte Bildverarbeitungseffekte und Animationen ermöglicht. Sie verwenden oft auch Bibliotheken, die auf WebGL aufbauen, wie z. B. three.js oder pixi.js. Beide sind sehr mächtige Werkzeuge zur Erstellung von 3D- bzw. 2D-Szenen.

Bedenken Sie jedoch, dass diese Bibliotheken ursprünglich nicht für die Erstellung von Diashows oder die Animation von DOM-Elementen konzipiert wurden. Es gibt jedoch eine Bibliothek, die speziell dafür entwickelt wurde, und wir werden in diesem Beitrag behandeln, wie Sie sie verwenden.

WebGL, CSS-Positionierung und Responsivität

Angenommen, Sie arbeiten mit einer Bibliothek wie three.js oder pixi.js und möchten sie für Interaktionen wie Mouseover- und Scroll-Events auf Elementen verwenden. Dabei könnten Sie auf Probleme stoßen! Wie positionieren Sie Ihre WebGL-Elemente relativ zum Dokument und zu anderen DOM-Elementen? Wie gehen Sie mit Responsivität um?

Genau das hatte ich im Sinn, als ich curtains.js entwickelt habe.

Curtains.js ermöglicht es Ihnen, Flächen mit Bildern und Videos (in WebGL werden wir sie Texturen nennen) zu erstellen, die sich wie einfache HTML-Elemente verhalten, wobei Position und Größe durch CSS-Regeln bestimmt werden. Diese Flächen können jedoch mit den endlosen Möglichkeiten von WebGL und Shadern erweitert werden.

Moment mal, Shader?

Shader sind kleine Programme, die in GLSL geschrieben sind und Ihrer GPU mitteilen, wie Ihre Flächen gerendert werden sollen. Das Verständnis der Funktionsweise von Shadern ist hier unerlässlich, da wir damit Animationen steuern. Wenn Sie noch nie davon gehört haben, sollten Sie sich vielleicht zuerst die Grundlagen aneignen. Es gibt viele gute Websites, um mit dem Erlernen zu beginnen, wie z. B. The Book of Shaders.

Nachdem Sie die Idee verstanden haben, erstellen wir unsere erste Fläche!

Einrichtung einer Basisebene

Um unsere erste Fläche anzuzeigen, benötigen wir etwas HTML, CSS und etwas JavaScript, um die Fläche zu erstellen. Dann werden unsere Shader sie animieren.

HTML

Das HTML ist hier wirklich einfach. Wir erstellen ein <div>, das unseren Canvas enthält, und ein <div>, das unser Bild enthält.

<body>
  <!-- div that will hold our WebGL canvas -->
  <div id="canvas"></div>

  <!-- div used to create our plane -->
  <div class="plane">

    <!-- image that will be used as a texture by our plane -->
    <img src="path/to/my-image.jpg" />
      
  </div>

</body>

CSS

Wir verwenden CSS, um sicherzustellen, dass das <div>, das den Canvas umschließt, das Fenster ausfüllt, und weisen der plane-div eine beliebige Größe zu. (Unsere WebGL-Fläche hat exakt die gleiche Größe und Position wie diese div.)

Wir stellen auch einige grundlegende CSS-Regeln bereit, die im Falle eines Fehlers während der Initialisierung angewendet werden.

body {
  /* make the body fit our viewport */
  position: relative;
  width: 100%;
  height: 100vh;
  margin: 0;
  
  /* hide scrollbars */
  overflow: hidden;
}

#canvas {
  /* make the canvas wrapper fit the window */
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
}

.plane {
  /* define the size of your plane */
  width: 80%;
  max-width: 1400px;
  height: 80vh;
  position: relative;
  top: 10vh;
  margin: 0 auto;
}

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

/*** in case of error show the image ***/

.no-curtains .plane {
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.no-curtains .plane img {
  display: block;
  max-width: 100%;
  object-fit: cover;
}

JavaScript

Im JavaScript gibt es etwas mehr zu tun. Wir müssen unseren WebGL-Kontext instanziieren, eine Fläche mit einheitlichen Parametern erstellen und sie verwenden. Für dieses erste Beispiel sehen wir auch, wie Fehler abgefangen werden.

window.onload = function() {
  // pass the id of the div that will wrap the canvas to set up our WebGL context and append the canvas to our wrapper
  var webGLCurtain = new Curtains("canvas");

  // if there's any error during init, we're going to catch it here
  webGLCurtain.onError(function() {
    // we will add a class to the document body to display original images
    document.body.classList.add("no-curtains");
  });

  // get our plane element
  var planeElement = document.getElementsByClassName("plane")[0];

  // set our initial parameters (basic uniforms)
  var params = {
    vertexShaderID: "plane-vs", // our vertex shader ID
    fragmentShaderID: "plane-fs", // our fragment shader ID
    uniforms: {
      time: {
        name: "uTime", // uniform name that will be passed to our shaders
        type: "1f", // this means our uniform is a float
        value: 0,
      },
    }
  }

  // create our plane mesh
  var plane = webGLCurtain.addPlane(planeElement, params);

  // if our plane has been successfully created
  // we use the onRender method of our plane fired at each requestAnimationFrame call
  plane && plane.onRender(function() {
    plane.uniforms.time.value++; // update our time uniform value
  });

}

Shader

Wir müssen den Vertex-Shader schreiben. Grundsätzlich müssen wir die Position unserer Fläche basierend auf der Modell-, Ansichts- und Projektionsmatrix festlegen und die variablen Variablen an den Fragment-Shader weitergeben.

Eine dieser variablen Variablen heißt vTextureCoord und wird im Fragment-Shader verwendet, um unsere Textur auf die Fläche abzubilden. Wir könnten direkt unser aTextureCoord-Attribut übergeben, aber dann bekämen wir eine gestreckte Textur, da unsere Fläche und unser Bild nicht unbedingt das gleiche Seitenverhältnis haben. Glücklicherweise stellt die Bibliothek eine Texturmatrix-Uniform bereit, die wir verwenden könnten, um neue Koordinaten zu berechnen, die die Textur zuschneiden, sodass sie immer auf die Fläche passt (denken Sie daran als Äquivalent zu „background-size: cover“).

<!-- vertex shader -->
<script id="plane-vs" type="x-shader/x-vertex">
  #ifdef GL_ES
  precision mediump float;
  #endif

  // those are the mandatory attributes that the lib sets
  attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;

  // those are mandatory uniforms that the lib sets and that contain our model view and projection matrix
  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  // our texture matrix uniform (this is the lib default name, but it could be changed)
  uniform mat4 uTextureMatrix0;

  // if you want to pass your vertex and texture coords to the fragment shader
  varying vec3 vVertexPosition;
  varying vec2 vTextureCoord;

  void main() {
    // get the vertex position from its attribute
    vec3 vertexPosition = aVertexPosition;
    // set its position based on projection and model view matrix
    gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);

    // set the varying variables
    // thanks to the texture matrix we will be able to calculate accurate texture coords
    // so that our texture will always fit our plane without being distorted
    vTextureCoord = (uTextureMatrix0 * vec4(aTextureCoord, 0.0, 1.0)).xy;
    vVertexPosition = vertexPosition;
  }
</script>

Nun zu unserem Fragment-Shader. Hier fügen wir basierend auf unserer Zeit-Uniform und den Texturkoordinaten einen kleinen Verdrängungseffekt hinzu.

<!-- fragment shader -->
<script id="plane-fs" type="x-shader/x-fragment">
  #ifdef GL_ES
  precision mediump float;
  #endif

  // get our varying variables
  varying vec3 vVertexPosition;
  varying vec2 vTextureCoord;

  // the uniform we declared inside our javascript
  uniform float uTime;

  // our texture sampler (this is the lib default name, but it could be changed)
  uniform sampler2D uSampler0;

  void main() {
    // get our texture coords
    vec2 textureCoord = vTextureCoord;

    // displace our pixels along both axis based on our time uniform and texture UVs
    // this will create a kind of water surface effect
    // try to comment a line or change the constants to see how it changes the effect
    // reminder : textures coords are ranging from 0.0 to 1.0 on both axis
    const float PI = 3.141592;

    textureCoord.x += (
      sin(textureCoord.x * 10.0 + ((uTime * (PI / 3.0)) * 0.031))
      + sin(textureCoord.y * 10.0 + ((uTime * (PI / 2.489)) * 0.017))
      ) * 0.0075;

    textureCoord.y += (
      sin(textureCoord.y * 20.0 + ((uTime * (PI / 2.023)) * 0.023))
      + sin(textureCoord.x * 20.0 + ((uTime * (PI / 3.1254)) * 0.037))
      ) * 0.0125;
          
    gl_FragColor = texture2D(uSampler0, textureCoord);
  }
</script>

Et voilà! Sie sind fertig, und wenn alles gut gelaufen ist, sollten Sie so etwas sehen.

Siehe den Pen curtains.js basic plane von Martin Laxenaire (@martinlaxenaire) auf CodePen.

Hinzufügen von 3D und Interaktionen

Okay, das ist bisher ziemlich cool, aber wir haben diesen Beitrag mit 3D und Interaktionen begonnen, also schauen wir uns an, wie wir diese hinzufügen könnten.

Über Vertices

Um einen 3D-Effekt hinzuzufügen, müssten wir die Position der Flächen-Vertices im Vertex-Shader ändern. In unserem ersten Beispiel haben wir jedoch nicht angegeben, wie viele Vertices unsere Fläche haben soll, daher wurde sie mit einer Standardgeometrie erstellt, die sechs Vertices enthält und zwei Dreiecke bildet.

Um ordentliche 3D-Animationen zu erhalten, bräuchten wir mehr Dreiecke, also mehr Vertices.

Diese Fläche hat fünf Segmente entlang ihrer Breite und fünf Segmente entlang ihrer Höhe. Das Ergebnis sind 50 Dreiecke und insgesamt 150 Vertices.

Refactoring unseres JavaScripts

Glücklicherweise ist es einfach, unsere Flächendefinition anzugeben, da sie in unseren anfänglichen Parametern festgelegt werden kann.

Wir werden auch auf die Mausposition hören, um etwas Interaktion hinzuzufügen. Um dies richtig zu tun, müssen wir warten, bis die Fläche bereit ist, unsere Maus-Dokumenten-Koordinaten in unsere WebGL-Clip-Space-Koordinaten umwandeln und sie als Uniform an die Shader senden.

// we are using window onload event here but this is not mandatory
window.onload = function() {
  // track the mouse positions to send it to the shaders
  var mousePosition = {
    x: 0,
    y: 0,
  };

  // pass the id of the div that will wrap the canvas to set up our WebGL context and append the canvas to our wrapper
  var webGLCurtain = new Curtains("canvas");

  // get our plane element
  var planeElement = document.getElementsByClassName("plane")[0];

  // set our initial parameters (basic uniforms)
  var params = {
    vertexShaderID: "plane-vs", // our vertex shader ID
    fragmentShaderID: "plane-fs", // our framgent shader ID
    widthSegments: 20,
    heightSegments: 20, // we now have 20*20*6 = 2400 vertices !
    uniforms: {
      time: {
        name: "uTime", // uniform name that will be passed to our shaders
        type: "1f", // this means our uniform is a float
        value: 0,
      },
      mousePosition: { // our mouse position
        name: "uMousePosition",
        type: "2f", // notice this is a length 2 array of floats
        value: [mousePosition.x, mousePosition.y],
      },
      mouseStrength: { // the strength of the effect (we will attenuate it if the mouse stops moving)
        name: "uMouseStrength", // uniform name that will be passed to our shaders
        type: "1f", // this means our uniform is a float
        value: 0,
      },

    }
  }

  // create our plane mesh
  var plane = webGLCurtain.addPlane(planeElement, params);

  // if our plane has been successfully created we could start listening to mouse/touch events and update its uniforms
  plane && plane.onReady(function() {
    // set a field of view of 35 to exaggerate perspective
    // we could have done it directly in the initial params
    plane.setPerspective(35);
    // listen our mouse/touch events on the whole document
    // we will pass the plane as second argument of our function
    // we could be handling multiple planes that way
    document.body.addEventListener("mousemove", function(e) {
      handleMovement(e, plane);
    });
    document.body.addEventListener("touchmove", function(e) {
      handleMovement(e, plane);
    });
  }).onRender(function() {
    // update our time uniform value
    plane.uniforms.time.value++;
    // continually decrease mouse strength
    plane.uniforms.mouseStrength.value = Math.max(0, plane.uniforms.mouseStrength.value - 0.0075);
  });

  // handle the mouse move event
  function handleMovement(e, plane) {
    // touch event
    if(e.targetTouches) {
      mousePosition.x = e.targetTouches[0].clientX;
      mousePosition.y = e.targetTouches[0].clientY;
    }
    // mouse event
    else {
      mousePosition.x = e.clientX;
      mousePosition.y = e.clientY;
    }
    // convert our mouse/touch position to coordinates relative to the vertices of the plane
    var mouseCoords = plane.mouseToPlaneCoords(mousePosition.x, mousePosition.y);
    // update our mouse position uniform
    plane.uniforms.mousePosition.value = [mouseCoords.x, mouseCoords.y];
    
    // reassign mouse strength
    plane.uniforms.mouseStrength.value = 1;
  }

}

Nachdem unser JavaScript fertig ist, müssen wir unsere Shader umschreiben, damit sie unsere Mauspositions-Uniform verwenden.

Shader refactoring

Betrachten wir zuerst unseren Vertex-Shader. Wir haben drei Uniforms, die wir für unseren Effekt nutzen können:

  1. die Zeit, die ständig ansteigt
  2. die Mausposition
  3. unsere Mausstärke, die ständig abnimmt, bis zur nächsten Mausbewegung

Wir werden alle drei nutzen, um eine Art 3D-Ripple-Effekt zu erzeugen.

<script id="plane-vs" type="x-shader/x-vertex">
  #ifdef GL_ES
  precision mediump float;
  #endif

  // those are the mandatory attributes that the lib sets
  attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;

  // those are mandatory uniforms that the lib sets and that contain our model view and projection matrix
  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  // our texture matrix uniform (this is the lib default name, but it could be changed)
  uniform mat4 uTextureMatrix0;

  // our time uniform
  uniform float uTime;

  // our mouse position uniform
  uniform vec2 uMousePosition;

  // our mouse strength
  uniform float uMouseStrength;

  // if you want to pass your vertex and texture coords to the fragment shader
  varying vec3 vVertexPosition;
  varying vec2 vTextureCoord;

  void main() {
    vec3 vertexPosition = aVertexPosition;

    // get the distance between our vertex and the mouse position
    float distanceFromMouse = distance(uMousePosition, vec2(vertexPosition.x, vertexPosition.y));

    // this will define how close the ripples will be from each other. The bigger the number, the more ripples you'll get
    float rippleFactor = 6.0;

    // calculate our ripple effect
    float rippleEffect = cos(rippleFactor * (distanceFromMouse - (uTime / 120.0)));

    // calculate our distortion effect
    float distortionEffect = rippleEffect * uMouseStrength;

    // apply it to our vertex position
    vertexPosition +=  distortionEffect / 15.0;

    gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);

    // varying variables
    // thanks to the texture matrix we will be able to calculate accurate texture coords
    // so that our texture will always fit our plane without being distorted
    vTextureCoord = (uTextureMatrix0 * vec4(aTextureCoord, 0.0, 1.0)).xy;
    vVertexPosition = vertexPosition;
  }
</script>

Was den Fragment-Shader angeht, so halten wir ihn einfach. Wir werden Licht und Schatten basierend auf der Position jedes Vertices simulieren.

<script id="plane-fs" type="x-shader/x-fragment">
  #ifdef GL_ES
  precision mediump float;
  #endif

  // get our varying variables
  varying vec3 vVertexPosition;
  varying vec2 vTextureCoord;

  // our texture sampler (this is the lib default name, but it could be changed)
  uniform sampler2D uSampler0;

  void main() {
    // get our texture coords
    vec2 textureCoords = vTextureCoord;

    // apply our texture
    vec4 finalColor = texture2D(uSampler0, textureCoords);

    // fake shadows based on vertex position along Z axis
    finalColor.rgb -= clamp(-vVertexPosition.z, 0.0, 1.0);

    // fake lights based on vertex position along Z axis
    finalColor.rgb += clamp(vVertexPosition.z, 0.0, 1.0);

    // handling premultiplied alpha (useful if we were using a png with transparency)
    finalColor = vec4(finalColor.rgb * finalColor.a, finalColor.a);

    gl_FragColor = finalColor;
  }
</script>

Und da haben Sie es!

Siehe den Pen curtains.js ripple effect example von Martin Laxenaire (@martinlaxenaire) auf CodePen.

Mit diesen beiden einfachen Beispielen haben wir gesehen, wie man eine Fläche erstellt und mit ihr interagiert.

Videos und Displacement Shader

Unser letztes Beispiel erstellt eine einfache Vollbild-Video-Slideshow mit einem Displacement-Shader zur Verbesserung der Übergänge.

Konzept des Displacement-Shaders

Der Displacement-Shader erzeugt einen schönen Verzerrungseffekt. Er wird in unserem Fragment-Shader mithilfe eines Graustufenbildes geschrieben und verschiebt die Pixelkoordinaten der Videos basierend auf den RGB-Werten der Textur. Hier ist das Bild, das wir verwenden werden

Der Effekt wird basierend auf jedem Pixel-RGB-Wert berechnet, wobei ein schwarzer Pixel [0, 0, 0] und ein weißer Pixel [1, 1, 1] (GLSL-Äquivalent für [255, 255, 255]) ist. Zur Vereinfachung verwenden wir nur den Rotkanalwert, da bei einem Graustufenbild Rot, Grün und Blau immer gleich sind.

Sie können versuchen, Ihr eigenes Graustufenbild zu erstellen (es funktioniert hervorragend mit geometrischen Formen), um Ihren einzigartigen Übergangseffekt zu erzielen.

Mehrere Texturen und Videos

Eine Fläche kann mehr als eine Textur haben, indem einfach mehrere Bild-Tags hinzugefügt werden. Dieses Mal möchten wir anstelle von Bildern Videos verwenden. Wir müssen lediglich die <img />-Tags durch ein <video />-Tag ersetzen. Beim Thema Video gibt es jedoch zwei Dinge zu beachten:

  • Auf Mobilgeräten können Videos nicht ohne Benutzergeste, wie z. B. ein Klick-Event, automatisch abgespielt werden. Es ist daher sicherer, eine Schaltfläche „Seite betreten“ hinzuzufügen, um unsere Videos anzuzeigen und zu starten.
  • Videos können erhebliche Auswirkungen auf Speicher, Leistung und Bandbreite haben. Sie sollten versuchen, sie so leicht wie möglich zu halten.

HTML

Das HTML ist immer noch recht einfach. Wir erstellen unseren Canvas-Div-Wrapper, unseren plane-Div, der die Texturen enthält, und eine Schaltfläche, um die Video-Autoplay-Funktion auszulösen. Beachten Sie die Verwendung des data-sampler-Attributs auf den Bild- und Video-Tags – es wird in unseren Shadern nützlich sein.

<body>
  <div id="canvas"></div>
  <!-- our plane -->
  <div class="plane">
    <!-- notice here we are using the data-sampler attribute to name our sampler uniforms -->
    <img src="path/to/displacement.jpg" data-sampler="displacement" />
    <video src="path/to/video.mp4" data-sampler="firstTexture"></video>
    <video src="path/to/video-2.mp4" data-sampler="secondTexture"></video>
  </div>
    
  <div id="enter-site-wrapper">
    <span id="enter-site">
      Click to enter site
    </span>
  </div>
</body>

CSS

Die Stylesheet kümmert sich um ein paar Dinge: Anzeige der Schaltfläche und Ausblenden des Canvas, bevor der Benutzer die Seite betreten hat, und dafür sorgen, dass unser plane-Div das Fenster ausfüllt.

@media screen {
    
  body {
    margin: 0;
    font-size: 18px;
    font-family: 'PT Sans', Verdana, sans-serif;
    background: #212121;
    line-height: 1.4;
    height: 100vh;
    width: 100vw;
    overflow: hidden;
  }
    
  /*** canvas ***/
    
  #canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    z-index: 10;
    
    /* hide the canvas until the user clicks the button */
    opacity: 0;
    transition: opacity 0.5s ease-in;
  }
  
  /* display the canvas */
  .video-started #canvas {
    opacity: 1;
  }
    
  .plane {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 15;
    
    /* tell the user he can click the plane */
    cursor: pointer;
  }
    
  /* hide the original image and videos */
  .plane img, .plane video {
    display: none;
  }
    
  /* center the button */
  #enter-site-wrapper {
    display: flex;
    justify-content: center;
    align-items: center;
    align-content: center;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 30;
  
    /* hide the button until everything is ready */
    opacity: 0;
    transition: opacity 0.5s ease-in;
  }
  
  /* show the button */
  .curtains-ready #enter-site-wrapper {
    opacity: 1;
  }
    
  /* hide the button after the click event */
  .curtains-ready.video-started #enter-site-wrapper {
    opacity: 0;
    pointer-events: none;
  }
    
  #enter-site {
    padding: 20px;
    color: white;
    background: #ee6557;
    max-width: 200px;
    text-align: center;
    cursor: pointer;
  }

}

JavaScript

Was das JavaScript angeht, gehen wir wie folgt vor:

  • Setzen einiger Variablen zur Speicherung des Slideshow-Zustands.
  • Erstellen des Curtains-Objekts und Hinzufügen der Fläche dazu.
  • Wenn die Fläche bereit ist, auf ein Klick-Event warten, um die Videowiedergabe zu starten (beachten Sie die Verwendung der playVideos()-Methode). Ein weiteres Klick-Event hinzufügen, um zwischen den beiden Videos zu wechseln.
  • Aktualisieren unseres Übergangs-Timer-Uniforms innerhalb der onRender()-Methode.
window.onload = function() {

  // here we will handle which texture is visible and the timer to transition between images
  var activeTexture = 1;
  var transitionTimer = 0;
  
  // set up our WebGL context and append the canvas to our wrapper
  var webGLCurtain = new Curtains("canvas");
  
  // get our plane element
  var planeElements = document.getElementsByClassName("plane");
  
  // some basic parameters
  var params = {
    vertexShaderID: "plane-vs",
    fragmentShaderID: "plane-fs",
    imageCover: false, // our displacement texture has to fit the plane
    uniforms: {
      transitionTimer: {
        name: "uTransitionTimer",
        type: "1f",
        value: 0,
      },
    },
  }
    
  var plane = webGLCurtain.addPlane(planeElements[0], params);
    
  // if our plane has been successfully created
  plane && plane.onReady(function() {
    // display the button
    document.body.classList.add("curtains-ready");
  
    // when our plane is ready we add a click event listener that will switch the active texture value
    planeElements[0].addEventListener("click", function() {
      if(activeTexture == 1) {
        activeTexture = 2;
      }
      else {
        activeTexture = 1;
      }
    });
    
    // click to play the videos
    document.getElementById("enter-site").addEventListener("click", function() {
      // display canvas and hide the button
      document.body.classList.add("video-started");
      
      // play our videos
      plane.playVideos();
    }, false);
    
  }).onRender(function() {
    // increase or decrease our timer based on the active texture value
    // at 60fps this should last one second
    if(activeTexture == 2) {
      transitionTimer = Math.min(60, transitionTimer + 1);
    }
    else {
      transitionTimer = Math.max(0, transitionTimer - 1);
    }
    // update our transition timer uniform
    plane.uniforms.transitionTimer.value = transitionTimer;
  });
}

Shader

Hier passiert die ganze Magie. Wie in unserem ersten Beispiel wird der Vertex-Shader nicht viel tun, und Sie müssen sich auf den Fragment-Shader konzentrieren, der einen „Dive-In“-Effekt erzeugt.

<script id="plane-vs" type="x-shader/x-vertex">
  #ifdef GL_ES
  precision mediump float;
  #endif

  // default mandatory variables
  attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  // our texture matrices
  // notice how it matches our data-sampler attributes + "Matrix"
  uniform mat4 firstTextureMatrix;
  uniform mat4 secondTextureMatrix;

  // varying variables
  varying vec3 vVertexPosition;
  // our displacement texture will use original texture coords attributes
  varying vec2 vTextureCoord;
  // our videos will use texture coords based on their texture matrices
  varying vec2 vFirstTextureCoord;
  varying vec2 vSecondTextureCoord;

  // custom uniforms
  uniform float uTransitionTimer;

  void main() {

    vec3 vertexPosition = aVertexPosition;

    gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);

    // varying variables
    // texture coords attributes because we want our displacement texture to be contained
    vTextureCoord = aTextureCoord;
    // our videos texture coords based on their texture matrices
    vFirstTextureCoord = (firstTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
    vSecondTextureCoord = (secondTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
    // vertex position as usual
    vVertexPosition = vertexPosition;
  }
</script>

<script id="plane-fs" type="x-shader/x-fragment">
  #ifdef GL_ES
  precision mediump float;
  #endif

  // all our varying variables
  varying vec3 vVertexPosition;
  varying vec2 vTextureCoord;
  varying vec2 vFirstTextureCoord;
  varying vec2 vSecondTextureCoord;

  // custom uniforms
  uniform float uTransitionTimer;

  // our textures samplers
  // notice how it matches our data-sampler attributes
  uniform sampler2D firstTexture;
  uniform sampler2D secondTexture;
  uniform sampler2D displacement;

  void main( void ) {
    // our texture coords
    vec2 textureCoords = vTextureCoord;

    // our displacement texture
    vec4 displacementTexture = texture2D(displacement, textureCoords);

    // our displacement factor is a float varying from 1 to 0 based on the timer
    float displacementFactor = 1.0 - (cos(uTransitionTimer / (60.0 / 3.141592)) + 1.0) / 2.0;

    // the effect factor will tell which way we want to displace our pixels
    // the farther from the center of the videos, the stronger it will be
    vec2 effectFactor = vec2((textureCoords.x - 0.5) * 0.75, (textureCoords.y - 0.5) * 0.75);

    // calculate our displaced coordinates of the first video
    vec2 firstDisplacementCoords = vec2(vFirstTextureCoord.x - displacementFactor * (displacementTexture.r * effectFactor.x), vFirstTextureCoord.y- displacementFactor * (displacementTexture.r * effectFactor.y));
    // opposite displacement effect on the second video
    vec2 secondDisplacementCoords = vec2(vSecondTextureCoord.x - (1.0 - displacementFactor) * (displacementTexture.r * effectFactor.x), vSecondTextureCoord.y - (1.0 - displacementFactor) * (displacementTexture.r * effectFactor.y));

    // apply the textures
    vec4 firstDistortedColor = texture2D(firstTexture, firstDisplacementCoords);
    vec4 secondDistortedColor = texture2D(secondTexture, secondDisplacementCoords);

    // blend both textures based on our displacement factor
    vec4 finalColor = mix(firstDistortedColor, secondDistortedColor, displacementFactor);

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

    // apply our shader
    gl_FragColor = finalColor;
  }
</script>

Hier ist unsere kleine Video-Slideshow mit einem coolen Übergangseffekt

Siehe den Pen curtains.js video slideshow von Martin Laxenaire (@martinlaxenaire) auf CodePen.

Dieses Beispiel zeigt Ihnen hervorragend, wie Sie mit curtains.js eine Slideshow erstellen: Sie möchten vielleicht Bilder anstelle von Videos verwenden, die Displacement-Textur ändern, den Fragment-Shader modifizieren…

Wir könnten auch weitere Folien hinzufügen, müssten dann aber den Texturwechsel handhaben. Dies werden wir hier nicht behandeln, aber Sie sollten wissen, dass es auf der Website der Bibliothek ein Beispiel gibt, das sich damit beschäftigt.

Tiefer eintauchen

Wir haben nur an der Oberfläche dessen gekratzt, was mit curtains.js möglich ist. Sie könnten versuchen, mehrere Flächen mit einem coolen Mouseover-Effekt für Ihre Artikel-Thumbnails zu erstellen. Die Möglichkeiten sind nahezu endlos.

Wenn Sie sich die vollständige API-Dokumentation ansehen oder weitere Beispiele zu all diesen grundlegenden Anwendungen sehen möchten, können Sie die Website der Bibliothek oder das GitHub-Repository besuchen.