preface
Hello, this is CSS magic – Alphardex.
We see a lot of hover and scroll effects, most of which are implemented using CSS and SVG, but there’s one effect they definitely can’t do: distort. Why? CSS is good at linear transformations, while SVG is good at curvilinear transformations. Contortions are neither. They are pixel-level transformations, and the only thing that can do that is canvas, and WebGL’s pixel shaders are really good at that, and we can use them to do all sorts of cool contortions. Here’s a look at the final implementation
Ah, Hajima route!
The preparatory work
The author’s three.js template can be copied by clicking on fork in the lower right corner
Implementation approach
The most important thing to achieve in this article is to synchronize the HTML world with the WebGL world!
Once the two worlds are in sync, all sorts of cool effects can be applied to native HTML elements without affecting basic interaction!
The world
Set the HTML
Step 1: Create a simple HTML page with all the images you want to display
<main class="overflow-hidden">
<div data-scroll>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/11/16/cqyJiYlRwnTeHmj.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/10/18/Ujf6n75o8TtIsWX.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/10/18/buDT4YS6zUMfHst.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/10/18/uXF1Kx7lzELB6wf.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/11/03/RtVq2wxQYySDb8L.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/11/16/FLnzi5Kq4tkRZSm.jpg" alt="" crossorigin="anonymous" />
</div>
</div>
</main>
<div class="twisted-gallery fixed -z-1 inset-0 w-screen h-screen"></div>
Copy the code
.w-240 {
width: 60rem;
}
.h-120 {
height: 30rem;
}
Copy the code
When you see the neat arrangement of images, you can hide them
img {
opacity: 0;
}
Copy the code
In our main class, we create a general function, and comment out any unimplemented functions that we will implement later
class TwistedGallery extends Base {...async init() {
this.createScene();
this.createPerspectiveCamera();
this.createRenderer();
this.createPlane();
// await preloadImages();
// this.createDistortImageMaterial();
// this.createImageDOMMeshObjs();
// this.setImagesPosition();
// this.listenScroll();
// this.createPostprocessingEffect()
this.createLight();
this.createOrbitControls();
this.addListeners();
this.setLoop(); }... }const start = () = > {
const twistedGallery = new TwistedGallery(".twisted-gallery".true);
twistedGallery.init();
};
Copy the code
Synchronize HTML and WebGL units
In order to render all the IMG in HTML in WebGL, we need to synchronize the pixel information of the IMG in HTML to WebGL
The question is, how do I make 1 unit in WebGL === 1px in HTML?
This is where we move on to the classic perspective diagram, and we need to calculate the foV according to the formula
class TwistedGallery extends Base {
constructor(sel: string, debug: boolean){...this.cameraPosition = new THREE.Vector3(0.0.600);
const fov = this.getScreenFov();
this.perspectiveCameraParams = {
fov,
near: 100.far: 2000}; }getScreenFov() {
return ky.rad2deg(
2 * Math.atan(window.innerHeight / 2 / this.cameraPosition.z) ); }}Copy the code
To verify this, set the width and height of the PlaneBufferGeometry function in createPlane to 100
The width and height of the image are 100px, exactly the same as the HTML!
The createPlane function has completed its test mission and can be deleted
Make sure the image loads
We want to make sure that all images are loaded before we display them, so we use the imagesLoaded library to check
import imagesLoaded from "https://cdn.skypack.dev/[email protected]";
const preloadImages = (sel = "img") = > {
return new Promise((resolve) = > {
imagesLoaded(sel, { background: true }, resolve);
});
};
Copy the code
Display the image on WebGL
We create a DOMMeshObject class that acts as a bridge to synchronize the information in the DOM to the WebGL world
You first get the length, width and position of the DOM elements, and then use this data to calculate their corresponding position and size in WebGL
class DOMMeshObject {
constructor(
el: Element,
scene: THREE.Scene,
material: THREE.Material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
) {
this.el = el;
const rect = el.getBoundingClientRect();
this.rect = rect;
const { width, height } = rect;
const geometry = new THREE.PlaneBufferGeometry(width, height, 10.10);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
this.mesh = mesh;
}
setPosition() {
const { mesh, rect } = this;
const { top, left, width, height } = rect;
const x = left + width / 2 - window.innerWidth / 2;
const y = -(top + height / 2 - window.innerHeight / 2) + window.scrollY;
mesh.position.set(x, y, 0); }}Copy the code
Create the image material in the WebGL world and pass the IMG element in HTML as a map to the shader, then synchronize the location
class TwistedGallery extends Base {
constructor(sel: string, debug: boolean){...this.images = [...document.querySelectorAll("img")];
this.imageDOMMeshObjs = [];
}
// Create a material
createDistortImageMaterial() {
const distortImageMaterial = new THREE.ShaderMaterial({
vertexShader: twistedGalleryMainVertexShader,
fragmentShader: twistedGalleryMainFragmentShader,
side: THREE.DoubleSide,
uniforms: {
uTexture: {
value: 0}}});this.distortImageMaterial = distortImageMaterial;
}
// Create the image DOM object
createImageDOMMeshObjs() {
const { images, scene, distortImageMaterial } = this;
const imageDOMMeshObjs = images.map((image) = > {
const texture = new THREE.Texture(image);
texture.needsUpdate = true;
const material = distortImageMaterial.clone();
material.uniforms.uTexture.value = texture;
const imageDOMMeshObj = new DOMMeshObject(image, scene, material);
return imageDOMMeshObj;
});
this.imageDOMMeshObjs = imageDOMMeshObjs;
}
// Set the image position
setImagesPosition() {
const { imageDOMMeshObjs } = this;
imageDOMMeshObjs.forEach((obj) = >{ obj.setPosition(); }); }}Copy the code
Vertex shader twistedGalleryMainVertexShader concur with the template
varying vec2 vUv;
void main(){
vec4 modelPosition=modelMatrix*vec4(position,1.);
vec4 viewPosition=viewMatrix*modelPosition;
vec4 projectedPosition=projectionMatrix*viewPosition;
gl_Position=projectedPosition;
vUv=uv;
}
Copy the code
A fragment shader twistedGalleryMainFragmentShader will map as a color display
uniform sampler2D uTexture;
varying vec2 vUv;
void main(){
vec2 newUv=vUv;
vec4 texture=texture2D(uTexture,newUv);
vec3 color=texture.rgb;
gl_FragColor=vec4(color,1.);
}
Copy the code
After some effort, the image appears on the screen, and now we have to scroll it around
Scroll up to
The scroll listener in this case can be implemented using the native Scroll event, but this article doesn’t do that. Why? Because the native Scroll event can only get the position of the scroll, not the speed of the scroll. If the user rolls fast, we need to show that in our special effects. So we’re going to use a library called Ectomic-Scroll, which captures the position and speed of the user’s scrolling
import LocomotiveScroll from "https://cdn.skypack.dev/[email protected]";
class TwistedGallery extends Base {
// Listen for scrolling
listenScroll() {
const scroll = new LocomotiveScroll({
getSpeed: true
});
scroll.on("scroll".() = > {
this.setImagesPosition();
});
this.scroll = scroll; }}Copy the code
Images are finally scrolling in the WEBGL world, and the two worlds are in sync
Now we begin our main show – distortion special effects!
Distortion effects
The effect at the beginning can be seen as a full-screen distortion of the image, so we’re going to use Postprocessing for that, which provides on-screen tDiffuse
import { RenderPass } from "https://cdn.skypack.dev/[email protected]/examples/jsm/postprocessing/RenderPass.js";
import { ShaderPass } from "https://cdn.skypack.dev/[email protected]/examples/jsm/postprocessing/ShaderPass.js";
import gsap from "https://cdn.skypack.dev/[email protected]";
class TwistedGallery extends Base {
constructor(sel: string, debug: boolean){...this.scrollSpeed = 0;
}
// Create a post-processing effect
createPostprocessingEffect() {
const composer = new EffectComposer(this.renderer);
const renderPass = new RenderPass(this.scene, this.camera);
composer.addPass(renderPass);
const customPass = new ShaderPass({
vertexShader: twistedGalleryPostprocessingVertexShader,
fragmentShader: twistedGalleryPostprocessingFragmentShader,
uniforms: {
tDiffuse: {
value: null
},
uRadius: {
value: 0.75
},
uPower: {
value: 0}}}); customPass.renderToScreen =true;
composer.addPass(customPass);
this.composer = composer;
this.customPass = customPass;
}
// Set the scroll speed
setScrollSpeed() {
const scrollSpeed = this.scroll.scroll.instance.speed || 0;
gsap.to(this, {
scrollSpeed: Math.min(Math.abs(scrollSpeed) * 1.25.2),
duration: 1
});
}
/ / animation
update() {
if (this.customPass) {
this.setScrollSpeed();
this.customPass.uniforms.uPower.value = this.scrollSpeed; }}}Copy the code
As you can see, we passed the scroll speed as uniform to the shader, and used GSAP to implement the easing effect. Now comes the shader part
Vertex shader twistedGalleryPostprocessingVertexShader keep up with the template, wen said the skip
Fragment shader twistedGalleryPostprocessingFragmentShader is responsible for the dynamic calculation of uv coordinates, a bit difficult to implement, the key is how to move, the value displayed with gl_FragColor inside, it’ll be easier to understand value changes
uniform sampler2D tDiffuse;
uniform float uRadius;
uniform float uPower;
varying vec2 vUv;
void main(){
vec2 pivot=vec2(. 5);
vec2 d=vUv-pivot;
float rDist=length(d);
float gr=pow(rDist/uRadius,uPower);
float mag=2.-cos(gr1.);
vec2 uvR=pivot+d*mag;
vec4 color=texture2D(tDiffuse,uvR);
gl_FragColor=color;
}
Copy the code
The project address
Twisted Gallery
The last
The effects implemented in this article are just one of many, but what really matters is the process of synchronizing HTML with the WebGL world, and once you’ve mastered this process, making a really cool site isn’t that hard.
This article is participating in the “Nuggets 2021 Spring Recruitment Campaign”, click to see the details of the campaign