<?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: Calvin</title>
    <description>The latest articles on DEV Community by Calvin (@sunkehappy).</description>
    <link>https://dev.to/sunkehappy</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%2F536815%2Fde043d5a-af99-41ef-94b1-9ba542596cb8.png</url>
      <title>DEV Community: Calvin</title>
      <link>https://dev.to/sunkehappy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sunkehappy"/>
    <language>en</language>
    <item>
      <title>How OmniGIF Removes GIF Backgrounds Entirely in the Browser</title>
      <dc:creator>Calvin</dc:creator>
      <pubDate>Fri, 25 Sep 2026 01:57:19 +0000</pubDate>
      <link>https://dev.to/sunkehappy/how-omnigif-removes-gif-backgrounds-entirely-in-the-browser-5e23</link>
      <guid>https://dev.to/sunkehappy/how-omnigif-removes-gif-backgrounds-entirely-in-the-browser-5e23</guid>
      <description>&lt;p&gt;Most "remove GIF background" tools upload your animation to a server, run a keying model, and send back a file. &lt;a href="https://www.omnigif.com/gif-tools/remove-background-from-gif" rel="noopener noreferrer"&gt;OmniGIF's Remove Background from GIF&lt;/a&gt; never uploads the GIF. Decoding, color keying, edge cleanup, and re-encoding all happen in the browser with Canvas &lt;code&gt;ImageData&lt;/code&gt; and a GIF encoder Worker.&lt;/p&gt;

&lt;p&gt;This post walks through that pipeline — from solid-color detection, through connected flood fill, to transparent GIF export.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GIF background problem
&lt;/h2&gt;

&lt;p&gt;A still photo background remover can lean on neural matting. An animated GIF is different:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Many frames&lt;/strong&gt; — the same key must stay stable across dozens or hundreds of frames, or the subject "flickers"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;256 colors max per frame&lt;/strong&gt; — soft alpha edges do not survive GIF encoding the way PNG does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solid or near-solid backgrounds are the common case&lt;/strong&gt; — white product shots, black meme templates, classic green screen stickers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subjects often share background hues&lt;/strong&gt; — a white logo on a white backdrop must not punch holes in the subject&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;OmniGIF targets &lt;strong&gt;chroma-style removal&lt;/strong&gt; (match a target RGB within tolerance), not full semantic segmentation. That keeps the tool fast, deterministic, and fully local — and matches what most GIF makers actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-level architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User drops a GIF
        ↓
Browser: decode with gifuct-js → GIFFrame[] (ImageData + delay)
        ↓
Auto-detect / pick / preset target RGB
        ↓
Per-frame keying (connected flood fill or all matching pixels)
   + optional edge feather + green spill reduction
   + optional solid / gradient fill under transparency
        ↓
Live preview on desktop (debounced) · Encode on demand
        ↓
gif.js encode (preserve transparency) → optional gifsicle optimize → download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack choices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App shell&lt;/td&gt;
&lt;td&gt;Next.js 15 (SSG) + React 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF decode&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/matt-way/gifuct-js" rel="noopener noreferrer"&gt;gifuct-js&lt;/a&gt; via shared &lt;code&gt;GifSession&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel math&lt;/td&gt;
&lt;td&gt;Canvas &lt;code&gt;ImageData&lt;/code&gt; on the main thread (chunked yields)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connectivity&lt;/td&gt;
&lt;td&gt;Iterative BFS flood fill from canvas edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF encode&lt;/td&gt;
&lt;td&gt;gif.js / shared &lt;code&gt;encodeFramesToGifBlob&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimize&lt;/td&gt;
&lt;td&gt;gifsicle-wasm-browser (preserve transparency)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;PostHog (tool id, mode, errors — not pixels)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No server receives the file. After the page loads, removal is local compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1 — Decode once, mutate copies
&lt;/h2&gt;

&lt;p&gt;Upload validates MIME/extension, file size (soft 10 MB cap), and decoded limits (edge length, pixels × frames). A &lt;code&gt;GifSession&lt;/code&gt; expands disposal/composite frames into full-canvas &lt;code&gt;ImageData&lt;/code&gt; so every frame is independent for keying.&lt;/p&gt;

&lt;p&gt;Source frames stay immutable in a ref. Preview and export always operate on clones — so resetting settings or aborting mid-job never corrupts the original decode.&lt;/p&gt;

&lt;p&gt;Processing stages the UI reports:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;decoding&lt;/code&gt; → &lt;code&gt;analyzing&lt;/code&gt; → &lt;code&gt;removing&lt;/code&gt; → &lt;code&gt;encoding&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2 — Finding the background color
&lt;/h2&gt;

&lt;p&gt;Three ways to set the target RGB:&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-detect
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;detectBackgroundColor&lt;/code&gt; samples the &lt;strong&gt;edges and four corners&lt;/strong&gt; of the first frame (every ~6 px), quantizes colors into coarse bins, and picks the dominant bin. Confidence is &lt;strong&gt;high&lt;/strong&gt; when that bin dominates (≥ ~35% of samples) and the palette of edge colors is small; otherwise the UI warns and suggests picking manually.&lt;/p&gt;

&lt;p&gt;Why edges? Backgrounds usually touch the canvas border. Sampling only the top-left pixel fails when a subject or watermark sits there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Presets
&lt;/h3&gt;

&lt;p&gt;White (&lt;code&gt;#ffffff&lt;/code&gt;), black (&lt;code&gt;#000000&lt;/code&gt;), and green screen (&lt;code&gt;#00b140&lt;/code&gt;-class) cover the majority of sticker and meme workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eyedropper
&lt;/h3&gt;

&lt;p&gt;"Pick from GIF" maps a click on the preview to pixel coordinates, reads RGB from the &lt;strong&gt;original&lt;/strong&gt; frame (not the keyed preview), and locks that hex as the target. Auto-detect turns off so later preview rebuilds do not overwrite the pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3 — Color match with tolerance
&lt;/h2&gt;

&lt;p&gt;Matching uses &lt;strong&gt;RGB Euclidean distance&lt;/strong&gt;, not HSL hue alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;distance = √(ΔR² + ΔG² + ΔB²)
maxDistance = (tolerance / 100) × √(3 × 255²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tolerance defaults to a conservative &lt;strong&gt;18 / 100&lt;/strong&gt;. At &lt;code&gt;0&lt;/code&gt;, only exact RGB equals the target. Higher values swallow compression banding and slight green-screen unevenness.&lt;/p&gt;

&lt;p&gt;GIF compression often spreads a "white" background across &lt;code&gt;#fefefe&lt;/code&gt;–&lt;code&gt;#f5f5f5&lt;/code&gt;. Without a distance band, keyed holes appear as speckles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4 — Connected mode vs all matching pixels
&lt;/h2&gt;

&lt;p&gt;This is the main product decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connected background only (default)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;buildConnectedBackgroundMask&lt;/code&gt; runs an &lt;strong&gt;iterative BFS&lt;/strong&gt; from every edge pixel that matches the target within tolerance, then expands to 4-neighbors that also match. The result is a &lt;code&gt;Uint8Array&lt;/code&gt; mask: &lt;code&gt;1&lt;/code&gt; = remove.&lt;/p&gt;

&lt;p&gt;Effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;White text &lt;em&gt;inside&lt;/em&gt; a dark subject is kept (not connected to the edge)&lt;/li&gt;
&lt;li&gt;Logo fills that share the background color but are enclosed by the subject stay intact&lt;/li&gt;
&lt;li&gt;Open holes in the subject that connect to the edge &lt;em&gt;will&lt;/em&gt; be keyed — correct for true background peek-through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Queue + visited arrays avoid recursive stack overflow on large canvases (important at 1080p).&lt;/p&gt;

&lt;h3&gt;
  
  
  All matching pixels
&lt;/h3&gt;

&lt;p&gt;Every pixel within tolerance is keyed, regardless of connectivity. Faster mentally to reason about, but dangerous when the subject contains the same color (white eyes, black clothing, green clothing on green screen).&lt;/p&gt;

&lt;p&gt;SEO copy on the tool page leans on connected mode as the reason OmniGIF does not "eat" interior colors the way naive global replace does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5 — Edge feather and color spill
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Edge softness
&lt;/h3&gt;

&lt;p&gt;When feather &amp;gt; 0, pixels near the distance threshold get &lt;strong&gt;partial alpha&lt;/strong&gt; proportional to how close they are to the max distance. That softens the preview against a checkerboard.&lt;/p&gt;

&lt;p&gt;Important GIF caveat: GIF89a transparency is &lt;strong&gt;binary&lt;/strong&gt; (a color index is transparent or not). Soft edges in preview may look slightly stepped after export. The encode path uses &lt;code&gt;transparencyMode: "preserve"&lt;/code&gt; with a matte color for residual fringing — same contract as other transparent GIF tools on the site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove color spill
&lt;/h3&gt;

&lt;p&gt;For green-screen targets (high G relative to R/B), a light pass reduces green excess on near-key opaque pixels (&lt;code&gt;spill = max(0, G − max(R, B))&lt;/code&gt;). It is not a full chroma spill matte from After Effects — just enough to cut the classic green halo around stickers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 6 — What replaces the background?
&lt;/h2&gt;

&lt;p&gt;After keying, optional &lt;strong&gt;post-removal backgrounds&lt;/strong&gt; composite under transparent pixels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transparent&lt;/td&gt;
&lt;td&gt;Leave alpha for GIF/APNG/WebP export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;Alpha-composite a hex color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear / radial gradient&lt;/td&gt;
&lt;td&gt;Rasterize gradient to &lt;code&gt;ImageData&lt;/code&gt;, then composite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gradients are baked opaque into each frame before encode, so the GIF does not need multi-level alpha. The encode layer then uses solid transparency mode with the gradient's "from" color as a safety matte for any leftover translucent pixels.&lt;/p&gt;

&lt;p&gt;Frame range is supported: apply keying only to frames &lt;code&gt;start…end&lt;/code&gt;, clone the rest unchanged — useful when only part of a loop has a solid backdrop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 7 — Staying responsive without a Worker (yet)
&lt;/h2&gt;

&lt;p&gt;Per-frame keying is pure JS over &lt;code&gt;Uint8ClampedArray&lt;/code&gt;. For long GIFs that can block the UI. The job helper:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Yields to the main thread every &lt;strong&gt;2 frames&lt;/strong&gt; via &lt;code&gt;setTimeout(0)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Honors &lt;code&gt;AbortSignal&lt;/code&gt; so changing settings or cancelling aborts the current generation&lt;/li&gt;
&lt;li&gt;Uses a &lt;strong&gt;generation counter&lt;/strong&gt; for desktop live preview so stale jobs cannot overwrite newer results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Desktop live preview debounces panel changes (~300 ms), runs the same &lt;code&gt;processBackgroundRemovalJob&lt;/code&gt; as export, and feeds &lt;code&gt;GifLivePreview&lt;/code&gt;. Mobile skips the heavy live path and shows before/after after generate — battery and screen space.&lt;/p&gt;

&lt;p&gt;Export then calls &lt;code&gt;encodeFramesToGifBlob&lt;/code&gt; (global palette, preserve transparency when appropriate) and &lt;code&gt;optimizeGifBlob&lt;/code&gt; with &lt;code&gt;preserveTransparency: true&lt;/code&gt;. Users can also download APNG / animated WebP from the same processed frames when they need softer edges than GIF allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  UX details that matter for keying
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live preview + magnifier.&lt;/strong&gt; Seeing connected vs all mode update in place beats guessing tolerance values. Pixel pick with a magnifier makes green-screen sampling accurate on phones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-confidence detect.&lt;/strong&gt; When edge colors are messy, the UI says so instead of silently keying the wrong color.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings groups.&lt;/strong&gt; Removal options stay expanded; shared GIF encode settings (quality, loop, frame delay, transparency matte) sit in a collapsed group — same pattern as other OmniGIF tool pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result scroll.&lt;/strong&gt; After encode, the result scrolls into view; download / share / re-edit actions stay consistent with the rest of the toolkit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy analytics.&lt;/strong&gt; Events carry tool name, removal mode, and completion — not filenames or image bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not upload to a cloud matting API?
&lt;/h2&gt;

&lt;p&gt;Server-side AI matting wins on complex hair and busy photos. For the GIF use cases OmniGIF optimizes for — stickers, green screen takes, white/black product loops — chroma keying is enough, and the tradeoffs favor local compute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No retention policy for meme faces or branded assets&lt;/li&gt;
&lt;li&gt;No GDPR transfer of the media itself&lt;/li&gt;
&lt;li&gt;Works offline after first load&lt;/li&gt;
&lt;li&gt;Deterministic results you can tune with tolerance and mode&lt;/li&gt;
&lt;li&gt;Same decode/encode stack as crop, trim, overlay, and compress tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When users need a new backdrop instead of transparency, the companion &lt;a href="https://www.omnigif.com/gif-tools/add-background-to-gif" rel="noopener noreferrer"&gt;Add Background to GIF&lt;/a&gt; tool picks up from a transparent GIF — often after this remover.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Tool: &lt;a href="https://www.omnigif.com/gif-tools/remove-background-from-gif" rel="noopener noreferrer"&gt;https://www.omnigif.com/gif-tools/remove-background-from-gif&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Related: &lt;a href="https://www.omnigif.com/gif-tools/add-background-to-gif" rel="noopener noreferrer"&gt;Add Background to GIF&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GIF transparency basics: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types#gif_graphics_interchange_format" rel="noopener noreferrer"&gt;MDN GIF&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Flood-fill concept: &lt;a href="https://en.wikipedia.org/wiki/Flood_fill" rel="noopener noreferrer"&gt;Wikipedia — Flood fill&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built as part of &lt;a href="https://www.omnigif.com" rel="noopener noreferrer"&gt;OmniGIF&lt;/a&gt; — a client-side GIF toolkit. Feedback welcome via &lt;a href="https://www.omnigif.com/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>gif</category>
      <category>canvas</category>
      <category>privacy</category>
    </item>
    <item>
      <title>How OmniGIF converts Live Photos to GIF entirely in the Browser</title>
      <dc:creator>Calvin</dc:creator>
      <pubDate>Sun, 13 Sep 2026 14:38:58 +0000</pubDate>
      <link>https://dev.to/sunkehappy/how-omnigif-converts-live-photos-to-gif-entirely-in-the-browser-174g</link>
      <guid>https://dev.to/sunkehappy/how-omnigif-converts-live-photos-to-gif-entirely-in-the-browser-174g</guid>
      <description>&lt;p&gt;iPhone Live Photos are not a single GIF-ready file. They are a still image plus a short video clip. Most "Live Photo to GIF" tools ask you to upload that clip to a server. &lt;a href="https://www.omnigif.com/live-to-gif" rel="noopener noreferrer"&gt;OmniGIF's Live Photo to GIF converter&lt;/a&gt; does the opposite: the MOV/MP4 never leaves the device.&lt;/p&gt;

&lt;p&gt;This post walks through how that pipeline is built — from Apple's export format, through engine selection, to palette-based GIF encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Live Photo input problem
&lt;/h2&gt;

&lt;p&gt;A Live Photo in the Photos library is typically a &lt;strong&gt;HEIC + paired video&lt;/strong&gt;. Browsers cannot reliably ingest that pair as one drop. Apple's supported path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the Live Photo&lt;/li&gt;
&lt;li&gt;Share → &lt;strong&gt;Save as Video&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Upload the exported &lt;strong&gt;MOV&lt;/strong&gt; or &lt;strong&gt;MP4&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That export is usually ~1.5–3 seconds of motion — short enough for a GIF, long enough to feel "alive." OmniGIF accepts &lt;code&gt;.mov&lt;/code&gt; / &lt;code&gt;.mp4&lt;/code&gt; (and the matching MIME types) and rejects raw HEIC Live Photo pairs on purpose. Forcing a clean video export keeps decoding deterministic across Chrome, Safari, and Firefox.&lt;/p&gt;

&lt;p&gt;On mobile, first-time users often try to pick the Live Photo still instead of the video. The page intercepts the file picker on narrow viewports and shows a short "Save as Video" guide GIF before opening the picker — same user gesture, so iOS Safari still allows the file dialog.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-level architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User exports Live Photo as MOV/MP4
        ↓
Browser: analyze container + duration + resolution
        ↓
Preview + crop + timeline trim (client UI)
        ↓
selectConversionEngine()
   ├─ WebCodecs path (Mediabunny decode → gif.js encode)
   └─ FFmpeg.wasm path (Worker + palettegen/paletteuse)
        ↓
GIF Blob → object URL → download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Live Photo page is a thin specialization of a shared &lt;strong&gt;media converter&lt;/strong&gt; used by Video to GIF, MOV to GIF, MP4 to GIF, and related tools. Same options model (&lt;code&gt;VideoToGifOptions&lt;/code&gt;), same analytics, same UI shell — different &lt;code&gt;accept&lt;/code&gt; list and copy.&lt;/p&gt;

&lt;p&gt;Stack choices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App shell&lt;/td&gt;
&lt;td&gt;Next.js 15 (SSG) + React 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast path&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Vanilagy/mediabunny" rel="noopener noreferrer"&gt;Mediabunny&lt;/a&gt; + WebCodecs + Canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compatible path&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://ffmpegwasm.netlify.app/" rel="noopener noreferrer"&gt;ffmpeg.wasm&lt;/a&gt; in a Web Worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF encode (fast path)&lt;/td&gt;
&lt;td&gt;gif.js / shared &lt;code&gt;encodeFramesToGifBlob&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Cloudflare (OpenNext)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No server receives the file. After the page loads, conversion is local compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dual engines, one contract
&lt;/h2&gt;

&lt;p&gt;Every converter implements the same interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;canHandle(input, options)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;convert(file, options, callbacks)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cancel()&lt;/code&gt; / &lt;code&gt;dispose()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Callbacks report &lt;strong&gt;stage&lt;/strong&gt; (&lt;code&gt;loading-engine&lt;/code&gt; → &lt;code&gt;decoding&lt;/code&gt; → &lt;code&gt;processing&lt;/code&gt; → &lt;code&gt;encoding&lt;/code&gt; → &lt;code&gt;completed&lt;/code&gt;) and a &lt;strong&gt;progress ratio&lt;/strong&gt;, so the UI can show meaningful feedback even when WASM does not emit smooth &lt;code&gt;progress&lt;/code&gt; events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine A — WebCodecs + Mediabunny (preferred)
&lt;/h3&gt;

&lt;p&gt;When the browser can decode the container (MOV/MP4/WebM/MKV-like) and is not Safari-preferring-FFmpeg:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the file with Mediabunny &lt;code&gt;Input&lt;/code&gt; + &lt;code&gt;BlobSource&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Take the primary video track; verify &lt;code&gt;canDecode()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sample frames at the target FPS between &lt;code&gt;startSeconds&lt;/code&gt; and &lt;code&gt;endSeconds&lt;/code&gt; via &lt;code&gt;CanvasSink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Apply crop / circular mask in Canvas → &lt;code&gt;ImageData&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Encode frames to GIF with palette quality derived from the color budget&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This path stays on the main thread for canvas work but avoids downloading a multi‑MB Wasm binary when hardware decoding works. Frame count is capped (order of hundreds) so a mis-set FPS cannot OOM a phone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine B — FFmpeg.wasm in a Worker (fallback / Safari / hard cases)
&lt;/h3&gt;

&lt;p&gt;FFmpeg runs in a &lt;strong&gt;module Worker&lt;/strong&gt; with the &lt;strong&gt;single-thread&lt;/strong&gt; &lt;code&gt;@ffmpeg/core&lt;/code&gt; build. That avoids requiring &lt;code&gt;SharedArrayBuffer&lt;/code&gt; / cross-origin isolation, which would conflict with many third-party scripts on a marketing site.&lt;/p&gt;

&lt;p&gt;Flow inside the worker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lazy-load &lt;code&gt;ffmpeg-core.js&lt;/code&gt; + &lt;code&gt;.wasm&lt;/code&gt; from a CDN into Blob URLs&lt;/li&gt;
&lt;li&gt;Write the uploaded buffer to the virtual FS as &lt;code&gt;input.mov&lt;/code&gt; / &lt;code&gt;input.mp4&lt;/code&gt; / …&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exec&lt;/code&gt; a carefully built argv list&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;output.gif&lt;/code&gt;, transfer the &lt;code&gt;ArrayBuffer&lt;/code&gt; back to the UI thread&lt;/li&gt;
&lt;li&gt;Delete temp files; support cancel via &lt;code&gt;terminate()&lt;/code&gt; + generation tokens&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Palette filters often report &lt;code&gt;progress ≈ 0&lt;/code&gt;. The main-thread engine layers a &lt;strong&gt;soft asymptotic progress ticker&lt;/strong&gt;, overridden whenever real FFmpeg time/progress arrives — so the bar still moves on phones.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the engine is chosen
&lt;/h2&gt;

&lt;p&gt;Selection is capability-driven, not page-driven. Live Photo → GIF uses the same rules as other video→GIF pages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AVI&lt;/strong&gt; or containers that do not prefer WebCodecs → FFmpeg&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari&lt;/strong&gt; (weaker WebCodecs reliability) → prefer FFmpeg&lt;/li&gt;
&lt;li&gt;Missing &lt;code&gt;VideoDecoder&lt;/code&gt; or unknown width/height from header probe → FFmpeg&lt;/li&gt;
&lt;li&gt;Otherwise try WebCodecs; if &lt;code&gt;canHandle&lt;/code&gt; fails → FFmpeg&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If WebCodecs starts and throws a &lt;strong&gt;recoverable&lt;/strong&gt; error (unsupported codec, empty frames, canvas failure), the conversion hook &lt;strong&gt;automatically falls back&lt;/strong&gt; to a fresh FFmpeg engine and marks &lt;code&gt;usedFallback: true&lt;/code&gt; for analytics. Users see a short "compatible mode" state instead of a hard failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a good GIF from video
&lt;/h2&gt;

&lt;p&gt;GIF is at most &lt;strong&gt;256 colors per frame&lt;/strong&gt;. Naïve frame dumps look posterized. OmniGIF uses two complementary strategies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fast path: per-frame quantize + optional dither
&lt;/h3&gt;

&lt;p&gt;Decoded canvases become &lt;code&gt;GIFFrame[]&lt;/code&gt; with delay &lt;code&gt;1000 / fps&lt;/code&gt;. Encoder quality is mapped from the &lt;code&gt;colors&lt;/code&gt; setting. Circular crop reserves transparency so the GIF can be a round sticker-style clip.&lt;/p&gt;

&lt;h3&gt;
  
  
  FFmpeg path: two-pass palette
&lt;/h3&gt;

&lt;p&gt;Args are built in pure TypeScript (no string-interpolated user paths). Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0:v] crop?, fps, scale=lanczos, (optional circle alpha)
  → split
  → palettegen (max_colors, stats_mode=diff)
  → paletteuse (dither=bayer|floyd_steinberg|sierra2_4a|…)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also supported in the filter chain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trim&lt;/strong&gt; via &lt;code&gt;-ss&lt;/code&gt; / &lt;code&gt;-t&lt;/code&gt; &lt;em&gt;after&lt;/em&gt; &lt;code&gt;-i&lt;/code&gt; (more reliable on awkward containers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; by adjusting effective FPS (&lt;code&gt;fps / speed&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop&lt;/strong&gt; count (&lt;code&gt;-loop&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Even dimensions&lt;/strong&gt; (encoder-friendly)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aspect-preserving scale&lt;/strong&gt; (&lt;code&gt;scale=W:-1&lt;/code&gt;) so crop regions are not stretched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Presets (&lt;code&gt;small&lt;/code&gt; / &lt;code&gt;balanced&lt;/code&gt; / &lt;code&gt;high&lt;/code&gt;) set default width, FPS, colors, and dither. Live Photos default to a &lt;strong&gt;centered square crop&lt;/strong&gt; because vertical phone footage rarely needs full frame for a chat GIF.&lt;/p&gt;

&lt;h2&gt;
  
  
  UX details that matter for Live Photos
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Preview before convert.&lt;/strong&gt; After upload, metadata analysis fills duration and resolution; the UI shows a video preview with crop handles and a timeline. Estimated output size updates as options change — important because GIF size grows roughly with &lt;code&gt;frames × resolution × color complexity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile guide.&lt;/strong&gt; Phones get a one-shot modal explaining "Save as Video," with optional "don't show again today" via &lt;code&gt;localStorage&lt;/code&gt; keyed by local date. Confirming the modal must call the file picker in the &lt;strong&gt;same tap&lt;/strong&gt; or Safari will block it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy analytics.&lt;/strong&gt; PostHog events carry tool id, engine id, duration, and error codes — not filenames or pixel data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Soft limits.&lt;/strong&gt; Duration, resolution, and file size caps fail early with clear errors rather than mid-encode OOMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not upload to a server?
&lt;/h2&gt;

&lt;p&gt;Client-side conversion is slower than a beefy GPU box for long 4K clips — but Live Photos are short. The tradeoffs win:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No retention policy for intimate phone videos&lt;/li&gt;
&lt;li&gt;No GDPR transfer of the media itself&lt;/li&gt;
&lt;li&gt;Works after first load with cached Wasm / scripts&lt;/li&gt;
&lt;li&gt;Same codebase for Live Photo, MOV, and MP4 tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When WebCodecs wins, first conversion can feel near-instant. When FFmpeg loads, idle-time preload (&lt;code&gt;requestIdleCallback&lt;/code&gt;) on related pages softens the cold start.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Tool: &lt;a href="https://www.omnigif.com/live-to-gif" rel="noopener noreferrer"&gt;https://www.omnigif.com/live-to-gif&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple's Live Photo help: &lt;a href="https://support.apple.com/en-us/104966" rel="noopener noreferrer"&gt;Take and edit Live Photos&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GIF format background: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types" rel="noopener noreferrer"&gt;MDN image types&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;YouTube video: &lt;a href="https://www.youtube.com/watch?v=7LSQiUI4J_M" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=7LSQiUI4J_M&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built as part of &lt;a href="https://www.omnigif.com" rel="noopener noreferrer"&gt;OmniGIF&lt;/a&gt; — a client-side GIF toolkit. Feedback welcome via &lt;a href="https://www.omnigif.com/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ffmpeg</category>
      <category>webcodecs</category>
    </item>
    <item>
      <title>Building OmniGIF: A Client-Side GIF Toolkit with WebAssembly (No Server Uploads)</title>
      <dc:creator>Calvin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 01:30:38 +0000</pubDate>
      <link>https://dev.to/sunkehappy/building-omnigif-a-client-side-gif-toolkit-with-webassembly-no-server-uploads-29o1</link>
      <guid>https://dev.to/sunkehappy/building-omnigif-a-client-side-gif-toolkit-with-webassembly-no-server-uploads-29o1</guid>
      <description>&lt;p&gt;Most online GIF tools send your file to a remote server. I built &lt;a href="https://www.omnigif.com" rel="noopener noreferrer"&gt;OmniGIF&lt;/a&gt; to prove that's unnecessary — modern browsers can decode, encode, and transform GIFs entirely on the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture problem
&lt;/h2&gt;

&lt;p&gt;Traditional flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → Upload to server → Process → Download result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OmniGIF flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → Load page → Process in browser (JS + WASM) → Download result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file never hits our infrastructure. From a privacy and latency perspective, this changes the product entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 15 (SSG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;next-intl (EN + ZH)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Cloudflare (OpenNext)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;PostHog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The site is statically generated. There's no server-side file processing — which is intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-side libraries
&lt;/h2&gt;

&lt;p&gt;GIF work on the web is surprisingly mature. OmniGIF relies on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/matt-way/gifuct-js" rel="noopener noreferrer"&gt;gifuct-js&lt;/a&gt;&lt;/strong&gt; — GIF89a decoder; parses frames and delay metadata locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jnordberg/gif.js" rel="noopener noreferrer"&gt;gif.js&lt;/a&gt;&lt;/strong&gt; — GIF encoder running in a Web Worker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ffmpegwasm.netlify.app/" rel="noopener noreferrer"&gt;ffmpeg.wasm&lt;/a&gt;&lt;/strong&gt; — FFmpeg compiled to WebAssembly for Live Photo / video → GIF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/photopea/UPNG.js" rel="noopener noreferrer"&gt;UPNG.js&lt;/a&gt;&lt;/strong&gt; — PNG/APNG encode/decode for high-color animated workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gifsicle-wasm-browser&lt;/strong&gt; — GIF optimization in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSZip&lt;/strong&gt; — bundling extracted frames into ZIP downloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per &lt;a href="https://developer.mozilla.org/en-US/docs/WebAssembly" rel="noopener noreferrer"&gt;MDN's WebAssembly docs&lt;/a&gt;, Wasm brings near-native performance to the web — which matters when you're encoding 30 frames at 1080p.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the toolkit includes
&lt;/h2&gt;

&lt;p&gt;31 tools across four categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  GIF Converter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.omnigif.com/gif-converter/gif-to-png" rel="noopener noreferrer"&gt;GIF to PNG&lt;/a&gt; / &lt;a href="https://www.omnigif.com/gif-converter/gif-to-webp" rel="noopener noreferrer"&gt;WebP&lt;/a&gt; / &lt;a href="https://www.omnigif.com/gif-converter/gif-to-apng" rel="noopener noreferrer"&gt;APNG&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.omnigif.com/gif-converter/png-to-gif" rel="noopener noreferrer"&gt;PNG to GIF&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-converter/apng-to-gif" rel="noopener noreferrer"&gt;APNG to GIF&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.omnigif.com/live-to-gif" rel="noopener noreferrer"&gt;Live Photo to GIF&lt;/a&gt; — MOV/MP4 → GIF with quality/FPS controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GIF Tools
&lt;/h3&gt;

&lt;p&gt;Frame inspection, &lt;a href="https://www.omnigif.com/gif-tools/crop-gif" rel="noopener noreferrer"&gt;crop&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-tools/trim-gif" rel="noopener noreferrer"&gt;trim&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-tools/reverse-gif" rel="noopener noreferrer"&gt;reverse&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-tools/gif-compressor" rel="noopener noreferrer"&gt;compress&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-tools/compress-gif-to-size" rel="noopener noreferrer"&gt;compress to target size&lt;/a&gt;, text/watermark overlay, &lt;a href="https://www.omnigif.com/gif-tools/gif-overlay" rel="noopener noreferrer"&gt;GIF overlay&lt;/a&gt;, watermark removal.&lt;/p&gt;

&lt;h3&gt;
  
  
  GIF Maker
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.omnigif.com/gif-maker/intensify" rel="noopener noreferrer"&gt;Intensify&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-maker/shake-image-effect" rel="noopener noreferrer"&gt;shake&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-maker/fire-text" rel="noopener noreferrer"&gt;fire text&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-maker/vinyl-spinning-gif-maker" rel="noopener noreferrer"&gt;vinyl spin&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GIF Effects
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.omnigif.com/gif-effects/black-and-white-gif" rel="noopener noreferrer"&gt;B&amp;amp;W&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-effects/add-border" rel="noopener noreferrer"&gt;border&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-effects/confetti-gif-maker" rel="noopener noreferrer"&gt;confetti&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-effects/gif-dither" rel="noopener noreferrer"&gt;dither&lt;/a&gt; (Floyd–Steinberg, Bayer, Atkinson), &lt;a href="https://www.omnigif.com/gif-effects/gif-pixelator" rel="noopener noreferrer"&gt;pixelate&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-effects/gif-fade-effect" rel="noopener noreferrer"&gt;fade&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-effects/gif-speed-changer" rel="noopener noreferrer"&gt;speed&lt;/a&gt;, &lt;a href="https://www.omnigif.com/gif-effects/glitch-gif-maker" rel="noopener noreferrer"&gt;glitch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Full list: &lt;a href="https://www.omnigif.com/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; (machine-readable sitemap for tools).&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why SSG, not SSR?
&lt;/h3&gt;

&lt;p&gt;All parameters are client-side. No need for server rendering of user files. Faster cold loads, simpler deployment on Cloudflare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Web Workers?
&lt;/h3&gt;

&lt;p&gt;GIF encoding blocks the main thread. gif.js and heavy transforms run off-thread so the UI stays responsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why per-tool pages instead of one mega-app?
&lt;/h3&gt;

&lt;p&gt;SEO, shareability, and focused UX. Each tool has its own URL, metadata, FAQ, and structured data. Users land on exactly what they need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-time preview on desktop
&lt;/h3&gt;

&lt;p&gt;For tools with tunable parameters (crop, effects, compression), the right panel shows a live preview on PC. Mobile gets before/after after generation — saves battery and screen space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome extension
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.omnigif.com/extension" rel="noopener noreferrer"&gt;OmniGIF extension&lt;/a&gt; bridges web → tool: right-click any image, pick a destination tool, and the image is passed via URL params / clipboard. Offline install package available for environments that need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — Large GIFs (many frames × high resolution) can OOM on mobile. We enforce file size limits and warn early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GIF color depth&lt;/strong&gt; — GIF is 256 colors max. Converting from APNG/PNG requires palette reduction; dithering quality varies by algorithm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ffmpeg.wasm bundle size&lt;/strong&gt; — First load downloads a sizable Wasm binary. Lazy-loading FFmpeg only on video tools helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-browser APNG&lt;/strong&gt; — APNG support is uneven; we document browser compatibility on converter pages.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Privacy as a feature, not a footnote
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/Graphics/GIF/spec-gif89a.txt" rel="noopener noreferrer"&gt;W3C GIF89a spec&lt;/a&gt; hasn't changed much in decades, but how we process GIFs can change. Client-side tooling means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No file retention policies to worry about&lt;/li&gt;
&lt;li&gt;No GDPR data-transfer questions for the image itself&lt;/li&gt;
&lt;li&gt;Works offline after first page load (extension + cached assets)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Live at &lt;a href="https://www.omnigif.com" rel="noopener noreferrer"&gt;https://www.omnigif.com&lt;/a&gt; — free, no account.&lt;/p&gt;

&lt;p&gt;More background: &lt;a href="https://www.omnigif.com/about" rel="noopener noreferrer"&gt;About OmniGIF&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions: &lt;a href="https://www.omnigif.com/faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; | &lt;a href="https://www.omnigif.com/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built by Calvin Sun as an indie project. Feedback welcome.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
