<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: João L.</title>
    <description>The latest articles on DEV Community by João L. (@lopis).</description>
    <link>https://dev.to/lopis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F21736%2F2b0bca36-7a63-4cc1-9dbc-2ba1ef07237e.jpg</url>
      <title>DEV Community: João L.</title>
      <link>https://dev.to/lopis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lopis"/>
    <language>en</language>
    <item>
      <title>If your "Unsubscribe" link requires users to login, you're training them to fall for phishing attacks</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Wed, 07 Jan 2026 14:16:31 +0000</pubDate>
      <link>https://dev.to/lopis/if-your-unsubscribe-link-requires-users-to-login-youre-training-them-to-fall-for-phishing-4kaf</link>
      <guid>https://dev.to/lopis/if-your-unsubscribe-link-requires-users-to-login-youre-training-them-to-fall-for-phishing-4kaf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is an opinion piece based on real events.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's a tale as old as Unix time. You never asked for these marketing e-mails -- you never select that checkbox out of principle! And yet, companies simply ignore it and send you unwanted communication.&lt;/p&gt;

&lt;p&gt;Most marketing e-mails nowadays have a convenient &lt;code&gt;Unsubscribe&lt;/code&gt; link at the bottom. Well, maybe not convenient; maybe it's 5pt light gray font, but it's usually there! You click, maybe sometimes you need to confirm with a button, and that's that.&lt;/p&gt;

&lt;h1&gt;
  
  
  The bad, the ugly!
&lt;/h1&gt;

&lt;p&gt;Some companies require users to sign-in to modify their notification settings. The reason is obvious - this adds &lt;em&gt;just&lt;/em&gt; enough friction that a lot of users don't bother and leave it as it is. But many will indeed try to sign-in and unsubscribe.&lt;/p&gt;

&lt;p&gt;If a user is ready to sever their relationship with a company, they already deem it very low value. They are very unlikely to watch out for the usual phishing e-mail tricks like suspicious URLs or the e-mail of the sender. Their defenses are likely to be relaxed because it's a low risk action. So when the company asks for their login credentials, they might just provide them without thinking.&lt;/p&gt;

&lt;p&gt;By requiring users to sign-in before they can unsubscribe from unwanted e-mails, these companies are normalizing this behaviour. A well crafted phishing marketing e-mail could lead to credential theft. Not only is this poor design and a dark pattern, this is a security risk!&lt;/p&gt;

&lt;h2&gt;
  
  
  Call to action
&lt;/h2&gt;

&lt;p&gt;When an unsubscribe link asks me to authenticate, I close the tab and mark the message as spam.&lt;/p&gt;

&lt;p&gt;I do this deliberately, and I strongly suggest others to do the same. Requiring credentials for a low-value action trains users to associate marketing emails with login prompts, which is indistinguishable from common phishing techniques. Once that pattern is normalized, credential harvesting becomes easier.&lt;/p&gt;

&lt;p&gt;This is the feedback loop companies create for themselves. If enough users follow my approach, it reduces sender reputation and harms email deliverability across major providers.&lt;/p&gt;

</description>
      <category>email</category>
      <category>phishing</category>
    </item>
    <item>
      <title>Meow Mountain - postmortem of a 13KB game</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Tue, 16 Sep 2025 13:04:49 +0000</pubDate>
      <link>https://dev.to/lopis/meow-mountain-postmortem-of-a-13kb-game-5fb6</link>
      <guid>https://dev.to/lopis/meow-mountain-postmortem-of-a-13kb-game-5fb6</guid>
      <description>&lt;p&gt;Another &lt;a href="https://www.js13kgames.com" rel="noopener noreferrer"&gt;JS13k Games&lt;/a&gt; just ended and I was able to preserve my 8-year streak of participating in this game jam. In this essay, I'd like to share some of the "low-tech tech" I've used—many of these are low-level development techniques that help me create tiny games.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljcm19992rztdzr1qdon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljcm19992rztdzr1qdon.png" alt="Game title card" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;About the game&lt;/li&gt;
&lt;li&gt;
Game concepts

&lt;ul&gt;
&lt;li&gt;♻️ Ideas that didn't make it
&lt;/li&gt;
&lt;li&gt;
Main concept

&lt;ul&gt;
&lt;li&gt;📖 Story Engine
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Reusable components

&lt;ul&gt;
&lt;li&gt;👾 Sprites
&lt;/li&gt;
&lt;li&gt;🔠 Font
&lt;/li&gt;
&lt;li&gt;👻 Emoji icons
&lt;/li&gt;
&lt;li&gt;🔉Sound effects
&lt;/li&gt;
&lt;li&gt;🎼 Music
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Other game features

&lt;ul&gt;
&lt;li&gt;🤖 NPC AI
&lt;/li&gt;
&lt;li&gt;🗺 Map generation
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Final notes&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the game
&lt;/h2&gt;

&lt;p&gt;This year's "black cat" theme inspired me to create a grid-based adventure game. Over the years, I've explored different genres, learning new game development concepts in the process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://js13kgames.com/2025/games/meow-mountain" rel="noopener noreferrer"&gt;Meow Mountain&lt;/a&gt; tells the story of a magical cat who is the protector of a mountain. After taking too long of a nap, the cat accidentally let the magic barrier protecting the mountain from evil spirits collapse. As a result, the villagers inhabiting the mountain stopped tending to the cat altars scattered around the mountain. These altars are essential to the cat's magical abilities, so our protagonist must repair all altars, restore the barrier, and bring peace back to the mountain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Game concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ideas that didn't make it
&lt;/h3&gt;

&lt;p&gt;This was my first time developing a grid-based game, as well as my first adventure game. The time and size restrictions meant many ideas I had for lore and game mechanics were left on the cutting room floor.&lt;/p&gt;

&lt;h4&gt;
  
  
  🧙‍♀️ Shapeshifting witch 🐈‍⬛
&lt;/h4&gt;

&lt;p&gt;Initially, the cat was going to be a witch who could transform into a cat to discreetly sneak around the mountain without being seen by villagers. The witch would use her magic powers to help the villagers by improving their harvests, clearing trails they needed to travel between villages, or ensuring they had access to food and water. These side-quests would then motivate the villagers to make offerings at the cat altars, which would provide magic power to the witch. In retrospective, this game mechanic was obviously far too complex to implement alongside everything else.&lt;/p&gt;

&lt;h4&gt;
  
  
  👨‍🌾 Villagers AI 🏘
&lt;/h4&gt;

&lt;p&gt;Another idea I initially wanted to explore was creating an AI for the villagers. The villagers in the game just wander aimlessly around their village (more on this below). My initial goal was for them to live in houses and having daily routines and jobs. They would leave home each day to go to their workplace, and some could travel between villages. Sadly, none of this made it into the game, so their movements are mostly random.&lt;/p&gt;

&lt;h4&gt;
  
  
  📋 Goals and Achievements ✅
&lt;/h4&gt;

&lt;p&gt;I also envisioned a more detailed goals &amp;amp; achievements system that players could consult to know what they needed to do next. Because the quest system ended up much simpler (repair the obelisk and the statues) this was no longer a priority. I still show a "new goal" pop-up, which has been well received, but there's no way to review current objectives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxugtvhw0g6u4s27fysvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxugtvhw0g6u4s27fysvv.png" alt="Screenshot: New goal popup" width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  🪄 Magic abilities 🔥
&lt;/h4&gt;

&lt;p&gt;Finally, I planned for the player to be able to use magic for things other than restoring the magic barrier. I had envisioned conjuring spells and special attacks that would aid in side-quests and fighting spirits. Conversely, I wanted the different spirits to also have different powers and attack patterns.&lt;/p&gt;

&lt;p&gt;I do wish to find the motivation and time to continue improving my game, and possibly revisit these ideas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Main concept
&lt;/h3&gt;

&lt;p&gt;I've been wanting to make a Zelda-style RPG game for a while. This year I finally felt confident I could pull it off. The game has a simple story: the protagonist fell into a long nap and the world fell into chaos. The main quest is to repair the barrier by finding the magic obelisk. When the player attempts to restore the barrier, they learn they've run out of magic, so something must be wrong with the cat altar. After repairing the cat altar, the player discovers this isn't enough to restore all their magic, and they must find all altars.&lt;/p&gt;

&lt;h4&gt;
  
  
  Story Engine
&lt;/h4&gt;

&lt;p&gt;I've used dialogs to guide the player step by step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frc7ojv79a7o3ab7au20o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frc7ojv79a7o3ab7au20o.png" alt="Screenshot: how long was I asleep?" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I created a simple reusable "story engine" that consumes a storyline data structure and keeps track of dialogs and game events. It's quite rudimentary, as this was my first time implementing something like this. I made creating a clear storyline and onboarding a priority this year because one recurring issue in JS13k is that games confuse players without lengthy manuals. But as they say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ain't nobody got time for that.&lt;br&gt;
-- Plato or Einstein or something.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;History shows that people don't like reading instructions. So the game starts with soft exposition. The player learns about the existence of spirits, and that this means that the magic barrier has disappeared.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi2q8ioo0ptmobyn5viqw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi2q8ioo0ptmobyn5viqw.png" alt="Screenshot: evil spirits?" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This also introduces the player to the protagonist's combat abilities. Technically, the first spirit (and in fact, all others) doesn't need to be killed. Running away is a viable strategy in this game.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1c2c66el8so4hftykds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1c2c66el8so4hftykds.png" alt="Screenshot: path to cat altar" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whenever possible, I tried showing instead of telling. The protagonist finds themselves surrounded by thick bushes. Technically, players are free to cut through the grass and leave the home meadow, but a subtle path tries to guide them toward the cat statue. Once the first statue is cleared, the main actions should be obvious: scratch, repair the obelisk, and repair the statues. I left two other mechanics unexplained for players to discover: teleporting from other statues to the home statue, and "sleeping" at home to restore health.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv19dy5ojuewv2d65mjsu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv19dy5ojuewv2d65mjsu.png" alt="Screenshot: the magic gauge" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The game occasionally shows a few more dialogs to guide the player, but game progress is primarily tracked by the magic gauge. Once that gauge is full, it means the player has restored their magic power and can restore the barrier once again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reusable components
&lt;/h3&gt;

&lt;p&gt;Over the years, I've learned how to implement various game features. While modern game engines handle most things related to sounds, images, physics, etc., in JS13k you can't afford to use a large, general-purpose game engine. This means much of the core engine functionality has to be carefully optimized.&lt;/p&gt;

&lt;p&gt;Following are some reusable blocks I've developed and improved over the years.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sprites
&lt;/h4&gt;

&lt;p&gt;I've written before about how I've made games without any image files (&lt;a href="https://dev.to/lopis/a-tiny-pixel-art-game-using-no-images-49df"&gt;read more here&lt;/a&gt;). There are different ways to achieve this. Images can be procedurally generated (e.g., the minimap in Meow Mountain), drawn using canvas primitives (e.g., all the background images in &lt;a href="https://jlopes.dev/games/market/" rel="noopener noreferrer"&gt;Market Stree Tycoon&lt;/a&gt;, as well as the UI elements in Meow Mountain), or encoded as part of the source code.&lt;/p&gt;

&lt;p&gt;I was inspired by &lt;a href="https://xem.github.io/" rel="noopener noreferrer"&gt;xem&lt;/a&gt; some years ago to streamline icon creation. When I made Market Street Tycoon, I created my &lt;a href="https://lopis.github.io/mini-pixelart-editor/image-editor.html" rel="noopener noreferrer"&gt;Mini Pixelart Editor&lt;/a&gt;, an online pixelart editor focused on creating pixelart with a limited palette, and then generating a string-encoded version of the sprite along with the necessary JavaScript to decode it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjpb2ofy2rxfu0h3lzwa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjpb2ofy2rxfu0h3lzwa.png" alt="Screenshot: mini image editor" width="800" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This served me well in recent years since my games were never very image-heavy. However, Meow Mountain has dozens of sprites, including several animations. My sprite editor became too cumbersome to use, so I reverted to plain old PNGs. PNGs compress well when using a limited palette, so this worked for a while. However, I soon felt the need to free up precious bytes from my game.&lt;/p&gt;

&lt;p&gt;I wrote scripts to help encode my PNGs into simpler strings, similarly to what the Mini Image Editor did. In general terms, this is how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first script reads a spritesheet and chops it into individual images&lt;/li&gt;
&lt;li&gt;The second script converts an image into an array of values and a list of colors:

&lt;ul&gt;
&lt;li&gt;The list of colors contains all colors present in the image&lt;/li&gt;
&lt;li&gt;The array of values contains the index of each color in the palette&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[[1,0,0,0,0,0,0,0],['#000000']]&lt;/code&gt; represents a 3x3 transparent image with a black pixel in the corner&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The third script encodes this information into a string:

&lt;ul&gt;
&lt;li&gt;Since the image has only 2 colors (black and transparent), we only need 1 bit per pixel&lt;/li&gt;
&lt;li&gt;Our array becomes the number &lt;code&gt;10000000&lt;/code&gt; in binary, which in base 32 is &lt;code&gt;40&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;One advantage of this method is that color information is now detached from pixel information, so I can reuse the same game palette throughout. Reusing the same sprite in different colors also becomes trivial. For my 16x16 icons, I achieved around 40% reduction in sprite size, even accounting for the extra decoding code.&lt;/p&gt;

&lt;p&gt;I'm hoping to further streamline sprite management into a reusable npm package.&lt;/p&gt;

&lt;h4&gt;
  
  
  Font
&lt;/h4&gt;

&lt;p&gt;Similar to sprite rendering, I've been using a pixel font where each character is a string-encoded sprite. Since text is just a single color, each letter can be thought of as a 1-bit sprite. For this game I used a 5x5 pixel font. However, unlike last year, I improved the rendering to allow for non-square glyphs. Until now, my fonts were always monospaced.&lt;/p&gt;

&lt;p&gt;To make font editing simpler, I use my &lt;a href="https://lopis.github.io/mini-pixelart-editor/font-editor.html" rel="noopener noreferrer"&gt;Mini Font Editor&lt;/a&gt;, which is a fork of the mini pixelart editor adapted for creating pixelart fonts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feikx8bhgyi17brunpi3q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feikx8bhgyi17brunpi3q.png" alt="Screenshot: Mini Font Editor" width="800" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The encoded font looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tinyFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6v7ic,6trd0,6to3o,6nvic,55eyo,2np50,2jcjo,3ugt8,34ao,7k,glc,1,opzc,3xdeu,3sapz,8rhfz,8ri26,1bzky,9j1ny,3ws2u,9dv9k,3xb1i,3xbmu,2t8g,2t8s,26ndv,ajmo,fl5ug,3x7nm,n75t,54br,59u0e,53if,rlev,4jrb,1yjk4,4eav,55q95,18zsz,mi3r,574tl,1aedd,ljn9,a1bd,4f1i,a1fs,549t,53ig,5832,1dwsh,6iw6,6ix0,cbsa,6gix,6fk4,aky7,7mbws,cvtyq,deehh,2sfi3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and just like the sprites, each string like &lt;code&gt;6v7ic&lt;/code&gt; is a base 32 number, which when converted to binary represents a 1-bit array of black and transparent pixels.&lt;/p&gt;

&lt;p&gt;The rendering logic is slightly different from sprites, but I'm hoping to bring both together in the same reusable npm package.&lt;/p&gt;

&lt;h4&gt;
  
  
  Emoji icons
&lt;/h4&gt;

&lt;p&gt;This year I also explored using pixel-art emoji icons as a way to save precious bytes. After all, an emoji costs 2-3 bytes, while a single multi-color icon costs tens of bytes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo3v5slukg28qmmcidrk8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo3v5slukg28qmmcidrk8.png" alt="Experimenting with drawing pixelated emoji" width="504" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem is that if you just try to render emoji in canvas, you get a blurry anti-aliased mess. What if we could somehow pre-process emoji to restrict the color palette and eliminate color and alpha anti-aliasing? You can find &lt;a href="https://codepen.io/lopis/pen/gbaKYVo" rel="noopener noreferrer"&gt;my experiments over on code pen&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The downside of this method is that devices and browsers use different emoji fonts. Unless we load an actual emoji font, we lose control of how parts of the game look. However, I believe this goes against the spirit of JS13kGames (though others might disagree). This was my first attempt at this approach, so the code might not be the most efficient or elegant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Quantizes rgba color values to 8bit.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quantizeToPalette&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1-bit transparency&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// transparent&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;qr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Converts an emoji to a pixelated image by quantizing the colors
 * to 8 bit and the transparency to 1 bit.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emojiToPixelArt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HTMLImageElement&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Some emoji are a bit bigger than the font&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spriteScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;spriteScale&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;spriteScale&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Create temporary canvas&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCanvasWithCtx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw emoji in chosen font size&lt;/span&gt;
  &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px sans-serif`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Read pixels&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imgData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImageData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imgData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Create new image data with quantized colors&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outImg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tmpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createImageData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quantizeToPalette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;// red&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// green&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// blue&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// alpha&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;outData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;outData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;outData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;outData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Create a new canvas to draw the quantized image&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;outCanvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outCtx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCanvasWithCtx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;outCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putImageData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outImg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Create an image element from the canvas&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;outCanvastoDataURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Sound effects
&lt;/h4&gt;

&lt;p&gt;Sound effects are small audio clips used in games. Meow Mountain has various sound effects for moving, attacking, taking damage, etc. Since sound files are typically very large, JS13K game developers usually produce sounds by rendering a sound wave and playing it with the Web Audio API. My "sound player" looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playSound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create a new audio buffer.&lt;/span&gt;
  &lt;span class="c1"&gt;// This buffer has 96000 samples (audio "pixels")&lt;/span&gt;
  &lt;span class="c1"&gt;// and 48000 samples per second. More samples&lt;/span&gt;
  &lt;span class="c1"&gt;// per second allows for higher sound quality.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Create an audio buffer, that will contain&lt;/span&gt;
  &lt;span class="c1"&gt;// the sound data.&lt;/span&gt;
  &lt;span class="c1"&gt;// Access a single channel data for mono sound.&lt;/span&gt;
  &lt;span class="c1"&gt;// For stereo, more channels can be used.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// This function expects an f() function,&lt;/span&gt;
  &lt;span class="c1"&gt;// which generates a sound wave for each sample i&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;)&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// The buffer source object controls the &lt;/span&gt;
  &lt;span class="c1"&gt;// playback.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBufferSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// We connect the buffer to the source&lt;/span&gt;
  &lt;span class="c1"&gt;// and connect source to the audio destination&lt;/span&gt;
  &lt;span class="c1"&gt;// which by default is your device's speakers.&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Start the audio.&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The f() wave function can be any function that consumes &lt;code&gt;i&lt;/code&gt; and returns a number.&lt;br&gt;
For example, &lt;code&gt;f(i) =&amp;gt; Math.sin(i)&lt;/code&gt; returns a pure sine wave. The function that plays a step sound in Meow Mountain looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;playSound&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Math.sin((Math.PI * i) / n)&lt;/code&gt; provides a certain pitch to the sound, while &lt;code&gt;(Math.random() * 2 - 1)&lt;/code&gt; provides random noise.&lt;br&gt;
Noise helps the wave sound more "natural" and less like a pure wave.&lt;br&gt;
In this example, &lt;code&gt;0.15&lt;/code&gt; is used to reduce the amplitude of the wave (the sound volume) to 15%.&lt;/p&gt;
&lt;h4&gt;
  
  
  Music
&lt;/h4&gt;

&lt;p&gt;To play music, I use an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet" rel="noopener noreferrer"&gt;audio worklet&lt;/a&gt;.&lt;br&gt;
Worklets are small browser workers that run in the background doing work on a separate thread, preventing the main JavaScript thread from being blocked.&lt;br&gt;
For example, to produce continuous music at a sample rate of 44000Hz, we would need to interrupt the main thread 44000 times per second. In comparison, the typical game cycle updates at 60Hz. Moving music processing to a background worker frees up significant resources for the rest of the game.&lt;/p&gt;

&lt;p&gt;Audio Worklets are a special kind of background worker that handles audio processing and has access to special Web Audio APIs not available in regular JavaScript, namely the &lt;code&gt;AudioWorkletProcessor&lt;/code&gt; class. &lt;em&gt;I'm writing a longer article specifically about creating music with Web Audio, so stay tuned.&lt;/em&gt; In essence, it works the same as I described in the sound effects section. A wave function generates a stream of values that form a wave. In this case, we continue producing successive waves in different frequencies and amplitudes, forming a melody. In Meow Mountain, the wave function looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAMPLE_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NOTE_LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SAMPLE_RATE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BaseSound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;pitchOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sustainTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PitchLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pitch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pitch&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pitchOffset&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pitch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;NOTE_LENGTH&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sustain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;NOTE_LENGTH&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sustainTime&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;NOTE_LENGTH&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sustainTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sustain&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base sound function provides an envelope for each note, with sustain and decay times.&lt;br&gt;
But the timbre of this sound is given by the &lt;code&gt;s()&lt;/code&gt; parameter, which can be something like &lt;code&gt;(t,p) =&amp;gt; Math.tan(Math.cbrt(Math.sin((t * p) / 30)))&lt;/code&gt;.&lt;br&gt;
Explaining why I used tangent, cube root, or sine in this example is beyond the scope of this article.&lt;br&gt;
I encourage you to experiment with wave functions to hear how they sound.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other game features
&lt;/h3&gt;

&lt;p&gt;Now that we've explored some general technical concepts that can be reused in other games, let's dive into some game-specific features.&lt;/p&gt;

&lt;h4&gt;
  
  
  NPC AI
&lt;/h4&gt;

&lt;p&gt;This was my first time implementing any sort of autonomous NPCs in a game. The algorithms are very rudimentary and probably quite inefficient.&lt;/p&gt;

&lt;h5&gt;
  
  
  The villager
&lt;/h5&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4nu4oho6gpud6mb9dd2s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4nu4oho6gpud6mb9dd2s.png" alt="Screenshot: villagers" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The villager spawns somewhere within the radius of the village and then follows this algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look around for empty directions to move to&lt;/li&gt;
&lt;li&gt;If it has a previous direction, move in that direction again with 80% chance&lt;/li&gt;
&lt;li&gt;Otherwise pick another possible direction&lt;/li&gt;
&lt;li&gt;Superstition subroutine:

&lt;ul&gt;
&lt;li&gt;Look ahead 3 cells&lt;/li&gt;
&lt;li&gt;While player is in one of those cells, increase superstition&lt;/li&gt;
&lt;li&gt;Repeat until player is not present&lt;/li&gt;
&lt;li&gt;Otherwise, continue moving&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Continue moving for a whole cell grid, then go to step 1&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As I mentioned above in "Ideas that didn't make it," I wanted complex villagers with proper paths, jobs, and homes. For this first version, I decided that just moving randomly was enough. However, this did mean that sometimes villagers get lost in the forest far away from their home village. In particularly unlucky cases, villagers can get stuck in the player's path, making it impossible to finish the game.&lt;/p&gt;

&lt;h5&gt;
  
  
  The spirit
&lt;/h5&gt;

&lt;p&gt;The spirits spawn somewhere in the vicinity of the cat altar they haunt. They use the pixel-emoji rendering described above. There are &lt;br&gt;
9 spirit species with increasing strength. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsvhbsj7wa5n8ubob89cd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsvhbsj7wa5n8ubob89cd.png" alt="Screenshot: spirits" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They use a simple breadth-first algorithm to find a path to the player.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look around in a 10x10 square for the player&lt;/li&gt;
&lt;li&gt;If the player is found, use breadth-first search to find an empty path from the spirit to the player, going around obstacles

&lt;ul&gt;
&lt;li&gt;Other spirits are not seen as obstacles by the algorithm, allowing spirits to "gang up" on the player&lt;/li&gt;
&lt;li&gt;Take one step towards the player&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If the player is not found, stay still&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Again, this might not be the most efficient. Namely, the BFS algorithm is very simple and makes it fairly easy to evade spirits as they have a tendency to move up and down before moving left or right. An improvement would be to move in the general direction of the player first, or to allow them to move diagonally.&lt;/p&gt;

&lt;h4&gt;
  
  
  Map generation
&lt;/h4&gt;

&lt;p&gt;I wanted to create a grid-based map that felt organic and not monotonous. At the same time, I needed the map generation to be deterministic to avoid cases where some players would find themselves on impossible-to-win maps (although somehow this ended up happening in the final version anyway due to poor testing). For this, I used a deterministic pseudo-random number generator based on a seed.&lt;br&gt;
To do this in a size-efficient way, I developed a map-rendering algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a 160x160 grid&lt;/li&gt;
&lt;li&gt;Fill the whole grid with trees&lt;/li&gt;
&lt;li&gt;Clear out cells along a list of paths in the map:

&lt;ul&gt;
&lt;li&gt;A path is a series of coordinates and widths like &lt;code&gt;[x, y, w]&lt;/code&gt;; for example &lt;code&gt;[[10, 10, 2], [10, 20, 2], [20, 20, 3]]&lt;/code&gt; is a path with 3 vertices and 2 edges, where the first segment has a width of 2 and the second segment has a width of 3&lt;/li&gt;
&lt;li&gt;For each path, apply a level of random jittering to the edges, making them look more organic&lt;/li&gt;
&lt;li&gt;Each path can also contain bushes in random cells&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Clear out a list of circles in the grid:

&lt;ul&gt;
&lt;li&gt;Similar to the paths algorithm, large circular clearings are cleared for some of the villages&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Place the villages on the map from a list of villagers:

&lt;ul&gt;
&lt;li&gt;Randomly place a number of houses within the village radius&lt;/li&gt;
&lt;li&gt;Randomly place a number of villagers&lt;/li&gt;
&lt;li&gt;Randomly place bushes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Place the cat altars on the map from a list:

&lt;ul&gt;
&lt;li&gt;Clear a 3x3 area around the altar and fill it with bushes&lt;/li&gt;
&lt;li&gt;This prevents the altar from being surrounded by trees, ensuring the player has access to it&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Place the Obelisk in the center of the map&lt;/li&gt;
&lt;li&gt;Place the starter spirit&lt;/li&gt;
&lt;li&gt;Place the cat&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;In general, there's nothing revolutionary here. I was able to pack quite a bit of content into the game within the size limit. In retrospect, quite a few things could have been optimized if I had more time, especially things I implemented early on when I wasn't yet settled on all the game mechanics. The map generation is quite messy and overcomplicated at times. The NPCs could have been a bit smarter.&lt;/p&gt;

&lt;p&gt;If you're curious about my code, the source is available on my GitHub. &lt;a href="https://github.com/lopis/meow-mountain" rel="noopener noreferrer"&gt;https://github.com/lopis/meow-mountain&lt;/a&gt;. If you want to check the game out, &lt;a href="https://js13kgames.com/2025/games/meow-mountain" rel="noopener noreferrer"&gt;play it here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>js13kgames</category>
      <category>gamedev</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>🎵 JavaScript music! 🎸 JS13K Games Community Soundtrack 2024 🎹</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Mon, 30 Sep 2024 12:39:04 +0000</pubDate>
      <link>https://dev.to/lopis/js13k-games-community-soundtrack-2024-javascript-music-1h7n</link>
      <guid>https://dev.to/lopis/js13k-games-community-soundtrack-2024-javascript-music-1h7n</guid>
      <description>&lt;p&gt;Just &lt;a href="https://dev.to/lopis/introducing-the-first-js13k-games-community-soundtrack-3kgm"&gt;like we did last year&lt;/a&gt;, the &lt;a href="https://js13kgames.com/" rel="noopener noreferrer"&gt;JS13K Games&lt;/a&gt; community has put together an album with some of the melodies and sounds of this years entries. &lt;/p&gt;

&lt;p&gt;These songs were all generated using javascript and the Web Audio API. You can find links to the respective games in each song.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://js13k.bandcamp.com/album/triskaidekaphobia-js13k-2024" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsgrfat29qt7vpq324gjj.png" alt="Screenshot of the playlist" width="400" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>music</category>
      <category>javascript</category>
      <category>js13k</category>
    </item>
    <item>
      <title>Gotchas while developing a tiny web game</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Thu, 26 Sep 2024 17:22:37 +0000</pubDate>
      <link>https://dev.to/lopis/gotchas-while-developing-a-tiny-web-game-4528</link>
      <guid>https://dev.to/lopis/gotchas-while-developing-a-tiny-web-game-4528</guid>
      <description>&lt;p&gt;Another year, another &lt;a href="https://js13kgames.com/" rel="noopener noreferrer"&gt;JS13K Games&lt;/a&gt; edition is over, the game jam where you have to develop a game under 13KB (&lt;a href="https://js13kgames.com/2024/games/thirteen-terrible-stunts" rel="noopener noreferrer"&gt;here's mine 🙌&lt;/a&gt;). This strict size limitation means that we often have to implement most things from scratch, as most game engines are prohibitively large. This means that we're constantly reimplementing the metaphorical wheel, which leads to fresh new bugs.&lt;/p&gt;

&lt;p&gt;As always, we're encouraged to playtest our colleagues' games. The following is a list of a few gotchas, in very limited depth, which I've collected while testing games this year.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Players don't have the same fonts as you
&lt;/h1&gt;

&lt;p&gt;This should be obvious, but each computer has a different set of fonts installed. This means text will look different for other players. It's not just that individual letters look different, but the whole size occupied by them is different, both in width and height, regardless of font size. Don't rely on text having the same size for other players.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdbct12ref8oql7tvycs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdbct12ref8oql7tvycs.png" alt="Screenshot of the same Dungeon of Curse" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above screenshot from the great game &lt;a href="https://js13kgames.com/2024/games/dungeon-of-curse" rel="noopener noreferrer"&gt;Dungeon Of Curse&lt;/a&gt; is an example of this. The title screen is cut off because the font is unexpectedly wide in my system.&lt;/p&gt;

&lt;p&gt;Because embedding whole font files is too expensive, many developers simply create tiny fonts for their own games. But if you go the easy route and use system fonts, be sure to leave enough wiggle room for chunkier fonts.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. There are screens with very high refresh rates.
&lt;/h1&gt;

&lt;p&gt;For the longest time, most screens have had refresh rates of 60Hz. And so when calling &lt;code&gt;requestAnimationFrame&lt;/code&gt; on javascript, you would expect to get 60 updates per second. However, higher refresh rates are very common nowadays, ranging from 75Hz to 120Hz or even 400Hz.&lt;/p&gt;

&lt;p&gt;This is important to keep in mind because if you update your game state equally on each frame while disregarding the actual frame rate, your game might become too fast. So you either have to limit your game's refresh rate, or make it so that updates in your game depend on the time interval between updates.&lt;/p&gt;

&lt;p&gt;I'm not going to pretend I'm an expert — this issue caught me off guard during this jam — so here is a helpful link on the subject:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gafferongames.com/post/fix_your_timestep/" rel="noopener noreferrer"&gt;https://gafferongames.com/post/fix_your_timestep/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Beware of accessibility features
&lt;/h1&gt;

&lt;p&gt;It's honestly already hard enough to fit a whole game in 13kb and to build it in just a month. But there are people out there who struggle with using technology &lt;em&gt;every day&lt;/em&gt;. They sometimes use accessibility features in their browser or OS which might clash with the games we create.&lt;/p&gt;

&lt;p&gt;One such case is the feature &lt;a href="https://support.mozilla.org/en-US/kb/advanced-panel-settings-in-firefox?redirectslug=settings-network-updates-and-encryption&amp;amp;redirectlocale=en-US#w_accessibility" rel="noopener noreferrer"&gt;"search as you type"&lt;/a&gt;, which causes typing to initiate finding in the page. This can be circumvented by calling &lt;code&gt;preventDefault()&lt;/code&gt; while the player is controlling your game with the keyboard.&lt;/p&gt;

&lt;p&gt;It's up to you whether and how much you want to prepare for computers with diverse setups. Just keep in mind that not paying attention to those details might mean your game is completely broken for some players, and it's unfair to blame &lt;em&gt;them&lt;/em&gt; for it.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. Voice synth
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"window.speechSynthesis is undefined"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a weird one. In case you're not familiar with it, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/speechSynthesis" rel="noopener noreferrer"&gt;Web Speech API&lt;/a&gt; is a web API that lets you reproduce voice&lt;/p&gt;

&lt;p&gt;Apparently on some systems (like mine) this is disabled or absent. It can be toggled in Firefox in &lt;code&gt;about:config&lt;/code&gt; under &lt;code&gt;media.webspeech.synth.enabled&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I didn't penalize any game when this happened, despite their games crashing catastrophically. But the fix is as easy as verifying that &lt;code&gt;speechSynthesis&lt;/code&gt; with conditional chaining: &lt;code&gt;window?.speechSynthesis&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;And do keep in mind that the browser is actually using the voice synthesizer of the operating system. No OS voice synthesizer installed, no &lt;code&gt;speechSynthesis&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  4.1 Incorrect voice language
&lt;/h1&gt;

&lt;p&gt;Even if the user has a voice synthesizer, has it enabled on the browser, and even if &lt;code&gt;window.speechSynthesis&lt;/code&gt; is defined, you're &lt;em&gt;still&lt;/em&gt; not off the hook! To everyone's surprise, there are people out there with devices in languages other than English (&lt;code&gt;&amp;lt;/s&amp;gt;&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When calling &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/getVoices" rel="noopener noreferrer"&gt;getVoices()&lt;/a&gt; to use the voice synthesizer, you might not be provided an English voice. Or perhaps not as the primary voice. Hilarity ensues when you try to read an English sentence in another language.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Browser compatibility is still important
&lt;/h1&gt;

&lt;p&gt;Talking about different setups, many people seem to think that Chrome is the only Internet browser around these days. Or worse, that whoever doesn't use a Chromium-based browser deserves a worse experience.&lt;/p&gt;

&lt;p&gt;I'm a strong apologist of the open Web and the current Chrome monoculture is harming it. Google shouldn't get to decide what becomes a standard and what doesn't.&lt;/p&gt;

&lt;p&gt;It's your job as a developer to make sure your code runs correctly on the different browsers.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. People are even more diverse than computers!
&lt;/h1&gt;

&lt;p&gt;While computers and browsers are diverse, people are even more! While developing a game, you will develop strong biases that can be hard to get rid of without external help. Is the game goal clear enough? Is it too hard, or does it ramp up in difficulty too fast? Are people used to playing games with just their keyboards anymore?&lt;/p&gt;

&lt;p&gt;Besides the generous help of playtesters from our community, this year we were lucky to have access to &lt;a href="https://poki.com" rel="noopener noreferrer"&gt;Poki&lt;/a&gt; as a playtesting tool. This experience truly humbled me.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Out of 20 playtesting sessions, 15 players quit within 5 seconds because they didn't understand that the game didn't use the mouse&lt;/li&gt;
&lt;li&gt;If the goal isn't clear without reading instructions, you already lost 90% of the players motivation to try your game. I'm not exaggerating. Like in cinema, you should show, not tell, as no one likes cheap exposition.&lt;/li&gt;
&lt;li&gt;Most players will not read things unless they realize they have to; they must be given time to accommodate to that mindset.&lt;/li&gt;
&lt;li&gt;Players have different backgrounds; while game concepts might be obvious for you, they won't be obvious for everyone.&lt;/li&gt;
&lt;li&gt;A little hand-holding in the beginning is very useful, as letting the player win a little improves their engagement&lt;/li&gt;
&lt;li&gt;However, too much hand-holding or lack of game progression and the game gets boring fast.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>js13k</category>
    </item>
    <item>
      <title>Introducing the first JS13K Games Community Soundtrack</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Mon, 23 Oct 2023 15:54:51 +0000</pubDate>
      <link>https://dev.to/lopis/introducing-the-first-js13k-games-community-soundtrack-3kgm</link>
      <guid>https://dev.to/lopis/introducing-the-first-js13k-games-community-soundtrack-3kgm</guid>
      <description>&lt;p&gt;The best games all have something in common: great soundtracks.&lt;br&gt;
But I'm sure I don't need to tell you that great game soundtracks don't need big budget philharmonic compositions to e fun experiences. In fact, many fun games have simple tunes created in Javascript using a myriad of tools, or even programmed by hand directly using the Web Audio API.&lt;/p&gt;

&lt;p&gt;Despite the tough size constraints, the top games in JS13K wouldn't reach the top if they didn't also get top marks in the audio category. Some of the games offered complex, dynamic, and catchy soundtracks that seamlessly blended with the gameplay.&lt;/p&gt;

&lt;p&gt;This year our community decided to extend our cooperation a bit longer. We're banding together to collect these tunes and create the first ever &lt;strong&gt;JS13K Games Community Soundtrack&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://js13k.bandcamp.com/album/js13kgames-2023-community-soundtrack" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24b72sm7zs1mmb7hm23v.png" alt="Playlist in Bandcamp" width="560" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Join the project
&lt;/h2&gt;

&lt;p&gt;Did you participate in this year's competition? Has your game included a melody that deserves to be heard on loop? I'd like to invite you to join us over on slack to join our group and share your music. If you need help extracting or recording your music, you'll find some helping hands on slack too.&lt;/p&gt;

</description>
      <category>js13k</category>
      <category>javascript</category>
      <category>music</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>A tiny pixel art game using no images</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Wed, 13 Sep 2023 10:49:00 +0000</pubDate>
      <link>https://dev.to/lopis/a-tiny-pixel-art-game-using-no-images-49df</link>
      <guid>https://dev.to/lopis/a-tiny-pixel-art-game-using-no-images-49df</guid>
      <description>&lt;p&gt;As is usual this time of the year, I participated in the &lt;a href="//js13kgames.github.io"&gt;Js13Games&lt;/a&gt; game jam where we have 1 month to create a game that fits under 13KB.&lt;/p&gt;

&lt;p&gt;I didn't have a lot of time this year for a variety of reasons, but I tried my best to complete something. This year's theme was "XIII Century". I didn't want to do the obvious and focus on wars and knights, both because that seemed too obvious and also because creating games about violence is just &lt;em&gt;not my jam&lt;/em&gt; (pun intended). So I thought I would do something about merchants and trading.&lt;/p&gt;

&lt;p&gt;This year I wanted to challenge myself to learn more about WebGL. And because images are often the densest part of these games (each tiny image can easily gobble up a couple precious KBs!), I wanted to create a game with images that were fully generated by code. This post talks about my experience with making pixel art in the web canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concept art
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9e8ndt2rdydpndrse04g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9e8ndt2rdydpndrse04g.png" alt="Initial game concept art made with GIMP" width="800" height="720"&gt;&lt;/a&gt;&lt;/p&gt;
Not actual gameplay!



&lt;p&gt;I idealized a stand in a market where the player would be selling the goods they purchased before. The play could choose the price and arrangement of items. Barrels could hold liquids like beer or conserves like olives, while the boxes could present fruits, bread and other objects.&lt;/p&gt;

&lt;p&gt;I called the game &lt;a href="https://lopis.github.io/market-street-tycoon/dist/" rel="noopener noreferrer"&gt;Market Street Tycoon&lt;/a&gt; because I dreamed that the game would have progressing levels where the player would control all the shops in the market. Alas that was a bit too much for my free time during the month-long competition.&lt;/p&gt;

&lt;p&gt;I started with drawing some concept art. GIMP is a pretty alright pixel art editor. I decided to create a game with the same resolution as the Gameboy Color, 160 x 144. This decision has several implications. On one hand it leaves little space to write text and add controls, and anything drawn will have very little detail. On the other hand, pixel art this small is great to mask your bad drawing skills. It's also much easier to develop a game with a fixed resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pixel art in the canvas
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdd20lqf3884xc1ofwys.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdd20lqf3884xc1ofwys.png" alt="Heart emoji, cloud emoji, and the text " width="409" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The web canvas is notoriously inhospitable for pixel art. Unless one takes care to align everything very neatly to the pixel grid, you can expect very blurry lines. While there are options to disable anti-aliasing of images, the same isn't possible for drawing shapes or text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkcy20morkdcn8uvzzcw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkcy20morkdcn8uvzzcw.png" alt="Close up of a  circle rendered using the ellipse function, anti-aliased" width="531" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the native 2D rendering functions won't be enough. Yes, it's possible do draw sharp rectangles, but ellipses and lines won't render sharply when zoomed in. Even when drawing rectangles, there are some quirks in the way certain browsers do it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rectangles and borders
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc01znnmy4z1ggz5ocsjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc01znnmy4z1ggz5ocsjx.png" alt="Comparison of a stroked square in firefox and chrome, showing blurry lines in both, and some quirks in the corners in firefox" width="400" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the example above, I'm rendering a 20px x 20px square with a black 1px stroke. Not only does the stroke render 2px wide, but it is also semi-transparent. This is because, although the square is aligned to the grid, the stroke is centered in the edge of shape, so the stroke goes half pixel to each side. Standard DPI computers can't deal with half-pixels, so that's what we get. Also, you might have noticed that it looks different in Firefox and Chrome. I had this problem initially when drawing the bricks in the background of the game.&lt;/p&gt;

&lt;p&gt;My quick solution was to simply draw 2 squares, one rendering inside the other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;black&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sharp eye might have noticed the obvious limitation of this approach: it draws opaque rectangles. This means it's not possible to draw transparent rectangles on top of other things. But for my use case of drawing bricks in the background, that's perfectly acceptable. In fact, I only need to draw the "stroke" as a single screen-sized dark backdrop to speed things up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Placing pixels on the canvas
&lt;/h3&gt;

&lt;p&gt;There are two common ways to draw individual pixels. The first one is to form an &lt;code&gt;ImageData&lt;/code&gt; object with the desired dimension and fill in its &lt;code&gt;data&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yellow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// copies a section of the canvas into imagedata&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;imageData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImageData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;       &lt;span class="c1"&gt;// red channel&lt;/span&gt;
  &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;// green&lt;/span&gt;
  &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;// blue&lt;/span&gt;
  &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="c1"&gt;// alpha&lt;/span&gt;
  &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// places the image data back into the canvas&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putImageData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68bdwkzxm5kz53s539gx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68bdwkzxm5kz53s539gx.png" alt="Example of a diagonal line drawn in pixels" width="666" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reason why we need to use &lt;code&gt;getImageData&lt;/code&gt; and then &lt;code&gt;putImageData&lt;/code&gt; is because, even though most pixels of the image data are transparent (alpha channel = 0), when we put the data back in the canvas, it replaces whatever was in that position.&lt;/p&gt;

&lt;p&gt;It's also confusing what we're doing in the &lt;code&gt;while&lt;/code&gt; in the example above. &lt;code&gt;imagedata.data&lt;/code&gt; is a one-dimensional array, but our image is two-dimensional, so we need to map &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; to the index of the array.&lt;/p&gt;

&lt;p&gt;The second method doesn't require any mapping, and can draw "pixels" directly on the canvas. Essentially, the second method draws a tiny rectangle for each pixel. The following snippet renders the same yellow square from before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yellow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;black&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="nx"&gt;posx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;posy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm pretty sure this is terribly inefficient, but it is also really intuitive because it uses the same functions (i.e. &lt;code&gt;rect&lt;/code&gt;) used to draw the bricks, the menus, the buttons etc. For that reason, I mostly used this approach for drawing pixel art.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drawing pixelated lines
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrjxt3tk3c0djbbgxro0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrjxt3tk3c0djbbgxro0.gif" alt="GIF of the clock in the game, showing the pointer line" width="428" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I didn't want to reinvent the wheel here. Surely someone has created a line-drawing algorithm. I looked around for algorithms and did find different ones with varying complexity. After spending sometime trying to make them work for my use case, I sat down and decided that this has to be simple enough... So I reinvented the wheel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;drawLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// It's necessary to start from rounded coordinated&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Calculate the deltas&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dmax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// Either dx or dy will equal 1&lt;/span&gt;
    &lt;span class="c1"&gt;// and the other will be less than 1&lt;/span&gt;
    &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;dmax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;dmax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dmax&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// The points won't have whole coordinated;&lt;/span&gt;
      &lt;span class="c1"&gt;// We'll round them later.&lt;/span&gt;
      &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;py&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// We round the coordinates when drawing the pixel.&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;py&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to draw the clock, I use &lt;code&gt;cos&lt;/code&gt; and &lt;code&gt;sin&lt;/code&gt; to draw lines that start in the center of the clock and go around in a circular motion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WHITE2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawCircle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WIDTH&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// t between 1 and 0&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WIDTH&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;xc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;yc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;BLACK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Drawing pixel art icons
&lt;/h3&gt;

&lt;p&gt;Despite the click-bate-y title of this article, I did use "images" in the game to draw the little icons for the apples and etc. But instead of image files, they are represented by a compressed string that codifies the position of the pixels and their color in a palette of 8 colors.&lt;/p&gt;

&lt;p&gt;Here I've used &lt;a href="https://xem.github.io/miniPixelArt/" rel="noopener noreferrer"&gt;xem's mini pixel art editor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Xem's code is strongly minified, so I had to rewrite it slightly to understand what was going on. I minimize all my code later, so I like to always keep my source code tidy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Adapted from https://xem.github.io/miniPixelArt/&lt;/span&gt;
  &lt;span class="c1"&gt;// An icon looks like this: '@X@@C@RSERRBWRGx@'&lt;/span&gt;
  &lt;span class="nf"&gt;drawIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;
      &lt;span class="c1"&gt;// Decodes each character&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Draw the pixels&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;PALETTE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Drawing text
&lt;/h3&gt;

&lt;p&gt;For this i simply used &lt;a href="https://github.com/xem/miniPixelFont" rel="noopener noreferrer"&gt;xem's mini pixel font&lt;/a&gt;. This is pretty ingenious. First the text is rendered in a separate canvas in black and white, but anti-aliased. Then the pixels are read from it and converted to black or white depending on a threshold of brightness. Finally, the text is drawn on the game pixel-by-pixel.&lt;/p&gt;

&lt;p&gt;The advantages of this method is that I don't need to include a pixel font in my game - I'm using your system's fonts! I also can leverage using unicode to provide symbols for my game.&lt;/p&gt;

&lt;p&gt;The downside is that is doesn't look great!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs56wuk0yguavn6jl706f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs56wuk0yguavn6jl706f.png" alt=" " width="495" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some letters can be hard to read and their shape isn't consistent. Finally, drawing all these pixels is, again, terribly inefficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;It was a lot of fun to make pixel art with the canvas. I look back at my older games and this one looks so much better. I'm particularly proud of the slick curtains animation.&lt;/p&gt;

&lt;p&gt;This article is by do means a tutorial or an explanation of bad practices. This project was built under strong constraints (game size limit and 1 month deadline) and these are simply the solutions I found for my problems.&lt;/p&gt;

&lt;p&gt;If you're still curious about the code of the game, you can check the source out on my github: &lt;a href="https://github.com/lopis/market-street-tycoon" rel="noopener noreferrer"&gt;https://github.com/lopis/market-street-tycoon&lt;/a&gt;. If you want to play the game, you can do so here: &lt;a href="https://js13kgames.com/entries/market-street-tycoon" rel="noopener noreferrer"&gt;https://js13kgames.com/entries/market-street-tycoon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Update
&lt;/h2&gt;

&lt;p&gt;In the time since publishing this article, I went back and redid the way I draw fonts in an easy and cheap (in kb) way. It's not without drawbacks. I hope to write a bit about it too. If there's interested, let me know in the comments!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>js13k</category>
    </item>
    <item>
      <title>Creating a tiny zen game using Kontra</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Thu, 01 Jun 2023 06:16:43 +0000</pubDate>
      <link>https://dev.to/lopis/creating-a-tiny-zen-game-using-kontra-jm8</link>
      <guid>https://dev.to/lopis/creating-a-tiny-zen-game-using-kontra-jm8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Note: I forgot to post this article, and almost 2 years have passed since. This game was submitted for the 2021 competition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another year has passed and with it another &lt;a href="https://js13kgames.com/" rel="noopener noreferrer"&gt;JS13K game jam&lt;/a&gt; has come to an end. If you're not familiar with this game jam, the challenge is to create a web game that fits in under 13KB (zipped), including code, and assets. This year's theme was "space", so it made me think of something like "dimension". My interpretation of this theme was to create an empty space where some agents would roam and interact.&lt;/p&gt;

&lt;h1&gt;
  
  
  The game concept
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdok8ypi8xm048j324tkj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdok8ypi8xm048j324tkj.png" alt="alt text" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had lots of ideas for this game, but not so much time to implement them. Even though the competition runs for 1 month, work got in the way, and then I was away with my family for some time too. I imagine an agent-based game where each agent was an ant explored the area around the ant nest. I envisioned a game map with predators, food and obstacles.&lt;/p&gt;

&lt;p&gt;The main game mechanic that I wanted to focus on was the ability of ants to use chemical signals to follow each other- If you want to learn more about ants, watch &lt;a href="https://www.youtube.com/watch?v=N8rGEiHI52c" rel="noopener noreferrer"&gt;these&lt;/a&gt; &lt;a href="https://www.youtube.com/watch?v=vG-QZOTc5_Q" rel="noopener noreferrer"&gt;videos&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Basically, each time an ant moves, she leaves a chemical trail behind. Other ants are able to pick up on this trail. The more ants follow a trail, the more they reinforce it, and the more ants tend to follow that path.&lt;/p&gt;

&lt;p&gt;While implementing these ant-like behaviours, I became so obsessed with watching my creations move around and follow that I ended up focusing my whole game on the concept of ant watching, and creating a relaxing artistic experience.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Algorithm
&lt;/h1&gt;

&lt;p&gt;I went back and forth thinking about how to better represent the ant space. I initially tried to use a square or hexagonal grid where ants would move in discreet steps. The chemical trails would also only exist in discreet locations in space. The problem with using a grid if that it would make the trails too restricted and ugly, and animating the ants would be more annoying. Furthermore, maintaining the 3-dimensional array to represent that hex grid and all its data got very ugly, very fast.&lt;/p&gt;

&lt;p&gt;The solution I came up with was to use the actual canvas as a data store for the trails. The ants's position is kept by Kontra (the game engine) and the ants "paint" the blue trail on the canvas on each update.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxm57ljpphwnxztm4uzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxm57ljpphwnxztm4uzf.png" alt="alt text" width="758" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each ant follows a very rudimentary algorithm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the "blueness" ahead is high, move forward&lt;/li&gt;
&lt;li&gt;If the "blueness" on the right if high, turn slightly to the right&lt;/li&gt;
&lt;li&gt;If the "blueness" on the left if high, turn slightly to the left&lt;/li&gt;
&lt;li&gt;Else, continue forward with a chance of turning slightly to the side randomly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "blueness" of a pixel, as the name indicates, is how blue a pixel is. The more ants leave a trail on a place, the bright the blue color, and the higher its blueness. In a future iteration of this experiment, ants would leave trails of different colors to mean different things. In natural, ants leave different chemical trails to mean "just exploring" or "i found food". By using the 3 RGB channels of each pixel on the canvas as separate data, I could theoretically have overlapping trails of different types.&lt;/p&gt;

&lt;p&gt;But for now, all my ants do is explore, so all trails are blue.&lt;/p&gt;

&lt;p&gt;Nonetheless, even with this very rudimentary algorithm, the emergent behavior of dozens of ants was already quite interesting to watch. I played around with the values a lot to try to fine tune how the "ant art" looked; the final version use these values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The "trail radius" was set to 2 pixels&lt;/li&gt;
&lt;li&gt;When looking ahead or to the sides, the ant looks about 4 pixels in that direction (the "lookout distance", twice the "trail radius")&lt;/li&gt;
&lt;li&gt;When inspecting the sides, the ant looks 60 degrees to the right and left (the "lookout angle")&lt;/li&gt;
&lt;li&gt;When turning to the sides, the ant only turns 30 degrees (half of the "lookout angle").&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Animation
&lt;/h1&gt;

&lt;p&gt;I wanted ants that were more than black dots on the screen, so I tried to learn &lt;a href="https://www.youtube.com/watch?v=DLczo4N3CPI" rel="noopener noreferrer"&gt;how ants walk&lt;/a&gt;. Essentially they walk 3 legs at a time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91yw9wxf26kcse9s9blg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91yw9wxf26kcse9s9blg.png" alt="Alt Text" width="800" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used Inkscape to create a basic model of the ants, and then moved their legs a little. I figured  8 frames for the whole movement should be enough, given the small size of the sprites. Using Kontra, it was trivial to implement the sprite animation. You just provide the sprite dimensions, the frame rate, and the frames that correspond to each animation, and the engine does the rest.&lt;/p&gt;

&lt;h1&gt;
  
  
  Kontra
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://straker.github.io/kontra/" rel="noopener noreferrer"&gt;Kontra&lt;/a&gt; is a lightweight library for creating javascript games. It's the first time I have used any sort of game engine in this competition, but I found it immensely helpful in allowing me to quickly prototype my game without worrying about the basics.&lt;/p&gt;

&lt;p&gt;Kontra provides a game loop, a sprite-based character system, events, input controls, as well as helpful utilities to check if sprites are colliding and move them easily.&lt;/p&gt;

&lt;p&gt;The documentation is a bit lacking at times, but for such a tiny library, it's really not that much of a problem.&lt;/p&gt;

&lt;h1&gt;
  
  
  Next steps
&lt;/h1&gt;

&lt;p&gt;Maybe because this was a very relaxing game to make and play, for the first time I actually feel like continuing the development of this project. Here are some extra ideas I have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sense of direction
&lt;/h2&gt;

&lt;p&gt;I want to improve the sense of direction of the ants. In the real world, ants seem to know in which direction home is on a trail. My ants just follow the path mindlessly. One way to do this would be to make the strength of the trail weaker as they move forward, creating a tendency for ants to return home vs move farther away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feed the ants
&lt;/h2&gt;

&lt;p&gt;I want ants to explore the space with a purpose. I want to add bits of food that they want to carry home. For this I will need to create a different trail for them to follow that indicates the presence of food.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smarter explorers
&lt;/h2&gt;

&lt;p&gt;Some ants in nature follow an interesting exploration algorithm: if they bump into many ants, turn more frequently; if they don't, follow a more straight line. This simple tactic allows ants to cover a bigger area more efficiently without a map. They will naturally venture outwards.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>js13k</category>
      <category>gamedev</category>
      <category>gamejam</category>
    </item>
    <item>
      <title>It's ♿️ Global Accessibility Awareness Day!🌈 Have you done your part?</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Wed, 17 May 2023 15:51:54 +0000</pubDate>
      <link>https://dev.to/lopis/its-global-accessibility-awareness-day-have-you-done-your-part-1bkb</link>
      <guid>https://dev.to/lopis/its-global-accessibility-awareness-day-have-you-done-your-part-1bkb</guid>
      <description>&lt;p&gt;Accessibility comes in many shapes and forms, and doesn't benefit just those with chronic disabilities. It's also often the burden of the developers to learn and "apply accessibility" as an after though, but everyone has responsibilities.&lt;/p&gt;

&lt;p&gt;The third Thursday of May is &lt;strong&gt;Global Accessibility Awareness Day&lt;/strong&gt;! How are you helping your company creating more accessible products? Is your workplace accessible? &lt;/p&gt;

</description>
      <category>a11y</category>
      <category>a11yday</category>
    </item>
    <item>
      <title>Who's responsible for accessibility in software development?</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Wed, 17 May 2023 15:00:00 +0000</pubDate>
      <link>https://dev.to/lopis/whos-responsible-for-accessibility-in-software-development-1b0c</link>
      <guid>https://dev.to/lopis/whos-responsible-for-accessibility-in-software-development-1b0c</guid>
      <description>&lt;p&gt;It's &lt;a href="https://a11y.day/" rel="noopener noreferrer"&gt;Global Accessibility Awareness Day&lt;/a&gt;! So in the spirit of a11y day, I have a question for you:&lt;/p&gt;

&lt;p&gt;In software development projects, namely those with UIs, &lt;em&gt;who's responsible for paying attention to the accessibility&lt;/em&gt; of said UIs? Is it the project manager? Or is it the designer? The software developer? Perhaps the QA engineer? As you can probably guess, everyone has their own responsibilities.&lt;/p&gt;

&lt;p&gt;I asked my colleagues to answer two questions: what is your role, and how does it contribute to developing an accessible product?&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Product Manager&lt;/li&gt;
&lt;li&gt;Product Designer&lt;/li&gt;
&lt;li&gt;Content Designer&lt;/li&gt;
&lt;li&gt;Frontend Engineer&lt;/li&gt;
&lt;li&gt;Mobile Engineer&lt;/li&gt;
&lt;li&gt;QA Engineer&lt;/li&gt;
&lt;li&gt;A11y Champion&lt;/li&gt;
&lt;li&gt;
Final Notes

&lt;ul&gt;
&lt;li&gt;Backend Engineer&lt;/li&gt;
&lt;li&gt;IT Admin&lt;/li&gt;
&lt;li&gt;User support&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F265y7yl5robnp9xhi925.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F265y7yl5robnp9xhi925.png" alt="Team planning around a table" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Product manager
&lt;/h2&gt;

&lt;p&gt;As a product manager, I define a vision, strategy and roadmap for our products, based on the overall company direction.&lt;/p&gt;

&lt;p&gt;My daily tasks include prioritising features and requirements,&lt;br&gt;
coordinating collaboration with other teams when needed, writing acceptance criteria (AC), analysing trends and patterns in the industry and analysing customer feedback and analytics data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I ensure that accessibility is part of the "definition of done"&lt;/strong&gt; of a project or feature by planning for the necessary capacity for implementing accessible features. I review projects regularly to stay up-to-date with the latest accessibility principles. I work with the company's accessibility experts - mostly developers - to establish a baseline of accessibility requirements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F43r2ig89r2i9w9e4t088.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F43r2ig89r2i9w9e4t088.png" alt="Abstract design popping out of a computer" width="789" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Designer
&lt;/h2&gt;

&lt;p&gt;As a product designer, I help keep our design system alive and up-to-date. This includes not only creating new components, composing layouts and design interactions and animations, but also establishing and following design principles across the organization.&lt;/p&gt;

&lt;p&gt;These design principles include a set of accessibility guidelines that must be followed when creating design system (DS) components, as well as when developing feature work. I routinely review these components in terms of color contrast, font size, spacing, copy, and navigability, to make sure they follow the established guidelines.&lt;/p&gt;

&lt;p&gt;I also lead user surveys and user tests that allow me to see how real users use our products, and how they perceive different prototypes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Designer
&lt;/h2&gt;

&lt;p&gt;I strive to understand how content (i.e. text, image or otherwise) influences user behaviour. I'm responsible for creating and shaping the content strategy and user experience in the different mediums (web, mobile, socials). My role blends creativity, storytelling, and data-driven insights to create cohesive and persuasive digital experiences.&lt;/p&gt;

&lt;p&gt;By considering accessibility guidelines and incorporating practices such as alt text for images, proper heading structure, descriptive text in links and buttons, succinct and clear copy, I help make information accessible to a broader audience, enhancing the overall user experience for people with diverse needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fio47vj4ujxgbvums4n2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fio47vj4ujxgbvums4n2y.png" alt="Engineering coding on a computer" width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend Engineer
&lt;/h2&gt;

&lt;p&gt;As a developer, I bring designs to life through code following the acceptance criteria. I write automated tests to validate my implementation, but also manually test it before a release. I also review my colleague's code, and participate in design reviews and project planning.&lt;/p&gt;

&lt;p&gt;I'm responsible for staying up-to-date with web accessibility standards. I use semantic HTML and ARIA labels, paying special attention to the interactive elements of our pages, like forms or dynamic content. Sometimes, special labels need to be added specifically for screen reader users.&lt;/p&gt;

&lt;p&gt;Finally, I set up a11y automation tools like Lighthouse to assure high code quality throughout the team, which flag various accessibility issues like color contrast and HTML problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  QA Engineer
&lt;/h2&gt;

&lt;p&gt;My main role is being the quality coach of the team. I establish cross-project QA guidelines, create templates for QA mind maps, create testing plans for developers, and establish cross-project acceptance criteria, like target browser versions.&lt;/p&gt;

&lt;p&gt;I also write automated tests, namely end-to-end acceptance tests, e.g. using tools like browserstack. Manual QA of whole projects is also often my responsibility.&lt;/p&gt;

&lt;p&gt;I ensure that accessibility requirements are included in the product ACs as well as in the technical ACs. I also use tools that report several a11y issues in the browser like WAVE while testing products.&lt;/p&gt;

&lt;h1&gt;
  
  
  Mobile Engineer
&lt;/h1&gt;

&lt;p&gt;As a mobile engineer, I create and maintain our company's mobile apps. I create new features based on a design, and fix bugs reported to us. It's my responsibility to understand mobile UX patterns. I communicate effectively with PM, Leads and Team Member regarding expectations, and participate in project planning.&lt;/p&gt;

&lt;p&gt;I must ensure that elements are easily discernible by color and contrast regardless of the users' theme. I identify where alt texts are needed for elements which are either directly relevant (e.g. buttons) or which is only represented graphically. A big part of this is knowing the semantics of each type of element.&lt;/p&gt;

&lt;p&gt;I also perform manual accessibility QA using the native screen reader, using alternative navigation methods, using apps without looking at the screen, or with a single hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6evke9dd4u13u5688hk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6evke9dd4u13u5688hk.png" alt="Person on a stage giving a talk while others listen and ask questions" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility Champion
&lt;/h2&gt;

&lt;p&gt;The "Accessibility Champion" can be anyone in the company, really. &lt;/p&gt;

&lt;p&gt;As the Accessibility Champion, I campaign for inclusive design at every step of product development, as well as in other company functions. To do that, I keep up-to-date with the community standards and strive to learn as much as possible about the topic. The Accessibility Champion can be an abled person.&lt;/p&gt;

&lt;p&gt;I organize cross-team and cross-discipline workshops. For example, I invite experts to increase company knowledge on a11y, and to show us how accessibility features are used both on computers and smartphones. I also write cross-team documentation and guidelines, and try to mentor new champions from different teams.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f4n4ebhptbzk2gnk9oj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f4n4ebhptbzk2gnk9oj.png" alt="Two people planting a tree" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;Without trying to be exhaustive, this is should give you an idea of who's responsible for accessibility in a company: everyone. I'll leave you with a few other examples:&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend Engineer
&lt;/h3&gt;

&lt;p&gt;I try to think about how different users will use the products I develop, e.g. accessing content in different languages and locations. I try to involve users early on to ensure proper terminology is used during project definition. I also believe good code documentation is important for the accessibility of my own colleagues.&lt;/p&gt;

&lt;h3&gt;
  
  
  IT Admin
&lt;/h3&gt;

&lt;p&gt;My responsibility is to provide my colleagues with the hardware, software, user accounts and other technical faculties they need to perform their work. Because I don't interact directly with our users, my main concern when it comes to accessibility is to fullfil the requirements of those with a11y needs. This could be making sure our video calls software transcripts are correctly set up, or provisioning special hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  User support
&lt;/h3&gt;

&lt;p&gt;My daily tasks include answering user requests, building up our help center, and facilitating feedback. I strive to use inclusive and easy language, and to make the help center more intuitive and accessible. It's important to establish guidelines on how to respond to user feedback in an inclusive manner.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>a11yday</category>
      <category>webaccessibility</category>
      <category>accessibledesign</category>
    </item>
    <item>
      <title>12 things I learned as a volunteer programming teacher</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Tue, 06 Jul 2021 09:54:08 +0000</pubDate>
      <link>https://dev.to/lopis/12-things-i-learned-as-a-volunteer-programming-teacher-2pd6</link>
      <guid>https://dev.to/lopis/12-things-i-learned-as-a-volunteer-programming-teacher-2pd6</guid>
      <description>&lt;p&gt;For the past semester, I've joined the &lt;a href="//redi-school.org/"&gt;ReDI School of Digital Integration&lt;/a&gt; as a volunteer teaching Introduction to Programming using Python. ReDI's students &lt;a href="https://www.redi-school.org/mission" rel="noopener noreferrer"&gt;are mainly refugees and migrants who want to learn digital skills&lt;/a&gt;, so their backgrounds are very diverse. Most of them had never written a single line of code before.&lt;/p&gt;

&lt;p&gt;This was not my first experience with teaching. I had taught high-school students to program using the visual programming tool &lt;a href="https://www.alice.org/" rel="noopener noreferrer"&gt;Alice&lt;/a&gt; for 2 weeks. I've also taught short workshops before. However, this was my first time teaching a whole semester.&lt;/p&gt;

&lt;p&gt;These are 12 things I learned over this semester. I wrote this article over the course of last months, and they are not written on any particular order, other than whatever occurred to me first.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Avoid imaginary "non sense" program examples.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;my_var&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;my_custom_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People who never learned programming before don't yet have the same mental model as a seasoned programmer. They are already struggling with the abstractions. If you use nonsense programs that don't mean anything, it's even harder to understand fundamental concepts. They are distracting. What is &lt;code&gt;my_var&lt;/code&gt;? Is it a function from python? And what about &lt;code&gt;my_custom_function&lt;/code&gt;? Do I need this function to create a custom function?&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reduce abstraction of examples
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;favourite_singers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Justin Bieber&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Lana Del Rey&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People respond better to examples based on real concepts like food, cities, or famous people. Everyone will understand that &lt;code&gt;favourite_singers&lt;/code&gt; is a list of my favourite singers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Avoid special syntax that was not taught yet
&lt;/h2&gt;

&lt;p&gt;When teaching introductory programming to newbies, it's important to not overwhelm them with new syntax. Students get confused and frustrated if you keep telling them "just ignore this for now; I will talk about it later". Always try to build on what they learned before.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Careful with the special meaning of words
&lt;/h2&gt;

&lt;p&gt;Programing is full of jargon. Many of the metaphors' meanings we use today have been lost to time. Take for instance the concept of "return" from a function. What does it mean to return? Return? Return what? From where? Where to? And to Whom?? Back in CS classes we learned about the runtime stack and how programs pass and retrieve data to a from other routines. But these people never heard about this before.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Mind people's backgrounds
&lt;/h2&gt;

&lt;p&gt;If you are teaching to the general public, specially if you are teaching adults, don't assume they know physics, or that they remember their Math classes from 20 years ago. They might not remember what the value of the number Pi is, let alone what prime numbers are, or how to calculate areas or polygons.&lt;/p&gt;

&lt;p&gt;If you want to use these concepts in your examples, introduce  them clearly first.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use the minimum viable code in examples
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;this line still runs! :)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I mentioned before, students will invariably get hang up on unexpected bits of your code. For example, if you are teaching loops, avoid mixing if/else blocks, and vice versa. In the example above, do we need &lt;code&gt;print('this line still runs! :)')&lt;/code&gt; for the function to return from the if? It might sound silly, but this was basically a confusing point in one class!&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Your time is a scarce and valuable resource - use it wisely
&lt;/h2&gt;

&lt;p&gt;In my case, I'm a volunteer teacher. I teach (or assist teaching) 2 hours per week, but am able to give a little bit more if a student needs some 1:1 help.&lt;/p&gt;

&lt;p&gt;Spending some time explaining something to a student can be invaluable to their learning. But if you have 20 students, there's no way you can support them all. &lt;/p&gt;

&lt;p&gt;It's hard, but giving students tools to learn by themselves is the only fair way. There will always be one or two students that will try to "monopolize" most of your time. It can become a bad habit, and they need to learn to solve problems by themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Space for questions during class is good, but keep it strict
&lt;/h2&gt;

&lt;p&gt;I tried to open the stage for questions about the last lesson, and about the homework. I believed that it would be useful to everyone and it would foster discussion.&lt;/p&gt;

&lt;p&gt;Instead, I got sucked in to reviewing some piece of homework that would only benefit a single student. Another time, the "open stage" led to questions about topics from future classes. This was a time sink and not fair for the other 20 students in the call.&lt;/p&gt;

&lt;p&gt;Allowing for questions is great, but you should make it clear about what the questions should be. If the current topic is last week's homework, I'm not answering questions about yesterday's class right now. But maybe we can talk shortly after class.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. The students that ask the most questions, are probably the most advanced
&lt;/h2&gt;

&lt;p&gt;The students that are totally overwhelmed will mostly likely stay quiet the whole time. The ones that make lots of questions, even if they sound totally lost, are probably the ones putting the most effort into understanding the topics and practicing.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. It's really hard work
&lt;/h2&gt;

&lt;p&gt;I never realized how much work it would be to prepare a 2-hour-long class. I've grown a lot of respect for teachers during this semester. I was only required to prepare a class every couple weeks, but it was quite laborious.&lt;/p&gt;

&lt;p&gt;I always wanted to make more engaging classes and avoid boring bullet points. However, even a simple pop quiz can take a couple hours to prepare and test.&lt;/p&gt;

&lt;p&gt;I also wanted to have the next class slides ready ASAP for students who like to read them beforehand. However that turned out to be quite hard to plan - I have a life and a full-time job who always got in the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Be ready to be disappointed
&lt;/h2&gt;

&lt;p&gt;There will always be students who have a hard time keeping up with the course and the reasons for that can vary widely - from lack of time, to lack of interest. It's really demotivating and it made me feel like a bad teacher. Could I have done better early on to not let them fall behind?&lt;/p&gt;

&lt;p&gt;Also, classes will never quite go the way you expect them. As I mentioned, students will get hung up on unexpected things and you need to be prepared to adapt the lesson on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Be ready to be amazed
&lt;/h2&gt;

&lt;p&gt;Despite the struggles, people are amazingly creative. My students had to work on a group project for the last 3 weeks of the semester and I was worried if they would be able to do so. In the days before they started the project they seemed to confused and lost!&lt;/p&gt;

&lt;p&gt;That's why I was seriously amazed by their drive, creativity and coordination, and how they were able to use the little knowledge of python we provided them and build very interesting prototypes. The final presentations were also very good and entertaining - so many great speakers in the class!&lt;/p&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;I've always liked teaching, so this experience was very rewarding for me. This semester was completely remote and over Zoom, so not exactly ideal. On the other hand it allowed us to use fun tools like pop quizzes and polls.&lt;/p&gt;

&lt;p&gt;If you want to give it a try, ReDI is usually looking for more teachers. I only committed to teach once every few weeks because I was one of 7 teachers. ReDI is currently present in Berlin, Munich, Copenhagen and NRW.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.redi-school.org" rel="noopener noreferrer"&gt;https://www.redi-school.org&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>teaching</category>
      <category>python</category>
      <category>volunteering</category>
    </item>
    <item>
      <title>Clicking Stuff in E2E tests - smooth scrolling, Electron flags, and Cypress</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Fri, 19 Mar 2021 15:12:26 +0000</pubDate>
      <link>https://dev.to/lopis/clicking-stuff-in-e2e-tests-smooth-scrolling-electron-flags-and-cypress-2a1c</link>
      <guid>https://dev.to/lopis/clicking-stuff-in-e2e-tests-smooth-scrolling-electron-flags-and-cypress-2a1c</guid>
      <description>&lt;p&gt;&lt;a href="https://www.cypress.io/" rel="noopener noreferrer"&gt;Cypress&lt;/a&gt; is an incredibly popular end-to-end test tool. It's very versatile and typically easy to setup and use. Writing tests in javascript is reasonably intuitive, following a syntax reminiscent of JQuery.&lt;/p&gt;

&lt;p&gt;There are several such tools. Selenium is probably the oldest around, released in 2004. The way they work is they run a browser and simulate user input on it. This sounds fairly simple, but as anyone that worked with Cypress, Selenium, or any other e2e runner will tell you, it's evidently anything but simple.&lt;/p&gt;

&lt;p&gt;In my (admittedly limited) experience, these programs have always been sort of big and complex, with quirky behaviour, as they are but a dev-friendly frontend to the chaos of the browser APIs. Invariably, &lt;code&gt;wait&lt;/code&gt; statements begin to plague the spec, waiting for the DOM dust to settle before going for the next click.&lt;/p&gt;

&lt;p&gt;My latest battle with Cypress at &lt;a href="https://ecosia.org" rel="noopener noreferrer"&gt;Ecosia&lt;/a&gt; included testing our simple snippet carousels:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fgkim4gly378jp7b2wmd0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fgkim4gly378jp7b2wmd0.png" alt="screenshot of the snippet carousels" width="650" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I set out to implement a rather simple test scenario:&lt;/p&gt;

&lt;blockquote&gt;
 - The mock data has 7 items (the UI shows 3); &lt;br&gt;
 - The last item is not visible initially; &lt;br&gt;
 - If I click "next", the first item should no longer be visible; &lt;br&gt;
 - If I then click "next" 3 more times, the last item should come into view; &lt;br&gt;
 - If I then click "previous", the last item should no longer be visible; &lt;br&gt;
 - If I then click "previous" 3 more times, the first item should come into view; &lt;br&gt;
&lt;/blockquote&gt;

&lt;p&gt;For starters, I wrote a simpler version of the test scenario, which simply clicks "next" 4 times and checks if the first item is no longer visible, and the last one is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.snippet-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;have.length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.carousel-nav-button-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.news-snippet-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.news-snippet-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this test, fully confident in my abilities, and the test failed. When I loaded up Cypress' GUI, I noticed that the click events were firing but nothing was happening.&lt;/p&gt;

&lt;p&gt;Then it occurred to me that maybe our smooth scrolling was at fault? We use &lt;code&gt;scrollIntoView&lt;/code&gt; in javascript with the option &lt;code&gt;behavior: smooth&lt;/code&gt; in this carousel. Cypress is supposed to wait for the element to be clickable before firing another click, but I was starting to see that the behaviour of this framework was less than deterministic.&lt;/p&gt;

&lt;p&gt;Disabling the smooth scrolling, the clicks events seemed to fire correctly. But how could I disable smooth scrolling just for Cypress?&lt;/p&gt;

&lt;h2&gt;
  
  
  Disabling smooth scrolling just for Cypress
&lt;/h2&gt;

&lt;p&gt;It turned out it's quite easy to detect Cypress. There is a runtime global &lt;code&gt;window.Cypress&lt;/code&gt; that one can check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrollOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would work, but it's really not ideal. We should not have our application code contain code related to our e2e test Framework. My next idea was to use some sort of browser flag that would disable smooth scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's no such thing as a browser flag to disable smooth scrolling
&lt;/h2&gt;

&lt;p&gt;There is an accessibility feature present in any modern browser called &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion" rel="noopener noreferrer"&gt;"reduced motion preference"&lt;/a&gt;. This preference will affect several animations in the browser chrome. You can (and should!) also use it to reduce the amount of animations in your applications, or tone them down. It doesn't, however, disable smooth scrolling on its own.&lt;/p&gt;

&lt;p&gt;You can detect that this feature is enabled via CSS or Javascript using media queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefersReducedMotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-reduced-motion: reduce)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrollOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prefersReducedMotion&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Furthermore, Firefox and Chrome can both be launched in "prefers reduced motion" mode by passing a flag. Cypress allows you to pass these flags using their &lt;a href="https://docs.cypress.io/api/plugins/browser-launch-api.html" rel="noopener noreferrer"&gt;browser launch API&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;before:browser:launch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REDUCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firefox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.prefersReducedMotion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;REDUCE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--force-prefers-reduced-motion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested this in Cypress' GUI and confirmed that smooth scrolling was effectively disabled. My trust in my abilities was restored. I could see the light at the end of the tunnel!&lt;/p&gt;

&lt;h2&gt;
  
  
  Electron doesn't support that
&lt;/h2&gt;

&lt;p&gt;It turns out Cypress doesn't use Chrome nor Firefox by default. The included browser, and the one we use in our CI, is Electron. "But Electron is just Chrome", I hear you say. That is only partially true. Electron is a wrapper, and not all features and APIs are exposed the same way as in Chrome.&lt;/p&gt;

&lt;p&gt;According to Cypress' &lt;a href="https://docs.cypress.io/api/plugins/browser-launch-api.html" rel="noopener noreferrer"&gt;browser launch API docs&lt;/a&gt;, the "prefers reduced flag" is not part of the list of flags and preferences I can pass to Electron.&lt;/p&gt;

&lt;p&gt;From reading &lt;a href="https://github.com/cypress-io/cypress/issues/1519" rel="noopener noreferrer"&gt;some helpful github discussions&lt;/a&gt;, I finally found that some extra flags can be passed to Electron using "app switches". Those switches are described &lt;a href="https://docs.cypress.io/api/plugins/browser-launch-api.html#Modify-Electron-app-switches" rel="noopener noreferrer"&gt;further down in the docs&lt;/a&gt;. So I tried with the flag I wanted, by passing an environment variable to cypress in my &lt;code&gt;package.json&lt;/code&gt; script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:e2e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ELECTRON_EXTRA_LAUNCH_ARGS=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;--force-prefers-reduced-motion&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; cypress run --project ./e2e-tests"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this worked! Not as elegant as I would have hoped, but it did the trick. If there's a way to enable this switch in code, instead of using env vars, please let me know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the test without smooth scrolling
&lt;/h2&gt;

&lt;p&gt;Implementing my test should be &lt;strong&gt;smooth sailing&lt;/strong&gt; henceforth. Without smooth scrolling, the clicks were registered correctly in Cypress' GUI.&lt;/p&gt;

&lt;p&gt;I ran this test in the headless browser and it worked. Hurrah. Oh wait, there was an extra &lt;code&gt;click()&lt;/code&gt; there by mistake. Silly me. I dropped the extra &lt;code&gt;click()&lt;/code&gt;, feeling still sure of my mental capabilities. But, as you are surely aware due to the fact that you're still only 70% through this article, the story didn't end here. The test failed. &lt;/p&gt;

&lt;h2&gt;
  
  
  A friendly frontend to chaotic browser APIs
&lt;/h2&gt;

&lt;p&gt;All devs, at some point, have moments where they doubt everything they know. So I spun the app up locally and clicked repetitively on the "next" button while counting the number of clicks on my fingers. Then I counted the fingers and there were 4 fingers. So I confirmed I had not lost my mind yet.&lt;/p&gt;

&lt;p&gt;I tried adding a &lt;code&gt;.wait(500)&lt;/code&gt; before the click, but that didn't help. So I headed to the Internet.&lt;/p&gt;

&lt;p&gt;I found a &lt;a href="https://stackoverflow.com/questions/51254946/cypress-does-not-always-executes-click-on-element" rel="noopener noreferrer"&gt;stack overflow thread&lt;/a&gt; where people made some odd suggestions. One was to add &lt;code&gt;.trigger('mouseover')&lt;/code&gt; before each click (?!). Another was to to replace the failing &lt;code&gt;.click()&lt;/code&gt; with &lt;code&gt;.click().click()&lt;/code&gt; (been there, done that). But the top answer suggested using &lt;code&gt;.click({ force: true })&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using force worked. Until I returned the next day and it didn't work anymore. I can't tell you why it was working, nor why it stopped, but it did, then it didn't. I'm glad it didn't because the solution was hacky and simply didn't sit right with me. Specially since it was ill-defined behaviour that would surely come bite me in the back in the future.&lt;/p&gt;

&lt;p&gt;I was seriously tempted to just use &lt;code&gt;.click().click()&lt;/code&gt; and leave it at that. Would I be able to live with it? Sure. Would I be able to sleep at night? Probably. But it's just wrong and I still had some sense left in me.&lt;/p&gt;

&lt;p&gt;At this point I asked my 2 colleagues if they could spare ""a  m i n u t e"" because Cypress was acting up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cypress will be Cypress?
&lt;/h2&gt;

&lt;p&gt;It's easy to blame Cypress for being a horrible tool brought upon us by Beelzebub himself. But as I mentioned before, Cypress provides a friendly interface to the very chaotic browser environment. Brushing aside any dreams of moving the team to The Next Great Thing™️, we started figuring out what was wrong and how we could tackle the issue.&lt;/p&gt;

&lt;p&gt;We considered that the click event might not be installed by the time the first click happens. However, &lt;code&gt;wait()&lt;/code&gt; would have solved this, and it doesn't explain why a second click works. But it does seem like the &lt;code&gt;.click()&lt;/code&gt; that always missed was sort of "waking up" the component.&lt;/p&gt;

&lt;p&gt;Further tests showed that this also happened when clicking on the previous button, even after clicking the next button.  I wish I had an explanation for this behaviour, but that's unfortunately not the case. I do, however, have a working solution for the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working solution for the problem
&lt;/h2&gt;

&lt;p&gt;We developed a solution that tries to ensure that the element is ready to be clicked, and call the next click once ready again. It sounds overkill, it looks overkill, but this was the only way we found that was bullet proof. It's also quite elegant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clickOnControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;times&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;clickOnControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;times&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final e2e test looks simple and elegant as it should:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`snippet-card`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;have.length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-previous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;clickOnControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-previous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;clickOnControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-previous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;clickOnControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-previous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-previous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;clickOnControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-previous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-previous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;byTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result-snippet-control-next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;I remember when I first learned about Cypress in a frontend meetup some years ago. It really was sold to me as an amazing tool that was super easy to use. I have great respect for the creators and maintainers of Cypress, and it seems like they are very active and helpful on github too. But the amount of headaches we get, and the brittleness of our e2e tests,makes us start seriously considering The Next Great Thing™️.&lt;/p&gt;

</description>
      <category>cypress</category>
      <category>e2e</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Writing Vue Storybook stories in markdown</title>
      <dc:creator>João L.</dc:creator>
      <pubDate>Mon, 15 Feb 2021 08:51:44 +0000</pubDate>
      <link>https://dev.to/lopis/writing-vue-storybook-stories-in-markdown-10gd</link>
      <guid>https://dev.to/lopis/writing-vue-storybook-stories-in-markdown-10gd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Important note: this article was written for Storybook 6. Things have changed a little in Storybook 7. If you're interested in an update, let me know!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At &lt;a href="https://www.ecosia.org/" rel="noopener noreferrer"&gt;Ecosia&lt;/a&gt; we started last year investing some resources into defining our Design System (DS). Building a DS allows us to focus on streamlining the design and implementation of our UIs, and to be more aware of the impact of our product design decisions. It helps our team move in unison when developing new products or refactoring old ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving from Vue Styleguidist?
&lt;/h3&gt;

&lt;p&gt;Most of the frontend stack at &lt;a href="https://www.ecosia.org/?refer=devto" rel="noopener noreferrer"&gt;Ecosia&lt;/a&gt; is built around with Vue. We also had a design style-guide built using &lt;a href="https://vue-styleguidist.github.io" rel="noopener noreferrer"&gt;Vue Styleguidist&lt;/a&gt;. Our style-guide is essentially a list of all the Vue components used across our frontend applications.&lt;/p&gt;

&lt;p&gt;Vue Styleguidist is pretty straight forward and flexible. In our current setup, we can write component stories in Markdown with code examples right inside the Vue single-file component. Component props are picked up automatically. That means that even without docs each component gets a docs page.&lt;/p&gt;

&lt;p&gt;While this worked great for developers, we found it a bit too bare bones. For the past year there were a series of features we desired that would mean too much custom work to implement them. We also found some limitations in the markdown docs, for instance regarding the use of the store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Storybook
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://storybook.js.org" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;. has been around for a long time. It started as "React Storybook" but has grown immensely, and now supports several frameworks. (Fun fact: like Storybook, Vue Styleguidist is also built using React). Storybook users can take advantage of a very active community and rich library of addons.&lt;/p&gt;

&lt;p&gt;Out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy theming using a theme API without the need for CSS;&lt;/li&gt;
&lt;li&gt;2 base themes: light and dark;&lt;/li&gt;
&lt;li&gt;Allows complex and custom organization of the pages, including nested stories and sections;&lt;/li&gt;
&lt;li&gt;Easy creation of plain text docs besides code documentation;&lt;/li&gt;
&lt;li&gt;Test/visualize each component individually, or all together in a pretty docs page;&lt;/li&gt;
&lt;li&gt;Zoom feature for individual stories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With storybook-maintained addons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ally features (e.g. including audits (&lt;a href="https://github.com/storybookjs/storybook/tree/master/addons/a11y" rel="noopener noreferrer"&gt;https://github.com/storybookjs/storybook/tree/master/addons/a11y&lt;/a&gt;), and color blindness simulation)&lt;/li&gt;
&lt;li&gt;Responsive design simulation (we can set our list of device dimensions)&lt;/li&gt;
&lt;li&gt;Event and behaviour manipulation&lt;/li&gt;
&lt;li&gt;Event tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With community addons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dark mode switcher&lt;/li&gt;
&lt;li&gt;Easier themes&lt;/li&gt;
&lt;li&gt;Easier documentation&lt;/li&gt;
&lt;li&gt;Generation of docs from code&lt;/li&gt;
&lt;li&gt;...???&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Writing stories - Why not CSF/MDX?
&lt;/h2&gt;

&lt;p&gt;CSF is the recommended way to write component stories in Storybook. However, it's hard to create examples for components with state, e.g. radio buttons. MDX, which is the format recommended for the &lt;code&gt;docs&lt;/code&gt; addon, has the same issue. And both of them require that I write my stories as a string (due to Vue not being a 2st class citizen in Storybook) which is less than ideal to say the least. Here's an example of a story from the MDX documentation of &lt;code&gt;addon-docs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Story&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'basic'&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'400px'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InfoButton&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;info-button label="I&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;m a button!"/&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Story&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://dev.to/josephuspaye/using-storybook-with-vue-single-file-components-2od"&gt;@josephuspaye came up with the brilliant idea&lt;/a&gt; of creating a simple Webpack loader to load Vue files into a CSF story. This approach has a series of advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each individual story is pure Vue.js instead of a string&lt;/li&gt;
&lt;li&gt;State of the story is handled just like in any vue component&lt;/li&gt;
&lt;li&gt;Syntax for styles or scripts is the same as other components and completely isolated from storybook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I identified the following shortcomings in the solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The source of the vue file is not picked up by addon-docs or addon-source;&lt;/li&gt;
&lt;li&gt;Stories are written in CSF which is much less elegant than MDX, which is markdown containing JSX.&lt;/li&gt;
&lt;li&gt;CSF doesn't let you write text between each example, so the documentation with CSF would be all code examples with no change to textual docs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Markdown All the way
&lt;/h2&gt;

&lt;p&gt;I wanted the documentation of my stories to be as lean as possible. The end result looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Meta&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Components/Button"&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ButtonComponent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Buttons&lt;/span&gt;

&lt;span class="nx"&gt;This&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="nx"&gt;It&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="nx"&gt;be&lt;/span&gt; &lt;span class="nx"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt;
&lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="nx"&gt;individually&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Preview&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Story&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'HorizontalGroup'&lt;/span&gt; &lt;span class="na"&gt;inline&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HorizontalGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;story&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HorizontalGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Story&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;Preview&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;story&lt;/code&gt; function is based on &lt;a href="https://dev.to/josephuspaye/using-storybook-with-vue-single-file-components-2od"&gt;@josephuspaye's implementation&lt;/a&gt; with some changes.&lt;/p&gt;

&lt;p&gt;Storybook provides the &lt;code&gt;addon-source&lt;/code&gt; which display the source code of each individual story. As convenient as it is, it won't work with our setup because &lt;code&gt;addon-source&lt;/code&gt; works automagically by loading the source of each story file. Because the source of our stories is found in the vue files, we must load them instead and display them in a custom source panel.&lt;/p&gt;

&lt;p&gt;First we need to indicate which files we want to load with this new "source loader". The way I did it was to add a tab &lt;code&gt;&amp;lt;include-source /&amp;gt;&lt;/code&gt; at the end of the each story to which I want the source code to be present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is a single story for a Button Group&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solid-primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;Primary&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outline-primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;Secondary&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ButtonExample&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;include&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we create the actual loader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceMap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Read the referenced file and remove the &amp;lt;include-source/&amp;gt; block, so it doesn't&lt;/span&gt;
  &lt;span class="c1"&gt;// show up in the source code that will be shown in the UI&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;include-source.*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate a function that'll receive the Vue component and attach the source&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`export default function (Component) {
            Component.options.source = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileContent&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;
        }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;sourceMap&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we tell webpack to use this loader when loading the &lt;code&gt;include-source&lt;/code&gt; block type. You could use another test here, such as filtering the &lt;code&gt;story.vue&lt;/code&gt; extension, but I found the &lt;code&gt;include-source&lt;/code&gt; approach gives me more control and is not really cumbersome to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;

&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resourceQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/blockType=include-source/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-loader.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we need to tell storybook to use the value added to &lt;code&gt;Component.options.source&lt;/code&gt; by the loader. There are two places where we want to be able to read the source code of the story: the &lt;code&gt;addon-docs&lt;/code&gt; code panel, and the individual source panel.&lt;/p&gt;

&lt;p&gt;Recalling the MDX code above, you can see I have two functions &lt;code&gt;story&lt;/code&gt; and &lt;code&gt;params&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Preview&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Story&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'HorizontalGroup'&lt;/span&gt; &lt;span class="na"&gt;inline&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HorizontalGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;story&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HorizontalGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Story&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Preview&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;story&lt;/code&gt; function simply wraps the story component in a function. If we were using the CSF format, this would be the place to set any additional parameters - namely the source code of the story.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * This is a convenience function that wraps the story in a function.
 * It can be used to set aditional parameters in CSF stories.
 * For MDX stories, params much be set in the params() function.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;StoryComponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storyExport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;StoryComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;storyExport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The params function creates a parameters object to be applied to the story component in MDX and it's where the content of the source tab can be set. This is necessary, otherwise &lt;code&gt;addon-docs&lt;/code&gt; just displays &lt;code&gt;story(HorizontalGroup)&lt;/code&gt; as the source code of the story. You could also set this directly in the MDX, but I found this approach allowed for a cleaner MDX syntax.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;StoryComponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storyParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;inlineStories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StoryComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;storyParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ideally, I would love to be able to simplify the markdown even more like the following, and hide all the boilerplate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Preview&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MyStoryComponent&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'HorizontalGroup'&lt;/span&gt; &lt;span class="na"&gt;story&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;HorizontalGroup&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Preview&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, the way that &lt;code&gt;addon-docs&lt;/code&gt; works, this code is nore &lt;em&gt;really&lt;/em&gt; actual JSX, but is instead partially parsed by the MDX loader, which internally expects a certain code structure. Any attempts at removing the boilerplate resulted in storybook crashing or rendering empty stories.&lt;/p&gt;

&lt;p&gt;This is also the reason why the official source code panel addon &lt;code&gt;addon-source&lt;/code&gt; doesn't work with our approach. The internals of that addon expect us to follow a righteous path, but we have rebelled against the oppressing docs. For that reason, we need to create our own source panel. The following addon is adapted from the one used in &lt;a href="https://dev.to/josephuspaye/using-storybook-with-vue-single-file-components-2od"&gt;@josephuspaye's solution&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// source-addon.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/addons&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useParameter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AddonPanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SyntaxHighlighter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ADDON_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vueStorySource&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARAM_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;docs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PANEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ADDON_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/panel`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The SourcePanel component (React)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SourcePanel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use the params from addon-docs&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;docsParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PARAM_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;docsParams&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;docsParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;docsParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;SyntaxHighlighter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;showLineNumbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;copyable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;padded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Register the addon&lt;/span&gt;
&lt;span class="nx"&gt;addons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ADDON_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;AddonPanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SourcePanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;addons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PANEL_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PANEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paramKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PARAM_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// manager.js&lt;/span&gt;

&lt;span class="c1"&gt;// Register our custom addon&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./util/source-addon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;It's unfortunate that Vue is still a second class citizen in Storybook, but it's still worth exploring all the possibilities provided by this platform. Storybook community and maintainers are very active on github which really helps keeping solutions flowing.&lt;/p&gt;

&lt;p&gt;If you want to explore the code I created for this article, head off to &lt;a href="https://github.com/lopis/vue-storybook-example" rel="noopener noreferrer"&gt;my github repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>storybook</category>
      <category>mdx</category>
    </item>
  </channel>
</rss>
