/showcases · Brand
Hero motion loop — from text prompt to 460 KB production asset
How we generated, encoded, and integrated the AudioLab.tools hero video — a subtle 5-second cyan-ribbon breathing loop. End-to-end pipeline including Higgsfield seedance generation, ffmpeg WebM + H.264 encoding, image fallback, and prefers-reduced-motion handling.
Outcome
460 KB total delivered (WebM + MP4). 5 seconds of subtle motion. Static image fallback for slow networks. Removed entirely for users with prefers-reduced-motion.
A motion hero on a tech website is a high-risk move. Get it wrong and it’s a distracting autoplaying ad; get it right and it’s the difference between “another developer site” and “premium brand”. The constraints we set:
- Subtle. The motion is breath, not action. Nothing should grab attention away from the text overlay.
- Tiny. Under 500 KB total transport, period.
- Fallback-first. The page should look great with no video at all.
- Reduced-motion respecting. Users who opted out get nothing animated.
This is how we got there in about 20 minutes of real work.
Step 1: Generate the motion (Higgsfield seedance 2.0)
Seedance is the right model for this — image-to-video, identity-preserving, with strong support for subtle motion. We used the same 21:9 cyan-ribbon image we built for the static hero (see brand imagery pipeline) as the reference frame.
{
model: 'seedance_2_0',
prompt: `The luminous cyan ribbon of light breathes and slowly rotates
in pitch-black space, with subtle volumetric particles drifting
through the frame. Very slow, gentle motion — the ribbon appears
to inhale and exhale, with soft glow pulses. Camera completely
static. The lighting and composition match the reference image
exactly. Premium, calm, atmospheric, ambient. No camera movement,
no cuts, just subtle organic motion of the light ribbon itself.`,
duration: 5,
resolution: '720p',
aspect_ratio: '21:9',
mode: 'std',
medias: [{ value: heroImageUrl, role: 'image' }],
}
Two prompt constraints that made the output usable:
- “Camera completely static.” Without this, seedance defaults to a slow push-in that looks like every other tech video.
- “No camera movement, no cuts, just subtle organic motion.” Reinforces 1 and prevents the model from adding stylistic cuts.
Cost: 22 credits. Render time: ~70 seconds.
Step 2: Strip the audio
Seedance outputs an MP4 with audio. We don’t need it — the hero is silent, and any audio would mean we couldn’t autoplay across most browsers anyway.
ffmpeg -i hero.mp4 -an -c:v copy hero-noaudio.mp4
The -an flag strips audio; -c:v copy skips re-encoding the video. Fast pass, no quality loss. We use this output as the source for both subsequent encodes.
Step 3: Encode VP9 WebM (modern browsers)
VP9 in a WebM container compresses better than H.264 and is the right choice for browsers that support it (~90 % of traffic).
ffmpeg -i hero.mp4 -an \
-c:v libvpx-vp9 \
-b:v 800k -crf 36 \
-row-mt 1 -tile-columns 2 -threads 4 \
-deadline good -cpu-used 2 \
hero.webm
Settings worth flagging:
-crf 36— quality target. 36 is on the aggressive side; the source material (a slow-moving abstract light ribbon over black) compresses extremely well, so we can afford it.-b:v 800k— bitrate ceiling. Stops VP9 from over-allocating on the few frames with motion.-row-mt 1 -tile-columns 2 -threads 4— multi-threaded encoding. Faster local encoding without affecting output quality.-deadline good -cpu-used 2— quality-vs-speed trade-off.good/2is the sweet spot for one-off assets.
Output: 252 KB for 5 seconds at 720p.
Step 4: Encode H.264 MP4 (Safari fallback)
Safari (and some older browsers) doesn’t support VP9. We need an H.264 MP4 as fallback.
ffmpeg -i hero-noaudio.mp4 -an \
-c:v libx264 -crf 28 -preset slow \
-movflags +faststart -pix_fmt yuv420p \
hero.mp4
Settings:
-crf 28— slightly more conservative than the WebM crf because H.264 doesn’t compress this material as efficiently.-preset slow— better compression. We’re encoding once, not in real time.-movflags +faststart— moves the MP4 metadata atom to the front so the browser can start playback before the whole file downloads.-pix_fmt yuv420p— required for Safari compatibility on iOS.
Output: 209 KB for 5 seconds at 720p.
Step 5: Wire up the markup
<section class="relative isolate overflow-hidden">
{/* Static image fallback — renders first, hidden when video plays */}
<Image
src={heroWideImg}
alt=""
widths={[1280, 1920, 2560, 3168]}
sizes="100vw"
loading="eager"
fetchpriority="high"
class="absolute inset-0 -z-10 h-full w-full object-cover"
/>
{/* Motion video — slow ambient loop */}
<video
class="absolute inset-0 -z-10 h-full w-full object-cover opacity-0
transition-opacity duration-700 motion-reduce:hidden"
id="hero-video"
autoplay loop muted playsinline
preload="metadata"
aria-hidden="true"
>
<source src="/media/hero.webm" type="video/webm" />
<source src="/media/hero.mp4" type="video/mp4" />
</video>
{/* Contrast overlays + text layer */}
...
</section>
<script is:inline>
// Fade the video in once it actually starts playing
const v = document.getElementById('hero-video');
v?.addEventListener('playing', () => { v.style.opacity = '1'; });
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) v?.remove();
</script>
Three patterns worth calling out:
- Image renders first, video fades in. The user sees a beautiful static hero immediately. Only once the video has decoded a frame and started playing does it fade in over the image. Zero janky black flash, zero perceived load time.
motion-reduce:hiddenTailwind class — declarative reduced-motion handling.- Imperative removal in the inline script — if the user has reduced motion enabled, the
<video>element is removed from the DOM entirely. No network request, no decode, no power draw.
What broke (briefly)
- First WebM encode was 800 KB. Too big. Bumped
-crffrom 30 to 36 and-cpu-usedfrom 4 to 2; got to 252 KB. - Initial autoplay issues on iOS. Forgot
playsinline. iOS requires it for inline video; without it, the video tries to go fullscreen on play. - MP4 was 1.2 MB before re-encoding. Seedance output is faithful but bigger than it needs to be for web. The re-encode pass with
-crf 28cut it to 209 KB.
Outcome
Total transport: 460 KB worst-case (both files cached after first load). Visual signature: a luminous, breathing cyan ribbon that elevates the entire homepage. Time from idea to production: ~20 minutes.
The general principle: when you ship motion on a brand hero, do the encoding work. The model output is the source asset, not the production asset.
Related
More build logs
-
Brand imagery pipeline — seven cinematic images, one design system
Generated 7 cinematic brand images (hero + 6 cluster-specific), optimised them from 5–9 MB PNGs into 50–400 KB WebP variants via Astro’s build pipeline, then integrated them across cluster cards, hero sections, and atmospheric brand beats.
-
Six interactive demos, one platform — the architecture
Six live in-browser demos sharing a unified Web Worker analysis pipeline, runtime audio synthesis for samples, and a per-cluster React island pattern that keeps the static-first Astro shell fast.