Displaying objects on scrolls is now almost standard. However, when it comes to HTML, there aren’t that many options available. So I decided to add a little excitement using WebGL effects.
start with the plane
Let’s examine a simple PlaneGeometry object with a ShaderMaterial. What possibilities does this combination offer?
Well, there are actually a lot! Check out our tutorial on expanding images for another interesting effect.
But this time I wanted to explore pixel effects. So how can we achieve pixelation in a shader?
It’s actually very simple. You just need to round the UVs to the nearest pixel. that’s all! It has a pixelated effect. Suppose you want a 10 “pixel” grid.
vec2 uv = floor(vUv * 10.0) / 10.0;
These are UVs, but by sampling the texture you get the following effect:
I also added a border to each pixel to emphasize the effect a bit.
Another small caveat is that if your image isn’t square, you’ll get the same amount of rows and columns, so the pixels won’t be square either. To avoid this, instead of multiplying the UVs by a scalar value, do the following: vec2
:
vec2 gridSize = vec2(
20.,
floor(20./ASPECT_RATIO)
);
vec2 uv = floor(vUv * gridSize) / gridSize;
In this way, there are not only (approximately) square pixels, but also an integral number of pixels in rows and columns. On top of that, I added a few more simple effects, such as background curtains and changing the amount of pixelation. Here are the great results:
Operate by scrolling
There are many ways to connect HTML and new effects. The easiest way right now is to use React Three Fiber.
R3F has a great library of useful modules thanks to the Poimandres team.The library name is Drei and there
The purpose of this component is to allow parts of a 3D scene to be inserted directly into the DOM. Yes, it’s basically magic (indistinguishable from super cutting-edge science 😅)
This example is taken from the documentation.
//react code
return (
<main ref=container>
<h1>Html content here</h1>
<!-- here we go, MESH in HTML, just like that -->
<View style= width: 200, height: 200 className="canvas-view">
<mesh geometry=foo />
<OrbitControls />
</View>
<Canvas eventSource=container>
<View.Port /> // this is where our Views will be in 3D world
</Canvas>
</main>
)
As you can see, you can incorporate Three.js objects directly into your HTML. Additionally, you can control events using native HTML events. This is a huge step forward in terms of integrating these two worlds.
So in my demo I was able to use the native IntersectionObserver API to run all the animations in WebGL.
Use GSAP
GSAP recently released some great hooks for React, so I decided to use them as well.have isIntersecting
I did this using parameters from the native IntersectionObserver API.
import useGSAP from "@gsap/react";
import gsap from "gsap";
...
useGSAP(() =>
gsap.to(material,
uProgress: isIntersecting ? 1 : 0,
duration: 1.5,
);
, [isIntersecting]);
This way the animation will run every time the image is displayed in the view.
Of course it could be more complicated, but in my case I just used the “uProgress” uniform and did all the magic inside the shader. For more information about GSAP/React integration, please refer to the official documentation.
Scroll synchronization between HTML and WebGL
The final complication is the synchronization issue between WebGL and HTML. Because they are still two different layers. All WebGL is rendered to a full-screen canvas at the top of the page. These two worlds must scroll together. And, of course, if you decide to use native scrolling, you’ll end up with a situation where the HTML scrolls natively and WebGL scrolls when it gets the scroll event.
You might imagine they’re the same thing, but there’s a slight delay and wobble between the layers.
To resolve this, you will need to use a custom scrolling solution. Something like Lenis or a locomotive scroll. In my case, I used Lenis as a global effect within React Three Fiber.
import addEffect from "@react-three/fiber";
import Lenis from "@studio-freight/lenis";
const lenis = new Lenis();
addEffect(
So now our <View />
It's in the viewport, so just change the uniform of the material uProgress
From 0 to 1. That's all. I created a shader animation in HTML.
Another important thing to note here is that although we are mixing HTML and WebGL declaratively in this nice cocktail, they are two different worlds, and the usual React Three Fiber hooks won't work for example. about it. <View />
component.
function MyView()
const scene = useThree()
>>R3F: Hooks can only be used within the Canvas component!
return (
<View style= width: 200, height: 200 className="canvas-view">
<mesh geometry=foo />
<OrbitControls />
</View>
)
Despite being inserted into the Three.js scene via <View.port>
, it's not actually from the WebGL root. It all seems pretty simple, but to understand these caveats, you have to keep in mind what's actually going on behind the scenes.
But even with these considerations, you can't underestimate how much simpler React makes these things.I'm not a big fan of using React for scrollable landing animations, but the integration that It's easy, you have to think twice.
The last word
I hope you like this integration example. Please share how you animate images and your favorite examples. And please support open source developers. Thanks to them, we have such great infrastructure today:
On-scroll animation ideas for sticky sections