Skip to content

Commit 1af0011

Browse files
committed
fix(web,api): trace the mark from the official export instead of the brand board
The board was the wrong source and the method was wrong for this shape. `Brand.png` is a picture of the logo: a dark panel, a glow around the mark, and a resolution chosen for a contact sheet. Tracing it meant colour-thresholding lime out of that and hoping the halo fell outside the threshold. `Logo SENT.png` is the artwork — transparent background, no glow, 1254px. The mask is "alpha > 127", which has nothing to tune and no halo to exclude. THE ROUNDING WAS DONE TWICE The previous trace eroded the mask, kept a small set of vertices, and told the consumer to stroke them with a round join. That is the right trick when a shape has SHARP corners in the source and you want them rounded. This one's are already rounded, so eroding and re-stroking rounded them again — and spent two dozen vertices retracing curves that were already in the file, which is what should have given it away. Tracing the outline as it stands reproduces the artwork including corners that differ from one another. Thirty vertices per form, simplification error near 0.05 viewBox units: a twentieth of a pixel at 32px, under one on a 512px icon. No stroke, no radius constant, nothing to keep in step. The script now prefers the official export and falls back to the board, and its tolerance is a fraction of the artwork's extent so a larger export does not produce proportionally more vertices. Verified by rendering the emitted paths and comparing against the export, then in the app and on the share card.
1 parent 805eccb commit 1af0011

4 files changed

Lines changed: 114 additions & 96 deletions

File tree

Logo SENT.png

289 KB
Loading

apps/web/src/components/Logo.tsx

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,26 @@
33
*
44
* WHERE THIS GEOMETRY COMES FROM
55
* ------------------------------
6-
* `Brand.png`, in the repository root, measured. The previous version of this
7-
* file was drawn by eye from that board and said so; it was three flat slabs and
8-
* the real mark is two stepped diagonal forms with 180° rotational symmetry, so
9-
* it did not look like the brand on any page that used it — which is every page,
10-
* plus the favicon.
6+
* `Logo SENT.png`, the official export, measured. Not drawn by eye — an earlier
7+
* version of this file was, and it was three flat slabs where the mark is two
8+
* stepped diagonal forms, on every page and in the favicon.
119
*
12-
* The board was there the whole time. The shape is a measurement rather than an
13-
* opinion: the volt-lime pixels were masked, the two components traced with a
14-
* Moore boundary walk, and each contour reduced to the handful of vertices it
15-
* actually has. Re-derive with `scripts/trace-mark.py` if the board changes.
10+
* `scripts/trace-mark.py` masks the artwork's alpha channel, walks each form's
11+
* boundary and reduces it to the vertices it actually has. It prints exactly the
12+
* constants below; run it when the artwork changes.
1613
*
17-
* WHY THE CORNERS ARE A STROKE AND NOT ARCS
18-
* -----------------------------------------
19-
* Every corner on the mark is rounded by the same radius. Writing that as arc
20-
* segments would mean twelve hand-tuned curves per path and a shape nobody can
21-
* safely edit afterwards.
14+
* WHY THESE ARE PLAIN FILLED POLYGONS
15+
* -----------------------------------
16+
* A version before this traced an eroded mask and stroked it with a round join,
17+
* which is how you round the corners of a shape whose corners are sharp in the
18+
* source. This one's are not — the export already has its rounding — so that
19+
* rounded everything twice and spent two dozen vertices retracing curves that
20+
* were already there.
2221
*
23-
* Instead each path is the outline INSET by the corner radius, stroked with
24-
* twice that radius and a round line join. The stroke grows the shape back to
25-
* its true size with every corner rounded uniformly and exactly. It is why the
26-
* polygons below are traced from an eroded mask rather than the raw one.
22+
* Tracing the outline as it stands reproduces the artwork, including corners
23+
* that differ from one another. Roughly thirty vertices per form, with a
24+
* simplification error near 0.05 viewBox units: a twentieth of a pixel at 32px,
25+
* under one pixel on a 512px app icon.
2726
*
2827
* The mark is never re-coloured beyond the LOCKED palette and never stretched:
2928
* `preserveAspectRatio` is left at its default for exactly that reason.
@@ -32,17 +31,17 @@
3231
import type { JSX } from "react";
3332

3433
/**
35-
* The two forms, inset by {@link CORNER}, in a 32×32 box.
34+
* The two forms, in a 32×32 box, traced from the official export.
3635
*
37-
* Traced from `Brand.png`. The pair reads as an S at a glance and as two
38-
* offset slabs up close, which is what the board's "clear space and grid" panel
39-
* is protecting — hence the margin baked into these coordinates.
36+
* The pair reads as an S at a glance and as two offset slabs up close, which is
37+
* what the board's clear-space panel is protecting — hence the margin baked
38+
* into these coordinates.
4039
*/
41-
const UPPER = "M 14.16 1.96 L 20.23 8.34 L 22.78 8.82 L 28.37 8.66 L 29.96 10.41 L 29.96 16.64 L 28.85 17.6 L 14.8 8.5 L 14.32 7.38 L 14.16 2.12 Z";
42-
const LOWER = "M 3.15 13.29 L 11.29 20.31 L 18.95 24.3 L 19.59 25.89 L 19.59 30.04 L 12.09 22.38 L 4.27 22.38 L 2.99 21.9 L 2.2 20.63 L 2.04 18.55 L 2.04 15.04 L 2.99 13.45 Z";
40+
const UPPER =
41+
"M 13.94 1.0 L 14.63 1.03 L 15.32 1.38 L 22.43 8.53 L 22.78 8.77 L 23.2 8.98 L 23.96 9.18 L 29.37 9.22 L 30.06 9.5 L 30.48 9.84 L 30.86 10.47 L 31.0 11.09 L 31.0 17.99 L 30.86 18.69 L 30.48 19.35 L 29.86 19.87 L 29.06 20.14 L 27.5 20.14 L 26.7 19.87 L 20.28 15.25 L 13.83 10.88 L 13.28 10.4 L 12.83 9.71 L 12.65 9.18 L 12.58 8.7 L 12.58 2.42 L 12.72 1.87 L 12.93 1.55 L 13.35 1.21 L 13.9 1.03 Z";
4342

44-
/** Corner radius in viewBox units, from the board. */
45-
const CORNER = 0.957;
43+
const LOWER =
44+
"M 3.08 11.92 L 4.68 11.96 L 5.13 12.1 L 5.61 12.38 L 11.96 18.24 L 18.72 22.29 L 19.38 22.85 L 19.8 23.47 L 20.04 24.41 L 20.04 29.72 L 19.94 30.17 L 19.76 30.48 L 19.31 30.86 L 18.86 31.0 L 18.27 30.93 L 17.96 30.76 L 17.27 30.03 L 11.58 23.4 L 11.09 22.95 L 10.36 22.54 L 9.53 22.33 L 2.8 22.33 L 1.97 22.05 L 1.49 21.64 L 1.14 21.05 L 1.0 20.42 L 1.03 13.8 L 1.31 13.03 L 1.66 12.58 L 2.21 12.17 L 2.7 11.99 L 3.05 11.96 Z";
4645

4746
export interface LogoProps {
4847
readonly size?: number;
@@ -71,13 +70,7 @@ export function Logo({
7170
aria-label={variant === "full" ? undefined : "SENT"}
7271
style={glow ? { filter: "drop-shadow(0 0 8px rgba(198, 246, 0, 0.5))" } : undefined}
7372
>
74-
<g
75-
fill="currentColor"
76-
stroke="currentColor"
77-
strokeWidth={CORNER * 2}
78-
strokeLinejoin="round"
79-
strokeLinecap="round"
80-
>
73+
<g fill="currentColor">
8174
<path d={UPPER} />
8275
<path d={LOWER} />
8376
</g>

scripts/trace-mark.py

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,93 @@
11
#!/usr/bin/env python3
22
"""
3-
Derive the SENT mark's geometry from `Brand.png`.
3+
Derive the SENT mark's geometry from the official artwork.
44
55
WHY THIS EXISTS
66
---------------
7-
The mark in `apps/web/src/components/Logo.tsx` was drawn by eye from the brand
8-
board and did not match it — three flat slabs where the real mark is two stepped
9-
diagonal forms. The board was in the repository the whole time, so the shape can
10-
be measured instead of remembered, and this is the measurement.
7+
The mark in `apps/web/src/components/Logo.tsx` was once drawn by eye and did not
8+
match — three flat slabs where the real mark is two stepped diagonal forms. The
9+
artwork is in the repository, so the shape can be measured instead of
10+
remembered, and this is the measurement.
1111
12-
Run it when `Brand.png` changes. It prints the two paths and the corner radius
13-
in the same form `Logo.tsx` holds them.
12+
Run it when the artwork changes. It prints the two paths in the same form
13+
`Logo.tsx` and `services/api/src/preview.ts` hold them.
1414
1515
pip install pillow numpy scipy
1616
python scripts/trace-mark.py
1717
18-
WHAT IT PRODUCES, AND WHY IT IS SHAPED THAT WAY
19-
-----------------------------------------------
20-
Every corner on the mark is rounded by one radius. Emitting that as arc segments
21-
would be a dozen hand-tuned curves per path that nobody could safely edit later.
22-
23-
So the polygons printed here are the outline INSET by that radius: the consumer
24-
strokes them with twice the radius and a round line join, which grows the shape
25-
back to true size with every corner rounded exactly. That is why the trace runs
26-
against an ERODED mask rather than the raw one.
27-
28-
The contour walk is Moore neighbour tracing. An earlier attempt ordered boundary
29-
pixels by angle around the centroid, which is only valid for a star-shaped
30-
region; this mark is a concave step, so that interleaved the two sides of the
31-
notch and produced a zigzag.
18+
SOURCE, IN ORDER OF PREFERENCE
19+
------------------------------
20+
`Logo SENT.png` is the official export: transparent background, no glow, no
21+
surrounding board. The mask is then simply "alpha > 0", which has no threshold
22+
to tune and no halo to exclude.
23+
24+
`Brand.png` is the brand board and the fallback. Tracing from it means colour-
25+
thresholding the volt lime out of a dark panel with a glow around it, which
26+
works but is measuring a picture of the logo rather than the logo.
27+
28+
WHAT IT PRODUCES
29+
----------------
30+
Two closed polygons, filled. Nothing else — no stroke, no arcs.
31+
32+
An earlier version traced an ERODED mask and told the consumer to stroke it with
33+
a round join, on the theory that this rounds every corner from a small set of
34+
sharp vertices. That is the right trick for a shape whose corners are sharp in
35+
the source. This one's are not: the official artwork already has its rounding,
36+
so eroding and re-stroking rounded it twice and spent two dozen vertices
37+
retracing curves that were already there.
38+
39+
Tracing the outline as it is reproduces the artwork exactly, including corners
40+
that differ from each other. It costs about thirty vertices per form and a
41+
simplification error of roughly 0.05 viewBox units — under a tenth of a pixel at
42+
the sizes the mark is drawn, and under a pixel on a 512px app icon.
43+
44+
The contour walk is Moore neighbour tracing. An attempt before that ordered
45+
boundary pixels by angle around the centroid, which is only valid for a
46+
star-shaped region; this mark is a concave step, so it interleaved the two sides
47+
of the notch and produced a zigzag.
3248
"""
3349

3450
import json
51+
import os
3552
import sys
3653

3754
import numpy as np
3855
from PIL import Image
3956
from scipy import ndimage
4057

58+
OFFICIAL = "Logo SENT.png"
4159
BOARD = "Brand.png"
4260

43-
# Corner radius in source pixels, and the breathing room the board's clear-space
44-
# panel asks for, in viewBox units.
45-
RADIUS_PX = 6
61+
# Breathing room the brand board's clear-space panel asks for, in viewBox units.
4662
MARGIN = 1.0
4763
VIEWBOX = 32.0
4864

65+
# Simplification tolerance as a FRACTION of the mark's largest dimension, so a
66+
# 4k export does not produce four times the vertices of a 1k one.
67+
EPS_FRACTION = 0.0015
4968

50-
def load_mask() -> np.ndarray:
51-
"""Volt-lime pixels of the hero mark, as a filled binary mask."""
52-
a = np.asarray(Image.open(BOARD).convert("RGB")).astype(np.int16)
53-
h, w = a.shape[:2]
54-
55-
# The hero panel, top left. Generous — the colour mask does the precision.
56-
crop = a[int(h * 0.03) : int(h * 0.30), int(w * 0.10) : int(w * 0.30)]
5769

58-
r, g, b = crop[..., 0], crop[..., 1], crop[..., 2]
59-
# Volt lime is #C6F600: high green, near-zero blue, and a wide green-blue gap
60-
# that the glow halo around the mark does not have.
61-
mask = (g > 150) & (b < 120) & (g - b > 90)
70+
def load_mask() -> np.ndarray:
71+
"""The mark as a filled binary mask, from the cleanest source available."""
72+
if os.path.exists(OFFICIAL):
73+
im = Image.open(OFFICIAL).convert("RGBA")
74+
alpha = np.asarray(im)[..., 3]
75+
# Transparent background: no threshold to tune, no glow to exclude.
76+
# Half-opaque is the antialiased edge, so that is the boundary.
77+
mask = alpha > 127
78+
print(f"source: {OFFICIAL} (alpha)", file=sys.stderr)
79+
else:
80+
a = np.asarray(Image.open(BOARD).convert("RGB")).astype(np.int16)
81+
h, w = a.shape[:2]
82+
# The hero panel, top left. Generous — the colour mask does the work.
83+
crop = a[int(h * 0.03) : int(h * 0.30), int(w * 0.10) : int(w * 0.30)]
84+
r, g, b = crop[..., 0], crop[..., 1], crop[..., 2]
85+
# Volt lime is #C6F600: high green, near-zero blue, and a green-blue gap
86+
# the glow halo does not have.
87+
mask = (g > 150) & (b < 120) & (g - b > 90)
88+
mask = ndimage.binary_opening(mask, np.ones((3, 3)))
89+
print(f"source: {BOARD} (colour)", file=sys.stderr)
6290

63-
mask = ndimage.binary_opening(mask, np.ones((3, 3)))
6491
return ndimage.binary_fill_holes(mask)
6592

6693

@@ -76,7 +103,7 @@ def moore(m: np.ndarray) -> np.ndarray:
76103
contour = [start]
77104
cur, back = start, 0
78105

79-
for _ in range(200_000):
106+
for _ in range(2_000_000):
80107
for k in range(8):
81108
i = (back + 1 + k) % 8
82109
cand = (cur[0] + nbr[i][0], cur[1] + nbr[i][1])
@@ -118,48 +145,49 @@ def main() -> int:
118145
mask = load_mask()
119146
lab, n = ndimage.label(mask)
120147
if n < 2:
121-
print(f"expected two components in the mark, found {n}", file=sys.stderr)
148+
print(f"expected two forms in the mark, found {n}", file=sys.stderr)
122149
return 1
123150

124151
sizes = ndimage.sum(mask, lab, range(1, n + 1))
125152
keep = [int(i) + 1 for i in np.argsort(sizes)[::-1][:2]]
126153

154+
ys, xs = np.nonzero(mask)
155+
extent = max(xs.max() - xs.min(), ys.max() - ys.min())
156+
eps = max(1.0, extent * EPS_FRACTION)
157+
127158
polys = []
128159
for comp in keep:
129-
inset = ndimage.binary_erosion(lab == comp, np.ones((RADIUS_PX * 2 + 1,) * 2))
130-
p = rdp(moore(inset), eps=2.2)
160+
p = rdp(moore(lab == comp), eps=eps)
131161
if np.allclose(p[0], p[-1]):
132162
p = p[:-1]
133163
polys.append(p)
134164

135165
# Upper form first, so the emitted order matches how the mark reads.
136166
polys.sort(key=lambda p: p[:, 1].min())
137167

138-
# The DRAWN extent is the polygon grown by the stroke radius, so the fit has
139-
# to account for it or the stroke overflows the viewBox.
140168
allp = np.vstack(polys)
141-
lo = allp.min(axis=0) - RADIUS_PX
142-
hi = allp.max(axis=0) + RADIUS_PX
169+
lo, hi = allp.min(axis=0), allp.max(axis=0)
143170
span = float((hi - lo).max())
144171

145172
scale = (VIEWBOX - 2 * MARGIN) / span
146173
offset = (VIEWBOX - (hi - lo) * scale) / 2.0
147174

148175
out = [(p - lo) * scale + offset for p in polys]
149-
radius = RADIUS_PX * scale
150176

151-
print(f"const CORNER = {radius:.3f};\n")
177+
print(
178+
f"// {extent}px extent, eps {eps:.1f}px, "
179+
f"{len(out[0])}/{len(out[1])} vertices, "
180+
f"~{eps * scale:.3f} viewBox units of error",
181+
file=sys.stderr,
182+
)
152183
for name, p in zip(("UPPER", "LOWER"), out):
153184
d = "M " + " L ".join(f"{x:.2f} {y:.2f}" for x, y in p) + " Z"
154-
print(f'const {name} = "{d}";')
185+
print(f'const {name} =\n "{d}";')
155186

156187
print("\n// JSON, for any other consumer:")
157188
print(
158189
json.dumps(
159-
{
160-
"radius": round(radius, 3),
161-
"paths": [[[round(float(x), 2), round(float(y), 2)] for x, y in p] for p in out],
162-
}
190+
{"paths": [[[round(float(x), 2), round(float(y), 2)] for x, y in p] for p in out]}
163191
)
164192
)
165193
return 0

services/api/src/preview.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,17 @@ const WARN = "#fbbf24";
7474
* diagonal forms, so the artefact that travels furthest carried a glyph that is
7575
* not the logo — on every link anyone ever posted.
7676
*
77-
* Measured from `Brand.png` by `scripts/trace-mark.py`, which prints exactly
78-
* these constants; `apps/web/src/components/Logo.tsx` holds the same values for
79-
* the same reason the colours are repeated here. An SVG served to a crawler has
80-
* no stylesheet and no imports — it has to be self-contained, and the price of
81-
* that is a copy. The script is the source; both copies are its output.
82-
*
83-
* The paths are the outline INSET by the corner radius, stroked with twice it
84-
* and a round join, which rounds every corner uniformly without a dozen arcs.
77+
* Measured from the official export by `scripts/trace-mark.py`, which prints
78+
* exactly these constants; `apps/web/src/components/Logo.tsx` holds the same
79+
* values for the same reason the colours are repeated here. An SVG served to a
80+
* crawler has no stylesheet and no imports — it has to be self-contained, and
81+
* the price of that is a copy. The script is the source; both copies are its
82+
* output.
8583
*/
8684
const MARK_UPPER =
87-
"M 14.16 1.96 L 20.23 8.34 L 22.78 8.82 L 28.37 8.66 L 29.96 10.41 L 29.96 16.64 L 28.85 17.60 L 14.80 8.50 L 14.32 7.38 L 14.16 2.12 Z";
85+
"M 13.94 1.0 L 14.63 1.03 L 15.32 1.38 L 22.43 8.53 L 22.78 8.77 L 23.2 8.98 L 23.96 9.18 L 29.37 9.22 L 30.06 9.5 L 30.48 9.84 L 30.86 10.47 L 31.0 11.09 L 31.0 17.99 L 30.86 18.69 L 30.48 19.35 L 29.86 19.87 L 29.06 20.14 L 27.5 20.14 L 26.7 19.87 L 20.28 15.25 L 13.83 10.88 L 13.28 10.4 L 12.83 9.71 L 12.65 9.18 L 12.58 8.7 L 12.58 2.42 L 12.72 1.87 L 12.93 1.55 L 13.35 1.21 L 13.9 1.03 Z";
8886
const MARK_LOWER =
89-
"M 3.15 13.29 L 11.29 20.31 L 18.95 24.30 L 19.59 25.89 L 19.59 30.04 L 12.09 22.38 L 4.27 22.38 L 2.99 21.90 L 2.20 20.63 L 2.04 18.55 L 2.04 15.04 L 2.99 13.45 Z";
90-
const MARK_CORNER = 0.957;
87+
"M 3.08 11.92 L 4.68 11.96 L 5.13 12.1 L 5.61 12.38 L 11.96 18.24 L 18.72 22.29 L 19.38 22.85 L 19.8 23.47 L 20.04 24.41 L 20.04 29.72 L 19.94 30.17 L 19.76 30.48 L 19.31 30.86 L 18.86 31.0 L 18.27 30.93 L 17.96 30.76 L 17.27 30.03 L 11.58 23.4 L 11.09 22.95 L 10.36 22.54 L 9.53 22.33 L 2.8 22.33 L 1.97 22.05 L 1.49 21.64 L 1.14 21.05 L 1.0 20.42 L 1.03 13.8 L 1.31 13.03 L 1.66 12.58 L 2.21 12.17 L 2.7 11.99 L 3.05 11.96 Z";
9188
/** The mark is authored in a 32-unit box; the card wants it at 44. */
9289
const MARK_SCALE = 44 / 32;
9390

@@ -216,7 +213,7 @@ export function renderPreview(m: PreviewMarket): string {
216213
217214
<!-- Brand mark. §231: the platform is named on anything that travels. -->
218215
<g transform="translate(80 66)">
219-
<g transform="scale(${MARK_SCALE})" fill="${VOLT}" stroke="${VOLT}" stroke-width="${MARK_CORNER * 2}" stroke-linejoin="round" stroke-linecap="round">
216+
<g transform="scale(${MARK_SCALE})" fill="${VOLT}">
220217
<path d="${MARK_UPPER}" />
221218
<path d="${MARK_LOWER}" />
222219
</g>

0 commit comments

Comments
 (0)