Skip to content

feat(brand): replace the circular seal with the shield mark - #37

Merged
rustyconover merged 1 commit into
mainfrom
logo-shield
Aug 4, 2026
Merged

rustyconover merged 1 commit into
mainfrom
logo-shield

Conversation

@rustyconover

Copy link
Copy Markdown
Collaborator

Every brand asset carried the old circular VGI seal — including the logo the landing, describe, 404 and 401 pages request by URL. The shield artwork was already in the repo as logo-shield.png, but only the README pointed at it and it sat on an opaque white background.

One master, eight derived assets

docs/assets/logo-master.png (1197×880) is now committed, and everything else is cut from it by scripts/regenerate_logo_assets.py:

uv run --with pillow --with numpy python scripts/regenerate_logo_assets.py
Asset Was Now
logo-shield.png 400×294, white bg 1176×855, transparent
logo-hero.png 200×200 seal 600×436 shield (3× for the ≤200px it renders at)
logo.png 256×256 seal 512×372 shield
apple-touch-icon.png 180×180 seal shield on cream, opaque
favicon-16/32.png, favicon.ico seal shield
social-card.png seal in a white box shield on the green

--master PATH cuts from a different source and replaces the committed one, so the next artwork change is one command instead of eight hand edits that drift apart. The social card is rebuilt by replacing only its logo block, so the text baked into it stays byte-identical rather than being re-typeset with whatever fonts happen to be installed.

Keying the background

A global near-white threshold, not a fill from the border. Keying only what the border can reach leaves the enclosed gaps opaque — the sky between the tree and the barn wall, the holes between the cloud's connector traces — which read as white specks on the cream page and glare on a dark one. The mark's lightest real ink is cream at min-channel 193, far below the 244 threshold, so a global key cannot eat it.

That margin isn't guaranteed for a future master, so the script reports how much keyed area was enclosed rather than marginal. Genuine white ink would show up there as a large number instead of the 1,206 stray pixels this master has:

keyed 426,816 px, of which 1,206 enclosed (gaps in the mark, not its margin)

Edge pixels take partial alpha from their distance to white and are then un-premultiplied, so the result composites back over white exactly as the master did — and over the cream page or a dark background without a pale outline.

The CSS had to move with it

Four rules sized the logo as a square and clipped it round: fixed width/height, border-radius: 50%, a rectangular box-shadow, and object-fit: cover on the docs header. The shield is 1.36:1 and transparent, so left alone they would squash it into a circle and float a box shadow around a shield-shaped hole.

  • vgi_rpc/http/_common.py — shared 404/401 page style
  • vgi_rpc/http/server/_pages.py — landing and describe
  • docs/stylesheets/extra.css — docs hero and header nav

Each now sizes by width with the height following, and shadows with drop-shadow, which traces the alpha silhouette.

Verification

Rendered all four pages locally with the src rewritten to the working-copy asset — the templates hardcode https://vgi-rpc-python.query.farm/assets/..., which keeps serving the old mark until the docs site deploys, so a plain local server would have shown the wrong logo and proved nothing.

Filenames are unchanged, so the URLs the pages request and the three tests/test_http.py assertions still resolve. Gate: ruff, pydoclint, mypy strict, ty (3 pre-existing nacl.bindings diagnostics), 3828 passed / 163 skipped.

Independent of #36 — that PR touches the same _ERROR_PAGE_STYLE block but different rules, so they merge in either order.

🤖 Generated with Claude Code

Every brand asset carried the old circular VGI seal — including the logo the
landing, describe, 404 and 401 pages request by URL. The shield artwork was
already in the repo as logo-shield.png but only the README pointed at it, and
it sat on an opaque white background.

The master (docs/assets/logo-master.png, 1197x880) is now committed and every
other asset is cut from it by scripts/regenerate_logo_assets.py, so a future
artwork change is one command rather than eight hand edits that drift.

Keying the background is a global near-white threshold rather than a fill from
the border. Keying only what the border reaches leaves the enclosed gaps
opaque — the sky between the tree and the barn wall, the holes between the
cloud's connector traces — which read as white specks on the cream page and
glare on a dark one. The mark's lightest ink is cream at min-channel 193, far
below the threshold, so nothing real is at risk; the script reports how much
keyed area was enclosed rather than marginal, so a future master carrying
genuine white ink shows up as a large number instead of a few hundred pixels.
Edge pixels get partial alpha from their distance to white and are then
un-premultiplied, so the mark composites back over white exactly as the master
did instead of wearing a pale outline on a coloured background.

The CSS had to move with it. Four rules sized the logo as a square and clipped
it round — `width/height` both fixed, `border-radius: 50%`, a rectangular
`box-shadow`, and `object-fit: cover` on the docs header. The shield is 1.36:1
and transparent, so unchanged they would squash it into a circle and float a
box shadow around a shield-shaped hole. Each now sizes by width with the height
following, and shadows with a `drop-shadow` that traces the alpha silhouette.

Asset filenames are unchanged, so the URLs the pages request and the tests
assert on still resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 3, 2026 23:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the project’s branding across HTTP-served pages and documentation to use the shield mark (transparent, non-square aspect) instead of the legacy circular seal, and introduces a script to regenerate all derived logo assets from a single committed master.

Changes:

  • Update inline page CSS (landing/describe + shared error pages) to size the new logo by width and use filter: drop-shadow(...) instead of circular clipping + box-shadow.
  • Update MkDocs theme overrides to prevent square cropping and apply a silhouette-tracing shadow.
  • Add scripts/regenerate_logo_assets.py to regenerate all derived assets in docs/assets/ from logo-master.png.

Reviewed changes

Copilot reviewed 4 out of 12 changed files in this pull request and generated 2 comments.

File Description
vgi_rpc/http/server/_pages.py Adjust landing/describe page logo sizing and shadowing for the shield mark.
vgi_rpc/http/_common.py Update shared 401/404-style logo rules to match the new transparent, landscape logo.
docs/stylesheets/extra.css Update docs hero/header logo sizing and shadowing to avoid cropping and match new aspect ratio.
scripts/regenerate_logo_assets.py Add an asset regeneration script to derive all brand images from a single master.
Suppressed comments (1)

scripts/regenerate_logo_assets.py:112

  • _dilate() uses np.roll() for neighbourhood growth, which wraps around the array edges. That makes dilation “teleport” across left/right and top/bottom borders, which can create an edge band on the opposite side of the image. Use pad+slice (non-wrapping) shifts for the 8-neighbour expansion instead.
def _dilate(mask: np.ndarray, radius: int) -> np.ndarray:
    """Grow *mask* by *radius* pixels in 8-connectivity."""
    out = mask.copy()
    for _ in range(radius):
        grown = out.copy()
        for dy in (-1, 0, 1):
            for dx in (-1, 0, 1):
                grown |= np.roll(np.roll(out, dy, axis=0), dx, axis=1)
        out = grown
    return out

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +87 to +100
# Iterated 8-connected dilation clipped to the candidate set. Simple enough
# to trust by reading, and a few seconds on a 1200x900 master.
while True:
grown = reached.copy()
for shift, axis in ((1, 0), (-1, 0), (1, 1), (-1, 1)):
grown |= np.roll(reached, shift, axis=axis)
# Diagonals, so a 1px-wide gap between two shapes still lets the fill through.
for dy in (-1, 1):
for dx in (-1, 1):
grown |= np.roll(np.roll(reached, dy, axis=0), dx, axis=1)
grown &= candidate
if np.array_equal(grown, reached):
return reached
reached = grown
Comment on lines +234 to +235
logo = key_out_background(Image.open(_MASTER))
print(f"master {Image.open(_MASTER).size} -> keyed mark {logo.size}")
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@rustyconover
rustyconover merged commit db7ed84 into main Aug 4, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants