From site-craft-skills
Turn a video file into a premium scroll-sequence website — the Apple-style experience where scrolling scrubs through video frames with choreographed text animations. Use this skill whenever the user provides a video file (MP4, MOV, WebM, etc.) and wants it turned into a scroll-sequence site, scroll-driven landing page, or scrollytelling experience. Also triggers when the user mentions "scroll sequence," "scrollytelling," "scroll-driven animation," "video frames on scroll," "Apple-style scroll animation," "frame-by-frame scroll," or wants a product reveal site built from video footage. If the user has a product video, demo reel, or any footage they want presented as a polished scroll-sequence web experience rather than just an embedded video player, this is the skill to use.
How this skill is triggered — by the user, by Claude, or both
Slash command
/site-craft-skills:scroll-sequenceThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Turn a video file into a scroll-driven animated website with **animation variety and choreography** — multiple animation types working together, not one repeated effect.
Turn a video file into a scroll-driven animated website with animation variety and choreography — multiple animation types working together, not one repeated effect.
The result is a website where scrolling controls video playback frame-by-frame on a canvas, with text sections animating in from different directions as the user scrolls. Think Apple product pages, but generated from any video — product demos, brand films, nature footage, music videos, or anything visual.
This skill does:
This skill does not:
The user provides a video file path (MP4, MOV, WebM, etc.) and optionally:
If the user doesn't specify these, ask briefly or use sensible creative defaults.
These are strong defaults backed by visual reasoning. Each solves a specific problem. If the user explicitly requests something different, follow their preference — they know their context better.
data-persist="true" keeps the final section visible after it animates inalign-left/align-right) so the video occupies the viewport center without competition. Exception: stats with full dark overlayclip-path: circle() as the hero scrolls awayFRAME_SPEED: 1.0). If the user wants the video to finish earlier (e.g., "video should finish at 80% scroll"), calculate FRAME_SPEED = 100 / desired_percent:
FRAME_SPEED: 1.0 (default, full sync)FRAME_SPEED: 1.25FRAME_SPEED: 1.67FRAME_SPEED: 2.0
After the video finishes, the last frame stays frozen for the remaining scroll. The dark scrim ensures text stays readable over both live and frozen frameswhich ffmpeg && which ffprobe
If not found, tell the user to install ffmpeg before proceeding.
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration,r_frame_rate,nb_frames -of csv=p=0 "<VIDEO_PATH>"
Determine resolution, duration, frame rate, total frames. Decide:
mkdir -p frames
ffmpeg -i "<VIDEO_PATH>" -vf "fps=<CALCULATED_FPS>,scale=<WIDTH>:-1" -c:v libwebp -quality 80 "frames/frame_%04d.webp"
After extraction, verify the count: ls frames/ | wc -l
Create an output directory named after the project (e.g., the brand name, or video-site as fallback):
<project-name>/
index.html
css/style.css
js/app.js
frames/frame_0001.webp ...
No bundler. Vanilla HTML/CSS/JS + CDN libraries. This keeps it portable and zero-config.
Required <head> content:
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="[PROJECT_DESCRIPTION]">
<meta property="og:title" content="[PROJECT_NAME]">
<meta property="og:description" content="[PROJECT_DESCRIPTION]">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>EMOJI</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
Required <body> structure (in this order):
1. Loader — #loader > .loader-brand, #loader-bar-track > #loader-bar, #loader-percent
2. Fixed header — .site-header > nav with logo + links
3. Hero — .hero-standalone (100vh, solid bg, word-split heading)
4. Canvas — .canvas-wrap > canvas#canvas (fixed, full viewport)
5. Dark overlay — #dark-overlay (fixed, full viewport, pointer-events:none)
6. Marquee(s) — .marquee-wrap > .marquee-text (fixed, 12vw font)
7. Scroll container — #scroll-container (800vh+)
Content sections with data-enter, data-leave, data-animation
Stats section with .stat-number[data-value][data-decimals]
CTA section with data-persist="true"
Read references/implementation.md for HTML templates (content sections, stats sections, CDN script tags).
Key concerns:
.align-left and .align-right keep text in the outer 40%, leaving center for videoposition: absolute within scroll container, positioned at midpoint of enter/leave rangebackground: rgba(0,0,0,0.92) !important on .section-inner. The !important is needed because GSAP's inline style cascade can interfere. This reads as a solid dark card with just a hint of the video at the edges. Subtle scrims (0.5-0.8) are NOT enough — video frames are busy and bright.opacity below 0.85 on .section-label, .section-body, or .section-note. These elements sit over video — reducing opacity makes them gray and invisible on bright frames. Use color values for hierarchy instead (e.g. #fff for headings, #ddd for body, #aaa for labels).Read references/implementation.md (CSS Patterns section) for the full CSS including mobile breakpoints.
The JavaScript has 9 components. Read references/implementation.md for the complete code. Here's what each does and why:
| Component | Purpose |
|---|---|
| Lenis setup | Smooth scroll interpolation — mandatory for frame playback to feel good. Must include smoothTouch: true for iOS Safari |
| Frame preloader | Two-phase: 10 frames fast, then the rest. Shows progress bar. |
| Canvas renderer | Padded cover mode (IMAGE_SCALE 0.82-0.90) with background color sampled from frame edges |
| Frame-to-scroll binding | Maps scroll progress to frame index with FRAME_SPEED acceleration |
| Section animations | Reads data-animation attribute, plays entrance on enter, reverses on leave (unless persist) |
| Counter animations | Numbers count up from 0 using GSAP snap |
| Marquee | Horizontal sliding text with scroll-linked fade in/out |
| Dark overlay | Fades to 0.9 opacity over the stats section range |
| Circle-wipe | Expands clip-path circle to reveal canvas as hero scrolls away |
npx serve . or python3 -m http.server 8000If the user wants the site live, choose a deployment target:
Option A — Vercel (default, no auth required):
bash skills/vercel-deploy/scripts/deploy.sh <project-directory>
Vercel's deploy endpoint has a ~4.5MB compressed payload limit. Full-resolution scroll-sequence sites (1920px, 200 frames) typically exceed this. Before deploying to Vercel, re-extract frames at deploy-friendly resolution:
ffmpeg -y -i video.mp4 -vf "fps=12,scale=960:-1" -c:v libwebp -quality 65 frames/frame_%04d.webp
Then update FRAME_COUNT in js/app.js to match the new count. A typical 8s video at 960px/12fps = ~96 frames, ~2MB compressed. No need to change FRAME_SPEED — it stays at 1.0 regardless of frame count.
Option B — AWS S3 + CloudFront (no size limit):
bash skills/aws-deploy/scripts/deploy.sh <project-directory> [--region us-east-1]
AWS has no payload size limit — deploy full-resolution 1920px frames without downsampling. Use this when the user requests AWS hosting, or when asset size exceeds Vercel's 4.5MB limit and the user doesn't want to reduce quality. Requires AWS CLI and credentials. First deploy takes 3-5 minutes; subsequent deploys reuse the same stack. If the aws-deploy skill is installed at a different path, look for it at ../aws-deploy/scripts/deploy.sh relative to this skill, or in .claude/skills/aws-deploy/scripts/.
Option C — Google Cloud / Firebase (no size limit):
bash skills/gcp-deploy/scripts/deploy.sh <project-directory> [project-id]
GCP/Firebase has no strict payload size limit for Hosting — deploy full-resolution frames without downsampling. Use this when the user requests GCP or Firebase hosting, or when asset size exceeds Vercel's 4.5MB limit. Requires Firebase CLI and local authentication. If the gcp-deploy skill is installed at a different path, look for it at ../gcp-deploy/scripts/deploy.sh relative to this skill, or in .claude/skills/gcp-deploy/scripts/.
Mobile is where scroll-driven video sites break hardest. These are requirements, not suggestions:
matchMedia("(max-width: 767px)") to load a reduced frame set on mobile — cap at 150 frames and 1280px resolution. The implementation reference includes a getMobileFrameCount() helper for this.smoothTouch: true — without it, iOS Safari's momentum scrolling fights with Lenis and causes choppy frame playback.IMAGE_SCALE: 0.85 causes excessive letterboxing — the video appears small with large background bars. Increase to IMAGE_SCALE: 0.95 on mobile so the video fills the screen. The implementation reference includes a responsive scale calculation.references/implementation.md.viewport-fit=cover in the meta tag so the canvas extends into the safe area on notched phones.| Type | Initial State | Animate To | Duration |
|---|---|---|---|
fade-up | y:50, opacity:0 | y:0, opacity:1 | 0.9s |
slide-left | x:-80, opacity:0 | x:0, opacity:1 | 0.9s |
slide-right | x:80, opacity:0 | x:0, opacity:1 | 0.9s |
scale-up | scale:0.85, opacity:0 | scale:1, opacity:1 | 1.0s |
rotate-in | y:40, rot:3, opacity:0 | y:0, rot:0, opacity:1 | 0.9s |
stagger-up | y:60, opacity:0 | y:0, opacity:1 | 0.8s |
clip-reveal | clipPath:inset(100% 0 0 0) | clipPath:inset(0%) | 1.2s |
All use stagger (0.1-0.15s). Easing: power3.out (scale-up: power2.out, clip-reveal: power4.inOut).
With the default FRAME_SPEED: 1.0, the video plays across the entire scroll — every section has live video behind it. If the user requests an earlier completion point, sections after that point animate over the frozen last frame. Either way, the dark scrim on .section-inner ensures readability.
Formula for N content sections (excludes the hero, which is a standalone element):
| Zone | Scroll range | Purpose |
|---|---|---|
| Hero reveal | 0-7% | Hero fades out, circle-wipe reveals canvas |
| Content sections | 8-100% | Sections animate over video (live or frozen) |
Within the content zone, divide evenly with ~2% gaps between sections. Each section needs at least 8% range for animations to breathe.
Example for 6 sections:
| Section | enter | leave | animation | alignment |
|---|---|---|---|---|
| 1 (intro) | 10 | 24 | slide-left | align-left |
| 2 (feature) | 26 | 40 | slide-right | align-right |
| 3 (feature) | 42 | 54 | fade-up | align-left |
| 4 (stats) | 56 | 72 | stagger-up | (centered, dark overlay) |
| 5 (detail) | 74 | 86 | scale-up | align-right |
| 6 (CTA) | 88 | 100 | clip-reveal | align-left, persist |
Dark overlay enter/leave should match the stats section range (here: 0.56 to 0.72).
Quick rules:
data-enter/data-leavedata-animation on consecutive sectionsalign-left / align-right (except stats, which are centered)data-persist="true"100 / desired_percentrgba(0,0,0,0.78) is too transparent. Use rgba(0,0,0,0.92) on .section-inner so it reads as a solid dark card. Never use gradients that fade below 0.9. Never reduce text opacity below 0.85 — use color values for hierarchy insteadBeyond the default circle-wipe, other reveal options:
inset(0 100% 0 0) -> inset(0 0% 0 0)inset(100% 0 0 0) -> inset(0% 0 0 0)polygon(50% 0%, 50% 0%, 50% 100%, 50% 100%) -> polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)prefers-reduced-motion: CSS in references/implementation.md disables animations and clip-paths when this media query matches. In JS, also check it to skip Lenis and show all sections immediately:
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reducedMotion) {
// Skip Lenis, show all sections, display frames as a static image
document.querySelectorAll(".scroll-section").forEach(s => { s.style.opacity = 1; s.style.position = "relative"; });
}
<header>, <main>, <section>, <footer> — not all <div>saria-label to the canvas element describing the video content:focus-visible outlines#scroll-container needs z-index: 8 (or higher than .canvas-wrap at z-index 5). Without it, the fixed canvas covers the scroll sections and the scrim background is invisible even though it's in the CSS.file://scrub value, reduce frame countdevicePixelRatio scaling to canvas dimensionslenis.on("scroll", ScrollTrigger.update) is connecteddata-value attribute exists and snap matches decimal placesnpx claudepluginhub cyranob/site-craft-skills --plugin gcp-deployGenerates MP4 motion graphics videos from a content brief using HTML/CSS animations rendered frame-by-frame in headless Chromium via Playwright, assembled with FFmpeg.
Captures a general website URL and turns it into a HyperFrames video (site tour, showcase, social clip) using headless Chrome screenshots and brand assets. Not for product launches or topic explainers.
Builds complete frontend pages with React/Next.js, Tailwind, cinematic animations, AI-generated media assets, persuasive copy, and generative art for landing pages, marketing sites, and dashboards.