avanier's banner

pullstring

Krita animation process for the theme switcher pullstring
this could’ve been a button

I’ve added a long-overdue theme switcher to this site, but instead of a basic button, I decided to add a bit of flair to it (partly because my energy for Drawtober evaporated in recent days so I need some small sense of accomplishment and partly because I couldn’t sleep).

Functionally, it’s your run-of-the-mill theme switcher: light and dark themes in CSS, something to cycle through themes (or toggle in this case, since it’s just two), and storing the setting locally. For the animation, I made 24 frames and set the speed to 24 fps.

The image is initially set to a static frame. When clicked on, the image source is switched over to the GIF and it’s allowed to run the sequence once before it’s switched back. It reaches its lowest point in the 12th frame or at the 0.5 second mark, so that’s when the theme is toggled. isPlaying prevents excessive clicking, which messes with the playing sequence and is also visually annoying.

Here’s the script:

const stringStatic = new Image();
stringStatic.src = "m/switch.png";
const stringGif = new Image();
stringGif.src = "m/switch.gif";

const pullstring = document.createElement("img");
pullstring.src = stringStatic.src;
pullstring.id = "string";
document.body.appendChild(pullstring);

const setTheme = localStorage.getItem("theme");
if (setTheme) document.body.classList.add(setTheme);

let isPlaying = false;

function switchSrc() {
  setTimeout(() => {
    pullstring.src = stringStatic.src;
    pullstring.removeEventListener("load", switchSrc);
    isPlaying = false;
  }, 1000);
}

pullstring.addEventListener("click", () => {
  if (isPlaying) return;
  isPlaying = true;

  pullstring.src = stringGif.src;
  pullstring.removeEventListener("load", switchSrc);
  pullstring.addEventListener("load", switchSrc);

  setTimeout(() => {
    document.body.classList.toggle("dark");
    localStorage.setItem("theme", document.body.classList.value)
  }, 500);
});

One last thing: because the GIF is not fetched until the pullstring is clicked, I’m preloading it in head. I changed it to fetch from the script instead. No point in preloading an asset that won’t be used where JS is blocked.

It’s fun to make itty bitty things like this. The animation isn’t perfect and I’ll probably refine it a bit more at some point, but for now it works.

It’s time for bed. Goodnight!

OPERANT CONDITIONING

Note: the GIF size could be reduced and I think there’s some caching issue. I’ll look into those tomorrow. 🫠


— josh