Live: paints.paulosalum.com
A static site that answers one question: I own this pot of paint, the recipe calls for a different range — what is the closest thing I can actually buy?
Start points: conversion charts · find a paint from a hex code · all 31 ranges
9,292 miniature paints across 31 ranges, every one of them compared against every other by CIEDE2000, published as ~10,000 pre-rendered pages. No server, no database, no request-time work. The catalog file carries 9,693 rows: 401 of them are one colour wearing a second range label, and those are folded into a single page rather than published twice.
Every range below is matched against all 30 others, both directions.
| Range | Paints | Range | Paints |
|---|---|---|---|
| AK Interactive | 1,130 | Mig | 252 |
| Acrilex | 95 | Mission Models | 201 |
| Apple Barrel | 213 | Monument | 131 |
| Arteza | 88 | Mr. Hobby | 668 |
| Citadel | 451 | Mr. Paint | 568 |
| Coat d'Arms | 150 | P3 | 131 |
| Creature | 53 | Reaper | 438 |
| Duncan | 180 | Revell | 88 |
| FolkArt | 435 | Scale75 | 358 |
| Foundry | 360 | Tamiya | 320 |
| Golden | 346 | The Army Painter | 703 |
| Green Stuff World | 220 | Tom Color | 271 |
| Humbrol | 115 | Turbo Dork | 40 |
| Italeri | 100 | Vallejo | 1,267 |
| Kimera Kolors | 38 | Warcolours | 178 |
| Liquitex | 105 |
The pairs people ask for most: Citadel → Vallejo · Vallejo → Citadel · Citadel → The Army Painter · Citadel → P3 · Tamiya → Vallejo
Comparing RGB numbers does not match human vision: two greens separated by the same RGB distance as two blues do not look equally different. CIEDE2000 (CIE 142:2001) corrects for that with lightness, chroma and hue weighting plus a rotation term in the blue region.
The implementation is verified against the 34-pair acceptance set from Sharma,
Wu & Dalal (2005) — internal/color/color_test.go. That set exists precisely
to catch implementations that get the hue-band and rotation terms wrong, which
most naive ports do.
go build ./... && go vet ./... && go test ./...
go run ./cmd/build -paints paints.json -out dist -base https://paints.paulosalum.comFlags worth knowing:
| Flag | Default | Meaning |
|---|---|---|
-per-brand |
3 | alternatives listed per range on a paint page |
-detail-brands |
12 | ranges given a full block before the rest are summarised |
-chart-min |
60 | paints a range needs before it gets conversion charts |
-adsense |
— | AdSense client id; injects the script and writes ads.txt |
-adsense-slot |
— | ad unit id for the one in-content slot; needs -adsense |
-amazon-tag |
— | Associates tracking id; adds shop links and the required disclosure |
-amazon-host |
www.amazon.com.br |
store the shop links point at; must match the marketplace that issued the tag |
-indexnow |
— | IndexNow key, served as <key>.txt |
-domain |
— | custom domain, written to CNAME |
-export |
— | write the publishable subset of the catalog and exit |
internal/color CIEDE2000, sRGB→Lab, WCAG contrast for swatch text
internal/catalog loading and the licence filter
internal/match ranks every paint against every other, top-N per range
internal/site templates, assets, all page rendering
cmd/build generator entry point
internal/match compares 9,292 × 9,292 pairs. It keeps only a bounded top-N
per range as it goes and stores pointers rather than paint copies — the naive
version allocates 87 million structs and exhausts a 32-bit heap.
Both channels are off until an id is passed, and neither changes what the site
says: the match order is the colour distance and nothing else. Set
-amazon-tag and the paint pages grow two shop links plus the disclosure the
Associates agreement requires; set -adsense and the layout loads the ad
script and writes ads.txt.
A tag is issued by one marketplace and earns nothing on any other, so
-amazon-host follows the tag, and two things follow the host: the search
qualifier (tinta on the Brazilian store, where paint matches almost
nothing) and the disclosure sentence, because each operating agreement
prescribes its own wording and the Brazilian one treats a departure from it as
a material breach.
-adsense-slot adds one explicit in-content placement — above the table on a
conversion chart, below the summary on a paint page. Both sit after the answer
the visitor came for and before the long tail of the page, which is the only
position that is worth anything without pushing the content down. Everything
else is left to Auto ads.
The paint data is MIT, from
Arcturus5404/miniature-paints.
Ranges whose upstream source withheld redistribution are excluded by
catalog.Publishable, and a test fails the build if that filter is loosened.
See LICENSE-DATA.md.
.github/workflows/deploy.yml builds the site and publishes it to GitHub
Pages on every push to main. The custom domain needs one DNS record:
CNAME paints paulosalum.github.io