RSS | JSON Feed

( ( ( featured projects ) ) )

CaveRibbon
Fireworks Tap Toy
Tap Toy
PCEImage Editor
Lights Off
FormulaGraph
Counterfeit Monkey
Dungeon Memalign
Pac-Man Dungeon
Similar Songs

( ( ( posts ) ) )


Posted on 2/19/2024
Tags: Programming, Games
Introducing my latest game, playable right in your web browser: CaveRibbon

CaveRibbon is a one-button game where you control a ribbon gliding through a shrinking cavern. Avoid the walls and obstacles as long as you can. Compete with yourself and your friends by sharing a replay. Submit your high scores to get into the hall of fame!

CaveRibbon can be saved to your home screen as a PWA (Progressive Web App).


This game is a tribute to Sunflat's SFCave for Palm OS. You can play the original on archive.org.

In fact, it was archive.org that stirred up my nostalgia over a year ago by publishing a collection of 565 Palm OS apps, all usable on the web thanks to a Palm OS emulator.

When I was in middle school, I would play SFCave on a Palm m130 at night when I was supposed to go to bed. My dad and I had an ongoing competition to see who could get the highest score.

This was my inspiration to add the replay and share features which let you replay the exact same round, with your previous playthrough visible as a head-to-head competitor.


Some technical details:

- I used PCEImage Editor to create the pixel font and many graphics. I created the PCEImage format and tools so I could simply store and edit graphics as ASCII art. See them here.

- This game is entirely static files. There's no server-side backend. To create replay URLs, I need to serialize the entire game state into the URL itself. This proved to be challenging because there are URL size limits in browsers and messaging apps. In my first approach, I wrote a bunch of code to serialize all of the wall, obstacle, and player positions. This proved to be too much data. I came up with a few tricks to significantly shrink the data:

  - I compress player positions lossily and then interpolate them as part of deserialization. I keep only positions at the top of a peak, bottom of a valley, and when the user is close to an obstacle. I only have to keep ~4% of the original data.

  - I use a deterministic random number generator called GameRand. As long as I reuse the random number generator's seed and don't change the code to use random numbers differently I will get the exact same wall and obstacle positions in a replay.

    - I first came across GameRand in phoboslab's game underrun (source).

    - I searched for the constant 0x49616E42 to find the origins of this function. This led me down a rabbit hole: a StackOverflow post which links to a blog post (now only available on archive.org) which links to this post from 2002 introducing a faster rand function by Stephan Schaem, who appears to be the original author.

  - I couldn't just serialize the replay as JSON. That's not nearly compact enough (every digit is stored as a character!). Instead, I packed raw bytes into a buffer and serialized that into Base64. I did some tweaks to squeeze some additional efficiency (such as replacing characters that need to be percent-encoded in URLs).

  - Replay URLs for scores of 700 or higher get a nice image preview (Open Graph image) that includes the score. I generated all of these files statically using PCEImage and a script.

- This is the first project where I've made use of an LLM to help. I used ChatGPT and it had great positive impact:

  - It helped me do some things faster, such as writing boilerplate or example code that I would have had to write myself while wading through documentation.

  - It helped me do some things I wasn't willing to do myself. I don't have time to learn the right way to get specific UI behaviors using HTML and CSS. I would have compromised the UI design to fit within my current skills. With ChatGPT's help, I was able to get exactly what I wanted much faster than having to learn how to do this on my own.

  - In general, I'm energized to work on more projects with these tools. I found the accuracy to be far above what I expected. It's more than good enough for a non-mission-critical side project.

- I'm trying out some design ideas from Bret Victor's Magic Ink paper (backup). My favorite insights:

  - Most software design should focus on information graphic design, not interaction design

  - The case study of Amazon's book listings drives that point home

  - Try using sentences to describe settings rather than have a list of toggles (illustrated with his BART widget). I use this concept for sound effects settings in CaveRibbon.

  - Instead of requiring interaction, make decisions for the user based on context. I use this idea in CaveRibbon by fading out the rest of the UI during gameplay instead of having a "full screen" button.


See also:

- Sunflat's latest games for iOS and Android

Posted on 12/23/2023
Tags: Games
In an HN post about a very cool PuzzleScript game called Enigmash, I learned sad news.

Jack Lance, the author of the game, died earlier this year.

I went to his website, jacklance.github.io, and saw that it had been updated to refer to him in the past tense.

Seeing that the site is hosted on GitHub, I looked at the recent updates. I discovered two things:

- He made this update himself, to show up a few days later

- He published one last game, Octogram, but unfortunately made a typo in the URL to the game
  - Update 12/30/23: I commented on the git commit and one of Jack's siblings saw it. They pushed a fix and now the link on Jack's website is working!

Here's a working link to Jack Lance's last game: Octogram.

It's a super tricky puzzle game with fun wordplay. Give the source code a peek if you get stuck -- but beware this will spoil all of the answers!


More resources:

- Jack's games
- Jack Lance on Puzzle Wiki
- A blog post about Jack's work
- Another blog post about Jack's work
- Backup of jacklance.github.io
  - Due to some technical details of PuzzleScript, none of his PuzzleScript games are in this repo. Instead, they are stored as GitHub gists. I've grabbed a separate backup of every gist linked to by his games page for posterity.
- A YouTube video where Jonathan Blow mentions Jack's death. "...he was 25... he was already one of the best game designers in the world and was on track to be even better"
- Obituary under his real name


Posted on 11/27/2023
Tags: Programming
In the same vein as FreshRSS, another open source project with a cool API is Mastodon.

Many users I followed on Twitter (now X) have migrated to Mastodon. I found that the Mastodon apps I tried didn't perfect the one thing I want: to read a chronological timeline of posts and reblogs from the accounts I follow - always keeping my spot since the last time I checked.

I had already written my own Twitter client for this, which I've been using for many years, and just finished adapting that for Mastodon.

The Mastodon API made this really easy! I can tell that a lot of care went into making this core use case straightforward. Thank you Mastodon developers!

Here's the short list of requests needed to make a simple Mastodon timeline viewer app:

Below, I'm using https://mstdn.party but you should substitute the Mastodon server you use.

1. Register your app.
- Docs: https://docs.joinmastodon.org/methods/apps/#create
- Replace YOURAPPNAME and YOURWEBSITE below. YOURWEBSITE should be a full url starting with "https://"

curl -X POST -F 'client_name=YOURAPPNAME' -F 'redirect_uris=urn:ietf:wg:oauth:2.0:oob' -F 'website=YOURWEBSITE' https://mstdn.party/api/v1/apps

The response is JSON and includes important fields you will need in later steps: client_id and client_secret. I'll refer to these as YOURCLIENTID and YOURCLIENTSECRET below.


2. Give your app permission to read your account information. Open this URL in your browser and authorize it. You'll be shown an authorization code which I'll refer to as YOURAUTHCODE below.
- Docs: https://docs.joinmastodon.org/methods/oauth/#authorize

https://mstdn.party/oauth/authorize?response_type=code&client_id=YOURCLIENTID&redirect_uri=urn:ietf:wg:oauth:2.0:oob


3. Get an access token for your app to use to request your account's timeline (and make any other helpful API requests).
- Docs: https://docs.joinmastodon.org/methods/oauth/#token

curl -X POST \
       -F 'client_id=YOURCLIENTID' \
       -F 'client_secret=YOURCLIENTSECRET' \
       -F 'redirect_uri=urn:ietf:wg:oauth:2.0:oob' \
       -F 'grant_type=authorization_code' \
       -F 'code=YOURAUTHCODE' \
       -F 'scope=read' \
       https://mstdn.party/oauth/token

The response is JSON and includes the access_token. I'll refer to that as YOURACCESSTOKEN below.


4. Now your app can load your timeline.
- Docs: https://docs.joinmastodon.org/methods/timelines/#home

curl 'https://mstdn.party/api/v1/timelines/home' -H 'Authorization: Bearer YOURACCESSTOKEN'

The result is JSON with everything you need to display the timeline in full. I love that it's all included in a single API request! This even includes account information (display name, username, the image URL for the poster's avatar, and more) for the people you follow and the people whose posts they have reblogged.

One helpful argument to include is "limit". I like to request the max number of posts supported by the API, 40.

curl 'https://mstdn.party/api/v1/timelines/home?limit=40' -H 'Authorization: Bearer YOURACCESSTOKEN'

When refreshing my app, I want to request all posts that are new since the last time I refreshed. I accomplish this by using the max_id argument.

I start by requesting the most recent 40 posts:

curl 'https://mstdn.party/api/v1/timelines/home?limit=40' -H 'Authorization: Bearer YOURACCESSTOKEN'

I check if this includes the last post my app is aware of. If not, I request farther back by including "max_id=" the id of the oldest post in the response I just got. Example:

curl 'https://mstdn.party/api/v1/timelines/home?limit=40&max_id=123456789' -H 'Authorization: Bearer YOURACCESSTOKEN'

I continue loading back like this until any of these conditions are true:
- I find posts that my app already knows about
- I get no posts in response
- I get posts older than 1 month (I don't want to accidentally load back too far)
- I've made more than 5 requests (I don't want to create excess traffic)
And that's it! My app only needs to use a single API to refresh my timeline. I did steps 1 through 3 manually and hard-coded the access token into my app, using it to perform step 4 every time I want to refresh. This is ok because this app is just for me so I don't need a more dynamic authentication flow.

Posted on 11/26/2023
Tags: Parenting, Tools, Leisure
There's not enough time in the day for everything I want and need to do. Parenting, work, exercise, hobbies, sleep, etc.

I've found that even optimizing a few minutes here and there can add up to make life easier and more enjoyable. I still use tools for consuming text and audio/video faster.

Here's one of the biggest new optimizations I was able to make this year.

I had a bunch of work time and a bunch of hobby time I wanted to spend in front of a keyboard. This is thoughtful writing or programming time that couldn't be stolen a few minutes at a time while waiting in line, walking between meetings, etc.

I also had a bunch of underutilized time on a stationary exercise bike. I mostly spent that time reading news feeds or watching TV/movies. I enjoyed that a lot, but it wasn't the right balance of time spent doing that when I had other things I wanted to prioritize.

I searched for a product that would let me use a laptop or iPad + keyboard case while exercising on the stationary bike.

I came across airdesks.com and bought their laptop desk. I got the 64" tall, "Average Size Laptop", No AirShelves config. It's been awesome. I use it on the bike every day and occasionally as a standing desk.

Right now, I'm writing this post while exercising on the bike while my son is napping -- something I couldn't have squeezed in without this desk!

Posted on 11/19/2023
Tags: Programming, RSS
This year has been rocky for free APIs I've enjoyed adopting in personal projects. Twitter (now X) killed 3rd party clients, Reddit made similarly killer API pricing changes, and now Feedly has started charging too.

Fortunately, Feedly's API pricing is very reasonable right now: $8/mo or $72/year!

Feedly provides a great service, as I wrote in March 2020. It's worth paying for at this price.

I've depended on Feedly ever since Google Reader shut down in 2013. I seamlessly migrated and never had problems for 10 years of use. Feedly is a great way to keep up to date with RSS feeds and there are several great RSS reader apps that support it. Feedly is very reliable and "just works".

But this is an era of paid subscription overload. Even though the price is reasonable, I sought out alternatives that wouldn't carry the risk of incremental price increases over the coming years.

And I found one that, for the past few weeks, has proven to be good enough to replace Feedly for me: FreshRSS (GitHub).

It's a free and open source project but using it came with the cost of time. I spent the past few weeks writing a bunch of code, finding a reliable host, and cross-checking with Feedly to ensure I'm getting good results.

Here are some notes on adopting FreshRSS in a personal project. The complexity and uncertainty are the downsides of using this instead of Feedly. The benefits include its price (free), that it is open source, and the option to self-host.

- Export existing feeds from Feedly as an OPML file. Instructions.

- Find a FreshRSS host or host it yourself. Here's a list of official hosts. I tried one, but found it to be unreliable -- it seemed to purge my history after ~1 week. I switched to a different one and have had better results.

- Once logged into the FreshRSS host, update some settings:
  - Profile: create an Authentication token. This will be used to automate updating all feeds. Create an API password. This will be used when you adopt the API to load feed information, news entries, and mark them as read. Take note of the API URL (example: https://host.tld/p/api/).
  - Archiving: change settings to keep longer history. I think this helps avoid old articles reappearing as unread in your feeds. These settings do not appear to be honored by all hosts.

- Import OPML file into FreshRSS: In the web UI, click the folder icon, then Subscription Management, then the folder icon (again), then Import / Export. Then you can pick a file to upload to import.

- To reliably refresh all feeds from the source, I've found it necessary to set up a cron job on a server I own to force FreshRSS to refresh. I'm running this every 4 minutes, but FreshRSS will only honor it every ~15-20 minutes. Use your username and the authentication token configured in FreshRSS settings. Make sure to URL encode them (space will need to be replaced by +, special characters will need to be %-encoded).
*/4 * * * * /usr/bin/sh /path/to/updateFreshRSS.sh
updateFreshRSS.sh:
/usr/bin/curl 'https://host.tld/p/i/?c=feed&a=actualize&ajax=1&force=1&user=YOURUSERNAME&token=AUTHENTICATIONTOKENSETABOVE'
  - Most RSS feeds keep enough history that you won't miss entries even if you refresh infrequently. For me, one feed in particular lost entries in FreshRSS compared to Feedly: the unofficial Hacker News front page feed from hnrss.org. Because this feed seems to provide a current snapshot rather than full history, FreshRSS's less frequent refreshes ended up missing entries that Feedly would catch. My frequent cron job plus changing the feed url from https://hnrss.org/frontpage to https://hnrss.org/frontpage?count=100 worked well enough to nearly match Feedly's results (and any discrepancies were actually desirable, like missing posts that were quickly flagged to death).

- Now, you're ready to adopt the API. FreshRSS's API is identical to Google Reader's! The official documentation even links to old web pages that detail the Google Reader API. I found a few docs helpful:
  - The official documentation
  - The server-side code that implements the API
  - This old archived webpage that details the Google Reader API

Some important requests:

Authenticate:
curl 'https://host.tld/p/api/greader.php/accounts/ClientLogin?Email=YOURUSERNAME&Passwd=YOURAPIPASSWORD'

This gives a response that looks like:
SID=YOURUSERNAME/123456123456123456abcdefabcdef1234561234
LSID=null
Auth=YOURUSERNAME/123456123456123456abcdefabcdef1234561234

Save the string that comes after "Auth=". I'll call that YOURAUTH below.

Get a "write token" that will allow you to mark entries as read:
curl -H "Authorization:GoogleLogin auth=YOURAUTH" 'https://host.tld/p/api/greader.php/reader/api/0/token'

The response is a string. Use the string as WRITETOKEN below.

Get all news entries in JSON form:
curl -H "Authorization:GoogleLogin auth=YOURAUTH" 'https://host.tld/p/api/greader.php/reader/api/0/stream/contents/reading-list'

Get all unread news entries in JSON form:
curl -H "Authorization:GoogleLogin auth=YOURAUTH" 'https://host.tld/p/api/greader.php/reader/api/0/stream/contents/reading-list?xt=user/-/state/com.google/read'

Mark an entry (or multiple entries) as read using their entry IDs. In this example, I've put ENTRYID1, ENTRYID2, and ENTRYID3 as example placeholders:
curl -H "Authorization:GoogleLogin auth=YOURAUTH" 'https://host.tld/p/api/greader.php/reader/api/0/edit-tag' -X POST -d 'T=WRITETOKEN&a=user/-/state/com.google/read&i=ENTRYID1&i=ENTRYID2&i=ENTRYID3'

Get your subscription list, which gives you an icon image URL for each feed:
curl -H "Authorization:GoogleLogin auth=YOURAUTH" 'https://host.tld/p/api/greader.php/reader/api/0/subscription/list?output=json'

Posted on 9/17/2023
Tags: Programming, Art, Games
Introducing my latest project: Fireworks Tap Toy

On this year's Fourth of July, my toddler son discovered a love of fireworks. With Fireworks Tap Toy, I'm trying to bring some of that magic to the touch screen.

Tap, click, type, or go into "auto launch" mode.

This project is a remix of a few other projects:

- Tap Toy, which has its own story
  - I copied and modified index.html and progressive web app pieces
  - I copied over touch, mouse, and keyboard support
  - I copied over sound effects and music utilities

- An old codepen (backup) someone created to teach others how to animate fireworks using JavaScript canvas
  - I chose this over fireworks JavaScript frameworks people have shared because it's so much simpler. I don't like the hassle of dependencies. I don't want to install some tool or import an opaque blob. I just want to write some (ideally self-contained, vanilla) code for fun. Here's the top hit for 'fireworks javascript' which I looked into and chose not to use. Where even is the code??


Some tips:

- You can add Fireworks Tap Toy to your home screen on iOS or Android. If you do that, it'll behave more like a normal app. (I did the extra bit of work to make this a "progressive web app")

- If you give this to a kid, you can use Guided Access to prevent them from leaving the app

Posted on 5/21/2023
Tags: Brain Hacking
I use this trick when I can't remember a name I used to know. It works for names of actors, teachers, friends of friends, etc.

The trick:

First, try to remember as much as you can about the person. Imagine their face, appearance, and other biographical information.

Then go through the alphabet saying a name for each letter. Try to pick names that are as plausible as possible for the person you're trying to remember.

Example:

What was my old roommate's girlfriend's name?

She was from Connecticut, they dated for a few years, she had long dark hair, she was allergic to dogs, she worked as an accountant, ...

A... Ashley
B... Blaire
C... Chelsea
D... Daphne
E... Eliza
F... Frances
G... Genevieve
H... Hillary
I... Irene
J... Jessica
K... Katherine
L... Laura
M... Monica
N... Nancy
O... Olivia
P... Penny
Q... Queenie
R... Rachel
S... Stephanie! That's it!

I can almost feel the old dusty neural pathways light up when it finally clicks!

Posted on 1/15/2023
Tags: Programming, Raspberry Pi
I recently completed a harrowing journey to install Emscripten on a Raspberry Pi 400. There were many hiccups along the way that brought back bad memories from the ~8 years that I used Linux (4 on Gentoo, 4 on Debian) as my daily driver OS.

If you're running a well-trodden configuration, things work pretty smoothly. If you're running anything else, you'll spend hours, days, or even years with systems that aren't working 100%.

For Emscripten, Raspberry Pi 400 is not a well-trodden configuration.

Emscripten provides official binaries for many configurations and I bet the official tutorial goes really quickly and smoothly if you are running an x64 CPU.

They even provide binaries if you're running an arm64 CPU, just install using this command (see this ticket for more info):
./emsdk install latest-arm64-linux
But if you are running a 32-bit Intel-based (x86) or ARM CPU, then you will need to build from source.

Raspberry Pi 400 is a 32-bit ARM CPU so I found myself needing to build from source.

And, at least on my machine, the default commands did not work.

(I also tried to get Emscripten installed in the iSH iOS app, which emulates an x86 CPU. The emsdk script spent hours cloning the LLVM repository and then started building from source but iSH consistently crashed early in the build process. I gave up. It's possible this will work in the future.)

Here are the steps I followed, what failed, and how I got it working:

My initial configuration: Raspberry Pi 400 running the Buster (Debian 10) version of Raspbian that came with the SD card. I had installed some packages for other purposes so it's possible I won't mention some packages vital to making these steps work.

I needed to install one package:
sudo apt-get install cmake
I think that the resources emscripten pulls plus the intermediate build products can end up taking tens of gigs of storage (I only checked once during build and it was 22GB). I didn't have that much free space on the Raspberry Pi's SD card so I used a USB-C external drive. My external drive is formatted exfat.

The exfat filesystem doesn't support symlinks and building will fail hours into the process because of that:
[ 50%] Linking CXX shared library ../../lib/libLTO.so
CMake Error: failed to create symbolic link '../../lib/libLTO.so': operation not permitted
CMake Error: cmake_symlink_library: System Error: Operation not permitted
make[2]: *** [tools/lto/CMakeFiles/LTO.dir/build.make:168: lib/libLTO.so.16git] Error 1
make[2]: *** Deleting file 'lib/libLTO.so.16git'
make[1]: *** [CMakeFiles/Makefile2:17719: tools/lto/CMakeFiles/LTO.dir/all] Error 2
make: *** [Makefile:152: all] Error 2
Build failed due to exception!
Instead of reformatting my external drive, I created a very large disk image on it which I formatted as ext4:
# 200GB image - I think this is overkill but I wasn't willing to fail because of insufficient disk space
# Some guides suggest using the fallocate command to create a disk image but I found that command does not work when writing to an exfat drive
dd if=/dev/zero of=image.iso bs=1G count=200

# Format the image ext4
mkfs.ext4 -j image.iso
Then I mounted the disk image and gave the pi user full ownership:
cd /media/
sudo mkdir emscripten-disk
sudo mount /path/to/image.iso emscripten-disk
cd emscripten-disk
sudo chown -R pi:pi .
Within that directory, I ran this:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install sdk-upstream-main-32bit -j1
The "-j1" argument forces the build to use only one CPU. When I originally attempted to install without that argument, the build failed with very little feedback. Just this generic error, no specific compiler errors:
Error 2
Build failed due to exception!
I think this is caused by the machine running out of memory (or maybe some other kind of compiler crash).

Of course, building with only one CPU makes this whole process take much longer. It took ~16 hours for everything to build.

When the build completes, run this:
./emsdk activate sdk-upstream-main-32bit
Pay attention to the output of that command because it tells you how to add the right directories to PATH for emcc to be found.

After all that, following the official tutorial, I created hello_world.c and tested the compiler:
emcc hello_world.c
node a.out.js
And I got this error:
/path/to/a.out.js:144
      throw ex;
            ^
ReferenceError: globalThis is not defined
This is because Buster's version of nodejs is 10.24.0, a version before globalThis is supported.

This command did produce hello.html which worked as expected in my browser:
emcc hello_world.c -o hello.html
To get node working, I needed to update my Raspberry Pi to Bullseye (Debian 11). I followed this guide.

This was an arduous process of running apt-get update, apt-get upgrade, rebooting, manually editing /etc/apt/sources.list, apt-get update, apt-get upgrade, rebooting, apt-get update, apt-get upgrade, apt-get install nodejs, babysitting all of these commands because some of them require user input, losing SSH access as a result of some updates (make sure to run these commands inside screen or tmux!), physically accessing the device to restart sshd, etc.

Finally at the end of it, this command worked:
node a.out.js
(output) Hello, world!
Hopefully these steps work for you!


Other Useful Info


If at any point you need to clean build, you can do this:
rm -rf llvm/git/build_main_32
I had to do this once when figuring out the workaround for the exfat symlink issue. If you change the directory emsdk is running in between attempts, the build scripts will not let you pick up where you left off.


Here's what got me up to the compiling phase in the iSH app on iPad:

apk add cmake
apk add make
apk add clang
apk add binutils
apk add libc-dev
apk add gcc
apk add libstdc++6
apk add g++
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install sdk-upstream-master-32bit
(note that this will take HOURS to clone LLVM)
Progress compiling got to 2% and then iSH crashes.

(I filed an issue.)

Posted on 11/22/2022
Tags: Food, Parenting, Tools
With a toddler at home, we've come to rely on DoorDash as a quick way for the adults to eat tasty food.

We're lucky to live in an area densely packed with restaurants. Despite the wealth of options, we often struggle to choose when dinnertime arrives.

My spouse and I will end up naming restaurants, taking turns rejecting options one-by-one. On some days, nothing sounds good. On some days, we're exhausted from a night of interrupted sleep and lack the energy to pick. Often both.

I got the idea to make picking a restaurant more mechanical on the hard days.

At first, I considered building a web app with a database and a nice UI for listing restaurants and saving our history. I spend most of my non-work hours taking care of the baby, exercising, or doing chores so even a simple side-project takes months and comes at the cost of precious sleep hours. I needed a lower-effort way to test the idea.

I realized I could make this tool in a shared spreadsheet. I used Numbers on iPhone/iPad and shared the sheet with my spouse using iCloud. Google Sheets could've worked too. Spreadsheets are a great low-effort way to solve problems without writing code. I bet a lot of software engineers underutilize spreadsheets.

Here's how it works:
- Add a row for every restaurant
- Have a column for the last time we ate there
- Have a column for each person with an estimate of how often they are willing to eat at each particular restaurant
- Have a column to note any restaurants someone is actively in the mood to eat at
- The sheet calculates a recommendation for each row:
    - Too soon: neither person is ready to eat there again
    - Let's eat (green): both people are ready OR someone is in the mood
    - Maybe eat (yellow): one person is ready
- At dinnertime, peruse the green and yellow rows to pick efficiently

Grab my example sheet to create your own.

Posted on 11/20/2022
Tags: Art
Here's a collection of trippy art from various corners of the internet. Some is generated with the help of neural networks. Some is human-made.

View the collection


Posted on 5/30/2022
Tags: Programming, Art, Games
Introducing my latest project: Tap Toy

Tap Toy is a fun little web app for relaxation. Tap the screen to launch some characters which then bounce off each other. Turn on sound for satisfying sound effects and relaxing public-domain background music. In some ways, I think this toy brings the catharsis of popping bubble wrap to your touch screen.

I got the idea for this project after watching my then-9-month-old's fascination with my iPhone and iPad. Note that the CDC guidelines recommend "no screen time for children younger than 2 years" (source, backup) but these kids will still find a way to grab your device for a fleeting moment before you take it back.

I wondered if I could make an app to soothe and distract our baby if we ever became truly desperate for a moment of peace. (Fortunately, we still haven't reached this point yet!)

I modeled some of the behavior after Baby Einstein Take Along Tunes, one of the more pleasant sound-making toys we have:


The result, as my wife described it, is "crack". She was having a bad day when Tap Toy was first ready to play with and she ended up zoning out, addicted, tapping it for 30 minutes while some of her stress melted away.

As is usual for me, this side-project is a remix of a few other projects:

- MagicKeyboard.io (backup, source) by Feross Aboukhadijeh
    - Which in turn is inspired by a reddit post about this art project by Moment Factory
    - In fact, I kept the keyboard triggers to launch sprites from the right location along the bottom of the screen
    - I fixed the sound effect code and added background music
    - I also added real multi-touch and mouse support

- PCEImage (note that the Koffing, Pikachu in Tap Toy show up in PCEImage Editor's list of examples)

Some tips:

- You can add this web page to your home screen on iOS or Android. If you do that, it'll behave more like a normal app. (I did the extra bit of work to make this a "progressive web app")

- If you give this to a kid, you can use Guided Access to prevent them from leaving the app

An idea to take this further:

- Holiday-themed Tap Toys: Halloween, Thanksgiving, Christmas, Chanukah, New Year's Eve, etc

Posted on 2/13/2022
Tags: Programming, Art, Tools
After defining the PCEImage specification, I wanted to create a great code editor that can color each character in the image.

Try out the editor in your browser or read on for more details.

I was inspired by the PuzzleScript editor (mentioned in my post about PuzzleScript), especially how sprites are defined and styled. Whereas PuzzleScript sprites use indexed color, PCEImage can use almost any ASCII character as a pixel so I couldn't just reuse the same editor.

I could reuse the same foundations, though! PuzzleScript's editor is built using CodeMirror, an open-source text editor implemented in JavaScript for the browser.

CodeMirror is super customizable and I was able to add the coloring support I needed by writing a parser and making some changes to codemirror.js.

I also added support to the editor to automatically generate PNGs and, to add a little fun, wobbling GIFs.


For the wobble effect, I was inspired by Wobblepaint (mentioned in my PICO-8 post) and did my best to try to capture the same organic and charming wobble zep invented.

I also included some "Color Utilites" on the editor page: a simple color picker, a button to invoke your system color picker, and some example palettes (including PICO-8's). Check out Lospec's Palette List for even more ideas.

If you create any cool PCEImages, I hope you'll consider submitting a pull request to add your example! I'll find some way to credit the submitters on the page if I get any!

Posted on 2/13/2022
Tags: Programming, Art
I recently needed to make pixel art for a game and wanted to define the images in code rather than having separate image asset files.

Defining the images in code comes with some benefits:
- I don't need separate versions of the images for different resolutions. I can easily scale them up automatically.
- I can easily slice the sprites to animate them programmatically
- I can make minor tweaks with a text editor (my tool of choice! I am much more at home in a text editor than image editing software)
- I get useful diffs when changing images in source control

To achieve this, I created a simple format I'm calling "PCEImage" (which is short for "Pixel-Character Encoded Image"). Here's what it looks like:

.:#00000000
@:#444444
#:#5555FF

.......
.@.@...
.@.@...
.@.@...
.@@@.#.
.@.@...
.@.@.#.
.@.@.#.
.@.@.#.
.......
Here's that image as a PNG (drawn with scale 10):
A neat benefit of this encoding is that the images are also ASCII art -- it's easy to see what a PCEImage looks like even without coloring. In fact, this format is a combination of ASCII art and indexed color.


PCEImage Format Specification


The first section of a PCEImage defines what color each character represents. Each line in this section looks like: CHARACTER:HEXCOLOR
Then there's one blank line.
Then the second section is the image itself. Every character in this section (except for the newline at the end of each line) represents a pixel in the image. Every line in this section should be the same length (the width of the image).
There should not be a newline character after the last pixel character on the last line.


PCEImage Reference Implementation


I wrote a reference implementation of this specification in JavaScript here.

I had a little fun and also added a wobble mode (similar to Wobblepaint, mentioned in my PICO-8 post).

See it in action in my PCEImage Editor.

Read more about the editor here.

Posted on 2/10/2022
Tags: Music
I've had the Beatles on my mind recently thanks to the buzz around Peter Jackson's "The Beatles: Get Back" documentary.

I grew up long after the Beatles had broken up and I had never listened to a full Beatles album despite liking a lot of their songs. I decided to listen to every album in order, looking up interesting details about the albums and songs on Wikipedia along the way. (I wish there were more pop-up videos!)

I now have a lot more appreciation for the Beatles members as people, how hard-working they were, and how their music evolved.

Here are my Beatles picks (in no particular order). Many of these songs are so popular that I already knew them; others I heard for the first time during this listen-through. I left off some songs (Hey Jude, Lucy in the Sky With Diamonds, and many more) that are objectively good but that I don't like enough to consider "my picks". Some of these songs are funny (such as Norwegian Wood, Michelle, Girl (how a pained John Lennon sucks in air through his teeth), Getting Better), some are just fun, and others are among the most beautiful songs I've ever heard.

- In My Life
- Here Comes the Sun
- Something
- All My Loving
- Love Me Do
- Please Please Me
- I'm So Tired
- Dear Prudence
- While My Guitar Gently Weeps
- You Never Give Me Your Money
- And I Love Her
- Eight Days a Week
- Help!
- The Night Before
- Yesterday
- You've Got to Hide Your Love Away
- Drive My Car
- Norwegian Wood
- The Word
- Michelle
- Girl
- Eleanor Rigby
- Sgt. Pepper's Lonely Hearts Club Band
- With a Little Help From My Friends
- Lucy In the Sky with Diamonds
- Getting Better
- Fixing a Hole
- Lovely Rita
- I Am the Walrus
- Strawberry Fields Forever
- Penny Lane
- The Continuing Story of Bungalow Bill
- Sexy Sadie
- I Want You (She's So Heavy)
- Because
- Across the Universe
- A Day In The Life (the version from "The Beatles / 1967-1970"; the original has creepy audio at the end that's not pleasant in a playlist)

Post-Beatles honorable mentions:

- John Lennon - How Do You Sleep?
- John Lennon - Jealous Guy
- Paul McCartney - Maybe I'm Amazed
- Paul McCartney & Michael Jackson - Say Say Say
- Paul McCartney & Wings - Live and Let Die
- Paul McCartney & Kanye West - Only One
- Rihanna & Kanye West & Paul McCartney - FourFiveSeconds
- George Harrison - My Sweet Lord
- George Harrison - Got My Mind Set On You
- George Harrison - Marwa Blues
- Traveling Wilburys - Handle with Care

I also watched the movies Yesterday and Across the Universe and enjoyed them more than I could've if I hadn't heard these albums and learned some of the history beforehand.

Posted on 12/26/2021
Tags: Music
I still enjoy finding songs that sound similar.

I recently listened to Dear Prudence by The Beatles and could've sworn I had heard the chord progression somewhere before. I searched online and found that I probably heard it in Beautiful by Christina Aguilera. The website that gave me this connection is SoundsJustLike.com.

SoundsJustLike.com is pretty cool with a long list of user-submitted song connections. Unfortunately, it's hard to search for a specific song. To make it easier, I created this one-page snapshot of all the songs users have submitted.

There are a lot of great songs on there to explore!

More posts:
PuzzleScript
Wheat Chex
PICO-8
Fussy Baby Checklist
Fall Asleep
Shape Your Destiny
Tim's Vermeer
Day and Night
Lights Off
Batman Curve
FormulaGraph
Bouncing Ball
Catalog of Math Shapes
Draw Shapes Using Math
Similar Songs
Building a Reddit app
Counterfeit Monkey
Listed Action Interactive Fiction
Andrew Plotkin's Zarfhome
Product Review Writing Prompts
Dungeon Memalign
Pac-Man Dungeon
The Queen's Gambit
Rebecca Meyer
Coding Machines
Clamps
Emoji Simulator
Universal Paperclips
Fall Down the Rabbit Hole
Live COVID-19 Dashboard
Create COVID-19 Graphs Using JavaScript
Books
notes.txt
Download Video Into Your Brain
Download Text Into Your Brain
Source Control
Personal Projects
How to Program a Text Adventure
RSS and Feedly
Welcome