<?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 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>
