Free
🛬 Landing Page

Vectrus Energy

Scroll-tied cinematic section: a 500vh track scrubs an aerial clip frame by frame through a WebCodecs frame bank, with three sequential copy blocks.

4.9k views 535 copies 1.9k palavras

O prompt

# Vectrus Energy — Scroll-Tied Video Section

Build a standalone cinematic one-page site that recreates this EXACTLY. Do not invent extra sections, extra copy, extra logos, extra overlays, GSAP, Lenis, or a different video. Match structure, copy, colors, type, spacing, breakpoints, animations, and scroll-video logic.

## Stack
- Vite + React 18 + TypeScript + Tailwind CSS 3
- `lucide-react` icons: `ArrowRight`, `ArrowDown`, `ChevronUp`, `Info`, `X`
- `mp4box` ^0.5.2 for WebCodecs frame extraction
- Path alias `@` → `src`
- No routing. Single `App`. One full-viewport sticky scene, 500vh tall scroll track.

## Video (use this URL exactly, nowhere else)
```
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260821_114821_a8ca298f-be2c-4613-a4dd-51b69e16bbde.mp4
```
The clip is a high-key aerial fly-through: pale clouds, mist, mountains, then darker atmospheric landscape. Full-bleed `object-cover` background. No poster image. No controls. `muted`, `playsInline`, `preload="auto"`. Video is NEVER autoplayed as a timeline; playback position is driven by scroll.

## Font
Load exactly:
```html
<link href="https://db.onlinewebfonts.com/c/95cecf452d3208890088a5b4c19c7ecf?family=Helvetica+Neue+ME" rel="stylesheet">
```
```css
body { font-family: 'Helvetica Neue ME', 'Helvetica Neue', Helvetica, Arial, sans-serif; }
html { scroll-behavior: smooth; }
body { overflow-x: hidden; }
```
Title: `Scroll Tied Video Section`

## Colour
```ts
const DARK = '#1D3045';
```
Navy text on light cloud frames. White text on dark later frames. No colour overlays / gradients on the video — text sits directly on the video.

## Page architecture
- Outer: `relative h-[500vh]` (this is the scroll distance)
- Inner sticky: `sticky top-0 w-full h-screen overflow-hidden`

Inside the sticky:
1. `<video>` full cover
2. `<canvas width=1920 height=1080>` `absolute inset-0 object-cover`, opacity 1 when the frame bank is live else 0, `transition-opacity duration-300`. The canvas draws decoded frames so scrubbing is smooth (`video.currentTime` seeking is fallback only).
3. Overlay `absolute inset-0 pointer-events-none` containing the navbar + 3 sequential sections.

## Scroll progress
```ts
p = clamp(0, 1, window.scrollY / (container.offsetHeight - window.innerHeight))
```
Recompute the span on `resize` and `orientationchange`.

## Text sections are sequential (previous fully fades out before next appears)

```
s1Opacity:
  p < 0.20 → 1
  else → max(0, 1 - (p - 0.20) / 0.08)

s2Opacity:
  p < 0.32 → 0
  p < 0.40 → (p - 0.32) / 0.08
  p < 0.55 → 1
  else → max(0, 1 - (p - 0.55) / 0.08)

s3Opacity:
  p < 0.67 → 0
  p < 0.75 → (p - 0.67) / 0.08
  else → 1
```

Each section: `absolute inset-0`, style opacity + transition `'opacity 0.1s ease-out'`.
Children use `Stagger`: visible when the section opacity > 0.3.
`Stagger`: opacity 0→1, `translateY(24px)`→0, 0.8s `cubic-bezier(0.16,1,0.3,1)`, delay in ms.

## Navbar (absolute top, `z-50`, `pointer-events-auto`)
Padding: `px-6 sm:px-8 md:px-12 pt-8 sm:pt-12 pb-6`, `flex items-center justify-between`.
Colour flips at `p > 0.55`: DARK → white (`duration-500`).

Desktop `lg+`: left cluster of 5 links, `gap-8 xl:gap-10`
- VECTRUS ENERGY (active, 2px underline `-bottom-3` full width)
- VECTRUS UPSTREAM
- VECTRUS MARKETS
- VECTRUS SYSTEMS
- VECTRUS+

Link style: `text-xs tracking-[0.15em] uppercase font-medium`, `hover:opacity-70`.
Nav entrance: after 200ms, each link fades/slides from `translateY(-12px)`, opacity 0.6s `cubic-bezier(0.16,1,0.3,1)`, delay `i*80+100`ms.

Right cluster (hidden below `sm`):
- NEWS (`text-xs tracking-[0.2em] uppercase font-medium`) + 20px circle filled with the current nav colour, `Info` icon size 10 inverted
- MENU label, same type (`lg+`: a span; below `lg`: a button that opens the overlay)
- Right cluster delay 500ms, same entrance

Mobile `<lg`: hamburger left, 3 bars — 24×2, 24×2, 16×2, gap 5px, colour follows `isLight`. The hamburger opens the full-screen overlay.

## Mobile menu overlay
`fixed inset-0 z-[100]`, background DARK.
Open: `opacity-100 visible`; closed: `opacity-0 invisible`; `duration-500`, ease `cubic-bezier(0.4,0,0.2,1)`.
Inner panel: closed `-translate-y-8`, open `translate-y-0`.
Close: top-right `px-6 sm:px-8 pt-8 sm:pt-12`, 40px circle `border-white/30`, `X` 18, `hover:border-white`.
Links centred vertically, `px-8 sm:px-12`, `py-3`, `text-2xl sm:text-3xl font-light tracking-wide uppercase`; active white, others `white/60 hover:white`; stagger in with delay `i*60`ms, `translateY(20px)`→0.
Footer: NEWS and CONTACT, `text-xs tracking-[0.2em] uppercase text-white/60`, `px-8 sm:px-12 pb-10`.
When open, body `overflow: hidden`.

## Section 1 (hero, left aligned, vertically centred)
`px-6 sm:px-8 md:px-20 lg:px-32`

- H1: **`Advancing resources for a cleaner future`** — `clamp(2rem,5vw,5rem)` `font-light uppercase leading-[1.2]`, colour DARK
- Subtitle below, `mt-6`: **`Sustainable power with purpose`** — `text-sm tracking-[0.3em] uppercase`, colour DARK at 90% alpha (`#1D304590`)
- Bottom-right `absolute bottom-12 right-6 sm:right-8 md:right-12`: 48px circle button, border DARK 50% alpha, `ArrowRight` 18, `hover:opacity-70`

Stagger delays: title 0, subtitle 150, button 300.

## Section 2 (centre)
`px-6 sm:px-8`, flex centre, `max-w-[900px]`.
H2 `clamp(1.5rem,4.5vw,4.5rem)` `font-extralight tracking-wide leading-[1.3] text-center uppercase`.

Copy exactly:
> `"We build lasting partnerships with vision "` + span colour DARK 80% `"and precision"` + `" "` + span colour DARK 50% `"across every frontier"`

Right column `absolute bottom-16 right-6 sm:right-8 md:right-12`, `flex-col items-center gap-4`:
- 48px circle, border DARK 40%, `ArrowDown` 18
- then `mt-4` three dots: 8px solid DARK (active), 6px DARK 40%, 6px DARK 40%, `gap-2`
- then 40px circle, border DARK 30%, `ChevronUp` 16, colour DARK 80%, `mt-2`

Stagger delays: headline 0, down 200, dots 350, up 500.

## Section 3 (right aligned, white type — the video is dark here)
`flex items-center justify-end px-6 sm:px-8 md:px-20 lg:px-32`, `max-w-2xl text-left`.

- Eyebrow: **`Halder | Nordvik`** — `text-white/60 text-lg tracking-wide mb-4`
- H2: **`Fueling ambition,`** line break **`shaping tomorrow.`** — `clamp(2rem,4vw,4rem)` `font-light text-white leading-[1.2] uppercase tracking-wide mb-8`
- CTA row `gap-4`: **`Contact Nordvik`** (`text-sm tracking-[0.3em] text-white/80 uppercase`) + 40px white filled circle, gray-800 `ArrowRight` 16, `hover:scale-110 duration-300`

Stagger delays: 0 / 150 / 300.

## Smooth video scroll — same logic (do not replace with GSAP or naive `currentTime` only)

Hook `useVideoScrub(videoSrc)`.

Constants:
```ts
LERP_TAU = 8
SNAP = 0.002
LRU_MAX = 24
LEAD = 24
WATCHDOG = 60000 // ms
```

State: `bank: {ts microseconds, blob webp}[]`, `lru: Map<index, ImageBitmap | null>`, current / target times in seconds, `ready`, `reverted`, `painted`, `building`, `dur`.

rAF loop every frame:
```
dt = min(0.1, deltaSeconds)
p = getProgress(); setScrollProgress(p)
if dur > 0:
  target = p * dur
  if prefers-reduced-motion: current = target
  else:
    current += (target - current) * (1 - exp(-dt * LERP_TAU))
    if abs(target - current) < SNAP: current = target
  if ready: draw nearest frame from bank
  else: fallback — if not seeking and abs(video.currentTime - current) > 0.01, set video.currentTime = current
```

Frame bank (build after `window load`, skip if reduced-motion or no `VideoDecoder`):
- fetch the CloudFront mp4 as an `ArrayBuffer`
- parse with `MP4Box.createFile()`, extract the video track, configure `VideoDecoder` (codec + `avcC`/`hvcC`/`vpcC`/`av1C` description)
- decode samples as `EncodedVideoChunk` (key vs delta)
- throttle with `LEAD` so decode doesn't outrun blob encoding
- each `VideoFrame` → offscreen canvas → `toBlob('image/webp', 0.82)` stored with its timestamp
- sort the bank by `ts`
- `nearestIndex`: binary search on timestamps (compare `t*1e6`)
- warm the LRU around `i-1..i+2`, `createImageBitmap`, evict oldest when size > `LRU_MAX`
- first successful canvas paint → `canvasLive` true (canvas fades in over the video)
- if hardware decode fails, retry once with `hardwareAcceleration: 'prefer-software'`
- 60s watchdog → revert to video seeking fallback, hide the canvas
- CORS: the fetch needs CloudFront CORS; the video element still works as a fallback if decode fails

The video element still renders underneath until the canvas is live.

## Do not
- Add extra brands, logos, cookie widgets, or "scroll down to discover"
- Change copy or nav labels
- Play the video with `play()`
- Use a different video host or local file
- Skip the mobile overlay or hamburger
- Skip the lerp (jumping frames = wrong)

## Deliver
A working Vite React TS app: `index.html`, `App.tsx`, `useVideoScrub.ts`, `index.css`, `mp4box.d.ts`, Vite alias `@`, `lucide-react` + `mp4box` installed. Pixel-faithful to the spec above.

## Quality Bar
- The whole page is one 500vh scroll track with a sticky viewport; nothing below it. `h-[500vh]` on a mobile browser whose toolbar resizes the viewport changes the span mid-scroll — that is exactly why the span is recomputed on `resize` and `orientationchange`, and why it must be read from `offsetHeight - innerHeight` rather than cached at mount.
- `dur` cannot come only from MP4Box's `onReady`. Reduced-motion and any browser without `VideoDecoder` never build the bank, and the rAF loop is gated on `dur > 0` — so read the duration from the video element's own metadata as well (`loadedmetadata` / `durationchange`), or the fallback seek path sits at frame 0 for the entire scroll. Verified: without it, nothing scrubs on exactly the paths that need the fallback most.
- `setScrollProgress(p)` runs inside the rAF loop, so the whole tree re-renders every frame while scrolling. Keep the per-frame work in that tree trivial — the moment a child does real work on render, the scrub drops frames. The canvas draw deliberately bypasses React entirely.
- Decoding a 10s 1080p clip produces ~240 full-frame webp blobs. That is tens of MB of Blob storage plus up to `LRU_MAX` live `ImageBitmap`s — real memory on a phone. Downscale the source to what actually renders before shipping, and keep the LRU small; the `LEAD` throttle exists so open `VideoFrame`s never pile up faster than they are encoded.
- One offscreen canvas is reused for every frame, and `convertToBlob` is async. Serialise draw → encode → next frame, or two frames race onto the same canvas and the bank ends up with duplicated or torn stills.
- `image/webp` from `canvas.toBlob` is not universal — older Safari silently hands back PNG, which is several times larger. Check the returned blob's `type` if the memory budget is tight.
- WebCodecs is the fast path, not the guarantee: no `VideoDecoder`, a codec the platform will not accept, a hardware decoder that fails, a CORS-blocked fetch, or a slow build all land on the `video.currentTime` fallback. That path must look right on its own — keep the video element mounted underneath, and give the clip enough keyframes that seeking is not a slideshow.
- Reduced-motion skips the frame bank and drops the lerp, which leaves scrubbing on `currentTime` seeks. That is the correct trade — the lerp is the motion smoothing this setting asks you to remove — but it means the reduced-motion path is also the fallback path, so it deserves the same testing.
- The overlay is `pointer-events-none` and only the navbar re-enables pointers, so the section buttons (arrow, dots, chevron, Contact) cannot be clicked even though they are real `<button>`s reachable by keyboard. Either re-enable pointers on them or make them non-interactive; a focusable control that ignores clicks is the worst of both.
- Text sits directly on the footage with no scrim, and the colour flip at `p > 0.55` is a hard-coded guess about where the clip goes dark. Re-check it against the actual frames before swapping the video — and note the flip is instant at one threshold while the section copy cross-fades over 0.08 of progress, so the navbar can change colour mid-fade.
- `Helvetica Neue ME` is served from a third-party font mirror, and a hotlink is not a licence for Helvetica Neue. Do not ship that link; license and self-host it, or fall back to the stack the CSS already names.
- The nav's five labels and the mobile overlay render the same list, but the mobile links are the only ones that are real anchors — the desktop cluster is `<span>`s. Make them the same kind of element before this ships as navigation.

Você também pode gostar

Verified DR - Verified Domain Rating for buildpromptkit.comBuildPromptKit - AI prompts that ship real UI — every one has a live demo | Product HuntBuildPromptKit — AI Website & App Prompts - Featured on YizhanFeatured on saasfame.comFeatured on Twelve Tools