summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--_projects/bumblebee.md8
-rw-r--r--_projects/matrix-digital-rain.md98
-rw-r--r--_site/feed.xml2
-rw-r--r--_site/posts.xml2
-rw-r--r--_site/projects/bumblebee/index.html8
-rw-r--r--_site/projects/matrix-digital-rain/index.html98
-rw-r--r--_site/robots.txt2
-rw-r--r--_site/sitemap.xml30
8 files changed, 200 insertions, 48 deletions
diff --git a/_projects/bumblebee.md b/_projects/bumblebee.md
index b322969..80acfe3 100644
--- a/_projects/bumblebee.md
+++ b/_projects/bumblebee.md
@@ -38,10 +38,10 @@ target="_blank" rel="noopener noreferrer">Scintilla.NET</a> editor), debounce
events, and block hidden elements and scripts.
Before settling on a desktop application, we contemplated designing Bumblebee
-as a browser extension. We decided against that because we didn't want the
-browser vendors to dictate Bumblebee's capabilities. Besides, the company's
-security policy, which prohibited browser extensions, would have complicated
-the deployment of an extension-based solution. The initial prototype used a C#
+as a browser extension. We chose the desktop app because extensions don't offer
+the deep, event-based control we needed. Besides, the company's security
+policy, which prohibited browser extensions, would have complicated the
+deployment of an extension-based solution. The initial prototype used a C#
wrapper of the Chromium project instead of WebView. WebView's more intuitive
API and its seamless integration with Windows Forms led us to choose it over
the Chromium wrapper.
diff --git a/_projects/matrix-digital-rain.md b/_projects/matrix-digital-rain.md
index f9a1a22..35a3d97 100644
--- a/_projects/matrix-digital-rain.md
+++ b/_projects/matrix-digital-rain.md
@@ -5,7 +5,8 @@ thumbnail: thumb.png
layout: post
---
-The famous digital rain from The Matrix, implemented in C.
+"All I see is blonde, brunette, red head." The iconic digital rain from The
+Matrix, implemented in C, without dependencies (not even ncurses).
<video style="max-width:100%;" controls="" poster="thumb.png">
<source src="matrix.mp4" type="video/mp4">
@@ -13,17 +14,94 @@ The famous digital rain from The Matrix, implemented in C.
This project is a fork of Domsson's beautiful <a
href="https://github.com/domsson/fakesteak" class="external" target="_blank"
-rel="noopener noreferrer">Fakesteak</a>.
+rel="noopener noreferrer">Fakesteak</a>. Use the following commands to compile
+and run the program:
-There are three color settings: head, tail, and background. They are configured
-by setting the 24-bit RGB channels using `COLOR_*_RED`, `COLOR_*_GRN`, and
-`COLOR_*_BLU` definitions. The ghosting effect of old monochrome screens is
-achieved by scaling the RGB channels. This results in a rain effect that
-closely resembles the original from the first Matrix movie.
+```
+$ cc -O3 main.c -o matrix
+$ ./matrix
+```
-In addition, this implementation supports UTF-32 character sets. The
-`UNICODE_MIN` and `UNICODE_MAX` controls the Unicode block used. For
-instance, setting them to `0x30A1` and `0x30F6` rains Katakana:
+While I loved Domsson's take on the
+digital rain, what blew my mind was the minimalistic elegance of his code. As I
+carefully examined it, I thought about what it might take to recreate the
+original digital rain from the first Matrix movie with it. The challenge is
+adding these features without destroying fakesteak's elegance.
+
+## How does it work?
+
+The `matrix` struct makes use of three 2D arrays to encode the Matrix: the
+`code` array for 32-bit Unicode characters, the `rgb` array for 24-bit RGB
+values of the character (foreground color), and the `shade` array for the
+degree of transparency of the character to simulate the ghosting effect of old
+monochrome displays. The dimensions of these arrays depend on the size of the
+terminal screen. Each slot in the array corresponds to a cursor position on the
+screen.
+
+The ghosting effect, which is arguably the crowning feature of my version, is
+implemented by carefully scaling and mixing the RGB channels:
+
+```
+static void mat_shade(matrix *mat, size_t row, size_t col)
+{
+ unsigned char *color;
+ color = mat->rgb[mat_idx(mat, row, col)].color;
+ color[R] = color[R] - (color[R] - COLOR_BG_RED) / 2;
+ color[G] = color[G] - (color[G] - COLOR_BG_GRN) / 2;
+ color[B] = color[B] - (color[B] - COLOR_BG_BLU) / 2;
+}
+```
+
+The above algorithm achieves transparency by iteratively bringing the
+foreground color closer to the background color with each pass of the rain.
+This approach offers multiple advantages, such as simpler and more natural
+color configuration (background, foreground, and the color of the first drop)
+that lends itself well to Unix ricing, and of course, recreates The Matrix rain
+with high fidelity.
+
+Rather than heavy-weight graphics tool kits, we use ANSI escape codes to
+control the terminal screen. It's the effective use of the ANSI escape codes
+that greatly contributes to the minimalism of the solution:
+
+```
+static void term_print(const matrix *mat, size_t row, size_t col)
+{
+ size_t idx;
+ idx = mat_idx(mat, row, col);
+ wprintf(L"\x1b[%d;%dH\x1b[38;2;%d;%d;%dm%lc",
+ row, col,
+ mat->rgb[idx].color[R],
+ mat->rgb[idx].color[G],
+ mat->rgb[idx].color[B],
+ mat->code[idx]);
+}
+```
+
+Finally, the glitch effect is controlled by the following code:
+
+```
+if (mat.row[i] > 0 && rand() % 6 == 0) {
+ j = rand() % mat.row[i];
+ if (mat.code[mat_idx(&mat, j, mat.col[i])] != ' ') {
+ mat_put_code(&mat, j, mat.col[i]);
+ term_print(&mat, j, mat.col[i]);
+ }
+}
+```
+
+The above code causes glitches in the Matrix with a probablity of 1/6.
+
+## Customizing the rain
+
+While you can customize almost any aspect of the rain including its speed,
+glitch frequency, and the density of the rain, the most useful settings for
+ricing are the color scheme and the character set used for the rain.
+
+There are three color settings: the head, the tail, and the background. They
+are configured by setting the `COLOR_*_RED`, `COLOR_*_GRN`, and `COLOR_*_BLU`
+definitions in main.c. The `UNICODE_MIN` and `UNICODE_MAX` values control the
+Unicode block used. For instance, setting them to `0x30A1` and `0x30F6` rains
+Katakana code points:
<img style="width: 100%;" src="katakana.png" />
diff --git a/_site/feed.xml b/_site/feed.xml
index 8a973b4..ff9191d 100644
--- a/_site/feed.xml
+++ b/_site/feed.xml
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-12-11T22:00:02+08:00</updated><id>/feed.xml</id><title type="html">ASCIIMX | Blog</title><author><name>W. D. Sadeep Madurange</name></author><entry><title type="html">How I manage Suckless software installations</title><link href="/blog/suckless-software/" rel="alternate" type="text/html" title="How I manage Suckless software installations" /><published>2025-11-30T00:00:00+08:00</published><updated>2025-11-30T00:00:00+08:00</updated><id>/blog/suckless-software</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Since suckless software requires users to modify the source code and recompile to customize, I need a way to maintain patches over the long term while retaining the ability to upgrade the software as new versions are released.]]></summary></entry><entry><title type="html">Neo4J A* search</title><link href="/blog/neo4j-a-star-search/" rel="alternate" type="text/html" title="Neo4J A* search" /><published>2025-09-14T00:00:00+08:00</published><updated>2025-09-14T00:00:00+08:00</updated><id>/blog/neo4j-a-star-search</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Back in 2018, we used Neo4J graph database to track the movement of marine vessels. We were interested in the shortest path a ship could take through a network of about 13,000 route points. Algorithms based on graph theory, such as A* search, provide optimal solutions to such problems. In other words, the set of route points lends itself well to a model based on graphs.]]></summary></entry><entry><title type="html">MOSFETs as electronic switches</title><link href="/blog/mosfet-switches/" rel="alternate" type="text/html" title="MOSFETs as electronic switches" /><published>2025-06-22T00:00:00+08:00</published><updated>2025-06-22T00:00:00+08:00</updated><id>/blog/mosfet-switches</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Recently, I needed a low-power circuit for one of my battery-operated projects. Much of the system’s power savings depended on its ability to electronically switch off components, such as servos, that draw high levels of quiescent currents. My search for a solution led me to MOSFETs, transistors capable of controlling circuits operating at voltages far above their own.]]></summary></entry><entry><title type="html">How to configure ATmega328P microcontrollers to run at 3.3V and 5V</title><link href="/blog/arduino-uno/" rel="alternate" type="text/html" title="How to configure ATmega328P microcontrollers to run at 3.3V and 5V" /><published>2025-04-10T00:00:00+08:00</published><updated>2025-04-10T00:00:00+08:00</updated><id>/blog/arduino-uno</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This is a quick reference for wiring up ATmega328P ICs to run at 5V and 3.3V. While the 5V configuration is common, the 3.3V configuration can be useful in low-power applications and when interfacing with parts that themselves run at 3.3V. In this guide, the 5V setup is configured with a 16MHz crystal oscillator, while the 3.3V configuration makes use of an 8MHz crystal oscillator.]]></summary></entry><entry><title type="html">How to set up ATSAM3X8E microcontrollers for bare-metal programming in C</title><link href="/blog/arduino-due/" rel="alternate" type="text/html" title="How to set up ATSAM3X8E microcontrollers for bare-metal programming in C" /><published>2024-10-05T00:00:00+08:00</published><updated>2024-10-05T00:00:00+08:00</updated><id>/blog/arduino-due</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This article is a step-by-step guide for programming bare-metal ATSAM3X8E chips found on Arduino Due boards. It also includes notes on the chip’s memory layout relevant for writing linker scripts. The steps described in this article were tested on an OpenBSD workstation.]]></summary></entry></feed> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="http://localhost:4000/feed.xml" rel="self" type="application/atom+xml" /><link href="http://localhost:4000/" rel="alternate" type="text/html" /><updated>2025-12-12T23:16:15+08:00</updated><id>http://localhost:4000/feed.xml</id><title type="html">ASCIIMX | Blog</title><author><name>W. D. Sadeep Madurange</name></author><entry><title type="html">How I manage Suckless software installations</title><link href="http://localhost:4000/blog/suckless-software/" rel="alternate" type="text/html" title="How I manage Suckless software installations" /><published>2025-11-30T00:00:00+08:00</published><updated>2025-11-30T00:00:00+08:00</updated><id>http://localhost:4000/blog/suckless-software</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Since suckless software requires users to modify the source code and recompile to customize, I need a way to maintain patches over the long term while retaining the ability to upgrade the software as new versions are released.]]></summary></entry><entry><title type="html">Neo4J A* search</title><link href="http://localhost:4000/blog/neo4j-a-star-search/" rel="alternate" type="text/html" title="Neo4J A* search" /><published>2025-09-14T00:00:00+08:00</published><updated>2025-09-14T00:00:00+08:00</updated><id>http://localhost:4000/blog/neo4j-a-star-search</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Back in 2018, we used Neo4J graph database to track the movement of marine vessels. We were interested in the shortest path a ship could take through a network of about 13,000 route points. Algorithms based on graph theory, such as A* search, provide optimal solutions to such problems. In other words, the set of route points lends itself well to a model based on graphs.]]></summary></entry><entry><title type="html">MOSFETs as electronic switches</title><link href="http://localhost:4000/blog/mosfet-switches/" rel="alternate" type="text/html" title="MOSFETs as electronic switches" /><published>2025-06-22T00:00:00+08:00</published><updated>2025-06-22T00:00:00+08:00</updated><id>http://localhost:4000/blog/mosfet-switches</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Recently, I needed a low-power circuit for one of my battery-operated projects. Much of the system’s power savings depended on its ability to electronically switch off components, such as servos, that draw high levels of quiescent currents. My search for a solution led me to MOSFETs, transistors capable of controlling circuits operating at voltages far above their own.]]></summary></entry><entry><title type="html">How to configure ATmega328P microcontrollers to run at 3.3V and 5V</title><link href="http://localhost:4000/blog/arduino-uno/" rel="alternate" type="text/html" title="How to configure ATmega328P microcontrollers to run at 3.3V and 5V" /><published>2025-04-10T00:00:00+08:00</published><updated>2025-04-10T00:00:00+08:00</updated><id>http://localhost:4000/blog/arduino-uno</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This is a quick reference for wiring up ATmega328P ICs to run at 5V and 3.3V. While the 5V configuration is common, the 3.3V configuration can be useful in low-power applications and when interfacing with parts that themselves run at 3.3V. In this guide, the 5V setup is configured with a 16MHz crystal oscillator, while the 3.3V configuration makes use of an 8MHz crystal oscillator.]]></summary></entry><entry><title type="html">How to set up ATSAM3X8E microcontrollers for bare-metal programming in C</title><link href="http://localhost:4000/blog/arduino-due/" rel="alternate" type="text/html" title="How to set up ATSAM3X8E microcontrollers for bare-metal programming in C" /><published>2024-10-05T00:00:00+08:00</published><updated>2024-10-05T00:00:00+08:00</updated><id>http://localhost:4000/blog/arduino-due</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This article is a step-by-step guide for programming bare-metal ATSAM3X8E chips found on Arduino Due boards. It also includes notes on the chip’s memory layout relevant for writing linker scripts. The steps described in this article were tested on an OpenBSD workstation.]]></summary></entry></feed> \ No newline at end of file
diff --git a/_site/posts.xml b/_site/posts.xml
index e303b1c..803d3c7 100644
--- a/_site/posts.xml
+++ b/_site/posts.xml
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/posts.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-12-11T22:00:02+08:00</updated><id>/posts.xml</id><title type="html">ASCIIMX</title><author><name>W. D. Sadeep Madurange</name></author></feed> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="http://localhost:4000/posts.xml" rel="self" type="application/atom+xml" /><link href="http://localhost:4000/" rel="alternate" type="text/html" /><updated>2025-12-12T23:16:15+08:00</updated><id>http://localhost:4000/posts.xml</id><title type="html">ASCIIMX</title><author><name>W. D. Sadeep Madurange</name></author></feed> \ No newline at end of file
diff --git a/_site/projects/bumblebee/index.html b/_site/projects/bumblebee/index.html
index d6873e4..d8fded3 100644
--- a/_site/projects/bumblebee/index.html
+++ b/_site/projects/bumblebee/index.html
@@ -74,10 +74,10 @@ the script at any point during the session (using the embedded <a src="https://g
events, and block hidden elements and scripts.</p>
<p>Before settling on a desktop application, we contemplated designing Bumblebee
-as a browser extension. We decided against that because we didn’t want the
-browser vendors to dictate Bumblebee’s capabilities. Besides, the company’s
-security policy, which prohibited browser extensions, would have complicated
-the deployment of an extension-based solution. The initial prototype used a C#
+as a browser extension. We chose the desktop app because extensions don’t offer
+the deep, event-based control we needed. Besides, the company’s security
+policy, which prohibited browser extensions, would have complicated the
+deployment of an extension-based solution. The initial prototype used a C#
wrapper of the Chromium project instead of WebView. WebView’s more intuitive
API and its seamless integration with Windows Forms led us to choose it over
the Chromium wrapper.</p>
diff --git a/_site/projects/matrix-digital-rain/index.html b/_site/projects/matrix-digital-rain/index.html
index 666456b..b69ac2d 100644
--- a/_site/projects/matrix-digital-rain/index.html
+++ b/_site/projects/matrix-digital-rain/index.html
@@ -44,23 +44,97 @@
<h2 class="center" id="title">THE MATRIX DIGITAL RAIN</h2>
<h6 class="center">12 JANUARY 2024</h5>
<br>
- <div class="twocol justify"><p>The famous digital rain from The Matrix, implemented in C.</p>
+ <div class="twocol justify"><p>“All I see is blonde, brunette, red head.” The iconic digital rain from The
+Matrix, implemented in C, without dependencies (not even ncurses).</p>
<video style="max-width:100%;" controls="" poster="thumb.png">
<source src="matrix.mp4" type="video/mp4" />
</video>
-<p>This project is a fork of Domsson’s beautiful <a href="https://github.com/domsson/fakesteak" class="external" target="_blank" rel="noopener noreferrer">Fakesteak</a>.</p>
-
-<p>There are three color settings: head, tail, and background. They are configured
-by setting the 24-bit RGB channels using <code class="language-plaintext highlighter-rouge">COLOR_*_RED</code>, <code class="language-plaintext highlighter-rouge">COLOR_*_GRN</code>, and
-<code class="language-plaintext highlighter-rouge">COLOR_*_BLU</code> definitions. The ghosting effect of old monochrome screens is
-achieved by scaling the RGB channels. This results in a rain effect that
-closely resembles the original from the first Matrix movie.</p>
-
-<p>In addition, this implementation supports UTF-32 character sets. The
-<code class="language-plaintext highlighter-rouge">UNICODE_MIN</code> and <code class="language-plaintext highlighter-rouge">UNICODE_MAX</code> controls the Unicode block used. For
-instance, setting them to <code class="language-plaintext highlighter-rouge">0x30A1</code> and <code class="language-plaintext highlighter-rouge">0x30F6</code> rains Katakana:</p>
+<p>This project is a fork of Domsson’s beautiful <a href="https://github.com/domsson/fakesteak" class="external" target="_blank" rel="noopener noreferrer">Fakesteak</a>. Use the following commands to compile
+and run the program:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cc -O3 main.c -o matrix
+$ ./matrix
+</code></pre></div></div>
+
+<p>While I loved Domsson’s take on the
+digital rain, what blew my mind was the minimalistic elegance of his code. As I
+carefully examined it, I thought about what it might take to recreate the
+original digital rain from the first Matrix movie with it. The challenge is
+adding these features without destroying fakesteak’s elegance.</p>
+
+<h2 id="how-does-it-work">How does it work?</h2>
+
+<p>The <code class="language-plaintext highlighter-rouge">matrix</code> struct makes use of three 2D arrays to encode the Matrix: the
+<code class="language-plaintext highlighter-rouge">code</code> array for 32-bit Unicode characters, the <code class="language-plaintext highlighter-rouge">rgb</code> array for 24-bit RGB
+values of the character (foreground color), and the <code class="language-plaintext highlighter-rouge">shade</code> array for the
+degree of transparency of the character to simulate the ghosting effect of old
+monochrome displays. The dimensions of these arrays depend on the size of the
+terminal screen. Each slot in the array corresponds to a cursor position on the
+screen.</p>
+
+<p>The ghosting effect, which is arguably the crowning feature of my version, is
+implemented by carefully scaling and mixing the RGB channels:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static void mat_shade(matrix *mat, size_t row, size_t col)
+{
+ unsigned char *color;
+ color = mat-&gt;rgb[mat_idx(mat, row, col)].color;
+ color[R] = color[R] - (color[R] - COLOR_BG_RED) / 2;
+ color[G] = color[G] - (color[G] - COLOR_BG_GRN) / 2;
+ color[B] = color[B] - (color[B] - COLOR_BG_BLU) / 2;
+}
+</code></pre></div></div>
+
+<p>The above algorithm achieves transparency by iteratively bringing the
+foreground color closer to the background color with each pass of the rain.
+This approach offers multiple advantages, such as simpler and more natural
+color configuration (background, foreground, and the color of the first drop)
+that lends itself well to Unix ricing, and of course, recreates The Matrix rain
+with high fidelity.</p>
+
+<p>Rather than heavy-weight graphics tool kits, we use ANSI escape codes to
+control the terminal screen. It’s the effective use of the ANSI escape codes
+that greatly contributes to the minimalism of the solution:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static void term_print(const matrix *mat, size_t row, size_t col)
+{
+ size_t idx;
+ idx = mat_idx(mat, row, col);
+ wprintf(L"\x1b[%d;%dH\x1b[38;2;%d;%d;%dm%lc",
+ row, col,
+ mat-&gt;rgb[idx].color[R],
+ mat-&gt;rgb[idx].color[G],
+ mat-&gt;rgb[idx].color[B],
+ mat-&gt;code[idx]);
+}
+</code></pre></div></div>
+
+<p>Finally, the glitch effect is controlled by the following code:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (mat.row[i] &gt; 0 &amp;&amp; rand() % 6 == 0) {
+ j = rand() % mat.row[i];
+ if (mat.code[mat_idx(&amp;mat, j, mat.col[i])] != ' ') {
+ mat_put_code(&amp;mat, j, mat.col[i]);
+ term_print(&amp;mat, j, mat.col[i]);
+ }
+}
+</code></pre></div></div>
+
+<p>The above code causes glitches in the Matrix with a probablity of 1/6.</p>
+
+<h2 id="customizing-the-rain">Customizing the rain</h2>
+
+<p>While you can customize almost any aspect of the rain including its speed,
+glitch frequency, and the density of the rain, the most useful settings for
+ricing are the color scheme and the character set used for the rain.</p>
+
+<p>There are three color settings: the head, the tail, and the background. They
+are configured by setting the <code class="language-plaintext highlighter-rouge">COLOR_*_RED</code>, <code class="language-plaintext highlighter-rouge">COLOR_*_GRN</code>, and <code class="language-plaintext highlighter-rouge">COLOR_*_BLU</code>
+definitions in main.c. The <code class="language-plaintext highlighter-rouge">UNICODE_MIN</code> and <code class="language-plaintext highlighter-rouge">UNICODE_MAX</code> values control the
+Unicode block used. For instance, setting them to <code class="language-plaintext highlighter-rouge">0x30A1</code> and <code class="language-plaintext highlighter-rouge">0x30F6</code> rains
+Katakana code points:</p>
<p><img style="width: 100%;" src="katakana.png" /></p>
diff --git a/_site/robots.txt b/_site/robots.txt
index e087884..d297064 100644
--- a/_site/robots.txt
+++ b/_site/robots.txt
@@ -1 +1 @@
-Sitemap: /sitemap.xml
+Sitemap: http://localhost:4000/sitemap.xml
diff --git a/_site/sitemap.xml b/_site/sitemap.xml
index 1b23cd3..5130d9f 100644
--- a/_site/sitemap.xml
+++ b/_site/sitemap.xml
@@ -1,59 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
-<loc>/blog/arduino-due/</loc>
+<loc>http://localhost:4000/blog/arduino-due/</loc>
<lastmod>2024-10-05T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/blog/arduino-uno/</loc>
+<loc>http://localhost:4000/blog/arduino-uno/</loc>
<lastmod>2025-04-10T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/blog/mosfet-switches/</loc>
+<loc>http://localhost:4000/blog/mosfet-switches/</loc>
<lastmod>2025-06-22T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/blog/neo4j-a-star-search/</loc>
+<loc>http://localhost:4000/blog/neo4j-a-star-search/</loc>
<lastmod>2025-09-14T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/blog/suckless-software/</loc>
+<loc>http://localhost:4000/blog/suckless-software/</loc>
<lastmod>2025-11-30T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/projects/e-reader/</loc>
+<loc>http://localhost:4000/projects/e-reader/</loc>
<lastmod>2023-10-24T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/projects/matrix-digital-rain/</loc>
+<loc>http://localhost:4000/projects/matrix-digital-rain/</loc>
<lastmod>2024-01-12T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/projects/etlas/</loc>
+<loc>http://localhost:4000/projects/etlas/</loc>
<lastmod>2024-09-05T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/projects/bumblebee/</loc>
+<loc>http://localhost:4000/projects/bumblebee/</loc>
<lastmod>2025-04-02T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/projects/my-first-pcb/</loc>
+<loc>http://localhost:4000/projects/my-first-pcb/</loc>
<lastmod>2025-07-14T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/projects/fpm-door-lock/</loc>
+<loc>http://localhost:4000/projects/fpm-door-lock/</loc>
<lastmod>2025-10-03T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>/about/</loc>
+<loc>http://localhost:4000/about/</loc>
</url>
<url>
-<loc>/blog/</loc>
+<loc>http://localhost:4000/blog/</loc>
</url>
<url>
-<loc>/</loc>
+<loc>http://localhost:4000/</loc>
</url>
<url>
-<loc>/projects/</loc>
+<loc>http://localhost:4000/projects/</loc>
</url>
</urlset>