<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Scrollsequence]]></title><description><![CDATA[Scrollsequence]]></description><link>https://scrollsequence.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 17:44:22 GMT</lastBuildDate><atom:link href="https://scrollsequence.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[From video to scroll animation - DIY with ffmpeg, or free with Scrollsequence vid2ssq]]></title><description><![CDATA[Scrubbing an MP4 with currentTime stutters. The reliable pattern is frames on a canvas. Here's the generic DIY path, then what free npx scrollsequence…]]></description><link>https://scrollsequence.hashnode.dev/from-video-to-scroll-animation-diy-with-ffmpeg-or-free-with-scrollsequence-vid2ssq</link><guid isPermaLink="true">https://scrollsequence.hashnode.dev/from-video-to-scroll-animation-diy-with-ffmpeg-or-free-with-scrollsequence-vid2ssq</guid><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[WordPress]]></category><category><![CDATA[animation]]></category><dc:creator><![CDATA[Alki]]></dc:creator><pubDate>Thu, 29 Oct 2026 09:10:08 GMT</pubDate><content:encoded><![CDATA[<p><img src="https://beta.scrollsequence.com/blog/ai-video-scroll-animation-wordpress-elementor/ai-video-on-scroll.png" alt="AI video to scroll animation" /></p>
<p>Whether the clip came from Kling, Veo, Seedance, After Effects, or a phone, the web problem is the same: <strong>scroll wants random access to frames</strong>. An MP4 wants a decoder and keyframes. That is why <code>video.currentTime = progress * duration</code> jumps - especially scrolling back, especially on iOS.</p>
<p>The industry pattern (Apple product pages included) is:</p>
<ol>
<li>Turn the video into numbered stills.</li>
<li>Draw the current still on a <code>&lt;canvas&gt;</code> from scroll progress.</li>
<li>Serve a lighter set on phones.</li>
</ol>
<p>You can do that by hand. Or you can generate a production bundle with Scrollsequence’s free local pipeline.</p>
<h2>Part 1 - The generic DIY way</h2>
<h3>1. Extract frames with ffmpeg</h3>
<pre><code># Example: ~24 fps stills, 1600px wide WebP
ffmpeg -i clip.mp4 -vf "fps=24,scale=1600:-1" frame_%04d.webp
</code></pre>
<p>Then <strong>thin the set</strong>. You rarely need every frame. A practical hero is often ~100–150 frames on desktop, fewer on mobile.</p>
<p>If you insist on scrubbing the MP4 anyway, re-encode with frequent keyframes first (<code>-g 1</code> or <code>-g 2</code>). That helps seeking; it still will not match true frame sequences for reverse scrub and memory control.</p>
<h3>2. Scrub on a canvas (vanilla idea)</h3>
<pre><code>&lt;section id="pin" style="height: 300vh;"&gt;
  &lt;canvas id="seq" style="position: sticky; top: 0; width: 100%; height: 100vh;"&gt;&lt;/canvas&gt;
&lt;/section&gt;
</code></pre>
<pre><code>const frames = []; // preload Image() / createImageBitmap for each frame_%04d.webp
const canvas = document.getElementById('seq');
const ctx = canvas.getContext('2d');
let current = -1;

function draw(i) {
  const img = frames[i];
  if (!img) return;
  // size canvas to CSS * devicePixelRatio (cap at 2 on phones)
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}

function onScroll() {
  const el = document.getElementById('pin');
  const rect = el.getBoundingClientRect();
  const total = el.offsetHeight - window.innerHeight;
  const scrolled = Math.min(total, Math.max(0, -rect.top));
  const progress = total &gt; 0 ? scrolled / total : 0;
  const i = Math.round(progress * (frames.length - 1));
  if (i === current) return;
  current = i;
  requestAnimationFrame(() =&gt; draw(i));
}

window.addEventListener('scroll', onScroll, { passive: true });
</code></pre>
<p>That is the whole “Apple-style” trick in spirit: <strong>progress → index → draw</strong>, only when the index changes.</p>
<h3>3. What DIY usually forgets (and why demos lag)</h3>
<table>
<thead>
<tr>
<th>Concern</th>
<th>DIY habit that breaks</th>
<th>Better default</th>
</tr>
</thead>
<tbody><tr>
<td>File weight</td>
<td>One 1920px set for everyone</td>
<td>Separate ~1280 and ~1920 tiers</td>
</tr>
<tr>
<td>Memory (iOS)</td>
<td>Decode 150 full-HD frames</td>
<td>Cap DPR at 2; fewer/narrower frames on phones</td>
</tr>
<tr>
<td>Loading</td>
<td>Wait for all frames</td>
<td>Load every Nth frame first, fill gaps</td>
</tr>
<tr>
<td>Layout</td>
<td>Fixed-height parent fights sticky</td>
<td>Tall scroll section + sticky canvas</td>
</tr>
<tr>
<td>SEO</td>
<td>Text baked into frames</td>
<td>Real HTML overlays beside/over the canvas</td>
</tr>
<tr>
<td>Window Resize</td>
<td>When window resizes, the canvas overflows or underflows</td>
<td>Resize canvas and redraw on resize</td>
</tr>
</tbody></table>
<p>You can wire GSAP ScrollTrigger instead of the raw scroll math - same idea: scrub a number from 0→1, map to <code>frameIndex</code>.</p>
<hr />
<h2>Part 2 - Free Scrollsequence path: <code>npx scrollsequence vid2ssq</code></h2>
<p>Scrollsequence v2 is built around the same model on every stack: <strong>Scene → Layer → Canvas</strong>. The asset side is a pipeline that turns raw <code>.mp4</code> / <code>.webm</code> into multi-resolution folders plus a <code>scrollsequence-manifest.json</code> that React, vanilla JS, and WordPress all consume the same way.</p>
<h3>Free vs optional AI (important)</h3>
<p>From the <a href="https://docs.scrollsequence.com/v2/asset-pipeline">Asset Pipeline docs</a> and <a href="https://docs.scrollsequence.com/v2/wordpress/credits">credits docs</a>:</p>
<ul>
<li><strong>100% free / local:</strong> frame extraction, responsive resizing, format/quality, frame step, manifest generation (CLI uses local FFmpeg via Node / Wasm - no credits).</li>
<li><strong>Optional / API key + credits:</strong> <code>--depth</code> (3D depth maps), <code>--track "sports car"</code> (subject tracking), <code>--vision</code> (scene / camera / subject metadata for agents and automation).</li>
</ul>
<p>So the default “video → scroll-ready bundle” path is free. Depth maps and vision are extras when you want WebGL tilt or agent-readable metadata.</p>
<h3>Run the wizard</h3>
<pre><code># Interactive wizard (beta package URL - see docs for current .tgz)
npx https://docs.scrollsequence.com/scrollsequence-2.0.0-beta.52.tgz vid2ssq

# Or if already installed:
npx scrollsequence vid2ssq
</code></pre>
<p>Non-interactive example (free local processing):</p>
<pre><code>npx scrollsequence vid2ssq input.mp4 \
  --name hero-sequence \
  --sizes 1280,1920 \
  --format jpg \
  --quality 80
</code></pre>
<p>Optional AI (needs <code>SSQ_API</code>):</p>
<pre><code>npx scrollsequence vid2ssq input.mp4 \
  --name car-reveal \
  --sizes 1280,1920 \
  --depth \
  --track "sports car" \
  --vision
</code></pre>
<h3>What you get that hand-rolled ffmpeg usually skips</h3>
<ol>
<li><strong>Multiple size tiers</strong> (e.g. <code>1280</code> + <code>1920</code>) so phones are not force-fed desktop frames. The runtime picks a tier from canvas CSS width × devicePixelRatio.</li>
<li><strong><code>scrollsequence-manifest.json</code></strong> - one file describing paths, frame counts, optional <code>files.d</code> depth maps, <code>tracking</code>, and <code>vision</code>.</li>
<li><strong>Depth maps (optional)</strong> - grayscale per frame for WebGL 2.5D mouse / gyro tilt (shared lower-res depth across color tiers).</li>
<li><strong>Tracking + vision (optional)</strong> - bounding data and scene/camera/subject JSON so an AI agent (or your own UI) can pin labels to a moving subject or reason about the shot.</li>
<li><strong>Same output for WordPress, React, and vanilla</strong> - no separate “Elementor-only” asset format.</li>
</ol>
<p>Inside Next.js / Vite, if a <code>public/</code> folder exists, the CLI writes straight to <code>public/&lt;name&gt;/</code> so the dev server can serve frames immediately.</p>
<h3>Drop the manifest into the page</h3>
<p><strong>Vanilla (free engine pattern):</strong></p>
<pre><code>&lt;div class="scrollsequence-scene" data-ssq-duration="1500" data-ssq-scrub="1"&gt;
  &lt;div class="scrollsequence-scene_inner"&gt;
    &lt;div class="scrollsequence-layer"&gt;
      &lt;div
        class="scrollsequence-canvas2d"
        data-ssq-media-groups='[{
          "type": "manifest",
          "rootDir": "/hero-sequence/",
          "imageSizes": { /* from scrollsequence-manifest.json */ }
        }]'
      &gt;
        &lt;canvas&gt;&lt;/canvas&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;script type="module"&gt;
  import ScrollsequenceManager from 'scrollsequence';
  import 'scrollsequence/scrollsequence.css';
  ScrollsequenceManager.initialize().preloadAll();
&lt;/script&gt;
</code></pre>
<p><strong>React / Next.js:</strong></p>
<pre><code>'use client';
import { Scene, Layer, Canvas2d } from 'scrollsequence/react';
import manifest from '@/public/hero-sequence/scrollsequence-manifest.json';

export default function Hero() {
  return (
    &lt;Scene duration={1500}&gt;
      &lt;Layer&gt;
        &lt;Canvas2d
          mediaGroups={[{
            type: 'manifest',
            rootDir: '/hero-sequence/',
            imageSizes: manifest.imageSizes,
          }]}
        /&gt;
      &lt;/Layer&gt;
    &lt;/Scene&gt;
  );
}
</code></pre>
<p>WordPress v2 uses the same Canvas 2D + Media Tools <code>vid2ssq</code> flow (basic extraction free in-browser; AI toggles credit-based).</p>
<p>Install note (public beta): package currently installs from the docs <code>.tgz</code> until the npm name settles — see <a href="https://docs.scrollsequence.com/v2">docs.scrollsequence.com/v2</a>.</p>
<hr />
<h2>Which path should you use?</h2>
<table>
<thead>
<tr>
<th></th>
<th>DIY ffmpeg + custom JS</th>
<th>Free <code>vid2ssq</code> + Scrollsequence runtime</th>
</tr>
</thead>
<tbody><tr>
<td>Learning / one-off</td>
<td>Great</td>
<td>Optional</td>
</tr>
<tr>
<td>Multi-size + manifest</td>
<td>You build it</td>
<td>Included, free</td>
</tr>
<tr>
<td>Depth / track / vision</td>
<td>DIY or other tools</td>
<td>Optional flags</td>
</tr>
<tr>
<td>WP + React + vanilla same assets</td>
<td>Manual</td>
<td>Native</td>
</tr>
<tr>
<td>Cost for basic convert</td>
<td>Your time</td>
<td>Free local</td>
</tr>
</tbody></table>
<p>DIY teaches the mechanism. <code>vid2ssq</code> is the same mechanism packaged so you are not reinventing responsive tiers and a manifest every project — and the basic convert stays free.</p>
<p>Docs: <a href="https://docs.scrollsequence.com/v2/asset-pipeline">Asset Pipeline &amp; CLI</a> · <a href="https://docs.scrollsequence.com/v2/vanilla">Vanilla</a> · <a href="https://docs.scrollsequence.com/v2/react">React</a> · <a href="https://docs.scrollsequence.com/v2/wordpress">WordPress</a></p>
<hr />
<p><em>Originally published on <a href="https://beta.scrollsequence.com/blog/ai-video-scroll-animation-wordpress-elementor">Scrollsequence</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Video on scroll in Elementor — why scrubbing an MP4 stutters (and what to do instead)]]></title><description><![CDATA[GSAP ScrollTrigger + Elementor video-on-scroll tutorials scrub MP4 currentTime. Here's why that jumps, especially on iOS, and how frame sequences fix it.]]></description><link>https://scrollsequence.hashnode.dev/video-on-scroll-in-elementor-why-scrubbing-an-mp4-stutters-and-what-to-do-instead</link><guid isPermaLink="true">https://scrollsequence.hashnode.dev/video-on-scroll-in-elementor-why-scrubbing-an-mp4-stutters-and-what-to-do-instead</guid><category><![CDATA[WordPress]]></category><category><![CDATA[Elementor]]></category><category><![CDATA[GSAP]]></category><category><![CDATA[webdev]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Alki]]></dc:creator><pubDate>Wed, 28 Oct 2026 14:33:53 GMT</pubDate><content:encoded><![CDATA[<p>Originally published at: <a href="https://beta.scrollsequence.com/blog/video-on-scroll-why-scrubbing-stutters-mp4-vs-image-sequence">https://beta.scrollsequence.com/blog/video-on-scroll-why-scrubbing-stutters-mp4-vs-image-sequence</a></p>
<p>Tutorials like this one from Nicolai Palmkvist that scrub a video with GSAP ScrollTrigger in Elementor look great in the demo. Then you try it on your site and it jumps, freezes when you scroll back up, or completely falls apart on iPhone.</p>
<p>That is not you "doing Elementor wrong." It is how browsers seek video.</p>
<h2>What Those Tutorials Actually Do</h2>
<p>The usual pattern you see in tutorials:</p>
<ol>
<li>Drop a <code>&lt;video&gt;</code> (or a background video widget) in a tall section.</li>
<li>Pin the section with ScrollTrigger.</li>
<li>On scroll progress, set:</li>
</ol>
<pre><code class="language-js">video.currentTime = progress * duration;
</code></pre>
<p>On paper, that cleanly maps scroll distance to video playback time. In practice, the browser does not store every frame as a clean, random-access image.</p>
<p>Modern video codecs store keyframes (I-frames) and delta frames (P-frames and B-frames). Seeking to an arbitrary <code>currentTime</code> forces the browser's video decoder to snap to the nearest keyframe or decode intermediate frames on the fly. As a result, the animation stutters — especially when you scrub backwards.</p>
<p>iOS Safari is the harshest case:</p>
<ul>
<li>Stricter seeking throttles.</li>
<li>Aggressive memory pressure and hardware decoder throttling.</li>
<li>Mandatory <code>muted</code> + <code>playsinline</code> attributes, or the video will refuse to behave like an inline scrubbable timeline at all.</li>
</ul>
<h2>Quick Fix If You Must Keep the MP4</h2>
<p>If you need to stay on video for now, these adjustments will get you the best possible results:</p>
<h3>1. Re-encode With Frequent Keyframes</h3>
<p>Use FFmpeg to force keyframes at short intervals:</p>
<pre><code class="language-bash">ffmpeg -i input.mp4 -an -c:v libx264 -g 2 -keyint_min 2 -sc_threshold 0 -crf 24 -pix_fmt yuv420p output.mp4
</code></pre>
<ul>
<li><code>-g 2</code> places a keyframe approximately every 2 frames.</li>
<li>Seeking becomes dramatically smoother because the decoder never has to jump far to find a reference frame.</li>
<li>Trade-off: file size increases significantly.</li>
</ul>
<h3>2. Keep the Video Short and Light</h3>
<ul>
<li><strong>Mute it (<code>-an</code>):</strong> Audio is completely useless for scroll scrubbing and wastes bandwidth.</li>
<li><strong>Keep it brief:</strong> Prefer 8–12 seconds over a long timeline.</li>
<li><strong>Cap resolution:</strong> Often 1280px wide is more than enough for a hero section.</li>
</ul>
<h3>3. Respect Mobile Video Rules</h3>
<ul>
<li>Ensure <code>muted</code> and <code>playsinline</code> attributes are set on the <code>&lt;video&gt;</code> element.</li>
<li>Preload carefully: avoid downloading a massive video file before the section is actually near the viewport.</li>
</ul>
<p>This can get your MP4 scrubbing to "pretty good." It rarely gets you to Apple-style, frame-perfect control.</p>
<h2>Better Approach: Scroll an Image Sequence</h2>
<p>Apple product pages (and most high-end scrollytelling websites) do not scrub an MP4. Instead, they render numbered image stills onto an HTML5 <code>&lt;canvas&gt;</code> (or swap images) driven by scroll progress.</p>
<h3>1. Extract and Prepare Stills</h3>
<p>You can extract frames manually with FFmpeg:</p>
<pre><code class="language-bash"># Extract ~24 fps stills, then thin them to ~100–150 frames total
ffmpeg -i clip.mp4 -vf "fps=24,scale=1600:-1" frame_%04d.jpg
</code></pre>
<p>Or run the automated Scrollsequence CLI wizard, which extracts frames, creates responsive resolution folders (e.g., mobile vs. desktop), and builds a manifest:</p>
<pre><code class="language-bash">npx scrollsequence vid2ssq
</code></pre>
<h3>2. Implement on the Page</h3>
<p>If writing custom JavaScript from scratch, you preload frames into an array, place a <code>&lt;canvas&gt;</code> in a sticky container, and draw on scroll:</p>
<pre><code class="language-js">const frameIndex = Math.round(progress * (frames.length - 1));
if (frameIndex !== currentFrameIndex) {
  currentFrameIndex = frameIndex;
  requestAnimationFrame(() =&gt; drawFrame(currentFrameIndex));
}
</code></pre>
<p>Or using Scrollsequence Canvas2D (vanilla JS), which handles responsive tiering, sticky pinning, and canvas rendering automatically:</p>
<pre><code class="language-html">&lt;div
  class="scrollsequence-canvas2d"
  data-ssq-media-groups='[{"type":"manifest","rootDir":"/sequence/","imageSizes":{"1920":{"width":1920,"height":1080,"files":{"i":"1920/%04d.jpg"},"frameCount":150}}}]'
&gt;
  &lt;canvas&gt;&lt;/canvas&gt;
&lt;/div&gt;
&lt;script type="module"&gt;
  import ScrollsequenceManager from 'scrollsequence';
  import 'scrollsequence/scrollsequence.css';
  ScrollsequenceManager.initialize().preloadAll();
&lt;/script&gt;
</code></pre>
<h3>Why This Feels Smoother</h3>
<ul>
<li><strong>Instant random-access:</strong> Every frame is a discrete, seekable image — no keyframe guessing or decoder lag.</li>
<li><strong>Bidirectional symmetry:</strong> Scrolling backward is just as instantaneous and fluid as scrolling forward.</li>
<li><strong>Responsive asset control:</strong> Serve a desktop set vs. a lightweight mobile set tailored to device capabilities.</li>
</ul>
<h2>Frame Budget (Where Most Demos Fail)</h2>
<p>Lag in image sequence demos is almost never because "GSAP is slow." It is almost always caused by serving too many, overly large frames.</p>
<h3>Recommended Starting Budget</h3>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Desktop</th>
<th>Mobile / Phone</th>
</tr>
</thead>
<tbody><tr>
<td>Frame count</td>
<td>~100–150</td>
<td>~60–80 (or every 2nd desktop frame)</td>
</tr>
<tr>
<td>Width</td>
<td>~1400–1920px</td>
<td>~960px</td>
</tr>
<tr>
<td>Format</td>
<td>jpg (or AVIF) ~q80</td>
<td>jpg</td>
</tr>
<tr>
<td>Draw logic</td>
<td>Only when frame index changes</td>
<td>Same, and cap canvas DPR at 2</td>
</tr>
</tbody></table>
<h3>Performance Wins (The Scrollsequence Strategy)</h3>
<ul>
<li><strong>Probability-based preloading (Gauss curve):</strong> Don't load frames purely sequentially from 0 to 150. Scrollsequence prioritizes images closest to the user's current scroll position using a Gaussian probability distribution. Frames near the active view load first so scrubbing is instantly responsive, expanding outward as bandwidth allows.</li>
<li><strong>Multi-size responsive tiers:</strong> Generate separate image sizes (e.g., 960px, 1280px, 1920px) matched to screen and container width. Serving an 800KB mobile batch instead of a 20MB 4K sequence prevents crashes and data waste.</li>
<li><strong>Avoid DOM bloat:</strong> Never inject hundreds of <code>&lt;img&gt;</code> tags into the DOM — draw to a single <code>&lt;canvas&gt;</code>.</li>
<li><strong>Watch iOS memory:</strong> Decoded memory is roughly width × height × 4 bytes × frame count. Decoding 150 uncompressed Full HD frames can consume over 1 GB of RAM and crash the mobile Safari tab.</li>
</ul>
<h2>Elementor-Specific Notes</h2>
<ul>
<li><strong>Pin / sticky height:</strong> The height of your container or spacer must match the scroll distance you want (e.g., 300vh). A parent container with an unexpected fixed height or broken overflow is the most common reason the effect appears to do nothing.</li>
<li><strong>One hero sequence per page at first:</strong> Multiple heavy sequences require separate preload arrays and a <code>ScrollTrigger.refresh()</code> after images finish loading so trigger start/end positions recalculate accurately.</li>
<li><strong>Background video widgets:</strong> Elementor's native background video widgets are designed for looping ambient background video — they are not built for interactive scroll scrubbing.</li>
</ul>
<h2>What About CSS Scroll-Driven Animations?</h2>
<p>As of Firefox 159, CSS scroll-driven animations (<code>animation-timeline: scroll()</code>) are effectively Baseline for fades, parallax, and progress bars.</p>
<p>However, they do not replace a 100+ frame product sequence. A giant image sprite sheet using <code>steps()</code> works for short micro-animations (10–20 frames), but quickly becomes impractically large for cinematic sequences. For Apple-style scrubbing, <code>&lt;canvas&gt;</code> with image frames remains the industry standard.</p>
<h2>Minimal Checklist</h2>
<ul>
<li><input disabled type="checkbox" /> If scrubbing MP4: re-encode with frequent keyframes (<code>-g 2</code>), mute audio (<code>-an</code>), and keep the timeline short.</li>
<li><input disabled type="checkbox" /> If you need frame-perfect scrub: export JPEG frames, render to <code>&lt;canvas&gt;</code>, and map scroll progress → frameIndex.</li>
<li><input disabled type="checkbox" /> Cap frame budgets: keep total frames under control and serve a lighter set to mobile devices.</li>
<li><input disabled type="checkbox" /> Render selectively: only invoke <code>canvas.drawImage()</code> when the calculated frame index actually changes.</li>
<li><input disabled type="checkbox" /> Test on real devices: test on a real iPhone or Android and a mid-range Android phone, not just desktop Chrome DevTools.</li>
</ul>
<h2>Optional: WordPress Without Pasting the Whole Stack</h2>
<p>If you are on WordPress / Elementor and would rather not hand-wire FFmpeg, canvas rendering loops, and responsive loaders every time, Scrollsequence is the plugin built specifically for this workflow.</p>
<p>You can upload your frames (or directly convert video in v2) and get an editable, responsive section or block. The checklist above still applies either way — the mechanics of image frames and mobile memory limits remain identical.</p>
]]></content:encoded></item></channel></rss>