A sophisticated fusion of trigonometric displacement and state-driven animation that transforms a simple slider into a fluid, high-fidelity interactive experience. It’s a masterclass in leveraging the GPU to achieve organic visual transitions without sacrificing architectural clarity.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
I Attempted a Shockwave Content Slider That Ripples Between Images Like Water (Three.js + GSAP)Added:
While browsing around for a new video idea, I stumbled upon this site called From Another. There is a lot of really impressive work on there, but the thing that caught my eye was the scroll-driven image slider. Every time you click, the text animates [music] out and the slide image transitions with this ripple distortion that radiates outward from the center, almost like dropping a stone into water. Super smooth, super satisfying. I knew right away I had to try building something like this. After some digging, I found a shader on ShaderToy that had a similar shockwave ripple effect going on. Not exactly the same, but close enough to use as a starting point. So, I took that as my base and built a simplified version of the slider around it. I didn't spend too much time on the text animations here.
We have covered such text transitions in plenty of past videos, so that part should feel familiar. The real focus this time is the click-driven image transition itself, that fluid, water-like ripple that sweeps across the frame as the slides [music] change. In this video, we'll go through the entire build, the HTML, the styling, the Three.js scene, the fragment shader that powers the ripple effect, and the GSAP animations that handle the text. If you'd like to grab the source code for this project, along with hundreds of other similar micro projects, and a brand new website template every month, you can check out the pro membership via the link in the description. And if you find my work helpful, make sure you leave a like on the video and subscribe to the channel if you haven't already.
All right, let's get into the code.
First, we'll set up a single full-screen wrapper, a div with the class slider.
This is the container for everything, the WebGL canvas, >> [music] >> the text, all of it. Inside that, we'll add a div with the class slide content.
This acts [music] as the overlay layer for the text that sits on top of the image, positioned absolutely so it covers the entire slider. Now, inside slide content, we'll place two divs, one with the class slide title and another with the class slide description. The title will sit on the left, the description on the right. That's [music] the layout we'll be going for. Inside slide title, I'll drop an H1 with the first slide setting and inside slide description, a paragraph with some placeholder copy to match. You only need to hard code this first slide in the HTML. The rest will be handled dynamically using a data object I'll show you later. All right, the markup is done. Let's get to styling.
First of all, we'll do a quick global reset, zero out margins, paddings, and set box sizing to border box across everything. Then on the body, we'll apply our font New Montreal. Clean and modern, fits the vibe perfectly. Next, the H1. We'll give it a fluid size using clamp, so it scales nicely with the viewport. A medium weight, tight letter spacing, and a snug line height. The paragraph just gets a medium weight to keep things balanced. Now, let's style the slider. We'll fix it to the viewport, full width, full height with a warm off-white background and overflow hidden, so nothing spills out. Then for the canvas element inside the slider, this is the WebGL surface 3.js will render into. We'll set it to block and have it fill the entire container.
Moving on to slide content, this sits absolutely positioned covering the full width and height of the slider. The key here is mix-blend-mode set to difference. That's what gives the text its inverted color effect against the image. We'll also disable user selection and pointer events since this layer is purely visual and bump its z-index so it sits above the canvas. Next, slide title. We'll position it absolutely, vertically centered on the left side with the color set to white. Width stays at max-content, so it hugs the text naturally. Then slide description, same absolute positioning, vertically centered, but pinned to the right side instead. We'll give it a narrow width with a minimum, so it doesn't collapse too small. Set the color to white and [music] use flexbox with a column direction and some gap for spacing. Now, we need to prep for the text animations.
[music] Both the character and line classes, these are the elements SplitText will generate later, get set to inline block with will-change on transform and relative positioning. This ensures everything animates cleanly without layout issues. And finally, [music] a media query for smaller screens, the title moves to the dead center of the viewport instead of sitting on the left, the description shifts to centered horizontally, pushed down near the bottom, and given a wider width so the text has room [music] to breathe. All right, that covers all the CSS. Let's get into the JavaScript.
Before we jump into the main script, let's look at our slide data. I've created a separate file called slides.js. This is the object I mentioned earlier. It exports [music] an array of four slides. Each one holds a title, a description, and an image path.
This is the single source of truth for all the content in the slider. The first slide matches what we already have in the HTML, and the rest get built dynamically as the user clicks through.
Next, the shaders. These live in their own file, too, shaders.js. The vertex shader is straightforward. It just passes the UV coordinates through to the fragment shader, nothing fancy here, standard boilerplate. The fragment shader is where the magic happens. It takes in a bunch of uniforms, the current and next textures, a progress value, resolution, image dimensions, and a set of wave controls for the ripple effect. There is also a mobile flag that we'll use to adjust the image framing on smaller screens. First up, there is a helper function called get image UV.
This handles the aspect ratio math. It figures out how to fit the image inside a bounding box without stretching.
Basically, a cover fit calculation done entirely on the GPU. Then a simple inside box check. This just tells us whether a pixel falls within the image bounds so we [music] can clip anything outside to transparent. Now, the main function. It starts by defining the bounding box for the image. On desktop, it's a centered crop. On mobile, it fills the full viewport, and the mobile uniform blends between the two. Then it corrects for the screen's aspect ratio and calculates each pixel's distance from the center. The core of the effect lives inside the progress check. As the ripple expands outward from the center, any pixel that falls behind the wave front gets displaced, pushed along the radial direction using a sine wave multiplied by an exponential decay that gives us the ripple distortion. On top of that, the same wave drives a brightness [music] boost. Finally, the displaced coordinates get fed back through the UV mapping function, both textures get sampled, and the final color is a mix of the two based on the blend value with the brightness boost added on top. Anything outside the bounding box gets clipped to transparent. Let's get to the script now.
First, we'll bring in everything we need: GSAP and the SplitText plugin for the text animations, Three.js for the WebGL rendering, then our vertex [music] and fragment shaders from the shaders file, and the slide data from slides.js.
After that, we'll register SplitText with GSAP, required step otherwise the plugin won't work. Next, a few state variables. We'll track the current slide index starting at zero, a boolean to flag whether a transition is in progress, and a reference for the ripple tween so we can kill it if needed. Then, we'll grab the slider element from the DOM. This is the main container everything hooks into. Now, let's write our first utility function, split title.
This takes a container, finds the H1 inside it, and runs SplitText on it, splitting into words and characters with masking on the characters. If there is no heading, it bails out early. We'll be calling this every time we need to animate a title in or out. Then, a similar one, split description. This grabs all the paragraphs inside the description, loops through each one, splits them into lines with masking, and collects all the line elements into a single flat array. This gives us one clean collection to animate against, regardless of how many paragraphs a slide has. Next, build slide content.
This is the function that dynamically creates a new slide when the user clicks through. It builds a div with the slide content class, sets its opacity to zero so it's hidden on entry, then populates the inner HTML with the title and description pulled straight from the slide data object. This is why we only needed one slide hardcoded in the HTML.
The rest get built on the fly using this function. Now, the exit animation, animate text out. It takes the current slide container, splits the title and description using the two functions we just wrote, then builds a GSAP timeline.
The title characters slide upward out of view with a stagger, and the description lines follow the same pattern with a slight offset. The timeline gets returned, so we can chain off it later.
Then the mirror of that, animate text in. Same idea, but in reverse. It splits the text, sets all the characters and lines [music] to start below their mask, then makes the container visible and animates everything upward into place.
Characters first, then lines with a slight overlap. This runs right after the new slide gets appended to the DOM.
All right, [music] those are all our helper functions. Now, let's set up the Three.js scene. This is the WebGL layer that handles the image transitions with the ripple effect. We'll create a new scene and an orthographic camera. No perspective direction here, just a flat plane filling the viewport. The camera sits one unit back on the Z axis, looking straight at the geometry. Then the renderer, we'll create a WebGL renderer with anti-aliasing and alpha enabled, cap the pixel ratio so it doesn't blow up on high DPI screens, and set the clear color to transparent. Then we'll prepend the canvas into the slider. Prepend, not append, so it sits behind the text overlay in the DOM order. Next, we'll set up the texture loader and an empty array to hold our textures. Then we'll loop through every slide in our data and load each image as a Three.js texture. We are awaiting each load, so we know all textures are ready before anything renders. On each one, we set the filtering to linear and the wrapping to clamp to edge. This keeps the images clean without any tilting or blurry edges. All right, that sets up all the foundations, imports, state, helper functions, text animations, [music] and the Three.js scene with all the textures preloaded. Next, we'll define the ripple config and wire everything into the shader. Let's define the ripple config, a flat object holding all the values that control the wave effect, frequency, power, width, falloff, boost strength, cross fade width, along with the duration and ease for the Gsap tween that drives it. [music] Having everything in one place makes it easy to tweak the feel of the ripple effect without digging through the shader code.
Next, the uniform subject. This is the bridge between JavaScript and the fragment shader. We'll pass in the current and next textures, [music] a progress value starting at zero, the viewport resolution, the image dimensions, and then the ripple config values we just defined. [music] There is also a mobile flag set to one if the viewport is below our breakpoint, zero otherwise. Every one of these maps directly to uniform we declared in the shader earlier. Then we'll create the shader material passing in our vertex and fragment shaders along with the uniforms with transparency enabled.
We'll build a simple plane geometry, apply the material to it, and add it to the scene. This single flat plane is our entire rendering surface. The shader does all the heavy lifting. Now, a small utility, get max corner distance. This calculates the distance from the center of the viewport to the farthest corner, accounting for the aspect ratio. We need this to know how far the ripple has to travel before it fully clears the screen. Then the resize handler. On every resize, we'll update the render size, push the new resolution into the uniforms, recalculate the mobile flag, and recompute the ripple's end value using that corner distance function plus the wave width. We also adjust the ripple duration, shorter on mobile so it doesn't feel sluggish on a smaller screen. We'll bind this to the window resize event and call it once right away so everything is correct on load. Now, for the intro animation, we'll grab the initial slide that's already in the HTML, split its title and description using a helper function, then animate both in with a from to method.
Characters and lines start below their mask and slide up into view with a stagger. The lines get a slight delay so they follow the title naturally. This is the first thing the user sees when the page loads. Next comes the main event, the transition function. This fires every time the user clicks the slider.
First, we check if a transition is already running and bail out if it is.
If there is an existing ripple tween still going, they kill it and reset progress to zero. Then we calculate the next slide index, wrapping around to zero when we hit the end of the array.
We grab the current slide from the DOM and kick off the text exit animation. On the shader side, we set the current and next textures, reset progress to zero, then start the ripple tween. Then this animates the progress uniform from zero all the way to the end value, driving the entire ripple expansion in the shader. Inside the on update callback, there is a clever bit. Once progress passes a certain threshold, we unlock clicking early, so the user doesn't have to wait for the full ripple to finish before triggering the next transition.
On complete, we swap the current texture to the new one, reset progress, and clean up. Meanwhile, once the text exit timeline finishes, we remove the old slide from the DOM, build a new one using our build slide content function, append it to the slider, and on the next frame, animate the new text in. The image ripple and the text swap happen in parallel. That's what gives the transition its layered feel. Then we'll hook it all up, a single click listener on the slider that calls the transition function.
And finally, the render loop, a straightforward request animation frame loop that tells 3.js to render the scene on every frame. We call it once to kick things off, and that's a wrap, a click-driven image slider with a WebGL ripple transition and masked text animations. 3.js handling the visuals, GSAP and SplitText handling the typography. Hope you found the video helpful. See you in the next one.
Related Videos
Agentforce NOW AMA: Build with React and Salesforce Multi-Framework
SalesforceDevs
490 views•2026-05-28
How agent o11y differs from traditional o11y — Phil Hetzel, Braintrust
aiDotEngineer
450 views•2026-05-28
WEB TECHNOLOGIES UNIT-2 | Degree 4th sem BCOM Computers web technologies unit-2 full explanation💯✅
LearnwithSahera
1K views•2026-05-29
More tests are always better? How to use AI to identify tests that bring little value
Alliance4Qualification
335 views•2026-05-29
Search Algorithms Explained in 60 Seconds! 🤖💨
samarthtuliofficial
218 views•2026-06-01
People of Game of Thrones using JavaScript DOM
AltCampus
296 views•2026-05-30
Introduction to Problem Solving Part - 1 | Lecture 1 | Intermediate DSA
ascensionix
107 views•2026-05-29
🚀 BCS613C Compiler Design | Module 1 to 5 Schema Evaluation 🔥 | VTU 6th Sem 💯 #VTU #bcs613c #exam
Pranavaa-y4y
104 views•2026-06-02











