Skip to content

One ink, any picture.
How our coral illustrations are made.

Every coral picture on this site started life as a black stipple drawing on yellow. A 14-line Python script turns the black into our coral and makes the yellow vanish, so each drawing sits straight on a white page. Here’s the prompt line, the script and the formula inside it, the five things that went wrong, and where the pictures ended up.

Illustration of a squat glass ink bottle in black stipple on flat marigold yellow, filling the frame, its torn paper label hand-lettered CORAL, with an old dip pen leaning against it

“I was hoping for the red images”

On 10 October I asked Claude for some logo hearts in the grainy style we use for our chimp pictures, the V4 style from Article 01. Codex drew each heart in black stipple on a flat yellow background. To try them in the logo, Claude turned the black ink into our coral, #FF4D57, and removed the yellow, so the heart stayed coral and didn’t clash with the yellow VIBE chip. Article 05 tells that story. I chose the retro key.

A worn 1980s computer keycap with a heart printed on top, in black stipple on flat marigold yellowCodex render · black on yellow
The same keycap as a coral cut-out on white: the heart and the shaded sides are solid coral, and the pale top is a light coral grainAfter the script · coral on white
The retro key, before and after. Nothing was redrawn: the right-hand picture is the left-hand one, run through the script below. Select either to see it full size.

The next evening I asked for five versions of our pricing section, with illustrations that fitted each package and no hearts. They came back in black ink on yellow tiles and banners. I asked for five more, and said:

“I was hoping for the red images.”

— Simon Bloom

I meant the keycap. The second round used the same coral treatment on every object, and I loved J: poster cards, with each coral scene running off the bottom of its card. A few hours later I asked for the homepage in J’s style, and the same coral objects ended up on Tools and the 404 page too.

Pricing version B: three white package cards, each topped with a black stipple picture on a yellow tile: a typewriter typing ISSUE 01, a pinboard reading SHOW & TELL and two armchairsRound 1 · B, black on yellow
Pricing version J: the same three cards with coral stipple scenes bleeding off the bottom of each card: a typewriter typing ISSUE 01, rubber ducks and two armchairsRound 2 · J, coral on white
Two of the ten pricing versions, exported from Paper. Round 1 kept the yellow. Round 2 is what I’d hoped for. Select either to see it full size.

What coral grain is

Coral grain is one ink. Every pixel in the picture is the same coral; only how see-through it is changes. Where the drawing was solid black, the coral is solid. Where it was grey grain, the coral is partly transparent, so the white page shows through and the grain survives. Where it was yellow, there’s nothing left at all.

That’s why it sits so easily on a page. There’s no background to cut around, no box and no halo. You can put it on a white card, half off the card’s edge or across two cards, and it still looks printed there.

It also follows a colour rule I gave on 11 October, about the icons:

“I don’t like the red on the yellow. For the icons I like it either when it’s the red on the white or the black on the yellow.”

— Simon Bloom

Coral grain is the red on the white. Our blog banners, like the one at the top of this page, are the black on the yellow.

Step 1: draw it in black on yellow

Every picture starts from one style prompt. It’s a paragraph about how the picture looks, followed by a single SCENE line about what’s in it, and only the SCENE line ever changes. Ours came out of the method in Article 01, which publishes a five-sentence version you can copy. A black stipple style on a flat yellow field is what makes the next step work.

For objects that will sit on white, the SCENE line needs one more phrase. Our style prompt crops tight and lets the subject run off the frame, which is right for a banner. On a white page, though, that leaves a hard cut edge in mid-air. This is the SCENE line for a kitchen timer from the pricing versions, exactly as we sent it:

scene-timer.txt
SCENE: a chunky vintage wind-up kitchen timer standing upright, its dial numbered in fives up to 60 with the pointer set at 60 and the words 60 MIN printed on its face, shown whole and complete in the middle of the frame with clear flat colour all around it, nothing touching or running off any edge

And here’s the pattern. Replace the placeholders with your own object and word:

scene-whole-object.txt
SCENE: a [DARK_OBJECT] with [ONE_SHORT_WORD] printed on it in bold block letters, shown whole and complete in the middle of the frame with clear flat colour all around it, nothing touching or running off any edge
A kitchen timer in black stipple on yellow, cropped so close that its body runs off the bottom and both sides of the frameWithout the whole-object phrase
A kitchen timer in black stipple on yellow, standing whole in the middle of the frame with yellow all around it and 60 MIN on its faceWith it
Two kitchen timers from the pricing work. Only the right one’s SCENE line ended with the whole-object phrase, though the two lines also differ in a few other words. Select either to see it full size.

We render through Codex’s built-in image generation, not Paper’s. In September, the same prompts through Paper’s generator came back with soft see-through edges: in one run, none of the 96 renders had a single fully opaque pixel. The same prompts through Codex came back fully opaque, 8 of 8. Our render script checks every result for three things: one image per request, the prompt delivered unchanged, and no transparent pixels at all.

Step 2: turn the black into coral

Claude wrote the recolour as a short Python script using the Pillow imaging library. For every pixel, it does four things:

  1. Find the brightest channel. Take the highest of the pixel’s red, green and blue values, from 0 to 255.
  2. Work out the ink. Ink is 1 − brightest ÷ 255. Black is nearly 1. Yellow and white are nearly 0.
  3. Turn ink into opacity. Opacity is (ink − 0.15) ÷ 0.6, kept between 0 and 1. Anything under 15% ink disappears. Anything over 75% ink is solid.
  4. Paint it all coral. Every pixel gets the RGB value of #FF4D57 and that opacity. Then the picture is cropped to the subject, with 24 pixels to spare.

These are real pixels from the keycap render, put through those four steps:

Six pixels from the retro keycap render and the coral opacity the script gives each one
PixelRed, green, blueBrightestInkCoral opacity
Yellow background254, 196, 202540.000%
Pale grain on the top199, 190, 1821990.2212%
Light grey170, 158, 1561700.3331%
Mid grey128, 120, 1091280.5058%
Dark grey90, 80, 78900.6583%
Black14, 12, 9140.95100%

Why the brightest channel, and not brightness as we see it? Yellow is bright in red and green but has almost no blue. Measured the way our eyes weigh red, green and blue, the render’s yellow is only about 77% bright, so it would count as 23% ink and leave a 14% coral film over the whole background: a pale pink box. Its brightest channel is 254, so the script treats it as almost no ink at all. Black is dark in every channel, so it counts as ink either way.

Here’s the whole script. Run python3 coral.py render.png coral.png to crop to the subject, or add --full to keep the whole frame for scenes that run off the edge of a card, like the poster cards. To use your own brand colour, change the CORAL line.

coral.py
"""V4 render -> coral grain cut-out, same ink formula as the logo's coralise.py.
usage: coral.py SRC.png OUT.png [--full]   (--full keeps the whole frame; default crops to the subject)"""
import sys
from PIL import Image, ImageChops
CORAL = (255, 77, 87)
src = Image.open(sys.argv[1]).convert("RGB")
r, g, b = src.split()
bright = ImageChops.lighter(r, ImageChops.lighter(g, b))
alpha = bright.point(lambda v: max(0, min(255, round((1 - v / 255 - .15) / .6 * 255))))
out = Image.new("RGBA", src.size, CORAL + (0,)); out.putalpha(alpha)
if "--full" not in sys.argv:
    box = alpha.point(lambda a: 255 if a > 60 else 0).getbbox(); pad = 24
    out = out.crop((max(0, box[0] - pad), max(0, box[1] - pad), min(src.width, box[2] + pad), min(src.height, box[3] + pad)))
out.save(sys.argv[2]); print(sys.argv[2], out.size)

What goes wrong

The script is simple. The trouble comes from the pictures, the colours and the exports around it. These are the problems we hit between 10 and 12 October, and what fixed them.

White things turn pale pink

The script keeps only the dark parts. A white envelope is mostly light paper, so after the recolour it’s a thin coral grain over white, which reads as pale pink, and my rules say no pale pink anywhere. In the pricing work, Claude flagged the envelope and replaced it with a dark leather postbag, whose SCENE line asks for “worn dark leather”. A few hours later, under the same rule, Claude swapped a fan of paint swatches on the homepage for a box of crayons.

A stuffed paper envelope sealed with wax, stamped FREE and postmarked WEEKLY, in black stipple on yellowThe envelope
The envelope after the recolour: mostly a faint coral speckle on white that reads as pale pink, with only the stamp and wax seal solidTurns pale pink
A worn leather postbag stuffed with letters, with a luggage tag reading WEEKLY, as a solid coral cut-out on whiteThe postbag instead
A white subject loses most of its ink. A dark one holds it. Select any image to see it full size.
A fanned-out deck of paint swatch cards with STYLE GUIDE on the front card, in black stipple on yellowThe swatch fan
The swatch fan after the recolour: the pale swatches have become pink tintsPale swatches
An open box of chunky crayons printed STYLE GUIDE, as a solid coral cut-out on whiteThe crayon box instead
The swatch fan’s coral version was rerun with the same script for this article. The crayon box is the one on the homepage now. Select any image to see it full size.

The fix: choose objects that are mostly dark, or ask for them dark in the SCENE line. A light area with dark detail works, like the keycap’s pale top and solid heart. A mostly white object doesn’t.

Doodled hearts creep in

The pricing brief said no hearts. The first pinboard came back with small doodled hearts on its notes. The rerender’s SCENE line added this: “every other card carrying only a few short words with no doodles, emoji or drawn symbols of any kind”. It came back clean.

A coral pinboard reading SHOW & TELL, with small hand-drawn hearts and a smiley on several of its notesFirst render
The rerendered coral pinboard reading SHOW & TELL, its notes carrying only short words such as Good Ideas and Small WinsWith the no-doodles phrase
Look at every render at full size before you use it. The hearts are tiny until they’re on your page. Select either to see it full size.

A grey haze on white

This one came from the black version of the same idea. For round 1’s objects that broke out of their tiles, Claude cut out the black ink with our black-ink script, which turns dark tones into opacity much like the coral one. At its default setting it left about 11 out of 255 opacity over the whole background. That’s invisible on yellow, but on white it’s a faint grey box. Raising its --alpha-floor setting from 0.018 to 0.065 took the corners to 0. The setting is a fraction from 0 to 1, not a value out of 255: a floor of 24 wipes the whole picture.

A newspaper paper plane reading FREE as a black-ink cut-out on white, sitting in a faint light grey boxDefault floor · 0.018
The same paper plane cut out with a higher floor, sitting on pure white with no grey boxFloor raised · 0.065
Look at the edges of the left image: the faint grey box is the haze. Select either to see it full size.

The coral script has a floor built in: the 0.15 in its formula. The coral cut-outs we checked had fully transparent backgrounds. Whatever script you use, measure the corner opacity before you upload.

Paper exports are Display P3

Paper exports PNGs tagged “Display P3 Gamut with sRGB Transfer”. Read raw, our brand yellow #F4FF3D comes out as 246, 255, 101, so colour checks miss and anything built straight from the pixels drifts. Convert to sRGB with the file’s own colour profile first. ImageMagick’s -colorspace sRGB doesn’t do it; -profile with an sRGB profile does. Our site’s image export converts every Paper image this way, and so did the build for this article: both pricing boards above came out of Paper tagged P3.

White ink turns the picture into a negative

On the pricing page, the Community card is solid red with black ink ducks, and I asked to see them in white instead. Because the ink was recoloured rather than redrawn, the darkest parts of the drawing, such as the shadows and the eyes, became the brightest. Claude pointed out it looks a bit like a photo negative. Those are the ducks on the pricing page now. If you want light ink on a dark card, you’d need the highlights as the ink instead, which this script doesn’t do.

Where it ended up

The v2 design went live on 13 October. These are the main places coral grain ended up:

  • Pricing. A typewriter typing ISSUE 01, the white rubber ducks, and Gary and me in two armchairs, plus a mailbox, a key on a MEMBERS tag, a tin-can phone and a desk lamp.
  • The homepage. The four topic cards carry the crayon box, a box camera, a card-catalogue drawer and a megaphone.
  • Tools. An instant camera, a retro computer, a ticket dispenser, a photo slide, a typewriter and the crayon box.
  • The 404 page. The card-catalogue drawer, labelled FOUND, and Gary and me in the armchairs.
The live pricing page: three package cards with coral scenes running off their bottom edges, a typewriter, rubber ducks on a red card, and Gary and Simon in armchairsPricing
The live 404 page: a coral card-catalogue drawer labelled FOUND above the heading This page is off the map404
The live site at 1440px wide, on 13 October 2026. Select either to see it full size.
The live homepage topic cards: a coral crayon box, box camera, card-catalogue drawer and megaphone poking out above four tilted white cardsHomepage topics
The live Tools page: a coral instant camera beside the OpenGraph Optimizer and a coral retro computer beside the Favicon CheckerTools
Every object here is the same coral ink on white. Select either to see it full size.

The drawer is on No SaaS Is Safe too, and the armchairs on the membership pages. The blog is the exception. When I saw the homepage in this style, I asked for its blog cards to use the real yellow article banners rather than coral versions, so each article keeps its yellow picture.

What it costs and what it can’t do

  • Every picture is a paid render. Codex’s image generation runs on a ChatGPT plan’s allowance, and every rejected subject is another render. The logo hearts took 73 and the pricing work 29. We didn’t track a cost per picture.
  • We can’t confirm the model. We ask for GPT Image 2.5, but Codex gives no way to pick or confirm the image model. One render’s reply said so: it “can’t confirm GPT Image 2.5”. The files’ content credentials say only “gpt-image”.
  • The model chooses the words in the end. One of Claude’s banner options for this article was a printing press with a sheet reading ONE INK. The prompt arrived unchanged, and the press printed MAKE GOOD THINGS. Check the spelling and wording on every render.
  • Checks can raise false alarms. Our render script flagged nine homepage renders because the prompt had changed. The only change was one extra line break at the end.
  • It’s a filter, not a cut-out. The script can’t tell the subject from the background. Anything light inside the object turns see-through too, which is the pale pink problem above.
  • Not tested: other brand colours. We’ve shipped coral on white and white on red, nothing else.

Try it on your own pictures

  1. Draw in black on one flat colour. Use a stipple style prompt with a single flat background, like the one in Article 01. Pick dark subjects.
  2. Ask for whole objects. End the SCENE line with “shown whole and complete in the middle of the frame with clear flat colour all around it, nothing touching or running off any edge”.
  3. Check the render at full size. It must be fully opaque, with a flat background and the right words, and nothing you didn’t ask for, such as stray hearts.
  4. Run the script. Set your colour in the CORAL line, then run python3 coral.py render.png out.png.
  5. Check it on white. Look for pale tints, a haze in the corners and cut edges. If a Paper export is involved, convert it to sRGB first.

Or hand it to a coding agent. Replace the placeholders, attach the script above, and paste this:

one-ink-recolour.txt
Please render [MY_OBJECTS] with my style prompt [MY_STYLE_PROMPT], changing only the SCENE line. End each SCENE line with: shown whole and complete in the middle of the frame with clear flat colour all around it, nothing touching or running off any edge.

Check each render is fully opaque, has a flat background and spells its words correctly. Then run the attached coral.py on each one with the CORAL line set to my brand colour [MY_BRAND_HEX].

Show me each result on white at full size. Flag anything that turns into a pale tint, has a haze in the corners, or contains something I didn’t ask for. Don’t replace a subject without telling me why.

Here’s this article’s banner, run through the same script with --full:

The ink bottle labelled CORAL with its dip pen, in black stipple on flat marigold yellow, running off the left and bottom edgesThe banner render
The same ink bottle and pen as coral ink on white, the torn label now a light coral grain with the word CORAL in solid coralThrough coral.py
One ink, any picture. Select either to see it full size.

Get the next experiment in your inbox.

Subscribe

Tools used