<?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: Antonio </title>
    <description>The latest articles on DEV Community by Antonio  (@antonioprosperi2svg).</description>
    <link>https://dev.to/antonioprosperi2svg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4092373%2F27c3ef5d-a3a1-442b-95b9-9e5f97c623dc.gif</url>
      <title>DEV Community: Antonio </title>
      <link>https://dev.to/antonioprosperi2svg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/antonioprosperi2svg"/>
    <language>en</language>
    <item>
      <title>I broke an Elementor site on purpose, then built a tool to find the bugs</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Tue, 20 Oct 2026 21:22:10 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/i-broke-an-elementor-site-on-purpose-then-built-a-tool-to-find-the-bugs-44hn</link>
      <guid>https://dev.to/antonioprosperi2svg/i-broke-an-elementor-site-on-purpose-then-built-a-tool-to-find-the-bugs-44hn</guid>
      <description>&lt;h2&gt;
  
  
  The night I broke my own site
&lt;/h2&gt;

&lt;p&gt;I'm 53, I work in a factory, and in the evenings I build things in vanilla JavaScript.&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%2Fbl0km5cp8a141favid9r.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%2Fbl0km5cp8a141favid9r.gif" alt="CSS THE BYUG" width="600" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lately I've been looking at freelance WordPress jobs. Almost every one of them says the same thing: &lt;em&gt;"Elementor site, layout is broken, fix it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I did something a bit crazy. I took an empty test site, installed Elementor, and asked an AI to break it for me — &lt;strong&gt;5 hidden CSS bugs&lt;/strong&gt;, packed into an encoded script so I couldn't just read the answer.&lt;/p&gt;

&lt;p&gt;Then I tried to find them.&lt;/p&gt;

&lt;p&gt;It was humbling. The page looked wrong, but the &lt;strong&gt;console said nothing&lt;/strong&gt;. Of course: CSS doesn't throw errors. A bad rule is still a &lt;em&gt;valid&lt;/em&gt; rule, so the browser applies it silently. That's exactly why clients say &lt;em&gt;"everything moved and I don't know why."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I found the first bug the slow way — DevTools, Styles panel, unchecking rules one by one until I saw it:&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;.elementor-section&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.e-con&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-60px&lt;/span&gt; &lt;span class="cp"&gt;!important&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;Twenty minutes for one line. I thought: there must be a faster way.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built one: CSS hunt
&lt;/h2&gt;

&lt;p&gt;🐞 &lt;a href="https://github.com/antonioprosperi2-svg/BeeLadybug-universal" rel="noopener noreferrer"&gt;BeeLadybug&lt;/a&gt;  is my zero-dependency debugging overlay. You paste one line in the console and it attaches to any page — WordPress dashboards, Elementor pages, mobile viewports. It already showed element size, classes, display type, FPS and memory.&lt;/p&gt;

&lt;p&gt;Version &lt;strong&gt;0.5&lt;/strong&gt; adds a new mode: &lt;strong&gt;CSS hunt&lt;/strong&gt; (press &lt;code&gt;F6&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When you hover an element, it tells you what's wrong with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;!important&lt;/code&gt; rules&lt;/strong&gt; that match the element, and where they come from (&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; inline or the stylesheet URL)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overflow causes&lt;/strong&gt;: which element actually leaves the viewport, plus negative margins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible text&lt;/strong&gt;: &lt;code&gt;color: transparent&lt;/code&gt;, &lt;code&gt;opacity: 0&lt;/code&gt;, or text the same color as its background&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click blockers&lt;/strong&gt;: what sits on top of the element's center, including fixed &lt;code&gt;::before&lt;/code&gt; / &lt;code&gt;::after&lt;/code&gt; on &lt;code&gt;html&lt;/code&gt; / &lt;code&gt;body&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stretched images&lt;/strong&gt;: natural ratio vs rendered ratio, when &lt;code&gt;object-fit&lt;/code&gt; is &lt;code&gt;fill&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Press &lt;code&gt;S&lt;/code&gt; to scan the whole page. Click a line and the element gets a red outline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part: telling the truth without noise
&lt;/h2&gt;

&lt;p&gt;My first version was technically right and practically useless.&lt;/p&gt;

&lt;p&gt;One card overflowing the screen produced &lt;strong&gt;ten red lines&lt;/strong&gt;: the card, its &lt;code&gt;article&lt;/code&gt;, its &lt;code&gt;p&lt;/code&gt;, its &lt;code&gt;h2&lt;/code&gt;… All "overflowing", because they inherited it. A fixed overlay blocking clicks? One warning &lt;strong&gt;per element underneath it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A real developer doesn't want ten alarms. They want &lt;strong&gt;the culprit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the scan now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reports the cause, not the containers.&lt;/strong&gt; An ancestor is skipped if a descendant overflows by the same amount (±2px).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groups by cause.&lt;/strong&gt; Instead of 12 lines you get:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;▾ body::after blocks clicks on 12 elements
    div.elementor-element-629ef32
    img#1301
    summary.elementor-element-3925463
    …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separates bugs from info.&lt;/strong&gt; Red = bug. Grey = info, like &lt;code&gt;unreadable sheet: fonts.googleapis.com&lt;/code&gt; (cross-origin stylesheets can't be read — that's a browser rule, not a problem on the page).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps the log calm.&lt;/strong&gt; Hover and DOM-mutation lines are hidden unless you turn on verbose mode (&lt;code&gt;F3&lt;/code&gt;). Each issue is logged once per element every 5 seconds. Press &lt;code&gt;P&lt;/code&gt; to &lt;strong&gt;pin&lt;/strong&gt; the panel on an element and read it in peace.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The real test
&lt;/h2&gt;

&lt;p&gt;The demo page bugs are easy — I wrote them. The real test was the broken Elementor page, with Elementor V4 "atomic" widgets, Google Fonts, Altervista ad scripts and all the usual mess.&lt;/p&gt;

&lt;p&gt;Result: CSS hunt found the &lt;strong&gt;click blocker&lt;/strong&gt;, the &lt;strong&gt;negative margin overflow&lt;/strong&gt;, the &lt;strong&gt;invisible button text&lt;/strong&gt; and the &lt;strong&gt;stretched image&lt;/strong&gt; on a live page I didn't build the bugs for.&lt;/p&gt;

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

&lt;p&gt;Open any page, press &lt;code&gt;F12&lt;/code&gt;, paste this in the console:&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;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/npm/bee-ladybug@0.5.3/src/index.js&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;BeeLadybugCore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WebDOMAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CSSAdapter&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bee&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;BeeLadybugCore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebDOMAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CSSAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bee&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bee&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 press &lt;code&gt;F6&lt;/code&gt;, hover, and press &lt;code&gt;S&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No build step, no framework, no dependencies. Just vanilla JavaScript.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS bugs are silent.&lt;/strong&gt; The console won't help you. You need to look at computed styles and geometry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The right answer is not enough.&lt;/strong&gt; A debugging tool must point to the &lt;em&gt;cause&lt;/em&gt;, or it's just another wall of text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking things on purpose is the best teacher.&lt;/strong&gt; I recommend it to anyone who wants to do WordPress fixes for clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm still learning. If you try CSS hunt on a broken page and it misses something, tell me in the comments — that's how the next version gets better.&lt;/p&gt;

&lt;p&gt;🐞 npm: &lt;a href="https://www.npmjs.com/package/bee-ladybug" rel="noopener noreferrer"&gt;bee-ladybug&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>css</category>
      <category>wordpress</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Nove classi per un pulsante: la mia piccola UI per canvas (e perché le voglio bene)</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Thu, 15 Oct 2026 21:13:44 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/nove-classi-per-un-pulsante-la-mia-piccola-ui-per-canvas-e-perche-le-voglio-bene-115f</link>
      <guid>https://dev.to/antonioprosperi2svg/nove-classi-per-un-pulsante-la-mia-piccola-ui-per-canvas-e-perche-le-voglio-bene-115f</guid>
      <description>&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%2Fv7w1ntjgkatgidnua060.jpg" 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%2Fv7w1ntjgkatgidnua060.jpg" alt="BeeUi" width="799" height="436"&gt;&lt;/a&gt;&lt;br&gt;
Volevo un pulsante "Play" sul canvas del mio motore di gioco 2D.&lt;br&gt;
Un pulsante. Uno.&lt;/p&gt;

&lt;p&gt;Poi ho aperto il file e ho trovato questo:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;BeeUI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;BeeControl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;BeePanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;BeeStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;BeeLabel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;BeeUIButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;BEE_ANCHOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;BEE_UI_DEFAULTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;drawNineSlice&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/ui/BeeUI.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nove import per un menu di pausa. Ho riso anch'io. Poi ho capito che ognuno di quei nomi ha un motivo per esistere, e alla fine sono diventati la parte del motore che preferisco. Ve li presento uno per uno, con la serietà che si meritano (poca, ma sufficiente).&lt;/p&gt;

&lt;h2&gt;
  
  
  Il problema: il canvas non sa cos'è un pulsante
&lt;/h2&gt;

&lt;p&gt;Il canvas HTML5 è un foglio bianco con i pixel. Non ha layout, non ha focus, non sa che il tuo menu dovrebbe restare centrato quando la finestra cambia dimensione. Tutto quello che il DOM ti regala gratis, sul canvas te lo devi costruire.&lt;/p&gt;

&lt;p&gt;Quindi sì, servono un po' di classi.&lt;/p&gt;

&lt;h2&gt;
  
  
  BeeControl: il nonno di tutti
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BeeControl&lt;/code&gt; è il nodo base. Non disegna niente (il suo &lt;code&gt;draw()&lt;/code&gt; è letteralmente vuoto), ma sa fare le cose noiose e importanti:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tenere una lista di figli e un genitore (con campi privati &lt;code&gt;#parent&lt;/code&gt; e &lt;code&gt;#children&lt;/code&gt;, così nessuno li tocca a mano);&lt;/li&gt;
&lt;li&gt;calcolare il proprio rettangolo a partire da &lt;strong&gt;ancore&lt;/strong&gt; e &lt;strong&gt;offset&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;capire se il mouse ci sta sopra con &lt;code&gt;hit(x, y)&lt;/code&gt;, partendo dall'ultimo figlio, cioè quello disegnato sopra.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le ancore sono valori da 0 a 1 rispetto al genitore. Il conto è tutto qui:&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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parentRect&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;parentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorLeft&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetLeft&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorRight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorLeft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetRight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetLeft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ancore uguali a sinistra e destra danno una larghezza fissa. Ancore diverse fanno allargare l'elemento insieme al genitore. Chi ha usato Godot riconoscerà il trucco, e non mi vergogno di averlo copiato.&lt;/p&gt;

&lt;h2&gt;
  
  
  BEE_ANCHOR: perché nessuno vuole scrivere &lt;code&gt;setAnchors(1, 1, 1, 1)&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Ricordarsi i numeri delle ancore è un incubo, quindi ci sono dei preset:&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="nx"&gt;BEE_ANCHOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOP_LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TOP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TOP_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;BOTTOM_LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BOTTOM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BOTTOM_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;FULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vuoi il punteggio in alto a destra con 16 pixel di margine?&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;new&lt;/span&gt; &lt;span class="nc"&gt;BeeLabel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Score: 0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BEE_ANCHOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOP_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ridimensioni la finestra e resta lì. Magia? No, sottrazioni.&lt;/p&gt;

&lt;h2&gt;
  
  
  BEE_UI_DEFAULTS: le decisioni che non voglio prendere ogni volta
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buttonWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buttonHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;È un oggetto congelato con &lt;code&gt;Object.freeze&lt;/code&gt;. Quattro numeri, zero discussioni. 44 pixel di altezza perché è una misura comoda da toccare col dito.&lt;/p&gt;

&lt;h2&gt;
  
  
  BeePanel: un rettangolo con ambizioni
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BeePanel&lt;/code&gt; è un controllo che finalmente disegna qualcosa: colore di sfondo, bordo, angoli arrotondati (se il browser supporta &lt;code&gt;roundRect&lt;/code&gt;) oppure un'immagine. E se gli passi anche &lt;code&gt;slice&lt;/code&gt;, usa…&lt;/p&gt;

&lt;h2&gt;
  
  
  drawNineSlice: l'eroe silenzioso
&lt;/h2&gt;

&lt;p&gt;Hai una cornice da 48×48 pixel e vuoi un pannello da 400×300 senza che gli angoli si deformino. La tecnica nine-slice taglia l'immagine in 9 pezzi: i 4 angoli restano fissi, i bordi si allungano in una sola direzione e il centro si allunga in tutte e due.&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;new&lt;/span&gt; &lt;span class="nc"&gt;BeePanel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BEE_ANCHOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frameImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&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;Nove chiamate a &lt;code&gt;drawImage&lt;/code&gt; e il tuo menu sembra uscito da un gioco vero.&lt;/p&gt;

&lt;h2&gt;
  
  
  BeeStack: il flexbox dei poveri (e mi basta)
&lt;/h2&gt;

&lt;p&gt;Mettere i pulsanti uno sotto l'altro calcolando le &lt;code&gt;y&lt;/code&gt; a mano è il modo migliore per odiare la gamedev. &lt;code&gt;BeeStack&lt;/code&gt; lo fa per te, in verticale (&lt;code&gt;'v'&lt;/code&gt;) o in orizzontale (&lt;code&gt;'h'&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;measure()&lt;/code&gt; somma le dimensioni dei figli visibili, più spazi e margini interni;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;layout()&lt;/code&gt; li posiziona uno dopo l'altro;&lt;/li&gt;
&lt;li&gt;se è ancorato al centro, si ricentra da solo in base alla dimensione calcolata.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  BeeLabel e BeeUIButton: finalmente quello che volevo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BeeLabel&lt;/code&gt; scrive del testo. Fine. &lt;code&gt;BeeUIButton&lt;/code&gt; è il pulsante vero: ha stati diversi per mouse sopra, premuto, disabilitato e focus, e si attiva al rilascio del mouse &lt;strong&gt;solo se sei ancora sopra al pulsante&lt;/strong&gt;, esattamente come un pulsante del browser. Se ti penti a metà click, trascini fuori e non succede niente.&lt;/p&gt;

&lt;h2&gt;
  
  
  BeeUI: il capo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BeeUI&lt;/code&gt; è la radice dell'albero. Occupa tutto il canvas e a ogni frame:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ricalcola il layout solo se qualcosa è cambiato (c'è un flag &lt;code&gt;#dirty&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;gestisce hover e click del mouse;&lt;/li&gt;
&lt;li&gt;gestisce la &lt;strong&gt;tastiera&lt;/strong&gt;: Tab e Shift+Tab, frecce o WASD, Invio e Spazio;&lt;/li&gt;
&lt;li&gt;gestisce il &lt;strong&gt;gamepad&lt;/strong&gt;: croce direzionale e tasto A.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La parte che mi piace di più è &lt;code&gt;focusToward(dx, dy)&lt;/code&gt;. Quando premi "giù", non va semplicemente al pulsante successivo nella lista: cerca il controllo più vicino in quella direzione, penalizzando quelli troppo spostati di lato. Funziona anche con griglie di pulsanti, non solo con le liste.&lt;/p&gt;

&lt;p&gt;Inoltre blocca il comportamento normale del Tab sul canvas, così il focus non scappa nella barra degli indirizzi a metà partita.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un menu di pausa completo
&lt;/h2&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;ui&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;BeeUI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;canvas&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;menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BeeStack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BEE_ANCHOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0f172acc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BeeLabel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAUSA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bold 24px monospace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BeeUIButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Riprendi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BeeUIButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Opzioni&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;openOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BeeUIButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Esci&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focusAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// nel game loop&lt;/span&gt;
&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Centrato, navigabile con mouse, tastiera e gamepad, e resta al suo posto quando ridimensioni la finestra. Il tutto in circa 700 righe senza dipendenze.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quindi, nove classi erano troppe?
&lt;/h2&gt;

&lt;p&gt;Probabilmente sì, per un pulsante.&lt;br&gt;
Per un pulsante che si centra da solo, si allarga, si naviga col joystick e non si rompe quando giri il telefono, no.&lt;/p&gt;

&lt;p&gt;E la prossima volta che qualcuno mi chiede "perché non usi semplicemente un &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; sopra il canvas?", gli mando questo articolo e vado a giocare.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>html</category>
      <category>canvas</category>
    </item>
    <item>
      <title>I'm 53, I build a JS game engine, and the bots think I'm fresh meat</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Mon, 12 Oct 2026 18:12:55 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/im-53-i-build-a-js-game-engine-and-the-bots-think-im-fresh-meat-33c4</link>
      <guid>https://dev.to/antonioprosperi2svg/im-53-i-build-a-js-game-engine-and-the-bots-think-im-fresh-meat-33c4</guid>
      <description>&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%2Ffjg7gm9258lu32w1y7xt.jpeg" 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%2Ffjg7gm9258lu32w1y7xt.jpeg" alt="Bot" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Someone told me that if bots keep landing on my articles, it means I'm "fresh content" for them.&lt;/p&gt;

&lt;p&gt;I'm 53. I've spent my life working in a warehouse and on machines. The only fresh thing around here is my patience, and it's leaving fast.&lt;/p&gt;

&lt;p&gt;In my part of Italy we have a saying for situations like this: &lt;strong&gt;"Le mazzate ci vo!"&lt;/strong&gt; Roughly: &lt;em&gt;what this needs is a good beating.&lt;/em&gt; Here's why I've been saying it a lot lately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The illusion of numbers
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://www.npmjs.com/package/beeengine" rel="noopener noreferrer"&gt;BeeEngine&lt;/a&gt;, a zero-dependency 2D game engine in vanilla JavaScript, and I write docs and posts about it.&lt;/p&gt;

&lt;p&gt;So I open my analytics and think: &lt;em&gt;wow, 100 visits from California, 50 from New York!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I dig a little. Zero-second sessions. Datacenter cities like Ashburn and Mountain View. Not a single human being. Just machines talking to machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then came the comments
&lt;/h2&gt;

&lt;p&gt;This week two comments showed up under my posts. Long, polite, full of words like "double-buffered draw lists", "GPU batching", "ECS queries".&lt;/p&gt;

&lt;p&gt;Sounds smart. Except my engine is single-threaded Canvas 2D: there is no GPU batch and no ECS to query. The comments were answering a post I didn't write.&lt;/p&gt;

&lt;p&gt;The pattern was always the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a random username, like &lt;code&gt;ywnigcsmku2m&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an account created a few days earlier, with a bio that says "404 bio not found"&lt;/li&gt;
&lt;li&gt;comments on a dozen unrelated articles on the same day, in several languages&lt;/li&gt;
&lt;li&gt;and, at the very end, a small "(site: ...)" with a link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line is the whole point. Everything else is packaging.&lt;/p&gt;

&lt;p&gt;A few hours later I found my own article copied on an aggregator site, wrapped in a "create an account" popup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not all bots are the same
&lt;/h2&gt;

&lt;p&gt;To be fair, a bot is just a script doing a repetitive job faster than any human. They come in different flavors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The useful ones.&lt;/strong&gt; Googlebot, Bingbot. They read your HTML, follow links and tell search engines your page exists. Without them nobody finds you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The collectors.&lt;/strong&gt; AI crawlers and scrapers. They harvest code, docs and tutorials to train models or to build mirror sites that catch traffic on the side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The spammers.&lt;/strong&gt; Fake profiles that use AI to write plausible technical comments and plant a link, hoping someone clicks or that search engines pass them some authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why us?
&lt;/h2&gt;

&lt;p&gt;At first I thought: &lt;em&gt;they're targeting me, maybe because I'm small and not "authoritative".&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;No. They don't pick anyone. They follow fresh, well-indexed content with technical tags: javascript, gamedev, canvas, webdev. A post about sprite sheets or particle pools is exactly what they're looking for.&lt;/p&gt;

&lt;p&gt;So, strangely, bots showing up is a side effect of being visible. It doesn't feel like a compliment, though.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want
&lt;/h2&gt;

&lt;p&gt;I don't need 500 fake sessions. I need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;people who actually try the library&lt;/li&gt;
&lt;li&gt;a star or a real issue on GitHub&lt;/li&gt;
&lt;li&gt;honest feedback from someone building an HTML5 game&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ghost traffic just dirties the stats and makes it harder to know if things are going well.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't click&lt;/strong&gt; the links in suspicious comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the profile&lt;/strong&gt;: brand-new account, random name, comments everywhere on the same day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report it&lt;/strong&gt; on DEV (the three dots on the comment, then "Report abuse") and &lt;strong&gt;hide&lt;/strong&gt; the comment on your own post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't reply&lt;/strong&gt; to bots. Do reply to real people, they're the reason you write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter analytics&lt;/strong&gt; by excluding datacenter traffic and zero-second sessions before celebrating any spike.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a canonical URL&lt;/strong&gt; on cross-posted articles, so search engines know which copy is the original.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As long as we publish openly on the web, bots will be around. We can't give them the beating they deserve, so we unmask them one comment at a time and keep writing real code for real humans.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Le mazzate ci vo!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Have you had fake comments or ghost traffic on your posts? How do you deal with them? Real humans only in the comments, please. 😄&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>devto</category>
      <category>discuss</category>
    </item>
    <item>
      <title>An inventory plugin that never lies: transactional add, equip and use in a vanilla JS 2D engine</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:50:02 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/an-inventory-plugin-that-never-lies-transactional-add-equip-and-use-in-a-vanilla-js-2d-engine-3cpm</link>
      <guid>https://dev.to/antonioprosperi2svg/an-inventory-plugin-that-never-lies-transactional-add-equip-and-use-in-a-vanilla-js-2d-engine-3cpm</guid>
      <description>&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%2Fbryninr9z5cjdz8myt9t.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%2Fbryninr9z5cjdz8myt9t.gif" alt="invwntario" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every 2D game ends up with an inventory. And almost every first inventory looks the same: an &lt;code&gt;items[]&lt;/code&gt; array living in the scene, a few &lt;code&gt;push&lt;/code&gt; calls, and a growing pile of &lt;code&gt;if&lt;/code&gt; statements around it.&lt;/p&gt;

&lt;p&gt;It works, until it doesn't. The bag is full and the potion silently disappears. An equip fails halfway and the sword is now in neither the slot nor the hand. A &lt;code&gt;use&lt;/code&gt; callback throws and you've already removed the item.&lt;/p&gt;

&lt;p&gt;I maintain a small vanilla JavaScript 2D engine for HTML5 Canvas (&lt;a href="https://www.npmjs.com/package/beeengine" rel="noopener noreferrer"&gt;BeeEngine JS&lt;/a&gt;), and for version &lt;strong&gt;2.10.0&lt;/strong&gt; I added an inventory. Here's how I designed it so it can't end up in those states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule zero: plugins don't own the loop
&lt;/h2&gt;

&lt;p&gt;Before the inventory, I built a tiny plugin registry (2.9.0). The engine &lt;strong&gt;does not import plugins&lt;/strong&gt;. A plugin has &lt;code&gt;attach&lt;/code&gt; / &lt;code&gt;detach&lt;/code&gt;, and the engine exposes &lt;code&gt;registerPlugin&lt;/code&gt; / &lt;code&gt;unregisterPlugin&lt;/code&gt;. If a plugin really needs a frame, it uses &lt;code&gt;engine.onTick(fn)&lt;/code&gt;. It never calls &lt;code&gt;loop()&lt;/code&gt; and never overrides &lt;code&gt;update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first plugin was a localization helper, &lt;code&gt;BeeLocale&lt;/code&gt;: read-only, with a fallback language, and missing keys return the key itself instead of an empty string. The inventory is the second plugin, built on the same pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it registers itself as &lt;code&gt;inventory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;it has &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;onTick&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;it &lt;strong&gt;doesn't draw&lt;/strong&gt; anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scene decides how to show it. The inventory only keeps state honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API in one snippet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BeeEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beeengine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BeeInventory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beeengine/plugins&lt;/span&gt;&lt;span class="dl"&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;game&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;BeeEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;testCanvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&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;inventory&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;BeeInventory&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;slots&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="na"&gt;equipSlots&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;hand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;potion&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Potion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxStack&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="na"&gt;consumable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onUse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&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;drop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;potion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// { added: 5, left: 3 }&lt;/span&gt;
&lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One slot, a stack limit of 5, and we try to add 8. The result isn't &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;: it's &lt;code&gt;{ added: 5, left: 3 }&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;add&lt;/code&gt; always tells you what didn't fit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;add&lt;/code&gt; fills &lt;strong&gt;existing stacks first&lt;/strong&gt;, then &lt;strong&gt;empty slots&lt;/strong&gt;. If there's no room left, it doesn't throw the extra items away. It returns them as &lt;code&gt;left&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This sounds small, but it moves the decision to where it belongs. The inventory doesn't know whether the leftover potions should stay on the ground, go to a chest, or show a "Bag full" message. The game does. So the inventory just reports the truth and lets the scene decide.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;remove&lt;/code&gt; works the other way around, from the last slot back to the first, so the stacks you filled first stay intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  All or nothing: &lt;code&gt;move&lt;/code&gt;, &lt;code&gt;equip&lt;/code&gt;, &lt;code&gt;unequip&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;These three either succeed completely or leave the state &lt;strong&gt;exactly as it was&lt;/strong&gt;. No half-moved stacks, no item that vanished between the bag and the hand.&lt;/p&gt;

&lt;p&gt;The idea is simple: check everything first, change things only when you know the whole operation can finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;use&lt;/code&gt; with a rollback
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use&lt;/code&gt; is the tricky one, because it runs user code: the item's &lt;code&gt;onUse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a piece is removed &lt;strong&gt;only if&lt;/strong&gt; &lt;code&gt;onUse&lt;/code&gt; doesn't throw and doesn't return &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;onUse&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;, nothing is consumed (for example "you're already at full health")&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;onUse&lt;/code&gt; &lt;strong&gt;throws&lt;/strong&gt;, the error is not swallowed: it propagates to the caller, and the inventory is restored to a snapshot taken before the call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't want to hide bugs in game code with a silent &lt;code&gt;try/catch&lt;/code&gt;. But I also didn't want a broken callback to leave the inventory corrupted. The snapshot gives you both: the error is loud, and the state stays clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copies out, plain data in
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;get&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt; and &lt;code&gt;equipped&lt;/code&gt; return &lt;strong&gt;copies&lt;/strong&gt;. Code outside can't mutate the inventory by accident by editing an object it got back.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toJSON&lt;/code&gt; saves only &lt;strong&gt;slots and equipment&lt;/strong&gt;. Item definitions (names, stack limits, &lt;code&gt;onUse&lt;/code&gt; functions) stay in code, where they belong. A save file never contains functions, and changing an item's behaviour in an update doesn't require migrating old saves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two plugins, zero coupling
&lt;/h2&gt;

&lt;p&gt;The demo drops coins and gems from the top of the screen. When the collector catches one, &lt;code&gt;onCollect&lt;/code&gt; calls &lt;code&gt;inventory.add&lt;/code&gt;. The counters on screen use &lt;code&gt;locale.t()&lt;/code&gt;, and pressing &lt;strong&gt;L&lt;/strong&gt; switches language.&lt;/p&gt;

&lt;p&gt;The interesting part is what's &lt;em&gt;not&lt;/em&gt; there: &lt;code&gt;BeeInventory&lt;/code&gt; and &lt;code&gt;BeeLocale&lt;/code&gt; don't import each other. The only thing that connects them is the scene. When the bag is full, the scene can read &lt;code&gt;left&lt;/code&gt; and decide what to show, like a "Bag full" message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;A plain Node script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/test-beeinventory.mjs
&lt;span class="c"&gt;# BeeInventory tests ok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript types (&lt;code&gt;BeeItemDef&lt;/code&gt;, &lt;code&gt;BeeInventoryOptions&lt;/code&gt;, &lt;code&gt;BeeInventory implements BeePlugin&lt;/code&gt;) ship in &lt;code&gt;index.d.ts&lt;/code&gt;, and &lt;code&gt;npm run typecheck&lt;/code&gt; passes.&lt;/p&gt;

&lt;p&gt;And the engine core (&lt;code&gt;BeeEngine.js&lt;/code&gt;) wasn't touched at all. That was the whole point of the plugin contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 2.10.0 and not 2.9.7
&lt;/h2&gt;

&lt;p&gt;It's a new export, not a fix. Semver minor bump, even if it's "just" a plugin.&lt;/p&gt;

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

&lt;p&gt;One plugin at a time: post-processing effects (fade, flash, shake) are next, then dialogue, which can reuse &lt;code&gt;BeeLocale&lt;/code&gt; for its strings.&lt;/p&gt;

&lt;p&gt;If you've built inventories before, I'm curious: do you return leftovers, or do you reject the whole &lt;code&gt;add&lt;/code&gt; when it doesn't fit?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/beeengine" rel="noopener noreferrer"&gt;beeengine&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://beeenginejs.com/" rel="noopener noreferrer"&gt;beeenginejs.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/antonioprosperi2-svg/BeeEngine-V2.5.0" rel="noopener noreferrer"&gt;BeeEngine&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I replaced our warehouse paper inventory with one HTML file</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Thu, 08 Oct 2026 19:17:53 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/i-replaced-our-warehouse-paper-inventory-with-one-html-file-1mfl</link>
      <guid>https://dev.to/antonioprosperi2svg/i-replaced-our-warehouse-paper-inventory-with-one-html-file-1mfl</guid>
      <description>&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%2Fpdm5ou1sc1ra1egbh3kw.jpg" 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%2Fpdm5ou1sc1ra1egbh3kw.jpg" alt="inventory" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At work, inventory day looks the same every time. We walk around the warehouse with printed sheets and pens: wood panels, aluminium profiles, glass and laminates, hundreds of entries. We write numbers in the margins and cross things out. A few weeks later at least one sheet has gone missing.&lt;/p&gt;

&lt;p&gt;I'm not a professional app developer. I build a 2D game engine in JavaScript as a hobby. One evening I thought: &lt;em&gt;I can't build an inventory system for the company, but I can build one for myself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post covers what I built, how it works, and what I'd do differently. Spoiler: there are better tools out there. This one exists because it solved &lt;strong&gt;my&lt;/strong&gt; problem in one evening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It opens on my phone, even with no signal in the warehouse.&lt;/li&gt;
&lt;li&gt;Finding an item takes two letters, not scrolling through 300 rows.&lt;/li&gt;
&lt;li&gt;Big &lt;strong&gt;+&lt;/strong&gt; and &lt;strong&gt;−&lt;/strong&gt; buttons I can use with one hand.&lt;/li&gt;
&lt;li&gt;Nothing to install, no server, no database, no login.&lt;/li&gt;
&lt;li&gt;At the end I can export everything to Excel.&lt;/li&gt;
&lt;li&gt;It's private: nobody needs to know it exists.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;It's one &lt;code&gt;index.html&lt;/code&gt; file with inline CSS and JavaScript. That's the whole thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The catalogue&lt;/strong&gt; (item codes and descriptions) is a JavaScript array embedded in the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The quantities&lt;/strong&gt; are saved in &lt;code&gt;localStorage&lt;/code&gt; on the phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting&lt;/strong&gt; is a folder with a random name on a site I already had, with a &lt;code&gt;noindex&lt;/code&gt; meta tag.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"robots"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"noindex, nofollow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if someone found the URL, they would only see an empty app. The counts live on my phone, not on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the data in
&lt;/h2&gt;

&lt;p&gt;The most boring part was the catalogue itself: several pages of printed tables, some photographed upside down, plus a scanned PDF. I gave the photos to an AI assistant, which transcribed them into plain text, one line per item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2|Line A|AL-001|White Matt
2|Line A|AL-002|Light Grey
3|Line B|AL-101|Bronze Brushed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tiny script turns that into the array that goes inside the HTML:&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;CATALOGUE&lt;/span&gt; &lt;span class="o"&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;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Line A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AL-001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;White Matt&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;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Line A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AL-002&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Light Grey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&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 codes above are made up. The real ones belong to our suppliers.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The core: search plus counters
&lt;/h2&gt;

&lt;p&gt;Search matches every word you type against the code, description and line, ignoring accents:&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;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0300-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;036f&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&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;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&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;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;norm&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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k&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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;d&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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;l&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&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;Typing &lt;code&gt;grey matt&lt;/code&gt; finds "Light Grey Matt" whatever order the words are in.&lt;/p&gt;

&lt;p&gt;Each row has a minus button, a number field and a plus button. One click listener on the list handles all of them:&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="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="nx"&gt;e&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-a]&lt;/span&gt;&lt;span class="dl"&gt;'&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inv-qty&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vibrate&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// a tiny buzz so you know it counted&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tapping the number lets you type a value directly, which is useful when you count 40 panels in one go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Panels have sizes, and they're never the same
&lt;/h2&gt;

&lt;p&gt;Aluminium is easy: one code, one count. Wood and laminate panels aren't. The same "Oak Natural" might be in stock as &lt;code&gt;200×200&lt;/code&gt; and as a leftover &lt;code&gt;270×120&lt;/code&gt;, and the sizes are different every time.&lt;/p&gt;

&lt;p&gt;So every panel can have its own list of sizes, each with its own counter:&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="c1"&gt;// sizes['P|Laminate|Oak Natural'] = [{ m: '200×200 sp.18', n: 3 }, { m: '270×120', n: 1 }]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&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="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I type the size once (&lt;code&gt;200x200&lt;/code&gt; becomes &lt;code&gt;200×200&lt;/code&gt;), and after that it's just + and −.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export and backup
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; has one big weakness: lose the phone or clear the browser data, and everything is gone. So there are two buttons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSV for Excel.&lt;/strong&gt; I use a semicolon separator and a UTF-8 BOM, otherwise Excel set to Italian puts everything in one column and mangles the accents:&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;csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;ufeff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&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;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&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="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JSON backup.&lt;/strong&gt; It saves the quantities and sizes to a file, and the same file can be restored on another phone. I send it to myself on WhatsApp after every count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing it like an app
&lt;/h2&gt;

&lt;p&gt;There's no app store involved. I open the page and choose &lt;strong&gt;Add to Home screen&lt;/strong&gt;. With &lt;code&gt;apple-mobile-web-app-capable&lt;/code&gt; and a &lt;code&gt;theme-color&lt;/code&gt;, it opens full screen like a native app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do better
&lt;/h2&gt;

&lt;p&gt;I know this isn't how you'd build a real inventory system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No sync.&lt;/strong&gt; The data lives on one phone, so two people can't count at the same time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The catalogue is hardcoded.&lt;/strong&gt; Adding items from the UI works, but those extras stay on that phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No service worker.&lt;/strong&gt; It works offline once loaded, but a proper PWA would cache it properly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No history.&lt;/strong&gt; It records the current count, not who changed what and when.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small backend (or even a shared Google Sheet) would fix most of this. But that would be a project. This was an evening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;You don't need permission, a budget or a framework to fix a small, real problem at work. One HTML file, &lt;code&gt;localStorage&lt;/code&gt; and a phone took us from "where's sheet number 3?" to typing two letters and tapping +.&lt;/p&gt;

&lt;p&gt;If you've built a tiny tool like this for your own job, I'd love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The child that vanished: an order-dependent bug in my 2D render pipeline</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Wed, 07 Oct 2026 21:39:24 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/the-child-that-vanished-an-order-dependent-bug-in-my-2d-render-pipeline-2f6j</link>
      <guid>https://dev.to/antonioprosperi2svg/the-child-that-vanished-an-order-dependent-bug-in-my-2d-render-pipeline-2f6j</guid>
      <description>&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%2Fsvty5mxf0elcp6mhqyec.jpg" 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%2Fsvty5mxf0elcp6mhqyec.jpg" alt="BeeLayer Rendering Pipeline" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm building &lt;strong&gt;BeeEngine&lt;/strong&gt;, a 2D HTML5 Canvas game engine in plain JavaScript. One of its core pieces is &lt;code&gt;BeeLayer&lt;/code&gt;, the draw pipeline. Every frame it walks the entities, decides which &lt;em&gt;pass&lt;/em&gt; each one belongs to, and hands them to the renderer in the right order.&lt;/p&gt;

&lt;p&gt;It looked solid, and the tests were green. Then a child entity simply stopped showing up on screen, and it only happened when the entity list was in a certain order.&lt;/p&gt;

&lt;p&gt;This post covers what was wrong, the first fix, the two bugs that fix still had, and the version that ships in 2.9.6.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pipeline works
&lt;/h2&gt;

&lt;p&gt;There are four default passes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pass&lt;/th&gt;
&lt;th&gt;order&lt;/th&gt;
&lt;th&gt;Space&lt;/th&gt;
&lt;th&gt;Sort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;background&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;world&lt;/td&gt;
&lt;td&gt;stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;world&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;world&lt;/td&gt;
&lt;td&gt;stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ysort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;world&lt;/td&gt;
&lt;td&gt;by feet (&lt;code&gt;y + height&lt;/code&gt;, or &lt;code&gt;sortY&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;screen&lt;/td&gt;
&lt;td&gt;stable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Entities can have children (a hat on a hero, an HP label above an enemy). The renderer, &lt;code&gt;engine.drawEntity&lt;/code&gt;, draws a node and then recurses into its children, &lt;strong&gt;skipping any child whose &lt;code&gt;drawLayer&lt;/code&gt; differs from the current pass&lt;/strong&gt;:&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;for &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;child&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&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;child&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawLayer&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawLayer&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;space&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pass&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;So the job of &lt;code&gt;BeeLayer&lt;/code&gt; is narrow. It only puts two kinds of entity in the buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;roots&lt;/strong&gt; (no &lt;code&gt;parent&lt;/code&gt;), and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;split children&lt;/strong&gt;, whose explicit &lt;code&gt;drawLayer&lt;/code&gt; differs from the one they inherit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A child on the same layer as its parent gets drawn &lt;em&gt;inside&lt;/em&gt; the parent's draw call. A child on a different layer gets its own slot. No entity is ever drawn twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Collection walked &lt;code&gt;currentScene.entities&lt;/code&gt;, then &lt;code&gt;engine.entities&lt;/code&gt;, calling &lt;code&gt;#visit&lt;/code&gt; on each item:&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="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inherited&lt;/span&gt;&lt;span class="p"&gt;)&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destroyed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// marked immediately&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;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;explicit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawLayer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawLayer&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;isRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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;split&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;explicit&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;inherited&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;explicit&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;inherited&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;isRoot&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* push into the bucket */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// recurse into children with the inherited layer...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now picture a hero with a &lt;code&gt;gun&lt;/code&gt; on &lt;code&gt;ysort&lt;/code&gt; and a &lt;code&gt;hud&lt;/code&gt; on &lt;code&gt;ui&lt;/code&gt;, and a list in this order:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hud&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;hud&lt;/code&gt; is visited first, straight from the list, so &lt;code&gt;inherited&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It isn't a root (it has a parent), and &lt;code&gt;split&lt;/code&gt; needs &lt;code&gt;inherited&lt;/code&gt;, so it's false.&lt;/li&gt;
&lt;li&gt;It doesn't go in any bucket, &lt;strong&gt;but it's already in &lt;code&gt;#seen&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Then &lt;code&gt;hero&lt;/code&gt; is visited and recurses into &lt;code&gt;hud&lt;/code&gt;, which is already seen, so it returns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;hud&lt;/code&gt; never gets drawn. With the list in the order &lt;code&gt;[hero, gun, hud]&lt;/code&gt; everything works. That's a bug that depends on the order of an array, which is the worst kind to chase.&lt;/p&gt;

&lt;h2&gt;
  
  
  First fix: two passes
&lt;/h2&gt;

&lt;p&gt;The idea is to stop letting children be visited before their parents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Roots pass:&lt;/strong&gt; visit only entities with no &lt;code&gt;parent&lt;/code&gt;, recursing into children as before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphans pass:&lt;/strong&gt; anything still not in &lt;code&gt;#seen&lt;/code&gt; has a parent that was never reached (the parent isn't in a list, or is hidden). Work out its inherited layer by climbing the parent chain, and treat it as an orphan root.
&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="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...reset buckets, #seen.clear()&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;walkRoots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sceneList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;walkRoots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engineList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;walkOrphans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sceneList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;walkOrphans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engineList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&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 tests passed, including &lt;code&gt;[hud, gun, hero]&lt;/code&gt; against &lt;code&gt;[hero, gun, hud]&lt;/code&gt;. I almost published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review: two more bugs
&lt;/h2&gt;

&lt;p&gt;Reading the new code line by line turned up two cases the tests didn't cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A double draw, order-dependent again
&lt;/h3&gt;

&lt;p&gt;Take a grandparent &lt;code&gt;G&lt;/code&gt; that is &lt;strong&gt;not&lt;/strong&gt; in any list, a parent &lt;code&gt;P&lt;/code&gt; and a child &lt;code&gt;C&lt;/code&gt; that &lt;strong&gt;are&lt;/strong&gt;, with no &lt;code&gt;drawLayer&lt;/code&gt; anywhere. The list is &lt;code&gt;[C, P]&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The orphans pass reaches &lt;code&gt;C&lt;/code&gt; first. Its parent was never seen, so &lt;code&gt;C&lt;/code&gt; goes in the bucket as an orphan root.&lt;/li&gt;
&lt;li&gt;Then it reaches &lt;code&gt;P&lt;/code&gt;, which also goes in the bucket as an orphan root.&lt;/li&gt;
&lt;li&gt;At draw time, &lt;code&gt;drawEntity(P)&lt;/code&gt; recurses into its children and &lt;strong&gt;draws &lt;code&gt;C&lt;/code&gt; again&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;code&gt;[P, C]&lt;/code&gt; it's fine. The bug I had just fixed was back in a new form.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The child of a destroyed parent
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;#visit&lt;/code&gt; returns on &lt;code&gt;destroyed&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; adding the node to &lt;code&gt;#seen&lt;/code&gt;, so the children of a destroyed parent are never marked. The orphan check only looked at &lt;code&gt;visible === false&lt;/code&gt;, so those children became orphans and got drawn. Before the fix that never happened, because &lt;code&gt;drawEntity&lt;/code&gt; on a destroyed parent draws nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final fix
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Enter each branch from the top.&lt;/strong&gt; Each frame I fill a reused &lt;code&gt;Set&lt;/code&gt; with everything in both lists. For each orphan, I climb to the &lt;strong&gt;highest&lt;/strong&gt; ancestor that is listed and not yet seen, and visit that one instead:&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="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;listedRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;listed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;top&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;&lt;strong&gt;Destroyed hides a branch, the same as invisible:&lt;/strong&gt;&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="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;hiddenAncestor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destroyed&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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 orphan walk now looks like this:&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="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;walkOrphans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;entity&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;hiddenAncestor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;listedRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;inheritedLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;Set&lt;/code&gt;s are cleared and reused every frame, and the buckets keep their rows, so the hot path doesn't allocate anything new.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tests that matter
&lt;/h2&gt;

&lt;p&gt;The most useful test didn't check the buckets. It &lt;strong&gt;simulated &lt;code&gt;drawEntity&lt;/code&gt;&lt;/strong&gt; and counted how many times each entity was actually drawn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[hero, gun, hud]&lt;/code&gt; and &lt;code&gt;[hud, gun, hero]&lt;/code&gt; give the same result.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;G&lt;/code&gt; missing, &lt;code&gt;[C, P]&lt;/code&gt; and &lt;code&gt;[P, C]&lt;/code&gt; give the same result: only &lt;code&gt;P&lt;/code&gt; is in a bucket, and &lt;code&gt;C&lt;/code&gt; is drawn once.&lt;/li&gt;
&lt;li&gt;A parent in the scene with a child in &lt;code&gt;engine.entities&lt;/code&gt;: the child doesn't vanish.&lt;/li&gt;
&lt;li&gt;The same entity in both lists is collected once.&lt;/li&gt;
&lt;li&gt;An invisible or destroyed parent turns off its whole branch, even a &lt;code&gt;ui&lt;/code&gt; child that is in a list.&lt;/li&gt;
&lt;li&gt;Regressions: default passes, &lt;code&gt;sortY = 0&lt;/code&gt; used as a key, stable ties, &lt;code&gt;add('')&lt;/code&gt; and duplicates throw.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A "seen" set marked too early means the first visit wins.&lt;/strong&gt; If the first visit has the least context, you lose information you can't get back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the result depends on array order, test with the array reversed.&lt;/strong&gt; Then test with a missing middle node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert on the output, not on internal state.&lt;/strong&gt; Checking the buckets looked fine. Counting the actual draws is what exposed the double draw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read your fix as if somebody else wrote it.&lt;/strong&gt; Green tests only prove the cases you thought of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix ships in BeeEngine &lt;strong&gt;2.9.6&lt;/strong&gt;. If you've handled parent/child draw ordering differently in your own engine, I'd love to hear how in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>canvas</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I almost edited 25 documentation pages by hand. Then one WordPress plugin did it in 1 second.</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:46:40 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/i-almost-edited-25-documentation-pages-by-hand-then-one-wordpress-plugin-did-it-in-1-second-3n67</link>
      <guid>https://dev.to/antonioprosperi2svg/i-almost-edited-25-documentation-pages-by-hand-then-one-wordpress-plugin-did-it-in-1-second-3n67</guid>
      <description>&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%2F6v5l1whja9qjkdplc8rg.jpg" 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%2F6v5l1whja9qjkdplc8rg.jpg" alt="aiuto" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm building &lt;strong&gt;&lt;a href="https://beeenginejs.com/" rel="noopener noreferrer"&gt;BeeEngine JS&lt;/a&gt;&lt;/strong&gt;, a lightweight 2D game engine for the browser, and I'm writing its documentation on WordPress, one page per module.&lt;/p&gt;

&lt;p&gt;Every doc page has the same sidebar listing all the modules. While a guide isn't written yet, its link is just an anchor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#BeeAnimatedSprite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;BeeAnimatedSprite.js&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I publish the real guide, that link has to point to the new page... &lt;strong&gt;on every single page&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The painful way
&lt;/h2&gt;

&lt;p&gt;Open page → Edit → find the line → paste the URL → Save. Repeat 25 times. Then again for the next module. And I still have ~50 pages of docs to write. 😩&lt;/p&gt;

&lt;h2&gt;
  
  
  The 1-second way: Better Search Replace
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/better-search-replace/" rel="noopener noreferrer"&gt;Better Search Replace&lt;/a&gt; searches and replaces text directly in the database. Yes, many WordPress devs already know it (it's a classic for site migrations), but if you write docs in WordPress and you've never used it, this is for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search for:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;href="#BeeAnimatedSprite"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replace with:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;href="https://beeenginejs.com/animazione-spritesheet-in-javascript-play-update-e-draw/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Table: &lt;code&gt;wp_posts&lt;/code&gt; (your prefix may differ, mine wasn't &lt;code&gt;wp_&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The dry run said &lt;strong&gt;444 cells&lt;/strong&gt;. I panicked for a second, then realized: 25 pages + all their &lt;strong&gt;revisions&lt;/strong&gt;. Totally normal.&lt;/p&gt;

&lt;p&gt;I ran it. &lt;strong&gt;1,144 ms.&lt;/strong&gt; Done. Every page updated. I almost cried. 😂&lt;/p&gt;

&lt;h2&gt;
  
  
  My checklist so you don't break anything
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Back up the database first.&lt;/strong&gt; The change is permanent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always do the dry run&lt;/strong&gt; and check the count makes sense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search something unique&lt;/strong&gt;, like the full &lt;code&gt;href="#Module"&lt;/code&gt;, not just &lt;code&gt;Module&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear your cache&lt;/strong&gt; (LiteSpeed, etc.) or you'll still see the old links.&lt;/li&gt;
&lt;li&gt;Open a page and click the link to verify.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The trick I'm keeping for the next 50 pages
&lt;/h2&gt;

&lt;p&gt;I write every not-yet-published link as &lt;code&gt;href="#ModuleName"&lt;/code&gt;. When the guide goes live, one search &amp;amp; replace and the whole documentation is linked. No more opening pages one by one.&lt;/p&gt;




&lt;p&gt;If you know an even better workflow for multi-page docs in WordPress, tell me in the comments. And if you like tiny 2D JS engines, come say hi at &lt;a href="https://beeenginejs.com/" rel="noopener noreferrer"&gt;beeenginejs.com&lt;/a&gt; 🐝&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>BeeEngine 2.9: plugins are here, starting with BeeLocale 🌍</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:22:58 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/beeengine-29-plugins-are-here-starting-with-beelocale-3bji</link>
      <guid>https://dev.to/antonioprosperi2svg/beeengine-29-plugins-are-here-starting-with-beelocale-3bji</guid>
      <description>&lt;h2&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%2Ftbeyl8h9vh06mewpdcch.jpg" alt="lingue" width="800" height="437"&gt;
&lt;/h2&gt;

&lt;p&gt;title: "BeeEngine 2.9: plugins are here, starting with BeeLocale 🌍"&lt;br&gt;
published: true&lt;/p&gt;
&lt;h2&gt;
  
  
  tags: javascript, gamedev, opensource, webdev
&lt;/h2&gt;

&lt;p&gt;Starting with &lt;strong&gt;v2.9.0&lt;/strong&gt;, BeeEngine supports plugins. The engine stays "blind": it doesn't import or know anything about any plugin. Each plugin hooks itself in with &lt;code&gt;attach(engine)&lt;/code&gt; and removes itself with &lt;code&gt;detach()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first official plugin is &lt;strong&gt;BeeLocale&lt;/strong&gt;: a tiny helper to make your canvas games multilingual.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;beeengine@2.9.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. The problem: hardcoded text
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Before: to translate the game you have to hunt every string in your code&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;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Score: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&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;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Game Over&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&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;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Press SPACE to restart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;260&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The solution: BeeLocale
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BeeEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beeengine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BeeLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beeengine/plugins&lt;/span&gt;&lt;span class="dl"&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;game&lt;/span&gt;&lt;span class="dl"&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;engine&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;BeeEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// All translations in one place&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&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;BeeLocale&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// active language&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// backup language&lt;/span&gt;
    &lt;span class="na"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Score&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;gameover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Game Over&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Press SPACE to restart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Punteggio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;gameover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hai perso!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Premi SPAZIO per ricominciare&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// The plugin registers itself in the engine as 'locale'&lt;/span&gt;
&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Use it wherever you draw text
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ After: only keys in your code&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;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&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;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gameover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&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;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;restart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;260&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;get(key)&lt;/code&gt; does exactly the same as &lt;code&gt;t(key)&lt;/code&gt;, if you prefer that name:&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="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gameover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "Hai perso!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also grab it from the engine, anywhere in your game:&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;loc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "Punteggio"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Switch language on the fly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Or automatically, from the browser language&lt;/span&gt;
&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// "it", "en", "fr"...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next frame already draws the text in the new language.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Fallback: never an empty string
&lt;/h2&gt;

&lt;p&gt;BeeLocale looks up a key in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;active language&lt;/strong&gt; (&lt;code&gt;it&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fallback language&lt;/strong&gt; (&lt;code&gt;en&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the key itself&lt;/strong&gt;, left visible
&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&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;BeeLocale&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Score&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lives&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Score&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;          &lt;span class="c1"&gt;// "lives" is missing in French&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// "Score"  → found in French&lt;/span&gt;
&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lives&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// "Lives"  → missing in French, falls back to English&lt;/span&gt;
&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bonus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// "bonus"  → missing everywhere, shows the key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A missing translation shows up on screen right away instead of silently disappearing. Empty strings (&lt;code&gt;''&lt;/code&gt;) count as missing too.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Detaching the plugin
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Under the hood: the plugin contract
&lt;/h2&gt;

&lt;p&gt;Every plugin needs just two methods: &lt;code&gt;attach(engine)&lt;/code&gt; and &lt;code&gt;detach()&lt;/code&gt;.&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;class&lt;/span&gt; &lt;span class="nc"&gt;MyPlugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-plugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;()&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unregisterPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-plugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A plugin can't&lt;/strong&gt; call &lt;code&gt;engine.loop()&lt;/code&gt;, override &lt;code&gt;update&lt;/code&gt;/&lt;code&gt;render&lt;/code&gt;, or patch the main loop.&lt;/p&gt;

&lt;p&gt;If it needs to run something every frame, it uses &lt;code&gt;onTick&lt;/code&gt;, which returns a function to turn the hook off:&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;class&lt;/span&gt; &lt;span class="nc"&gt;FrameCounter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frames&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="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frames&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onTick&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;()&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// always turn the hook off&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unregisterPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frames&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BeeLocale doesn't use &lt;code&gt;onTick&lt;/code&gt; at all: it only works when you call &lt;code&gt;t()&lt;/code&gt;, so it costs nothing per frame.&lt;/p&gt;




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

&lt;p&gt;More plugins are coming, one at a time. Which one would you like to see next? Let me know in the comments! 🐝&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/beeengine" rel="noopener noreferrer"&gt;beeengine&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/antonioprosperi2-svg/BeeEngine-V2.5.0" rel="noopener noreferrer"&gt;BeeEngine&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A tiny deep-link trick so my Instagram stories open the exact game (3,000 games, 25 lines of JS)</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Fri, 02 Oct 2026 17:56:39 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/a-tiny-deep-link-trick-so-my-instagram-stories-open-the-exact-game-3000-games-25-lines-of-js-48la</link>
      <guid>https://dev.to/antonioprosperi2svg/a-tiny-deep-link-trick-so-my-instagram-stories-open-the-exact-game-3000-games-25-lines-of-js-48la</guid>
      <description>&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%2Frhi5igm4imyesv9q9ixx.jpg" 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%2Frhi5igm4imyesv9q9ixx.jpg" alt="soluzione" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not rocket science, and many of you have probably done it a hundred times. But it solved a real, annoying problem for me in 10 minutes, so I'm sharing it in case it helps someone else. 🙂&lt;/p&gt;

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

&lt;p&gt;I run &lt;a href="https://playcombo.it" rel="noopener noreferrer"&gt;PlayCombo&lt;/a&gt;, a small site with around 3,000 free HTML5 games split into 13 category pages. The games are loaded from a JSON list and rendered with plain JavaScript inside WordPress.&lt;/p&gt;

&lt;p&gt;Every day I post a story or a short video on Instagram, Facebook and YouTube showing one game. Then I add the link... and the user lands on a category page with hundreds of games. The game they just saw? Somewhere in there. Good luck. 😅&lt;/p&gt;

&lt;p&gt;People suggested sorting by date, but I didn't want to change the order of the whole catalog every evening, or move things around by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Put the game in the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://playcombo.it/?gioco=pinch-bubbles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the page loads, a small script reads &lt;code&gt;?gioco=&lt;/code&gt;, finds the matching game in the list that's already in the page, and opens its preview. If the parameter isn't there, the script does nothing.&lt;/p&gt;

&lt;p&gt;No backend, no database, no changes to the JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;My game engine already exposes two things on &lt;code&gt;window&lt;/code&gt;: the game list (&lt;code&gt;listaGiochi&lt;/code&gt;) and the function that opens a game preview (&lt;code&gt;apriAnteprima&lt;/code&gt;). (Yes, the names are Italian: &lt;em&gt;gioco&lt;/em&gt; = game, &lt;em&gt;titolo&lt;/em&gt; = title.) The snippet just uses them:&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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Read ?gioco=... from the URL. No parameter? Stop right here.&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;richiesto&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gioco&lt;/span&gt;&lt;span class="dl"&gt;'&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;richiesto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Turn any title into a URL-friendly slug:&lt;/span&gt;
  &lt;span class="c1"&gt;//    "Pinch Bubbles" -&amp;gt; "pinch-bubbles", "Città Café" -&amp;gt; "citta-cafe"&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;slug&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0300-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;036f&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// strip accents&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&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="c1"&gt;// anything else -&amp;gt; "-"&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^-+|-+$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                         &lt;span class="c1"&gt;// trim dashes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cercato&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;richiesto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;tentativi&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="c1"&gt;// 3. The engine loads asynchronously, so wait until the list&lt;/span&gt;
  &lt;span class="c1"&gt;//    and the open function are ready (check every 100 ms).&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;attesa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;tentativi&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;lista&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listaGiochi&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apriAnteprima&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lista&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lista&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attesa&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// 4. Exact match first, then a partial match as a fallback.&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;gioco&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lista&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titolo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;cercato&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
                  &lt;span class="nx"&gt;lista&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titolo&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;cercato&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apriAnteprima&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Give up after ~10 seconds so it never runs forever.&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;tentativi&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attesa&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;100&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;
  
  
  Why I like it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero impact on normal visits.&lt;/strong&gt; Without &lt;code&gt;?gioco=&lt;/code&gt;, the script exits on the first line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate from the engine.&lt;/strong&gt; I added it with a "custom JS/CSS/HTML" plugin in WordPress, not inside the engine file. If something goes wrong, the worst case is that the game just doesn't open by itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works for every game.&lt;/strong&gt; No per-game setup: the slug is generated from the title, so &lt;code&gt;?gioco=air-zero&lt;/code&gt; works out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgiving links.&lt;/strong&gt; Accents and capital letters don't matter, and partial names still work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Possible improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use a stable &lt;code&gt;id&lt;/code&gt; or &lt;code&gt;slug&lt;/code&gt; field instead of the title, so renaming a game doesn't break old links.&lt;/li&gt;
&lt;li&gt;Update the URL with &lt;code&gt;history.replaceState&lt;/code&gt; when a game is opened, so people can share it directly.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;og:&lt;/code&gt; meta tags per game for nicer previews on social media (that one needs server-side help).&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That's it: a small trick, but now every story link opens exactly the game people just saw. If you've solved this differently, I'd love to hear how in the comments! 👇&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I built a lightweight 2D Web Game Engine in Pure Vanilla JS (BeeEngine v2.8.4)</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Wed, 30 Sep 2026 20:42:52 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/i-built-a-lightweight-2d-web-game-engine-in-pure-vanilla-js-beeengine-v284-4n9</link>
      <guid>https://dev.to/antonioprosperi2svg/i-built-a-lightweight-2d-web-game-engine-in-pure-vanilla-js-beeengine-v284-4n9</guid>
      <description>&lt;p&gt;Hi DEV community! 👋&lt;/p&gt;

&lt;p&gt;Over the past few months, I've been working on &lt;strong&gt;BeeEngine&lt;/strong&gt;, a lightweight 2D game engine built with pure &lt;strong&gt;Vanilla JavaScript&lt;/strong&gt; and &lt;strong&gt;HTML5 Canvas&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;My main goal was to create a clean, modular engine without relying on heavy external frameworks or complex build setups. It features a custom built-in visual inspector (&lt;strong&gt;BeeLadybug&lt;/strong&gt;), a State-Graph Animator, and a robust Audio Mixer.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Project Structure
&lt;/h2&gt;

&lt;p&gt;Here is how BeeEngine is structured under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BeeEngine-V2.8/
├── index.html                 # Entry point &amp;amp; Canvas config
├── index.js                   # ESM Barrel (re-exporting BeeEngine.js)
├── main.js                    # Visual Demo (BeeUI: anchor, stack, focus, HUD)
├── BeeEngine.js               # CORE (Core Loop &amp;amp; System Coordinator)
├── index.d.ts                 # Global TypeScript definitions &amp;amp; IntelliSense
└── src/
    ├── audio/                 # BeeAudioMixer (bus, fade, duck, pan 2D)
    ├── core/                  # Entities, Prefabs, Tweens, Timelines, Pooling, Scene Manager
    ├── gameplay/              # Player, Enemy, Platform, Collectibles, Menus
    ├── graphics/              # Layers, Animator, Camera, Tilemaps, Particles
    ├── input/                 # Keyboard, Mouse, Touch &amp;amp; Virtual Joysticks
    ├── ui/                    # BeeUI (Panels, Stacks, Labels, Buttons, Nine-slice)
    ├── physics/               # Spatial Hashing, RigidBody &amp;amp; AABB Collisions
    └── debug/                 # BeeLadybug (Visual Inspector &amp;amp; Hitbox Overlay)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ✨ Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero External Dependencies:&lt;/strong&gt; Built with 100% pure Vanilla JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physics &amp;amp; Collision:&lt;/strong&gt; Integrated Spatial Hash grid for efficient AABB collision detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input System:&lt;/strong&gt; Built-in support for Keyboard, Mouse, Touch, and Virtual Joysticks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Engine (BeeUI):&lt;/strong&gt; Flex-like layouts, anchors, focus management, and nine-slice UI rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Debugger (BeeLadybug):&lt;/strong&gt; Built-in tool for real-time inspection, data tracking, and hitbox rendering.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎬 Deep Dive: BeeAnimator (State Graph, Not Just Clips)
&lt;/h2&gt;

&lt;p&gt;Most simple engines just play an animation clip. &lt;strong&gt;BeeAnimator&lt;/strong&gt; is a proper State Graph Coordinator. While &lt;code&gt;BeeAnimatedSprite&lt;/code&gt; renders the frames, the Animator decides &lt;em&gt;which&lt;/em&gt; clip to play and &lt;em&gt;when&lt;/em&gt;, handling priorities, hit-locks, and automatic transitions. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;entity.animator&lt;/code&gt; ticks inside the entity loop using the simulation's delta time (&lt;code&gt;dt&lt;/code&gt;), meaning if the game pauses, the animation lifecycle freezes correctly.&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="c1"&gt;// 1. Create the base animated sprite (The Renderer)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sprite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAnimatedSprite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;animations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="c1"&gt;// 2. Wrap it in the State Graph Animator&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAnimator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;animator&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&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;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;run&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;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&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="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attack&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;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;exitTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;// State Transitions via Predicates&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&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;run&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="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;run&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;idle&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="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;when&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attack&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="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wantsAttack&lt;/span&gt;&lt;span class="p"&gt;)&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;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;animator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Core Concepts:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Mechanics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;lock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;As long as the clip isn't finished, states with &lt;code&gt;priority&lt;/code&gt; (\le) current cannot interrupt it (they get queued).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;exitTo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines where to transition automatically once a one-shot/locked clip finishes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;when('*', to, pred)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines a global transition predicate checkable from any active state.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;play(name, { force })&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual override request; &lt;code&gt;force: true&lt;/code&gt; breaks active animation locks.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: &lt;code&gt;BeeAnimatedSprite.play(name, { restart: true })&lt;/code&gt; and &lt;code&gt;sprite.finished&lt;/code&gt; are exposed so the animator knows precisely when an action (like an attack) ends.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔊 Deep Dive: BeeAudioMixer (Audio Nodes &amp;amp; Bus Hierarchy)
&lt;/h2&gt;

&lt;p&gt;Instead of poorly cloning HTML5 audio nodes (&lt;code&gt;cloneNode&lt;/code&gt;), BeeEngine uses a proper audio graph node system via &lt;strong&gt;BeeAudioMixer&lt;/strong&gt;. It handles a full hierarchy: &lt;code&gt;master&lt;/code&gt; (\rightarrow) &lt;code&gt;music&lt;/code&gt; / &lt;code&gt;sfx&lt;/code&gt; / &lt;code&gt;ui&lt;/code&gt; / &lt;code&gt;voice&lt;/code&gt;. It supports volume control, muting per channel, smooth fading, 2D panning relative to the camera listener, and automatic &lt;strong&gt;audio ducking&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;The mixer ticks every frame using real-time execution independent of the game simulation pause state.&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="c1"&gt;// Unlock Audio Graph (requires user gesture interaction)&lt;/span&gt;
&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Play background music with crossfade and custom volume&lt;/span&gt;
&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;music&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Play a 2D localized sound effect spatialized vs the camera listener&lt;/span&gt;
&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hit&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;bus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sfx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Procedural beep generation (perfect for web demos without downloading MP3s)&lt;/span&gt;
&lt;span class="c1"&gt;// Triggers automatic music ducking because it runs on the 'voice' bus&lt;/span&gt;
&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; 

&lt;span class="c1"&gt;// Global Bus Controls&lt;/span&gt;
&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sfx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;gioco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;music&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Core Concepts:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Mechanics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;bus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hierarchy chain controller. Muting a parent bus automatically silences all child streams.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;duck&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default behavior: Active signals on the &lt;code&gt;voice&lt;/code&gt; bus automatically duck the &lt;code&gt;music&lt;/code&gt; bus volume down to &lt;code&gt;0.35&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;x, y&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic distance attenuation and left/right stereo panning relative to the listener position.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tone()&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low-overhead procedural beep generator for quick sound design prototyping.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📦 Try It Out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;jsDelivr CDN:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BeeEngine&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/npm/beeengine@latest/BeeEngine.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;npm:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;beeengine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📊 &lt;strong&gt;CDN Stats &amp;amp; Package:&lt;/strong&gt; [BeeEngine on jsDelivr](&lt;a href="https://www.jsdelivr.com/package/npm/beeengine" rel="noopener noreferrer"&gt;https://www.jsdelivr.com/package/npm/beeengine&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I'd love to hear your thoughts, feedback, or suggestions from fellow web developers and gamedevs! What features would you like to see next?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>BeeLadybug — un overlay per il debug DOM in tempo reale</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Sat, 26 Sep 2026 16:08:34 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/beeladybug-un-overlay-per-il-debug-dom-in-tempo-reale-3go0</link>
      <guid>https://dev.to/antonioprosperi2svg/beeladybug-un-overlay-per-il-debug-dom-in-tempo-reale-3go0</guid>
      <description>&lt;p&gt;Un piccolo overlay in azione per il debug in tempo reale.&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%2Fshs6c0krvdvpoepyyrww.jpg" 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%2Fshs6c0krvdvpoepyyrww.jpg" alt="controllo" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Invece di riempire il codice di console.log o stare sempre a smanettare con gli DevTools, ho creato questa piccola utility per tracciare e illuminare gli elementi del DOM e i nodi in tempo reale mentre ci si passa sopra.&lt;/p&gt;

&lt;p&gt;Molto comoda per fare controlli al volo mentre si sviluppa, senza appesantire nulla.&lt;/p&gt;

&lt;p&gt;Che ne pensate? Vi capita mai di impazzire per tracciare gli elementi a schermo? &lt;br&gt;
Provatelo e fatemi sapere se puo' essere utile."(testato su Chrome/Edge)"&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;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/npm/bee-ladybug@0.4.0/src/index.js&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;BeeLadybugCore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WebDOMAdapter&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bee&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;BeeLadybugCore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebDOMAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bee&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;🔗 &lt;a href="https://www.npmjs.com/package/bee-ladybug" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/bee-ladybug&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>debugging</category>
    </item>
    <item>
      <title>BeeLadybug Ironic: Code Inspection meets Self-Inspection</title>
      <dc:creator>Antonio </dc:creator>
      <pubDate>Mon, 21 Sep 2026 18:49:39 +0000</pubDate>
      <link>https://dev.to/antonioprosperi2svg/beeladybug-ironic-debugging-the-code-vs-debugging-the-mind-mhh</link>
      <guid>https://dev.to/antonioprosperi2svg/beeladybug-ironic-debugging-the-code-vs-debugging-the-mind-mhh</guid>
      <description>&lt;p&gt;🐞 BeeLadybug Ironic: A Small End-of-Day Ritual for Devs&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What if we applied the rubber-duck debugging concept to our end-of-day workflow?&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea
&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%2Fzmh08j1fr5ynwqol2qdt.jpg" 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%2Fzmh08j1fr5ynwqol2qdt.jpg" alt="realta" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you close your code editor after hours of problem-solving, refactoring, or hunting down subtle layout bugs, you don't always need another complex dashboard or analytical tool.&lt;/p&gt;

&lt;p&gt;Sometimes, a simple circuit breaker helps you transition out of "dev mode" and step away from the desk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BeeLadybug Ironic&lt;/strong&gt; is an experimental project separate from the main &lt;code&gt;bee-ladybug&lt;/code&gt; debug tool, combining two main concepts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visual Debugging:&lt;/strong&gt; Inspecting Canvas elements and DOM boundaries under the hood.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Inspection &amp;amp; Irony:&lt;/strong&gt; A lightweight interactive canvas where you can write down thoughts, log simple notes, or reflect on a session before shutting down the system.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mascot Interface:&lt;/strong&gt; A small 2D bee-ladybug mascot welcomes you to the canvas session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflection Prompts:&lt;/strong&gt; Quick interaction nodes to evaluate where you stand:

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Is this a logic issue or just time for a break?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What's the main blocker left for tomorrow?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Ready to call it a day?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canvas Transition:&lt;/strong&gt; Selecting an option transitions the interface into a clean workspace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Dissolve Effect:&lt;/strong&gt; You type your temporary notes, hit the action button, and watch the text dissolve into a pixel field on screen. Once cleared, it's gone.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runs Locally:&lt;/strong&gt; Everything executes inside the browser's RAM during the session. No backend API calls, no databases, no tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript &amp;amp; Canvas:&lt;/strong&gt; Built on a minimal 2D rendering loop to keep performance fast and dependency-free.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Feedback &amp;amp; Ideas
&lt;/h2&gt;

&lt;p&gt;This prototype is evolving, and I'd love to hear your thoughts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do you use any specific routine or visual break when wrapping up a long coding session?&lt;/li&gt;
&lt;li&gt;What kind of canvas particle effect would feel best for clearing text (e.g., pixel dissolve, 8-bit blast, or terminal sweep)?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Feel free to leave a comment below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
