MoreRSS

site iconRobb KnightModify

I am the lead developer at Radweb working on InventoryBase and related products. I also work part-time as a developer for MacStories.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of Robb Knight

I Was a Guest on Web Weaving

2026-08-11 16:41:57

Update

This episode was recorded prior to me deciding to shut down EchoFeed

James invited me onto his podcast, Web Weaving, to discuss the IndieWeb, EchoFeed (RIP), stickers, pens, and some of my other web projects.

A full transcript is available on the website.

Listen to Web Weaving 14: Robb

Doodle Scan

2026-08-11 04:46:01

While scanning my time travel zine I thought it would be nice to include some drawings I've done as part of my website, similar to how Ana has on her site.

Two boxes on Anas website. One says cursed thoughts with a drawing of a bird, the other says blogroll and has a spider web drawing

Whenever I've tried in the past, removing a background to standard that is good enough to put on another colour background is near impossible without spending a long time in Pixelmator fixing all the edge artifacts.

I came across a couple of websites that do this and I noticed that they do it all in-browser so I started digging around — they convert the uploaded image to a bitmap with createImageBitmap and then going over every pixel and it's rgba values to determine if it should be kept, or made transparent. It was time to do some maths which I am famously not good at - most of what is below is my best estimate at explaining how this works.

tl;dr: The website is up at doodlescan.rknight.me — read on for the technical nonsense. Also Jeff is back.

A beige backround that says Doodle Scan at the top in red and a dinosaur holding a pencil below it.

Update mere hours after this went live

Apparently Jeff looked like a penis so I redid him

A beige backround that says Doodle Scan at the top in red and a dinosaur that doesn't look like a penis.

Removing the Background

"Relative luminance" is the key to this part as defined by WCAG 2.x. Take the RGB values and do the following calculation to get the relative luminance, a single value to compare against. I can't find where I read this now but green and blue are weighted higher because the human eye are more sensitive to those colours. An easy example is a grey where all the values are the same — given an RGB value of 51,51,51 the output will be 51. For something like sky blue the RGB is 130,200,222 we get 186.7064.

getLuminance = (r, g, b) => {
return 0.2126 * r + 0.7152 * g + 0.0722 * b = 5154.213
}

const r = g = b = 51
getLuminance(r, g, b) // 51

const r = 130
const g = 200
const b = 222

getLuminance(r, g, b) // 186.7064

This example range of six colours from pure black to pure white gives an easy to understand example of how the threshold affects what gets removed. The default I set of 235 will only removed the pure white one but the lower than threshold gets, we only keep colours closer to black.

Six coloured squared starting at black getting increasingly lighter until the last one is white

So for each pixel, get the relative luminance value, then compare it to the threshold. If it's lower than the threshold, it gets kept, otherwise it gets set to fully transparent. I'm also checking if it's transparent already and if it is, ignoring it.

const { r: inkR, g: inkG, b: inkB } = hexToRGB(settings.inkColour)
const { r: paperR, g: paperG, b: paperB } = hexToRGB(settings.paperColour)
const alpha = settings.transparent ? 0 : 255

for (let i = 0; i < data.length; i += 4) {
let lum = 0.2126 * data[i] +
0.7152 * data[i + 1] +
0.0722 * data[i + 2]

const isNotTransparent = data[i + 3] > 0
const keep = lum < threshold && isNotTransparent

if (ui.controlBW.checked) {
// For black and white/single colour
// set the ink colour
data[i] = keep ? inkR : paperR
data[i + 1] = keep ? inkG : paperG
data[i + 2] = keep ? inkB : paperB
data[i + 3] = keep ? 255 : alpha
} else {
// when keeping colour, keep the pixels colour
data[i] = keep ? data[i] : paperR
data[i + 1] = keep ? data[i + 1] : paperG
data[i + 2] = keep ? data[i + 2] : paperB
data[i + 3] = keep ? 255 : alpha
}
}

I wanted a black ink only mode where it would set all kept pixels to black (or a colour of my choice) which is what that if/else is doing above. I also added options to set the paper colour because why not. I actually ended up using that feature to make the logo for the site.

A bonus of the threshold control is how much it helps in removing backgrounds on uneven scans or photos, like the example below. With the default threshold nothing gets removed but if I drop it to ~100 and switch to black and white mode, I'm able to remove most of the background while not losing too much detail on the drawings themselves. If this were more important I would use a proper scanner or light the image better but this will be handy for quick doodles.

On the top is a badly lit photo of dotgrid paper with sketches of some people. The bottom shows the same image but with the paper removed

Edge Shrink

I looked through how this worked on the other websites I found, read the code, made some notes, had a little cry, drew some diagrams, read more code, then I finally understood it. It looks at every pixel and if it has a transparent neighbour, then set it to transparent and do the same for each pass up to five times. This diagram I made explains better than I can about how it works over two passes.

On the top is a badly lit photo of dotgrid paper with sketches of some people. The bottom shows the same image but with the paper removed

Darken Lines

This was the the part where I really had to focus to understand what this was doing. The darken lines slider sets a percentage which is then used to determine how much to darken the ink by. It involves getting the darkness or inverse luminance of the pixel, then calculating the amount to darken by by multiplying the darken value by darkness squared, multplied by the alpha level divided by 255 — as best I can tell this last bit reduces the effect on semi-transparent pixels. So yeah, maths. This feature is only useful when using colour mode.

const darkness = 1 - (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
const amountToDarken = strength * darkness * darkness * (alpha / 255)

data[i] = Math.round(r * (1 - amountToDarken))
data[i + 1] = Math.round(g * (1 - amountToDarken))
data[i + 2] = Math.round(b * (1 - amountToDarken))

Out of the rabbit hole

Now I'm "done" with this I can loop back around to where I started which was making some illustrations for my new website design and all I had to do was build an entirely new tool to do it. This will also be handy in September when where I'll be doing drawings for St Jude again.

Types of Time Travel Zine

2026-08-09 03:07:00

Three pieces of paper on a green cutting board with illustrations about time travel on them

I've wanted to make a zine for a while — it has a nice combination of drawing, writing, and being a physical item that appeals to me but I couldn't find the right topic to tackle until I realised I'd already done the writing part of it with my post entitled My Favourite Type of Time Travel.

The format I've come across most is the eight-page foldable zine which means including the cover there are eight pages of stuff to fill. As it turns out my time travel post had seven main types which was handy.

My plan was to draw directly to the single sheet of A4 paper, scan it it, and the zine be ready but I immediately did the the cover, which I really like, on entirely the wrong page. So The zine was actually done over four different pieces of paper as you can see in the photo at the top.

I scanned[1] all the pages in, dropped them all in Pixelmator, and set out each page in the correct order. Doing it like this also allowed me to move about some things that weren't quite right and fix some mistakes but what you see is more of less exactly as I drew it.

You can download Types of Time Travel zine in PDF or PNG formats. I hope you enjoy it. As seems to be tradition with zine websites, I am including a link to this illustration of unknown origin that shows how to fold an eight-page zine: Update: image source via Spike.

I was surprised there's no web component or drop-in library to pass in a zine and make it readable easily online. Watch this space but for now you can read it below.


  1. This led me down a rabbit hole but that's a story for another day

Shutting Down EchoFeed

2026-08-06 21:05:15

TL;DR

I'm shutting down EchoFeed within the next 12 months when the final subscriptions expire. It will continue to run as it is until then so if you've already paid you have access until the end of your subscription.

Last night, after a few months on and off of problems with EchoFeed from my side and general feed problems I made the decision to shut it down. As of right now, this is what I've done:

  • Registration for new accounts is closed
  • Existing free accounts can no longer upgrade to EchoFeed Pro
  • Free accounts and their associated Echoes will no longer cross post
  • EchoFeed Amplify has been turned off
  • All subscriptions have been set to cancel at the end of the billing period

When EchoFeed is running smoothly, it's great. It posts quickly and I think overall most people are happy with it but when I get an email saying there's a problem my heart sinks: it could be a quick reboot fixes it or it could be an endless cycle of fiddling with the database, debugging someone else's feed, or maybe EchoFeed has been blocked for making too many requests[1]. Sometimes I'd be doing the same fixes every couple of hours, usually overnight because the stress meant I couldn't sleep, only for it to magically be fine again seemingly at random.

The sheer volume of feeds EchoFeed was handling caused some of the problems - hundreds of feeds all need to be checked regularly so updates can be found quickly which adds up to hundreds of jobs a minute. At some point, probably now if I'm honest, EchoFeed would need to be upgraded to multiple servers just for running the fetching which for a service charging $25 a year just isn't sustainable. The price is part of the product in that I want normal people to be able to access it and not start charging enterprise prices for cross posting.

Spam was also a big problem and one thats hard to get a handle on. Requiring email verification stopped a lot of traditional spam but there's an endless supply of crypto bros setting up accounts to cross post their shitty pretend money news websites to Bluesky, sometimes with hundreds of posts a day. I don't have the resources to review every single account and feed. The other type is the paid accounts which have 100s of echoes that just post news sites to mirror accounts on Mastodon and Bluesky.

Then there's the money. Once you take into account taxes, server and email costs, I make about the same a month as I would charge for a couple of hours work and most months I am spending at least 4-5 hours looking at issues on EchoFeed.

Then there is the issue of feeds in general. No one, including me, gets RSS or Atom feeds entirely right[2]. EchoFeed's feed fetcher, which should be simple, is filled with multiple lines of code to catch edges cases in different formats, platforms, even some hard-coded personal sites where the platform provider has made a silly mistake. The same is true for the image extraction code - between jpeg, gif, avif, heic, srcset, and various CDN techniques, the code is horrible. This part I don't mind because this is the (usually) fun code bit but it's just another reason EchoFeed has become difficult to maintain.

All of these things, on top of having two young children, have led to to shut down EchoFeed as outlined at the top of the post. It wasn't an easy decision and not one I took lightly. I appreciate the support of everyone who used EchoFeed, paid for it, linked to it, and suggested it as an option for people. I'm particularly grateful for Adam who has given me advice from the start of EchoFeed as well as talking through the shut down.

I have some emails to send out to customers and I need to update the home page to reflect the status but those should get those done in the next couple of days.


Technical notes. Firstly, I will attempt to get the code in such a state that I can release it for people to run themselves but no promises.

Secondly, this is the script I used to cancel everyone's subscriptions in Stripe. To get the subscription IDs go to the subscriptions tab in Stripe and export the list.

// `npm install stripe`
import Stripe from 'stripe';

const SUBSCRIPTION_IDS = [
'sub_XXXXXX',
];

// your API key here
const SECRET = 'sk_live_XXXX'
const stripe = new Stripe(SECRET);
const results = { cancelled: [], failed: [] };

for (const [index, id] of SUBSCRIPTION_IDS.entries()) {
try {
const subscription = await stripe.subscriptions.update(id, {
cancel_at_period_end: true,
});
const cancelAt = subscription.cancel_at
? new Date(subscription.cancel_at * 1000).toISOString()
: 'unknown date';

console.log(`${id} — cancels at ${cancelAt}`);
results.cancelled.push(id);
} catch (error) {
console.error(`${id} — failed: ${error.message}`);
results.failed.push({ id, message: error.message });
}
}

console.log(`Done. ${results.cancelled.length} scheduled, ${results.failed.length} failed.`);

  1. I never really thought of a good way to handle this - you want quick updates when a post appears in a feed but it could be days or weeks in between updates and EchoFeed has checked the feed hundreds of times during that span. WebSub I think is the solution but it's required at the publishers end and not something I could control.

  2. JSON feed is mostly good because the spec is very clear

Drawing Resources

2026-07-28 16:22:00

I asked for recommendations for courses and books related to learning drawing techniques and I got a bunch of great suggestions. I've not tried any of these yet (except a single video from Alphonso Dunn but I want to collect these in one place I can refer back to.

I'm not going to get everything on this list because that would be chaos but there's lots of great stuff in here that I can choose from.

The Art Coach was the course advertised to me on Instagram which triggered me to ask this question in the first place.

I Came Second in the Hemispheric Views June-Boree and Got This Neat Trophy

2026-07-26 17:13:44

I posted about my June-Boree entries a couple of weeks ago and the winners were announced on episode 167. Turns out I came second, beaten narrowly by Arcadia king Eric, which means I get one of Jason's famous trophies.

After some shenanigans with trying to send it to a pickup location which UPS couldn't do three days in a row, I finally got my hands on it and it's glorious.

A floppy disc on a 3d-printed stand on a desk. The label says Hemispheric Views June-Boree and it has a second place trophy on it