agentleFS
Taltal190002-movTaltal190002-mov/claude-skills/scroll-video/SKILL.md

scroll-video

Skill2 starsChanged 6 months agoMITView on GitHub

Create scroll-driven video playback for landing pages and websites. The video plays forward as the user scrolls down and reverses when scrolling up — like Apple product pages and drinksom.eu. Trigger on: "סרטון בגלילה", "scroll video", "scroll-driven video", "video on scroll", "סרטון שרץ בגלילה", "video scrubbing", "scroll playback", "video parallax scroll", "apple style video scroll", "play video on scroll down".

  • Installs packages

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Create buttery-smooth scroll-controlled video playback using Lenis + GSAP ScrollTrigger + Canvas.

    line 13
  2. Very specific:

    Place hero-video.mp4 in the same directory as the landing page HTML.

    line 66
  3. Very specific:

    Video doesn't play at all: Check canplaythrough event fires.

    line 311
  4. Very specific:

    Headline doesn't fade: Verify hero-headline ID exists and section height is tall enough.

    line 316
  5. Very specific:

    Content below hero invisible: Check data-reveal scroll triggers aren't hiding elements that should be visible.

    line 318

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Scroll-Driven Video

Create buttery-smooth scroll-controlled video playback using Lenis + GSAP ScrollTrigger + Canvas.


How it works

The video plays forward as the user scrolls down and reverses when scrolling up. The hero section stays pinned while the video progresses. A headline overlay fades out with parallax as you scroll.

Architecture:

  1. Hidden <video> element (muted, no autoplay) — serves as the frame source
  2. <canvas> draws the current video frame on every rAF — smoother than native video display
  3. GSAP ScrollTrigger maps scroll progress (0→1) to a proxy object
  4. A render loop uses lerp interpolation to smoothly approach the target time
  5. Lenis provides smooth scroll behavior connected to ScrollTrigger

Critical: Video encoding

This is the #1 factor for smoothness. The video MUST be re-encoded with a keyframe on every single frame. Without this, the browser can only seek to sparse keyframes, causing visible jumps.

ffmpeg command

ffmpeg -i INPUT.mp4 -c:v libx264 -preset slow -crf 22 -g 1 -keyint_min 1 -an -movflags +faststart OUTPUT.mp4

Flags explained:

  • -g 1 -keyint_min 1 — keyframe on EVERY frame (the secret sauce)
  • -an — strip audio (not needed for scroll video)
  • -movflags +faststart — move moov atom to start for instant web playback
  • -crf 22 — good quality without bloating file size
  • -preset slow — better compression (file will be ~13MB for 8s video)

If ffmpeg is not installed

winget install Gyan.FFmpeg    # Windows
brew install ffmpeg            # macOS

Step-by-step implementation

Step 1: Copy and re-encode the video

ffmpeg -i original-video.mp4 -c:v libx264 -preset slow -crf 22 -g 1 -keyint_min 1 -an -movflags +faststart hero-video.mp4

Place hero-video.mp4 in the same directory as the landing page HTML.

Step 2: HTML structure

<!-- Hero section — height creates scroll runway for video -->
<section id="hero-video-section" class="relative h-[500vh]" style="background:#0d0d0d;">
  <div class="sticky top-0 h-screen w-full overflow-hidden">
    <!-- Hidden video source -->
    <video id="hero-video" muted playsinline preload="auto" src="hero-video.mp4"
           style="position:absolute;width:0;height:0;opacity:0;pointer-events:none;"></video>
    <!-- Canvas renders video frames smoothly -->
    <canvas id="hero-canvas" style="position:absolute;inset:0;width:100%;height:100%;"></canvas>
    <!-- Dark gradient overlay -->
    <div class="absolute inset-0 pointer-events-none"
         style="background:linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.1) 40%, rgba(0,0,0,0.25) 100%);z-index:1;"></div>
    <!-- Headline overlay -->
    <div id="hero-headline" class="absolute inset-0 flex items-center" style="z-index:2;">
      <div class="max-w-7xl mx-auto px-6 w-full">
        <div class="md:max-w-2xl text-right">
          <h1 class="text-6xl md:text-8xl font-black text-white leading-[0.95] mb-8 tracking-tight">
            כותרת ראשית
          </h1>
          <p class="text-lg md:text-xl text-white/60 mb-12 max-w-md leading-relaxed font-light">
            תיאור משני
          </p>
          <div class="flex flex-wrap gap-4">
            <button class="bg-red-700 text-white font-bold px-10 py-4 rounded-full text-base hover:bg-red-600 transition-all duration-200">
              CTA ראשי
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</section>

Step 3: CDN scripts (add before closing </body>)

<script src="https://unpkg.com/lenis@1.1.18/dist/lenis.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script>gsap.registerPlugin(ScrollTrigger);</script>
<script src="scroll-video.js"></script>

Step 4: JavaScript — scroll-video.js

(function () {
  'use strict';

  /* ── Lenis smooth scroll ── */
  var lenis = new Lenis({
    duration: 1.6,
    easing: function (t) { return Math.min(1, 1.001 - Math.pow(2, -10 * t)); },
    smoothWheel: true
  });
  function raf(time) { lenis.raf(time); requestAnimationFrame(raf); }
  requestAnimationFrame(raf);
  lenis.on('scroll', ScrollTrigger.update);
  gsap.ticker.add(function (time) { lenis.raf(time * 1000); });
  gsap.ticker.lagSmoothing(0);

  /* ── Grain overlay (optional — cinematic texture) ── */
  var grain = document.createElement('div');
  grain.id = 'grain-overlay';
  grain.style.cssText = [
    'position:fixed;inset:0;z-index:9998;pointer-events:none',
    'opacity:0.04',
    'background-image:url("data:image/svg+xml,%3Csvg viewBox=\'0 0 256 256\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'noise\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.9\' numOctaves=\'4\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23noise)\'/%3E%3C/svg%3E")',
    'background-repeat:repeat',
    'mix-blend-mode:overlay'
  ].join(';');
  document.body.appendChild(grain);
  setInterval(function () {
    grain.style.transform = 'translate(' + (Math.random() * -100) + 'px,' + (Math.random() * -100) + 'px)';
  }, 80);

  /* ── Scroll-driven video with canvas + lerp ── */
  var heroSection = document.getElementById('hero-video-section');
  var heroVideo = document.getElementById('hero-video');
  var heroCanvas = document.getElementById('hero-canvas');
  var heroHeadline = document.getElementById('hero-headline');

  if (heroVideo && heroCanvas && heroSection) {
    var ctx = heroCanvas.getContext('2d');
    var targetTime = 0;
    var currentTime = 0;
    var videoReady = false;
    var lerpFactor = 0.06; // Lower = smoother but slower to catch up

    function resizeCanvas() {
      var dpr = window.devicePixelRatio || 1;
      heroCanvas.width = window.innerWidth * dpr;
      heroCanvas.height = window.innerHeight * dpr;
      heroCanvas.style.width = window.innerWidth + 'px';
      heroCanvas.style.height = window.innerHeight + 'px';
      ctx.scale(dpr, dpr);
    }
    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);

    function drawFrame() {
      if (!videoReady) return;
      var cw = window.innerWidth;
      var ch = window.innerHeight;
      var vw = heroVideo.videoWidth;
      var vh = heroVideo.videoHeight;
      if (!vw || !vh) return;
      var scale = Math.max(cw / vw, ch / vh);
      var dw = vw * scale;
      var dh = vh * scale;
      var dx = (cw - dw) / 2;
      var dy = (ch - dh) / 2;
      ctx.clearRect(0, 0, cw, ch);
      ctx.drawImage(heroVideo, dx, dy, dw, dh);
    }

    /* Render loop — lerp currentTime toward target */
    function renderLoop() {
      if (videoReady) {
        var diff = targetTime - currentTime;
        if (Math.abs(diff) > 0.001) {
          currentTime += diff * lerpFactor;
          heroVideo.currentTime = currentTime;
        }
        drawFrame();
      }
      requestAnimationFrame(renderLoop);
    }

    function initVideoScroll() {
      if (!heroVideo.duration || !isFinite(heroVideo.duration)) return;
      if (videoReady) return;
      videoReady = true;
      heroVideo.currentTime = 0;
      currentTime = 0;
      targetTime = 0;

      /* GSAP sets TARGET time — render loop lerps toward it */
      var proxy = { t: 0 };
      gsap.to(proxy, {
        t: 1,
        ease: 'none',
        scrollTrigger: {
          trigger: heroSection,
          start: 'top top',
          end: 'bottom bottom',
          scrub: 0
        },
        onUpdate: function () {
          targetTime = proxy.t * heroVideo.duration;
        }
      });

      requestAnimationFrame(renderLoop);
      setTimeout(drawFrame, 200);

      /* Headline parallax + fade */
      if (heroHeadline) {
        gsap.to(heroHeadline, {
          y: 200, opacity: 0, ease: 'none',
          scrollTrigger: {
            trigger: heroSection,
            start: 'top top',
            end: '30% top',
            scrub: 1.5
          }
        });
      }
    }

    heroVideo.addEventListener('loadedmetadata', function () { heroVideo.currentTime = 0; });
    heroVideo.addEventListener('canplaythrough', initVideoScroll);
    if (heroVideo.readyState >= 4) initVideoScroll();
  }

  /* ── Scroll reveal ── */
  gsap.utils.toArray('[data-reveal]').forEach(function (el) {
    gsap.from(el, {
      y: 60, opacity: 0, duration: 1, ease: 'power3.out',
      scrollTrigger: { trigger: el, start: 'top 85%', toggleActions: 'play none none none' }
    });
  });
  gsap.utils.toArray('[data-reveal-stagger]').forEach(function (container) {
    gsap.from(container.children, {
      y: 50, opacity: 0, duration: 0.8, stagger: 0.15, ease: 'power3.out',
      scrollTrigger: { trigger: container, start: 'top 80%', toggleActions: 'play none none none' }
    });
  });

  /* ── Navbar — transparent → solid on scroll ── */
  var nav = document.querySelector('nav');
  if (nav) {
    var header = nav.closest('header');
    ScrollTrigger.create({
      start: 60,
      onUpdate: function (self) {
        if (self.direction === 1 && window.scrollY > 60) {
          header.style.background = 'rgba(13,13,13,0.95)';
          header.style.backdropFilter = 'blur(20px)';
        } else if (window.scrollY <= 60) {
          header.style.background = 'transparent';
          header.style.backdropFilter = 'blur(0px)';
        }
      }
    });
  }
})();

Key parameters to tune

ParameterDefaultEffect
h-[500vh] on section500vhMore height = slower video relative to scroll
lerpFactor0.06Lower = smoother but more lag; higher = snappier
scrub on GSAP00 = instant target update; higher = GSAP also smooths
crf in ffmpeg22Lower = better quality, bigger file
Lenis duration1.6Higher = more momentum in scroll

Checklist before deploying

  1. Video re-encoded with -g 1 -keyint_min 1 (every frame is a keyframe)
  2. Audio stripped with -an
  3. +faststart flag set for web delivery
  4. Canvas has devicePixelRatio scaling for retina displays
  5. Video has preload="auto" and playsinline attributes
  6. Gradient overlay ensures text readability over video
  7. Grain overlay for cinematic feel (optional — remove if not needed)
  8. data-reveal attributes on content sections below the video

Troubleshooting

Video jumps/stutters: Re-encode with -g 1. This is almost always the cause.

Video doesn't play at all: Check canplaythrough event fires. Ensure preload="auto".

Black canvas: Video may not have loaded yet. The setTimeout(drawFrame, 200) handles initial paint, but on slow connections increase the delay or add a loading indicator.

Headline doesn't fade: Verify hero-headline ID exists and section height is tall enough.

Content below hero invisible: Check data-reveal scroll triggers aren't hiding elements that should be visible. First section after hero should NOT have data-reveal if it starts in viewport.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.