This post is the beginning of a series of technical writeups of things I learned creating the game Symbology. Some things I demonstrate here may not be good ideas for general app development. Symbology is a game, so I consider it more art than infrastructure. That is to say, read this content as things that can be done, and not necessarily as things that should be done.
But while initially developing a Symbology prototype almost seven years ago, I was simultaneously trying to re-learn the charting library D3. That caused me to structure the code such that every time the player clicked, I updated a data structure representing the game state, passed that to D3's .data() function, and then added and removed elements in an SVG (via the DOM) to match in my handlers.
This has always been the suggestion for how to structure a webpage, but it hasn't always been a great idea for web applications. It requires quite a bit of interaction with the DOM, and that has been relatively slow (or at least easy to accidentally make slow), historically. I think I have two things working in my favor, though. Number one, none of my manipulations cause page reflows (a major source of performance trouble). Number two, browser software and the hardware it's running on has advanced far enough that this doesn't seem slow anymore.
Another thing that has made this possible is advancement and wide support of HTML5 and CSS3 features, like data attributes on DOM elements. Classes are amazingly powerful for many things, and the lastest classList access to them offers much easier manipulation. But being able to set essentially arbitrary additional key/data values on an element in game logic, and then clue on them for rendering in CSS is amazingly powerful.
How about some real code examples before getting any more abstract? Let's start with this last one - data attributes. Symbology allows you to undo your most recent move, in case you accidentally tapped or saw a better move right after tapping. The button to do it is coded like this in SVG:
Don't worry too much about the encoding. Just notice that this is setting and unsetting data attributes on the #undo path element. Specifically, data-location is set when there is a move that can be reverted, and it is not set when it's not possible to undo a move. CSS is where the magic happens:
If there is a data-location attribute on the undo element, it is rendered an opaque white, ready to be tapped. If there is no data-location attribute, the undo element is rendered nearly-transparent, hiding itself away. In addition, when the attribute is added or removed, the button smoothly transitions between the two opacities in 150ms.
The trash is a slightly more complex case. There are two visuals going on here that represent the same state. I want to both show a "water line" summary of how full the trash is, and also show the most recent pieces added to the trash. The second bit isn't strictly necessary, but I think it makes things more visually interesting, while also helping to reinforce what the rules about the trash filling and emptying are. So, the SVG code for the trash looks like this:
The code does three things. First it replaces the current "fill" class with the next larger "fill" class. This could have been done with a data attribute, but I wrote this code before learning about them, and haven't gone back to redo it. For now, it's an example to show contrast. Which fill class to use is computed by the number of child elements of the discarded list element, #trashPieces. Adding the newly-discarded piece to that list element is the second thing the function does. Finally, the function returns a boolean value indicating whether all the pieces fit in the trash (true) or that the trash has just overflowed, ending the game in a loss (false). Let's look at how the CSS treats this information in parts.
Here are the rules for the "water line" elements. They are all transparent by default. But, when the "fillN" class climbs above zero, the water line element matching that fill level becomes half-opaque. The transition attribute makes that an even cross-fade between the two levels. Additional rules that I've omitted here for brevity key on the fill0-4 classes to apply color to the outline, pieces, and fill for each level (white, yellow, orange, red).
Meanwhile, the pieces that have been discarded into the trash are placed in the can in a sort of messy stack atop each other. The placement, visually, in the can depends on the placement, structurally, in the element, using another CSS3 feature - the :nth-child pseudo-class selector.
When a player places a piece on a game board tile while having a non-zero trash level, the level lowers one notch. A neat aspect of using position in the element to determine visual position is that it allows the element to be used like a queue. Newly discarded pieces are added to the end. The element that is removed during a tile placement is the element at the start. When that piece is removed, each remaining piece moves forward one position in the element, and therefore gets a different CSS rule applied to it, giving it a different visual position. Because of the "transition" directive, its change in visual position occurs smoothly across 500ms.
And this is why I'm so excited about all of my rendering code being in a declarative style. I didn't tell those pieces to move. The rules applied to them changed, and the browser moved them to match for me. This is what computers are meant to do! Give them rules to apply, and they apply them. Don't even tell them how to do the application - let other software figure out the best way to do that.
Before I sign off on this post, I want to mention one more thing. All of the code quoted here is taken directly from the Symbology source. I haven't put that source in an open repository yet, but I'm also not minifying or otherwise obscuring any of it on the live site. So if you want to see it in situ, load up https://symbology.app/ and open your web inspector. I'll warn you that it's barely documented, but it's also hand-written (as opposed to being generated), so there's at least some human context there to help guide you.
But now I will end this post before I dig into any more examples that might force me to explain topics that I'd rather cover in more depth in another post. Thanks for reading!
Post Copyright © 2022 Bryan Fink