When my kids were young, we did lots of science and engineering things with them to entertain them or just to alleviate boredom. We visited many science museums. We did numerous kits (e.g., Tinker Crate a.k.a KiwiCo), as well as followed published activities like the Marshmallow Challenge or recipes like
homemade ginger ale (which was just okay).
But most of our favorite projects were things we made up.
Make a “bagpipe” out of a recorder, a plastic bag, and a straw. A bagpipe can play a constant note for longer than a human can exhale: the player blows into a bag, then squeezes the bag to force the air through an instrument. You can rig something together that will do the same thing.
Try to figure out how best to keep an ice cube from melting. Do this as little experiments to figure out what helps. E.g., place a control ice cube in a cup and another on a wire mesh suspended over the cup. Which lasts longer? Why? Also: put two ice cubes in cups, then have your kid cover one with their warmest winter jacket. Which will melt first?
Use a protractor and some math to try to measure the height of a tall tree in the neighbor’s yard, then figure out whether it’s close enough to your house to hit it.
Make a wood swing. Our swing still sits in our front yard and is used daily by passing kids.
Create a scale model of the Earth/Moon system. A globe of the Earth has an enormous amount of play and educational value. Somehow we also ended up with a globe of the Moon. (You could also just print pictures out on a paper.) We worked out how far apart they should be at scale, then physically put them that distance apart. It’s further than you think. In our case the straight-line distance just barely fit inside our first floor.
“Potion Lab”: mix household substances together and see what you get. (Our kids generally pursued two lines of research: fragrances and insecticides.) If the substances include baking soda and vinegar, eventually something surprising will happen.
Construct a maze for a hamster.
Create an elevator for stuffed animals.
Create a zipline for stuffed animals.
Weigh all the stuffed animals on a kitchen scale to determine who is the lightest and who is the heaviest.
Gather all the Legos in the house and try to build a freestanding tower that can reach the ceiling. This is a good use for Duplos. Our house has a light well to the second floor, and we were just barely able to make a tower on the first floor that reached up to the level of the second.
Body conductivity. One electronics kit included a tiny speaker alarm powered by AA batteries. Our kids discovered that if their fingers were wet, and they each held a wire leading to the speaker, they could touch each other and the alarm would sound. They found this to be absolutely hilarious.
Get a can of soda or any processed sweet food with an ingredients label. Find out much sugar is in the thing, covert the grams to teaspoons (4 g = 1 tsp), then make a pile of that much sugar. Compare different things to find the thing with the most added sugar, then make that pile of sugar. Consider whether you now want to eat that pile of sugar.
Proprioception challenge: stand a couple of feet away from some cups on the floor. Close your eyes and keep them closed as you try to drop a marble into a cup. (This was part of a longer series of challenges to show that you have way more than five senses.)
Build a big crane that can dangle a cat toy from the second floor to the first floor.
Business/index card “sailboats”. You may be able to do this at a restaurant if the host counter offers business cards and you have a glassy table. Fold a card any way you want, place it on a smooth table, then blow on it. Try to come up with the one that glides the best across the table.
These specific activities were all great fun — but the point is that you can do a lot with what you already have. The main thing is to decide to do something, spot something you can work with, and then announce that it’s time for a project.
Tip: You can make any project more interesting by giving it a distinctive name. You’re not making a crane, you’re making a Sky Crane. You’re not making a cat bed, you’re making The Circle of Comfort. Sometimes the name comes at the beginning; sometimes one of you will say something funny in the middle and you can use that.
The open source WESL (#WebGPU Shading Language) project recently launched a new WESL documentation site. While helping the group create their site, I realized they could pull their markdown content directly from the places they already keep it.
A key question for any documentation project: how and where should the manually-authored content be stored?
The WESL project maintains two collections of markdown content aimed at different audiences:
The WESL wiki has how-to guidance for devs that want to use WESL in WebGPU projects.
The WESL spec is a collection of more formal documents for implementers.
These collections work well just as they already are, so I thought it’d be good to let the documentation site pull content from these sources using git submodules. The spec is a regular git repository, and behind the scenes a GitHub wiki is a git repository too. Submodules introduce complexities and are not for everyone, but here they let the site project access both repos locally as subfolders.
It was easy to write a program in Origami that pulls content from these subfolders, transforms the markdown to HTML, and pours the HTML into a page template.
The Origami site definition added navigation links, including a pop-up navigation menu for small window sizes.
The wiki and spec both link to each other. A small JavaScript function fixes up those links; the Origami program can easily call that for each document.
I was happy to discover that using a standard HTML <details> element with absolute positioning could do the heavy lifting for a reasonable pop-up menu with very little client-side JavaScript.
Adding full-text search to the complete document collection was just a one-liner via the Origami pagefind extension.
Using git submodules means that wiki or spec updates don’t automatically appear on the documentation site; someone has to go into the site project and pull the latest wiki and spec changes. Having a manual step like that might count as an advantage or a disadvantage depending on your situation.
I was really happy with how small the source for this project ended up being. Setting aside the HTML templates, only ~200 lines of Origami and vanilla JavaScript are required to define the entire site and the client-side behavior.
This is tiny. Origami is a general-purpose system for building sites; it’s not specifically a documentation site generator. This small amount of code defines a bespoke documentation system from scratch.
Using a wiki for documentation this way is really interesting! Project contributors can freely edit wiki pages using familiar tools, then have all that content turned into a static documentation site that project users can freely browse.
VS Code is moving towards letting people write VS Code extensions directly in native ES modules but as of this writing it’s still not possible. If you are writing a new VS Code extension in early 2025, here is a way to write your extension nearly entirely in ES modules today.
I haven’t published a version of a VS Code extension that uses this technique yet, but an in-progress branch works locally and I believe this will work in production. I’m sharing this technique before shipping it because it’s clear other people are also actively searching for a solution to this problem.
This strategy leverages Node’s current support for mixing CommonJS and ES modules. You create a small CommonJS wrapper for your extension, then do all your real work in ES modules. Everything can be done in plain JavaScript (no compilation or bundling required).
CommonJS portion
In package.json, set "type": "commonjs". This lets Node treat plain .js file extensions as CommonJS so that VS Code’s own modules can load.
Create an entry point to your VS Code extension with a .cjs file extension: e.g. extension.cjs. (You could potentially use a .js extension but the .cjs will help you and others remember that this is CommonJS.) This file is just a wrapper, and the only place where you write using CommonJS conventions: require and module.exports.
In package.json, set this wrapper as the extension entry point: "main": "./src/extension.cjs"
Create an ES module with an .mjs file extension: extension.mjs. This module is your extension’s real code, and here you’ll use the ES module conventions: import and export.
Have extension.cjs use a dynamic import to load extension.mjs. You can’t use require() for this, because require is synchronous and ES modules are fundamentally asynchronous. Example
The main export of extension.cjs is a tiny VS Code extension that delegates all lifecycle methods like activate to the real code in the ES module.
ES portion
Your extension.mjs code will want to use the vscode package, but that’s not a regular npm package. The VS Code extension host makes that dynamically available but only to CommonJS modules. Work around this by having extension.cjs obtain a vscode reference and pass it to extension.mjs. You could pass it as a function parameter, but to keep things simpler, I just had extension.cjs set a global variable on globalThis so extension.mjs can read that global. I believe each extension runs in its own process; this should be safe enough. [Updated March 18: A GitHub comment explains that, contrary to what I wrote, “VS Code loads extensions into a single extension host process”.]
Inside your extension.mjs module you can freely import additional ES modules in your project as long as they have .mjs file extensions. (The project’s "type": "commonjs" will treat plain .js files as CommonJS.)
Your .mjs modules can import VS Code dependencies like vscode-languageclient. However, since those are CommonJS packages, you can not extract specific package members with the ES syntax import { thing }. Instead, import the entire package as a constant, then destructure the constant to extract the members you want. Example
Your .mjs modules can import dependencies from external ES module projects. Their own "type": "module" declaration will let them use .js file extensions as usual.
If you’re writing a language server, you can use the same technique to define the server. The CommonJS wrapper for the server is simpler because it just needs to load the server’s ES module; that will trigger running the server code. Note that a CommonJS module can’t contain a dynamic import at the top level, so you’ll need to put the import inside an immediately-executed async function. Example
Once this is set up, you can do your real work in ES modules and generally ignore the CommonJS wrapper. When VS Code eventually supports extensions as native ES modules, migration should mostly entail deleting the CommonJS wrapper, setting "type": "module", and renaming the .mjs files to plain .js files.
Great: Generating audio from a screenplay made things go much, much faster!
A little bad: Writing a keyboard macro to drive a programming environment is a bit tedious and finicky. I wanted to use a real programming language.
A little bad: Even with a keyboard macro triggering the action on screen, it’s still cumbersome to set up a screen capture program to record the action into video. It was annoying enough that I was reluctant to go through the process again whenever I needed to re-record video.
Bad: Manually editing together the audio fragments and the video was still time-consuming.
Bad: The video showed a session in Microsoft VS Code, but during the days I was working on the video, Microsoft changed the UI of VS Code! That prevented me from incorporating new video, because I didn’t want the distraction of the UI changing back and forth during the video.
Bad: YouTube doesn’t allow you to replace a video with an updated one at the same URL, so each time I edited my video I had to post it at a new URL. This eroded any theoretical value of likes or comments.
What I really wanted to be able to do was write a screenplay and have both the audio and video completely generated from that. I eventually concluded that it would be easier to mock a user interface (like a terminal or editor) than to drive an actual application.
Motion comics
Meanwhile I was fascinated by two UI ideas:
Researcher Bret Victor places thumbnails next to his videos. You can read the thumbnails like a comic. I wish YouTube did this, although it’s been pointed out that anything along these lines would probably reduce their ad revenue.
Interactive motion comics like Florence explore the space in between print comics and interactive games.
I decided to try to create a system that would take a screenplay as input and then output a motion comic. I loved comics as a kid and still enjoy them today. They can feel fun in a way that a tech video often does not.
One strength of a comic is that, unlike a video, the user controls the pace. It’s only a small act to scroll the page, but it feels engaging like reading, not as passive as watching a video.
Building a motion comic in HTML/CSS
One architectural principle I adopted for this was to render the initial form of the complete comic using just HTML and CSS. This not only serves the small audience that don’t or can’t use JavaScript, but also works with the grain of the web.
This static-first approach meant I could easily build the comic page in Origami itself. The main build process feeds the screenplay to a template that generates panels for each screenplay segment. A given panel might the appearance of a terminal window or show a graphic, for example.
Given the advancing state of CSS, building a page in plain HTML and CSS still requires a lot of knowledge, but things mostly work as expected. A particularly important feature for this project was using CSS scroll-snap to center the current panel on the page.
The scroll-snap feature more or less works as advertised, although I notice some slightly odd behaviors on iOS Safari. iOS Safari also has some deeply annoying behavior related to audio autoplay that make it very difficult even to let users opt into audio. These days iOS is my least favorite browser to work in.
Once I could render the basic comic, I went through and added a bit of JavaScript animation to the panels as a progressive enhancement. For now this animation mostly takes the form of typing, but it’s a start. Just as Grant Sanderson has evolved his system for programmatic math animations, this comic system can evolve in the future.
It was really fun to round out the experience with stock vector illustrations, sound effects, and gorgeous comic lettering fonts from BlamBot. As soon as I dropped in a dialogue font with ALL CAPS, the comic feel snapped into focus.
Building this mostly as plain HTML and CSS has two other important benefits:
Change detection. As with all Origami projects, I can use Origami’s own changes function to test the built files against a previously-generated baseline. That includes checking the text of any comic panels that incorporate the output of Origami expressions. If I make a change to the language itself that inadvertently changes the output shown in the comic, the changes function should flag those for me.
These plain web files can be hosted anywhere. I don’t have any particular beef with YouTube at this time but their market position as a capricious and rapacious monopolist should give us all pause. Without the constraints of YouTube, I can update the comic whenever I want and keep the same URL. And you don’t have to sit through ads!
What I really want to do is direct
I now have the basics of the system I’ve wanted: I can edit a screenplay and have that produce a (hopefully) engaging user experience with dynamic visual and audio components.
This feels more like directing than video production. With a video, I often couldn’t get a sense for how a particular line would feel until the video was finished — but unless I was really unhappy with it, it was inconceivable that I would go back and redo a line.
Being able to focus on the screenplay makes it much easier for me to step back, perceive the comic as a viewer, and spot something that can be improved. Editing the comic is as fast as editing any other text and the result of the edit can be viewed instantly.
How does it feel?
This kind of motion comic sits somewhere on a spectrum between plain text documentation and recorded video tutorials. It wouldn’t take much to move this closer to regular text documentation, or push it further to the other end and render all the animated frames to video.
I’m pretty happy with this as it is, but if you go through the comic and have thoughts, I’d love to hear them.