Today I’d like to share a little menu effect with you. It is composed of two things which is an SVG path overlay animation when it opens (or closes) and an infinite CSS powered background animation of an image grid.
Nothing special really, but I enjoyed putting it together and hopefully it is somehow useful to you!
The SVG path animation for the overlay is based on this demo by Sebastien Gilbert which is a good starter for a nice motion. If you need to adjust paths, I can recommend this fantastic path editor tool by Yann Armelin.
The infinite background animation of the menu is made with a CSS animation. The trick is to have a repeated set of images and once we translate to the visually equal part, we restart the animation.
In this tutorial we’ll implement an infinite circular gallery using WebGL with OGL based on the website Lions Good News 2020 made by SHIFTBRAIN inc.
Most of the steps of this tutorial can be also reproduced in other WebGL libraries such as Three.js or Babylon.js with the correct adaptations.
With that being said, let’s start coding!
Creating our OGL 3D environment
The first step of any WebGL tutorial is making sure that you’re setting up all the rendering logic required to create a 3D environment.
Usually what’s required is: a camera, a scene and a renderer that is going to output everything into a canvas element. Then inside a requestAnimationFrame loop, you’ll use your camera to render a scene inside the renderer. So here’s our initial snippet:
In our createRenderer method, we’re initializing a renderer with a fixed color background by calling this.gl.clearColor. Then we’re storing our GL context (this.renderer.gl) reference in the this.gl variable and appending our <canvas> (this.gl.canvas) element to our document.body.
In our createCamera method, we’re creating a new Camera() instance and setting some of its attributes: fov and its z position. The FOV is the field of view of your camera, what you’re able to see from it. And the z is the position of your camera in the z axis.
In our createScene method, we’re using the Transform class, that is the representation of a new scene that is going to contain all our planes that represent our images in the WebGL environment.
The onResize method is the most important part of our initial setup. It’s responsible for three different things:
Making sure we’re always resizing the <canvas> element with the correct viewport sizes.
Updating our this.camera perspective dividing the width and height of the viewport.
Storing in the variable this.viewport, the value representations that will help to transform pixels into 3D environment sizes by using the fov from the camera.
The approach of using the camera.fov to transform pixels in 3D environment sizes is an approach used very often in multiple WebGL implementations. Basically what it does is making sure that if we do something like: this.mesh.scale.x = this.viewport.width; it’s going to make our mesh fit the entire screen width, behaving like width: 100%, but in 3D space.
And finally in our update, we’re setting our requestAnimationFrame loop and making sure we keep rendering our scene.
You’ll also notice that we already included the wheel, touchstart, touchmove, touchend, mousedown, mousemove and mouseup events, they will be used to include user interactions with our application.
Creating a reusable geometry instance
It’s a good practice to keep memory usage low by always reusing the same geometry reference no matter what WebGL library you’re using. To represent all our images, we’re going to use a Plane geometry, so let’s create a new method and store this new geometry inside the this.planeGeometry variable.
The reason for including heightSegments and widthSegments with these values is being able to manipulate vertices in a way to make the Plane behave like a paper in the air.
Importing our images using Webpack
Now it’s time to import our images into our application. Since we’re using Webpack in this tutorial, all we need to do to request our images is using import:
import Image1 from 'images/1.jpg'
import Image2 from 'images/2.jpg'
import Image3 from 'images/3.jpg'
import Image4 from 'images/4.jpg'
import Image5 from 'images/5.jpg'
import Image6 from 'images/6.jpg'
import Image7 from 'images/7.jpg'
import Image8 from 'images/8.jpg'
import Image9 from 'images/9.jpg'
import Image10 from 'images/10.jpg'
import Image11 from 'images/11.jpg'
import Image12 from 'images/12.jpg'
Now let’s create our array of images that we want to use in our infinite slider, so we’re basically going to call the variables above inside a createMedia method, and use .map to create new instances of the Media class (new Media()), which is going to be our representation of each image of the gallery.
As you’ve probably noticed, we’re passing a bunch of arguments to our Media class, I’ll explain why they’re needed when we start setting up the class in the next section. We’re also duplicating the amount of images to avoid any issues of not having enough images when making our gallery infinite on very wide screens.
It’s important to also include some specific calls in the onResize and update methods for our this.medias array, because we want the images to be responsive:
Our Media class is going to use Mesh, Program and Texture classes from OGL to create a 3D plane and attribute a texture to it, which in our case is going to be our images.
In our constructor, we’re going to store all variables that we need and that were passed in the new Media() initialization from index.js:
Explaining a few of these arguments, basically the geometry is the geometry we’re going to apply to our Mesh class. The this.gl is our GL context, useful to keep doing WebGL manipulations inside the class. The this.image is the URL of the image. Both of the this.index and this.length will be used to do positions calculations of the mesh. The this.scene is the group which we’re going to append our mesh to. And finally this.screen and this.viewport are the sizes of the viewport and environment.
Now it’s time to create the shader that is going to be applied to our Mesh in the createShader method, in OGL shaders are created with Program:
In the snippet above, we’re basically creating a new Texture() instance, making sure to use generateMipmaps as false so it preserves the quality of the image. Then creating a new Program() instance, which represents a shader composed of fragment and vertex with some uniforms used to manipulate it.
We’re also creating a new Image() instance to preload the image before applying it to the texture.image. And also updating the this.program.uniforms.uImageSizes.value because it’s going to be used to preserve the aspect ratio of our images.
It’s important to create our fragment and vertex shaders now, so we’re going to create two new files: fragment.glsl and vertex.glsl:
The Mesh instance is stored in the this.plane variable to be reused in the onResize and update methods, then appended as a child of the this.scene group.
The only thing we have now on the screen is a simple square with our image:
Let’s now implement the onResize method and make sure we’re rendering rectangles:
The scale.y and scale.x calls are responsible for scaling our element properly, transforming our previous square into a rectangle of 700×900 sizes based on the scale.
And the uViewportSizes and uPlaneSizes uniform value updates makes the image display correctly. That’s basically what makes the image have the background-size: cover; behavior, but in WebGL environment.
Now we need to position all the rectangles in the x axis, making sure we have a small gap between them. To achieve that, we’re going to use this.plane.scale.x, this.padding and this.index variables to do the calculation required to move them:
And in the update method, we’re going to set the this.plane.position to these variables:
update () {
this.plane.position.x = this.x
}
Now you’ve setup all the initial code of Media, which results in the following image:
Including infinite scrolling logic
Now it’s time to make it interesting and include scrolling logic on it, so we have at least an infinite gallery in place when the user scrolls through your page. In our index.js, we’ll do the following updates.
First, let’s include a new object called this.scroll in our constructor with all variables that we will manipulate to do the smooth scrolling:
In our update method with requestAnimationFrame, we’ll lerp the this.scroll.current with this.scroll.target to make it smooth, then we’ll pass it to all medias:
As you’ve noticed, it’s not infinite yet, to achieve that, we need to include some extra code. The first step is including the direction of the scroll in the update method from index.js:
Now in the Media class, you need to include a variable called this.extra in the constructor, and do some manipulations on it to sum the total width of the gallery, when the element is outside of the screen.
That’s it, now we have the infinite scrolling gallery, pretty cool right?
Including circular rotation
Now it’s time to include the special flavor of the tutorial, which is making the infinite scrolling also have the circular rotation. To achieve it, we’ll use Math.cos to change the this.mesh.position.y accordingly to the rotation of the element. And map technique to change the this.mesh.rotation.z based on the element position in the z axis.
First, let’s make it rotate in a smooth way based on the position. The map method is basically a way to serve values based on another specific range, let’s say for example you use map(0.5, 0, 1, -500, 500);, it’s going to return 0 because it’s the middle between -500 and 500. Basically the first argument controls the output of min2 and max2:
And that’s the result we get so far. It’s already pretty cool because you’re able to see the rotation changing based on the plane position:
Now it’s time to make it look circular. Let’s use Math.cos, we just need to do a simple calculation with this.plane.position.x / this.widthTotal, this way we’ll have a cos that will return a normalized value that we can just tweak multiplying by how much we want to change the y position of the element:
Simple as that, we’re just moving it by 75 in environment space based in the position, this gives us the following result, which is exactly what we wanted to achieve:
Snapping to the closest item
Now let’s include a simple snapping to the closest item when the user stops scrolling. To achieve that, we need to create a new method called onCheck, it’s going to do some calculations when the user releases the scrolling:
Now the gallery is always being snapped to the correct entry:
Writing paper shaders
Finally let’s include the most interesting part of our project, which is enhancing the shaders a little bit by taking into account the scroll velocity and distorting the vertices of our meshes.
The first step is to include two new uniforms in our this.program declaration from Media class: uSpeed and uTime.
Now let’s write some shader code to make our images bend and distort in a very cool way. In your vertex.glsl file, you should include the new uniforms: uniform float uTime and uniform float uSpeed:
uniform float uTime;
uniform float uSpeed;
Then inside the void main() of your shader, you can now manipulate the vertices in the z axis using these two values plus the position stored variable in p. We’re going to use a sin and cos to bend our vertices like it’s a plane, so all you need to do is including the following line:
Also don’t forget to include uTime increment in the update() method from Media:
this.program.uniforms.uTime.value += 0.04
Just this line of code outputs a pretty cool paper effect animation:
Including text in WebGL using MSDF fonts
Now let’s include our text inside the WebGL, to achieve that, we’re going to use msdf-bmfont to generate our files, you can see how to do that in this GitHub repository, but basically it’s installing the npm dependency and running the command below:
After running it, you should now have a .png and .json file in the same directory, these are the files that we’re going to use on our MSDF implementation in OGL.
Now let’s create a new file called Title and start setting up the code of it. First let’s create our class and use import in the shaders and the files:
import AutoBind from 'auto-bind'
import { Color, Geometry, Mesh, Program, Text, Texture } from 'ogl'
import fragment from 'shaders/text-fragment.glsl'
import vertex from 'shaders/text-vertex.glsl'
import font from 'fonts/freight.json'
import src from 'fonts/freight.png'
export default class {
constructor ({ gl, plane, renderer, text }) {
AutoBind(this)
this.gl = gl
this.plane = plane
this.renderer = renderer
this.text = text
this.createShader()
this.createMesh()
}
}
Now it’s time to start setting up MSDF implementation code inside the createShader() method. The first thing we’re going to do is create a new Texture() instance and load the fonts/freight.png one stored in src:
Then we need to start setting up the fragment shader we’re going to use to render the MSDF text, because MSDF can be optimized in WebGL 2.0, we’re going to use this.renderer.isWebgl2 from OGL to check if it’s supported or not and declare different shaders based on it, so we’ll have vertex300, fragment300, vertex100 and fragment100:
createShader () {
const vertex100 = `${vertex}`
const fragment100 = `
#extension GL_OES_standard_derivatives : enable
precision highp float;
${fragment}
`
const vertex300 = `#version 300 es
#define attribute in
#define varying out
${vertex}
`
const fragment300 = `#version 300 es
precision highp float;
#define varying in
#define texture2D texture
#define gl_FragColor FragColor
out vec4 FragColor;
${fragment}
`
let fragmentShader = fragment100
let vertexShader = vertex100
if (this.renderer.isWebgl2) {
fragmentShader = fragment300
vertexShader = vertex300
}
this.program = new Program(this.gl, {
cullFace: null,
depthTest: false,
depthWrite: false,
transparent: true,
fragment: fragmentShader,
vertex: vertexShader,
uniforms: {
uColor: { value: new Color('#545050') },
tMap: { value: texture }
}
})
}
As you’ve probably noticed, we’re prepending fragment and vertex with different setup based on the renderer WebGL version, let’s create also our text-fragment.glsl and text-vertex.glsl files:
Finally let’s create the geometry of our MSDF font implementation in the createMesh() method, for that we’ll use the new Text() instance from OGL, and then apply the buffers generated from it to the new Geometry() instance:
Simple as that, we’re just including a new Title() instance inside our Media class, this will output the following result for you:
One of the best things about rendering text inside WebGL is reducing the overload of calculations required by the browser when animating the text to the right position. If you go with the DOM approach, you’ll usually have a little bit of performance impact because browsers will need to recalculate DOM sections when translating the text properly and checking composite layers.
For the purpose of this demo, we also included a new Number() class implementation that will be responsible for showing the current index that the user is seeing. You can check how it’s implemented in source code, but it’s basically the same implementation of the Title class with the only difference of it loading a different font style:
Including background blocks
To finalize the demo, let’s implement some blocks in the background that will be moving in x and y axis to enhance the depth effect of it:
To achieve this effect we’re going to create a new Background class and inside of it we’ll initialize some new Plane() geometries in a new Mesh() with random sizes and positions by changing the scale and position of the meshes of the for loop:
import { Color, Mesh, Plane, Program } from 'ogl'
import fragment from 'shaders/background-fragment.glsl'
import vertex from 'shaders/background-vertex.glsl'
import { random } from 'utils/math'
export default class {
constructor ({ gl, scene, viewport }) {
this.gl = gl
this.scene = scene
this.viewport = viewport
const geometry = new Plane(this.gl)
const program = new Program(this.gl, {
vertex,
fragment,
uniforms: {
uColor: { value: new Color('#c4c3b6') }
},
transparent: true
})
this.meshes = []
for (let i = 0; i < 50; i++) {
let mesh = new Mesh(this.gl, {
geometry,
program,
})
const scale = random(0.75, 1)
mesh.scale.x = 1.6 * scale
mesh.scale.y = 0.9 * scale
mesh.speed = random(0.75, 1)
mesh.xExtra = 0
mesh.x = mesh.position.x = random(-this.viewport.width * 0.5, this.viewport.width * 0.5)
mesh.y = mesh.position.y = random(-this.viewport.height * 0.5, this.viewport.height * 0.5)
this.meshes.push(mesh)
this.scene.addChild(mesh)
}
}
}
Then after that we just need to apply endless scrolling logic on them as well, following the same directional validation we have in the Media class:
Note: from now on I’m planning to release simple “components” and explain their basic working principle in tiny articles. In this first one I’m going to look at the infinite looping scroll illusion.
A while back a came across a really nice menu on Madeleine Dalla’s incredible website that was infinitely scrollable. I wondered how that was achieved and after searching for existing solutions, I found this great demo by Vincent Orback on Codepen. It shows how to pull off that effect with sections on a page. I wanted to use his script to make it work for a menu.
The principle of how this works is not too complicated: there’s a bunch of menu items that we need to clone in order to make sure that we have enough items to create a scroll illusion. The illusion works like this: once we scroll and reach the cloned items, we reset the scroll position to 0. So, as soon as the same (visual) point is reached, we jump back to the beginning.
How many clones do we need? We need as many clones as items fit into the visible area. As an example, if 8 items fit into the height of the viewport, than we need to create 8 clones.
The amount of menu items is important when considering how much space they’ll take up on the screen (or scroll area). If your items don’t fill the screen fully, the illusion will break. So you need to make sure to have enough and to set a reasonable font size for the items to occupy enough space.
We let the menu be scrollable but we hide the scrollbar. The menu is covering the whole viewport and this is the element we scroll.
Tip: if you want to visualize the illusion, just make sure that the scrollbar is not hidden. You’ll see it jumping back to the top, once the “cloned” zone is reached. Just delete:
::-webkit-scrollbar {
display: none;
}
… and the line scrollbar-width: none; for Firefox.
Note that I use a fallback for mobile where I simply want to show the complete menu.