GeistHaus
log in · sign up

SOS

Part of wordpress.com

Shane O'Sullivan's technical blog... really ties the room together

stories
Mazers – a WebOS app rises again on iOS & iPad
Technical
Way, waaayyy back in 2010, I built a fun little game for the Palm WebOS series of phones called Mazer. I was happy with it, loads of people downloaded and played it, and then WebOS died. I recently found the source code again, and with the help of Claude AI I rewrote it to run … Continue reading Mazers – a WebOS app rises again on iOS & iPad →
Show full content

Way, waaayyy back in 2010, I built a fun little game for the Palm WebOS series of phones called Mazer. I was happy with it, loads of people downloaded and played it, and then WebOS died. I recently found the source code again, and with the help of Claude AI I rewrote it to run on iOS and iPad!

Get it for free today from the iOS App Store. (Android version coming soon)

There are four different game types

You can find your way around a simple maze, or race a terrifying fiery ball to the finish.

Over 120 hand crafted obstacle courses to get around with worm holes, force fields, evil fiery balls, and more.

My personal favourite, a Pacman like maze where the four ghosts chase your little ball around as you try to open the portal and get outta there!

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1420
Extensions
Analyse and run simulations on your energy usage
Technical
I’ve been using the Irish energy provider Energia for 5 years or so (as of writing, 2026) and they used to have a useful insights dashboard that let me analyse my power usage. Well, they seem to have removed it so I built a handy dashboard that anyone can use. It’s at https://energy.chofter.com/ , try … Continue reading Analyse and run simulations on your energy usage →
Show full content

I’ve been using the Irish energy provider Energia for 5 years or so (as of writing, 2026) and they used to have a useful insights dashboard that let me analyse my power usage. Well, they seem to have removed it so I built a handy dashboard that anyone can use.

It’s at https://energy.chofter.com/ , try it out!

You simply download your power usage information as a CSV file (a spreadsheet) from their site, currently at https://energyonline.energia.ie/my-account/half-hourly-usage/ . Then drop the file into the web app and it will:

Show a useful overview of your usage for the full time period
You can configure your current home setup

This includes specifying your current tariff, whether or not you have a home battery or a car

Compare usage versus last year
Shows a heatmap of your usage by every 30 minutes
Simulate the change in cost if you change your home setup

Try out what would happen if you kept your consumption the same but changed your tariff, or added a battery or a car. This one is particularly useful.

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1405
Extensions
My 2025: The most productive year yet?
Technicalaiartificial-intelligencechatgpttechnologywriting
As 2026 begins, thinking back on my 2025 is a bit nuts. I can’t believe how much new product I shipped. I think AI had a lot to do with it, freeing me up to take on projects that I still wrote probably 60% of the code I shipped this year, but when I used … Continue reading My 2025: The most productive year yet? →
Show full content

As 2026 begins, thinking back on my 2025 is a bit nuts. I can’t believe how much new product I shipped. I think AI had a lot to do with it, freeing me up to take on projects that

  • Had a technical component that would have discouraged me from starting
  • Are purely personal but fairly complex, meaning I wouldn’t otherwise have taken the time.

I still wrote probably 60% of the code I shipped this year, but when I used AI I mostly used Claude Code to either write an entire app from scratch or to implement code in a language I don’t know, like Swift or 3d shaders.

What did I work on in 2025?

Stainless SDKs

My main job, which took up most of my time, was managing the core SDKs team at Stainless. This is a wonderful company full of great people, and our team shipped some great new products in the year, like the Java, Kotlin, C#, PHP, Ruby, Terraform and CLI SDKs. The code was written by my amazing colleagues, but I like to think my management helped smooth the path for them to be as great as they can be.

Kidz Fun Art [Web, iPad, Windows]

My main side project for the last 4 years has been Kidz Fun Art, a pretty awesome art app I built for my two young daughters. I added so many new features to it in 2025 it warranted its own post, so go read it here. I still wrote most of the code myself, but Claude helped me tackle really complex problems I likely couldn’t have on my own, like writing a WebGL based greeting card feature, a realistic paint brush written from scratch in C and compiled to WASM and a bunch of Swift code in the iPad app.

Gitmeme [Web, Code]

Years ago I built a Chrome extension for Github called GitMeme that makes it simple to add funny GIFs to your commit comments. Well, Github rewrote their site in a very silly way and broke it, so I spent a bit of time fixing it up, and now it works well again.

JS Adventure Courses [Web, Code]

I wanted to teach my two young kids to code in JavaScript last summer and couldn’t find a good entry point for them, so I built JavaScript Adventure Courses (code is open source here). It is an interactive app that takes the student through many parts of the web and JavaScript ecosystem, with an AI tutor to work alongside you when you get stuck.

Super Bubbly [iPad]

Many years ago, when my now 7 and 9 year olds were much younger, they liked a bubble popping game on my phone. I started to write one in React Native, but gave up when I realized I had no way to create the graphics for it. Well, AI is now a thing, so I polished it off, used Googles amazing Nano Banana to generate all the game graphics and shipped it as Super Bubbly on the iPhone and iPad. I wrote a post about it here.

Show My Kids [Web]

About 4 years ago I had the idea to create a simple app that allowed me to quickly write down a film, book or piece of music I’d like to share with my kids when they were older. I built a very early version, enough for me to use and gave up. Well, in 2025 I decided to actually finish it so that it looks good, runs quickly, has all the functionality I need and actually does the thing it’s supposed to do and send you a reminder email when your kids are old enough to watch something you saved. It’s live at showmykids.com and free to use.

Memory Maths [Web, Code]

My kids were struggling a bit with maths at school, so I built an app to help them get better at memorising some of the simpler calculations. It’s called Memory Maths and it’s free for everyone to use. The code is open source here. It supports tracking the progress of multiple people, gives little rewards when you hit some milestones, and is highly optimized to work well on an iPad.

Gif2Vid [Web, NPM, Code]

When my kids create animations in Kidz Fun Art and export them as Gifs, I need to turn them into a video file to upload to Instagram. The app I was using for (that I paid good money for a few years ago!!) decided to shut down and take my money with it. All of the replacement apps were an absolute disaster with terrible ads everywhere. So rather than subject myself to that nonsense I built Gif2Vid.

Gif2Vid is first and foremost an NPM module that anyone can use to enable converting a Gif into an MP4 video on a website. For my own use I also created a mobile optimized site at gif2vid.com that is super fast and easy to use.

I used Claude extensively in writing Gif2Vid, as I needed to write a video encoder in C, which is then compiled to Web Assembly. I wrote a post about it here.

Android Test Management [Web, Code]

As part of shipping Super Bubbly on Android (I really need to get back to that…. It’s still not shipped, so much to do so little time), I ran into Google’s annoying new policy that for any new Android app to be published, you need a certain number of testers to install it. There seemed to be no simple and organised way to do this, other than a bunch of people on Reddit asking each other to install their apps. Another complication is that Super Bubbly is a paid app, and there is no good way to distribute free Promotional Codes to testers.

So I built an app almost 100% with Claude to do this for you.

The site at https://androidtest.chofter.com/ lets you register your Android app, which provides you with a single link to share with testers. When they go to that link they are assigned a Promotional Code that lets them install your Beta app for free, and that code is never shared with anyone else. The app shows you how many codes have been redeemed and track your progress.

The code is open source here.

My Elevation [Web]

I wanted an excuse to mess around with mapping data, inspired a bit by the creator of pastmaps.com on Threads. I thought the idea of creating a site to check my current elevation would be an interesting challenge. I did a lot of research on good places to get elevation data around the world, which was the main challenge. I then used Claude to find efficient ways to process this data to be easily consumed on a lightweight, mostly frontend web app.

The result is my-elevation.com . Try it out, it really works! The site was almost 100% coded with Claude, as were all the scripts for processing Gigabytes of mapping files into many, many tiny JSON files.

iwittr [Web]

I’ve been a fan the Kermode & Mayo movie review podcast for 20 years or more. There used to be a fan app called iWittr on the Apple App Store that let their fans say where in the world they lived. I decided to recreate it as a web app, and it’s live at iwittr.com .

I wrote almost all of it using Claude in a few days, and I’m very happy to say that the global community of Wittitainment fans have been very thankful for the app, as have my movie-review-gods, who have given it many shout outs on their podcast during the year.

Explore Wallpapers [Mac]

Back in the early 2010s I wrote a fun little app for the Palm Pre called Flickr Addict. It would change your phone background regularly using images from the Flickr Explore page, and very high quality images, as well as your own images.

Cut to 2025, and I wanted to see if I could ship an app written entirely in Swift, a language I don’t know, to the App Store by using AI. So I used Claude to recreate most of Flickr Addict as Explore Wallpapers, a fully native Mac app. You can get it here.

I wrote a post about it here.

Huge JSON [Web, Code]

At Stainless I sometimes had to try to understand the content inside huge JSON files, many GBs in size. However these can be very difficult to work with – many IDEs try to format them and freeze, needing a restart, and all the web based tools I tried also failed to scale and froze.

So I used Claude Code to write Huge JSON, a web based tool specifically built to search massive JSON files, and tested to make sure it has no performance hot spots that prevent it from scaling. It lets you search by text, JSON Path and the JQ query syntax (that one was fun to get working!)

I wrote a post about it here.

Feed Finder [Web]

A final tiny project that took about an hour with Claude, I wanted to have an app that you give a website and it returns any RSS or Atom feeds in that site. You can use it at https://feedfinder.chofter.com/


What will 2026 bring? Well, lots more shipping with Stainless, that’s for damn sure :-). Other than that, who knows, we’ll see where inspiration takes me. I might just circle back to some of these projects and see if I can find some more users for them…..

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1376
Extensions
2025: A big year for Kidz Fun Art
Technicalartfamilyipadpaintingtechnology
Kidz Fun Art grew a lot in 2025! Many more people use it regularly, many more choose the paid version & I added far more cool features. Loads of Work… First off, here’s a graph of the number of changes (“commits” in technical terms) that I made. 612 changes in total with a few quiet … Continue reading 2025: A big year for Kidz Fun Art →
Show full content

Kidz Fun Art grew a lot in 2025! Many more people use it regularly, many more choose the paid version & I added far more cool features.

Loads of Work…

First off, here’s a graph of the number of changes (“commits” in technical terms) that I made. 612 changes in total with a few quiet months and many very busy months. In total, 121,946 lines of code were added and 31,579 lines were deleted.

And here is a calendar of my code contributions in Github for the year. The vast majority of these are to Kidz Fun Art

New Features Added

All that work has to mean lots of great new features, and this year didn’t disappoint. Here are the bigger changes made this year.

Fountain Pen

The fountain pen uses the pressure sensor in some digital pens to give a more natural writing style.

Mirror

The mirror is a fun tool that lets you draw anything and then have it be recreated identically in reverse. This is great for drawing things like portraits, people or animals.

Custom Gradients

Everyone likes using the Rainbow in Kidz Fun Art, whether it be drawing with it, filling in with the Rainbow colours, or dropping rainbow coloured sand and watching it fall. But what if you want different colours (as my eldest daughter did)? Fear not! I added a Custom Gradient tool that you can use to design the gradient you want to use.

Smudge

My eldest daughter asked to be able to smudge her drawing, to mix up the colours, so I added a simple smudge tool.

Payment in USD

Less a fun tool, and more of a practical matter, a user asked to able to pay in US dollars as he didn’t understand what a Euro was. No trouble at all, I went to my Stripe account, set up the ability to accept dollars and shipped it in the app!

Greeting Cards

Well this was a huge, technically mountainous feature to build!! I wanted to make the creation of greeting/birthday cards really intuitive for kids, and the best way to do that is to have a real 3D card to work with. This meant working with WebGL for the first time, getting help from Claude Code to write the shaders, and ensuring that it seamlessly transitioned from the 3D card to the 2D editing experience. I even added cool 3D sparkle effects for the hell of it 🙂

A small design detail that I’m really proud of is that when going to export it for printing, I placed a tiny faint + symbol between the four panes so you can always fold it in the right place, but otherwise you’d never notice it on the page once folded. Compared to all the 3D fanciness it’s a tiny thing, but it’s important to sweat the details!

The Blog

To help potential users find the app better, I decided to invest some time in quality written content. This led to me hand coding my own highly optimized, visually attractive blog at https://kidzfun.art/blog . I put a lot of time into making it really useful and informative, and even added its content to the Help tab in the main app.

Sticker Library & Clone Brush

I asked an artist friend who draws the very popular Dark Legacy Comics series what tools he would need to make a great comic authoring app, and the one he said was a must have was a Clone Brush. This is where you can choose a shape, e.g. a leaf, and draw with it, varying it’s shape, shade and size to give the impression that you drew hundreds of individual items. So I built it!!

You can now select any part of the picture and add it to your Sticker Library. Come back any time later and paint with it.

Paint Brush

This was another hugely difficult feature to build, but very rewarding in the end. I read a number of different academic papers to get an understanding of how best to simulate a real paint brush in a digital format. I then implemented it in C code and compiled it to Web Assembly (WASM) for speed, as the rest of the app is written in TypeScript. It took a lot of tuning to get just the right feel, to make the paints mix and fade out in as close to a real world manner as possible, and I think it turned out great!

I’m not aware of another Web based implementation of realistic painting other than some very basic demos that look terrible, and I did look. I don’t want to say that this is the first, but … it might be? Below is my 9 year old daughter Hannah quickly painting a landscape using it.

Save to your iPad

As Kidz Fun Art is a web application first, Apple has never made it possible to save images directly to your Photos app – this is something what works just fine on any desktop computer. I finally got around this using the native iPad app that I wrap the web app with by writing some native Swift code. This means now that users of the iPad app can save directly to their iPad instead of having to email their exported photos to themselves.

Sign in with Apple

No one likes typing into an iPad, and the sign up flow for the subscription has too many friction points. So, on the iPad app I wrote some native Swift code to let the web app sign in with the user’s Apple account. This means that they no longer need to:

  • Type in their email address
  • Open a confirmation email
  • Tap a link in that email
  • Come back to the app

This should hopefully result in more iPad users getting through the sign up flow and enjoying the full set of features that subscribers have access to.

Magic Wand

The last cool new feature of the year is the Magic Wand for quickly selecting shapes. You’ve seen this is most “grown up” art programs like Photoshop, and now here it is for kids! I simplified it a bit by defaulting to selecting objects on a white background, rather than by colour. This means little artists can more intuitively move objects around without having to draw a rectangle. However, if you just want to select a single colour, just tap the “By Colour” toggle, and you can choose how closely related two pixels must be to each other to be included in the selection.

User Growth

The number of monthly users has increased about 5x in 2025 alone. The app has been live since 2022, and I’ve been consistently adding features to it, but this is the year that it really took off. As you see above I’ve been investing a lot of time into things like ease of onboarding, help articles, SEO (helping Google find it), and posting about it in various places online, and it seems to be working!

The graph below shows the total users in the app this year.

Subscriber Growth

Even better, this year has seen a meaningful increase in subscriber numbers, meaning that more people are seeing the real value the app provides, and are willing to pay for it. It’s just €5 a year, so it’s not much, but they’re making the effort to put in the card details and trusting that their kids will use it for years to come. It feels amazing that this app that I built for my own little kids is being used daily by so many others.

The numbers are still small, and I still wouldn’t even make minimum wage from this (if it grows 1000x then maybe it’d be a reasonable financial return 🙂 ) but still, it feels great to see it grow. The graph below from Stripe shows it growing 297% in 2025 compared to 2024.

What’s next in 2026?

Who knows?? I’ve never had a roadmap, not really, I just build things as either my kids ask for them or I think of them. I have a pretty cool 3D printer on order though, and I’m kind of fascinated with designing for laser etching. There are also some cool AI models out there that take a 2D drawing and turn it into a 3D model from printing, which seems like wonderful magic. Perhaps I’ll play with that, I think my two girls (now 7 and 9) would really like it.

As ever, you can get Kidz Fun Art

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1331
Extensions
Gif to MP4 encoding in the browser, the terminal & everywhere else
Technical
TLDR: I’ve shipped a new NPM module called gif2vid that enables video encoding of GIF files in the browser, in your terminal and in a web app backend. I’ve also created an uber simple website at https://gif2vid.com where you can use it. My kids enjoy building cute (and sometimes downright weird) animations using Kidz Fun … Continue reading Gif to MP4 encoding in the browser, the terminal & everywhere else →
Show full content

TLDR: I’ve shipped a new NPM module called gif2vid that enables video encoding of GIF files in the browser, in your terminal and in a web app backend. I’ve also created an uber simple website at https://gif2vid.com where you can use it.

My kids enjoy building cute (and sometimes downright weird) animations using Kidz Fun Art, and I upload the exported Gifs to Instagram as a way to keep them forever. However Instagram doesn’t allow the upload of Gifs, so in the past I bought an iOS app to convert each Gif to an MP4, then uploaded that. Well, that app stopped working, and all the replacement apps are horror shows of aggressive adverts and terrible user interfaces.

I figured I could fix that, so I did. I built gif2vid.com, then open sourced:

What can the gif2vid module do?

Well, it allows you to run a JavaScript only process (containing an inlined WASM file for ease of packaging) in:

  • The UI thread of a browser
  • A worker thread of a browser
  • A terminal on your computer with
    • npx gif2vid input.gif output.mp4
  • The backend of a web app, running on Node.

Check out the README.md file of the source code for instructions on how to do each of these, and the sample projects in the source repo for fully functional example projects of how to use it in an Express app, NextJS app on client and server, Web Worker, and with a simple <script> drop in to a page.

Enjoy!

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1315
Extensions
Super Bubbly: React Native kids game
TechnicalgameipadiphoneJavascriptreactnativetypescript
TLDR: I built a simple & fun game using React Native, try it out on iPhone and iPad! In the Beginning…. My kids are 7 & 9 as I write this in 2025, but 6 years ago in 2019 they loved a simple little bubble popping app on my phone, and I thought it’d be … Continue reading Super Bubbly: React Native kids game →
Show full content

TLDR: I built a simple & fun game using React Native, try it out on iPhone and iPad!

In the Beginning….

My kids are 7 & 9 as I write this in 2025, but 6 years ago in 2019 they loved a simple little bubble popping app on my phone, and I thought it’d be fun to make the best possible version of that type of game.

That means make it the most ridiculously cute game possible, but keep it utterly simple, relaxing and satisfying. I had the idea of making it based on cute animals, and when you pop a bubble, little kittens and puppies run back to their mommies and daddies.

I wrote basically all the code for it, then hit a wall. Where would I get all the cute character and background images I needed? It didn’t make sense to pay an artist thousands of dollars for such a tiny app, and I wasn’t going to be that guy and reuse other peoples assets.

So I gave up.

Present Day….

Cut to 2025, and my now 2 year old twin nephew and niece made me remember my old game. Since 2019 generative AI had become a thing, so creating great new visuals is now pretty trivial. So I dusted off my old app & asked Claude to upgrade it to the latest version of Expo. I did this by creating a brand new Expo project in a sub folder and told the AI agent to move the code from the old app into the new app. It seemed much more likely to succeed than asking it to apply 6 years of migration instructions. And it worked!

Fun and no profit with AI

I found a great site at OpenArt.ai which lets you generate all kinds of styles of images using many different AI models. I found the most success using Google’s Nano Banana model, which was great at creating both characters on a white background (other models often ignored that instruction) and creating large background images.

One area that all models struggled with was the instruction of where to put the “camera”. They all wanted to create images of a character facing you directly, whereas I wanted the camera to be looking down on the top of the characters, as the game is top down. Oh well, it’s probably cuter that you can see all the animals’ faces…

I generated sounds using Adobe Firefly, which is really quite fantastic. It followed instructions very well, and gives away a lot of functionality for free. Well done Adobe! (never thought I’d be saying that).

A game in React Native? Seriously?

The physics of the game are done using MatterJS, which is sufficiently performant in React Native. I tested it on my 6 year old iPad and it runs just fine!

The initial rendering was quite naive, and just moved standard <View> components around, but the app would just crash randomly complaining about memory issues. I rewrote it to draw the main game surface using React Native Skia from Shopify, resulting in the framerate rising and the memory issues disappearing.

Epilogue

It honestly took longer to get the game ready for submission to Apple and Google than it did to shake off the cobwebs, generate the assets and submit it for review. Maybe there’s an opening there for an AI solution that takes care of that process? Please, someone build it that and I’ll happily pay you to use it!

You can download Super Bubbly for

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1293
Extensions
Fighting with YouTube to show a preview image
Technicaldigital-marketingperformanceseowebspeedyoutube
TLDR: Click here for code to implement a lightweight, resilient clickable YouTube preview. YouTube videos are expensive to load on a web page. This compounds if you want your page to display many videos at the same time for the user to choose from. Before the user even plays the video, it loads 4MB onto … Continue reading Fighting with YouTube to show a preview image →
Show full content

TLDR: Click here for code to implement a lightweight, resilient clickable YouTube preview.

YouTube videos are expensive to load on a web page. This compounds if you want your page to display many videos at the same time for the user to choose from. Before the user even plays the video, it loads 4MB onto your previously zippy page.

Of course, YouTube does a decent job of caching these resources, but your browser is still doing all this work, creating another complete page context per video, executing all it’s JavaScript etc.

The ideal state is that an iframe is only created when the user plays the video, so if you’re listing dozens of videos that can be played in place you only pay the performance penalty when the user actually requests the video to play.

A solution … that YouTube makes brittle

One solution is to first show a preview image using a thumbnail image from the video, and when that is clicked, swapping it out for the iframe. This works great! … until it doesn’t.

The Problem

The url for a thumbnail image looks like https://img.youtube.com/vi/bJ7mTY9501c/maxresdefault.jpg . Note the maxresdefault.jpg at the end, this specifies the highest quality possible preview image. The problem is that this only exists on some videos and not others.

It should be easy to add an onerror handler though right? Then we can fall back to a lower res preview image. Haha, yeah, but no.

Instead of simply failing to return the image, sending a 404 response header and causing the <img /> element to trigger it’s onerror listener, YouTube both sends a 404 response and sends a small ugly placeholder image that makes your site look broken. This triggers the onload listener instead of the the expected onerror listener.

The Solution

Instead of an onerror , add an onload handler to the <img /> element, and check the size of the image. The ugly fallback image is 120px wide and 90px tall. Check the size of the image, and if the fallback image is detected, iterate through the available thumbnail images until one loads. The image below shows an implementation of this.

Full Example

There’s a full no-framework example at https://chofter.com/examples/youtubePreview.html , view the source for the full example. This can be easily adapted for any framework like React, Vue etc.

Prior Art

Paul Irish describes another solution to this problem that I found after I built this, check it out here. That also points to these three implementations that are also worth perusing.

I like mine as it uses the least code and is trivial to implement in whatever framework you like, but another of these might suit your needs or taste better.

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1269
Extensions
On Designing For Children
TechnicalaiappdesigneducationipadparentingTablettechnology
I’ve been building Kidz Fun Art (web, iPad & Windows) since 2021, so 4 years at time of writing. It’s a tablet optimized application intended to be used by children of all ages – my daughters were 3 and 5 when I started, and are 7 and 9 now, so I’ve seen how they use … Continue reading On Designing For Children →
Show full content

I’ve been building Kidz Fun Art (web, iPad & Windows) since 2021, so 4 years at time of writing. It’s a tablet optimized application intended to be used by children of all ages – my daughters were 3 and 5 when I started, and are 7 and 9 now, so I’ve seen how they use it almost daily at various ages. I’ve learned a few things about designing for their usability and how it differs from some more common patterns found in adult focused applications.

Update: There’s some good discussion on this on Hacker News – https://news.ycombinator.com/item?id=44711745

Topics Covered

  1. Minimize Text Use
  2. Show, Co-locate and Hint tools
  3. Mistakes should be easy to fix
  4. Know when to involve an adult
  5. Reduce the need for fine motor control
  6. Try to solve Palm Rejection
  7. Simplify, then add delight(ness)
  8. Maintain visual context when changing state
  9. Monetize without ads, or not at all
  10. Plan for app growth without social sharing
  11. Children should never spend money
  12. Epilogue
Minimize Text Use

If you want kids under 8 to use your app, find a way to communicate all its primary functions with as little text as possible.

  1. Text does not actually provide guidance to a large percentage of your user base as they can’t read it
  2. Text takes up valuable space that could be used for better graphics or aesthetically pleasing empty space
  3. Text is visually unattractive and off-putting to most children.

In the image below, note that there is not a single piece of text. While secondary and tertiary features sometimes require text, do what you can to make all primary controls text-free.

Show, Co-locate and Hint tools

Adult focused apps often have menus where the user must know to hunt and peck for the features they need. This is not well suited to younger users. I’ve found that it works much better to design ways to co-locate tools with the objects on which they are to be used.

For example, if a user wants to select and rotate part of the image, place the control for rotating directly on or next to the selected area, not far away in a control bar or menu.

Place tools for manipulating objects directly on top of the objects, with large and obvious hit targets. Feedback from my in-house testers was that putting things like rotation controls on the border of a selected area (like most paint apps) was not a good affordance, whereas overlaying a circle on top of the object to be rotated was immediately obvious.

If the available controls cannot be shown due to space constraints, find a way to hint at their existence in a way that does not require reading or tool tips.

The image above shows my attempt at gently suggesting to the user that there is a menu hidden under each of these buttons if they click a second time. A key point for avoiding a messy UI that is overly noisy is to only show any given hint a small number of times, ideally just once.

Mistakes should be easy to fix

Kids make mistakes, they mess around and generally use the app in every way you never thought of. They are naturally inquisitive and will tap, drag, slap, spit on and high five the screen at any given moment. In all cases, but especially if they are creating content, you should try at all times to make any actions quick and easy to reverse (I need to be better here, and my next mini-project is aimed at improving this for Kidz Fun Art). Some ideas:

  • Have an undo button placed prominently, and a redo button of course. This means building to support a stack of states, which definitely complicates things, but your app will be far less frustrating with these.
  • Use soft deletes, so the user can undo an accidental deletion. Nothing is more likely to make a kid never come back to your app than accidentally hard deleting something they’d spent hours creating and were about to proudly show their parents. This happened a year ago to one of my daughters, full on tears ensued and if it wasn’t their dad building the app that would’ve been the end of their usage of it. Safe to say I was up until 2am working on the fix, feeling like an absolute piece of shit. Don’t be that person.
  • Clean up the soft deletes at some point. I tend to do this after a few days, by which time the child has likely forgotten all about it. No need to keep the data forever. It bloats local storage and is just better for privacy all round.
Know when to involve an adult

If you have features aimed at older kids, your app will likely have stronger retention and usage of more advanced features if you encourage the child to involve the parent at the right times.

Use graphics as much as possible for these prompts, like in the example below. Once the user confirms that they are able to handle what comes next, it’s OK to use a bit more text than you normally would.

To reduce frustration for the user, for any single feature, only request the help of an adult once. This will require you to store the fact that you did so on a per-feature basis either locally, remotely or both.

Reduce the need for fine motor control

Younger children’s motor control is not as developed as that of older children, meaning that if you want them to get as much value from your app as possible, you should try to find ways to provide that value without perfect finger and pen control.

Some ways I’ve found effective to achieve this are:

Large hit targets

This is a pretty obvious one, all buttons and links should be plenty big enough for a child to tap with their finger. If you’re working on the Web, you can easily make the hit target on a button (or anchor tag) larger using CSS, without visually increasing its size. See this code example for one method of achieving this. There are other methods of achieving the same on other platforms, go forth and Google/Claude/ChatGPT (is that a verb?) them.

Long press is your friend

Children naturally long press on touch devices. If your feature can work via long press rather than dragging from one precise point to another, do so, even if it means losing some accuracy. I’ve seen it spark delight in children and adults alike.

For example, if inserting an image, like above, the common way to do this is to insert it with a size chosen using some heuristic, with resize handles for modifying it later. I found that kids much preferred the long press approach above, especially with the added sound effects. I’ve even had a few of their parents email me calling out their delight at this approach.

Try to solve Palm Rejection

I personally never rest my hand on a tablet when using a pen, and so worked on solving this later than I should have. Palm Rejection is the effort by the app to identify which touches represent a command from the user, and which are spurious connections with the skin, like the heel of a hand.

It’s a notoriously difficult problem to solve well, but is critical when building apps for children, and they frequently rest their little hands on the tablet, just as they would on a piece of paper (or their bedroom walls with a crayon….).

Depending on the technology you’re building your app with, there are various approaches to solving this, which I encourage you to research. I found the best that I could do was to use the touchType attribute on touch Event on iOS Safari (read more here) or the pointerType attribute on Pointer Events , which tells you if the user is using a stylus or not. If they are, I remember that for all future interactions and ignore all future events that are not from the stylus. I tried to use a number of different heuristics for the size of the contact area, the pressure applied and more, but when tested with my kids they always got different, less reliable results than I did in my testing. So, the simpler approach seems to work better.

The user may sometimes want to switch back to using a finger, so how best to allow this? My solution is to float this icon near any non-stylus touch point for a short time, two seconds or so, then automatically hide it.

When resting a hand on the screen it usually covers it, but it makes it simple to toggle between modes, and follows the previous guidance of keeping controls near to the location where they are needed, in this case, a few pixels away from the finger or palm (not directly under it).

If you’re aware of solid palm rejection techniques that work in other browsers, please let me know! See chofter.com for my contact info.

Simplify, then add delight(ness)

(with apologies to Colin Chapman for butchering his wonderful line) Even more so than with older users, designing small moments of delight are critical to the retention, both short and long term, of users.

When testing some little touches intended to delight users, I’ve seen my kids laugh out loud, and gotten emails from parents stating the same. This is what you’re aiming for. So what kind of things are we talking about? Well, that’s specific to your app.

In the artistic sphere, I’ve found that kids love bright colours, and especially rainbows. The rainbow pen and gradient fill you see above easily get the most positive reaction, and are relatively simple to build.

Adding sound can be another great way to add delight. When adding an emoji, as shown earlier, there is a nice growing sound that plays.

Kids also love tactility, and the more your 2D app can feel like a real physical object the better. Adding sound to interactions with real-seeming objects is even better. For example, I added the ability to grow and pop bubbles, which is something I used to do as a kid with washing up liquid and a straw (try it, you’ll lose hours of your life, seriously). The bubbles make a satisfying popping sound, and splash out as if you’re making a paint related mess. Not much artistic merit in this, but all kids giggle when they use it.

Adding sparkle effects is another easy way to add delight. These can happen when tapping a button, dragging something around or anywhere you like – kids are just happy to see them. For example, when adding the greeting card feature, I thought my kids would like it if I made rainbow coloured particles fly off the corners of the card when it was rotated. They agreed with loud giggles and “whoa” noises. Find ways to make kids say that fairly regularly and your app will go down well.

Maintain visual context when changing state

For some adults and most kids, if they interact with a visual element and it immediately disappears to be replaced with something else, they can lose context of where they were and what they were doing.

You should always try to make it extremely obvious to the user how they got where they are, and how they can get back to where they were. There are a number of approaches that can help with this. Some that I’ve used include:

  • Dialogs with a semi-opaque background. Popping these up when the user must make a choice makes it very clear what is needed from them, and dismissing them to get back to where they were previously is really simple. A mistake that some people make is to force users to tap a button to close a dialog. Some people struggle with this, so always make the dialog dismiss when the background is tapped too.
  • Only use one dialog at a time. It’s ideal to design flows where no dialog opens another dialog, but if you find yourself in this situation, it can be very confusing for younger users that by dismissing the second dialog, they are still in another dialog. If you must open a second dialog, close the first immediately.
  • Use tasteful animated transitions. It is very possible to overuse animated transitions, so be careful here, but animating from one state to the next is a great way to bring a user along with you. This is particularly important if you’re introducing your user to more complex ideas or objects. For example, when building a comic in Kidz Fun Art, when the user clicked a comic panel to zoom in, I initially just immediately swapped out the view for the zoomed in view, but my kids told me it made no sense at all. This led to me spending a lot (and I mean a lot) of time figuring out the exact way to play with pixels and CSS transitions to enable the zoom in and out effects you see below. It also makes it more clear when moving up, down, left and right between panels. This ended up ballooning the time to build the Comics feature by 300% or so, but it just wasn’t shippable to younger users (the main demographic) without this level of effort. Building for kids is hard, you’ve got to own this fact before you start, or you’ll ship sub-par products.
Monetize without ads, or not at all

To ethically ship an app intended for young children, there is no safe strategy to show 3rd party ads. Children will either be shown inappropriate content, or be tricked into clicking out of the app, which is the beginning of a slippery slope to inappropriate content. Even if the ads are non-click and display only, they are designed to trick and manipulate minds that are not ready for it (are any of us, really?).

If you plan to monetize, find another way. With Kidz Fun Art, I chose freemium + optional yearly subscription, what you choose should best suit your product. But save yourself time and don’t even consider ads – take it from someone who at one point led a decent sized chunk of the Facebook Ads org, ads work too well and should never be shown to children.

A small caveat to this ultimatum can be email related advertising. If you get the parent to sign up with their email, you can potentially use that as a surface for advertising or pushing content not intended for children. I’ve never done this, but you’re probably a lot safer taking this approach than showing ads in the app itself.

Plan for app growth without social sharing

Most apps these days turbo charge their growth by sharing user information or user generated content with others online. For apps with young users, there is almost no safe way to do this. If a young person writes, draws, colours, and uses media on their phone in your app, this opens them up to abuse the moment it is shared outside the app.

This makes it much more difficult to achieve explosive growth with child focused apps, which it partly why so few companies focus on building them – Roblox being an obvious exception, and their approach is more than questionable, I certainly don’t allow my children anywhere near it.

In Kidz Fun Art, the only sharing I found to be safe is:

  • Direct email to the owning account email whenever a new drawing is shared. This is obviously OK, as the parent can see anything the child wants them to .
  • AI generated images created by choosing pre-populated tokens, like “unicorn” or “spaceship” are shared with all users, even non-subscribers. This is safe as the user cannot provide any personal information, and they cannot create any content that might be inappropriate for others.

Outside of these, I’ve avoided any and all sharing: the safety and privacy of the user must be the foremost design principle at all times. If you find yourself straying from this, it’s time to do a hard reset and think about either achieving your growth goals another way, or deciding to abandon the project as a money making concern.

Children should never spend money

This is obvious enough if you’re building ethically, but children should never directly spend their parent’s money, so design your app to enforce this. If you have in app purchases, require proof that it is the adult making the purchase decision.

The pin for the tablet is not sufficient, as the person using the app almost certainly knows it. In Kidz Fun Art I use Stripe for subscription payments, which makes the last 4 digits of the credit card knowable, so I use this as a pin for any in app purchases (only AI image generation credits as of 2025, nothing else planned). Of course this is validated server side and never sent to the client. I figure that if the child has the parent’s credit card literally in hand, spending a little in our app is the least damage they’re likely to be doing that day. If you have better solutions, I’d love to hear them!

Epilogue

Thanks for reading all this, I hope it was of use and didn’t scare you away from building delightful apps for children. I’m still very much fumbling my way through this, so if you have any good tips, feel free to comment, or reach out to me on Threads, X or any other contact method you see on my personal site chofter.com .

Finally, if you’d like to try out my attempt at putting these ideas into practice and maybe get some inspiration, check it out at https://kidzfun.art

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1207
Extensions
Search Huge JSON files on the Web
Technicalcrashformattinghugejqjsonlargeparsingqueriesschema
Working with very large JSON files (20MB+) using online tools tends to be a crashy affair. Whether you’re looking to format or search them, all the tools I found just crash. I found myself having to work with huge JSON files recently, so I built a tool specifically optimized for huge JSON files, called Huge … Continue reading Search Huge JSON files on the Web →
Show full content

Working with very large JSON files (20MB+) using online tools tends to be a crashy affair. Whether you’re looking to format or search them, all the tools I found just crash. I found myself having to work with huge JSON files recently, so I built a tool specifically optimized for huge JSON files, called Huge JSON Viewer (https://hugejson.chofter.com/).

Why does it not break like other JSON viewers?

It scales to very large and deeply nested JSON files using a few optimizations

  • No attempt to do syntax highlighting
  • All large operations take place in web workers.
  • Search results are rendered using a virtualized list, so it can easily scale to thousands of search results.
  • Some fancy algorithms for stringifying deeply nested files that break JSON.stringify (slower, but at least they don’t crash!)
What can it do?

I’m glad you asked! It’s focused on useful and fast search operations. It has three search modes:

  • Simple text search
  • JSON Path queries, when you know the path to the data you’re looking for.
  • JQ searches, for more advanced queries.
Simple Text Search

This does what it sounds like, you type in a text prompt and it finds all occurrences of that string in the file.

JSON Path Search

When you know the path to the data you want, Huge JSON Viewer will find it for you. A nice little touch is that when you click on any search result, it scrolls to the matching position in the left pane and highlights it so you can see the context of the data around the search result.

JQ Search

JQ is like sed but for JSON data. If you already know how to use JQ, great, you’re off to a good start! If not, Huge JSON Viewer makes it really easy to get started.

It provides:

  • A step by step UI builder that let’s you construct a JQ search string visually.
  • A list of commonly used queries which will most likely get you what you want
  • The ability to save your commonly used queries locally so you don’t have to remember them by heart.
Code

The code for this is all open source and available at https://github.com/shaneosullivan/hugejson, pull requests welcome!

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1185
Extensions
Explore Wallpapers for Mac
Technical
Many years ago (2010 or so) I released an app called Flickr Addict for Palm WebOS phones, which automatically downloaded beautiful images from the Flickr photography website and changed the phone background image on a regular basis. I wanted to see if I could rebuild that app in the Apple language Swift, which I didn’t … Continue reading Explore Wallpapers for Mac →
Show full content

Many years ago (2010 or so) I released an app called Flickr Addict for Palm WebOS phones, which automatically downloaded beautiful images from the Flickr photography website and changed the phone background image on a regular basis.

I wanted to see if I could rebuild that app in the Apple language Swift, which I didn’t know. I used a lot of AI (Claude Desktop from Anthropic mostly) to build the app and learn Swift, and I’m happy to say it’s now available on the Mac app store!

Get it from https://apps.apple.com/us/app/explore-wallpapers/id6744676867

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1180
Extensions
Optimizing iWittr.com to reduce Google Cloud & Vercel costs
TechnicalaiapiclaudeCloudtechnology
Last month I decided to do a quick fun project as an excuse to try out AI coding tools, called iWittr.com. It’s a fan site for the Kermode & Mayo podcast, which I’ve been listening to for over 10 years. I might do another post about that experience, but this one is about reducing it’s … Continue reading Optimizing iWittr.com to reduce Google Cloud & Vercel costs →
Show full content

Last month I decided to do a quick fun project as an excuse to try out AI coding tools, called iWittr.com. It’s a fan site for the Kermode & Mayo podcast, which I’ve been listening to for over 10 years.

I might do another post about that experience, but this one is about reducing it’s costs. I found that when it became popular (it’s been mentioned twice so far on their podcast), I began to get alerts from Google that I’d used half of my monthly budget (€20) in two days.

TLDR

  • Firestore is too bloody opaque to properly understand your site’s usage
  • Check your NextJS build logs to ensure that pages you think are cached on the Edge are not accidentally prevented from being cached
  • Close the Firestore console when you’re not using it
  • If you’re generating a huge HTML page that causes thousands of reads, in development mode just fetch a few records.
  • If you have a small data set, move it to a client Worker and read from it repeatedly rather than getting the same few hundred / low thousands of records repeatedly from the server
  • If you have a page that is expensive to load, make sure that any <Link> tags to it have prefetch={false} set

For context, the tech stack is

  • Firebase Firestore for storing data
  • Google Cloud Storage for files
  • NextJS for development
  • Vercel for deployment (UI and functions)
Debugging the costs

Finding the reason for the costs was initially quite difficult. Google’s Billing page lists them under App Engine, which I didn’t think I was using – I had nothing deployed on Google infrastructure. However, it seems they bundle Firebase related costs under App Engine, good to know.

This is where it gets difficult – Firestore will just tell you the total number of reads you are doing, but not the collections you are reading most from, forcing me to try to guess where I was being wasteful.

Optimizing the Map

My first guess was the Map page. I knew that this was the primary page that all users, both logged in and casual browsers, would go to, and it was reading (in the backend) over a thousand records every time they moved the map. Of course I was grouping these together to send far less to the client, but the reads were happening.

I decided to instead do all the map based filtering and grouping on the client, in a Worker thread. It turns out that if I just store the entire set of map data (a list of towns and the number of people checked into them) in a JSON file on Google Cloud Storage, it’s under 200k in size. Every time a new user checks in and adds themselves to the map, I now

  • Download the previous JSON file from storage
  • Update this list with the new town information and upload it again

The client app previously used to hit the /api/marker endpoint every time the map moved, passing it the bounds of the visible map (in longitude and latitude). This resulted in thousands of reads and CPU usage in grouping the data into as few visible map markers as possible all in the cloud.

The client app now does a single read from the server, asking for the URL of the most recent JSON upload. It downloads that file in the Worker thread and stores it in local storage. The UI thread now sends the map bounds to the Worker thread, it reads from memory, does all the filtering and grouping and returns in just a few milliseconds.

There’s two really cool things about this.

  • The first is that it’s much faster, basically instant, and works offline.
  • The second is that the refactoring from running on the API to running in the Worker took less than 10 minutes, as I simply told Claude Code to do it for me. It
    • Created the Worker file
    • Instantiated it in the React component for the Map
    • Moved all the logic from the API route to the Worker
    • Changed all calls in the React component from going to the API to speaking to the Worker
    • Wrote the LocalStorage code in the React component and had the Worker tell it to do the data storage, as the Worker does not have access to LocalStorage
    • When the Map page loads it first populates the map from LocalStorage, then gets the latest JSON file from the internet, if available, and updates the map with the latest data.

This approach only works of course because the data set is small, and I expect it to not grow to a huge size.

Result: a small reduction in the number of reads! Somehow, I’d guessed incorrectly.

Optimizing the Wiki

There was an old defunct wiki that contained loads of great user created content about the podcast, now stored graciously on Archive.org. I had taken all that data and rebuilt a new wiki from scratch, and decided that I wanted one huge page that lists all of the 1300+ articles. I’d told Vercel to cache this on the edge using the revalidate configuration.

However, it turns out that at some point I’d added a check for the user cookie (to determine whether or not they have Editor permissions), and this was causing it never to be cached!

Even worse, the home page used the <Link> component to link to all the other pages, and by default it would be preloaded, so regardless of whether or not someone visited the wiki, it would do its 1300 reads.

Finally, in development there is no Edge caching of course, and when I was working on the code, every time I was saving a file it was reloading the data.

The Fix

I told Claude Code to refactor all user related code to a client component that checks in the browser whether or not they are logged in, allowing the page to be cached properly. In development, I added a simple check to the page and if in development mode, I just load 20 articles instead of 1300+. Finally, I changed the link to <Link prefetch={false} ... > as it’s a huge HTML page and most people won’t navigate to it.

Avoiding the Firestore Console

After making these changes, the read count for Firestore dropped a lot, but there were still hundreds of thousands of unexplained reads. It turns out that if you leave the Firestore Console (their web dashboard) open on an active Collection, it repeatedly reads from the collection over and over again.

Solution: I closed the Firestore console, and only open it when I need it. With this, the number of reads finally became reasonable.

Results

Total number of reads per day have dropped from 9 Million to under 0.5 million, and improvement of around 20x. The map is far faster, and the Wiki loads much more quickly. All in all a good mornings work.

Wishlist
  • Google’s Firestore should give a breakdown per Collection, or even by Index, for reads, to make debugging easier.
  • Vercel should let you know if a previously statically cached page has become uncached, as happened to me accidentally
shaneosullivan
http://shaneosullivan.wordpress.com/?p=1146
Extensions
Custom Gradients in Kidz Fun Art
Technical
For a long time in Kidz Fun Art, the tablet app I built, you could draw with a rainbow brush, which is the favourite feature of many people. I noticed my eldest daughter trying to use it in such a way so to only use a subset of the colors and it was really awkward, … Continue reading Custom Gradients in Kidz Fun Art →
Show full content

For a long time in Kidz Fun Art, the tablet app I built, you could draw with a rainbow brush, which is the favourite feature of many people. I noticed my eldest daughter trying to use it in such a way so to only use a subset of the colors and it was really awkward, so I added this fun new feature where a child can intuitively design their own gradient.

It was a fun challenge to make it both powerful but still intuitive enough for a 3 year old. I think I achieved it, what do you think?

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1139
Extensions
Translate your UI Strings with AI
Technical
TLDR: Easily keep your NodeJS projects translated in many languages with my JSON AI Translation NPM package. A few months ago I translated KidzFun.art to multiple other languages, using ChatGPT’s AI for the translations. This worked fine when dealing with a blank slate, but I found that every time I wanted to add a new … Continue reading Translate your UI Strings with AI →
Show full content

TLDR: Easily keep your NodeJS projects translated in many languages with my JSON AI Translation NPM package.

A few months ago I translated KidzFun.art to multiple other languages, using ChatGPT’s AI for the translations. This worked fine when dealing with a blank slate, but I found that every time I wanted to add a new string, it was labourious to manually translate those new strings into every other language and then update the JSON files myself.

Like any good engineer, I solved this 5 minute problem by doing 10 hours of work to automate it! If you have a NodeJS project (e.g. NextJS or similar), try it out yourself at https://www.npmjs.com/package/json-ai-translation

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1133
Extensions
Fast Linear Gradient Fills with OffscreenCanvas
Technical
TLDR: Demo is here, Code is here, App is here In Kidz Fun Art, the web app for tablets I’ve built for my kids and hopefully yours, I recently added a nice little feature where you can fill in any area with a linear gradient. It’s highly responsive to the user moving their pen/finger, and … Continue reading Fast Linear Gradient Fills with OffscreenCanvas →
Show full content

TLDR: Demo is here, Code is here, App is here

In Kidz Fun Art, the web app for tablets I’ve built for my kids and hopefully yours, I recently added a nice little feature where you can fill in any area with a linear gradient. It’s highly responsive to the user moving their pen/finger, and can quickly let them change the direction and spacing of the gradient at something like 60 frames per second. This post describes the technical details of how it is achieved.

To see it in action, either try it out yourself at https://kidzfun.art or on the minimal demo page, or watch the video below

Step 1

The user clicks inside some shape that they want to fill with a gradient, in the example below it’s the green diamond.

At this point, the app sends a few things to the Worker Thread:

  • An OffscreenCanvas. A Canvas is a 2D drawing element in a web browser. For performance reasons, you can transfer control of a Canvas to a Worker thread, so that as the user is moving their pen around in the single threaded user thread, any paint operations can happen simultaneously without blocking the user’s actions.
  • A copy of the pixel data from the user’s Canvas, containing the green diamond you see above
  • Some data about the point that the user clicked, and the colours to use in the gradient
Step 2

Now the Worker thread performs a simple flood fill with a solid colour, starting from the point that the user clicked. The resulting pixels are set to black in the OffscreenCanvas (the actual colour doesn’t matter, it just has to be opaque). While doing this we record the bounding box around the pixels for use later.

Step 3

Since we now know the bounding box for the filled in pixels, we can fill it with a linear gradient using the context.createLinearGradient function, as below

const colours = ["#FF0000", "#FFFFFF"];
const gradient = context.createLinearGradient(x1, y1, x2, y2);
colours.forEach((color, index) => {
  const stopPosition = index / (colours.length - 1);
  gradient.addColorStop(stopPosition, color);
});

context.fillStyle = gradient;
context.fillRect(x1, y1, x2 - x1, y2 - y1);

Normally the code above would fill the entire rectangle with the gradient colours. However we prevent this by using the very cool globalCompositeOperation property on the Canvas context. By setting it to the value “source-in”, any modified pixels are only actually changed if the existing pixel is already non-transparent. So in this case, only the black pixels we previously drew are now drawn with the gradient colours, achieving the goal of rapidly filling any shape at all with a linear gradient.

Step 4

Finally, when the user lifts their pen/finger, the main thread draws the OffscreenCanvas to the main user canvas using the code

userCanvasContext.drawImage(offscreenCanvas, 0, 0);

and the operation is complete: the user canvas now has the selected shape filled with the gradient colours.

Note that you must use the drawImage function to get the image data out of the OffscreenCanvas. You cannot use other operations as you would in a user thread Canvas, such as getting the ImageData from the Canvas context object.

Try it out

I’ve put an example implementation of this online for you to try out.

Epilogue

If you’re still here, thanks for following along, and give Kidz Fun Art a try – it’s packed full of goodies, all built using the best that the web and my kids’ imaginations have to offer.

screenshot-2024-05-27-at-23.08.03
shaneosullivan
http://shaneosullivan.wordpress.com/?p=1097
Extensions
Using Dall-E/AI to create kids colouring pages in KidzFun.art
Technical
Over the past couple of years I’ve been building KidzFun.art, an art & education app for my young kids and hopefully yours. The first feature I ever added was simple colouring pages, hand drawn by my lovely and talented wife. However that was a slow and laborious process, and with the advances in AI since … Continue reading Using Dall-E/AI to create kids colouring pages in KidzFun.art →
Show full content

Over the past couple of years I’ve been building KidzFun.art, an art & education app for my young kids and hopefully yours. The first feature I ever added was simple colouring pages, hand drawn by my lovely and talented wife. However that was a slow and laborious process, and with the advances in AI since I began, I decided to add the ability for young children to generate a near infinite number of fun, age-appropriate, colouring pages using AI.

I chose to use Open AI’s Dall-E for this, partly as an excuse to use their SDK in production, which is generated by a fantastic company I’m advising called Stainless.

There were four main things required in order to ship this feature:

  1. Use the Open AI Node SDK to generate the image. This turned out to be by far the simplest, hat tip to Stainless and the Open AI team
  2. Build a UI suitable for young children that gave them the ability to easily create an infinite number of colouring pages, while ensuring that the generated content is age-appropriate.
  3. Allow parents (but not their kids) to pay for the Open AI costs, so I don’t go broke.
  4. Cache as much generated content as is feasible to minimize the costs
Using the OpenAI Node SDK

This part was trivial. I created a new NextJS API endpoint that accepted a query parameter, created the OpenAI object using either an API token provided by the user (more on that later), or the default one on my account.

Then simply call the openai.images.generate function and after a few seconds (it’s not particularly fast) it returns you an array of URLs, in my case just one.

A UI suitable and safe for young kids

To make the UI simple, I created a tabbed UI that let kids selected up to ten things to put into the colouring page. When testing this with my 5 and 7 year olds, they found it intuitive – you can’t beat having your users living in your house!

Some older kids may want more control, so I also allowed them to type in the image description manually, using the pencil icon you see in the image above.

To ensure the images are safe, I add a number of instructions on the server side to instruct OpenAI to only generate age appropriate content.

Simple payment for adults only

Using Dall-E costs money, and so it’s necessary for users to pay for this. I kept it simple, allowing parents to buy a pack of 100 image generations at a time, which is likely to last a long long time (see later for why).

You never want to be in a situation as an app developer where a child accidentally spends their parents money. To prevent this, the parent must provide the last four digits of their credit card. I use Stripe for payments, and users of the AI generation feature must already be subscribers, so I have a record of their credit card and I can simply match against that.

Of course, if you’re technically minded and want full control over the spending and budget, you can generate your own Open AI API key and provide that instead of purchasing a pack of pictures.

Caching to minimize costs

A large benefit of providing pre-determined items to place in the picture to kids, rather than free text, is that it is likely that there will be many similar requests. I use this to cut down on generation costs. I use Firebase and Google Storage on the back end, and every time that a child accepts a generated image, I cache both a large and a small version of the image in Google Storage, and make a record of it in Firebase, noting the “tokens” associated with it, e.g. “dog, cat, classroom”.

The next time a child selects “dog, cat, classroom”, they will be shown the cached image first, without subtracting from their count of purchased image generations. It’s only once the child rejects all the cached images that they cause a new image to be generated with Dall-E and subtracts from their pre-purchased allocation. In this way, as more children use the feature, it will take longer and longer for the purchased allocation to run out.

For the sake of safety, only images generated by selecting the provided tokens are cached. If a user writes in free form text that image is never cached nor shown to anyone else. They simply download it and colour it in.

That’s all folks!

Go try out KidzFun.art today on any tablet, laptop or desktop!

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1064
Extensions
Quick chicken soup recipe for sick people
Food
Thanks to ChatGPT for this recipe with no stupid SEO in it. Total time to make: 80 minutes if you’re a normal slow chopper of vegetables (like me), 50 minutes if you’re super fast with a knife. For a simple chicken soup with potatoes, you’ll need the following ingredients: 500g (1 lb) chicken breast or … Continue reading Quick chicken soup recipe for sick people →
Show full content

Thanks to ChatGPT for this recipe with no stupid SEO in it. Total time to make: 80 minutes if you’re a normal slow chopper of vegetables (like me), 50 minutes if you’re super fast with a knife.

For a simple chicken soup with potatoes, you’ll need the following ingredients:

500g (1 lb) chicken breast or thighs, cut into bite-sized pieces

  • 4 medium potatoes, peeled and diced
  • 1 large onion, chopped
  • 2 cloves of garlic, minced
  • 1.2L (6 cups) chicken broth or stock
  • 1 teaspoon salt (adjust to taste)
  • 1/2 teaspoon black pepper
  • 2 tablespoons olive oil or butter
  • Optional: chopped fresh parsley or dill for garnish

Here’s how to make it:

  1. In a large pot, heat the olive oil or butter over medium heat. Add the chopped onion and garlic, sautéing until they’re soft and fragrant, about 2-3 minutes.
  2. Add the chicken pieces to the pot and cook until they’re no longer pink on the outside, about 5-7 minutes.
  3. Add the diced potatoes to the pot along with the chicken broth. Bring the mixture to a boil.
  4. Once boiling, reduce the heat to a simmer and cover the pot. Let it simmer for about 20-25 minutes, or until the potatoes are tender.
  5. Season the soup with salt and pepper. Taste and adjust the seasoning as necessary.
  6. Serve hot, garnished with chopped fresh parsley or dill if desired.

Here’s how it looked when I made it for my 7 year old. She got some of it down the first time and liked it, then had more as she felt better.

No need to read this bit, look after yourself/your person.

But yeah, I hate all the recipes online these days that bury what you really want under mountains of SEO crap. So, while my blog is almost exclusively tech, every now and then I have a sick kid and no bloody patience for that SEO nonsense so this is the recipe that worked for me thanks to ChatGPT, and now I can easily find it forever!

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1056
Extensions
How to list all files in a browser’s Origin Private File System (OPFS)
TechnicalFirefoxopfssafari
In case you’re working with the Origin Private File System on a browser whose dev tools don’t yet support browsing the files (all browsers as of Nov 2023, though Chrome does have an unofficial extension which is nice), then here’s a code snippet you can use to list all the contents of the file system. … Continue reading How to list all files in a browser’s Origin Private File System (OPFS) →
Show full content

In case you’re working with the Origin Private File System on a browser whose dev tools don’t yet support browsing the files (all browsers as of Nov 2023, though Chrome does have an unofficial extension which is nice), then here’s a code snippet you can use to list all the contents of the file system. Great for working on Safari, Firefox etc, though of course native support in the browsers would be much preferred.

listDirectoryContents = async (directoryHandle, depth) => {
  depth = depth || 1;
  directoryHandle = directoryHandle || await navigator.storage.getDirectory();
  const entries = await directoryHandle.values();

  for await (const entry of entries) {
    // Add proper indentation based on the depth
    const indentation = '    '.repeat(depth);

    if (entry.kind === 'directory') {
      // If it's a directory, log its name 
      // and recursively list its contents
      console.log(`${indentation}${entry.name}/`);
      await listDirectoryContents(entry, depth + 1);
    } else {
      // If it's a file, log its name
      console.log(`${indentation}${entry.name}`);
    }
  }
}
shaneosullivan
http://shaneosullivan.wordpress.com/?p=1051
Extensions
How to clean up after your NextJS dev server
BashNextJSTechnical
How to run a clean up script when your NextJS dev server is shut down
Show full content

Sometimes you need to modify files when building a web application that must be reverted before committing. In my case I’m building a Chrome extension that reads from a NextJS based web service, and when I’m working on the browser extension it reads from http://localhost:3005, so I have to modify its manifest.json file to allow this. Of course, I cannot leave that change in the file as it would be a privacy issue and Google would rightly reject it.

Rather than leaving this up to me remembering to manually revert the manifest.json change, here’s how you can do it in bash. The idea is that, when starting up the NextJS process, you run your setup script, and then you listen to the termination signal for the server and execute the cleanup script

Modify package.json

We’re going to use the standard npm run dev command to do all the setup and cleanup work, so make a new script command in the package.json file that runs the standard `next dev` command, e.g.

"scripts": {
  "dev": "./scripts/dev.sh",
  "nextdev": "next dev"
}
Create a dev.sh script

Now create the dev.sh script mentioned above, assuming it is the scripts folder and your setup and cleanup scripts are in the same folder and named run_setup_script.sh and run_cleanup_script.sh respectively

# Get the directory of the script
script_dir="$(dirname "$0")"

"$script_dir/run_setup_script.sh"

on_termination() {
    # Add your cleanup script or command here
    echo "cleaning up dev environment"
    "$script_dir/run_cleanup_script.sh"  
}

# Set up the trap to call on_termination() 
# when a signal is received that shuts it down

# SIGINT is sent when you kill it with Ctrl+C
trap on_termination SIGINT
trap on_termination SIGTERM

# EXIT is sent when the node process calls process.exit()
trap on_termination EXIT

# Now run your NextJS server
npm run nextdev

Screenshot 2023-11-10 at 11.39.47
shaneosullivan
http://shaneosullivan.wordpress.com/?p=1026
Extensions
Lightweight NextJS example of uploading files to Google Cloud Storage
Technical
Many years ago, back in 2018, I wrote a tiny NPM package called gcloud-storage-json-upload, which lets you authenticate with Google Cloud Storage and upload a file without needing to install any huge Google SDKs. I recently needed to use it with NextJS to upload Gifs created in my iPad/tablet/browser app Kidz Fun Art (you can … Continue reading Lightweight NextJS example of uploading files to Google Cloud Storage →
Show full content

Many years ago, back in 2018, I wrote a tiny NPM package called gcloud-storage-json-upload, which lets you authenticate with Google Cloud Storage and upload a file without needing to install any huge Google SDKs. I recently needed to use it with NextJS to upload Gifs created in my iPad/tablet/browser app Kidz Fun Art (you can make animations now!), so I wrote a simple example of how you can do this too.

It shows how you create an API endpoint that uses the gcloud-storage-json-upload package to authenticate with Google and returns a token to the client. The client then uses this token to upload a file to a Google Cloud Storage bucket.

All the code is available on GitHub, I hope it’s helpful.

shaneosullivan
http://shaneosullivan.wordpress.com/?p=1020
Extensions
Instant colour fill with HTML Canvas
Technical
TLDR: Demo is at https://shaneosullivan.github.io/example-canvas-fill/ , code is at https://github.com/shaneosullivan/example-canvas-fill . The Problem When building a website or app using HTML Canvas, it’s often a requirement to support a flood fill. That is, when the user chooses a colour and clicks on a pixel, fill all the surrounding pixels that match the colour of the … Continue reading Instant colour fill with HTML Canvas →
Show full content

TLDR: Demo is at https://shaneosullivan.github.io/example-canvas-fill/ , code is at https://github.com/shaneosullivan/example-canvas-fill .

The Problem

When building a website or app using HTML Canvas, it’s often a requirement to support a flood fill. That is, when the user chooses a colour and clicks on a pixel, fill all the surrounding pixels that match the colour of the clicked pixel with the user’s chosen colour.

To do so you can write a fairly simple algorithm to step through the pixels one at a time, compare them to the clicked pixel and either change their colour or not. If you redraw the canvas while doing this, so as to provide the user with visual feedback, it can look like this.

This works but is slow and ugly. It’s possible to greatly speed this up, so that it is essentially instant, and looks like this

To achieve this we pre-process the source image and use the output to instantly apply a coloured mask to the HTML Canvas.

Why did I work on this?

I’ve built a web based app called Kidz Fun Art for my two young daughters, optimised for use on a tablet. The idea was to build something fun that never shows adverts to them or tricks them into sneaky purchases by “accident”. I saw them get irritated by the slow fill algorithm I first wrote, so my personal pride forced me to go solve this problem! Here’s what the final implementation of the solution to this problem looks like on the app.

The Solution

[Edit: After initially publishing, a large speed up was achieved by using OffscreenCanvas in this commit]

Start with an image that has a number of enclosed areas, each with a uniform colour inside those areas. In this example, we’ll use an image with four enclosed areas, numbered 1 through 4.

Now create a web worker, which is JavaScript that runs on a separate thread to the browser thread, so it does not lock up the user interface when processing a lot of data.

let worker = new Worker("./src/worker.js");

The worker.js file contains the code to execute the fill algorithm. In the browser UI code, send the image pixels to the worker by drawing the image to a Canvas element and calling the getImageData function. Note that you send an ImageBuffer object to the worker, not the ImageData itself


const canvas = document.getElementById('mycanvas');const context = canvas.getContext('2d');

const dimensions = { height: canvas.height, width: canvas.width };

const img = new Image();
img.onload = () => {
  context.drawImage(img, 0, 0);
  
  const imageData = 
    canvas.getImageData(0, 0, dimensions.width, dimensions.height);

  worker.postMessage({
      action: "process",
      dimensions,
      buffer: imageData.data.buffer,
    }, 
    [imageData.data.buffer]
  );
};

The worker script then asynchronously inspects every pixel in the image. It starts by setting the alpha (transparency) value of each pixel to zero, which marks the pixel as unprocessed. When it finds a pixel with a zero alpha value, it executes a FILL operation from that pixel, where every surrounding pixel is given an incremental alpha value. That is, the first time a fill is executed, all surrounding pixels are given an alpha version of 1, the second time an alpha value of 2 is assigned, and so on.

Each time a FILL completes, the worker stores an standalone image of just the area used by the FILL (stored as an array of numbers). When it has inspected all pixels in the source image, it will send back to the UI thread all the individual image ‘masks’ it has calculated, as well as a single image with all of the alpha values set numbers between 1 and 255. This means that using this methodology, we can support a maximum of 255 distinct areas to instant-fill, which should be fine, as we can fall back to a slow fill if a given pixel has not been pre-processed.

You see in the fully processed image above that all pixels in the source image are assigned an alpha value. The numeric value corresponds to one of the masks, as shown below.

For this image, it would generate four masks as in the image above. The red areas are the pixels with non-zero alpha values, and the white are the pixels with alpha values of zero.

When the user clicks on a pixel of the HTML Canvas node, the UI code checks the alpha value in the image returned from the worker. If the value is 2, it selects the second item in the array of masks it received.

Now it is time to use some HTML Canvas magic, by way of the globalCompositeOperation property. This property enables all sorts of fun and interesting operations to be performed with Canvas, but for our purposes we are interested in the source-in value. This makes it so that calling fillRect() on the Canvas context will only fill the non-transparent pixels, and leave the others unchanged.

const pixelMaskContext = pixelMaskCanvasNode.getContext('2d');
const pixelMaskImageData = new ImageData(
  pixelMaskInfo.width,
  pixelMaskInfo.height
);

pixelMaskImageData.data.set(
  new Uint8ClampedArray(pixelMaskInfo.pixels)
);

pixelMaskContext.putImageData(pixelMaskImageData, 0, 0);

// Here's the canvas magic that makes it just draw the non
// transparent pixels onto our main canvas
pixelMaskContext.globalCompositeOperation = "source-in";
pixelMaskContext.fillStyle = colour;

pixelMaskContext.fillRect(
  0, 0, pixelMaskInfo.width, pixelMaskInfo.height
);

Now you’ve filled the mask with a colour, in this example purple, then you just have to draw that onto the canvas visible to the user at the top left location of the mask, and you’re done!

context.drawImage(
  pixelMaskCanvasNode,
  pixelMaskInfo.x,
  pixelMaskInfo.y
);

It should look like the image below when done

All the code for this is available on Github at https://github.com/shaneosullivan/example-canvas-fill

You can see the demo running at https://shaneosullivan.github.io/example-canvas-fill/

One caveat is that if you try this code on your local computer by just opening the index.html file, it will not work as browser security will not let the Worker be registered. You need run a localhost server and run it from there.

P.S.

Thanks to the Excalidraw team for making it so easy to create these diagrams, what a fantastic app!

shaneosullivan
http://shaneosullivan.wordpress.com/?p=983
Extensions