- Go 88%
- JavaScript 10%
- CSS 1%
- Nix <1%
- Shell <1%
- 1 more language <1%
blogart #
Seed-reproducible abstract art for blog article covers. One command, one PNG, one seed you can record in your front-matter and replay later for a byte-identical image. The same binary also serves a local web gallery that learns what you keep — see The web gallery below.
Install #
Requires Go 1.27+. Build from a checkout of this repo:
go build -o blogart ./cmd/blogart
Or a static, stripped release binary (the committed web/dist is embedded,
so no node toolchain is needed either way):
just release # current OS, into out/blogart
just release linux # or: darwin, windows...
Then put blogart somewhere on your PATH. From there, one command makes
a cover:
$ blogart cover.png
Jump to Usage for all flags, The web gallery for the interactive mode, or keep reading for the reproducibility idea in one glance:
$ blogart cover.png
seed pwbb7qll · style waves+julia · palette sand · 4800x2520
reproduce: blogart -seed pwbb7qll cover.png
$ blogart cover2.png
seed 00000007 · style blobs+strings · palette ocean · 4800x2520 · symmetry 3
reproduce: blogart -seed 00000007 cover2.png
Every run without a -seed picks a fresh random seed, and that seed picks
everything else — the style (or pair of styles), the palette, and whether the
cover folds into a kaleidoscope — so articles get varied covers with zero
thought. Pass the seed back and the same binary reproduces the same image,
byte for byte. Output depends only on (seed, style, palette, width, height,
symmetry) for a given binary version.
The default experience: mixing and symmetry #
Plain single-style covers are only half the story:
- Style mixing. About half of runs composite two styles: the background
style renders the full canvas, the foreground style renders an offscreen
layer of the same size, and the two are alpha-blended at an opacity the
seed draws (roughly 25–70%). The seed picks the pair, the order, and the
opacity — any pair is possible, including a style with itself. The summary
shows the composite background-first (
style waves+julia). - Radial symmetry. About one run in three folds the composed image into
an N-fold radial mirror around the center (a mandala/kaleidoscope look),
with N drawn from {2, 3, 4, 5, 6, 8}. The summary appends
· symmetry N.
Both decisions live in the seed, so the seed alone reproduces them — a
mixed, folded cover regenerates from nothing but its seed. Pass -style to
pin exactly one style (which also disables mixing) or -symmetry to control
the fold; see below.
Usage #
blogart [flags] <output.png>
| Flag | Default | Meaning |
|---|---|---|
-seed |
random each run | 8-character lowercase base36 seed to replay a render |
-style |
chosen by the seed | art style name; pins one style and disables mixing |
-palette |
chosen by the seed | color palette name (see below) |
-symmetry |
chosen by the seed | fold count N (≥ 2), or off to force no fold |
-w |
4800 |
image width in pixels |
-h |
2520 |
image height in pixels |
The default 4800x2520 is 4x the Open Graph landscape size (sharp on retina/hi-DPI), so hero covers need no
flags. Square inline figures: -w 900 -h 900.
-symmetry N overrides the seed's fold decision and count — useful when you
want exact geometry:
$ blogart -seed 0flvkxbp -symmetry 5 cover.png
seed 0flvkxbp · style waves · palette autumn · 4800x2520 · symmetry 5
reproduce: blogart -seed 0flvkxbp -symmetry 5 cover.png
-symmetry off (0 is accepted as a synonym) forces no folding even on a seed
that would fold. -symmetry 1 and negative values are mistakes, not choices,
and fail with an error. Pinning -style does not suppress the seed's
symmetry — pinning controls composition, not geometry.
The output path must end in .png — anything else is rejected with an error.
Overwriting an existing file just works: regenerating a cover in place is the
normal workflow. The tool is fully offline, and so is the web gallery: it
binds loopback only and bundles every asset it serves.
The seed workflow for blog front-matter #
-
Generate a cover with one command:
$ blogart posts/my-article/cover.png seed 00000007 · style blobs+strings · palette ocean · 4800x2520 · symmetry 3 reproduce: blogart -seed 00000007 posts/my-article/cover.png -
Record the seed in your article's front-matter, e.g.:
--- title: Notes on Seeded Art cover: posts/my-article/cover.png coverSeed: 00000007 ---The seed alone is enough — it encodes the style pair, the blend, the fold, and the palette. Record the other flags only if you passed them.
-
Regenerate any time — a lost file, a CI rebuild, a new machine — by running the printed reproduction line (the copy-pasteable command from the original run, or reconstructed from the front-matter):
$ blogart -seed 00000007 posts/my-article/cover.pngThe regenerated file is byte-identical to the original, so a published cover never silently changes.
The reproduction line carries only what the seed cannot reproduce: the
seed itself, the -style/-palette/-symmetry flags you actually passed,
and -w/-h whenever the dimensions aren't the default. Copy it verbatim
and it works:
$ blogart -style waves -palette ocean -symmetry 6 cover.png
seed 9avegutt · style waves · palette ocean · 4800x2520 · symmetry 6
reproduce: blogart -seed 9avegutt -style waves -palette ocean -symmetry 6 cover.png
The web gallery #
Hunting for a cover one CLI run at a time — eyeball, rerun, note the seed — is exactly what the web mode replaces. One command opens a local gallery in your browser:
$ blogart --web
serving on http://127.0.0.1:8080 — Ctrl-C to stop
picks log: ~/.local/state/blogart/picks.jsonl
The page opens in a dark theme (a header toggle flips to light — the choice is remembered, and a machine set to light mode is respected on first visit). The first batch of covers (default 10) renders automatically when the page loads. Each cover is a framed card with its configuration as labeled badges — seed, style, palette, symmetry, size — and the seed sits in monospace with a copy button, so any image you like can later be replayed from the CLI:
$ blogart -seed pwbb7qll -style waves -palette sand cover.png
The loop #
- Select. Click cards to mark favorites — the click order is the rank, shown as a rank badge on the card, and any number can be selected. Click again to deselect.
- Generate. The page's one primary button. Replaces the grid with a fresh batch (skeletons while it renders) whose style and palette sampling is weighted toward your selections and past downloads. The visual selection clears; its weight stays, so bias accumulates round by round. A fixed 25% exploration floor keeps fully random images in every batch, so the gallery never collapses into a narrow rut.
- Knobs. Batch size, the style pool, the palette pool, and the canvas dimensions (default 4800×2520) live in a sidebar pinned beside the grid (a toolbar above it on narrow windows) — adjust and regenerate without scrolling back to the top. Empty or invalid dimension fields mean "the server's default".
- Persistence. On the next launch, the picks log re-seeds the weights with a 30-day recency half-life, so your taste profile accumulates across sittings.
Save is the pick #
Each image has a Save button: the browser downloads the PNG as
<seed>.png — the download is the only copy; the server never stores
images — and the server appends one line to the picks log. Downloads are
the only durable signal: selections shape the live session but are never
written to disk, and right-click "save image as" is deliberately not
counted. Downloading the same image again appends a second line —
repetition is a legitimate stronger signal.
Bias steers style and palette only: every image still gets a fresh random seed and a seed-driven symmetry. Each image is exactly reproducible from its config; the batches are deliberately not. That asymmetry is the feature — determinism at the artifact level, exploration at the curation level.
Web-mode flags #
| Flag | Default | Meaning |
|---|---|---|
-web |
off | serve the local gallery instead of rendering one image |
-port |
8080 |
port the gallery listens on — always loopback-only (127.0.0.1) |
-picks |
state dir | picks-log path (see below) |
-w, -h |
4800, 2520 |
default batch dimensions for the page |
Web mode takes no positional arguments — passing one fails with a usage diagnostic and a non-zero exit. An unwritable picks log is also a launch failure: a gallery whose Save clicks could never be remembered refuses to open. Ctrl-C (or SIGTERM) shuts the server down gracefully; every pick already appended is durable, so a crash or closed tab loses nothing.
The picks log #
The picks log is an append-only JSONL file, one object per download:
{"ts":"2026-10-16T18:30:00+02:00","session":"k3x7hq2v","seed":"pwbb7qll","style":"waves","palette":"sand","symmetry":0,"w":4800,"h":2520}
| Field | Meaning |
|---|---|
ts |
download time, RFC 3339 |
session |
8-character id of the server run (fresh per launch) |
seed |
the image's seed — enough to replay it |
style |
style name (the web layer pins one style per image, like the CLI's -style) |
palette |
palette name |
symmetry |
fold count, 0 means no fold |
w, h |
canvas dimensions |
The default location is $XDG_STATE_HOME/blogart/picks.jsonl, falling back
to ~/.local/state/blogart/picks.jsonl — the OS user state directory, not
the project dir, so working directories stay clean and your taste profile
is machine-global. -picks relocates it (e.g. into a synced directory).
Lines are flushed per event and never rewritten; at startup each line
re-weights its style and palette, and a malformed line (a hand-edited log)
is skipped, never fatal. The log is input for a future de-randomizer, not
a learned model itself — the web layer only re-weights its sampling from
it.
Styles #
| Name | Look |
|---|---|
waves |
layered flowing wave bands |
geo-grid |
geometric grid compositions |
blobs |
soft organic radial-gradient blobs |
gradient-mesh |
smooth multi-point color mesh |
tiles |
tiled mosaic patterns |
rings |
concentric ring compositions |
strings |
thread/string art line patterns |
dots |
dot-field compositions |
domain-warp |
warped gradient noise field (vendored) |
silk-sky |
smooth layered silk-like sky (vendored) |
julia |
Julia-set fractal in palette colors (vendored) |
random-walk |
random-walk line traces (vendored) |
Every pair of styles can be mixed, including a style with itself — there is
no exclusion table, and no can't mix those error.
The four vendored styles are adapted from
jdxyw/generativeart (MIT); they live
in internal/generativeart with upstream attribution and consume the same
seeded RNG as the own-engine styles.
Palettes #
| Name | Feel |
|---|---|
sunset |
dusk: slate blue, violet, rose, peach |
ocean |
depth: navy, steel blue, teal, pale aqua |
forest |
pine to sage greens with a warm gold accent |
berry |
wine, purple, magenta, blush |
sand |
desert earth: teal, ochre, terracotta |
autumn |
bark, rust, burnt orange, amber, olive |
neon |
cyberpunk night: indigo, electric purple, magenta, cyan |
mono |
ink-on-paper grayscale ramp |
flexoki |
warm paper, black ink, and eight muted print accents — the Flexoki scheme by Steph Ango (MIT) |
A palette is a color pool: the seeded RNG draws individual colors from it, so the same palette still yields visibly different art across seeds while keeping your blog's look consistent. Both layers of a mixed render share the one selected palette, so composites stay color-coherent.
Unknown style or palette names, and invalid -symmetry values, fail with an
error listing the valid options, so typos are one-glance fixes:
$ blogart -style cubism cover.png
unknown style "cubism" (valid styles: blobs, domain-warp, dots, geo-grid, gradient-mesh, julia, random-walk, rings, silk-sky, strings, tiles, waves)
$ blogart -symmetry 1 cover.png
-symmetry must be "off" or an integer N >= 2, got "1"
Seed stability #
Seeds are stable per binary version: the same seed, flags, and binary reproduce the same bytes. Improving a style in a future release may change a seed's output — re-record the seed (or commit the generated PNG) when you upgrade.
Heads-up when upgrading from the first version: mixing and symmetry became part of the default experience, so roughly half of existing seeds now render differently (remixed, refolded, or both). Re-record those seeds — or simply commit the PNGs they already produced.
Development #
just test-unit # fast test suite through the two test seams (under 10s; deep race run: just test-race)
just lint-go # go vet + golangci-lint
just web-build # rebuild web/dist, the committed SPA bundle (needs node)
Tests exercise the two test seams: the CLI entry function
(Run(args, stdout, stderr)) for the render path, flag/error contracts, and
announcements, and the web layer's exported Server (an http.Handler in
internal/web) via real HTTP roundtrips for status codes, headers, JSON
shapes, and PNG bytes. web/e2e.mjs is an optional real-browser end-to-end
check of the gallery loop (Firefox via geckodriver plus node — run it against
a built, running binary; the unit suite never needs either). The frontend
source lives in web/; its built web/dist is committed and embedded into
the binary, so plain go build, go test, and CI need no node toolchain.
The specs and ticket history live in backlog/. MIT licensed — see
LICENSE.