135

RGB Split Infinite Canvas

£7 or the whole Vault, £79.20

An endless WebGL field of photographs you drag in any direction, with no walls: at speed every print splits into red, green and blue reads along its own direction of travel, while the print under your hand lifts and stays sharp.

advanced
7 more details
webgl-shaderdragmomentumvelocity-reactiveinfinite-loopchromatic-aberrationkeyboard-navigation

This demo reads better at your own screen size than in the frame below:

Released

About this effect

Your photographs sit as prints on a quiet grey ground, in staggered columns that carry on forever in every direction. Grab the field and pull: there are no edges to hit, and a throw keeps it gliding until it settles.

Read the full effect overview

Speed is what you see. As the field moves, each print separates into red, green and blue reads that trail and lead along its own direction of travel, so a black and white field turns to colour only while it is moving, and the faster it moves the wider the reads sit. The print under your hand is the exception: it lifts, its shadow deepens and it stays in register, while the field streams past around it, splitting more the further a print is from your hand. Prints near your hand also set off a fraction earlier than the ones further out, so for a moment the faster ones slide over the slower ones like prints pushed across a table. Let go and the field glides, catches up and every print lands back on the grid with the colour gone, exactly.

RGB Split Infinite Canvas - GSAP animation effect preview

What's included

10 items
  • Endless field that wraps in both directions: drag anywhere, forever, with no walls
  • Red, green and blue split along each print's own velocity, rising from a dead band so slow movement stays clean
  • The print under your hand lifts and stays sharp while the rest of the field splits around it
  • Delayed wake: prints near the hand move first and ride over the slower ones, then settle back onto the grid
  • Momentum with a frame-rate independent glide; the split and lift return to exactly zero
  • Correct per-channel compositing, so the reads overlap the ground, the shadows and other prints cleanly
  • Arrow keys step the field one print at a time with no animation; drag and touch work alike
  • Each print fades in once its photograph has decoded: no grey placeholders while images load
  • Small API (panBy, jumpBy) and options for split strength, speed range, wake, focus and glide
  • Reduced motion, no JavaScript or no WebGL: a scrolling grid of the same photographs

Perfect for

4 use cases
  • Photographer and studio portfolios that want the whole archive explorable at once
  • Agency and design studio work indexes, labs and experiments pages
  • Fashion, music and editorial image archives with a fast, graphic feel
  • Campaign moodboards and exhibition walls

How it works

2 sections

The photographs are an ordinary list of images, which stays in the page for screen readers and is the whole effect as a scrolling grid when JavaScript or WebGL is unavailable or reduced motion is set. The script textures each print from those images and draws the field with three.js: a pool of prints that wraps in both directions as you drag.

The field keeps a short history of where it has been. Each print reads that history a fraction of a second late, more the further it is from your hand, which is what makes the prints near your hand set off first. Each print's speed sets how far apart its red, green and blue reads sit, and the print under your hand is held in register. The render loop owns all of that motion. GSAP handles the reduced-motion and pointer-type branches, the clean teardown, and the short fade each print gets when its photograph arrives. Everything it adds is removed on revert().

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying RGB Split Infinite Canvas opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£7 Standard license, unlimited projects

Browse free effects

Or the whole library: the Vault, £79.20, one payment.

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

The inline script is not optional. The list of photographs is the fallback for no JavaScript, no WebGL and reduced motion, and without this it paints for the half second it takes three.js to arrive, then gets swapped for the canvas. The probe runs before first paint, stamps html.gl when a canvas is coming, and the stylesheet hides the list under that class. No JavaScript, no class, the list shows; the effect removes the class again if it cannot build a renderer after all.

2. Add the effect HTML where you want the field:

Code snippet omitted: it ships with the download.

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

The script textures each print from your <img> elements (no second download), drops a canvas in front of the list, and moves the list off screen once the canvas is actually there. The list stays in the page, so screen readers still get every photograph and its description.

Why the import map rather than a plain <script src> for three.js: three ships as ES modules only, and its old UMD build logs a deprecation warning on every page load. The shim imports it as a module (dynamically, so a blocked CDN falls back to your photographs instead of a blank stage), puts it on window, and then loads assets/script.js as an ordinary script, so the effect itself stays a plain file you can drop into any build, or none.

Already using three.js as a module? Skip the shim and set window.THREE before assets/script.js runs:

Code snippet omitted: it ships with the download.

Choosing Your Images

This effect has a real content requirement. At speed the shader pulls each print's red, green and blue channels apart along the direction it is moving, and the drag can go any way. So:

  • Hard edges in both directions work; soft gradients do not. Silhouettes, shadow lines, stairs, facades, type and graphic work give the reads something to separate. Skies, fog, bokeh and flat colour fields split into a faint blur and look like a mistake.
  • Strong tonal contrast. A dark shape on a light ground (or the reverse) gives the clearest coloured fringes.
  • Black and white is the demo's choice, not a rule. In monochrome the split is the only colour on the page, so colour appears only while the field moves. Colour photographs work too: the reads then fringe each photograph's own colours, so the split reads as a shimmer on top of the colour rather than the colour arriving from nowhere.

Test at a fast throw, not at rest. Images do not need to share an aspect ratio: each print takes the aspect of its photograph inside the same box, which is what gives the field its mix of portrait and landscape prints. Around 640px wide is enough for the default print size on a 2x screen.

Using It With Your Own Design

What the effect needs from your markup: an element with data-split-canvas containing a [data-split-canvas-source] list of <img> elements. The width and height attributes are read for each print's aspect before the photograph decodes, so keep them accurate. The images are spread around the field so that, with 8 or more, neighbouring prints never repeat. The cue paragraph is optional.

What is only the demo's styling: the grey ground, the Mona Sans cue pill and its accent dot, and the fallback grid's spacing. Change the ground with one custom property on the root, which the canvas also reads as its clear colour:

Code snippet omitted: it ships with the download.

A dark ground (below about 18% luminance) automatically gets deeper shadows and no vignette.

CSS the effect depends on:

  • The container must have a real size. The canvas is sized from the root's clientWidth and clientHeight; the demo makes it position: absolute; inset: 0 inside a pinned stage. A root with a height of 0 gets a canvas of 0.
  • The .split-canvas__source list must stay in the document. It is the fallback for no JavaScript, no WebGL and reduced motion, and it is hidden only by .gl before first paint and moved off screen by .split-canvas.is-live, a class the script adds after a canvas exists.
  • touch-action: none on .split-canvas.is-live. The field owns both axes of a touch drag, so on a phone the page does not scroll from on top of it. Give it a bounded height when you place it mid-page, so visitors can scroll past it from outside.

Options

All options are data attributes on the root element.

Attribute Values Default Description
data-cell px 300 Column pitch. Rows are 1.24 times this; prints fit a box about 77% wide and 100% tall of it. Phones under 600px wide use 62% of it
data-max-split px 24 How far the red and blue reads can sit from the print at full speed (scaled with the print size)
data-full-speed px/s 2400 Drag speed at which the split is full
data-split-from px/s 220 Below this speed there is no split at all, so slow moves and the end of a glide stay clean
data-smear 0 to 1 0.35 How far each read smears along the motion. 0 gives three crisp copies
data-wake seconds 0.08 Delay of the prints furthest from the hand. 0 moves the field as one sheet
data-wake-radius px 1100 Distance from the hand at which the full delay applies
data-focus-radius px 540 Distance from the hand within which prints are held in register
data-focus-hold 0 to 1 0.1 Share of the split kept by the print right under the hand. 1 turns the focus off
data-lift number 0.05 Extra scale of a lifted print
data-shadow number 1 Shadow strength multiplier. 0 removes shadows
data-glide seconds 0.26 Half-life of a throw. Larger glides further
data-grain 0 to 0.12 0.045 Paper grain over the canvas
data-drift number 1 Touch devices only: a slow drift between touches (below the split threshold). 0 turns it off

Accessibility

  • Keyboard: the root is focusable, shows an accent outline on focus, and the arrow keys move the field one print at a time. Steps land at once; they do not animate.
  • Screen readers: the canvas is aria-hidden; the list of photographs with their alt text stays in the page, moved off screen rather than removed.
  • Reduced motion: the WebGL field never starts. The list shows as a scrolling grid of the same photographs on the same ground, and the drag cue is hidden.
  • Without JavaScript, or without WebGL: the same grid, for the same reason. One fallback, three failure modes, and it is never hidden until a canvas has actually been created.
  • Loading: each print fades in once its photograph has decoded, shadow and all, so a slow connection shows ground and finished prints, never grey placeholders.

Dependencies

Dependency Version Required
three.js 0.180.0 Yes, as an ES module (see Quick Start)
GSAP core 3.15.0 Yes: matchMedia branching (reduced motion and pointer type), teardown, and each print's fade in

No GSAP plugins. The effect's own requestAnimationFrame loop owns the drag, the momentum, the wake and the split; GSAP never drives that motion.

Everything the effect uses (WebGLRenderer, OrthographicCamera, PlaneGeometry, ShaderMaterial, Texture) is long-stable three.js API, so pinning to a different version is a one-line change in the import map.

Browser Support

Anything with WebGL, which is every current browser. Support is probed before three.js is asked for a renderer, deliberately: three logs its own failure to the console, as errors, several times over, before it throws. Probed first, a visitor with WebGL disabled simply gets the grid of photographs and a clean console. In a browser too old for import maps the module never runs and the grid is again what shows.

Performance

Each print on screen is three draw calls: a shadow, then the print in two passes (one darkens the ground by each channel's coverage, the other adds that channel's image). That pair is what lets the red read sit over the ground while the blue read is still over the print, and it cannot be batched without breaking where prints overlap. Prints are culled by hand outside the viewport, so a 1440x900 field draws about 55 to 75 calls instead of 145, and a phone about 60 to 70. The fragment cost is twelve texture reads per pixel while the field moves.

The loop renders only while something is changing: a field at rest costs nothing, it stops entirely while off screen (IntersectionObserver), and pixel ratio is capped at 2 (1.5 on touch devices).

To buy back frames on low-end hardware, lower data-max-split first: the quads grow with the split, so a smaller split shades fewer pixels. A larger data-cell puts fewer prints on screen.

Credits

The demo photographs are from Pexels, used under the Pexels licence and converted to black and white for this demo. Source pages are listed in assets/img-manifest.json. Replace them with your own photographs.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.