Two copies for two jobs. rippr-src/ is a browsable git archive export of the tracked tree at 2f76983 - no build outputs, no local.properties, no nested .git - which is convenient to read in gitea but carries no history and will drift. rippr-full-history.bundle is the real backup: all 18 commits, verified as "records a complete history" and test-cloned before committing. This matters because ~/dojo/rippr has no git remote and otherwise exists only on one machine. rippr-src/SNAPSHOT.md explains the difference and how to restore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
263 lines
8.5 KiB
Python
263 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Builds the logo concept review page, inlining each generated SVG."""
|
|
|
|
import pathlib
|
|
import re
|
|
|
|
import gen_logos
|
|
|
|
HERE = pathlib.Path(__file__).parent
|
|
LOGOS = HERE / "logos"
|
|
|
|
NOTES = {
|
|
"apex": (
|
|
"Strongest speed signal in the set, and the gauge reads as an instrument cluster "
|
|
"rather than a generic dial. Verified weakness: the tick marks disappear by 24px "
|
|
"and the needle loses its point, so the status-bar version needs the ticks removed."
|
|
),
|
|
"route": (
|
|
"Describes what the app records rather than what it rides on — the most specific "
|
|
"idea of the three. The accent segment reads as recording progress. "
|
|
"Ambiguous at 24px — the trace could be almost anything."
|
|
),
|
|
"heading": (
|
|
"Cleanest silhouette of the outlined concepts and the second-best small-size "
|
|
"performer. The accent arc marks where the arrow points, so ring and arrow read as "
|
|
"one object, and it reduces to a single flat color more cleanly than the other two — which makes it the natural pick for the status-bar icon."
|
|
),
|
|
}
|
|
|
|
# The status-bar icon must be a flat single-color silhouette; Android discards
|
|
# color and alpha nuance. Worth previewing since it rules some concepts out.
|
|
MONO_CSS = """
|
|
.mono svg *, .mono svg { fill: #fff !important; stroke: #fff !important; }
|
|
"""
|
|
|
|
|
|
def inner_svg(key: str) -> str:
|
|
raw = (LOGOS / f"rippr-{key}.svg").read_text()
|
|
return re.sub(r"^<svg[^>]*>|</svg>$", "", raw).strip()
|
|
|
|
|
|
def card(letter: str, key: str, name: str, desc: str) -> str:
|
|
body = inner_svg(key)
|
|
|
|
def mark(size, extra_class="", guide=False):
|
|
g = '<span class="safe"></span>' if guide else ""
|
|
return (
|
|
f'<span class="mark {extra_class}" style="--s:{size}px">'
|
|
f'<svg viewBox="0 0 108 108" width="{size}" height="{size}" '
|
|
f'aria-hidden="true">{body}</svg>{g}</span>'
|
|
)
|
|
|
|
sizes = "".join(
|
|
f'<div class="spec"><div class="tile sm">{mark(n)}</div>'
|
|
f'<span class="cap">{n}px</span></div>'
|
|
for n in (48, 32, 24)
|
|
)
|
|
|
|
return f"""
|
|
<article class="card">
|
|
<header class="card-hd">
|
|
<span class="letter">{letter}</span>
|
|
<div>
|
|
<h2>{name}</h2>
|
|
<p class="desc">{desc}</p>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="stage">{mark(168, guide=True)}</div>
|
|
|
|
<div class="specs">
|
|
<div class="spec">
|
|
<div class="tile mask-circle">{mark(72)}</div>
|
|
<span class="cap">circle mask</span>
|
|
</div>
|
|
<div class="spec">
|
|
<div class="tile mask-squircle">{mark(72)}</div>
|
|
<span class="cap">squircle</span>
|
|
</div>
|
|
<div class="spec">
|
|
<div class="tile sm mono">{mark(24)}</div>
|
|
<span class="cap">status bar</span>
|
|
</div>
|
|
{sizes}
|
|
</div>
|
|
|
|
<p class="note">{NOTES[key]}</p>
|
|
</article>"""
|
|
|
|
|
|
def main():
|
|
gen_logos.main()
|
|
cards = "".join(
|
|
card(chr(65 + i), key, name, desc)
|
|
for i, (key, (name, desc, _)) in enumerate(gen_logos.CONCEPTS.items())
|
|
)
|
|
html = TEMPLATE.replace("{{CARDS}}", cards).replace("{{MONO_CSS}}", MONO_CSS)
|
|
out = HERE / "logo-review.html"
|
|
out.write_text(html)
|
|
print(f"wrote {out}")
|
|
|
|
|
|
TEMPLATE = """<title>Rippr — logo concepts</title>
|
|
<style>
|
|
:root {
|
|
--ground: #F2F3F5;
|
|
--surface: #FFFFFF;
|
|
--line: #D8DCE1;
|
|
--ink: #161A1F;
|
|
--ink-dim: #5C6874;
|
|
--accent: #E8480F;
|
|
--tile: #101418;
|
|
--tile-line: #2A323B;
|
|
--radius: 14px;
|
|
}
|
|
:root:not([data-theme="light"]) { color-scheme: light dark; }
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--ground: #0C0F13;
|
|
--surface: #151A20;
|
|
--line: #262E37;
|
|
--ink: #E8EDF2;
|
|
--ink-dim: #8B98A6;
|
|
--accent: #FF5722;
|
|
--tile: #06080A;
|
|
--tile-line: #222A33;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--ground: #0C0F13;
|
|
--surface: #151A20;
|
|
--line: #262E37;
|
|
--ink: #E8EDF2;
|
|
--ink-dim: #8B98A6;
|
|
--accent: #FF5722;
|
|
--tile: #06080A;
|
|
--tile-line: #222A33;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--ground);
|
|
color: var(--ink);
|
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
line-height: 1.55;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
.wrap { max-width: 1080px; margin: 0 auto; padding: 56px 24px 80px; }
|
|
|
|
.eyebrow {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
|
|
color: var(--accent); margin: 0 0 12px;
|
|
}
|
|
h1 {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: clamp(30px, 5vw, 46px); font-weight: 700; letter-spacing: -.02em;
|
|
margin: 0 0 14px; text-wrap: balance;
|
|
}
|
|
.lede { max-width: 62ch; color: var(--ink-dim); font-size: 17px; margin: 0; }
|
|
.lede strong { color: var(--ink); font-weight: 600; }
|
|
|
|
.grid { display: grid; gap: 22px; margin-top: 44px; }
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--line);
|
|
border-radius: var(--radius);
|
|
padding: 22px;
|
|
display: grid; gap: 18px;
|
|
}
|
|
.card-hd { display: flex; gap: 14px; align-items: flex-start; }
|
|
.letter {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 12px; font-weight: 700; color: var(--accent);
|
|
border: 1px solid var(--accent); border-radius: 6px;
|
|
width: 26px; height: 26px; display: grid; place-items: center; flex: none;
|
|
}
|
|
.card h2 { font-size: 19px; margin: 0; letter-spacing: -.01em; }
|
|
.desc { margin: 2px 0 0; color: var(--ink-dim); font-size: 14px; }
|
|
|
|
.stage {
|
|
background: var(--tile);
|
|
border: 1px solid var(--tile-line);
|
|
border-radius: 10px;
|
|
display: grid; place-items: center;
|
|
padding: 30px 0;
|
|
}
|
|
.mark { position: relative; display: grid; place-items: center; }
|
|
.mark svg { display: block; }
|
|
/* Adaptive-icon safe zone: content outside the centre 66/108 circle can be cropped. */
|
|
.safe {
|
|
position: absolute; inset: 0; margin: auto;
|
|
width: calc(var(--s) * 0.611); height: calc(var(--s) * 0.611);
|
|
border: 1px dashed var(--tile-line); border-radius: 50%;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.specs { display: flex; flex-wrap: wrap; gap: 14px; }
|
|
.spec { display: grid; gap: 6px; justify-items: center; }
|
|
.tile {
|
|
background: var(--tile); border: 1px solid var(--tile-line);
|
|
width: 88px; height: 88px; display: grid; place-items: center; border-radius: 10px;
|
|
}
|
|
.tile.sm { width: 64px; height: 64px; }
|
|
.mask-circle { border-radius: 50%; overflow: hidden; }
|
|
.mask-squircle { border-radius: 26%; overflow: hidden; }
|
|
.cap {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
|
|
color: var(--ink-dim); font-variant-numeric: tabular-nums;
|
|
}
|
|
{{MONO_CSS}}
|
|
|
|
.note {
|
|
margin: 0; font-size: 14.5px; color: var(--ink-dim);
|
|
border-left: 2px solid var(--line); padding-left: 14px; max-width: 68ch;
|
|
}
|
|
|
|
.foot {
|
|
margin-top: 44px; padding-top: 24px; border-top: 1px solid var(--line);
|
|
color: var(--ink-dim); font-size: 14.5px; max-width: 68ch;
|
|
}
|
|
.foot h3 { color: var(--ink); font-size: 15px; margin: 0 0 8px; }
|
|
.foot code {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 12.5px; background: var(--ground); border: 1px solid var(--line);
|
|
padding: 1px 5px; border-radius: 4px; color: var(--ink);
|
|
}
|
|
@media (min-width: 760px) { .grid { grid-template-columns: repeat(2, 1fr); } }
|
|
</style>
|
|
|
|
<div class="wrap">
|
|
<p class="eyebrow">Rippr · icon direction</p>
|
|
<h1>Three marks for a ride recorder</h1>
|
|
<p class="lede">
|
|
Each concept is drawn on Android's <strong>108-unit adaptive-icon canvas</strong>, so
|
|
whichever you pick converts straight to a vector drawable with no redrawing. The dashed
|
|
circle marks the safe zone — anything outside it can be cropped by an OEM mask. The
|
|
small tiles are the real test: a launcher icon is seen at 48px and the status-bar icon
|
|
is a flat 24px silhouette.
|
|
</p>
|
|
|
|
<div class="grid">{{CARDS}}</div>
|
|
|
|
<div class="foot">
|
|
<h3>What happens after you pick one</h3>
|
|
<p>
|
|
The chosen mark becomes <code>ic_launcher_foreground.xml</code> as a vector drawable,
|
|
paired with <code>#101418</code> as the adaptive background, replacing the placeholder
|
|
compass. Its monochrome variant replaces
|
|
<code>android.R.drawable.ic_menu_compass</code> in the recording notification. Source
|
|
SVGs live in <code>design/logos/</code>; regenerate with
|
|
<code>python3 design/gen_logos.py</code>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
if __name__ == "__main__":
|
|
main()
|