Out of Road

Out of Road
Out of Road

I don’t remember where I got the inspiration for this one. I guess I’ve been thinking a lot about climate change recently, what with the great crypto/NFT debates earlier in the year and recent extreme weather events and wildfires. It seems particularly timely given the recent IPCC report.

In terms of technique, I used a 3D render as a base, which is something I’ve done before, but this time I used somebody else’s CC licensed model because cars are kinda complex.

Ford Mustang Mach 1” by BaldGuyMartin is licensed under Creative Commons Attribution.


Out of Road

Wrong Turn… into Wokeness

Beyond woke, yet the unintended result is the victims are punished for being woke. Skip it. This is not a Wrong Turn movie.

David J, May 02, 2021 - 1.5/5 stars on rottentomatoes.com

Content warning: homophobia, misogyny, racism, spoilers for the movie Wrong Turn (2021)

It seemed pretty clear to me after watching Wrong Turn that it had a conservative message. I haven’t seen the previous movies in the franchise, but I understand they are based on some unfair stereotypes about Appalachian people, so it seems a fair enough twist even if it doesn’t resonate with me personally. When I looked at the audience reviews on Rotten Tomatoes, however, I discovered that a good many people who seemed like they would be on-board with that perspective instead understood it to be “woke” propaganda.

The Message

In Wrong Turn (2021), a young, white, American woman, Jen, throws off the shackles of a guaranteed prominent position in her father’s construction business to hike the Appalachian trail with her boyfriend and their friends. Despite warnings from the locals, they stray off the well-worn path, and fall victim to a primitivist cult known as The Foundation.

It’s clear from the start that Jen is the character that the audience are supposed to relate to. She’s torn between the path her father has laid out for her and the ideals of her friends, and is never shown to have taken those ideals to heart personally. She is more down-to-earth and capable than the other characters - she is the one that has to change the tyre when they get a flat en-route, for example, and later she is the only one able to think on her feet in high-pressure situations.

A woman changing a tyre is obviously peak woke
A woman changing a tyre is obviously peak woke

Her boyfriend, Darius, is an idealistic, politically active black man who expresses socialist and environmentalist ideas. Their friend group are well educated young urbanites, and include a gay couple - pretty much every review of the movie describes them as “diverse”. They’re incredibly cringey to be honest - when an aggressive local accuses them of never having done a day’s work in their lives they actually start bragging about their educational achievements and white-collar jobs - all except our hero, Jen, who is just a little lost in life.

Wokevengers assemble!
Wokevengers assemble!

Not long after setting out they divert from the trail to find a civil-war fort that one of them is interested in. They quickly become lost, and fall victim to various traps before finally encountering, and murdering, a member of the Foundation. Shortly thereafter they are captured and taken to the Foundation’s camp.

Finally we learn what the villains of the piece are all about - not inbred hill people as expected, but an egalitarian, primitivist, socialist cult whose leader has an impeccable hipster coiffure. The surviving friends are put on trial, and their every defense is twisted back on them - they are the intruders! They rushed to judgement based on appearances! They murdered someone in cold blood! They need to respect the Foundation’s culture! Such hypocrites!

King of the hipsters

Far from being inbred, this cult thrives on the recruitment of wayward travelers who they either brainwash into accepting their ideology, or blind with a hot poker and leave to fumble in a dark cave. Really subtle stuff.

Jen and Darius are the only survivors of the trial, and only because Jen convinces the group’s leader, Venable, that they could be useful members of the community. Jen, considering herself to have no relevant skills apparently, is only able to offer herself, as Venable’s wife.

As I mentioned earlier, Jen is actually the only member of the friend group that is portrayed as having any degree of competence or skill relevant to life in the real world. The rest of them are useless, out-of-touch, and varying degrees of obnoxious. But in the woke socialist utopia of the Foundation, she is only valued for her body.

As a result of her relationship with Venable, Jen ends up pregnant. Of course she never considers a termination.

It’s true that “woke” people are amongst the victims in this movie, but more importantly it is “wokeness” that is the monster, leading good All-American girls like Jen off the conservative middle-class path and into a life of bondage, exploitation and sin. She’s the character we’re supposed to find relatable - the rest of them deserve their fate because of their embrace of “woke ideology”, and their deaths are likely intended to be entertaining for that reason. They are so in tune with the villains that Darius chooses to stay with them instead of taking the opportunity to escape.

Let me have another go at summarising what I think this movie is trying to express.

In Wrong Turn (2021), a young, white, American woman is led astray by her “woke” boyfriend and her “woke” friends. While seeking to dig up civil war history that is best left buried, they encounter the logical end-point of “woke” ideology made manifest, and it abuses them horrifically. Jen escapes thanks to the savvy and skills instilled in her by her conservative upbringing, the refusal of her father to abandon his search for her, and the kindness of the misunderstood locals. She returns to her middle-class path through life by working in the family business, and violently rejects further attempts to lead her back to the horrors of “wokeness”.

In short, this is a conservative movie espousing conservative ideals. I disagree with David J, quoted above - the characters are intentionally punished for being “woke”.

Hollyweird is dying a slow death”

Let’s take a look at some of the audience reviews on Rotten Tomatoes from people who seem to have missed the point.

Its just more hand fisted political bs with pretty crappy character development.

Gage S, Apr 14, 2021 - 1.5/5 stars

I guess you could interpret “hand fisted political bs” to be referring to the conservative political messaging. I choose not to.

Woke” America is destroying our culture Absolutely irredeemable

James A, Mar 14, 2021 - 0.5/5 stars

It might seem like James gets it, if not for the 0.5 stars.

I would have given this a four, if it wasn’t for the “over the top” libtardation seen in the movie that Hollyweird so much loves these days. The mixed race couple, the gay couple, the Arab guy, the Asian guy, Black guy wearing “Black Owned” T-Shirt, The racist Sheriff, White-Guilt guy gets mad, calling Confederate monument “Racism” like a White-Knight. Pretty embarrassing stuff. Original from 2003 was better, this wasn’t bad. Hollyweird is dying a slow death, stuff like this in movies is asinine…

Davis H, Mar 06, 2021 - 3/5 stars

It’s like this guy stopped watching a third of the way through. Also a pretty explicit example of how the mere existence of characters who are not straight or white is unacceptable political content to some people.

Another woke joke bad movie

michael b, Mar 07, 2021 - 0.5/5 stars

Joke’s on you, buddy.

This next one’s pretty gross and misogynistic, and you won’t miss much if you choose to skip it.

Revolves around a queer braindead friend group, which just so happens to have every single race in it. Due to the fact that the whore with the least amount of brain cells becomes a rambo bitch and survives till the end makes me not able to give this more than 4 stars. Giving this 4 stars because you get to see a dumbass friend group suffer. Also in the foundations court room they never mentioned how the foundation drew first blood with the gay dude getting a big log to the face for the last time.

the b, Jun 10, 2021 - 2/5 stars

I had to highlight this one because they understood at least part of what the movie was about - watching “woke” straw-people being punished. I can’t give this review more than 4 stars however because of their views on Jen - I guess because she has sex she must be a “dumb whore”. 2/5 stars.

Final Observation

What strikes me about these reviews is that they reveal how the movie reinforces a conservative (or more broadly right-wing) worldview regardless of whether the viewer actually understands the messaging. Either the messaging is understood and received as intended, or the mere presence of POC and gay characters reinforces a perception of a liberal Hollywood elite pushing a “woke” agenda.

Gemini Launch!

In the olden-times, before the Web became basically synonymous with the Internet itself in many people’s minds, there was another, competing hypertext protocol: Gopher.

I say “was”, but of course Gopher never really went away - it was kept alive by enthusiasts, and in recent years there has been a resurgence of interest in it as a sort of haven from the ubiquitous surveillance and relentless commercialisation of the Web.

I’ve long been interested in Gopher (I even made a game about it), and have intended to start a phlog for a while without ever going ahead with it. Something about it always just seemed a little bit awkward and off-putting. I was torn between using Gophermap (i.e. menu) files for everything, or using plain text for posts and sacrificing any hypertextuality. I was torn between finding the need to wrap text to be cool and retro, or a hassle that results in an inferior experience for both creating and consuming content.

Gemini is a new protocol which takes inspiration from both Gopher and the Web, and from a certain perspective, improves on both.

When I heard about Gemini I didn’t really get it at first. I thought it was just Gopher with SSL, which is nice, but I figured I’d get set up on Gopher first and then consider a Gemini mirror. A few days ago I saw a screenshot of the Lagrange browser on Mastodon and started to look into it a bit more. When I realised just how many issues of both Gopher and the web it addresses, I was hooked! I spent several days after that setting up a capsule (the Gemini equivalent of a “site”).

My Gemlog in Lagrange
My Gemlog in Lagrange

Static Generation

After experimenting with a Gemini server for a bit and creating a few static text/gemini files, I decided that I wanted to statically generate my gemlog the same way that I do my blog. I expected to have to write something from scratch to do this, but after some experimentation I was able to get the Pelican static site generator (which I also use for my blog) to both read and output .gmi files. It does take a bit of configuration however, and I had to monkeypatch a couple of methods in Pelican.

Unfortunately this means that it is only guaranteed to work with the current version of Pelican, 4.6.0, and could break at any time. Nonetheless, the plugin is available on GitHub if you want to try it out.

Gemini Reader

The first thing required was a custom “Reader” that can handle .gmi files instead of the usual Markdown or reStructuredText files. It’s simple enough - it just parses the file up to the first blank line as metadata, and the rest of the content is returned unmodified, since we are also going to output the same format.

class GeminiReader(BaseReader):
    enabled = True

    file_extensions = ['gmi', 'gemini']

    def read(self, filename):
        metadata = {}
        content = ""
        with open(filename, mode='r') as f:
            end_of_meta = False
            while not end_of_meta:
                current = f.readline()
                if current == '\n' or current == '':
                    end_of_meta = True
                current = current.strip()
                split = current.split(': ')
                metadata[split[0].lower()] = split[1]
            # After the first blank line, the rest is content.
            content = f.read()

        parsed = {}
        for key, value in metadata.items():
            parsed[key] = self.process_metadata(key, value)

        return content, parsed

Handling Internal Links

Pelican has a mechanism for linking to content internal to the site where you start the URL as {static} or {filename} and it replaces those with the appropriate paths during generation. However, this didn’t work with the Gemini link syntax - the replacement is based on a regular expression that assumes the placeholder will be found in an attribute of a HTML element.

I couldn’t find any setting or hook in the plugin system to alter this regular expression. There is a setting to customise the part that specifies the braces, so you could change the placeholders to ¿¿static?? or something if you like, as long as it is still found in HTML. It seemed like my only option was to replace the method where the problem regex pattern is defined, and use something that matches Gemini links instead.

def _get_intrasite_link_regex(self):
    intrasite_link_regex = self.settings['INTRASITE_LINK_REGEX']
    regex = r"(?P<markup>=> )(?P<quote>)(?P<path>{}(?P<value>[\S]*))".format(intrasite_link_regex)
    return re.compile(regex)

You’ll notice this also has to include a “quote” group because that was present in the HTML version and was expected elsewhere - here it will always be an empty string.

Unfortunately, the problems didn’t end there. I found that the placeholders were removed, but not replaced with the absolute URL of the capsule. This turned out to be because urllib is used to join the URL components, and it doesn’t recognise the gemini protocol. To get around this I had to replace another method, and make a call to a wrapper around urllib.urljoin.

def _urljoin(base, url, *args, **kwargs):
    is_gemini = base.startswith('gemini://')
    if is_gemini:
        base = base.replace('gemini://', 'https://')
    result = urljoin(base, url, *args, **kwargs)
    if is_gemini:
        result = result.replace('https://', 'gemini://')
    return result

Gemini Output

Pelican uses Jinja2 for its templating, which is happy to work with any type of text file, so creating .gmi templates wasn’t an issue. Handily, there is a setting to look for templates with extensions other than .html.

THEME = 'themes/hypergem'
TEMPLATE_EXTENSIONS = ['.gmi', '.gemini']

To get Pelican to output files with a .gmi extension instead of .html, there are a bunch of settings for the different parts of the site. A single “extension” setting like for the templates would be nice, but whatchagonnado? I took the opportunity to customise the article location and file names as well.

# These settings are required to output files as .gmi instead of .html
ARTICLE_URL = 'articles/{date:%Y}-{date:%m}-{date:%d}-{slug}.gmi'

DRAFT_URL = 'drafts/{slug}.gmi'

PAGE_URL = 'pages/{slug}.gmi'

DRAFT_PAGE_URL = 'drafts/pages/{slug}.gmi'

AUTHOR_URL = 'author/{slug}.gmi'

CATEGORY_URL = 'category/{slug}.gmi'

TAG_URL = 'tag/{slug}.gmi'

ARCHIVES_SAVE_AS = 'archives.gmi'
AUTHORS_SAVE_AS = 'authors.gmi'
CATEGORIES_SAVE_AS = 'categories.gmi'
TAGS_SAVE_AS = 'tags.gmi'


I haven’t got much to say about this. I wanted the article links to be a bit more descriptive than just the date and title, so I did something similar to what medusae.space does and included the article summary, the category, and the tags.

It’s close to general purpose but not quite - I added a custom SITELOGO setting that is used on the index page with an ASCII art version of my logo generated using ascii-generator.site, and there is also a custom template for the custom landing page. The index is renamed using a setting, and another page is renamed to index.gmi to take its place. This is so if I want to add content that isn’t generated by Pelican, I have the scope to do so.

INDEX_SAVE_AS = 'gemlog.gmi'
Title: Hyperlink Your Heart
Date: 2021-06-23 22:59
Slug: index
Authors: Kevin Houlihan
Summary: Capsule index
URL: index.gmi
save_as: index.gmi
Template: capsule_intro
Status: hidden


I’m serving the capsule using Jetforce from a first generation Raspberry Pi which I had lying around and haven’t done anything with in a while. There was nothing really involved in setting it up beyond what is described in the documentation, except that I installed it in a virtualenv.

I also took steps to make sure it is running as a dedicated user with no permissions to anything else on the system.

Real professional operation
Real professional operation


I’m not sure what’s next, but I’m excited! I might discuss with the Pelican crew if there are any ways around the issues I encountered that I might have overlooked, or if it could be adapted to be more suited to non-HTML output. If not, maybe a Gemini fork is in order. I have no idea if there are further issues with it beyond the functionality that I’ve used.

I have quite a few posts to port over from my blog yet, and I need to get some image optimisation happening there like I have here. Besides that, I guess all I have to do is get to know the community!

Visit My Capsule (and Beyond)

If you’re already familiar with Gemini please check out my capsule.

If you’re not, well, I still encourage you to visit, but I should probably give you some guidance on getting started.

If you just want to dip your toes you can browse the Gemini network using a HTTP proxy (here’s one, and another). For what I would consider the “full experience” you will need a dedicated browser. I’ve been using Lagrange, and highly recommend it, but there are a whole bunch of others if that doesn’t suit you. Many of them also support Gopher, which makes browsing both into a seamless experience outside of the modern Web.

When you want to move beyond my capsule, here are some others I recommend:

I leave you with my anxious young poppy, Jennifer, on AstroBotany:

.  , _ . ., l, _ ., _ .  
^      '        `    '

name  : "Jennifer"
stage : anxious young poppy
age   : 2 days
rate  : 1st generation (x1.0)
score : 326788
water : |██████████| 100%
bonus : |          | 2%


I’ve been plagued by temptation lately to buy a Pinebook Pro. My current laptop is really a desktop replacement, a beast that can hardly last an hour untethered from a power socket. It’s usually not worth the hassle of extracting it from its tangled nest of cables when I want to compute elsewhere, and that’s fine - it’s the workhorse. But as a result, the idea of a light, efficient laptop is alluring, especially when it’s one that runs Linux.

Out with the New

However, I don’t really like to buy new devices without good reason. I already have another laptop that meets the criteria of being relatively light and portable - the MacBook Pro that served as my main work machine between 2015 and 2019 when my wife and I were floating around Ireland, France and Spain and living out of the back of our car. I have been using it as a more portable option already on occasion, but it has a few annoying problems:

  • The battery isn’t in great shape, so while it’s a lot better than my main laptop, it’s nothing like that expected of the Pinebook Pro.
  • The OS is outdated. It’s demanding constantly that I update to a newer version of MacOS, but I don’t want to. Apparently it could run the latest version, but I don’t trust Apple to preserve the usability of old devices.
  • It runs MacOS. MacOS is fine - it’s a Unix, it’s not Windows… but it still has a lot of little annoyances, it’s proprietary, and to be honest, I’m bored with it.

Basically it’s lost its shine, and isn’t fun to use anymore.

Happier times house-sitting in Belfast
Happier times house-sitting in Belfast

A Cunning Plan

One of Linux’s oft-heralded killer use-cases is in giving old hardware new life. I’ve never really used it for that explicit purpose - whenever I use Linux, it’s just because I prefer to use Linux, even if it happens to be on old or low-powered machines. This one isn’t exactly an ancient artifact, but I thought maybe installing a Linux distro with a lightweight desktop environment would help stretch the battery life and make it feel a bit snappier, more like a new machine.

The two distros I considered were ElementaryOS and Xubuntu. I’m not sure how lightweight Elementary’s DE is, but I liked the look of it, so I decided to try it out with an eye to maybe using it as my main OS some day.

First impressions were great - it only used 700MB of RAM after booting (compared to nearly 4GB for MacOS!), and the degree of visual flair and polish were incredibly impressive. Unfortunately a couple of things put me off - when I cut the CPU frequency to 800MHz I began to experience occasional lag, and at one point the shell crashed with no way to recover it!

I didn’t have any experiences like that with Xubuntu. I ran it for a whole day from a USB stick, installed a bunch of software, worked on a blog post, and had no issues - so that was that decision made! It’s definitely not as visually impressive as Elementary, but I’d rather a responsive system than a pretty one in this case.


Installation was pretty smooth, especially compared to installing Linux on PowerPC Macs back in the day. The only snag was with the proprietary wireless driver. This was easily enough installed using the “Additional Drivers” settings dialog when running from the USB stick, but after installation the required driver was no longer available. Having no other means to connect to a network, this was a serious problem!

Solving this involved a couple of steps. First I enabled the “CDROM” source in the Software & Updates settings, under the “Other Software” tab. This caused the driver to become available in the Additional Drivers dialog, but it wouldn’t install. The problem was that the live USB stick was mounted somewhere under /media/kevin, but apt expected it to be mounted at /media/cdrom, which didn’t even exist. Unmounting the USB stick and running the following commands sorted it out, and allowed me to connect to the WiFi to install and upgrade other packages.

sudo mkdir /media/cdrom
sudo mount /dev/sdb1 /media/cdrom
sudo apt install bcmwl-kernel-source


Unfortunately the results were not quite what I’d hoped. I performed a test where I played a movie and music on a loop under both OSes, and Xubuntu was down to 10% battery in 1 hour 46 minutes, while Mac OS took 2 hours and 28 minutes to reach the same level. This was with all cores throttled to 800MHz under Xubuntu, and MacOS doing whatever it does naturally to save energy, but full screen and keyboard backlight brightness on both.

While Xubuntu runs great, and is much more pleasant to use for me, it doesn’t seem to achieve the same battery life under similar loads. I’m torn now between the user experience I prefer under Xubuntu and the superior energy efficiency of MacOS… Or perhaps buying a Pinebook after all!

Cheers on a game-jam well done
Cheers on a game-jam well done

Recent Art & Portfolio

Once again I have become neglectful of updating this blog with my artwork, so let’s do a roundup of the last uh… 7 months?!?! and maybe I’ll try to get it back on track from here on. Though I do have a nice portfolio site now that I have been keeping up to date, so if you really like my art you could be following that as well. More on that below.

Socialist Revolutionaries Past & Future

Last October I was trying to get back into the development of my game Just a Robot, and started as I always do by completely re-imagining its entire look. In this case I was inspired by the look of Soviet propaganda and Anarchist woodcuts.

Robot Propaganda

I followed that up in December with this tribute to the luxurious moustache of Irish revolutionary socialist James Connolly.

James Connolly
The Irish people will only be free, when they own everything from the plough to the stars


For the New Year I committed myself to working on more space and sci-fi themed stuff. I started with this depiction of a space station roughly based on the ISS.

Space station
Space Station

Also in January, a spaceship approaching Mercury, with some tricky perspective.

Mercury approach
Getting warm

In February I did another piece loosely inspired by Soviet propaganda, and a theme that seemed to be somewhat controversial - the idea of billionaires fleeing into space and leaving the rest of us to our fates. Some people took that as a literal prediction of future events, but I think of it in more allegorical terms - capital is ruining the natural and social environment without any sense of responsibility to the rest of us, while its masters can escape the consequences of their actions without literally leaving the planet - depicting them doing so is just a good way of describing the situation, in my opinion.

Flight of the Billionaires
Pretty rockets though...

In March I brought it back to Earth and explored the same theme from another perspective, more or less.

Left Behind
So long...

I started a piece in April for Cosmonautics day, but I didn’t get it finished until June - the capsule and final stage of Vostok 1 in orbit. I did a game jam and an oil painting in the meantime though!

Vostok 1
Vostok 1

And that pretty much brings us up to date!


My portfolio site, which went live in July 2020, is another statically-generated site based on Pelican, but focused on image galleries instead of blog entries (using the standard gallery plugin). I wanted a central place to put my art that wasn’t a social media platform, and that would display it optimally. I’ve linked to it a few times from here despite never actually mentioning it.

It is inspired by the ideas of Matej Jan on displaying art on the internet, and attempts to display my pieces at the best integer scaling to fit in the browser window, on a background with appropriate contrast, and with no distractions.

Of course, I also wanted to keep it as small and responsive as possible, so it does this with about 3kB of javascript, 11kB of CSS, and a selection of minimal SVG backgrounds. Because all the art is pixel art, transferring it at 1x resolution and resizing it in the browser (as discussed in recent posts) keeps things extremely compact, with the entire portfolio currently only amounting to 390kB. The portfolio does a lot better job of displaying the art than this blog does though, here the images are just resized to max width without attempting an integer scaling.

I think it looks real nice and that my art looks real nice on it, so go check it out!

Image Optimisation

In the last instalment of my epic blogging saga I recounted my discovery that the index page of this site had grown to over 1.7MB of content when loaded fresh, largely due to the images. One of my goals for this site was that it be lean - fast to load and energy efficient - and it was not meeting that goal at all. Clearly avoiding Javascript and CSS frameworks was not enough!

I immediately started thinking about how to improve the situation. Though I had previously ruled out the approach used by Low-tech magazine’s solar-powered website because I didn’t think it would fit my aesthetic, I decided to see if dithering coloured, rather than monochrome, PNGs would work better for me, and improve on the size and quality of an appropriately-sized 75% quality JPEG.

PNG Thunderdome

The one to beat - baseline 75% JPEG, Size: 16.1kb


Using Pillow and the same hitherdither library that Low-tech magazine used for their site, I iterated on a script that output hundreds of compressed variations of a given image using different dithering algorithms and parameters.

The best results I found were with the Bayesian algorithm with a 32 colour palette, a 2x2 matrix, and an image size half the expected display size. This produced a result that was relatively readable, and reminiscent of pixel art. The size savings varied by image - sometimes up to 10kB, but often only 2-3kB as for this example. These savings are modest compared to the loss in detail, and I think this approach could only be considered because of the unique aesthetic it produces.

Palette: 32, Dither: bayer, Threshold: 256/8-256-256/8, Order: 2, Size: 13.9kb


The three “threshold” parameters expected by hitherdither were a bit of a mystery to me. Through trial and error I found that some produced much smaller images, but unfortunately not at a level of quality that I found acceptable. Lower palette sizes also resulted in savings, but below 32 colours they started to look too abstract and unreadable to me.

Palette: 32, Dither: bayer, Threshold: 256/2-256-256/2, Order: 2, Size: 9.6kb


A Challenger Appears

I was about ready to commit to this approach and start converting all the images when my wife reminded me that WEBP exists! After converting a few of my test images to WEBP it was clear that it had my dithered PNGs beat - half the size of the JPEG without any loss of quality.

80% quality WEBP, Size: 9.9kb


Apparently support for WEBP is pretty good these days, but there are a couple of annoying outliers - Safari only supports it on Big Sur, and IE11 still exists, as I’m sure it always will.

As such, I decided I should probably try and fallback gracefully to a JPEG or PNG where WEBP isn’t supported. This can be achieved using <picture> and <source> elements to allow the browser to choose the format it likes best.

    <source type="image/webp" srcset="{optimal image url}"/>
    <source type="image/jpeg" srcset="{compatible image url}"/>
    <img src="{compatible image url}"/>

Let’s Automate

The above HTML snippet presents a problem - my posts are not written in HTML but in Markdown, and processed by Pelican into HTML, and that process just results in an <img> tag by default.

I threw together a quick Pelican plugin to post-process the generated HTML and replace any <img> tags with <picture> tags, if the referenced images could be replaced with WEBPs. It also processes the referenced images to create scaled JPEG/PNG versions as well as the WEBP version, so I don’t have to do any of that manually either.


The index of the blog is now 778kB at the time of writing - so reduced by over half! I did also replace a particularly large and troublesome GIF with a static image as well, and converted some PNGs to JPEGs. This results in greater savings because the plugin converts PNGs to lossless WEBPs and JPEGs to lossy ones.

The plugin is actually not even working fully yet - for some reason it is missing some <img> tags on the index pages, leaving them serving their original unprocessed files.

I also haven’t done anything about the pixel art images, which if served at their original resolution could be a significant saving.

So in short, huge progress, but still much scope for improvement!

I am almost sorry I’m not going to end up using these dithered PNGs though…

Tracer and Chun-Li

Update 01/06/2021

I fixed the problem with the plugin and all images are now optimised except the few in this post that are flagged to be skipped. I also swapped out all the pixel art images and gifs for 1x resolution versions. These are just being scaled up by CSS, with reasonable results.

The index page is now 539kB, the whole site is just over 1MB, and it is in the 80th percentile for energy usage according to websitecarbon.com (whatever that’s worth). I think the link above showed 75th or 76th percentile or thereabouts at the time of posting, but it will show 80th now.

Energy Usage Update

When I started this blog, one of my goals was for it to be lean and efficient, both to improve the reader’s experience and to reduce the power usage involved in serving it.

I’ve been planning recently to write a post on a related topic - the energy efficiency of the Python language and how using it as the backend or API of a more dynamic website would fit with those same goals. In preparation for that, I thought to check in on this blog and whether it is still as lean as it was back when I was figuring out how to embed the SVG icons in it.

Unfortunately, it seems that it is not. Unlike one of my inspirations, low-tech magazine’s solar powered website, I never found a solution for reducing the size of image files. For my posts about TV shows and movies I use fairly low quality JPEGs. But for my art and game dev posts I have been unwilling to compromise, and, frankly, a bit lazy, and have just been throwing large PNGs and GIFs into my posts without thinking about their size.

As a result, loading the index of this site now transfers 1.7MB if nothing is already cached. The text content and CSS are in the tens of kilobytes, with the rest being images. This is comparable to the initial load of many corporate news sites and large blogs with all their tracking javascript and ads blocked - an improvement over the norm perhaps, since I’m not doing any of the tracking or advertising that have become such a burden on the web, but not particularly notable. This site is now only in the 57th percentile according to websitecarbon.com.

For pixel art, one solution does present itself immediately. For my portfolio site, all of the artwork is sent to the browser at 1x resolution and resized there by an integer factor by a small amount of javascript, and perhaps the same thing could be achieved with css. The index of that site is under 400kB despite including all of my artwork, and does not require specially resized image thumbnails or anything like that. I could do the same thing here instead of throwing manually resized art into posts.

I’m less sure what to do about screenshots and screencaps. The solution used by low-tech magazine doesn’t really work for me, I think it compromises the readability of the images too much and doesn’t fit this site’s aesthetic. I tried a number of different techniques for vectorising, posterising and dithering screencaps from TV shows and movies to reduce the number of colours, but they didn’t result in significant savings. On the other hand, they did actually look kind of cool and stylised!

Vectorised Beth Harmon
Vectorised Beth Harmon

Something I should probably try is scaling such images down significantly and then back up to the required display size in CSS. Obviously this will degrade the quality, but it might be an acceptable trade-off.

Screenshots of things like a text-editor or IDE are even more problematic - if the quality is degraded there they are probably not useful. I will probably just have to try to keep that kind of thing to a minimum, and use tighter crops where necessary.

It’s easy to become complacent about efficiency when computers are so powerful that a few tens or hundreds of kilobytes don’t seem to cause them any strain, but when you have that mindset they can quickly add up to quite a significant waste of time and power. I think this is well demonstrated by USA Today’s pissy response to the GDPR, where instead of the usual 5.2MB bloated garbage site, readers from the EU were served a lean, content focused site that loaded almost instantly and didn’t track them.

Nothing I do is going to have the kind of impact that a site like USA Today improving their efficiency would have, purely because of the scale of the traffic involved, but I still want to try to do better.

Ludum Dare 48 Results

Ludum Dare 48 results are out a while now, but I forgot to post about them. I did not do as well as last time, but since my results for Gophers were exceptional for me, that was probably to be expected. This time I placed 433rd overall, with the stand-out category being graphics, where I just about broke the top 100, at 99th place.

Category Rating Placing Percentile
Overall 3.81 433 84th
Fun 3.259 1044 61st
Theme 3.672 873 67th
Innovation 3.345 754 72nd
Humor 3.596 277 89th
Graphics 4.466 99 96th
Audio 3.621 526 80th
Mood 3.846 460 83rd


A couple of graphs demonstrating my trends.

Ratings Graph Placings Graph

Still haven’t surpassed that Rattendorf peak, sigh

More Out of Gas

I am committed to working on a post-jam release of this game, as I think with a few tweaks and a bit more content it could be something quite special (more special than the ratings above suggest anyway!)

I will be posting here with development updates, and also over on the itch.io page, where you can also still play the jam version for now.

Out of Gas

Out of Gas

Out of Gas is my entry for the recent Ludum Dare 48 game jam. It was intended to be a blatant FTL rip-off, but in having much simplified clicker-like combat mechanics and an unusual cars-in-space aesthetic, I think it is sufficiently its own thing (I hope anyway!)


My initial thought was more of a straight sci-fi concept of travelling into deep space, facing combat and resource management challenges similar to those which ended up in the game. The first thing that came to mind as a title for this was “Out of Gas”, after the Modest Mouse song of the same name.

Thinking about that song got me thinking in another direction - essentially the same mechanics, but Earth-bound, set in a Modest Mouse world of highways and drifters, fleeing problems and starting over. The problem to flee in this case would obviously be debt - something that you can get deeper and deeper into.

As you can probably tell by now I ended up combining these two ideas! I didn’t want to let either of them go, and I quickly fell in love with the idea of a universe where space combat meant winding down your window and firing a handgun at your enemy.

The combined concept suggested that a new, sci-fi twist on the debt problem was required, and that’s where the idea of a constantly growing cryptocurrency debt came in. I’ve been thinking about cryptocurrencies a lot lately since they’re going through another hype cycle. One commonly touted feature of them (or some, like Bitcoin, at least), is that they’re deflationary, so any debt denominated in them would constantly grow in value instead of decreasing in value like debts denominated in inflationary fiat currencies. This seemed like the perfect notion for the bizarre world of my game. It doesn’t really have any impact on the gameplay, but I think it’s a nice bit of narrative flavour, and it fits the theme perfectly.

Late payment

The final piece of the conceptual puzzle was the characters. I couldn’t very well have two anonymous nobodies riding along with the player on a trip like this! I turned to two characters I had vague notions of making a game about years ago, Haze & Lee, a gun-toting, post-apocalyptic outlaw couple. Unfortunately I don’t think their personalities really come across in the game due to time constraints, but at least they have names!


As usual, I did all the art in Pyxel Edit. I started off very rough with just pink boxes for the ships and a partial circle for the background, and moved immediately onto the gameplay with those placeholders. This is a somewhat unusual approach for me as I usually try to tie down the look of a game early on.

First mockup

The thing about rough mockups is that by the time I came back to the art a bunch of stuff in the game was already tied to the resolution and shape of the outlines. I wasn’t even sure I had done the perspective correctly, but I was stuck with it anyway, and I guess it was close enough because nobody has complained!

First ships

I had planned to do some character portraits as well for the narrative side of the game, and express encounters as dialogues between characters, but unfortunately there wasn’t time for that.

The map screen also didn’t get much love, with the initial placeholder art surviving into the final game.

Map screen


As for my last Ludum Dare entry, Gophers, a cutscene graph editor plugin for Godot that I had previously developed was essential to getting this game done within the time constraints of the jam.

Those darn teenagers!

I had done a lot of work on this plugin in recent months, applying lessons learned from using it for Gophers to improve the workflow, functionality, and stability. This paid huge dividends, and I found almost no issues while churning out encounter cutscenes. The cutscene graphs created by this plugin power every encounter in the game up to and after combat.

My main task on the first day was to get the map screen working - it wouldn’t be much of a game if you couldn’t travel between systems. I implemented it as a graph defined by custom MapSystem and MapConnection control nodes.

Map graph

The new fun thing about this for me was declaring the MapConnection class to be a “tool” script, allowing the connections between systems to be drawn in the editor. This kind of custom tooling is important for the efficient design of systems in larger games, so it was fun to give it a try. Here, it mostly served to prevent me from getting confused about which MapSystem nodes I had already connected to each other and which I hadn’t.

func _draw():
    if usable or used or _can_draw_in_editor():
        # TODO: Decide on proper colours for this
        var color = Color.gray
        if usable:
            color = Color.hotpink
        # No idea why the offset is required
        var source_center = _source_node.rect_global_position + CENTER_OFFSET
        var target_center = _target_node.rect_global_position + CENTER_OFFSET
        var start = source_center + ((target_center - source_center).normalized() * SPACE)
        var finish = target_center + ((source_center - target_center).normalized() * SPACE)

Implementing the combat was a lot tougher. The battle scene is almost entirely UI nodes, which was perhaps a mistake, and for some reason I struggled most of day two to even get player input to register properly. I don’t really remember now what the obstacle was to this - maybe I was just tired.

Battle UI

Sound and Music

There’s nothing special about the sound this time around. Unlike for Gophers I didn’t have time to do any foley work, and I figured I would probably have a hard time creating gunshot and explosion sounds anyway. Instead I fell back to SFXR (specifically, jsfxr).

The music I put together in BeepBox, my favourite game jam tool for chiptune music these days due to its simplicity. I tend not to get as bogged down with it as I do with LMMS. I initially wanted to try to create something in the style of Modest Mouse, given the game’s inspiration, but it was hard to recreate a guitar sound. Instead I ended up with a simple arpeggiated chord progression over a bass drone that I hope feels kind of epic, kind of longing and lonesome, I dunno.

The battle music is a variation of the same tune but just swaps out one of the instruments, doubles up the drone, and compresses and spreads the arpeggios across more octaves for a more urgent and chaotic feeling. The two tracks play in sync throughout the game and just cut between each other as necessary.

Abandoned Ideas

As usual there were many ideas that I had to put aside due to time constraints. I had planned to include a small number of missiles which could be fired during combat with devastating effect, to allow the player to pull themselves back from the brink of defeat. I had also intended to have targetable/damageable systems on the ships, including injury to the characters, so that you could take out the enemy’s weapon to gain a reprieve from being fired upon, or their engines and reduce their chance to dodge. You can see the proposed UI of these mechanics in the initial mockup above. Finally, I had planned a system of weapon upgrades which almost made it in, but not quite.

On the art side, I had wanted to include a number of different backgrounds to improve the sense of travelling between systems.

A big regret that I have is that I wasn’t able to include the character portraits, and express the encounters primarily as dialogue between characters. I think this would have introduced a lot more personality to the narrative side of the game, as opposed to the third-person narration that I had to go with.


I like this game a lot, and I feel it is one that could be polished up and rounded out into a complete experience with relative ease, so I am determined to do a post-jam release with some of the missing features described above, and some of the problems with the jam release resolved (such as the RNG sometimes producing the same encounter multiple times in a row). I have already begun work on this, and I hope to have it completed in the next few months. I’ll do another post about my progress on it so far, but for now here’s a little sneak peek:

Haze speaks!

Coroutine Callbacks

I’m working on a cutscene/dialogue graph editor plugin for the Godot game engine (I mentioned using it for the Ludum Dare in a previous post). When I came to deciding how to actually process the graphs it creates in order to display the dialogue and perform actions in the game, I discovered an interesting use for coroutines that I haven’t seen described elsewhere. I’m not all that used to working with coroutines so maybe what I’ve done is completely normal and everybody does it all the time, or indeed it might be ill-advised for some reason. Either way, I will describe it here and you can ignore me if I’m being an idiot!

Graph editor

Processing a graph that results in activity in-game is a task which frequently needs to be put on hold - waiting for text to be displayed in the UI in a juicy manner, moving characters around, waiting for input etc. This could be achieved in a Node by implementing _process() and maintaining state about whether you’re currently walking a graph or waiting on something, and having methods that other nodes can use to indicate when processing can continue. I have found such methods to be quite messy in the past.

I decided instead to process the graph in a while loop and yield as necessary while other nodes perform tasks initiated by a number of signals. Where tasks are expected to take some time to complete, a coroutine state object is passed to the signal which the listener can use to indicate that processing should continue, or in some cases return values.

Let’s See Some Code

Below is an abridged version of the main method that processes the graphs. Processing a dialogue node yields so that the UI that displays the text can wait for player input to continue. Processing a branch node and others like it just proceed directly to the next iteration of the loop.

func process_cutscene(cutscene):
    _local_store = {}
    _current_graph = cutscene
    _current_node = _current_graph.root_node
    while _current_node != null:
        if _current_node is DialogueTextNode:
            yield(_process_dialogue_node(), "completed")
        elif _current_node is BranchNode:

Here is the code that processes dialogue nodes and raises the signal for their text to be displayed in-game. A coroutine is put in a frozen state to be passed with the signal, and the call to emit the signal is deferred so we can make sure we are waiting for the coroutine to complete before any signal listeners have a chance to resume it.

func _await_response():
    return yield()

func _emit_dialogue_signal(

func _process_dialogue_node():
    var text = _current_node.text

    var process = _await_response()
    yield(process, "completed")

    _current_node = _current_node.next

The code that consumes the signal can take however long it likes to do its thing, and when it’s ready it can just call resume() on the coroutine state and the graph processing will proceed to the next node.

func _on_CutsceneController_dialogue_display_requested(
    _process = process

func _on_ContinueButton_pressed():
    if _process != null:

If a node in the graph needs a response in order to know what to do next (e.g. a choice between multiple dialogue options), we can just accept the return value of the coroutine passed in the signal. The consumer can just pass the value to the resume() call.

func _process_choice_node():
    var process = _await_response()
    var choice = yield(process, "completed")
    _current_node = _current_node.branches[choice]

Potential Pitfalls

There are two pitfalls that come to mind with this approach, but they’re not really any different than what could occur with a _process() based approach like the one I described above.

  1. Processing of another graph could be triggered while one is still in progress. If the previous one is yielding it would end up processing the new graph as well when it resumes. This is quite easy to avoid by just noping out of the method if there is already work in progress.
  2. If there are multiple listeners connected to the signals there could be ambiguity about which one is responsible for resuming. If there are multiple resume calls, all but the first will result in a runtime exception.

In the event that you actually want multiple listeners to report in before resuming, it might be possible to craft a different coroutine that takes the number of listeners and yields until they have all resumed. I think it would look something like this:

func _await_many(count):
  while count > 0:
    count -= 1

func _await_many_responses(count):
  var responses = []
  while count > 0:
    count -= 1
  return responses

I haven’t tested that though.


I don’t really have one, I just thought it was a neat pattern that doesn’t require the listeners to keep a reference to the node doing the processing or have any knowledge of it aside from the signals it emits. You can check out the full code on GitHub as it stands currently. I haven’t tested it in a game yet, but I will probably enter the Ludum Dare later this month and I might get a chance to use it then. I will update afterwards whether it is a disaster or a triumph!