Lab

Scroll-scrubbed waves

I wanted a hero where the water moves as you scroll. I tried to generate one, then I went and looked at footage I already had. Both attempts are still on this page, in the order I made them, because the comparison turned out to be the useful part.

Attempt one

Generated from a still

Scroll the panel. It works — and if you watch the sand rather than the water, you can see the problem.

What I did

I started from one of my own drone stills — a straight-down frame of water, foam and dry sand — flipped so the wave runs down the screen instead of up. In a top-down shot there is no horizon, so flipping it reads as normal rather than upside down. I trimmed a sliver of tree canopy off the edge that would have given it away.

That still went into PixVerse as an image-to-video reference, with a prompt asking for a locked-off camera and moving water. Two settings had to be switched off before generating:multi-shot, which would let the model cut between shots and make scrubbing jump, and audio, which a muted scrubbed video cannot use anyway.

What worked

The water. Fluid is the documented weak spot for this class of model, and I expected foam that dissolved into soup. It held together across the whole clip. Animating a photograph I had taken, rather than asking for an invented beach, is probably why — the model had somewhere real to start.

What did not

The camera would not stay still. Every generation added movement. The first drifted forward, and after a re-prompt that named the fixed objects and forbade zoom, pan, tilt and drift in as many words as I could find, the next one moved upward instead. A prompt is a suggestion, not a constraint, and camera motion is something these models clearly want to add.

The first draft also carried a watermark, which rules it out for a site that links to two businesses. And the free tier gave 60 credits against 38 for a single 540p five-second generation — so effectively one attempt before paying.

What it cost

A ten-dollar subscription and four generations, of which none was usable as a hero. The useful output was information: the frame worked, the orientation worked, the water was possible, and the camera was not going to hold still.

Attempt two

Footage I already had

Same mechanism, same page, different source. This is my own drone footage, and it is better in every way that matters.

Why it wins

It solves, for free, every problem the generated route could not. The camera is locked because the drone was hovering — the marks in the sand hold position across the whole clip. The water behaves because it is water. There is no watermark, no licence question about commercial use, and no ceiling on resolution: the original is 4K at 92 Mbps, roughly ten times the bitrate of anything generated.

It was also already in the right orientation. Water across the top, foam, wet sand, dry sand, trees at the bottom — the wave washes down the frame natively, with no flip needed and no upside-down palm trees to hide.

The steps it actually took

  1. Grade it. Straight off the drone the footage was flat and desaturated next to the stills — plenty of headroom, but nothing like the turquoise the rest of the site is built on. That is a DaVinci Resolve pass, and it is where most of the improvement came from.
  2. Export a master, not a web file. My first export used APV, an intra-frame production codec: five seconds, an estimated 1.14 GB, and unplayable in any browser. The second was H.264 but ignored the bitrate cap and came out at 56 MB. That file became the graded master, and everything below derives from it.
  3. Re-encode for seeking. Video is built to play forwards, not to be seeked, and scrubbing is nothing but seeking. Compression stores occasional whole frames — keyframes — and between them only what changed, so landing on an arbitrary moment means finding the previous keyframe and rebuilding forward from it. In a normal encode those are seconds apart. Forcing one every ten frames makes seeking cheap and the file bigger, and for a scrubbed hero that trade is always worth making.
  4. Ship two formats and two sizes. iOS Safari cannot play WebM; Firefox seeks MP4 badly. Neither format alone works everywhere. Phones get a 1280-wide cut, because sending a desktop file to a phone on mobile data is the difference between an effect and a punishment.

The numbers

Measured, not estimated. Keyframes land every 9.8 frames. The 1080p cut is5.7 MB and the 1280-wide one 2.2 MB, against budgets of 8 MB and 3 MB. The poster is 148 KB against 150. Nothing above is a target I hoped to hit; they are what the files came out at.

What I would tell someone starting this

On a small budget, real footage beats generated footage — not narrowly, and not only on cost. Generation is a lottery you re-enter every time something is wrong, and each ticket costs money and produces a slightly different scene. Footage you already own is fixed, gradeable, and yours. The generated attempt was not wasted, but what it bought was a rehearsal.

The exception is genuine: if the thing you need does not exist and cannot be photographed, generation is the only option. A wave is not that thing.

How the scrubbing works

Both panels above use the same code. Each is several screen-heights tall with sticky contents, so they pin to the viewport while the page scrolls past. That gives a number from 0 to 1 for how far through the panel you are, which is multiplied by the clip’s duration and written to the video’s currentTime.

Nothing plays. The video is never told to start. Your scroll position is the playhead, which is why scrolling up runs the water backwards — something a playing video cannot do.

The scroll handler does almost nothing, deliberately. Scroll events fire irregularly, and writing currentTime straight from them produces visible jerk. So the handler only records where the playhead should be, and an animation-frame loop eases the real value toward it. The smoothness you feel is the easing, not the scrolling.

Neither video is fetched on page load. Each element starts with no source at all, and the script attaches one only when its panel is within a screen of the viewport — so arriving here costs a poster image, not several megabytes of video nobody has scrolled to.

What is unfinished

  • Neither panel has been tested on a mid-range Android, which is the realistic worst case for decode performance.
  • The panels are 320svh tall, which is a guess. That single number decides how fast the water moves for a given amount of scrolling.
  • Neither clip loops seamlessly. It does not matter here, because scrolling back up runs them backwards, but it would if either ever ran on its own.