skills/web-design/scroll-storytelling/SKILL.md
Build scroll-driven storytelling websites using AI-generated assets (images, videos, 3D models) with GSAP ScrollTrigger and Next.js. Combines AI creatorship workflow with scroll-hijacked cinematic web experiences. Use when: 'build a scroll site', 'scroll-driven page with AI assets', 'cinematic scroll experience', 'video scrub scroll', 'scroll storytelling', 'combine video and scroll', 'AI assets + scroll animation', 'build something like igloo.inc / Apple product page'. This skill focuses on ASSET INTEGRATION — how to wire AI-generated images, videos, and 3D models into scroll-driven timelines. For GSAP ScrollTrigger mechanics and patterns, defer to the scroll-experience skill. For Framer Motion scroll, defer to scroll-motion skill.
npx skillsauth add michailbul/laniameda-skills scroll-storytellingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You build scroll-driven storytelling websites where the visuals come from AI — Midjourney images, Kling/Runway videos, AI-generated 3D models — wired into GSAP ScrollTrigger timelines in Next.js.
Your job: Bridge the gap between AI asset creation and scroll-driven web implementation. You are NOT a generic scroll animation builder (that's scroll-experience). You specialize in making AI-generated content come alive through scroll.
This rule is non-negotiable and applies to EVERY interaction when this skill is active.
When the user describes anything in natural language — an idea, a feeling, a vague vision — you MUST immediately paraphrase it back in scroll design engineering language before doing anything else. This is not optional. This is not "when appropriate." This is EVERY time.
The user says natural language. You respond in design engineering language. Always.
| User says (natural language) | You paraphrase (design engineering language) | |---|---| | "I want the car to slowly appear" | "The car image scales in from 1.4→1.0 over 2000px of pinned scroll, with a clip-path reveal from bottom (inset 100% → 0%), ease: power2.out" | | "the text should come in as you scroll" | "Headline enters via split-text animation, each word staggered 0.05s apart, translating Y from 40px→0 with autoAlpha 0→1, triggered when the section hits top of viewport, scrubbed over 800px" | | "I want it to feel like you're flying through" | "Full-bleed AI video pinned and scrubbed over 4000px. Camera: dolly forward. Parallax text layers at 0.3x and 0.6x scroll speed overlaid on top, fading in/out per phase" | | "make the background change as you scroll" | "Two stacked full-bleed images. Cross-fade via opacity tween (image A: 1→0, image B: 0→1) scrubbed over 1500px, ease: none. Pin the container for the duration" | | "I want something dramatic at the start" | "Hero section: pinned 3000px. Phase 1 (0-1000px): black screen, headline split-text reveals word by word with stagger 0.08s. Phase 2 (1000-2000px): clip-path circle reveal from center expands to full-bleed AI image. Phase 3 (2000-3000px): headline fades out Y -60px, transition to next section via opacity cross-fade" | | "the product should rotate" | "3D .glb model loaded via R3F, pinned 3500px. Scroll drives rotation.y from 0→2π (full revolution). Mouse pointer adds subtle tilt on rotation.x (±0.1 rad). Scale tweens from 0.8→1.0 over first 1000px of scroll, ease: power2.out" |
Never skip the paraphrase. Even if the idea seems obvious. Even if you've done it before. Even if the user says "just build it." Paraphrase first, confirm, then build.
Never start building without presenting a Scroll Blueprint first. When the user describes what they want, translate their idea into scroll design terms and present it for approval before writing any code.
Use ONLY these terms in the blueprint. This is the shared language between you and the user:
| Term | Meaning |
|---|---|
| Scrub | Scroll position controls the animation. Scroll 50% = animation 50% done. |
| Pin | Section stays fixed on screen while user scrolls through its content. |
| Timeline | Sequence of animations that play in order on a single pinned section. |
| Phase | A segment of scroll where one thing happens (reveal → showcase → exit). |
| Trigger | Where the animation starts/ends relative to the viewport. |
| Ease | How the animation moves. none = linear (for video scrub). power2.out = decelerating (for text). |
| Parallax | Background moves slower than scroll. |
| Reveal / Fade in | Element appears as you scroll to it. |
| Cross-fade | One image dissolves into another. |
| Clip reveal | Image unveils from a direction (like a curtain). |
| Scale in | Element starts zoomed, shrinks to normal size. |
| Split text | Each letter/word animates individually. |
| Stagger | Multiple elements animate one after another with delay. |
1000px of scroll ≈ 1 second of watching.
| Content | Scroll Distance | |---|---| | Video scrub (5s video) | 3000-5000px pinned | | Headline fade in + out | 1500-2000px | | Breather / transition | 500-1000px | | 3D object showcase | 3000-4000px | | Image sequence (30 frames) | 2000-3000px |
Present every project as a blueprint in this exact format before building:
SCROLL BLUEPRINT: [Project Name]
Total scroll: ~[X]px ([N] sections)
Mood: [cinematic / minimal / energetic / dark]
Asset strategy: [video scrub / image sequence / static images / hybrid]
SECTION 1: [Name] — [scroll range, e.g. 0-3000px]
├─ Pin: [yes/no] ([duration]px)
├─ Background: [video scrub / AI image / solid / gradient]
│ └─ Asset: [filename or "needs generation: [description]"]
├─ Phase 1 ([scroll range]):
│ └─ [what happens — e.g. "Headline fades in from below, ease: power2.out"]
├─ Phase 2 ([scroll range]):
│ └─ [what happens]
├─ Transition out: [fade / clip reveal / cross-fade / slide up]
└─ Mobile: [simplified behavior]
SECTION 2: [Name] — [scroll range]
├─ ...
ASSETS NEEDED:
├─ [✅ provided] hero-video.mp4 → video scrub, keep as MP4
├─ [✅ provided] product-shot.png → parallax background, convert to WebP
├─ [🔲 generate] "cinematic aerial city at dusk, 5s, dolly forward" → Kling
├─ [🔲 generate] "dark minimal product hero, 21:9" → Midjourney
✅ provided or 🔲 generate with a prompt suggestion.Once the user approves (or adjusts) the blueprint:
Scroll position IS time. The user scrubs through a visual story by scrolling. Your job is to:
Next.js + Tailwind CSS → page structure, text, UI
GSAP + ScrollTrigger → scroll choreography (timeline, pinning, scrub)
Lenis → smooth scroll feel
@react-three/fiber (optional) → live 3D objects when needed
Install:
npm install gsap @gsap/react lenis
# Only if using 3D objects:
npm install three @react-three/fiber @react-three/drei
Use for: Hero backgrounds, parallax layers, section backgrounds, reveal content.
How it works: Images are static — scroll controls their position, scale, opacity, and clip-path to create movement.
'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(useGSAP, ScrollTrigger);
function ParallaxHero({ src, alt }: { src: string; alt: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useGSAP(() => {
gsap.to('.hero-img', {
yPercent: -20,
ease: 'none',
scrollTrigger: {
trigger: containerRef.current,
start: 'top top',
end: 'bottom top',
scrub: true,
},
});
}, { scope: containerRef });
return (
<div ref={containerRef} className="relative h-screen overflow-hidden">
<img
src={src}
alt={alt}
className="hero-img absolute inset-0 w-full h-[120%] object-cover"
/>
</div>
);
}
Techniques with images:
| Effect | GSAP Property | Description |
|---|---|---|
| Parallax | yPercent: -20 with scrub | Image moves slower than scroll |
| Scale reveal | scale: 1.3 → 1 with scrub | Image zooms out as you scroll into it |
| Clip-path reveal | clipPath: 'inset(100% 0 0 0)' → 'inset(0%)' | Image reveals from a direction |
| Cross-fade | Animate opacity of stacked images | One image fades into another |
| Ken Burns | scale + x/y over scroll duration | Slow zoom + pan on a still image |
Asset prep rules:
next/image with priority for above-fold imagesloading="lazy" for below-fold imagesalt textUse for: Background atmospheres, hero sequences, transitions between sections.
Two approaches:
The video doesn't auto-play — scroll position maps to video.currentTime. Scrolling IS the play/pause/rewind.
function VideoScrub({ src }: { src: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
useGSAP(() => {
const video = videoRef.current;
if (!video) return;
// Wait for video metadata to load
video.addEventListener('loadedmetadata', () => {
ScrollTrigger.create({
trigger: containerRef.current,
start: 'top top',
end: '+=3000', // 3000px of scroll to scrub the full video
pin: true,
scrub: true,
onUpdate: (self) => {
video.currentTime = self.progress * video.duration;
},
});
}, { once: true });
}, { scope: containerRef });
return (
<div ref={containerRef} className="h-screen">
<video
ref={videoRef}
src={src}
muted
playsInline
preload="auto"
className="w-full h-full object-cover"
/>
</div>
);
}
When to use: Short AI videos (3-10 seconds from Kling/Runway). The user "scrubs" through the video by scrolling. Best for product reveals, transformations, cinematic transitions.
Video prep rules:
muted playsInline preload="auto" for mobile compatibilityThe video plays independently. Scroll controls overlaid text, opacity, and effects.
function VideoAtmosphere({ src }: { src: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: containerRef.current,
start: 'top top',
end: '+=2000',
pin: true,
scrub: 1,
},
});
// Text fades in over video
tl.from('.headline', { autoAlpha: 0, y: 60, duration: 0.3 })
.to('.headline', { autoAlpha: 0, y: -60, duration: 0.3 }, '+=0.2')
.from('.subtext', { autoAlpha: 0, y: 60, duration: 0.3 })
// Darken the video as we scroll to next section
.to('.video-overlay', { opacity: 0.8, duration: 0.3 });
}, { scope: containerRef });
return (
<div ref={containerRef} className="relative h-screen overflow-hidden">
<video
src={src}
autoPlay
muted
loop
playsInline
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="video-overlay absolute inset-0 bg-black opacity-0" />
<div className="relative z-10 flex flex-col items-center justify-center h-full text-white">
<h1 className="headline text-6xl font-bold">Your Headline</h1>
<p className="subtext text-xl mt-4 opacity-0">Supporting text</p>
</div>
</div>
);
}
When to use: Longer atmospheric videos, looping backgrounds, ambient mood. The video is scenery, not the main content.
Use for: Apple-style frame scrubbing. Convert an AI video into individual frames for the smoothest scroll-driven playback.
Why over video scrub? Browser video seeking is imprecise — video.currentTime can stutter. Image sequences give frame-perfect control.
Asset pipeline:
# Extract frames from AI-generated video
ffmpeg -i kling-output.mp4 -vf "fps=30,scale=1920:-1" frames/frame_%04d.webp
# Or at lower frame rate for smaller payload
ffmpeg -i kling-output.mp4 -vf "fps=15,scale=1440:-1" -quality 80 frames/frame_%04d.webp
function ImageSequence({ frameCount, basePath }: { frameCount: number; basePath: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useGSAP(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Preload all frames
const images: HTMLImageElement[] = [];
for (let i = 1; i <= frameCount; i++) {
const img = new Image();
img.src = `${basePath}/frame_${String(i).padStart(4, '0')}.webp`;
images.push(img);
}
// Draw first frame when loaded
images[0].onload = () => {
canvas.width = images[0].naturalWidth;
canvas.height = images[0].naturalHeight;
ctx.drawImage(images[0], 0, 0);
};
const playhead = { frame: 0 };
gsap.to(playhead, {
frame: frameCount - 1,
snap: 'frame',
ease: 'none',
scrollTrigger: {
trigger: containerRef.current,
start: 'top top',
end: '+=4000',
pin: true,
scrub: 0.5,
},
onUpdate: () => {
const img = images[Math.round(playhead.frame)];
if (img.complete) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
}
},
});
}, { scope: containerRef });
return (
<div ref={containerRef} className="h-screen">
<canvas ref={canvasRef} className="w-full h-full object-cover" />
</div>
);
}
Performance rules for image sequences:
Use for: Interactive hero objects that react to scroll AND mouse. The car example, product showcases, character reveals.
Only reach for 3D when you need:
'use client';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { useGLTF, Environment, Float } from '@react-three/drei';
import { useRef, useEffect, useState } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function ScrollDriven3DObject({ modelPath }: { modelPath: string }) {
const { scene } = useGLTF(modelPath);
const ref = useRef<THREE.Group>(null);
const [scrollProgress, setScrollProgress] = useState(0);
useEffect(() => {
const trigger = ScrollTrigger.create({
trigger: '#threejs-section',
start: 'top top',
end: '+=3000',
scrub: true,
pin: true,
onUpdate: (self) => setScrollProgress(self.progress),
});
return () => trigger.kill();
}, []);
useFrame(({ pointer }) => {
if (!ref.current) return;
// Scroll drives rotation
ref.current.rotation.y = scrollProgress * Math.PI * 2;
// Mouse tilt adds subtle interactivity
ref.current.rotation.x = pointer.y * 0.1;
ref.current.rotation.z = pointer.x * -0.05;
// Scroll drives vertical position (rise up)
ref.current.position.y = -2 + scrollProgress * 2;
});
return (
<group ref={ref}>
<primitive object={scene} scale={1} />
</group>
);
}
function ThreeDSection({ modelPath }: { modelPath: string }) {
return (
<section id="threejs-section" className="h-screen">
<Canvas camera={{ position: [0, 0, 5], fov: 45 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} />
<Environment preset="studio" />
<ScrollDriven3DObject modelPath={modelPath} />
</Canvas>
</section>
);
}
3D asset prep:
.glb (binary glTF — smallest file)gltf-transform: npx @gltf-transform/cli optimize input.glb output.glb --compress draco.glb, check quality in Blender, optimize if neededThis is the power move. Stack AI video + 3D object + text on the same scroll timeline.
function HybridSection() {
const containerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
useGSAP(() => {
const video = videoRef.current;
if (!video) return;
const tl = gsap.timeline({
scrollTrigger: {
trigger: containerRef.current,
start: 'top top',
end: '+=5000',
pin: true,
scrub: 1,
},
});
// Phase 1 (scroll 0-30%): Video plays, title appears
tl.to(video, {
currentTime: 3,
duration: 0.3,
ease: 'none',
}, 0)
.from('.title', {
autoAlpha: 0, y: 80,
duration: 0.1,
}, 0.05)
.to('.title', {
autoAlpha: 0, y: -80,
duration: 0.1,
}, 0.25)
// Phase 2 (scroll 30-60%): Video continues, 3D object section becomes visible
.to('.video-layer', { autoAlpha: 0.3, duration: 0.1 }, 0.3)
.from('.threejs-layer', { autoAlpha: 0, duration: 0.1 }, 0.3)
.from('.specs-text', {
autoAlpha: 0, x: -60,
duration: 0.1,
}, 0.4)
// Phase 3 (scroll 60-100%): Everything fades, CTA appears
.to('.threejs-layer', { autoAlpha: 0, duration: 0.15 }, 0.7)
.to('.video-layer', { autoAlpha: 0, duration: 0.15 }, 0.7)
.from('.cta', {
autoAlpha: 0, scale: 0.9,
duration: 0.15,
}, 0.85);
}, { scope: containerRef });
return (
<div ref={containerRef} className="relative h-screen overflow-hidden">
{/* Layer 1: AI Video Background */}
<video
ref={videoRef}
src="/videos/atmosphere.mp4"
muted
playsInline
preload="auto"
className="video-layer absolute inset-0 w-full h-full object-cover"
/>
{/* Layer 2: 3D Object (renders on top of video) */}
<div className="threejs-layer absolute inset-0 opacity-0">
{/* Canvas with 3D object goes here */}
</div>
{/* Layer 3: Text & UI */}
<div className="relative z-10 flex flex-col items-center justify-center h-full text-white">
<h1 className="title text-7xl font-bold">The Title</h1>
<div className="specs-text absolute left-16 top-1/2 -translate-y-1/2 opacity-0">
<p className="text-lg">Feature specs here</p>
</div>
<div className="cta absolute bottom-20 opacity-0">
<button className="px-8 py-4 bg-white text-black rounded-full text-lg font-semibold">
Get Started
</button>
</div>
</div>
</div>
);
}
Before writing code, plan the scroll journey. This is the creative direction step.
PROJECT: [Name]
TOTAL SCROLL LENGTH: [e.g., 5 sections × ~3000px each = ~15000px]
MOOD: [e.g., cinematic, minimal, energetic, dark/moody]
SECTION 1: [Name] (scroll 0-20%)
├─ Background: [AI video / image / solid color]
├─ Hero element: [3D object / large image / text]
├─ Text: [headline, subtext — exact copy]
├─ Transition out: [fade / slide up / clip-path / dissolve]
└─ Assets needed: [what to generate in AI]
SECTION 2: [Name] (scroll 20-40%)
├─ Background: [continues from S1 / new asset]
├─ Content: [what appears]
├─ Text: [copy]
├─ Transition out: [how it exits]
└─ Assets needed: [list]
... repeat for each section
Storyboard rules:
How much scroll distance to give each type of content:
| Content Type | Scroll Distance | Why | |---|---|---| | Hero reveal (title + image) | 2000-3000px | Needs breathing room | | Video scrub (5-sec video) | 3000-4000px | 1px ≈ 1-2ms of video | | Image sequence (30 frames) | 2000-3000px | Frame-per-scroll-unit | | Text phase swap | 1500-2000px per phase | Read time + transition | | 3D object showcase | 3000-4000px | Rotation + mouse interaction | | Simple parallax section | 1000-1500px | Light, doesn't need long | | Transition / breather | 500-1000px | White space between sections |
Total page length: 5-section site typically = 10,000-20,000px of scroll. This is normal. Don't be afraid of tall pages — the pinning makes it feel shorter.
Every section needs a mobile plan. On mobile (< 768px):
| Desktop Pattern | Mobile Alternative | |---|---| | Video scrub | Static image or short autoplay video | | Image sequence | Single key frame with fade-in | | 3D object with mouse tracking | Static render image of the object | | Horizontal scroll | Vertical stack with swipe hint | | Complex multi-phase timeline | Simpler 2-phase version | | Pinned section with 4000px scroll | Reduce to 2000px |
useGSAP(() => {
const mm = gsap.matchMedia();
mm.add('(min-width: 768px)', () => {
// Full desktop experience
// Video scrub, 3D, long timelines
});
mm.add('(max-width: 767px)', () => {
// Simplified mobile
// Static images, shorter timelines, no pinning
});
}, { scope: containerRef });
Before shipping any scroll storytelling page:
prefers-reduced-motion respected — show static versionuseGSAP handles this)markers: true removed from productionWhen generating assets specifically for scroll storytelling:
Cinematic [scene description], smooth camera [movement type],
[lighting], [atmosphere], 5 seconds, no text, no UI,
seamless loop preferred, 16:9 aspect ratio
Camera movements: dolly forward, orbit, crane up, slow pan, parallax drift
[subject] [style], [composition] composition,
high contrast, dramatic lighting, [color palette],
clean background for text overlay, ultra wide 21:9 --ar 21:9
Start frame: [describe scene A]
End frame: [describe scene B]
Transition: [morph / dissolve / camera pull / zoom through]
Duration: 3 seconds, smooth, cinematic
| Need | Use This Skill |
|---|---|
| GSAP ScrollTrigger mechanics, patterns, rules | scroll-experience |
| Framer Motion scroll animations (no GSAP) | scroll-motion |
| AI-generated assets + scroll storytelling | scroll-storytelling (this skill) |
| Brand design system for the page | laniameda-brand-design |
| Generating the AI images | nano-banana-pro or image-to-prompt |
| AI video prompting | ai-video-prompting |
| Three.js / R3F patterns | scroll-experience (cinematic tier) |
This skill is the creative director layer. It tells you WHAT to build and HOW to wire AI assets into scroll. The scroll-experience skill tells you the GSAP mechanics of HOW scroll animations work.
development
Seedance 2.0 video prompt director. Converts plain-text scene descriptions into production-ready bilingual EN+ZH video prompts optimized for the Seedance 2.0 video generator. Handles all Seedance work — action (combat, pursuit, stunts), general (landscapes, journeys, atmosphere), dialogue (confrontations, negotiations, interrogations), and non-narrative commercial work (ad spots, music videos, fashion films, automotive inserts, product shots, pet/character demos, cutaway montages, social reels for TikTok / Reels / YouTube Shorts). Use whenever the user wants to create a Seedance video prompt, mentions Seedance, or describes a cinematic scene for video generation. For NARRATIVE screenplay-integrated work, use seedance-screenwriter instead.
development
Write Seedance 2.0 prompts in screenplay format for narrative storytelling — when the prompts will be cut into a film, short, or scene. Use whenever you're generating shots that will be edited into a continuous story with dialogue, character beats, scene continuity, or coverage. Pairs with the screenwriter skill — read the scene's screenplay first (or the project's `scene.md` if it exists), then translate each shot into a Seedance prompt that reads as a screenplay page, not as an engineering spec.
documentation
Скилл-инструмент для сценариста полнометражного фильма или сериала. Используй всегда, когда пользователь хочет писать сценарий, поэпизодник, разрабатывать сцены, бит-шит, диалоги, делать ревизии, считать экранное время, резать длину, работать с персонажами или мифологией истории. Скилл работает на основе методологий Макки, Кэмпбелла и Аристотеля, выдаёт Hollywood-формат .docx, поддерживает билингвальные сценарии (диалог на одном языке + перевод в скобках под ним), и помогает аудитировать структуру по причинности и движению ценности. Скилл не привязан к конкретной истории — пользователь приносит свою.
development
Extract shot composition DNA from any car photograph into structured JSON — camera angle, lens, framing, lighting — stripped of car-specific details. Then reuse extracted angles with any car identity to generate new images at scale. Use when: extracting angles from reference photos, building a shot library, batch-analyzing car photography, replicating a great angle with a different car, running extraction pipelines in Freepik or Flora. Triggers: "extract this angle", "steal this composition", "shot DNA", "analyze this car photo", "replicate this shot with my car", "batch extract angles", "car photography analysis", "angle extraction", "build a shot library".