A tutorial on how to build a fluid media gallery in React powered by WebGL.

This delightfully playful gallery is perfect for spicing up a user flow between multiple full pages of content. It’s responsive and very simple to use.

react-fluid-gallery and live demo

The implementation of react-fluid-gallery is broken up into two layers: React and WebGL. The majority of the gallery’s code is agnostic to React, which allows us to easily extend the technique to other UI frameworks like Vue, Angular, or plain HTML+JS.

Play around with the demo & then let’s dive into how it all works!

TL;DR Show me the code!

React Wrapper

The ReactFluidGallery component kicks things off by mounting an HTML5 canvas. This canvas will serve as the render target for the underlying WebGL simulation, FluidGallery, which we’ll talk about in a bit.

It’s worth noting that is a common pattern in graphics programming. React stores a retained scenegraph that handles render updates when changes occur, and we’re introducing an imperative escape hatch into this retained tree of components via an HTML5 canvas. (more info on this distinction)

In addition to mounting the canvas render target, ReactFluidGallery includes logic for passing on resize, scroll, and touch events to the underlying FluidGallery simulation.

ReactFluidGallery wrapper component.

WebGL Simulation

The underlying FluidGallery class implements the core gallery scrolling and WebGL display logic.

All of the WebGL bits are greatly simplified thanks to Three.js, which provides some convenient abstractions and renders its output to the previously mounted canvas.

WebGL FluidGallery class.

Let’s break down what’s going on here in detail.

  1. FluidGallery.contructor(): First, we initialize all of our slides as WebGL Textures. _initTexture takes special care to differentiate between video and image textures. We then follow fairly standard Three.js initialization steps, including creating a WebGLRenderer, a PerspectiveCamera, a ShaderMaterial that wraps our custom vertex and fragment shaders, and a 3D Scene that contains one object, a single Plane that will be resized to fit the entire render canvas. The Plane has our ShaderMaterial attached, so our custom fragment shader will run once per pixel on the output canvas.
  2. FluidGallery.onScroll(event): This is called from our React wrapper every time the user scrolls via the mouse wheel or touch events.
  3. FluidGallery.update(): This gets called once per animation frame. We’re using requestAnimationFrame via the raf package to ensure that our simulation updates and renders smoothly. Internally, we’re aggregating the current scroll velocity via this._speed and adding it to this._position each frame. We also apply some friction to the scroll speed so it eventually slows down. this._position is a floating point number that goes from zero up to the number of slides in the gallery, where an integer value means a single slide is being shown and a floating point value between two integers represents a partial transition between those two slides. Finally, we update all of the shader parameters to reflect their current values depending on which slide(s) are currently visible.
  4. FluidGallery.render(): This also gets called once per animation frame. It just renders the Three.js scene to the target canvas.
Fragment shader for FluidGallery effect.

The Three.js scene uses a shader material to render pixels on the canvas. WebGL v1 shader pipelines include a vertex shader and a fragment shader. For our purposes, we’re using a very simple vertex shader with a more complicated fragment shader displayed above.

Note that these shaders are not JavaScript, but rather GLSL source files that are exported from JavaScript via ES6 template strings for simplicity of bundling. Most bundlers don’t know how to handle glsl files, but by exporting them as strings from JS files, they’ll work with any JavaScript bundler.

The real magic happens in this fragment shader, which takes in two textures and some parameters detailing the current state of the simulation. For example, if progress is 0, texture1 will be the first slide, texture2 will be the second slide, and the output of the fragment shader will be a pixel for pixel reconstruction of texture1. If progress is 2.5, for instance, texture1 will be the third slide, texture2 will be the fourth slide, and the output will be a roughly 50% mix between texture1 and texture2.

If you’d like to learn more about the power of GLSL shaders, check out this great list of resources!

⭐Please ️star react-fluid-gallery on GitHub! ⭐

Credit

The original version of this awesome gallery technique was published on the personal website of Tao Tajima.

The React package was bundled using create-react-library.

If you enjoyed this article, please consider ⭐️ starring ⭐️ the repo to help others find it as well!