<!-- 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="https://www.martin-laxenaire.fr/csstricks/images/second-example-texture.jpg" crossorigin="anonymous" />

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

			// 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 / 30.0;

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

				// varyings
        // 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 id="plane-fs" type="x-shader/x-fragment">
			#ifdef GL_ES
			precision mediump float;

			// get our varyings
			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 src="https://www.curtainsjs.com/build/curtains.min.js" type="text/javascript"></script>
body {
  /* make the body fits our viewport */
  position: relative;
  width: 100%;
  height: 100vh;
  margin: 0;
  /* hide scrollbars */
  overflow: hidden;

#canvas {
  /* make the canvas wrapper fits 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;
// 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({
    container: "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 exagerate perspective
    // we could have done  it directly in the initial params

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

    // 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;

Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.