<?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: PHT</title>
    <description>The latest articles on DEV Community by PHT (@pht1991).</description>
    <link>https://dev.to/pht1991</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172930%2F3196c55c-2972-4519-a19a-f9070c48883d.jpg</url>
      <title>DEV Community: PHT</title>
      <link>https://dev.to/pht1991</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pht1991"/>
    <language>en</language>
    <item>
      <title>One link is all it takes: syncing bead patterns between phone and PC without a backend</title>
      <dc:creator>PHT</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:12:09 +0000</pubDate>
      <link>https://dev.to/pht1991/one-link-is-all-it-takes-syncing-bead-patterns-between-phone-and-pc-without-a-backend-4h4e</link>
      <guid>https://dev.to/pht1991/one-link-is-all-it-takes-syncing-bead-patterns-between-phone-and-pc-without-a-backend-4h4e</guid>
      <description>&lt;p&gt;A couple of weeks ago I wrote about &lt;a href="https://dev.to/pht1991/i-built-a-perler-bead-pattern-generator-that-exports-your-work-as-a-single-html-file-1nd4"&gt;my Perler bead pattern generator&lt;/a&gt; — a 100% client-side tool where the whole app plus your work exports as one self-contained HTML file.&lt;/p&gt;

&lt;p&gt;That post ended with a problem I hadn't solved: &lt;strong&gt;people don't craft at one device.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They browse reference photos on the phone on the couch, but they build the actual board and print the pattern at a desk. My tool was web-only, and my users (mostly in China) increasingly asked for a WeChat mini program version. Building it was the easy part — the hard part was that once your pattern lived on the phone, it was stuck there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints make the design
&lt;/h2&gt;

&lt;p&gt;The web tool has a hard rule I'm not willing to break: &lt;strong&gt;no accounts, no backend, nothing ever leaves the browser.&lt;/strong&gt; Free tool + hobby crafters + uploaded family photos = cloud storage is a non-starter, both for trust and for my hosting budget.&lt;/p&gt;

&lt;p&gt;So how do you move a pattern from a WeChat mini program to a PC browser with zero infrastructure?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Accounts + cloud sync — violates the whole premise&lt;/li&gt;
&lt;li&gt;❌ File export → send file → import — clumsy on mobile, and WeChat file handling is its own adventure&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;A URL. The pattern IS the link.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mini program copies a link to the clipboard; the user sends it to themselves (WeChat's File Transfer Assistant is made for this), opens it on the PC, and the pattern is just… there. Everyone already knows how to share a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fitting a 4000-cell pattern into a URL
&lt;/h2&gt;

&lt;p&gt;The format is versioned, dot-separated, and lives entirely in the URL &lt;strong&gt;fragment&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://perler.phtbyte.com/#p=1.49.69.mard.H01-H02-H08-….zqn3zj01…
        └─┬─┘ ─┬─────────────────────────────┘
       version  payload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why the fragment (&lt;code&gt;#&lt;/code&gt;) and not a query string? Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fragments are never sent to the server.&lt;/strong&gt; The pattern data exists only in the browser. No logging, no GDPR-adjacent questions, and my static host (GitHub Pages) doesn't need to care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No practical length limit.&lt;/strong&gt; Query strings start breaking things (proxies, analytics, some CDNs) around 2KB. Fragments just work — the browser never transmits them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The payload itself:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Format version — so I can change the scheme later without breaking old links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;49.69&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Board width × height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Palette ID (Perler / MARD / ARTKAL / YANT…)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;H01-H02-H08-…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;code table&lt;/strong&gt;: every bead color used, in order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zqn3zj01…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run-length encoded grid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The code table — or, the bug I designed my way out of
&lt;/h3&gt;

&lt;p&gt;My first instinct was to encode each cell as the palette &lt;em&gt;index&lt;/em&gt;: one character in base36 covers values 0–35. Then I actually counted the MARD palette: &lt;strong&gt;221 colors.&lt;/strong&gt; Base36 dies at 36. A two-character index would have worked, but it bloats every single run.&lt;/p&gt;

&lt;p&gt;The better move: put the &lt;em&gt;used colors&lt;/em&gt; in a table up front and encode cells as &lt;strong&gt;table positions&lt;/strong&gt;. A real pattern uses maybe 10–30 colors out of 221, so table positions fit in one base36 character with room to spare (the table caps at 34 — index 35 is reserved). Bonus: cell data now references &lt;em&gt;semantic&lt;/em&gt; color codes, not positional indices, so palette data can evolve without invalidating old links.&lt;/p&gt;

&lt;h3&gt;
  
  
  The RLE run: 3 characters per run
&lt;/h3&gt;

&lt;p&gt;Each run is exactly 3 chars: 1 char = table index (&lt;code&gt;z&lt;/code&gt; = empty cell), 2 chars = run length in base36 (max &lt;code&gt;zz&lt;/code&gt; = 1295).&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;// encode: row-major, runs of same color (or empty)&lt;/span&gt;
&lt;span class="nx"&gt;rle&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;B36&lt;/span&gt;&lt;span class="p"&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;B36&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 49×69 board (3,381 cells) with ~15 colors lands around &lt;strong&gt;500–800 characters of URL&lt;/strong&gt;. Short enough to survive a clipboard round-trip through WeChat without anyone noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust nothing: decoding defensively
&lt;/h2&gt;

&lt;p&gt;A link can be mangled by any layer between the phone and the PC — truncated copy, URL shortener, autocorrect, a sibling editing the text for fun. The decoder validates &lt;em&gt;everything&lt;/em&gt; and rejects instead of rendering something half-broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;parts&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="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&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;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// version&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PALETTE_BY_ID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;paletteId&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;w&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pal&lt;/span&gt; &lt;span class="o"&gt;||&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;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40000&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;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// sane geometry&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;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;table&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;pal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cc&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;cc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;c&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;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// real color code?&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;rle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// well-formed runs&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;codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;h&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;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// exact cell count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every check is cheap; the alternative is a corrupted grid that a crafter discovers halfway through beading. A bad link gets a polite "invalid link" toast and the app stays untouched. Same philosophy as the single-file export: &lt;strong&gt;fail closed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff at the desk: a printable grid
&lt;/h2&gt;

&lt;p&gt;Importing was half the feature. The other half is what you do once the pattern is on the big screen: &lt;strong&gt;print it.&lt;/strong&gt; So I added a grid export — a proper work sheet, not a screenshot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;24px cells (halving automatically, down to the canvas cap of 4096px, so a 200-column monster still exports)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bead codes printed in every cell&lt;/strong&gt;, with the text color chosen per-cell by background luminance so it stays readable on both white and black beads&lt;/li&gt;
&lt;li&gt;Gridlines at 1px opacity 0.16 — visible on paper, invisible in the colors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done-marks&lt;/strong&gt;: teal checkmarks on colors you've finished, matching the in-app tracker — print a fresh sheet and your progress carries over&lt;/li&gt;
&lt;li&gt;A header with title, size, and palette name, because worksheets get stacked and shuffled
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// text stays readable on any bead color&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.299&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.587&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.114&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&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="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#333&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;#fff&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.54&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole workflow now fits in one sentence: &lt;em&gt;photo on the phone → pattern on the couch → link on the desk → print → iron some beads.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned (again)
&lt;/h2&gt;

&lt;p&gt;The humble URL is still the best state-transfer mechanism ever shipped. No SDK, no auth flow, no rate limits, works across apps that were never designed to talk to each other — a WeChat mini program and a static web page share data through something every chat app already knows how to carry.&lt;/p&gt;

&lt;p&gt;If your app's state is small and user-owned, try making it a link before you build a sync service.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://perler.phtbyte.com/" rel="noopener noreferrer"&gt;perler.phtbyte.com&lt;/a&gt; (works offline once loaded) · &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/pht1991/perler-blueprint" rel="noopener noreferrer"&gt;github.com/pht1991/perler-blueprint&lt;/a&gt; (MIT) · On WeChat, search mini program &lt;strong&gt;我要拼豆图纸&lt;/strong&gt; — patterns sync between the two via the link format above.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I built a Perler bead pattern generator that exports your work as a single HTML file</title>
      <dc:creator>PHT</dc:creator>
      <pubDate>Thu, 29 Oct 2026 04:00:41 +0000</pubDate>
      <link>https://dev.to/pht1991/i-built-a-perler-bead-pattern-generator-that-exports-your-work-as-a-single-html-file-1nd4</link>
      <guid>https://dev.to/pht1991/i-built-a-perler-bead-pattern-generator-that-exports-your-work-as-a-single-html-file-1nd4</guid>
      <description>&lt;p&gt;If you've never heard of Perler beads (fuse beads): you place tiny plastic beads on a pegged board, iron them, and they melt into pixel art you can keep. It's physical pixel art — and people make genuinely impressive stuff with it.&lt;/p&gt;

&lt;p&gt;Planning a big project requires a pattern: a grid telling you which bead color goes in which cell. Existing online generators do the quantization for you, but I wanted one with a specific superpower: &lt;strong&gt;the whole tool, plus your current work, exportable as ONE self-contained HTML file you can keep, email, or open years later&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://perler.phtbyte.com/" rel="noopener noreferrer"&gt;Perler Bead Blueprint Maker&lt;/a&gt; — 100% client-side, no account, no backend, images never leave your browser. Source is on &lt;a href="https://github.com/pht1991/perler-blueprint" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; (MIT).&lt;/p&gt;

&lt;p&gt;This post is about how it works and the one bug that taught me to never regex-replace HTML again.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR — what it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Upload an image → quantized to real bead palettes (Perler, MARD, ARTKAL, YANT — including their actual color codes and names)&lt;/li&gt;
&lt;li&gt;Preset sizes (29/38/49/69/104…) or any custom N×N&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color budget&lt;/strong&gt;: k-means clustering in Lab space to hit "≤ 25 colors" without visual mush&lt;/li&gt;
&lt;li&gt;Interactive 2D grid (bead codes printed in cells, highlight tracking, "done" marks that persist)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D board view&lt;/strong&gt; with real perler beads (Three.js &lt;code&gt;InstancedMesh&lt;/code&gt;) and a depth-layering mode that stacks the board into relief layers&lt;/li&gt;
&lt;li&gt;Editing tools: brush / fill / eraser / eyedropper with undo history&lt;/li&gt;
&lt;li&gt;Export: poster PNG/PDF, multi-size PDF collection + CSV shopping list, and — the reason this project exists — &lt;strong&gt;a single HTML file that IS the app WITH your work inside&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The quantization: Lab space + seeded k-means
&lt;/h2&gt;

&lt;p&gt;Naive RGB distance quantizes badly (blues and purples especially). Everything runs in CIELAB and uses ΔE76 for matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// rgb → Lab, then nearest palette color by ΔE76&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nearest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;Color&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&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="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rgb2lab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&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;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;palette&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="nx"&gt;bestD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;Infinity&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;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deltaE76&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="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lab&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;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bestD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bestD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;best&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;For the color budget, a palette of 221 colors reduced to "25 colors max" is a clustering problem. k-means++ in Lab space works well, with one addition: &lt;strong&gt;a fixed seed&lt;/strong&gt;, so the same image always quantizes to the same pattern. Sounds obvious, but most generators re-roll the dice on every page load — you refresh and your carefully-tuned 20-color selection becomes 23 different colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3D board
&lt;/h2&gt;

&lt;p&gt;Beads are &lt;code&gt;InstancedMesh&lt;/code&gt; cylinders — one draw call for thousands of beads — with a canvas-generated texture that fakes the hole. The pegboard underneath has bumps. Nothing fancy, but it sells the illusion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CylinderGeometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&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;mesh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InstancedMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// one setMatrixAt per bead, y offset = board layer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;depth-layering mode&lt;/strong&gt; is my favorite: a heuristic (brightness + center weighting + smoothing, then equal-frequency bucketing into 7 layers) turns a flat pattern into stacked relief layers — for making actual 3D bead sculptures, shakelight-style.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single-file export (and the bug that broke it)
&lt;/h2&gt;

&lt;p&gt;The exported file is the entire app bundle + your current state (blueprint, progress, source image as a compressed data URL) embedded in a &lt;code&gt;&amp;lt;script type="application/json"&amp;gt;&lt;/code&gt; tag. On open, the app checks that tag, restores state into &lt;code&gt;localStorage&lt;/code&gt;, and you continue where you left off. No server, no database — the file &lt;em&gt;is&lt;/em&gt; the database.&lt;/p&gt;

&lt;p&gt;My first implementation did the injection with a regex:&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;// ❌ DO NOT DO THIS&lt;/span&gt;
&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&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;/&amp;lt;script id="perlerSaved"&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;script&amp;gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`&amp;lt;script id="perlerSaved"&amp;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="s2"&gt;&amp;lt;/script&amp;gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked… until it didn't. The check I skipped: &lt;strong&gt;the marker string &lt;code&gt;perlerSaved&lt;/code&gt; also exists inside the bundled JS&lt;/strong&gt; — in the very restore code that reads the tag, and inside minified variable-containing chunks. The regex matched &lt;em&gt;there first&lt;/em&gt;, and replaced ~84KB of application code with a JSON blob. The exported file opened to a white screen.&lt;/p&gt;

&lt;p&gt;Two lessons, now burned into the project's docs:&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;// ✅ DOM-based injection: the parser finds THE element, not any text occurrence&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tpl&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;tpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&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;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#perlerSaved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// textContent never executes, never mis-matches&lt;/span&gt;
&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;!DOCTYPE html&amp;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;tpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&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;strong&gt;Never regex-replace structured markup.&lt;/strong&gt; Parse, query, mutate, serialize. The regex version produced a file that worked exactly once — when I tested it before the bundle contained the string, and never again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the export against the real production bundle&lt;/strong&gt;, not a dev build. Dev bundles don't contain minified code that collides with your markers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bonus property of this design: the exported file strips external &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; tags, so your exported copy has &lt;strong&gt;no analytics, no external requests&lt;/strong&gt; — it works offline forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small things that mattered more than expected
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Printing bead codes inside grid cells&lt;/strong&gt; came from a single user comment ("no way to know which color goes where without cross-checking the list"). Cells auto-hide codes below ~13px, and text color flips black/white by cell luminance with a contrast outline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language toggle (中文/EN)&lt;/strong&gt; — first-use detection from &lt;code&gt;navigator.language&lt;/code&gt;, persisted choice. A pixel-art tool is visual, but "MARD 221" labels and toasts still need to be readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pointer Events everywhere&lt;/strong&gt; — one code path for mouse and touch, pinch-zoom included. Writing mouse + touch handlers separately in 2026 is self-harm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-friendly analytics&lt;/strong&gt;: &lt;a href="https://www.goatcounter.com" rel="noopener noreferrer"&gt;GoatCounter&lt;/a&gt;, no cookies, event names like &lt;code&gt;e/upload/104/mard48/zh/m&lt;/code&gt; (event/size/palette/language/device). One gotcha I hit the hard way: GoatCounter silently classifies traffic from &lt;strong&gt;cloud-provider IP ranges as bots&lt;/strong&gt; — if you test your own site through a VPN/proxy, your dashboard stays at zero and nothing tells you why.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live&lt;/strong&gt;: &lt;a href="https://perler.phtbyte.com/" rel="noopener noreferrer"&gt;https://perler.phtbyte.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://github.com/pht1991/perler-blueprint" rel="noopener noreferrer"&gt;https://github.com/pht1991/perler-blueprint&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs locally; upload an image, hit the 3D view, and export a single file as a souvenir.&lt;/p&gt;

&lt;p&gt;If you build something with a pattern from it (or find a quantization case where it picks a clearly wrong color), I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>threejs</category>
    </item>
    <item>
      <title>COCOS 4 Is Open Source — But Is It Time to Leave Cocos Creator 3.8?</title>
      <dc:creator>PHT</dc:creator>
      <pubDate>Tue, 13 Oct 2026 03:51:19 +0000</pubDate>
      <link>https://dev.to/pht1991/cocos-4-is-open-source-but-is-it-time-to-leave-cocos-creator-38-577m</link>
      <guid>https://dev.to/pht1991/cocos-4-is-open-source-but-is-it-time-to-leave-cocos-creator-38-577m</guid>
      <description>&lt;h2&gt;
  
  
  description: "COCOS 4 went fully open source under MIT in early 2026. I run a Cocos Creator 3.8 mini-game in production — here's my honest call on whether to migrate, and what actually changed."
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cross-post note:&lt;/strong&gt; This is the original article from my blog, &lt;a href="https://phtbyte.com/articles/cocos-4-open-source.html" rel="noopener noreferrer"&gt;Indie Game Lab&lt;/a&gt;. Read it there for the properly styled version, or keep reading here — it's identical.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I run a Cocos Creator 3.8 WeChat mini-game in production. COCOS 4 — fully open-sourced under MIT in early 2026 — is the biggest shift in the Cocos ecosystem in years, but it is &lt;em&gt;not&lt;/em&gt; a "drop 3.8 now" signal, especially for mini-game developers. The engine and editor have been split apart (the new IDE is called PinK), WeChat support is rolling out gradually, and 3.8 is still the more stable bet for my use case. Here's the honest breakdown and the call I'm making.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The headline: what actually shipped
&lt;/h2&gt;

&lt;p&gt;Cocos was acquired by the Chinese gamification company SUD for around $72M in November 2025. On January 5, 2026, SUD announced that &lt;strong&gt;COCOS 4 is fully open source under the standard MIT license&lt;/strong&gt; — every commercial clause from the old Cocos Creator has been removed. The public repositories include the engine core (&lt;code&gt;github.com/cocos/cocos4&lt;/code&gt;), the cross-platform code, the &lt;code&gt;cocos-cli&lt;/code&gt;, and a full headless IDE mode. There is no commercialization target behind it; SUD's stated motive is reaching more developers and feeding more game content into its own ecosystem.&lt;/p&gt;

&lt;p&gt;For a solo dev, "MIT, no strings" is a genuinely big deal. But the headlines skipped the part that actually affects your day-to-day: this isn't a normal version bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The part that confuses everyone: it's not "Cocos Creator 4"
&lt;/h2&gt;

&lt;p&gt;Through versions 1.x, 2.x, and 3.x, "Cocos Creator" meant &lt;em&gt;one thing&lt;/em&gt;: the engine and the editor bundled together. COCOS 4 breaks that apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;COCOS&lt;/strong&gt; = the engine only (version 4 is just the latest release number).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PinK&lt;/strong&gt; = the new standalone IDE. The visual editing features from Cocos Creator migrate here over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cocos-cli&lt;/strong&gt; = headless mode. Core editing functions move to CLI-accessible headless operations that become part of the engine core.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when you search "Cocos Creator 4," what you actually mean is &lt;strong&gt;COCOS 4 (engine) + PinK (IDE)&lt;/strong&gt;. Same lineage, different packaging. If you're evaluating it, search "COCOS 4" — you'll find the right repos and docs.&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%2Fdgkdgt3ejzg0l7nrajur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgkdgt3ejzg0l7nrajur.png" alt="Before (one bundle) vs After (engine + PinK IDE + cocos-cli split apart)" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The brand rename is the surface change. The structural one is engine/editor separation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What changed that a solo dev actually cares about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MIT, no commercial clauses.&lt;/strong&gt; You can read, patch, and even fork the engine into your own modified build. For a one-person team that's been burned by licensing surprises elsewhere, this removes a whole class of risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-native by design.&lt;/strong&gt; JavaScript and TypeScript stay the primary languages, and new capabilities are delivered as MCPs or Agents rather than traditional libraries — UI components, for example, ship as fully deliverable Agents, not call-and-use widgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighter and more modular.&lt;/strong&gt; The stated goal is "faster, smaller, more efficient," meant to run on any device with a screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform, but read the fine print.&lt;/strong&gt; Native platforms are covered across the board; &lt;em&gt;mini-game platform support rolls out gradually&lt;/em&gt;, and Steam-class native targets are on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward compatibility.&lt;/strong&gt; COCOS 4 is an evolution of 3.x, follows SemVer, and keeps a minimum six-month deprecate-to-remove window.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. What it means for a solo indie
&lt;/h2&gt;

&lt;p&gt;The upside is real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero licensing anxiety, forever.&lt;/li&gt;
&lt;li&gt;You can fix engine bugs yourself instead of waiting on a vendor.&lt;/li&gt;
&lt;li&gt;An open PR flywheel — more contributors, faster fixes.&lt;/li&gt;
&lt;li&gt;AI-tooling that actually understands the engine because the source is open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the open questions matter more for someone shipping today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PinK is a new IDE.&lt;/strong&gt; The Cocos Creator editor you know (and that I've shipped two projects on) is battle-tested. PinK is where visual editing is migrating — its maturity is unproven, and as of this writing it's shipping in alpha behind activation codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration has a cost.&lt;/strong&gt; API cleanup and the deprecation cycle mean a 3.x project won't lift-and-shift for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The WeChat mini-game target is "gradual."&lt;/strong&gt; For me this is the dealbreaker line — see below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. My call as a Cocos Creator 3.8 developer
&lt;/h2&gt;

&lt;p&gt;My constraints are specific: my game &lt;em&gt;is&lt;/em&gt; a WeChat mini-game, it's in production on 3.8, and all my hard-won optimization work — engine subpackaging, asset splitting, a runtime PerfTier — is built on 3.8 (I wrote it up in &lt;a href="https://phtbyte.com/articles/bundle-size-optimization.html" rel="noopener noreferrer"&gt;my bundle-size teardown&lt;/a&gt;). COCOS 4's WeChat support is still rolling out. Migrating now trades proven stability for an "open source" label I don't strictly need yet.&lt;/p&gt;

&lt;p&gt;So my decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stay on 3.8&lt;/strong&gt; for anything already shipping to WeChat — it's the stable, complete path today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate COCOS 4&lt;/strong&gt; for a &lt;em&gt;new&lt;/em&gt; project that needs native/Steam targets, a hard MIT requirement, and can tolerate an early PinK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch, don't rush.&lt;/strong&gt; The six-month deprecation window and the gradual mini-game rollout both reward patience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4x8h98om26ais22lepma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4x8h98om26ais22lepma.png" alt="Migration decision tree: platform target decides before version number" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The decision I run: platform target decides before version number does.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. If you're still choosing an engine
&lt;/h2&gt;

&lt;p&gt;None of this changes the core advice in &lt;a href="https://phtbyte.com/articles/cocos-vs-unity-vs-godot.html" rel="noopener noreferrer"&gt;my Cocos vs Unity vs Godot breakdown&lt;/a&gt;: for a WeChat or HTML5 mini-game, the 3.x line's native build pipeline and tiny first-load package are still unmatched. COCOS 4 makes the long-term future of that engine more open and more interesting — it just isn't the tool you need to switch to this afternoon. Decide by your distribution channel first; version number second.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Related reads
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://phtbyte.com/articles/cocos-vs-unity-vs-godot.html" rel="noopener noreferrer"&gt;Cocos Creator vs Unity vs Godot: My Solo-Indie Pick (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://phtbyte.com/articles/how-i-built-kubi.html" rel="noopener noreferrer"&gt;How I Built "Super Miserable Adventurer" — A Cocos Creator WeChat Mini-Game Case Study&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://phtbyte.com/articles/bundle-size-optimization.html" rel="noopener noreferrer"&gt;How I Cut My Mini-Game Bundle Size 60% (Engine Subpackaging + PerfTier)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://phtbyte.com/articles/cocos-4-open-source.html" rel="noopener noreferrer"&gt;Indie Game Lab&lt;/a&gt;. If you found this useful, that's where the deeper teardowns (bundle size, the full game post-mortem) live.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cocos</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>indiegame</category>
    </item>
    <item>
      <title>Cocos Creator 3.8 for Beginners: Build Your First Mini-Game</title>
      <dc:creator>PHT</dc:creator>
      <pubDate>Fri, 09 Oct 2026 09:37:23 +0000</pubDate>
      <link>https://dev.to/pht1991/cocos-creator-38-for-beginners-build-your-first-mini-game-5bdn</link>
      <guid>https://dev.to/pht1991/cocos-creator-38-for-beginners-build-your-first-mini-game-5bdn</guid>
      <description>&lt;h2&gt;
  
  
  description: "A hands-on walkthrough to your first Cocos Creator 3.8 WeChat mini-game — project setup, the node + component model, your first TypeScript script, and the build-to-DevTools flow, with the gotchas I hit shipping a real game."
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cross-post note:&lt;/strong&gt; This is the original article from my blog, &lt;a href="https://phtbyte.com/articles/cocos-creator-beginner-guide.html" rel="noopener noreferrer"&gt;Indie Game Lab&lt;/a&gt;. Read it there for the properly styled version, or keep reading here — it's identical.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; You don't need a game-design degree to ship something on Cocos Creator 3.8 — you need the &lt;em&gt;flow&lt;/em&gt;: create a WeChat mini-game project, understand that everything is a &lt;strong&gt;node with components&lt;/strong&gt;, write one tiny TypeScript script, then build and preview in WeChat DevTools. I shipped a full survival/crafting game on this exact pipeline (the post-mortem is &lt;a href="https://phtbyte.com/articles/how-i-built-kubi.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;), and the beginner path below is the same five steps I wish someone had handed me on day one — plus the three gotchas that cost me real hours.&lt;/p&gt;

&lt;p&gt;This guide assumes zero Cocos experience. If you're still deciding whether Cocos is even the right engine, my &lt;a href="https://phtbyte.com/articles/cocos-vs-unity-vs-godot.html" rel="noopener noreferrer"&gt;engine comparison&lt;/a&gt; makes the call for a solo indie. If you've already built a prototype, skip to the build-and-preview section — but read the gotchas even if you skip everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What you'll have at the end
&lt;/h2&gt;

&lt;p&gt;By the end of this walkthrough you'll have a mini-game you can tap to play in WeChat DevTools: a character sprite that moves when you press a key or tap the screen, sitting in a scene you assembled yourself. It won't be the next hit — but it will be &lt;em&gt;real, running code&lt;/em&gt;, and every game I've shipped started as exactly this: one sprite that does one thing.&lt;/p&gt;

&lt;p&gt;More importantly, you'll internalize the one mental model Cocos is built around, because almost every beginner confusion (why doesn't my image show up? why won't it move? where do I put my code?) comes from not seeing it yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Step 1 — Create the project (and pick the right template)
&lt;/h2&gt;

&lt;p&gt;Open Cocos Creator 3.8 LTS and hit &lt;strong&gt;New Project&lt;/strong&gt;. Two choices matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Template:&lt;/strong&gt; pick the &lt;em&gt;2D&lt;/em&gt; template (or the "Empty" 2D one if you want zero boilerplate). Most mini-games — including mine — are 2D.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save path:&lt;/strong&gt; avoid deep paths with spaces or Chinese characters. A short ASCII path like &lt;code&gt;D:/dev/my-first-game&lt;/code&gt; saves you from cryptic build errors later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do &lt;em&gt;not&lt;/em&gt; need to pick "WeChat Mini Game" as a template — that's not a starting template, it's a &lt;em&gt;build target&lt;/em&gt; you choose later. Start in the editor like any other project; the WeChat part is a one-click build at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Step 2 — The node + component model (the thing to understand)
&lt;/h2&gt;

&lt;p&gt;This is the core. In Cocos, the scene is a &lt;strong&gt;tree of nodes&lt;/strong&gt;. A node is just a position in space with a transform. It does nothing by itself. To make a node &lt;em&gt;do&lt;/em&gt; something — show an image, play sound, run logic — you attach &lt;strong&gt;components&lt;/strong&gt; to it. A sprite on screen is a node that happens to carry a &lt;code&gt;Sprite&lt;/code&gt; component (which points at a texture) and a &lt;code&gt;Transform&lt;/code&gt; component (where it sits).&lt;/p&gt;

&lt;p&gt;This separation is why beginners get stuck: they add an image but forget the Sprite component, or write code but never attach the script component to a node. The rule is simple — &lt;strong&gt;code lives in components, components live on nodes, nodes live in the scene tree.&lt;/strong&gt; Nothing runs unless it's attached to a node in the tree.&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%2F494sf8slwebbkmvkwwbo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F494sf8slwebbkmvkwwbo.png" alt="The Cocos scene-tree model: a Player node carrying Transform, Sprite, and a custom MoveScript component, with update() running ~60×/sec." width="799" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The whole engine in one picture: a node in the tree, the components that give it behavior, and the update loop that runs your script ~60 times a second.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Step 3 — Your first script (make it move)
&lt;/h2&gt;

&lt;p&gt;Right-click the &lt;code&gt;assets&lt;/code&gt; panel → &lt;strong&gt;New → TypeScript&lt;/strong&gt; → name it &lt;code&gt;MoveScript&lt;/code&gt;. Double-click to open it. Cocos gives you a stub with &lt;code&gt;start()&lt;/code&gt; and &lt;code&gt;update()&lt;/code&gt;. Edit it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;_decorator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cc&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ccclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_decorator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ccclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MoveScript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MoveScript&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&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;deltaTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// move right at 100 units per second&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPosition&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&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="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;deltaTime&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the crucial step beginners miss: &lt;strong&gt;attach the script to the node.&lt;/strong&gt; Drag &lt;code&gt;MoveScript.ts&lt;/code&gt; from the assets panel onto your Player node in the &lt;em&gt;Hierarchy&lt;/em&gt;, or select the node and click &lt;strong&gt;Add Component → Custom → MoveScript&lt;/strong&gt; in the &lt;em&gt;Inspector&lt;/em&gt;. Hit the play button at the top — the sprite drifts right. That's your first game logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Step 4 — Build for WeChat Mini Game
&lt;/h2&gt;

&lt;p&gt;Top menu → &lt;strong&gt;Project → Build…&lt;/strong&gt;. Set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform:&lt;/strong&gt; WeChat Mini Game&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start Scene:&lt;/strong&gt; your main scene&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appid:&lt;/strong&gt; your WeChat mini-game appid (or use the "test" appid while learning — Cocos accepts a placeholder for local preview)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two flags matter for real projects and trip up everyone eventually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate Engine:&lt;/strong&gt; ON. It splits the engine from your code so the engine is cached by WeChat and your updates stay tiny. I left this off once and wondered why every upload was huge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MD5 Cache:&lt;/strong&gt; ON for production. It lets WeChat cache unchanged files so players re-download less on update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Build&lt;/strong&gt;. Cocos outputs a folder (default &lt;code&gt;build/wechatgame/&lt;/code&gt;). You don't open that folder directly — you open it &lt;em&gt;with&lt;/em&gt; DevTools, next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Step 5 — Preview in WeChat DevTools (the gotcha)
&lt;/h2&gt;

&lt;p&gt;Install &lt;strong&gt;WeChat DevTools&lt;/strong&gt; (the mini-program IDE, not the consumer WeChat app). In Cocos &lt;strong&gt;Preferences → Engine Manager&lt;/strong&gt;, point "WeChat DevTools Path" at the installed executable — Cocos uses it to auto-launch the preview.&lt;/p&gt;

&lt;p&gt;The gotcha: &lt;strong&gt;never open WeChat DevTools first and then build.&lt;/strong&gt; Build from Cocos &lt;em&gt;first&lt;/em&gt;, then let Cocos (or the "Preview" button) launch DevTools and import the folder. If DevTools is already open on a stale build, it silently shows the old version and you'll think your code didn't take. Close DevTools, build, then preview.&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%2Fisxy92sps8w9jiyj7dwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fisxy92sps8w9jiyj7dwu.png" alt="The five-step path from an empty Cocos project to a running WeChat mini-game preview." width="800" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Five steps, in order. The only non-obvious part is step 5: build before you open DevTools, or you'll be debugging yesterday's code.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Three gotchas that cost me real hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The WeChat size ceiling is real.&lt;/strong&gt; A mini-game's main package is capped at 4 MB, with subpackages adding more. My first build blew past it in a weekend. The fix isn't "make fewer features" — it's engine subpackaging, an asset audit, and a device-tier split, which I wrote up in my &lt;a href="https://phtbyte.com/articles/bundle-size-optimization.html" rel="noopener noreferrer"&gt;bundle-size teardown&lt;/a&gt;. Learn the ceiling now, not at launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perf is a device problem, not an editor problem.&lt;/strong&gt; It runs smooth on your laptop and stutters on a three-year-old phone. Draw calls and overdraw dominate the frame on mobile — my &lt;a href="https://phtbyte.com/articles/cocos-draw-call-optimization.html" rel="noopener noreferrer"&gt;runtime perf teardown&lt;/a&gt; shows the three moves that fixed mine. Profile on a real device early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code that isn't attached to a node doesn't exist.&lt;/strong&gt; If your script "does nothing," 9 times out of 10 it's not on a node in the scene tree, or the node isn't in the active scene. Check the Hierarchy, not the code, first.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  8. Where to go next
&lt;/h2&gt;

&lt;p&gt;You now have a sprite that moves. That's the seed of everything. From here the natural path is: add input (tap/keyboard) to control direction, add a second node as an obstacle, then a score. When you're ready to ship something bigger than a demo, the two articles that saved my real release are the &lt;a href="https://phtbyte.com/articles/bundle-size-optimization.html" rel="noopener noreferrer"&gt;bundle-size teardown&lt;/a&gt; and the &lt;a href="https://phtbyte.com/articles/cocos-draw-call-optimization.html" rel="noopener noreferrer"&gt;draw-call optimization&lt;/a&gt; piece — read them before your build crosses a few megabytes or your frame time crosses 16 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Takeaways
&lt;/h2&gt;

&lt;p&gt;Cocos Creator 3.8 is approachable: create a 2D project, internalize that &lt;em&gt;nodes carry components and components carry your code&lt;/em&gt;, write one &lt;code&gt;update()&lt;/code&gt; loop, then build to WeChat and preview through DevTools (build first, open DevTools second). The engine hides the hard parts — package ceilings and mobile perf — until launch day, so learn those constraints early via the bundle and draw-call write-ups. Everything you ship later is just more nodes, more components, and more loops. Start small, ship the sprite that moves, then grow it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://phtbyte.com/articles/cocos-creator-beginner-guide.html" rel="noopener noreferrer"&gt;Indie Game Lab&lt;/a&gt;. If you found this useful, that's where the deeper teardowns (bundle size, draw calls, the full game post-mortem) live.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cocoscreator</category>
      <category>gamedev</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
