Documentation

CubeScroll turns one image into a finished scroll effect. Drop a photograph or logo into the studio and it hands back complete, self-contained code: a page where the image begins as a cloud of scattered cubes and assembles itself, seamlessly, as the visitor scrolls. No build step, no framework, no dependencies beyond a pinned copy of three.js.

The trick

Every cube's front face is parked at a random depth z and scaled by (ASSEMBLE_Z − z) / ASSEMBLE_Z. Apparent size on screen is then size × scale ÷ distance — which cancels to a constant, whatever the depth. So once the camera reaches ASSEMBLE_Z, every cube projects to exactly the same screen rectangle and they tile into the photograph with no seams. Scattered in depth, the same cubes read as noise. The scroll is nothing but camera travel.

That whole transform is a function of three numbers — column, row, and parked depth — so three numbers are all an instance stores: 12 bytes per cube instead of a 64-byte matrix. The vertex shader derives everything else, which is why a grid of nearly a million cubes still loads instantly and draws in a single call.

Two ways in

node cli.mjs photo.jpg                  # → photo-scroll.html
node cli.mjs photo.jpg --theme          # → a whole landing page, themed from the image
node cli.mjs photo.jpg --rows 128 --emit json    # sizing report, no file written
node cli.mjs photo.jpg --emit js --stdout        # the bare module, for existing pages
node cli.mjs photo.jpg --probe 0,0.5,1  # + headless screenshots to verify

Two styles

--style picks which scene the module builds. Cubes is the one above: a scatter that assembles. Kinetic is one plane carrying the photograph on a shader — the picture is on screen the whole way down, displaced by three octaves of noise and split into its colour channels, and the camera pulls it into focus. Same camera, same clock, same seams; only the scene and four parameters differ. See it on the kinetic example.

All four are kinetic's own and are ignored by cubes, exactly as --rows, --cube, --alpha, --faces and --spread are ignored by kinetic. Switching styles never resets the other style's numbers. --seed, --ease and --stagger mean the same thing in both: the seed offsets the noise field the way it parks the cubes, and stagger spreads the arrival across the track by region instead of by cube.

Whichever style is running, the frame at the end of the track is the photograph: every displacement term scaled by progress reaches exactly zero at full assembly, the same way every cube reaches its seat. Two kinetic terms are not scaled by progress — the pointer lean and the momentum stretch, which are what keeps a resolved picture alive under the reader's hand — and both are exactly zero at rest, under a motion still, and in every frame an export draws.

Sizing: the grid and its tiers

Grid rows set how many cubes fly in. Instance count is rows × columns, and the studio reports it in tiers: ok (≤60,000 — comfortable anywhere), amber (60,000–200,000 — fine on desktops), red (over 200,000 — for audiences whose GPUs you know). With the default full-image faces, sharpness comes from the texture rather than the grid, so rows beyond ~256 rarely earn their cost.

Cube faces decide where the detail comes from. Full image hands the photograph to the GPU whole, giving every cube its own tile of it — the landed frame is as sharp as the source. Flat colour averages each tile to one colour: the chunky mosaic look, where the assembled image resolves to exactly the grid and no finer. Use flat faces for logos and pixel-art; pair with enough rows.

Themed pages

--theme composes a complete landing page around the effect instead of leaving it bare. The colour system is extracted from the image itself — quantized with median cut, derived in OKLCH, and held to WCAG AA by construction: body text at least 4.5:1, accent at least 3:1 against its background, verified on the final hex values. Three voices are built in: editorial (serif, airy), technical (mono, gridded, with a live assembly readout), and poster (condensed caps, solid accent blocks). See them on the examples page.

Sections are paced against the assembly: the masthead lands while the image is still noise, body copy sits on panels over peak chaos, the quote waits for the image to resolve, and the call to action arrives over the finished photograph. The page still reads as designed over the flat background colour alone — which is exactly what renders if WebGL is unavailable.

Steering the palette

Four optional flags, applied in order, all landing upstream of the contrast enforcement — so none of them can break the guarantee:

  1. --bg #rrggbb — force the page background; everything re-derives around it.
  2. --scheme dark|light — override what the image implies.
  3. --tint warm|cool|neutral|muted|vivid — a tonal steer over the derived neutrals and accent chroma.
  4. --accent #rrggbb — a brand accent. If it is already legible it is used verbatim; otherwise it moves as little as possible and the page's HTML comments record exactly what shifted and why.

Contrast is guaranteed by construction, with one honest limit: a --bg pinned near mid-tone can be too close to the middle for either white or black ink to clear 4.5:1. The palette picks whichever side does clear it, so this only bites if you pin --scheme as well — and when the floor genuinely cannot be met the page says so, in a CLI warning and in the page's own HTML comments. It is never met silently.

If a page needs different colours, use these flags or change the image — never hand-edit the generated values.

Mounting the module in your own page

The JS module mounts its own fixed canvas at z-index: 0 with pointer-events: none. Your page provides two things: visible content in a wrapper with position: relative; z-index: 1, and real scroll height — the assembly track is the emitted scroll length minus one viewport, so anything parked below the track scrolls over the finished image.

The module publishes seams your page can ride:

Accessibility and motion

Scroll-coupled whole-field motion is the canonical trigger for vestibular discomfort, and the studio treats that as a design input, not a checkbox. Under prefers-reduced-motion the camera does not travel: the photograph is simply there, assembled, and progress is published as complete so paced content shows its resting state. Themed pages also honour prefers-contrast and forced-colors, keep body copy on panels opaque enough to hold its contrast over any frame, and refuse to compose at all if an image is supplied without alt text.

Verifying a forge

A clean exit code cannot tell you whether a headline survives the noise behind it. --probe 0,0.5,1 opens the finished page headlessly, scrolls to each assembly fraction, waits for the damped camera to settle, and writes screenshots — reporting the engine's own published progress per frame and flagging any frame that is off its requested fraction. Then you look at the pictures. Frames near 0.5–0.7 of the track are where text fails first.