Creating Oneshot Hero Landing Pages
An Antigravity skill to design and build video-driven parallax hero landing pages and looping motion showcases.
1. 4-Step Execution Pipeline
Execute every landing page request in this order:
- Step 1: Reference Images: Generate reference images via
generate_image(the built-in Antigravity tool powered by Nano Banana /gemini-3.1-flash-image) and save them into./assets/(e.g../assets/hero_ref.jpg,./assets/showcase_ref.jpg). - Step 2: Video Generation: Execute Python REST calls against
gemini-omni-flash-preview(Section 3) using the reference images to generate 10s 16:9.mp4video files into./assets/. - Step 3: Frontend Implementation: Implement the Hero Video Scrub Scaffold (Section 4) in
index.htmlwith smooth scroll video scrubbing, progress-locked hero text, and clean navigation. - Step 4: Verification & Local Hosting: Launch a local preview server (
python3 -m http.server 3000 --directory <dir>), verify all video assets return200 OK, and inspect the visual layout.
2. Design Guidelines
- Navigation Guidelines:
- Pick ONE Focused Composition:
[Logo]+[2–4 Editorial Links Max](strictly <= 4 links)[Logo]+[Menu / Index Trigger][Logo]+[Brand Tagline or Location]+[Menu][Logo]+[Single Quiet CTA][Split Nav]with[2 Links]Left,[Logo]Center,[2 Links or 1 CTA]Right
- Keep It Simple: If you have 2–4 inline links, do not stack a heavy CTA button and hamburger menu next to them. If using a drawer, keep the bar quiet.
- No Mechanical Numbering or Fake Tickers: Do not prefix mechanical numbers (
01,02,03) onto nav links. Do not add fake GPS coordinates, sensor readings, or status dots (● LIVE).
- Pick ONE Focused Composition:
- No Decorative SVG Icons on Cards or UI:
- Do not place decorative SVG icons, Lucide icons (
<i data-lucide=...>), or icon badge containers inside feature cards, headings, bullet points, or buttons. - Cards, pillars, and sections should rely on typography, whitespace, and photography/video. (SVGs are fine for a logo mark or modal close button).
- Do not place decorative SVG icons, Lucide icons (
- No Toy Widgets, Interactive Simulators, or Mini-Games:
- Landing pages are editorial showcases.
- Do not generate character stat calculators, RPG inventory boxes, physics toy widgets, interactive sandbox cards, or
canvas-confettiparticle scripts. - For playful brands or games, express personality through color palettes, typography, and video motion rather than playable JavaScript mini-games or calculators.
- No Unrequested Audio Buttons or Sound Synthesizers:
- Do not generate audio toggle buttons (
🔊), background sound generators, or Web Audio API synthesizers (AudioContext, oscillators).
- Do not generate audio toggle buttons (
- Clean Typography Over Badges:
- Default to unbordered typography, subtle font weights, fine line accents (
—), or roman numerals (I,II) instead of encasing words inside rounded pill badges ([ BADGE ]). - When a tag or table cell is necessary:
- 1–2 Words Maximum: Do not put long descriptions or sentences into compact badges (e.g. use
LEO, notLEO Station Rendezvous). - Include
whitespace-nowrap: Keep badge text on a single line so it never breaks into awkward stacks. - Single-Row Filter Bars: Filter buttons should stay on a single unbroken row (
flex items-center gap-2 overflow-x-auto whitespace-nowrap). - Responsive Tables: Data tables should use responsive scroll wrappers (
overflow-x-auto min-w-[650px]).
- 1–2 Words Maximum: Do not put long descriptions or sentences into compact badges (e.g. use
- Default to unbordered typography, subtle font weights, fine line accents (
- Media & Hero Video Rules:
- The hero must use a
<video id="hero-video">element. Do not substitute video with<canvas>image slideshows or static<img>tags. - Supplemental showcases generated by Omni should be looping
<video autoplay muted loop playsinline>elements. - No Visible Scrubber HUDs: Do not render timeline scrub bars, progress percentage meters (
"00%"), or cockpit dials over media.
- The hero must use a
- Progress-Locked Hero Text:
- Calculate hero text opacity and translation directly from scroll progress in JavaScript (see Section 4). Scrolling reveals text, pausing keeps it readable, reversing hides it.
- Do not use time-based CSS transitions (
transition: opacity 0.7s) on hero scroll text that cause text to lag or scroll past unread. (CSS animations are fine in standard body sections).
- Layout & Stacking Rules:
- Root containers (
html,body,#root, wrappers) must haveoverflow: visible(oroverflow-x: clip). Do not useoverflow-hiddenoroverflow-x-hiddenon parent wrappers, which breaks CSSposition: sticky. - Hero videos should stretch edge-to-edge (
w-full h-full object-cover), not styled as small rounded UI cards.
- Root containers (
- Typography & Color:
- Use modern Google Fonts (e.g. Italiana, Cormorant Garamond, Plus Jakarta Sans, Space Grotesk).
- Use curated color palettes (e.g. terracotta, basalt, obsidian, titanium, alabaster) rather than default generic rainbow colors.
3. Omni Video Python Generator
Install dependencies:
bashpip install google-genai pillow
Save generated .mp4 files directly to ./assets/ using this script:
pythonimport base64 import io import json import os import ssl import sys import urllib.error import urllib.request from PIL import Image def generate_omni_video( api_key: str, img_path: str, prompt: str, output_mp4_path: str ) -> bool: ctx = ssl.create_default_context() url = "https://generativelanguage.googleapis.com/v1beta/interactions" try: img = Image.open(img_path).convert("RGB") buf = io.BytesIO() img.save(buf, format="PNG") b64_img = base64.b64encode(buf.getvalue()).decode("utf-8") except Exception as e: print(f"Error opening image '{img_path}': {e}", file=sys.stderr) return False payload = { "model": "models/gemini-omni-flash-preview", "generation_config": {"thinking_level": "high"}, "response_format": {"type": "video", "aspect_ratio": "16:9", "duration": "10s"}, "input": [ {"type": "text", "text": prompt + " No text, no titles, no subtitles, no overlays."}, {"type": "image", "mime_type": "image/png", "data": b64_img} ] } req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={ "Content-Type": "application/json", "x-goog-api-key": api_key } ) try: with urllib.request.urlopen(req, context=ctx) as response: res = json.loads(response.read().decode("utf-8")) for step in res.get("steps", []): for item in step.get("content", []): if item.get("type") == "video" or "video" in str(item.get("mime_type")): os.makedirs(os.path.dirname(output_mp4_path) or ".", exist_ok=True) with open(output_mp4_path, "wb") as f: f.write(base64.b64decode(item["data"])) return True print("No video data found in response payload.", file=sys.stderr) return False except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8", errors="replace") print(f"API HTTP Error {e.code} ({e.reason}): {error_body}", file=sys.stderr) return False except urllib.error.URLError as e: print(f"Network / URL Error: {e.reason}", file=sys.stderr) return False except Exception as e: print(f"Unexpected error during video generation: {e}", file=sys.stderr) return False
4. Hero Video Scrub Scaffold (HTML + CSS + JS)
Use this architecture for the sticky 400vh video scrubbing engine:
html<!DOCTYPE html> <html lang="en" class="scroll-smooth"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Brand — Hero Showcase</title> <script src="https://cdn.tailwindcss.com"></script> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,400&family=Plus+Jakarta+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"> <style> html, body { margin: 0; padding: 0; overflow-x: clip; /* Clean clipping without breaking position:sticky */ background: #08090d; color: #f7f5f0; font-family: 'Plus Jakarta Sans', system-ui, sans-serif; } .phase-text { will-change: opacity, transform; } header.nav-scrolled { background: rgba(8, 9, 13, 0.85); backdrop-filter: blur(16px); border-bottom: 1px solid rgba(255, 255, 255, 0.08); padding-top: 1rem; padding-bottom: 1rem; } </style> </head> <body class="relative antialiased"> <!-- Navigation goes here --> <!-- 400vh Sticky Scroll Hero Video Engine --> <div id="hero-section" class="relative h-[400vh] w-full"> <div class="sticky top-0 w-full h-screen overflow-hidden flex items-center justify-center"> <video id="hero-video" class="absolute inset-0 w-full h-full object-cover pointer-events-none" playsinline muted preload="auto"></video> <div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-black/30 pointer-events-none"></div> <!-- Phase 0 Typography (0% - 25% Scroll) --> <div id="phase-0" class="phase-text absolute bottom-20 left-8 md:left-20 max-w-xl pointer-events-none"> <h1 class="font-serif text-5xl md:text-7xl font-light text-white leading-tight">First Headline Statement.</h1> </div> <!-- Phase 1 Typography (30% - 60% Scroll) --> <div id="phase-1" class="phase-text absolute top-1/3 right-8 md:right-24 max-w-lg pointer-events-none opacity-0"> <h2 class="font-serif text-4xl md:text-5xl font-light text-white leading-snug">Second Narrative Phase.</h2> </div> <!-- Phase 2 Typography (65% - 95% Scroll) --> <div id="phase-2" class="phase-text absolute bottom-24 left-8 md:left-20 max-w-lg pointer-events-none opacity-0"> <h2 class="font-serif text-4xl md:text-5xl font-light text-white leading-snug">Third Concluding Statement.</h2> </div> </div> </div> <!-- Supplemental Content & Looping Showcases --> <section class="py-32 px-8 md:px-20 max-w-7xl mx-auto"> <!-- Body Content Here --> </section> <!-- Video Scrubbing Engine & Progress-Locked Text --> <script> document.addEventListener('DOMContentLoaded', () => { const heroVideo = document.getElementById('hero-video'); const heroSection = document.getElementById('hero-section'); const nav = document.querySelector('header'); const phase0 = document.getElementById('phase-0'); const phase1 = document.getElementById('phase-1'); const phase2 = document.getElementById('phase-2'); // 1. In-Memory Blob Preloader with explicit video.load() fetch('./assets/hero.mp4') .then(res => { if (!res.ok) throw new Error('Video network response was not ok'); return res.blob(); }) .then(blob => { heroVideo.src = URL.createObjectURL(blob); heroVideo.load(); }) .catch(() => { heroVideo.src = './assets/hero.mp4'; heroVideo.load(); }); // 2. Normalized Scroll Progress let targetProgress = 0, currentProgress = 0; function updateScroll() { const rect = heroSection.getBoundingClientRect(); const max = rect.height - window.innerHeight; if (max > 0) targetProgress = Math.max(0, Math.min(1, -rect.top / max)); if (nav) { if (window.scrollY > 80) nav.classList.add('nav-scrolled'); else nav.classList.remove('nav-scrolled'); } } window.addEventListener('scroll', updateScroll, { passive: true }); window.addEventListener('resize', updateScroll); // 3. Progress-Locked Text Opacity Calculator function calcOpacity(progress, enterStart, enterEnd, exitStart, exitEnd) { if (progress < enterStart || progress > exitEnd) return 0; if (progress < enterEnd) return (progress - enterStart) / (enterEnd - enterStart); if (progress > exitStart) return Math.max(0, 1 - (progress - exitStart) / (exitEnd - exitStart)); return 1.0; } // 4. Animation Loop function scrubLoop() { currentProgress += (targetProgress - currentProgress) * 0.15; // Seek Video if (heroVideo.duration && !heroVideo.seeking) { const targetTime = currentProgress * heroVideo.duration; if (Math.abs(heroVideo.currentTime - targetTime) > 0.015) { heroVideo.currentTime = targetTime; } } // Lock Typography directly to scroll progress const op0 = calcOpacity(targetProgress, 0.0, 0.05, 0.20, 0.28); const op1 = calcOpacity(targetProgress, 0.28, 0.38, 0.55, 0.65); const op2 = calcOpacity(targetProgress, 0.65, 0.75, 0.90, 0.98); if (phase0) { phase0.style.opacity = op0.toFixed(3); phase0.style.transform = `translateY(${-targetProgress * 40}px)`; } if (phase1) { phase1.style.opacity = op1.toFixed(3); phase1.style.transform = `translateY(${(0.45 - targetProgress) * 30}px)`; } if (phase2) { phase2.style.opacity = op2.toFixed(3); phase2.style.transform = `translateY(${(0.80 - targetProgress) * 30}px)`; } requestAnimationFrame(scrubLoop); } requestAnimationFrame(scrubLoop); }); </script> </body> </html>


