Add Rippr source snapshot and full-history bundle
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>
262
rippr-src/design/build_preview.py
Normal file
@@ -0,0 +1,262 @@
|
||||
#!/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()
|
||||
187
rippr-src/design/gen_logos.py
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generates the Rippr logo concept set as standalone SVGs, then rasterises them.
|
||||
|
||||
Everything is authored on Android's adaptive-icon canvas (108x108) with the key
|
||||
shapes kept inside the 66-unit safe circle centred at (54,54), so a chosen concept
|
||||
converts to res/drawable/ic_launcher_foreground.xml without re-drawing anything.
|
||||
|
||||
The figurative geometry is Google's Material Symbols (Apache-2.0, vendored under
|
||||
design/material/) rather than hand-approximated curves. Only the framing — gauge
|
||||
arcs, badge grounds, rings — is authored here, because arcs and circles are the
|
||||
shapes that can be computed exactly instead of eyeballed.
|
||||
"""
|
||||
|
||||
import math
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
OUT = HERE / "logos"
|
||||
GLYPHS = HERE / "material"
|
||||
PNG = HERE / "preview"
|
||||
|
||||
CX = CY = 54.0
|
||||
|
||||
INK = "#F2F5F8"
|
||||
ACCENT = "#FF5722"
|
||||
DEEP = "#101418"
|
||||
DIM = "#66727E"
|
||||
|
||||
# Material glyphs are drawn in a 960 box spanning y = -960..0.
|
||||
GLYPH_BOX = 960.0
|
||||
|
||||
|
||||
def glyph_path(name: str) -> str:
|
||||
"""Extract the single path's `d` attribute from a vendored Material Symbol."""
|
||||
svg = (GLYPHS / f"{name}.svg").read_text()
|
||||
m = re.search(r'<path[^>]*\sd="([^"]+)"', svg)
|
||||
if not m:
|
||||
raise ValueError(f"no path found in {name}.svg")
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def glyph(name: str, size: float, cx: float = CX, cy: float = CY, fill: str = INK,
|
||||
rotate: float = 0.0) -> str:
|
||||
"""Place a Material glyph centred on (cx, cy), scaled to `size` units square."""
|
||||
s = size / GLYPH_BOX
|
||||
# Map x: 0..960 -> centred; y: -960..0 -> centred.
|
||||
tx = cx - size / 2
|
||||
ty = cy + size / 2
|
||||
rot = f' transform="rotate({rotate} {cx} {cy})"' if rotate else ""
|
||||
return (
|
||||
f'<g{rot}><path d="{glyph_path(name)}" fill="{fill}" '
|
||||
f'transform="translate({tx:.3f},{ty:.3f}) scale({s:.6f})"/></g>'
|
||||
)
|
||||
|
||||
|
||||
def polar(cx, cy, r, deg):
|
||||
a = math.radians(deg)
|
||||
return cx + r * math.cos(a), cy - r * math.sin(a)
|
||||
|
||||
|
||||
def arc(cx, cy, r, start_deg, end_deg):
|
||||
x0, y0 = polar(cx, cy, r, start_deg)
|
||||
x1, y1 = polar(cx, cy, r, end_deg)
|
||||
large = 1 if abs(end_deg - start_deg) > 180 else 0
|
||||
sweep = 0 if end_deg > start_deg else 1
|
||||
return f"M{x0:.3f},{y0:.3f} A{r:.3f},{r:.3f} 0 {large} {sweep} {x1:.3f},{y1:.3f}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# A — Apex. Material's `speed` gauge inside a computed tachometer sweep, with the
|
||||
# last quarter of the sweep in accent as a redline.
|
||||
# --------------------------------------------------------------------------
|
||||
def concept_apex():
|
||||
parts = [
|
||||
f'<path d="{arc(CX, CY, 44, 215, -35)}" fill="none" stroke="{DIM}" '
|
||||
f'stroke-width="5" stroke-linecap="round" opacity="0.55"/>',
|
||||
f'<path d="{arc(CX, CY, 44, 25, -35)}" fill="none" stroke="{ACCENT}" '
|
||||
f'stroke-width="5" stroke-linecap="round"/>',
|
||||
]
|
||||
for i in range(11):
|
||||
deg = 215 - i * 25
|
||||
major = i % 5 == 0
|
||||
r0 = 34 if major else 37
|
||||
x0, y0 = polar(CX, CY, r0, deg)
|
||||
x1, y1 = polar(CX, CY, 39.5, deg)
|
||||
parts.append(
|
||||
f'<line x1="{x0:.2f}" y1="{y0:.2f}" x2="{x1:.2f}" y2="{y1:.2f}" '
|
||||
f'stroke="{INK}" stroke-width="{2.6 if major else 1.6}" '
|
||||
f'stroke-linecap="round" opacity="{0.85 if major else 0.45}"/>'
|
||||
)
|
||||
parts.append(glyph("speed", 46))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D — Route. Material's `route` glyph ringed by a thin border, reading as a map
|
||||
# trace rather than an instrument.
|
||||
# --------------------------------------------------------------------------
|
||||
def concept_route():
|
||||
return (
|
||||
f'<circle cx="{CX}" cy="{CY}" r="43" fill="none" stroke="{DIM}" '
|
||||
f'stroke-width="3.5" opacity="0.6"/>'
|
||||
+ glyph("route", 54, fill=INK)
|
||||
+ f'<circle cx="{CX}" cy="{CY}" r="43" fill="none" stroke="{ACCENT}" '
|
||||
f'stroke-width="3.5" stroke-linecap="round" '
|
||||
f'stroke-dasharray="34 236" stroke-dashoffset="-100"/>'
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# E — Heading. `navigation` is the cleanest silhouette Material offers and the
|
||||
# only one that stays unambiguous as a flat 24px status-bar icon.
|
||||
# --------------------------------------------------------------------------
|
||||
def concept_heading():
|
||||
heading = 20 # degrees clockwise from north
|
||||
# The accent segment marks where the arrow points, so the two read as one object
|
||||
# rather than a ring with an arrow dropped inside it.
|
||||
centre = 90 - heading
|
||||
parts = [
|
||||
f'<circle cx="{CX}" cy="{CY}" r="42" fill="none" stroke="{DIM}" '
|
||||
f'stroke-width="2.5" opacity="0.7"/>',
|
||||
f'<path d="{arc(CX, CY, 42, centre + 26, centre - 26)}" fill="none" '
|
||||
f'stroke="{ACCENT}" stroke-width="5.5" stroke-linecap="round"/>',
|
||||
# The glyph's ink sits below its box centre, so nudge up to centre it optically.
|
||||
glyph("navigation", 48, cy=CY - 2, rotate=heading),
|
||||
]
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
CONCEPTS = {
|
||||
"apex": ("Apex", "Tachometer sweep with redline", concept_apex),
|
||||
"route": ("Route", "Recorded trace inside a ring", concept_route),
|
||||
"heading": ("Heading", "Navigation arrow with heading arc", concept_heading),
|
||||
}
|
||||
|
||||
|
||||
def svg_doc(body, bg=None):
|
||||
bg_rect = f'<rect width="108" height="108" fill="{bg}"/>' if bg else ""
|
||||
return (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" '
|
||||
f'viewBox="0 0 108 108">{bg_rect}{body}</svg>'
|
||||
)
|
||||
|
||||
|
||||
def rasterise():
|
||||
"""Render each concept large so the output can actually be reviewed."""
|
||||
PNG.mkdir(parents=True, exist_ok=True)
|
||||
for key in CONCEPTS:
|
||||
src = OUT / f"rippr-{key}-onbg.svg"
|
||||
dst = PNG / f"{key}.png"
|
||||
subprocess.run(
|
||||
["rsvg-convert", "-w", "512", "-h", "512", str(src), "-o", str(dst)],
|
||||
check=True,
|
||||
)
|
||||
# Strips for review. `montage` needs a font for its auto-labels and fails without
|
||||
# one, so compose with +append instead — no text involved.
|
||||
def strip(px: int, name: str):
|
||||
subprocess.run(
|
||||
["magick"]
|
||||
+ [str(PNG / f"{k}.png") for k in CONCEPTS]
|
||||
+ ["-resize", f"{px}x{px}", "-background", "#05070A", "-splice", "8x0",
|
||||
"+append", str(PNG / name)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
strip(512, "contact-sheet.png")
|
||||
# True launcher and status-bar sizes: where detail either survives or turns to mush.
|
||||
strip(48, "small-48.png")
|
||||
strip(24, "small-24.png")
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for key, (_name, _desc, fn) in CONCEPTS.items():
|
||||
body = fn()
|
||||
(OUT / f"rippr-{key}.svg").write_text(svg_doc(body))
|
||||
(OUT / f"rippr-{key}-onbg.svg").write_text(svg_doc(body, bg=DEEP))
|
||||
print(f"wrote rippr-{key}.svg")
|
||||
rasterise()
|
||||
print(f"rendered PNGs -> {PNG}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
245
rippr-src/design/logo-review.html
Normal file
1
rippr-src/design/logos/rippr-apex-onbg.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" viewBox="0 0 108 108"><rect width="108" height="108" fill="#101418"/><path d="M17.957,79.237 A44.000,44.000 0 1 1 90.043,79.237" fill="none" stroke="#66727E" stroke-width="5" stroke-linecap="round" opacity="0.55"/><path d="M93.878,35.405 A44.000,44.000 0 0 1 90.043,79.237" fill="none" stroke="#FF5722" stroke-width="5" stroke-linecap="round"/><line x1="26.15" y1="73.50" x2="21.64" y2="76.66" stroke="#F2F5F8" stroke-width="2.6" stroke-linecap="round" opacity="0.85"/><line x1="17.56" y1="60.42" x2="15.10" y2="60.86" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="18.26" y1="44.42" x2="15.85" y2="43.78" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="25.66" y1="30.22" x2="23.74" y2="28.61" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="38.36" y1="20.47" x2="37.31" y2="18.20" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="54.00" y1="20.00" x2="54.00" y2="14.50" stroke="#F2F5F8" stroke-width="2.6" stroke-linecap="round" opacity="0.85"/><line x1="69.64" y1="20.47" x2="70.69" y2="18.20" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="82.34" y1="30.22" x2="84.26" y2="28.61" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="89.74" y1="44.42" x2="92.15" y2="43.78" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="90.44" y1="60.42" x2="92.90" y2="60.86" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="81.85" y1="73.50" x2="86.36" y2="76.66" stroke="#F2F5F8" stroke-width="2.6" stroke-linecap="round" opacity="0.85"/><g><path d="M480-316.5q38-.5 56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61q24 24 62 23.5Zm0-483.5q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800Zm7 313Z" fill="#F2F5F8" transform="translate(31.000,77.000) scale(0.047917)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
rippr-src/design/logos/rippr-apex.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" viewBox="0 0 108 108"><path d="M17.957,79.237 A44.000,44.000 0 1 1 90.043,79.237" fill="none" stroke="#66727E" stroke-width="5" stroke-linecap="round" opacity="0.55"/><path d="M93.878,35.405 A44.000,44.000 0 0 1 90.043,79.237" fill="none" stroke="#FF5722" stroke-width="5" stroke-linecap="round"/><line x1="26.15" y1="73.50" x2="21.64" y2="76.66" stroke="#F2F5F8" stroke-width="2.6" stroke-linecap="round" opacity="0.85"/><line x1="17.56" y1="60.42" x2="15.10" y2="60.86" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="18.26" y1="44.42" x2="15.85" y2="43.78" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="25.66" y1="30.22" x2="23.74" y2="28.61" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="38.36" y1="20.47" x2="37.31" y2="18.20" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="54.00" y1="20.00" x2="54.00" y2="14.50" stroke="#F2F5F8" stroke-width="2.6" stroke-linecap="round" opacity="0.85"/><line x1="69.64" y1="20.47" x2="70.69" y2="18.20" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="82.34" y1="30.22" x2="84.26" y2="28.61" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="89.74" y1="44.42" x2="92.15" y2="43.78" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="90.44" y1="60.42" x2="92.90" y2="60.86" stroke="#F2F5F8" stroke-width="1.6" stroke-linecap="round" opacity="0.45"/><line x1="81.85" y1="73.50" x2="86.36" y2="76.66" stroke="#F2F5F8" stroke-width="2.6" stroke-linecap="round" opacity="0.85"/><g><path d="M480-316.5q38-.5 56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61q24 24 62 23.5Zm0-483.5q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800Zm7 313Z" fill="#F2F5F8" transform="translate(31.000,77.000) scale(0.047917)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
rippr-src/design/logos/rippr-heading-onbg.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" viewBox="0 0 108 108"><rect width="108" height="108" fill="#101418"/><circle cx="54.0" cy="54.0" r="42" fill="none" stroke="#66727E" stroke-width="2.5" opacity="0.7"/><path d="M49.610,12.230 A42.000,42.000 0 0 1 84.212,24.824" fill="none" stroke="#FF5722" stroke-width="5.5" stroke-linecap="round"/><g transform="rotate(20 54.0 52.0)"><path d="m200-120-40-40 320-720 320 720-40 40-280-120-280 120Zm84-124 196-84 196 84-196-440-196 440Zm196-84Z" fill="#F2F5F8" transform="translate(30.000,76.000) scale(0.050000)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 589 B |
1
rippr-src/design/logos/rippr-heading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" viewBox="0 0 108 108"><circle cx="54.0" cy="54.0" r="42" fill="none" stroke="#66727E" stroke-width="2.5" opacity="0.7"/><path d="M49.610,12.230 A42.000,42.000 0 0 1 84.212,24.824" fill="none" stroke="#FF5722" stroke-width="5.5" stroke-linecap="round"/><g transform="rotate(20 54.0 52.0)"><path d="m200-120-40-40 320-720 320 720-40 40-280-120-280 120Zm84-124 196-84 196 84-196-440-196 440Zm196-84Z" fill="#F2F5F8" transform="translate(30.000,76.000) scale(0.050000)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 542 B |
1
rippr-src/design/logos/rippr-route-onbg.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" viewBox="0 0 108 108"><rect width="108" height="108" fill="#101418"/><circle cx="54.0" cy="54.0" r="43" fill="none" stroke="#66727E" stroke-width="3.5" opacity="0.6"/><g><path d="M247-167q-47-47-47-113v-327q-35-13-57.5-43.5T120-720q0-50 35-85t85-35q50 0 85 35t35 85q0 39-22.5 69.5T280-607v327q0 33 23.5 56.5T360-200q33 0 56.5-23.5T440-280v-400q0-66 47-113t113-47q66 0 113 47t47 113v327q35 13 57.5 43.5T840-240q0 50-35 85t-85 35q-50 0-85-35t-35-85q0-39 22.5-70t57.5-43v-327q0-33-23.5-56.5T600-760q-33 0-56.5 23.5T520-680v400q0 66-47 113t-113 47q-66 0-113-47Zm-7-513q17 0 28.5-11.5T280-720q0-17-11.5-28.5T240-760q-17 0-28.5 11.5T200-720q0 17 11.5 28.5T240-680Zm480 480q17 0 28.5-11.5T760-240q0-17-11.5-28.5T720-280q-17 0-28.5 11.5T680-240q0 17 11.5 28.5T720-200ZM240-720Zm480 480Z" fill="#F2F5F8" transform="translate(27.000,81.000) scale(0.056250)"/></g><circle cx="54.0" cy="54.0" r="43" fill="none" stroke="#FF5722" stroke-width="3.5" stroke-linecap="round" stroke-dasharray="34 236" stroke-dashoffset="-100"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
rippr-src/design/logos/rippr-route.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" viewBox="0 0 108 108"><circle cx="54.0" cy="54.0" r="43" fill="none" stroke="#66727E" stroke-width="3.5" opacity="0.6"/><g><path d="M247-167q-47-47-47-113v-327q-35-13-57.5-43.5T120-720q0-50 35-85t85-35q50 0 85 35t35 85q0 39-22.5 69.5T280-607v327q0 33 23.5 56.5T360-200q33 0 56.5-23.5T440-280v-400q0-66 47-113t113-47q66 0 113 47t47 113v327q35 13 57.5 43.5T840-240q0 50-35 85t-85 35q-50 0-85-35t-35-85q0-39 22.5-70t57.5-43v-327q0-33-23.5-56.5T600-760q-33 0-56.5 23.5T520-680v400q0 66-47 113t-113 47q-66 0-113-47Zm-7-513q17 0 28.5-11.5T280-720q0-17-11.5-28.5T240-760q-17 0-28.5 11.5T200-720q0 17 11.5 28.5T240-680Zm480 480q17 0 28.5-11.5T760-240q0-17-11.5-28.5T720-280q-17 0-28.5 11.5T680-240q0 17 11.5 28.5T720-200ZM240-720Zm480 480Z" fill="#F2F5F8" transform="translate(27.000,81.000) scale(0.056250)"/></g><circle cx="54.0" cy="54.0" r="43" fill="none" stroke="#FF5722" stroke-width="3.5" stroke-linecap="round" stroke-dasharray="34 236" stroke-dashoffset="-100"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
7
rippr-src/design/material/LICENSE.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Material Symbols
|
||||
|
||||
Source glyphs fetched from Google Fonts (Material Symbols, Outlined, 24px).
|
||||
Licensed under the Apache License 2.0 — https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
These are vendored as reference geometry for the Rippr icon set. Glyph viewBox is
|
||||
"0 -960 960 960" (x: 0..960, y: -960..0).
|
||||
1
rippr-src/design/material/explore.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m300-300 280-80 80-280-280 80-80 280Zm180-120q-25 0-42.5-17.5T420-480q0-25 17.5-42.5T480-540q25 0 42.5 17.5T540-480q0 25-17.5 42.5T480-420Zm0 340q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Zm0-320Z"/></svg>
|
||||
|
After Width: | Height: | Size: 547 B |
1
rippr-src/design/material/moving.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m136-240-56-56 212-212q35-35 85-35t85 35l46 46q12 12 28.5 12t28.5-12l178-178H640v-80h240v240h-80v-103L621-405q-35 35-85 35t-85-35l-47-47q-11-11-28-11t-28 11L136-240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 271 B |
1
rippr-src/design/material/navigation.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m200-120-40-40 320-720 320 720-40 40-280-120-280 120Zm84-124 196-84 196 84-196-440-196 440Zm196-84Z"/></svg>
|
||||
|
After Width: | Height: | Size: 205 B |
1
rippr-src/design/material/route.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M247-167q-47-47-47-113v-327q-35-13-57.5-43.5T120-720q0-50 35-85t85-35q50 0 85 35t35 85q0 39-22.5 69.5T280-607v327q0 33 23.5 56.5T360-200q33 0 56.5-23.5T440-280v-400q0-66 47-113t113-47q66 0 113 47t47 113v327q35 13 57.5 43.5T840-240q0 50-35 85t-85 35q-50 0-85-35t-35-85q0-39 22.5-70t57.5-43v-327q0-33-23.5-56.5T600-760q-33 0-56.5 23.5T520-680v400q0 66-47 113t-113 47q-66 0-113-47Zm-7-513q17 0 28.5-11.5T280-720q0-17-11.5-28.5T240-760q-17 0-28.5 11.5T200-720q0 17 11.5 28.5T240-680Zm480 480q17 0 28.5-11.5T760-240q0-17-11.5-28.5T720-280q-17 0-28.5 11.5T680-240q0 17 11.5 28.5T720-200ZM240-720Zm480 480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 705 B |
1
rippr-src/design/material/speed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-316.5q38-.5 56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61q24 24 62 23.5Zm0-483.5q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800Zm7 313Z"/></svg>
|
||||
|
After Width: | Height: | Size: 511 B |
1
rippr-src/design/material/sports_motorsports.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-480Zm80 240q100 0 170-70t70-170q0-101-73.5-170.5T550-720q-48 0-93 11t-87 33l100 40q41 17 65.5 52.5T560-504q0 60-41.5 102T418-360H162q-2 24-2 54.5v65.5h400ZM176-440h240q27 0 45.5-18.5T480-504q0-19-10.5-34.5T440-562l-148-60q-42 37-71.5 84T176-440Zm384 280H160q-33 0-56.5-23.5T80-240v-90q0-98 37-183.5t100.5-149Q281-726 367-763t183-37q68 0 128 25t105 68.5Q828-663 854-605t26 125q0 66-25 124.5t-68.5 102Q743-210 684.5-185T560-160Z"/></svg>
|
||||
|
After Width: | Height: | Size: 537 B |
1
rippr-src/design/material/two_wheeler.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M160-200q-66 0-113-47T0-360q0-57 36.5-101t93.5-55l-28-24H0v-60h180l100 60 160-60h126l-62-80H400v-80h142l84 108 134-68v120h-92l70 92q15-6 30.5-9t31.5-3q66 0 113 47t47 113q0 66-47 113t-113 47q-66 0-113-47t-47-113q0-27 9.5-52.5T676-460l-20-24-136 204H400l-80-70q-5 63-51 106.5T160-200Zm0-80q33 0 56.5-23.5T240-360q0-33-23.5-56.5T160-440q-33 0-56.5 23.5T80-360q0 33 23.5 56.5T160-280Zm294-240-144 54 144-54h130-130Zm346 240q33 0 56.5-23.5T880-360q0-33-23.5-56.5T800-440q-33 0-56.5 23.5T720-360q0 33 23.5 56.5T800-280Zm-322-80 106-160H454l-144 54 120 106h48Z"/></svg>
|
||||
|
After Width: | Height: | Size: 659 B |
173
rippr-src/design/to_vector_drawable.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Emits the Android vector drawables for the chosen Route mark.
|
||||
|
||||
This is a real conversion, not a copy: VectorDrawable understands a subset of SVG.
|
||||
Three things in the source SVG have no direct equivalent and are translated here.
|
||||
|
||||
<circle> -> two arc commands, since only <path> exists
|
||||
stroke-dasharray -> trimPathStart / trimPathEnd, which cut one segment out of
|
||||
a path; the dash offset becomes the trim start, expressed
|
||||
as a fraction of the circle's circumference
|
||||
opacity -> strokeAlpha / fillAlpha
|
||||
|
||||
Run after gen_logos.py; writes straight into app/src/main/res/drawable/.
|
||||
"""
|
||||
|
||||
import math
|
||||
import pathlib
|
||||
|
||||
import gen_logos
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
RES = HERE.parent / "app" / "src" / "main" / "res" / "drawable"
|
||||
|
||||
CX = CY = 54.0
|
||||
|
||||
# The standalone logo draws its ring at r=43, which is outside the adaptive-icon safe
|
||||
# circle (r=33) — a launcher mask crops it clean off, taking the accent segment with
|
||||
# it. The launcher build therefore shrinks the whole mark to fit inside the safe zone.
|
||||
DESIGN_RING_R = 43.0
|
||||
RING_R = 32.0
|
||||
GLYPH_SIZE = 40.0
|
||||
|
||||
# Must match concept_route() in gen_logos.py.
|
||||
DASH_LEN = 34.0
|
||||
DASH_OFFSET = 100.0
|
||||
|
||||
# Trim fractions describe an angular span, so they are derived from the design radius
|
||||
# and stay correct at any ring size.
|
||||
DESIGN_CIRCUMFERENCE = 2 * math.pi * DESIGN_RING_R
|
||||
|
||||
# Starts at due east and sweeps clockwise, matching how a browser renders <circle>,
|
||||
# so the trim fractions below land the accent in the same place as the preview.
|
||||
RING_PATH = (
|
||||
f"M{CX + RING_R},{CY} "
|
||||
f"A{RING_R},{RING_R} 0 1 1 {CX - RING_R},{CY} "
|
||||
f"A{RING_R},{RING_R} 0 1 1 {CX + RING_R},{CY}"
|
||||
)
|
||||
|
||||
LAUNCHER = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by design/to_vector_drawable.py — do not edit by hand. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- Track ring. -->
|
||||
<path
|
||||
android:pathData="{ring}"
|
||||
android:strokeColor="{dim}"
|
||||
android:strokeWidth="3.5"
|
||||
android:strokeAlpha="0.6" />
|
||||
|
||||
<!-- Material Symbols `route`, Apache-2.0. Scaled from its 960 box onto the icon canvas. -->
|
||||
<group
|
||||
android:translateX="{tx}"
|
||||
android:translateY="{ty}"
|
||||
android:scaleX="{scale}"
|
||||
android:scaleY="{scale}">
|
||||
<path
|
||||
android:pathData="{glyph}"
|
||||
android:fillColor="{ink}" />
|
||||
</group>
|
||||
|
||||
<!-- Recording-progress segment: the SVG's dash pattern, cut with trimPath. -->
|
||||
<path
|
||||
android:pathData="{ring}"
|
||||
android:strokeColor="{accent}"
|
||||
android:strokeWidth="3.5"
|
||||
android:strokeLineCap="round"
|
||||
android:trimPathStart="{trim_start:.4f}"
|
||||
android:trimPathEnd="{trim_end:.4f}" />
|
||||
</vector>
|
||||
"""
|
||||
|
||||
# Status-bar icons are flattened to a single color with only alpha preserved, so the
|
||||
# ring and accent are dropped — at 24dp they would collapse into a grey smudge.
|
||||
MONO = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by design/to_vector_drawable.py — do not edit by hand. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFFFF">
|
||||
<group
|
||||
android:translateX="{tx}"
|
||||
android:translateY="{ty}"
|
||||
android:scaleX="{scale}"
|
||||
android:scaleY="{scale}">
|
||||
<path
|
||||
android:pathData="{glyph}"
|
||||
android:fillColor="#FFFFFFFF" />
|
||||
</group>
|
||||
</vector>
|
||||
"""
|
||||
|
||||
# Themed ("monochrome") launcher icons keep the 108 canvas and are tinted by the
|
||||
# system, so this one keeps the ring for a fuller shape at launcher size.
|
||||
THEMED = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by design/to_vector_drawable.py — do not edit by hand. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="{ring}"
|
||||
android:strokeColor="#FF000000"
|
||||
android:strokeWidth="3.5" />
|
||||
<group
|
||||
android:translateX="{tx}"
|
||||
android:translateY="{ty}"
|
||||
android:scaleX="{scale}"
|
||||
android:scaleY="{scale}">
|
||||
<path
|
||||
android:pathData="{glyph}"
|
||||
android:fillColor="#FF000000" />
|
||||
</group>
|
||||
</vector>
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
glyph = gen_logos.glyph_path("route")
|
||||
|
||||
launcher_scale = GLYPH_SIZE / gen_logos.GLYPH_BOX
|
||||
common = dict(
|
||||
ring=RING_PATH,
|
||||
glyph=glyph,
|
||||
dim=gen_logos.DIM,
|
||||
ink=gen_logos.INK,
|
||||
accent=gen_logos.ACCENT,
|
||||
tx=f"{CX - GLYPH_SIZE / 2:g}",
|
||||
ty=f"{CY + GLYPH_SIZE / 2:g}",
|
||||
scale=f"{launcher_scale:.6f}",
|
||||
trim_start=DASH_OFFSET / DESIGN_CIRCUMFERENCE,
|
||||
trim_end=(DASH_OFFSET + DASH_LEN) / DESIGN_CIRCUMFERENCE,
|
||||
)
|
||||
|
||||
RES.mkdir(parents=True, exist_ok=True)
|
||||
(RES / "ic_launcher_foreground.xml").write_text(LAUNCHER.format(**common))
|
||||
(RES / "ic_launcher_monochrome.xml").write_text(THEMED.format(**common))
|
||||
|
||||
# The status-bar glyph fills a 24dp box edge to edge, minus a little padding.
|
||||
mono_size = 20.0
|
||||
mono_scale = mono_size / gen_logos.GLYPH_BOX
|
||||
(RES / "ic_stat_rippr.xml").write_text(
|
||||
MONO.format(
|
||||
glyph=glyph,
|
||||
tx=f"{(24 - mono_size) / 2:g}",
|
||||
ty=f"{(24 + mono_size) / 2:g}",
|
||||
scale=f"{mono_scale:.6f}",
|
||||
)
|
||||
)
|
||||
|
||||
for name in ("ic_launcher_foreground", "ic_launcher_monochrome", "ic_stat_rippr"):
|
||||
print(f"wrote {RES / (name + '.xml')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||