Quick Start
1. Copy assets/ beside your page, then add this to your HTML <head>:
Code snippet omitted: it ships with the download.
The small synchronous probe prevents the fallback gallery from flashing before WebGL is ready. Its timeout reveals the source if a dependency fails to arrive.
2. Add the gallery to your HTML <body>:
Copy the <main data-lens-wall> block from index.html, then replace its twenty source links with your own. Each source item has this shape:
Code snippet omitted: it ships with the download.
Keep at least five images. The script distributes the source items across five desktop columns or two mobile columns and creates enough mesh instances to fill the stage. The source order is also the viewer’s previous/next order. data-caption is optional; the image’s alt text is used when it is absent.
3. Add this before your closing </body> tag:
Code snippet omitted: it ships with the download.
Already using three.js? Reuse your existing module import and assign it to window.THREE before loading script.js. Keep one three.js version on the page. The unminified and minified scripts are equivalent; load one of them. Serve your page over HTTP or HTTPS so module imports and images can load normally.
Using It With Your Own Design
Required markup: one [data-lens-wall] stage, a [data-wall-source] container containing image links marked [data-wall-item], and the control hooks from the supplied main block. Keep [data-wall-keys] empty: the script builds one keyboard control per lane. The native [data-wall-dialog] contains the close, previous, next, caption, counter and fallback-image hooks. Use a unique caption ID if mounting several stages in one document.
Demo styling: the dark ground, Mona Sans, compact charcoal controls and photographic selection are presentation choices. Replace the palette, typeface, caption styling and toolbar position without changing the data hooks. The twenty example images can all be replaced; their names have no special meaning to the effect.
CSS that must survive: the root needs a definite height, position: relative and clipped overflow. The canvas fills that root. Keep the html.gl and .is-live source-visibility rules, the fallback’s scrollable grid, .is-live canvas sizing, the native dialog’s transparent WebGL state and its opaque fallback state. touch-action: none belongs on the active full-screen wall so vertical touch dragging controls a lane; this is a takeover gallery, so place ordinary page navigation outside it. The keyboard lane controls intentionally use pointer-events: none while remaining focusable. Keep the image’s draggable="false" and CSS native-drag guard.
The supplied viewer is composed for a stage that fills its viewport, including an iframe viewport. A short embedded component requires adapting the viewer’s DOM positioning and mesh target together.
Options
Set these attributes on [data-lens-wall] before the script starts.
| Attribute | Values | Default | Description |
|---|---|---|---|
data-speed |
Pixels per second, including 0 |
14 |
Alternating automatic lane drift continues in the other columns while one is scrolled or dragged. 0 starts paused. |
data-strength |
0–1.3 |
1 |
Maximum lens intensity. 0 leaves sliding photographs flat. |
data-wheel |
true, false, all |
true |
Wheel input moves the lane under the pointer. all moves every lane; false leaves the wheel to the page. Drag and keyboard browsing still work. |
data-duration |
Seconds, including 0 |
0.82 |
Viewer opening/closing duration. Interrupted transitions use their remaining distance. |
data-caption |
Text | Image alt text | Optional caption on each [data-wall-item] link. |
Desktop uses five columns and viewports below 700px use two. These layout choices live in layout() and the matching CSS media query. Explicit zero values are preserved for every numerical option.
Choosing Your Images
The lens rewards visible geometry: architectural edges, layered interiors, a horizon, rows of objects or a clearly framed subject. At speed, those lines bow around the central field. Natural landscapes work too, but a wall containing only fog and featureless sky hides much of the mechanism.
The demo ships twenty WebP photographs at 1600 × 1067. Images are cover-fitted in the wall and contained in the large viewer, preserving their natural colour. Other aspect ratios work; leave space around a subject that must survive the wall’s landscape crop. Meaningful alt text doubles as a useful default viewer caption.
Use local or same-origin images. For cross-origin images, the host must permit CORS and the <img> must set crossorigin="anonymous" before loading. WebGL cannot upload a photograph the browser treats as an unsafe cross-origin source.
Accessibility
- Keyboard: Tab to a column. Up/Down selects every photograph in that column and centres it immediately; Left/Right moves focus between columns. Enter or Space opens the selected image. Each column button announces the current photograph and its navigation instructions.
- Viewer: native
<dialog>supplies focus containment and makes the background inert. Escape closes, Left/Right browses, and focus returns to the control that opened it. Close and navigation buttons retain visible focus rings. - Pause: the visible Pause control stops automatic drift and existing momentum. Its choice persists through opening and closing the viewer. A visitor can still move a paused wall deliberately.
- Reduced motion: the canvas and continuous motion are disabled. A normal, scrollable grid remains, with an instant image viewer and the same previous/next controls.
- No JavaScript or WebGL: semantic image links stay usable. With JavaScript but no WebGL, the native viewer still works. Without JavaScript, each link opens its original image.
Browser Support
The animated version requires WebGL 2, JavaScript modules/import maps, ResizeObserver and native <dialog>. Current desktop and mobile browsers supporting those APIs run the full wall. The source gallery remains available when WebGL is disabled, texture loading fails or a dependency cannot load. A lost GPU context temporarily restores the source; restoring the context resumes rendering.
Performance
Reduce photograph dimensions first if GPU memory is tight. Twenty 1600px images are deliberately generous for the large viewer; the script caps texture uploads at 960px wide on coarse pointers while keeping the original source image for the static viewer. Recycled meshes share those textures and one segmented geometry. No postprocessing or blur passes are used.
Canvas pixel ratio is capped at 2 on fine pointers and 1.5 on coarse pointers. Lower those limits in buildLensWall() for weaker devices. The ticker stops when the root leaves the viewport, the document is hidden or the WebGL context is lost. Continuous velocities and lens settling use elapsed time with a capped delta, keeping the feel consistent across refresh rates.
Dependencies
| Dependency | Role |
|---|---|
| GSAP 3.12+ (demo uses 3.15.0) | Plays the mesh-to-viewer transition, owns motion media queries and cleanup, and supplies the shared ticker. |
| three.js 0.180.0 | WebGL renderer, segmented mesh geometry and image textures. |
| GSAP plugins | None. |
| Lenis | Not used. |
Continuous column travel, momentum and lens decay are calculated in the ticker callback. The vertex shader applies the lens and interpolates the selected mesh into its viewer shape. GSAP animates that interpolation; it does not apply CSS transforms to the photographs.
The bundled control icons are from Tabler Icons, licensed under MIT. Keep assets/tabler-icons-LICENSE.txt with redistributed copies. They are inline SVGs and require no icon runtime.
Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.