The site wears the product's mark
The nav and footer carried a purple gradient square — a placeholder that outlived its excuse — and the site had no favicon and no og:image at all, so a shared link rendered as a bare URL with a blank tab icon. All of it now comes from one artwork, through `Scripts/make-brand-assets.py`. 🔴 The outputs are generated: an icon edited in place is an icon that silently stops matching the next time the script runs, which is the same shape as the generated doc-page mistake this repo already made once. Two things about the source worth recording. The export carried a leftover dark matte — RGB (33,33,33) at alpha 38 across the background — invisible on a dark page and a grey haze on a light one; the script strips anything below alpha 48. And the wordmark is interlocked with the emblem: "FlameNet" is drawn ON the globe, and the speech bubbles hang down into the letter tops, so no rectangular crop gets the whole mark without text. The crop used, (280,0,1040,548), is the one that takes both bubbles and the flame arc and leaves the wordmark behind. ⚠️ `assetVersion` now hashes the brand images too. Without that a logo change alone leaves the hash where it was, so every browser that has ever loaded the site keeps serving the old mark out of cache — which looks exactly like the deploy not having happened. ⚠️ Not changed, and worth a decision separately: the site's accent is purple and the logo is blue with an orange flame. They do not clash badly at 30px, but they are not one palette either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 874 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 585 KiB |
@@ -93,10 +93,14 @@ a { color: inherit; }
|
||||
font-weight: 700; letter-spacing: -.02em; font-size: 17px;
|
||||
text-decoration: none; flex: none;
|
||||
}
|
||||
/* The product's mark, not a coloured square. The gradient block that used to
|
||||
sit here was a placeholder that outlived its excuse. */
|
||||
.mark {
|
||||
width: 30px; height: 30px; border-radius: 9px; flex: none;
|
||||
background: var(--grad);
|
||||
box-shadow: 0 4px 12px rgba(88,66,188,.34);
|
||||
/* The artwork is painted on the panel navy, so the radius has to clip it —
|
||||
without this the square corners show against the page. */
|
||||
object-fit: cover; display: block;
|
||||
box-shadow: 0 4px 12px rgba(18,26,46,.28);
|
||||
}
|
||||
|
||||
.nav-links { display: flex; align-items: center; justify-content: center; gap: 34px; flex: 1; }
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
<meta property="og:title" content="#(title)">
|
||||
<meta property="og:description" content="#(description)">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://flamenetmessenger.com/brand/og.png?v=#(assetVersion)">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<link rel="icon" href="/brand/favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/brand/favicon-32.png?v=#(assetVersion)">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/brand/favicon-16.png?v=#(assetVersion)">
|
||||
<link rel="apple-touch-icon" href="/brand/apple-touch-icon.png?v=#(assetVersion)">
|
||||
<link rel="stylesheet" href="/landing.css?v=#(assetVersion)">
|
||||
</head>
|
||||
<body>
|
||||
@@ -25,7 +31,8 @@
|
||||
<nav class="nav">
|
||||
<div class="wrap nav-inner">
|
||||
<a class="brand" href="/">
|
||||
<span class="mark" aria-hidden="true"></span>
|
||||
<img class="mark" src="/brand/mark-128.png?v=#(assetVersion)" alt="" aria-hidden="true"
|
||||
width="30" height="30">
|
||||
<span>Flamenet Messenger</span>
|
||||
</a>
|
||||
<button class="nav-toggle" aria-expanded="false" aria-controls="nav-links" aria-label="Menu">
|
||||
@@ -64,7 +71,8 @@
|
||||
|
||||
<div class="footer-cols">
|
||||
<div class="footer-brand">
|
||||
<span class="mark" aria-hidden="true"></span>
|
||||
<img class="mark" src="/brand/mark-128.png?v=#(assetVersion)" alt="" aria-hidden="true"
|
||||
width="30" height="30">
|
||||
<span>Flamenet Messenger</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Every icon and favicon, derived from one source artwork.
|
||||
|
||||
Run it, do not hand-edit the outputs — an icon edited in place is an icon that
|
||||
silently stops matching the next time this runs, which is the same shape as the
|
||||
generated doc pages this repo already got wrong once.
|
||||
|
||||
python3 Scripts/make-brand-assets.py
|
||||
|
||||
It writes into three repositories, because one logo has to be the same logo in
|
||||
all three: this repo's Public/brand, the iPhone app's asset catalogue, and the
|
||||
Android app's mipmaps. Paths below assume they are siblings under ~/Code.
|
||||
|
||||
Two rules that are not obvious and both cost a rejected build if broken:
|
||||
|
||||
- 🔴 The iOS icon must have NO alpha channel. App Store Connect rejects an icon
|
||||
that has one, and the rejection arrives after the upload rather than during
|
||||
the build.
|
||||
- 🔴 The Android foreground must sit inside the adaptive-icon safe zone (72 of
|
||||
108), or a launcher with a circular mask cuts the flame arc off the mark.
|
||||
"""
|
||||
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
HOME = os.path.expanduser("~/Code")
|
||||
SRC = f"{HOME}/flamenet-relay/brand/flamenet-messenger-logo.png"
|
||||
NAVY = (18, 26, 46) # #121A2E — the app palette's panel navy
|
||||
CROP = (280, 0, 1040, 548) # the emblem: globe, both bubbles, the flame arc
|
||||
PAD = 0.06 # breathing room so nothing sits flush to the edge
|
||||
|
||||
def master():
|
||||
"""The source with its leftover matte removed.
|
||||
|
||||
The export carried a dark grey (33,33,33) at alpha 38 across the
|
||||
background — invisible on a dark page, a grey haze on a light one.
|
||||
"""
|
||||
src = Image.open(SRC).convert("RGBA")
|
||||
r, g, b, a = src.split()
|
||||
a = a.point(lambda v: 0 if v < 48 else v)
|
||||
return Image.merge("RGBA", (r, g, b, a))
|
||||
|
||||
M = master()
|
||||
|
||||
def emblem(size, background=None, content_fraction=1.0):
|
||||
"""The square mark. `background=None` leaves it transparent."""
|
||||
c = M.crop(CROP)
|
||||
side = int(max(c.size) * (1 + PAD))
|
||||
fill = (background[0], background[1], background[2], 255) if background else (0, 0, 0, 0)
|
||||
canvas = Image.new("RGBA", (side, side), fill)
|
||||
canvas.alpha_composite(c, ((side - c.size[0]) // 2, (side - c.size[1]) // 2))
|
||||
out = canvas.resize((size, size), Image.LANCZOS)
|
||||
if content_fraction >= 1.0:
|
||||
return out
|
||||
# Shrink inside the canvas — for Android's adaptive-icon safe zone.
|
||||
inner = int(size * content_fraction)
|
||||
small = out.resize((inner, inner), Image.LANCZOS)
|
||||
framed = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
framed.alpha_composite(small, ((size - inner) // 2, (size - inner) // 2))
|
||||
return framed
|
||||
|
||||
def wordmark(width):
|
||||
c = M.crop(M.split()[3].getbbox())
|
||||
h = round(c.size[1] * width / c.size[0])
|
||||
return c.resize((width, h), Image.LANCZOS)
|
||||
|
||||
def save(img, path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
img.save(path)
|
||||
print(f"{path} {img.size[0]}x{img.size[1]} alpha={'yes' if img.mode=='RGBA' else 'no'}")
|
||||
|
||||
# ---------------------------------------------------------------- public pages
|
||||
P = f"{HOME}/flamenet-relay/Public/brand"
|
||||
save(wordmark(1100), f"{P}/logo.png")
|
||||
for s in (64, 128, 256, 512):
|
||||
save(emblem(s, NAVY), f"{P}/mark-{s}.png")
|
||||
save(emblem(180, NAVY), f"{P}/apple-touch-icon.png")
|
||||
for s in (16, 32, 48):
|
||||
save(emblem(s, NAVY), f"{P}/favicon-{s}.png")
|
||||
ico = emblem(256, NAVY).convert("RGB")
|
||||
ico.save(f"{P}/favicon.ico", sizes=[(16,16),(32,32),(48,48)])
|
||||
print(f"{P}/favicon.ico multi-size")
|
||||
|
||||
# The social card: the full logo on the panel navy, at the size every
|
||||
# platform crops from.
|
||||
card = Image.new("RGBA", (1200, 630), NAVY + (255,))
|
||||
w = wordmark(760)
|
||||
card.alpha_composite(w, ((1200 - 760) // 2, (630 - w.size[1]) // 2))
|
||||
save(card.convert("RGB"), f"{P}/og.png")
|
||||
|
||||
# ------------------------------------------------------------------------- iOS
|
||||
# 🔴 No alpha. App Store Connect rejects an icon with an alpha channel.
|
||||
ios = emblem(1024, NAVY).convert("RGB")
|
||||
save(ios, f"{HOME}/flamenetmessenger/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png")
|
||||
|
||||
# --------------------------------------------------------------------- Android
|
||||
# Adaptive foreground: transparent, content inside the 66/108 safe zone so a
|
||||
# circular or squircle mask cannot cut the mark.
|
||||
A = f"{HOME}/flamenet-messenger-android/app/src/main/res"
|
||||
for bucket, px in [("mdpi",108), ("hdpi",162), ("xhdpi",216), ("xxhdpi",324), ("xxxhdpi",432)]:
|
||||
save(emblem(px, None, content_fraction=0.72), f"{A}/mipmap-{bucket}/ic_launcher_foreground.png")
|
||||
@@ -1312,7 +1312,13 @@ final class BootTask: LifecycleHandler, @unchecked Sendable {
|
||||
/// boot from the file contents, so it changes exactly when they do.
|
||||
enum AssetVersion {
|
||||
static let value: String = {
|
||||
let paths = ["Public/landing.css", "Public/landing.js", "Public/donate.js"]
|
||||
// ⚠️ The brand images are in here too, and belong here. Without them a
|
||||
// logo change alone leaves the hash where it was, so every browser
|
||||
// that has ever loaded the site keeps serving the old mark from cache
|
||||
// — which looks exactly like the deploy not having happened.
|
||||
let paths = ["Public/landing.css", "Public/landing.js", "Public/donate.js",
|
||||
"Public/brand/mark-128.png", "Public/brand/og.png",
|
||||
"Public/brand/apple-touch-icon.png"]
|
||||
var hasher = SHA256()
|
||||
for path in paths {
|
||||
if let data = FileManager.default.contents(atPath: path) { hasher.update(data: data) }
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 MiB |