Skip to content

Instantly share code, notes, and snippets.

@CodeMyUI
Created December 13, 2019 00:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save CodeMyUI/e2a1e7e2a705056a194064502b36ce69 to your computer and use it in GitHub Desktop.
Save CodeMyUI/e2a1e7e2a705056a194064502b36ce69 to your computer and use it in GitHub Desktop.
WebGL enhanced javascript drag slider (performance improved - bonus)
<!-- div that will hold our WebGL canvas -->
<div id="canvas"></div>
<div id="content">
<h1 id="title">Skylines</h1>
<!-- drag slider -->
<div id="planes">
<div class="plane-wrapper">
<span class="plane-title">Hong Kong</span>
<div class="plane">
<img src="https://source.unsplash.com/4hluhoRJokI/1280x720" data-sampler="planeTexture"alt="Photo by Simon Zhu on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Chicago</span>
<div class="plane">
<img src="https://source.unsplash.com/waedcXSIAKk/1280x720" data-sampler="planeTexture" alt="Photo by Pedro Lastra on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Shanghai</span>
<div class="plane">
<img src="https://source.unsplash.com/D8iZPlX-2fs/1280x720" data-sampler="planeTexture" alt="Photo by Denys Nevozhai on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">New York</span>
<div class="plane">
<img src="https://source.unsplash.com/y9WmMWaiftc/1280x720" data-sampler="planeTexture" alt="Photo by Pedro Lastra on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Tokyo</span>
<div class="plane">
<img src="https://source.unsplash.com/IocJwyqRv3M/1280x720" data-sampler="planeTexture" alt="Photo by Louie Martinez on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Singapore</span>
<div class="plane">
<img src="https://source.unsplash.com/6wvbIX02V9M/1280x720" data-sampler="planeTexture" alt="Photo by Sonali Sharma on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Toronto</span>
<div class="plane">
<img src="https://source.unsplash.com/451DMKNITAM/1280x720" data-sampler="planeTexture" alt="Photo by Syed Ahmed on Unsplash" crossorigin />
</div>
</div>
<div class="plane-wrapper">
<span class="plane-title">Kuala Lumpur</span>
<div class="plane">
<img src="https://source.unsplash.com/a1TnJHVCGN0/1280x720" data-sampler="planeTexture" alt="Photo by Azlan Baharudin 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 top edge is a float from 0 to 1
float distanceToTop = distance(vec2(vTextureCoord.x, 1.0), vTextureCoord);
// calculate an effect that goes from 0 to 1 depenging on uOpacity and distanceToTop
float spreadFromTop = clamp((uOpacity * (1.0 - distanceToTop) - 1.0) + uOpacity * 2.0, 0.0, 1.0);
// handle pre-multiplied alpha on rgb values and use spreadFromTop as alpha.
finalColor = vec4(vec3(finalColor.rgb * spreadFromTop), spreadFromTop);
// 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 renderTexture;
// 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 / 3600.0)) / vec2(1.0, 1.0));
vec2 verticalPhase = fract(vec2(vDispTextureCoord.x * (uOffset.x / 3600.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, 3.0);
// 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 / 2.0;
horizontalTexCoords.y += spreadFromMouse * fishEye.y * displacementEffect * 3.0;
vec2 verticalTexCoords = textureCoords;
verticalTexCoords.x += spreadFromMouse * fishEye.x * displacementEffect * 3.0;
verticalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect / 2.0;
// use the right value for the right direction
textureCoords = mix(horizontalTexCoords, verticalTexCoords, uDirection);
// get our final colored and BW vec4
vec4 finalColor = texture2D(renderTexture, 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);
float spreadFromMouseAdjusted = spreadFromMouse / sqrt(2.0);
// apply a grey background where we don't have nothing to draw
finalColor = mix(vec4(uBgColor.r * spreadFromMouseAdjusted / 255.0, uBgColor.g * spreadFromMouseAdjusted / 255.0, uBgColor.b * spreadFromMouseAdjusted / 255.0, spreadFromMouseAdjusted), finalColor, finalColor.a);
gl_FragColor = finalColor;
}
</script>
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: [13, 13, 13], // 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/skylines-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 = {
duration: 500,
dragSpeed: 1.5,
}
// let's go!
var slider = new WebGLSlider(options);
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/2.2.0/anime.min.js"></script>
<script src="https://www.curtainsjs.com/build/curtains.min.js"></script>
@media screen {
html, body {
min-height: 100%;
}
body {
margin: 0;
font-size: 18px;
font-family: 'Oswald', Verdana, sans-serif;
background: black;
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: white;
text-align: center;
}
#planes {
width: calc(((100vw / 1.75) + 10vw) * 8); /* width of items * number of items */
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: black;
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 {
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;
pointer-events: none;
font-size: 0.9em;
text-transform: uppercase;
color: #888;
text-align: center;
}
/*** handling WebGL errors ***/
.no-curtains #planes {
transition: background-color 0.5s;
}
.no-curtains #planes.dragged {
background-color: #0d0d0d;
}
.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 {
position: relative;
width: 70vw;
height: calc(100vh / 1.75);
margin: 5vw 0;
}
.plane-title {
font-size: 10vw;
}
}

WebGL enhanced javascript drag slider (performance improved - bonus)

To help promote the release of curtains.js v3.0 and introduce the post-processing capabilities of the library, I've wrote a 3 parts tutorial on how to build a WebGL enhanced drag slider on medium.

This pen is a fork of the original end result of the third part of this tutorial: https://medium.com/@martin.laxenaire/webgl-enhanced-drag-slider-tutorial-with-curtains-js-part-3-4ea0791b4a54

A Pen by Martin Laxenaire on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment