<?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: Holomatar</title>
    <description>The latest articles on DEV Community by Holomatar (@holomatar).</description>
    <link>https://dev.to/holomatar</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%2F4100449%2F298a7eb9-3030-4084-9477-25133c1d9b3c.png</url>
      <title>DEV Community: Holomatar</title>
      <link>https://dev.to/holomatar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/holomatar"/>
    <language>en</language>
    <item>
      <title>I Built a Browser Gallery for Every MP4 Frame</title>
      <dc:creator>Holomatar</dc:creator>
      <pubDate>Sat, 29 Aug 2026 18:08:57 +0000</pubDate>
      <link>https://dev.to/holomatar/i-built-a-browser-gallery-for-every-mp4-frame-1nej</link>
      <guid>https://dev.to/holomatar/i-built-a-browser-gallery-for-every-mp4-frame-1nej</guid>
      <description>&lt;p&gt;I thought the feature was finished when FFmpeg.wasm could turn an MP4 into a ZIP of PNG files. The conversion worked, but the workflow did not: users still had to download and unpack everything before they could find one useful frame.&lt;/p&gt;

&lt;p&gt;So I moved the missing step into the browser. &lt;strong&gt;Video Frame Tool&lt;/strong&gt; now extracts every frame, renders the complete gallery, tracks a selection, and exports one PNG, a selected ZIP, or the entire sequence. The source MP4 stays on the device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://holometer.net/video-frame-tool/en/" rel="noopener noreferrer"&gt;https://holometer.net/video-frame-tool/en/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post covers the implementation choices that mattered most: numbered FFmpeg output, Blob URL lifecycles, selection state, memory costs, and browser-safe downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a ZIP-only workflow was not enough
&lt;/h2&gt;

&lt;p&gt;The first version converted an MP4 and returned a ZIP. Technically, that solved frame extraction. In practice, it moved the real work somewhere else: users still had to download the archive, extract it, open a file browser, change to thumbnail view, and search through hundreds of nearly identical images.&lt;/p&gt;

&lt;p&gt;The useful workflow is closer to a contact sheet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;choose a video;&lt;/li&gt;
&lt;li&gt;inspect every result in sequence;&lt;/li&gt;
&lt;li&gt;select only the moments that matter;&lt;/li&gt;
&lt;li&gt;download those frames without repeating the conversion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds like a small interface change, but it affects memory management, progress reporting, object URL cleanup, selection state, and download behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser-only extraction pipeline
&lt;/h2&gt;

&lt;p&gt;The conversion runs with FFmpeg compiled to WebAssembly. The selected MP4 is copied into FFmpeg's in-memory filesystem, then every decoded frame is written as a numbered PNG.&lt;/p&gt;

&lt;p&gt;The extraction step is conceptually equivalent to:&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; input.mp4 &lt;span class="nt"&gt;-vsync&lt;/span&gt; 0 frame_%08d.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight-digit filenames keep lexical and numeric order identical. That avoids the common &lt;code&gt;frame_1&lt;/code&gt;, &lt;code&gt;frame_10&lt;/code&gt;, &lt;code&gt;frame_2&lt;/code&gt; sorting problem when the files are displayed or passed back into an encoder.&lt;/p&gt;

&lt;p&gt;After FFmpeg finishes, the app reads the PNG files, sorts them by frame number, creates browser object URLs, and adds them to the gallery. The source video and extracted PNGs are not sent to a conversion server.&lt;/p&gt;

&lt;p&gt;The first run downloads roughly 32 MB of FFmpeg WebAssembly assets. That cost is shown before conversion because hiding a large first-load dependency makes a local tool feel broken on a slow connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real test: 36 full-resolution frames
&lt;/h2&gt;

&lt;p&gt;For a repeatable check, I used a 1.2-second, 1080×1920 MP4. The browser extracted and displayed 36 PNG frames. Together, those PNGs occupied 50.4 MB in memory even though the source MP4 was only about 140 KB.&lt;/p&gt;

&lt;p&gt;That difference is the central constraint of client-side frame extraction: compressed video is small, while decoded lossless frames are not.&lt;/p&gt;

&lt;p&gt;The gallery reports the frame count, dimensions, and total PNG size before the user downloads anything. In this test I selected three frames from different moments, then confirmed that the interface offered all three paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;download one frame as a PNG;&lt;/li&gt;
&lt;li&gt;download the selected three as one ZIP;&lt;/li&gt;
&lt;li&gt;download all 36 as one ZIP.&lt;/li&gt;
&lt;/ul&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%2Ftqt0ue16b0cyrmbbbnn7.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%2Ftqt0ue16b0cyrmbbbnn7.png" alt="Every extracted video frame displayed in a browser gallery with one-frame, selected-ZIP, and all-ZIP download controls" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping hundreds of frames responsive
&lt;/h2&gt;

&lt;p&gt;Every gallery thumbnail is backed by a Blob URL rather than a base64 string. Base64 expands the data and makes large DOM attributes expensive. Blob URLs let the browser keep the binary data outside the HTML while normal &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements render the previews.&lt;/p&gt;

&lt;p&gt;Those URLs still need a lifecycle. When a new video is chosen, the old gallery is cleared and its object URLs are revoked. Otherwise, repeatedly converting videos in one tab would leave the previous PNG data in memory.&lt;/p&gt;

&lt;p&gt;The selection model is deliberately simple: a &lt;code&gt;Set&lt;/code&gt; stores the selected frame names, and the buttons derive their labels and enabled state from that set. “Select all” adds the complete ordered list; “Clear selection” empties it. Building the ZIP happens only after the user chooses a download action, so merely browsing the gallery does not create a second full copy of every PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why individual browser downloads need special handling
&lt;/h2&gt;

&lt;p&gt;Triggering 300 separate downloads is hostile to both the user and the browser. It can produce hundreds of permission prompts or be blocked as abusive behavior.&lt;/p&gt;

&lt;p&gt;Video Frame Tool therefore uses three explicit operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;one PNG&lt;/strong&gt; for a single useful moment;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;selected ZIP&lt;/strong&gt; for a small reference set;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;all ZIP&lt;/strong&gt; for animation and batch-processing work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This also makes the tool useful outside illustration. A developer can pick state-transition frames for a bug report, an animator can compare spacing, and a pixel artist can export a short sequence for cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading a public X video without pretending it is private
&lt;/h2&gt;

&lt;p&gt;The local-file path is fully client-side. The X URL path has a necessary network boundary: the app sends the public post ID to the FxTwitter API to resolve the available media URL, then downloads the selected public MP4 from X's delivery host. Frame conversion happens locally after that download.&lt;/p&gt;

&lt;p&gt;Private, deleted, or age-restricted posts are not supported. A public URL also does not grant reuse rights, so the page tells users to process only videos they own or have permission to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning image sequences back into MP4 or GIF
&lt;/h2&gt;

&lt;p&gt;The reverse path accepts individual images or a ZIP. Filenames are naturally sorted so &lt;code&gt;frame_2.png&lt;/code&gt; stays before &lt;code&gt;frame_10.png&lt;/code&gt;. Images with different aspect ratios are not stretched; each frame is fitted inside the first image's canvas and padded with black or white.&lt;/p&gt;

&lt;p&gt;MP4 output uses H.264 with a broadly compatible pixel format. GIF output first builds a palette, then applies it during encoding to avoid the worst color banding. Users can choose common frame rates such as 8, 12, 15, 24, 30, or 60 fps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits that still matter
&lt;/h2&gt;

&lt;p&gt;This is not a replacement for a desktop video editor. Long 4K clips can exhaust browser memory, and PNG extraction is intentionally lossless and therefore large. The safest workflow is to begin with a short clip, confirm the result, and only then increase duration or resolution.&lt;/p&gt;

&lt;p&gt;The tool also does not repair motion blur, camera shake, compression artifacts, or hidden body parts. It exposes the frames that already exist; it does not invent missing visual information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Video Frame Tool is free, requires no account, and works in a modern desktop browser:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://holometer.net/video-frame-tool/en/" rel="noopener noreferrer"&gt;https://holometer.net/video-frame-tool/en/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you try it, I am especially interested in the point where the workflow becomes uncomfortable: extraction time, gallery size, selection, or ZIP creation. Those limits are more useful than a successful five-frame demo when deciding what to optimize next.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ffmpeg</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>How I Turned AI-Generated “Pixel Art” into a Real Pixel Grid in the Browser</title>
      <dc:creator>Holomatar</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:56:32 +0000</pubDate>
      <link>https://dev.to/holomatar/how-i-turned-ai-generated-pixel-art-into-a-real-pixel-grid-in-the-browser-4e6f</link>
      <guid>https://dev.to/holomatar/how-i-turned-ai-generated-pixel-art-into-a-real-pixel-grid-in-the-browser-4e6f</guid>
      <description>&lt;p&gt;AI image generators can produce artwork that &lt;em&gt;looks&lt;/em&gt; like pixel art from a distance, while its cell sizes, outlines, and colors fall apart under zoom. This post explains how I built &lt;strong&gt;Pixel Maker&lt;/strong&gt; , a browser-only converter that detects a logical grid, samples one color per cell, reduces palettes in Oklab, preserves small accents, and exports clean nearest-neighbor PNGs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try the tool:&lt;/strong&gt; &lt;a href="https://holometer.net/pixel-maker/en/" rel="noopener noreferrer"&gt;https://holometer.net/pixel-maker/en/&lt;/a&gt;&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%2Fraw.githubusercontent.com%2FMojah2929Mojaha%2Fzenn-holometer-barzine%2Fmain%2Fimages%2Fpixel-maker-real-pixel-art%2F06.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%2Fraw.githubusercontent.com%2FMojah2929Mojaha%2Fzenn-holometer-barzine%2Fmain%2Fimages%2Fpixel-maker-real-pixel-art%2F06.png" alt="A side-by-side comparison of the AI-generated source, grid-corrected pixel art, and a color-preserving conversion" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post explains the parts that mattered most: grid detection, representative-color sampling, perceptual palette reduction, rare accent preservation, and clean export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why isn't nearest-neighbor resizing enough?
&lt;/h2&gt;

&lt;p&gt;My first version did what many quick converters do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;shrink the source image;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;enlarge it again with nearest-neighbor interpolation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That produces visible squares, but it does not repair the source. A blurred edge becomes a larger blurred-looking block. JPEG noise becomes a collection of unrelated colors. If the generated image uses different implied pixel sizes in the hair, face, and background, resizing simply compresses those inconsistencies into a smaller canvas.&lt;/p&gt;

&lt;p&gt;The problem was not “how do I make the pixels larger?” It was “how do I infer a useful logical grid, then choose one intentional color for every cell?”&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the browser-only pipeline work?
&lt;/h2&gt;

&lt;p&gt;Pixel Maker runs entirely in the browser with Canvas and a Web Worker. The selected image is not uploaded to a server.&lt;/p&gt;

&lt;p&gt;The conversion pipeline is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source image
  -&amp;gt; optional grid estimation
  -&amp;gt; cell-level color sampling
  -&amp;gt; palette extraction and perceptual merging
  -&amp;gt; accent and skin-tone preservation
  -&amp;gt; small-cluster cleanup
  -&amp;gt; logical-size PNG export
  -&amp;gt; optional 1x–10x nearest-neighbor enlargement

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

&lt;/div&gt;



&lt;p&gt;Keeping these stages separate made the tool easier to tune. A single resize or quantization pass could not handle both AI-generated pseudo-pixel art and smooth illustrations well.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you detect a hidden pixel grid?
&lt;/h2&gt;

&lt;p&gt;AI-generated pixel-art-style images often contain a weak periodic structure. Some edges line up every 6, 8, or 10 source pixels, while other regions drift away from that rhythm.&lt;/p&gt;

&lt;p&gt;I estimate candidate cell sizes from repeated vertical and horizontal color boundaries, then compare several signals instead of trusting one peak. The detected value is only a suggestion: users can switch to a fixed logical width when the source does not contain a reliable grid.&lt;/p&gt;

&lt;p&gt;That fallback matters. A portrait with soft gradients may not have a meaningful source grid at all, while a generated game sprite often does.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you choose one color per cell?
&lt;/h2&gt;

&lt;p&gt;Once the logical cells are known, each cell needs a single color. A simple average tends to create muddy colors around outlines, so the default is &lt;strong&gt;dominant-color sampling&lt;/strong&gt; : choose the most frequent color family inside the cell.&lt;/p&gt;

&lt;p&gt;The tool also exposes median, mean, and center sampling because no single strategy wins for every image:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dominant color&lt;/strong&gt; is strongest against AI noise and JPEG artifacts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Median&lt;/strong&gt; is stable when a cell contains a few extreme pixels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mean&lt;/strong&gt; preserves smooth photographic lighting, but can soften edges.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Center&lt;/strong&gt; is predictable for already-clean sprites.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This choice made a larger difference than I expected. On character faces, averaging often washed out the skin and weakened the eyes; dominant sampling kept the main color planes intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reduce colors in Oklab instead of RGB?
&lt;/h2&gt;

&lt;p&gt;RGB distance is a poor approximation of visual similarity. Pixel Maker converts palette candidates to &lt;strong&gt;Oklab&lt;/strong&gt; , measures perceptual distance there, and merges nearby colors while weighting each cluster by how much of the image it occupies.&lt;/p&gt;

&lt;p&gt;Large regions are protected from being collapsed too aggressively. Small colors can be merged more freely unless they qualify as important accents.&lt;/p&gt;

&lt;p&gt;The default hand-crafted mode currently uses 16 colors, while the grid-repair mode starts at 64 colors. Users can increase the count when faces or tiny details disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you preserve tiny accents without keeping noise?
&lt;/h2&gt;

&lt;p&gt;The hard part of palette reduction is distinguishing a useful rare color from random noise. A one-pixel highlight in an eye may be important; a one-pixel compression artifact is not.&lt;/p&gt;

&lt;p&gt;I score rare candidates using a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;distance from the retained palette;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;chroma;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;local population;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;whether nearby cells support the same color family.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps small stars, eye highlights, and interface accents more reliably. A separate skin-tone safeguard can reserve a representative warm color when palette reduction would otherwise make a face too pale or gray.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gets cleaned before PNG export?
&lt;/h2&gt;

&lt;p&gt;After quantization, isolated color fragments are compared with their neighbors. A cell is replaced only when a strong local majority exists, so cleanup removes speckle without erasing every deliberate single-pixel detail.&lt;/p&gt;

&lt;p&gt;The final PNG is rendered from the requested logical dimensions—anywhere from 16 to 512 pixels on the long side. If a larger display asset is needed, the logical image is enlarged up to 10× with nearest-neighbor interpolation.&lt;/p&gt;

&lt;p&gt;That distinction is important: the tool resamples the source for the selected logical size instead of stretching one previously generated thumbnail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which conversion mode should you use?
&lt;/h2&gt;

&lt;p&gt;The stricter correction mode prioritizes a consistent grid and compact palette. It works well for game assets and sprites.&lt;/p&gt;

&lt;p&gt;The color-preserving mode keeps more of the original gradients and atmosphere. It is better when visual resemblance matters more than strict pixel-art construction.&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%2Fraw.githubusercontent.com%2FMojah2929Mojaha%2Fzenn-holometer-barzine%2Fmain%2Fimages%2Fpixel-maker-real-pixel-art%2F02.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%2Fraw.githubusercontent.com%2FMojah2929Mojaha%2Fzenn-holometer-barzine%2Fmain%2Fimages%2Fpixel-maker-real-pixel-art%2F02.png" alt="The Pixel Maker interface with grid, palette, sampling, and export controls" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is no universal best conversion. The useful workflow is to start with the hand-crafted preset, inspect the eyes and outline at high zoom, then adjust logical size and palette count before editing individual pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool still cannot solve
&lt;/h2&gt;

&lt;p&gt;This is image processing, not semantic redrawing. It cannot infer the artist’s intended eye shape or reconstruct a hand that was malformed in the generated source. Lines thinner than one logical cell may disappear. Images with no repeated structure can also produce unreliable automatic grid estimates.&lt;/p&gt;

&lt;p&gt;For those cases, Pixel Maker includes a one-pixel editor so the last few decisions stay human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Pixel Maker is free, requires no account, and processes images locally in the browser:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://holometer.net/pixel-maker/en/" rel="noopener noreferrer"&gt;https://holometer.net/pixel-maker/en/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you test it with an especially difficult image, I would like to know which part breaks first: grid detection, palette reduction, outlines, or small details. Those failures are the most useful input for the next version.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was edited with AI assistance from my implementation notes and verified against the live tool.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>imageprocessing</category>
      <category>webdev</category>
      <category>pixelart</category>
    </item>
  </channel>
</rss>
