{"id":3015,"date":"2022-04-14T04:37:16","date_gmt":"2022-04-14T04:37:16","guid":{"rendered":"https:\/\/rengga.dev\/blog\/?p=3015"},"modified":"2023-06-12T14:31:52","modified_gmt":"2023-06-12T14:31:52","slug":"js-tutorial-creating-webgl-effects-with-curtainsjs","status":"publish","type":"post","link":"https:\/\/rengga.dev\/blog\/js-tutorial-creating-webgl-effects-with-curtainsjs\/","title":{"rendered":"JS Tutorial &#8211; Creating WebGL Effects with CurtainsJS"},"content":{"rendered":"<p><span style=\"color: #ef3207;\"><a style=\"color: #ef3207;\" href=\"https:\/\/rengga.dev\/\" target=\"_blank\" rel=\"noopener\"><strong>Rengga Dev<\/strong><\/a> <\/span>&#8211; This article focuses adding WebGL effects to\u00a0<code>&lt;image&gt;<\/code>\u00a0and\u00a0<code>&lt;video&gt;<\/code>\u00a0elements of an already \u201ccompleted\u201d web page. While there are a few helpful resources out there on this subject (like\u00a0<a href=\"https:\/\/medium.com\/better-programming\/webgl-enhanced-drag-slider-tutorial-with-curtains-js-part-2-bf32aa5a15c0\" rel=\"noopener\">these<\/a>\u00a0two), I hope to help simplify this subject by distilling the process into a few steps:<\/p>\n<ul>\n<li>Create a web page as you normally would.<\/li>\n<li>Render pieces that you want to add WebGL effects to with WebGL.<\/li>\n<li>Create (or find) the WebGL effects to use.<\/li>\n<li>Add event listeners to connect your page with the WebGL effects.<\/li>\n<\/ul>\n<p>Specifically, we\u2019ll focus on the\u00a0<em>connection<\/em>\u00a0between regular web pages and WebGL. What are we going to make? How about a draggle image slider with an interactive mouse hover!<\/p>\n<h2 id=\"h-you-take-it-from-here\"><strong>You take it from here!<\/strong><\/h2>\n<p>This is, of course, just the tip of the iceberg when it comes to what we can do with the slider now that it is in WebGL. For example, \u00a0common effects like turbulence and displacement can be added to the images in WebGL. The core concept of a displacement effect is to move pixels around based on a gradient lightmap that we use as an input source. We can use\u00a0<a href=\"https:\/\/s3-us-west-2.amazonaws.com\/s.cdpn.io\/58281\/rock-_disp.png\" rel=\"noopener\">this texture<\/a>\u00a0(that I pulled from\u00a0<a href=\"https:\/\/codepen.io\/ReGGae\/pen\/bmyYEj\" rel=\"noopener\">this displacement demo<\/a>\u00a0by\u00a0<a href=\"https:\/\/twitter.com\/Jesper_Landberg\" rel=\"noopener\">Jesper Landberg<\/a>\u00a0\u2014 you should give him a follow) as our source and then plug it into our shader.<\/p>\n<p>&nbsp;<\/p>\n<h3>CSS<\/h3>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"css\">* {\r\n  box-sizing: border-box;\r\n}\r\n\r\nhtml {\r\n  font-size: 25px;\r\n}\r\n\r\nbody {\r\n  background: #1d1d1d;\r\n  font-family: Asap, sans-serif;\r\n}\r\n\r\n#canvas {\r\n  position: fixed;\r\n  top: 0;\r\n  right: 0;\r\n  left: 0;\r\n  height: 100vh;\r\n}\r\n#content {\r\n  position: relative;\r\n  z-index: 1;\r\n}\r\n\r\n.wrapper {\r\n  position: relative;\r\n  overflow: hidden;\r\n}\r\n\r\n.slide-container {\r\n  position: relative;\r\n}\r\n.slide-container::before {\r\n  display: block;\r\n  content: \"\";\r\n  width: 100%;\r\n  padding-top: 42.857%; \/* 9 \/ 21 *\/\r\n}\r\n\r\n.slide {\r\n  position: absolute;\r\n  width: 100%;\r\n  height: 100%;\r\n  top: 0;\r\n  left: 0;\r\n}\r\n.slide img {\r\n  width: 100%;\r\n}\r\n\r\n\r\n.webGLSlider img {\r\n  position: absolute;\r\n  top: 0;\r\n  right: 0;\r\n  bottom: 0;\r\n  left: 0;\r\n}\r\n\r\n\r\n.webGLSlider {\r\n  color: white;\r\n}\r\n.slider-heading {\r\n  text-transform: uppercase;\r\n  margin: 0;\r\n}\r\n\r\n.webGLSlider a {\r\n  color: inherit;\r\n  text-decoration: none;\r\n}\r\n\r\n.text-right {\r\n  text-align: right;\r\n}\r\n.text-right .text {\r\n  display: inline-block;\r\n  vertical-align: middle;\r\n  margin-right: 2rem;\r\n}\r\n.hover-arrow {\r\n  vertical-align: middle;\r\n}\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n.split-child {\r\n  display: inline-block;\r\n}\r\n.split-parent {\r\n  overflow: hidden;\r\n}\r\n\r\n\r\n\r\n.webGLSlider img {\r\n  display: none;\r\n}\r\n.no-curtains img:not(.texture) {\r\n  display: block;\r\n}\r\n<\/pre>\n<p>&nbsp;<\/p>\n<h3>JS<\/h3>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\/\/ Save references to elements that we will use\r\nconst wrapper = document.querySelector(\".webGLSlider .wrapper\");\r\nconst slideContainer = document.querySelector(\".webGLSlider .slide-container\");\r\nconst slides = gsap.utils.toArray(\".webGLSlider .slide\");\r\nconst proxy = document.createElement(\"div\");\r\nconst webGLSlider = document.querySelector('.webGLSlider');\r\nlet sliderTitle = document.querySelector('#slider-title');\r\nconst sliderLink = document.querySelector('#slider-link');\r\n\r\nconst numSlides = slides.length;\r\nconst wrapIndex = gsap.utils.wrap(0, numSlides);\r\nconst gap = 0;\r\nlet lastIndex = 0; \/\/ Used in resize and to change out headings\r\nlet firstRun = true;\r\n\r\n\/\/ These are set in the resize function\r\nlet slideHeight;\r\nlet slideWidth;\r\nlet wrapWidth;\r\nlet wrapVal;\r\nlet animation;\r\nlet draggable;\r\n\r\n\/\/ Variables related to the WebGL functionality\r\nconst planes = [];\r\nconst maxVelocity = 10000;\r\nconst halfMaxVelocity = maxVelocity \/ 2;\r\n\r\n\/\/ Our shaders\r\nconst v300Shader = `#version 300 es\r\n\/\/ Determines how much precision for the GPU to use\r\n#ifdef GL_ES\r\nprecision mediump float;\r\n#endif\r\n\r\n\/\/ in = passed in from a data buffer\r\n\/\/ uniforms = passed in from CPU (our program)\r\n\/\/ out = passed from our vertex shader to our fragment shader\r\n\r\n\/\/ Default mandatory attributes\r\nin vec3 aVertexPosition;\r\nin vec2 aTextureCoord;\r\n\r\n\/\/ Mandatory projection and model view matrices are generated by curtains\r\n\/\/ It will position and size our plane based on its HTML element CSS values\r\nuniform mat4 uMVMatrix;\r\nuniform mat4 uPMatrix;\r\n\r\n\/\/ This is generated by curtains based on the sampler name we provided\r\n\/\/ It will be used to map adjust our texture coords so the texture will fit the plane\r\nuniform mat4 planeTextureMatrix;\r\n\r\n\/\/ The un-transformed mouse position\r\nuniform vec2 uMouse;\r\n\r\n\/\/ Texture coord varying that will be passed to our fragment shader\r\nout vec2 vTextureCoord;\r\n\/\/ Our transformed mouse position that will be passed to our fragment shader\r\nout vec2 vMouse;\r\n\r\nvoid main() {\r\n    \/\/ Use texture matrix and original texture coords to generate accurate texture coords\r\n    vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;\r\n\r\n    \/\/ Convert from vertex pos to texture pos and apply texture matrix to the mouse position as well \r\n    vMouse = (planeTextureMatrix * vec4((uMouse + 1.0) * 0.5, 0.0, 1.0)).xy - 0.5;\r\n\r\n    \/\/ Apply our vertex position based on the projection and model view matrices\r\n    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\r\n}`;\r\nconst f300Shader = `#version 300 es\r\n#ifdef GL_ES\r\nprecision mediump float;\r\n#endif\r\n\r\n\/\/ Our texture samplers - Their names match the data-sampler attributes on our image tags\r\nuniform sampler2D planeTexture; \/\/ The image texture\r\nuniform sampler2D displacementTexture; \/\/ The displacement texture\r\n\r\n\/\/ Our texture coords (on a pixel-by-pixel basis)\r\nin vec2 vTextureCoord;\r\n\r\nin vec2 vMouse; \/\/ The mouse position in texture coordinates\r\n\r\nout vec4 outputColor; \/\/ The output color of each pixel (what was gl_FragColor in WebGL1)\r\n\r\n\/\/ FOR MOUSE EFFECTS\r\nuniform float uRadius; \/\/ Radius of pixels to warp\/invert\r\nconst float transAmnt = 0.05; \/\/ Amount to translate the texture by\r\n\r\nconst float PI = 3.1415926535;\r\n\r\n\/\/ FOR DISPLACEMENT\r\n\/\/ The power and intensity of our displacement - passed in\r\nuniform float uPower;\r\nuniform float uIntensity;\r\n\r\nconst float aspect = 2.33333333; \/\/ 21 \/ 9\r\n\r\n\/\/ FOR ANTIALIAS\r\nuniform vec2 uResolution;\r\n#define R    uResolution\r\n\/\/ This basically gives you antialias for free without the need of a second pass\r\n\/\/ If you don't need antialias in another place, local antiliasing is probably best\r\n\/\/ From here: https:\/\/www.shadertoy.com\/view\/3sjGDh\r\n\/\/ Which is explained here: https:\/\/shadertoyunofficial.wordpress.com\/\r\n\/\/ (Look for antialias in the search box)\r\n#define S(v) smoothstep(2.0\/R.y, 0.0, v)\r\n\r\nvoid main() {    \r\n  vec2 myUV = vTextureCoord;\r\n  outputColor.a = 1.0;\r\n\r\n  \/\/ Check if pixel is within the given radius of the mouse\r\n  vec2 diff = myUV - vMouse - 0.5;\r\n  diff.x *= aspect;\r\n  float distance = length(diff);\r\n\r\n  \/\/ Create the fish-eye effect\r\n  if (distance &lt;= uRadius) {\r\n    float scale = (1.0 - cos(distance\/uRadius * PI * 0.5));\r\n    myUV = vMouse + normalize(diff) * uRadius * scale + 0.5;\r\n  }\r\n\r\n  \/\/ Translate the texture\r\n  myUV += -vMouse * transAmnt;\r\n\r\n  \/\/ Displacement\r\n  \/\/ Based on https:\/\/codepen.io\/ReGGae\/pen\/bmyYEj\r\n  vec4 disp = texture(displacementTexture, myUV);\r\n  vec2 dispVec = vec2(disp.x, disp.y);\r\n  vec2 distPos = myUV + (dispVec * uIntensity * uPower);\r\n\r\n  vec3 tex = texture(planeTexture, distPos).rgb;\r\n\r\n  \/\/ Antialiasing\r\n  vec3 inverted = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);\r\n  if(uRadius &gt; 0.0) {\r\n    outputColor.rgb = mix( inverted, tex, S(distance - uRadius));\r\n  } else {\r\n    outputColor.rgb = inverted.rgb;\r\n  }\r\n\r\n  \/\/ Not antialiased\r\n  \/\/ if(distance &lt;= uRadius) {\r\n  \/\/   outputColor.rgb = tex;\r\n  \/\/ } else { \/\/ invert\r\n  \/\/   outputColor.rgb = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);\r\n  \/\/ }\r\n}`;\r\nconst v100Shader = `\r\n\/\/ Determines how much precision for the GPU to use\r\n#ifdef GL_ES\r\nprecision mediump float;\r\n#endif\r\n\r\n\/\/ Default mandatory attributes\r\nattribute vec3 aVertexPosition;\r\nattribute vec2 aTextureCoord;\r\n\r\n\/\/ Mandatory projection and model view matrices are generated by curtains\r\n\/\/ It will position and size our plane based on its HTML element CSS values\r\nuniform mat4 uMVMatrix;\r\nuniform mat4 uPMatrix;\r\n\r\n\/\/ This is generated by curtains based on the sampler name we provided\r\n\/\/ It will be used to map adjust our texture coords so the texture will fit the plane\r\nuniform mat4 planeTextureMatrix;\r\n\r\n\/\/ The un-transformed mouse position\r\nuniform vec2 uMouse;\r\n\r\n\/\/ Texture coord varying that will be passed to our fragment shader\r\nvarying vec2 vTextureCoord;\r\n\/\/ Our transformed mouse position that will be passed to our fragment shader\r\nvarying vec2 vMouse;\r\n\r\nvoid main() {\r\n    \/\/ Use texture matrix and original texture coords to generate accurate texture coords\r\n    vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;\r\n\r\n    \/\/ Convert from vertex pos to texture pos and apply texture matrix to the mouse position as well \r\n    vMouse = (planeTextureMatrix * vec4((uMouse + 1.0) * 0.5, 0.0, 1.0)).xy - 0.5;\r\n\r\n    \/\/ Apply our vertex position based on the projection and model view matrices\r\n    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\r\n}`;\r\nconst f100Shader = `\r\n#ifdef GL_ES\r\nprecision mediump float;\r\n#endif\r\n\r\n\/\/ Our texture samplers - Their names match the data-sampler attributes on our image tags\r\nuniform sampler2D planeTexture; \/\/ The image texture\r\nuniform sampler2D displacementTexture; \/\/ The displacement texture\r\n\r\n\/\/ Our texture coords (on a pixel-by-pixel basis)\r\nvarying vec2 vTextureCoord;\r\n\r\nvarying vec2 vMouse; \/\/ The mouse position in texture coordinates\r\n\r\n\/\/ FOR MOUSE EFFECTS\r\nuniform float uRadius; \/\/ Radius of pixels to warp\/invert\r\nconst float transAmnt = 0.05; \/\/ Amount to translate the texture by\r\n\r\nconst float PI = 3.1415926535;\r\n\r\n\/\/ FOR DISPLACEMENT\r\n\/\/ The power and intensity of our displacement - passed in\r\nuniform float uPower;\r\nuniform float uIntensity;\r\n\r\nconst float aspect = 2.33333333; \/\/ 21 \/ 9\r\n\r\n\/\/ FOR ANTIALIAS\r\nuniform vec2 uResolution;\r\n#define R    uResolution\r\n\/\/ This basically gives you antialias for free without the need of a second pass\r\n\/\/ If you don't need antialias in another place, local antiliasing is probably best\r\n\/\/ From here: https:\/\/www.shadertoy.com\/view\/3sjGDh\r\n\/\/ Which is explained here: https:\/\/shadertoyunofficial.wordpress.com\/\r\n\/\/ (Look for antialias in the search box)\r\n#define S(v) smoothstep(2.0\/R.y, 0.0, v)\r\n\r\nvoid main() {    \r\n  vec2 myUV = vTextureCoord;\r\n  gl_FragColor.a = 1.0;\r\n\r\n  \/\/ Check if pixel is within the given radius of the mouse\r\n  vec2 diff = myUV - vMouse - 0.5;\r\n  diff.x *= aspect;\r\n  float distance = length(diff);\r\n\r\n  \/\/ Create the fish-eye effect\r\n  if (distance &lt;= uRadius) {\r\n    float scale = (1.0 - cos(distance\/uRadius * PI * 0.5));\r\n    myUV = vMouse + normalize(diff) * uRadius * scale + 0.5;\r\n  }\r\n\r\n  \/\/ Translate the texture\r\n  myUV += -vMouse * transAmnt;\r\n\r\n  \/\/ Displacement\r\n  \/\/ Based on https:\/\/codepen.io\/ReGGae\/pen\/bmyYEj\r\n  vec4 disp = texture2D(displacementTexture, myUV);\r\n  vec2 dispVec = vec2(disp.x, disp.y);\r\n  vec2 distPos = myUV + (dispVec * uIntensity * uPower);\r\n\r\n  vec3 tex = texture2D(planeTexture, distPos).rgb;\r\n\r\n  \/\/ Antialiasing\r\n  vec3 inverted = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);\r\n  if(uRadius &gt; 0.0) {\r\n    gl_FragColor.rgb = mix( inverted, tex, S(distance - uRadius));\r\n  } else {\r\n    gl_FragColor.rgb = inverted.rgb;\r\n  }\r\n\r\n  \/\/ Not antialiased\r\n  \/\/ if(distance &lt;= uRadius) {\r\n  \/\/   gl_FragColor.rgb = tex;\r\n  \/\/ } else { \/\/ invert\r\n  \/\/   gl_FragColor.rgb = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);\r\n  \/\/ }\r\n}`;\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ Heading change functions \/\/\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\nfunction setupHeadingAnim() {\r\n  \/\/ Split it twice to be able to reveal it from hidden overflow\r\n  webGLSlider.childSplit = new SplitText('.slider-heading', {\r\n    type: 'lines',\r\n    linesClass: 'split-child'\r\n  });\r\n  webGLSlider.parentSplit = new SplitText('.slider-heading', {\r\n    type: 'lines',\r\n    linesClass: 'split-parent'\r\n  });\r\n  \r\n  \/\/ The text reveal animation\r\n  webGLSlider.anim = gsap.from(webGLSlider.childSplit.lines, {\r\n    paused: true,\r\n    yPercent: 100, \r\n    stagger: 0.2\r\n  });\r\n  if(firstRun) {\r\n    webGLSlider.anim.play();\r\n    firstRun = false;\r\n  }\r\n}\r\n\r\n\/\/ Check to see if the index should be changed and set up the heading changes\r\nfunction checkIndex(endX) {\r\n  \/\/ Get the new index\r\n  endX = -endX || 0;\r\n  const newIndex = wrapIndex(endX \/ (slideWidth + gap));\r\n  \r\n  \/\/ Only do stuff if it's a new index\r\n  if(typeof webGLSlider.anim === \"undefined\" || lastIndex !== newIndex) {\r\n    \/\/ Undo pre anim\r\n    if(webGLSlider.anim) {\r\n      webGLSlider.anim.progress(1).kill();\r\n      webGLSlider.parentSplit.revert();\r\n      webGLSlider.childSplit.revert();\r\n    }\r\n    \r\n    \/\/ Update index, text, and link\r\n    sliderTitle = document.querySelector('#slider-title');\r\n    lastIndex = newIndex;\r\n    const dataset = slides[newIndex].dataset;\r\n    sliderTitle.innerText = dataset.name;\r\n    sliderLink.href = dataset.url;\r\n    \r\n    \/\/ Create new anim\r\n    setupHeadingAnim();\r\n  \r\n    setupArrowLinks();\r\n  }\r\n}\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ Create and handle the slider \/\/\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\nfunction resize() {\r\n  \/\/ Get the new values\r\n  slideWidth  = gsap.getProperty('.slide', 'width');\r\n  slideHeight = gsap.getProperty('.slide', 'height');\r\n  \r\n  const widthUnit = (slideWidth + gap);\r\n  wrapWidth = numSlides * widthUnit;\r\n  wrapVal = gsap.utils.wrap(0, wrapWidth);\r\n  \r\n  \/\/ Setup our slider with the new values\r\n  const pxOffset = lastIndex !== numSlides - 1 ? lastIndex * -widthUnit : widthUnit;\r\n  gsap.set(slideContainer, { left: -widthUnit });\r\n  gsap.set(proxy, { x: pxOffset });\r\n  \r\n  \/\/ The animation that's used to do the sliding\r\n  animation = gsap.fromTo(slides, {\r\n    x: i =&gt; wrapVal(i * widthUnit)\r\n  }, {\r\n    duration: 1,\r\n    x: `+=${wrapWidth}`,\r\n    ease: \"none\",\r\n    paused: true,\r\n    \/\/ This creates the infinite looping\r\n    modifiers: {\r\n      x: function(x, target) {\r\n        return `${parseInt(x) % wrapWidth}px`;\r\n      }\r\n    }\r\n  })\r\n  \/\/ Set progress to correct value\r\n  .progress(1 - lastIndex \/ numSlides);\r\n  \r\n  \/\/ Kill off the old draggable instance\r\n  if(draggable) {\r\n    draggable.kill();\r\n  }\r\n  \r\n  \/\/ Recreate the draggable with the new values\r\n  draggable = Draggable.create(proxy, {\r\n    type: \"x\",\r\n    trigger: \".wrapper\",\r\n    inertia: true,\r\n    snap: { \r\n      x: (x) =&gt; {\r\n        return Math.round(x \/ widthUnit) * widthUnit;\r\n      } \r\n    },\r\n    \r\n    \/\/ Our event listeners\r\n    onPress: onPress,\r\n    onDragStart: () =&gt; webGLSlider.anim.timeScale(-3),\r\n    onDrag: updateProgress,\r\n    onRelease: onRelease,\r\n    onDragEnd: endPress,\r\n    onThrowUpdate: updateProgress,\r\n    onThrowComplete: () =&gt; webGLSlider.anim.play()\r\n  })[0];\r\n  \r\n  \/\/ Update the resolution in the WebGL planes\r\n  planes.forEach(plane =&gt; plane.uniforms.resolution.value = [innerWidth, innerHeight]);\r\n}\r\n\r\n\/\/ Update the slider along with the necessary WebGL variables\r\nfunction updateProgress() {\r\n  \/\/ Update the actual slider\r\n  animation.progress(wrapVal(this.x) \/ wrapWidth);\r\n  \r\n  \/\/ Update the WebGL slider planes\r\n  planes.forEach(plane =&gt; plane.updatePosition());\r\n  \r\n  \/\/ Update the WebGL \"mouse\"\r\n  updateWebGLMouse(0);\r\n  \r\n  \/\/ Affect the displacement based on the drag velocity\r\n  if(this.isDragging) {\r\n    let velocity = InertiaPlugin.getVelocity(proxy, \"x\");\r\n    velocity &gt; halfMaxVelocity ? velocity = halfMaxVelocity : null;\r\n    velocity = Math.abs(velocity);\r\n    gsap.to(disp, {\r\n      power: velocity \/ maxVelocity + 0.5,\r\n      onUpdate: updatePower,\r\n      overwrite: 'auto',\r\n      duration: 0.2,\r\n      ease: \"power4.out\"\r\n    });\r\n  }\r\n}\r\n\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ Keep mouse synced with WebGL \/\/\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\nconst mouse = new Vec2(0, 0);\r\nfunction addMouseListeners() {\r\n  if (\"ontouchstart\" in window) {\r\n    wrapper.addEventListener(\"touchstart\", updateMouse, false);\r\n    wrapper.addEventListener(\"touchmove\", updateMouse, false);\r\n    wrapper.addEventListener(\"blur\", mouseOut, false);\r\n  } else {\r\n    wrapper.addEventListener(\"mousemove\", updateMouse, false);\r\n    wrapper.addEventListener(\"mouseleave\", mouseOut, false);\r\n  }\r\n}\r\n\r\n\/\/ Update the stored mouse position along with WebGL \"mouse\"\r\nfunction updateMouse(e) {\r\n  radiusAnim.play();\r\n  \r\n  if (e.changedTouches &amp;&amp; e.changedTouches.length) {\r\n    e.x = e.changedTouches[0].pageX;\r\n    e.y = e.changedTouches[0].pageY;\r\n  }\r\n  if (e.x === undefined) {\r\n    e.x = e.pageX;\r\n    e.y = e.pageY;\r\n  }\r\n  \r\n  mouse.x = e.x;\r\n  mouse.y = e.y;\r\n  \r\n  \r\n  updateWebGLMouse();\r\n}\r\n\r\n\/\/ Updates the mouse position for all planes\r\nfunction updateWebGLMouse(dur) {\r\n  \/\/ update the planes mouse position uniforms\r\n  planes.forEach((plane, i) =&gt; {\r\n    const webglMousePos = plane.mouseToPlaneCoords(mouse);\r\n    updatePlaneMouse(plane, webglMousePos, dur);\r\n  });\r\n}\r\n\r\n\/\/ Updates the mouse position for the given plane\r\nfunction updatePlaneMouse(plane, endPos = new Vec2(0, 0), dur = 0.1) {\r\n  gsap.to(plane.uniforms.mouse.value, {\r\n    x: endPos.x,\r\n    y: endPos.y,\r\n    duration: dur,\r\n    overwrite: true,\r\n  });\r\n}\r\n\r\n\/\/ When the mouse leaves the slider, animate the WebGL \"mouse\" to the center of slider\r\nfunction mouseOut(e) {\r\n  planes.forEach((plane, i) =&gt; updatePlaneMouse(plane, new Vec2(0, 0), 1) );\r\n  \r\n  radiusAnim.reverse();\r\n}\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ Radius hover functionality \/\/\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\nconst radius = { val: 0.1 };\r\nconst radiusAnim = gsap.from(radius, { \r\n  val: 0, \r\n  duration: 0.3, \r\n  paused: true,\r\n  onUpdate: updateRadius\r\n});\r\nfunction updateRadius() {\r\n  planes.forEach((plane, i) =&gt; {\r\n    plane.uniforms.radius.value = radius.val;\r\n  });\r\n}\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ Image displacement functionality \/\/\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\nconst disp = { power: 0, intensity: 0.1 };\r\nconst intensityAnim = gsap.to(disp, {\r\n  intensity: 0.2,\r\n  onUpdate: updateIntensity,\r\n  yoyo: true, \r\n  repeat: -1,\r\n  duration: 4,\r\n  ease: \"power1.inOut\"\r\n});\r\n\/\/ Animate in the displacement\r\nfunction onPress() {\r\n  gsap.to(disp, {\r\n    power: 0.5,\r\n    onUpdate: updatePower,\r\n    overwrite: 'auto',\r\n    duration: 0.5,\r\n  });\r\n}\r\n\r\nfunction onRelease() {\r\n  \/\/ Update our index (and headings if need be)\r\n  checkIndex(this.endX);\r\n  \r\n  endPress();\r\n}\r\n\r\n\/\/ Animate out the displacement\r\nfunction endPress() {\r\n  gsap.to(disp, {\r\n    power: 0,\r\n    onUpdate: updatePower,\r\n    overwrite: 'auto',\r\n    duration: draggable.tween ? draggable.tween.duration() : 0.5,\r\n  });\r\n}\r\n\r\n\/\/ Update the displacement power value in our planes\r\nfunction updatePower() {\r\n  planes.forEach((plane, i) =&gt; plane.uniforms.power.value = disp.power );\r\n}\r\n\/\/ Update the displacement intensity value in our planes\r\nfunction updateIntensity() {\r\n  planes.forEach((plane, i) =&gt;  plane.uniforms.intensity.value = disp.intensity );\r\n}\r\n\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ Arrow hover code \/\/\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\nconst arrowLinks = document.querySelectorAll(\".arrow-link\");\r\nconst playLinkAnim = (e) =&gt; e.target.anim.play();\r\nconst reverseLinkAnim = (e) =&gt; e.target.anim.reverse();\r\nfunction setupArrowLinks() {\r\n  arrowLinks.forEach(link =&gt; {\r\n    if(link.anim) {\r\n      link.anim.kill();\r\n      link.removeEventListener(\"mouseenter\", playLinkAnim);\r\n      link.removeEventListener(\"mouseleave\", reverseLinkAnim);\r\n    }\r\n\r\n    link.svg = link.querySelector(\"svg\");\r\n    link.path = link.svg.querySelector(\"path\");\r\n    link.anim = gsap.timeline({\r\n      paused: true,\r\n      defaults: {\r\n        duration: 0.3\r\n      }\r\n    })\r\n      .from(link.svg, {\r\n      attr: { \r\n        width: 44,\r\n        height: 36,\r\n        viewBox: \"0 0 44 36\"\r\n      }\r\n    })\r\n      .fromTo(link.path, {\r\n      attr: { d: \"M25.5295 0.48999L21.8195 4.33999L33.5095 15.19H0.189453V20.3H33.5795L21.8195 31.22L25.5295 35.07L43.4495 17.78L25.5295 0.48999Z\" }\r\n    }, {\r\n      attr: { d: \"M58.5295 0L54.8195 3.85L66.5095 14.7H0.189453V19.81H66.5795L54.8195 30.73L58.5295 34.58L76.4495 17.29L58.5295 0Z\" }\r\n    }, 0)\r\n\r\n    link.addEventListener(\"mouseenter\", playLinkAnim);\r\n    link.addEventListener(\"mouseleave\", reverseLinkAnim);\r\n  });\r\n}\r\n\r\n\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\n\/\/ Init stuff \/\/\r\n\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\r\nwindow.addEventListener(\"load\", e =&gt; {\r\n  window.addEventListener(\"resize\", resize);\r\n  resize();\r\n\r\n  checkIndex();\r\n  \r\n  \/\/ Create a new curtains instance\r\n  const curtains = new Curtains({ container: \"canvas\", autoRender: false });\r\n  \/\/ Use a single rAF for both GSAP and Curtains\r\n  function renderScene() {\r\n    curtains.render();\r\n  }\r\n  gsap.ticker.add(renderScene);\r\n  \/\/ Create a curtains plane for each slide\r\n  const planeElements = document.querySelectorAll(\".slide\");\r\n  \r\n  \/\/ Params passed to the curtains instance\r\n  const supportsWebGL2 = curtains.renderer._isWebGL2;\r\n  const params = {\r\n    vertexShader: supportsWebGL2 ? v300Shader : v100Shader, \/\/ The vertex shader we want to use\r\n    fragmentShader: supportsWebGL2 ? f300Shader : f100Shader, \/\/ The fragment shader we want to use\r\n    \r\n    \/\/ The variables that we're going to be animating to update our WebGL state\r\n    uniforms: {\r\n      \/\/ For the cursor effects\r\n      mouse: { \r\n        name: \"uMouse\", \/\/ The shader variable name\r\n        type: \"2f\",     \/\/ The type for the variable - https:\/\/webglfundamentals.org\/webgl\/lessons\/webgl-shaders-and-glsl.html\r\n        value: mouse    \/\/ The initial value to use\r\n      },\r\n      radius: { \r\n        name: \"uRadius\",\r\n        type: \"1f\",\r\n        value: radius.val\r\n      },\r\n      \r\n      \/\/ For the displacement\r\n      power: { \r\n        name: \"uPower\",\r\n        type: \"1f\", \r\n        value: disp.power\r\n      },\r\n      intensity: { \r\n        name: \"uIntensity\",\r\n        type: \"1f\", \r\n        value: disp.intensity\r\n      },\r\n      \r\n      \/\/ For the antialiasing\r\n      resolution: { \r\n        name: \"uResolution\",\r\n        type: \"2f\", \r\n        value: [innerWidth, innerHeight] \r\n      }\r\n    },\r\n  };\r\n  \r\n  \/\/ Create a new plane for each slider\r\n  planeElements.forEach((planeEl, i) =&gt; {\r\n    const plane = new Plane(curtains, planeEl, params);\r\n\r\n    \/\/ If our plane has been successfully created\r\n    if(plane) {\r\n        \/\/ Push it into our planes array\r\n        planes.push(plane);\r\n\r\n        \/\/ onReady is called once our plane is ready and all its texture have been created\r\n        plane.onLoading(function(texture) {\r\n          \/\/ Scale up the texture (for the translation effect)\r\n          texture.setScale(new Vec2(1.2, 1.2));\r\n        }).onReady(function() {\r\n          \/\/ One could use \"this\" instead of \"plane\" here \r\n          \r\n          \/\/ Add a \"loaded\" class to display image container\r\n          plane.htmlElement.closest(\".slide\").classList.add(\"loaded\");\r\n        });\r\n    }\r\n  });\r\n  \r\n  \/\/ Add our mouse listeners to our slider\r\n  addMouseListeners();\r\n});<\/pre>\n<p>&nbsp;<\/p>\n<p>If we hook up the texture above and animate the displacement power and intensity so that they vary over time and based on our drag velocity, then it will create a nice semi-random, but natural-looking displacement effect:<br \/>\n<iframe style=\"width: 100%;\" title=\"Untitled\" src=\"https:\/\/codepen.io\/renggagumilar\/embed\/vYRVxqB?default-tab=result&amp;theme-id=dark\" height=\"800\" frameborder=\"no\" scrolling=\"no\" allowfullscreen=\"allowfullscreen\"><br \/>\nSee the Pen <a href=\"https:\/\/codepen.io\/renggagumilar\/pen\/vYRVxqB\"><br \/>\nUntitled<\/a> by Rengga Gumilar (<a href=\"https:\/\/codepen.io\/renggagumilar\">@renggagumilar<\/a>)<br \/>\non <a href=\"https:\/\/codepen.io\">CodePen<\/a>.<br \/>\n<\/iframe><br \/>\nIt\u2019s also worth noting that\u00a0<a href=\"https:\/\/github.com\/martinlaxenaire\/react-curtains\" rel=\"noopener\">Curtains has its own React version<\/a>\u00a0if that\u2019s how you like to roll.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Rengga Dev &#8211; This article focuses adding WebGL effects to\u00a0&lt;image&gt;\u00a0and\u00a0&lt;video&gt;\u00a0elements of an <a class=\"read-more\" href=\"https:\/\/rengga.dev\/blog\/js-tutorial-creating-webgl-effects-with-curtainsjs\/\" title=\"JS Tutorial &#8211; Creating WebGL Effects with CurtainsJS\" itemprop=\"url\"><\/a><\/p>\n","protected":false},"author":1,"featured_media":3751,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[20,12],"tags":[266,264,263,103,227,265],"newstopic":[],"class_list":{"0":"post-3015","1":"post","2":"type-post","3":"status-publish","4":"format-standard","5":"has-post-thumbnail","7":"category-javascript","8":"category-web-development","9":"tag-curtainsjs","10":"tag-javascript-tutorial","11":"tag-js-tutorial","12":"tag-web-design","13":"tag-web-designer","14":"tag-webgl"},"_links":{"self":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3015","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/comments?post=3015"}],"version-history":[{"count":4,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3015\/revisions"}],"predecessor-version":[{"id":3387,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3015\/revisions\/3387"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/media\/3751"}],"wp:attachment":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/media?parent=3015"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/categories?post=3015"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/tags?post=3015"},{"taxonomy":"newstopic","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/newstopic?post=3015"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}