<?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: Gavin Rose</title>
    <description>The latest articles on DEV Community by Gavin Rose (@gavin_rose).</description>
    <link>https://dev.to/gavin_rose</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%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png</url>
      <title>DEV Community: Gavin Rose</title>
      <link>https://dev.to/gavin_rose</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gavin_rose"/>
    <language>en</language>
    <item>
      <title>Plinth! My first arcade-style game.</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 10 Oct 2026 23:58:37 +0000</pubDate>
      <link>https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8</link>
      <guid>https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8</guid>
      <description>&lt;p&gt;I just shipped &lt;strong&gt;Plinth&lt;/strong&gt;, a small one-tap arcade game for Android. You stand on a column, a new column rises next to you, and you tap to stop it, ideally at your height. If you stop it too high, the extra gets sliced off, so every column after that is shorter. Stop it too low and you fall.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://play.google.com/store/apps/details?id=com.gtfrexo.plinth" rel="noopener noreferrer"&gt;Get Plinth on Google Play&lt;/a&gt; · &lt;a href="https://plinth-mobile.web.app" rel="noopener noreferrer"&gt;plinth-mobile.web.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's built with Flutter and &lt;a href="https://flame-engine.org/" rel="noopener noreferrer"&gt;Flame&lt;/a&gt;. Here's the story!&lt;/p&gt;

&lt;h2&gt;
  
  
  Faking 3D to promote performance
&lt;/h2&gt;

&lt;p&gt;Plinth looks 3D, but it's plain 2D canvas drawing. Every column is a box in isometric projection: three filled polygons (top, left side, right side) computed from grid coordinates.&lt;/p&gt;

&lt;p&gt;The only real trick is draw order. Boxes further "back" have to be painted first, so each frame I sort everything by &lt;code&gt;gx + gy&lt;/code&gt; and paint back to front (the painter's algorithm). The player gets drawn last, except when falling into a gap, where it slots into the sort so the columns in front correctly cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Game feel is a pile of tiny things
&lt;/h2&gt;

&lt;p&gt;The first playable version worked but felt dead. What fixed it was a slew of small touches, none of which were too hard to implement...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manipulating the character&lt;/strong&gt; on the character: a little crouch before a hop, a stretch in the air, a squash on landing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rising chime for perfect stops&lt;/strong&gt; that climbs a scale with your streak, so a streak of perfects sounds like a melody.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen shake and debris&lt;/strong&gt; when a column gets sliced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "NEW BEST!" moment mid-run&lt;/strong&gt; whenever you beat your previous best score, complete with confetti.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of the sound effects were generated with a short Python + numpy script (sine waves, envelopes, some filtered noise).&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncing coins across devices without a backend
&lt;/h2&gt;

&lt;p&gt;I wanted coins and unlocks to follow players between phones, without running a server. Google Play Games' &lt;strong&gt;Saved Games&lt;/strong&gt; stores a blob per player for free, so I only needed a merge strategy for when a local copy and the cloud copy disagree.&lt;/p&gt;

&lt;p&gt;A plain "take the max coins" merge is wrong: spend coins on one phone, sync an older copy, and you get the coins back. Instead, coins are stored as per-device running totals of earned and spent:&lt;/p&gt;

&lt;p&gt;Each device only ever increases its own counters, so merging is just "take the max per device". Owned items merge as a union.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rejection that taught me to test release builds
&lt;/h2&gt;

&lt;p&gt;My first production submission was rejected: &lt;strong&gt;"Plinth keeps stopping."&lt;/strong&gt; It had never crashed for me once. Here is where I used Claude Code to find a quick fix. &lt;/p&gt;

&lt;p&gt;The cause: release builds run R8 (code shrinking), debug builds don't. The ads SDK pulls in an old version of WorkManager, which uses Room, which finds its generated database class by name at startup. R8 renamed it, so the app crashed before drawing a single frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to create an instance of androidx.work.impl.WorkDatabase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was a keep rule plus a newer WorkManager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# android/app/proguard-rules.pro
&lt;/span&gt;&lt;span class="err"&gt;-keep&lt;/span&gt; &lt;span class="err"&gt;class&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="err"&gt;extends&lt;/span&gt; &lt;span class="err"&gt;androidx.room.RoomDatabase&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;init&amp;gt;();&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// android/app/build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.work:work-runtime:2.11.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real lesson: before every upload, install a &lt;strong&gt;release&lt;/strong&gt; build fresh on an emulator and actually play it. &lt;code&gt;flutter run&lt;/code&gt; in debug mode tells you nothing about R8.&lt;/p&gt;

&lt;p&gt;While testing that, I found a second problem: on a phone with no Google account signed in (like many review devices), my startup code triggered the &lt;em&gt;interactive&lt;/em&gt; Play Games sign-in, which opened Google's full account login screen. Now the app only silently checks sign-in status at launch, and the interactive flow lives behind a "Sign in" button on the leaderboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two small gotchas worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AdMob's app-ads.txt check vs Firebase Hosting.&lt;/strong&gt; &lt;code&gt;firebase init&lt;/code&gt; offers a single-page-app rewrite that sends every unknown URL to &lt;code&gt;index.html&lt;/code&gt; with a 200 status. That means &lt;code&gt;/app-ads.txt&lt;/code&gt; "exists" but returns HTML, and AdMob reports a mismatch. Use real files with &lt;code&gt;cleanUrls&lt;/code&gt; and no catch-all rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The consent form is not optional in the EU and UK.&lt;/strong&gt; Google's UMP SDK ships inside &lt;code&gt;google_mobile_ads&lt;/code&gt;. Request consent before initializing ads, and add a "Privacy options" entry so players can change their choice later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monetization: rewarded only
&lt;/h2&gt;

&lt;p&gt;Every ad in Plinth is opt-in: continue a run once, double your coins, or earn coins in the shop. No banners and no interstitials. It earns less per player than forced ads would, but I'd rather people keep playing than bounce off an ad between every run.&lt;/p&gt;

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

&lt;p&gt;Plinth is free on Android: &lt;strong&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.gtfrexo.plinth" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;&lt;/strong&gt;. If you give it a go, I'd love to hear your best score, and any feedback on how it feels. And if you're building something similar in Flutter and Flame, ask away in the comments.&lt;/p&gt;

</description>
      <category>android</category>
      <category>flutter</category>
      <category>gamedev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Rockhounds should try this!</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 10 Oct 2026 23:05:47 +0000</pubDate>
      <link>https://dev.to/gavin_rose/rockhounds-should-try-this-53lo</link>
      <guid>https://dev.to/gavin_rose/rockhounds-should-try-this-53lo</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-story__hidden-navigation-link"&gt;Pocket rockhound buddy that works anywhere&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/gavin_rose" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png" alt="gavin_rose profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/gavin_rose" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Gavin Rose
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Gavin Rose
                
                
              
              &lt;div id="story-author-preview-content-4828855" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/gavin_rose" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Gavin Rose&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 10&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" id="article-link-4828855"&gt;
          Pocket rockhound buddy that works anywhere
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hf26challenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hf26challenge&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>mobile</category>
      <category>showdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>Pocket rockhound buddy that works anywhere</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:27:51 +0000</pubDate>
      <link>https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod</link>
      <guid>https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I love to explore nature, and while I'm familiar with the minerals, fossils, and stones around my area, I wanted something that could help people that may not know as much, and more importantly, something that could be used completely offline so that it would still work even if you are far into a trail.&lt;/p&gt;

&lt;p&gt;The idea is that a photo alone isn't normally enough to identify a rock, and even using flagship close models like gemini can give mixed results. There are several tests you can perform at the site or at home such as  scratching it, weighing it in their hand, or putting vinegar on it to see if there is a reaction. This app takes a photo for a first guess, then walks you through those same physical tests one at a time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can your fingernail scratch it? A coin? A nail?&lt;/li&gt;
&lt;li&gt;Rub it on an unglazed tile. What color is the powder?&lt;/li&gt;
&lt;li&gt;Does a magnet stick to it?&lt;/li&gt;
&lt;li&gt;Put a drop of vinegar on it. Does it fizz?&lt;/li&gt;
&lt;li&gt;Is it a shell with two halves? Is each half symmetrical?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each answer updates the odds, and the app always asks whichever test will narrow things down the most next. Most identifications take 5 to 8 tests. Between questions the phone is in your pocket and the rock is in your hand, which is the part I like best: &lt;strong&gt;the screen is the shortest part of the experience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model knows 94 things: 53 minerals, 22 rocks and 19 fossils. It also knows where you're standing. It looks up the bedrock under you from open geologic map data, so in limestone country it leans toward calcite and crinoids, on granite toward feldspar and mica. It even knows the age of the rock. A trilobite can't turn up in rock that's 100 million years too young.&lt;/p&gt;

&lt;p&gt;When you're done, you get a specimen label (type, hardness, streak, where to look for more) and can save the find with its GPS location. Finds export as GeoJSON, so you can map everything you've found if desired.&lt;/p&gt;

&lt;p&gt;It's for anyone who picks up rocks on a hike, at a creek or on a beach and wonders what they're holding. That covers kids, rockhounds, and people like me with pockets full of crinoids.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; [&lt;a href="https://blue-math-567b.gavin-rose.workers.dev/" rel="noopener noreferrer"&gt;https://blue-math-567b.gavin-rose.workers.dev/&lt;/a&gt;]. Open it in Safari or Chrome on your phone and add it to your home screen. The first launch downloads the model (about 63 MB, Wi-Fi is recommended). After that, it works wherever you go! While a connection is available, it will use a larger hosted open weight model to give better results more quickly, and will fall back to the lightweight model when necessary or if you toggle the other one off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe01hy4o2chai4mc5oi47.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe01hy4o2chai4mc5oi47.webp" alt=" " width="710" height="1536"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fir6xsby05a313igo9fhq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fir6xsby05a313igo9fhq.webp" alt=" " width="710" height="1536"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ju4xdi1mvzqm2vdriht.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ju4xdi1mvzqm2vdriht.webp" alt=" " width="710" height="1536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  I took it outside
&lt;/h3&gt;

&lt;p&gt;I went out on a few walks near home, where the bedrock is shale and sandstone, and here's how it went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crinoids, three different ways.&lt;/strong&gt; Loose stem discs in my palm: crinoid stems, 92%. A stem lying along a rock face: 87%. A slab of fossil hash: 88%. Each one took about five tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It got things wrong too.&lt;/strong&gt; A small white chip came back as calcite, and it wasn't. One find only reached 19% for "conglomerate", which is really a shrug dressed up as an answer. And when I photographed nothing to test the app, it confidently named a rock anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It crashed.&lt;/strong&gt; On my iPhone the app downloaded the model, then Safari killed the page. In the field I had to time my taps to get in before it went down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The questions felt off.&lt;/strong&gt; The fossil-shape question ("stacked discs? coiled shell?") kept coming up before anything suggested a fossil, so for most rocks I was tapping "None of these".&lt;/p&gt;

&lt;p&gt;Every one of those turned into a fix, covered below. Taking it outside was the most useful testing I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;It is an installable web app (PWA) written in TypeScript with Vite. There's no app store and no account. The AI runs directly in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  The photo model: SigLIP 2, on the phone
&lt;/h3&gt;

&lt;p&gt;The photo step uses &lt;strong&gt;&lt;a href="https://huggingface.co/onnx-community/siglip2-base-patch16-224-ONNX" rel="noopener noreferrer"&gt;SigLIP 2&lt;/a&gt;&lt;/strong&gt;, Google's open-weight image and text model, running fully on-device through &lt;strong&gt;&lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;ONNX Runtime Web&lt;/strong&gt;. On phones it runs as a 4-bit model (63 MB) on the CPU. On desktops with WebGPU it uses the fp16 version on the GPU.&lt;/p&gt;

&lt;p&gt;I didn't start with SigLIP. I started with CLIP, then benchmarked five open backbones on the same images and the same held-out split, training the same small classifier on top of each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backbone&lt;/th&gt;
&lt;th&gt;Top-1&lt;/th&gt;
&lt;th&gt;Top-3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CLIP ViT-B/32 (where I started)&lt;/td&gt;
&lt;td&gt;43.5%&lt;/td&gt;
&lt;td&gt;61.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLIP ViT-B/16&lt;/td&gt;
&lt;td&gt;48.6%&lt;/td&gt;
&lt;td&gt;66.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DINOv2 small&lt;/td&gt;
&lt;td&gt;47.7%&lt;/td&gt;
&lt;td&gt;62.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DINOv3 small&lt;/td&gt;
&lt;td&gt;51.2%&lt;/td&gt;
&lt;td&gt;67.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SigLIP 2 base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SigLIP 2 won clearly, at the same 63 MB on the phone. Swapping it in took about an hour. That's only possible because every one of these models is open and runs in the same runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Training it on rocks
&lt;/h3&gt;

&lt;p&gt;Off the shelf, SigLIP 2 manages 29% top-1 across all 94 classes. I trained a small classification layer on top of it, using about 5,000 openly licensed images:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wikimedia Commons&lt;/strong&gt;: Creative Commons photos of minerals and rocks, with attribution saved for every file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GBIF&lt;/strong&gt;: CC0 and CC-BY photos of fossil specimens from natural history museums. These were the key to fossils, including plenty of the loose crinoid discs I actually find.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own field photos&lt;/strong&gt;: always trusted and never filtered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Web data is messy. A search for "granite" returns mountains, statues and kitchen counters. So the training script uses the model itself as a filter: it drops any image that looks more like a landscape, building or diagram than a rock specimen. Museum photos had the opposite problem: a tiny fossil next to a big ruler and a color card. For those, the script tries a grid of crops and keeps the one that looks most like a fossil and least like a ruler.&lt;/p&gt;

&lt;p&gt;The trained model gets &lt;strong&gt;62.3% top-1 and 78.6% top-3&lt;/strong&gt; on held-out images, up from 29% and 45% for the untrained model. Top-3 matters most, because the photo is only a starting point and the physical tests do the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tests: odds that update with every answer
&lt;/h3&gt;

&lt;p&gt;Each of the 94 entries has its properties written down: hardness range, streak color, luster, how it breaks, whether it's magnetic or fizzes, and for fossils, shape and age. The app keeps a probability for every candidate and updates it with each answer. Answers are treated as noisy, because people misjudge luster and vinegar is a weak acid.&lt;/p&gt;

&lt;p&gt;To pick the next question, it calculates which one is expected to shrink the uncertainty most (information gain). A few rules keep it sensible. It always opens with a broad "What did you find?", and fossil-only questions wait until the evidence actually leans toward a fossil.&lt;/p&gt;

&lt;h3&gt;
  
  
  Geology from where you're standing
&lt;/h3&gt;

&lt;p&gt;With your GPS position (GPS works without signal), Streak looks up the bedrock in &lt;strong&gt;&lt;a href="https://macrostrat.org" rel="noopener noreferrer"&gt;Macrostrat&lt;/a&gt;&lt;/strong&gt;, an open geologic map database. It turns rock types into odds: a granite map unit boosts quartz, feldspar and mica, and "crinoidal limestone" in the description boosts crinoids directly.&lt;/p&gt;

&lt;p&gt;It never rules anything out. Creeks, glaciers and landscaping gravel carry rocks for miles, so local geology only boosts candidates, and there's a "Found it somewhere else?" switch. Fossils also get an age check: outside their time range they're down-weighted, unless you're standing on river gravel, which can hold fossils of any age. Before a trip you can save the geology for about 20 km around you, so it works offline too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional online mode: Llama 4 Scout
&lt;/h3&gt;

&lt;p&gt;With signal, Streak can also send the photo to &lt;strong&gt;Llama 4 Scout&lt;/strong&gt;, Meta's open-weight vision model, running on Cloudflare Workers AI. The request goes through the app's own Cloudflare Worker, so the phone only ever talks to the app's domain. It sends the photo and the name of the bedrock, never your coordinates. The larger model's answer is blended with the on-device one, and if anything fails, it quietly falls back to offline. A toggle on the home screen turns it off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing what the field test found
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The crash:&lt;/strong&gt; iOS was killing the page because loading the 176 MB fp16 model needed several copies in memory at once. Phones now get the 4-bit model, and there's a crash guard: if the page dies while loading the model, the next launch automatically drops to the lightest setting instead of crashing in a loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"That's a rock" about nothing:&lt;/strong&gt; before identifying, the app now checks whether the photo shows a rock, mineral or fossil at all, by comparing it against descriptions of both specimens and non-specimens (an empty hand, the sky, a room). If not, it says so. Any result under 35% confidence now shows as &lt;strong&gt;Unknown&lt;/strong&gt; with its closest matches, instead of a confident wrong name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fossil questions too early:&lt;/strong&gt; fixed by the question ordering described above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rare minerals coming up too often:&lt;/strong&gt; a rarity prior. Halite dissolves in rain, so it almost never lies on the ground outside deserts, and the app shouldn't jump to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this app, open models weren't just a nicer choice. Several features only work because of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works with no signal.&lt;/strong&gt; Good rocks are in road cuts, creek beds and canyons, where you usually have no bars. A closed vision API is useless there. Because SigLIP 2 is open, its weights sit on the phone and run in the browser. The geology lookup is open data too, cached ahead of time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could host the weights myself, and that saved the project.&lt;/strong&gt; On the first real test, the model wouldn't download. My network was blocking huggingface.co entirely. With a closed API that would have been the end of it. Because the weights are open, I bundled them into the app and served them from my own Cloudflare site, split into 20 MB pieces the browser stitches back together. Now the app works on any network that can load it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could swap models and measure.&lt;/strong&gt; I benchmarked five open backbones on my own data and switched to the winner for an 11-point gain, without asking anyone's permission or changing any API contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could train it on my own rocks.&lt;/strong&gt; Museum and web photos don't look like a phone photo of a dusty pebble in your palm. With open weights I train my own layer on top of the model and keep adding my own field photos. Every crinoid I photograph makes the next ID better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your location stays on your phone.&lt;/strong&gt; Offline mode sends nothing anywhere. Online mode sends a photo and a bedrock name to a model I chose and deployed, not your coordinates to a company whose retention policy I'd have to trust. Where you find good rocks is something collectors like to keep to themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; On-device inference is free. Hosting is on Cloudflare's free tier, and online mode fits in Workers AI's free daily allowance, roughly 120 to 160 photo IDs a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything underneath is open.&lt;/strong&gt; Open models (SigLIP 2, Llama 4 Scout), open runtimes (Transformers.js, ONNX Runtime), open data (Macrostrat, GBIF, Wikimedia Commons). It's also honest about its limits: it shows you the odds, the closest alternatives, and when it doesn't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built the app with the help of Claude, where I asked the agent to do the user interface and wiring so I could focus on the logic and the AI under the hood.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Stay on topic with trance</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 03 Oct 2026 18:04:59 +0000</pubDate>
      <link>https://dev.to/gavin_rose/stay-on-topic-with-trance-2kb5</link>
      <guid>https://dev.to/gavin_rose/stay-on-topic-with-trance-2kb5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;trance&lt;/strong&gt; is a small Mac app that keeps you focused on what matters most to you in the moment.&lt;/p&gt;

&lt;p&gt;To start, just type what you're doing ("answer emails", "study for my upcoming assignment", "do anything except doomscroll"), pick a track, and get on with your day. Every 45 seconds, a Gemma model running locally looks at what you've actually been doing. From there, it decides whether to stay quiet, nudge you back, or tell you it's time for five minutes away from the screen.&lt;/p&gt;

&lt;p&gt;I built this for my friend that has mentioned previously having a hard time staying on track. He was looking for something that could help him focus, provide insights, and gently keep him on track. &lt;/p&gt;

&lt;p&gt;Most focus apps block the websites or applications you don't want to visit: you list the bad sites and they slam the door. In my opinion, that doesn't fit how people actually drift away from their focused tasks. YouTube is a distraction until it's the lecture you need. Slack is a distraction until you're clearing your inbox. This app doesn't block anything. Instead, it tries to understand what you're doing, compared with what you said you wanted to do.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notices drift.&lt;/strong&gt; Each window you land on gets labeled against your intent: on-task or off-task, plus what kind of thing it is (work, reference, feed, video, game, etc.). The live view shows the label a few seconds after you switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nudges.&lt;/strong&gt; When you continue drifting, a small card slides into view, just like a notification. This nudge is intended to bring you back on track without disruption, but also helps the model judge better throughout the session if you tell it that whatever you're doing is actually on-task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommends breaks.&lt;/strong&gt; Each track has a working period and a recommended break period. If you remain on task, this app will let you continue working. If you start drifting, tab switching, or seem otherwise unfocused, it will suggest taking a break and show a nice calming visual during the break. Breaks are 5 minutes, but you can come back early or add time if you think you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracks.&lt;/strong&gt; Deep Work, Study, Create, Admin Sprint and Cruise each set what on-task looks like, how much switching is normal and how often breaks come. You can also make your own track if you need something a little more customized, along with setting the intent for the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recaps.&lt;/strong&gt; Whenever you end a session, Gemma writes a short summary of how the session went, any potential time sinks, what you did well, and what you could do better next time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&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%2Fjpeoin1gzdlq2x71xnro.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%2Fjpeoin1gzdlq2x71xnro.png" alt=" " width="800" height="1340"&gt;&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1o599gicb54t3ogojzs0.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%2F1o599gicb54t3ogojzs0.png" alt=" " width="800" height="1340"&gt;&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl5noqjzlt6wg6bzjynqc.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%2Fl5noqjzlt6wg6bzjynqc.png" alt=" " width="800" height="1340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The open pieces:&lt;/strong&gt; &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3&lt;/a&gt; 4B (instruction-tuned), running locally in &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, inside a &lt;a href="https://tauri.app" rel="noopener noreferrer"&gt;Tauri&lt;/a&gt; app written in Rust.&lt;/p&gt;

&lt;p&gt;Gemma reads, then the Rust code decides. Every 45 seconds the model returns a structured opinion of how your focus is going. I used Ollama's &lt;code&gt;format&lt;/code&gt; field, which takes a JSON schema, so that the answer always parses properly. Then plain code applies the rules: drift has to show up in two reads in a row before trance says anything, nudges have cooldowns, and flow is never interrupted.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a 4B model got wrong, and what fixed it
&lt;/h3&gt;

&lt;p&gt;This is the part that required the most time and attention. My first version of the prompt gave Gemma all of the raw activity logs and asked for a verdict. On a 12B Gemma model, this was a pretty good approach. The problem is that a 12B model running constantly while you're doing other work eats a lot of resources, and some people may not have the computer to handle that. When I switched to a 4B model and did a test run, the model decided that me spending 90% of my time on distractions instead of the task at hand was on-task. To combat this, the model now gets the window information and labels it, the Rust code calculates switch rates, scroll rates, typing, on-task time, etc., and the model then gets this structured information rather than being expected to do all of the calculations itself and getting less than ideal results.&lt;/p&gt;

&lt;p&gt;Another problem was stale labels. Each window gets labeled the first time you open it, and that label sticks for the session. A browser card game got marked as work and stayed that way, making me look far more on-task than I was. Now the labeler also gets a screenshot of the window, and if a later check-in sees the screen and disagrees with the label, the label is corrected on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Because of what trance has to look at.&lt;/strong&gt; To do its job, trance sees every window title you have open, which sites you're on, and how much you're typing and scrolling. With the glance feature on, it sees a capture of your screen about every 45 seconds. With an open model in Ollama, the most private data trance handles never leaves the workstation or laptop. Screenshots are read and deleted, and session logs are plain JSONL files in the user's own directories. No third parties peeking at your usage, no training being done on your data, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost and customization.&lt;/strong&gt; If you're doing a full work day session, you're probably going to leave this running in the background, even while away from the computer. Running locally, each check-in costs nothing but a bit of battery; on a metered API, a full work day in the background adds up fast. Using Ollama also gives you access to a whole plethora of open models, so you can swap them on command to see what works best for your situation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where a closed model would have been easier:&lt;/strong&gt; To be honest, a big hosted model would have needed fewer guardrails and probably would have produced better results more quickly. Most of my weekend went into making a 4B model reliable, but the solutions to those pesky edge cases turned into features that otherwise might not have been considered. The rules that decide whether trance interrupts you are just code that anyone can read: no black box, and no server calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I've always had a weak spot with frontend development and making things look professional, clean, and polished. Because I used Claude Code to create the user interface, I was able to spend my time where I shine best, building the functionality. Claude also helped me track down a few bugs with rendering and permission issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 4B runs locally through Ollama. It reads user intent, labels windows (with vision), judges focus every 45 seconds, and writes the session recap, all with no cloud calls!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Atlas</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sun, 19 Apr 2026 19:25:20 +0000</pubDate>
      <link>https://dev.to/gavin_rose/atlas-learn-about-the-earth-cio</link>
      <guid>https://dev.to/gavin_rose/atlas-learn-about-the-earth-cio</guid>
      <description>&lt;h1&gt;
  
  
  Explore Earth, One Swipe at a Time
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;Some of us spend hours every day scrolling. The format is compelling, and the habit is already formed. The question we asked ourselves was what if that scroll actually taught you something?&lt;/p&gt;

&lt;p&gt;Atlas takes the interaction pattern everyone already knows and fills it with things worth knowing about the planet we live on... things like geology, ecology, ocean science, extreme weather, wildlife, and geography. No headlines, no opinions. Just Earth facts, beautifully presented.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Atlas is a vertical swipe feed of Earth fact cards, built and deployed as a Flutter web app.  Beyond the content, Atlas has a full progression system aimed to keep people engaged as they learn about the Earth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XP and levels&lt;/strong&gt;: you earn XP for every new card you read, every quiz you reveal, and every card you save. The XP system has eight explorer levels from Seedling to World Wonder, and each level unlocks a new ambient visual theme (Ocean, Forest, Dusk, Aurora, Glacier, Desert, Volcanic).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Achievements&lt;/strong&gt;: ten unlockable achievements tied to meaningful milestones; exploring five nature cards, saving five cards, revealing five quizzes, exploring all categories, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily streak&lt;/strong&gt;: the app tracks consecutive days of use and surfaces your streak in the HUD once it hits day two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic content&lt;/strong&gt;: the feed mixes 60+ curated cards with live data pulled from four public APIs, refreshed every 24 hours: NASA APOD (astronomy images), USGS Significant Earthquakes (real-time seismic events), Wikipedia article summaries (25 curated Earth topics, four chosen at random per session), and Open Trivia DB (science and geography questions turned into Real or Fake cards).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How We Built It
&lt;/h2&gt;

&lt;p&gt;The entire app is written in Flutter, hosted on Firebase Hosting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack/Packages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flutter/Dart&lt;/li&gt;
&lt;li&gt;Provider for state management&lt;/li&gt;
&lt;li&gt;SharedPreferences for local persistence&lt;/li&gt;
&lt;li&gt;http for API calls&lt;/li&gt;
&lt;li&gt;flutter_animate for card entry animations and toast transitions&lt;/li&gt;
&lt;li&gt;share_plus for native share sheet&lt;/li&gt;
&lt;li&gt;Firebase Hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges We Ran Into
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Swipe-one-card-at-a-time.&lt;/strong&gt; Flutter's PageView will happily let you queue multiple &lt;code&gt;animateToPage&lt;/code&gt; calls, which causes the view to snap to the wrong card when the user swipes quickly. The fix was disabling the PageView's built-in gesture handling entirely and replacing it with a manual drag detector and an animation lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accomplishments We're Proud Of
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A scroll experience that feels genuinely close to TikTok or Reels, built entirely in Flutter for web, with no JavaScript.&lt;/li&gt;
&lt;li&gt;A progression system that changes the visual environment of the app as you level up, so the reward is aesthetic and immediate, not just a number going up.&lt;/li&gt;
&lt;li&gt;Live data from four different public APIs, unified into the same card format as the curated content, with graceful offline fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;Short form content, especially as text, forces content to be better. Writing 60+ cards where every headline had to land in under 15 words was harder than any of the actual programming or engineering! XD&lt;/p&gt;

&lt;p&gt;We observed that while typically Flutter can be lacking for web development, when developing for designs that are mobile-first, Flutter was still able to shine although it felt like it took a little longer than doing something react-based may have taken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User-generated cards&lt;/strong&gt;: a simple submission form where anyone can contribute a fact, queued for moderation before it enters the live feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More API sources&lt;/strong&gt;: NOAA for weather extremes, iNaturalist for real-time species observations, NASA Earth imagery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personalisation&lt;/strong&gt; — mood-based filtering (Relax / Curious / Mind-Blown tags are already on every card, just not yet surfaced in the UI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native mobile builds&lt;/strong&gt;: the codebase is already cross-platform; the only web-specific concession is SharedPreferences over localStorage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Built With
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;flutter&lt;/code&gt; &lt;code&gt;dart&lt;/code&gt; &lt;code&gt;firebase&lt;/code&gt; &lt;code&gt;nasa-api&lt;/code&gt; &lt;code&gt;usgs-api&lt;/code&gt; &lt;code&gt;wikipedia-api&lt;/code&gt; &lt;code&gt;open-trivia-db&lt;/code&gt; &lt;code&gt;provider&lt;/code&gt; &lt;code&gt;shared-preferences&lt;/code&gt; &lt;code&gt;flutter-animate&lt;/code&gt; &lt;code&gt;glassmorphism&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;The repo is publicly available &lt;a href="https://github.com/gavin-rose/Atlas" rel="noopener noreferrer"&gt;here&lt;/a&gt; &lt;br&gt;
The build itself can be found &lt;a href="https://atlas-26.web.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>Scarab | Weekend Data</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 18 Apr 2026 12:48:38 +0000</pubDate>
      <link>https://dev.to/gavin_rose/scarab-weekend-data-38i7</link>
      <guid>https://dev.to/gavin_rose/scarab-weekend-data-38i7</guid>
      <description>&lt;p&gt;Scarab can give you a summary on what data is missing from your data with only a few clicks&lt;br&gt;
&lt;a href="https://debugr.gumroad.com/l/scarab" rel="noopener noreferrer"&gt;Download Now&lt;/a&gt;&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2F4b01u06r9ic36k4d33i1.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.amazonaws.com%2Fuploads%2Farticles%2F4b01u06r9ic36k4d33i1.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>datascience</category>
      <category>data</category>
      <category>rust</category>
    </item>
    <item>
      <title>The new way to explore data</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Thu, 16 Apr 2026 17:12:37 +0000</pubDate>
      <link>https://dev.to/gavin_rose/the-new-way-to-explore-data-1i2</link>
      <guid>https://dev.to/gavin_rose/the-new-way-to-explore-data-1i2</guid>
      <description>&lt;p&gt;Scarab was built to allow people with no programming or statistical language experience analyze their data. It produces code snippets for those that want to bring insights into their own program, and can help gain valuable insights from your data as you progress. Free while in the testing phase!&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://debugr.gumroad.com/l/scarab" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;debugr.gumroad.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fq7lezp679wovmwj3m6sd.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.amazonaws.com%2Fuploads%2Farticles%2Fq7lezp679wovmwj3m6sd.png" alt="Natural language analysis" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>data</category>
      <category>statistics</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
