MoreRSS

site iconAlex WlchanModify

I‘m a software developer, writer, and hand crafter from the UK. I’m queer and trans.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of Alex Wlchan

Slipstitch, Queer Craft, and community spaces

2025-07-30 17:26:41

Two weeks ago, I was at Queer Craft – a fortnightly meet-up at Slipstitch, a haberdashery and yarn shop near Alexandra Palace. I was working on a cross stitch piece of a train in a snowy landscape, chatting to my friends, admiring their creations, and gently snacking on a variety of baked goods.

This week, I wasn’t there, because Slipstitch closed its doors on Saturday.

A photo of the shop's exterior. It has bright turquoise paint with the word ‘Slipstitch’ in gold lettering across the top, and large windows that show the shop interior. You can see shelves, balls of wool, and two large wooden knitting needles.
Slipstitch in sunnier times. Photo from the Slipstitch website.

I can’t remember exactly when I first came across Slipstitch, but I remember why. Slipstitch was sometimes the target of stickering campaigns from TERFs and anti-trans campaigners, who objected to Rosie’s vocal support of trans people. (Rosie is the shop’s owner and a dear friend.)

This discussion spilled onto Twitter, and at some point Rosie’s tweets appeared in my timeline. I saw the shop account calling out the stickers and re-affirming its support of trans people, and since I’m trans and I do lots of cross-stitch, I decided to check it out. I looked around the online store, thinking I might buy some thread – then I found an event called “Queer Craft”, and booked.

Turning up the next Monday, I was a bit nervous – would I be queer enough? Would I be crafty enough? For whatever reason, my mental image of a craft meetup is people doing knitting or crochet – does anybody bring cross-stitch to these things?

My nerves were quickly put at ease – Rosie welcomed me enthusiastically, and I settled in. I sat down at the table, put on a name badge, and took out my cross-stitch. As I stitched away, I started chatting to strangers who would soon become friends.

Queer Craft was every other Monday, and I began making regular trips to Muswell Hill for two hours of crafting and conversation. We’d admire each other’s work, and share tips and advice if somebody was struggling. The group was always generous with knowledge, equipment, and sympathy for unexpected snags – but the conversation often drifted away from our crafts.


As we got to know each other more, we developed in-jokes and stories and familiar topics. We’d talk about Taskmaster and the careers of cutlery salesmen. We’d discuss what it’s like to grow up in Cornwall. We’d chat about theatre shows and West End drama. Everyone made jokes about how I’m a spy. (I’m not a spy.) Rosie would tell us about the wrong way to make coffee. We passed around many, many photos of our pets.

I know that Rosie was always keen for Queer Craft to be welcoming to newcomers and not too “clique”-y – I suspect the rate of in-jokes made that difficult, but I admire the effort. (I wonder if that’s the fate of all groups designed to help strangers meet? Either it fizzles out, or a group of regulars gradually forms that makes it harder for new people to join.) I confess I was never too worried about this, because I was too busy having a nice time with my friends.

Craft is such a personal hobby, and we saw glimpses of each other’s lives through the things we were making, especially when they were gifts for somebody else. Somebody making a baby blanket for a friend, or a stuffed toy for a parent, or some socks for a partner. Everyone poured so much time and love into their work. It felt very intimate and special.

I’m always a bit nervous about how visibly trans to be, but that was never an issue at Queer Craft – something I should have known from the start, thanks to Rosie’s vocal support of her trans staff and customers. Everyone treated transness as so self-evidently normal, it didn’t bear comment.

Sometimes I was femme, and sometimes I was masc, and nobody batted an eyelid. (Apart from the time Rosie took one look at my officewear and described it as “corporate drag”, a burn so savage I may never recover.) That sense of casual, unconditional trans acceptance feels like it’s getting more common – but it’s still nice every time.

These friendships have spilled out of Slipstitch and into the world beyond. One Queer Craft regular is a published author, and some of us went to celebrate her at a book event. Several others are in choirs, and I’ve been to see them sing. Last year, I invited people round to my house for “Queer Crafternoon”, where we all crafted in my living room and ate scones with jam and cream.


Nine weeks ago, Rosie told us that she’d decided to close the physical shop. The world is tough for small businesses, and tougher for the person running them. I’m sad, but I could see how much stress it was putting on Rosie, and I respect her decision to put her own wellbeing first, and to close the doors on her own terms.

On Saturday, Rosie held a party to mark the closing of the space. The shelves were empty, the room anything but. There were Queer Craft friends, regulars from the other Meet & Make group, people who’d come to Rosie’s classes, regular customers, and other friends of the shop. The community around Slipstitch is much more than just the queers who met on Monday evenings. The shop was the busiest I’ve ever seen it, and it was lovely to see so many people there to celebrate and mourn.

A rendition of the shopfront in cross-stitch, mounted in a gold frame on a brick wall. The shop is a simple geometric design in turquoise thread, and each of the three windows shows a mini-display – a pair of barbies, a pair of jumpers on stands, six balls of wool in rainbow colours.
My closing gift for Rosie was her shopfront, rendered in cross-stitch. I had a lot of fun designing the little details – the barbies in the shop window, the jumpers on display, the balls of wool in a rainbow pattern. And of course, I bought all the thread from her, but fortunately she never thought to ask why I was buying so much turquoise thread.

I’m sure Queer Craft and our friendships will continue in some shape or form, but sitting here now, I can’t help but be a little upset about what we’ve lost. Of course, there are other haberdasheries, and there are other queer craft groups – it’s hardly a unique idea – but Slipstitch was the haberdashery where I shopped, it’s where our group met, and I’m sad to know it’s gone.

In her final newsletter before the closure, Rosie wrote “[Slipstitch] never wanted for community”. I think that’s a lovely sentiment, and one that rung true in my experience – it always felt like such a friendly, welcoming space, and I’m glad I found it. I hope the friendships forged in Queer Craft will survive a long time after the physical shop is gone. I know that Rosie wants Slipstitch to continue as an idea, if not a physical venue, and I’m excited to see what happens next.

Her words also made me reflect on the fragility of our community spaces – those places where we can meet strangers and bond over a common interest. They’re getting scarcer and scarcer. As every bit of land is forced into more and more commercialisation, we’re running out of places to just hang out and meet people. We often talk about how hard it is to make friends as an adult – and that’s in part because the spaces where we might do so are dwindling.

These community spaces are precious for queer people, yes, but for everyone else too. I’m sad that the shop has closed, and I’m sad that this iteration of Queer Craft is over, and I’m sad that this is a trend. These spaces are rare, and getting rarer – we shouldn’t take them for granted.

[If the formatting of this post looks odd in your feed reader, visit the original article]

Today was my last day at the Flickr Foundation

2025-07-26 04:12:14

Today was my last day at the Flickr Foundation. At 5pm I closed my laptop, left the office for the last time, and took a quiet walk along Regent’s Canal. I saw an adorable family of baby coots, and a teenage coot who was still a bit fluffy and raggedy around the edges.

I’ve got another job lined up, but I’m taking a short break before I start.

My new role is still in software engineering, but in a completely different field. I’m stepping away from the world of libraries, archives, and photography. I’ve met some amazing people, and I’m very proud of everything we accomplished in digital preservation and cultural heritage. I’ll always treasure those memories, but I’m also excited to try something new.

For the last few years, I’ve been among the more senior engineers in my team. In my next role, I’ll be firmly middle of the pack, and I’m looking forward to learning from people who have more wisdom and experience than me.

But first: rest.

[If the formatting of this post looks odd in your feed reader, visit the original article]

Minifying HTML on my Jekyll website

2025-07-25 05:59:10

I minify all the HTML on this website – removing unnecessary whitespace, tidying up attributes, optimising HTML entities, and so on. This makes each page smaller, and theoretically the website should be slightly faster.

I’m not going to pretend this step is justified by the numbers. My pages are already pretty small pre-minification, and it only reduces the average page size by about 4%. In June, minification probably saved less than MiB of bandwidth.

But I do it anyway. I minify HTML because I like tinkering with the website, and I enjoy finding ways to make it that little bit faster or more efficient. I recently changed the way I’m minifying HTML, and I thought this would be a good time to compare the three approaches I’ve used and share a few things I learned about HTML along the way.

I build this website using Jekyll, so I’ve looked for Jekyll or Ruby-based solutions.

Table of contents

Approach #1: Compress HTML in Jekyll, by Anatol Broder

This is a Jekyll layout that compresses HTML. It’s a single HTML file written in pure Liquid (the templating language used by Jekyll).

First you save the HTML file to _layouts/compress.html, then reference it in your highest-level layout. For example, in _layouts/default.html you might write:

---
layout: compress
---

<html>
{{ content }}
</html>

Because it’s a single HTML file, it’s easy to install and doesn’t require any plugins. This is useful if you’re running in an environment where plugins are restricted or disallowed (which I think includes GitHub Pages, although I’m not 100% sure).

The downside is that the single HTML file can be tricky to debug, it only minifies HTML (not CSS or JavaScript), and there’s no easy way to cache the output.

Approach #2: The htmlcompressor gem, by Paolo Chiodi

The htmlcompressor gem is a Ruby port of Google’s Java-based HtmlCompressor. The README describes it as an “alpha version”, but in my usage it was very stable and it has a simple API.

I start by changing my compress.html layout to pass the page content to a compress_html filter:

---
---

{{ content | compress_html }}

This filter is defined as a custom plugin; I save the following code in _plugins/compress_html.rb:

def run_compress_html(html)
  require 'htmlcompressor'

  options = {
    remove_intertag_spaces: true
  }
  compressor = HtmlCompressor::Compressor.new(options)
  compressor.compress(html)
end

module Jekyll
  module CompressHtmlFilter
    def compress_html(html)
      cache = Jekyll::Cache.new('CompressHtml')

      cache.getset(html) do
        run_compress_html(html)
      end
    end
  end
end

Liquid::Template.register_filter(Jekyll::CompressHtmlFilter)

I mostly stick with the default options; the only extra rule I enabled was to remove inter-tag spaces. Consider the following example:

<p>hello world</p> <p>my name is Alex</p>

By default, htmlcompressor will leave the space between the closing </p> and the opening <p> as-is. Enabling remove_intertag_spaces makes it a bit more aggressive, and it removes that space.

I’m using the Jekyll cache to save the results of the compression – most pages don’t change from build-to-build, and it’s faster to cache the results than recompress the HTML each time.

The gem seems abandoned – the last push to GitHub was in 2017.

Approach #3: The minify-html library, by Wilson Lin

This is a Rust-based HTML minifier, with bindings for a variety of languages, including Ruby, Python, and Node. It’s very fast, and even more aggressive than other minifiers.

I use it in a very similar way to htmlcompressor. I call the same compress_html filter in _layouts/compress.html, and then my run_compress_html in _plugins/compress_html.rb is a bit different:

def run_compress_html(html)
  require 'minify_html'

  options = {
    keep_html_and_head_opening_tags: true,
    keep_closing_tags: true,
    minify_css: true,
    minify_js: true
  }

  minify_html(html, options)
end

This is a much more aggressive minifier. For example, it turns out that the <html> and <head> elements are optional in an HTML5 document, so this minifier removes them if it can. I’ve disabled this behaviour, because I’m old-fashioned and I like my pages to have <html> and <head> tags.

This library also allows minifying inline CSS and JavaScript, which is a nice bonus. That has some rough edges though: there’s an open issue with JS minification, and I had to tweak several of my if-else statements to work with the minifier. Activity on the GitHub repository is sporadic, so I don’t know if that will get fixed any time soon.

Minify, but verify

After I minify HTML, but before I publish the site, I run HTML-Proofer to validate my HTML.

I’m not sure this has ever caught an issue introduced by a minifer, but it gives me peace of mind that these tools aren’t mangling my HTML. (It has caught plenty of issues caused by my mistakes!)

Comparing the three approaches

There are two key metrics for HTML minifiers:

  • Speed: this is a dead heat. When I built the site with a warm cache, it takes about 2.5s whatever minifier I’m using. The htmlcompressor gem and minify-html library are much slower if I have a cold cache, but that’s only a few extra seconds and it’s rare for me to build the site that way.

  • File size: the Ruby and Rust-based minifiers achieve slightly better minification, because they’re more aggressive in what they trim. For example, they’re smarter about removing unnecessary spaces and quoting around attribute values.

    Here’s the average page size after minification:

    Approach Average HTML page size
    Without minification 14.9 KiB
    Compress HTML in Jekyll 3.2.0 14.3 KiB
    htmlcompressor 0.4.0 14.0 KiB
    minify-html 0.16.4 13.5 KiB

I’m currently using minify-html. This is partly because it gets slightly smaller page sizes, and partly because it has bindings in other languages. This website is my only major project that uses Ruby, and so I’m always keen to find things I can share in my other non-Ruby projects. If minify-html works for me (and it is so far), I can imagine using it elsewhere.

[If the formatting of this post looks odd in your feed reader, visit the original article]

Moving my Glitch apps to my own web server

2025-07-08 23:51:22

About six weeks ago, Glitch announced that they’re shutting down. Glitch was a platform where you could make websites and web apps, with a heavy emphasis on creativity and sharing. You could read the source code for any project to understand how it worked, and remix somebody else’s project to create your own thing.

Unfortunately, Glitch is shutting down project hosting today. If you had an app on Glitch, it’s about to stop running, but you can set up redirects to another copy of it running elsewhere.

I’ve created redirects for all of my apps, and moved them to the web server that runs this site. This was pretty straightforward, because all of my “apps” were static websites that I can upload to my server, and they get served like the rest of my site:

Not all of my Glitch apps made the jump – I deleted a couple of very early-stage experiments, and I have yet to spin up new copies of my Chinese vocabulary graph or the dominant colours web app. I might port them later, but they’re not static websites so they’re a bit more complicated to move.

Glitch felt like a throwback to the spirit of the early web – the platonic ideal of “view source” and “anyone can make a website”. I always liked the idea of Glitch, and I enjoyed making the fun apps that I hosted there. I’m sad to see it close – another space for playful creativity crushed by the commercial tide of the web.

[If the formatting of this post looks odd in your feed reader, visit the original article]

Recreating the bird animation from Swift.org

2025-06-11 21:52:05

Last week, the Swift.org website got a redesign. I don’t write much Swift at the moment, but I glanced at the new website to see what’s up and OOH COOL BIRD!

When you load the page, there’s a swooping animation as the bird appears:

I was curious how the animation worked. I thought maybe it was an autoplaying video with no controls, but no, it’s much cooler than that! The animation is implemented entirely in code – there are a few image assets, and then the motion uses JavaScript and the HTML5 canvas element.

I’ve never done anything with animation, so I started reading the code to understand how it works. I’m not going to walk through it in detail, but I do want to show you what I learnt.

All the code from the Swift.org website is open source on GitHub, and the JavaScript file that implements this animation was written by three engineers: Federico Bucchi, Jesse Borden, and Nicholas Krambousanos.

Table of contents

What are the key steps in this animation?

Most of the animation is made up of five “swoop” images, which look like strokes of a paintbrush. These were clearly made by an artist in a design app like Photoshop.

These images are gradually revealed, so it looks like somebody actually painting with a brush. This is more complex than a simple horizontal wipe, the sort of animation you might do in PowerPoint. Notice how, for example, the purple swoop doubles back on itself – if you did a simple left-to-right wipe, it would start as two separate swoops before joining into one. It would look very strange!

Each swoop is animated in the same way, so let’s focus on the purple one, just because it’s the most visually interesting.

The animation is applying a mask to the underlying image, and the mask gradually expands to show more and more of the image. The mask matches the general shape of the brush stroke, so as it expands, it reveals more of the image. I wrote about masking with SVG four years ago, and the principle is similar here – but the Swift.org animation uses HTML5 canvas, not SVG.

The best way to explain this is with a quick demo: as you drag the slider back and forth, you can see the mask get longer and shorter, and that’s reflected in the final image.

original image
+
mask
final image
animation progress:

We can break this down into a couple of steps:

  • Only draw part of a curved path (drawing the mask)
  • Combine the partially-drawn path with the original image (applying the mask)
  • Gradually increase the amount of the path that we draw (animating the path)
  • Start the animation when the page loads

Let’s go through each of these in turn.

Only draw part of a curved path with a dash pattern

Alongside the graphical image of a brush stroke, the artist supplied an SVG path for the mask:

M-34 860C-34 860 42 912 102 854C162 796 98 658 50 556C2 454 18 48 142 88C272 130 290 678 432 682C574 686 434 102 794 90C1009 83 1028 280 1028 280

If you’re not familiar with SVG path syntax, I really recommend Mathieu Dutour’s excellent SVG Path Visualizer tool. You give it a path definition, and it gives you a step-by-step explanation of what it’s doing, and you can see where each part of the path appears in the final shape.

Screenshot of the path visualizer, with a breakdown of how the path works and an annotated swoop that matches the purple swoop.

</a>

We can draw this path on an HTML5 canvas like so:

const canvas = document.querySelector('canvas');

const ctx = canvas.getContext('2d');
ctx.lineWidth = 100;
ctx.lineCap = 'round';
ctx.strokeStyle = 'black';

const path = new Path2D(
  "M-34 860C-34 860 42 912 102 854C162 796 98 658 50 556C2 454 18 48 142 88C272 130 290 678 432 682C574 686 434 102 794 90C1009 83 1028 280 1028 280"
);

ctx.stroke(path);

The way Swift.org draws a partial path is a really neat trick: they’re using a line dash pattern with a variable offset. It took me a moment to figure out what their code was doing, but then it all clicked into place.

First they set a line dash pattern using setLineDash(), which specifies alternating lengths of lines and gaps to draw the line. Here’s a quick demo:

ctx.setLineDash([100])

The path starts in the lower left-hand corner, and notice how it always starts with a complete dash, not a gap. You can change this by setting the lineDashOffset property, which causes the patern to start on a gap, or halfway through a dash. Here’s a demo where you can set both variables at once:

ctx.setLineDash([75])
ctx.lineDashOffset = 0;

I find the behaviour of lineDashOffset a bit counter-intuitive: as I increase the offset, it looks like the path is moving backward. I was expecting increasing the offset to increase the start of the first dash, so the line would move in the other direction. I’m sure it makes sense if you have the right mental model, but I’m not sure what it is.

If you play around with these two variables, you might start to see how you can animate the path as if it’s being drawn from the start. Here are the steps:

  1. Set the dash length to the exact length of the path. This means every dash and every gap is the same length as the entire path.

    (The length of the purple swoop path is 2776, a number I got from the Swift.org source code. This must have been calculated with an external tool; I can’t find a way to calculate this length in a canvas.)

  2. Set the dash offset to the exact length of the path. This means the entire path is just a gap, which makes it look like there’s nothing there.

  3. Gradually reduce the dash offset to zero. A dash becomes visible at the beginning of the path, and the closer the offset gets to zero, the more of that dash is visible. Eventually it fills the entire path.

Here’s one more demo, where I’ve set up the line dash pattern, and you can adjust the progress. Notice how the line gradually appears:

const progress = 0.0;
const pathLength = 2776
ctx.setLineDash([pathLength]);
ctx.lineDashOffset = pathLength * (1 - progress);

Now we have a way to draw part of a path, and as we advance the progress, it looks it’s being drawn with a brush. The real code has a couple of extra styles – in particular, it sets a stroke width and a line cap – but it’s the way the animation uses the dash pattern that really stood out to me.

Once we have our path, how do we use it to mask an image?

Mask an image with a globalCompositeOperation

The masking uses a property of HTML5 canvas called globalCompositeOperation. If you’ve already drawn some shapes on a canvas, you can control how new shapes will appear on top of them – for example, which one appears on top, or whether to clip one to fit inside the other.

I’m familiar with the basic idea – I wrote an article about clips and masks in SVG in 2021 that I still look back on fondly – but I find this feature a bit confusing, especially the terminology. Rather than talking about clips or masks, this property is defined using sources (shapes you’re about to draw on the canvas) and destinations (shapes that are already on the canvas). I’m sure that naming makes sense to somebody, but it’s not immediately obvious to me.

First we need to load the bitmap image which will be our “source”. We can create a new img element with document.createElement("img"), then load the image by setting the src attribute:

const img = document.createElement("img");
img.src = url;

In the Swift.org animation, the value of globalCompositeOperation is source-in – the new shape is only drawn where the new shape and the old shape overlap, and the old shape becomes transparent.

Here’s the code:

// The thick black stroke is the "destination"
ctx.stroke(path)

// The "source-in" mode means only the part of the source that is
// inside the destination will be shown, and the destination will
// be transparent.
ctx.globalCompositeOperation = 'source-in'

// The bitmap image is the "source"
ctx.drawImage(img, 0, 0)

and here’s what the result looks like, when the animation is halfway complete:

destination
+
source
final image

There are many different composite operations, including ones that combine colours or blend pixels from both shapes. If you’re interested, you can read the docs on MDN, which includes a demo of all the different blending modes.

This is a bit of code where I can definitely understand what it does when I read it, but I wouldn’t feel confident writing something like this myself. It’s too complex a feature to wrap my head around with a single example, and the other examples I found are too simple and unmotivating. (Many sites use the example of a solid red circle and a solid blue rectangle, which I find completely unhelpful because I can produce the final result in a dozen other ways. What’s the real use case for this property? What can I only do if I use globalCompositeOperation?)

Then again, perhaps I’m not the target audience for this feature. I mostly do simple illustrations, and this is a more powerful graphics operation. I’m glad to know it’s there, even if I’m not sure when I’ll use it.

Now we can draw a partial stroke and use it as a mask, how do we animate it?

Animate the brush stroke with Anime.js

Before I started reading the code in detail, I tried to work out how I might create an animation like this myself.

I haven’t done much animation, so the only thing I could think of was JavaScript’s setTimeout() and setInterval() functions. Using those repeatedly to update a progress value would gradually draw the stroke. I tried it, and that does work! But I can think of some good reasons why it’s not what’s used for the animation on Swift.org.

The timing of setTimeout() and setInterval() isn’t guaranteed – the browser may delay longer than expected if the system is under load or you’re updating too often. That could make the animation jerky or stuttery. Even if the delays fire correctly, it could still look a bit janky – you’re stepping between a series of discrete frames, rather than smoothly animating a shape. If there’s too much of a change between each frame, it would ruin the illusion.

Swift.org is using Julian Garnier’s Anime.js animation library. Under the hood, this library uses web technologies like requestAnimationFrame() and hardware acceleration – stuff I’ve heard of, but never used. I assume these browser features are optimised for doing smooth and efficient animations – for example, they must sync to the screen refresh rate, only drawing frames as necessary, whereas using setInterval() might draw lots of unused frames and waste CPU.

Anime.js has a lot of different options, but the way Swift.org uses it is fairly straightforward.

First it creates an object to track the state of the animation:

const state = { progress: 0 };

Then there’s a function that redraws the swoop based on the current progress. It clears the canvas, then redraws the partial path and the mask:

function updateSwoop() {
  // Clear canvas before next draw
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw the part of the stroke that we want to display
  // at this point in the animation
  ctx.lineDashOffset = swoop.pathLength * (1 - state.progress);
  ctx.stroke(new Path2D(swoop.path));

  // Draw the image, using "source-in" to apply a mask
  ctx.globalCompositeOperation = 'source-in'
  ctx.drawImage(img, 0, 0);

  // Reset to default for our next stroke paint
  ctx.globalCompositeOperation = 'source-out';
}

Finally, it creates a timeline, and adds an animation for each swoop.

When it adds the animation, it passes five things:

  • the state object
  • the desired end state (progress: 1)
  • the duration of the animation (1000ms = 1s)
  • an easing function; in this case in(1.8) means the animation will start slowly and gradually speed up
  • the updateSwoop function as a callback for every time the animation updates
const tl = anime.createTimeline()

tl.add(
  state,
  { progress: 1, duration: 1000, ease: 'in(1.8)', onUpdate: updateSwoop }
);

You may have wondered why the state is an object, and not a single value like const progress = 0. If we passed a numeric value to tl.add(), JavaScript would pass it by value, and any changes wouldn’t be visible to the updateSwoop() function. By wrapping the progress value in an object, JavaScript will pass by reference instead, so changes made inside tl.add() will be visible when updateSwoop() is called.

Now we can animate our swoop, as if it was a brush stroke. There’s one final piece: how do we start the animation?

Start the animation with a MutationObserver

If I want to do something when a page loads, I normally watch for the DOMContentLoaded event, for example:

window.addEventListener("DOMContentLoaded", () => {
  runAnimation();
});

But the Swift.org animation has one more thing to teach me, because it does something different.

In the HTML, it has a <div> that wraps the canvas elements where it draws all the animations:

<div class="animation-container">
    <canvas id="purple-swoop" width="1248" height="1116"></canvas> <canvas id="purple-swoop" width="1248" height="1116"></canvas>
    <canvas id="white-swoop-1" width="1248" height="1116"></canvas>
    <canvas id="orange-swoop-top" width="1248" height="1116"></canvas>
    <canvas id="orange-swoop-bottom" width="1248" height="1116"></canvas>
    <canvas id="white-swoop-2" width="1248" height="1116"></canvas>
    <canvas id="bird" width="1248" height="1116"></canvas>
</div>

Then it uses a MutationObserver to watch the entire page for changes, and start the animation once it finds this wrapper <div>:

// Start animation when container is mounted
const observer = new MutationObserver(() => {
  const animContainer = document.querySelector('.animation-container')
  if (animContainer) {
    observer.disconnect()
    heroAnimation(animContainer)
  }
})

observer.observe(document.documentElement, {
  childList: true,
  subtree: true,
})

It achieves the same effect as watching for DOMContentLoaded, but in a different way.

I don’t think there’s much difference between DOMContentLoaded and MutationObserver in this particular case, but I can see that MutationObserver is more flexible for the general case. You can target a more precise element than “the entire document”, and you can look for changes beyond just the initial load.

I suspect the MutationObserver approach may also be slightly faster – I added a bit of console logging, and if you don’t disconnect the observer, it gets called three times when loading the Swift.org homepage. If the animation container exists on the first call, you can start the animation immediately, rather than waiting for the rest of the DOM to load. I’m not sure if that’s a perceptible difference though, except for very large and complex web pages.

This step completes the animation. When the page loads, we can start an animation that draws the brush stroke as a path. As the animation continues, we draw more and more of that path, and the path is used as a mask for a bitmap image, gradually unveiling the purple swoop.

Skip the animation if you have (prefers-reduced-motion: reduce)

There’s one other aspect of the animation on Swift.org that I want to highlight. At the beginning of the animation sequence, it checks to see if you have the “prefers reduced motion” preference. This is an accessibility setting that allows somebody to minimise non-essential animations.

const isReduceMotionEnabled = window.matchMedia(
  '(prefers-reduced-motion: reduce)',
).matches

Further down, the code checks for this preference, and if it’s set, it skips the animation and just renders the final image.

I’m already familiar with this preference and I use it on a number of websites. sites, but it’s still cool to see.


Closing thoughts

Thanks again to the three people who wrote this animation code: Federico Bucchi, Jesse Borden, and Nicholas Krambousanos. They wrote some very readable JavaScript, so I could understand how it worked. The ability to “view source” and see how a page works is an amazing feature of the web, and finding the commit history as open source is the cherry on the cake.

I really enjoyed writing this post, and getting to understand how this animation works. I don’t know that I could create something similar – in particular, I don’t have the graphics skills to create the bitmap images of brush strokes – but I’d feel a lot more confident trying than I would before. I’ve learned a lot from reading this code, and I hope you’ve learned something as well.

[If the formatting of this post looks odd in your feed reader, visit the original article]

My favourite websites from my bookmark collection

2025-06-02 16:18:27

Over the last three weeks, I’ve been writing about how I manage my bookmarks. how I use a static site to store them, how I built a personal web archive by hand, and what I learnt about web development along the way.

I wanted to end this series on a lighter note, so here’s a handful of my favorite sites I rediscovered while reviewing my bookmarks – fun, creative corners of the web that make me smile.

This article is the final part of a four part bookmarking mini-series:

  1. Creating a static site for all my bookmarks – why I bookmark, why I use a static site, and how it works.
  2. Building a personal archive of the web, the slow way – how I built a web archive by hand, the tradeoffs between manual and automated archiving, and what I learnt about preserving the web.
  3. What I learnt about making websites by reading two thousand web pages – how to write thoughtful HTML, new-to-me features of CSS, and some quirks and relics I found while building my personal web archive.
  4. My favourite websites from my bookmark collection (this article) – websites that change randomly, that mirror the real world, or even follow the moon and the sun, plus my all-time favourite website design.

The ever-changing “planets” of kottke.org

Jason Kottke’s website, kottke.org, has a sidebar that shows four coloured circles, different for every visitor. There are nearly a trillion possible combinations, which means everyone gets their own unique version of the page.

A web page with black text on a white background, and down the left hand side are four circles showing different textures in a variety of pink/red shades.
I happened to capture a particularly aesthetically pleasing collection of reds and pinks in this preserved snapshot. They add a pop of colour to the page, but they don’t overwhelm it.

I think this adds a dash of fun whimsy, and I’ve tried adding something similar to my own sites, but it’s easy to get wrong. My experiments in randomness often failed because they lacked constraints – for example, I’d pick random tint colours, but some combinations were unreadable. The Kottke “planets” strike a nice balance: the randomness stands out, but it’s reined in so the overall page will always look good.

There are thousands of snapshots of kottke.org in the Wayback Machine, many saved automatically. That means there are unique combinations of circles already archived that have yet to be seen by a person – frozen moments that may only be seen by a future reader, long after this design has gone. I rather like that: a tiny, quiet, time capsule on the web.

Physical meets digital on panic.org

The software company Panic has a circular logo: a stylised “P” on a two-tone blue background. But for years, if you visited their website, you might see that logo in a different colour, like this:

A screenshot of the Panic blog, with a dark blue P logo in the top left-hand corner.A screenshot of the Panic blog, with a red/green P logo in the top left-hand corner.

Where did those colours come from? The logo image gets loaded from signserver.panic.com, which makes me think it reflected the current colours of the physical sign on their building. They even had a website where anybody could change the colours of the sign (though it’s offline now – they took the sign down when they moved offices).

I love this detail: a tiny bit of the physical world seeping into the digital.

A Tumblr theme that follows the sun

Another instance of the physical world affecting the digital comes from one of my bookmarked Tumblr posts, which has a remarkable theme Circadium 2.0, made by Tumblr user Laighlin. Forget a binary switch between light and dark mode, this is a theme that slowly changes the appearance through the entire day.

The background changes colour, stars fade in and out, and the moon and the sun gradually rise and set. It cycles through noon, twilight, and dusk, before starting the same thing over again. It’s hard to describe it in words, so here’s a screen recording of the demo site for a 24 hour cycle:

This effect is very subtle, because the appearance is set based on the time you loaded the page, and doesn’t change after that. Unless you reload the same page repeatedly, you may even not notice the background is changing.

This is the sort of creativity I love about sites like Tumblr and LiveJournal, where users can really customise the appearance of their sites – not just pick a profile picture and a tint colour.

Subtle transitions at Campo Santo

The Campo Santo blog has a more restrained design, but still makes fun use of shifting colours – the tint colour of the page gradually switches from a reddish orange to brown, to green, to a dark yellow, and back to orange. This tint colour affects multiple elements on the page: the header, the sidebar promo, headings and social media links.

Here’s what it looks like:

I so enjoyed Firewatch, and I’m still a little bitter that In the Valley of Gods got cancelled. I would have loved another first-person exploration game from that team.

Sadly, this animation only lives on in web archives and in memory – something has broken in the JavaScript that means it no longer works on the live site. The fragility of the web isn’t just entire pages or sites going offline, it’s also the gradual breaking of pages that remain online.

The hand-drawn aesthetic of Owltastic

My favourite website is the old design of Meagan Fisher Couldwell’s website. It has a beautiful, hand-drawn aesthetic, and it’s full of subtle texture and round corners – no straight lines, no hard edges. It has a soft and gentle appearance, and a friendly owl mascot to boot.

I bookmarked this particular page in 2013, before iOS 7 when loud textures and skeuomorphism were still in fashion – but unlike many designs from that era which now look dated, I think this site still looks good today.

Screenshot of a website with a light turquoise background, with various similar shades of teal, with a light brown as an accent colour. In the top left is a hand-drawn illustration of an owl, which is smiling and gesturing towards an article titled ‘Writign and publishing are important’.
I just know that owl and I would be friends.

Owltastic is the first site I remember seeing and thinking “wow”, and wanting to build something that looked that good. Meagan has since redesigned her site, but I have a lot of nostalgia for that hand-drawn look.


Final thoughts

A lot of the creativity has been squeezed out of the web. I’ve been working on a separate social media archiving project recently, and it’s depressing how many sites look essentially the same – black sans serif text on a white background. (Many of them even use the default system font, because heaven forbid a site have any personality.)

Going through my bookmarks has been a fun reminder that the web is still a creative, colourful, and diverse space – the variety is there, even if it’s getting harder to find. Lots of people are doing interesting stuff on the web, and my bookmarks are a way to remember and celebrate that.

Revamping the way I organise my bookmarks has taken a lot of work, but I’m so pleased with the result. Now I have a list of my most important web pages in an open format, saved locally, with an archived copy of each page as well. I can browse them in a simple web interface, and see every page as I remember it, even if the original website has disappeared.

I don’t like making predictions, but this feels like a system that should last a long time. There are no third-party dependencies, nothing that will need upgrading, no external service that could be abandoned or enshittified. I feel like I could manage my bookmarks this way for the rest of my life – stay tuned to see if that holds true!

Writing this four-part series has been the capstone to this year-long project. I had a lot of time to think about bookmarks and web archiving, and I didn’t want those thoughts to disappear. I hope you’ve enjoyed it, and that I’ve given you some new ideas. Thank you for reading this far.

My favourite parts of the web are the spaces where people share interesting ideas. This mini-series – and this entire blog – is my contribution to that collective work.

[If the formatting of this post looks odd in your feed reader, visit the original article]