This article utilizes React, but the techniques are frontend-agnostic.
A Novel Algorithm for Stateless Navigation, Transitions and Buffering in Image Galleries
While working on a recent project, I needed to implement an image gallery with a navigable lightbox. I wanted the navigation to include swiping animations and opacity fades, and I wanted the previous and next images to pre-load for an optimal user experience. Initial brainstorms involved various bits of state management, Effect hooks and/or react-transition-group. I eschewed those paths in favor of number theory. The result is a single map function that handles all behavior declaratively via modular arithmetic.
To begin with, I created a lightbox to hold my images. A dissection of the lightbox itself is beyond the scope of this article, but a functional, simplified component follows.
- First, we’ll be using FontAwesome for our navigation arrows. Include the following in your index.html:
- Next, the Lightbox.js component. For content, I selected 10 random images from Unsplash.com.
- Lastly, Lightbox.css handles the styling. I intentionally used garish colors to help illustrate the various sections.
Though we’re a good distance from our goal, we now have a functional, responsive lightbox.
Let us imagine that we display 3 images at all times — previous, current and next. By intelligently varying their content and properties, we can ensure that only the current image is visible. Further, when navigating, we’re only required to swap one image which we are neither viewing nor transitioning from viewing.
This seems like a good use-case for linked lists, but digging deeper exposes patterns that can be exploited. A pencil, paper, coffee and 45 minutes produced this rough draft:
Let’s clean that up, fix the errors and make sense of it.
Note the patterns in the indexes (3, 3, 3, 6, 6, 6, etc) and in the offsets (0, +1, -1, 0, +1, -1, etc). These little bits of repetition and symmetry are no coincidence — we are looking at a collection of interconnected modular equations. If we can deduce algebraic formulae for this behavior, then we will have constructed a useful machine from modular arithmetic.
Recall that we have 3 images — previous, current and next — and that we only wish to see the current image. On the above graph, the “current” images (red squares, bold) are those whose “index we want to display” match the navIndex.
We can observe that these “current” images are rotating in a consistent pattern, returning to the same image every 3 steps; this yields a simple set of pseudocode equations for setting opacity:
Calculating navIndex Offsets
How do we calculate the offsets needed to generate the correct values of indexWeWantToDisplay? First, we can simplify things by observing that the offsets repeat every 3 steps.
Thus, we are looking for three equations, such that inputs of 0, 1 and 2 modulo 3 yield various outputs of 0, -1 and+1 modulo 3. Trial and error yielded three such equations:
To calculate indexWeWantToDisplay, these offsets are added to the navIndex. To ensure that our navigation wraps around, we take the result modulo the length of arrayOfUrls.
A Declarative Gallery
With our equations in hand, we can let mathematics worry about the content and opacity of our images. Let’s refactor generateImageAndBuffers().
Though not present in the above video, an issue does present itself. Occasionally, when clicking in rapid succession, an out-of-order image will flash momentarily. This is due to the GET request delay of the next-next image; before the request is fulfilled, a stale image is holding it’s place.
Five Beats Three
A solution to the stale image problem is to generalize the algorithm to higher orders. Any odd number may be used, but 5 images appear to be adequate. This ensures that the next-next and previous-previous images are always preloaded. The patterns and mathematics are similar, but we are now operating modulo 5.
Again, we refactor generateImageAndBuffers().
The last piece of the puzzle is translation — images should slide into and out of view. As the images are position: absolute in an overflow: hidden container, we are free to locate them at various points offscreen. Revisiting the above graph, we can add location identifiers Left1, Left2, Center, Right1 and Right2.
Here, again, a simple modular pattern emerges; our location identifiers are directly correlated with the offsets, which we are already calculating! We can simply multiply the offsets by a constant value and plug them into a transform: translateX. I found 50 view widths to be an acceptable constant.
Our generateImageAndBuffers function is the antithesis of DRY. We can refactor everything to a single map function. We’ll add a pointerEvents style to ensure that right-clicks target the current image. As well, we can abstract the buffer length to be dealer’s choice; though doing so requires generalizing the modular equations, which is outside the scope of this article. The end result brings us back to where we started:
In order to demonstrate what is happening under the hood, I made a version with modified styles; all of the mechanics are in plain view:
A final note: if the gallery size does not evenly divide the buffer size, there will be no transition when navigation wraps between the last and first image; this is expected behavior. One solution is to exploit least-common-multiples by updating the navigation to operate modulo (arrayOfUrls.length * buffer). Keep in mind other components relying on navIndex (such as a thumbnail carriage) will need to account for the change.
A fully functioning repository of the code in this article may be found at
A gallery lightbox utilizing modular arithmetic to declaratively handle navigation, transitions and image preloading. A…
A demonstration of the originating project may be viewed here: