<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nikhil Kumar Swain</title>
    <description>The latest articles on DEV Community by Nikhil Kumar Swain (@nikhilswain).</description>
    <link>https://dev.to/nikhilswain</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F254651%2F5b25f613-69e1-45d3-bc15-2c490ffe42af.jpg</url>
      <title>DEV Community: Nikhil Kumar Swain</title>
      <link>https://dev.to/nikhilswain</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nikhilswain"/>
    <language>en</language>
    <item>
      <title>Why I drew my own icons for a doodle app</title>
      <dc:creator>Nikhil Kumar Swain</dc:creator>
      <pubDate>Sat, 24 Oct 2026 14:20:19 +0000</pubDate>
      <link>https://dev.to/nikhilswain/why-i-drew-my-own-icons-for-a-doodle-app-3fdp</link>
      <guid>https://dev.to/nikhilswain/why-i-drew-my-own-icons-for-a-doodle-app-3fdp</guid>
      <description>&lt;p&gt;I built Zketch for doodling, and doodles don't follow straight lines. Yet every button around its canvas was drawn with a ruler: dead-straight edges, compass-perfect arcs, corners that meet at exact angles. Replacing them took me from a pile of uneven sketches to 143 brush-drawn icons whose small imperfections are the whole point, and then to zicons, a set anyone can use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F486pcxok11a85mqymdmz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F486pcxok11a85mqymdmz.webp" alt="A sketchbook page scattered with dozens of brush-drawn zicons icons, a few in coral red, at slightly tilted angles on a dotted paper background" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. A handful of the 143 zicons icons: one pen, one line, a little wobble.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  A doodle app with ruler-straight buttons
&lt;/h2&gt;

&lt;p&gt;Zketch is a drawing app that runs in the browser. You open it, pick a brush and scribble. For a long time, the buttons around that scribbling came from Lucide, a popular open-source icon set, plus a few extra SVGs I added in the same style so they'd match. The undo arrow, for example, used the path from Feather, the set Lucide grew out of: a perfect arc with a radius of exactly 9, ending in an arrowhead bent at a right angle. Everything matched, and that was the problem.&lt;/p&gt;

&lt;p&gt;There's nothing wrong with Lucide or Feather. They're made for precise, neutral interfaces, and they're very good at that. But Zketch isn't a neutral interface. It exists for loose, wobbly lines. Everything on the canvas said "draw freely," and everything around it said "measure twice." The icons didn't carry the feel of the reason I was making the app. Put side by side with what replaced them, the difference is small but it's everywhere:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnw6uof2norielsls6kfd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnw6uof2norielsls6kfd.webp" alt="Two dark toolbars with the same tools (select, pencil, eraser, shapes, hand, four shapes and fit): the top row drawn with Lucide, the bottom row with zicons" width="800" height="209"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Zketch's toolbar tools in Lucide (top) and zicons (bottom), shown larger than in the app: the same tools, with softer corners and lines that aren't quite straight.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Drawing them by hand, and why it didn't work
&lt;/h2&gt;

&lt;p&gt;The obvious fix was to draw new ones myself, and I had the right tool for it. I sketched icons in Zketch, with its own brushes.&lt;/p&gt;

&lt;p&gt;They had plenty of character, and not one came out the same size as the next. Charming on paper. Less charming as a button you're trying to click.&lt;/p&gt;

&lt;p&gt;An icon set has a job that a sketch doesn't. Every icon has to sit on the same grid, carry the same weight, and stay readable at 14 pixels as well as at 56. Drawn freehand, each icon was a one-off, and a toolbar of one-offs looks messy rather than playful. None of those files are left, but the lesson stayed: I wanted my pen's feel without my hand's inconsistency.&lt;/p&gt;
&lt;h2&gt;
  
  
  Imperfect on purpose
&lt;/h2&gt;

&lt;p&gt;Things drawn by hand aren't perfect, and that's exactly why they feel human. A line drawn without a ruler drifts a little. A circle never quite closes the way a compass would. Corners come out soft because a pen can't make a sharp one. Those were the qualities I wanted, so the question became: how do you keep the imperfections and drop the mess?&lt;/p&gt;

&lt;p&gt;My answer was to decide in advance exactly where the imperfection is allowed to live, and to make everything else strict. The strict part became a short set of rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every icon is drawn on a 24 by 24 grid and stays roughly inside the middle 18 by 18.&lt;/li&gt;
&lt;li&gt;One even line, 1.6 units wide, with round ends and round joins. No fills.&lt;/li&gt;
&lt;li&gt;Rounded corners everywhere. Nothing ends in a sharp point.&lt;/li&gt;
&lt;li&gt;Few strokes, and each stroke is a single pen movement. Most icons need two or three.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The imperfect part is small. Straight edges bow very slightly, and circles are a little out of round. That's all. But a bow that small, repeated across a whole toolbar, is enough to make the set feel drawn rather than built.&lt;/p&gt;
&lt;h2&gt;
  
  
  Teaching code to hold the pen
&lt;/h2&gt;

&lt;p&gt;I already had a drawing pipeline in Zketch, and I needed the feel of my brush on these icons. So I split the work in two. I describe each icon as a few pen movements, and code does the actual drawing, the same way every time.&lt;/p&gt;

&lt;p&gt;The description is a centerline: the path the middle of the pen travels, before any line width is added. For the pencil icon it's two strokes, a body with five rounded corners and the little line underneath:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/icons/defs/core.mjs&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pencil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;strokes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;14.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;18.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;9.3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;19.2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;19.1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;15.4&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;M3.5 21.5c2.5-1.4 4.4-1.6 6-2.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first list is where the corners go, and the second is how round each corner is. From there, a small generator takes over. It rounds the corners, bends the straight edges, walks along the result dropping a point roughly every third of a unit, and then thins those points down to the few that actually shape the line. Zketch uses the same trick to keep drawn strokes small. What comes out is one SVG path per icon, whatever the icon looks like. You can follow the pencil through it, left to right:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2crzp7tvhism5yuhqjt1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2crzp7tvhism5yuhqjt1.webp" alt="Four panels of the pencil icon on a 24 by 24 grid: straight lines between five corner dots, then a dotted rounded outline, then a sparse set of dots, then the finished thick line" width="800" height="239"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. One icon through the generator: 5 corners and a curve become 135 sampled points, then 24, then a single 1.6-wide SVG path.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  A wobble that never changes
&lt;/h3&gt;

&lt;p&gt;The bow on each straight edge is random, and that's a problem. Randomness in an icon set means a different icon on every build: the undo arrow you shipped yesterday quietly changes shape today.&lt;/p&gt;

&lt;p&gt;So the randomness is borrowed from the icon's name. The generator turns the name into a number with a tiny hash function and uses that number as the seed for its random choices:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/icons/lib/catalog.mjs&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sampleIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toPathData&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every icon wobbles in its own way, and each one wobbles the same way forever. Here's the same square drawn with three different names. The inner red dashes are the straight square it started from (the outer ones mark the area every drawing stays inside), and the black line is where the pen went:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc58byu444cngygls8pbw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc58byu444cngygls8pbw.webp" alt="The same rounded square drawn three times with seeds square, square-2 and square-3; each black centerline drifts slightly off the straight red dashed reference in a different place" width="800" height="291"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4. One square, three seeds: the bow changes with the name, never between builds.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt;&lt;br&gt;
Seed the randomness from the icon's name, and a wobbly line becomes as reproducible as a straight one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Goodbye, Lucide
&lt;/h2&gt;

&lt;p&gt;With the generator doing the drawing, my job changed. I was no longer fighting my own hand to make two icons the same size. I was deciding what each icon should say in two or three pen movements, and the code made sure every one of them said it in the same voice.&lt;/p&gt;

&lt;p&gt;I worked outward from the canvas: first the tools Zketch uses most, then the rest of the app, then everyday icons and art tools for whatever comes next. Each new icon went onto a contact sheet beside all the others, at several sizes, because an icon that looks lovely at 56 pixels can turn to mush at 14. On one sheet, an icon that doesn't belong is easy to spot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0yzvnoiu04ebvudapgdz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0yzvnoiu04ebvudapgdz.webp" alt="A dark contact sheet of twenty icons, including pencil, eraser, brush, layers, undo and calendar, each shown at four sizes from tiny to large" width="800" height="483"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 5. Twenty of the 143 icons at 14, 18, 24 and 56 px, all drawn with the same line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once the sheet looked like one set, I swapped them in. Every screen in Zketch moved to the brush-drawn icons, and Lucide came out of the project. The buttons around the canvas finally looked like they belonged to the drawing inside it. Zketch's project notes now carry a one-line rule: no Lucide.&lt;/p&gt;
&lt;h2&gt;
  
  
  Letting the lines breathe
&lt;/h2&gt;

&lt;p&gt;The bow made the icons look drawn. I also wanted them to feel alive.&lt;/p&gt;

&lt;p&gt;In old hand-drawn cartoons, every frame was traced again by hand, so the lines shimmer slightly even when nothing moves. Animators call it line boil. zicons fakes it with two SVG filters: one makes a field of noise, and the other nudges each part of the icon's line by the amount the noise says. The noise cycles through three patterns in a 0.36-second loop, so a new one arrives every 0.12 seconds:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/icons/runtime/render.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SEEDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BOIL_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.36&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's about eight new frames a second, which reads as a traced cartoon rather than smooth motion. The drawing itself never changes; only the noise over it does. Here are the three frames, with the real line in red on top:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlr6qsupu0s2fkxlx9f4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlr6qsupu0s2fkxlx9f4.webp" alt="The pencil icon three times, each thick line slightly ragged in different places around a thin red line that stays identical" width="799" height="303"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 6. Three boil frames, 0.12 s apart: the red path is the same in each, only the filter's noise moves.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The hard part was size. A shimmer that looks lively on a big icon makes a small one unreadable, so icons under 32 pixels get a gentler boil than larger ones, and within each group the wobble is scaled so it looks the same on screen at any size. You can have it on all the time, or only when someone hovers the icon or the button it sits in.&lt;/p&gt;

&lt;p&gt;In your own project, the boil is off until you ask for it, one icon at a time, so whether it runs, and whether it follows reduced-motion settings, is your call. On the zicons site it's on for everyone, because the boil is how these icons show what they can do. If it's too much, a switch at the top of the page stops it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Zketch's toolbar to everyone's
&lt;/h2&gt;

&lt;p&gt;For a while the icons lived only inside Zketch. But I kept thinking about other people who want what I wanted: something fun, a little unusual, with a taste of its own instead of the same neat icons as every other app. So I gave the drawings their own project and called it zicons.&lt;/p&gt;

&lt;p&gt;zicons is now where new icons get drawn. One file holds every icon with its name, group, search words and path, and everything else is generated from it: the website, plain SVG files, a sprite, and a component package for each of the big frameworks. Importing one icon adds about 3 KB to a bundle, not all 143.&lt;/p&gt;

&lt;h2&gt;
  
  
  The future of zicons
&lt;/h2&gt;

&lt;p&gt;The 143 icons cover a drawing app and the everyday basics. That's a start, not a finish. I don't have a target number in mind. What I want is a place where all your icon needs get covered, so you never have to mix in a ruler-straight icon from somewhere else because zicons didn't have one.&lt;/p&gt;

&lt;p&gt;Here's what's planned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Figma file&lt;/strong&gt;, so designers can use the icons where they design, not only where they build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributions and icon requests.&lt;/strong&gt; Right now every icon is coordinates I type into a file. I'd like a way for you to ask for the icon you're missing, and eventually to draw one yourself that follows the same rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More icons&lt;/strong&gt;, steadily, wherever people keep finding gaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A playground&lt;/strong&gt; with small mock interfaces (a toolbar, a sidebar, a card) so you can see the set working together before you use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A note before you go
&lt;/h2&gt;

&lt;p&gt;zicons works with React, Vue, Svelte and plain HTML. Everything you need to browse, restyle, copy or install the icons is on &lt;a href="https://zicons.zerro.dev" rel="noopener noreferrer"&gt;zicons.zerro.dev&lt;/a&gt;, and the drawings live on &lt;a href="https://github.com/nikhilswain/zicons" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you'd rather draw with them than read about them, they're in &lt;a href="https://zketch.zerro.dev" rel="noopener noreferrer"&gt;Zketch&lt;/a&gt;'s toolbar.&lt;/p&gt;

&lt;p&gt;Icons are the smallest things on a screen, so it's easy to grab them off the shelf without a second thought. But they're on every page, and they tell people what kind of place they've walked into before a single word does. Zketch is a place for loose lines, and now its buttons look like someone held a pen. Whatever you're making, it's allowed to have a hand in it too. A little wobble never hurt anyone.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zerro.dev/blog/brush-drawn-icons-doodle-app/" rel="noopener noreferrer"&gt;zerro.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>svg</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>animation</category>
    </item>
    <item>
      <title>Scroll-scrubbed video that stays smooth with any video file</title>
      <dc:creator>Nikhil Kumar Swain</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:01:53 +0000</pubDate>
      <link>https://dev.to/nikhilswain/scroll-scrubbed-video-that-stays-smooth-with-any-video-file-5d8k</link>
      <guid>https://dev.to/nikhilswain/scroll-scrubbed-video-that-stays-smooth-with-any-video-file-5d8k</guid>
      <description>&lt;p&gt;A video that moves with the page is one of the nicer effects on the web. A product turns as you scroll, or a scene plays out at the reader's pace. It looks like a few lines of JavaScript, and with the right file it is. With a video exported straight from an editor, the same few lines stutter, and scrolling backwards is worse. This article explains where that stutter comes from, what the usual fixes cost, and the approach I ended up building into a small library, &lt;a href="https://vidscroll.js.org/" rel="noopener noreferrer"&gt;vidscroll&lt;/a&gt;, so that any video file scrubs smoothly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcz0zvfxg90f8w9ubhoc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcz0zvfxg90f8w9ubhoc.webp" alt="An animated scene of passengers in a train carriage, with a caption over the video and a list of six scenes on the right" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. A scroll-scrubbed video: it moves only as fast as the page scrolls. Animation by roenais.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The obvious approach: set &lt;code&gt;currentTime&lt;/code&gt; on scroll
&lt;/h2&gt;

&lt;p&gt;My first version was the obvious one. Pin a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, put a tall spacer behind it, and on each animation frame map the scroll position to a time in the video. Simplified, it comes down to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onScroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;spacer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onScroll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a video prepared for scrubbing, this keeps up with the scroll. With a typical export, the same code stutters.&lt;/p&gt;

&lt;p&gt;Nothing in the handler changes between those two cases. The only difference is the file, so that's where the problem is. Rewriting the scroll logic won't fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why exported videos stutter when you scrub them
&lt;/h2&gt;

&lt;p&gt;A video file doesn't store every frame as a picture. Every so often it stores a complete one, a keyframe, and the frames in between only store what changed. To show any frame, the browser starts at the keyframe before it and decodes forward.&lt;/p&gt;

&lt;p&gt;Normal playback never notices, because it only moves forward. Scrubbing jumps to a new point many times a second, in both directions, and every jump pays for the walk from the previous keyframe.&lt;/p&gt;

&lt;p&gt;Most exported videos have keyframes a few seconds apart, because that keeps files small. A 60-second 1080p export I tested had 8 keyframes in total, one every 8.3 seconds. On the demo clip I measured, the median seek took 287 ms. That's under four new frames per second, however fast the rest of the page runs.&lt;/p&gt;

&lt;p&gt;For scrubbing, keyframe spacing matters more than resolution, bitrate or codec. Lowering the resolution of a file with sparse keyframes rarely fixes the stutter.&lt;/p&gt;

&lt;p&gt;You can check a file with &lt;code&gt;ffprobe&lt;/code&gt;, which lists the time of every keyframe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="nt"&gt;-select_streams&lt;/span&gt; v:0 &lt;span class="nt"&gt;-skip_frame&lt;/span&gt; nokey &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pts_time &lt;span class="nt"&gt;-of&lt;/span&gt; &lt;span class="nv"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 hero.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gaps of up to about half a second scrub well. Anything much longer and every seek gets expensive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbsjm6d029v1fq7y4c9m7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbsjm6d029v1fq7y4c9m7.webp" alt="Two timelines of the same clip: keyframes every few seconds on top, every third of a second below" width="800" height="183"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. The same clip as exported (top) and prepared for scrubbing (bottom).&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Two common fixes, and what they cost
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Prepare every file
&lt;/h3&gt;

&lt;p&gt;Re-encode the video with keyframes close together, for example one every 10 frames. Scrubbing becomes smooth, because any frame is at most about 10 frames from a keyframe. With ffmpeg, for a landscape video, the flags that matter are these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; hero.mp4 &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="nv"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-2&lt;/span&gt;:720 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; slow &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; 10 &lt;span class="nt"&gt;-keyint_min&lt;/span&gt; 10 &lt;span class="nt"&gt;-sc_threshold&lt;/span&gt; 0 &lt;span class="nt"&gt;-crf&lt;/span&gt; 22 &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart hero.scroll.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-g 10 -keyint_min 10&lt;/code&gt; puts a keyframe every 10 frames, and &lt;code&gt;-sc_threshold 0&lt;/code&gt; stops the encoder adding extra ones at scene cuts.&lt;/p&gt;

&lt;p&gt;The cost is that someone has to do it for every video, every time the video changes, and the files get bigger:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The same 60 s clip&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Keyframes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original export, 1080p&lt;/td&gt;
&lt;td&gt;4.7 MB&lt;/td&gt;
&lt;td&gt;8, one every 8.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prepared, 720p&lt;/td&gt;
&lt;td&gt;11.9 MB&lt;/td&gt;
&lt;td&gt;180, one every 0.33 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prepared, 1080p&lt;/td&gt;
&lt;td&gt;22.4 MB&lt;/td&gt;
&lt;td&gt;180, one every 0.33 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Decode every frame up front
&lt;/h3&gt;

&lt;p&gt;Decode all frames into images ahead of time and draw them to a canvas. Scrubbing is then instant, whatever the file.&lt;/p&gt;

&lt;p&gt;The cost is memory. A decoded 1080p frame is about 8 MB, so 10 seconds at 30 fps comes to about 2.5 GB. That works for a few seconds of small video, and not for a minute of HD on a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A third route: fix the file only when it needs fixing
&lt;/h2&gt;

&lt;p&gt;The third route is to take whatever file you're given, keep it compressed, and fix it only when its keyframes are too far apart. That takes four pieces, and vidscroll is built from them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read the keyframe layout without decoding
&lt;/h3&gt;

&lt;p&gt;An MP4 already says where its keyframes are. The sample table in the file's index lists which frames are keyframes (the &lt;code&gt;stss&lt;/code&gt; box) and how long each frame lasts (&lt;code&gt;stts&lt;/code&gt;), and fragmented MP4s carry the same information in their &lt;code&gt;moof&lt;/code&gt;/&lt;code&gt;trun&lt;/code&gt; boxes. Reading those boxes takes a small parser and no decoding at all; in vidscroll it's &lt;code&gt;src/core/probe.ts&lt;/code&gt;. Other containers, such as WebM, need a demuxer instead.&lt;/p&gt;

&lt;p&gt;From that list, the largest gap between keyframes tells you whether the file is fine as it is. vidscroll uses 0.5 seconds as the limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Re-encode in the browser with WebCodecs
&lt;/h3&gt;

&lt;p&gt;If the gaps are too long, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API" rel="noopener noreferrer"&gt;WebCodecs API&lt;/a&gt; lets the page re-encode the video with the browser's own encoder. vidscroll uses &lt;a href="https://mediabunny.dev/" rel="noopener noreferrer"&gt;Mediabunny&lt;/a&gt; on top of it, loaded only when a re-encode is needed, so pages with prepared files never download it.&lt;/p&gt;

&lt;p&gt;The copy has a keyframe every quarter of a second, a short side of at most 720 px, at most 30 frames per second, and no audio. In my tests, a 60-second 1080p clip took about 8 to 10 seconds to re-encode on a desktop computer and about 20 seconds on a phone. Shorter clips are proportionally faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache the result
&lt;/h3&gt;

&lt;p&gt;Re-encoding on every visit would waste those seconds each time. The copy goes into the browser's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage" rel="noopener noreferrer"&gt;Cache Storage&lt;/a&gt;, so later visits load it straight away, with no download and no re-encode.&lt;/p&gt;

&lt;p&gt;The cache key has to change whenever the result would. vidscroll ties each entry to the video's URL, the re-encode settings, and the file's &lt;code&gt;ETag&lt;/code&gt;, &lt;code&gt;Last-Modified&lt;/code&gt; or &lt;code&gt;Content-Length&lt;/code&gt;, so replacing the file on the server starts over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let people scroll while it works
&lt;/h3&gt;

&lt;p&gt;Waiting behind a spinner would leave the page blank for the whole re-encode. Instead, the downloaded original goes on screen straight away.&lt;/p&gt;

&lt;p&gt;Scrolling forward plays it, with &lt;code&gt;playbackRate&lt;/code&gt; following the scroll speed. Playing forward never goes back to a keyframe, so it stays smooth even on a badly encoded file. Scrolling backwards still has to seek, so it's choppy for those few seconds.&lt;/p&gt;

&lt;p&gt;When the copy is ready, a second &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; stacked underneath seeks to the current time and then takes over. It lands on exactly the frame on screen, so the switch isn't visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it on a page
&lt;/h2&gt;

&lt;p&gt;In React, a &lt;code&gt;&amp;lt;ScrollVideo&amp;gt;&lt;/code&gt; is a block in your page, and each &lt;code&gt;&amp;lt;Section&amp;gt;&lt;/code&gt; is active over part of the scroll:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ScrollVideo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Section&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vidscroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScrollVideo&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/videos/hero.mp4"&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"400vh"&lt;/span&gt; &lt;span class="na"&gt;smoothScroll&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Section&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.33&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Intro&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Section&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.33&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.66&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Middle&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Section&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.66&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Outro&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ScrollVideo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;src&lt;/code&gt; is any URL to a video file: your public folder, a CDN, or a bundler import. Without React, the experimental &lt;a href="https://vidscroll.js.org/docs/without-react/element/" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;vid-scroll&amp;gt;&lt;/code&gt; element&lt;/a&gt; does the same on any page.&lt;/p&gt;

&lt;p&gt;For production, the package's CLI runs that same kind of ffmpeg command for you, so visitors never wait for a re-encode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vidscroll encode public/videos/hero.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I built it for landing pages, homepages and storytelling sites, the kind of scrollytelling where text is tied to scenes and the reader sets the pace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it doesn't fit yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long videos, especially at 1080p.&lt;/strong&gt; Anything over about two minutes or 100 MB gets slow to download and to re-encode, and vidscroll warns in the console. Trim the video, or prepare it with the CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser's copy is bigger than the CLI's.&lt;/strong&gt; For the 60-second clip, the in-browser re-encode made 18.6 MB where the CLI makes 11.9 MB at the same resolution. It only lives in the visitor's browser, but it's one more reason to prepare videos for production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first visit to an unprepared video.&lt;/strong&gt; Scrolling backwards is choppy until the smooth copy arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Videos with sound.&lt;/strong&gt; Scroll-driven video is always muted, and the re-encode drops the audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching needs the right headers.&lt;/strong&gt; Without &lt;code&gt;ETag&lt;/code&gt;, &lt;code&gt;Last-Modified&lt;/code&gt; or &lt;code&gt;Content-Length&lt;/code&gt;, there's no way to tell whether the file changed, so the re-encode runs on every visit. Caching also needs HTTPS or &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before you build your next scroll video
&lt;/h2&gt;

&lt;p&gt;Whatever you build it with, three things decide whether it feels smooth:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the keyframes before the code.&lt;/strong&gt; If scrubbing stutters, look at the file first. Gaps over half a second will hurt, however good the scroll handler is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepare the files you control.&lt;/strong&gt; One ffmpeg command with closely spaced keyframes beats fixing it at runtime: a smaller download and smooth scrubbing from the first visit. Use 720p unless the video fills a large screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the files you don't, fix them once and remember it.&lt;/strong&gt; Read the container instead of decoding it, re-encode only when the gaps are too long, cache the result, and keep the page scrollable while that runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole idea behind vidscroll, and you can try it without installing anything. Drop one of your own videos into the &lt;a href="https://vidscroll.js.org/docs/videos/checker/" rel="noopener noreferrer"&gt;video checker&lt;/a&gt;: it shows every keyframe, times seeks on your device and gives you the command to fix the file. The video is read in the page and never uploaded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgoddc61t7fzi6ddbdk3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgoddc61t7fzi6ddbdk3.webp" alt="The vidscroll video checker reporting 8 keyframes in a 60-second 1080p file, a median seek of 272 ms, and the command to prepare it" width="800" height="992"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. The checker on the 60-second export from earlier: keyframes 8.3 s apart and seeks too slow to scrub smoothly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the result looks like the one above, you've found your stutter, and fixing it is one command. To see the rest, there are &lt;a href="https://vidscroll.js.org/#demos" rel="noopener noreferrer"&gt;demos&lt;/a&gt;, a &lt;a href="https://vidscroll.js.org/playground/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; for the options, and a longer explanation of &lt;a href="https://vidscroll.js.org/docs/start/how-it-works/" rel="noopener noreferrer"&gt;how it works&lt;/a&gt;. The code is on &lt;a href="https://github.com/nikhilswain/vidscroll" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and the package is on &lt;a href="https://www.npmjs.com/package/vidscroll" rel="noopener noreferrer"&gt;npm&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i vidscroll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scroll-driven video is one of the few effects on the web that hands the pace of a story to the reader. When it stutters, the whole effect falls apart, and it's easy to blame the scroll code, the framework or the device. Often the cause is sitting in the file: keyframes a few seconds apart. Once you know that, it stops being a mystery and becomes a fixable problem, whether you fix it with one ffmpeg command, build the runtime fix yourself, or let a library do it.&lt;/p&gt;

&lt;p&gt;If you try it on a video that doesn't behave, &lt;a href="https://github.com/nikhilswain/vidscroll/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; with the checker's report. Odd files make the best test cases.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zerro.dev/blog/smooth-scroll-scrubbed-video-any-file/" rel="noopener noreferrer"&gt;zerro.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>react</category>
      <category>animation</category>
    </item>
    <item>
      <title>5 Famous Array Methods in JavaScript.</title>
      <dc:creator>Nikhil Kumar Swain</dc:creator>
      <pubDate>Mon, 16 Sep 2019 15:03:03 +0000</pubDate>
      <link>https://dev.to/nikhilswain/5-famous-array-methods-in-javascript-1i6a</link>
      <guid>https://dev.to/nikhilswain/5-famous-array-methods-in-javascript-1i6a</guid>
      <description>&lt;h4&gt;
  
  
  map() , reduce() , filter() , some() , every() .
&lt;/h4&gt;

&lt;p&gt;JavaScript provide us lots of useful methods to ease our work.In this article we will take a closer look at some JavaScript famous &lt;strong&gt;Array methods&lt;/strong&gt; that will help us to code efficiently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcdqrc0pl0vyi0jyc0nk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcdqrc0pl0vyi0jyc0nk5.png" width="627" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Array.prototype.map()&lt;/strong&gt;
The map() method creates a &lt;strong&gt;new array&lt;/strong&gt; with the results of calling a function for every array element.It accepts a callback function as a parameter and we have access to a ccurrent value of an array, current index and array itself as well.
&lt;strong&gt;Syntax :&lt;/strong&gt; &lt;em&gt;array&lt;/em&gt;.map(&lt;em&gt;function(currentValue, index, arr), thisValue&lt;/em&gt;)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const array = [2,4,6,8];

const newArr = array.map(val =&amp;gt; val*2);

// [4, 8, 12, 16]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could also pass a callback function as a parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const callbackFunc = val =&amp;gt; val + 2;

const array = [2,4,6,8];

cons newArr = array.map(sum)

// [4, 6, 8, 10]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaining is also available in so we can do something like this :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const sum = val =&amp;gt; val + 2;

const mul = val =&amp;gt; val * 2;

const array = [1, 2, 3, 4, 5];

const newArray = array.map(sum).map(mul);

// [6, 8, 10, 12, 14]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Array.prototype.reduce()&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The reduce() method executes a &lt;strong&gt;reducer&lt;/strong&gt; function (that you provide) on each element of the array, resulting in a single output value.&lt;br&gt;&lt;br&gt;
The reducer function takes four arguments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accumulator&lt;/li&gt;
&lt;li&gt;Current Value&lt;/li&gt;
&lt;li&gt;Current Index&lt;/li&gt;
&lt;li&gt;Source Array&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your reducer function’s returned value is assigned to the accumulator, whose value is remembered across each iteration throughout the array and ultimately becomes the final, single resulting value.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Syntax :&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;array&lt;/em&gt;.reduce(&lt;em&gt;function(total, currentValue, currentIndex, arr), initialValue&lt;/em&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function myFunc( total, num ) {
return total + num ;
}

const numbers = [30, 20, 10];

numbers.reduce(myFunc)
// 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also provide an initial value instead of taking the first value as an accumulator by default :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const num = [5, 10, 15, 10];

const sum = num.reduce(( acc, val ) =&amp;gt; acc + val, 100);

// 140
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Array.prototype.filter()&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The filter() method creates a new array with all elements that pass the test implemented by the provided function.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Syntax :&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
const newArray = arr.filter(callback(element[, index[, array]])[, thisArg])&lt;/p&gt;

&lt;p&gt;callback : Function is a predicate, to test each element of the array. Return true to keep the element, false otherwise.&lt;/p&gt;

&lt;p&gt;It accepts three arguments:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;element&lt;/em&gt; : The current element being processed in the array.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;index :&lt;/em&gt; (Optional) The index of the current element being processed in the array.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;array&lt;/em&gt; : (Optional) The array filter was called upon.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;thisArg :&lt;/em&gt; (Optional) Value to use as this when executing callback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const band = ['Against', 'the', 'current', 'is', 'my', 'favourite', 'band'];


const newBand = band.filter( word =&amp;gt; word.length &amp;gt; 5 );

// ["Against", "current", "favourite"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s look at another example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const isGreater = (val) =&amp;gt; {
return val &amp;gt; 12;
}

const realNumbers = [3,6,12,15,18,21].filter(isGreater);

// [15, 18, 21]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Array.prototype.some()&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
As the name suggest the some() method tests whether at least &lt;strong&gt;one element&lt;/strong&gt; in the array passes the test implemented by the provided function. It returns a Boolean value .&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Syntax :&lt;/strong&gt; &lt;em&gt;array&lt;/em&gt;.some(&lt;em&gt;function(currentValue, index, arr), thisValue&lt;/em&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const names = [const names = ['sheldon', 'lenoard', 'raj', 'howard', 'penny'];

names.some(having =&amp;gt; having === 'sheldon');

// true

names.some(having =&amp;gt; having === 'chrissy');

// false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another example of some() method&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const number = [5,7,10,12,15,17,20];

number.some(mulOfFive =&amp;gt; mulOfFive % 5 === 0);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Array.prototype.every()&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Unlike some() the every() method tests whether all elements in the array pass the test implemented by the provided function. It also returns a Boolean value.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Syntax :&lt;/strong&gt; &lt;em&gt;array&lt;/em&gt;.every(&lt;em&gt;function(currentValue, index, arr), thisValue&lt;/em&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const band = ['ATC','ATC','ATC','ATC'];

band.every(having =&amp;gt; having === 'ATC');

// true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another example of some() method&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const marks = [80, 75, 52, 91, 63];

marks.every(mark =&amp;gt; mark &amp;gt; 50);

// true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>arraymethods</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
