<?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: Embralynn 🔥</title>
    <description>The latest articles on DEV Community by Embralynn 🔥 (@embralynn).</description>
    <link>https://dev.to/embralynn</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%2F4133841%2F13ebf139-f38f-43b9-bc65-650eca2ecc7e.jpg</url>
      <title>DEV Community: Embralynn 🔥</title>
      <link>https://dev.to/embralynn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/embralynn"/>
    <language>en</language>
    <item>
      <title>Building a Pomodoro app with zero numbers: a procedural pixel fire in Tauri</title>
      <dc:creator>Embralynn 🔥</dc:creator>
      <pubDate>Sun, 20 Sep 2026 08:08:41 +0000</pubDate>
      <link>https://dev.to/embralynn/building-a-pomodoro-app-with-zero-numbers-a-procedural-pixel-fire-in-tauri-326p</link>
      <guid>https://dev.to/embralynn/building-a-pomodoro-app-with-zero-numbers-a-procedural-pixel-fire-in-tauri-326p</guid>
      <description>&lt;p&gt;A few months ago I noticed something annoying about how I actually use focus timers: I'd start a 25-minute session, and within a couple of minutes I was checking the countdown more than I was working. The number itself had become a little compulsion — not the work, the &lt;em&gt;clock&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I built Embralynn on a deliberately restrictive bet: what if the app told you nothing numeric at all? No countdown, no percentage, no session count, nowhere in the UI. Just a small pixel fire that grows while you're genuinely focused, settles to embers if you take a break, and fades if you wander off. That's the entire feedback loop.&lt;/p&gt;

&lt;p&gt;Here's what I learned building it — the technical decisions that turned out to matter more than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tauri over Electron, because idle is the whole product
&lt;/h2&gt;

&lt;p&gt;Embralynn's job is to sit in the corner of your screen indefinitely, doing basically nothing most of the time. That makes idle footprint the actual product requirement, not a nice-to-have.&lt;/p&gt;

&lt;p&gt;Electron ships a full Chromium + Node process per app — realistically 150-400MB of RAM for something that's supposed to just glow quietly. That's not a rounding error for an "ambient" widget; it's the difference between "install and forget" and "why is this thing eating my battery."&lt;/p&gt;

&lt;p&gt;Tauri v2 wraps a Rust backend around the &lt;em&gt;system&lt;/em&gt; WebView (WKWebView on macOS), so the bundle is a few MB of Rust plus web assets instead of a bundled browser. That gets idle RAM comfortably under 80MB. The whole design system — the fire, the tokens, the flicker/spark/crackle interactions — was already built as React + CSS, so Tauri's web-based frontend meant reusing it almost unchanged. Rewriting all of that natively in Swift would have meant redoing every animation from scratch for a footprint win we'd get for free with Tauri anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state machine: six states, and the two that matter most
&lt;/h2&gt;

&lt;p&gt;The fire isn't just "on" or "off." There are six states — idle, low, medium, full, smolder, dying — and the two design decisions I'm most glad I made are the ones that aren't obvious from a state diagram:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reversible dying.&lt;/strong&gt; If you walk away and the fire dies from inactivity, your accumulated focus isn't just gone. The embers stay warm for a short window — relighting within that window resumes the stage you were at instead of starting over from idle. Stepping away to think for two minutes shouldn't torch a 40-minute deep-work session. Only after the ember window closes does the focus bank actually reset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smolder isn't eternal.&lt;/strong&gt; Early on, a "break" (smolder) could sit there indefinitely. But a break that runs an hour isn't a break — it's abandonment wearing a break's clothes. So smolder now self-cools to idle after a longer timeout and releases the focus bank. The fire is honest about the difference between "I stepped away for coffee" and "I stopped working."&lt;/p&gt;

&lt;p&gt;Neither of these shows up as a number anywhere. They're just... how the fire behaves. That was the actual design challenge — encoding nuance into behavior instead of into a label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Procedural pixel art, not sprite sheets
&lt;/h2&gt;

&lt;p&gt;The fire isn't a set of pre-rendered PNG frames flipping through a loop. It's CSS pixel art generated and animated in code, driven by a continuous signal from the Rust backend rather than snapping between a fixed set of frames.&lt;/p&gt;

&lt;p&gt;Practically, this means the Rust side streams a small snapshot every second — state, intensity (how deep into a stage you are), an idle-drift fraction, and whether the embers are still warm — and the frontend renders continuously off of that instead of just switching sprite sheets on state change. The fire can lean and dim slightly as you approach an idle timeout — a quiet "you still there?" — without needing a seventh discrete state for it. It also means the landing page's live interactive demo and the actual app are pixel-identical, because they're running the same rendering code, not a marketing mockup next to a different runtime implementation.&lt;/p&gt;

&lt;p&gt;The tradeoff is that "just swap the image" is no longer an option if you want to change how the fire looks — you're touching animation code, not asset files. For a project where the fire &lt;em&gt;is&lt;/em&gt; the entire product, that tradeoff was worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One shared 8fps heartbeat instead of free-running CSS animations
&lt;/h2&gt;

&lt;p&gt;This is the detail I didn't expect to matter as much as it did: all the continuous "life" in the fire — flicker, glow, sparks, ember pops — samples off a single shared heartbeat timer running at 8fps, instead of each element running its own free-running CSS animation.&lt;/p&gt;

&lt;p&gt;8fps is a classic pixel-art-game frame rate — smooth enough to read as alive, slow enough that the browser's compositor only wakes up 8 times a second instead of 60-120. Measured effect: roughly a third of the CPU of letting everything animate freely, for a burning fire. And when there's genuinely nothing to animate — a cold idle fire with no subscribers, or the widget hidden in another Space — the heartbeat stops entirely rather than idling. document.getAnimations() reports zero at rest, on purpose.&lt;/p&gt;

&lt;p&gt;The one-shot transitions (a stage surging up, embers fading into smolder, a last wisp of smoke on dying) stay as regular CSS animations — they fire once and stop, so there's no ongoing cost to amortize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idle detection lives in Rust, not a JS timer
&lt;/h2&gt;

&lt;p&gt;The dying transition is driven by OS-level idle time (seconds since the last mouse/keyboard input), read on the Rust side, not a setTimeout in the webview. That matters because a JS timer in a backgrounded or throttled webview isn't reliable — the whole point of idle detection is that it has to keep working exactly when the frontend might not be. The state machine itself is unit-tested in Rust (stage growth, focus banking on break/idle, the warm-ember window, smolder cooldown), which is a nice side effect of keeping the logic out of the UI layer entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd want feedback on
&lt;/h2&gt;

&lt;p&gt;The bet underneath all of this is that &lt;em&gt;seeing&lt;/em&gt; progress (a fire growing) reads differently than &lt;em&gt;reading&lt;/em&gt; it (a number ticking down) — calmer, less compulsively checkable. I think it does, for me. I'm genuinely not sure it's not just a stylistic preference dressed up as a UX insight.&lt;/p&gt;

&lt;p&gt;If you've built something in the menu-bar-utility or ambient-focus-tool space — or you've just used enough Pomodoro apps to have opinions — I'd love to hear whether removing the number entirely reads as calming or just as &lt;em&gt;missing information&lt;/em&gt; to you. That's the actual open question I don't have a confident answer to yet.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
