<?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: Andy Schelb</title>
    <description>The latest articles on DEV Community by Andy Schelb (@plaidscientist).</description>
    <link>https://dev.to/plaidscientist</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%2F4100476%2Fecefadf5-815f-4bed-abbf-12b22d2847b2.jpg</url>
      <title>DEV Community: Andy Schelb</title>
      <link>https://dev.to/plaidscientist</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/plaidscientist"/>
    <language>en</language>
    <item>
      <title>Doodle Gallery - a Moderated art wall</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:09:00 +0000</pubDate>
      <link>https://dev.to/plaidscientist/doodle-gallery-a-moderated-art-wall-3b4g</link>
      <guid>https://dev.to/plaidscientist/doodle-gallery-a-moderated-art-wall-3b4g</guid>
      <description>&lt;p&gt;A few weeks ago I added a Gallery to &lt;a href="https://discodoodle.com" rel="noopener noreferrer"&gt;Disco Doodle&lt;/a&gt; — my little side project with drawing-prompt generators (Daily Doodle, Monster Maker, Theme Park Edition) — so people could actually show off what they drew instead of the prompts just disappearing into the void. Simple idea. But I didn't know how much would go into doing something like this. &lt;/p&gt;

&lt;p&gt;I wanted to be sure to have a moderation component because, the internet being the internet, people are always trolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The Gallery is pretty straightforward on the surface: a person draws something from one of the prompt generators, snaps a photo, and submits it through a simple form. But nothing shows up on the public Gallery page until I've actually looked at it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A submission form (&lt;code&gt;wall/submit.html&lt;/code&gt;) that takes the photo plus which pack/prompt it was for&lt;/li&gt;
&lt;li&gt;Every submission lands in a private moderation queue — invisible to the public, only I can see it&lt;/li&gt;
&lt;li&gt;I get an email notification the moment something's waiting on me&lt;/li&gt;
&lt;li&gt;I approve or reject from an admin page, and only approved photos ever hit the public Gallery&lt;/li&gt;
&lt;li&gt;A basic profanity filter runs client-side too, in case someone tries to get something inappropriate into a custom category text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffn5mz12lef1gwiu712w9.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%2Ffn5mz12lef1gwiu712w9.png" alt=" " width="739" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;Nothing fancy on purpose — I wanted this running on the same free-tier, no-backend-server stack the rest of the site uses, because it's a hobby project and I'm not trying to run a server for it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Firebase Realtime Database&lt;/strong&gt; holds the moderation queue. Submissions write in as "pending," and only get flipped to visible once I approve them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudinary&lt;/strong&gt; hosts the actual images. We chose them because of the completely free tier. Another version we looked at had a free tier, but you had to have a credit card incase the volume got too high. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EmailJS&lt;/strong&gt; fires off a notification email when something new comes in, so I'm not manually checking a queue every day. Free tier caps at 200 requests a month, which appears to be plenty for the moment.&lt;/li&gt;
&lt;li&gt;A plain admin page, gated behind Google Sign-in through Firebase Auth, is where I actually approve or reject things&lt;/li&gt;
&lt;li&gt;The whole moderation page is blocked in &lt;code&gt;robots.txt&lt;/code&gt; too, since there's no reason for it to show up in search results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6uocfldca99czlwjuf48.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%2F6uocfldca99czlwjuf48.png" alt=" " width="625" height="826"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It all seemed to work ok. There was some back and forth on getting the google services side wired up and being sure I copied the right text into the right spaces in the setup.&lt;/p&gt;

&lt;p&gt;The profanity filter is intentionally basic — it normalizes leetspeak, collapses repeated letters, and matches whole words so it doesn't flag things like "classic." It's client-side, so it's a deterrent, not a guarantee — a determined person could get around it. &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%2Fki7i84j45wduog8jp0oa.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%2Fki7i84j45wduog8jp0oa.png" alt=" " width="564" height="776"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We added a way for someone to send an email to notify us if their artwork was uploaded to our site without their permission. We also put a disclaimer checkbox, making the person agree that it's their artwork and reminding them that we're not claiming ownership of it. &lt;/p&gt;

&lt;p&gt;Do you have any questions about this build? What should we have done differently? Let us know in the comments. &lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>firebase</category>
    </item>
    <item>
      <title>I'm Not a Developer, But I Directed an AI to Build a Real Internal Tool</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:09:00 +0000</pubDate>
      <link>https://dev.to/plaidscientist/im-not-a-developer-but-i-directed-an-ai-to-build-a-real-internal-tool-5co6</link>
      <guid>https://dev.to/plaidscientist/im-not-a-developer-but-i-directed-an-ai-to-build-a-real-internal-tool-5co6</guid>
      <description>&lt;p&gt;A few months ago I sat down and described a broken process to an AI, piece by piece, until it wasn't broken anymore. I'm not a developer. I didn't read documentation. I just refused to stop until the thing actually worked. This is the story of that process — what it replaced, what it took to build, and the three separate times it broke in ways that made the final version better.&lt;/p&gt;

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

&lt;p&gt;Part of my job involves collecting the same kind of repetitive information from a group of people, on a regular schedule, and turning it into a finished document that has to go somewhere else for approval. Think: expense-style reporting. Everyone fills something out, attaches proof of what they spent, and it all needs to land in a consistent, complete format — not five slightly different versions that someone then has to clean up by hand.&lt;/p&gt;

&lt;p&gt;That "someone" was me.&lt;/p&gt;

&lt;p&gt;Everyone wanted to help, but when there's not one consistent format each person's info was just a little bit off. Every submission had 2-3 things to correct. Sometimes involving a back and forth email chain requesting info and waiting on the response. &lt;/p&gt;

&lt;p&gt;The first fix was simple: a basic online form that at least got everyone's submissions into a consistent shape and emailed to me automatically. That alone cut my own processing time by roughly half to two-thirds. It was a real improvement. It also had a hard ceiling — it could collect information, but it couldn't &lt;em&gt;think&lt;/em&gt;. It couldn't look at what someone submitted and decide whether it was actually complete. It couldn't assemble anything. It just handed me a pile of slightly-more-organized raw material and I still did the rest by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't just buy something off the shelf
&lt;/h2&gt;

&lt;p&gt;Whatever I built had to live inside tools my organization already trusted. That ruled out a lot of otherwise reasonable options — new software means procurement reviews, security reviews, and a price tag, and none of that was going to happen for an internal process serving a couple dozen people. It had to be free, it had to work on a phone in a parking lot, and it couldn't require anyone else to install anything or sign up for anything new.&lt;/p&gt;

&lt;p&gt;That combination — free, already-trusted, zero new sign-ups — pointed toward building something custom inside the Workspace/Office ecosystem the organization already used, rather than adopting a new product.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually got built
&lt;/h2&gt;

&lt;p&gt;Here's the part I want to be honest about, because I think the honest version is more useful to other non-engineers than the impressive-sounding one: I didn't teach myself to code. I sat down with an AI assistant and described the problem — what the people submitting needed, what the final output had to look like, what was breaking in the old process — and we built it together, piece by piece, testing it against real submissions the whole way.&lt;/p&gt;

&lt;p&gt;My job in that process wasn't writing syntax. It was staying relentlessly clear on what "done" actually meant, and refusing to accept an answer that technically worked but didn't solve the real problem.&lt;/p&gt;

&lt;p&gt;I wanted to sometimes yell at the computer because it kept suggesting answers that either didn't solve my problem, or solved it, but in a way that our users wouldn't be able to access or input info. &lt;/p&gt;

&lt;p&gt;This is where my problem solving skills were put to the test and stretched to the limit. &lt;/p&gt;

&lt;p&gt;I think that's the actual skill that mattered here, more than any programming knowledge would have: knowing the process well enough, and caring about it enough, to keep saying "not quite" until it was actually right.&lt;/p&gt;

&lt;h2&gt;
  
  
  It wasn't a straight line — three real breaks
&lt;/h2&gt;

&lt;p&gt;None of this worked perfectly the first time. Three separate failures stand out, because each one taught me something I wouldn't have known to ask about in advance.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The attachment that claimed to exist but didn't
&lt;/h3&gt;

&lt;p&gt;Early on, a real submission came back and something the person had attached wasn't actually in the finished document. The tool had technically logged that a file existed — it even wrote a line saying so — but it had never actually embedded or attached it anywhere. It was a confident, specific, completely false claim. That's a special kind of dangerous bug: not a crash, not an error message, just quiet, plausible-looking data loss.&lt;/p&gt;

&lt;p&gt;The fix wasn't a patch — it was a rule change. Now nothing can be marked as "included" unless the system can prove it actually embedded the file, and every attachment gets its own dedicated page in the final document instead of a thumbnail or a link that can quietly go stale.&lt;/p&gt;

&lt;p&gt;Yeah it did seem like at this moment that the solution was going to work in theory, but not in real live situations. &lt;/p&gt;

&lt;h3&gt;
  
  
  2. The link that looked fine and did nothing
&lt;/h3&gt;

&lt;p&gt;I tried to build a simple companion page linking out to the tool — logo, a button, one clear call to action. It rendered beautifully on the first platform I tried. Then I clicked the button. Nothing happened. I tried a second platform. Same result: looked perfect, dead on click. It took a third attempt, and a fair amount of research into how each platform actually sandboxes embedded content, before I landed on an approach using that platform's own native building blocks instead of injected custom code — which turned out to be the one thing that reliably kept links clickable.&lt;/p&gt;

&lt;p&gt;Was I going crazy? Was it me? The browser? The User? Was it me again? I can't accurately convey how frustrating it is to not be able to figure out why something isn't working. &lt;/p&gt;

&lt;h3&gt;
  
  
  3. The instructions that quietly fell out of date
&lt;/h3&gt;

&lt;p&gt;I'd built a step-by-step guide early on to help people use the tool. Then the tool kept improving — which was good — except the guide didn't improve with it. It was still showing an older version of the interface by the time I actually looked at it again. The fix was less "patch the guide" and more "stop hand-maintaining it" — I rebuilt the process so the guide gets generated fresh from the real, current version of the tool every time, instead of being a snapshot that quietly rots.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does now
&lt;/h2&gt;

&lt;p&gt;The finished version handles the things that made the old process so heavy by hand: it knows who's submitting and fills in what it can predict, it enforces that nothing gets submitted incomplete (no more chasing someone down after the fact for a missing attachment), it turns multiple file types into a single consistent finished document automatically, and it delivers that finished document the moment someone hits submit — no manual assembly step left for me to do at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;I started out trying to build a slightly better form. I ended up with a process that takes fewer touches from everyone and produces a better result than the original had. What we ended up with: a free, mobile-friendly tool that auto-fills each traveler's home office, handles multiple trips per submission with address auto-fill, requires a receipt on every expense before it'll let you submit, and emails a submission-ready packet the moment someone hits Submit.&lt;/p&gt;

&lt;p&gt;It's all built out using a google script in a web app (which I did not know was a thing before this), and it assembles it all seamlessly. I started out building a form, and came away with a process that takes less touches and accomplishes more.&lt;/p&gt;

&lt;p&gt;Have you ever started to solve a problem and get a much better solution than you set out to find? &lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>productivity</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>More than a smiley face...creating expressions</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 30 Sep 2026 14:09:00 +0000</pubDate>
      <link>https://dev.to/plaidscientist/more-than-a-smiley-facecreating-expressions-2k1e</link>
      <guid>https://dev.to/plaidscientist/more-than-a-smiley-facecreating-expressions-2k1e</guid>
      <description>&lt;p&gt;When you first start drawing you learn a smiley face. It's easy, it's simple, it's universally understood to mean happy.&lt;/p&gt;

&lt;p&gt;I made &lt;a href="https://discodoodle.com/index.html" rel="noopener noreferrer"&gt;Disco Doodle&lt;/a&gt; as a prompt generator for drawing. The natural outgrowth of that was thinking about characters and expressions. If I wanted to improve my subjects, they can't just all be the classic happy or sad. They could use some real emotions! &lt;/p&gt;

&lt;p&gt;I wanted a prompt that forced a little more range: spin a wheel, land on a feeling, draw &lt;em&gt;that&lt;/em&gt; face on your character.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;Expressions&lt;/strong&gt; — a spinning wheel of emoji "faces" that lands on top of a hand-drawn character body, shows a plain-language definition of the feeling, and keeps a little history so you can see what you've drawn so far.&lt;/p&gt;

&lt;p&gt;Here's roughly where it landed:&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%2Fr9m7zw0pchp29r6kiqf9.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%2Fr9m7zw0pchp29r6kiqf9.png" alt=" " width="450" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with a spec, not a sketch
&lt;/h2&gt;

&lt;p&gt;I didn't have art yet, so the first version was built against a full written spec: a three-tier taxonomy of feelings (Surface Moods, Deep Waters, Complex Blends), a spinning wheel behind a circular "head" viewport, sound effects, and a rolling history of the last five spins. The plan going in was: build the whole interaction against a placeholder, then swap in real art once it existed — as opposed to designing around a fixed drawing from day one.&lt;/p&gt;

&lt;p&gt;That turned out to matter, because the real art changed the layout more than once (more on that below).&lt;/p&gt;

&lt;p&gt;The taxonomy itself went through a few passes of quality control. Early on it included feelings like "Hopeful" and "Nostalgic" that don't really have a dedicated face emoji — you end up reaching for something like 🙏 or 🌇 that doesn't actually read as a &lt;em&gt;face&lt;/em&gt;, which defeats the point of a drawing prompt. Anything without a genuine face emoji got cut. It also went through a couple of rounds of "does this word actually mean what the emoji looks like" — 😬 ended up as "Nervous" rather than "Conflicted," 😐 became "Meh" instead of "Calm," and 🫠 landed on "Overwhelmed" with a definition that nods at embarrassment too, since that melting-face emoji covers a specific mix of feelings that no single word quite captures on its own.&lt;/p&gt;

&lt;p&gt;Final lineup: 25 feelings across the three tiers — 8 surface, 10 deep, 7 complex.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EMOTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="c1"&gt;// surface&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;surface&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;😊&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;feeling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Happy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Happy is that warm, bright feeling you get when something good happens...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;surface&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;😐&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;feeling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Meh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Meh is that shrug feeling when you don't feel excited or upset about something...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// deep&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;😬&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;feeling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nervous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Nervous is that jittery, jumpy feeling in your stomach right before something big...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// complex&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🫠&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;feeling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Overwhelmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Overwhelmed is when so much is happening at once — maybe you're embarrassed too — that you just smile and think 'this is fine'...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...25 total&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting real art into a responsive layout
&lt;/h2&gt;

&lt;p&gt;Once I had a hand-drawn body — t-shirt, pants, shoes, no head, on plain white paper — the job was turning a photo of paper into something that could sit believably underneath a spinning emoji.&lt;/p&gt;

&lt;p&gt;That meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alpha-keying the drawing.&lt;/strong&gt; Instead of a white background, the ink line work needed to become a transparent PNG so it could layer under the spinning head. I inverted the luminance of the scan and used that as the alpha channel, then recolored the remaining ink to match the site's actual brand color, so it doesn't look like a pasted-in scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cropping tight.&lt;/strong&gt; Bounding-box crop down to the art itself, plus a small safety margin, so there's no dead space around the body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compressing it.&lt;/strong&gt; Ran the result through &lt;code&gt;pngquant&lt;/code&gt; to bring the file size down before embedding it as a base64 &lt;code&gt;data:&lt;/code&gt; URI directly in the page — no separate image request, no loading flash.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;129KB scanned PNG → alpha-keyed, recolored, cropped → ~26KB compressed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trickier part wasn't the image processing — it was getting the emoji to sit exactly where a head should sit, &lt;em&gt;responsively&lt;/em&gt;, across every screen width. The first pass used fixed pixel values for the head size and wheel geometry while the body image itself scaled fluidly with &lt;code&gt;width: 100%&lt;/code&gt;. That's a bug waiting to happen: on a narrow phone screen, the art scales down but the head window doesn't, and suddenly the emoji is floating in the wrong spot.&lt;/p&gt;

&lt;p&gt;The fix was to stop hardcoding pixels entirely and make everything a percentage of the containing stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.stage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;784&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.wheel-viewport&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--head-top&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--head-size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&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;Then, since the wheel's tile height still needs to be an actual pixel number for the animation math, I measure the rendered viewport at runtime instead of assuming a value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;measureItemH&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wheelViewport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ITEM_H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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;...with a debounced &lt;code&gt;resize&lt;/code&gt; listener calling it again whenever the layout changes. That combination — percentage-based CSS plus a JS layer that measures rather than assumes — is the part of this build I'd reach for again on basically any "art that has to line up with an animated element" problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mockup pivot: emoji in front, body behind
&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%2F7w5ajal3t1h58oaqnczf.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%2F7w5ajal3t1h58oaqnczf.png" alt=" " width="480" height="950"&gt;&lt;/a&gt;&lt;br&gt;
This was how Claude saw the idea orginally.&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%2Fr5lolzun4z6vm7738h2q.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%2Fr5lolzun4z6vm7738h2q.png" alt=" " width="360" height="800"&gt;&lt;/a&gt;&lt;br&gt;
This was my first sketch mockup where I knew we were close.&lt;/p&gt;

&lt;p&gt;I initially had the drawing layered &lt;em&gt;above&lt;/em&gt; the wheel, with the emoji peeking out through a circular cutout — basically a porthole. It looked fine. Then I got a mockup with a big, expressive emoji head and a small, simple body underneath, with a note that the emoji should be the visual focus and the body should read as secondary — layered &lt;em&gt;under&lt;/em&gt; the head, even mid-spin, not equal to it.&lt;/p&gt;

&lt;p&gt;That was a real "redo the compositing" moment, not a tweak: it meant flipping the z-index relationship between the two layers and resizing the head window to match the proportions in the mockup rather than the proportions I'd guessed at originally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wheel-viewport&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c"&gt;/* head: on top now */&lt;/span&gt;
&lt;span class="nc"&gt;.body-image&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c"&gt;/* body: underneath, always */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I matched the mockup's proportions by overlaying a measurement grid on the reference image and then checking the live implementation against those same ratios with a headless browser, reading back the actual rendered bounding boxes rather than eyeballing it. It's a small technique but a reliable one whenever someone hands you a rough mockup and says "make it look like this."&lt;/p&gt;

&lt;p&gt;I was so happy when it finally looked like I wanted. We'd come a long way to get Claude to understand exactly what I was asking for. Sometimes it even felt like I was just in an hours long argument, trying to restate my case so that Claude could do exactly what I wanted.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Sound design, and a rejected experiment
&lt;/h2&gt;

&lt;p&gt;The spin needed sound — something in the spirit of a game-show wheel, not a system beep. This went through more iterations than anything else in the build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First pass:&lt;/strong&gt; a simple square-wave tick on every tile that scrolls past, plus a soft chime on landing, all synthesized with the Web Audio API (no audio files, just oscillators and gain envelopes) — no strong opinion here initially, just "does it make noise."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second pass:&lt;/strong&gt; slowing the whole animation down and giving it a two-phase feel — fast at first, then easing hard into a slow, dramatic settle, like it's building anticipation for which face you're about to get. This also got a filtered noise-burst "clack" instead of a plain tone, to feel more like a physical wheel clicker.&lt;/p&gt;

&lt;p&gt;That second version got a clean "I don't like this one, go back" — which is a completely normal thing to happen partway through a build, and honestly a useful signal: the two-phase timing &lt;em&gt;sounded&lt;/em&gt; more sophisticated in my head than it &lt;em&gt;felt&lt;/em&gt; in practice. So it got fully reverted — the phase-split logic, the extra easing curve, the noise-burst clack, all of it — back to the original single continuous ease with a plain, simple tick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;easeOutQuint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;playTick&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;osc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOscillator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exponentialRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.06&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;And on landing, a short three-note ascending flourish (G5 → B5 → E6) instead of a single chime — enough to feel like a small "ta-da" without turning into a jingle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;playChime&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;784&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;987.77&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1318.51&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// G5, B5, E6&lt;/span&gt;
  &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// staggered oscillators, each with its own gain envelope&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;h2&gt;
  
  
  Keeping the wheel, the card, and the history honest with each other
&lt;/h2&gt;

&lt;p&gt;There's a rolling history of your last five spins, and each entry is clickable — tap an old one and it brings back that feeling's definition card. The bug that showed up here was subtle: clicking a history item updated the definition card, but not the actual face sitting on the drawing. So you could end up looking at a card that says "Bittersweet" while the drawing still shows the surprised face from two spins ago — visually incoherent in a way that undermines the whole "practice drawing this specific expression" point of the feature.&lt;/p&gt;

&lt;p&gt;The fix ties both together through one function, so there's no path where the card updates without the wheel (or vice versa):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;selectHistoryEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emotionIdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EMOTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ORDER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emotionIdx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTrackY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;ITEM_H&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// scroll the wheel to match&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// the tier filter changed since this was spun — just paint the tile directly&lt;/span&gt;
    &lt;span class="nx"&gt;tileEls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;centerIndex&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;renderCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;renderHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;That "tier filter changed since this was spun" branch matters because of the other feature in here: a slider that lets you filter the wheel down to just Surface Moods, just Deep Waters, just Complex Blends, or all of them combined. If you spin something from "all," then narrow the filter to "surface" only, a history click on that older entry can't just scroll the now-filtered wheel to it — it has to paint the tile directly instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What resets, and what doesn't
&lt;/h2&gt;

&lt;p&gt;Worth calling out directly: nothing here is saved to the browser. No &lt;code&gt;localStorage&lt;/code&gt;, no cookies — that's a deliberate, site-wide choice on Disco Doodle, not an oversight. So the history, the mute setting, and whatever face is currently showing all live in plain JavaScript memory for that page session. Reload the page, close the tab, come back later — it's a clean slate every time.&lt;/p&gt;

&lt;p&gt;It was really important to me that it's all browser based. I want a site that feels zippy to use and loads quickly. Any moment waiting and I'm putting my phone down or scrolling on to something else. &lt;/p&gt;

&lt;p&gt;We'll see where this takes me. I'm not sure what should be added next. Do you have any ideas? Post them in the comments! &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
      <category>sidebproject</category>
    </item>
    <item>
      <title>Monster Maker! It's like Daily Doodle but with 100 percent more Monsters</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:09:00 +0000</pubDate>
      <link>https://dev.to/plaidscientist/monster-maker-its-like-daily-doodle-but-with-100-percent-more-monsters-e7g</link>
      <guid>https://dev.to/plaidscientist/monster-maker-its-like-daily-doodle-but-with-100-percent-more-monsters-e7g</guid>
      <description>&lt;p&gt;&lt;a href="https://discodoodle.com" rel="noopener noreferrer"&gt;Disco Doodle&lt;/a&gt; is my hobby site — free, no-signup drawing prompt generators, a division of my little umbrella brand &lt;a href="https://theplaidscientist.github.io/" rel="noopener noreferrer"&gt;Plaid Labs&lt;/a&gt;. Daily Doodle was the original: pick a few categories (animal, occupation, prop, style...), hit spin, get a silly little drawing idea. Sometimes you just need to get past that blank page. More on that &lt;a href="https://dev.to/plaidscientist/i-vibe-coded-my-first-app-ever-a-daily-drawing-prompt-spinner-beginner-advanced-modes-sound-ble-temp-slug-6383031?preview=b762184a3071d0eebdebf6979c59cd0162fdad6d253c77bf0d990c290352d49974bd8142d01f63d6fccb724114d9dc1099f66fe84aa2e1decbbf909a"&gt;here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A while back I asked Claude to build a second generator, Monster Maker, as a straight-up fork of Daily Doodle's engine but with monster-flavored categories: eyeballs, horns, skin texture, arm style, eyestalks, patterns, a "silly / scary / sleepy" vibe dial. Same beginner/advanced modes, same spin animation, same everything under the hood — just a new skin and a new set of categories.&lt;/p&gt;

&lt;p&gt;That part went fast. What I actually want to write about is everything that happened &lt;em&gt;after&lt;/em&gt; "it works" — the design back-and-forth, a Firebase security rules rabbit hole, building a share feature that doesn't feel like an ad, and then talking an AI agent into recording its own tutorial video of the thing it just built.&lt;/p&gt;

&lt;p&gt;Monsters are different than drawing animals or people. I relate to drawing them because they can't look, "wrong". No one can say your proportions are off, or you forgot to add a nose, or hey that arm is longer than the other...because it's a monster. They only enter our world at nighttime while we're sleeping. Via a network of magical doors... oh wait. &lt;/p&gt;

&lt;h2&gt;
  
  
  Forking an app by pasting it into a fresh chat
&lt;/h2&gt;

&lt;p&gt;Small but real gotcha worth mentioning: I was working in a fresh Cowork session, and it had no memory of Daily Doodle's code — it lives on my machine / in the repo, not in the AI's head. So the very first thing that happened wasn't code, it was a clarifying question: &lt;em&gt;do you have the file, can I reach your computer, or should I build from scratch?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I pasted the whole &lt;code&gt;daily-doodle.html&lt;/code&gt; file in. From there the fork was genuinely mechanical — same CSS custom properties, same category-card component, same slot-machine spin animation, just new data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;eyeballs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Eyeballs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;👀&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card-teal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No Eyes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;One Eye&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Two Eyes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Three Eyes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Five Eyes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A Dozen Eyes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;horns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Horns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;😈&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card-blue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No Horns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;One Horn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Two Horns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Three Horns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Four Horns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A Crown of Horns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;skinTexture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Skin Texture&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🖐️&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card-purple&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hairy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Slimy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Scaly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bumpy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fuzzy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Leathery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...armStyle, eyestalks, pattern, vibe&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CORE_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eyeballs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;horns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;skinTexture&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// beginner mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sentence builder was the one piece that actually needed new logic, since "Your monster has Two Eyes and Four Horns, with Hairy skin, wiggly eyestalks..." doesn't follow the same grammar as Daily Doodle's "a [style] drawing of a [job] [animal] wearing [outfit]." Small thing, but it's the difference between a reskin and something that reads like it was actually written for monsters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The background took longer than the app did
&lt;/h2&gt;

&lt;p&gt;This is the part I'll happily admit: I could not settle on a background, and the AI just... kept building whatever I said next. Light blue polka dots. Then "big scalloped scales, olive green and khaki" (which, credit where due, it built as actual CSS &lt;code&gt;radial-gradient&lt;/code&gt; math and got right on basically the first try — no image assets, just gradients tiled in an offset grid). Then "actually just olive green with giant polka dots." Then "remove the polka dots." We landed on flat olive green.&lt;/p&gt;

&lt;p&gt;I really just didn't want it to look ugly, but wanted it to be fun when you changed from Daily Doodle to Monster Maker. A few of the tries were exactly what I asked for, and what I asked for was wrong and hideous! &lt;/p&gt;

&lt;p&gt;The one genuinely useful thing that came out of that back-and-forth: once the background got dark, the plain gray subtitle/instruction text became almost unreadable against it. Rather than just darkening the text globally, the fix was giving those bits of text the same rounded "pill" treatment the app already used for buttons and badges — so it reads as an intentional design choice instead of a patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.subtitle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;999px&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;Small detail, but it's the kind of thing that's easy to miss when you're iterating fast — worth actually looking at your own contrast, not just trusting that "it compiles."&lt;/p&gt;

&lt;h2&gt;
  
  
  A Firebase rules rabbit hole
&lt;/h2&gt;

&lt;p&gt;Both generators share one Firebase Realtime Database for their visitor counters — &lt;code&gt;/counters/dailyDoodle&lt;/code&gt; and &lt;code&gt;/counters/monsterMaker&lt;/code&gt;, two independent keys in the same JSON tree. That part's simple. What wasn't obvious to me was &lt;em&gt;why&lt;/em&gt; the counter worked for one and not the other, and whether I needed a whole second Firebase project.&lt;/p&gt;

&lt;p&gt;I ended up screenshotting my actual Firebase console rules tab a couple of times and just asking "does this help?" The short version of what I learned: Realtime Database rules can be scoped to a specific path or left wide open at the root, and my original rules were the &lt;em&gt;default test-mode&lt;/em&gt; rules — open, but with a hard expiry date baked in (&lt;code&gt;"now &amp;lt; &amp;lt;timestamp&amp;gt;"&lt;/code&gt;). That's the kind of thing that quietly breaks a hobby project months later with zero warning. We replaced it with a permanent rule scoped just to &lt;code&gt;/counters&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"counters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;".read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;".write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open where it needs to be, closed everywhere else, no expiry ticking down in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a share feature that doesn't feel like an ad
&lt;/h2&gt;

&lt;p&gt;The ask was: after you spin, let people save an image — the result sentence, a counter badge, something you'd actually want to post next to a photo of your drawing — without it screaming "please post my app." That "not overly promotional" constraint mattered more than it sounds like it should.&lt;/p&gt;

&lt;p&gt;The whole thing is a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element, never attached to the page, rendered on demand and downloaded as a PNG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildShareImageBlob&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// measure the sentence first on a scratch canvas, so the final&lt;/span&gt;
  &lt;span class="c1"&gt;// canvas can size itself to the text instead of leaving dead space&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrapCanvasText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;measureCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sentenceText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cardWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cardPadX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cardHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cardPadY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;680&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cardY&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cardHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...draw wordmark, badge, card, and a small watermark bottom-right&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&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;That "measure first, then size the canvas" step mattered a lot in practice — a short beginner-mode sentence and a long seven-category advanced-mode sentence are wildly different lengths, and a fixed-size card either wastes half the image on empty space or clips the text. Sizing the canvas to the content fixed both at once.&lt;/p&gt;

&lt;p&gt;The watermark is one small line of text in a corner, not a banner. That was the actual design decision — the whole point is that someone wants to post this next to their own art, and a self-promotional overlay works against that.&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%2Fiwwctcencd1rm1yldcct.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%2Fiwwctcencd1rm1yldcct.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a screenshot from the test of the daily doodle verison. I probably misplaced the Monster Maker one. &lt;/p&gt;

&lt;h2&gt;
  
  
  The part I didn't expect: scripting a tutorial video with a fake mouse cursor
&lt;/h2&gt;

&lt;p&gt;This is the bit I'd actually recommend trying yourself. Once the app worked, I asked for a vertical walkthrough video — and rather than screen-recording a live demo, the approach was to script an actual headless-browser session that performs the demo itself: type into the input, click spin, click share, all timed out, with a synthetic cursor and text captions injected directly into the page so they show up &lt;em&gt;in&lt;/em&gt; the recording.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tapEl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoViewIfNeeded&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boundingBox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;moveCursorTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&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 cursor and captions are just DOM elements injected via &lt;code&gt;page.evaluate()&lt;/code&gt; before the "recording" starts — a styled &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; that animates to each click target, and a caption pill that updates at each beat ("Add your own ideas to any category," "Tap Spin for an instant idea!"). Since they're real elements on the real page, Playwright's built-in video recorder captures them for free.&lt;/p&gt;

&lt;p&gt;Two bugs worth knowing about if you try this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Playwright's video recorder doesn't actually scale to the &lt;code&gt;size&lt;/code&gt; you give it.&lt;/strong&gt; If your &lt;code&gt;recordVideo.size&lt;/code&gt; is bigger than your CSS viewport, it pastes the unscaled frame into the corner of the bigger canvas instead of stretching it — you get a video that's mostly gray dead space. Fix: record at your real viewport size and let &lt;code&gt;ffmpeg&lt;/code&gt; do a proper upscale afterward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your page's own background needs to fill the full viewport height&lt;/strong&gt;, or the same dead-space problem shows up below short pages. A one-line CSS fix (&lt;code&gt;html { min-height: 100%; background: ...; }&lt;/code&gt;) injected just for the recording session solved it without touching the real site's CSS.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; raw.webm &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nv"&gt;anullsrc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;channel_layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;stereo:sample_rate&lt;span class="o"&gt;=&lt;/span&gt;44100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=1080:1920:flags=lanczos,fade=t=in:st=0:d=0.4"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; slow &lt;span class="nt"&gt;-crf&lt;/span&gt; 18 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-shortest&lt;/span&gt; &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart walkthrough.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The silent audio track is deliberate — some platforms handle video-with-no-audio-stream oddly, so &lt;code&gt;anullsrc&lt;/code&gt; gives it one for free.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the AI actually hit a wall
&lt;/h2&gt;

&lt;p&gt;I asked for a voiceover next, and this is the honest part: there's no ElevenLabs- or OpenAI-quality voice available in that sandbox. What &lt;em&gt;was&lt;/em&gt; available: &lt;code&gt;espeak-ng&lt;/code&gt; (classic robotic TTS, works instantly, zero setup) and Piper, a free offline neural TTS that's a meaningful step up in naturalness — installable in about thirty seconds, no API key, no network dependency at runtime.&lt;/p&gt;

&lt;p&gt;I still haven't posted this video. I'm not sure why. Anyone else get super self-conscious before posting to social media? Me neither! &lt;/p&gt;

&lt;p&gt;What I did get out of the AI: a clean, timed caption script broken into beats that match the video almost to the second, ready to paste into whatever TTS tool I wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0:00–0:03 — "Welcome to Monster Maker."
0:03–0:06 — "Add your own ideas to any category."
0:10–0:15 — "Tap Spin for an instant monster!"
0:18–0:20 — "...or share it as an image!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That felt like the right division of labor, honestly — the AI is great at the mechanical parts (timing, syncing captions to on-screen actions, getting the technical plumbing right) and much less useful the moment "which voice sounds right for my brand" becomes the actual question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away from this
&lt;/h2&gt;

&lt;p&gt;You should absolutely ask to get a video generated on how to use your product. It may not be great for marketing or even an explainer video. You do get to see how someone will use your idea before hitting publish. Who knows, it may give you some insight on things to tweak or change. Ways to really make your idea even shinier. &lt;/p&gt;

&lt;p&gt;If you want to see the actual result, both generators are live and free at &lt;a href="https://discodoodle.com/monster-maker.html" rel="noopener noreferrer"&gt;discodoodle.com&lt;/a&gt; — no signup, just pick your categories and hit spin.&lt;/p&gt;

&lt;p&gt;While you're there, make a Monster and doodle it up on a post-it. Share Your &lt;a href="https://discodoodle.com/gallery.html" rel="noopener noreferrer"&gt;Art&lt;/a&gt; in our &lt;a href="https://discodoodle.com/gallery.html" rel="noopener noreferrer"&gt;Doodle Gallery.&lt;/a&gt; We could always use more submissions!&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I tried to add a simple hit counter to my app and ended up learning why "simple" free APIs quietly fail on mobile</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 16 Sep 2026 14:09:00 +0000</pubDate>
      <link>https://dev.to/plaidscientist/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple-free-apis-quietly-1lnd</link>
      <guid>https://dev.to/plaidscientist/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple-free-apis-quietly-1lnd</guid>
      <description>&lt;p&gt;Social proof is good when you have a new app. I wanted people to know that others were using the app. I literally asked Claude for a 90s style website counter. I used the example of the famous burger sign that says, "billions served." &lt;/p&gt;

&lt;p&gt;&lt;a href="https://fandom.com" rel="noopener noreferrer"&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%2Ftqc8a1psjmcgdmxdokc3.webp" alt="RocDonald's Sign from The Flintstones 1994 Film" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Image source: &lt;a href="https://fandom.com" rel="noopener noreferrer"&gt;The Flintstones Wiki on Fandom&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
side note- this movie was so magical when I was a little kid!&lt;/p&gt;

&lt;p&gt;Turned out to be a good little rabbit hole, so I figured it was worth writing&lt;br&gt;
up on its own — not just "here's a counter," but what actually broke along&lt;br&gt;
the way and why.&lt;/p&gt;

&lt;p&gt;🔗 See it in action: &lt;a href="https://theplaidscientist.github.io/dailydoodle/" rel="noopener noreferrer"&gt;https://theplaidscientist.github.io/dailydoodle/&lt;/a&gt;&lt;br&gt;
💻 Code: &lt;a href="https://github.com/theplaidscientist/dailydoodle" rel="noopener noreferrer"&gt;https://github.com/theplaidscientist/dailydoodle&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Attempt 1: a free, no-signup counter API
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/plaidscientist/i-vibe-coded-my-first-app-ever"&gt;Daily Doodle&lt;/a&gt;&lt;br&gt;
is a static site on GitHub Pages — no backend, no server I control. So the&lt;br&gt;
first move was a free public counter service&lt;br&gt;
(&lt;a href="https://countapi.mileshilliard.com/" rel="noopener noreferrer"&gt;countapi.mileshilliard.com&lt;/a&gt;) — no&lt;br&gt;
account, no API key, just a &lt;code&gt;GET&lt;/code&gt; request that increments a number tied to a&lt;br&gt;
key I made up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://countapi.mileshilliard.com/api/v1/hit/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;COUNTER_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;counterEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;Worked immediately on desktop. Yay! It's working! This is gonna be so cool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it quietly stopped working on mobile
&lt;/h2&gt;

&lt;p&gt;I switched to my phone before sending the link to my friend and realized it was still at triple ---. No matter what I did, I couldn't get the counter to update.. &lt;/p&gt;

&lt;p&gt;The counter would just... not move on mobile. No error the user would ever&lt;br&gt;
see, because I'd deliberately built it to fail silently (dashes on screen&lt;br&gt;
instead of a broken-looking blank) rather than break the actual app if the&lt;br&gt;
counter service ever had a bad day.&lt;/p&gt;

&lt;p&gt;First fix I tried: fire the request two ways at once — the normal &lt;code&gt;fetch()&lt;/code&gt;&lt;br&gt;
call, plus a fallback using an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag pointed at the same endpoint,&lt;br&gt;
since some ad blockers treat image requests differently than fetch/XHR calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pixel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://countapi.mileshilliard.com/api/v1/hit/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;COUNTER_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?_=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Didn't help. Which was actually useful information — if both request types&lt;br&gt;
fail identically, that's not a request-type problem, that's the whole&lt;br&gt;
&lt;em&gt;domain&lt;/em&gt; being blocked at the network level (an ad blocker, a mobile&lt;br&gt;
carrier's filtering, a DNS-level blocklist like NextDNS/AdGuard). Generic&lt;br&gt;
counter/analytics-sounding domains get swept up in filter lists a lot more&lt;br&gt;
than people realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: Firebase instead
&lt;/h2&gt;

&lt;p&gt;The fix wasn't cleverer code — it was picking a backend domain that's&lt;br&gt;
essentially never blocklisted, because too much of the internet depends on&lt;br&gt;
it. Firebase fit: &lt;code&gt;firebaseio.com&lt;/code&gt; is Google infrastructure that a huge&lt;br&gt;
number of mainstream apps rely on, so blocklists generally leave it alone.&lt;/p&gt;

&lt;p&gt;Setup, for anyone who wants to do this on their own static site:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a free project at &lt;a href="https://console.firebase.google.com" rel="noopener noreferrer"&gt;Firebase Console&lt;/a&gt;
(no credit card needed for the free Spark plan)&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Realtime Database&lt;/strong&gt;, start it in test mode (public read/write —
fine for something as low-stakes as a number)&lt;/li&gt;
&lt;li&gt;Use Firebase's REST API directly, no SDK, no auth needed in test mode:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Read the current count&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DB_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/counters/dailyDoodle.json`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;counterEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Increment it atomically (safe even if two people spin at once)&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DB_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/counters/dailyDoodle.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.sv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;increment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;counterEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&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;That &lt;code&gt;.sv: { increment: 1 }&lt;/code&gt; bit is Firebase's server-side increment — the&lt;br&gt;
math happens on their server, not in the browser, so there's no race&lt;br&gt;
condition if two people hit spin at the same moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tradeoff
&lt;/h2&gt;

&lt;p&gt;Test mode means the database is publicly writable by anyone who finds the&lt;br&gt;
URL — genuinely fine for a number nobody can really abuse in a meaningful&lt;br&gt;
way, but worth knowing if you're reusing this pattern for anything with&lt;br&gt;
actual sensitive data. &lt;/p&gt;

&lt;h2&gt;
  
  
  Sources / further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://countapi.mileshilliard.com/" rel="noopener noreferrer"&gt;countapi.mileshilliard.com&lt;/a&gt; — the
free counter service I started with&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/database/rest/start" rel="noopener noreferrer"&gt;Firebase Realtime Database REST API docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://console.firebase.google.com" rel="noopener noreferrer"&gt;Firebase Console&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you had this problem? Did you solve it similarly or differently? &lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>firebase</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Does anyone have suggestions on who I should follow? I’m completely brand new, and every article I read so far feels very technologically deeper than my current skills. Drop comments for suggestions. I’m just a beginner.</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 09 Sep 2026 20:44:31 +0000</pubDate>
      <link>https://dev.to/plaidscientist/does-anyone-have-suggestions-on-who-i-should-follow-im-completely-brand-new-and-every-article-i-1hg2</link>
      <guid>https://dev.to/plaidscientist/does-anyone-have-suggestions-on-who-i-should-follow-im-completely-brand-new-and-every-article-i-1hg2</guid>
      <description></description>
      <category>beginners</category>
      <category>discuss</category>
      <category>learning</category>
    </item>
    <item>
      <title>I vibe-coded my first app ever — a daily drawing-prompt spinner (Beginner + Advanced modes, sound, the works)</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 09 Sep 2026 14:09:00 +0000</pubDate>
      <link>https://dev.to/plaidscientist/i-vibe-coded-my-first-app-ever-a-daily-drawing-prompt-spinner-beginner-advanced-modes-sound-181g</link>
      <guid>https://dev.to/plaidscientist/i-vibe-coded-my-first-app-ever-a-daily-drawing-prompt-spinner-beginner-advanced-modes-sound-181g</guid>
      <description>&lt;p&gt;I was at dinner and my friend asked me if I'd ever vibe coded. He was like you really should try it. It's incredible what you can do. I took web design in high school, but in the year 2000 it was all about learning HTML tags. That was the most coding I've ever done.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Daily Doodle&lt;/strong&gt; — a drawing-prompt spinner. Pick which categories&lt;br&gt;
you want in play, hit spin, and it mashes them together into something like&lt;br&gt;
"a Pencil Anime-style drawing of a Detective Octopus wearing a Hoodie, with a&lt;br&gt;
Boombox, riding a Scooter, looking Happy, in the Woods." No two spins land&lt;br&gt;
the same, and it's built to actually get used, not just looked at once.&lt;/p&gt;

&lt;p&gt;🔗 Try it: &lt;a href="https://theplaidscientist.github.io/dailydoodle/" rel="noopener noreferrer"&gt;https://theplaidscientist.github.io/dailydoodle/&lt;/a&gt;&lt;br&gt;
💻 Code: &lt;a href="https://github.com/theplaidscientist/dailydoodle" rel="noopener noreferrer"&gt;https://github.com/theplaidscientist/dailydoodle&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%2F1x5hpfr3guql6kxdmgio.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%2F1x5hpfr3guql6kxdmgio.png" alt=" " width="433" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Nine categories, all toggleable: Animal, Occupation, Prop, Vehicle, Style,&lt;br&gt;
Medium, Outfit, Mood, Setting. Each one is fully editable — add your own&lt;br&gt;
items, clear a category out completely, or restore the original starter&lt;br&gt;
list if you mess with it too much.&lt;/p&gt;

&lt;p&gt;Two modes so it doesn't overwhelm anyone on first visit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginner&lt;/strong&gt; — just the original three (Animal, Occupation, Prop), no
toggles, nothing to configure. Load it and spin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced&lt;/strong&gt; — all nine categories, each with its own on/off switch, so
you build exactly the level of chaos you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few other things that ended up in there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A live counter (backed by Firebase) tracking total spins across everyone
who's used it, styled to match the rest of the app instead of looking
like a bolted-on stat&lt;/li&gt;
&lt;li&gt;Retro synthesized sound effects — a boop-boop-boop while it spins, a tada
when it lands — built entirely with the Web Audio API, so there's no
audio file to load and nothing to license&lt;/li&gt;
&lt;li&gt;An "avoid repeats until all shown" option, so a category won't repeat an
item until everything in it has come up once&lt;/li&gt;
&lt;li&gt;A copy button that grabs the result as shareable text, plus a collapsed
"recent doodles" list holding your last 3 spins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When my oldest was little. They'd always ask me for ideas of things to draw. I'm a massive Walt Disney World fan and I've read almost anything I can on the subject of the creation and maintenance of the most magical place on earth.  &lt;/p&gt;

&lt;p&gt;I remember reading this quote from the head of Disney Imagineering:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“There are two ways to look at a blank sheet of paper. It can be the most frightening thing in the world, because you have to make the first mark on it. Or it can be the greatest opportunity in the world, because you get to make the first mark—you can let your imagination fly in any direction, and create whole new worlds!”&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Marty Sklar&lt;/strong&gt;, Creative Leader of Walt Disney Imagineering, from his memoir &lt;a href="https://www.scribd.com/document/634991348/Dream-It-Do-It-My-Half-Century-Creating-Disneys-Magic-Kingdoms-Sklar-Marty" rel="noopener noreferrer"&gt;&lt;em&gt;Dream It! Do It!: My Half-Century Creating Disney's Magic Kingdoms&lt;/em&gt;&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was his go to when in 1974 when taking over Walt Disney Imagineering to help the team overcome paralysis and self-doubt. &lt;/p&gt;

&lt;h2&gt;
  
  
  Github, Pull, Push, and endless screenshots asking, Am I doing this right?
&lt;/h2&gt;

&lt;p&gt;I'd not coded before. I pulled some software for my 3d printers from github years and years ago, so it existed and what it was vaguely for, but I didn't have any idea how to use it. &lt;/p&gt;

&lt;p&gt;I was super nervous that my human touch would be the part where the ideas would break. &lt;/p&gt;

&lt;p&gt;The stack is about as simple as it gets: one &lt;code&gt;index.html&lt;/code&gt; file, vanilla&lt;br&gt;
JavaScript, no framework, no build step, no local dev environment — I edit&lt;br&gt;
it directly in the GitHub web UI. In the beginning I was copy and pasting full html files to edit the site because I wasn't sure how to use github desktop. &lt;/p&gt;

&lt;p&gt;It's hosted completely free on GitHub Pages, and the whole thing — including&lt;br&gt;
the live counter — costs nothing to run.&lt;/p&gt;

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

&lt;p&gt;This is the second entry in &lt;strong&gt;Plaid Labs&lt;/strong&gt;, a little hub of web experiments&lt;br&gt;
I keep building and then figuring other people might want too. First one was&lt;br&gt;
&lt;a href="https://dev.to/plaidscientist/i-built-a-checklist-for-droid-tycoon-fusion-recipes-so-id-stop-scrolling-screenshots-g71"&gt;Droid Tycoon Fusion Cookbook&lt;/a&gt;,&lt;br&gt;
a fan checklist for tracking fusion recipes — more on the way.&lt;/p&gt;

&lt;p&gt;Are you also new to coding/app creation? Put your newbie experience posts below in the comments and I'll read them and comment back! &lt;/p&gt;

</description>
      <category>showdev</category>
      <category>buildinpublic</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a checklist for Droid Tycoon fusion recipes so I’d stop scrolling screenshots</title>
      <dc:creator>Andy Schelb</dc:creator>
      <pubDate>Wed, 02 Sep 2026 15:21:46 +0000</pubDate>
      <link>https://dev.to/plaidscientist/i-built-a-checklist-for-droid-tycoon-fusion-recipes-so-id-stop-scrolling-screenshots-g71</link>
      <guid>https://dev.to/plaidscientist/i-built-a-checklist-for-droid-tycoon-fusion-recipes-so-id-stop-scrolling-screenshots-g71</guid>
      <description>&lt;p&gt;Recently in Droid Tycoon, a Star Wars game built in Fortnite, they added a feature where you could fuse multiple droids together. They kept the recipes a secret, but people started making lists. I grabbed a screenshot from the forum, but when I kept using the markup tool on my phone it never saved right. Probably because I was multitasking between my TV and my phone.&lt;br&gt;
I thought I just need a simple checklist so I know what droid blueprints to target off the belt.&lt;br&gt;
So I built Droid Tycoon Fusion Cookbook — a simple checklist for tracking Droid Tycoon’s fusion recipes. Check off the parts you’ve collected, and the result glows green the moment you’re ready to fuse it.&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%2Fk24vwu7h2om09wscwlkc.gif" 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%2Fk24vwu7h2om09wscwlkc.gif" alt="Fusion Cookbook demo" width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 Try it: &lt;a href="https://theplaidscientist.github.io/fusioncookbook/" rel="noopener noreferrer"&gt;https://theplaidscientist.github.io/fusioncookbook/&lt;/a&gt;&lt;br&gt;
💻 Code: &lt;a href="https://github.com/theplaidscientist/fusioncookbook" rel="noopener noreferrer"&gt;https://github.com/theplaidscientist/fusioncookbook&lt;/a&gt;&lt;br&gt;
What it does&lt;br&gt;
Droid Tycoon’s fusion system has 17 not-so-secret recipes for new hybrid droids, across four rarity tiers, each one combining three specific parts into a new droid. Cross-referencing a static image against “wait, do I have two GONKs or one?” got old fast. So the tool is just:&lt;br&gt;
    • Tap each part as you collect it&lt;br&gt;
    • All three parts checked → the result lights up green, no more digging back through a screenshot&lt;br&gt;
    • Tap the result once you’ve actually fused it in-game → the row strikes through and fades, so what’s left on your list is only what’s left to do&lt;br&gt;
I kept having the issue where I had ⅔ of any equation, but nothing I could actually use.&lt;br&gt;
The build:&lt;br&gt;
Nothing fancy on purpose — it’s a single index.html, vanilla JavaScript, no framework, no build step. I just wanted an easy persistent checklist that wasn’t dependent on my paying too much attention.&lt;br&gt;
The one detail I actually like: progress saves to localStorage, keyed per recipe, so closing the tab or restarting your phone doesn’t lose your checkmarks — but nothing leaves your device and there’s no account to manage. Add it to your home screen via browser and it behaves like its own tiny app.&lt;/p&gt;

&lt;p&gt;It’s an unofficial fan tool — not affiliated with Epic Games, Lucasfilm, or Disney — just something I wanted to exist and didn’t see anywhere else.&lt;br&gt;
What’s next&lt;br&gt;
This is the first in a series I’m doing on Plaid Labs, a little hub of web experiments I keep building for myself and then figure other people might want too — there’s also Daily Doodle, a drawing-prompt generator, with more on the way.&lt;br&gt;
Have you ever built something to make your video game more fun? What was it?&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
