<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet href="../atomfeed.xslt" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>tag:redblobgames.com,2011:blog.posts</id>
  <title>Red Blob Games: latest blog posts</title>
  <updated>2026-04-17T16:13:29.681265-07:00</updated>
  <author>
    <name>Amit Patel</name>
    <email>redblobgames@gmail.com</email>
    <uri>http://www-cs-students.stanford.edu/~amitp/</uri>
  </author>
  <link href="https://www.redblobgames.com/blog/" rel="alternate"/>
  <link href="https://www.redblobgames.com/blog/posts.xml" rel="self"/>
  <generator uri="https://lkiesow.github.io/python-feedgen" version="1.0.0">python-feedgen</generator>
  <logo>https://www.redblobgames.com/img/logo-square-400.png</logo>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2026-04-16-highlighting-interactive-code-blocks</id>
    <title>Highlighting interactive code blocks</title>
    <updated>2026-04-17T16:13:29.681265-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
I have sample code on some of my pages. I try to use syntax highlighting when I can, but I don't use it everywhere. In particular, <a href="https://www.redblobgames.com/grids/hexagons/">Hexagons guide</a> and <a href="https://www.redblobgames.com/maps/terrain-from-noise/">Terrain from Noise</a> don't have syntax highlighting. Why?
</p>

<figure>
  <img width="75%" border="1" src="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/hex-without-highlighting.png" alt="(screenshot of sample code without highlightint)"/>
  <img width="75%" border="1" src="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/screenshot-interactive-code-sample.png" alt="(screenshot of sample code with an interactive element)"/>
  <figcaption>Sample code with no highlighting</figcaption>
</figure>

<x:cut/>


<p>
It's a lot of work to manually syntax highlight everything, and it makes my HTML harder to read and maintain:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">code</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">q + s + r</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">code</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
→
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">code</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">span</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"q"</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">q</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">span</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text"> + </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">span</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"s"</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">s</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">span</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
 + </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">span</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"r"</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">r</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">span</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text"> = 0</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">code</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
Why not use a syntax highlighting library? These libraries take text as input and produce HTML as output. But on these interactive pages, the input is HTML. I have manually marked up the code with embedded interactive elements that respond to the reader's choices:
</p>


<figure id="orgaa54ad0">
<img src="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/build/html-merge.png" alt="html-merge.png"/>

</figure>

<p>
The syntax highlighting libraries I looked at don't work on HTML out of the box. It's <em>possible</em> to merge two highlighters together but it can be tricky. Let's see what the merging looks like. To keep the examples concise, I'll use <kbd>&lt;b&gt;</kbd> for tags I added manually and <kbd>&lt;i&gt;</kbd> for tags the highlighter added:
</p>

<ol class="org-ol">
<li><p>
highlighting tags <code>&lt;i&gt;</code> enclosing the manual markup <code>&lt;b&gt;</code>:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-text">function </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">abc</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">_to_hex(p):
→
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">function</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">abc</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">_to_hex</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">(p):
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span>        <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>   <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>       <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
                   [--------]
                [----------------------]</span>
</pre>
</div></li>

<li><p>
manual markup <code>&lt;b&gt;</code> enclosing the highlighting tags <code>&lt;i&gt;</code>:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">hex.q + 1</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
→
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">hex.</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">q</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text"> + 1</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span>    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
       [------]
[---------------------]</span>
</pre>
</div></li>

<li><p>
manual markup and highlighting tags crossing:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-text">var y_</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">prev = 3</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
→
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">var</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">y_</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">prev</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text"> = 0</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span>   <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span>  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span>    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
           [--------------]
                [-----------------]</span>
</pre>
</div>

<p>
Oops, that's not right. Tags must be nested correctly. The <code>&lt;/i&gt;</code> can't be until after the <code>&lt;/b&gt;</code>. We have to split the <code>&lt;b&gt;</code> tag here:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">var</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">y_</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">prev</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text"> = 0</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span>   <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span>  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span>    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
                [---------]    [---------]
           [------------------]</span>
</pre>
</div>

<p>
Note that splitting it will break hover and some other interactions.
</p></li>
</ol>

<p>
On the A* page I wrote a highlighter that takes HTML input and produces HTML output. It works for the samples on the A* page, but doesn't work on code in general. It's simple but fragile.
</p>


<figure id="org46dd4e0">
<img src="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/build/html-to-html.png" alt="html-to-html.png"/>

</figure>

<figure>
  <img border="1" width="50%" src="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/screenshot-nonsyntax-highlighting.png" alt="(screenshot)"/>
  <figcaption>Highlighting non-interactive code</figcaption>
</figure>

<p>
The code samples on the A* page aren't interactive. Interactivity complicates things. Actions on the page can change the text of the code being displayed. The simplest thing is to highlight it again when the code changes:
</p>


<figure id="org2a1237d">
<img src="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/build/interactive-loop.png" alt="interactive-loop.png"/>

</figure>

<p>
In an interactive system, the DOM has more than just the HTML: it can have event handlers, focus status, hover state, text selection, pointer capture, and other things. All of that is lost when we generate HTML from scratch. This type of problem is partly solved with a Virtual DOM library (React/Vue/etc.) that attempts to reuse existing DOM nodes where possible.
</p>

<p>
I wasn't sure how best to solve this problem so I decided to study the modules of an existing syntax highlighting library:
</p>

<ol class="org-ol">
<li>Tokenize: find the ranges in the text that need to be highlighted.</li>
<li>Markup: surround those ranges with markup such as <kbd>&lt;span class=keyword&gt;</kbd>.</li>
<li>Style: use CSS to apply colors to <kbd>span.keyword</kbd>.</li>
</ol>

<p>
I wanted to replace step 2 of an existing library with the new <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a>, available in mid-2025. This is a non-destructive approach to highlighting, similar to how selecting text applies styling to the selected text without modifying the HTML. However, the Custom Highlight API also requires new CSS rules, so I have to replace step 3. And it turns out the only part I could reuse, step 1, is tiny (under 30 lines). So I decided to write everything myself:
</p>

<ol class="org-ol">
<li>Convert the DOM element to plain text using <kbd>el.textContent</kbd>. Don't make the same mistake I did of using <kbd>el.innerText</kbd>. That is <em>almost</em> right but has some differences. For example, <code>&lt;br&gt;</code> has text content <code>""</code> and inner text <code>"\n"</code>.  That leads to off-by-one errors in a later step.</li>
<li>Tokenize the plain text, the same way a syntax highlighter library would. I looked at libraries such as <a href="https://github.com/rse/tokenizr">rse/tokenizr</a> and <a href="https://github.com/no-context/moo">no-context/moo</a> but they are overkill for my needs, around 10✕ as much code as I ended up with.</li>
<li>Construct <a href="https://developer.mozilla.org/en-US/docs/Web/API/Range">Range</a> objects for each token. The tokenizer gives me an index into the plain text but I need to convert that into an index into the specific DOM node. I can walk the tree with <kbd>document.createTreeWalker()</kbd> until I find the correct node.</li>
<li>Create a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Highlight">Highlight</a> object to hold all the ranges for each token <em>type</em> (comment, keyword, etc.). Add the ranges to that highlight object.</li>
<li>Write CSS rules <kbd>::highlight(keyword)</kbd> for each token type. There are limitations on the styling. These rules can't cause a reflow, so I can change foreground/background but not bold or any other change in fonts.</li>
<li>Use <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MutationObserver</a> to watch for changes to the DOM element. When the text changes, create new Range objects to replace the existing ones. One limitation of this is that I'm watching the existing <code>&lt;pre&gt;</code> elements, not watching for new or removed <code>&lt;pre&gt;</code> elements. I made sure the page doesn't create any after initial load.</li>
</ol>

<p>
This worked really well! I can apply highlighting without messing up any of the interactivity.
</p>

<p>
On the Hexagons guide I decided to primarily use highlighting for non-syntax (variable names) rather than syntax (keywords, comments, etc.). The diagrams use three colors for the three axes of a hexagonal grid, and I use the same colors for the variables representing those axes:
</p>

<figure>
  <img width="75%" border="1" src="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/hex-with-highlighting.png" alt="(screenshot with highlighting)"/>
  <figcaption>Highlighting using custom highlight api</figcaption>
</figure>

<p>
The code highlighting on the <a href="https://www.redblobgames.com/grids/hexagons/">Hexagons guide</a> is now live, with 829 tokens highlighted. Try it out! If you want to see my code, it's <a href="https://www.redblobgames.com/grids/hexagons/code-highlighting.js">code-highlighting.js</a>.
</p>

<style>
  img[border] { border: 1px solid #000; box-shadow: 0 1px 5px 2px rgb(0 0 0 / 0.3); }
</style>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2026-04-16-highlighting-interactive-code-blocks/"/>
    <published>2026-04-16T15:32:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2026-03-25-optimizing-page-size</id>
    <title>Optimizing page size</title>
    <updated>2026-03-29T10:42:23.269145-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
When I started my web site, we didn't have static site generators or WordPress or GitHub Pages or much else. People wrote HTML1 by hand. Over the years, I've built my own setup to manage my site. My current setup involves writing XHTML that gets transformed into HTML using XSLT.
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2026-03-25-optimizing-page-size/_build_process.png" alt=""/>
  <figcaption> My build process, without 10 additional steps</figcaption>
</figure>


<p>
My XSLT template and CSS styling are <em>global</em>. They apply to the more than 30 years of pages I've written. That means whenever I change the XSLT or CSS, I need to make sure the change works for the entire site, over 800 articles. Until now I've been doing that manually by spot checking the popular articles. 
</p>

<p>
While working on my <a href="https://www.redblobgames.com/articles/sdf-fonts/">SDF font guide</a>, I noticed an issue with the white space. There were some spaces missing. It's easy to work around, so I did — I added <kbd>&amp;nbsp;</kbd> in a few places. This has been a problem for a while and I just work around it each time. After I finished the project, I decided to dig into the root cause.
</p>

<x:cut/>


<p>
<h3>Part 1: de-optimizing</h3>
</p>

<p>
For a long time I've been removing as many spaces as I can from the final HTML, to make the page smaller and faster to load. But it makes the pages hard to read and debug. When I write this:
</p>

<div class="org-src-container">
<pre class="src src-xml">  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">ol</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">Wilma</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span> <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">Fred</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">and          Dino</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">ol</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
my transform rules turn it into:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">ol</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">Wilma</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">i</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text"> </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">Fred</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">b</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">and Dino</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">li</span><span class="nxml-tag-delimiter">&gt;&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">ol</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
In this example it's ok to remove the spaces between <code>&lt;/li&gt;&lt;li&gt;</code> but it's <em>not</em> ok to remove the spaces between <code>&lt;/i&gt; &lt;b&gt;</code>. And it's often ok to collapse multiple spaces into one, but <em>not</em> inside <code>&lt;pre&gt;</code> or <code>&lt;script&gt;</code>. It's tricky. I looked through my XSLT and found that I had kept adding more rules over the years:
</p>

<ol class="org-ol">
<li>Remove a bunch of spaces.</li>
<li>Add a bunch of spaces to undo <em>some</em> of step 1.</li>
<li>Remove more spaces to undo <em>some</em> of step 2.</li>
<li>Add a bunch of spaces to undo <em>some</em> of step 1 and 3.</li>
<li>Remove more spaces to undo <em>some</em> of step 2 and 4.</li>
</ol>

<p>
What a mess! But each time I had added a rule, I hadn't been looking at the whole process. I was fixing one situation. If I did that again, I could add rule 6 to add more spaces to partially undo a previous step.
</p>

<p>
<em>Why am I doing this at all?</em>
</p>

<p>
When I started my web site in 1994, compression wasn't common, so removing 1000 spaces from the input would make the page load 3 seconds faster on a 2400 baud modem. The spaces mattered a lot to me. But in 2025? It's much less important, both because of compression and because internet speeds are much higher now.
</p>

<p>
These rules are tricky and I was still getting it wrong sometimes. I've <a href="https://www.redblobgames.com/blog/2025-04-22-de-optimizing-mapgen4/">written before about de-optimizing</a> to make something easier to work with. I decided to de-optimize here by removing these whitespace rules. Not only will make it easier for me to debug my pages, it also makes it easier for anyone using View Source to see how I wrote the page.
</p>

<p>
Since browsers collapse multiple spaces in HTML into one, I expected only minor <em>visual</em> differences. An extra space here or there is hard to spot by quickly skimming a page. White space changes are <em>subtle</em> and hard to catch with manual inspection. I decided the best approach would be to break up the change into small steps, and take screenshots before/after each step.
</p>

<p>
I'm not the only one who wants to test the effect of a CSS change across an entire site. I used Playwright, as recommended in <a href="https://marending.dev/notes/visual-testing/">Florian Marending's blog</a>. It can take screenshots using the three major rendering engines (Chrome's Blink, Firefox's Gecko, and Safari's WebKit). Instead of using their output format, I wrote my own, using <a href="https://github.com/mapbox/pixelmatch">pixelmatch</a> to create a diff image:
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="keyword">for</span> f<span class="keyword"> in</span> /tmp/yes/*firefox*.png
<span class="keyword">do</span> 
    node_modules/pixelmatch/bin/pixelmatch $<span class="variable-name">f</span> /tmp/no/$(basename $<span class="variable-name">f</span>) /tmp/diff-$(basename $<span class="variable-name">f</span>) 0.1 || <span class="sh-escaped-newline">\</span>
        <span class="builtin">echo</span> <span class="string">"&lt;div style='display:grid;grid-template-columns:repeat(3,1fr)'&gt;&lt;img src=diff-$(basename $f)&gt;&lt;img src=yes/$(basename $f)&gt;&lt;img src=no/$(basename $f)&gt;&lt;/div&gt;"</span> <span class="sh-escaped-newline">\</span>
             &gt;&gt;/tmp/diff.html
<span class="keyword">done</span>
</pre>
</div>

<p>
Usually a one space difference will cause the rest of the line to shift, showing up as a red bar. In this change both outputs could be considered correct:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2026-03-25-optimizing-page-size/whitespace-diff-pathfinding.png" alt=""/>
  <figcaption> Diff, before, after</figcaption>
</figure>


<p>
In this change the whitespace rule had mangled the output in a way that I hadn't noticed:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2026-03-25-optimizing-page-size/whitespace-diff-minor.png" alt=""/>
  <figcaption> Diff, before, after</figcaption>
</figure>


<p>
I found <strong>no examples</strong> where the whitespace rules made things better. I found plenty of examples where they made things slightly worse, and a few that made things much worse, like this:
</p>

<figure>
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:1em">
    <img src="https://www.redblobgames.com/blog/2026-03-25-optimizing-page-size/whitespace-diff-before.png" alt="With whitespace collapse"/>
    <img src="https://www.redblobgames.com/blog/2026-03-25-optimizing-page-size/whitespace-diff-after.png" alt="Without whitespace collapse"/>
  </div>
</figure>

<p>
After testing my site locally, I made the change and pushed to production.
</p>

<p>
<h3>Part 2: re-optimizing</h3>
</p>

<p>
I had mentioned the whitespace experiments on social media and got <a href="https://bsky.app/profile/kg.luminance.org/post/3mhn2atodrk2l">a suggestion to switch from Gzip to Brotli</a>. I knew Brotli existed but I hadn't looked into it. And wow, it is <em>great</em>! I ran <a href="https://tools.paulcalvano.com/compression-tester/">Paul Calvano's compression tester</a> to get a sense of how it would perform on my site. Here's the compression ratios for the hexagons guide:
</p>

<table class="standard">


<colgroup>
<col class="org-left"/>

<col class="org-right"/>

<col class="org-right"/>

<col class="org-right"/>

<col class="org-right"/>
</colgroup>
<thead>
<tr>
<th scope="col" class="text-left">page</th>
<th scope="col" class="text-right">gzip dynamic</th>
<th scope="col" class="text-right">brotli dynamic</th>
<th scope="col" class="text-right">brotli static</th>
<th scope="col" class="text-right">zstd static</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-left">theory page</td>
<td class="text-right">3.4</td>
<td class="text-right">4.5</td>
<td class="text-right">5.1</td>
<td class="text-right">4.7</td>
</tr>

<tr>
<td class="text-left">implementation page</td>
<td class="text-right">3.6</td>
<td class="text-right">4.4</td>
<td class="text-right">5.0</td>
<td class="text-right">4.6</td>
</tr>

<tr>
<td class="text-left">diagrams javascript</td>
<td class="text-right">3.8</td>
<td class="text-right">4.8</td>
<td class="text-right">5.3</td>
<td class="text-right">4.9</td>
</tr>

<tr>
<td class="text-left">vue.js minified</td>
<td class="text-right">2.4</td>
<td class="text-right">2.9</td>
<td class="text-right">3.1</td>
<td class="text-right">3.0</td>
</tr>
</tbody>
</table>

<p>
The dynamic version makes the web server compress each time the browser requests the page. It has to balance compression time and compression ratio. The static version compresses ahead of time, and can use a slower algorithm with a better compression ratio. I also looked at Zstd. Caddy supports Zstd dynamic and static, and Brotli static only. Nginx has third party modules for Zstd dynamic and static, and Brotli dynamic and static.
</p>

<p>
Switching from Gzip dynamic to Brotli dynamic is a far bigger improvement than the small amount I saved on whitespace. I may try switching to Brotli static at some point, but I need to restructure my build step a bit.
</p>

<p>
So I now have a smaller faster web site with better formatting, and also learned a tool that will help me test future global changes. It was a good week!
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2026-03-25-optimizing-page-size/"/>
    <published>2026-03-25T12:05:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2026-02-26-writing-a-guide-to-sdf-fonts</id>
    <title>Writing a guide to SDF fonts</title>
    <updated>2026-03-23T15:06:24.937671-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
(TL;DR: this is a blog post about the process of writing <a href="https://www.redblobgames.com/articles/sdf-fonts/">my guide to SDF fonts</a>.)
</p>

<p>
Back in 2024 I learned about SDF (signed distance field) rendering of fonts. I was trying to implement outlines and shadows in a single pass instead of drawing over the text multiple times in different styles. I intended to use these fonts for two different projects, a game and a map generator. I got things working but didn't fully understand why certain things worked or didn't work. I wrote some notes on my site about what I tried. In the end, I stopped working on both the game's fonts and the map generator, so I put all of this on hold.
</p>

<figure>
  <img src="https://www.redblobgames.com/articles/sdf-fonts/blog/sdf-aemrange-03-01.png" alt="Contour line visualization of distance fields"/>
</figure>

<p>
Fast forward to late 2025, and my incomplete notes sometimes show up on the first page of search results for <a href="https://www.google.com/search?q=sdf+fonts">"sdf fonts"</a>! Surely that isn't the best page on the topic. It would be better to point to library documentation or maybe one of the research papers about the topic. My page <em>isn't that good</em>.
</p>

<p>
Initially my thought was "search engines are in their decline" but then I decided "this is an opportunity". I decided to <strong>make a page worthy</strong> of being the top search result.
</p>

<x:cut/>


<p>
I first looked through everything I had written. 
I already had started an "overview" page but hadn't gotten very far on it. 
I also have <em>22 separate pages</em> that were "diary style", about what I did rather than what you should know. 
</p>

<p>
The overview page covered how to use various SDF font libraries (msdfgen, stb_truetype, tiny-sdf, etc.). I wrote code for multiple libraries, had sketched out diagrams for various concepts, and had screenshots of outputs from each of those libraries.
</p>

<figure>
  <img src="https://www.redblobgames.com/articles/sdf-fonts/blog/flow.png" alt="Sketch of distance fields"/>
  <figcaption>Sketched out concepts, <a href="https://excalidraw.com/">using Excalidraw</a></figcaption>
</figure>

<p>
At some point I realized the scope was too large. I had spent the most time with msdfgen and hadn't yet learned enough about the other libraries to write a proper guide. They all worked differently. I kept getting stuck. So <strong>I reduced the scope</strong>. In redesign 2 I decided to only use msdfgen, but show the various tradeoffs involved (atlas size, antialias width, shader derivatives, smoothing function).
</p>

<p>
I made several diagrams for concepts, such as:
</p>

<figure>
  <img style="width:30%" src="https://www.redblobgames.com/articles/sdf-fonts/blog/msdf-planebounds-1.png" alt="Diagram of glyph bounds"/>
  <img style="width:30%" src="https://www.redblobgames.com/articles/sdf-fonts/blog/msdf-layout-1.png" alt="Diagram of layout algorithm"/>
  <img style="width:30%" src="https://www.redblobgames.com/articles/sdf-fonts/blog/msdf-atlas-read-1.png" alt="Diagram of reading from font atlas"/>
  <figcaption>Diagrams from redesign 2, <a href="https://excalidraw.com/">using Excalidraw</a></figcaption>
</figure>

<p>
And I started running tests. I wanted to compare the effect of atlas size, so I made lots of screenshots and started looking closely. I wanted to come up with a way to recommend a specific size. I wanted to make recommendations for all the other parameters. I showed all the commands I ran.
</p>

<p>
At some point I realized I could run tests forever. And I had already done that last year, and wrote it up in blog posts (<a href="https://www.redblobgames.com/blog/2024-11-08-sdf-headless-tests/">one</a> and <a href="https://www.redblobgames.com/blog/2024-11-17-sdf-headless-tests/">two</a>). Doing it again here didn't seem especially valuable. So <strong>I pivoted</strong> to a "how to" page. In redesign 3 I decided to show the concepts, then a JavaScript implementation using CPU rendering, and then another implementation using GPU rendering. I made new versions of the diagrams:
</p>

<figure>
  <img style="width:30%" src="https://www.redblobgames.com/articles/sdf-fonts/blog/msdf-planebounds-2.png" alt="Cleaned up diagram of glyph bounds"/>
  <img style="width:30%" src="https://www.redblobgames.com/articles/sdf-fonts/blog/msdf-layout-2.png" alt="Cleaned up diagram of layout algorithm"/>
  <img style="width:30%" src="https://www.redblobgames.com/articles/sdf-fonts/blog/msdf-atlas-read-2.png" alt="Cleaned up diagram of reading from font atlas"/>
  <figcaption>Diagrams from redesign 3, hand-written SVG</figcaption>
</figure>

<p>
I was making progress on that page but it didn't <em>feel</em> like a Red Blob Games page. 
The page started out with tons of shell commands, and then showed lots of code. 
It felt like a page that only I would find useful. 
So <strong>I started over</strong> and designed a "concepts" page. 
In redesign 4 I focused on what effects I wanted, how SDF works, and how to use it to create those effects. 
I again reduced the scope by removing the implementation details. 
What I had already written, I moved to a separate (unpolished) page. 
And I never wrote a standalone downloadable project like I originally wanted.
</p>

<p>
Sometimes it takes a long time before I figure out what I actually want to write, and then everything falls into place:
</p>

<figure>
  <img src="https://www.redblobgames.com/articles/sdf-fonts/blog/project-timeline.png" alt="Chart of how active I was each day"/>
  <figcaption>Work over the past year, using <a href="https://cal-heatmap.com/">Cal-heatmap</a></figcaption>
</figure>

<p>
<strong>I'm finally happy with the page.</strong> <a href="https://www.redblobgames.com/articles/sdf-fonts/">Take a look!</a>  I hope search engines point to it eventually.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2026-02-26-writing-a-guide-to-sdf-fonts/"/>
    <published>2026-02-26T13:36:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2026-02-12-urls-with-trailing-punctuation</id>
    <title>URLs with trailing punctuation</title>
    <updated>2026-03-23T15:06:24.935200-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
When a url is posted on a forum/chat, it's often automatically made linkable. But sometimes the regexp that grabs the url also grabs a trailing punctuation mark.  For example, if the text is "<kbd>visit red blob (https://www.redblobgames.com) and scroll to the bottom</kbd>" and regexp picks up the trailing parenthesis, it will make "<code>https://www.redblobgames.com)</code>" clickable. When you click on this, it will request <code>GET /)</code> which will be a 404 error.
</p>

<p>
I don't get a lot of these in my server error logs but it's easy to do something about it.
</p>

<x:cut/>


<p>
I use both nginx and Caddy for my web sites.
</p>

<p>
In nginx I can put a permanent rewrite inside the server block:
</p>

<div class="org-src-container">
<pre class="src src-nginx"><span class="keyword">server</span> {
    …
    <span class="keyword">rewrite</span> ^(.*)[\;:,.)]$ <span class="constant">$1</span> <span class="function-name">permanent</span>;
}
</pre>
</div>

<p>
In Caddy the syntax is different:
</p>

<div class="org-src-container">
<pre class="src src-caddyfile"><span class="caddyfile-label">https://www.redblobgames.com</span> {
    <span class="caddyfile-directive">…</span>
    <span class="caddyfile-directive">@trailing_punctuation</span> path_regexp url ^(.*?)[;:.,\)]$
    <span class="caddyfile-directive">redir</span> @trailing_punctuation <span class="caddyfile-variable">{re.url.1}</span> 301
}
</pre>
</div>

<p>
Now it will 301-redirect "<code>https://www.redblobgames/)</code>" to "<code>https://www.redblobgames/</code>".
</p>

<p>
For both of these web servers, I never remember what characters need to be escaped, and I kind of wish I could use existing programming language syntax with delimiters like <kbd>/^(.*)[;:,.)]$/</kbd> or <kbd>r'^(.*)[;:,.)]$'</kbd>.
</p>

<style>
  .src .caddyfile-directive { color: #268bd2; font-weight: bold; }
  .src .constant, .src .caddyfile-variable { color: hsl(120 50% 30%); }
</style>
</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2026-02-12-urls-with-trailing-punctuation/"/>
    <published>2026-02-12T11:04:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-12-29-what-i-did-in-2025</id>
    <title>What I did in 2025</title>
    <updated>2026-03-23T15:06:24.933028-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
It's time for my annual self review. <a href="https://www.redblobgames.com/blog/2024-12-25-what-i-did-in-2024/">In last year's review</a> I said I wanted to spend time learning things for myself, and less time writing for other people.  The main themes for 2025 were:
</p>

<ul class="org-ul">
<li>playing with text (very broadly)</li>
<li>playing with maps (specifically mapgen4)</li>
<li>improving my web site</li>
</ul>

<p>
Outside of Red Blob Games work, I had two goals: improve my health and improve my quality of life. So how did the year go? I think it went well.
</p>

<p>
Since I have forgotten everything I did, I went through <a href="https://redblobgames.notion.site/f8bc2f44fba94607afa9c06711d23245?v=0766432cb1534ce582ce35b33cbbef7e">my Notion page</a> and my git logs:
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="keyword">for</span> git<span class="keyword"> in</span> $(find . -name .git)
<span class="keyword">do</span> 
    <span class="variable-name">dir</span>=$(dirname <span class="string">"$git"</span>)
    <span class="builtin">cd</span> <span class="string">"$dir"</span>
    <span class="builtin">echo</span> ___ <span class="string">"$dir"</span>
    git --no-pager log --since=2025-01-01 --pretty=format:<span class="string">"%as %s%d%n"</span>
    <span class="builtin">cd</span> - &gt;/dev/null
<span class="keyword">done</span>
</pre>
</div>

<x:cut/>


<p>
<h3>Interpreters/Compilers</h3>
</p>

<p>
The main theme for this year was <em>text</em>. I wanted to be very broad in applying this theme: interpreters/compilers, procedural generation, rendering, language models, text editors, search engines, and anything else related to text. Last year I bought and started reading munificent's excellent book <a href="https://craftinginterpreters.com/"><em>Crafting Interpreters</em></a>, but it wasn't until this year that I spent a lot of time on it. It creates a new language, Lox, and then teaches you how to write an interpreter and compiler for it.
</p>

<ul class="org-ul">
<li>I implemented the first part, an interpreter. The code samples in the book are in Java, but I used Python instead. When I use the same language as the book/tutorial, I am more likely to copy code without fully understanding it. By using a different language, it forces me to understand the code so that I can reimplement it.</li>
<li>I read but didn't implement the second part, a compiler. I instead used what I learned with Web Assembly (wasm). I hand-compiled some simple code from Lox to wasm, but didn't tackle the harder topics (closures and function calls). I also compiled some Javascript to wasm, and tried AssemblyScript. I used tools that compile wasm to C and wasm to x86 to understand better what the opcodes do. <a href="https://www.redblobgames.com/x/2512-learning-wasm/">I wrote partial notes for myself</a>.</li>
<li>I've made LL(1) and LALR parsers before, and also an LL(1) parser generator which is now <a href="https://packages.debian.org/sid/yapps2">a Debian/Ubuntu package</a>. I've long been interested in <a href="https://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/">Pratt Parsers</a>, and <em>Crafting Interpreters</em> finally got me to try it. Pratt Parsers turn out to be really cool for the "expression" part of a grammar. Also see <a href="https://www.oilshell.org/blog/2016/11/02.html">Oil Shell's links to Pratt Parsing tutorials</a> and <a href="https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html">matklad's blog post</a>. After implementing the lexer and parser, I <a href="https://www.redblobgames.com/x/2512-learning-wasm/#programatically-generating-wasm">compiled to wasm</a>, loaded the wasm module at run time, and ran the code.</li>
</ul>

<p>
I also read lots more about interpreters and compilers. I made notes <a href="https://pinboard.in/u:amitp/t:compilers">and bookmarks</a>. It's an endlessly fascinating area to study.
</p>

<p>
<h3>Text</h3>
</p>

<p>
This year's theme was <em>text</em>, and although intepreters/compilers were a large part of that, I also did other text-related projects.
</p>

<ul class="org-ul">
<li>I had explored Signed Distance Field (SDF) font rendering in 2024 but this year I wanted to <a href="https://www.redblobgames.com/articles/sdf-fonts/">write a tutorial</a> with a complete working example. Part of the motivation for this is that my blog posts are already showing up in search engines, and those blog posts are "what I tried" and not "what I recommend". Knowing that my pages are <a href="https://www.google.com/search?q=sdf+fonts">showing up in the Google search results</a> makes me a bit embarassed by the page it's returning, and I want to make a better one. I had hoped to finish this in 2025 but it's still incomplete.</li>
<li>I wrote a very simple search engine for my site, and wrote about it: <a href="https://www.redblobgames.com/blog/2025-08-29-lets-write-a-search-engine-1/">Part 1</a> and <a href="https://www.redblobgames.com/blog/2025-08-30-lets-write-a-search-engine-2/">Part 2</a>. <a href="https://www.redblobgames.com/search/"><strong>You can try it here</strong></a>.</li>
<li>It seems like everyone's talking about "AI", especially Large Language Models (LLMs). Some people have very strong positive or negative feelings about them. I've had fairly negative feelings about them for the past ten years and am exhausted. As part of the <em>text</em> theme for this year, I decided I'd look into them a bit more. 
<ul class="org-ul">
<li>I wrote a blog post about <a href="https://amitp.blogspot.com/2025/10/llms-screenwriters-vs-characters.html">creative things we might be able to do with them</a> if we could disassemble the chat interface into ingredients and then reassemble the ingredients into new uses.</li>
<li>I wrote a blog post about <a href="https://www.redblobgames.com/blog/2025-09-22-harnessing-chatgpt-hallucinations/">the wrong information ChatGPT writes about Red Blob Games</a>, and how I might be able to harness that.</li>
<li>I tried using them occasionally for "side quests" like <a href="https://www.redblobgames.com/x/2548-business-cards/">cropping images for my business cards</a>, coming up with example code I can study, or reformatting/rewriting my code in a different style, but I just haven't put enough effort to learn how to use them effectively for anything other than simple things.</li>
</ul></li>
<li>I made a text editor tool that lets me quickly tweak the SVG of a diagram. <a href="https://www.redblobgames.com/x/2550-scrubbable-codemirror/">I used CodeMirror with a plugin</a> and "attached" it to SVG diagrams on my page. I can then press Alt while dragging the mouse to tweak a number, and see the results. Once I have something I like, I can paste the updated SVG back into the source page. The goal here was that I want to preserve the formatting and comments from the SVG I had hand written, and only make minimal edits with the interactive tool.</li>
<li>I started watching for <em>and fixing</em> my text editing annoyances while using Emacs. As a side effect, I've been also watching for and fixing annoyances with Linux, Mac, shell, and other tools I use regularly. My computing environment is getting better!</li>
</ul>

<p>
I wanted to work on procedural name generation for maps, which would've connected my text projects to my map projects, but didn't get around to it. That's ok. I always have more ideas than time.
</p>

<p>
<h3>Maps</h3>
</p>

<p>
Maps are such a rabbit hole for me that I tell myself not to work on map projects unless someone other than me wants me to work on maps. It turns out this year I had two companies approach me about map work, so I gave myself permission to work on my other map projects too.
</p>

<ul class="org-ul">
<li>I had originally written <a href="https://github.com/redblobgames/mapgen2">mapgen2</a> to have the <em>generation</em> algorithms only, and I had the UI "demo" as a separate repository that I hadn't shared. The idea was that you could use mapgen2 to generate a map and then attach it to your own game, which already has its own UI and rendering. But this year I decided to merge the two. It's easier to start with a complete repository <em>that works</em> and remove some files you don't need than to start with an incomplete repository and try to figure out how to make it work.</li>
<li>I <a href="https://www.redblobgames.com/blog/2025-09-29-mapgen4-renderer/">rewrote mapgen4's renderer</a> from using <a href="https://github.com/regl-project/regl">regl.js</a> to using WebGL directly. Regl is nice but there were WebGL 2 features I wanted to use. WebGL 2 has been <a href="https://caniuse.com/?search=webgl2">widely available since 2022</a>, and there are no plans to make Regl support it.</li>
<li>I <a href="https://www.redblobgames.com/blog/2025-09-30-mapgen4-river-shader/">reimplemented mapgen4's river renderer</a>. I had been drawing river segments on the CPU, putting those segments into a texture, and then using that render on the GPU. The new renderer is a shader that runs on the GPU.</li>
<li>Since I had just gone through the mapgen4 rendering code, I had lots of ideas for improvements, and <a href="https://www.redblobgames.com/blog/2025-10-13-mapgen4-webgl2/">I implemented many of them</a>, and fixed some bugs along the way.</li>
<li>I started writing <a href="https://www.redblobgames.com/maps/rivers/">a guide to procedural river generation</a>. Some of my tutorials on terrain generation don't include the rivers because rivers are tricky. I know how to make them work in mapgen2 and mapgen4 but I <em>don't</em> know how to generate them in general. But I am only halfway through this.</li>
<li>I updated <a href="https://www.redblobgames.com/x/2022-voronoi-maps-tutorial/">my voronoi map tutorial</a> to include biomes and rudimentary rivers.</li>
<li>I <a href="https://www.redblobgames.com/blog/2025-04-22-de-optimizing-mapgen4/">de-optimized parts of Mapgen4 to make it easier to experiment with</a>.</li>
<li>I <a href="https://www.redblobgames.com/x/2503-mapgen2-towns/">modified mapgen2's renderer to work with mapgen4</a>.</li>
<li>I experimented with <a href="https://www.redblobgames.com/x/2503-mapgen2-towns/">towns</a>, roads, and <a href="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/">trade routes</a>.</li>
<li>I <a href="https://www.redblobgames.com/x/2501-mapgen4-noise-explorer/">played with noise functions</a>.</li>
<li>I tried <a href="https://www.redblobgames.com/x/2542-mapgen4-painting/">increasing the paintability of mapgen4</a>, allowing more elevations as well as a choice between plateaus and mountain peaks.</li>
<li>Realm of the Mad God used my map generator for their 13 maps, but didn't keep track of the seeds. I have long been curious if I could find the seeds from the map. I <a href="https://www.redblobgames.com/blog/2025-11-07-rotmg-seeds/">searched map seeds to match existing maps</a> and found half of them.</li>
<li>I <a href="https://www.redblobgames.com/x/2547-mountain-icon/">tried a stylized hill/mountain image</a>.</li>
<li>I made a <a href="https://www.redblobgames.com/x/2543-mapgen1-hexagons/">biome painter</a> on top of mapgen1.</li>
<li>I added more to <a href="https://www.redblobgames.com/x/1830-jittered-grid/">my jittered grid page</a> but I think it's time to move it onto its own URL outside of <kbd>/x/</kbd>. I usually use <kbd>/x/</kbd> for short-lived projects, and this page is slowly turning into a reference page.</li>
</ul>

<p>
It was good to play with mapgen4 again, and I started getting ideas of what might go into a mapgen5.
</p>

<p>
<h3>Site Management</h3>
</p>

<p>
This is the second year that I'm using my own home grown blogging software. I like it a lot better than what I was using before. But I didn't blog as much this year as last year. Part of that was that I was spending a lot of time on my own projects (particularly learning interpreters/compilers) but also because I spent some time working with Jetbolt Games on <a href="https://gasgame.net/">Galactic Assault Squad</a> and another game studio on an unannounced project.
</p>

<p>
For the blog but also the rest of the site I've been wanting to <a href="https://redblobgames.notion.site/Add-tags-to-my-pages-b5670bbdd2b946728cc7495ac63465d3?pvs=74">add tags like "#hexagons"</a> along with browsing by topic. I have hundreds of pages so it's going to take a while. I've made progress on this but want to label more of my site before I add browsing by topic.
</p>

<p>
My site is older than most any site out there, and most sites don't last that long. I have around 5000 links from my site to others. A lot of the links I have on my site have broken over the years. I made some good progress on fixing broken links but there's still more to do. It's tricky because some links report as good (status 200) but are actually broken in the sense that the content I linked to is gone, replaced by some generic marketing or spam. And some links report as bad (404) but are actually good, with the server reporting 404 to my crawler but 200 to a regular browser. So there's a lot of manual checking involved.
</p>

<p>
I continue to update my existing pages. For example this year I improved the <a href="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/">conversions</a> and <a href="https://www.redblobgames.com/blog/2025-03-12-hexagon-spiral-coordinates/">spiral coordinates</a> sections of the Hexagons Guide. I've also updated my pages using Flash applets <a href="https://www.redblobgames.com/blog/2025-02-02-thoughts-on-flash/">to use Ruffle</a>, and it's been great to make those experiments available again. 
</p>

<p>
I significantly sped up my web site build. It had been fast initially but as I've added more and more to the site (now 26,079 individual files) it's gotten slower to process. I've been rewriting parts of it from using Bash to Python. If I need further optimizations I may use Rust or C++ in places. I've also switched my Python projects to use <a href="https://docs.astral.sh/uv/">uv</a>, which I'm enjoying very much.
</p>

<p>
I simplified my web site build by <a href="https://www.redblobgames.com/blog/2025-12-27-goodbye-sass/">removing the Sass compiler</a>. CSS has added the features I had been using from Sass. The build step is now faster, and the pages are slightly smaller.
</p>

<p>
<h3>Next year</h3>
</p>

<p>
I've been thinking about <a href="https://bsky.app/profile/redblobgames.com/post/3lzfeh6qtis2e">how to choose projects</a>, and I ask myself:
</p>

<ol class="org-ol">
<li>Is it that <em>I want it done</em>?</li>
<li>Is it that <em>I want to be doing it?</em></li>
<li>Is it that <em>I want to have done it?</em></li>
</ol>

<p>
There are often times I want to have done something, but when I sit down to do it, I don't actually want to do it. That's ok. And sometimes the problem is that I don't know <em>how</em> to do it. That's ok too. But I need to get better at understanding what's stopping me, and not beating myself up about it. Here are some projects ideas for this year:
</p>

<ul class="org-ul">
<li>A specialized text transformation language, using what I learned about interpreters/compilers this year. I'd like to design something specific to make example code for my Hexagon guide. I currently use Haxe macros to generate hexagon code in C++, Python, C#, Haxe, Java, JavaScript, TypeScript, Lua, and Rust.</li>
<li>My guide to SDF font rendering.</li>
<li>My guide to procedural river generation.</li>
<li>Version 2 of my A* page. I have <a href="https://redblobgames.notion.site/Pathfinding-Introduction-to-A-v2-e0410e0df1e1429fac3b773137e91aae?pvs=74">some ideas</a> of how to improve this page. Last time I worked on it, I focusd on the diagrams, but the next round should be focusing on better explanations.</li>
<li>Attempt 3 of explaining coordinate transforms. I've tried this twice before but didn't like the results. I have some ideas of how to approach it differently this time.</li>
<li>Using SQLite as an ECS (<a href="https://en.wikipedia.org/wiki/Entity_component_system">Entity Component System</a>). I've long thought of ECS as a rediscovery of the principles of relational databases. It might be interesting to <em>use</em> a relational database directly instead of an ECS as an intermediary.</li>
<li>A "first pass" at all the topics ChatGPT falsely thinks I have written. I could spend my time complaining about its hallucinations, or I could instead consider it a list of ideas.</li>
<li>A procedural name generator. I want to try something more interesting than Markov chains, and have lots of ideas.</li>
</ul>

<p>
Whichever ones I end up picking, I think they will be a lot of fun. Happy 2026 everyone!
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-12-29-what-i-did-in-2025/"/>
    <published>2025-12-29T10:02:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-12-27-goodbye-sass</id>
    <title>Goodbye Sass</title>
    <updated>2026-03-23T15:06:24.930592-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
My “site” is spread across five different servers. I use slightly different CSS on each one. Since 2013 I’ve been using Sass (SCSS) to manage the variants, using these features:
</p>

<ol class="org-ol">
<li>Variables → available in CSS since 2020</li>
<li>Calculations → available in CSS since 2015</li>
<li>Nesting → available in CSS since late 2023</li>
</ol>

<p>
Even though I want to use new browser features right away, I usually wait a year or two for them to stabilize and get into extended support releases.
</p>

<x:cut/>


<p>
A few years ago I had switched variables and calculations from Sass to CSS, but I still needed Sass for nesting. Since CSS nesting has now been supported for two years, I decided it should be safe for me to use it. And that means I no longer need Sass.
</p>

<p>
I divided the CSS into eight files that I can simply concatenate together (using <code>cat</code>), then <a href="https://esbuild.github.io">esbuild</a> to minify the result:
</p>

<div class="org-src-container">
<pre class="src src-sh">cat input1.css input2.css … <span class="sh-escaped-newline">\</span>
  | esbuild --loader=css --minify --bundle --external:* <span class="sh-escaped-newline">\</span>
            --outfile=output1.css
</pre>
</div>

<p>
Since the web server uses <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding">gzip content encoding</a>, does it really matter if I minify? Yes, it does, and the help from minify seems to be independent of the help from gzip:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-12-27-goodbye-sass/gzip-minify-comparison.png" alt=""/>
  <figcaption> Size reduction file when using gzip or minify</figcaption>
</figure>


<p>
The main goal of this change was to reduce dependencies. But it looks like I still have a dependency on <code>esbuild</code> instead of on <code>sassc</code>. How is that better?
</p>

<p>
I have been maintaining my site for over 30 years. It’s older than Wikipedia or Google. Most software doesn’t last that long, and I don’t expect <code>sassc</code> <em>or</em> <code>esbuild</code> to last as long as my web site. When I'm choosing tools I ask myself "what happens when this software disappears?"
</p>

<p>
There’s a difference between the <code>sassc</code> risk and the <code>esbuild</code> risk. I’m using <code>sassc</code> to translate Sass to CSS. If it disappears (<a href="https://github.com/sass/sassc">it's already deprecated</a>), I have to manually translate it, or find an alternative. I can't update the site without that software. I’m using <code>esbuild</code> only to minify. If it disappears, I lose an optimization but I don't have to do more work. I can continue updating the site without it. So in terms of dependencies I'm happier with <code>esbuild</code> than <code>sassc</code>.
</p>

<p>
I generally want to prioritize changes that make the site better for readers. This change does not help in the short term. But years from now, when I want to fix a typo or add information to the site, it will be easier for me to update if I have fewer dependencies to update. Everything is about tradeoffs. <em>Not</em> using a useful tool can make things harder, but using a useful tool can make things harder in a different way.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-12-27-goodbye-sass/"/>
    <published>2025-12-27T10:02:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-11-07-rotmg-seeds</id>
    <title>RotMG map seeds</title>
    <updated>2026-03-23T15:06:24.928147-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
I had just wrapped up a project and wanted to do something small/fun before I started the next one. The Flash runtime <a href="https://ruffle.rs/">is being reimplemented in Rust</a>, and I've enjoyed <a href="http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/demo.html">re-enabling the Flash on my site</a>. That had me thinking about my Flash projects again. I decided to try to revisit an unsolved mystery from one of my Flash projects.
</p>

<p>
Back in 2010, I wrote an article about <a href="http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/">procedurally generating wilderness maps using Voronoi polygons</a>, and I made a demo in Flash. The authors of <a href="https://en.wikipedia.org/wiki/Realm_of_the_Mad_God">Realm of the Mad God</a> used it to generate <a href="https://www.realmeye.com/wiki/maps-of-the-realm">thirteen maps</a> that shipped with the game. The authors don't remember the map seeds they put into the map generator. These maps were used from 2010 to 2024.
</p>

<p>
I've been curious what the map seeds were.
</p>

<p>
I told myself at the time that it would be a pain to recover the seeds. There are 2<sup>32</sup> = 4 billion map seeds. It took 5 seconds to generate each map. That'd take 20 billion seconds, or around 630 years, to generate all the maps, each with 2<sup>22</sup> = 4 million tiles.
</p>

<p>
But with some optimizations and shortcuts … <strong>I found a match!</strong>
</p>

<figure>
  <img style="width:45%" src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world5.png" alt="World 5 from RotMG"/>
   
  <img style="width:45%" src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-30997-1.png" alt="Mapgen2 seed 30997-1"/>
  <figcaption>World 5 from RotMG compared to Mapgen2 seed 30997-1</figcaption>
</figure>

<x:cut/>


<p>
After some more work, I found more matches:
</p>

<table id="table-of-results" class="standard">


<colgroup>
<col class="org-right"/>

<col class="org-left"/>

<col class="org-left"/>

<col class="org-left"/>
</colgroup>
<thead>
<tr>
<th scope="col" class="text-right">world</th>
<th scope="col" class="text-left">rotmg from <a href="https://www.realmeye.com/wiki/maps-of-the-realm">realmeye</a></th>
<th scope="col" class="text-left">seed</th>
<th scope="col" class="text-left">mapgen2 from <a href="http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/demo.html">my page</a></th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-right">1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world1.png" alt="world1.png"/></td>
<td class="text-left"> </td>
<td class="text-left"> </td>
</tr>

<tr>
<td class="text-right">2</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world2.png" alt="world2.png"/></td>
<td class="text-left">perlin 20927-1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-20927-1.png" alt="mapgen2-20927-1.png"/></td>
</tr>

<tr>
<td class="text-right">3</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world3.png" alt="world3.png"/></td>
<td class="text-left">perlin 84542-1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-84542-1.png" alt="mapgen2-84542-1.png"/></td>
</tr>

<tr>
<td class="text-right">4</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world4.png" alt="world4.png"/></td>
<td class="text-left"> </td>
<td class="text-left"> </td>
</tr>

<tr>
<td class="text-right">5</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world5.png" alt="world5.png"/></td>
<td class="text-left">perlin 30997-1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-30997-1.png" alt="mapgen2-30997-1.png"/></td>
</tr>

<tr>
<td class="text-right">6</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world6.png" alt="world6.png"/></td>
<td class="text-left"> </td>
<td class="text-left"> </td>
</tr>

<tr>
<td class="text-right">7</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world7.png" alt="world7.png"/></td>
<td class="text-left">perlin 65166-1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-65166-1.png" alt="mapgen2-65166-1.png"/></td>
</tr>

<tr>
<td class="text-right">8</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world8.png" alt="world8.png"/></td>
<td class="text-left">perlin  7785-1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-7785-1.png" alt="mapgen2-7785-1.png"/></td>
</tr>

<tr>
<td class="text-right">9</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world9.png" alt="world9.png"/></td>
<td class="text-left">perlin 43671-1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-43671-1.png" alt="mapgen2-43671-1.png"/></td>
</tr>

<tr>
<td class="text-right">10</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world10.png" alt="world10.png"/></td>
<td class="text-left"> </td>
<td class="text-left"> </td>
</tr>

<tr>
<td class="text-right">11</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world11.png" alt="world11.png"/></td>
<td class="text-left">perlin 59103-1</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-59103-1.png" alt="mapgen2-59103-1.png"/></td>
</tr>

<tr>
<td class="text-right">12</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world12.png" alt="world12.png"/></td>
<td class="text-left">blob, many</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-blob-11737-9.png" alt="mapgen2-blob-11737-9.png"/></td>
</tr>

<tr>
<td class="text-right">13</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/1745-rotmg-maps-webgl/images/world13.png" alt="world13.png"/></td>
<td class="text-left">radial, many</td>
<td class="text-left"><img src="https://www.redblobgames.com/x/2544-rotmg-map-seeds/blog/mapgen2-radial-23730-1.png" alt="mapgen2-radial-23730-1.png"/></td>
</tr>
</tbody>
</table>

<p>
They don't match exactly, but they are close. <a href="https://www.redblobgames.com/x/2544-rotmg-map-seeds/">I wrote up the process and results</a>. I had allocated 4 hours for this project but ended up spending 9.
</p>

<style>
  #table-of-results {
    td { vertical-align: middle; }
    img { max-width: 20em; }
  }
</style>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-11-07-rotmg-seeds/"/>
    <published>2025-11-07T08:51:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-10-13-mapgen4-webgl2</id>
    <title>Mapgen4's use of WebGL2</title>
    <updated>2026-03-23T15:06:24.925830-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
I try to avoid big software rewrites. But sometimes the rewrites are just an excuse to re-familiarize myself with the code. I rationalized  
<a href="https://www.redblobgames.com/blog/2025-09-29-mapgen4-renderer/">rewriting Mapgen4's renderer</a> by saying I wanted to use WebGL2. And I did use WebGL2, but the improvements turned out to be minor:
</p>

<ol class="org-ol">
<li>Vertex Array Objects to simplify and speed up code — but <a href="https://developer.mozilla.org/en-US/docs/Web/API/OES_vertex_array_object">OES_vertex_array_object</a> makes this available in WebGL1 (<a href="https://web3dsurvey.com/webgl/extensions/OES_vertex_array_object">good support on all platforms</a>)</li>
<li><code>gl_VertexID</code> to simplify and speed up code — only available in WebGL2</li>
<li><code>R16F</code> texture format for elevation and depth — needing <a href="https://developer.mozilla.org/en-US/docs/Web/API/EXT_color_buffer_half_float">EXT_color_buffer_half_float</a> in WebGL1 (<a href="https://web3dsurvey.com/webgl/extensions/EXT_color_buffer_half_float">poor support on Android</a>) or <a href="https://developer.mozilla.org/en-US/docs/Web/API/EXT_color_buffer_float">EXT_color_buffer_float</a> in WebGL2 (<a href="https://web3dsurvey.com/webgl2/extensions/EXT_color_buffer_float">good support on all platforms</a>)</li>
<li>Linear filtering of elevation and depth — needing the <code>R16F</code> format first</li>
<li><code>SRGB8</code> texture format for linear rgb color handling — supported in WebGL2, but could be emulated in WebGL1</li>
</ol>

<p>
I did <em>not</em> need to switch to WebGL2 for features, but implementing those features would make the code more complex and error prone. I decided to switch to WebGL2 so that I could simplify my code. 
</p>

<p>
The Vertex Array Objects and <code>gl_VertexID</code> didn't change the rendering output, but the texture format did, <em>both good and bad</em>, and I wanted to share some screenshots.
</p>

<x:cut/>


<p>
Let's start with a screenshot comparison I showed last time:
</p>

<figure>
  <img style="width: 45%; padding-right: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-texture-nearest.png"/>
  <img style="width: 45%; padding-left: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-texture-linear.png"/>
  <figcaption>
    <code>GL_NEAREST</code> vs <code>GL_LINEAR</code> filtering
  </figcaption>
</figure>

<p>
The differences are slight — there are blue blotches on the ground when I use <code>GL_LINEAR</code> filtering. But why?
</p>

<p>
Elevation is slightly too big for an 8 bit color channel, so I encode the elevation in the red and green channels (8 bits each),
[<code style="background:hsl(0 50% 95%)">fract(elevation)</code>, <code style="background:hsl(120 40% 90%)">floor(elevation)/256.0</code>]. 
For example an elevation of 32.5 would be encoded as [<code style="background:hsl(0 50% 95%)">32.5%1.0</code>, <code style="background:hsl(120 40% 90%)">32.0/256.0</code>], or [<code style="background:hsl(0 50% 95%)">0.5</code>, <code style="background:hsl(120 40% 90%)">0.125</code>]. In the fragment shader I decode it with with <code style="background:hsl(120 40% 90%)">green*256.0</code> + <code style="background:hsl(0 50% 95%)">red</code>, in this case <code style="background:hsl(120 40% 90%)">0.125*256.0</code> + <code style="background:hsl(0 50% 95%)">0.5</code> = 32.5.
</p>

<p>
<code>GL_LINEAR</code> texture filtering will blend each channel independently,
<em>then</em> combine the channels into one value:
</p>

<p>
(<code style="background:hsl(120 40% 90%)">a.hi</code> + <code style="background:hsl(120 40% 90%)">b.hi</code>) / 2.0 + ((<code style="background:hsl(0 50% 95%)">a.lo</code> + <code style="background:hsl(0 50% 95%)">b.lo</code>) / 2.0) / 256.0
</p>

<p>
But that produces a different value than correctly combining the channels first and then blending:
</p>

<p>
((<code style="background:hsl(120 40% 90%)">a.hi</code> + <code style="background:hsl(0 50% 95%)">a.lo</code> / 256.0) + (<code style="background:hsl(120 40% 90%)">b.hi</code> + <code style="background:hsl(0 50% 95%)">b.lo</code> / 256.0)) / 2.0
</p>

<p>
Near sea level, some of the below-sea-level elevation must be getting blended incorrectly into the above-sea-level ground. So I can't use <code>GL_LINEAR</code> filtering with this encoding.
</p>

<p>
If I could store a single 16-bit value instead of two 8-bit values, <code>GL_LINEAR</code> would work correctly. And I wouldn't have to split the elevation into two channels, and I wouldn't have to combine them together again. It'd be simpler code <em>and</em> it would look better.
</p>

<p>
Or so I thought. Here's the comparison:
</p>

<figure>
  <img style="width: 45%; padding-right: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed195-rgba8-nearest.png"/>
  <img style="width: 45%; padding-left: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed195-r16f-nearest.png"/>
  <figcaption>
    <code>RGBA8</code> vs <code>R16F</code> texture, with <code>GL_NEAREST</code> filtering
  </figcaption>
</figure>

<p>
It looks the same, right? Well, no, it's slightly different, especially in the mountains:
</p>

<figure>
  <img style="width: 45%; padding-right: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed195-rgba8-nearest-zoomed.png"/>
  <img style="width: 45%; padding-left: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed195-r16f-nearest-zoomed.png"/>
  <figcaption>
    <code>RGBA8</code> vs <code>R16F</code> texture, zoomed in on mountains
  </figcaption>
</figure>

<p>
The <code>R16F</code> format makes the mountains smoother! It also makes the valleys smoother but that difference is harder to see.
</p>

<p>
The outputs <em>should</em> have been the same. Why aren't they?
</p>

<p>
It turns out mapgen4 had a bug in it. I didn't actually encode the elevation the way I described above. I encoded with <kbd>vec4(fract(256.0*e), e, 0, 1)</kbd> and decoded with <kbd>dot(color.xy, vec2(1.0/256.0, 1.0))</kbd>. That doesn't preserve the value. I should have used <kbd>vec4(fract(256.0*e), floor(256.0*e)/256.0, 0, 1)</kbd>.  I used <a href="https://observablehq.com/d/db9bb925879d1152">ObservableHQ Plot</a> to plot the difference in these two calculations. The black dots are the incorrect encoding, and we can see that half of them are a little bit off:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/bug-two-channel-encoding.png" alt=""/>
  <figcaption> Correct vs incorrect encoding</figcaption>
</figure>


<p>
This bug adds a "texture" to the sides of mountains. <strong>I like the old look better</strong>. But don't want to keep the bug. I would prefer adding a texture <em>intentionally</em>.
</p>

<p>
After I got <code>R16F</code> working, I decided to compare <code>GL_NEAREST</code> to <code>GL_LINEAR</code>. I couldn't use <code>GL_LINEAR</code> because it interpolated each channel separately, but I can use it now that I have only one channel:
</p>

<figure>
  <img style="width: 45%; padding-right: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed187-r16f-nearest.png"/>
  <img style="width: 45%; padding-left: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed187-r16f-linear.png"/>
  <figcaption>
    <code>GL_NEAREST</code> and <code>GL_LINEAR</code> looks almost the same
  </figcaption>
</figure>

<p>
It looks almost the same. Looking closer, the coastlines look slightly smoother:
</p>

<figure>
  <img style="width: 45%; padding-right: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed187-r16f-nearest-zoomed.png"/>
  <img style="width: 45%; padding-left: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/seed187-r16f-linear-zoomed.png"/>
  <figcaption>
    <code>GL_NEAREST</code> vs <code>GL_LINEAR</code>, coastlines are smoother
  </figcaption>
</figure>

<p>
So that wasn't as much of a win as I thought it would be.
</p>

<p>
To compensate for the mountains looking smooth, I added a slider <code>mountain_folds</code> that adds geometry to make the sides of mountains look more interesting:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/slider-mountain-fold-low.png"/>
  <img src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/slider-mountain-fold-high.png"/>
  <figcaption>
    <code>mountain_folds</code> slider to add geometric interestingness
  </figcaption>
</figure>

<p>
I enjoyed the graphics rewrite. I hadn't looked at the graphics code in a long time, and I found lots of places for improvement. I found and understood a bug that I had long suspected. All of this <em>could</em> have been done without a rewrite, but the rewrite got me to actually do it.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-10-13-mapgen4-webgl2/"/>
    <published>2025-10-13T09:23:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-09-30-mapgen4-river-shader</id>
    <title>Mapgen4 river shader</title>
    <updated>2026-03-23T15:06:24.922145-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
Back in 2018 I had the idea to prerender short segments of river bends and confluences into a texture, and then use that texture to draw the rivers to the screen. I was trying to have draw exactly one triangle per Delaunay triangle, so that I could generate the geometry ahead of time  and only change the texture coordinates. <a href="https://simblob.blogspot.com/2018/09/mapgen4-river-appearance.html">I planned to implement this</a> as a 2D table:
</p>

<figure>
  <img src="https://www.redblobgames.com/maps/mapgen4/blog/debugging-river-t1.png" alt=""/>
  <figcaption> Debug visualization of river rendering texture</figcaption>
</figure>


<x:cut/>


<p>
To simplify, I switched to using bezier curves:
</p>

<figure>
  <img src="https://www.redblobgames.com/maps/mapgen4/blog/debugging-river-curves-on-triangles.png" alt=""/>
  <figcaption> Debug visualization of curved river rendering texture</figcaption>
</figure>


<p>
I was planning to use the input and output width as the indices into the table, but ended up using only one width. What I didn't realize at the time was that I no longer needed a 2D table; I could've used a 1D table. And even better, I could've drawn the curve in a shader, like I did 
<a href="https://www.redblobgames.com/x/1730-terrain-shader-experiments/#curved-paths">in these shader experiments</a>. But <em>I was focused on shipping</em> and not doing things the cleanest or most efficient way.
</p>

<p>
After I <a href="https://www.redblobgames.com/blog/2025-09-29-mapgen4-renderer/">rewrote the main renderer</a>, I decided to rewrite the river renderer too. I adapted one of the earlier shader experiments to make a river shader, and compared before/after:
</p>

<figure class="w-150b">
  <a target="_blank" href="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-river-old-1.png"><img style="width:45%;padding-right:2px" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-river-old-1.png"/></a>
  <a target="_blank" href="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-river-new-1.png"><img style="width:45%;padding-left:2px" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-river-new-1.png"/></a>
  <figcaption>Old and new river renderer</figcaption>
</figure>

<p>
It's a bit hard to tell at that zoom level, but if you open the two images in separate tabs you can flip back and forth to compare. Here's a closer view:
</p>

<figure class="w-150b">
  <img style="width:45%;padding-right:2px" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/river-renderer-2018.png"/>
  <img style="width:45%;padding-left:2px" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/river-renderer-2025.png"/>
  <figcaption>Old and new rendering, close up</figcaption>
</figure>

<p>
The rivers look sharper. But some of the flaws that were hidden before by the blurriness are now visible.
</p>

<p>
To fix those flaws, I wanted to use a shader to draw wide bezier curves inside a triangle. I didn't have a good intuition for bezier curves with barycentric coordinates, so I had an LLM generate a quick &amp; dirty tool to help me build my intuition. <a href="https://www.redblobgames.com/x/2531-barycentric-bezier/">It's like shadertoy but for one triangle</a>. This was an attempt at learning how to "vibe code", and I have mixed feelings about it, but that's a story for another time.
</p>

<p>
Playing with the toy made me see that there <em>should be</em> a way to do what I want, but it's not from bezier curves like I originally thought. Instead, I can curve fit two degree-5 polynomials, one for each side of the river. Using the LLM-generated code helped me figure this out, but it wasn't able to directly give me a solution. I'll have to work out the details myself at some point.
</p>

<p>
I added "better river shader" to my list, but at a low priority, so I'm going to work on some other things next and come back to it.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-09-30-mapgen4-river-shader/"/>
    <published>2025-09-30T13:23:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-09-29-mapgen4-renderer</id>
    <title>Mapgen4 renderer</title>
    <updated>2026-03-23T15:06:24.919815-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
It's been a while since I've worked on mapgen4 features. In 2018 I declared it <a href="https://simblob.blogspot.com/2018/11/mapgen4.html">finished</a>. I then put it away and worked on other projects. Over the years I have updated the code (es6 modules, typescript, pointer events, boundary points, pnpm) without adding any features. I had been leaving all feature updates for a future map generator.
</p>

<p>
Last year I decided it was time to start thinking about new features. I started experimenting with some prototypes. That made me realize there are many improvements I'd like to make to mapgen4 before getting into big features.
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/debug-river-shader-1-small.png" alt=""/>
  <figcaption> Mapgen4 debug view</figcaption>
</figure>


<x:cut/>


<p>
I made a list. One of the projects on the list is rewriting the renderer. But why?
</p>

<ol class="org-ol">
<li><strong>Migrate to WebGL2</strong>. I started mapgen4 in 2017. WebGL2 wasn't <a href="https://caniuse.com/?search=webgl2">widely available until 2021</a>. I'd like to use it both for <a href="https://webgl2fundamentals.org/webgl/lessons/webgl2-whats-new.html">features</a> and performance. I'm using a WebGL1 library, <a href="https://github.com/regl-project/regl/blob/gh-pages/API.md">Regl.js</a>, which <a href="https://github.com/regl-project/regl/issues/378">does not support WebGL2</a>.</li>
<li><strong>Reduce load time</strong>. Regl.js almost doubles the JS size of mapgen4. I would be able to shrink mapgen4 significantly by switching away from it.</li>
</ol>

<p>
Rewriting the renderer was not something I was looking forward to. It's work that has to be done in "one shot", where the intermediate steps aren't testable. I decided to use an LLM to help me. I rarely use LLMs for coding and learned a lot.
</p>

<ol class="org-ol">
<li>In this project I used the LLM for <em>translating</em> existing working code from Regl.js to WebGL1. This felt different from having it write new code.</li>
<li>The resulting code had some subtle bugs. I was able to track some of them down by comparing the output of the original mapgen4 renderer with the rewritten renderer. Having a previous working version was important.</li>
<li><strong>It got me out of my analysis paralysis</strong>. Having <em>something</em> that runs rather than a blank editor window was a huge win.</li>
<li>I didn't like the structure of the code it generated. It triggered <a href="https://xkcd.com/386/">XKCD 386</a> for me. I ended up replacing all of its code, bit by bit, testing after each change.</li>
</ol>

<p>
Along the way I re-evaluated some of how the renderer worked. For example, I'm using <em>nearest neighbor</em> filtering in many places, even though <em>linear</em> might be better. 
</p>

<figure>
  <img style="width: 45%; padding-right: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-texture-nearest.png"/>
  <img style="width: 45%; padding-left: 0.25em" src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/render-texture-linear.png"/>
  <figcaption>
  <code>GL_NEAREST</code> vs <code>GL_LINEAR</code> filtering
  </figcaption>
</figure>

<p>
Look at how much smoother the ocean colors are with linear filtering! But there are blue splotches near rivers. I decided to keep nearest neighbor filtering for now.
</p>

<p>
I also discovered some bugs, like this one where one edge of the map is jagged, but only noticeable when the camera is rotated and zoomed in:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2524-mapgen4-renderer/blog/mapgen4-bug-top-edge-bumpy.png" alt=""/>
  <figcaption> Bug: jagged edge</figcaption>
</figure>


<p>
Overall, I don't think the LLM saved me any time. I didn't end up keeping the code it gave me. But <strong>it got me unstuck</strong>, and that meant I actually made progress.
</p>

<p>
The main benefit of the rewrite was output size:
</p>

<table class="standard">


<colgroup>
<col class="org-left"/>

<col class="org-right"/>

<col class="org-right"/>
</colgroup>
<thead>
<tr>
<th scope="col" class="text-left">File</th>
<th scope="col" class="text-right">Size before</th>
<th scope="col" class="text-right">Size after</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-left">_worker.js</td>
<td class="text-right">18543</td>
<td class="text-right">18579</td>
</tr>

<tr>
<td class="text-left">_bundle.js</td>
<td class="text-right">150672</td>
<td class="text-right">69469</td>
</tr>
</tbody>
<tbody>
<tr>
<td class="text-left">(total)</td>
<td class="text-right">169215</td>
<td class="text-right">88048</td>
</tr>
</tbody>
</table>

<p>
But the source code did get bigger, from 600 lines to 750 lines (to avoid 9500 lines of Regl.js).
</p>

<p>
The secondary benefits are that I got to revisit some of the decisions I made, I found some bugs, and I got unstuck. I didn't stop at the main renderer. I also rewrote the river renderer, which saved another 8666 bytes. That's for the <a href="https://www.redblobgames.com/blog/2025-09-30-mapgen4-river-shader/">next blog post</a>.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-09-29-mapgen4-renderer/"/>
    <published>2025-09-29T15:05:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-09-22-harnessing-chatgpt-hallucinations</id>
    <title>Harnessing ChatGPT hallucinations</title>
    <updated>2026-03-23T15:06:24.914322-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
The web server I use (nginx) logs errors to a file. I check this occasionally to make sure nothing's going wrong with my server. I typically get these kinds of 404 requests:
</p>

<dl class="org-dl">
<dt>typos</dt><dd>the url <kbd>/pathfinding/a-star/inroduction.html</kbd> was meant to be <code>/pathfinding/a-star/introduction.html</code></dd>
<dt>parsing errors</dt><dd>the url <kbd>/articles/visibility/)and</kbd>  was some text <code>/articles/visibility/) and</code>  that went through a url regexp</dd>
<dt>misconfigured crawlers</dt><dd>the url <kbd>/region-graph-path.png</kbd> was originally from html <code>&lt;img src="region-graph-path.png" …&gt;</code> but the crawler tried fetching it from the top level instead of the subfolder</dd>
<dt>attackers</dt><dd>the url <kbd>/FCKeditor/editor/filemanager/connectors/asp/connector.asp?Command=FileUpload&amp;Type=File&amp;CurrentFolder=%2F</kbd> is some crawler looking for weaknesses in my web site</dd>
</dl>

<p>
Last week I noticed a request for <kbd>/articles/autotiles/</kbd> with the HTTP Referer <code>chatgpt.com</code>. This is an unusual request. It's not <em>syntactic</em> like the above. It's a <em>semantic</em> error. An AI  <a href="https://en.wikipedia.org/wiki/Hallucination_(artificial_intelligence)">confabulation/hallucination</a> probably. The referrer means that the user was on <code>chatgpt.com</code> and clicked a link to <code>https://www.redblobgames.com/articles/autotiles/</code>, which  is a 404 error.
</p>

<p>
I decided to investigate more.
</p>

<x:cut/>


<p>
If I ask <kbd>what does red blob games have to say about autotiles</kbd>, ChatGPT will tell me:
</p>

<blockquote>
<p>
I couldn’t find any evidence that Red Blob Games (Amit Patel) has written anything specifically about autotiles (in the sense of tilesets that automatically pick tile variants based on neighbors). Their site covers many things like grids, map generation, procedural terrain, pathfinding, etc., but I didn’t locate a page dedicated to autotiling.
</p>
</blockquote>

<p>
But if I ask <kbd>tell me about redblobgames tutorial on wave function collapse</kbd>, ChatGPT will tell me:
</p>

<blockquote>
<p>
The Wave Function Collapse (WFC) tutorial by Red Blob Games is an in-depth guide to understanding and implementing the WFC algorithm for procedural content generation. It's highly regarded for breaking down complex concepts in a very approachable way.
</p>
</blockquote>

<p>
<strong>I don't actually have a tutorial on Wave Function Collapse</strong>, but ChatGPT will give me the url <a href="https://www.redblobgames.com/articles/wfc/">https://www.redblobgames.com/articles/wfc/</a>.
</p>

<p>
This is a reminder that you shouldn't believe everything an LLM tells you! But lots of people have already written about that. I want to explore something different. Instead of treating the hallucinations as something to discard, I want to treat them as something to harness.
</p>

<p>
I looked through my error logs to find other non-existent URLs that ChatGPT gave out. I also looked at what OpenAI's <a href="https://platform.openai.com/docs/bots">crawlers</a> found but they weren't as useful. I looked at the top ten:
</p>

<ul class="org-ul">
<li><code>/sliding-puzzle/</code>:  this is a classical AI topic that could fit into game-ai</li>
<li><code>/articles/minimax/</code>:  this is also a classical AI topic that could fit into game-ai</li>
<li><code>/grids/isometric/</code>:  this is a topic I'd like to write about</li>
<li><code>/maps/noise/introduction.html</code>:  this is the wrong folder for <code>/articles/noise/introduction.html</code></li>
<li><code>/pathfinding/a-star/visualization.html</code>: this is an existing folder but a non-existing page</li>
<li><code>/grids/hex-grids/</code>:  this is the wrong version of <code>/grids/hexagons/</code></li>
<li><code>/articles/procgen/</code>:  this is a page I could've written, to point to other procgen topics</li>
<li><code>/grids/mazes/</code>:  this is a reasonable guess of what I might have written about</li>
<li><code>/articles/mazes/</code>:  same</li>
<li><code>/maps/noise/</code>:  wrong folder</li>
</ul>

<p>
A subset of these are interesting to me — topics that I could write about. I went deeper into the list of 404s and collected these hallucinated URLs:
</p>

<pre id="urls" class="example">
/x/2048/hilbert.html
/pathfinding/a-star/maze.html
/pathfinding/a-star/visualization.html
/x/1725-aabb-rectangle-overlap/
/x/1842-agent-based-model/
/x/1845-ai-car/
/grids/algorithms/
/x/1813-alpha-beta/
/pathfinding/ants/ /x/1844-ants/
/x/1841-atmosphere-simulation/
/articles/autotiles/
/x/1614-balance/
/x/1840-barycentric-coordinates/ /x/1842-barycentric-coordinates/
/x/1819-battleship-probability/
/x/1723-battleship-strategy/
/x/1846-battleship/
/x/1911-bits/
/x/1910-bitwise/
/x/1743-boids/ /x/1847-boids/ /x/2020-boids/
/x/1729-bresenham/
/maps/catan/ /x/1830-catan/
/maps/cave-generation/
/x/1845-cell-automata/
/x/2020-cells/
/articles/cellular-automata/ /grids/cellular-automata/ /x/1840-cellular-automata/ /x/1841-cellular-automata/ /x/1843-cellular-automata/ /x/2020-cellular-automata/ /x/2023-cellular-automata/ /x/2302-cellular-automata/
/grids/circle-circle-collision/
/x/1728-circle-drawing/
/x/1721-circle-packing/ /x/1830-circle-packing/ /x/1844-circle-packing/ /x/1945-circle-packing/
/articles/cities/
/x/1948-citizenship/
/maps/city-building/
/x/1840-city-generator/ /x/1841-city-generator/ /x/2023-city-generator/
/x/1842-citygen/ /x/1843-citygen/
/articles/collision-detection/
/x/1841-combat-model/
/x/compression/brotli.html
/x/compression/string.html
/articles/connected-tiles/
/articles/convex-hull/
/articles/conways-game-of-life/ /x/1721-conways-game-of-life/
/x/2020/cookie-clicker-save-editor/
/x/2020-cookie-clicker-save-viewer/
/grids/coordinates/ /maps/coordinates/
/x/2020-creature-trainer/
/x/1728-curve-editor/ /x/1729-curve-editor/
/articles/2d-geometry/
/articles/3d-octrees/
/pathfinding/d-star/introduction.html
/articles/3d-voxel-traversal/
/grids/3d/
/articles/data-oriented-design/
/x/1724-delaunay-triangulation/
/x/1722-delaunay/delaunay/
/x/1845-dialogue-trees/
/articles/dialogue/
/x/2018-dice-calculator/
/x/1846-digging/
/x/dither/
/x/1844-dots-and-crosses/
/articles/dungeon-generation/ /maps/dungeon-generation/
/articles/procedural-generation/dungeon/
/x/dungeon/dungeon.html
/x/2020/easing-graphs/
/x/1842-economy-sim/
/x/1843-ecosystem-simulation/
/x/1843-emacs-movement/
/x/1843-erosion/
/x/1843-evolution-creatures/
/x/1724-evolution-simulation/ /x/1841-evolution-simulation/ /x/1843-evolution-simulation/ /x/1844-evolution-simulation/ /x/1845-evolution-simulation/
/x/1811-evolution-simulator/ /x/1849-evolution-simulator/
/x/1810-evolution/ /x/1840-evolution/ /x/1842-evolution/ /x/1844-evolution/ /x/1846-evolution/ /x/1904-evolution/
/x/1942-evolutionary-algorithms/
/articles/finite-state-machines/
/x/1842-fire/
/maps/flag/ /x/flag/
/x/2023-flatbuffers/
/x/2020-float-to-bits/
/pathfinding/flowfield/
/fluid-simulation/ /x/1843-fluid-simulation/
/articles/fog-of-war/
/articles/formation-control/
/x/1843-fractal-tree/
/x/1843-frame-timing/
/pathfinding/funnel/
/maps/galaxy/
/x/1728-game-architecture/
/x/1842-game-loop/
/x/2023-game-programming-links/
/x/1618-genetic-algorithm/ /x/1842-genetic-algorithm/ /x/1844-genetic-algorithm/
/articles/genetic-algorithms/ /x/1717-genetic-algorithms/ /x/1724-genetic-algorithms/ /x/1816-genetic-algorithms/ /x/1830-genetic-algorithms/ /x/1845-genetic-algorithms/ /x/1951-genetic-algorithms/ /x/2020-genetic-algorithms/
/x/1844-genetic-drift/
/articles/geometry/
/x/1845-gradient-descent/ /x/1908-gradient-descent/
/x/1811-graph-maps/mazes.html
/x/1843-graphics-pipeline/
/articles/graphs/
/x/1844-gravity-simulator/
/grids/circle-rect-intersection.html
/grids/floodfill.html
/grids/wordsearch.html
/articles/hash-table/
/maps/heightmaps/
/x/1840-hex-map-editor/ /x/1843-hex-map-editor/
/x/hex-map-maker/
/x/1840-hex-range/
/x/2342-hex-roads/
/x/1843-hex-sphere/
/x/1843-hex-tile-mesh/
/x/1842-hex-viewer/
/x/1844-hexagon-tilings-spherical/
/x/hexagonal-grid-generator/
/x/1843-hexagons-on-sphere/
/x/1840-hexagons-vs-squares/
/maps/hexagons/
/x/honeycomb/
/x/1729-hydraulic-erosion/
/x/1843-icosphere-hexagons/
/x/1812-ik/ /x/1816-ik/ /x/1843-ik/ /x/1844-ik/ /x/1846-ik/
/x/2020-infection-model/
/x/1923-intro-aabb/
/x/1722-intro-spatial-partitioning/
/x/1724-inverse-kinematics/ /x/1840-inverse-kinematics/ /x/1842-inverse-kinematics/ /x/1849-inverse-kinematics/ /x/1908-inverse-kinematics/
/maps/islands/
/x/1842-isometric-depth-sorting/
/x/1911-isometric-projection/
/x/1847-isometric-tiles/
/x/1911-isometric-voxels/ /x/1911-isometric-voxels/
/articles/isometric/ /grids/isometric/ /isometric/
/x/1930-jigsaw-puzzle/
/x/1806-jumping/
/x/1721-kdtree/
/articles/kinematics/
/x/1728-langstons-ant/
/x/1840-langtons-ant/ /x/1844-langtons-ant/ /x/1943-langtons-ant/
/x/1844-life-simulation/
/x/2048-life/
/x/2022-lighting/
/x/1842-line-drawing-algorithms/
/x/2020-lorenz-attractor/
/x/1847-lowpoly/
/x/2023-lsystem/
/x/1722-lsystems/ /x/1843-lsystems/
/articles/map-generation/
/catan/map-generator/
/maps/mapgen/ /x/1844-mapgen/
/x/1843-mapgen4/
/maps/mapmaker/
/x/1843-mapping-spherical-worlds/
/x/1910-marching-squares/
/x/1728-markov-decision-processes/
/x/1843-matrix-transformations/
/matrix-transforms/
/grids/maze-generation/ /x/1847-maze-generation/
/articles/maze/ /pathfinding/maze/
/x/2020-mcts/
/maps/mesh/
/articles/minimax/ /pathfinding/minimax/ /x/1941-minimax/
/x/1840-model-view-controller/
/articles/monads/
/articles/monte-carlo-tree-search/
/x/monty-hall/
/x/1830-morphogenesis/
/pathfinding/multi/
/pathfinding/navmesh/
/x/1841-neural-networks/
/x/2217-nodal/
/x/1847-noise-viewer/
/x/1729-noise/ /x/1840-noise/
/x/1724-np-completeness/
/x/1846-octrees/
/x/1726-offset-paths/
/x/1845-optical-flow/
/articles/organisms/
/x/1729-p-vs-np/
/x/1924-packing-boxes/
/pathfinding/pacman/
/x/2020-pandemic-simulation/
/x/1843-particle-life/ /x/2020-particle-life/
/x/1846-particles/
/articles/partitioning/
/pathfinding/jump-point-search.html
/x/1843-pedestrian-simulation/
/maps/perilous-shores/
/maps/perlin-noise/ /x/1729-perlin-noise/
/x/1842-physics-simulations/
/articles/physics/ /x/1723-physics/ /x/1841-physics/
/x/pixel-count/
/x/1613-pixel-grid/
/x/1841-planet-generation/ /x/1844-planet-generation/
/x/1843-planet-generator/
/x/planet/
/articles/noise/poisson-disc/
/articles/poisson-disk-sampling/
/x/1830-poisson-disk/
/x/1848-polygon-boolean/
/x/1843-polygon-editor/
/articles/polygon-fill/
/articles/polygon-map-generation/
/x/1923-polygon-offsets/
/articles/polygon-partitioning/
/x/1721-polygons/
/articles/procedural-cave/
/x/1843-procedural-city/
/x/1843-procedural-creatures/
/articles/procedural-generation/
/x/1843-procedural-paths/
/x/1843-procedural-planet-gen/
/x/1723-procedural-rivers/
/articles/procedural-road-generation/
/articles/procgen-cave/
/maps/procgen/
/pathfinding/puzzle/
/x/1728-quadtree-visualization/ /x/1840-quadtree-visualization/
/x/1848-quadtree/
/articles/quadtrees/ /grids/quadtrees/ /x/1729-quadtrees/
/x/1843-ray-optics/
/x/1642-reaction-diffusion/ /x/1724-reaction-diffusion/ /x/1726-reaction-diffusion/ /x/1846-reaction-diffusion/ /x/1849-reaction-diffusion/ /x/2022-reaction-diffusion/ /x/reaction-diffusion/
/grids/rectangles/
/x/1844-robot-arm/
/x/1726-rock-paper-scissors/
/x/roguelike-java/
/articles/rooms-and-corridors/
/grids/rotation/
/x/1724-rotations/
/x/1723-rps-simulation/
/x/1720-sand/
/x/2216-sdf-physics/
/x/1922-self-driving-car/
/articles/shunting-yard/
/sliding-puzzle/
/x/2020-smooth-curve/
/pathfinding/smoothing/
/x/1849-soft-body-creatures/
/x/2001-solar-system-generator/ /x/2044-solar-system-generator/
/x/2142-solar-system/
/articles/spatial-indexing/
/x/1724-spherical-voronoi/
/grids/spiral/
/x/1843-spring-damper/
/x/1845-spring-mass/
/x/1843-star-map-generator/
/x/2020-starscape/
/x/1843-stat-scaling/
/articles/steering-behaviors/
/x/1844-straight-skeleton/
/x/1840-svg-vs-png/
/pathfinding/tangent-bug/
/maps/tectonics/ /x/1847-tectonics/
/x/2311-tensor-cores/
/x/1842-terrain-blending/
/x/1845-terrain-generation/
/maps/terrain/ /x/terrain/
/x/1841-tetris-ai/
/x/2312-tetris-art/
/x/1725-threading/
/articles/tile-map-logic/
/articles/tilemaps/
/grids/tiles/ /maps/tiles/
/x/1845-town/
/x/1840-triangle-intrusion/
/grids/triangle/
/x/1843-trig/
/x/1846-tsp-genetic/
/pathfinding/tsp/
/x/1723-turing-patterns/ /x/1729-turing-patterns/ /x/1844-turing-patterns/ /x/1847-turing-patterns/
/articles/turn-based-ai/
/x/1846-turn-based-loop/
/x/turtle/
/x/1841-vanishing-points/
/articles/vectors/ /x/1840-vectors/
/x/2022/06/virtual-machines-in-rust/
/x/1707-visibility-2d/
/x/1723-visibility/
/articles/visible-area/
/articles/visualizing-parallax/
/x/1846-volcano/
/x/1847-volumetric-light/
/x/1908-voronoi-3d/
/x/1836-voronoi-circle-growth/
/x/1830-voronoi-diagram/ /x/1841-voronoi-diagram/ /x/1843-voronoi-diagram/ /x/1846-voronoi-diagram/ /x/1847-voronoi-diagram/ /x/1848-voronoi-diagram/
/x/1840-voronoi-map-generator/ /x/1849-voronoi-map-generator/
/x/1721-voronoi-map/ /x/1723-voronoi-map/
/x/1730-voronoi-maps/
/x/1843-voronoi-noise/ /x/1912-voronoi-noise/
/x/1842-voronoi-relaxation/
/x/1843-voronoi-tile/
/x/1846-voronoi-tsp/
/x/1722-voronoi/ /x/1729-voronoi/ /x/1847-voronoi/ /x/2023-voronoi/ /x/voronoi/
/x/1728-voxel-meshes/
/articles/voxel-raycasting/
/x/1720-voxel-terrain/
/articles/voxels/ /grids/voxels/
/x/1840-wang-tiles/
/x/1847-water-simulation/
/x/1844-water/
/x/1843-waterflow/ /x/1844-waterflow/
/x/1728-wave-function-collapse/ /x/1729-wave-function-collapse/ /x/2023-wave-function-collapse/
/articles/wavefunction-collapse/ /x/wavefunction-collapse/
/x/2020-wavefunctioncollapse/
/x/2022-weather/
/articles/weighted-random/
/x/1836-weighted-voronoi/
/x/2023-wildfire-ca/
/maps/worldgen/
/x/1844-wraparound/
/x/1840-quadtrees
/x/1843-solar-system-generator
/x/1847-sine-wave-art
/x/1849-town
/x/1941-minimax
</pre>

<p>
<strong>Most of the URLs it's giving out are plausibly something I could have written</strong>.
</p>

<p>
I think it's also interesting to see what it has "learned" about my URL structure. For my <code>x/</code> pages, I use the first two digits for the year. ChatGPT is making up lots of URLs starting with <code>x/18</code> which means that it thinks that 2018 was my most productive year. And … maybe it's right. Hm.
</p>

<p>
This was a fun diversion. My server's error logs tell me that ChatGPT thinks I've written pages I haven't. Maybe I should actually write those pages!
</p>

<style>
#urls {
  color: hsl(45 50% 30%);
  max-height: 20em;
  scrollbar-y: scroll;
}
blockquote {
  padding-inline: 2em;
}
</style>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-09-22-harnessing-chatgpt-hallucinations/"/>
    <published>2025-09-22T13:14:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-08-30-lets-write-a-search-engine-2</id>
    <title>Let's write a search engine, part 2 of 2</title>
    <updated>2026-03-23T15:06:24.912173-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
<a href="https://www.redblobgames.com/blog/2025-08-29-lets-write-a-search-engine-1/">Last time</a> we implemented a search feature that found all matching documents, sorted in some arbitrary order. This time I want to sort them in a more reasonable order. Here are some ideas, some which depend on the query and others which don't:
</p>

<ul class="org-ul">
<li>popular pages first</li>
<li>newer (or older) pages first</li>
<li>promote (or demote) certain folders manually</li>
<li>promote (or demote) based on the type of content in the page</li>
<li>pages that have more occurrences of the query string</li>
<li>pages where the query matches are spread out (or concentrated)</li>
<li>pages that have the query string in titles or headings</li>
</ul>

<p>
I'm going to try building this:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-08-30-lets-write-a-search-engine-2/search-engine-6.png" alt=""/>
  <figcaption> What we're trying to build</figcaption>
</figure>


<x:cut/>


<p>
To keep the scope small, I'm going to stick to substring match and not other query matching algorithms:
</p>

<ul class="org-ul">
<li>word match (<kbd>graph pathfinding</kbd> matches <code>graph for pathfinding</code> and <code>pathfinding graph</code>)</li>
<li>ordered word match (<kbd>big dog</kbd> matches <code>big red dog</code> but not <code>dog was big</code>)</li>
<li>regexp match (<kbd>r.*dog</kbd> matches <code>red dog</code> but not <code>dog was red</code>)</li>
<li>wildcard match (<kbd>r* dog</kbd> matches <code>red dog</code> but not <code>dog was red</code>)</li>
<li>plurals / stemming (<kbd>hexagon</kbd> matches <code>hexagons</code> and <code>hexagonal</code>)</li>
<li>stop words (<kbd>the algorithm</kbd> matches <code>an algorithm</code>)</li>
<li>spelling (<kbd>dikjstra</kbd> matches <code>dijkstra</code>)</li>
<li>synonym match (<kbd>mapgen</kbd> matches <code>map generator</code>)</li>
<li>case match (<kbd>OPEN set</kbd> matches <code>OPEN set</code> but not <code>open set</code>)</li>
<li>punctuation match (<kbd>can't</kbd> matches <code>can't</code> but not <code>cant</code>)</li>
<li>parent category match (<kbd>#mapgen</kbd> matches <code>#mapgen4</code>)</li>
</ul>

<p>
Note that sometimes matching should be asymmetric, because the way we write things in queries is not the same way we write things in documents:
</p>

<ul class="org-ul">
<li><kbd>open set</kbd> should match both <code>OPEN set</code> and <code>open set</code>, but <kbd>OPEN set</kbd> should match only  <code>OPEN set</code></li>
<li><kbd>cant</kbd> should match both <code>can't</code> and <code>cant</code>, but <kbd>can't</kbd> should match only <code>can't</code></li>
<li><kbd>mapgen</kbd> should match both <code>#mapgen</code> and <code>#mapgen4</code>, but <kbd>#mapgen4</kbd> should match only <code>#mapgen4</code></li>
</ul>

<p>
There's a whole lot to explore with matching algorithms but I want to finish this project in a weekend so I will stick to case-insensitive substring match.
</p>

<p>
For some experiments I need access to headings, so I added headings to the search data. Some of my headings are <kbd>title="heading"</kbd> and some are <kbd>&lt;h3&gt;heading&lt;/h3&gt;</kbd> so I extracted both:
</p>

<div class="org-src-container">
<pre class="src src-sh">fd --extension bxml | <span class="keyword">while </span><span class="builtin">read</span> -r file; <span class="keyword">do</span>
    <span class="builtin">echo</span> <span class="string">"%%%% $file %%%%"</span>
    perl -ne <span class="string">'print "HEADER=$1\n" if /title="(.*)?"/'</span> <span class="string">"$file"</span>
    perl -ne <span class="string">'print "HEADER=$2\n" if /&lt;h(\d).*?&gt;(.*?)&lt;\/h\1&gt;/'</span> <span class="string">"$file"</span>
    cat <span class="string">"$file"</span> <span class="sh-escaped-newline">\</span>
        | pandoc --read html --write plain --wrap none <span class="sh-escaped-newline">\</span>
        | grep <span class="string">'[a-zA-Z0-9]'</span>
<span class="keyword">done</span> &gt;all-pages.txt
</pre>
</div>

<p>
I'm using regexps instead of <code>xpath</code> or <code>xmllint</code> to extract from my xhtml files, so it misses some things and improperly formats others. That's ok for now. The main goal is to try out ranking, and then I can go back and improve the extraction if I want to pursue this project.
</p>

<p>
I then refactored the code into smaller functions to make experimentation easier.  I also improved the formatting and made the search results clickable (opening in a new window).
</p>

<div class="org-src-container">
<pre class="src src-js"><span class="keyword">function</span> <span class="function-name">escapeHTML</span>(<span class="variable-name">text</span>) { <span class="comment-delimiter">// </span><span class="comment">wish this was built in
</span>    <span class="keyword">const</span> <span class="variable-name">map</span> = {
        <span class="string">'&amp;'</span>: <span class="string">'&amp;amp;'</span>,
        <span class="string">'&lt;'</span>: <span class="string">'&amp;lt;'</span>,
        <span class="string">'&gt;'</span>: <span class="string">'&amp;gt;'</span>,
        <span class="string">'"'</span>: <span class="string">'&amp;quot;'</span>,
        <span class="string">"'"</span>: <span class="string">'&amp;#39;'</span>
    };
    <span class="keyword">return</span> text.replace(<span class="string">/[&amp;&lt;&gt;"']/</span>g, m =&gt; map[m]);
}

<span class="keyword">function</span> <span class="function-name">parseIntoDocuments</span>(<span class="variable-name">text</span>) {
    <span class="comment-delimiter">// </span><span class="comment">results[0] will be "", then [1] is a filename, [2] is text, alternating
</span>    <span class="keyword">const</span> <span class="variable-name">results</span> = text.split(<span class="string">/%%%% (.*?) %%%%$/</span>gm); 
    <span class="keyword">if</span> (results[0] !== <span class="string">""</span>) <span class="keyword">throw</span> <span class="string">`expected first split to be empty ${results[0]}`</span>;
    <span class="keyword">if</span> (results[1].length &gt; 100) <span class="string">"expected second split to be filename"</span>;
    <span class="keyword">let</span> <span class="variable-name">documents</span> = [];
    <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">i</span> = 1; i+1 &lt; results.length; i += 2) {
        <span class="keyword">let</span> <span class="variable-name">filename</span> = results[i];
        <span class="keyword">let</span> <span class="variable-name">lines</span> = results[i+1].split(<span class="string">"\n"</span>);
        lines = lines.map(line =&gt; line.trim()); <span class="comment-delimiter">// </span><span class="comment">remove extra whitespace
</span>        lines = lines.filter(line =&gt; line); <span class="comment-delimiter">// </span><span class="comment">remove blank lines
</span>        <span class="keyword">let</span> <span class="variable-name">title</span> = (lines.find((line) =&gt; line.startsWith(<span class="string">"HEADER="</span>)) ?? <span class="string">""</span>).slice(7) || filename;
        <span class="keyword">let</span> <span class="variable-name">url</span> = filename.startsWith(<span class="string">"redblobgames/"</span>)
            ? <span class="string">"https://www.redblobgames.com/"</span> + filename.slice(13)
            : <span class="string">"http://www-cs-students.stanford.edu/~amitp/"</span> + filename;
        url = url.replace(<span class="string">".bxml"</span>, <span class="string">".html"</span>);
        url = url.replace(<span class="string">"/index.html"</span>, <span class="string">"/"</span>);
        documents.push({url, title, lines});
    }
    <span class="keyword">return</span> documents;
}
<span class="keyword">const</span> <span class="variable-name">words</span> = <span class="keyword">await</span> (<span class="keyword">await</span> fetch(<span class="string">"./all-pages.txt"</span>)).text();
<span class="keyword">const</span> <span class="variable-name">documents</span> = parseIntoDocuments(words);

<span class="keyword">function</span> <span class="function-name">search</span>(<span class="variable-name">query</span>) {
    <span class="keyword">let</span> <span class="variable-name">results</span> = []; <span class="comment-delimiter">// </span><span class="comment">list of {document, snippet}
</span>    <span class="comment-delimiter">// </span><span class="comment">where document is {url, title, lines}
</span>    <span class="comment-delimiter">// </span><span class="comment">where snippet is a list of {line, matches, isHeader}
</span>    <span class="keyword">if</span> (query) {
        query = <span class="keyword">new</span> <span class="type">RegExp</span>(RegExp.escape(query), <span class="string">'gi'</span>);
        <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">document</span> <span class="keyword">of</span> documents) {
            <span class="keyword">let</span> <span class="variable-name">snippet</span> = [];
            <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">line</span> <span class="keyword">of</span> document.lines) {
                <span class="keyword">let</span> <span class="variable-name">isHeader</span> = <span class="constant">false</span>;
                <span class="keyword">if</span> (line.startsWith(<span class="string">"HEADER="</span>)) {
                    line = line.slice(7);
                    isHeader = <span class="constant">true</span>;
                }
                <span class="keyword">let</span> <span class="variable-name">matches</span> = line.matchAll(query).toArray();
                <span class="keyword">if</span> (matches.length &gt; 0) {
                    snippet.push({line, matches, isHeader});
                }
            }
            <span class="keyword">if</span> (snippet.length &gt; 0) results.push({document, snippet});
        }
    }
    <span class="keyword">return</span> results;
}

<span class="keyword">function</span> <span class="function-name">formatSnippet</span>(<span class="variable-name">snippet</span>) {
    <span class="comment-delimiter">// </span><span class="comment">TODO: be able to sort snippets before picking the top matches
</span>    <span class="keyword">const</span> <span class="variable-name">MAX_SNIPPETS</span> = 8;
    <span class="keyword">let</span> <span class="variable-name">result</span> = <span class="string">"… "</span>;
    <span class="keyword">for</span> (<span class="keyword">let</span> {line, matches} <span class="keyword">of</span> snippet.slice(0, MAX_SNIPPETS)) {
        <span class="keyword">let</span> <span class="variable-name">pos</span> = 0;
        <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">m</span> <span class="keyword">of</span> matches) {
            result += escapeHTML(line.slice(pos, m.index)) + <span class="string">`&lt;b&gt;${m[0]}&lt;/b&gt;`</span>;
            pos = m.index + m[0].length;
        }
        result += <span class="string">`${escapeHTML(line.slice(pos))} … `</span>;
    }
    <span class="keyword">return</span> result;
}

<span class="keyword">function</span> <span class="function-name">formatSearchResults</span>(<span class="variable-name">query</span>, <span class="variable-name">results</span>) {
    <span class="keyword">return</span> results
          .map(({document, snippet}) =&gt; 
              <span class="string">`&lt;a target="_blank" href="${document.url}"&gt;
                  ${document.title}
               &lt;/a&gt;
               &lt;div class="url"&gt;
                  ${document.url}
               &lt;/div&gt;
               &lt;div class="snippet"&gt;
                  ${formatSnippet(snippet)}
               &lt;/div&gt;`</span>)
          .join(<span class="string">"\n"</span>);
}

<span class="keyword">function</span> <span class="function-name">setUpSearch</span>(<span class="variable-name">id</span>, <span class="variable-name">sortResults</span>) {
    <span class="keyword">const</span> <span class="variable-name">MAX_DOCUMENTS</span> = 10;
    <span class="keyword">const</span> <span class="variable-name">el</span> = document.getElementById(id);
    <span class="keyword">const</span> <span class="variable-name">update</span> = () =&gt; {
      <span class="keyword">let</span> <span class="variable-name">query</span> = el.querySelector(<span class="string">"input"</span>).value;
      <span class="keyword">let</span> <span class="variable-name">results</span> = sortResults(search(query));
      results = results.slice(0, MAX_DOCUMENTS);
      el.querySelector(<span class="string">".results"</span>).innerHTML = formatSearchResults(query, results);
    };
    el.querySelector(<span class="string">"input"</span>).addEventListener(<span class="string">'input'</span>, update);
    update();
}

setUpSearch(<span class="string">'search-4'</span>, (results) =&gt; results);
</pre>
</div>

<p>
I wanted to test the refactored code before implementing ranking. This shows documents in some arbitrary order, like before:
</p>

<figure id="search-4" class="search">
  <div class="results"/>
  <figcaption><label>Search engine 4: <input value="hexagon" placeholder="query"/></label></figcaption>
</figure>

<p>
Let's try ranking by the <em>number</em> of matches in the document:
</p>

<div class="org-src-container">
<pre class="src src-js">setUpSearch(<span class="string">'search-5'</span>, (results) =&gt; {
   <span class="keyword">function</span> <span class="function-name">score</span>({document, snippet}) {
     <span class="keyword">let</span> <span class="variable-name">numberOfMatches</span> = 0;
     <span class="keyword">for</span> (<span class="keyword">let</span> {matches} <span class="keyword">of</span> snippet) {
       numberOfMatches += matches.length;
     }
     <span class="keyword">return</span> numberOfMatches;
   }

   <span class="keyword">return</span> results.toSorted((a, b) =&gt; score(b) - score(a));
});
</pre>
</div>

<figure id="search-5" class="search">
  <div class="results"/>
  <figcaption><label>Search engine 5: <input value="hexagon" placeholder="query"/></label></figcaption>
</figure>

<p>
This is certainly <em>better</em>. The top 5 results before were four hexagon articles and then one about colors. The top 5 results now all have lots of hexagon content.
</p>

<p>
Let's try ranking header matches higher than other matches:
</p>

<div class="org-src-container">
<pre class="src src-js">setUpSearch(<span class="string">'search-6'</span>, (results) =&gt; {
   <span class="keyword">function</span> <span class="function-name">score</span>({document, snippet}) {
     <span class="keyword">let</span> <span class="variable-name">numberOfMatches</span> = 0;
     <span class="keyword">for</span> (<span class="keyword">let</span> {matches, isHeader} <span class="keyword">of</span> snippet) {
       numberOfMatches += matches.length;
       <span class="keyword">if</span> (isHeader) numberOfMatches += 10; <span class="comment-delimiter">// </span><span class="comment">needs tuning!
</span>     }
     <span class="keyword">return</span> numberOfMatches;
   }

   <span class="keyword">return</span> results.toSorted((a, b) =&gt; score(b) - score(a));
});
</pre>
</div>

<figure id="search-6" class="search">
  <div class="results"/>
  <figcaption><label>Search engine 6: <input value="hexagon" placeholder="query"/></label></figcaption>
</figure>

<p>
I think this is even better than before. But it's still mediocre. Should I prioritize matches that occur in a cluster (max matches per block) or matches that are spread out (number of matching blocks)? I don't know! It will take a lot of testing. <strong>Ranking takes a lot of work</strong>. There's <em>so much more</em> that can be done to make search better. Some ideas:
</p>

<ul class="org-ul">
<li>try out the other ranking ideas I listed at the top of the page</li>
<li>extract documents correctly; there are going to be lots of little details to get right (data scientists know that "data cleaning" is one of the most time consuming parts of a project)</li>
<li>identify pages that are just linking to other pages (for example, my blog home page), and prefer returning search results from the linked-to pages</li>
<li>identify pages that are older versions of other pages, and prefer returning the current pages</li>
<li>trim snippets at word boundaries centered at the query matches instead of picking the beginning of the line</li>
<li>split documents into blocks better (for example, &lt;dt&gt; and &lt;dd&gt; should go together, not separate blocks), or maybe get rid of the block idea entirely</li>
<li>try the query matching rules I mentioned earlier, to handle words, phrases, ordering, synonyms, plurals, etc.</li>
<li>set up a test suite of queries and hand-picked best results, so that I can evaluate the different ranking algorithms and tuning parameters</li>
<li>change ranking philosophy from the <em>set of best pages</em> to the <em>best set of pages</em></li>
<li>use word2vec or a vector db (from the LLM world) to implement fuzzy matching or group related pages together</li>
<li>generate a <em>good</em> snippet instead of the first N matches</li>
</ul>

<p>
Tangent: I think the description of search results is underrated. Early web search engines used a static description provided by the author of the page ("meta tag" description) or by a curator (Yahoo / DMoz directories), or generated a description per page. Google generated a description by scanning the document to find the query words, so that you could see the keywords in context of the document. Being able to see the matches of <em>your</em> query instead of a generic description made people feel that Google was actually paying attention to the query words. This is a surprisingly hard problem that I don't see much written about. (For many queries, I think this was more important than PageRank™)
</p>

<p>
Another question is how to scale this up. I decided not to here. <em>All the words on all my web pages put together</em> are only a few megabytes, smaller than the javascript alone on the NYTimes home page. I can load the entire web site and search over it. But if I had many more pages, I might want an <a href="https://en.wikipedia.org/wiki/Inverted_index">inverted index</a>, which can tell us which documents contain a word. See <a href="https://artem.krylysov.com/blog/2020/07/28/lets-build-a-full-text-search-engine/">Let's Build a Full Text Search Engine</a>. If I wanted to index pages written by other people, I might want ranking functions that were more resistant to spamming, such as using links between documents and the anchor text used to describe those links.
</p>

<p>
I'm not planning to implement these right now. I wanted to see how far I could get in a weekend. The search results I'm getting here are decent. If you want to play with it some more, I made a <a href="https://www.redblobgames.com/search/">standalone version</a> that uses the full page to show search results.
</p>

<p>
<strong>This was a fun project!</strong>
</p>

<style>
  /* for this page I need to more strongly distinguish inputs (kbd) from outputs (code) */
  kbd {
    padding-inline: 0.4em;
    background-color: hsl(120 15% 95%);
    color: #486;
    border: 0.5px solid #486;
    border-radius: 4px;
    white-space: nowrap;
  }
  code {
    padding-inline: 0.4em;
    background-color: hsl(60 15% 95%);
    border-radius: 4px;
    white-space: nowrap;
  }

  figure.search { 
    box-shadow: 0 1px 3px 3px rgb(0 0 0 / 0.5); 
    border: 1px solid oklab(0.5 0.0 -0.15);
    border-radius: 4px; 
    font-family: var(--sans-serif);
    input { 
      /* I want this style to be similar to kbd */
      font-family: var(--monospace); 
      font-size: 1rem;
      font-weight: bold;
      border-width: 0.5px;
      border-radius: 4px;
      background-color: hsl(120 15% 95%);
      border-color: #486;
      color: #486;
      accent-color: #486;
    }
    figcaption { 
      font-size: unset;
      background: oklab(0.9 0.0 -0.01); 
    }
    .results { 
      text-align: left;
      height: 12em;
      overflow: scroll;
      b { background: oklab(1.0 0.1 0.2); }
      a { 
        display: block; 
        padding-left: 0.5rem; 
        background: oklab(0.9 0.0 -0.01); 
        color: oklab(0.5 0.0 -0.15);
        text-decoration-color: oklab(0.5 0.0 -0.15);
      }
      .url {
        padding-inline: 2rem;
        font-family: var(--monospace);
        font-size: 50%;
        line-height: 1.5;
        color: oklab(0.5 -0.15 0.0);
      }
      .snippet {
        padding-inline: 2rem;
        max-height: 6em;
        overflow: clip;
        font-size: 75%; 
        line-height: 1.5;
      }
    }
  }

  /* allow code to be wider than the main column */
  section &gt; .org-src-container { 
    width: fit-content; 
    max-width: unset; 
    pre { 
      width: fit-content; 
      min-width: var(--body-width); 
      max-width: unset; 
      max-height: 20em;
      overflow-y: auto;
    }
  }
</style>

<x:head>
  <script type="module" src="https://www.redblobgames.com/blog/2025-08-30-lets-write-a-search-engine-2/_search-engine-2.js"/>
</x:head>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-08-30-lets-write-a-search-engine-2/"/>
    <published>2025-08-30T16:19:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-08-29-lets-write-a-search-engine-1</id>
    <title>Let's write a search engine, part 1 of 2</title>
    <updated>2026-03-23T15:06:24.909707-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
I have a search box on my web site that uses Google to search my site only using the <a href="https://developers.google.com/search/docs/monitor-debug/search-operators/all-search-site">site: operator</a>. Try it — type in <kbd>hexagon tiles</kbd> and you'll see that it shows my hexagon tile page.
</p>

<figure>
  <search>
    <form action="https://www.google.com/search" target="_blank" style="font-family: var(--sans-serif)">
      Search my site using Google: 
      <input type="search" name="q" value="hexagon tiles" placeholder="search query" size="16"/>
      <input type="hidden" name="hq" value="site:www.redblobgames.com OR site:theory.stanford.edu/~amitp/ OR site:www-cs-students.stanford.edu/~amitp/ OR site:amitp.blogspot.com OR site:simblob.blogspot.com"/>
    </form>
  </search>
</figure>

<p>
I wondered though: what would it take to make my own search feature that worked as you typed? This is what I'll implement:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-08-29-lets-write-a-search-engine-1/search-engine-3.png" alt=""/>
  <figcaption> Prototype of search feature</figcaption>
</figure>


<p>
I also wondered if there are other things I might want to build with this data, like <a href="https://simonwillison.net/2003/Apr/25/relatedArticles/">Simon Willison's "related pages" feature</a>. Or browsing by category. Or browsing by date.
</p>

<x:cut/>


<p>
The first thing I need to do is extract the text of the pages I want to search, without formatting or images or html. The page might contain this xhtml tree:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">p</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">em</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
    Graph search
  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">em</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
  algorithms let us find the shortest path on a map 
  represented as a graph. One popular algorithm is
  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">a</span> <span class="nxml-attribute-local-name">href</span>=<span class="string">"…"</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
    A*
  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">a</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
  .
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">p</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">p</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
  Let's start with
  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">a</span> <span class="nxml-attribute-local-name">href</span>=<span class="string">"…"</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
    Breadth First Search
  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">a</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
  and work our way up from there.
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">p</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
For searching, I'd like to simplify this into a set of lines, each representing one "block" of text:
</p>

<pre class="example" id="org378ddbb">
Graph search algorithms let us find the shortest path on a map represented as a graph. One popular algorithm is A*.
Let's start with Breadth First Search and work our way up from there.
</pre>

<p>
I can then run a tool like <code>grep</code> on these lines. Will this produce good search results? <em>I don't know!</em> I need to try it and see.
</p>

<p>
I started thinking about the extraction algorithm from xhtml to text. Which tags should I consider "block"? I started looking at <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements#content_sectioning">MDN</a> for a list. I also wondered what to do about situations like this:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">div</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
  This is a block of text
  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">div</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
    inside another
  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">div</span><span class="nxml-tag-delimiter">&gt;</span><span class="nxml-text">
  block of text
</span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">div</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
Should it be
</p>

<pre class="example" id="org0a88d6a">
This is a block of text inside another block of text
</pre>

<p>
or
</p>

<pre class="example" id="org872a2b3">
This is a block of text
inside another
block of text
</pre>

<p>
But I realized that trying to figure this out is <em>a distraction</em>. Maybe it'll matter. Maybe it won't. But I don't know yet, and I shouldn't <em>prematurely</em> try to solve this problem. Instead, the most important thing is to <strong>investigate the biggest unknown</strong>. The biggest unknown in this case: is searching over lines even useful?
</p>

<p>
So I instead tried this (using the xhtml input file, which is the page without the header, nav bar, footer, copyright, etc.):
</p>

<div class="org-src-container">
<pre class="src src-sh">cat pathfinding/a-star/introduction.bxml <span class="sh-escaped-newline">\</span>
    | pandoc --read html --write plain --wrap=none <span class="sh-escaped-newline">\</span>
    | less
</pre>
</div>

<p>
I looked at the output and decided <em>this is a reasonable starting point</em>! It gives me the text of the page. I can refine it later.
</p>

<p>
The next step is to collect the text from all my pages:
</p>

<div class="org-src-container">
<pre class="src src-sh">fd --extension bxml | <span class="keyword">while </span><span class="builtin">read</span> -r file; <span class="keyword">do</span>
    cat <span class="string">"$file"</span> | pandoc --read html --write plain --wrap none
<span class="keyword">done</span> &gt;all-pages.txt
</pre>
</div>

<p>
That's only 1.6MB. That could <em>easily</em> be loaded into a web browser. There are many web pages bigger than that!
</p>

<p>
I made a few changes to my shell script:
</p>

<ol class="org-ol">
<li>Added a separator with the filename</li>
<li>Skipped lines that don't have text (pandoc outputs some blank lines)</li>
</ol>

<div class="org-src-container">
<pre class="src src-sh">fd --extension bxml | <span class="keyword">while </span><span class="builtin">read</span> -r file; <span class="keyword">do</span>
    <span class="builtin">echo</span> <span class="string">"%%%% $file %%%%"</span>;
    cat <span class="string">"$file"</span> <span class="sh-escaped-newline">\</span>
        | pandoc --read html --write plain --wrap none <span class="sh-escaped-newline">\</span>
        | grep <span class="string">'[a-zA-Z0-9]'</span>
<span class="keyword">done</span> &gt;all-pages.txt
</pre>
</div>

<p>
Great! What's next? Let me load this into a web page:
</p>

<div class="org-src-container">
<pre class="src src-js"><span class="keyword">const</span> <span class="variable-name">words</span> = <span class="keyword">await</span> (<span class="keyword">await</span> fetch(<span class="string">"./all-pages.txt"</span>)).text();
<span class="keyword">const</span> <span class="variable-name">lines</span> = words.split(<span class="string">"\n"</span>);
</pre>
</div>

<p>
Now I can search over these lines. 
</p>

<div class="org-src-container">
<pre class="src src-js"><span class="keyword">function</span> <span class="function-name">search1</span>(<span class="variable-name">event</span>) {
    <span class="keyword">let</span> <span class="variable-name">results</span> = [];
    <span class="keyword">let</span> <span class="variable-name">query</span> = event.currentTarget.value;
    <span class="keyword">if</span> (query) {
        <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">line</span> <span class="keyword">of</span> lines) {
            <span class="keyword">if</span> (line.indexOf(query) &gt;= 0) results.push(line);
            <span class="keyword">if</span> (results.length &gt; 10) <span class="keyword">break</span>;
        }
    }
    document.querySelector(<span class="string">"#search-results-1"</span>).textContent = results.join(<span class="string">"\n"</span>);
}
document.querySelector(<span class="string">"#search-query-1"</span>).addEventListener(<span class="string">'input'</span>, search1);
</pre>
</div>

<p>
<strong>Type <kbd>hexagon</kbd> into this search box:</strong>
</p>

<figure class="search">
  <pre id="search-results-1" style="height: 8em"/>
  <figcaption><label>Search engine 1: <input id="search-query-1" placeholder="filter"/></label></figcaption>
</figure>

<p>
Ok, great, this works <em>and</em> is fast! I don't have to worry about building an inverted index or other optimizations. But it's hard to see the matches. Let's add:
</p>

<ol class="org-ol">
<li>highlight the matches</li>
<li>trim extra whitespace from the document</li>
<li>case insensitive match</li>
</ol>

<div class="org-src-container">
<pre class="src src-js"><span class="keyword">function</span> <span class="function-name">escapeHTML</span>(<span class="variable-name">text</span>) { <span class="comment-delimiter">// </span><span class="comment">wish this was built in
</span>    <span class="keyword">const</span> <span class="variable-name">map</span> = {
        <span class="string">'&amp;'</span>: <span class="string">'&amp;amp;'</span>,
        <span class="string">'&lt;'</span>: <span class="string">'&amp;lt;'</span>,
        <span class="string">'&gt;'</span>: <span class="string">'&amp;gt;'</span>,
        <span class="string">'"'</span>: <span class="string">'&amp;quot;'</span>,
        <span class="string">"'"</span>: <span class="string">'&amp;#39;'</span>
    };
    <span class="keyword">return</span> text.replace(<span class="string">/[&amp;&lt;&gt;"']/</span>g, m =&gt; map[m]);
}

<span class="keyword">function</span> <span class="function-name">search2</span>(<span class="variable-name">event</span>) {
    <span class="keyword">let</span> <span class="variable-name">results</span> = [];
    <span class="keyword">let</span> <span class="variable-name">query</span> = event.currentTarget.value;
    <span class="keyword">if</span> (query) {
        query = <span class="keyword">new</span> <span class="type">RegExp</span>(RegExp.escape(query), <span class="string">'gi'</span>);
        <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">line</span> <span class="keyword">of</span> lines.map(line =&gt; line.trim())) {
            <span class="keyword">let</span> <span class="variable-name">matches</span> = line.matchAll(query).toArray();
            <span class="keyword">if</span> (matches.length &gt; 0) {
                <span class="keyword">let</span> <span class="variable-name">result</span> = <span class="string">""</span>;
                <span class="keyword">let</span> <span class="variable-name">pos</span> = 0;
                <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">m</span> <span class="keyword">of</span> matches) {
                    result += escapeHTML(line.slice(pos, m.index)) + <span class="string">"&lt;b&gt;"</span> + m[0] + <span class="string">"&lt;/b&gt;"</span>;
                    pos = m.index + m[0].length;
                }
                result += escapeHTML(line.slice(pos));
                results.push(result);
            }
            <span class="keyword">if</span> (results.length &gt; 10) <span class="keyword">break</span>;
        }
    }
    document.querySelector(<span class="string">"#search-results-2"</span>).innerHTML = results.join(<span class="string">"\n"</span>);
}
document.querySelector(<span class="string">"#search-query-2"</span>).addEventListener(<span class="string">'input'</span>, search2);
</pre>
</div>

<p>
<strong>Type <kbd>hexagon</kbd> into this search box:</strong>
</p>

<figure class="search">
  <pre id="search-results-2" style="height: 10em"/>
  <figcaption><label>Search engine 2: <input id="search-query-2" placeholder="filter"/></label></figcaption>
</figure>

<p>
Hey, that's looking pretty good. We're matching <em>lines</em>. 
</p>

<p>
But I want to match <em>documents</em>. I had inserted document separators <kbd>%%%%</kbd> into the text file. Let's use that now. 
</p>

<div class="org-src-container">
<pre class="src src-js"><span class="keyword">function</span> <span class="function-name">parseIntoDocuments</span>(<span class="variable-name">text</span>) {
    <span class="comment-delimiter">// </span><span class="comment">results[0] will be "", then [1] is a filename, [2] is text, alternating
</span>    <span class="keyword">const</span> <span class="variable-name">results</span> = text.split(<span class="string">/%%%% (.*?) %%%%$/</span>gm); 
    <span class="keyword">if</span> (results[0] !== <span class="string">""</span>) <span class="keyword">throw</span> <span class="string">`expected first split to be empty ${results[0]}`</span>;
    <span class="keyword">if</span> (results[1].length &gt; 100) <span class="string">"expected second split to be filename"</span>;
    <span class="keyword">let</span> <span class="variable-name">documents</span> = [];
    <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">i</span> = 1; i+1 &lt; results.length; i += 2) {
        <span class="keyword">let</span> <span class="variable-name">filename</span> = results[i];
        <span class="keyword">let</span> <span class="variable-name">lines</span> = results[i+1].split(<span class="string">"\n"</span>);
        lines = lines.map(line =&gt; line.trim()); <span class="comment-delimiter">// </span><span class="comment">remove extra whitespace
</span>        lines = lines.filter(line =&gt; line); <span class="comment-delimiter">// </span><span class="comment">remove blank lines
</span>        documents.push({filename, lines});
    }
    <span class="keyword">return</span> documents;
}
<span class="keyword">const</span> <span class="variable-name">documents</span> = parseIntoDocuments(words);

<span class="keyword">function</span> <span class="function-name">search3</span>(<span class="variable-name">event</span>) {
    <span class="keyword">const</span> <span class="variable-name">MAX_DOCUMENTS</span> = 5;
    <span class="keyword">const</span> <span class="variable-name">SNIPPET_LENGTH</span> = 3;
    <span class="keyword">let</span> <span class="variable-name">results</span> = []; <span class="comment-delimiter">// </span><span class="comment">list of documents
</span>    <span class="keyword">let</span> <span class="variable-name">query</span> = event.currentTarget.value;
    <span class="keyword">if</span> (query) {
        query = <span class="keyword">new</span> <span class="type">RegExp</span>(RegExp.escape(query), <span class="string">'gi'</span>);
        <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">document</span> <span class="keyword">of</span> documents) {
            <span class="keyword">let</span> <span class="variable-name">snippet</span> = []; <span class="comment-delimiter">// </span><span class="comment">list of lines
</span>            <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">line</span> <span class="keyword">of</span> document.lines) {
                <span class="keyword">let</span> <span class="variable-name">matches</span> = line.matchAll(query).toArray();
                <span class="keyword">if</span> (matches.length &gt; 0) {
                    <span class="keyword">let</span> <span class="variable-name">result</span> = <span class="string">" … "</span>;
                    <span class="keyword">let</span> <span class="variable-name">pos</span> = 0;
                    <span class="keyword">for</span> (<span class="keyword">let</span> <span class="variable-name">m</span> <span class="keyword">of</span> matches) {
                        result += escapeHTML(line.slice(pos, m.index)) + <span class="string">`&lt;b&gt;${m[0]}&lt;/b&gt;`</span>;
                        pos = m.index + m[0].length;
                    }
                    result += escapeHTML(line.slice(pos));
                    snippet.push(result);
                }
                <span class="keyword">if</span> (snippet.length &gt; SNIPPET_LENGTH) <span class="keyword">break</span>;
            }
            <span class="keyword">if</span> (snippet.length &gt; 0) results.push({document, snippet});
            <span class="keyword">if</span> (results.length &gt; MAX_DOCUMENTS) <span class="keyword">break</span>;
        }
    }
    <span class="keyword">let</span> <span class="variable-name">html</span> = results
         .map(({document, snippet}) =&gt; <span class="string">`&lt;i&gt;[${document.filename}]&lt;/i&gt;\n&lt;small&gt;${snippet} …&lt;/small&gt;`</span>)
         .join(<span class="string">"\n"</span>);
    document.querySelector(<span class="string">"#search-results-3"</span>).innerHTML = html;
}
document.querySelector(<span class="string">"#search-query-3"</span>).addEventListener(<span class="string">'input'</span>, search3);
</pre>
</div>

<p>
<strong>Type <kbd>hexagon</kbd> into this search box:</strong>
</p>

<figure class="search">
  <pre id="search-results-3" style="height: 10em; white-space: pre-line"/>
  <figcaption><label>Search engine 3: <input id="search-query-3" placeholder="filter"/></label></figcaption>
</figure>

<p>
Great! We're now grouping matches into documents, and limiting how many show up.
</p>

<p>
But we're showing the first N matching documents. This is like <kbd>Ctrl</kbd> + <kbd>F</kbd> over all documents. A "search engine" should show the <em>best</em> documents instead of the first in some arbitrary order. Let's attempt that in the <a href="https://www.redblobgames.com/blog/2025-08-30-lets-write-a-search-engine-2/">next post</a>.
</p>

<style>
  /* for this page I need to more strongly distinguish inputs (kbd) from outputs (code) */
  kbd {
    padding-inline: 0.4em;
    background-color: hsl(120 15% 95%);
    color: #486;
    border: 0.5px solid #486;
    border-radius: 4px;
    white-space: nowrap;
  }
  code {
    padding-inline: 0.4em;
    background-color: hsl(60 15% 95%);
    border-radius: 4px;
    white-space: nowrap;
  }
  figure.search { 
    box-shadow: 0 1px 3px 3px rgb(0 0 0 / 0.5); 
    border: 1px solid oklab(0.8 0.01 0.02);
    border-radius: 4px; 
    font-family: var(--sans-serif);
    input { 
      /* I want this style to be similar to kbd */
      font-family: var(--monospace); 
      font-size: 1rem;
      font-weight: bold;
      border-width: 0.5px;
      border-radius: 4px;
      background-color: hsl(120 15% 95%);
      border-color: #486;
      color: #486;
      accent-color: #486;
    }
    figcaption { 
      font-size: unset;
      background: oklab(0.85 0.01 0.02); 
    }
  }
  pre b { background: oklab(1.0 0.1 0.2); }
  pre i { background: oklab(1.0 -0.1 -0.2); }

  /* allow code to be wider than the main column */
  section &gt; .org-src-container { 
    width: fit-content; 
    max-width: unset; 
    pre { 
      width: fit-content; 
      min-width: var(--body-width); 
      max-width: unset; 
      max-height: 20em;
      overflow-y: auto;
    }
  }
</style>

<x:head>
  <script type="module" src="https://www.redblobgames.com/blog/2025-08-29-lets-write-a-search-engine-1/_search-engine-1.js"/>
</x:head>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-08-29-lets-write-a-search-engine-1/"/>
    <published>2025-08-29T13:59:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-05-28-hexagon-conversions</id>
    <title>Hexagon conversions</title>
    <updated>2026-03-23T15:06:24.907154-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
My <a href="https://www.redblobgames.com/grids/hexagons/">hexagon guide</a> has many conversion routines — axial to cube, cube to offset, hex to pixel, etc. Sometimes these steps can be combined into larger steps or separated into smaller steps. There's a balancing act between:
</p>

<ol class="org-ol">
<li>give the reader the final answer</li>
<li>give the reader the ingredients so they can adapt them as needed</li>
</ol>

<p>
My original goal was to provide code for these 22 conversions:
</p>


<figure id="org71e0271">
<img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/conversions-all.png" alt="conversions-all.png"/>

</figure>

<x:cut/>


<p>
However, I didn't know all the conversion formulas, and ended up providing only these 16:
</p>


<figure id="org090cbe1">
<img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/conversions-initial.png" alt="conversions-initial.png"/>

</figure>

<p>
So that means if you want to go in reverse from <code>pixel</code> to <code>odd_r</code>, you build the chain <code>pixel</code> → <code>axial</code> → <code>cube</code> → <code>odd_r</code>. Not great.
</p>

<p>
Things got more complicated when I added <code>doubled_h</code>, <code>doubled_v</code>. And I want to add several spiral systems:
</p>


<figure id="org7622a18">
<img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/conversions-more.png" alt="conversions-more.png"/>

</figure>

<p>
Last week I wanted to add conversions for non-uniform pixel sizes. That was adding far more edges to this graph. I simplified by splitting the pixel conversion into multiple steps:
</p>


<figure id="orgf6c2de6">
<img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/conversions-steps.png" alt="conversions-steps.png"/>

</figure>

<p>
The problem is that … I think most readers want a formula that solves their problem. Breaking things into steps makes it easier for me, and I think it's better for understanding what's going on, but it's less convenient for the reader. I took the single step conversion from axial to pixel:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/coordinate-conversion-inlined-steps-1.png" alt=""/>
  <figcaption> Before: single step axial to pixel</figcaption>
</figure>


<p>
And split it into axial to cartesian and then scaling the cartesian:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/coordinate-conversion-separate-steps-1.png" alt=""/>
  <figcaption> After: separate scaling step</figcaption>
</figure>


<p>
Why? Because it allows scaling <em>non-uniformly</em>, to match a desired pixel art size:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/coordinate-conversion-separate-steps-2.png" alt=""/>
  <figcaption> Non-uniform scaling</figcaption>
</figure>


<p>
If we inline the calculations we end up with this:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/coordinate-conversion-inlined-steps-2.png" alt=""/>
  <figcaption> Non-uniform scaling</figcaption>
</figure>


<p>
It's nice. There's no more <code>sqrt(3)</code> there!
</p>

<p>
By keeping the steps separate, it allows for adapting to more situations:
</p>

<ul class="org-ul">
<li><em>translate</em> transform to get a non-zero origin</li>
<li><em>scale</em> transform to get non-regular hexagon sizes</li>
<li><em>rotation</em> transform to get angles other than "pointy" and "flat"</li>
<li><em>isometric</em> view combined a shear and rotation operation</li>
</ul>

<p>
I think it's easier to understand inverting the process from hex-to-pixel to pixel-to-hex when the steps are separate. I have mixed feelings about this change but I made it in part because I wanted to show how to adjust the conversions to match the size of art assets.
</p>

<p>
You can see the new code in the <a href="https://www.redblobgames.com/grids/hexagons/#hex-to-pixel">hex-to-pixel</a> and <a href="https://www.redblobgames.com/grids/hexagons/#pixel-to-hex">pixel-to-hex</a> sections of the hexagons guide. I've added a section where you can enter the pixel art asset size, and I output the conversion routine. Maybe I can extend that interactive code generator to work for all the coordinate systems. Let me know what you think. I might change it back based on feedback. 
</p>

<p>
While working on this section, I realized I want to add more direct support for doubled coordinates. It probably makes more sense to go from offset coordinates to doubled to pixel than offset to cube to axial to pixel. But that will wait for another day. [Update: that day was in mid-June.]
</p>

<p>
I also realized that the main page has pixel-to-hex that returns a <em>rounded</em> value, but the implementation guide has a pixel-to-hex that returns a <em>fractional</em> value, and you have to round it yourself. That's because the fractional value is sometimes useful. I updated the implementation guide to provide both.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-05-28-hexagon-conversions/"/>
    <published>2025-05-28T15:27:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-05-08-mapgen4-trade-routes</id>
    <title>Mapgen4 trade routes</title>
    <updated>2026-03-23T15:06:24.904939-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
Last time <a href="https://www.redblobgames.com/blog/2025-04-22-de-optimizing-mapgen4/">I talked about de-optimizing mapgen4</a> so that I could more easily create experiments. I wanted to share one of those experiments. I recently assembled some of my existing ingredients into something that was just plain fun to watch, and I wanted to share this simulation of traders moving around a map:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/top.png" alt=""/>
  <figcaption> Trader simulation</figcaption>
</figure>


<x:cut/>


<p>
Conceptually, each dot on this simulation represents a trader following a path between two random points. The first ingredient is a <a href="https://www.redblobgames.com/pathfinding/a-star/introduction.html">pathfinding algorithm</a> such as A*. Pathfinding works on a graph. So the second ingredient is a graph:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/region-graph.png" alt=""/>
  <figcaption> Graph of regions</figcaption>
</figure>


<p>
I'm using a <a href="https://www.redblobgames.com/x/2312-dual-mesh/">Delaunay + Voronoi dual graph</a>. Here's a random path on this graph:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/region-graph-path.png" alt=""/>
  <figcaption> Path on the region graph</figcaption>
</figure>


<p>
Each trader follows a path. But I decided it'd be more interesting to animate the points on a graph of <em>edges</em> instead of the <em>regions</em>. Here's a graph between the edges of the original map. Each node in this pathfinding graph is an edge in the original graph, and each edge in this pathfinding graph is between two edges in the original graph:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/edge-graph.png" alt=""/>
  <figcaption> Graph of edges</figcaption>
</figure>


<p>
That's quite a bit more dense!
</p>

<p>
Here's the path on the new graph — it looks smoother:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/edge-graph-path.png" alt=""/>
  <figcaption> Path on the edge graph</figcaption>
</figure>


<p>
The choice if graph is something to keep in mind. There may be many choices of <em>pathfinding graph</em> for a given map. I usually start by making the pathfinding graph the same as the underlying game map, but there are many more choices, and lots of tradeoffs to consider. In this case I chose the graph between edges because it let me create multiple "lanes" of traffic. I separated the traffic going one direction from the traffic going the other direction. That made the visuals nicer.
</p>

<p>
I can run pathfinding on a graph, but I actually want to create <em>lots</em> of paths. A* finds one path at a time. There are algorithms that can find multiple paths at once. This is another thing to keep in mind. Sometimes we're so focused on a <em>small</em> part of the problem that we miss out on on simpler or faster solutions that solve the <em>larger</em> problem. There are several classes of pathfinding algorithms:
</p>

<figure>
  <table class="standard">
    <thead>
      <tr><th colspan="2">Problems</th><th colspan="2">Solutions</th></tr>
      <tr><th>source</th><th>destination</th><th>algorithm class</th><th>algorithms</th></tr>
    </thead>
    <tbody>
      <tr>
        <td>one</td><td>one</td>
        <td><a href="https://en.wikipedia.org/wiki/Shortest_path_problem">single pair shortest path</a></td>
        <td>A*</td>
      </tr>
      <tr>
        <td>one<br/>all</td><td>all<br/>one</td>
        <td><a href="https://en.wikipedia.org/wiki/Shortest_path_problem#Single-source_shortest_paths">single source shortest paths</a></td>
        <td>Dijkstra's, Bellman-Ford</td>
      </tr>
      <tr>
        <td>all</td><td>all</td>
        <td><a href="https://en.wikipedia.org/wiki/Shortest_path_problem#All-pairs_shortest_paths">all pairs shortest path</a></td>
        <td>Floyd-Warshall, Johnson's</td>
      </tr>
    </tbody>
  </table>
</figure>

<p>
It could be useful to precompute <em>all</em> the paths here, using Floyd-Warshall. Then all the animated points would move along the already-computed paths. In my <a href="https://www.redblobgames.com/pathfinding/all-pairs/">previous experiments</a> I found that Johnson's Algorithm was faster than Floyd-Warshall. Johnson's Algorithm runs Dijkstra's Algorithm for each source location and then stores all the outputs.
</p>

<div class="org-src-container">
<pre class="src src-nil">for each location:
  run dijkstra's from source location

create lots of traders:
  follow a path between a random source and a random destination
</pre>
</div>

<p>
Because the map is changing as the user paints on it, I wanted to avoid an expensive precomputation step. I decided to implement something slightly different. Instead of running Dijkstra's from <em>all</em> locations, then creating <em>all</em> the traders, I ran Dijkstra to <em>one</em> location, created the traders from <em>that</em> location, then ran Dijkstra to another location, created the traders there, etc.:
</p>

<div class="org-src-container">
<pre class="src src-nil">for each location:
  run dijkstra's from source location
  create lots of traders:
    follow a path between the known source and a random destination
</pre>
</div>

<p>
I run Dijkstra's Algorithm to build <a href="https://www.redblobgames.com/pathfinding/tower-defense/">flow fields</a> for pathfinding :
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/region-graph-flow-field.png" alt=""/>
  <figcaption> Flow field</figcaption>
</figure>


<p>
It was fun putting together ingredients into something new:
</p>

<ol class="org-ol">
<li>Flow field pathfinding - Dijsktra's Algorithm</li>
<li>Voronoi/Delaunay graphs</li>
<li>Mapgen2's renderer</li>
<li>Mapgen4's generator + painting</li>
</ol>

<p>
<strong>Try out <a href="https://www.redblobgames.com/x/2515-mapgen-trading/">experiment 2515</a></strong> — simulation of traders in an editable procedurally generated map.
</p>


</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-05-08-mapgen4-trade-routes/"/>
    <published>2025-05-08T09:43:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-04-22-de-optimizing-mapgen4</id>
    <title>De-optimizing mapgen4</title>
    <updated>2026-03-23T15:06:24.901093-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
Lately I've been experimenting with map algorithms. I have three starting points:
</p>

<table class="standard">


<colgroup>
<col class="org-left"/>

<col class="org-left"/>

<col class="org-left"/>

<col class="org-left"/>

<col class="org-left"/>
</colgroup>
<thead>
<tr>
<th scope="col" class="text-left">Generator</th>
<th scope="col" class="text-left">Elevation</th>
<th scope="col" class="text-left">Biomes</th>
<th scope="col" class="text-left">Rivers</th>
<th scope="col" class="text-left">Editable?</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-left"><a href="https://www.redblobgames.com/maps/terrain-from-noise/">mapgen1</a></td>
<td class="text-left">noise</td>
<td class="text-left">noise</td>
<td class="text-left">down</td>
<td class="text-left">no</td>
</tr>

<tr>
<td class="text-left"><a href="https://www.redblobgames.com/maps/mapgen2/">mapgen2</a></td>
<td class="text-left">distance field</td>
<td class="text-left">distance field</td>
<td class="text-left">down</td>
<td class="text-left">no</td>
</tr>

<tr>
<td class="text-left"><a href="https://www.redblobgames.com/maps/mapgen4/">mapgen4</a></td>
<td class="text-left">noise</td>
<td class="text-left">simulation</td>
<td class="text-left">up</td>
<td class="text-left">yes</td>
</tr>
</tbody>
</table>

<p>
(What happened to mapgen3? It was a failure. I had tried to change the programming language, data structures, <em>and</em> algorithms at the same time. I had better luck changing one thing at a time.)
</p>

<p>
I'd prefer to use mapgen4 for my experiments. But it's hard.
</p>

<x:cut/>


<p>
Mapgen1 and mapgen2 were non-interactive. They had to run at a reasonable speed, but just once. My goal was to have it take less than 1 second. In mapgen4 I wanted <em>every</em> edit to re-run the entire simulation from scratch. I re-calculated the elevation, evaporation, wind, rainfall, biomes, and river flow, and then render the whole thing, ideally in 30ms. But the simulation was far more expensive than what I was using in mapgen1 and mapgen2. It took <em>30 seconds</em>.
</p>

<p>
I  needed to speed it up by a factor of 1000.
</p>

<p>
It took a few months in 2018, but I did it. I'm proud of that. 
</p>

<p>
To make the code run faster, I:
</p>

<ol class="org-ol">
<li>Precomputed as much as I could (triangle mesh, polygon vertices, simplex noise, mountain positions, …)</li>
<li>Made assumptions about what algorithms ran, in what order, and what data would not change.</li>
<li>Rendered everything on the GPU instead of on the CPU. Precomputed textures encoding data.</li>
<li>Used multiple threads with message passing (no shared memory or locking back then).</li>
<li>Reduced the resolution of some of the data so that I would have less data to process.</li>
<li>Cached results of some algorithms.</li>
<li>Used fixed-sized arrays of numbers instead of variable-sized arrays of objects.</li>
</ol>

<p>
But all of this made the code hard to work with.  I had optimized it to do <em>only</em> the things I needed in mapgen4. It's hard to change anything, especially at run time, with a slider. And I grew to hate working with that code.
</p>

<p>
Back in 2019 <a href="https://twitter.com/redblobgames/status/1362852520096198656">I posted to twitter</a>:
</p>

<blockquote style="padding-inline:2em;font-size:90%;line-height:1.5;border-block:1px solid #ccc;">
<p>
When I'm working with simple but inefficient code, I can move from design A to B to C easily. When I optimize the code, I'm often pushing myself into a local minima. It's not only harder to change designs, I often can't even <strong>see</strong> the other designs anymore. Mapgen4 is stuck.
</p>

<p>
<img style="display:block;max-width:90%" src="https://www.redblobgames.com/x/screenshots/mapgen4-optimization-vs-flexibility.png" alt="Optimization vs flexibility"/>
</p>

<p>
This also happens to me with abstractions. When I use abstractions early, I push myself into local minima. Harder to change, also harder to see other possibilities. Last year I had to de-abstract the code on my A* page before I could make progress <a href="https://simblob.blogspot.com/2020/04/graph-search-diagrams-and-reusable-code.html">https://simblob.blogspot.com/2020/04/graph-search-diagrams-and-reusable-code.html</a>
</p>

<p>
The next time I'm working on a map generation project, it will be hard to start from mapgen4 because it's so optimized into its niche. I hate the code. I'm going to have to de-optimize it first before I can move around in generator design space. Then re-optimize it at the end.
</p>
</blockquote>

<p>
It's been many years now, and I'm ready to revisit the code. I wanted to experiment with some map algorithms, and tried using mapgen4 as the base.  But it was a pain. Drawing a bezier curve or text in webgl is so much more work than drawing it in 2D Canvas. So I thought: what if I drew to a 2D Canvas, and then "draped" that over the mapgen4 map? <a href="https://www.redblobgames.com/x/2502-mapgen4-overlay/">I tried that back in January</a>. It worked. Mostly. I don't have the map data in the main thread; it's only in the worker thread. So I drew to the 2D Canvas in the worker thread, using <code>OffscreenCanvas</code>, a feature that wasn't available back in 2017 when I started mapgen4, but is available as of 2023. It was a little awkward. And sometimes glitchy. And inconsistent across browsers. It's good enough for my own experiments but not as good for things I want to share.
</p>

<p>
So when I wanted to try an actual experiment, I went back to mapgen2. It's reliable. I tried <a href="https://www.redblobgames.com/x/2503-mapgen2-towns/">an experiment to build towns and roads on the map</a>. It was so much easier to work with mapgen2 code than mapgen4 code. But it doesn't let me paint on the map. And the generation algorithm is <a href="http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/">designed for Realm of the Mad God</a>, not for other projects of mine. 
</p>

<p>
So I decided to start <strong>de-optimizing mapgen4</strong>. Mapgen2 and mapgen4 share some "DNA". They're both built on the same underlying data structures and libraries, but they use different map generation and rendering code. So I tried gluing the mapgen2 renderer onto the mapgen4 generator, and … it worked! For now, I left in the multithreaded part of mapgen4, but I removed all the webgl rendering code and replaced it with mapgen2's software renderer. There's plenty more de-optimization I could do, but that'll be another day.
</p>

<p>
So try it out! It's a <a href="https://www.redblobgames.com/x/2513-mapgen4-alternate-ui/">mapgen2 + mapgen4 hybrid</a>.  You can draw on the map like mapgen4 but it will draw in the style of mapgen2. It's not optimized to the extent mapgen4 was, but I think it'll be a good place for me to try some map experiments.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-04-22-de-optimizing-mapgen4/"/>
    <published>2025-04-22T14:20:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-03-12-hexagon-spiral-coordinates</id>
    <title>Hexagon spiral coordinates</title>
    <updated>2026-03-23T15:06:24.898831-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
My <a href="https://www.redblobgames.com/grids/hexagons/">guide to hexagonal grids</a> is one of the most popular pages on my site. <a href="https://redblobgames.notion.site/Hexagonal-Grids-7d2d4d624bc5483dafbe615d75ab3902">I keep a list of things I want to add to that page</a>. One of them has been <em>spiral coordinate systems</em>. I had thought I would wait until I actually use them in a real project, so that I would have real world experience with the thing I'm writing about. I'm afraid of writing about things I'm unsure about, or information that's incomplete. But I haven't used them yet.
</p>

<p>
I decided to stop waiting.
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-03-12-hexagon-spiral-coordinates/2025-hexagon-spiral-coordinates.png" alt=""/>
  <figcaption> Spiral coordinates on a hexagonal grid</figcaption>
</figure>


<x:cut/>


<p>
I decided to make some diagrams showing the parts I do understand. This also led me to try to understand more. I gave some <em>unoptimized</em> sample code. Part of me wanted to wait until I have the best sample code to present. But … I've already been waiting for so many years. I decided to publish the unoptimized code for now.
</p>

<p>
There are also lots of variants I could have covered: 0-based vs 1-based, outside-in vs inside-out, ring-based vs path-based, uniform direction vs alternating direction, single spiral vs recursive spirals, and probably more. But if I wait until I have figured out all of these, it will take even longer. So I published just one variant for now.
</p>

<p>
<strong><a href="https://www.redblobgames.com/grids/hexagons/#rings-spiral-coordinates">Take a look at the new section</a></strong>. Please let me know what you'd like to see changed or improved!
</p>

<p>
While working on this, I also added an accidental discovery: running <code>atan2(r,q)</code> on <em>axial coordinates</em> instead of <code>atan2(y,x)</code> on <em>cartesian coordinates</em> produces something angle-like that can be used for <em>sorting by angle</em>, while being slightly cheaper than actually calculating the angles:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-03-12-hexagon-spiral-coordinates/hexagon-pseudo-angles.png" alt=""/>
  <figcaption> Something angle-like that can be used for sorting by angle</figcaption>
</figure>


<p>
That's not a topic I wanted for the main page, so I put it on <a href="https://www.redblobgames.com/grids/hexagons/directions.html#angles">a separate page</a>.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-03-12-hexagon-spiral-coordinates/"/>
    <published>2025-03-12T11:16:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2025-02-02-thoughts-on-flash</id>
    <title>Thoughts on Flash</title>
    <updated>2026-03-23T15:06:24.893914-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
I know a lot of people hated the Flash Player web plugin, but I found it to be quite useful for my experiments. It gave me vector graphics in the browser so that I could share demos without asking people to download an executable from me. And it ran long before SVG / HTML5 was widely available in browsers. <a href="https://simblob.blogspot.com/2023/03/tank-control-experiments.html">I had been porting some of my old Flash code to Javascript</a>, but that takes time that I could be instead spending on new projects. So I'm glad to see that the <a href="https://ruffle.rs/">Ruffle Flash emulator</a> has made <a href="https://ruffle.rs/compatibility/avm2">so much progress on ActionScript 3</a>:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2025-02-02-thoughts-on-flash/ruffle-actionscript-3-compatibility.jpg" alt=""/>
  <figcaption> Ruffle support for ActionScript 3 <a href="https://ruffle.rs/compatibility/avm2/tree.svg">from their site</a></figcaption>
</figure>


<x:cut/>


<p>
Some of the things I resurrected:
</p>

<ul class="org-ul">
<li><a href="http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/demo.html">My map "polygon map generator" demo</a> that accompanied the <a href="http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/">article of the same name</a></li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/game-programming/mapgen-realm-of-the-mad-god/">Realm of the Mad God experiments</a> - mapgen1, mapgen2, dungeons</li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/_test/faces_oryx.html">Blinking faces using the Oryx tileset</a></li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/_test/level_editor.html">An old level editor for Realm of the Mad God</a>, which I later used <a href="https://old.reddit.com/r/RotMG/comments/50p70k/we_are_the_creators_of_rotmg_alex_rob_amit_ama/">in a reddit AMA</a> five years later</li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/_test/secret-sheep-level.html">"There is no secret sheep level"</a>, an experiment with isometric graphics and field of view, which led me to <a href="https://www.redblobgames.com/x/1942-isometric/">this experiment</a> seven years later</li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/game-programming/road-applet/roads.html">Two road drawing experiments</a>, one for intersections and one for bezier vs circular curves, which led me to write <a href="https://www.redblobgames.com/articles/curved-paths/">this article</a> nine years later</li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/game-programming/game-ideas/corridor/">Corridor map generation</a> for when you're inside the belly of a giant beast</li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/game-programming/game-ideas/radial-base/">Radial space station map generation</a></li>
<li><a href="http://www-cs-students.stanford.edu/~amitp/game-programming/game-ideas/spaceship-editor/">Spaceship editor</a> that figures out how to fly from your spaceship designs</li>
</ul>

<p>
Back when <a href="https://simblob.blogspot.com/2007/07/interactive-illustrations.html">I initially got interested in making interactive tutorials</a> (2007), HTML5 wasn't around. Java applets and Flash applets were the best choices to run in a web browser, and I found Java was the respectable but slow/clunky choice, whereas Flash was the fast/lightweight choice, but didn't get any respect. ActionScript 3 was a decent programming language. Think of it like TypeScript + JSX but <a href="https://auth0.com/blog/the-real-story-behind-es4/">ten years ahead of its time, and based on the ECMAscript standard</a>. It had type checking, classes, modules, etc. The Flash graphics system offered 2D vector graphics, 2D bitmap graphics, and 3D graphics, and ways to combine all three in a fine-grained way. That's something I can't easily do in HTML5.
</p>

<p>
Many of the interactive parts of my pages, including the ones about pathfinding, hexagons, and procedural map generation, have their origins in experiments I did in Flash. I was quite glad that Ruffle made some of these work again.
</p>

<p>
But while looking at the <a href="http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/">Polygon Map Generator article</a>, I realized I haven't updated it since 2010. It has lots of references to the ActionScript 3 source code. I think ActionScript was nice —  But Flash is dead, so nobody's using ActionScript anymore. I decided to remove specific references to ActionScript code, and instead point to either descriptions of the algorithms or  JavaScript/TypeScript code.
</p>

<p>
I also took the opportunity to update some of the text based on what I've learned since then. A big one is that the article was meant to describe <em>what I did</em> and not <em>what you should do</em>, but I didn't convey that well. I made specific decisions based on the game design, and those decisions may not be right for another project. In each section where I made such a decision, I added alternative decisions that I've used or seen in other projects.
</p>

<p>
I used to think of my pages as something I wrote <em>once</em> and then published. 
I'm trying instead to of think of them as <em>living documents</em> that I update as I find better ways of explaining things. 
Updating the Flash parts of my site led me to revisit and update some of my older pages.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2025-02-02-thoughts-on-flash/"/>
    <published>2025-02-02T14:28:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-12-25-what-i-did-in-2024</id>
    <title>What I did in 2024</title>
    <updated>2026-03-23T15:06:24.891581-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
It's time for my annual self review. <a href="https://simblob.blogspot.com/2023/12/what-i-did-in-2023.html">In last year's review</a> I said I wanted to improve my site:
</p>

<ol class="org-ol">
<li>fix broken links</li>
<li>organize with tags</li>
<li>improve search</li>
<li>post to my site instead of to social media</li>
<li>move project tracking to my own site</li>
</ol>

<p>
I didn't have any specific goals for writing articles or topics to learn. So what did I do? The biggest thing is that <em>I'm blogging more</em> than in recent years:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-25-what-i-did-in-2024/chart-number-of-blog-posts-per-year.png" alt=""/>
  <figcaption> 
     Number of blog posts per year
     <small>(plot from <a href="https://observablehq.com/@redblobgames/what-i-did-in-2024-blog-post">Observable Plot</a>)</small>
  </figcaption>
</figure>

<x:cut/>


<p>
<h3>Site management</h3>
</p>

<p>
Changes to Twitter and Reddit in recent years have made me think about how I share knowledge.
When I share something, I want it to be readable by everyone, forever. I don't want it to be readable only to "members" or "subscribers", like Quora or Medium. I had posted to some of these sites because they were open. But they're sometimes closed now, requiring a login to view what I posted.
</p>

<p>
My web site has been up for <strong>30 years</strong>. The <a href="https://en.wikipedia.org/wiki/Lindy_effect">Lindy Effect</a> suggests that what I post to my own site will last longer than what I post to Google+, FriendFeed, MySpace, Reddit, or Twitter. I don't expect Mastodon, Threads, or Bluesky to be up forever either.  The article <a href="https://howtomarketagame.com/2021/11/01/dont-build-your-castle-in-other-peoples-kingdoms/">Don't Build Your Castle in Other People's Kingdoms</a> recommends I focus on my own site. But while my own site is easy to post to, my blog hosted by Blogger is not.
</p>

<p>
I want to make blogging easier for me.
<a href="https://www.redblobgames.com/blog/2024-03-08-new-blog/">I looked at my options for blogging software</a>, and concluded that my web site <em>already</em> supports many of the things I need for a blog. 
<em>So I decided to write my own blogging software</em>. How hard could it be? Famous last words, right? It's foolish in the same way as "write a game, not a game engine".
</p>

<p>
But it actually went pretty well! I only had to support the features needed for my own blog, not for everyone's blogs. I didn't need it to scale. I could reuse the existing features I have built for my web site. There are still some features I want to add, but I think I got 80% of what I wanted in &lt;200 lines of Python.
</p>

<p>
I made it easier to post to my blog, and I posted a lot more this year than in the previous few years. I'm happy about this.
</p>

<p>
<h3>New pages</h3>
</p>

<p>
I sometimes pair a "theory" page with an "implementation" page. The <a href="https://www.redblobgames.com/pathfinding/a-star/introduction.html">A* theory page</a> describes the algorithms and the <a href="https://www.redblobgames.com/pathfinding/a-star/implementation.html">A* implementation page</a> describes how to implement them. The <a href="https://www.redblobgames.com/grids/hexagons/">Hexagons theory page</a> describes the math and algorithms and the <a href="https://www.redblobgames.com/grids/hexagons/implementation.html">Hexagons implementation page</a> describes how to implement them. 
</p>

<p>
Last year, <a href="https://simblob.blogspot.com/2023/02/making-of-draggable-objects.html">I studied mouse+touch drag events in the browser</a> and then wrote up <a href="https://www.redblobgames.com/making-of/draggable/">a theory page</a> with my recommendations for how to handle the browser events. I claimed that the way I structured the code led to a lot of flexibility in how to handle UI state. This year I made an <a href="https://www.redblobgames.com/making-of/draggable/examples.html">implementation page</a> with lots of runnable examples showing that flexibility. I show basic dragging, constraints, snapping, svg vs div vs canvas, handles, scrubbable numbers, drawing strokes, painting areas, sharing state, resizing, and Vue components. I show the code for each example, and also link to a runnable CodePen and JSFiddle.
</p>

<figure>
  <img src="https://www.redblobgames.com/making-of/draggable/build/diagram-state-and-event-handlers.svg" alt=""/>
  <figcaption> Concepts and implementation pages</figcaption>
</figure>


<p>
I'm very happy with that page, and I <a href="https://www.redblobgames.com/blog/2024-04-17-draggable-examples/">wrote a blog post about it</a>.
</p>

<p>
I also wanted to write a reference page about Bresenham's Line Drawing Algorithm. <strong>This page failed</strong>. I  had started in 2023 with an interactive page that lets you run different implementations of the algorithm, to see how they don't match up. But I realized this year that my <em>motivation</em> for writing that page was anger, not curiosity. My goal was to show that all the implementations were a mess.
</p>

<p>
But anger isn't a good motivator for me. I don't end up with a good result..
</p>

<p>
I put the project on hold to let my anger dissipate. Then I started over, wanting to learn it out of curiosity. I re-read the original paper. I read lots of implementations. I took out my interactive visualizations of brokenness. I changed my focus to the properties I might want in a line drawing algorithm.
</p>

<p>
But I lost motivation again. I asked myself: <em>why am I doing this?</em> and I didn't have a good answer. There are <a href="https://redblobgames.notion.site/f8bc2f44fba94607afa9c06711d23245?v=0766432cb1534ce582ce35b33cbbef7e">so many things I want to explore</a>, and this topic doesn't seem feel like it's that interesting in the grand scheme of things. So I put it on hold again.
</p>

<p>
<h3>Updates to pages</h3>
</p>

<p>
I treat my main site like a personal wiki. I publish new pages and also improve old pages. I treat my blog differently. I post new pages, but almost never update the existing posts. This year on the main site I made many small updates:
</p>

<ul class="org-ul">
<li>Wrote up <a href="https://www.redblobgames.com/blog/2024-04-27-flow-field-pathfinding/">what I currently understand about "flow field" pathfinding</a></li>
<li>Rewrote parts of <a href="https://www.redblobgames.com/blog/2024-05-05-wip-heuristics/">a page about differential heuristics</a>, but still quite unhappy and thinking about more rewrites</li>
<li><a href="https://www.redblobgames.com/blog/2024-12-16-hexagon-page-animations/">Simplified the implementation of animations</a> in the hexagon guide, when switching from pointy-top to flat-top and back</li>
<li>Added <a href="https://www.redblobgames.com/x/2218-mapgen4-animated/">more animation modes to my animated mapgen4</a>. This is a fun page you can just stare at for a while.</li>
<li>Fixed a long-standing bug in A* diagrams - a reader alerted me to mouse positions not quite lining up with tiles, and I discovered that functions like <code>getBoundingClientRect()</code> include the border and padding of an element.</li>
<li>Added a demo of <a href="https://www.redblobgames.com/pathfinding/distance-to-any/#combining-fields">combining distance fields</a> to my page about multiple start points for pathfinding.</li>
<li>Updated my two tutorials on how to make interactive tutorials (<a href="https://www.redblobgames.com/making-of/line-drawing/">1</a> and <a href="https://www.redblobgames.com/making-of/circle-drawing/">2</a>) to be more consistent, point to each other, and say why you might want one or the other.</li>
<li>Updated <a href="https://github.com/redblobgames/helloworld-sdl2-opengl-emscripten">my "hello world" opengl+emscripten code</a> with font rendering and other fixes</li>
<li>Continued working on version 3 of my <a href="https://www.redblobgames.com/x/2312-dual-mesh/">dual-mesh library</a>. I don't plan to make it a standalone project on GitHub until I have used it in a new project, but you can browse the copy of the library <a href="https://github.com/redblobgames/mapgen4">inside mapgen4</a>.</li>
<li>Made my <a href="https://www.redblobgames.com/grids/hexagons/">hexagon guide</a> printable and also savable for offline use using the browser's "Save As" feature.</li>
<li>Improved typography across my site, including some features that Safari and Firefox support but Chrome still doesn't.</li>
<li>Reduced my use of CDNs after the <a href="https://fossa.com/blog/polyfill-supply-chain-attack-details-fixes/">polyfill.io supply chain attack</a>. I continue to use CDNs for example code that I expect readers to copy/paste.</li>
<li>Switched from <code>yarn</code> to <code>pnpm</code>. I liked yarn 1 but never followed it to yarn 2 or yarn 3, and decided it was time to move away from it.</li>
<li>Made of my pages internally linkable, so you can link to a specific section instead of the whole page.</li>
<li>Used <a href="https://ruffle.rs/">Ruffle</a>'s Flash emulator to restore some of the Flash diagrams and demos on my site. When I tried it a few years ago, it couldn't handle most of my swf files, but now it does, hooray!</li>
</ul>

<p>
I didn't remember all of these. I looked through my blog, my notes, and version control history. Here's the <code>git</code> command to go through all my project folders and print out commits from 2024:
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="keyword">for</span> git<span class="keyword"> in</span> $(find . -name .git)
<span class="keyword">do</span> 
    <span class="variable-name">dir</span>=$(dirname <span class="string">"$git"</span>)
    <span class="builtin">cd</span> <span class="string">"$dir"</span>
    <span class="builtin">echo</span> ___ <span class="string">"$dir"</span>
    git --no-pager log --since=2024-01-01 --pretty=format:<span class="string">"%as %s%d%n"</span>
    <span class="builtin">cd</span> - &gt;/dev/null
<span class="keyword">done</span>
</pre>
</div>

<p>
<h3>Learning</h3>
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/top-slice.png" alt=""/>
  <figcaption> Curved and stretched map labels</figcaption>
</figure>


<p>
I decided that I should be focusing more on <strong>learning new things for myself</strong>, instead of learning things to write a tutorial. The main theme this year was <em>maps</em>:
</p>

<ul class="org-ul">
<li>I made a list of topics related to <a href="https://www.redblobgames.com/blog/2024-08-20-labels-on-maps/">labels on maps</a>. These were all potential projects.</li>
<li>I ended up spending a <em>lot</em> of time on basic font rendering. What a rabbit hole! Most of the <a href="https://www.redblobgames.com/blog/">blog posts in 2024</a> are about font rendering.</li>
<li>I did some small projects using square, triangle, hexagon tiles.</li>
<li>I experimented with generating map features and integrating them into an existing map. For example, instead of generating a map and detecting peninsulas, I might want to say "there will be a peninsula here" so that I can guarantee that one exists, and what size it is.</li>
<li>I tried my hand at gradient descent for solving the parameter dragging problem. In my interactive diagrams, I might have some internal state <code>s</code> that maps into a draggable "handle" on the diagram. We can represent this as a function <code>pos(s₁)</code> returning position <code>p₁</code>. When the handle is moved to a new location <code>p₂</code>, I want to figure out what state <code>s₂</code> will have <code>pos(s₂)</code> closest to <code>p₂</code>. Gradient descent seems like a reasonable approach to this problem. However, trying to learn it made me realize it's more complicated than it seems, and my math skills are weak.</li>
<li>I wanted to create a starter project for <a href="https://github.com/redblobgames/2421-helloworld-rotjs-tiles">rot.js with Kenney tiles</a>. I was hoping to use this for something, but then never did.</li>
<li>While learning about font rendering, I also got to learn about graphics, antialiasing, sRGB vs linear RGB, gamma correction, WebGL2. This was a rabbit hole <small>in a rabbit hole <small>in a rabbit hole <small>in a rabbit hole…</small></small></small></li>
</ul>

<p>
But secondarily, I got interested in programming language implementation:
</p>

<ul class="org-ul">
<li>I'm reading <em><a href="https://craftinginterpreters.com/">Crafting Interpreters</a></em>, Bob Nystrom's book about how to write interpreters and compilers. It's been great so far. I haven't done the exercises yet.</li>
<li>I'm learning more about Web Assembly (wasm). I first got interested in Emscripten in <em>2011</em>, before wasm or even asm.js. I want to try out some of the new features that became available this year, like <a href="https://webassembly.org/features/">garbage collection and tail calls</a>.</li>
<li>I followed part of <a href="https://d3s.mff.cuni.cz/teaching/nprg077/">Tomas Petricek's programming language course</a>, and did the exercises after learning some F#.</li>
<li>I watched some of <a href="https://www.piumarta.com/cv/">Ian Piumarta</a>'s talks (<a href="https://www.youtube.com/watch?v=cn7kTPbW6QQ">1</a>, <a href="https://www.youtube.com/watch?v=EGeN2IC7N0Q">2</a>) and read some of the papers (<a href="https://tinlizzie.org/VPRIPapers/tr2011002_oecm.pdf">Open, extensible composition models</a> from 2011, <a href="https://www.piumarta.com/software/cola/colas-whitepaper.pdf">Making COLAs with Pepsi and Coke</a> "a white-paper advocating widespread, unreasonable behaviour" from 2005)</li>
</ul>

<p>
At the beginning of the year I was following my one-week <a href="https://en.wikipedia.org/wiki/Timeboxing">timeboxing</a> strategy. I've found it's good to prevent me from falling into rabbit holes. But my non-work life took priority, and I ended up relaxing my one-week limits for the rest of the year. I also fell into lots of rabbit holes. I am planning to resume timeboxing next year.
</p>

<p>
<h3>Next year</h3>
</p>

<p>
I want to continue learning lots of new things for myself instead of learning them for writing tutorials. The main theme for 2025 will probably be <em>text</em>:
</p>

<ul class="org-ul">
<li>name generators</li>
<li>large language models</li>
<li>programming languages</li>
<li>procedurally generating code</li>
</ul>

<p>
I also want to continue working on maps. It has been six years since I finished <a href="https://www.redblobgames.com/maps/mapgen4/">mapgen4</a>, and I am starting to collect ideas for new map projects.
I won't do all of these but I have lots of choose from:
</p>

<ul class="org-ul">
<li>towns, nations, cultures, factions, languages</li>
<li>roads, trading routes</li>
<li>farms, oil, gold, ore</li>
<li>valleys, mountain ranges, lakes, peninsulas, plateaus</li>
<li>rivers, coral reefs, caves, chasms, fjords, lagoons</li>
<li>forests, trees, snow, waterfalls, swamps, marshes</li>
<li>soil and rock types</li>
<li>groundwater</li>
<li>atmospheric circulation</li>
<li>ocean currents</li>
<li>tectonic plates</li>
<li>animal and plant types</li>
<li>named areas</li>
<li>icons, stylized drawing</li>
<li>update the graphics code in mapgen4</li>
</ul>

<p>
I don't plan to make a full map generator (but who knows!). Instead, I want to learn techniques and write quick&amp;dirty prototype code.  I also plan to continue enhancing my web site structure and build process, including navigation, link checking, project management, bookmarks, more blog features, and maybe <a href="https://gwern.net/sidenote">sidenotes</a>. Although text and maps are the main themes, I have <a href="https://redblobgames.notion.site/f8bc2f44fba94607afa9c06711d23245?v=0766432cb1534ce582ce35b33cbbef7e&amp;pvs=74">many more project ideas</a> that I might work on.  Happy 2025 everyone!
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-12-25-what-i-did-in-2024/"/>
    <published>2024-12-31T00:00:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-12-16-hexagon-page-animations</id>
    <title>Hexagon page animations</title>
    <updated>2026-03-23T15:06:24.886793-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
When I first wrote my <a href="https://www.redblobgames.com/grids/hexagons/">hexagon guide</a> in 2013 I used <a href="https://d3js.org/">d3.js</a>, which has a nice animation system.
I had some trouble with CSS transitions in SVG back then, so I was using Javascript transitions using SVG attributes instead of CSS. This looked something like:
</p>

<div class="org-src-container">
<pre class="src src-js">d3.select(<span class="string">".grid"</span>)
  .transition()
  .attr(<span class="string">"transform"</span>, <span class="string">"rotate(30)"</span>);
</pre>
</div>

<p>
That would rotate the <em>grid</em> from flat-topped to pointy-topped, but the text would be rotated too:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-16-hexagon-page-animations/rotated-text.png" style="max-width:20em" alt="Screenshot of a hexagonal grid rotated 30 degrees into 'pointy top' orientation, with the text rotated too"/>
  <figcaption>Text rotates with the diagram</figcaption>
</figure>

<x:cut/>


<p>
The solution was to undo the rotation on the <code>&lt;text&gt;</code> nodes:
</p>

<div class="org-src-container">
<pre class="src src-js">d3.select(<span class="string">".grid text"</span>)
  .transition()
  .attr(<span class="string">"transform"</span>, <span class="string">"rotate(-30)"</span>);
</pre>
</div>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-16-hexagon-page-animations/unrotated-text.png" style="max-width:20em" alt="Screenshot of the same hexagonal grid rotated 30 degrees, with the text rotated back to be upright"/>
  <figcaption>Text re-rotated to undo the diagram rotation</figcaption>
</figure>

<p>
When <a href="https://simblob.blogspot.com/2018/04/april-updates-hex-grid-guide.html">I rewrote the hexagon guide in Vue in 2018</a>, I wrote my own animations that set a Javascript value between 0 and 1, and turned that into an angle between 0° and 30°. I then applied that angle to both the grid and the text:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">g</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"grid"</span> :<span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotationTransform"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">g</span> <span class="nxml-attribute-local-name">v-for</span>=<span class="string">"hex in grid"</span><span class="nxml-tag-delimiter">&gt;</span>
      <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> :<span class="nxml-attribute-local-name">transform</span>=<span class="string">"undoRotationTransform"</span> <span class="nxml-tag-slash">/</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">g</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">g</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
In both of these implementations, I used a Javascript value to change SVG <em>attributes</em>. As an optimization, I limited the animation to diagrams that were visible. Diagrams that weren't visible would "snap" to the final position.
</p>

<p>
During the rewrite I was hoping to simplify the implementation a little bit.
</p>

<p>
SVG2 added a feature in 2016 specifically to solve the problem of rotated text: <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/vector-effect"><code>vector-effect: non-rotation</code></a>.  
 <a href="https://caniuse.com/?search=vector-effect">Caniuse shows it being supported</a>, and the <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/vector-effect">MDN page shows it as supported</a>.
However <a href="https://play.vuejs.org/#eNp9Uk1v2zAM/SucLmmB5mPtesnSAetQYNthG7YBvehi2LSsVpYMinYSBP7vpeQm7aHowQLJ90S+R+ugvnbdYuhRrdUmlmQ7hojcd1+0t20XiOEAhDWMUFNoYSbUmfbal8FHhjYauEn42ew7OhfgPpCrPszOtd8sp3bSSBLGtnMFo2QAmzgYGCxub8PuRqv5x9UK8nE5fVplmhDNcyAhYcmQ6ZfXWsH+FG1txY1k13IPGrSm4WNWW+ck3idpW8mXL+0YdwzpmBe+bAIJrbVV5VCGM0beLBN41LGchIinIUUJPPmRNPLeTdaMrIup8LEO1K6BAgvp7GpVoTn/DGOi5MkHGMRPoDnWtQRr8MHPM9sGn4kyK3dVF4qjbLu2ZvEQg5cfdUhttCpD21mH9LtLl6JWa8hIwork+GeuMfV4cayXDZaPb9Qf4i7VtPpDGJEGWcMJ44IM8gTf/fsl8l+Bbaj6tLR3wL8Yg+uTxol223vZM73iZbU/8nOz3vyPdztGH4+mktDEHDNfK3mC396x/iL3avEp39N+VOMTIZTw4Q">when I tested it back in 2018, and again in 2024, it didn't work anywhere</a>. 
There's a <a href="https://issues.chromium.org/issues/40506103">Chrome Bug</a> that says the <code>non-rotation</code> value is unimplemented since 2017, and a <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1318208">Firefox bug</a> that says they won't implement it unless Chrome does (probably because developers won't use all the features Firefox has implemented that Chrome didn't). So I can't use this feature.
</p>

<p>
I wanted to use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions/Using_CSS_transitions">CSS transitions</a> back then, but browser features and bugs back then limited what I could do.
Since 2018, browsers have adopted lots of new features, and many bugs have been fixed. 
I decided to try using CSS transitions again.
</p>

<p>
But why CSS transitions? Partly because it's easier to program, and partly because it should be faster. 
With Javascript transitions, I can start from something like this:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">g</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(0)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(0)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(0)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(0)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(0)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(0)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(0)"</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-text">    …</span>
<span class="nxml-text">  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">g</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
and want to turn it into this:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">g</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(30)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(-30)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(-30)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(-30)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(-30)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(-30)"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">transform</span>=<span class="string">"rotate(-30)"</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-text">    …</span>
<span class="nxml-text">  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">g</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
During the transition, I have to change <em>all</em> of those numbers <em>every</em> animation frame. As of today, I have 2145 elements ✕ 60frames/sec ✕ 0.5sec = 64,350 html updates to run the full animation. With CSS transitions, I can do this instead:
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">g</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"rotate-by-0"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-text">    …</span>
<span class="nxml-text">  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">g</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
and then change it to
</p>

<div class="org-src-container">
<pre class="src src-xml"><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
  <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">g</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"rotate-by-30"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
    <span class="nxml-tag-delimiter">&lt;</span><span class="nxml-element-local-name">text</span> <span class="nxml-attribute-local-name">class</span>=<span class="string">"unrotate"</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-text">    …</span>
<span class="nxml-text">  </span><span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">g</span><span class="nxml-tag-delimiter">&gt;</span>
<span class="nxml-tag-delimiter">&lt;</span><span class="nxml-tag-slash">/</span><span class="nxml-element-local-name">svg</span><span class="nxml-tag-delimiter">&gt;</span>
</pre>
</div>

<p>
just <em>once</em>, not every frame. And I only have to change the class at the top, not all the individual <code>&lt;text&gt;</code> elements. That means I have 39 html updates to run the full animation … down from 64,350!
</p>

<p>
First, I needed to make sure CSS transitions would work. I designed a CSS easing function and <a href="https://play.vuejs.org/#eNrFVE1v2zAM/SuagKIJEDtOkwBDlhZbhx62wzZsO/riKLSjRpYMiU7SBfnvoyTnA9jSaw8WSL5H+pH62PNPTZNuWuAzPnfCygaZA2ybh1zLujEW2Z5ZKNmBldbU7Jaot7nOtTDaIbMGC4Qlu/ecXlkoB/1cz4exEtUgB6FuFLHIY2y+aBGNZh+FkmJ9n/NzhXedmfMHNFWlYD6M5JjoNhWbCVU4R1n7jnvIOdtI2D6aHUWTUZaxsNzFj0r5XMquOoNMCwJZoN9NKf3lZG3lElfkTSmPrUBWKzx6pVSK7JVTvXHGptkNe5/d9AlwaM0aCFqoQqwpMDz/CGGHzC9JocXKWGLVcrlU4DsEh/OhB48Kh1EizW7jLQ+e5kauwxcVR1ixfUxBW2hXGlvPun3oZUuo+h8uUInS6NmZyUbuHzhBWUtdJWWrRaSLdiFFsoA/EmwvS6cDlmTpZMCCOUon8RcHv6TH/SNV7JqscdQV8JAVJnOd/6ZtRG3XpCVdL10m7VbYFz7g6OhKlLJKn53RdJtCiZwLUzdSgf3eeFEu57Nj8ZwXSpnt1xBD28LgGBcrEOv/xJ/dzsdy/sOCA7uhg3TCsLAVYISffn2jJi7A2ixbf+xeAX+CM6r1GiPtsdV0Uu0FL6j9Et4EmvNv97RDoNl3TXmhYSiBn3N6Jz6/0vpZ7jiddMM88MNf0gV4Cg==">tested it with an online playground</a>. It worked across the browsers I test on.
</p>

<p>
I implemented this change in small steps and tested each step across browsers:
</p>

<ol class="org-ol">
<li>Switch from <code>transform</code> attribute to <code>transform</code> style, still controlled via Javascript.</li>
<li>Refactor styles to have the diagram rotate and the text unrotate.</li>
<li>Switch from inline <code>style</code> to <code>class</code>, moving the style rules to the global CSS.</li>
<li>Add CSS transition rule, <kbd>transition: transform 0.5s; transition-timing-function: cubic-bezier(0.5, -0.2, 0.5, 1.2);</kbd>.</li>
<li>Some elements couldn't use CSS transitions so wrote a Javascript approximation to it, along with a test to make sure the CSS and Javascript versions stay in sync.</li>
</ol>

<p>
I did run into one bug, with Safari this time. The animations should all play when I change the CSS class. In Safari, for elements not currently on the screen, it would start the animation when the element is scrolled into view. That's too late. It should have finished by then. To work around this bug, I used <code>IntersectionObserver</code> to trigger the animation only on visible elements.
</p>

<p>
I'm glad I was able to simplify the implementation. I hope the animation runs smoother on more devices, but on my computers I wasn't able to tell.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-12-16-hexagon-page-animations/"/>
    <published>2024-12-16T10:36:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-12-08-sdf-halos</id>
    <title>SDF Halos</title>
    <updated>2026-03-23T15:06:24.884422-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org4f2fe2e">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
I had previously posted about <a href="https://www.redblobgames.com/blog/2024-08-27-sdf-font-outlines/">drawing outlines around fonts</a>. The goal is to make labels easier to read in situations like these:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/top1.png" alt=""/>
  <figcaption> Small text on a map</figcaption>
</figure>


<p>
Especially where the text and background are the same lightness, an outline with the inverse lightness can make it more readable:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/top2.png" alt=""/>
  <figcaption> Small outlined text on a map</figcaption>
</figure>


<p>
These outlines are called "halos" in the cartography world. There are <a href="https://www.esri.com/arcgis-blog/products/product/mapping/can-you-read-me-now/">many styles</a> one can use. A solid outline is the simplest thing to implement, so I started with that. Apple Maps and Google Maps use a solid outline too.  However, a solid color outline <a href="http://blog.gretchenpeterson.com/archives/2511">can be distracting</a>, and Cartographer Tom Patterson instead uses <em>background blurring</em> with <em>feathering</em> (variable strength) <a href="https://shadedrelief.com/type-halos/">to produce nicer looking halos</a>. That article made me want to try other styles.
</p>

<x:cut/>


<p>
With signed distance fields, the shader can map <em>signed distance</em> to a <em>color</em>. I made a visualization of this mapping. For plain text, everything below 0 is filled and everything above 0 is transparent:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/distance-to-color-plain.png" alt=""/>
  <figcaption> Distance to color for plain text</figcaption>
</figure>


<p>
I implemented outlines by setting the color at distances between 0.0 and 0.1:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/distance-to-color-outline.png" alt=""/>
  <figcaption> Distance to color for outlined text</figcaption>
</figure>


<p>
I also tried adding a separate soft outline (halo) that fades out over some distance:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/distance-to-color-outline-halo.png" alt=""/>
  <figcaption> Distance to color for soft outlines</figcaption>
</figure>


<p>
Let's look at little closer at the output:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-plain.png" alt="Plain text rendering"/>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-with-outline.png" alt="Text rendered with outline"/>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-with-outline-halo.png" alt="Text rendered with outline that fades"/>
  <figcaption>Three styles of text rendering: plain, hard outline, hard+soft outline</figcaption>
</figure>

<p>
We don't need outlines in regular UIs because the text is readable by default. Outlines are distracting. But in maps the variety of backgrounds means the safest thing is to use an outline. Let's look at different areas of the output:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-plain-annotated.png" alt=""/>
  <figcaption> Areas to evaluate in rendered text</figcaption>
</figure>


<p>
In area <b style="color:hsl(0 50% 50%);font-family:sans-serif">A</b> the dark lines for the mountaintops make the text harder to read, especially at the bottom of the <code>T</code> and <code>G</code>. In the original image at the top of the page, the beginning of the word <code>PEAKS</code> is especially hard to read.
</p>

<p>
In area <b style="color:hsl(0 50% 50%);font-family:sans-serif">B</b> the text is already readable without an outline. The same is true in area <b style="color:hsl(0 50% 50%);font-family:sans-serif">F</b>. Area <b style="color:hsl(0 50% 50%);font-family:sans-serif">E</b> is readable although it could be better.
</p>

<p>
In area <b style="color:hsl(0 50% 50%);font-family:sans-serif">C</b> the black text is hard to read against a dark ocean background. Area <b style="color:hsl(0 50% 50%);font-family:sans-serif">D</b> also has low contrast, especially the letter <code>R</code>.
</p>

<p>
What can we do to improve these? Regular hard outlines are the obvious answer. But I think area <b style="color:hsl(0 50% 50%);font-family:sans-serif">B</b> looks better <em>without</em> the outline. And area <b style="color:hsl(0 50% 50%);font-family:sans-serif">B</b> may be a common case in some map styles. So I wanted to try some other options for outline styles. Tom Patterson suggests blurring the background. I tried implementing an "outline" that uses the blurred background color:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-plain-with-blur.png" alt=""/>
  <figcaption> Text rendered with a blur around it</figcaption>
</figure>


<p>
I think this helps with area <b style="color:hsl(0 50% 50%);font-family:sans-serif">A</b>. We can think of the label as being printed on frosted glass. The downside is that the map detail is lost. And it doesn't help in areas <b style="color:hsl(0 50% 50%);font-family:sans-serif">C</b>, <b style="color:hsl(0 50% 50%);font-family:sans-serif">D</b>, <b style="color:hsl(0 50% 50%);font-family:sans-serif">E</b>. Let's combine this with hard and soft outlines:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-outline-halo-with-blur.png" alt=""/>
  <figcaption> Outlined text on blurred background</figcaption>
</figure>


<p>
I still prefer area <b style="color:hsl(0 50% 50%);font-family:sans-serif">B</b> without the outline. What would happen if I <em>selectively</em> applied the outlines? The shader can calculate the contrast at each pixel and suppress the halo over areas where contrast is good: <b style="color:hsl(0 50% 50%);font-family:sans-serif">B</b> and <b style="color:hsl(0 50% 50%);font-family:sans-serif">F</b>. Let's see the halo only:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/halo-without-selective-contrast.png" alt=""/>
  <figcaption> The halo rendering</figcaption>
</figure>


<p>
Suppressing the halo in some areas gives us this (and it could be tuned up or down):
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/halo-with-selective-contrast.png" alt=""/>
  <figcaption> The halo rendering only when contrast is poor</figcaption>
</figure>


<p>
Here's the result:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-selective-outline-halo-with-blur.png" alt=""/>
  <figcaption> Selectively outlined text on blurred background</figcaption>
</figure>


<p>
However the downside of this style is that a label over different types of areas will look inconsistent. 
</p>

<p>
Another thing I wanted to try is improving the outline in area <b style="color:hsl(0 50% 50%);font-family:sans-serif">C</b>. It looks much stronger than the outline in area <b style="color:hsl(0 50% 50%);font-family:sans-serif">D</b>. I tried tinting it the same color as the background:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/text-selective-outline-halo-with-blur-and-tinting.png" alt=""/>
  <figcaption> Outlining text to match the background color</figcaption>
</figure>


<p>
My experimental code only worked for the light outlines and not for dark outlines. I think I'll have to revisit if I want to use it in a real project.
</p>

<p>
In terms of implementation, selective outlining, outline tint, and background blur all require me to read the background pixels from the map image. I converted my code to use linear rgb to make these calculations correct. Of these three features, background blur is the trickiest, because I'm also <em>writing</em> the blurred value out near the labels. If there are overlapping labels, what happens is that I draw the blurred background, draw the first label, draw the blurred background again, and draw the second label. This looks bad. It would be even worse if I didn't <a href="https://www.redblobgames.com/blog/2024-09-27-sdf-combining-distance-fields/">combine distance fields as I described in a previous blog post</a>. I didn't try to fix any of this for these experiments. My goal was to see whether selective outlining, outline tint, and background blur help, not to make a production-quality text renderer.
</p>

<p>
I don't know which of these techniques will look best in a real project, but I now have some options:
</p>

<ol class="org-ol">
<li>Hard outline</li>
<li>Soft outline (halo)</li>
<li>Blurred background</li>
<li>Selective outline</li>
<li>Tinted outline</li>
</ol>

<p>
Applying all of them to the example from the top of the page, I think it's an improvement over the regular hard outlining:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/top2.png" alt="Text with outlines"/>
  <img src="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/bot1.png" alt="Text with hard and soft tinted selective outlines with blurred background"/>
  <figcaption>Before and after comparison</figcaption>
</figure>

<p>
In games like Magic The Gathering you spend some of your time collecting cards for your deck and some of your time playing those cards in a game. These font experiments for me are the "deck building". I'm learning a lot, and I now have more options available. But at some point I want to use this in a real project.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-12-08-sdf-halos/"/>
    <published>2024-12-08T15:29:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-11-17-sdf-headless-tests</id>
    <title>SDF headless tests, part 2</title>
    <updated>2026-03-23T15:06:24.881878-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org7e09ad5">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
In the <a href="https://www.redblobgames.com/blog/2024-11-08-sdf-headless-tests/">last blog post</a> I wrote about how I wanted to test the parameters of <a href="https://github.com/Chlumsky/msdfgen">msdfgen</a>, which generates multi-signed distance fields for fonts and other shapes. While testing the <code>emrange</code> parameter, I found lots of bugs in my renderer.
</p>

<p>
I also wanted to try out msdfgen's new <em>asymmetric</em> range parameter, <code>aemrange</code>. The distance field goes both inside and outside the font edge, but if I want outlines, I want it to go pretty far outside the font. I made a visualization to show the range:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/blog/sdf.png"/>
  <figcaption>Contour lines showing the distance field</figcaption>
</figure>

<p>
From this we can see that the interior has far fewer contour lines than the exterior. That's because fonts are relatively thin. If it were a large object like a solid circle, it would have many contour lines on the interior.
</p>

<x:cut/>


<p>
The <em>encoding</em> of the distance field maps distances to a value 0.0–1.0, which gets stored in the texture as 0–255. By default, the encoding sets distance=0 to value=0.5 which will be 127.5. Let's look at how much of the font space is used by each encoded value. Blue means I'm using that value for the interior of the font. Green means I'm using it for the exterior of the font. Gray means I'm not using that value.
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-11-17-sdf-headless-tests/range-symmetric.png" alt=""/>
  <figcaption> Symmetric use of 0-255 values within the font atlas image</figcaption>
</figure>


<p>
This histogram tells us two things:
</p>

<ol class="org-ol">
<li>Of all the pixels in the distance field encoding, most of them are used (blue or green). There's not a lot of waste representing values that I don't need (gray). This is good.</li>
<li>Of all the 0–255 values in the encoding, around half of them are used. There's a lot of waste on the right side where no pixels use that value. This is bad.</li>
</ol>

<p>
Most of the values I care about are on the left half. MSDFgen v1.12 and MSDF-Atlas-Gen 1.3 add support for an asymmetric encoding. I switched from <kbd>-emrange 0.8</kbd> (which corresponds to -0.4 to +0.4) to <kbd>-aemrange -0.7 0.1</kbd>, keeping the full range at 0.8.
</p>

<p>
Does this improve the quality?
</p>

<figure class="w-auto">
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-symmetric-4-4.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-asymmetric-7-1.png"/>
  <figcaption>Symmetric vs asymmetric use of range</figcaption>
</figure>

<p>
It did <strong>not</strong> improve quality! If anything it's slightly worse. What's going on?
</p>

<p>
Keeping the full range at 0.8 was the problem. I had <em>shifted</em> the range, which extended the contour lines out farther, but didn't make them more accurate:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-11-17-sdf-headless-tests/range-asymmetric-shift.png" alt=""/>
  <figcaption> Asymmetric shifted use of 0-255 values</figcaption>
</figure>


<ol class="org-ol">
<li>Of all the pixels in the distance field encoding, <em>fewer</em> of them than before are being used (blue or green), and a lot more are wasted (gray). This is bad.</li>
<li>Of all the 0–255 values in the encoding, around half of them are used. There's less wasted on the right side but more wasted on the left side. This is bad.</li>
</ol>

<p>
I instead want to <em>stretch</em> the range, with <kbd>-aemrange -0.4 0.1</kbd>:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-11-17-sdf-headless-tests/range-asymmetric-stretch.png" alt=""/>
  <figcaption> Asymmetric stretched use of 0-255 values</figcaption>
</figure>


<figure class="w-auto">
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-symmetric-4-4.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-asymmetric-4-1.png"/>
  <figcaption>Symmetric vs asymmetric use of range</figcaption>
</figure>

<ol class="org-ol">
<li>Of all the pixels in the distance field encoding, <em>most</em> of them are being used (blue or green). This is good.</li>
<li>Of all the 0–255 values in the encoding, msot of them are being used. This is good.</li>
</ol>

<p>
The output does look better. The contour lines don't go out farther; instead they are spaced more closely together, which means more precision. The curves are smoother.
</p>

<p>
Asymmetry helps by throwing away the range that is unused. I also wanted to check for the opposite problem: that some value is needed but <em>doesn't</em> fit in the 0–255 range. I added code to the shader to detect this:
</p>

<div class="org-src-container">
<pre class="src src-glsl"><span class="keyword">if</span> (distances.g &gt;= 1.0) gl_FragColor = <span class="type">vec4</span>(1, 0.5, 0, 1);
</pre>
</div>

<p>
and here's what it looks like if I cut off too much of the range:
</p>

<figure class="w-auto">
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/blog/warning-clipped-distances.png"/>
  <figcaption>If the range is too asymmetric, the interior distances get clipped</figcaption>
</figure>

<p>
This is an example of a shader "assert" that helps me figure out when something is wrong. 
</p>

<p>
What is the optimal <code>aemrange</code>? It took me way too long to realize that I should set the low and high endpoints independently:
</p>

<ol class="org-ol">
<li><strong>Low</strong> endpoint is based on the <em>maximum</em> range I want for outline, halo, and other special effects.</li>
<li><strong>High</strong> endpoint is based on the <em>maximum</em> distance value inside the font, as calculated by <code>msdfgen</code>.</li>
</ol>

<p>
Setting the range this way should maximize font rendering quality.  In both cases the range needs to be extended slightly beyond the limit so that the texture interpolation can find an accurate value. But I wasn't so smart. I tweaked repeatedly until I found values that were good enough. Writing this blog post helped me understand what is going on, and I'll be able to choose better next time.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-11-17-sdf-headless-tests/"/>
    <published>2024-11-17T09:47:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-11-08-sdf-headless-tests</id>
    <title>SDF headless tests, part 1</title>
    <updated>2026-03-23T15:06:24.879392-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org88907e4">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
In the last few posts I have shown some of the experiments I did with font rendering. Those experiments were all in the <em>renderer</em>. I'm using <code>msdfgen-atlas</code> to generate the textures used by the renderer, and I wanted to experiment with <code>msdfgen</code>'s parameters. Instead of generating new font data and then reloading the browser, I decided to try "headless" rendering controlled by a shell script.
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/blog/font-lora-italic.png" alt=""/>
  <figcaption> Testing with different parameters</figcaption>
</figure>


<x:cut/>


<p>
In <a href="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/">my blog post about antialiasing</a> I found that instead of changing a slider, I wanted to see the entire range of outputs at once. I did that here as well. For each parameter value I ran <code>msdfgen</code> and generated an output file. Then I compared all the output files against each other to learn about the effect of each parameter.
</p>

<p>
Getting my code running in <code>stackgl/headless-gl</code> took a lot of work. My code wasn't designed to work headlessly. But I also had trouble with the installation process (which needs python?!) and poor interactions with <code>twgl.js</code> not liking headless-gl. I'm not sure what to think of this. Is there another headless gl library I should use? Or maybe I can use gl in the browser, with the File API to save the output to files? Or maybe Electron/Tauri? I was frustrated the whole time, and didn't take good notes on what went wrong. I should have stopped, taken a break, and evaluated the situation with a level head. But instead I kept pushing through with tweaks and fixes until I got something running. [*Update* <time>&lt;2024-11-18 Mon&gt; </time> maybe I should look at <a href="https://pptr.dev/guides/screenshots/">Puppeteer</a>.]
</p>

<p>
In any case, I did get something running, and here are some findings—
</p>

<p>
The first thing I wanted to test was the effect of the <code>emrange</code> / <code>pxrange</code> parameter.
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="keyword">for</span> test<span class="keyword"> in</span> 1 2 3 4 5 6 7 8 9
<span class="keyword">do</span>
    ~/Projects/src/msdf-atlas-gen/bin/msdf-atlas-gen <span class="sh-escaped-newline">\</span>
        -type mtsdf -emrange 0.$<span class="variable-name">test</span> -dimensions 511 511 <span class="sh-escaped-newline">\</span>
        -font ~/Library/Fonts/FiraSans-Regular.otf <span class="sh-escaped-newline">\</span>
        -imageout assets/FiraSans-Regular.png <span class="sh-escaped-newline">\</span>
        -json assets/FiraSans-Regular.json
    node msdfgen-parameters.js
    cp _output.png output/test-emrange-$<span class="variable-name">test</span>.png
<span class="keyword">done</span>
</pre>
</div>

<figure>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-1.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-2.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-3.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-4.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-5.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-6.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-7.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-8.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-9.png"/>
  <figcaption>emrange from 0.1 to 0.9</figcaption>
</figure>

<p>
There's a lot going on here! 
</p>

<p>
Columns 1 and 2 show that the <code>emrange</code> affects how far out I can draw a halo.
The other columns show that when I extend <code>emrange</code> to be higher, the font quality degrades.
</p>

<p>
Columns 3 through 6 should look the same in every row (except for font quality). But they don't. The outline thickness changes (see column 4), the font thickness changes (see column 5), and the drop shadow distance changes (see column 6).  That means there are bugs in my rendering code. If I hadn't run this test, I might not have found them.
</p>

<p>
I thought about these bugs for a while and realized:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/blog/distance-vs-encoding.png"/>
  <figcaption>emrange affects the slope</figcaption>
</figure>

<p>
The <code>emrange</code> affects the <em>slope</em> of the mapping from distance to the value in the texture. My shader was using the <code>y</code> value, the encoded sdf. But I want be using the <code>x</code> value, the distance value. After I understood what I did wrong, I was able to fix the bugs. The outline thickness, font thickness, and drop shadow distance are now consistent:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-fixed-2.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-fixed-5.png"/>
  <img src="https://www.redblobgames.com/x/2437-msdfgen-parameters/output/test-emrange-fixed-9.png"/>
  <figcaption>fix outline and font thickness to handle emrange</figcaption>
</figure>

<p>
While going through these tests, I found and fixed several other bugs, including how I was using msdf+sdf for outlines and how my drop shadow shader worked.
</p>

<p>
I'm glad I worked on this headless rendering test, but I had enough trouble making my code working headless that I want to look for an easier approach next time.
</p>

<p>
In the <a href="https://www.redblobgames.com/blog/2024-11-17-sdf-headless-tests/">next post</a> I explored the <em>asymmetric</em> version of <code>emrange</code>.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-11-08-sdf-headless-tests/"/>
    <published>2024-11-08T15:29:00-08:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-10-09-sdf-curved-text</id>
    <title>SDF curved text</title>
    <updated>2026-03-23T15:06:24.877074-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org3ead785">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
Over the last few posts I wrote about things I did to improve font quality, such as <a href="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/">antialiasing</a> and <a href="https://www.redblobgames.com/blog/2024-09-27-sdf-combining-distance-fields/">combining distance fields</a> to merge outlines and halos. But I want to "pop up the stack" a bit and talk about one of the bigger goals for this project. I want to render text in styles that I've seen in maps, both online and offline, both fantasy and real. In particular, I want to apply spacing, rotation, and curvature to the labels. 
</p>

<figure class="w-150b">
  <a href="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/top-slice.png">
  <picture>
    <source srcset="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/top-slice.avif" type="image/avif"/>
    <source srcset="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/top-slice.webp" type="image/webp"/>
    <img src="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/top-slice.png" alt="Sample map"/>
  </picture>
  </a>
  <figcaption>Sample map with labels</figcaption>
</figure>

<x:cut/>


<p>
These are common in cartography, not only in fantasy maps like Tolkein's but also in real-world maps.  Eduard Imhof's classic 1975 paper, <a href="https://web.archive.org/web/20100821084646/https://www.lojic.org/techhelp/pdfs/Positioning_Names_on_Maps.pdf">Positioning Names on Maps</a> has a ton of great advice on how to position labels, and not only recommends curving text, but also sketches out examples:
</p>

<figure>
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:1em;align-items:center">
    <img src="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/imhof-fig-40-41.png" alt="Imhof paper, figures 40,41 showing label placement on a mountain pass"/>
    <img src="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/imhof-fig-42.png" alt="Imhof paper, figure 42 showing label placement along rivers"/>
  </div>
  <blockquote>
    Clear areal association often requires bending and spreading a name so that it is stretched as much as possible across the horizontal axis of an area.
  </blockquote>
  <figcaption>Figures 40, 41, 42 from Imhof's paper</figcaption>
</figure>  

<p>
I've had this paper sitting on my computer desktop <strong>since 2011</strong>. And I'm sure I was interested in this topic long before then. I will be re-reading it again before writing a label placement algorithm. Both Apple Maps and Google Maps use curved text, as shown in these examples:
</p>

<figure class="w-150b">
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:1em">
    <img src="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/apple-maps-labels.jpg" alt="curved labels on Apple Maps"/>
    <img src="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/google-maps-labels.png" alt="curved labels on Google Maps"/>
  </div>
  <figcaption>Screenshots: curved labels on Apple and Google maps</figcaption>
</figure>

<p>
Fantasy maps used curved text too. Tolkein's Lord of the Rings maps used curved text for areas and rivers. Scott Turner's inspirational blog includes a post <a href="https://heredragonsabound.blogspot.com/2017/09/labeling-coast-part-two.html">Labeling the Coast part two</a> in which he shows how he analyzed the coastline to find a suitable curved arc, and also that he liked curved arcs better than more complex paths. There are also posts on <a href="https://www.cartographersguild.com/">Cartographers Guild</a> about when and how to use curved labels.
</p>

<p>
So I want to implement curved labels.
</p>

<p>
To figure out how to make text flow along a curve, I first sketched it out on paper, then made a standalone interactive widget. There are three cases to handle:
</p>

<ol class="org-ol">
<li>Positive curvature: the text should be positioned along the baseline.</li>
<li>Zero curvature: the text is not curved.</li>
<li>Negative curvature: the text should be positioned along the <em>ascender</em> line.</li>
</ol>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/curvature-positive-baseline.png"/>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/curvature-zero.png"/>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/curvature-negative-ascender.png"/>
  <figcaption>Three curvature cases: &gt;0, =0, &lt;0</figcaption>
</figure>

<p>
If I <em>always</em> curve at the baseline, the tops of the characters are too close together. That's why I have to curve at the ascender line instead of the baseline when the curvature is negative:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/curvature-negative-baseline.png"/>
  <figcaption>Upwards curvature at baseline makes letters too close together</figcaption>
</figure>

<p>
After calculating the geometry, there are two ways I know of to render the curved text. 
</p>

<ol class="org-ol">
<li>"Curving" or "Wrapping": Position and rotate <em>each letter</em> along a curved path, <em>then</em> render them onto the intermediate combined distance field.</li>
<li>"Shaping" or "Warping": Draw the letters first onto the intermediate combined distance field, <em>then</em> distort the <em>entire label</em> into a curved shape.</li>
</ol>

<p>
One advantage of warping over wrapping is that it allows for many more effects, such as these:
</p>

<figure class="w-auto">
  <div style="display:grid; grid-gap: 1em; grid-template-columns: 1fr 1fr 1fr">
    <img src="https://www.redblobgames.com/x/2428-map-typography/blog/shape-two-curvatures.jpg"/>
    <img src="https://www.redblobgames.com/x/2428-map-typography/blog/shape-vertical-curvature.jpg"/>
    <img src="https://www.redblobgames.com/x/2428-map-typography/blog/shape-both-sign-curvature.jpg"/>
  </div>
  <figcaption>More shapes to fit the text to</figcaption>
</figure>

<p>
However, whenever stretching the label non-uniformly, the distance field gets distorted. In this example the white halo on the left side (<kbd>The</kbd>) is much larger vertically than horizontally. I can find a way to fix this but I decided not to pursue it, since I was primarily interested in curved text and not arbitary warpings:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/shape-trapezoid.jpg"/>
  <figcaption>Trapezoid shows the distance field is non-uniform</figcaption>
</figure>

<p>
<a href="https://fonts.google.com/knowledge/using_type/typesetting_on_a_curved_path">Google's guide</a> says wrapping generally works better than warping, but I ended up trying warping first, and decided I liked it well enough. However, in the <a href="https://www.redblobgames.com/blog/2024-09-27-sdf-combining-distance-fields/">last post</a> I had said next time I might choose to not use an intermediate combined distance field. I think without that step, it would be easier to use wrapping than warping.
</p>

<p>
Using distance fields, I can apply the outline, halo, and antialiasing after the curving step. I'm very happy with the way the curved labels turned out. 
</p>

<figure class="w-150b">
  <a href="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/bottom-slice.png">
  <picture>
    <source srcset="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/bottom-slice.avif" type="image/avif"/>
    <source srcset="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/bottom-slice.webp" type="image/webp"/>
    <img src="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/bottom-slice.png" alt="Another sample map"/>
  </picture>
  </a>
  <figcaption>Sample map with labels</figcaption>
</figure>


</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-10-09-sdf-curved-text/"/>
    <published>2024-10-09T15:23:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-09-27-sdf-combining-distance-fields</id>
    <title>SDF combining distance fields</title>
    <updated>2026-03-23T15:06:24.873755-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="orgbd1e242">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
Learning about font rendering, I was looking at text closely <a href="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/">last time</a>, and I noticed another issue. The shadows of each letter overlap the previous letter. That's because I'm drawing one letter at a time. So for example in the <kbd>fl</kbd>, I draw the <kbd>f</kbd>'s letter, outline, and shadow, <em>then</em> I draw <kbd>l</kbd>'s letter, outline, and shadow. So <kbd>l</kbd>'s shadow is drawn on top of <kbd>f</kbd>'s letter.
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/spacing-test.png" alt=""/>
  <figcaption> Shadows of each letter overlap the previous letter</figcaption>
</figure>


<x:cut/>


<figure>
  <img width="504" style="width:504px;image-rendering: pixelated" src="https://www.redblobgames.com/blog/2024-09-27-sdf-combining-distance-fields/spacing-test-closeup.png"/>
  <figcaption>Closeup showing the shadow drawn over the character to the left</figcaption>
</figure>

<p>
The first thing I considered was to draw all the shadows first and then draw all the letters. But a second problem here, harder to see, is that shadows are drawn on top of the adjacent letter's shadows, and that causes them to be darker than they should be. The distance fields for adjacent letters always overlap:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/spacing-s.png" alt=""/>
  <figcaption> Sprite distance fields always overlap</figcaption>
</figure>


<p>
It's only when the letters are close enough that you can see artifacts of the double rendering.
</p>

<p>
To solve both problems, I can generate a <em>new</em> distance field which is the <em>min()</em> of the distance fields for each character. The min() of signed distance fields is the <em>union</em> of the shapes. I used the <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/blendEquation">blendEquation()</a> function with <code>MAX_EXT</code> instead of the default <code>FUNC_ADD</code> (it's max instead of min because msdfgen's encoding is inverted). The <code>MAX_EXT</code> extension seems to have <a href="https://web3dsurvey.com/webgl/extensions/EXT_blend_minmax">100% support</a> in WebGL 1, and is always included with WebGL 2.
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-09-27-sdf-combining-distance-fields/SDF-combination.jpg" alt=""/>
  <figcaption> Adding an intermediate distance field texture</figcaption>
</figure>


<p>
The output texture holds a combined distance field. I then use the distance field shader to draw that combined distance field to the map.
</p>

<p>
I could do this once per string I want to draw or once for the entire scene. I decided to do it once per string, because that allows me to use different colors and styles (thickness, outline width, shadow, halo) per string.
</p>

<p>
I ran into a few bugs with this:
</p>

<ul class="org-ul">
<li>because I couldn't figure out how to min() msdf fields, I put an sdf in the combined distance field instead of msdf, and that meant the corners of fonts got a little rounder; to compensate I increased the resolution of the combined distance field</li>
<li>having a different resolution for the original and combined distance fields messed up the antialiasing; in the previous post I described a "slope" of distance field vs output pixels, but now there's an intermediary with the slope stretched out</li>
<li>the letters got out of alignment when I moved them around for the intermediate texture; in particular, I had to calculate a new baseline position accounting for the change in resolution</li>
</ul>

<p>
One way I compared parameters was by rendering them in alternating frames. Here's an example showing that the combined distance field resolution matters (slightly):
</p>

<figure>
  <video style="max-width: 100%" controls="yes" loop="yes" src="/x/2428-map-typography/blog/resolution-3L-5L-comparison.mp4"/>
  <figcaption>Comparison of 3X vs 5X resolution on the combined distance field</figcaption>
</figure>

<p>
Here's the final result, showing that the overlapped drawing is fixed, especially at the bottom left of the second <kbd>g</kbd>:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/shadow-overlap-text.png" width="204" style="image-rendering: pixelated"/>  
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/shadow-behind-text.png" width="204" style="image-rendering: pixelated"/>
  <figcaption>Before and after combining distance fields</figcaption>
</figure>

<p>
The combined distance field approach solves the problem but it means I need to write each string to a separate intermediate texture, which leads to a rabbit hole of having to allocate texture space during rendering, possibly reusing textures, and dealing with gpu pipeline stalls. That's an area I don't understand well. Fortunately I don't need it to be optimized for my project. But for a game project, I might choose to do the two pass approach instead, letting the shadows get drawn twice in overlapping areas. Are there better approaches? I don't know.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-09-27-sdf-combining-distance-fields/"/>
    <published>2024-09-27T11:37:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-09-22-sdf-antialiasing</id>
    <title>SDF antialiasing</title>
    <updated>2026-03-23T15:06:24.871152-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org0c9abdf">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
<a href="https://www.redblobgames.com/blog/2024-09-08-sdf-font-spacing/">Last time</a> I was looking at letter spacing with my renderer to see how it compared to Google Chrome on Mac. But while doing that I noticed that their antialiasing looked nicer than mine. So I tweaked parameters, including antialias edge width, gamma, and threshold bias.
</p>

<figure class="w-auto comparison">
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/msdf-fonts.png" alt="My renderer"/>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/google-fonts.png" alt="Google's renderer"/>
  <figcaption>My renderer (first) vs Google's (second)</figcaption>
</figure>

<x:cut/>


<p>
I got results that were close to Google Chrome on Mac, but beyond that it became unclear which variant was actually <em>better</em>. I kept tweaking parameters:
</p>

<figure class="w-auto comparison">
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/msdf-fonts.png" alt="tweak 0"/>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/tweak-anti-alias-1.png" alt="tweak 1"/>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/tweak-anti-alias-2.png" alt="tweak 2"/>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/tweak-anti-alias-3.png" alt="tweak 3"/>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/tweak-anti-alias-4.png" alt="tweak 4"/>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/msdf-fonts-tweaked.png" alt="tweak 5"/>
  <figcaption>Many attempts to tweak parameters</figcaption>
</figure>

<p>
Sometimes it came down to looking really closely at the pixels. But which is better? I don't know.
</p>

<figure class="w-auto">
  <img width="256" style="width:256px" class="pixel-peep" src="https://www.redblobgames.com/x/2428-map-typography/blog/gamma-comparison-mine.png" alt="My renderer"/>
  <img width="256" style="width:256px" class="pixel-peep" src="https://www.redblobgames.com/x/2428-map-typography/blog/gamma-comparison-google.png" alt="Google's renderer"/>
  <noscript>Some of these comparisons will look better in the original blog post than in a feed reader</noscript>
  <figcaption>My renderer (first) vs Google's (second)</figcaption>
</figure>

<p>
I realized after a while that this is a rabbit hole. I had to force myself out of this endless tweaking cycle. Besides, Chrome on Mac is different from other browser + windowing systems, so I shouldn't spend all my time trying to match it.
</p>

<p>
But two months later, I revisited antialiasing, because I needed to better understand it to implement halos and drop shadows. This happens a lot with my experiments. I'll discover something, then I'll tweak a lot, then I'll put it away for a while, and later I can come back to it and try to understand what I did.
</p>

<p>
To implement antialiasing let's look at the mapping from distance to color.
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/distance-to-color-antialias-off.png" alt=""/>
  <figcaption> Distance maps to either black or white</figcaption>
</figure>


<p>
Here's the output. Pixels are either black or white:
</p>

<figure>
  <img width="160" style="width:160px" class="pixel-peep" src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/single-glyph-antialias-off.png"/>
  <figcaption>The letter <kbd>e</kbd> rendered with no antialiasing</figcaption>
</figure>

<p>
For antialiasing we want to smoothly transition between colors, so that it looks like this:
</p>

<figure>
  <img width="160" style="width:160px" class="pixel-peep" src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/single-glyph-antialias-on.png"/>
  <figcaption>The letter <kbd>e</kbd> rendered with antialiasing</figcaption>
</figure>

<p>
But how much? If we do too much, it looks blurry:
</p>

<figure>
  <img width="160" style="width:160px" class="pixel-peep" src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/single-glyph-antialias-more.png"/>
  <figcaption>The letter <kbd>e</kbd> rendered with more antialiasing</figcaption>
</figure>

<p>
We want it to scale based on the <em>output pixels</em>, not the input distance field. And that means we need to know something about the number of pixels on screen.
</p>

<p>
On the msdfgen page there's a shader that includes antialiasing. They're measuring  <code>screenPxRange</code> representing how many pixels in the <em>output</em> corresponds to one "distance unit" in the signed distance field <em>input</em>.
</p>

<p>
If we want antialiasing to occur over <code>edge_blur_px</code> pixels in the output, we can divide <code>edge_blur_px ÷ screenPxRange</code> to find out what signed distance range this represents.  For example if we want to antialias over 2 pixels, and <code>screenPxRange</code> is 8 px / distanceunit, then 2 px ÷ 8 px / distanceunit is ¼ distanceunits. The msdfgen code will antialias between 0 and +¼. Another option would be to antialias between −⅛ and +⅛.
</p>

<p>
This is what the blending looks like:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/distance-to-color-antialias-on.png" alt=""/>
  <figcaption> Color blends between black and white</figcaption>
</figure>


<p>
The slope of that line is <code>screenPxRange / edge_blur_px</code>.
</p>

<p>
I wanted to get a better sense of what <code>edge_blur_px</code> should be. Over how many pixels should I apply antialiasing?
I had previously tweaked it, then made the antialiasing width an interactive parameter to tweak faster. When I revisited it a few weeks later, I realized it'd be better to see all the outputs at once instead of using interactivity to see one at a time. For more on one interactive vs multiple non-interactive visualization, see <a href="https://worrydream.com/LadderOfAbstraction/">Bret Victor's page about "Ladder of Abstraction"</a>.
</p>

<figure class="w-auto comparison">
  <img class="pixel-peep" src="https://www.redblobgames.com/x/2437-msdfgen-parameters/blog/antialiasing-edge-widths-1.png"/>
  <figcaption>Testing a range of antialiasing widths</figcaption>
</figure>

<p>
I had set <code>edge_blur_px</code> to 1.0 in my previous tweaking, and this confirms that 1.0 is a reasonable choice. Lower values from 0.5 down look a little blocky, and higher values from 2.0 up look a little blurry. I decided to zoom into that range:
</p>

<figure class="w-auto comparison">
  <img class="pixel-peep" src="https://www.redblobgames.com/x/2437-msdfgen-parameters/blog/antialiasing-edge-widths-2.png"/>
  <figcaption>Testing a range of antialiasing widths</figcaption>
</figure>

<p>
These may all look the same at first, but look closely with a magnifying glass and you'll see differences. From eyeballing this, I think maybe 1.2 or 1.3 might be the best choice, at least for large text. I don’t have any explanation for this. Could be it 1.25? Could it be sqrt(1.5)? If I looked at other sizes and characters, would I conclude it has to be higher, like sqrt(2) or 1.5? Is there signal processing math to prove the best value? Would it be different with gamma correction? Should I use <code>smoothstep</code> instead of <code>linearstep</code>? I don’t know. I'll set it to 1.2 for now.
</p>

<p>
I'm quite happy with what I have, but not happy with how long it took. Two months went by between the first and last image on this blog post. I had many fixes and tweaks in those two months. I'll describe those in the next few posts.
</p>

<p>
[Update: <time>&lt;2025-07-25 Fri&gt; </time> This is a great post about antialiasing with distance fields: <a href="https://blog.pkh.me/p/44-perfecting-anti-aliasing-on-signed-distance-functions.html">https://blog.pkh.me/p/44-perfecting-anti-aliasing-on-signed-distance-functions.html</a>]
</p>

<style>
  figure.comparison {
    overflow-x: auto;
    overflow-y: clip;
    img {
      box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 5px 0px; 
    }
  }
  img.pixel-peep, figure.comparison img {
    image-rendering: pixelated;
    width: unset;
    max-width: unset;
  }
</style>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-09-22-sdf-antialiasing/"/>
    <published>2024-09-22T08:27:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-09-08-sdf-font-spacing</id>
    <title>SDF letter spacing</title>
    <updated>2026-03-23T15:06:24.865785-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="orgfd7ce84">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
My <a href="https://www.redblobgames.com/blog/2024-08-20-labels-on-maps/">summer project</a> is to work on labels for maps. In the <a href="https://www.redblobgames.com/blog/2024-08-27-sdf-font-outlines/">previous post</a> I described how I created outlines, and how I had a bug in the rendering. While looking closely at text to fix that bug, I noticed in one of my tests that the <kbd>k</kbd> and <kbd>s</kbd> seemed too close together. The <kbd>h</kbd> and <kbd>e</kbd> seemed a little too far apart. 
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/map-outline-2.png" alt=""/>
  <figcaption> Letter spacing issues</figcaption>
</figure>


<x:cut/>


<p>
The spacing between characters is normally set in the metrics for the characters. There's <em>also</em> a way to override the spacing between specific pairs of characters, using "<a href="https://learn.microsoft.com/en-us/typography/opentype/spec/kern">kerning tables</a>", named <code>kern</code> and <code>GPOS</code> in TrueType. I don't have kerning implemented.
</p>

<p>
Hypothesis: I need to implement kerning. To test this, I rendered <kbd>k</kbd> and <kbd>s</kbd> with other letters to see if either one was the issue:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/spacing-s.png"/>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/spacing-k.png"/>
  <figcaption>Testing spacing of s and k</figcaption>
</figure>

<p>
It does seem like <kbd>k</kbd> is generally close to the letter to the right of it. So that suggests it's the letter <kbd>k</kbd> and not a kerning issue.
</p>

<p>
But I was curious about kerning, so I checked the font data and this font (Fira Sans) doesn't have a kerning table in it. So that means <kbd>k</kbd> really is a little too close to the neighbor. I verified this by checking the rendering in Google Chrome on the Google Fonts web site (second image) and compared to my rendering (first image):
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/msdf-spacing.png"/>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/google-spacing.png"/>
  <figcaption>Spacing issues</figcaption>
</figure>

<p>
Sure enough, both the <kbd>h</kbd> + <kbd>e</kbd> and <kbd>k</kbd> + <kbd>s</kbd> spacing issues are there. So that's just how the font is. Ok, I guess there's nothing I can do here, at least for this font. 
Later I will try other fonts, and then I can revisit this issue.
</p>

<p>
I was <em>extremely</em> pleased that my font renderer looked so close to Google's, not only the spacing but also the shapes. Looking closely at the edges led me down another rabbit hole … a tale for the next blog post.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-09-08-sdf-font-spacing/"/>
    <published>2024-09-08T16:07:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-08-27-sdf-font-outlines</id>
    <title>SDF font outlines</title>
    <updated>2026-03-23T15:06:24.863284-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="orge68610b">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
In the <a href="https://www.redblobgames.com/blog/2024-08-20-labels-on-maps/">previous post</a> I introduced my summer project, to render labels on maps. As part of this, I want to be able to draw <em>outlines</em>, <em>halos</em>, and <em>drop shadows</em>. 
<a href="https://en.wikipedia.org/wiki/Signed_distance_function">Signed distance field</a> fonts are well suited for this. The basic use is to consider the signed distances -1 to 0 to be "inside" (filled) and signed distances 0 to +1 to be "outside" (transparent).
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-08-27-sdf-font-outlines/diagram-2-color.png" alt=""/>
  <figcaption> Mapping distance to color</figcaption>
</figure>


<p>
To draw an outline, we can add another range, maybe 0.0 to +0.2: 
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-08-27-sdf-font-outlines/diagram-3-color.png" alt=""/>
  <figcaption> Adding outlines to the distance map</figcaption>
</figure>


<x:cut/>


<p>
While "pixel peeping" the quality of the outlines, I noticed a little bit of black interior color leaking outside the white outline:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/black-leaks-outside-white-outline-1.png" style="height:216px;image-rendering: pixelated"/>
  <figcaption>Noticed a tiny bit of black outside the white outline</figcaption>
</figure>

<p>
What could be causing this? I looked through the code but nothing was obvious.
</p>

<p>
To troubleshoot/debug, I need to make the problem happen reliably. Once I can reproduce it, I can start generating hypotheses and then testing them. 
</p>

<p>
So the first step is to make sure this problem happened on initial run, and not only after changing anything interactively. I made sure the <kbd>d</kbd> got rendered in that spot and that the black color leak was there.
</p>

<p>
The next step is to come up with a hypothesis. I thought that the rendering of black pixels was extending past the rendering of white pixels.
</p>

<p>
The next step is to come up with a test to disprove or prove the hypothesis. If the <em>black</em> pixels were rendering outside the outline, one way to see this would be to change the colors. I changed the black pixels to yellow, and to my surprise, yellow did <em>not</em> go outside the outline. I should've changed the outline color too, to make it more visible, but in this screenshot we can see that what's outside the outline is still black, not yellow:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/black-leaks-outside-white-outline-2.png" style="height:216px;image-rendering: pixelated"/>
  <figcaption>It's not the interior color that's leaking out</figcaption>
</figure>

<p>
This means <em>my hypothesis was wrong</em>. That's ok. Debugging may take several hypotheses and tests. But what could cause black pixels outside a sprite, when none of the sprite colors were black?
</p>

<p>
This is when I remembered articles I read from <a href="https://tomforsyth1000.github.io/blog.wiki.html#%5B%5BPremultiplied%20alpha%5D%5D">Tom Forsyth</a>, <a href="https://www.facebook.com/permalink.php?story_fbid=1818885715012604&amp;id=100006735798590">John Carmack</a>, <a href="https://www.realtimerendering.com/blog/gpus-prefer-premultiplication/">Eric Haines</a>, and others say that I should be using premultiplied alpha to avoid black fringing around sprites. Do I really need that? I don't have sprite graphics here. But that became my next hypothesis. How can I test it? By changing the blending. This fixed the problem:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/black-leaks-outside-white-outline-3.png" style="height:220px;image-rendering: pixelated"/>
  <figcaption>Noticed and fixed black leaking outside the outline</figcaption>
</figure>

<p>
I think most people would be happy with fixing the bug, but I call this only a <em>partial</em> success. I fixed it but don't understand why my previous code was broken. And sadly, I hadn't checked in the buggy version, so I can't easily go back and study the cause. Without understanding the cause, I may end up making the same mistake again in a future project. If I run into this bug again I will want to study it more closely.
</p>

<p>
This was one of many bugs and mysteries I encountered in this project.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-08-27-sdf-font-outlines/"/>
    <published>2024-08-27T16:41:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-08-20-labels-on-maps</id>
    <title>Labels on Maps</title>
    <updated>2026-03-23T15:06:24.859338-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org4929690">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
My friend L recently mentioned that he hadn't seen any blog posts from me. It's true, I haven't posted for a while. Earlier this year I had explored <a href="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/">signed distance field fonts</a>, in particular using <a href="https://github.com/Chlumsky/msdfgen">Viktor Chlumský's multi-channel distance fields</a>. I really enjoyed the many experiments I did, and I learned a lot. I had <a href="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/">intended to use it in a real game project</a> but the timing wasn't right. So I put it away. Then before I got started on a new project, life happened. I had to take a break and attend to other things. I haven't been blogging because I haven't had projects to blog about.
</p>

<p>
Then in July I started thinking about other places I could apply the font code. I thought back to my <a href="https://simblob.blogspot.com/2019/11/procedurally-generating-annotations.html">2019 blog post about map annotations</a>, where I observed that even small amounts of text added to a procedurally generated map could make the map much more interesting. I started dreaming up a new map generator. Broadly,  the tasks would be along these four categories:
</p>

<ol class="org-ol">
<li>Procedurally generate a <strong>map</strong> that has interesting "point" features including chasms, volcanos, waterfalls, and towns.</li>
<li>Identify large scale "area" <strong>features</strong> on a map such as peninsulas, bays, and mountain ranges.</li>
<li>Generate <strong>names</strong> for both point and area features, tied into geography, history, cultures, etc.</li>
<li>Place <strong>labels</strong> on the map corresponding to these features.</li>
</ol>

<x:cut/>


<p>
Normally I'd approach this as separate steps, but I wanted to consider the possibility that these steps influence each other. For example what if I identify two adjacent mountain ranges that are each too small for a label? I could <em>modify</em> the map to turn these into one larger mountain range. Maybe I modify a label to match the size of the mountain range, or modify the mountain range to match the length of the label. Maybe I favor east-west oriented geographic features because it's easier to read the labels for them.
</p>

<p>
Before I got too far, I wanted to get a sense of what the final maps would look like. 
<strong>Seeing the end result can help me decide if this is going to be inspiring or mundane</strong>. 
For now, I can manually identify features and generate names to get started.
I wanted to quickly get <em>something</em> running, the equivalent of rendering a triangle in a 3D world. 
I used a static map image and then I drew text on top of it using my distance field font renderer from earlier this year:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2428-map-typography/blog/initial-label-test.png" alt=""/>
  <figcaption> Initial label test</figcaption>
</figure>


<p>
That's the "hello world" level for this project. The next step was to try text that I might actually want to render:
</p>

<figure class="w-auto">
<img style="width:50%" src="https://www.redblobgames.com/x/2428-map-typography/blog/map-outline-1.png"/>
<img style="width:50%" src="https://www.redblobgames.com/x/2428-map-typography/blog/map-outline-2.png"/>
<figcaption>Outlined labels on a map</figcaption>
</figure>

<p>
I decided <strong>yes, this would be a nice summer project</strong>.
</p>

<p>
The next step was to spend a lot of time reading about map labeling. I made notes for myself. I don't normally share these notes because interpreting them is dependent on what's in my head, but I'm going to share them here so you can get a sense of the kinds of notes I take while reading:
</p>

<div style="font: 80% var(--monospace),monospace;background-color:#eee;width:calc(var(--body-width) * 1.2)" class="notes" id="org6dec873">
<ul class="org-ul">
<li><a href="https://en.wikipedia.org/wiki/Automatic_label_placement">https://en.wikipedia.org/wiki/Automatic_label_placement</a> and also a paper evaluating algorithms suggests simulated annealing is best</li>
<li><a href="https://github.com/mapbox/polylabel">for areas</a> - polylabel</li>
<li><a href="https://web.archive.org/web/20100821084646/https://www.lojic.org/techhelp/pdfs/Positioning_Names_on_Maps.pdf">Positioning Names on Maps</a> - paper by Edward Imhof, 1975, paywall</li>
<li><a href="https://observablehq.com/@veltman/centerline-labeling">https://observablehq.com/@veltman/centerline-labeling</a> - @veltman's page used by Azgaar's project</li>
<li>heredragonsabound has many posts on this subject
<ul class="org-ul">
<li>mewo2's post <a href="http://mewo2.com/notes/terrain/">http://mewo2.com/notes/terrain/</a> → <a href="https://github.com/mewo2/terrain/blob/master/terrain.js#L854">code</a>
<ul class="org-ul">
<li>city labels are placed on one side of the city icon, with penalties for overlapping with other things</li>
<li>region labels get a score for each possible location, with overlaps decreasing score, bonus for being over land</li>
</ul></li>
<li><a href="https://heredragonsabound.blogspot.com/2017/04/use-force-layout-luke.html">https://heredragonsabound.blogspot.com/2017/04/use-force-layout-luke.html</a> also - <a href="https://imgur.com/gallery/ag8re">on imgur</a>
<ul class="org-ul">
<li>scott says mewo2's tries label positions but doesn't let them move around; he's using force layout instead</li>
</ul></li>
<li><a href="https://heredragonsabound.blogspot.com/2017/05/simulated-annealing.html">simulated annealing part 1</a> and <a href="https://heredragonsabound.blogspot.com/2017/04/some-initial-optimizations-for-label.html">part 2</a>
<ul class="org-ul">
<li>uses simulated annealing because force layout got stuck in local minima</li>
<li>tip: put margins on the left/right of labels so that they don't merge with an adjacent label</li>
<li>tip: save intermediate steps because sometimes with simulated annealing the final result is worse</li>
<li>tip: spend more time moving labels that are currently bad, and not the ones that look good already</li>
<li>tip: use a spatial hash to speed up the intersection tests</li>
</ul></li>
<li><a href="https://heredragonsabound.blogspot.com/2017/05/area-labels.html">labeling areas</a> is different than labeling points
<ul class="org-ul">
<li>some shapes are much harder to label, but he avoids making those shapes</li>
</ul></li>
<li><a href="https://heredragonsabound.blogspot.com/2017/09/labeling-coast-part-one.html">labeling coasts part 1</a>, <a href="https://heredragonsabound.blogspot.com/2017/09/labeling-coast-part-two.html">part 2,</a> <a href="https://heredragonsabound.blogspot.com/2017/10/labeling-coast-part-three.html">part 3</a>, <a href="https://heredragonsabound.blogspot.com/2017/10/labeling-coast-part-four.html">part 4</a>, part 5, part 6
<ul class="org-ul">
<li>measures the curvature of the coastline to decide what goes there (coast label vs bay)</li>
<li>labels along a curved path didn't look good in his experiments, but arcs worked ok</li>
<li>to make a mysterious area like "The Lost Coast", eliminate some of the features generated there</li>
<li>he also has a special "islands at the end of a peninsula" feature he wants to label</li>
<li>he's using code to detect existing peninsulas and bays; try the simplest thing, test it a lot, tweak it</li>
<li>bays can have multi-line labels</li>
<li>also label "points" as navigation aids</li>
</ul></li>
<li><a href="https://heredragonsabound.blogspot.com/2017/09/labeling-ocean-part-one.html">labeling oceans part 1</a> and <a href="https://heredragonsabound.blogspot.com/2017/09/labeling-ocean-part-two.html">part 2</a>
<ul class="org-ul">
<li>unlike other area labels, there's an implied extension of the ocean past the map border</li>
<li>places candidate circles in water, bonus for touching the map border, might be better if circle goes partly off the map</li>
<li>labels along an arc</li>
<li>arc curve should be pointed away from the center of the map</li>
<li>label text should be upright</li>
</ul></li>
<li>river path labels <a href="https://heredragonsabound.blogspot.com/2017/06/path-labels-part-one.html">part 1</a>, <a href="https://heredragonsabound.blogspot.com/2017/04/path-labels-part-two.html">part 2</a>, <a href="https://heredragonsabound.blogspot.com/2017/06/path-labels-part-three.html">part 3</a>, <a href="https://heredragonsabound.blogspot.com/2017/06/path-labels-part-four.html">part 4</a>, <a href="https://heredragonsabound.blogspot.com/2017/07/path-labels-part-five.html">part 5</a>, <a href="https://heredragonsabound.blogspot.com/2017/07/path-labels-part-six.html">part 6</a>, <a href="https://heredragonsabound.blogspot.com/2017/07/labels-postscript-part-seven.html">part 7</a>
<ul class="org-ul">
<li>looks for less-curvy location along the river</li>
<li>smooths out the path using <a href="https://bost.ocks.org/mike/simplify/">Visvalingam's Algorithm</a></li>
<li>offsets the label from the river</li>
<li>uses simulated annealing to rule out bad placements
<ul class="org-ul">
<li>uses the <a href="https://github.com/w8r/GreinerHormann">Greiner-Horman</a> polygon clipping algorithm to detect polygon intersection</li>
<li>the Martinez-Rueda can handle more cases</li>
<li><a href="https://github.com/mapbox/lineclip">Sutherland-Hodgman</a> is faster but more limited, and he uses it first before Greiner-Horman</li>
<li>getting the area of the polygon intersection worked better for overlap avoidance than a boolean yes/no intersection</li>
<li>he plotted number of iterations vs quality of results, and found most of the improvement happens at the beginning</li>
</ul></li>
<li>check the font metrics to make sure you're getting the bounding box you want</li>
<li>don't need to label all rivers; he labels the ones that are "substantially longer" than the river name, and only ones that are straight enough</li>
<li>Imhof's paper suggests adding more labels for the same river, above major fork points</li>
<li>he's using svg so he can reuse svg's text along path, zoom for debugging, and other svg features</li>
<li>using a rectangle approximation to the curved path led to labels not being placed right, so switched to a polygon approximation</li>
<li>fixing upside down labels was trickier than it seemed at first</li>
<li>sometimes it's ok to have labels drawn on top of graphics (forest, mountains), but masking/outlining can help make it stand out</li>
<li>to make labels look hand-drawn, he offsets/rotates characters occasionally</li>
</ul></li>
<li>island labels <a href="https://heredragonsabound.blogspot.com/2017/10/labeling-islands-part-one.html">part 1</a>, <a href="https://heredragonsabound.blogspot.com/2017/10/labeling-islands-part-two.html">part 2</a>, <a href="https://heredragonsabound.blogspot.com/2017/11/labeling-islands-part-three.html">part 3</a>
<ul class="org-ul">
<li>he generates groups of islands with a separate algorithm, not just single islands as part of noise generation</li>
<li>draws convex hull around them</li>
<li>wants a label for the whole group</li>
<li>had an issue where an island merged into the mainland, so the labels were wrong</li>
<li>when there are lots of islands, the labels are often too crowded</li>
</ul></li>
</ul></li>
<li><a href="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/">https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/</a>
<ul class="org-ul">
<li>instead of force layout or simulated annealing, tries a bunch of possibilities in one pass</li>
</ul></li>
</ul>

</div>

<p>
I'll <em>sometimes</em> take parts of my notes and turn them into a proper page, but most of the time I don't. I use text files instead of bookmark services because I want to be able to group things together in outline form. I hear good things about <a href="https://obsidian.md/">Obsidian</a> but I use emacs org-mode.
</p>

<p>
That's just for step 2 of the four steps I listed. There are also lots of things to learn for step 3 and step 4. After reading all of this, <strong>I felt overwhelmed</strong>. Scott's heredragonsabound blog is amazing, and you can see he's put <em>many years of work</em> into these problems. I wanted a summer project, not a multi-year project. I decided the scope of this project was too large for me with my current skill level. To level up, I should work on smaller projects first.
</p>

<p>
So I started a one week project to manually place and style labels on maps, focusing on step 4. That was six weeks ago. I'm still working on it. I've had many more bugs and unexpected rabbit holes than expected. I'll blog about them soon!
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-08-20-labels-on-maps/"/>
    <published>2024-08-20T12:25:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-05-31-font-distortion</id>
    <title>Font Distortion</title>
    <updated>2026-03-23T15:06:24.856662-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org4ba5c3a">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
<a href="https://www.redblobgames.com/blog/2024-03-30-text-effects/">I had previously blogged about text effects</a>, and how I accidentally discovered that I could alter the personality of the font by rendering a distorted shape. At the time, I was focused on text effects and didn't want to get distracted by this discovery. So I wrote it down for later.
</p>

<p>
Well, later came, and I decided to explore it:
</p>

<figure style="width: calc(1.25 * var(--body-width))">
  <video controls="true" style="width: fit-content; max-width: 100%">
    <source src="https://www.redblobgames.com/x/2414-font-distortion/blog/animated.mp4" type="video/mp4"/>
    <a href="https://www.redblobgames.com/x/2414-font-distortion/blog/animated.mp4">View the video (mp4)</a>
  </video>
</figure>

<x:cut/>


<p>
Here's the regular text rendering (with some spacing issues unrelated to this exploration):
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2414-font-distortion/blog/font-original.png" alt=""/>
  <figcaption>Original text</figcaption>
</figure>


<p>
Here's the effect I liked:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2414-font-distortion/blog/font-bottom-heavy.png" alt=""/>
  <figcaption>Bottom-heavy variant</figcaption>
</figure>


<p>
I had suspected that the problem was the way I turned a rectangle into two triangles, and some comments I got confirmed that. I wanted to see it for myself, so I painted the two triangles in different colors, and I added a grid:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2414-font-distortion/blog/two-triangles.png" alt=""/>
  <figcaption>Trapezoid is two triangles</figcaption>
</figure>


<p>
This diagram confirmed that the distortion was indeed because of the triangulation. I made an interactive test page where I could try out different triangulations to see the effects, and I found another I liked:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2414-font-distortion/blog/font-spooky.png" alt=""/>
  <figcaption>Spooky variant</figcaption>
</figure>


<p>
<a href="https://www.redblobgames.com/x/2414-font-distortion/">Try out the interactive test</a>. There's still more to explore, like what happens with serif fonts, or what happens with non-linear distortions.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-05-31-font-distortion/"/>
    <published>2024-05-31T18:09:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-05-05-wip-heuristics</id>
    <title>Work in progress: heuristics</title>
    <updated>2024-05-06T17:00:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
I have several pages that are unfinished because I can't find an explanation I'm happy with. Sometimes while trying to come up with an explanation, I realize <em>I don't actually understand the topic</em> as well as I thought! One of these topics is heuristics for the A* algorithm.While trying to understand the topic better, I came up with this example:
</p>

<figure class="w-full">
  <img width="313" src="https://www.redblobgames.com/pathfinding/heuristics/blog/differential-heuristic-top-1.png" alt="Diagram showing a nearby goal"/>
   
  <img width="316" src="https://www.redblobgames.com/pathfinding/heuristics/blog/differential-heuristic-top-2.png" alt="Diagram showing a faraway goal"/>
  <figcaption>Diagrams showing a nearby and faraway goal, with the same distance heuristic</figcaption>
</figure>

<x:cut/>


<p>
That wasn't the first attempt at an explanation. But I was still unhappy with it. I decided to remake the diagrams again. The problem I'm trying to demonstrate isn't specific to grids, so I decided to use a non-grid example. Conveniently, I had a non-grid example at the top of the <a href="https://www.redblobgames.com/pathfinding/a-star/introduction.html">A* page</a>, so I adapted it for this page. I wanted to display numbers on the map but they didn't fit in some of the smaller cells so I removed all the small cells.
</p>

<figure>
  <img src="https://www.redblobgames.com/pathfinding/heuristics/blog/comparing-true-distance-with-estimate.jpg" alt=""/>
  <figcaption> New diagram</figcaption>
</figure>


<p>
I rewrote not only the diagrams but also the introductory text. But I'm still not happy with it! I think the grid has some advantages for visualization. I'll try rewriting this section again at some point.
</p>

<p>
There's still a <em>lot</em> more to work on. The introductory section is just to set up the motivation. Section 2 is about basic insight for improving the heuristic. Section 3 is about the solution. Section 4 is the code. Then there are several more sections, including demos with maps from real games. 
</p>

<p>
I've been working on <a href="https://www.redblobgames.com/pathfinding/heuristics/differential.html">the differential heuristics page</a> since 2015 and I'm still not happy with it! Maybe I need to rethink my strategy, either by reducing the scope of the page or by planning to rewrite the page after I publish it. I love the idea — it's very little code (&lt;20 lines?), can produce a big speedup, and not limited to grid maps like some other A* optimizations. My <a href="https://www.redblobgames.com/pathfinding/heuristics/">general heuristics</a> page is in even worse shape. The demos on that page are completely broken. I feel bad that after all this time I still haven't finished these pages. I'll keep working on them (slowly).
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-05-05-wip-heuristics/"/>
    <published>2024-05-05T17:30:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-04-27-flow-field-pathfinding</id>
    <title>Flow field pathfinding</title>
    <updated>2024-04-27T14:40:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
You may know me for my <em>interactive</em> tutorials. But before that, I was writing visual but non-interactive tutorials. In particular, there wasn't a lot of information about A* on the web, so I decided to collect <a href="http://theory.stanford.edu/~amitp/GameProgramming/">all my notes about pathfinding</a> together in one place in the 1990s. But then in the 2010s I started making interactive pages. The newer pages are narrower in scope; I covered a broader set of topics on the older pages. I maintain both sets now.
</p>

<p>
Over the years people have asked me about "flow field pathfinding". I felt like the early papers about it conflated the flow fields with hierarchical pathfinding, but I wasn't sure, and I didn't want to write about it until I was sure.
</p>

<figure>
  <img src="https://www.redblobgames.com/pathfinding/tower-defense/blog/flow-field.png" alt=""/>
  <figcaption> Flow field</figcaption>
</figure>


<x:cut/>


<p>
But it's been many years now and I would like to write <em>something</em> even if it's not complete. My understanding so far is:
</p>

<ol class="org-ol">
<li><em>flow fields</em> are a vector field that tells agents from <em>any</em> location what direction to move to find a <em>single</em> destination</li>
<li>optionally, agents that are in between locations on the pathfinding graph can <em>interpolate</em> between the vectors in the flow field</li>
<li>optionally, a <em>hierarchy</em> of coarse and fine stepped fields can speed up pathfinding</li>
</ol>

<p>
In addition, there are <em>distance fields</em> which are themselves interesting for things like "<a href="https://www.roguebasin.com/index.php/Dijkstra_Maps_Visualized">Dijkstra maps</a>" in roguelike games and "<a href="https://www.gamedev.net/tutorials/programming/artificial-intelligence/the-core-mechanics-of-influence-mapping-r2799/">influence maps</a>" in strategy games. Distance fields can also be used for <a href="https://iquilezles.org/articles/distfunctions/">3D modeling</a> and <a href="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/">font rendering</a>. The flow fields and distance fields are related: in vector calculus, the gradient (∇) of the distance field produces the flow field.
</p>

<figure>
  <img src="https://www.redblobgames.com/pathfinding/tower-defense/blog/distance-field.png" alt=""/>
  <figcaption> Distance field</figcaption>
</figure>


<p>
The graph-based pathfinding algorithms I cover on my <a href="https://www.redblobgames.com/pathfinding/a-star/introduction.html">A* page</a> output <em>both</em> distance fields and flow fields, in the <code>cost_so_far</code> and <code>came_from</code> outputs. I updated my pages to mention both flow and distance fields:
</p>

<ul class="org-ul">
<li>I added a <a href="https://theory.stanford.edu/~amitp/GameProgramming/Variations.html#flow-fields">new section to my older pathfinding notes</a>.</li>
<li>I updated <a href="https://www.redblobgames.com/pathfinding/tower-defense/">my tower defense pathfinding page</a>.</li>
</ul>

<p>
Although I mention <a href="https://theory.stanford.edu/~amitp/GameProgramming/MapRepresentations.html#hierarchical">hierarchical pathfinding</a> in these notes, I don't cover it in detail. Maybe I will do so one day, but I don't have to wait until then before publish the pages. That's one of the advantages of web pages over textbooks or academic papers. I can update my pages as I learn more. I still update my pathfinding pages that I started in 1997, and I still update my interactive pathfinding pages that I started in 2014. 
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-04-27-flow-field-pathfinding/"/>
    <published>2024-04-27T13:50:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-04-17-draggable-examples</id>
    <title>Draggable examples</title>
    <updated>2024-04-17T17:00:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
On my pages I often want to be able to move an object around in a diagram using the mouse or touch. Last year I spent some time learning about browser mouse+touch events, and <a href="https://www.redblobgames.com/making-of/draggable/">wrote a page about event handlers</a> for dragging objects around. I hadn't realized it at the time, but it was only half the solution.
</p>

<figure>
  <img src="https://www.redblobgames.com/making-of/draggable/build/diagram-state-and-event-handlers.svg" alt=""/>
  <figcaption> Event handlers and state handlers</figcaption>
</figure>


<x:cut/>


<p>
What was missing was that the events should affect the application state. I had been writing this code for many years, but I never explained it on the event handler page. I decided to write some examples, starting with: drag events should change application state <kbd>{x,y}</kbd> which should change the position of an object on the screen, either using positioning or transforms:
</p>

<ol class="org-ol">
<li>svg <kbd>&lt;​circle cx="$x" cy="$y" /&gt;</kbd></li>
<li>svg <kbd>&lt;​circle transform="translate($x,$y)" /&gt;</kbd></li>
<li>html <kbd>&lt;​div style="position:absolute; left: $x; top: $y" /&gt;</kbd></li>
<li>html <kbd>&lt;​div style="transform: translate($x,$y)" /&gt;</kbd></li>
</ol>

<p>
The main idea is that <strong>the event handlers are independent of the state handlers</strong>. The event handlers do <em>not</em> know whether I'm using <kbd>position:absolute</kbd> or <kbd>transform:translate()</kbd>. The event handlers do not apply snap-to-grid or constraints, like they do in other libraries like jquery-ui-draggable or react-draggable. Instead, the state handler handles those things. That means the same event handlers can be used for a wide variety of dragging behaviors, including non-rectangular constraints and non-grid snapping. The event handlers can work for both svg and html. This structure gives me the flexibility I wanted for my projects. I made some examples to show that this separation allows the kinds of common features other libraries have:
</p>

<ol class="org-ol">
<li>Constrained drag (could be a rectangle, but could be any shape)</li>
<li>Snap to nearest point (could be a grid, but could be anything)</li>
<li>Drag with a handle (could be a rectangle, but could be anything)</li>
</ol>

<p>
The state handlers do <em>not</em> know whether I'm capturing mouse or touch or pointer events. Only the event handlers handle those things. That means the state handlers can also be connected to other systems, such as animation libraries or text input boxes or scripts.
</p>

<figure>
  <img src="https://www.redblobgames.com/making-of/draggable/blog/constrained-drag.png" alt=""/>
  <figcaption> Constrained drag with snapping</figcaption>
</figure>


<p>
The second idea I wanted to show on this page is that <strong>the same event handlers can be used when not dragging an object</strong>, so I made some examples:
</p>

<ol class="org-ol">
<li>"Scrub" a number input left and right to change it</li>
<li>Draw on a canvas</li>
<li>Draw on an svg grid</li>
<li>Drag a circle drawn inside a canvas, even though no DOM object is being dragged</li>
<li>Move and resize a rectangle using nested drag handlers</li>
</ol>

<figure>
  <img src="https://www.redblobgames.com/making-of/draggable/blog/canvas-drawing.png" alt=""/>
  <figcaption> Drawing on a canvas</figcaption>
</figure>


<p>
I thought making these examples would take a week or two, but I ended up spending a lot more time on it. I made a code viewer that showed the event and state handlers for each example. I made a custom highlighter that highlighted lines of code to pay attention to. I made a code generator that partially generated the example code, then ran it. I considered making the examples editable, but decided to instead link to live editable editables on jsfiddle and codepen.
</p>

<figure>
  <img src="https://www.redblobgames.com/making-of/draggable/blog/code-display.png" alt=""/>
  <figcaption> Displaying the code powering an example</figcaption>
</figure>


<p>
Taking longer than a week was ok. Some of my projects are <a href="https://en.wikipedia.org/wiki/Timeboxing">timeboxed</a>, and I give myself a week to work on them. I typically use timeboxing when I'm learning something. But this page isn't one of those. It's a reference page that I'm willing to spend a lot more time on. I'll come back to it as I learn more or better ways of doing things. I probably spent 4 weeks on this.
</p>

<p>
One thing I had wondered about was: <em>why isn't this a library?</em> I had presented it as a <strong>recipe</strong>, something to be copied and modified for each situation. But it could've been a library, right? But in making these examples I got to see that my own uses are so varied that a library would be overly complicated. The event handlers and state handlers are <em>not</em> fully independent, like I had thought they would/should be. So for now I'll leave it as a recipe, and revisit it later.
</p>

<p>
<a href="https://www.redblobgames.com/making-of/draggable/examples.html"><strong>Take a look at the examples</strong></a> and feel free to copy them and use them in your own projects!
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-04-17-draggable-examples/"/>
    <published>2024-04-17T15:00:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-04-13-testing-font-code</id>
    <title>Testing font code</title>
    <updated>2024-04-13T20:00:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org8c2128b">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
Earlier this year <a href="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/">I was trying to improve font rendering in some of my C++ projects</a>, and that led me down a rabbit hole of learning signed distance field (SDF) font rendering. I wanted to try out the SDF fonts in a real project. I occasionally help with <a href="https://gasgame.net/">Galactic Assault Squad</a> (GAS), especially "engine" code, so for Week 6 I decided to try SDF fonts there.
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/font-translucency-2.png" alt=""/>
  <figcaption> Fonts rendered in the game world</figcaption>
</figure>


<x:cut/>


<p>
The first day or two I was navigating the code to see how it was structured. Then I could figure out where I could add the SDF font renderer. I tested first by writing new C++ code and reusing the same shader from my JS experiments. It was a lot of hackery, including global variables scattered throughout, but I got some text on the screen:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/font-atlas.png" alt=""/>
  <figcaption> Font atlas</figcaption>
</figure>


<p>
The next step was to read the font atlas, which says <em>where</em> in the image each letter is. I started heading down a rabbit hole thinking about how best <em>optimize</em> that file format and code, but remembered that this is just an hacky proof of concept right now. I stopped trying to optimize and focused on getting something working.
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/one-glyph-2.png" alt=""/>
  <figcaption> One glyph extracted from the font atlas</figcaption>
</figure>


<p>
Along the way I ran into some differences in coordinate systems between my JS code and the C++ code:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/buggy-rendering.png" alt=""/>
  <figcaption> Buggy font rendering</figcaption>
</figure>


<p>
But that's fixed by flipping things upside down a few times. The next goal was to draw text <em>in</em> the game world. I decided to draw labels on roads:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/text-above-ground.png" alt=""/>
  <figcaption> Text above objects</figcaption>
</figure>


<p>
But it might also be interesting for text to be <em>on</em> the ground, underneath other objects (note that this was tested with a different font):
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/text-below-ground.png" alt=""/>
  <figcaption> Text below objects</figcaption>
</figure>


<p>
I also made a demo of text trailing behind a player:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/sponsored-by-nordvpn.png" alt=""/>
  <figcaption> Fake sponsorship message</figcaption>
</figure>


<p>
One thing I learned was that the text shadow scaling I put in place for my standalone experiments were <em>not</em> suitable when there's a busy game in the background. Compared to my previous experiments, I needed the small text to have a larger shadow and the large text to have a smaller shadow. That's exactly the reason I want to test things in a real project. Sometimes it's not obvious from a standalone experiment.
</p>

<p>
I compared the new renderer to the existing one:
</p>

<figure>
  <img src="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/screenshots/comparison-of-imgui-renderer-bottom-my-renderer-top-1.png" alt=""/>
  <figcaption> Text comparison</figcaption>
</figure>


<p>
I think the SDF renderer is really nice at large sizes but not necessarily better at small sizes. The main value of a new font renderer would not be in making the rendering <em>better</em>, but in allowing <em>new</em> ways to render fonts (shadows, glows, distortions, and other effects).
</p>

<p>
My goal for Week 6 was to try the SDF fonts in a real game project. I think that went well. In Week 7 I looked at what it would take to properly integrate the code into the game. Right now the development priority is combat and progression systems, not font rendering. Until we have some text to render in new ways, replacing the font renderer has low value. If it were easy to integrate, I could justify cleaning up the hacky proof of concept. However I concluded that it would take some restructuring of other rendering code. That makes it a high cost and low value. It's probably best to wait until we're refactoring for other reasons (lowering the cost) or until we want to render text with effects (raising the value).
</p>

<p>
In a real project, I think it's wise to evaluate the cost/benefit to the players, not adopt new things just because the tech is cool. So maybe <a href="https://gasgame.net/">Galactic Assault Squad</a> will get a new font renderer in the future, but not today.
</p>

<p>
With that, I am finished with my font rendering experiments. I have a different project to work on next.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-04-13-testing-font-code/"/>
    <published>2024-04-13T19:00:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-03-30-text-effects</id>
    <title>Text effects</title>
    <updated>2024-03-31T13:00:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org23335e6">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
In Week 4 of the year, I tried out various ways of using <em>distances</em> in signed distance field fonts. In Week 5 I wanted to do something different. I decided to explore what I could do treating each character as its own sprite and then applying sprite antimation. The week turned out to be <em>fun</em> but I didn't learn as much as I hoped I would. To start, I copied the code from the previous week so that I would have a working program right away. Then I removed things I didn't care about this week and added new code. This is like forking a project but for my weekly experiments I tend to copy the code instead of forking. 
</p>

<p>
During the initial experiments I made an interesting discovery:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2405-text-effects/blog/shapes-3.png" alt=""/>
  <figcaption>Varying sprite shape</figcaption>
</figure>


<p>
This is an ordinary font made more interesting by using a non-rectangular sprite.
</p>

<x:cut/>


<p>
This is what the font normally looks like:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2405-text-effects/blog/shapes-1.png" alt=""/>
  <figcaption>Rectangular sprite shape</figcaption>
</figure>


<p>
I had changed the shape from a rectangle to a trapezoid, thinking it'd be fun to see the bottom or top wider. But if you look closely, it's more than that. There are straight lines that <em>have become bent</em>. Look in particular near the bottom of the <kbd>h</kbd> or <kbd>p</kbd>, or the inside left of the <kbd>h</kbd>. What could cause this?
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2405-text-effects/blog/shapes-2.png" alt=""/>
  <figcaption>Trapezoid sprite shape</figcaption>
</figure>


<p>
<em>I don't know</em>. And I could spend time going down this rabbit hole, but my goal this week was to play with sprite animation. So I wrote it on a list of things to investigate later. And I made a list of things I did want to try:
</p>

<ul class="org-ul">
<li class="off"><code>[ ]</code> sink into ground</li>
<li class="off"><code>[ ]</code> rise out of ground</li>
<li class="off"><code>[ ]</code> bold</li>
<li class="off"><code>[ ]</code> slant</li>
<li class="off"><code>[ ]</code> rotate</li>
<li class="off"><code>[ ]</code> wavy (left/right) character</li>
<li class="off"><code>[ ]</code> shadow with variable distance</li>
<li class="off"><code>[ ]</code> fall from above, bounce when it hits the ground</li>
<li class="off"><code>[ ]</code> curved path</li>
<li class="off"><code>[ ]</code> break into smaller squares / dissolve</li>
<li class="off"><code>[ ]</code> horizontal spin</li>
<li class="off"><code>[ ]</code> flip up/down like a train station sign</li>
<li class="off"><code>[ ]</code> rotate left/right on a cube</li>
<li class="off"><code>[ ]</code> text rotates on a giant wheel like price of right</li>
<li class="off"><code>[ ]</code> apply any of the above effects with a ramp or sine wave pattern to some characters</li>
</ul>

<p>
I find that making a list like this helps me a lot. It keeps me from getting too distracted. Breaking things down into even smaller steps helps me become even more productive. So I went through the various ideas, one by one, and it was fun. But I didn't learn as much as I had hoped to. Most of these were routine animations, and doing them in code without some kind of tool was sometimes tedious. One of the more fun animations was the "War And Peace Gun" that shot out the text of <em>War And Peace</em>:
</p>

<figure>
  <video controls="true" style="width:fit-content;max-width:100%">
    <source src="https://www.redblobgames.com/x/2405-text-effects/blog/war-and-peace-gun.mp4" type="video/mp4"/>
    <source src="https://www.redblobgames.com/x/2405-text-effects/blog/war-and-peace-gun.webm" type="video/webm"/>

    <a href="https://www.redblobgames.com/x/2405-text-effects/blog/war-and-peace-gun.mp4">View the video (mp4)</a>
  </video>
</figure>

<p>
I also really enjoyed putting different animations in a sequence:
</p>

<figure>
  <video controls="true" style="width:fit-content;max-width:100%">
    <source src="https://www.redblobgames.com/x/2405-text-effects/blog/dancing.mp4" type="video/mp4"/>
    <source src="https://www.redblobgames.com/x/2405-text-effects/blog/dancing.webm" type="video/webm"/>

    <a href="https://www.redblobgames.com/x/2405-text-effects/blog/dancing.mp4">View the video (mp4)</a>
  </video>
</figure>

<p>
I didn't write up my experiments in nearly as much detail as the previous few weeks, but I saved some videos of the animations. 
<a href="https://www.redblobgames.com/x/2405-text-effects/">See the collection here</a>.
</p>

<p>
So that was it for my experiments with signed distance field font rendering. I spent three weeks on it and I learned a lot. I now have something I can use for future projects. In particular, I'd like to draw text on my procedurally generated maps. But that's a project for another day. The next week (Week 6) I tried putting this knowledge into practice by integrating it into a C++ game.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-03-30-text-effects/"/>
    <published>2024-03-30T09:15:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-03-27-distance-field-effects</id>
    <title>Distance field effects</title>
    <updated>2024-03-28T09:50:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org3acac74">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
In the <a href="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/">last post</a> I described how I fell into the font rendering rabbit hole. 
I try to put some time limits on each topic — otherwise I would explore forever! I try to pick a theme each week:
</p>

<ul class="org-ul">
<li>Week 3 was the basics: SDF, MSDF, atlas, shader</li>
<li>Week 4 was effects on individual glyphs: outlines, shadows, glow, bevel</li>
<li>Week 5 was effects on how glyphs move: sink, rise, bold, slant, rotate, wavy, bounce, warp</li>
</ul>

<p>
The key idea I wanted to explore this week is that a distance field font can be thought of as contour lines:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2404-distance-field-effects/blog/contour-8.png" alt=""/>
  <figcaption>Contour line representation of a glyph</figcaption>
</figure>


<p>
When rendering a font normally like I did last week, I considered <kbd>distance &lt; 0 ? "white" : "transparent"</kbd>. But there are so many more things to do with this distance!
</p>

<x:cut/>


<p>
The first thing I wanted to do is outlines. This is roughly <kbd>distance &lt; 0 ? "white" : distance &lt; 0.1 ? "black" : "transparent"</kbd>:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2404-distance-field-effects/blog/basic-outline-large-msdf.png" alt=""/>
  <figcaption>Font rendering with outline</figcaption>
</figure>


<p>
<em>I think this looks beautiful!</em>
</p>

<p>
A variant is to apply blur to make this into a shadow, or apply blur <em>and</em> a bright color to make it into a glow effect. But I noticed that MSDF vs SDF makes a difference. SDF produces rounded corners, and I think the outlines don't look nearly as good as with MSDF:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2404-distance-field-effects/blog/basic-outline-large-sdf.png" alt=""/>
  <figcaption>Font rendering with outline</figcaption>
</figure>


<p>
I wanted to see the difference, so I made this to highlight the pixels where they were different:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2404-distance-field-effects/blog/sdf-vs-msdf.png" alt=""/>
  <figcaption>Round vs sharp shapes</figcaption>
</figure>


<p>
For some effects like shadow and blur I wanted the rounded corners, and for others like outlines I wanted the sharp corners. So I experimented with using both.
</p>

<p>
I played with lots of other things too, including taking the <em>gradient</em> of the distance field to get angles:
</p>

<figure>
  <img src="https://www.redblobgames.com/x/2404-distance-field-effects/blog/gradient-direction-colored.png" alt=""/>
  <figcaption>Visualization of angles</figcaption>
</figure>


<p>
This gave me a way to apply lighting and make a bevel effect.
</p>

<p>
I learned a lot in Week 4. I had <em>lots of bugs</em> and I generally found it hard to debug shaders compared to cpu code. Sometimes I didn't have the right frame of mind to fix a bug, so I continued with other experiments until I had a new idea, then came back to the bug and fixed it. <a href="https://www.redblobgames.com/x/2404-distance-field-effects/"><strong>I wrote up my notes</strong></a> originally for myself, and then cleaned them up for sharing. I had more I wanted to do, but the week was over, so I wrapped up and moved on to the next topic in Week 5.
</p>


</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-03-27-distance-field-effects/"/>
    <published>2024-03-27T10:30:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-03-21-sdf-fonts</id>
    <title>Signed Distance Field Fonts</title>
    <updated>2024-03-25T00:00:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><aside id="org2a3afa0">
<p>
Also see my <strong><a href="https://www.redblobgames.com/articles/sdf-fonts/">guide to SDF font rendering</a>.</strong> (2026)
</p>
</aside>

<p>
Each week I pick one or two things to work on. In week 2 of this year, I decided I should update my <a href="https://github.com/redblobgames/helloworld-sdl2-opengl-emscripten/">"hello world" OpenGL+Emscripten code</a> from 2015. It's boilerplate I use occasionally in other projects. It wasn't compiling anymore, and I wanted to fix that as well as several other things.
</p>

<p>
One of the unsolved issues in that starter code was that the fonts never looked good. I was using the <a href="https://github.com/nothings/stb/blob/master/stb_truetype.h">stb_truetype</a> library, and got this output:
</p>


<figure id="org5423050">
<img src="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/stb-before.jpg" alt="screenshot of stb-truetype rendering, with my old code"/>

</figure>

<p>
Look at how <kbd>H</kbd> and <kbd>e</kbd> are too far apart, and <kbd>S</kbd> and <kbd>D</kbd> are too close together. I tried various things but couldn't figure it out, so I had two workarounds at the time:
</p>

<ol class="org-ol">
<li>Use Omar Cornut's <a href="https://github.com/ocornut/imgui">Dear ImGui</a>, which has nice looking font rendering.</li>
<li>Use a monospace font to hide the spacing problems.</li>
</ol>

<x:cut/>


<p>
I decided to look into it again. After all, Dear ImGui <em>uses <code>stb_truetype</code></em>, so why would my output be so different? After looking through the docs and also several examples, I concluded that my code was all wrong. I tried kerning but that wasn't the issue. I tried a few different fonts but that wasn't the issue. <a href="https://github.com/nothings/stb/issues/281">One issue on stb_truetype</a> suggests "left side bearing". I found <a href="https://github.com/justinmeiners/stb-truetype-example.git">example code from Justin Meiners</a> that rendered nicely, and it was using left side bearing. But Dear ImGui <em>doesn't</em> use it. And <a href="https://github.com/breuckelen/stb-truetype-opengl-examples/blob/master/Example1.cpp">example code from Benjamin Attal</a> didn't use it either. I spent many hours studying the docs and examples before I figured out how to <a href="https://github.com/redblobgames/helloworld-sdl2-opengl-emscripten/commit/ba7da2e11aaea638b7bd2858f9bf3300d93f1b3c">rewrite my code</a>. It looks better now (although still not great — look at the gap between <kbd>m</kbd> and <kbd>o</kbd>):
</p>


<figure id="orgfe7de68">
<img src="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/stb-after.jpg" alt="screenshot of stb-truetype rendering, with my new code"/>

</figure>

<p>
This awakened my desire to play with font rendering. In particular, back in 2016 I bookmarked <a href="https://github.com/Chlumsky/msdfgen">https://github.com/Chlumsky/msdfgen</a> with the note:
</p>

<blockquote>
<p>
Multi-channel distance fields can represent sharp corners, unlike Valve's original paper, which had rounded corners. This is C++ code to implement the algorithm described here: <a href="https://computergraphics.stackexchange.com/questions/306/sharp-corners-with-signed-distance-fields-fonts/2151#2151">https://computergraphics.stackexchange.com/questions/306/sharp-corners-with-signed-distance-fields-fonts/2151#2151</a> (there's also a thesis paper)
</p>
</blockquote>

<p>
So I decided week 3's project would be to try that out! It was a <em>great week</em>. I learned a lot. 
</p>


<figure id="orga60dd5c">
<img src="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/msdf-fonts.png" alt="screenshot of msdf rendering using msdfgen's library"/>

</figure>

<p>
Along the way I kept <a href="https://www.redblobgames.com/x/2403-distance-field-fonts/">some notes</a> about what commands I ran and what I learned. <strong>These notes were originally meant for myself only</strong> but I'm making them available because I think sometimes people don't realize what my learning process is like. My articles especially present everything in the "final form" and readers might think I just write code like that from the start. I don't! I have <em>lots</em> of bugs and dead ends and misconceptions along the way. I find that keeping a journal (for myself) helps me work through them.
</p>

<p>
Some high level notes:
</p>

<ul class="org-ul">
<li>Signed Distance Fields are cool.</li>
<li>There are some things I don't understand about gamma correction, supersampling, outlines, antialiasing, etc. I experimented some but I feel like I still don't understand these topics well.</li>
<li>Towards the end of the week there were several things I felt like I <em>should</em>  have understood much earlier, but didn't.</li>
<li>The code to <em>generate</em> the MSDF files may be complex, but the code to <em>render</em> is incredibly simple, probably under 50 lines.</li>
<li>I should take <em>lots</em> of screenshots and make lots of notes when I'm learning something.</li>
</ul>

<p>
Take a look <a href="https://www.redblobgames.com/x/2403-distance-field-fonts/">my notes</a>, as well as an interactive demo I used for testing my knowledge. Random tidbit: the <code>2403</code> in the url means year=20<code>24</code> week=<code>03</code>. I've been using this naming scheme since 2015 for my weekly projects.
</p>

<p>
After playing with fonts week 2 and week 3, I still had more I wanted to do. But I am trying to keep to a project-per-week rhythm, so I decided to wrap up the basic font rendering and move on. In week 4 I'll explore outlines and other effects. That's the next blog post.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-03-21-sdf-fonts/"/>
    <published>2024-03-23T00:00:00-07:00</published>
  </entry>
  <entry>
    <id>tag:redblobgames.com,2011:blog:2024-03-08-new-blog</id>
    <title>New Blog for 2024</title>
    <updated>2024-03-22T00:00:00-07:00</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<section xmlns:x="http://local/"><p>
For some time now I've been unhappy with how much more friction there is when posting to <a href="https://simblob.blogspot.com/">my blog</a> than posting <a href="https://twitter.com/redblobgames">to twitter</a>. I keep wanting to blog more but I don't. Part of the problem is content. I did blog more in 2018, when I was working on projects that had more to share. Part of the problem is expectations. On Twitter it's expected that I write very little (280 character limit, up to 4 images or 1 animation). That constraint makes it easier to post. On my blog I tend to write longer more involved posts. But part of the problem is the process. My posts on Twitter take a lot less effort than my posts on Blogger. The Blogger UI <a href="https://blogger.googleblog.com/2020/05/a-better-blogger-experience-on-web.html">got a lot worse in 2020</a>, but it was already inconvenient for me.
</p>

<p>
So I've been thinking about what I actually want, and what I want isn't Twitter <em>or</em> Blogger. I want something much closer to Hugo or Jekyll — a static site generator. I want to be able to save a <em>file</em> and have it become a blog post. I want to be able to <em>grep</em> over my existing files. I want to be able to write a <em>perl script</em> to fix something across pages. I decided a goal in 2024 is to switch from Blogspot to a static site generator.
</p>

<x:cut/>


<p>
But when I looked at Hugo and Jekyll I started thinking of more things I wanted. In particular, I want to integrate into the rest of my site, which <em>isn't</em> Markdown, but instead a messy mix of technologies that I've worked with over the past nearly 30 years of having a web site. I <em>already</em> have a static site generator. I use Emacs Org-Mode instead of Markdown. Hugo <a href="https://gohugo.io/content-management/front-matter/#emacs-org-mode">does support Org-Mode</a>, and <a href="https://orgmode.org/manual/Publishing.html">Org-Mode has a way to publish posts</a>, and <a href="https://github.com/bastibe/org-static-blog/">there are Org-Mode blogging packages</a>. But I don't <em>always</em> use Org-Mode. I sometimes use Markdown. I sometimes use XHTML. And I sometimes use other formats. Also I have my own templating system based on XSLT. So either:
</p>

<ol class="org-ol">
<li>I use Hugo/Jekyll/etc and <em>replicate</em> my template and other aspects in their format, which means I need to maintain two versions.</li>
<li>I write my own blog software, which I have to maintain.</li>
</ol>

<p>
But what is <em>actually</em> involved in writing my own blog software? I already have the markup and templating and comments. What's missing is:
</p>

<ul class="org-ul">
<li>Atom/RSS feed</li>
<li>Home page showing the most recent posts</li>
<li>Browse by category</li>
<li>Browse by date</li>
</ul>

<p>
Of these, I already want to add browse by category and browse by date to the rest of the site, so the main thing I need to add is an Atom/RSS feed and a home page listing the posts.
</p>

<p>
Well, I decided to try it.
</p>

<ol class="org-ol">
<li>Each blog post will be in <kbd>/blog/YYYY-MM-DD-slug/index.html</kbd> → no extra work</li>
<li>I'll use the templating system I already use for the rest of the site → no extra work</li>
<li>When I save a file (markdown or org-mode) it will automatically update the html → no extra work</li>
<li>A short Python script will find all <kbd>/blog/*/index.html</kbd> files and generate the feed (xml) and home page (html) → some work</li>
<li>The home page will use my standard page template → no extra work</li>
<li>Each blog post will have comments like any other page on my site → no extra work</li>
</ol>

<p>
"How hard could it be?" → famous last words, right? I'll see how it goes. I'm using <a href="https://feedgen.kiesow.be/">python-feedgen</a> and <a href="https://docs.python.org/3/library/xml.etree.elementtree.html">ElementTree</a>, and I think it will be around 200 lines of Python code to implement the blog, assuming nothing goes wrong.
</p>

</section>

</div>
    </content>
    <link href="https://www.redblobgames.com/blog/2024-03-08-new-blog/"/>
    <published>2024-03-08T09:00:00-08:00</published>
  </entry>
</feed>
