<div id="canvas"></div>
<div id="content">
<div class="webGLSlider">
<div class="wrapper">
<div class="slide-container">
<div class="slide" data-name="Patterns and Waves" data-url="#">
<img src="https://unsplash.it/1920/823?random=1" data-sampler="planeTexture" crossorigin>
</div>
<div class="slide" data-name="Cadillac" data-url="#">
<img src="https://unsplash.it/1920/823?random=2" data-sampler="planeTexture" crossorigin>
</div>
<div class="slide" data-name="Laso" data-url="#">
<img src="https://unsplash.it/1920/823?random=3" data-sampler="planeTexture" crossorigin>
</div>
<div class="slide" data-name="WebGL is fun" data-url="#">
<img src="https://unsplash.it/1920/823?random=4" data-sampler="planeTexture" crossorigin>
</div>
<div class="slide" data-name="Zach Saucier" data-url="#">
<img src="https://unsplash.it/1920/823?random=5" data-sampler="planeTexture" crossorigin>
</div>
<div class="slide" data-name="You found me!" data-url="#">
<img src="https://unsplash.it/1920/823?random=6" data-sampler="planeTexture" crossorigin>
</div>
</div>
</div>
<h6 class="line-reveal">Recent work</h6>
<a id="slider-link" class="arrow-link" href="#">
<h3 id="slider-title" class="slider-heading">Project name</h3>
<h3 class="slider-heading text-right"><div class="text">View Project</div><svg class="hover-arrow" width="77" height="35" viewBox="0 0 77 35" xmlns="http://www.w3.org/2000/svg"><path d="M58.5295 0L54.8195 3.85L66.5095 14.7H0.189453V19.81H66.5795L54.8195 30.73L58.5295 34.58L76.4495 17.29L58.5295 0Z" fill="#fff"/></svg></h3>
</a>
</div>
</div>
<script id="slider-planes-vs" type="x-shader/x-vertex">#version 300 es
// Determines how much precision for the GPU to use
#ifdef GL_ES
precision mediump float;
#endif
// in = passed in from a data buffer
// uniforms = passed in from CPU (our program)
// out = passed from our vertex shader to our fragment shader
// Default mandatory attributes
in vec3 aVertexPosition;
in vec2 aTextureCoord;
// Mandatory projection and model view matrices are generated by curtains
// It will position and size our plane based on its HTML element CSS values
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// This is generated by curtains 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;
// The un-transformed mouse position
const vec2 uMouse = vec2(0.0, 0.0);
// Texture coord varying that will be passed to our fragment shader
out vec2 vTextureCoord;
// Our transformed mouse position that will be passed to our fragment shader
out vec2 vMouse;
void main() {
// Use texture matrix and original texture coords to generate accurate texture coords
vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
// Convert from vertex pos to texture pos and apply texture matrix to the mouse position as well
vMouse = (planeTextureMatrix * vec4((uMouse + 1.0) * 0.5, 0.0, 1.0)).xy - 0.5;
// Apply our vertex position based on the projection and model view matrices
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>
<script id="slider-planes-fs" type="x-shader/x-fragment">#version 300 es
// Determines how much precision for the GPU to use
#ifdef GL_ES
precision mediump float;
#endif
// Our texture samplers - Their names match the data-sampler attributes on our image tags
uniform sampler2D planeTexture; // The image texture
// Our texture coords (on a pixel-by-pixel basis)
in vec2 vTextureCoord;
in vec2 vMouse; // The mouse position in texture coordinates
out vec4 outputColor; // The output color of each pixel (what was gl_FragColor in WebGL1)
// FOR MOUSE EFFECTS
const float uRadius = 0.1; // Radius of pixels to warp/invert
const float transAmnt = 0.05; // Amount to translate the texture by
const float PI = 3.1415926535;
const float aspect = 2.33333333; // 21 / 9
// FOR ANTIALIAS
uniform vec2 uResolution;
#define R uResolution
// This basically gives you antialias for free without the need of a second pass
// If you don't need antialias in another place, local antiliasing is probably best
// From here: https://www.shadertoy.com/view/3sjGDh
// Which is explained here: https://shadertoyunofficial.wordpress.com/
// (Look for antialias in the search box)
#define S(v) smoothstep(2.0/R.y, 0.0, v)
void main() {
vec2 myUV = vTextureCoord;
outputColor.a = 1.0;
// Check if pixel is within the given radius of the mouse
vec2 diff = myUV - vMouse - 0.5;
diff.x *= aspect;
float distance = length(diff);
// Create the fish-eye effect
if (distance <= uRadius) {
float scale = (1.0 - cos(distance/uRadius * PI * 0.5));
myUV = vMouse + normalize(diff) * uRadius * scale + 0.5;
}
// Translate the texture
myUV += -vMouse * transAmnt;
vec3 tex = texture(planeTexture, myUV).rgb;
// Antialiasing
vec3 inverted = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);
if(uRadius > 0.0) {
outputColor.rgb = mix( inverted, tex, S(distance - uRadius));
} else {
outputColor.rgb = inverted.rgb;
}
}
</script>
* {
box-sizing: border-box;
}
html {
font-size: 25px;
}
body {
background: #1d1d1d;
font-family: Asap, sans-serif;
}
#canvas {
position: fixed;
top: 0;
right: 0;
left: 0;
height: 100vh;
}
#content {
position: relative;
z-index: 1;
}
.wrapper {
position: relative;
overflow: hidden;
}
.slide-container {
position: relative;
}
.slide-container::before {
display: block;
content: "";
width: 100%;
padding-top: 42.857%; /* 9 / 21 */
}
.slide {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.slide img {
width: 100%;
}
.webGLSlider img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.webGLSlider {
color: white;
}
.slider-heading {
text-transform: uppercase;
margin: 0;
}
.webGLSlider a {
color: inherit;
text-decoration: none;
}
.text-right {
text-align: right;
}
.text-right .text {
display: inline-block;
vertical-align: middle;
margin-right: 2rem;
}
.hover-arrow {
vertical-align: middle;
}
.split-child {
display: inline-block;
}
.split-parent {
overflow: hidden;
}
.webGLSlider img {
display: none;
}
.no-curtains img:not(.texture) {
display: block;
}
View Compiled
// Save references to elements that we will use
const wrapper = document.querySelector(".webGLSlider .wrapper");
const slideContainer = document.querySelector(".webGLSlider .slide-container");
const slides = gsap.utils.toArray(".webGLSlider .slide");
const proxy = document.createElement("div");
const webGLSlider = document.querySelector('.webGLSlider');
let sliderTitle = document.querySelector('#slider-title');
const sliderLink = document.querySelector('#slider-link');
const numSlides = slides.length;
const wrapIndex = gsap.utils.wrap(0, numSlides);
const gap = 0;
let lastIndex = 0; // Used in resize and to change out headings
let firstRun = true;
// These are set in the resize function
let slideHeight;
let slideWidth;
let wrapWidth;
let wrapVal;
let animation;
let draggable;
// Variables related to the WebGL functionality
const planes = [];
//////////////////////////////
// Heading change functions //
//////////////////////////////
function setupHeadingAnim() {
// Split it twice to be able to reveal it from hidden overflow
webGLSlider.childSplit = new SplitText('.slider-heading', {
type: 'lines',
linesClass: 'split-child'
});
webGLSlider.parentSplit = new SplitText('.slider-heading', {
type: 'lines',
linesClass: 'split-parent'
});
// The text reveal animation
webGLSlider.anim = gsap.from(webGLSlider.childSplit.lines, {
paused: true,
yPercent: 100,
stagger: 0.2
});
if(firstRun) {
webGLSlider.anim.play();
firstRun = false;
}
}
// Check to see if the index should be changed and set up the heading changes
function checkIndex(endX) {
// Get the new index
endX = -endX || 0;
const newIndex = wrapIndex(endX / (slideWidth + gap));
// Only do stuff if it's a new index
if(typeof webGLSlider.anim === "undefined" || lastIndex !== newIndex) {
// Undo pre anim
if(webGLSlider.anim) {
webGLSlider.anim.progress(1).kill();
webGLSlider.parentSplit.revert();
webGLSlider.childSplit.revert();
}
// Update index, text, and link
sliderTitle = document.querySelector('#slider-title');
lastIndex = newIndex;
const dataset = slides[newIndex].dataset;
sliderTitle.innerText = dataset.name;
sliderLink.href = dataset.url;
// Create new anim
setupHeadingAnim();
setupArrowLinks();
}
}
//////////////////////////////////
// Create and handle the slider //
//////////////////////////////////
function resize() {
// Get the new values
slideWidth = gsap.getProperty('.slide', 'width');
slideHeight = gsap.getProperty('.slide', 'height');
const widthUnit = (slideWidth + gap);
wrapWidth = numSlides * widthUnit;
wrapVal = gsap.utils.wrap(0, wrapWidth);
// Setup our slider with the new values
const pxOffset = lastIndex !== numSlides - 1 ? lastIndex * -widthUnit : widthUnit;
gsap.set(slideContainer, { left: -widthUnit });
gsap.set(proxy, { x: pxOffset });
// The animation that's used to do the sliding
animation = gsap.fromTo(slides, {
x: i => wrapVal(i * widthUnit)
}, {
duration: 1,
x: `+=${wrapWidth}`,
ease: "none",
paused: true,
// This creates the infinite looping
modifiers: {
x: function(x, target) {
return `${parseInt(x) % wrapWidth}px`;
}
}
})
// Set progress to correct value
.progress(1 - lastIndex / numSlides);
// Kill off the old draggable instance
if(draggable) {
draggable.kill();
}
// Recreate the draggable with the new values
draggable = Draggable.create(proxy, {
type: "x",
trigger: ".wrapper",
inertia: true,
snap: {
x: (x) => {
return Math.round(x / widthUnit) * widthUnit;
}
},
// Our event listeners
onDragStart: () => webGLSlider.anim.timeScale(-3),
onDrag: updateProgress,
onRelease: function() { checkIndex(this.endX) }, // Update our index (and headings if need be)
onThrowUpdate: updateProgress,
onThrowComplete: () => webGLSlider.anim.play()
})[0];
// Update the resolution in the WebGL planes
planes.forEach(plane => plane.uniforms.resolution.value = [innerWidth, innerHeight]);
}
// Update the slider along with the necessary WebGL variables
function updateProgress() {
// Update the actual slider
animation.progress(wrapVal(this.x) / wrapWidth);
// Update the WebGL slider planes
planes.forEach(plane => plane.updatePosition());
}
//////////////////////
// Arrow hover code //
//////////////////////
const arrowLinks = document.querySelectorAll(".arrow-link");
const playLinkAnim = (e) => e.target.anim.play();
const reverseLinkAnim = (e) => e.target.anim.reverse();
function setupArrowLinks() {
arrowLinks.forEach(link => {
if(link.anim) {
link.anim.kill();
link.removeEventListener("mouseenter", playLinkAnim);
link.removeEventListener("mouseleave", reverseLinkAnim);
}
link.svg = link.querySelector("svg");
link.path = link.svg.querySelector("path");
link.anim = gsap.timeline({
paused: true,
defaults: {
duration: 0.3
}
})
.from(link.svg, {
attr: {
width: 44,
height: 36,
viewBox: "0 0 44 36"
}
})
.fromTo(link.path, {
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" }
}, {
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" }
}, 0)
link.addEventListener("mouseenter", playLinkAnim);
link.addEventListener("mouseleave", reverseLinkAnim);
});
}
////////////////
// Init stuff //
////////////////
window.addEventListener("load", e => {
window.addEventListener("resize", resize);
resize();
checkIndex();
// Create a new curtains instance
const curtains = new Curtains({ container: "canvas", autoRender: false });
// Use a single rAF for both GSAP and Curtains
function renderScene() {
curtains.render();
}
gsap.ticker.add(renderScene);
// Create a curtains plane for each slide
const planeElements = document.querySelectorAll(".slide");
// Params passed to the curtains instance
const params = {
vertexShaderID: "slider-planes-vs", // The vertex shader we want to use
fragmentShaderID: "slider-planes-fs", // The fragment shader we want to use
// The variables that we're going to be animating to update our WebGL state
uniforms: {
// For the antialiasing
resolution: {
name: "uResolution",
type: "2f",
value: [innerWidth, innerHeight]
}
},
};
// Create a new plane for each slider
planeElements.forEach((planeEl, i) => {
const plane = new Plane(curtains, planeEl, params);
// If our plane has been successfully created
if(plane) {
// Push it into our planes array
planes.push(plane);
// onReady is called once our plane is ready and all its texture have been created
plane.onLoading(function(texture) {
// Scale up the texture (for the translation effect)
texture.setScale(new Vec2(1.2, 1.2));
}).onReady(function() {
// One could use "this" instead of "plane" here
// Add a "loaded" class to display image container
plane.htmlElement.closest(".slide").classList.add("loaded");
});
}
});
});
This Pen doesn't use any external CSS resources.