HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<!-- div that will hold our WebGL canvas -->
<div id="canvas"></div>
<div id="content">
<h1 id="title">Islands</h1>
<!-- drag slider -->
<div id="planes">
<div class="plane-wrapper">
<span class="plane-title">Bahamas</span>
<div class="plane">
<img src="https://source.unsplash.com/2fFd7SIVpz8/1280x720" alt="Photo by Gregory Culmer on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Philippines</span>
<div class="plane">
<img src="https://source.unsplash.com/SDaJRmZYMDA/1280x720" alt="Photo by James Connolly on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Indonesia</span>
<div class="plane">
<img src="https://source.unsplash.com/DxmBSgUYKis/1600x900" alt="Photo by sutirta budiman on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Maldives</span>
<div class="plane">
<img src="https://source.unsplash.com/lT9rqfG7lcQ/1280x720" alt="Photo by Jailam Rashad on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Greece</span>
<div class="plane">
<img src="https://source.unsplash.com/QXW1YEMhq_4/1280x720" data-sampler="planeTexture" alt="Photo by Chris Karidis on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Fiji</span>
<div class="plane">
<img src="https://source.unsplash.com/3jtm7BMsaPA/1280x720" alt="Photo by Lili Ortiz on Unsplash" data-sampler="planeTexture" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Thailand</span>
<div class="plane">
<img src="https://source.unsplash.com/Mwg_MdX-Jx4/1280x720" data-sampler="planeTexture" alt="Photo by Samule Sun on Unsplash" crossorigin />
</div>
</div>
</div>
<div id="drag-tip">
Drag to explore
</div>
</div>
<script id="slider-planes-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// default mandatory attributes
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
// those projection and model view matrices are generated by the library
// it will position and size our plane based on its HTML element CSS values
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// this is generated by the library based on the sampler name we provided
// it will be used to map adjust our texture coords so the texture will fit the plane
uniform mat4 planeTextureMatrix;
// texture coord varying that will be passed to our fragment shader
varying vec2 vTextureCoord;
void main() {
// apply our vertex position based on the projection and model view matrices
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
// varying
// use texture matrix and original texture coords to generate accurate texture coords
vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
}
</script>
<script id="slider-planes-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
// our texture coords varying
varying vec2 vTextureCoord;
// our texture sampler (see how its name matches the data-sampler attribute on our image tags)
uniform sampler2D planeTexture;
// our opacity uniform that goes from 0 to 1
uniform float uOpacity;
void main( void ) {
// map our texture to the varying texture coords
vec4 finalColor = texture2D(planeTexture, vTextureCoord);
// the distance from this point to the left edge is a float from 0 to 1
float distanceToLeft = distance(vec2(0.0, vTextureCoord.y), vTextureCoord);
// calculate an effect that goes from 0 to 1 depenging on uOpacity and distanceToLeft
float spreadFromLeft = clamp((uOpacity * (1.0 - distanceToLeft) - 1.0) + uOpacity * 2.0, 0.0, 1.0);
// handle pre-multiplied alpha on rgb values and use spreadFromLeft as alpha.
finalColor = vec4(vec3(finalColor.rgb * spreadFromLeft), spreadFromLeft);
// this is it
gl_FragColor = finalColor;
}
</script>
<script id="distortion-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// default mandatory attributes
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
// our displacement texture matrix uniform
uniform mat4 displacementTextureMatrix;
// mouse position and direction uniforms
uniform vec2 uMousePos;
uniform float uDirection;
// custom varyings
varying vec2 vTextureCoord;
varying vec2 vDispTextureCoord;
varying vec2 vMouseTexCoords;
void main() {
gl_Position = vec4(aVertexPosition, 1.0);
// varyings
vTextureCoord = aTextureCoord;
vDispTextureCoord = (displacementTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
// we will handle our mouse coords here for better performance
// get our texture coords for both directions
vec2 mouseHorizontalTexCoords = (uMousePos + 1.0) / 2.0;
mouseHorizontalTexCoords.y = 0.5;
vec2 mouseVerticalTexCoords = (uMousePos + 1.0) / 2.0;
mouseVerticalTexCoords.x = 0.5;
// use the right value for the right direction
vMouseTexCoords = mix(mouseHorizontalTexCoords, mouseVerticalTexCoords, uDirection);
}
</script>
<script id="distortion-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
// varyings
varying vec2 vTextureCoord;
varying vec2 vDispTextureCoord;
varying vec2 vMouseTexCoords;
// our render texture is basically what's being drawn in our canvas
uniform sampler2D uRenderTexture;
// the displacement texture we've loaded into our shader pass
uniform sampler2D displacementTexture;
// all our uniforms
uniform float uDragEffect;
uniform vec2 uMousePos;
uniform vec2 uOffset;
uniform float uDirection;
uniform vec3 uBgColor;
void main( void ) {
vec2 textureCoords = vTextureCoord;
// repeat and offset our displacement map texture coords for both slider directions
vec2 horizontalPhase = fract(vec2(vDispTextureCoord.x + uOffset.x, vDispTextureCoord.y + (uOffset.y / 3000.0)) / vec2(1.0, 1.0));
vec2 verticalPhase = fract(vec2(vDispTextureCoord.x * (uOffset.x / 3000.0), vDispTextureCoord.y + uOffset.y) / vec2(1.0, 1.0));
// use the correct repeated and offseted texture coords
vec2 phase = mix(horizontalPhase, verticalPhase, uDirection);
vec4 displacement = texture2D(displacementTexture, phase);
// use our varying mouse texture coords
vec2 mouseTexCoords = vMouseTexCoords;
float distanceToMouse = distance(mouseTexCoords, textureCoords);
// calculate an effect that goes from 0 to 1 depenging on uDragEffect and distanceToMouse
float spreadFromMouse = clamp((uDragEffect * (1.0 - distanceToMouse) - 1.0) + uDragEffect * 2.0, 0.0, 1.0);
// calculate our fish eye like distortions
vec2 fishEye = (vec2(textureCoords - mouseTexCoords).xy) * pow(distanceToMouse, 2.5);
// add a displacement based on our map and our time uniform
float displacementEffect = displacement.r * 1.25;
// spread our fish eye and displacement effects from our mouse
// calculate for both directions
vec2 horizontalTexCoords = textureCoords;
horizontalTexCoords.x -= spreadFromMouse * fishEye.x * displacementEffect / 4.0;
horizontalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect;
vec2 verticalTexCoords = textureCoords;
verticalTexCoords.x -= spreadFromMouse * fishEye.x * displacementEffect;
verticalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect / 4.0;
// use the right value for the right direction
textureCoords = mix(horizontalTexCoords, verticalTexCoords, uDirection);
// get our final colored and BW vec4
vec4 finalColor = texture2D(uRenderTexture, textureCoords);
float grey = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
vec4 finalGrey = vec4(vec3(grey), 1.0);
// mix our both vec4 based on our spread value
finalColor = mix(finalColor, finalGrey, spreadFromMouse * finalColor.a);
// apply a grey background where we don't have nothing to draw
finalColor = mix(vec4(uBgColor.r * spreadFromMouse / 255.0, uBgColor.g * spreadFromMouse / 255.0, uBgColor.b * spreadFromMouse / 255.0, spreadFromMouse), finalColor, finalColor.a);
gl_FragColor = finalColor;
}
</script>
@media screen {
html, body {
min-height: 100%;
}
body {
margin: 0;
font-size: 18px;
font-family: 'Arvo', Verdana, sans-serif;
background: #ece5d1;
line-height: 1.4;
overflow: hidden;
}
/*** canvas ***/
/* our canvas will have the size of our window */
#canvas {
position: fixed;
top: 0;
right: 0;
left: 0;
height: 100vh;
z-index: 1;
}
/*** content ***/
#content {
position: relative;
z-index: 2;
overflow: hidden;
}
#title {
position: fixed;
top: 20px;
right: 20px;
left: 20px;
z-index: 1;
pointer-events: none;
font-size: 1.5em;
line-height: 1;
margin: 0;
text-transform: uppercase;
color: #032f4d;
text-align: center;
}
#planes {
/* width of items * number of items */
width: calc(((100vw / 1.75) + 10vw) * 7);
padding: 0 2.5vw;
height: 100vh;
display: flex;
align-items: center;
cursor: move;
}
.plane-wrapper {
position: relative;
width: calc(100vw / 1.75);
height: 70vh;
margin: auto 5vw;
text-align: center;
}
/* disable pointer events and text selection during drag */
#planes.dragged .plane-wrapper {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.plane-title {
position: absolute;
top: 50%;
left: 50%;
z-index: 1;
transform: translate3D(-50%, -50%, 0);
font-size: 4vw;
font-weight: 700;
line-height: 1.2;
text-transform: uppercase;
color: #032f4d;
text-stroke: 1px white;
-webkit-text-stroke: 1px white;
opacity: 0;
transition: color 0.5s, opacity 0.5s;
}
#planes.dragged .plane-title {
color: transparent;
}
.plane-wrapper.loaded .plane-title, .no-curtains .plane-title {
opacity: 1;
}
.plane {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.plane img {
/* hide original images if there's no WebGL error */
display: none;
/* prevent original image from dragging */
pointer-events: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
#drag-tip {
position: fixed;
right: 20px;
bottom: 20px;
left: 20px;
z-index: 1;
pointer-events: none;
font-size: 0.9em;
text-transform: uppercase;
color: #032f4d;
text-align: center;
}
/*** handling WebGL errors ***/
.no-curtains #planes {
transition: background-color 0.5s;
}
.no-curtains #planes.dragged {
background-color: #03879a;
}
.no-curtains .plane-title {
opacity: 1;
}
.no-curtains .plane {
display: flex;
overflow: hidden;
transition: filter 0.5s;
}
.no-curtains #planes.dragged .plane {
filter: grayscale(1);
}
.no-curtains .plane img {
display: block;
min-width: 100%;
min-height: 100%;
object-fit: cover;
}
}
@media screen and (orientation: portrait) {
#content {
max-height: 100vh;
}
#planes {
overflow: hidden;
width: 100vw;
padding: 2.5vh 0;
height: auto;
flex-direction: column;
}
.plane-wrapper {
width: 70vw;
height: calc(100vh / 1.75);
margin: 5vw 0;
}
.plane-title {
font-size: 10vw;
}
}
class Slider {
/*** CONSTRUCTOR ***/
constructor(options = {}) {
// our options
this.options = {
// slider state and values
// the div we are going to translate
element: options.element || document.getElementById("planes"),
// easing value, the lower the smoother
easing: options.easing || 0.1,
// translation speed
// 1: will follow the mouse
// 2: will go twice as fast as the mouse, etc
dragSpeed: options.dragSpeed || 1,
// duration of the in animation
duration: options.duration || 750,
};
// if we are currently dragging
this.isMouseDown = false;
// if the slider is currently translating
this.isTranslating = false;
// current position
this.currentPosition = 0;
// drag start position
this.startPosition = 0;
// drag end position
this.endPosition = 0;
// slider translation
this.translation = 0;
this.animationFrame = null;
// set up the slider
this.setupSlider();
}
/*** HELPERS ***/
// lerp function used for easing
lerp(value1, value2, amount) {
amount = amount < 0 ? 0 : amount;
amount = amount > 1 ? 1 : amount;
return (1 - amount) * value1 + amount * value2;
}
// return our mouse or touch position
getMousePosition(e) {
var mousePosition;
if(e.targetTouches) {
if(e.targetTouches[0]) {
mousePosition = [e.targetTouches[0].clientX, e.targetTouches[0].clientY];
}
else if(e.changedTouches[0]) {
// handling touch end event
mousePosition = [e.changedTouches[0].clientX, e.changedTouches[0].clientY];
}
else {
// fallback
mousePosition = [e.clientX, e.clientY];
}
}
else {
mousePosition = [e.clientX, e.clientY];
}
return mousePosition;
}
// set the slider boundaries
// we will translate it horizontally in landscape mode
// vertically in portrait mode
setBoundaries() {
if(window.innerWidth >= window.innerHeight) {
// landscape
this.boundaries = {
max: -1 * this.options.element.clientWidth + window.innerWidth,
min: 0,
sliderSize: this.options.element.clientWidth,
referentSize: window.innerWidth,
};
// set our slider direction
this.direction = 0;
}
else {
// portrait
this.boundaries = {
max: -1 * this.options.element.clientHeight + window.innerHeight,
min: 0,
sliderSize: this.options.element.clientHeight,
referentSize: window.innerHeight,
};
// set our slider direction
this.direction = 1;
}
}
/*** HOOKS ***/
// this is called once our mousedown / touchstart event occurs and the drag started
onDragStarted(mousePosition) {
}
// this is called while we are currently dragging the slider
onDrag(mousePosition) {
}
// this is called once our mouseup / touchend event occurs and the drag started
onDragEnded(mousePosition) {
}
// this is called continuously while the slider is translating
onTranslation() {
}
// this is called once the translation has ended
onTranslationEnded() {
}
// this is called before our slider has been resized
onBeforeResize() {
}
// this is called after our slider has been resized
onSliderResized() {
}
/*** ANIMATIONS ***/
// this will translate our slider HTML element and set up our hooks
translateSlider(translation) {
translation = Math.floor(translation * 100) / 100;
// should we translate it horizontally or vertically?
var direction = this.direction === 0 ? "translateX" : "translateY";
// apply translation
this.options.element.style.transform = direction + "(" + translation + "px)";
// if the slider translation is different than the translation to apply
// that means the slider is still translating
if(this.translation !== translation) {
// hook function to execute while we are translating
this.onTranslation();
}
else if(this.isTranslating && !this.isMouseDown) {
// if those conditions are met, that means the slider is no longer translating
this.isTranslating = false;
// hook function to execute after translation has ended
this.onTranslationEnded();
}
// finally set our translation
this.translation = translation;
}
// this is our request animation frame loop where we will translate our slider
animate() {
// interpolate values
var translation = this.lerp(this.translation, this.currentPosition, this.options.easing);
// apply our translation
this.translateSlider(translation);
this.animationFrame = requestAnimationFrame(this.animate.bind(this));
}
/*** EVENTS ***/
// on mouse down or touch start
onMouseDown(e) {
// start dragging
this.isMouseDown = true;
// apply specific styles
this.options.element.classList.add("dragged");
// get our touch/mouse start position
var mousePosition = this.getMousePosition(e);
// use our slider direction to determine if we need X or Y value
this.startPosition = mousePosition[this.direction];
// drag start hook
this.onDragStarted(mousePosition);
}
// on mouse or touch move
onMouseMove(e) {
// if we are not dragging, we don't do nothing
if(!this.isMouseDown) return;
// get our touch/mouse position
var mousePosition = this.getMousePosition(e);
// get our current position
this.currentPosition = this.endPosition + ((mousePosition[this.direction] - this.startPosition) * this.options.dragSpeed);
// if we're not hitting the boundaries
if(this.currentPosition > this.boundaries.min && this.currentPosition < this.boundaries.max) {
// if we moved that means we have started translating the slider
this.isTranslating = true;
}
else {
// clamp our current position with boundaries
this.currentPosition = Math.min(this.currentPosition, this.boundaries.min);
this.currentPosition = Math.max(this.currentPosition, this.boundaries.max);
}
// drag hook
this.onDrag(mousePosition);
}
// on mouse up or touchend
onMouseUp(e) {
// we have finished dragging
this.isMouseDown = false;
// remove specific styles
this.options.element.classList.remove("dragged");
// update our end position
this.endPosition = this.currentPosition;
// send our mouse/touch position to our hook
var mousePosition = this.getMousePosition(e);
// drag ended hook
this.onDragEnded(mousePosition);
}
// on resize we will need to apply old translation value to new sizes
onResize(e) {
this.onBeforeResize();
// get our old translation ratio
var ratio = this.translation / this.boundaries.sliderSize;
// reset boundaries and properties bound to window size
this.setBoundaries();
// reset all translations
this.options.element.style.transform = "tanslate3d(0, 0, 0)";
// calculate our new translation based on the old translation ratio
var newTranslation = ratio * this.boundaries.sliderSize;
// clamp translation to the new boundaries
newTranslation = Math.min(newTranslation, this.boundaries.min);
newTranslation = Math.max(newTranslation, this.boundaries.max);
// apply our new translation
this.translateSlider(newTranslation);
// reset current and end positions
this.currentPosition = newTranslation;
this.endPosition = newTranslation;
// call our resize hook
this.onSliderResized();
}
/*** SET UP AND DESTROY ***/
// set up our slider
// init its boundaries, add event listeners and start raf loop
setupSlider() {
this.setBoundaries();
// event listeners
// mouse events
window.addEventListener("mousemove", this.onMouseMove.bind(this), {
passive: true,
});
window.addEventListener("mousedown", this.onMouseDown.bind(this));
window.addEventListener("mouseup", this.onMouseUp.bind(this));
// touch events
window.addEventListener("touchmove", this.onMouseMove.bind(this), {
passive: true,
});
window.addEventListener("touchstart", this.onMouseDown.bind(this), {
passive: true,
});
window.addEventListener("touchend", this.onMouseUp.bind(this));
// resize event
window.addEventListener("resize", this.onResize.bind(this));
// launch our request animation frame loop
this.animate();
}
// will be called silently to cleanly remove the slider
destroySlider() {
// remove event listeners
// mouse events
window.removeEventListener("mousemove", this.onMouseMove, {
passive: true,
});
window.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mouseup", this.onMouseUp);
// touch events
window.removeEventListener("touchmove", this.onMouseMove, {
passive: true,
});
window.removeEventListener("touchstart", this.onMouseDown, {
passive: true,
});
window.removeEventListener("touchend", this.onMouseUp);
// resize event
window.removeEventListener("resize", this.onResize);
// cancel request animation frame
cancelAnimationFrame(this.animationFrame);
}
// call this method publicly to destroy our slider
destroy() {
// destroy everything related to the slider
this.destroySlider();
}
};
class WebGLSlider extends Slider {
/*** CONSTRUCTOR ***/
constructor(options) {
super(options);
// tweening
this.animation = null;
// value from 0 to 1 to pass as uniform to the WebGL
// will be tweened on mousedown / touchstart and mouseup / touchend events
this.effect = 0;
// our WebGL variables
this.curtains = null;
this.planes = [];
// we will keep track of the previous translation values on resize
this.previousTranslation = {
x: 0,
y: 0,
};
this.shaderPass = null;
// set up the WebGL part
this.setupWebGL();
}
/*** WEBGL INIT ***/
// set up WebGL context and scene
setupWebGL() {
// set up our WebGL context, append the canvas to our wrapper and create a requestAnimationFrame loop
// the canvas will be our scene containing all our planes
// this is the scene we will post process
this.curtains = new Curtains({
container: "canvas"
});
this.curtains.onError(function() {
// onError handles all errors during WebGL context initialization or plane creation
// we will add a class to the document body to display original images (see CSS)
document.body.classList.add("no-curtains");
});
// planes and shader pass
this.setupPlanes();
this.setupShaderPass();
}
/*** PLANES CREATION ***/
setupPlanes() {
// Planes
// each plane is bound to a HTML element to copy its size and position
// in this case this will be the slider inner items
// it will automatically create a WebGL texture for each image, canvas and video child of that element
var planeElements = document.getElementsByClassName("plane");
// our planes params
// we just pass our shaders tag ID and a uniform to animate opacity on load
var params = {
vertexShaderID: "slider-planes-vs",
fragmentShaderID: "slider-planes-fs",
uniforms: {
opacity: {
name: "uOpacity", // variable name inside our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
},
};
// add all our planes and handle them
for(var i = 0; i < planeElements.length; i++) {
// addPlane method adds a plane to our WebGL scene
// takes 2 params: our HTML referent element and the params set above
// it returns a Plane class object if creation is successful, false otherwise
var plane = this.curtains.addPlane(planeElements[i], params);
// if our plane has been successfully created
if(plane) {
// push it into our planes array
this.planes.push(plane);
// onReady is called once our plane is ready and all its texture have been created
plane.onReady(function() {
// inside our onReady function scope, this represents our plane
var currentPlane = this;
// add a "loaded" class to display the title
currentPlane.htmlElement.closest(".plane-wrapper").classList.add("loaded");
// animate plane opacity once they are loaded
var opacity = {
value: 0,
};
anime({
targets: opacity,
value: 1,
easing: "linear",
duration: 750,
update: function() {
// continualy increase opacity from 0 to 1
currentPlane.uniforms.opacity.value = opacity.value;
},
});
});
}
}
}
/*** SHADER PASS CREATION ***/
setupShaderPass() {
// Shader pass
// we will post process our scene
// that means we will apply shaders to our whole scene
// like for regular planes we will need params
// they will contain vertex and fragment shaders ID and our uniforms
var shaderPassParams = {
vertexShaderID: "distortion-vs",
fragmentShaderID: "distortion-fs",
uniforms: {
// apply the whole effect
// 0: no effect
// 1: full effect
dragEffect: {
name: "uDragEffect", // variable name inside our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
// our mouse position (in WebGL clip space coordinates)
mousePos: {
name: "uMousePos",
type: "2f", // this means our uniform is a length 2 array of floats
value: [0, 0],
},
// direction of our slider
// 0: horizontal drag
// 1: vertical drag
direction: {
name: "uDirection",
type: "1f",
value: this.direction,
},
// the background color when effect is applied
bgColor: {
name: "uBgColor",
type: "3f", // this means our uniform is a length 3 array of floats
value: [3, 135, 154], // rgb values
},
// our displacement texture offset
offset: {
name: "uOffset",
type: "2f",
value: [0, 0],
},
},
};
// addShaderPass adds a shader pass (Frame Buffer Object) to our WebGL scene
// returns a ShaderPass class object if successful, false otherwise
this.shaderPass = this.curtains.addShaderPass(shaderPassParams);
// if our shader pass has been successfully created
if(this.shaderPass) {
// we will add our displacement map texture
// first we load a new image
var image = new Image();
image.src = "https://www.martin-laxenaire.fr/medium/medias/islands-displacement.jpg";
// then we set its data-sampler attribute to use in fragment shader
image.setAttribute("data-sampler", "displacementTexture");
// finally we load it into our shader pass via the loadImage method
this.shaderPass.loadImage(image);
var self = this;
// onRender is called at each requestAnimationFrame call
this.shaderPass.onRender(function() {
// we will continuously offset our displacement texture on secondary axis
var secondaryDirection = self.direction === 0 ? 1 : 0;
self.shaderPass.uniforms.offset.value[secondaryDirection] = self.shaderPass.uniforms.offset.value[secondaryDirection] + 1;
});
}
}
/*** HELPER ***/
// this will update our shader pass mouse position uniform
updateMousePosUniform(mousePosition) {
// if our shader pass exists, update the mouse position uniform
if(this.shaderPass) {
// mouseToPlaneCoords converts window coordinates to WebGL clip space
var relativeMousePos = this.shaderPass.mouseToPlaneCoords(mousePosition[0], mousePosition[1]);
this.shaderPass.uniforms.mousePos.value = [relativeMousePos.x, relativeMousePos.y];
}
}
/*** HOOKS ***/
// this is called once our mousedown / touchstart event occurs and the drag started
onDragStarted(mousePosition) {
// pause and remove previous animation
if(this.animation) this.animation.pause();
anime.remove(slider);
// get a ref
var self = this;
// animate our mouse down effect
this.animation = anime({
targets: self,
effect: 1,
easing: 'easeOutCubic',
duration: self.options.duration,
update: function() {
if(self.shaderPass) {
// update our shader pass uniforms
self.shaderPass.uniforms.dragEffect.value = self.effect;
}
}
});
// enableDrawing to re-enable drawing again if we disabled it earlier
this.curtains.enableDrawing();
// update our shader pass mouse position uniform
this.updateMousePosUniform(mousePosition);
}
// this is called while we are currently dragging the slider
onDrag(mousePosition) {
// update our shader pass mouse position uniform
this.updateMousePosUniform(mousePosition);
}
// this is called once our mouseup / touchend event occurs and the drag started
onDragEnded(mousePosition) {
// calculate duration based on easing
var duration = 100 / this.options.easing;
var easing = 'linear';
// if there's no movement just tween the shader pass effect
if(Math.abs(this.translation - this.currentPosition) < 5) {
easing = 'easeOutCubic';
duration = this.options.duration;
}
// pause remove previous animation
if(this.animation) this.animation.pause();
anime.remove(slider);
// get a ref
var self = this;
this.animation = anime({
targets: self,
effect: 0,
easing: easing,
duration: duration,
update: function() {
if(self.shaderPass) {
// update drag effect
self.shaderPass.uniforms.dragEffect.value = self.effect;
}
}
});
// update our shader pass mouse position uniform
this.updateMousePosUniform(mousePosition);
}
// this is called continuously while the slider is translating
onTranslation() {
// get our slider translation and take our previous translation into account
var planeTranslation = {
x: this.direction === 0 ? this.translation - this.previousTranslation.x : 0,
y: this.direction === 1 ? this.translation - this.previousTranslation.y : 0,
};
// keep our WebGL planes position in sync with their HTML elements
for(var i = 0; i < this.planes.length; i++) {
// in the previous CodePen we were using updatePosition the method which handles positioning automatically
// however this method internally calls getBoundingClientRect() which causes a reflow and therefore impacts performance
// so we will position our planes manually with setRelativePosition instead, which does not trigger a layout repaint call
this.planes[i].setRelativePosition(planeTranslation.x, planeTranslation.y);
}
// shader pass displacement texture offset
if(this.shaderPass) {
// we will offset our displacement effect on main axis so it follows the drag
var offset = ((this.direction - 1) * 2 + 1) * this.translation / this.boundaries.referentSize;
this.shaderPass.uniforms.offset.value[this.direction] = offset;
}
}
// this is called once the translation has ended
onTranslationEnded() {
// we will stop rendering our WebGL until next drag occurs
if(this.curtains) {
this.curtains.disableDrawing();
}
}
// this is called after our slider has been resized
onSliderResized() {
// we need to update our previous translation value
this.previousTranslation = {
x: this.direction === 0 ? this.translation : 0,
y: this.direction === 1 ? this.translation : 0,
};
// reset our slides relative positions
// because during the resize their positions has already been updated internally
for(var i = 0; i < this.planes.length; i++) {
this.planes[i].setRelativePosition(0, 0);
}
// update our direction uniform
if(this.shaderPass) {
// update direction
this.shaderPass.uniforms.direction.value = this.direction;
}
}
/*** DESTROY ***/
// destroy all WebGL related things
destroyWebGL() {
// if you want to totally remove the WebGL context uncomment next line
// and remove what's after
//this.curtains.dispose();
// if you want to only remove planes and shader pass and keep the context available
// that way you could re init the WebGL later to display the slider again
if(this.shaderPass) {
this.curtains.removeShaderPass(this.shaderPass);
}
for(var i = 0; i < this.planes.length; i++) {
this.curtains.removePlane(this.planes[i]);
}
}
// call this method publicly to destroy our slider and the WebGL part
// override the destroy method of the Slider class
destroy() {
// destroy everything related to WebGL and the slider
this.destroyWebGL();
this.destroySlider();
}
}
// custom options
var options = {
easing: 0.1,
duration: 500,
dragSpeed: 1.75,
}
// let's go!
var slider = new WebGLSlider(options);
Also see: Tab Triggers