Ryan Thomson2023-12-30T00:00:00ZRyan Thomsonhttps://www.ryanthomson.netYou Shouldn’t Call window.open() Asynchronouslyhttps://www.ryanthomson.net/articles/you-shouldnt-call-window-open-asynchronously/2023-12-30T00:00:00Z2023-12-30T00:00:00Z<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/open"><code>Window.open()</code></a> function lets one webpage open another page in a new window or tab. This ability was, of course, immediately abused to serve popup ads, so browsers now have popup-blockers that try to detect and block that kind of behavior. If you’re using this API, you <em>need</em> to design around the heuristics of the popup-blocker.</p>
<figure>
<blockquote>
<p>Modern browsers have strict popup blocker policies. Popup windows must be opened in direct response to user input, and a separate user gesture event is required for each <code>Window.open()</code> call. This prevents sites from spamming users with lots of windows.</p>
</blockquote>
<figcaption>MDN Web Docs</figcaption>
</figure>
<p>The particulars of exactly how the popup-blocker works is a browser-specific implementation detail, but in general <code>Window.open()</code> should only be called with user input event (e.g. <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event"><code>click</code></a>) in the call stack. However, when you start throwing asynchronous operations into the mix, like fetching a URL from the server before opening it, the browser has a tendency to get confused and block the new window anyways.</p>
<p>Here’s a naive implementation that can exhibit this behavior:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">handleButtonClicked</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> targetUrl <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getUrlFromServer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span>targetUrl<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>This results in a suboptimal user experience where the user needs to explicitly approve the new window, turning one click into two.</p>
<p>I’ve found that taking a slightly different approach avoids this problem.</p>
<p>Instead of trying to open the window asynchronously, you can open a blank window <em>synchronously</em> in the event handler, then asynchronously alter its contents. This is possible because <code>Window.open()</code> returns a <a href="https://developer.mozilla.org/en-US/docs/Glossary/WindowProxy"><code>WindowProxy</code></a> instance (if the new window opens successfully), which can be used to manipulate the newly-opened window. In this case, we just need to alter its <code>location</code> to navigate it to a different URL.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">handleButtonClicked</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> newWindow <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>newWindow<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">getUrlFromServer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">targetUrl</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
newWindow<span class="token punctuation">.</span>location<span class="token punctuation">.</span>href <span class="token operator">=</span> targetUrl<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
newWindow<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>By default, the new window will open with <code>about:blank</code>. If the async operation is expected to take more than a fraction of a second, then I’d recommend initially populating the new window with some loading indicator.</p>
<p>An important caveat to this approach is that you cannot set the <code>noopener</code> or <code>noreferrer</code> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/open#windowfeatures">window features</a>, since providing either of those causes <code>Window.open()</code> to return <code>null</code> instead of a <code>WindowProxy</code> instance as usual. If you’re opening a page with potentially untrusted content, then one workaround for this is to manually set <code>newWindow.opener</code> to <code>null</code>.</p>
Respect the Userhttps://www.ryanthomson.net/articles/respect/2023-12-17T00:00:00Z2023-12-17T00:00:00Z<p>Please briefly indulge me as I complain about how Bad Thing is bad.</p>
<p>Popups suck. Whether it’s a desperate plea for newsletter signups, pestering for a paid upgrade, or just a plain ol’ ad, there’s nothing more instantaneously annoying than something rudely intruding between you and the thing you were trying to look at. But there’s a special place in UI Design Hell for the popups that try to hide a their close button— doubly so if it’s on a touchscreen, where there’s no chance that your big dumb finger is going to be able to nail that tiny little button on the first try.</p>
<p>Most will just file this experience under “mild annoyance” and move on, but I’d like to take a moment to point out the inherent weirdness in this situation.</p>
<p>The purpose of a button is to be pressed (duh). Go find any half-decent set of <a href="https://developer.apple.com/design/human-interface-guidelines/buttons">user interface design guidelines</a> and you’ll see plenty of recommendations for making buttons clear and easy to use. And yet here someone has gone and made a button that’s notably difficult to press. It’s not like this was an accident; someone make their user interface <em>intentionally</em> bad.</p>
<hr />
<p>If I were to suggest a golden rule for software, it’d be <strong>respect the user.</strong></p>
<p>For software, what that ultimately means is respecting their agency. If the user wants to do something, then the software should just do it— no more, no less, and no backtalk. Disrespectful software, then, is software that tries to manipulate or subvert the user’s intent.</p>
<p>Look for it, and you’ll see that disrespectful design is everywhere.</p>
<ul>
<li>Websites that obnoxiously beg you to stop what you’re doing and download their mobile app instead (the term for this is a <a href="https://daringfireball.net/linked/2022/08/02/banish">“dickpanel”</a>)</li>
<li>Commonly-used functionality moved to out-of-the-way corners of the UI (either to discourage its use or to get you to look at something else along the way)</li>
<li>Autoplaying videos which forcibly follow you down the page as you scroll, because <em>there is no escape</em></li>
<li>Convoluted, multi-step, labyrinthine opt-out procedures
<ul>
<li><a href="https://old.reddit.com/r/pcmasterrace/comments/4m63i5/my_experience_of_deactivating_g2a_shield_stay/">The worst example I’ve ever seen</a> hilariously includes the phrase “we respect your opt-out decision” somewhere around the halfway point.</li>
</ul>
</li>
<li>Websites that <em>technically</em> don’t require an account to use, but arbitrarily degrade the experience if you aren’t signed in</li>
<li>Useful push notifications bundled in the same setting as spammy promotional notifications</li>
<li>Basically anything on <a href="https://www.deceptive.design/">deceptive.design</a></li>
</ul>
<p>How does this happen? While there’s plenty of just plain bad software out there, disrespectful software is different. This isn’t the result of incompetent developers, or a team that’s in way over their head, or even just an honest mistake.</p>
<p>Someone, somewhere, is just being a dick.</p>
<hr />
<p>Running any piece of software, or even just visiting a webpage, is a display of trust. When you do so, you are temporarily loaning its author your computer’s hardware.</p>
<p>There’s no intrinsic reason that a computer has to do what you tell it to do. People don’t control computers, software does— and chances are, you didn’t write it. But we’ve all generally agreed that it would be a bad thing if the computer deletes a file without you clicking the “delete” button first, so software generally does what you tell it to. But that’s more of a social contract than innate requirement, and sometimes software does things that you most definitely don’t want it to do. Normally we call that “malware”.</p>
<p>Disrespectful software isn’t malware, but it only just barely toes that line. It won’t forcibly make the choice for you, but it will try to “subtly discourage” you from choosing the “wrong option”. It maintains the facade of legitimate software, acting like its creators truly do respect and value you as a user, but they don’t— they just don’t want to get caught looking that way.</p>
<p>“Sign up for my newsletter or I won’t let you see the rest of my website” is still enough to turn most away. But hiding an obscure little “No thanks” link lends you just enough legitimacy to get by.</p>
<p>There’s just such a strangely uncomfortable feeling that comes from dealing with this kind of software, like you’re feeling the machine’s will press against your own. It’s a reminder that you’re only nominally in control.</p>
Big Hyperlinks the Right Wayhttps://www.ryanthomson.net/articles/big-hyperlinks-right-way/2023-11-12T00:00:00Z2023-11-12T00:00:00Z<p>Something I’ve often had to do while building webpages is to turn a large block of content into a link. The obvious solution is to just slap an <code><a></code> tag (or perhaps a <code><button></code>) around it and call it a day, but I’d argue that’s usually the wrong approach.</p>
<p>For starters, from an accessibility/semantics standpoint, the link’s text content should serve as a descriptive label for its destination. If you wrap a whole block of content in an <code><a></code> tag, then you’re probably including too much in that description. Plus, you might be creating invalid HTML. <a href="https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element">The specification</a> explicitly forbids nesting <a href="https://html.spec.whatwg.org/multipage/dom.html#interactive-content-2">“interactive content”</a> within <code>a</code> elements, so links within links are a no-go. There are also many elements that don’t allow an <code>a</code> element as a direct child, so you can’t wrap a <code><li></code> in an <code><a></code> tag, for example.</p>
<p>The next-most-obvious solution would be to reach for a little JavaScript: just attach a <code>click</code> event handler to the container and handle the navigation yourself. That’s a perfectly valid approach, if you don’t mind a little unnecessary JavaScript. Oh, and you remembered to reimplement all of the browser’s built-in link-handling behavior, right?</p>
<p>In this post I’d like to share my pure-CSS approach to the problem.</p>
<hr />
<p>Let’s start with this little post component:</p>
<iframe src="https://www.ryanthomson.net/embeds/articles/big-hyperlinks-right-way-1/" width="560" height="180" class="embed"></iframe>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>item<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>item-image<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>item-info<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>item-title<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Lorem Ipsum Dolor Sit Amet<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>item-byline<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Submitted by <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Lorem Ipsum<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span></code></pre>
<pre class="language-css"><code class="language-css"><span class="token selector">.item</span> <span class="token punctuation">{</span>
<span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span>
<span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
<span class="token property">column-gap</span><span class="token punctuation">:</span> 0.625rem<span class="token punctuation">;</span>
<span class="token property">padding</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span>
<span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.item-image</span> <span class="token punctuation">{</span>
<span class="token property">border-radius</span><span class="token punctuation">:</span> 6px<span class="token punctuation">;</span>
<span class="token property">width</span><span class="token punctuation">:</span> 3.5rem<span class="token punctuation">;</span>
<span class="token property">height</span><span class="token punctuation">:</span> 3.5rem<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.item-title</span> <span class="token punctuation">{</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> 1.25rem<span class="token punctuation">;</span>
<span class="token property">font-weight</span><span class="token punctuation">:</span> 600<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.item-byline</span> <span class="token punctuation">{</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> 0.875rem<span class="token punctuation">;</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">hsl</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0%<span class="token punctuation">,</span> 60%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">margin-top</span><span class="token punctuation">:</span> 0.375rem<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Our ultimate goal here is to make the entire component into a link to the post. Because there’s a second link for the post’s author, we can’t just wrap the whole component with an <code><a></code> tag, since then we would be nesting interactive elements. Besides, we don’t want to mess with the current semantics of our markup.</p>
<p>So instead of trying to change our markup, let’s approach this as a presentation problem. Using CSS, we can expand the link to visually (or in this case, invisibly) cover the entire component without changing the markup. To do this, we’ll add a <code>::before</code> <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements">pseudo-element</a> to the first <code><a></code> element, then use <code>position: absolute</code> to stretch it across the entire container, which we’ll also add <code>position: relative</code> to. Don’t forget to match the <code>border-radius</code>!</p>
<pre class="language-diff-css"><code class="language-diff-css"><span class="token unchanged language-css"><span class="token prefix unchanged"> </span><span class="token selector">.item</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">column-gap</span><span class="token punctuation">:</span> 0.625rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">padding</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span>
</span><span class="token inserted-sign inserted language-css"><span class="token prefix inserted">+</span> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span>
</span><span class="token unchanged language-css"><span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
<span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span><span class="token selector">.item-image</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 6px<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">width</span><span class="token punctuation">:</span> 3.5rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">height</span><span class="token punctuation">:</span> 3.5rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
<span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span><span class="token selector">.item-title</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">font-size</span><span class="token punctuation">:</span> 1.25rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">font-weight</span><span class="token punctuation">:</span> 600<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
</span><span class="token inserted-sign inserted language-css"><span class="token prefix inserted">+</span>
<span class="token prefix inserted">+</span><span class="token selector">.item-title :any-link::before</span> <span class="token punctuation">{</span>
<span class="token prefix inserted">+</span> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span>
<span class="token prefix inserted">+</span> <span class="token property">inset</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token prefix inserted">+</span> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">''</span><span class="token punctuation">;</span>
<span class="token prefix inserted">+</span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span>
<span class="token prefix inserted">+</span><span class="token punctuation">}</span>
</span><span class="token unchanged language-css"><span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span><span class="token selector">.item-byline</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">font-size</span><span class="token punctuation">:</span> 0.875rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">hsl</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0%<span class="token punctuation">,</span> 60%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">margin-top</span><span class="token punctuation">:</span> 0.375rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
</span></code></pre>
<p>However, we’ve just introduced a pretty serious problem with this component— the second link isn’t clickable anymore. Because our pseudo-element is sitting on top of everything else in the component, none of the other elements can receive mouse events.</p>
<p>Check out this version with the bounding boxes visualized:</p>
<iframe src="https://www.ryanthomson.net/embeds/articles/big-hyperlinks-right-way-2/" width="560" height="180" class="embed"></iframe>
<p>To get around this, we need to make sure any other interactive elements are stacked above the pseudo-element. We could just directly target the <code>.item-byline</code> element, but to avoid tightly coupling the CSS to the specific structure of the HTML I’ll write the code to handle links that appear anywhere in the component.</p>
<p>One thing we need to make careful not to do is apply <code>position: relative</code> to the title link (or any of its ancestors), since otherwise the pseudo-element would end up being positioned relative to that rather than the outer container. The selector for this used to be really tricky to write cleanly, but thanks to the improvements to <code>:not()</code> in <a href="https://www.w3.org/TR/selectors-4/">Selectors Level 4</a> we can easily express a “not a descendant of” constraint.</p>
<pre class="language-diff-css"><code class="language-diff-css"><span class="token unchanged language-css"><span class="token prefix unchanged"> </span><span class="token selector">.item</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">column-gap</span><span class="token punctuation">:</span> 0.625rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">padding</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
<span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span><span class="token selector">.item-image</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 6px<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">width</span><span class="token punctuation">:</span> 3.5rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">height</span><span class="token punctuation">:</span> 3.5rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
<span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span><span class="token selector">.item-title</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">font-size</span><span class="token punctuation">:</span> 1.25rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">font-weight</span><span class="token punctuation">:</span> 600<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
<span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span><span class="token selector">.item-title :any-link::before</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">inset</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">''</span><span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
<span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span><span class="token selector">.item-byline</span> <span class="token punctuation">{</span>
<span class="token prefix unchanged"> </span> <span class="token property">font-size</span><span class="token punctuation">:</span> 0.875rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">hsl</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0%<span class="token punctuation">,</span> 60%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span> <span class="token property">margin-top</span><span class="token punctuation">:</span> 0.375rem<span class="token punctuation">;</span>
<span class="token prefix unchanged"> </span><span class="token punctuation">}</span>
</span><span class="token inserted-sign inserted language-css"><span class="token prefix inserted">+</span>
<span class="token prefix inserted">+</span><span class="token selector">.item :any-link:not(.item-title *)</span> <span class="token punctuation">{</span>
<span class="token prefix inserted">+</span> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span>
<span class="token prefix inserted">+</span> <span class="token property">z-index</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span>
<span class="token prefix inserted">+</span><span class="token punctuation">}</span>
</span></code></pre>
<p>Here’s the final version:</p>
<iframe src="https://www.ryanthomson.net/embeds/articles/big-hyperlinks-right-way-3/" width="560" height="180" class="embed"></iframe>
<p>There is one major downside with this approach, which is that it prevents any elements behind the link from receiving hover events, and therefore renders the <code>:hover</code> selector useless. Unfortunately CSS does not have any way to separately control handling of “click” and “hover” events at the moment.</p>
We Need a New Paradigm for the Social Webhttps://www.ryanthomson.net/articles/we-need-new-paradigm-social-web/2023-09-25T00:00:00Z2023-09-25T00:00:00Z<p>Earlier this year, Twitter— er, “𝕏”, <a href="https://twitter.com/XDevelopers/status/1615405842735714304">suddenly killed off access</a> for third-party clients and substantially hiked its API pricing. More recently, Reddit pulled <a href="https://old.reddit.com/r/apolloapp/comments/144f6xm/apollo_will_close_down_on_june_30th_reddits/">effectively the same stunt</a>, inciting a short-lived rebellion that saw thousands of subreddits shutting their doors. Granted, Reddit’s pitiful 30-day notice was still 30 days longer than Twitter’s, but that doesn’t make the whole affair any less <a href="https://old.reddit.com/r/reddit/comments/145bram/addressing_the_community_about_changes_to_our_api/">horrendously communicated</a>, on top of just being a rotten idea. What’s clearly observed from both of these companies is the desire to clamp down on their user’s data, to ensure you can only interact with <em>their</em> website on <em>their</em> terms.</p>
<p>It’s hard not to feel a sense of impending doom hanging over every online service we use nowadays: they start out as nice places to be, but over time become increasingly user-hostile until (if you’re lucky) enough people jump ship to make an alternative viable. This is the cycle of <a href="https://www.wired.com/story/tiktok-platforms-cory-doctorow/">enshittification</a>.</p>
<figure>
<blockquote>
<p>Here is how platforms die: First, they are good to their users; then they abuse their users to make things better for their business customers; finally, they abuse those business customers to claw back all the value for themselves. Then, they die.</p>
</blockquote>
<figcaption>Cory Doctorow</figcaption>
</figure>
<p>For today’s average tech startup, enshittification is in the standard playbook. Initially, the fledgeling startup’s number one priority is acquiring maximum market share while burning through mountains of investor cash. Only once they’ve achieved a dominant market position do they start squeezing their users in a desperate attempt to become profitable.</p>
<p>The key to enshittification is that it only works when your customers can’t leave. One way to do that is to be the only game in town. But for social media, the lock-in is based on network effects. For any website that relies on user-generated content, there’s a certain critical mass of users required for the site to be practically functional. Below that threshold, your site is basically a dead mall. Plus, leaving a social media platform is hard because you can’t take your stuff with you. You can pick up your files and move them from one cloud storage provider to another, but you can’t do the same with the people you follow. And so we arrive at the modern internet: <a href="https://twitter.com/tveastman/status/1069674780826071040">five giant websites filled with screenshots of the other four</a>.</p>
<p>Resilient as they may be, enshittified platforms can fall. Digg was replaced by Reddit. Discord displaced Skype. But this isn’t a solution, just a short reprieve until the usurper inescapably repeats all the sins of its predecessor. This failure mode is an inevitability of the current paradigm. We need a different model for the social web.</p>
<p>Do you know what has never, and will never be enshittified? Web feeds.</p>
<p><a href="https://en.wikipedia.org/wiki/Web_feed">Web feeds</a> are a fantastically useful bit of tech that’s tragically obscure nowadays, so I’ll provide a brief overview. A <dfn>web feed</dfn> is a standard machine-readable format (such as <a href="https://en.wikipedia.org/wiki/RSS">RSS</a>) that websites like blogs or news sites can choose to publish their content in. A separate website/app called a <em>feed reader</em> or <em>aggregator</em> (I use <a href="https://netnewswire.com/">NetNewsWire</a> and <a href="https://feedbin.com/">Feedbin</a>, personally) then pulls in content from any feed you’ve subscribed to and presents them in a unified list.</p>
<p>(Yes, <a href="https://www.ryanthomson.net/feed.xml">this blog has a web feed</a>.)</p>
<p>What I love about web feeds is that they truly put you in control of your experience by decoupling the content you see (the feeds) from the software you use (the reader). There’s no lock-in here: <em>any</em> website with a feed can be accessed by <em>any</em> feed reader, so long as everyone’s following the spec. And that interoperability isn’t just good for user choice, but also for stability. In 2013, Google shut down their feed reader product, <a href="https://en.wikipedia.org/wiki/Google_Reader">Google Reader</a>, forever cementing their reputation as a company that will mercilessly <a href="https://killedbygoogle.com/">kill off that product you love</a>. Some people certainly gave up on web feeds after that, but for everyone else it was just a minor disruption. Any former Reader user could pack up their subscriptions (<a href="https://en.wikipedia.org/wiki/OPML">there’s a format for that</a>, too) and move them to another feed reader without too much hassle.</p>
<p>Now, from an end user perspective, web feeds are strictly a read-only technology: publishers publish content, consumers consume it. Contrast that with social media which is interactive by nature. But there’s no reason that the same principles of decentralization and interoperability couldn’t be applied to an interactive experience— like, say, Reddit.</p>
<p>Reddit is essentially just a bunch of forums stapled together, so what made it so successful while traditional forums have become more niche? The voting system and nested comments were novel, but not technically difficult to replicate. No, what Reddit offered was <em>consolidation</em>: the aggregation of its communities’ posts into a single feed. Using Reddit was more convenient than visiting dozens of separate forums. We’ve been led to believe that centralization is a requirement for this kind of experience, but this is the <em>exact</em> problem web feeds solve for blog-like content. Imagine instead a network of independently-hosted web forums which all communicate via a common protocol, accessed and aggregated by whatever client <em>you</em> choose. Using a third-party client wouldn’t be a privilege granted and revoked at the whim of Reddit Inc.; it would be an integral part of the system, baked into its very design.</p>
<p>There are, I think, some technical problems that need to be solved to make this social media model truly viable— identity, for instance, seems to be particularly hard to get right under this model. But I think it’s the best shot we’ve got at building a truly sustainable and user-focused social web. New social media projects will always face the challenge of getting past the “dead mall” phase, but I still hope to see more new projects embrace this philosophy gain a healthy niche userbase.</p>
<p>Now, I’ve probably come across as hopelessly optimistic up to this point, so let me bring this back down to earth: I don’t see decentralized platforms ever breaking into the mainstream in a substantial way. It’s been made crystal clear at this point that the general public just does not care about this stuff. That’s why web feeds are hopelessly niche, and why Discord is eating all the forums Reddit didn’t kill. And it’s why <a href="https://joinmastodon.org/">Mastadon</a>, a <a href="https://en.wikipedia.org/wiki/Federation_(information_technology)">federated</a>, open-source Twitter alternative was absolutely dwarfed by Facebook’s Threads overnight.</p>
<p>As always, <a href="https://xkcd.com/743/">relevant xkcd</a>.</p>
The Magenta Gamehttps://www.ryanthomson.net/articles/magenta-game/2023-08-01T00:00:00Z2023-08-01T00:00:00Z<figure>
<img src="https://www.ryanthomson.net/media/article/magenta-game/magenta-three.jpg" srcset="https://www.ryanthomson.net/media/article/magenta-game/magenta-three@2x.jpg 2x" width="1000" height="600" class="full" alt="Collage showing characters from three different video games sharing a similar aesthetic." />
<figcaption>Left to right: <cite>Bleeding Edge</cite>, <cite>Apex Legends</cite>, <cite>RAGE 2</cite></figcaption>
</figure>
<p>Over the last few years, I’ve become more and more aware of a recent phenomena in popular video games. <em>Redfall</em>. <em>Apex Legends</em>. <em>RAGE 2</em>. <em>Borderlands 3</em>. <em>Bleeding Edge</em>. All of these games have something distinct about them, and I don’t mean among each other. Actually, that’s the point: they’re strikingly similar. Not in gameplay or technology, but in presentation and style. And it’s not something that was common in the industry until relatively recently.</p>
<p>Enter Ben “Yahtzee” Croshaw, who’s been producing his weekly video game review series <em><a href="https://www.escapistmagazine.com/category/zero-punctuation/">Zero Punctuation</a></em> since 2007. He coined the term “glorious PC gaming master race“ in his review of <em>The Witcher</em>, defined the <a href="https://store.steampowered.com/tags/en/Spectacle+fighter">“spectacle fighter”</a> genre while reviewing <em>MadWorld</em>, and formally dubbed all modern military shooters “spunkgargleweewee” (seriously). He noticed this trend too, and gave these types of games a name: “Magenta games”.</p>
<p>What are the magenta games? Where did they come from? And what has any of this got to do with hot pink?</p>
<h2 id="defining-the-genre" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/magenta-game/#defining-the-genre" aria-hidden="true">§</a>Defining the Genre</h2>
<figure>
<blockquote>
<p>Funny, isn’t it, how whenever a game talks about being “over the top” or “tongue in cheek” that always seems to mean the same thing these days— that it’s going to look like an irresponsibly violent version of Jet Set Radio, probably cel shaded, every character is introduced with a freeze frame profile and dresses like a Tank Girl cosplayer with color blindness, and a lot of things will be magenta.</p>
<p>Oh yeah, and there’ll be a panda for some reason.</p>
</blockquote>
<figcaption><a href="https://www.escapistmagazine.com/zero-punctuation-e3-2019/">Zero Punctuation: E3 2019</a></figcaption>
</figure>
<p>So what is it about a magenta game that makes it a magenta game? I’ve come to identify these as the core factors:</p>
<ul>
<li><strong>An alt/punk art style.</strong> A prominent feature of the aesthetic is the use of vibrant neon colors, hence the term “magenta game”.</li>
<li><strong>A diverse cast of quirky characters.</strong> While these characters can be NPCs, they’re often playable. The genre is a natural companion of the <a href="https://en.wikipedia.org/wiki/Hero_shooter">hero shooter</a> or other class-based genres.</li>
<li><strong>Quippy dialogue,</strong> meaning lines like “so <em>that</em> just happened” and “well, this is awkward”. Joss Whedon is often credited with popularizing this type of scriptwriting.</li>
<li><strong>An over-the-top, self-aware tone.</strong> The self-awareness is usually used as an excuse to do wacky things, rather than any interesting meta shenanigans.</li>
<li>Bonus points if it’s a looter shooter.</li>
</ul>
<p>That said, it’s important to note that just because a game has some of these traits doesn’t necessarily mean it’s a magenta game. <em>Forspoken</em>’s dialogue quipped its way into being one of the most clowned-on games of the last few years, but I wouldn’t call it a magenta game.</p>
<p>It’d be hard to identify exactly where the magenta game genre started, but I think <em>Sunset Overdrive</em> is a good candidate for patient zero; it was showing all the symptoms way back in 2014. The genre slowly grew in relevance from there until exploding around 2019, which I’m going to go ahead and declare as the year of Peak Magenta, hosting titles such as <em>Apex Legends</em>, <em>Far Cry: New Dawn</em>, <em>RAGE 2</em>, and <em>Borderlands 3</em>. And the genre is still alive and well today, with recent releases such as <em>Redfall</em>, <em>Saints Row (2022)</em>, and <em>Destiny 2: Lightfall</em> carrying the torch.</p>
<h2 id="magenta%3A-origins" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/magenta-game/#magenta%3A-origins" aria-hidden="true">§</a>Magenta: Origins</h2>
<p>The magenta game as we know it today basically didn’t exist a decade ago. Of course, everything has to start somewhere. But while many genres have an obvious originator or popularizer (<em>Rogue</em> for roguelikes, <em>Doom</em> for first person shooters, <em>Overwatch</em> for online casinos), the magenta games genre just sort of appeared. I don’t think we can attribute magenta games to any particular game or technical innovation; instead, it was a product of the general state of the gaming landscape in the 2010s.</p>
<p>During the <a href="https://en.wikipedia.org/wiki/Seventh_generation_of_video_game_consoles">seventh generation of consoles</a>, the games industry was enamored with a different trend. It was the time of darker, grittier, more “realistic” games. The <a href="https://tvtropes.org/pmwiki/pmwiki.php/Main/RealIsBrown">grey-and-brown color palette</a> dominated this era to the point where some games may as well have been rendered in sepia. Bloom, film grain, and other post-processing effects were over-applied to enhance the “cinematic” feel. The go-to protagonist would be <a href="https://gamerant.com/average-video-game-heroes/">a no-nonsense gruff-looking white dude</a>, probably pictured on the cover holding a gun and looking grim. And these games took themselves <em>very</em> seriously.</p>
<p>And you know what? It sucked. Games were really, really boring. Certainly there are plenty of counterexamples, but most of the trend-followers ended up worse games for it.</p>
<p>The magenta game genre can be seen as a response to the trend of “gritty realism”— the pendulum swinging in the other direction. Games went from bland and brown to wacky and colorful; generic everymen were replaced with larger-than-life characters. The <a href="https://www.youtube.com/watch?v=s_LmilGAhaM">E3 2014 trailer for <em>Sunset Overdrive</em></a> really captures the zeitgeist with its neat bait-and-switch opener.</p>
<figure>
<img src="https://www.ryanthomson.net/media/article/magenta-game/magenta-four.jpg" srcset="https://www.ryanthomson.net/media/article/magenta-game/magenta-four@2x.jpg 2x" width="960" height="640" class="full" alt="Collage showing four groups of characters from different video games sharing a similar aesthetic." />
<figcaption>Clockwise from top left: <cite>Redfall</cite>, <cite>XDefiant</cite>, <cite>Contra Rogue Corps</cite>, <cite>Saints Row (2022)</cite></figcaption>
</figure>
<p>But, I have to mention the more cynical angle as well: the money angle.</p>
<p>The business model of video games has changed. At first it was just game sales, then DLCs and expansion packs, then microtransactions and loot boxes, and nowadays the “live service” model is in vogue. Whether that’s a good or bad thing isn’t the focus of this article (bad, by the way), but there’s no doubt that the way games are monetized affects the way they’re designed.</p>
<p>Lots of games today, particularly the free-to-play ones, make their dough by continually selling in-game cosmetics, since being perceived as pay-to-win is one of the few things that can still damage your game’s reputation. And at a certain point, any game that makes its money from a cosmetic economy will start selling “jokey” items. Because when quarterly revenue targets are on the line, consistency in tone or artstyle will be thrown right out the window if it means selling more skins. <em>Halo Infinite</em> didn’t even make it to the end of its first season before adding cat ear helmets to the store.</p>
<p>“So hey,” says the magenta game, “why not skip the serious bit and start shipping wacky cosmetics on day one?” That’s not to suggest that there isn’t an organic audience for these games, because there clearly is. But it’s certainly convenient for the guys with the cash.</p>
<h2 id="my-take" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/magenta-game/#my-take" aria-hidden="true">§</a>My Take</h2>
<p>My first introduction to magenta games was <em>Sunset Overdrive</em>, and I was all for it. <em>Finally</em>, a refreshing break from the monotonous grey-and-brown deluge we’d been stuck with for years. I was still in the honeymoon period with these games, when a lot of missteps can be forgiven. While I remember somewhat enjoying Sunset Overdrive’s writing when it came out, it does <em>not</em> have the same effect today. And as time has gone on, my general sentiment towards these two trends has started to converge.</p>
<p>See, that’s the thing about trends: it’s very easy for something that was once fresh and exciting to become clichéd and boring once everyone starts jumping on the bandwagon. Like the aesthetic, for instance. The alt/punk art style that magenta games wear like a security blanket was novel at first, but now it’s ubiquitous enough that often these games feel like they don’t have an aesthetic at all.</p>
<p>And yet, I still think Sunset Overdrive is one of the better entries in the genre. Sure, nowadays the dialogue makes me want to mute/defenestrate the TV (I shit you not, this game drops the term “awesome-pocalypse” unironically), but <em>at least</em> the game’s tone was in sync with its gameplay. This is a game where every car is a bounce-pad and every power line is a grind rail; of course they’d never be able to play it straight. That’s more than you can ask of quite a few magenta games, where the presentation is paired with bog-standard gameplay. For games trying so hard to appear “over the top”, they sure do tend to do so in the least interesting ways.</p>
<img src="https://www.ryanthomson.net/media/article/magenta-game/sunset-overdrive.jpg" srcset="https://www.ryanthomson.net/media/article/magenta-game/sunset-overdrive@2x.jpg 2x" width="960" height="540" class="full" alt="Screenshot of the game Sunset Overdrive, showing the main character riding an overhead power cable like a zip line while shooting a makeshift gun." />
<p>Then there’s the characters. While the character design in these games can be hit or miss for me, it’s the overly quippy dialogue that consistently makes these personalities more annoying than endearing. Even in cases where it’s not overtly grating, this style of dialogue tends to deflate the impact of the storytelling. It’s as if the game is saying, “the characters don’t care about what’s going on, so why should you?”</p>
<p>But specifics aside, there’s just something about the holistic experience of playing a magenta game that puts me off. To be honest, I’ve had a difficult time putting my finger on exactly what it is, but if I had to try and express it in one sentence, it’d be this: <strong>A magenta game gives off the image of someone trying <em>desperately</em> to be cool, which of course only makes them more uncool.</strong></p>
<p>Then again, perhaps these games just aren’t for me. There’s no doubt that these games are beloved by many people (well, some of these games, at least). And while I think there’s plenty of similarities between the magenta games genre and the “gritty realism” trend, I can’t deny that this trend isn’t nearly as suffocating. There’ll always be trendy genres, and there’ll always be bad games, so ultimately, what’s the harm? They’re just games that I don’t like, and that’s okay. But I can’t help but feel a pang of regret whenever I see good ideas shackled to annoying tropes in the name of mass appeal.</p>
<p>Or maybe I’m just salty that <a href="https://www.ign.com/articles/respawn-worked-on-titanfall-3-for-10-months-before-pivoting-to-apex-legends-ex-dev-reveals">we’re never getting a <em>Titanfall 3</em></a>.</p>
Forbidden JavaScripthttps://www.ryanthomson.net/articles/forbidden-javascript/2023-06-28T00:00:00Z2023-06-28T00:00:00Z<p>You know, I actually like JavaScript.</p>
<p>The language gets a bad rap, and honestly for good reason. But from ECMAScript 2015 onward, it’s received fantastic additions that have made it my personal favorite scripting language. It’s just that those additions are built upon a foundation of <em>questionable</em> decisions that have been baked into the language and can’t be reversed for backwards compatibility (<code>typeof null</code>, anyone?). But I suppose that’s what happens when you’ve only got ten days to make a programming language.</p>
<p>This article is a collection of those unfortunate choices. The obscure corners of the language better left untouched. The features so great, they had be disabled in <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode">strict mode</a>.</p>
<p>I’m going to be dunking on JavaScript in this article. But in fairness, there will be no jabs at JavaScript’s… <em>special</em> type system here. Because frankly, it’s <a href="https://www.destroyallsoftware.com/talks/wat">already</a> been <a href="https://www.youtube.com/watch?v=et8xNAc2ic8">done</a> to <a href="https://old.reddit.com/r/ProgrammerHumor/comments/88gniv/old_meme_format_timeless_javascript_quirks/">death</a>.</p>
<h2 id="caller-and-callee" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/forbidden-javascript/#caller-and-callee" aria-hidden="true">§</a>Caller and Callee</h2>
<p>All <code>Function</code>s have a property named <code>caller</code> which gives you the function that’s currently calling that function.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">f</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>f<span class="token punctuation">.</span>caller<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// [Function: g]</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>g<span class="token punctuation">.</span>caller<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// [Function: h]</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>h<span class="token punctuation">.</span>caller<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// null</span>
<span class="token punctuation">}</span>
<span class="token keyword">function</span> <span class="token function">g</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">f</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">function</span> <span class="token function">h</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">g</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">h</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Similarly, <code>arguments.callee</code> will tell you what function is currently being executed. This isn’t very useful since you already have that information, but according to <a href="https://stackoverflow.com/a/235760">this StackOverflow answer</a>, in earlier versions of JavaScript it was situationally necessary for implementing recursion.</p>
<p>Aside from <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments/callee#description">preventing certain kinds of optimizations</a>, letting functions know where they’re being called from is just asking for people to write downright <em>diabolical</em> code. I suppose if you <em>really</em> wanted to make the argument for caller/callee, they could potentially be used for debugging, but— <em>don’t</em>. <code>console.trace()</code> exists. Use it.</p>
<h2 id="the-with-statement" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/forbidden-javascript/#the-with-statement" aria-hidden="true">§</a>The <code>with</code> Statement</h2>
<p>The <code>with</code> statement brings all of an object’s properties into the current scope. In a sense, it makes the specified object act like the <a href="https://developer.mozilla.org/en-US/docs/Glossary/Global_object">global object</a> temporarily.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">areaOfCircle</span><span class="token punctuation">(</span><span class="token parameter">r</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">with</span> <span class="token punctuation">(</span>Math<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token constant">PI</span> <span class="token operator">*</span> <span class="token function">pow</span><span class="token punctuation">(</span>r<span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>The <code>with</code> statement has the advantage of <em>slightly</em> reducing the amount of keystrokes you need to perform in some situations. The disadvantages?</p>
<p>For starters, it makes code harder to read because you can no longer tell where identifiers are being defined. Looking at the above example, you can’t tell by just looking at the code if <code>PI</code> is a part of <code>Math</code> or defined in an outer scope. The same ambiguity also limits the optimizations JavaScript engines can make.</p>
<p>It also creates potential future compatibility problems. Looking at the example above again, if <code>Math</code> added a new property called <code>r</code>, our code would break because it would shadow the argument we were trying to access. This concern led to the addition of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables"><code>@@unscopables</code></a>, a hack that exists only to defend against use of <code>with</code>.</p>
<aside class="callout">
<p>As I was researching for this article, I noticed that the only use of <code>@@unscopables</code> in the standard library was on <code>Array.prototype</code>. I thought this was odd, so I spent way too much time looking into it.</p>
<p>During development of ECMAScript 2015, it was discovered that the addition of <code>Array.prototype.values</code> created a compatibility problem with an ancient proprietary JavaScript framework called <a href="https://www.sencha.com/products/extjs/">Ext JS</a> . I couldn’t find the actual source for an affected version, but based on <a href="http://rwaldron.github.io/tc39-notes/2013-07_july-23.html#43-arrayprototypevalues">the meeting notes</a> we can assume the code in question looked something like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">f</span><span class="token punctuation">(</span><span class="token parameter">values</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">with</span> <span class="token punctuation">(</span>values<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// …</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>If <code>f()</code> is passed an array, then <code>Array.prototype.values</code> would shadow the <code>values</code> parameter, changing the meaning of the code and breaking websites. And so, <code>@@unscopables</code> was born, EX JS leaves its permanent mark on the internet, and I lose several hours of my life.</p>
</aside>
<h2 id="globals-for-html-elements" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/forbidden-javascript/#globals-for-html-elements" aria-hidden="true">§</a>Globals for HTML Elements</h2>
<p>Here’s a common pattern in web apps: Give an element on the page a unique identifier with the <code>id</code> attribute, then use <code>Document.getElementById()</code> to get a reference to it in the <abbr title="Document Object Model">DOM</abbr>.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my_div<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Hello world<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword">const</span> myDiv <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'my_div'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>myDiv<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// <div id="my_div">…</div></span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>But what if we didn’t need <code>getElementById()</code>? What if we actually had a reference all along?</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my_div<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Hello world<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>my_div<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// <div id="my_div">…</div></span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>Yep, using the <code>id</code> attribute on any element makes a global with the same name. This also works for the <code>name</code> attribute on certain elements.</p>
<p>Don’t believe me? Here’s <a href="https://html.spec.whatwg.org/multipage/nav-history-apis.html#named-access-on-the-window-object">the spec:</a></p>
<blockquote>
<p>The <code>Window</code> object supports named properties. The supported property names of a <code>Window</code> object <em>window</em> at any moment consist of the following, in tree order according to the element that contributed them, ignoring later duplicates:</p>
<ul>
<li><em>window</em>'s document-tree child navigable target name property set;</li>
<li><strong>the value of the <code>name</code> content attribute for all <code>embed</code>, <code>form</code>, <code>img</code>, and <code>object</code> elements</strong> that have a non-empty <code>name</code> content attribute and are in a document tree with <em>window</em>'s associated <code>Document</code> as their root; and</li>
<li><strong>the value of the <code>id</code> content attribute for all HTML elements</strong> that have a non-empty <code>id</code> content attribute and are in a document tree with <em>window</em>'s associated <code>Document</code> as their root.</li>
</ul>
</blockquote>
<h2 id="labeled-statements" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/forbidden-javascript/#labeled-statements" aria-hidden="true">§</a>Labeled Statements</h2>
<p>The <code>goto</code> was once a standard part of the programmer’s control flow toolkit, but after decades of criticism and debate, including Dijkstra’s infamous <a href="https://dl.acm.org/doi/10.1145/362929.362947"><cite>Goto Statement Considered Harmful</cite></a>, it’s now a rarity. JavaScript isn’t cursed enough to have proper <code>goto</code>s, but it does have a close cousin.</p>
<p>You can give any statement a name, making it a <em>labeled statement</em>. Their only use is with the <code>break</code> and <code>continue</code> statements, which accept an optional label name.</p>
<pre class="language-js"><code class="language-js"><span class="token literal-property property">outerBlock</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">innerBlock</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token keyword">break</span> outerBlock<span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// skipped</span>
<span class="token punctuation">}</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'2'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// also skipped</span>
<span class="token punctuation">}</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'3'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The implementation is rather constrained when compared with the chaotic energy of a proper <code>goto</code>, since you can only jump to labeled statements that you’re currently nested within. But I’d go as far as to say that, in contrast with the rest of this article, this is actually a pretty useful feature, particularly for breaking out from within nested loops. Yet I don’t think I’ve ever seen this used in the wild, or discussed as more than an obscure curiosity.</p>
<p>But the labeled statement has recently seen a second life. The developers of <a href="https://svelte.dev/">Svelte</a> saw this obscure piece of syntax laying around and— <em>yoink</em>. Since you can label <em>any</em> statement, they decided to add <code>$:</code> as special bit of syntax in their compiler, and now any statement labeled with <code>$</code> becomes <a href="https://svelte.dev/docs/svelte-components#script-3-$-marks-a-statement-as-reactive">“reactive”</a>.</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// Non-reactive assignment (LAME)</span>
<span class="token keyword">let</span> y <span class="token operator">=</span> x <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token comment">// Cool reactive assignment</span>
<span class="token literal-property property">$</span><span class="token operator">:</span> z <span class="token operator">=</span> x <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span></code></pre>
<h2 id="html-comments" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/forbidden-javascript/#html-comments" aria-hidden="true">§</a>HTML Comments</h2>
<p>In addition to line comments (<code>//</code>) and block comments (<code>/* */</code>), JavaScript also supports HTML-style comments (<code><!-- --></code>). This works in <code><script></code> tags as well as in <code>.js</code> files (so long as they’re not loaded as a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">module</a>).</p>
<pre class="language-js"><code class="language-js"><span class="token operator"><</span><span class="token operator">!</span><span class="token operator">--</span> Print a message to the console <span class="token operator">--</span><span class="token operator">></span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Hello world'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<aside class="callout">
<p>There’s technically a fourth type of supported comment— <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#hashbang_comments">hashbang comments</a>.</p>
</aside>
<p>They may look like HTML comments, but they don’t work like them. While comments in HTML are functionally equivalent to JS’s block comments, they actually act as line comments in JavaScript, and you’re under no obligation to match the opening and closing comments.</p>
<p>Why does this exist? Well, it goes all the way back to the beginning of JavaScript. The concern was that if you loaded a page with <code><script></code>s in a browser without JavaScript support, the script’s source code would be displayed on the page since the browser didn’t know not to render it. So the idea was that to avoid this, you could wrap your scripts in an HTML comment. To a JavaScript-supporting browser, the comment would be quietly ignored and the script executed; to browsers without JS, any text in the <code><script></code> tags would be commented out and not appear on the page. Decades later this concern doesn’t exist, but for backwards compatibility it’s stuck.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token operator"><</span><span class="token operator">!</span><span class="token operator">--</span>
<span class="token comment">// JavaScript here</span>
<span class="token operator">--</span><span class="token operator">></span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>But my absolute favorite part is just how much effort goes into supporting this obscure, obsolete feature.</p>
<p>Within the context of JavaScript, the syntax of an HTML comment becomes ambiguous. You might have noticed that <code><!--</code> is the same as <code>< ! --</code> (less than, logical not, prefix decrement) except with the whitespace stripped out (<em>watch out for that one, minifiers!</em>). And in a module file, <code><!--</code> is indeed treated as those three separate operators.</p>
<p>You also have to deal with the similarity to <code><!-</code> (less than, logical not, unary negation). Disambiguating requires the lexer to consider four characters at once, the most required in the language. V8’s lexer has a <a href="https://source.chromium.org/chromium/v8/v8.git/+/2ffec4a51ddc954fc9e00f1c89224970cb901027:src/parsing/scanner.cc;l=331-343">special case</a> coded in just for dealing with HTML comments.</p>
<figure>
<blockquote>
<p>The scanner chooses a specific scanner method or token based on a maximum lookahead of 4 characters, the longest ambiguous sequence of characters in JavaScript.</p>
</blockquote>
<figcaption><a href="https://v8.dev/blog/scanner"><cite>Blazingly fast parsing, part 1: optimizing the scanner</cite></a></figcaption>
</figure>
<p>All this to support backwards compatibility of a feature that was added for backwards compatibility with browsers from thirty years ago.</p>
<p><em>JavaScript.</em></p>
I Am Here to Complain About the iOS 16 Lock Screenhttps://www.ryanthomson.net/articles/i-am-here-complain-about-ios-16-lock-screen/2023-05-31T00:00:00Z2023-05-31T00:00:00Z<p>With <a href="https://developer.apple.com/wwdc23/">WWDC ’23</a> just around the corner, I figured now was a perfectly punctual time to talk about last year’s hot new features. iOS 16 was the first update in quite a while to touch the lock screen in any substantial way. Now that we’ve all had <em>plenty</em> of time for the novelty to wear off, I have to say that I remain thoroughly underwhelmed.</p>
<p>There’s no doubt that lock screen customization in iOS was well overdue, since previously the <em>only</em> option you had was changing the wallpaper. And if all you were looking for was <em>more options</em>, then iOS 16 probably left you pretty satisfied— there’s now fonts, colors, and widgets galore to choose from. But from my perspective, I think Apple fumbled the execution on this one.</p>
<p>There were lots of big and small tweaks to the lock screen, but one of the foundational changes to the system is that you can now have multiple lock screens and switch between them, including via automation with <a href="https://support.apple.com/guide/shortcuts/welcome/ios">Shortcuts</a> and <a href="https://support.apple.com/en-us/HT212608">Focus</a>. This sure sounds like something useful, but one year later I’m still rocking a single lock screen. I’m sure this feature is useful to <em>someone</em>, but for me it’s just added friction to the simple process of changing my phone’s wallpaper. There’s been substantial usability improvements in subsequent patches, but the whole system still reeks of awkwardness, like how there’s still no way to reorder your lock screens if you have more than one.</p>
<p>Speaking of wallpapers, iOS’s once-great set of built-in wallpapers has been steadily whittled down with each release, and now we’re left with a handful of mostly-mediocre backgrounds, which are now called “collections” for some reason (though I am surprised the dynamic wallpapers from iOS 7 have made it this far). If you’re not a fan of those then you could, of course, instead choose one of the <em>absolutely hideous</em> and bizarrely configurable new emoji wallpapers. But if you actually want your phone to look… good, and aren’t interested in gimmicks, then you’re going to need to bring your own photo, or choose one of the new “Color” backgrounds, which are actually quite nice.</p>
<p>But the big iOS 16 lock screen feature was widgets, which I’m going to use as an excuse to segue into one of my favorite <abbr title="User Interface">UI</abbr> design topics: affordances.</p>
<p><dfn>Affordances</dfn> are the environmental cues that tell you what possible actions you can take in that environment. A classic real-world example is the door handle: not only does it let you know that the segment of wall it’s attached to is a door, but the handle’s shape also tells you whether you need to push, pull, or twist to open the door.</p>
<p>You tend to hear the term “affordance” mostly applied to digital design, where those cues are more abstract. For example, consider <a href="https://www.ryanthomson.net/articles/i-am-here-complain-about-ios-16-lock-screen/#;">this link</a>, which is styled differently from the rest of this text to indicate that it’s clickable.</p>
<figure>
<img src="https://www.ryanthomson.net/media/article/i-am-here-complain-about-ios-16-lock-screen/door-handles.jpg" srcset="https://www.ryanthomson.net/media/article/i-am-here-complain-about-ios-16-lock-screen/door-handles@2x.jpg 2x" width="960" height="640" alt="Photos of three different door handles; from left to right: a pull handle, a turn handle, and a push handle." class="full" />
<figcaption>Left to right: <a href="https://unsplash.com/photos/black-metal-door-lever-in-close-up-photography-rJhJD3Z654M">AR</a>, <a href="https://unsplash.com/photos/gold-door-lever-on-brown-wooden-door-hfiainyla0c">Sneaky Head</a>, <a href="https://unsplash.com/photos/a-close-up-of-a-door-with-a-handle-CpUgNqJDinw">Bernard Hermant</a></figcaption>
</figure>
<p>Affordances are a big deal in UI design, and understandably it’s a topic that many people have strong opinions on. On one side you have the function-over-form purists who insist that every button must have the <em>exact</em> same bevel as every other button, and then go on to explain how Windows 95 was the peak of all UI design ever. At the other extreme you have trendy <em>Dribbble</em>-bullshit that expects you to intuit that <em>this</em> bit of grey text that’s a slightly different shade of grey from all the other grey text is actually a clickable button. I tend to fall somewhere between these two blatant straw men.</p>
<p>Good UI design exhibits many different qualities, which can sometimes conflict with each other: ease of use, discoverability, visual hierarchy, efficient use of screen space, and just making things look good, to name a few. So while I’d say that <em>in general</em> everything should have good affordances, and <em>in general</em> those affordances should be reasonably consistent, there are plenty of good reasons to make exceptions.</p>
<p>Complex software, for example, can usually get away with skipping on affordances for core functionality if it streamlines the experience for experienced users. Learning those UI interactions just becomes part of the initial learning curve (which the software was going to have anyway), and you’ll have to either trial-and-error you way into basic competency or <a href="http://www.catb.org/jargon/html/R/RTFM.html">RFTM</a>.</p>
<p>On the other end of the learning curve, I’d say it’s fine for advanced functionality to remain un-afforded so long as there’s an obvious-but-slow way of accomplishing the same task. For example, lots of software supports drag-and-drop as a faster alternative to digging through menus, but that doesn’t mean the UI should be carpeted in drag handles.</p>
<p>Let’s use the iOS home screen as a case study. If you want to edit your home screen, you need to tap and hold on the screen. There’s no affordance for this, it’s just something you learn when you get your first iWhatever. Apple could have put an explicit “edit” button or a label on screen, but that would have wasted space and cluttered a screen you’ll be seeing constantly. Once you enter <a href="https://www.youtube.com/watch?v=pAOjDXdiUzM">jiggle mode</a>, the icons start to shake, which is a clever way of letting you know the icons are ready to move, and that touching an icon will start a drag instead of launching the app. Apps also get an obvious “delete” button in this mode. There’s a “hidden” feature here as well: while dragging one icon, you can tap other icons to pick them up as a stack. It’s a nice time saver once you find it, but you’re not missing out on any functionality if you didn’t know that.</p>
<p>With that overlong digression out of the way, let’s go back to the lock screen and analyze the widgets UI through the lens of affordances.</p>
<p>Editing your lock screen requires the same long-press as the home screen, which has no affordance. This is fine.</p>
<p>Once you’re in, there are rounded rectangles drawn around each customizable part of the lock screen, which I think read well enough as tappable areas.</p>
<img src="https://www.ryanthomson.net/media/article/i-am-here-complain-about-ios-16-lock-screen/lock-screen.png" srcset="https://www.ryanthomson.net/media/article/i-am-here-complain-about-ios-16-lock-screen/lock-screen@2x.png 2x" width="480" height="640" alt="A screenshot showing the iOS lock screen being edited." />
<aside class="callout">
<p>If you want my wallpaper, it’s called <em>Lucent Lines</em> in the <a href="https://apps.apple.com/app/id1500143735">Backdrops app</a>.</p>
</aside>
<p>Tapping the bottom section lets you start editing your widgets. There’s a sheet that opens where you can add new widgets, and a clear “delete” button on existing widgets to remove them.</p>
<p>One thing that the widgets here <em>don’t</em> do is shake when you’re in “edit mode” like the icons on the home screen, so it’s not at all clear that you can rearrange them once placed. However, there’s a much bigger hidden interaction here, a dark secret that took me half a year to accidentally discover.</p>
<p>Did you know that you can tap on these widgets to open an options menu?</p>
<p><video src="https://www.ryanthomson.net/media/article/i-am-here-complain-about-ios-16-lock-screen/edit-widget.mp4" width="480" height="470" controls="" muted="" playsinline=""></video></p>
<p>If this is your first time seeing this, you wouldn’t be the first.</p>
<p>The kicker is, not all widgets <em>have</em> options, and tapping one that doesn’t does nothing. After some judicious pixel peeping, I can confidently say that there is <em>absolutely zero</em> visual difference between a widget that can be edited, and one that can’t. This feature is so well hidden it may as well be an Easter egg.</p>
<hr />
<p>The lock screen widgets suffer from a lot of the same warts as the home screen widgets from iOS 14, but with their own set of new problems added on top. Sure, on the whole, I’d take the new lock screen over the old one. But the level of quality is just not up to the standard that we usually expect from Apple.</p>
<p>There’s an idea that’s been going around for a while that Android will get a new feature first, then Apple will drop a better, more polished version a few years later. Whether that’s more true than false is debatable, but the idea hinges on the idea that Apple’s version is actually polished, and this just isn’t.</p>
How DOOM Renders Colorshttps://www.ryanthomson.net/articles/how-doom-renders-colors/2023-04-29T00:00:00Z2023-04-29T00:00:00Z<p>I’ve recently been reading through <a href="https://fabiensanglard.net/gebbdoom/"><em>Game Engine Black Book: DOOM</em></a>, a fascinating deep dive into the tech behind the legendary 1993 game <em>Doom</em>. Given that Doom is celebrating its 30th birthday this year, calling the technology antiquated would be an understatement. Yet the engine has some mystic enduring quality that compels programmers to port it to <a href="https://en.wikipedia.org/wiki/List_of_Doom_ports#Third-party_source_ports">anything with a microprocessor</a>.</p>
<p>That’s not to suggest that Doom’s engine was anything less than revolutionary at the time; it was arguably the most advanced game engine in ’93. Unlike its more advanced “true 3D” successor Quake (1996), however, the engine has essentially nothing in common with modern renderers. Yet the Doom engine is simple enough that us mere mortals can pick apart and understand its inner workings, making it easier to appreciate all the clever tricks that id Software used to get the game running on early 90’s home computer hardware.</p>
<p>Something I found particularly interesting is the way the engine handles color rendering. Doom was designed around <abbr title="Video Graphics Array">VGA</abbr>’s limited <a href="https://en.wikipedia.org/wiki/Mode_13h">320×200 resolution and 256-color palette</a>, giving the game that crunchy 90’s look. id Software pulled out some clever techniques to make the most of these limitations.</p>
<h2 id="the-color-palette" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-doom-renders-colors/#the-color-palette" aria-hidden="true">§</a>The Color Palette</h2>
<p>With a maximum of 256 unique colors on screen at once, each pixel can be represented by just one byte— its index in the palette. Since Doom is software-rendered and frame data is stored in regular memory before being sent to the display, you can imagine that the framebuffer looks something like this:</p>
<pre class="language-c"><code class="language-c"><span class="token class-name">uint8_t</span> screen<span class="token punctuation">[</span><span class="token number">200</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">320</span><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>While VGA only lets you show 256 different colors on screen at once, you do at least get to choose what those colors are. The Doom engine loads its color palette at startup from the <a href="https://doomwiki.org/wiki/WAD">.wad file</a>. The <code>PLAYPAL</code> <a href="https://doomwiki.org/wiki/Lump">“lump”</a> stores the actual RGB values that will be displayed on screen for each 0–255 color index.</p>
<p>This is <code>DOOM.WAD</code>’s color palette, arranged here with index 0 at the top left and 255 at the bottom right.</p>
<img src="https://www.ryanthomson.net/media/article/how-doom-renders-colors/playpal.png" width="256" height="256" alt="Doom’s color palette arranged as a 16x16 grid" style="image-rendering: pixelated;" />
<p>All of Doom’s graphics are also stored in the .wad file alongside the color palette. Doom’s custom image format doesn’t use RGB values. Instead, each pixel in the image is stored as a one-byte color index.</p>
<h2 id="lighting" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-doom-renders-colors/#lighting" aria-hidden="true">§</a>Lighting</h2>
<p>Where Doom gets really clever is with one of the engine’s <em>cutting edge</em> graphical features: lighting. In a Doom map, each area of the map (called a “sector”) has a light level somewhere between pitch black fully lit (in fixed steps). There’s also a neat effect called “light diminishing”, where areas further from the player appear darker than closer ones. It’s sort of like an early version of distance fog as seen in modern renderers.</p>
<figure>
<blockquote>
<p>Another touch adding realism is light diminishing. With distance, your surroundings become enshrouded in darkness. This makes areas seem huge and intensifies the experience.</p>
</blockquote>
<figcaption><cite>Doom press release</cite> (1993)</figcaption>
</figure>
<p>While not the most eye-catching feature, it does add a lot of atmosphere to the game’s maps, especially in darker areas. Notice the color banding on the ceiling and floors in this screenshot.</p>
<figure>
<img src="https://www.ryanthomson.net/media/article/how-doom-renders-colors/diminishing-lighting-screenshot.png" width="1600" height="1200" alt="A screenshot of Doom showing the player standing in a dark corridor" class="full" style="image-rendering: pixelated;" />
<figcaption>The blue keycard room on E1M5</figcaption>
</figure>
<p>In a modern game engine, making some pixels on the screen darker is trivial. But for Doom, that’s a significant ask, for a few reasons. One, any time spent on color calculations would chip away at the game’s already thin performance budget. And two, thanks to that 256-color limit, the darkened color would then need to be mapped back to its nearest approximation in the palette. Doom elegantly solves both of these problems with one of its favorite tricks: pre-computing the hard part.</p>
<p>The <code>DOOM.WAD</code> file contains another lump called <code>COLORMAP</code>. This is the game’s lighting lookup table, visualized below.</p>
<img src="https://www.ryanthomson.net/media/article/how-doom-renders-colors/colormap.png" width="1024" height="128" alt="Doom's color map, showing progressively darker shades of the same colors" class="full" style="image-rendering: pixelated;" />
<p>The <code>COLORMAP</code> uses other shades in the palette to approximate darkened versions of each color. There are 256 columns, one for each color in <code>PLAYPAL</code> (in the same order). Each row represents a brightness level, with index 0 being the brightest and 31 being the darkest. Look closely and you’ll see that the top row is essentially the color palette “unfurled”.</p>
<p>It’s not perfect— you can see how a lot of the colors devolve into a murky grey-brown as they get darker due to the limited shades available in the game’s color palette. But it gets the job done, and honestly I think it adds a bit of charm to the game’s visuals.</p>
<aside class="callout">
<p>Raven Software would cleverly abuse this system in <em>Hexen</em> (built on the same engine) to <a href="https://doomwiki.org/wiki/Fog">simulate fog</a>. An alternate <code>COLORMAP</code> which fades to a light grey instead of black is used in certain levels.</p>
</aside>
<p>During the rendering process, the game computes the brightness level that each pixel needs to be rendered at, taking into account the containing sector’s light level, distance from the viewport, and other light-affecting effects. That value and the color defined in the texture are plugged into the <code>COLORMAP</code> to get the appropriately darkened color, which can then be written to the framebuffer.</p>
<pre class="language-c"><code class="language-c"><span class="token comment">// pseudo-code</span>
<span class="token class-name">uint8_t</span> <span class="token function">ShadeColor</span><span class="token punctuation">(</span><span class="token class-name">uint8_t</span> color<span class="token punctuation">,</span> <span class="token keyword">int</span> lightlevel<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> colormap<span class="token punctuation">[</span>lightlevel<span class="token punctuation">]</span><span class="token punctuation">[</span>color<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h2 id="screen-tints" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-doom-renders-colors/#screen-tints" aria-hidden="true">§</a>Screen Tints</h2>
<p>Another interesting quirk is the way the engine handles screen tinting, like how the screen turns progressively redder as you take more damage. Again, nowadays blending colors at runtime is trivial, but not for Doom. To implement this, id reused a trick from <em>Wolfenstein 3D</em>.</p>
<p>The <code>PLAYPAL</code> lump actually contains not one, but fourteen different versions of the same color palette. The first is the base palette which the game uses most of the time. Then there are some red-tinted versions for indicating damage taken and the <a href="https://doomwiki.org/wiki/Berserk">berserk effect</a>, some yellows for picking up items, and a green for the <a href="https://doomwiki.org/wiki/Radiation_shielding_suit">radiation suit</a>. Whenever a screen tint needs to be displayed, the game swaps the base palette out for a tinted version.</p>
<picture>
<source srcset="https://www.ryanthomson.net/media/article/how-doom-renders-colors/playpal-tints.webp" type="image/webp" />
<img src="https://www.ryanthomson.net/media/article/how-doom-renders-colors/playpal-tints.gif" width="256" height="256" alt="Animation showing Doom’s different color palettes" style="image-rendering: pixelated;" />
</picture>
<p>This approach does have some downsides, mainly that it’s impossible to blend different tints together. But for Doom’s needs, it works remarkably well.</p>
<hr />
<p>If you’d like to experience Doom’s classic renderer on modern hardware, then I’d recommend checking out the <a href="https://www.chocolate-doom.org/wiki/index.php/Chocolate_Doom">Chocolate Doom source port</a>.</p>
How I (Re)built This Sitehttps://www.ryanthomson.net/articles/how-i-rebuilt-this-site/2023-03-31T00:00:00Z2023-03-31T00:00:00Z<p>Earlier this month I finished redesigning this website. It wasn’t just a facelift, but a full from-the-ground-up rewrite. In this article I’d like to discuss the new tech stack which now powers this site.</p>
<h2 id="why-rebuild%3F" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-i-rebuilt-this-site/#why-rebuild%3F" aria-hidden="true">§</a>Why Rebuild?</h2>
<p>The previous version of this website was built on <a href="https://www.11ty.dev/">Eleventy</a> 0.9 and copious jank. It was actually running on <em>two</em> build systems, Eleventy for rendering HTML and <a href="https://gulpjs.com/">Gulp</a> (remember Gulp?) for asset handling. These tools were brought together in an unholy union through a mess of npm scripts.</p>
<p>That was… <em>unpleasant</em>, but what was most damning about the old codebase was how hard it was to work on. Every time I went more than a few months without touching the project, <em>something</em> broke and I’d have to jiggle around the dependencies to get it working again. The result, unsurprisingly, is that I just didn’t want to work on the site anymore. That’s different now.</p>
<h2 id="the-setup" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-i-rebuilt-this-site/#the-setup" aria-hidden="true">§</a>The Setup</h2>
<p>If you want to build a website from scratch in <code>$CURRENT_YEAR</code>, you’ll be flush with shiny new JavaScript-y tools to choose from. In spite of that, I’m sticking with Eleventy, because while it’s not perfect it does what I need it to do. While the current wave of frontend frameworks has plenty of great buzzwordy features— server-side rendering, code splitting, client-side routing, etc.— I’m here to tell you that if you’re just building a blog like I am, <em>you’re not gonna need it</em>. All I want is to throw some HTML files up on a server without shipping a single byte of JavaScript to the client, and for that, Eleventy works.</p>
<p>Speaking of Eleventy, it just celebrated its <a href="https://www.11ty.dev/blog/eleventy-v2/">2.0 release</a> shortly before I started work on the rewrite. Eleventy has certainly come a long way since I built the previous version of this site, and thanks to some new features this time around, the end result is something I feel a lot better about working on going forward.</p>
<p>The biggest improvement is that I’ve managed to integrate all asset processing directly into Eleventy, so it’s now the only built tool I’m using. Another big win was using Eleventy’s plugin system to modularize my configuration file. The main config function is mostly just plugin imports, and the code for all the different asset types is neatly separated.</p>
<h3 id="directory-structure" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-i-rebuilt-this-site/#directory-structure" aria-hidden="true">§</a>Directory Structure</h3>
<p>Eleventy has a lot of functionality pre-configured out of the box, but I am just not a fan of the directory structure it expects by default. The layout I’ve been using for almost all my web projects for years now is <code>src/</code> for source (unprocessed) files, <code>public/</code> for files which should be copied over untouched, and <code>dist/</code> for build artifacts. Luckily, Eleventy makes this setup easy enough to achieve.</p>
<p>Here’s what that looks like in my <code>eleventy.config.cjs</code> file.</p>
<pre class="language-js"><code class="language-js">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addPassthroughCopy</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token string-property property">'./public/'</span><span class="token operator">:</span> <span class="token string">'/'</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span>
<span class="token literal-property property">dir</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">input</span><span class="token operator">:</span> <span class="token string">'src'</span><span class="token punctuation">,</span>
<span class="token literal-property property">output</span><span class="token operator">:</span> <span class="token string">'dist'</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h3 id="build-directory-cleaning" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-i-rebuilt-this-site/#build-directory-cleaning" aria-hidden="true">§</a>Build Directory Cleaning</h3>
<p>Something that Eleventy <em>doesn’t</em> do out of the box is delete old artifacts between builds. Luckily, clean builds can be achieved by just adding this snippet to the config function.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs/promises'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'eleventy.before'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> dir <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">await</span> fs<span class="token punctuation">.</span><span class="token function">rm</span><span class="token punctuation">(</span>dir<span class="token punctuation">.</span>output<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">recursive</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">force</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Much better.</p>
<h2 id="template-engine" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-i-rebuilt-this-site/#template-engine" aria-hidden="true">§</a>Template Engine</h2>
<p>Eleventy boasts support for <a href="https://www.11ty.dev/docs/languages/">a lot of built-in template languages</a>. I write all of my articles in Markdown, but I still need something to build page layouts with.</p>
<p>I have some beef with template languages. They always end up implementing their own custom syntax for data processing, and any time you need to go beyond the builtins you have to write a bunch of bridging code. I just want to call <code>foo(bar)</code>, but now I need to jump through hoops to implement <code>{{ bar | foo }}</code>. I suppose this limitation makes sense with a compiled language, but I’m using a scripting language, so why can’t it just <code>eval()</code> whatever I put in the template?</p>
<p>On the previous version of this site I used <a href="https://ejs.co/">EJS</a>, which <em>does</em> let you write JavaScript right in the template, and it was nice. But, I still wasn’t completely happy with it because it felt like there weren’t good options for organizing more complex templates. What I eventually realized is that I didn’t want <em>JS-in-HTML</em>, I wanted <em>HTML-in-JS</em>. I wanted <a href="https://facebook.github.io/jsx/">JSX</a>.</p>
<p>Eleventy doesn’t have built-in support for JSX, so I had to implement it myself. First, the JSX files need to be transpiled to regular JavaScript, which I used <a href="https://esbuild.github.io/">esbuild</a> to do. After that, I could use <a href="https://github.com/developit/vhtml">vhtml</a> to render it directly to an HTML string without React.</p>
<p>I came up with three ways of integrating this with Eleventy:</p>
<ol>
<li>Transpile <code>.jsx</code> files to <code>.11ty.js</code> files on disk before building</li>
<li>Implement JSX as a custom Eleventy template language</li>
<li>Extend <code>require()</code> to transpile <code>.jsx</code> as they’re imported</li>
</ol>
<p>Option #1 would have been easiest to implement, but having the transpiled <code>.js</code> files cluttering up the project was a deal-breaker. Option #2 is probably the most robust solution, but I quickly realized that it would have required <em>by far</em> the most effort to get working. That left #3.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> Module <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'module'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> esbuild <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'esbuild'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> pirates <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'pirates'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">function</span> <span class="token function">jsxPlugin</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addExtension</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'11ty.jsx'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">key</span><span class="token operator">:</span> <span class="token string">'11ty.js'</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token punctuation">(</span><span class="token string">'.jsx'</span> <span class="token keyword">in</span> Module<span class="token punctuation">.</span>_extensions<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
pirates<span class="token punctuation">.</span><span class="token function">addHook</span><span class="token punctuation">(</span>
<span class="token punctuation">(</span><span class="token parameter">code<span class="token punctuation">,</span> filename</span><span class="token punctuation">)</span> <span class="token operator">=></span>
esbuild<span class="token punctuation">.</span><span class="token function">transformSync</span><span class="token punctuation">(</span>code<span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">platform</span><span class="token operator">:</span> <span class="token string">'node'</span><span class="token punctuation">,</span>
<span class="token literal-property property">loader</span><span class="token operator">:</span> <span class="token string">'jsx'</span><span class="token punctuation">,</span>
<span class="token literal-property property">format</span><span class="token operator">:</span> <span class="token string">'cjs'</span><span class="token punctuation">,</span>
<span class="token literal-property property">jsx</span><span class="token operator">:</span> <span class="token string">'transform'</span><span class="token punctuation">,</span>
<span class="token literal-property property">jsxFactory</span><span class="token operator">:</span> <span class="token string">'h'</span><span class="token punctuation">,</span>
<span class="token literal-property property">jsxFragment</span><span class="token operator">:</span> <span class="token string">'null'</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span>code<span class="token punctuation">,</span>
<span class="token punctuation">{</span> <span class="token literal-property property">exts</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'.jsx'</span><span class="token punctuation">]</span> <span class="token punctuation">}</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>And with just this little bit of code, I had full JSX support. As far as Eleventy is concerned, these JSX files are just <a href="https://www.11ty.dev/docs/languages/javascript/"><code>.11ty.js</code> templates</a>, so we have full access to Eleventy data. It even supports ECMAScript modules, <a href="https://github.com/11ty/eleventy/issues/836">something Eleventy normally doesn’t like</a>.</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> h <span class="token keyword">from</span> <span class="token string">'vhtml'</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">title</span><span class="token operator">:</span> <span class="token string">'Hello World'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">render</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span><span class="token plain-text">Hello world</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h2 id="postcss-support" tabindex="-1"><a class="heading-anchor" href="https://www.ryanthomson.net/articles/how-i-rebuilt-this-site/#postcss-support" aria-hidden="true">§</a>PostCSS Support</h2>
<p>For CSS processing, I’ve dropped <a href="https://sass-lang.com/">SASS</a> and replaced it with <a href="https://postcss.org/">PostCSS</a>. CSS has come a long way in the last several years, and I’ve found that tools like SASS are just not worth the hassle. PostCSS is a lot more minimal, though that of course depends on your selection of plugins.</p>
<p>To integrate with Eleventy, I added CSS as a <a href="https://www.11ty.dev/docs/languages/custom/">custom template language</a> (something Eleventy didn’t support when I last used it). Raw <code>.css</code> files go in, processed <code>.css</code> files come out. My approach is roughly based on <a href="https://www.11ty.dev/docs/languages/custom/#example-add-sass-support-to-eleventy">this example from the docs</a>.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> postcss <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'postcss'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> loadConfig <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'postcss-load-config'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">function</span> <span class="token function">cssPlugin</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> postcssConfig<span class="token punctuation">;</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addTemplateFormats</span><span class="token punctuation">(</span><span class="token string">'css'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addExtension</span><span class="token punctuation">(</span><span class="token string">'css'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">outputFileExtension</span><span class="token operator">:</span> <span class="token string">'css'</span><span class="token punctuation">,</span>
<span class="token keyword">async</span> <span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
postcssConfig <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">loadConfig</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token keyword">async</span> <span class="token function">compile</span><span class="token punctuation">(</span><span class="token parameter">inputContent<span class="token punctuation">,</span> inputPath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> plugins<span class="token punctuation">,</span> options <span class="token punctuation">}</span> <span class="token operator">=</span> postcssConfig<span class="token punctuation">;</span>
<span class="token keyword">const</span> processor <span class="token operator">=</span> <span class="token function">postcss</span><span class="token punctuation">(</span>plugins<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> page <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>options<span class="token punctuation">,</span> <span class="token literal-property property">from</span><span class="token operator">:</span> inputPath<span class="token punctuation">,</span> <span class="token literal-property property">to</span><span class="token operator">:</span> page<span class="token punctuation">.</span>outputPath <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> processor<span class="token punctuation">.</span><span class="token function">process</span><span class="token punctuation">(</span>inputContent<span class="token punctuation">,</span> config<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> result<span class="token punctuation">.</span>css<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>As for my selection of plugins, I focused on improving developer ergonomics while still writing mostly “vanilla” CSS files rather than extending the language with a bunch of custom syntax. I chose <a href="https://www.npmjs.com/package/postcss-import">postcss-import</a> so I could split up my CSS into multiple files and <code>@import</code> them without the performance penalty of actually using <code>@import</code>. I also added <a href="https://www.npmjs.com/package/postcss-nesting">postcss-nesting</a> so I could use the new nesting syntax, which is a <em>huge</em> quality of life improvement but has basically no <a href="https://caniuse.com/css-nesting">browser support</a> right now. Both of these were good choices, and I’m very happy with how the CSS on this site has come together.</p>
Building Pixel-Perfect Skeleton Loaders in CSShttps://www.ryanthomson.net/articles/building-pixel-perfect-skeleton-loaders-css/2023-02-26T00:00:00Z2023-02-26T00:00:00Z<p>Skeleton loaders are a common alternative to the <em>ol’ reliable</em> loading spinner. If you have a good idea of how much space the content will take once it’s finished loading, then skeleton loaders can provide a pretty seamless user experience. But for the effect to work, the skeleton needs to be near-perfect to avoid layout shift.</p>
<p>There’s lots of different techniques for building skeleton loaders in CSS. Graphical elements are reasonably easy to swap out with simple CSS shapes or inline SVGs, but text makes things more difficult. There’s a lot of variables that affect the dimensions of a text element, so it’s easy to end up with a skeleton that’s just a <em>little</em> bit off. To get that pixel-perfect layout, you need to use the browser’s text layout engine to lay out the non-text placeholder elements.</p>
<p>Dummy text is one possible approach, but I’m not a fan. It feels like too much of a hack for me, and I want to be able to set the <em>exact</em> width of the placeholder via the <code>width</code> property. While working on <a href="https://www.hackerneue.com/">Hacker Neue</a> I stumbled upon a technique that cleanly fulfills these requirements.</p>
<p>(I’ll be using <a href="https://tailwindcss.com/docs/utility-first">Tailwind CSS</a> classes in these examples for brevity.)</p>
<p>We’re going to start with the content we want to skeletonize:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-sm font-medium uppercase text-gray-700<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Lorem ipsum dolor
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-xl font-semibold leading-tight<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Lorem Ipsum Dolor Sit Amet
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mt-1 leading-snug<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Lorem ipsum dolor sit amet
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span></code></pre>
<iframe src="https://www.ryanthomson.net/embeds/articles/building-pixel-perfect-skeleton-loaders-css-1/" width="450" height="150" class="embed"></iframe>
<p>First, we’ll strip the code down to just its basic layout. I picked an example with a good variety of font sizes and line heights, and we’ll be careful to preserve those.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-sm<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
…
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-xl leading-tight<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
…
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mt-1 leading-snug<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
…
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span></code></pre>
<p>Since we need something with a settable <code>width</code> and text-like layout, my first thought was to use element with <code>display: inline-block</code> as the placeholder. We also need to add a little bit of text content (<code>&nbsp;</code>) to give the element some height.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block bg-gray-300 w-64<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token entity named-entity" title=" ">&nbsp;</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>This works, but it looks a little… chunky?</p>
<iframe src="https://www.ryanthomson.net/embeds/articles/building-pixel-perfect-skeleton-loaders-css-2/" width="450" height="150" class="embed"></iframe>
<p>This has to do with how text layout works in the CSS spec. There’s a distinction between the the vertical space taken up by the font’s characters and the whitespace added around each line by the <code>line-height</code> property. For <code>inline-block</code> elements, their boundaries include that extra whitespace. <code>inline</code> elements don’t include the <code>line-height</code> space (nice), but you can’t set the <code>width</code> of those elements (lame).</p>
<p>The solution is to use both. We’ll add two elements: an outer <code>inline</code> element that has the background color applied to it, then an inner <code>inline-block</code> that we can set the width on. We don’t even need our <code>&nbsp;</code> anymore.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bg-gray-300<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-64<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>Here’s what that looks like when we apply it to our original example:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-sm<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bg-gray-300<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-32<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-xl leading-tight<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bg-gray-300<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-72<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mt-1 leading-snug<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bg-gray-300<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-48<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span></code></pre>
<p>Then just tweak the border radius, sprinkle in a bit of animation, and we get a pretty nice looking skeleton loader.</p>
<iframe src="https://www.ryanthomson.net/embeds/articles/building-pixel-perfect-skeleton-loaders-css-3/" width="450" height="150" class="embed"></iframe>
<p>Bonus tip: if you need line breaks, you can just keep adding more inner elements. Each <code>inline-block</code> element can wrap like words in a sentence.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bg-gray-300<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-32<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-32<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-32<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block w-32<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>